pwwang/python-varname

Support multiple variables on LHS for varname.varname

EngHabu opened this issue · 10 comments

The code in here

python-varname/varname.py

Lines 243 to 266 in 6a1e8bd

ret = []
for arg in node.args:
if not full or isinstance(arg, ast.Name):
ret.append(_node_name(arg))
else:
# traverse the node to get the full name: nameof(a.b.c)
# arg:
# Attribute(value=Attribute(value=Name(id='a', ctx=Load()),
# attr='b',
# ctx=Load()),
# attr='c',
# ctx=Load())
full_name = []
while not isinstance(arg, ast.Name):
if not isinstance(arg, ast.Attribute):
raise VarnameRetrievingError(
'Can only retrieve full names of '
'(chained) attribute calls by nameof.'
)
full_name.append(arg.attr)
arg = arg.value
# now it is an ast.Name
full_name.append(arg.id)
ret.append('.'.join(reversed(full_name)))

assumes the Node is a function call. There are other AST Node types. For example, the code below:

    class MyType(object):
        def __eq__(self, other):
            print(varname.nameof(self, caller=2))
            return super().__eq__(other)

    a = MyType()
    print(a == 3)

Throws an exception because The Node in this case is a Compare Node that doesn't have args. It does, however, have n.left.id to access the variable name we are calling __eq__() on.

What are you expecting this to output:

print(varname.nameof(self, caller=2))

"self" or "a" ?

"a".. I'm not positive about the value passed in to caller (maybe 3?)

You should never use the caller argument in a direct nameof call, unless you are wrapping it in another function.
nameof is supposed to get the variable name right at that stack.

To get the instance name, I suggest you do this:

    import varname

    class MyType(object):
        def __init__(self, *args, **kwargs):
            self.name = varname.varname()

        def __eq__(self, other):
            print(self.name)
            return super().__eq__(other)

    a = MyType()
    print(a == 3)

My pattern is more like this:

    import varname

    class MyType(object):
        def __init__(self, *args, **kwargs):
            self.name = varname.varname(caller=3)

        def __eq__(self, other):
            print(f"name: {self.name}")
            return super().__eq__(other)

    def create_MyType():
        return MyType(), MyType()

    a, b = create_MyType()
    print(a == 3)
    print(b == 5)

Is there a way to get a and b here?

I think the use of caller is sensible, the intended behaviour is meant to be similar to this:

import varname


class MyType(object):
    def __len__(self):
        print(varname.nameof(self, caller=2))
        return 3
    
a = MyType()
len(a)  # prints a

But the problem as originally stated is impossible. Consider this code:

class MyType(object):
    def __eq__(self, other):
        print("eq")
        return True

a = MyType()
b = 3
a == b
b == a

There is no way to tell in a == b which name is calling __eq__.

@alexmojaki True. And __len__ now is actually a wrapper for nameof (and of course you are doing something else inside).

@EngHabu
I see your point now. Now the problem is here:

python-varname/varname.py

Lines 411 to 424 in 6a1e8bd

def _node_name(node: ast.AST) -> str:
"""Get the node node name.
Raises VarnameRetrievingError when failed
"""
if isinstance(node, ast.Name):
return node.id
if isinstance(node, ast.Attribute):
return node.attr
raise VarnameRetrievingError(
f"Can only get name of a variable or attribute, "
f"not {ast.dump(node)}"
)

instead of the fragment you attached initially. We only allow a Name or Attribute node currently.

Thinking about extending it into a List/Tuple of Name or Attribute nodes.

@EngHabu

If the above is implemented, are you satisfied with this?

import varname

class MyType(object):
    def __init__(self, name, *args, **kwargs):
        self.name = name

    def __eq__(self, other):
        print(f"name: {self.name}")
        return super().__eq__(other)

def create_MyType():
    names = varname.varname()
    types = [MyType(name) for name in names]
    return types

a, b = create_MyType()
print(a == 3)
print(b == 5)

However, this will be a problem then:

a = create_MyType()

Since varname.varname doesn't know it's going to be a single variable or multiple on the left-hand side.

So now it's a fight of giving it up or adding another argument in varname.varname to explicitly require a single variable or multiple variables on LHS.

I like your idea @pwwang although I also agree with your assessment of the problem it raises.

The same thing can be done now with sorcery:

import sorcery

class MyType(object):
    def __init__(self, name, *args, **kwargs):
        self.name = name

    def __eq__(self, other):
        print(f"name: {self.name}")
        return super().__eq__(other)

@sorcery.no_spells
def create_MyType():
    names = sorcery.assigned_names()
    types = [MyType(name) for name in names]
    return types

a, b = create_MyType()
print(a == 3)
print(b == 5)

@EngHabu does that solve your problem?

Link to commit: 2694c21

How to use:

def func():
    return varname(nvars=None)

a = func() # a == 'a'
a, b = func() # (a, b) == ('a', 'b')
[a, b] = func() # (a, b) == ('a', 'b')

# limit the number of variables on LHS
def func():
    return varname(nvars=2)

a = func() # VarnameRetrievingError
a, b, c = func() # VarnameRetrievingError
a, b = func() # (a, b) == ('a', 'b')

In your case:

import varname

class MyType(object):
    def __init__(self, name, *args, **kwargs):
        self.name = name

    def __eq__(self, other):
        print(f"name: {self.name}")
        return super().__eq__(other)

def create_MyType():
    names = varname.varname(nvars=2)
    types = [MyType(name) for name in names]
    return types

a, b = create_MyType()
print(a == 3)
print(b == 5)

Closing this via v0.5.4
Now you can do:

import varname

class MyType(object):
    def __init__(self, name, *args, **kwargs):
        self.name = name

    def __eq__(self, other):
        print(f"name: {self.name}")
        return super().__eq__(other)

def create_MyType():
    names = varname.varname(multi_vars=True)
    types = [MyType(name) for name in names]
    return types

a, b = create_MyType()
print(a == 3)
print(b == 5)