Skip to content

Commit 09498d8

Browse files
pwwangalexmojaki
andauthored
Allow varname.varname to receive multiple variables on the left-hand side (#29)
* Allow `varname.varname` to receive multiple variables on the left-hand side * Fix typo in test * Add tests for mismatch nvars for varname.varname * Fix the numbers in mismatch nvars test for varname.varname * Update docs for varname.varname * Make docs clearer * Make docs clearer * Change behavor of nvars=None for varname * Update varname.py Co-authored-by: Alex Hall <[email protected]> * Add tests for hierarchical LHS for varname * Change nvars to multi_vars and make it boolean * Add test for attributes on LHS for varname * Fix docs for varname; add StringOrTuple for type hints * Fix type hints Co-authored-by: Alex Hall <[email protected]>
1 parent 6a1e8bd commit 09498d8

File tree

5 files changed

+87
-11
lines changed

5 files changed

+87
-11
lines changed

README.md

Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -92,6 +92,22 @@ Special thanks to [@HanyuuLu][2] to give up the name `varname` in pypi for this
9292
k2 = k.copy() # k2.id == 'k2'
9393
```
9494

95+
- Multiple variables on Left-hand side
96+
97+
```python
98+
# since v0.5.4
99+
100+
def func():
101+
return varname(multi_vars=True)
102+
103+
a = func() # a == ('a', )
104+
a, b = func() # (a, b) == ('a', 'b')
105+
[a, b] = func() # (a, b) == ('a', 'b')
106+
107+
# hierarchy is also possible
108+
a, (b, c) = func() # (a, b, c) == ('a', 'b', 'c')
109+
```
110+
95111
- Some unusual use
96112

97113
```python

docs/CHANGELOG.md

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,6 @@
1+
## v0.5.4
2+
- Allow `varname.varname` to receive multiple variables on the left-hand side
3+
14
## v0.5.3
25
- Add `debug` function
36
- Deprecate `namedtuple` (will be removed in `0.6.0`)

pyproject.toml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,7 @@ build-backend = "poetry.masonry.api"
44

55
[tool.poetry]
66
name = "varname"
7-
version = "0.5.3"
7+
version = "0.5.4"
88
description = "Dark magics about variable names in python."
99
authors = [ "pwwang <[email protected]>",]
1010
license = "MIT"

tests/test_varname.py

Lines changed: 34 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -168,18 +168,48 @@ def func2():
168168

169169
assert func2() == ('p1', 'p2')
170170

171-
def test_only_one_lhs():
171+
def test_single_var_lhs_error():
172172
"""Only one variable to receive the name on LHS"""
173173

174174
def function():
175175
return varname()
176176

177-
with pytest.raises(VarnameRetrievingError):
177+
with pytest.raises(VarnameRetrievingError,
178+
match='Expecting a single variable on left-hand side'):
178179
x, y = function()
179180
with pytest.raises(VarnameRetrievingError):
180181
x, y = function(), function()
181-
with pytest.raises(VarnameRetrievingError):
182-
[x] = function()
182+
183+
# This is now supported in 0.5.4
184+
# with pytest.raises(VarnameRetrievingError):
185+
# [x] = function()
186+
187+
def test_multi_vars_lhs():
188+
"""Tests multiple variables on the left hand side"""
189+
190+
def function():
191+
return varname(multi_vars=True)
192+
193+
a, b = function()
194+
assert (a, b) == ('a', 'b')
195+
[a, b] = function()
196+
assert (a, b) == ('a', 'b')
197+
a = function()
198+
assert a == ('a', )
199+
# hierarchy
200+
a, (b, c) = function()
201+
assert (a, b, c) == ('a', 'b', 'c')
202+
# with attributes
203+
x = lambda: 1
204+
a, (b, x.c) = function()
205+
assert (a, b, x.c) == ('a', 'b', 'c')
206+
207+
# Not all LHS are variables
208+
with pytest.raises(
209+
VarnameRetrievingError,
210+
match='Can only get name of a variable or attribute, not Starred'
211+
):
212+
a, *b = function()
183213

184214
def test_raise():
185215

varname.py

Lines changed: 33 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -10,7 +10,7 @@
1010

1111
import executing
1212

13-
__version__ = "0.5.3"
13+
__version__ = "0.5.4"
1414
__all__ = [
1515
"VarnameRetrievingError", "varname", "will",
1616
"inject", "nameof", "namedtuple", "Wrapper", "debug"
@@ -19,22 +19,33 @@
1919
class VarnameRetrievingError(Exception):
2020
"""When failed to retrieve the varname"""
2121

22-
def varname(caller: int = 1, raise_exc: bool = True) -> Optional[str]:
22+
def varname(
23+
caller: int = 1,
24+
multi_vars: bool = False,
25+
raise_exc: bool = True
26+
) -> Optional[Union[str, Tuple[Union[str, tuple]]]]:
2327
"""Get the variable name that assigned by function/class calls
2428
2529
Args:
2630
caller: The call stack index, indicating where this function
2731
is called relative to where the variable is finally retrieved
32+
multi_vars: Whether allow multiple variables on left-hand side (LHS).
33+
If `True`, this function returns a tuple of the variable names,
34+
even there is only one variable on LHS.
35+
If `False`, and multiple variables on LHS, a
36+
`VarnameRetrievingError` will be raised.
2837
raise_exc: Whether we should raise an exception if failed
2938
to retrieve the name.
3039
3140
Returns:
3241
The variable name, or `None` when `raise_exc` is `False` and
3342
we failed to retrieve the variable name.
43+
A tuple or a hierarchy (tuple of tuples) of variable names
44+
when `multi_vars` is `True`.
3445
3546
Raises:
36-
VarnameRetrievingError: When there is invalid variable used on the
37-
left of the assign node. (e.g: `a.b = func()`) or
47+
VarnameRetrievingError: When there is invalid variables or
48+
invalid number of variables used on the LHS; or
3849
when we are unable to retrieve the variable name and `raise_exc`
3950
is set to `True`.
4051
@@ -63,7 +74,21 @@ def varname(caller: int = 1, raise_exc: bool = True) -> Optional[str]:
6374
"on the very left will be used.",
6475
UserWarning)
6576
target = node.targets[0]
66-
return _node_name(target)
77+
78+
names = _node_name(target)
79+
80+
if not isinstance(names, tuple):
81+
names = (names, )
82+
83+
if multi_vars:
84+
return names
85+
86+
if len(names) > 1:
87+
raise VarnameRetrievingError(
88+
f"Expecting a single variable on left-hand side, got {len(names)}."
89+
)
90+
91+
return names[0]
6792

6893
def will(caller: int = 1, raise_exc: bool = True) -> Optional[str]:
6994
"""Detect the attribute name right immediately after a function call.
@@ -408,7 +433,7 @@ def _lookfor_parent_assign(node: ast.AST) -> Optional[ast.Assign]:
408433
return node
409434
return None
410435

411-
def _node_name(node: ast.AST) -> str:
436+
def _node_name(node: ast.AST) -> Optional[Union[str, Tuple[Union[str, tuple]]]]:
412437
"""Get the node node name.
413438
414439
Raises VarnameRetrievingError when failed
@@ -417,6 +442,8 @@ def _node_name(node: ast.AST) -> str:
417442
return node.id
418443
if isinstance(node, ast.Attribute):
419444
return node.attr
445+
if isinstance(node, (ast.List, ast.Tuple)):
446+
return tuple(_node_name(elem) for elem in node.elts)
420447

421448
raise VarnameRetrievingError(
422449
f"Can only get name of a variable or attribute, "

0 commit comments

Comments
 (0)