Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
77 changes: 77 additions & 0 deletions Lib/test/test_ast/test_ast.py
Original file line number Diff line number Diff line change
Expand Up @@ -160,6 +160,83 @@ def test_parse_invalid_ast(self):
self.assertRaises(TypeError, ast.parse, ast.Constant(42),
optimize=optval)

def test_parse_removes_single_null_container_unpack(self):
tests = [
('[*()]', ast.List, []),
('{*()}', ast.Set, []),
('(*(),)', ast.Tuple, []),
]

for source, node_type, expected_values in tests:
with self.subTest(source=source):
node = ast.parse(source, mode='eval').body
self.assertIsInstance(node, node_type)
self.assertEqual(
[elt.value for elt in node.elts],
expected_values,
)

def test_parse_removes_single_null_container_unpack_in_assignment(self):
tests = [
('x = [*()]', ast.List, []),
('x = {*()}', ast.Set, []),
('x = *(),', ast.Tuple, []),
('x = (*(),)', ast.Tuple, []),
]

for source, node_type, expected_values in tests:
with self.subTest(source=source):
node = ast.parse(source).body[0]
self.assertIsInstance(node, ast.Assign)
self.assertIsInstance(node.value, node_type)
self.assertEqual(
[elt.value for elt in node.value.elts],
expected_values,
)

def test_parse_keeps_null_container_unpack_in_larger_literal(self):
tests = [
('[*(), 2]', ast.List, 2),
('{*(), 2}', ast.Set, 2),
('(*(), 2)', ast.Tuple, 2),
('[1, *(), 2]', ast.List, 3),
('{1, *(), 2}', ast.Set, 3),
('(1, *(), 2)', ast.Tuple, 3),
('[*(), *()]', ast.List, 2),
('{*(), *()}', ast.Set, 2),
('(*(), *())', ast.Tuple, 2),
]

for source, node_type, expected_len in tests:
with self.subTest(source=source):
node = ast.parse(source, mode='eval').body
self.assertIsInstance(node, node_type)
self.assertEqual(len(node.elts), expected_len)
self.assertTrue(any(isinstance(elt, ast.Starred) for elt in node.elts))

def test_parse_keeps_null_container_unpack_in_larger_assignment_value(self):
tests = [
('x = [*(), 2]', ast.List, 2),
('x = {*(), 2}', ast.Set, 2),
('x = *(), 2', ast.Tuple, 2),
('x = [1, *(), 2]', ast.List, 3),
('x = {1, *(), 2}', ast.Set, 3),
('x = 1, *(), 2', ast.Tuple, 3),
('x = [*(), *()]', ast.List, 2),
('x = {*(), *()}', ast.Set, 2),
('x = *(), *()', ast.Tuple, 2),
]

for source, node_type, expected_len in tests:
with self.subTest(source=source):
node = ast.parse(source).body[0]
self.assertIsInstance(node, ast.Assign)
self.assertIsInstance(node.value, node_type)
self.assertEqual(len(node.value.elts), expected_len)
self.assertTrue(
any(isinstance(elt, ast.Starred) for elt in node.value.elts)
)

def test_optimization_levels__debug__(self):
cases = [(-1, '__debug__'), (0, '__debug__'), (1, False), (2, False)]
for (optval, expected) in cases:
Expand Down
7 changes: 6 additions & 1 deletion Lib/test/test_unparse.py
Original file line number Diff line number Diff line change
Expand Up @@ -392,10 +392,15 @@ def test_set_literal(self):
self.check_ast_roundtrip("{'a', 'b', 'c'}")

def test_empty_set(self):
empty_set = ast.Set(elts=[])
self.assertASTEqual(
ast.parse(ast.unparse(ast.Set(elts=[]))),
ast.parse(ast.unparse(empty_set)),
ast.parse('{*()}')
)
self.assertASTEqual(
empty_set,
ast.parse('{*(),}', mode='eval').body
)

def test_set_comprehension(self):
self.check_ast_roundtrip("{x for x in range(5)}")
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
ast.parse() now canonicalizes lone empty-tuple unpacks in container
literals, improving AST round-tripping for cases such as the empty-set idiom
``{*()}`` produced by ast.unparse().
25 changes: 25 additions & 0 deletions Python/ast_preprocess.c
Original file line number Diff line number Diff line change
Expand Up @@ -167,6 +167,28 @@ has_starred(asdl_expr_seq *elts)
return 0;
}

static int
is_null_container_unpack(expr_ty elt)
{
if (elt->kind != Starred_kind) {
return 0;
}
expr_ty value = elt->v.Starred.value;
return value->kind == Tuple_kind && asdl_seq_LEN(value->v.Tuple.elts) == 0;
}

static void
remove_null_container_unpacks(asdl_expr_seq *elts)
{
if (elts == NULL || asdl_seq_LEN(elts) != 1) {
return;
}
if (!is_null_container_unpack(asdl_seq_GET(elts, 0))) {
return;
}
elts->size = 0;
}

static expr_ty
parse_literal(PyObject *fmt, Py_ssize_t *ppos, PyArena *arena)
{
Expand Down Expand Up @@ -546,6 +568,7 @@ astfold_expr(expr_ty node_, PyArena *ctx_, _PyASTPreprocessState *state)
break;
case Set_kind:
CALL_SEQ(astfold_expr, expr, node_->v.Set.elts);
remove_null_container_unpacks(node_->v.Set.elts);
break;
case ListComp_kind:
CALL(astfold_expr, expr_ty, node_->v.ListComp.elt);
Expand Down Expand Up @@ -615,9 +638,11 @@ astfold_expr(expr_ty node_, PyArena *ctx_, _PyASTPreprocessState *state)
break;
case List_kind:
CALL_SEQ(astfold_expr, expr, node_->v.List.elts);
remove_null_container_unpacks(node_->v.List.elts);
break;
case Tuple_kind:
CALL_SEQ(astfold_expr, expr, node_->v.Tuple.elts);
remove_null_container_unpacks(node_->v.Tuple.elts);
break;
case Name_kind:
if (state->syntax_check_only) {
Expand Down
Loading