From 016af14d93cfba43e7a95721a97fa954c534af8e Mon Sep 17 00:00:00 2001 From: "Miss Islington (bot)" <31488909+miss-islington@users.noreply.github.com> Date: Wed, 14 Jul 2021 18:00:35 -0700 Subject: [3.10] bpo-44589: raise a SyntaxError when mapping patterns have duplicate literal keys (GH-27131) (GH-27157) (cherry picked from commit 2693132292b2acf381ac6fa729bf3acf41d9d72b) Co-authored-by: Jack DeVries <58614260+jdevries3133@users.noreply.github.com> Automerge-Triggered-By: GH:brandtbucher --- Doc/reference/compound_stmts.rst | 9 +++-- Lib/test/test_patma.py | 45 ++++++++++++++++------ .../2021-07-13-23-19-41.bpo-44589.59OH8T.rst | 2 + Python/compile.c | 45 ++++++++++++++++++++-- 4 files changed, 82 insertions(+), 19 deletions(-) create mode 100644 Misc/NEWS.d/next/Core and Builtins/2021-07-13-23-19-41.bpo-44589.59OH8T.rst diff --git a/Doc/reference/compound_stmts.rst b/Doc/reference/compound_stmts.rst index 0274095..a743898 100644 --- a/Doc/reference/compound_stmts.rst +++ b/Doc/reference/compound_stmts.rst @@ -968,9 +968,9 @@ Syntax: At most one double star pattern may be in a mapping pattern. The double star pattern must be the last subpattern in the mapping pattern. -Duplicate key values in mapping patterns are disallowed. (If all key patterns -are literal patterns this is considered a syntax error; otherwise this is a -runtime error and will raise :exc:`ValueError`.) +Duplicate keys in mapping patterns are disallowed. Duplicate literal keys will +raise a :exc:`SyntaxError`. Two keys that otherwise have the same value will +raise a :exc:`ValueError` at runtime. The following is the logical flow for matching a mapping pattern against a subject value: @@ -982,7 +982,8 @@ subject value: mapping, the mapping pattern succeeds. #. If duplicate keys are detected in the mapping pattern, the pattern is - considered invalid and :exc:`ValueError` is raised. + considered invalid. A :exc:`SyntaxError` is raised for duplicate literal + values; or a :exc:`ValueError` for named keys of the same value. .. note:: Key-value pairs are matched using the two-argument form of the mapping subject's ``get()`` method. Matched key-value pairs must already be present diff --git a/Lib/test/test_patma.py b/Lib/test/test_patma.py index a8889ca..69a648a 100644 --- a/Lib/test/test_patma.py +++ b/Lib/test/test_patma.py @@ -2901,6 +2901,40 @@ class TestSyntaxErrors(unittest.TestCase): pass """) + def test_mapping_pattern_duplicate_key(self): + self.assert_syntax_error(""" + match ...: + case {"a": _, "a": _}: + pass + """) + + def test_mapping_pattern_duplicate_key_edge_case0(self): + self.assert_syntax_error(""" + match ...: + case {0: _, False: _}: + pass + """) + + def test_mapping_pattern_duplicate_key_edge_case1(self): + self.assert_syntax_error(""" + match ...: + case {0: _, 0.0: _}: + pass + """) + + def test_mapping_pattern_duplicate_key_edge_case2(self): + self.assert_syntax_error(""" + match ...: + case {0: _, -0: _}: + pass + """) + + def test_mapping_pattern_duplicate_key_edge_case3(self): + self.assert_syntax_error(""" + match ...: + case {0: _, 0j: _}: + pass + """) class TestTypeErrors(unittest.TestCase): @@ -3008,17 +3042,6 @@ class TestTypeErrors(unittest.TestCase): class TestValueErrors(unittest.TestCase): - def test_mapping_pattern_checks_duplicate_key_0(self): - x = {"a": 0, "b": 1} - w = y = z = None - with self.assertRaises(ValueError): - match x: - case {"a": y, "a": z}: - w = 0 - self.assertIs(w, None) - self.assertIs(y, None) - self.assertIs(z, None) - def test_mapping_pattern_checks_duplicate_key_1(self): class Keys: KEY = "a" diff --git a/Misc/NEWS.d/next/Core and Builtins/2021-07-13-23-19-41.bpo-44589.59OH8T.rst b/Misc/NEWS.d/next/Core and Builtins/2021-07-13-23-19-41.bpo-44589.59OH8T.rst new file mode 100644 index 0000000..23b2472 --- /dev/null +++ b/Misc/NEWS.d/next/Core and Builtins/2021-07-13-23-19-41.bpo-44589.59OH8T.rst @@ -0,0 +1,2 @@ +Mapping patterns in ``match`` statements with two or more equal literal +keys will now raise a :exc:`SyntaxError` at compile-time. diff --git a/Python/compile.c b/Python/compile.c index 78d5fbe..0508ea1 100644 --- a/Python/compile.c +++ b/Python/compile.c @@ -5998,20 +5998,53 @@ compiler_pattern_mapping(struct compiler *c, pattern_ty p, pattern_context *pc) } // Collect all of the keys into a tuple for MATCH_KEYS and // COPY_DICT_WITHOUT_KEYS. They can either be dotted names or literals: + + // Maintaining a set of Constant_kind kind keys allows us to raise a + // SyntaxError in the case of duplicates. + PyObject *seen = PySet_New(NULL); + if (seen == NULL) { + return 0; + } + + // NOTE: goto error on failure in the loop below to avoid leaking `seen` for (Py_ssize_t i = 0; i < size; i++) { expr_ty key = asdl_seq_GET(keys, i); if (key == NULL) { const char *e = "can't use NULL keys in MatchMapping " "(set 'rest' parameter instead)"; SET_LOC(c, ((pattern_ty) asdl_seq_GET(patterns, i))); - return compiler_error(c, e); + compiler_error(c, e); + goto error; + } + + if (key->kind == Constant_kind) { + int in_seen = PySet_Contains(seen, key->v.Constant.value); + if (in_seen < 0) { + goto error; + } + if (in_seen) { + const char *e = "mapping pattern checks duplicate key (%R)"; + compiler_error(c, e, key->v.Constant.value); + goto error; + } + if (PySet_Add(seen, key->v.Constant.value)) { + goto error; + } } - if (!MATCH_VALUE_EXPR(key)) { + + else if (key->kind != Attribute_kind) { const char *e = "mapping pattern keys may only match literals and attribute lookups"; - return compiler_error(c, e); + compiler_error(c, e); + goto error; + } + if (!compiler_visit_expr(c, key)) { + goto error; } - VISIT(c, expr, key); } + + // all keys have been checked; there are no duplicates + Py_DECREF(seen); + ADDOP_I(c, BUILD_TUPLE, size); ADDOP(c, MATCH_KEYS); // There's now a tuple of keys and a tuple of values on top of the subject: @@ -6046,6 +6079,10 @@ compiler_pattern_mapping(struct compiler *c, pattern_ty p, pattern_context *pc) pc->on_top--; ADDOP(c, POP_TOP); return 1; + +error: + Py_DECREF(seen); + return 0; } static int -- cgit v0.12