cpython/Lib/test/test_unpack_ex.py
adam j hartz ccbe41e27c
gh-143055: Implementation of PEP 798 (#143056)
Co-authored-by: Jelle Zijlstra <jelle.zijlstra@gmail.com>
2026-01-30 20:37:52 -08:00

571 lines
14 KiB
Python

# Tests for extended unpacking, starred expressions.
import doctest
import unittest
doctests = """
Unpack tuple
>>> t = (1, 2, 3)
>>> a, *b, c = t
>>> a == 1 and b == [2] and c == 3
True
Unpack list
>>> l = [4, 5, 6]
>>> a, *b = l
>>> a == 4 and b == [5, 6]
True
Unpack implied tuple
>>> *a, = 7, 8, 9
>>> a == [7, 8, 9]
True
Unpack nested implied tuple
>>> [*[*a]] = [[7,8,9]]
>>> a == [[7,8,9]]
True
Unpack string... fun!
>>> a, *b = 'one'
>>> a == 'o' and b == ['n', 'e']
True
Unpack long sequence
>>> a, b, c, *d, e, f, g = range(10)
>>> (a, b, c, d, e, f, g) == (0, 1, 2, [3, 4, 5, 6], 7, 8, 9)
True
Unpack short sequence
>>> a, *b, c = (1, 2)
>>> a == 1 and c == 2 and b == []
True
Unpack generic sequence
>>> class Seq:
... def __getitem__(self, i):
... if i >= 0 and i < 3: return i
... raise IndexError
...
>>> a, *b = Seq()
>>> a == 0 and b == [1, 2]
True
Unpack in for statement
>>> for a, *b, c in [(1,2,3), (4,5,6,7)]:
... print(a, b, c)
...
1 [2] 3
4 [5, 6] 7
Unpack in list
>>> [a, *b, c] = range(5)
>>> a == 0 and b == [1, 2, 3] and c == 4
True
Multiple targets
>>> a, *b, c = *d, e = range(5)
>>> a == 0 and b == [1, 2, 3] and c == 4 and d == [0, 1, 2, 3] and e == 4
True
Assignment unpacking
>>> a, b, *c = range(5)
>>> a, b, c
(0, 1, [2, 3, 4])
>>> *a, b, c = a, b, *c
>>> a, b, c
([0, 1, 2], 3, 4)
Set display element unpacking
>>> a = [1, 2, 3]
>>> sorted({1, *a, 0, 4})
[0, 1, 2, 3, 4]
>>> {1, *1, 0, 4}
Traceback (most recent call last):
...
TypeError: 'int' object is not iterable
Dict display element unpacking
>>> kwds = {'z': 0, 'w': 12}
>>> sorted({'x': 1, 'y': 2, **kwds}.items())
[('w', 12), ('x', 1), ('y', 2), ('z', 0)]
>>> sorted({**{'x': 1}, 'y': 2, **{'z': 3}}.items())
[('x', 1), ('y', 2), ('z', 3)]
>>> sorted({**{'x': 1}, 'y': 2, **{'x': 3}}.items())
[('x', 3), ('y', 2)]
>>> sorted({**{'x': 1}, **{'x': 3}, 'x': 4}.items())
[('x', 4)]
>>> {**{}}
{}
>>> a = {}
>>> {**a}[0] = 1
>>> a
{}
>>> {**1}
Traceback (most recent call last):
...
TypeError: 'int' object is not a mapping
>>> {**[]}
Traceback (most recent call last):
...
TypeError: 'list' object is not a mapping
>>> len(eval("{" + ", ".join("**{{{}: {}}}".format(i, i)
... for i in range(1000)) + "}"))
1000
>>> {0:1, **{0:2}, 0:3, 0:4}
{0: 4}
Comprehension element unpacking
>>> a, b, c = [0, 1, 2], 3, 4
>>> [*a, b, c]
[0, 1, 2, 3, 4]
>>> l = [a, (3, 4), {5}, {6: None}, (i for i in range(7, 10))]
>>> [*item for item in l]
[0, 1, 2, 3, 4, 5, 6, 7, 8, 9]
>>> [*[0, 1] for i in range(5)]
[0, 1, 0, 1, 0, 1, 0, 1, 0, 1]
>>> [*'a' for i in range(5)]
['a', 'a', 'a', 'a', 'a']
>>> [*[] for i in range(10)]
[]
>>> [*(x*2) for x in [[1, 2, 3], [], 'cat']]
[1, 2, 3, 1, 2, 3, 'c', 'a', 't', 'c', 'a', 't']
>>> {**{} for a in [1]}
{}
>>> {**{7: i} for i in range(10)}
{7: 9}
>>> dicts = [{1: 2}, {3: 4}, {5: 6, 7: 8}, {}, {9: 10}, {1: 0}]
>>> {**d for d in dicts}
{1: 0, 3: 4, 5: 6, 7: 8, 9: 10}
>>> gen = (*(0, 1) for i in range(5))
>>> next(gen)
0
>>> list(gen)
[1, 0, 1, 0, 1, 0, 1, 0, 1]
Comprehension unpacking with conditionals and double loops
>>> [*[i, i+1] for i in range(5) if i % 2 == 0]
[0, 1, 2, 3, 4, 5]
>>> [*y for x in [[[0], [1, 2, 3], [], [4, 5]], [[6, 7]]] for y in x]
[0, 1, 2, 3, 4, 5, 6, 7]
>>> [*y for x in [[[0], [1, 2, 3], [], [4, 5]], [[6, 7]]] for y in x if y and y[0]>0]
[1, 2, 3, 4, 5, 6, 7]
>>> dicts = [{1: 2}, {3: 4}, {5: 6, 7: 8}, {}, {9: 10}, {1: 0}]
>>> {**d for d in dicts if len(d) != 2}
{1: 0, 3: 4, 9: 10}
Scoping of assignment expressions in comprehensions
>>> [*((y := i**2), 2*y) for i in range(4)]
[0, 0, 1, 2, 4, 8, 9, 18]
>>> y
9
>>> [*(y := [i, i+1, i+2]) for i in range(4)]
[0, 1, 2, 1, 2, 3, 2, 3, 4, 3, 4, 5]
>>> y
[3, 4, 5]
>>> g = (*(z := [i, i+1, i+2]) for i in range(4))
>>> z
Traceback (most recent call last):
...
NameError: name 'z' is not defined
>>> next(g)
0
>>> z
[0, 1, 2]
>>> next(g)
1
>>> z
[0, 1, 2]
>>> next(g)
2
>>> z
[0, 1, 2]
>>> next(g)
1
>>> z
[1, 2, 3]
>>> x = [1, 2, 3]
>>> y = [4, 5, 6]
>>> def f(*args):
... print(args)
>>> f(*x if x else y)
(1, 2, 3)
Malformed comperehension element unpacking
>>> [*x for x in [1, 2, 3]]
Traceback (most recent call last):
...
[*x for x in [1, 2, 3]]
^^
TypeError: Value after * must be an iterable, not int
Error messages for specific failure modes of unpacking
>>> [*x if x else y for x in z]
Traceback (most recent call last):
...
[*x if x else y for x in z]
^^^^^^^^^^^^^^
SyntaxError: invalid starred expression. Did you forget to wrap the conditional expression in parentheses?
>>> [*x if x else y]
Traceback (most recent call last):
...
[*x if x else y]
^^^^^^^^^^^^^^
SyntaxError: invalid starred expression. Did you forget to wrap the conditional expression in parentheses?
>>> [x if x else *y for x in z]
Traceback (most recent call last):
...
[x if x else *y for x in z]
^
SyntaxError: cannot unpack only part of a conditional expression
>>> [x if x else *y]
Traceback (most recent call last):
...
[x if x else *y]
^
SyntaxError: cannot unpack only part of a conditional expression
>>> {**x if x else y}
Traceback (most recent call last):
...
{**x if x else y}
^^^^^^^^^^^^^^^^
SyntaxError: invalid double starred expression. Did you forget to wrap the conditional expression in parentheses?
>>> {x if x else **y}
Traceback (most recent call last):
...
{x if x else **y}
^^
SyntaxError: cannot use dict unpacking on only part of a conditional expression
>>> [**x for x in [{1: 2}]]
Traceback (most recent call last):
...
[**x for x in [{1: 2}]]
^^^
SyntaxError: cannot use dict unpacking in list comprehension
>>> (**x for x in [{1:2}])
Traceback (most recent call last):
...
(**x for x in [{1:2}])
^^^
SyntaxError: cannot use dict unpacking in generator expression
>>> dict(**x for x in [{1:2}])
Traceback (most recent call last):
...
dict(**x for x in [{1:2}])
^^^
SyntaxError: cannot use dict unpacking in generator expression
>>> {*a: b for a, b in {1: 2}.items()}
Traceback (most recent call last):
...
{*a: b for a, b in {1: 2}.items()}
^^
SyntaxError: cannot use a starred expression in a dictionary key
>>> {**a: b for a, b in {1: 2}.items()}
Traceback (most recent call last):
...
{**a: b for a, b in {1: 2}.items()}
^^^
SyntaxError: cannot use dict unpacking in a dictionary key
>>> {a: *b for a, b in {1: 2}.items()}
Traceback (most recent call last):
...
{a: *b for a, b in {1: 2}.items()}
^^
SyntaxError: cannot use a starred expression in a dictionary value
>>> {a: **b for a, b in {1: 2}.items()}
Traceback (most recent call last):
...
{a: **b for a, b in {1: 2}.items()}
^^^
SyntaxError: cannot use dict unpacking in a dictionary value
# Generator expression in function arguments
>>> list(*x for x in (range(5) for i in range(3)))
[0, 1, 2, 3, 4, 0, 1, 2, 3, 4, 0, 1, 2, 3, 4]
>>> def f(arg):
... print(type(arg), list(arg), list(arg))
>>> f(*x for x in [[1,2,3]])
<class 'generator'> [1, 2, 3] []
Iterable argument unpacking
>>> print(*[1], *[2], 3)
1 2 3
Make sure that they don't corrupt the passed-in dicts.
>>> def f(x, y):
... print(x, y)
...
>>> original_dict = {'x': 1}
>>> f(**original_dict, y=2)
1 2
>>> original_dict
{'x': 1}
Now for some failures
Make sure the raised errors are right for keyword argument unpackings
>>> from collections.abc import MutableMapping
>>> class CrazyDict(MutableMapping):
... def __init__(self):
... self.d = {}
...
... def __iter__(self):
... for x in self.d.__iter__():
... if x == 'c':
... self.d['z'] = 10
... yield x
...
... def __getitem__(self, k):
... return self.d[k]
...
... def __len__(self):
... return len(self.d)
...
... def __setitem__(self, k, v):
... self.d[k] = v
...
... def __delitem__(self, k):
... del self.d[k]
...
>>> d = CrazyDict()
>>> d.d = {chr(ord('a') + x): x for x in range(5)}
>>> e = {**d}
Traceback (most recent call last):
...
RuntimeError: dictionary changed size during iteration
>>> d.d = {chr(ord('a') + x): x for x in range(5)}
>>> def f(**kwargs): print(kwargs)
>>> f(**d)
Traceback (most recent call last):
...
RuntimeError: dictionary changed size during iteration
Overridden parameters
>>> f(x=5, **{'x': 3}, y=2)
Traceback (most recent call last):
...
TypeError: test.test_unpack_ex.f() got multiple values for keyword argument 'x'
>>> f(**{'x': 3}, x=5, y=2)
Traceback (most recent call last):
...
TypeError: test.test_unpack_ex.f() got multiple values for keyword argument 'x'
>>> f(**{'x': 3}, **{'x': 5}, y=2)
Traceback (most recent call last):
...
TypeError: test.test_unpack_ex.f() got multiple values for keyword argument 'x'
>>> f(x=5, **{'x': 3}, **{'x': 2})
Traceback (most recent call last):
...
TypeError: test.test_unpack_ex.f() got multiple values for keyword argument 'x'
>>> f(**{1: 3}, **{1: 5})
Traceback (most recent call last):
...
TypeError: test.test_unpack_ex.f() got multiple values for keyword argument '1'
Unpacking non-sequence
>>> a, *b = 7
Traceback (most recent call last):
...
TypeError: cannot unpack non-iterable int object
Unpacking sequence too short
>>> a, *b, c, d, e = Seq()
Traceback (most recent call last):
...
ValueError: not enough values to unpack (expected at least 4, got 3)
Unpacking sequence too short and target appears last
>>> a, b, c, d, *e = Seq()
Traceback (most recent call last):
...
ValueError: not enough values to unpack (expected at least 4, got 3)
Unpacking a sequence where the test for too long raises a different kind of
error
>>> class BozoError(Exception):
... pass
...
>>> class BadSeq:
... def __getitem__(self, i):
... if i >= 0 and i < 3:
... return i
... elif i == 3:
... raise BozoError
... else:
... raise IndexError
...
Trigger code while not expecting an IndexError (unpack sequence too long, wrong
error)
>>> a, *b, c, d, e = BadSeq()
Traceback (most recent call last):
...
test.test_unpack_ex.BozoError
Now some general starred expressions (all fail).
>>> a, *b, c, *d, e = range(10) # doctest:+ELLIPSIS
Traceback (most recent call last):
...
SyntaxError: multiple starred expressions in assignment
>>> [*b, *c] = range(10) # doctest:+ELLIPSIS
Traceback (most recent call last):
...
SyntaxError: multiple starred expressions in assignment
>>> a,*b,*c,*d = range(4) # doctest:+ELLIPSIS
Traceback (most recent call last):
...
SyntaxError: multiple starred expressions in assignment
>>> *a = range(10) # doctest:+ELLIPSIS
Traceback (most recent call last):
...
SyntaxError: starred assignment target must be in a list or tuple
>>> *a # doctest:+ELLIPSIS
Traceback (most recent call last):
...
SyntaxError: can't use starred expression here
>>> *1 # doctest:+ELLIPSIS
Traceback (most recent call last):
...
SyntaxError: can't use starred expression here
>>> x = *a # doctest:+ELLIPSIS
Traceback (most recent call last):
...
SyntaxError: can't use starred expression here
>>> (*x),y = 1, 2 # doctest:+ELLIPSIS
Traceback (most recent call last):
...
SyntaxError: cannot use starred expression here
>>> (((*x))),y = 1, 2 # doctest:+ELLIPSIS
Traceback (most recent call last):
...
SyntaxError: cannot use starred expression here
>>> z,(*x),y = 1, 2, 4 # doctest:+ELLIPSIS
Traceback (most recent call last):
...
SyntaxError: cannot use starred expression here
>>> z,(*x) = 1, 2 # doctest:+ELLIPSIS
Traceback (most recent call last):
...
SyntaxError: cannot use starred expression here
>>> ((*x),y) = 1, 2 # doctest:+ELLIPSIS
Traceback (most recent call last):
...
SyntaxError: cannot use starred expression here
Some size constraints (all fail.)
>>> s = ", ".join("a%d" % i for i in range(1<<8)) + ", *rest = range(1<<8 + 1)"
>>> compile(s, 'test', 'exec') # doctest:+ELLIPSIS
Traceback (most recent call last):
...
SyntaxError: too many expressions in star-unpacking assignment
>>> s = ", ".join("a%d" % i for i in range(1<<8 + 1)) + ", *rest = range(1<<8 + 2)"
>>> compile(s, 'test', 'exec') # doctest:+ELLIPSIS
Traceback (most recent call last):
...
SyntaxError: too many expressions in star-unpacking assignment
(there is an additional limit, on the number of expressions after the
'*rest', but it's 1<<24 and testing it takes too much memory.)
"""
__test__ = {'doctests' : doctests}
def load_tests(loader, tests, pattern):
tests.addTest(doctest.DocTestSuite())
return tests
if __name__ == "__main__":
unittest.main()