Skip to content

Commit 41f2541

Browse files
committed
Fix #5 and fit nameof in more cases
1 parent b0fda50 commit 41f2541

File tree

3 files changed

+154
-58
lines changed

3 files changed

+154
-58
lines changed

README.md

Lines changed: 9 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -210,16 +210,16 @@ a == b
210210

211211
## Limitations
212212
- Working in `ipython REPL` but not in standard `python console`
213-
- You have to know at which stack the function/class will be called
213+
- You have to know at which stack the function/class will be called (caller's depth)
214214
- Not working with `reticulate` from `R` since it cuts stacks to the most recent one.
215-
- `nameof` cannot be used in statements in `pytest`
216-
```
217-
a = 1
218-
assert nameof(a) == 'a'
219-
# Retrieving failure.
220-
# The right way:
221-
aname = nameof(a)
222-
assert aname == 'a'
215+
- ~~`nameof` cannot be used in statements in `pytest`~~ (supported in `v0.2.0`)
216+
```diff
217+
-a = 1
218+
+assert nameof(a) == 'a'
219+
-# Retrieving failure.
220+
-# The right way:
221+
-aname = nameof(a)
222+
-assert aname == 'a'
223223
```
224224

225225
[1]: https://github.com/pwwang/python-varname

tests/test_varname.py

Lines changed: 43 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -7,7 +7,7 @@
77
will,
88
inject,
99
namedtuple,
10-
_get_executing,
10+
_get_node,
1111
nameof)
1212

1313
@pytest.fixture
@@ -246,8 +246,9 @@ def func():
246246
f = func()
247247
assert f == 'fabc'
248248

249-
with pytest.raises(VarnameRetrievingError):
250-
assert nameof(f)
249+
assert nameof(f) == 'f'
250+
assert 'f' == nameof(f)
251+
assert len(nameof(f)) == 1
251252

252253
fname1 = fname = nameof(f)
253254
assert fname1 == fname == 'f'
@@ -258,6 +259,44 @@ def func():
258259
with pytest.raises(VarnameRetrievingError):
259260
nameof()
260261

262+
def test_nameof_statements():
263+
a = {'test': 1}
264+
test = {}
265+
del a[nameof(test)]
266+
assert a == {}
267+
268+
def func():
269+
return nameof(test)
270+
271+
assert func() == 'test'
272+
273+
def func2():
274+
yield nameof(test)
275+
276+
assert list(func2()) == ['test']
277+
278+
def func3():
279+
raise ValueError(nameof(test))
280+
281+
with pytest.raises(ValueError) as verr:
282+
func3()
283+
assert str(verr.value) == 'test'
284+
285+
for i in [0]:
286+
assert nameof(test) == 'test'
287+
assert len(nameof(test)) == 4
288+
289+
def test_nameof_expr():
290+
import varname
291+
test = {}
292+
assert len(varname.nameof(test)) == 4
293+
294+
lam = lambda: 0
295+
lam.a = 1
296+
with pytest.raises(VarnameRetrievingError) as vrerr:
297+
varname.nameof(test, lam.a)
298+
assert str(vrerr.value) == ("Only variables should "
299+
"be passed to nameof.")
261300

262301
def test_class_property():
263302
class C:
@@ -327,7 +366,7 @@ def get_will():
327366
def test_frame_fail(monkey_patch):
328367
"""Test when failed to retrieve the frame"""
329368
# Let's monkey-patch inspect.stack to do this
330-
assert _get_executing(1) is None
369+
assert _get_node(1) is None
331370

332371
def test_frame_fail_varname(monkey_patch):
333372

varname.py

Lines changed: 102 additions & 45 deletions
Original file line numberDiff line numberDiff line change
@@ -18,18 +18,83 @@ class VarnameRetrievingWarning(Warning):
1818
class VarnameRetrievingError(Exception):
1919
"""When failed to retrieve the varname"""
2020

21-
def _get_executing(caller):
22-
"""Try to get the executing object
21+
def _get_node(caller):
22+
"""Try to get node from the executing object.
23+
2324
This can fail when a frame is failed to retrieve.
2425
One case should be when python code is executed in
2526
R pacakge `reticulate`, where only first frame is kept.
27+
28+
When the node can not be retrieved, try to return the first statement.
2629
"""
2730
try:
2831
frame = inspect.stack()[caller+2].frame
2932
except IndexError:
3033
return None
3134
else:
32-
return executing.Source.executing(frame)
35+
exet = executing.Source.executing(frame)
36+
37+
if exet.node:
38+
return exet.node
39+
40+
return list(exet.statements)[0]
41+
42+
def _lookfor_parent_assign(node):
43+
"""Look for an ast.Assign node in the parents"""
44+
while hasattr(node, 'parent'):
45+
node = node.parent
46+
47+
if isinstance(node, ast.Assign):
48+
return node
49+
return None
50+
51+
def _lookfor_child_nameof(node):
52+
"""Look for ast.Call with func=Name(id='nameof',...)"""
53+
# pylint: disable=too-many-return-statements
54+
if isinstance(node, ast.Call):
55+
# if node.func.id == 'nameof':
56+
# return node
57+
58+
# We want to support alias for nameof, i.e. nameof2
59+
# Or called like: varname.nameof(test)
60+
# If all args are ast.Name, then if must be alias of nameof
61+
# Since this is originated from it, and there is no other
62+
# ast.Call node in args
63+
if not any(isinstance(arg, ast.Call) for arg in node.args):
64+
return node
65+
66+
# print(nameof(test))
67+
for arg in node.args:
68+
found = _lookfor_child_nameof(arg)
69+
if found:
70+
return found
71+
72+
elif isinstance(node, ast.Compare):
73+
# nameof(test) == 'test'
74+
found = _lookfor_child_nameof(node.left)
75+
if found:
76+
return found
77+
# 'test' == nameof(test)
78+
for comp in node.comparators:
79+
found = _lookfor_child_nameof(comp)
80+
if found:
81+
return found
82+
83+
elif isinstance(node, ast.Assert):
84+
# assert nameof(test) == 'test'
85+
found = _lookfor_child_nameof(node.test)
86+
if found:
87+
return found
88+
89+
elif isinstance(node, ast.Expr): # pragma: no cover
90+
# print(nameof(test)) in ipython's forloop
91+
# issue #5
92+
found = _lookfor_child_nameof(node.value)
93+
if found:
94+
return found
95+
96+
return None
97+
3398

3499
def varname(caller=1, raise_exc=False):
35100
"""Get the variable name that assigned by function/class calls
@@ -53,42 +118,40 @@ def varname(caller=1, raise_exc=False):
53118
in the assign node. (e.g: `a = b = func()`, in such a case,
54119
`b == 'a'`, may not be the case you want)
55120
"""
56-
exec_obj = _get_executing(caller)
57-
if not exec_obj:
121+
node = _get_node(caller)
122+
if not node:
58123
if raise_exc:
59-
raise VarnameRetrievingError("Unable to retrieve the frame.")
124+
raise VarnameRetrievingError("Unable to retrieve the ast node.")
60125
VARNAME_INDEX[0] += 1
61126
warnings.warn(f"var_{VARNAME_INDEX[0]} used.",
62127
VarnameRetrievingWarning)
63128
return f"var_{VARNAME_INDEX[0]}"
64129

65-
node = exec_obj.node
66-
while hasattr(node, 'parent'):
67-
node = node.parent
68-
69-
if isinstance(node, ast.Assign):
70-
# Need to actually check that there's just one
71-
if len(node.targets) > 1:
72-
warnings.warn("Multiple targets in assignment, variable name "
73-
"on the very left will be used.",
74-
MultipleTargetAssignmentWarning)
75-
target = node.targets[0]
76-
77-
# Need to check that it's a variable
78-
if isinstance(target, ast.Name):
79-
return target.id
80-
130+
node = _lookfor_parent_assign(node)
131+
if not node:
132+
if raise_exc:
81133
raise VarnameRetrievingError(
82-
f"Invaid variable assigned: {ast.dump(target)!r}"
134+
'Failed to retrieve the variable name.'
83135
)
136+
VARNAME_INDEX[0] += 1
137+
warnings.warn(f"var_{VARNAME_INDEX[0]} used.", VarnameRetrievingWarning)
138+
return f"var_{VARNAME_INDEX[0]}"
139+
140+
# Need to actually check that there's just one
141+
# give warnings if: a = b = func()
142+
if len(node.targets) > 1:
143+
warnings.warn("Multiple targets in assignment, variable name "
144+
"on the very left will be used.",
145+
MultipleTargetAssignmentWarning)
146+
target = node.targets[0]
84147

85-
if raise_exc:
86-
raise VarnameRetrievingError('Failed to retrieve the variable name.')
148+
# must be a variable
149+
if isinstance(target, ast.Name):
150+
return target.id
87151

88-
VARNAME_INDEX[0] += 1
89-
warnings.warn(f"var_{VARNAME_INDEX[0]} used.",
90-
VarnameRetrievingWarning)
91-
return f"var_{VARNAME_INDEX[0]}"
152+
raise VarnameRetrievingError(
153+
f"Invaid variable assigned: {ast.dump(target)!r}"
154+
)
92155

93156
def will(caller=1, raise_exc=False):
94157
"""Detect the attribute name right immediately after a function call.
@@ -118,15 +181,14 @@ def i_will():
118181
VarnameRetrievingError: When `raise_exc` is `True` and we failed to
119182
detect the attribute name (including not having one)
120183
"""
121-
exec_obj = _get_executing(caller)
122-
if not exec_obj:
184+
node = _get_node(caller)
185+
if not node:
123186
if raise_exc:
124187
raise VarnameRetrievingError("Unable to retrieve the frame.")
125188
return None
126189

127190
ret = None
128191
try:
129-
node = exec_obj.node
130192
# have to be used in a call
131193
assert isinstance(node, (ast.Attribute, ast.Call)), (
132194
"Invalid use of function `will`"
@@ -174,24 +236,19 @@ def nameof(*args):
174236
*args: A couple of variables passed in
175237
176238
Returns:
177-
tuple|str:
239+
tuple|str: The names of variables passed in
178240
"""
179-
exec_obj = _get_executing(0)
180-
if not exec_obj:
181-
raise VarnameRetrievingError("Unable to retrieve the frame.")
182-
183-
if not exec_obj.node:
184-
# we cannot do: assert nameof(a) == 'a' in pytest
185-
raise VarnameRetrievingError("Callee's node cannot be detected.")
186-
187-
assert isinstance(exec_obj.node, ast.Call)
241+
node = _get_node(0)
242+
node = _lookfor_child_nameof(node)
243+
if not node:
244+
raise VarnameRetrievingError("Unable to retrieve callee's node.")
188245

189246
ret = []
190-
for node in exec_obj.node.args:
191-
if not isinstance(node, ast.Name):
247+
for arg in node.args:
248+
if not isinstance(arg, ast.Name):
192249
raise VarnameRetrievingError("Only variables should "
193250
"be passed to nameof.")
194-
ret.append(node.id)
251+
ret.append(arg.id)
195252

196253
if not ret:
197254
raise VarnameRetrievingError("At least one variable should be "

0 commit comments

Comments
 (0)