I rewrote some code I posted earlier this month. It attempts to mimic C#'s nameof()
by extracting names from bytecode instructions.
This seems more reliable than looking into the previous frame's locals and asserting that there is an id match between some value in the current scope. However, I'm unsure just how much because it uses dis
:
Use of this module should not be considered to work across Python VMs or Python releases.
I tried reducing the amount of bad input possible, but there is a quirky case where if I pass the function name_of((Cls.member(), var))
it returns 'member'
, if I remove the parentheses it will raise a SyntaxError
because it no longer mistakes them for the end of the function call, and attribute access has been broken, so what the hell does it make of var
.
import dis as _dis
from itertools import dropwhile as _dropwhile, takewhile as _takewhile
__all__ = ['name_of']
_LOAD = (
'LOAD_CONST',
'LOAD_NAME',
'LOAD_ATTR',
'LOAD_GLOBAL',
'LOAD_FAST',
'LOAD_CLOSURE',
'LOAD_DEREF',
'LOAD_CLASSDEREF'
)
class _Scopes:
known = {}
def _dis_f(frame):
return iter(_dis.Bytecode(frame.f_code))
def _known():
def scope():
return scope.cached[scope.no]
scope.cached = []
scope.no = 0
return scope
def _not_eq(instruction):
return instruction.argrepr != 'name_of'
def _in_load(instruction):
return instruction.opname in _LOAD
def _invalidate(name):
_Scopes.known.clear()
raise SyntaxError('Invalid: {!r}'.format(name))
def _is_ident(name):
if not name.isidentifier():
_invalidate(name)
def _eq(instruction):
return instruction.opname == _LOAD[2]
def _all_attr(instructions, name):
if instructions[:-1] and not all(
_eq(instruction) for instruction in instructions[1:]
):
_invalidate(name)
def _cache(bytecode, scope):
while True:
instructions = _dropwhile(_not_eq, bytecode)
if next(instructions, None) is None:
break
instructions = list(_takewhile(_in_load, instructions))
name = instructions[-1].argrepr
_is_ident(name)
_all_attr(instructions, name)
scope.cached.append(name)
def _pos(frame_id, scope):
scope.no += 1
if scope.no == len(scope.cached):
del _Scopes.known[frame_id]
def name_of(_):
try:
raise NameError
except Exception as exc:
frame = exc.__traceback__.tb_frame.f_back
frame_id = id(frame)
if frame_id not in _Scopes.known:
bytecode = _dis_f(frame)
scope = _Scopes.known[frame_id] = _known()
_cache(bytecode, scope)
else:
scope = _Scopes.known[frame_id]
name = scope()
_pos(frame_id, scope)
return name
Overall it seems to work as intended, in both 3.4.4 and 3.6.3.