https://github.com/python/cpython/commit/2b428dadab504c9a34f445eaca8def9bcb6d73b0
commit: 2b428dadab504c9a34f445eaca8def9bcb6d73b0
branch: 3.15
author: Miss Islington (bot) <[email protected]>
committer: pablogsal <[email protected]>
date: 2026-09-24T23:51:39+01:00
summary:

[3.15] gh-152708: Fix AST generation asymmetry for unary positive in match 
statements (GH-152712) (#158079)

gh-152708: Fix AST generation asymmetry for unary positive in match statements 
(GH-152712)
(cherry picked from commit 7884f48af47f3e33640a964e55f3391698726a26)

Co-authored-by: Rishit Agnihotri <[email protected]>
Co-authored-by: Petr Viktorin <[email protected]>
Co-authored-by: johnslavik <[email protected]>

files:
A 
Misc/NEWS.d/next/Core_and_Builtins/2026-06-30-14-50-00.gh-issue-152708.aBcDeF.rst
M Grammar/python.gram
M Lib/test/test_unparse.py
M Parser/parser.c
M Python/ast.c
M Python/ast_preprocess.c

diff --git a/Grammar/python.gram b/Grammar/python.gram
index 036e3fc0f73a9f..c0b1d879d3c9e3 100644
--- a/Grammar/python.gram
+++ b/Grammar/python.gram
@@ -555,12 +555,12 @@ complex_number[expr_ty]:
 
 signed_number[expr_ty]:
     | NUMBER
-    | '+' number=NUMBER { number }
+    | '+' number=NUMBER { _PyAST_UnaryOp(UAdd, number, EXTRA) }
     | '-' number=NUMBER { _PyAST_UnaryOp(USub, number, EXTRA) }
 
 signed_real_number[expr_ty]:
     | real_number
-    | '+' real=real_number { real }
+    | '+' real=real_number { _PyAST_UnaryOp(UAdd, real, EXTRA) }
     | '-' real=real_number { _PyAST_UnaryOp(USub, real, EXTRA) }
 
 real_number[expr_ty]:
diff --git a/Lib/test/test_unparse.py b/Lib/test/test_unparse.py
index 28faede2e2a1a5..9bc04fc8e0affb 100644
--- a/Lib/test/test_unparse.py
+++ b/Lib/test/test_unparse.py
@@ -693,6 +693,24 @@ def test_unparse_interactive_integrity_3(self):
 
             self.check_src_roundtrip(src, out, mode='single')
 
+    @test.support.subTests('case', [
+        "case 'a string':",
+        "case True:",
+        "case _:",
+        "case [*_]:",
+        "case tuple():",
+        "case cls(arg, more=arg2):",
+        "case tuple() | list():",
+        "case [tuple() as obj, _, {'a': a, **more}]:",
+        "case -2:",
+        "case +2:",
+        "case 2 + 3j:",
+        "case +2 - 3j:",
+    ])
+    def test_unparse_match(self, case):
+        src = 'match x:\n    ' + case + '\n        pass'
+        self.check_src_roundtrip(src)
+
 
 class CosmeticTestCase(ASTTestCase):
     """Test if there are cosmetic issues caused by unnecessary additions"""
diff --git 
a/Misc/NEWS.d/next/Core_and_Builtins/2026-06-30-14-50-00.gh-issue-152708.aBcDeF.rst
 
b/Misc/NEWS.d/next/Core_and_Builtins/2026-06-30-14-50-00.gh-issue-152708.aBcDeF.rst
new file mode 100644
index 00000000000000..edcf99f56ff907
--- /dev/null
+++ 
b/Misc/NEWS.d/next/Core_and_Builtins/2026-06-30-14-50-00.gh-issue-152708.aBcDeF.rst
@@ -0,0 +1 @@
+Fix AST generation asymmetry where unary positive ``+`` was implicitly dropped 
in pattern match expressions, restoring parsing parity with unary negative 
``-``.
diff --git a/Parser/parser.c b/Parser/parser.c
index 7d78449a6ba2f3..3f7385381276ed 100644
--- a/Parser/parser.c
+++ b/Parser/parser.c
@@ -9144,7 +9144,16 @@ signed_number_rule(Parser *p)
         )
         {
             D(fprintf(stderr, "%*c+ signed_number[%d-%d]: %s succeeded!\n", 
p->level, ' ', _mark, p->mark, "'+' NUMBER"));
-            _res = number;
+            Token *_token = _PyPegen_get_last_nonnwhitespace_token(p);
+            if (_token == NULL) {
+                p->level--;
+                return NULL;
+            }
+            int _end_lineno = _token->end_lineno;
+            UNUSED(_end_lineno); // Only used by EXTRA macro
+            int _end_col_offset = _token->end_col_offset;
+            UNUSED(_end_col_offset); // Only used by EXTRA macro
+            _res = _PyAST_UnaryOp ( UAdd , number , EXTRA );
             if ((_res == NULL || p->error_indicator) && PyErr_Occurred()) {
                 p->error_indicator = 1;
                 p->level--;
@@ -9254,7 +9263,16 @@ signed_real_number_rule(Parser *p)
         )
         {
             D(fprintf(stderr, "%*c+ signed_real_number[%d-%d]: %s 
succeeded!\n", p->level, ' ', _mark, p->mark, "'+' real_number"));
-            _res = real;
+            Token *_token = _PyPegen_get_last_nonnwhitespace_token(p);
+            if (_token == NULL) {
+                p->level--;
+                return NULL;
+            }
+            int _end_lineno = _token->end_lineno;
+            UNUSED(_end_lineno); // Only used by EXTRA macro
+            int _end_col_offset = _token->end_col_offset;
+            UNUSED(_end_col_offset); // Only used by EXTRA macro
+            _res = _PyAST_UnaryOp ( UAdd , real , EXTRA );
             if ((_res == NULL || p->error_indicator) && PyErr_Occurred()) {
                 p->error_indicator = 1;
                 p->level--;
diff --git a/Python/ast.c b/Python/ast.c
index f625c59fe5bffc..1befd177811492 100644
--- a/Python/ast.c
+++ b/Python/ast.c
@@ -426,11 +426,11 @@ ensure_literal_number(expr_ty exp, bool allow_real, bool 
allow_imaginary)
 }
 
 static int
-ensure_literal_negative(expr_ty exp, bool allow_real, bool allow_imaginary)
+ensure_literal_signed(expr_ty exp, bool allow_real, bool allow_imaginary)
 {
     assert(exp->kind == UnaryOp_kind);
-    // Must be negation ...
-    if (exp->v.UnaryOp.op != USub) {
+    // Must be negation or positive ...
+    if (exp->v.UnaryOp.op != USub && exp->v.UnaryOp.op != UAdd) {
         return 0;
     }
     // ... of a constant ...
@@ -461,7 +461,7 @@ ensure_literal_complex(expr_ty exp)
             }
             break;
         case UnaryOp_kind:
-            if (!ensure_literal_negative(left, /*real=*/true, 
/*imaginary=*/false)) {
+            if (!ensure_literal_signed(left, /*real=*/true, 
/*imaginary=*/false)) {
                 return 0;
             }
             break;
@@ -512,9 +512,9 @@ validate_pattern_match_value(expr_ty exp)
             // Constants and attribute lookups are always permitted
             return 1;
         case UnaryOp_kind:
-            // Negated numbers are permitted (whether real or imaginary)
+            // Signed numbers are permitted (whether real or imaginary)
             // Compiler will complain if AST folding doesn't create a constant
-            if (ensure_literal_negative(exp, /*real=*/true, 
/*imaginary=*/true)) {
+            if (ensure_literal_signed(exp, /*real=*/true, /*imaginary=*/true)) 
{
                 return 1;
             }
             break;
diff --git a/Python/ast_preprocess.c b/Python/ast_preprocess.c
index 54dec3dfe04268..951b3f71522b25 100644
--- a/Python/ast_preprocess.c
+++ b/Python/ast_preprocess.c
@@ -867,11 +867,11 @@ fold_const_match_patterns(expr_ty node, PyArena *ctx_, 
_PyASTPreprocessState *st
     {
         case UnaryOp_kind:
         {
-            if (node->v.UnaryOp.op == USub &&
+            if ((node->v.UnaryOp.op == USub || node->v.UnaryOp.op == UAdd) &&
                 node->v.UnaryOp.operand->kind == Constant_kind)
             {
                 PyObject *operand = node->v.UnaryOp.operand->v.Constant.value;
-                PyObject *folded = PyNumber_Negative(operand);
+                PyObject *folded = node->v.UnaryOp.op == USub ? 
PyNumber_Negative(operand) : PyNumber_Positive(operand);
                 return make_const(node, folded, ctx_);
             }
             break;

_______________________________________________
Python-checkins mailing list -- [email protected]
To unsubscribe send an email to [email protected]
https://mail.python.org/mailman3//lists/python-checkins.python.org
Member address: [email protected]

Reply via email to