diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml index fca07d9..2a5083a 100644 --- a/.github/workflows/build.yml +++ b/.github/workflows/build.yml @@ -13,7 +13,7 @@ jobs: runs-on: ubuntu-latest strategy: matrix: - python-version: [3.8, 3.9, "3.10", "3.11", "3.12-dev"] + python-version: [3.9, "3.10", "3.11", "3.12-dev"] steps: - uses: actions/checkout@v3 diff --git a/README.md b/README.md index a9bf786..5695967 100644 --- a/README.md +++ b/README.md @@ -199,7 +199,7 @@ Special thanks to [@HanyuuLu][2] to give up the name `varname` in pypi for this func = function2() # func == 'func' a = lambda: 0 - a.b = function() # a.b == 'b' + a.b = function() # a.b == 'a.b' ``` ### The decorator way to register `__varname__` to functions/classes diff --git a/README.raw.md b/README.raw.md index 05dec9b..247f482 100644 --- a/README.raw.md +++ b/README.raw.md @@ -199,7 +199,7 @@ Special thanks to [@HanyuuLu][2] to give up the name `varname` in pypi for this func = function2() # func == 'func' a = lambda: 0 - a.b = function() # a.b == 'b' + a.b = function() # a.b == 'a.b' ``` ### The decorator way to register `__varname__` to functions/classes diff --git a/docs/CHANGELOG.md b/docs/CHANGELOG.md index 0a302b5..1d3f542 100644 --- a/docs/CHANGELOG.md +++ b/docs/CHANGELOG.md @@ -1,5 +1,13 @@ # Change Log +## 0.13.0 + +- style: change max line length to 88 +- style: clean up test code styles +- feat: support subscript node for varname (#104) +- ci: remove python3.8 from CI +- breaking!: `varname` of `a.b` now returns `"a.b"` instead of `"a"` + ## 0.12.2 - Add `helpers.exec_code` function to replace `exec` so that source code available at runtime diff --git a/pyproject.toml b/pyproject.toml index 1fdadef..7e2ac45 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -4,7 +4,7 @@ build-backend = "poetry.masonry.api" [tool.poetry] name = "varname" -version = "0.12.2" +version = "0.13.0" description = "Dark magics about variable names in python." authors = [ "pwwang ",] license = "MIT" @@ -42,6 +42,6 @@ show_error_codes = true strict_optional = false [tool.black] -line-length = 80 +line-length = 88 target-version = ['py37', 'py38', 'py39', 'py310'] include = '\.pyi?$' diff --git a/tests/test_argname.py b/tests/test_argname.py index 3def423..eb16e21 100644 --- a/tests/test_argname.py +++ b/tests/test_argname.py @@ -2,7 +2,12 @@ from functools import singledispatch import pytest -from varname import * +from varname import ( + argname, + UsingExecWarning, + ImproperUseError, + VarnameRetrievingError, +) def test_argname(): @@ -72,7 +77,7 @@ def func(a): def test_argname_non_argument(): - x = 1 + x = 1 # noqa F841 y = lambda: argname("x") with pytest.raises(ImproperUseError, match="'x' is not a valid argument"): y() @@ -96,7 +101,7 @@ def func(a, b, c, d=4): def test_argname_funcnode_not_call(): - x = 1 + x = 1 # noqa F841 class Foo: @@ -194,7 +199,7 @@ def func(*args, **kwargs): def test_argname_argname_node_na(): source = textwrap.dedent( - f"""\ + """\ from varname import argname def func(a): return argname(a) @@ -222,17 +227,6 @@ def func(a): exec("x=1; func(x)") -def test_argname_func_na(): - def func(a): - return argname("a") - - with pytest.raises( - VarnameRetrievingError, - match="The source code of 'argname' calling is not available", - ): - exec("x=1; func(x)") - - def test_argname_wrapper(): def decorator(f): def wrapper(arg, *more_args): @@ -264,7 +258,7 @@ def func(a, *args, **kwargs): def test_argname_nosuch_varpos_arg(): def func(a, *args): - another = [] + another = [] # noqa F841 return argname("a", "*another") x = y = 1 @@ -283,36 +277,6 @@ def func(a, b): assert names == "x" -def test_argname_singledispatched(): - # GH53 - @singledispatch - def add(a, b): - aname = argname("a", "b", func=add.dispatch(object)) - return aname + (1,) # distinguish - - @add.register(int) - def add_int(a, b): - aname = argname("a", "b", func=add_int) - return aname + (2,) - - @add.register(str) - def add_str(a, b): - aname = argname("a", "b", dispatch=str) - return aname + (3,) - - x = y = 1 - out = add(x, y) - assert out == ("x", "y", 2) - - t = s = "a" - out = add(t, s) - assert out == ("t", "s", 3) - - p = q = 1.2 - out = add(p, q) - assert out == ("p", "q", 1) - - def test_argname_func_na(): def func(a): return argname("a") @@ -427,7 +391,6 @@ def __setitem__(self, name, value) -> None: self.__dict__["meta"]["name2"] = argname("name", vars_only=False) self.__dict__["meta"]["value"] = argname("value") - a = A() out = a.x assert out == "'x'" diff --git a/tests/test_bytecode_nameof.py b/tests/test_bytecode_nameof.py index ec87cfb..29d1f82 100644 --- a/tests/test_bytecode_nameof.py +++ b/tests/test_bytecode_nameof.py @@ -3,13 +3,16 @@ import pytest import unittest from varname.utils import bytecode_nameof as bytecode_nameof_cached -from varname import * +from varname import nameof, varname, ImproperUseError, VarnameRetrievingError + # config.debug = True + def bytecode_nameof(frame): frame = sys._getframe(frame) return bytecode_nameof_cached(frame.f_code, frame.f_lasti) + def nameof_both(var, *more_vars): """Test both implementations at the same time""" result = nameof(var, *more_vars, frame=2) @@ -18,58 +21,62 @@ def nameof_both(var, *more_vars): assert result == bytecode_nameof(frame=2) return result + class Weird: def __add__(self, other): bytecode_nameof(frame=2) + class TestNameof(unittest.TestCase): def test_original_nameof(self): x = 1 - self.assertEqual(nameof(x), 'x') - self.assertEqual(nameof_both(x), 'x') - self.assertEqual(bytecode_nameof(x), 'x') + self.assertEqual(nameof(x), "x") + self.assertEqual(nameof_both(x), "x") + self.assertEqual(bytecode_nameof(x), "x") def test_bytecode_nameof_wrong_node(self): with pytest.raises( - VarnameRetrievingError, - match="Did you call 'nameof' in a weird way", + VarnameRetrievingError, + match="Did you call 'nameof' in a weird way", ): Weird() + Weird() def test_bytecode_pytest_nameof_fail(self): with pytest.raises( - VarnameRetrievingError, - match=("Found the variable name '@py_assert2' " - "which is obviously wrong."), + VarnameRetrievingError, + match=( + "Found the variable name '@py_assert2' " "which is obviously wrong." + ), ): lam = lambda: 0 lam.a = 1 - assert bytecode_nameof(lam.a) == 'a' + assert bytecode_nameof(lam.a) == "a" def test_nameof(self): a = 1 b = nameof_both(a) - assert b == 'a' + assert b == "a" nameof2 = nameof_both c = nameof2(a, b) - assert b == 'a' - assert c == ('a', 'b') + assert b == "a" + assert c == ("a", "b") + def func(): - return varname() + 'abc' + return varname() + "abc" f = func() - assert f == 'fabc' + assert f == "fabc" - self.assertEqual(nameof_both(f), 'f') - self.assertEqual('f', nameof_both(f)) + self.assertEqual(nameof_both(f), "f") + self.assertEqual("f", nameof_both(f)) self.assertEqual(len(nameof_both(f)), 1) fname1 = fname = nameof_both(f) - self.assertEqual(fname, 'f') - self.assertEqual(fname1, 'f') + self.assertEqual(fname, "f") + self.assertEqual(fname1, "f") with pytest.raises(ImproperUseError): - nameof_both(a==1) + nameof_both(a == 1) with pytest.raises(VarnameRetrievingError): bytecode_nameof(a == 1) @@ -79,7 +86,7 @@ def func(): # nameof_both() def test_nameof_statements(self): - a = {'test': 1} + a = {"test": 1} test = {} del a[nameof_both(test)] assert a == {} @@ -87,22 +94,22 @@ def test_nameof_statements(self): def func(): return nameof_both(test) - assert func() == 'test' + assert func() == "test" def func2(): yield nameof_both(test) - assert list(func2()) == ['test'] + assert list(func2()) == ["test"] def func3(): raise ValueError(nameof_both(test)) with pytest.raises(ValueError) as verr: func3() - assert str(verr.value) == 'test' + assert str(verr.value) == "test" for i in [0]: - self.assertEqual(nameof_both(test), 'test') + self.assertEqual(nameof_both(test), "test") self.assertEqual(len(nameof_both(test)), 4) def test_nameof_expr(self): @@ -126,4 +133,4 @@ def test_nameof_expr(self): self.assertEqual(nameof_both(lam.lam.lam.lam), "lam") self.assertEqual(nameof_both(lams[0].lam), "lam") self.assertEqual(nameof_both(lams[0].lam.a), "a") - self.assertEqual(nameof_both((lam() or lams[0]).lam.a), "a") \ No newline at end of file + self.assertEqual(nameof_both((lam() or lams[0]).lam.a), "a") diff --git a/tests/test_nameof.py b/tests/test_nameof.py index d00a178..9caca6a 100644 --- a/tests/test_nameof.py +++ b/tests/test_nameof.py @@ -2,88 +2,98 @@ import pytest import subprocess -from varname import * +from varname import nameof, VarnameRetrievingError, ImproperUseError + def test_nameof_pytest_fail(): with pytest.raises( VarnameRetrievingError, match="Couldn't retrieve the call node. " - "This may happen if you're using some other AST magic" + "This may happen if you're using some other AST magic", ): - assert nameof(nameof) == 'nameof' + assert nameof(nameof) == "nameof" + def test_frame_fail_nameof(no_getframe): a = 1 with pytest.raises(VarnameRetrievingError): nameof(a) + def test_nameof_full(): x = lambda: None a = x a.b = x a.b.c = x name = nameof(a) - assert name == 'a' + assert name == "a" name = nameof(a, frame=1) - assert name == 'a' + assert name == "a" name = nameof(a.b) - assert name == 'b' + assert name == "b" name = nameof(a.b, vars_only=False) - assert name == 'a.b' + assert name == "a.b" name = nameof(a.b.c) - assert name == 'c' + assert name == "c" name = nameof(a.b.c, vars_only=False) - assert name == 'a.b.c' + assert name == "a.b.c" d = [a, a] - with pytest.raises( - ImproperUseError, - match='is not a variable or an attribute' - ): + with pytest.raises(ImproperUseError, match="is not a variable or an attribute"): name = nameof(d[0], vars_only=True) # we are not able to retreive full names without source code available with pytest.raises( - VarnameRetrievingError, - match=('Are you trying to call nameof from exec/eval') + VarnameRetrievingError, match=("Are you trying to call nameof from exec/eval") ): - eval('nameof(a.b, a)') + eval("nameof(a.b, a)") def test_nameof_from_stdin(): - code = ('from varname import nameof; ' - 'x = lambda: 0; ' - 'x.y = x; ' - 'print(nameof(x.y, x))') - p = subprocess.Popen([sys.executable], - stdin=subprocess.PIPE, - stdout=subprocess.PIPE, - stderr=subprocess.STDOUT, - encoding='utf8') + code = ( + "from varname import nameof; " + "x = lambda: 0; " + "x.y = x; " + "print(nameof(x.y, x))" + ) + p = subprocess.Popen( + [sys.executable], + stdin=subprocess.PIPE, + stdout=subprocess.PIPE, + stderr=subprocess.STDOUT, + encoding="utf8", + ) out, _ = p.communicate(input=code) - assert 'Are you trying to call nameof in REPL/python shell' in out + assert "Are you trying to call nameof in REPL/python shell" in out + def test_nameof_node_not_retrieved(): """Test when calling nameof without sourcecode available but filename is not or """ - source = ('from varname import nameof; ' - 'x = lambda: 0; ' - 'x.y = x; ' - 'print(nameof(x.y, x))') + source = ( + "from varname import nameof; " + "x = lambda: 0; " + "x.y = x; " + "print(nameof(x.y, x))" + ) code = compile(source, filename="", mode="exec") - with pytest.raises(VarnameRetrievingError, match='Source code unavailable'): + with pytest.raises(VarnameRetrievingError, match="Source code unavailable"): exec(code) - source = ('from varname import nameof; ' - 'x = lambda: 0; ' - 'x.y = x; ' - 'print(nameof(x.y, vars_only=True))') + source = ( + "from varname import nameof; " + "x = lambda: 0; " + "x.y = x; " + "print(nameof(x.y, vars_only=True))" + ) code = compile(source, filename="", mode="exec") with pytest.raises( - VarnameRetrievingError, - match="'nameof' can only be called with a single positional argument"): + VarnameRetrievingError, + match="'nameof' can only be called with a single positional argument", + ): exec(code) + def test_nameof_wrapper(): def decorator(f): @@ -95,4 +105,4 @@ def wrapper(var, *more_vars): wrap1 = decorator(nameof) x = y = 1 name = wrap1(x, y) - assert name == ('x', 'y') + assert name == ("x", "y") diff --git a/tests/test_varname.py b/tests/test_varname.py index ddf9821..ce00a6c 100644 --- a/tests/test_varname.py +++ b/tests/test_varname.py @@ -1,26 +1,33 @@ import sys import subprocess -from pathlib import Path from functools import wraps import pytest from executing import Source -from varname import * -from varname.helpers import * -from varname.utils import ImproperUseError, get_node +from varname import varname +from varname.utils import ( + VarnameRetrievingError, + QualnameNonUniqueError, + ImproperUseError, + MaybeDecoratedFunctionWarning, + MultiTargetAssignmentWarning, + get_node, +) from .conftest import run_async, module_from_source SELF = sys.modules[__name__] + def test_function(): def function(): return varname() func = function() - assert func == 'func' + assert func == "func" + def test_function_with_frame_arg(): @@ -35,21 +42,24 @@ def function2(): return function1() func = function2() - assert func == 'func' + assert func == "func" + def test_class(): class Foo: def __init__(self): self.id = varname() + def copy(self): return varname() k = Foo() - assert k.id == 'k' + assert k.id == "k" k2 = k.copy() - assert k2 == 'k2' + assert k2 == "k2" + def test_class_with_frame_arg(): @@ -70,10 +80,11 @@ def copy_id_internal(self): return varname(frame=3) k = Foo() - assert k.id == 'k' + assert k.id == "k" k2 = k.copy() - assert k2 == 'k2' + assert k2 == "k2" + def test_single_var_lhs_required(): """Only one variable to receive the name on LHS""" @@ -81,12 +92,14 @@ def test_single_var_lhs_required(): def function(): return varname() - with pytest.raises(ImproperUseError, - match='Expect a single variable on left-hand side'): + with pytest.raises( + ImproperUseError, match="Expect a single variable on left-hand side" + ): x, y = function() with pytest.raises(ImproperUseError): - x, y = function(), function() + x, y = function(), function() # noqa: F841 + def test_multi_vars_lhs(): """Tests multiple variables on the left hand side""" @@ -95,26 +108,27 @@ def function(): return varname(multi_vars=True) a, b = function() - assert (a, b) == ('a', 'b') + assert (a, b) == ("a", "b") [a, b] = function() - assert (a, b) == ('a', 'b') + assert (a, b) == ("a", "b") a = function() - assert a == ('a', ) + assert a == ("a",) # hierarchy a, (b, c) = function() - assert (a, b, c) == ('a', 'b', 'c') + assert (a, b, c) == ("a", "b", "c") # with attributes x = lambda: 1 a, (b, x.c) = function() - assert (a, b, x.c) == ('a', 'b', 'c') + assert (a, b, x.c) == ("a", "b", "x.c") # Not all LHS are variables y = {} with pytest.raises( ImproperUseError, - match='Can only get name of a variable or attribute, not Subscript' + match=r"Node 'y\[BinOp\]' detected", ): - a, y["a"] = function() + y[1 + 1] = function() + def test_raise_exc(): @@ -129,6 +143,7 @@ def get_name(raise_exc): with pytest.raises(ImproperUseError): name += str(get_name(False)) + def test_strict(): def foo(x): @@ -138,7 +153,7 @@ def function(): return varname(strict=True) func = function() - assert func == 'func' + assert func == "func" with pytest.raises(ImproperUseError): func = function() + "_" @@ -149,43 +164,94 @@ def function(): with pytest.raises(ImproperUseError): func = [function()] + def test_not_strict(): def function(): return varname(strict=False) func = function() - assert func == 'func' + assert func == "func" func = [function()] - assert func == ['func'] + assert func == ["func"] func = [function(), function()] - assert func == ['func', 'func'] + assert func == ["func", "func"] - func = (function(), ) - assert func == ('func', ) + func = (function(),) + assert func == ("func",) func = (function(), function()) - assert func == ('func', 'func') + assert func == ("func", "func") + @pytest.mark.skipif( - sys.version_info < (3, 8), - reason="named expressions require Python >= 3.8" + sys.version_info < (3, 8), reason="named expressions require Python >= 3.8" ) def test_named_expr(): from .named_expr import a + assert a == ["b", "c"] + def test_multiple_targets(): def function(): return varname() - with pytest.warns(MultiTargetAssignmentWarning, - match="Multiple targets in assignment"): + with pytest.warns( + MultiTargetAssignmentWarning, match="Multiple targets in assignment" + ): y = x = function() - assert y == x == 'x' + assert y == x == "x" + + +def test_subscript(): + + class C: + def __init__(self): + self.value = None + + def __setitem__(self, key, value): + self.value = value + + x = {"a": 1, "b": 2} + y = [0, 1, 2, 3, 4] + a = "a" + b = 1 + c = C() + + def func(): + return varname() + + x[0] = func() + assert x[0] == "x[0]" + x[a] = func() + assert x[a] == "x[a]" + x["a"] = func() + assert x["a"] == "x['a']" + c[[1]] = func() + assert c.value == "c[[1]]" + c[(1,)] = func() + assert c.value == "c[(1,)]" + c[(1, 2)] = func() + assert c.value == "c[(1, 2)]" + y[b] = func() + assert y[b] == "y[b]" + y[1] = func() + assert y[1] == "y[1]" + c[1:4:2] = func() + assert c.value == "c[1:4:2]" + c[1:4:2, 1:4:2] = func() + assert c.value == "c[(1:4:2, 1:4:2)]" + c[1:] = func() + assert c.value == "c[1:]" + c[:4] = func() + assert c.value == "c[:4]" + c[:] = func() + assert c.value == "c[:]" + def test_unusual(): @@ -194,17 +260,18 @@ def function(): # something ridiculous xyz = function()[-1:] - assert xyz == 'z' + assert xyz == "z" - x = 'a' + x = "a" with pytest.raises(ImproperUseError): x += function() - assert x == 'a' + assert x == "a" # alias func = function x = func() - assert x == 'x' + assert x == "x" + def test_from_property(): class C: @@ -214,7 +281,8 @@ def var(self): c = C() v1 = c.var - assert v1 == 'v1' + assert v1 == "v1" + def test_frame_fail(no_getframe): """Test when failed to retrieve the frame""" @@ -225,55 +293,61 @@ def func(raise_exc): return varname(raise_exc=raise_exc) with pytest.raises(VarnameRetrievingError): - a = func(True) + a = func(True) # noqa: F841 b = func(False) assert b is None + def test_ignore_module_filename(): - source = ('def foo(): return bar()') + source = "def foo(): return bar()" + + code = compile(source, "", "exec") - code = compile(source, '', 'exec') def bar(): - return varname(ignore='') + return varname(ignore="") - globs = {'bar': bar} + globs = {"bar": bar} exec(code, globs) - foo = globs['foo'] + foo = globs["foo"] f = foo() - assert f == 'f' + assert f == "f" + def test_ignore_module_no_file(tmp_path): module = module_from_source( - 'ignore_module', + "ignore_module", """ def foo(): return bar() """, - tmp_path + tmp_path, ) # force injecting __varname_ignore_id__ del module.__file__ def bar(): - return varname(ignore=[ - (module, 'foo'), # can't get module by inspect.getmodule - module - ]) + return varname( + ignore=[ + (module, "foo"), # can't get module by inspect.getmodule + module, + ] + ) + module.bar = bar f = module.foo() - assert f == 'f' + assert f == "f" def test_ignore_module_qualname_no_source(tmp_path): module = module_from_source( - 'ignore_module_qualname_no_source', + "ignore_module_qualname_no_source", """ def bar(): return 1 """, - tmp_path + tmp_path, ) source = Source.for_filename(module.__file__) # simulate when source is not available @@ -281,92 +355,105 @@ def bar(): source.tree = None def foo(): - return varname(ignore=(module, 'bar')) + return varname(ignore=(module, "bar")) + + f = foo() # noqa: F841 - f = foo() def test_ignore_module_qualname_ucheck_in_match( - tmp_path, - frame_matches_module_by_ignore_id_false + tmp_path, frame_matches_module_by_ignore_id_false ): module = module_from_source( - 'ignore_module_qualname_no_source_ucheck_in_match', + "ignore_module_qualname_no_source_ucheck_in_match", """ def foo(): return bar() """, - tmp_path + tmp_path, ) # force uniqueness to be checked in match # module cannot be fetched by inspect.getmodule module.__file__ = None def bar(): - return varname(ignore=[ - (module, 'foo'), # frame_matches_module_by_ignore_id_false makes this fail - (None, 'foo') # make sure foo to be ignored - ]) + return varname( + ignore=[ + ( + module, + "foo", + ), # frame_matches_module_by_ignore_id_false makes this fail + (None, "foo"), # make sure foo to be ignored + ] + ) module.bar = bar f = module.foo() - assert f == 'f' + assert f == "f" + def test_ignore_module_qualname(tmp_path, capsys, enable_debug): module = module_from_source( - 'ignore_module_qualname', - ''' + "ignore_module_qualname", + """ def foo1(): return bar() - ''', - tmp_path + """, + tmp_path, ) module.__file__ = None # module.__varname_ignore_id__ = object() def bar(): - var = varname(ignore=(module, 'foo1')) + var = varname(ignore=(module, "foo1")) return var module.bar = bar f = module.foo1() - assert f == 'f' + assert f == "f" + def test_ignore_filename_qualname(): - source = ('import sys\n' - 'import __main__\n' - 'import varname\n' - 'varname.config.debug = True\n' - 'from varname import varname\n' - 'def func(): \n' - ' return varname(ignore=[\n' - ' ("unknown", "wrapped"), \n' # used to trigger filename mismatch - ' ("", "wrapped")\n' - ' ])\n\n' - 'def wrapped():\n' - ' return func()\n\n' - 'variable = wrapped()\n') + source = ( + "import sys\n" + "import __main__\n" + "import varname\n" + "varname.config.debug = True\n" + "from varname import varname\n" + "def func(): \n" + " return varname(ignore=[\n" + ' ("unknown", "wrapped"), \n' # used to trigger filename mismatch + ' ("", "wrapped")\n' + " ])\n\n" + "def wrapped():\n" + " return func()\n\n" + "variable = wrapped()\n" + ) # code = compile(source, '', 'exec') # # ??? NameError: name 'func' is not defined # exec(code) - p = subprocess.Popen([sys.executable], - stdin=subprocess.PIPE, - stdout=subprocess.PIPE, - stderr=subprocess.STDOUT, - encoding='utf8') + p = subprocess.Popen( + [sys.executable], + stdin=subprocess.PIPE, + stdout=subprocess.PIPE, + stderr=subprocess.STDOUT, + encoding="utf8", + ) out, _ = p.communicate(input=source) assert "Ignored by IgnoreFilenameQualname('', 'wrapped')" in out + def test_ignore_function_warning(): def my_decorator(f): @wraps(f) def wrapper(): return f() + return wrapper @my_decorator @@ -376,14 +463,18 @@ def func1(): def func2(): return varname(ignore=[func1, (func1, 1)]) - with pytest.warns(MaybeDecoratedFunctionWarning, - match="You asked varname to ignore function 'func1'"): - f = func1() + with pytest.warns( + MaybeDecoratedFunctionWarning, + match="You asked varname to ignore function 'func1'", + ): + f = func1() # noqa: F841 + def test_ignore_decorated(): def my_decorator(f): def wrapper(): return f() + return wrapper @my_decorator @@ -394,7 +485,7 @@ def foo5(): return varname(ignore=(foo4, 1)) f4 = foo4() - assert f4 == 'f4' + assert f4 == "f4" @my_decorator def foo6(): @@ -404,23 +495,26 @@ def foo7(): return varname(ignore=(foo4, 100)) with pytest.raises(ImproperUseError): - f6 = foo6() + f6 = foo6() # noqa: F841 + def test_ignore_dirname(tmp_path): module = module_from_source( - 'ignore_dirname', + "ignore_dirname", """ from varname import varname def bar(dirname): return varname(ignore=[dirname]) """, - tmp_path + tmp_path, ) + def foo(): return module.bar(tmp_path) f = foo() - assert f == 'f' + assert f == "f" + def test_type_anno_varname(): @@ -429,10 +523,10 @@ def __init__(self): self.id = varname() foo: Foo = Foo() - assert foo.id == 'foo' + assert foo.id == "foo" + def test_generic_type_varname(): - import typing from typing import Generic, TypeVar T = TypeVar("T") @@ -442,29 +536,31 @@ def __init__(self): # Standard libraries are ignored by default now (0.6.0) # self.id = varname(ignore=[typing]) self.id = varname() + foo = Foo[int]() - assert foo.id == 'foo' + assert foo.id == "foo" - bar:Foo = Foo[str]() - assert bar.id == 'bar' + bar: Foo = Foo[str]() + assert bar.id == "bar" baz = Foo() - assert baz.id == 'baz' + assert baz.id == "baz" + def test_async_varname(): from . import conftest async def func(): - return varname(ignore=(conftest, 'run_async')) + return varname(ignore=(conftest, "run_async")) async def func2(): return varname(ignore=run_async) x = run_async(func()) - assert x == 'x' + assert x == "x" x2 = run_async(func2()) - assert x2 == 'x2' + assert x2 == "x2" # frame and ignore together async def func3(): @@ -475,41 +571,48 @@ async def main(): return await func3() x3 = run_async(main()) - assert x3 == 'x3' + assert x3 == "x3" + def test_invalid_ignores(): # unexpected ignore item def func(): return varname(ignore=1) + with pytest.raises(ValueError): f = func() def func(): - return varname(ignore=(1,2)) + return varname(ignore=(1, 2)) + with pytest.raises(ValueError): f = func() def func(): - return varname(ignore=(1, '2')) + return varname(ignore=(1, "2")) + with pytest.raises(ValueError): - f = func() + f = func() # noqa: F841 + def test_qualname_ignore_fail(): # non-unique qualname def func(): - return varname(ignore=[ - (SELF, 'test_qualname_ignore_fail..wrapper') - ]) + return varname( + ignore=[(SELF, "test_qualname_ignore_fail..wrapper")] + ) def wrapper(): return func() - wrapper2 = wrapper + wrapper2 = wrapper # noqa: F841 + def wrapper(): return func() with pytest.raises(QualnameNonUniqueError): - f = func() + f = func() # noqa: F841 + def test_ignore_lambda(): def foo(): @@ -518,12 +621,14 @@ def foo(): bar = lambda: foo() b = bar() - assert b == 'b' + assert b == "b" + def test_internal_debug(capsys, enable_debug): def my_decorator(f): def wrapper(): return f() + return wrapper @my_decorator @@ -541,21 +646,30 @@ def foo3(): ignore=[ (SELF, "*.wrapper"), # unrelated qualname will not be hit at all - (sys, 'wrapper') - ] + (sys, "wrapper"), + ], ) x = foo1() - assert x == 'x' + assert x == "x" msgs = capsys.readouterr().err.splitlines() assert ">>> IgnoreList initiated <<<" in msgs[0] assert "Ignored by IgnoreModule('varname')" in msgs[1] assert "Skipping (2 more to skip) [In 'foo3'" in msgs[2] - assert "Ignored by IgnoreModuleQualname('tests.test_varname', '*.wrapper')" in msgs[3] + assert ( + "Ignored by IgnoreModuleQualname('tests.test_varname', '*.wrapper')" + in msgs[3] + ) assert "Skipping (1 more to skip) [In 'foo2'" in msgs[4] - assert "Ignored by IgnoreModuleQualname('tests.test_varname', '*.wrapper')" in msgs[5] + assert ( + "Ignored by IgnoreModuleQualname('tests.test_varname', '*.wrapper')" + in msgs[5] + ) assert "Skipping (0 more to skip) [In 'foo1'" in msgs[6] - assert "Ignored by IgnoreModuleQualname('tests.test_varname', '*.wrapper')" in msgs[7] + assert ( + "Ignored by IgnoreModuleQualname('tests.test_varname', '*.wrapper')" + in msgs[7] + ) assert "Gotcha! [In 'test_internal_debug'" in msgs[8] @@ -567,4 +681,4 @@ def func(): a, *b, c = func() assert a == 1 assert b == [2, 3] - assert c == ('a', '*b', 'c') + assert c == ("a", "*b", "c") diff --git a/tests/test_will.py b/tests/test_will.py index 41866b8..8ec7229 100644 --- a/tests/test_will.py +++ b/tests/test_will.py @@ -1,7 +1,6 @@ -import sys - import pytest -from varname import * +from varname import will, VarnameRetrievingError, ImproperUseError + def test_will(): def i_will(): @@ -14,8 +13,9 @@ def i_will(): return func func = i_will().abc - assert func.will == 'abc' - assert getattr(func, 'will') == 'abc' + assert func.will == "abc" + assert getattr(func, "will") == "abc" + def test_will_deep(): @@ -32,7 +32,8 @@ def i_will(): return func func = i_will().abc - assert func.will == 'abc' + assert func.will == "abc" + # issue #17 def test_will_property(): @@ -47,15 +48,15 @@ def iwill(self): return self def do(self): - return 'I will do something' + return "I will do something" c = C() - x = c.iwill + x = c.iwill # noqa F841 assert c.will is None result = c.iwill.do() - assert c.will == 'do' - assert result == 'I will do something' + assert c.will == "do" + assert result == "I will do something" def test_will_method(): @@ -73,17 +74,15 @@ def permit(self, *_): self.wills.append(will(raise_exc=False)) if self.wills[-1] is None: - raise AttributeError( - 'Should do something with AwesomeClass object' - ) + raise AttributeError("Should do something with AwesomeClass object") # let self handle do return self def do(self): - if self.wills[-1] != 'do': + if self.wills[-1] != "do": raise AttributeError("You don't have permission to do") - return 'I am doing!' + return "I am doing!" __getitem__ = permit @@ -94,40 +93,41 @@ def do(self): with pytest.raises(AttributeError) as exc: awesome.permit() - assert str(exc.value) == 'Should do something with AwesomeClass object' + assert str(exc.value) == "Should do something with AwesomeClass object" # clear wills awesome = AwesomeClass() ret = awesome.permit().do() - assert ret == 'I am doing!' - assert awesome.wills == [None, 'do'] + assert ret == "I am doing!" + assert awesome.wills == [None, "do"] awesome = AwesomeClass() ret = awesome.myself().permit().do() - assert ret == 'I am doing!' - assert awesome.wills == [None, 'do'] + assert ret == "I am doing!" + assert awesome.wills == [None, "do"] awesome = AwesomeClass() ret = awesome().permit().do() - assert ret == 'I am doing!' - assert awesome.wills == [None, 'do'] + assert ret == "I am doing!" + assert awesome.wills == [None, "do"] awesome = AwesomeClass() ret = awesome.attr.permit().do() - assert ret == 'I am doing!' - assert awesome.wills == [None, 'do'] + assert ret == "I am doing!" + assert awesome.wills == [None, "do"] awesome = AwesomeClass() ret = awesome.permit().permit().do() - assert ret == 'I am doing!' - assert awesome.wills == [None, 'permit', 'do'] + assert ret == "I am doing!" + assert awesome.wills == [None, "permit", "do"] with pytest.raises(AttributeError) as exc: print(awesome[2]) - assert str(exc.value) == 'Should do something with AwesomeClass object' + assert str(exc.value) == "Should do something with AwesomeClass object" ret = awesome[2].do() - assert ret == 'I am doing!' + assert ret == "I am doing!" + def test_will_decorated(): @@ -155,10 +155,10 @@ def __getattr__(self, name): return self.will x = Foo().get_will().x - assert x == 'x' + assert x == "x" x = Foo().get_will_decor().x - assert x == 'x' + assert x == "x" def test_will_fail(): diff --git a/tox.ini b/tox.ini index f572d7f..4246a73 100644 --- a/tox.ini +++ b/tox.ini @@ -3,4 +3,4 @@ ignore = E203, W503, E731 per-file-ignores = # imported but unused __init__.py: F401, E402 -max-line-length = 81 +max-line-length = 89 diff --git a/varname/__init__.py b/varname/__init__.py index ac5a8c7..c161220 100644 --- a/varname/__init__.py +++ b/varname/__init__.py @@ -13,4 +13,4 @@ ) from .core import varname, nameof, will, argname -__version__ = "0.12.2" +__version__ = "0.13.0" diff --git a/varname/utils.py b/varname/utils.py index 5099eab..a9dfa72 100644 --- a/varname/utils.py +++ b/varname/utils.py @@ -185,7 +185,10 @@ def lookfor_parent_assign(node: ast.AST, strict: bool = True) -> AssignType: return None -def node_name(node: ast.AST) -> Union[str, Tuple[Union[str, Tuple], ...]]: +def node_name( + node: ast.AST, + subscript_slice: bool = False, +) -> Union[str, Tuple[Union[str, Tuple], ...]]: """Get the node node name. Raises ImproperUseError when failed @@ -193,15 +196,50 @@ def node_name(node: ast.AST) -> Union[str, Tuple[Union[str, Tuple], ...]]: if isinstance(node, ast.Name): return node.id if isinstance(node, ast.Attribute): - return node.attr - if isinstance(node, (ast.List, ast.Tuple)): + return f"{node_name(node.value)}.{node.attr}" + if isinstance(node, ast.Constant): + return repr(node.value) + if isinstance(node, (ast.List, ast.Tuple)) and not subscript_slice: return tuple(node_name(elem) for elem in node.elts) + if isinstance(node, ast.List): + return f"[{', '.join(node_name(elem) for elem in node.elts)}]" # type: ignore + if isinstance(node, ast.Tuple): + if len(node.elts) == 1: + return f"({node_name(node.elts[0])},)" + return f"({', '.join(node_name(elem) for elem in node.elts)})" # type: ignore if isinstance(node, ast.Starred): return f"*{node_name(node.value)}" + if isinstance(node, ast.Slice): + return ( + f"{node_name(node.lower)}:{node_name(node.upper)}:{node_name(node.step)}" + if node.lower is not None + and node.upper is not None + and node.step is not None + else f"{node_name(node.lower)}:{node_name(node.upper)}" + if node.lower is not None and node.upper is not None + else f"{node_name(node.lower)}:" + if node.lower is not None + else f":{node_name(node.upper)}" + if node.upper is not None + else ":" + ) + + name = type(node).__name__ + if isinstance(node, ast.Subscript): + try: + return f"{node_name(node.value)}[{node_name(node.slice, True)}]" + except ImproperUseError: + name = f"{node_name(node.value)}[{type(node.slice).__name__}]" raise ImproperUseError( - f"Can only get name of a variable or attribute, " - f"not {ast.dump(node)}" + f"Node {name!r} detected, but only following nodes are supported: \n" + " - ast.Name (e.g. x)\n" + " - ast.Attribute (e.g. x.y, x be other supported nodes)\n" + " - ast.Constant (e.g. 1, 'a')\n" + " - ast.List (e.g. [x, y, z])\n" + " - ast.Tuple (e.g. (x, y, z))\n" + " - ast.Starred (e.g. *x)\n" + " - ast.Subscript with slice of the above nodes (e.g. x[y])" )