https://github.com/python/cpython/commit/30840706b029645b9631b92c687834fcced6413e
commit: 30840706b029645b9631b92c687834fcced6413e
branch: main
author: Samuel <sam...@knutsen.co>
committer: JelleZijlstra <jelle.zijls...@gmail.com>
date: 2025-05-04T16:03:38-07:00
summary:

gh-131421: fix ASDL grammar for `Dict` to have an `expr?*` keys field (#131419)

In the `ast` documentation for Python:

* https://docs.python.org/3/library/ast.html#ast.Dict
it is made clear that:

> When doing dictionary unpacking using dictionary literals the expression to 
> be expanded goes in the values list, with a `None` at the corresponding 
> position in `keys`.

Hence, `keys` is really a `expr?*` and *not* a `expr*`.

Co-authored-by: Bénédikt Tran <10796600+picn...@users.noreply.github.com>
Co-authored-by: Jelle Zijlstra <jelle.zijls...@gmail.com>

files:
M Parser/Python.asdl
M Parser/asdl.py
M Python/Python-ast.c

diff --git a/Parser/Python.asdl b/Parser/Python.asdl
index db61d8b1f66467..96f3914b029d4c 100644
--- a/Parser/Python.asdl
+++ b/Parser/Python.asdl
@@ -63,7 +63,7 @@ module Python
          | UnaryOp(unaryop op, expr operand)
          | Lambda(arguments args, expr body)
          | IfExp(expr test, expr body, expr orelse)
-         | Dict(expr* keys, expr* values)
+         | Dict(expr?* keys, expr* values)
          | Set(expr* elts)
          | ListComp(expr elt, comprehension* generators)
          | SetComp(expr elt, comprehension* generators)
diff --git a/Parser/asdl.py b/Parser/asdl.py
index e3e6c34d2a98e3..7a9c9d8628dce6 100644
--- a/Parser/asdl.py
+++ b/Parser/asdl.py
@@ -12,7 +12,7 @@
 # type          ::= product | sum
 # product       ::= fields ["attributes" fields]
 # fields        ::= "(" { field, "," } field ")"
-# field         ::= TypeId ["?" | "*"] [Id]
+# field         ::= TypeId { "?" | "*" } [Id]
 # sum           ::= constructor { "|" constructor } ["attributes" fields]
 # constructor   ::= ConstructorId [fields]
 #
@@ -20,6 +20,7 @@
 #     http://asdl.sourceforge.net/
 
#-------------------------------------------------------------------------------
 from collections import namedtuple
+import enum
 import re
 
 __all__ = [
@@ -64,34 +65,43 @@ def __init__(self, name, fields=None):
     def __repr__(self):
         return 'Constructor({0.name}, {0.fields})'.format(self)
 
+class Quantifier(enum.Enum):
+    OPTIONAL = enum.auto()
+    SEQUENCE = enum.auto()
+
 class Field(AST):
-    def __init__(self, type, name=None, seq=False, opt=False):
+    def __init__(self, type, name=None, quantifiers=None):
         self.type = type
         self.name = name
-        self.seq = seq
-        self.opt = opt
+        self.seq = False
+        self.opt = False
+        self.quantifiers = quantifiers or []
+        if len(self.quantifiers) > 0:
+            self.seq = self.quantifiers[-1] is Quantifier.SEQUENCE
+            self.opt = self.quantifiers[-1] is Quantifier.OPTIONAL
 
     def __str__(self):
-        if self.seq:
-            extra = "*"
-        elif self.opt:
-            extra = "?"
-        else:
-            extra = ""
+        extra = ""
+        for mod in self.quantifiers:
+            if mod is Quantifier.SEQUENCE:
+                extra += "*"
+            elif mod is Quantifier.OPTIONAL:
+                extra += "?"
 
         return "{}{} {}".format(self.type, extra, self.name)
 
     def __repr__(self):
-        if self.seq:
-            extra = ", seq=True"
-        elif self.opt:
-            extra = ", opt=True"
-        else:
-            extra = ""
+        extra = ""
+        for mod in self.quantifiers:
+            if mod is Quantifier.SEQUENCE:
+                extra += ", SEQUENCE"
+            elif mod is Quantifier.OPTIONAL:
+                extra += ", OPTIONAL"
+
         if self.name is None:
-            return 'Field({0.type}{1})'.format(self, extra)
+            return 'Field({0.type}, quantifiers=[{1}])'.format(self, extra)
         else:
-            return 'Field({0.type}, {0.name}{1})'.format(self, extra)
+            return 'Field({0.type}, {0.name}, quantifiers=[{1}])'.format(self, 
extra)
 
 class Sum(AST):
     def __init__(self, types, attributes=None):
@@ -314,10 +324,10 @@ def _parse_fields(self):
         self._match(TokenKind.LParen)
         while self.cur_token.kind == TokenKind.TypeId:
             typename = self._advance()
-            is_seq, is_opt = self._parse_optional_field_quantifier()
+            quantifiers = self._parse_optional_field_quantifier()
             id = (self._advance() if self.cur_token.kind in self._id_kinds
                                   else None)
-            fields.append(Field(typename, id, seq=is_seq, opt=is_opt))
+            fields.append(Field(typename, id, quantifiers=quantifiers))
             if self.cur_token.kind == TokenKind.RParen:
                 break
             elif self.cur_token.kind == TokenKind.Comma:
@@ -339,14 +349,14 @@ def _parse_optional_attributes(self):
             return None
 
     def _parse_optional_field_quantifier(self):
-        is_seq, is_opt = False, False
-        if self.cur_token.kind == TokenKind.Asterisk:
-            is_seq = True
-            self._advance()
-        elif self.cur_token.kind == TokenKind.Question:
-            is_opt = True
+        quantifiers = []
+        while self.cur_token.kind in (TokenKind.Asterisk, TokenKind.Question):
+            if self.cur_token.kind == TokenKind.Asterisk:
+                quantifiers.append(Quantifier.SEQUENCE)
+            elif self.cur_token.kind == TokenKind.Question:
+                quantifiers.append(Quantifier.OPTIONAL)
             self._advance()
-        return is_seq, is_opt
+        return quantifiers
 
     def _advance(self):
         """ Return the value of the current token and read the next one into
diff --git a/Python/Python-ast.c b/Python/Python-ast.c
index 94d9a76d28306e..df035568f84be1 100644
--- a/Python/Python-ast.c
+++ b/Python/Python-ast.c
@@ -6362,7 +6362,7 @@ init_types(void *arg)
         "     | UnaryOp(unaryop op, expr operand)\n"
         "     | Lambda(arguments args, expr body)\n"
         "     | IfExp(expr test, expr body, expr orelse)\n"
-        "     | Dict(expr* keys, expr* values)\n"
+        "     | Dict(expr?* keys, expr* values)\n"
         "     | Set(expr* elts)\n"
         "     | ListComp(expr elt, comprehension* generators)\n"
         "     | SetComp(expr elt, comprehension* generators)\n"
@@ -6419,7 +6419,7 @@ init_types(void *arg)
     if (!state->IfExp_type) return -1;
     state->Dict_type = make_type(state, "Dict", state->expr_type, Dict_fields,
                                  2,
-        "Dict(expr* keys, expr* values)");
+        "Dict(expr?* keys, expr* values)");
     if (!state->Dict_type) return -1;
     state->Set_type = make_type(state, "Set", state->expr_type, Set_fields, 1,
         "Set(expr* elts)");

_______________________________________________
Python-checkins mailing list -- python-checkins@python.org
To unsubscribe send an email to python-checkins-le...@python.org
https://mail.python.org/mailman3/lists/python-checkins.python.org/
Member address: arch...@mail-archive.com

Reply via email to