Skip to content

Commit 2693132

Browse files
authored
bpo-44589: raise a SyntaxError when mapping patterns have duplicate literal keys (GH-27131)
1 parent 3b8075f commit 2693132

File tree

4 files changed

+82
-19
lines changed

4 files changed

+82
-19
lines changed

Doc/reference/compound_stmts.rst

Lines changed: 5 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -968,9 +968,9 @@ Syntax:
968968
At most one double star pattern may be in a mapping pattern. The double star
969969
pattern must be the last subpattern in the mapping pattern.
970970

971-
Duplicate key values in mapping patterns are disallowed. (If all key patterns
972-
are literal patterns this is considered a syntax error; otherwise this is a
973-
runtime error and will raise :exc:`ValueError`.)
971+
Duplicate keys in mapping patterns are disallowed. Duplicate literal keys will
972+
raise a :exc:`SyntaxError`. Two keys that otherwise have the same value will
973+
raise a :exc:`ValueError` at runtime.
974974

975975
The following is the logical flow for matching a mapping pattern against a
976976
subject value:
@@ -982,7 +982,8 @@ subject value:
982982
mapping, the mapping pattern succeeds.
983983

984984
#. If duplicate keys are detected in the mapping pattern, the pattern is
985-
considered invalid and :exc:`ValueError` is raised.
985+
considered invalid. A :exc:`SyntaxError` is raised for duplicate literal
986+
values; or a :exc:`ValueError` for named keys of the same value.
986987

987988
.. note:: Key-value pairs are matched using the two-argument form of the mapping
988989
subject's ``get()`` method. Matched key-value pairs must already be present

Lib/test/test_patma.py

Lines changed: 34 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -2901,6 +2901,40 @@ def test_wildcard_makes_remaining_patterns_unreachable_5(self):
29012901
pass
29022902
""")
29032903

2904+
def test_mapping_pattern_duplicate_key(self):
2905+
self.assert_syntax_error("""
2906+
match ...:
2907+
case {"a": _, "a": _}:
2908+
pass
2909+
""")
2910+
2911+
def test_mapping_pattern_duplicate_key_edge_case0(self):
2912+
self.assert_syntax_error("""
2913+
match ...:
2914+
case {0: _, False: _}:
2915+
pass
2916+
""")
2917+
2918+
def test_mapping_pattern_duplicate_key_edge_case1(self):
2919+
self.assert_syntax_error("""
2920+
match ...:
2921+
case {0: _, 0.0: _}:
2922+
pass
2923+
""")
2924+
2925+
def test_mapping_pattern_duplicate_key_edge_case2(self):
2926+
self.assert_syntax_error("""
2927+
match ...:
2928+
case {0: _, -0: _}:
2929+
pass
2930+
""")
2931+
2932+
def test_mapping_pattern_duplicate_key_edge_case3(self):
2933+
self.assert_syntax_error("""
2934+
match ...:
2935+
case {0: _, 0j: _}:
2936+
pass
2937+
""")
29042938

29052939
class TestTypeErrors(unittest.TestCase):
29062940

@@ -3008,17 +3042,6 @@ class Class:
30083042

30093043
class TestValueErrors(unittest.TestCase):
30103044

3011-
def test_mapping_pattern_checks_duplicate_key_0(self):
3012-
x = {"a": 0, "b": 1}
3013-
w = y = z = None
3014-
with self.assertRaises(ValueError):
3015-
match x:
3016-
case {"a": y, "a": z}:
3017-
w = 0
3018-
self.assertIs(w, None)
3019-
self.assertIs(y, None)
3020-
self.assertIs(z, None)
3021-
30223045
def test_mapping_pattern_checks_duplicate_key_1(self):
30233046
class Keys:
30243047
KEY = "a"
Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,2 @@
1+
Mapping patterns in ``match`` statements with two or more equal literal
2+
keys will now raise a :exc:`SyntaxError` at compile-time.

Python/compile.c

Lines changed: 41 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -6176,20 +6176,53 @@ compiler_pattern_mapping(struct compiler *c, pattern_ty p, pattern_context *pc)
61766176
}
61776177
// Collect all of the keys into a tuple for MATCH_KEYS and
61786178
// COPY_DICT_WITHOUT_KEYS. They can either be dotted names or literals:
6179+
6180+
// Maintaining a set of Constant_kind kind keys allows us to raise a
6181+
// SyntaxError in the case of duplicates.
6182+
PyObject *seen = PySet_New(NULL);
6183+
if (seen == NULL) {
6184+
return 0;
6185+
}
6186+
6187+
// NOTE: goto error on failure in the loop below to avoid leaking `seen`
61796188
for (Py_ssize_t i = 0; i < size; i++) {
61806189
expr_ty key = asdl_seq_GET(keys, i);
61816190
if (key == NULL) {
61826191
const char *e = "can't use NULL keys in MatchMapping "
61836192
"(set 'rest' parameter instead)";
61846193
SET_LOC(c, ((pattern_ty) asdl_seq_GET(patterns, i)));
6185-
return compiler_error(c, e);
6194+
compiler_error(c, e);
6195+
goto error;
6196+
}
6197+
6198+
if (key->kind == Constant_kind) {
6199+
int in_seen = PySet_Contains(seen, key->v.Constant.value);
6200+
if (in_seen < 0) {
6201+
goto error;
6202+
}
6203+
if (in_seen) {
6204+
const char *e = "mapping pattern checks duplicate key (%R)";
6205+
compiler_error(c, e, key->v.Constant.value);
6206+
goto error;
6207+
}
6208+
if (PySet_Add(seen, key->v.Constant.value)) {
6209+
goto error;
6210+
}
61866211
}
6187-
if (!MATCH_VALUE_EXPR(key)) {
6212+
6213+
else if (key->kind != Attribute_kind) {
61886214
const char *e = "mapping pattern keys may only match literals and attribute lookups";
6189-
return compiler_error(c, e);
6215+
compiler_error(c, e);
6216+
goto error;
6217+
}
6218+
if (!compiler_visit_expr(c, key)) {
6219+
goto error;
61906220
}
6191-
VISIT(c, expr, key);
61926221
}
6222+
6223+
// all keys have been checked; there are no duplicates
6224+
Py_DECREF(seen);
6225+
61936226
ADDOP_I(c, BUILD_TUPLE, size);
61946227
ADDOP(c, MATCH_KEYS);
61956228
// There's now a tuple of keys and a tuple of values on top of the subject:
@@ -6224,6 +6257,10 @@ compiler_pattern_mapping(struct compiler *c, pattern_ty p, pattern_context *pc)
62246257
pc->on_top--;
62256258
ADDOP(c, POP_TOP);
62266259
return 1;
6260+
6261+
error:
6262+
Py_DECREF(seen);
6263+
return 0;
62276264
}
62286265

62296266
static int

0 commit comments

Comments
 (0)