Skip to content

Commit aff7a91

Browse files
authored
Alternative way of testing bytecode nameof (#10)
* Add bytecode nameof as fallback when executing fails * Test both nameofs without TESTING check * Remove TESTING * Test _bytecode_nameof with non-named argument * Test some more edge cases for bytecode * Fix test_frame_fail_nameof * Still need to handle raise_exc=False
1 parent 165c54a commit aff7a91

File tree

2 files changed

+95
-13
lines changed

2 files changed

+95
-13
lines changed

tests/test_varname.py

Lines changed: 47 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -9,7 +9,27 @@
99
inject,
1010
namedtuple,
1111
_get_node,
12-
nameof)
12+
_bytecode_nameof,
13+
nameof as original_nameof)
14+
15+
import varname as varname_module
16+
17+
18+
def nameof(*args):
19+
"""Test both implementations at the same time"""
20+
result = original_nameof(*args, caller=2)
21+
if len(args) == 1:
22+
assert result == _bytecode_nameof(caller=2)
23+
return result
24+
25+
26+
varname_module.nameof = nameof
27+
28+
29+
def test_original_nameof():
30+
x = 1
31+
assert original_nameof(x) == nameof(x) == _bytecode_nameof(x) == 'x'
32+
1333

1434

1535
@pytest.fixture
@@ -263,6 +283,9 @@ def func():
263283
with pytest.raises(VarnameRetrievingError):
264284
nameof(a==1)
265285

286+
with pytest.raises(VarnameRetrievingError):
287+
_bytecode_nameof(a == 1)
288+
266289
with pytest.raises(VarnameRetrievingError):
267290
nameof()
268291

@@ -294,14 +317,13 @@ def func3():
294317
assert len(nameof(test)) == 4
295318

296319
def test_nameof_expr():
297-
import varname
298320
test = {}
299-
assert len(varname.nameof(test)) == 4
321+
assert len(varname_module.nameof(test)) == 4
300322

301323
lam = lambda: 0
302324
lam.a = 1
303325
with pytest.raises(VarnameRetrievingError) as vrerr:
304-
varname.nameof(test, lam.a)
326+
varname_module.nameof(test, lam.a)
305327
assert str(vrerr.value) == ("Only variables should "
306328
"be passed to nameof.")
307329

@@ -392,7 +414,7 @@ def func(raise_exc):
392414
def test_frame_fail_nameof(no_getframe):
393415
a = 1
394416
with pytest.raises(VarnameRetrievingError):
395-
nameof(a)
417+
assert nameof(a) == 'a'
396418

397419

398420
def test_frame_fail_will(no_getframe):
@@ -430,3 +452,23 @@ class A(list):
430452
a.append(1)
431453
b.append(1)
432454
assert a == b
455+
456+
457+
def test_no_source_code_nameof():
458+
assert eval('nameof(list)') == eval('original_nameof(list)') == 'list'
459+
460+
with pytest.raises(VarnameRetrievingError):
461+
eval("original_nameof(list, list)")
462+
463+
464+
class Weird:
465+
def __add__(self, other):
466+
_bytecode_nameof(caller=2)
467+
468+
469+
def test_bytecode_nameof_wrong_node():
470+
with pytest.raises(
471+
VarnameRetrievingError,
472+
match='Did you call nameof in a weird way',
473+
):
474+
Weird() + Weird()

varname.py

Lines changed: 48 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -1,14 +1,18 @@
11
"""Get the variable name that assigned by function/class calls"""
22
import ast
3+
import dis
34
import sys
45
import warnings
56
from collections import namedtuple as standard_namedtuple
7+
from functools import lru_cache
8+
69
import executing
710

811
__version__ = "0.2.0"
912

1013
VARNAME_INDEX = [-1]
1114

15+
1216
class MultipleTargetAssignmentWarning(Warning):
1317
"""When multiple-target assignment found, i.e. y = x = func()"""
1418

@@ -18,6 +22,14 @@ class VarnameRetrievingWarning(Warning):
1822
class VarnameRetrievingError(Exception):
1923
"""When failed to retrieve the varname"""
2024

25+
26+
def _get_frame(caller):
27+
try:
28+
return sys._getframe(caller + 1)
29+
except Exception as e:
30+
raise VarnameRetrievingError from e
31+
32+
2133
def _get_node(caller):
2234
"""Try to get node from the executing object.
2335
@@ -28,16 +40,20 @@ def _get_node(caller):
2840
When the node can not be retrieved, try to return the first statement.
2941
"""
3042
try:
31-
frame = sys._getframe(caller + 2)
32-
except Exception:
43+
frame = _get_frame(caller + 2)
44+
except VarnameRetrievingError:
3345
return None
34-
else:
35-
exet = executing.Source.executing(frame)
46+
47+
exet = executing.Source.executing(frame)
3648

3749
if exet.node:
3850
return exet.node
3951

40-
return list(exet.statements)[0]
52+
if exet.source.text and exet.source.tree:
53+
return list(exet.statements)[0]
54+
55+
return None
56+
4157

4258
def _lookfor_parent_assign(node):
4359
"""Look for an ast.Assign node in the parents"""
@@ -229,7 +245,7 @@ def inject(obj):
229245
raise VarnameRetrievingError('Unable to inject __varname__.')
230246
return obj
231247

232-
def nameof(*args):
248+
def nameof(*args, caller=1):
233249
"""Get the names of the variables passed in
234250
235251
Args:
@@ -238,9 +254,11 @@ def nameof(*args):
238254
Returns:
239255
tuple|str: The names of variables passed in
240256
"""
241-
node = _get_node(0)
257+
node = _get_node(caller - 1)
242258
node = _lookfor_child_nameof(node)
243259
if not node:
260+
if len(args) == 1:
261+
return _bytecode_nameof(caller + 1)
244262
raise VarnameRetrievingError("Unable to retrieve callee's node.")
245263

246264
ret = []
@@ -256,6 +274,28 @@ def nameof(*args):
256274

257275
return ret[0] if len(args) == 1 else tuple(ret)
258276

277+
278+
def _bytecode_nameof(caller=1):
279+
frame = _get_frame(caller)
280+
return _bytecode_nameof_cached(frame.f_code, frame.f_lasti)
281+
282+
283+
@lru_cache()
284+
def _bytecode_nameof_cached(code, offset):
285+
instructions = list(dis.get_instructions(code))
286+
(current_instruction_index, current_instruction), = (
287+
(index, instruction)
288+
for index, instruction in enumerate(instructions)
289+
if instruction.offset == offset
290+
)
291+
if current_instruction.opname not in ("CALL_FUNCTION", "CALL_METHOD"):
292+
raise VarnameRetrievingError("Did you call nameof in a weird way?")
293+
name_instruction = instructions[current_instruction_index - 1]
294+
if not name_instruction.opname.startswith("LOAD_"):
295+
raise VarnameRetrievingError("Argument must be a variable or attribute")
296+
return name_instruction.argrepr
297+
298+
259299
def namedtuple(*args, **kwargs):
260300
"""A shortcut for namedtuple
261301
@@ -267,7 +307,7 @@ def namedtuple(*args, **kwargs):
267307
>>> Name = namedtuple('Name', ['first', 'last'])
268308
269309
You can do:
270-
>>> from variables import namedtuple
310+
>>> from varname import namedtuple
271311
>>> Name = namedtuple(['first', 'last'])
272312
"""
273313
typename = varname(raise_exc=True)

0 commit comments

Comments
 (0)