Skip to content

Commit bd6b2d5

Browse files
authored
Allow varname to play with type annotation context (#32)
* Allow assignment with type annontation for varname * Implement #33 * Fix for python3.6 * Implement expected behavior for ignore and caller for varname.varname * Fix python3.6 for async.run in tests * Add uniqueness check for qualname in ignore; Change _get_executing back to _get_frame to avoid expense of converting each frame into an executing object. * Fix typo * Put ignore list assertion into _check_qualname
1 parent e00c071 commit bd6b2d5

File tree

5 files changed

+332
-66
lines changed

5 files changed

+332
-66
lines changed

README.rst

Lines changed: 27 additions & 29 deletions
Original file line numberDiff line numberDiff line change
@@ -60,8 +60,7 @@ Features
6060
* Fetching variable names directly using ``nameof``
6161
* A value wrapper to store the variable name that a value is assigned to using ``Wrapper``
6262
* Detecting next immediate attribute name using ``will``
63-
* Shortcut for ``collections.namedtuple``
64-
* Injecting ``__varname__`` to objects
63+
* Injecting ``__varname__`` to classes
6564
* A ``debug`` function to print variables with their names and values.
6665

6766
Credits
@@ -146,6 +145,23 @@ Retrieving the variable names from inside a function call/class instantiation
146145
147146
k2 = k.copy() # k2.id == 'k2'
148147
148+
*
149+
Multiple variables on Left-hand side
150+
151+
.. code-block:: python
152+
153+
# since v0.5.4
154+
155+
def func():
156+
return varname(multi_vars=True)
157+
158+
a = func() # a == ('a', )
159+
a, b = func() # (a, b) == ('a', 'b')
160+
[a, b] = func() # (a, b) == ('a', 'b')
161+
162+
# hierarchy is also possible
163+
a, (b, c) = func() # (a, b, c) == ('a', 'b', 'c')
164+
149165
*
150166
Some unusual use
151167

@@ -256,41 +272,23 @@ Detecting next immediate attribute name
256272
awesome.permit() # AttributeError: Should do something with AwesomeClass object
257273
awesome.permit().do() == 'I am doing!'
258274
259-
Shortcut for ``collections.namedtuple``
260-
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
275+
Injecting ``__varname__`` to classes
276+
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
261277

262278
.. code-block:: python
263279
264-
# instead of
265-
from collections import namedtuple
266-
Name = namedtuple('Name', ['first', 'last'])
280+
from varname import inject_varname
267281
268-
# we can do:
269-
from varname import namedtuple
270-
Name = namedtuple(['first', 'last'])
271-
272-
Injecting ``__varname__``
273-
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
274-
275-
.. code-block:: python
276-
277-
from varname import inject
278-
279-
class MyList(list):
282+
@inject_varname
283+
class Dict(dict):
280284
pass
281285
282-
a = inject(MyList())
283-
b = inject(MyList())
284-
286+
a = Dict(a=1)
287+
b = Dict(b=2)
285288
a.__varname__ == 'a'
286289
b.__varname__ == 'b'
287-
288-
a == b
289-
290-
# other methods not affected
291-
a.append(1)
292-
b.append(1)
293-
a == b
290+
a.update(b)
291+
a == {'a':1, 'b':2}
294292
295293
Debugging with ``debug``
296294
^^^^^^^^^^^^^^^^^^^^^^^^^^^^

setup.py

Lines changed: 5 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,4 @@
1+
12
# -*- coding: utf-8 -*-
23

34
# DO NOT EDIT THIS FILE!
@@ -9,6 +10,7 @@
910
except ImportError:
1011
from distutils.core import setup
1112

13+
1214
import os.path
1315

1416
readme = ''
@@ -18,16 +20,14 @@
1820
with open(readme_path, 'rb') as stream:
1921
readme = stream.read().decode('utf8')
2022

23+
2124
setup(
2225
long_description=readme,
2326
name='varname',
24-
version='0.5.3',
27+
version='0.5.5',
2528
description='Dark magics about variable names in python.',
2629
python_requires='==3.*,>=3.6.0',
27-
project_urls={
28-
"homepage": "https://github.com/pwwang/python-varname",
29-
"repository": "https://github.com/pwwang/python-varname"
30-
},
30+
project_urls={"homepage": "https://github.com/pwwang/python-varname", "repository": "https://github.com/pwwang/python-varname"},
3131
author='pwwang',
3232
author_email='pwwang@pwwang.com',
3333
license='MIT',

tests/test_bytecode_nameof.py

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,8 @@
55

66
def nameof_both(*args):
77
"""Test both implementations at the same time"""
8+
# No need caller=2 anymore, since internal calls from varname are ignored
9+
# now by default
810
result = nameof(*args, caller=2)
911
if len(args) == 1:
1012
assert result == _bytecode_nameof(caller=2)
@@ -46,7 +48,6 @@ def test_nameof(self):
4648
c = nameof2(a, b)
4749
assert b == 'a'
4850
assert c == ('a', 'b')
49-
5051
def func():
5152
return varname() + 'abc'
5253

tests/test_varname.py

Lines changed: 135 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,4 @@
11
import sys
2-
import unittest
32

43
import pytest
54
import subprocess
@@ -22,6 +21,12 @@ def getframe(_context):
2221
finally:
2322
sys._getframe = orig_getframe
2423

24+
@pytest.fixture
25+
def enable_debug():
26+
import varname as _varname
27+
_varname.DEBUG = True
28+
yield
29+
_varname.DEBUG = False
2530

2631
def test_function():
2732

@@ -573,6 +578,42 @@ def test_debug(capsys):
573578
debug(a, b, merge=True)
574579
assert 'DEBUG: a=1, b=<object' in capsys.readouterr().out
575580

581+
def test_internal_debug(capsys, enable_debug):
582+
def my_decorator(f):
583+
def wrapper():
584+
return f()
585+
return wrapper
586+
587+
@my_decorator
588+
def foo1():
589+
return foo2()
590+
591+
@my_decorator
592+
def foo2():
593+
return foo3()
594+
595+
@my_decorator
596+
def foo3():
597+
return varname(
598+
caller=3,
599+
ignore=[(
600+
sys.modules[__name__],
601+
"test_internal_debug.<locals>.my_decorator.<locals>.wrapper"
602+
)]
603+
)
604+
605+
x = foo1()
606+
assert x == 'x'
607+
msgs = capsys.readouterr().err.splitlines()
608+
assert "Skipping frame from varname [In 'varname'" in msgs[0]
609+
assert "Skipping (2 more to skip) [In 'foo3'" in msgs[1]
610+
assert "Ignored [In 'wrapper'" in msgs[2]
611+
assert "Skipping (1 more to skip) [In 'foo2'" in msgs[3]
612+
assert "Ignored [In 'wrapper'" in msgs[4]
613+
assert "Skipping (0 more to skip) [In 'foo1'" in msgs[5]
614+
assert "Ignored [In 'wrapper'" in msgs[6]
615+
assert "Gotcha! [In 'test_internal_debug'" in msgs[7]
616+
576617
def test_inject_varname():
577618

578619
@inject_varname
@@ -587,3 +628,96 @@ def __init__(self, a=1):
587628
f2 = Foo(2)
588629
assert f2.__varname__ == 'f2'
589630
assert f2.a == 2
631+
632+
def test_type_anno_varname():
633+
634+
class Foo:
635+
def __init__(self):
636+
self.id = varname()
637+
638+
foo: Foo = Foo()
639+
assert foo.id == 'foo'
640+
641+
def test_generic_type_varname():
642+
import typing
643+
from typing import Generic, TypeVar
644+
645+
T = TypeVar("T")
646+
647+
class Foo(Generic[T]):
648+
def __init__(self):
649+
self.id = varname(ignore=[typing])
650+
foo = Foo[int]()
651+
assert foo.id == 'foo'
652+
653+
bar:Foo = Foo[str]()
654+
assert bar.id == 'bar'
655+
656+
baz = Foo()
657+
assert baz.id == 'baz'
658+
659+
def test_async_varname():
660+
661+
import asyncio
662+
663+
def run_async(coro):
664+
if sys.version_info < (3, 7):
665+
loop = asyncio.get_event_loop()
666+
return loop.run_until_complete(coro)
667+
else:
668+
return asyncio.run(coro)
669+
670+
async def func():
671+
return varname(
672+
ignore=[
673+
asyncio,
674+
(sys.modules[__name__], 'test_async_varname.<locals>.run_async')
675+
]
676+
)
677+
678+
x = run_async(func())
679+
assert x == 'x'
680+
681+
async def func():
682+
# also works this way
683+
return varname(
684+
caller=2,
685+
ignore=[asyncio, (sys.modules[__name__],
686+
'test_async_varname.<locals>.run_async')]
687+
)
688+
689+
async def main():
690+
return await func()
691+
692+
x = run_async(main())
693+
assert x == 'x'
694+
695+
def test_qualname_ignore_fail():
696+
# not a list
697+
def func():
698+
return varname(ignore=sys)
699+
700+
with pytest.raises(AssertionError):
701+
f = func()
702+
703+
# non-existing qualname
704+
def func():
705+
return varname(ignore=[(sys.modules[__name__], 'nosuchqualname')])
706+
707+
with pytest.raises(AssertionError):
708+
f = func()
709+
710+
# non-unique qualname
711+
def func():
712+
return varname(ignore=[(sys.modules[__name__],
713+
'test_qualname_ignore_fail.<locals>.wrapper')])
714+
715+
def wrapper():
716+
return func()
717+
718+
wrapper2 = wrapper
719+
def wrapper():
720+
return func()
721+
722+
with pytest.raises(AssertionError):
723+
f = func()

0 commit comments

Comments
 (0)