https://github.com/python/cpython/commit/448d7b96c181d13ca7f8977780e85b53b2716294
commit: 448d7b96c181d13ca7f8977780e85b53b2716294
branch: main
author: Bartosz Sławecki <[email protected]>
committer: Eclips4 <[email protected]>
date: 2026-04-23T22:07:28+03:00
summary:
gh-145239: Accept unary plus literal pattern (#148566)
Add '+' alternatives to signed_number and signed_real_number grammar
rules, mirroring how unary minus is already handled for pattern matching.
Unary plus is a no-op on numbers so the value is returned directly without
wrapping in a UnaryOp node.
files:
A
Misc/NEWS.d/next/Core_and_Builtins/2026-04-13-23-21-45.gh-issue-145239.pL8qRt.rst
M Doc/reference/compound_stmts.rst
M Doc/whatsnew/3.15.rst
M Grammar/python.gram
M Lib/test/.ruff.toml
M Lib/test/test_patma.py
M Parser/parser.c
diff --git a/Doc/reference/compound_stmts.rst b/Doc/reference/compound_stmts.rst
index 0cf0a41bfb400c..72e1cad3bbd892 100644
--- a/Doc/reference/compound_stmts.rst
+++ b/Doc/reference/compound_stmts.rst
@@ -858,7 +858,7 @@ A literal pattern corresponds to most
: | "None"
: | "True"
: | "False"
- signed_number: ["-"] NUMBER
+ signed_number: ["+" | "-"] NUMBER
The rule ``strings`` and the token ``NUMBER`` are defined in the
:doc:`standard Python grammar <./grammar>`. Triple-quoted strings are
diff --git a/Doc/whatsnew/3.15.rst b/Doc/whatsnew/3.15.rst
index 500797910edb90..dbdd5de01700a3 100644
--- a/Doc/whatsnew/3.15.rst
+++ b/Doc/whatsnew/3.15.rst
@@ -645,6 +645,10 @@ Other language changes
* Allow the *count* argument of :meth:`bytes.replace` to be a keyword.
(Contributed by Stan Ulbrych in :gh:`147856`.)
+* Unary plus is now accepted in :keyword:`match` literal patterns, mirroring
+ the existing support for unary minus.
+ (Contributed by Bartosz Sławecki in :gh:`145239`.)
+
New modules
===========
diff --git a/Grammar/python.gram b/Grammar/python.gram
index 3a91d426c36501..9bf3a67939fcf3 100644
--- a/Grammar/python.gram
+++ b/Grammar/python.gram
@@ -554,10 +554,12 @@ complex_number[expr_ty]:
signed_number[expr_ty]:
| NUMBER
+ | '+' number=NUMBER { number }
| '-' number=NUMBER { _PyAST_UnaryOp(USub, number, EXTRA) }
signed_real_number[expr_ty]:
| real_number
+ | '+' real=real_number { real }
| '-' real=real_number { _PyAST_UnaryOp(USub, real, EXTRA) }
real_number[expr_ty]:
@@ -565,6 +567,7 @@ real_number[expr_ty]:
imaginary_number[expr_ty]:
| imag=NUMBER { _PyPegen_ensure_imaginary(p, imag) }
+ | '+' imag=NUMBER { _PyPegen_ensure_imaginary(p, imag) }
capture_pattern[pattern_ty]:
| target=pattern_capture_target { _PyAST_MatchAs(NULL, target->v.Name.id,
EXTRA) }
diff --git a/Lib/test/.ruff.toml b/Lib/test/.ruff.toml
index a960543f277935..dca74eb6e14bbd 100644
--- a/Lib/test/.ruff.toml
+++ b/Lib/test/.ruff.toml
@@ -18,6 +18,8 @@ extend-exclude = [
"test_lazy_import/__init__.py",
"test_lazy_import/data/*.py",
"test_lazy_import/data/**/*.py",
+ # Unary plus literal pattern is not yet supported by Ruff (GH-145239)
+ "test_patma.py",
]
[lint]
diff --git a/Lib/test/test_patma.py b/Lib/test/test_patma.py
index 5d0857b059ea23..29cce4ee6d271f 100644
--- a/Lib/test/test_patma.py
+++ b/Lib/test/test_patma.py
@@ -2762,6 +2762,96 @@ def test_patma_255(self):
self.assertEqual(y, 1)
self.assertIs(z, x)
+ def test_patma_256(self):
+ x = 0
+ match x:
+ case +0:
+ y = 0
+ self.assertEqual(x, 0)
+ self.assertEqual(y, 0)
+
+ def test_patma_257(self):
+ x = 0
+ match x:
+ case +0.0:
+ y = 0
+ self.assertEqual(x, 0)
+ self.assertEqual(y, 0)
+
+ def test_patma_258(self):
+ x = 0
+ match x:
+ case +0j:
+ y = 0
+ self.assertEqual(x, 0)
+ self.assertEqual(y, 0)
+
+ def test_patma_259(self):
+ x = 0
+ match x:
+ case +0.0j:
+ y = 0
+ self.assertEqual(x, 0)
+ self.assertEqual(y, 0)
+
+ def test_patma_260(self):
+ x = 1
+ match x:
+ case +1:
+ y = 0
+ self.assertEqual(x, 1)
+ self.assertEqual(y, 0)
+
+ def test_patma_261(self):
+ x = 1.5
+ match x:
+ case +1.5:
+ y = 0
+ self.assertEqual(x, 1.5)
+ self.assertEqual(y, 0)
+
+ def test_patma_262(self):
+ x = 1j
+ match x:
+ case +1j:
+ y = 0
+ self.assertEqual(x, 1j)
+ self.assertEqual(y, 0)
+
+ def test_patma_263(self):
+ x = 1.5j
+ match x:
+ case +1.5j:
+ y = 0
+ self.assertEqual(x, 1.5j)
+ self.assertEqual(y, 0)
+
+ def test_patma_264(self):
+ x = 0.25 + 1.75j
+ match x:
+ case +0.25 + 1.75j:
+ y = 0
+ self.assertEqual(x, 0.25 + 1.75j)
+ self.assertEqual(y, 0)
+
+ def test_patma_265(self):
+ x = 0.25 - 1.75j
+ match x:
+ case 0.25 - +1.75j:
+ y = 0
+ self.assertEqual(x, 0.25 - 1.75j)
+ self.assertEqual(y, 0)
+
+ def test_patma_266(self):
+ x = 0
+ match x:
+ case +1e1000:
+ y = 0
+ case 0:
+ y = 1
+ self.assertEqual(x, 0)
+ self.assertEqual(y, 1)
+
def test_patma_runtime_checkable_protocol(self):
# Runtime-checkable protocol
from typing import Protocol, runtime_checkable
diff --git
a/Misc/NEWS.d/next/Core_and_Builtins/2026-04-13-23-21-45.gh-issue-145239.pL8qRt.rst
b/Misc/NEWS.d/next/Core_and_Builtins/2026-04-13-23-21-45.gh-issue-145239.pL8qRt.rst
new file mode 100644
index 00000000000000..282b99176642bc
--- /dev/null
+++
b/Misc/NEWS.d/next/Core_and_Builtins/2026-04-13-23-21-45.gh-issue-145239.pL8qRt.rst
@@ -0,0 +1,3 @@
+Unary plus is now accepted in :keyword:`match` literal patterns, mirroring
+the existing support for unary minus.
+Patch by Bartosz Sławecki.
diff --git a/Parser/parser.c b/Parser/parser.c
index f853d309de9180..c55c081dfc3d8e 100644
--- a/Parser/parser.c
+++ b/Parser/parser.c
@@ -9066,7 +9066,7 @@ complex_number_rule(Parser *p)
return _res;
}
-// signed_number: NUMBER | '-' NUMBER
+// signed_number: NUMBER | '+' NUMBER | '-' NUMBER
static expr_ty
signed_number_rule(Parser *p)
{
@@ -9107,6 +9107,33 @@ signed_number_rule(Parser *p)
D(fprintf(stderr, "%*c%s signed_number[%d-%d]: %s failed!\n",
p->level, ' ',
p->error_indicator ? "ERROR!" : "-", _mark, p->mark,
"NUMBER"));
}
+ { // '+' NUMBER
+ if (p->error_indicator) {
+ p->level--;
+ return NULL;
+ }
+ D(fprintf(stderr, "%*c> signed_number[%d-%d]: %s\n", p->level, ' ',
_mark, p->mark, "'+' NUMBER"));
+ Token * _literal;
+ expr_ty number;
+ if (
+ (_literal = _PyPegen_expect_token(p, 14)) // token='+'
+ &&
+ (number = _PyPegen_number_token(p)) // NUMBER
+ )
+ {
+ D(fprintf(stderr, "%*c+ signed_number[%d-%d]: %s succeeded!\n",
p->level, ' ', _mark, p->mark, "'+' NUMBER"));
+ _res = number;
+ if ((_res == NULL || p->error_indicator) && PyErr_Occurred()) {
+ p->error_indicator = 1;
+ p->level--;
+ return NULL;
+ }
+ goto done;
+ }
+ p->mark = _mark;
+ D(fprintf(stderr, "%*c%s signed_number[%d-%d]: %s failed!\n",
p->level, ' ',
+ p->error_indicator ? "ERROR!" : "-", _mark, p->mark, "'+'
NUMBER"));
+ }
{ // '-' NUMBER
if (p->error_indicator) {
p->level--;
@@ -9149,7 +9176,7 @@ signed_number_rule(Parser *p)
return _res;
}
-// signed_real_number: real_number | '-' real_number
+// signed_real_number: real_number | '+' real_number | '-' real_number
static expr_ty
signed_real_number_rule(Parser *p)
{
@@ -9190,6 +9217,33 @@ signed_real_number_rule(Parser *p)
D(fprintf(stderr, "%*c%s signed_real_number[%d-%d]: %s failed!\n",
p->level, ' ',
p->error_indicator ? "ERROR!" : "-", _mark, p->mark,
"real_number"));
}
+ { // '+' real_number
+ if (p->error_indicator) {
+ p->level--;
+ return NULL;
+ }
+ D(fprintf(stderr, "%*c> signed_real_number[%d-%d]: %s\n", p->level, '
', _mark, p->mark, "'+' real_number"));
+ Token * _literal;
+ expr_ty real;
+ if (
+ (_literal = _PyPegen_expect_token(p, 14)) // token='+'
+ &&
+ (real = real_number_rule(p)) // real_number
+ )
+ {
+ D(fprintf(stderr, "%*c+ signed_real_number[%d-%d]: %s
succeeded!\n", p->level, ' ', _mark, p->mark, "'+' real_number"));
+ _res = real;
+ if ((_res == NULL || p->error_indicator) && PyErr_Occurred()) {
+ p->error_indicator = 1;
+ p->level--;
+ return NULL;
+ }
+ goto done;
+ }
+ p->mark = _mark;
+ D(fprintf(stderr, "%*c%s signed_real_number[%d-%d]: %s failed!\n",
p->level, ' ',
+ p->error_indicator ? "ERROR!" : "-", _mark, p->mark, "'+'
real_number"));
+ }
{ // '-' real_number
if (p->error_indicator) {
p->level--;
@@ -9275,7 +9329,7 @@ real_number_rule(Parser *p)
return _res;
}
-// imaginary_number: NUMBER
+// imaginary_number: NUMBER | '+' NUMBER
static expr_ty
imaginary_number_rule(Parser *p)
{
@@ -9312,6 +9366,33 @@ imaginary_number_rule(Parser *p)
D(fprintf(stderr, "%*c%s imaginary_number[%d-%d]: %s failed!\n",
p->level, ' ',
p->error_indicator ? "ERROR!" : "-", _mark, p->mark,
"NUMBER"));
}
+ { // '+' NUMBER
+ if (p->error_indicator) {
+ p->level--;
+ return NULL;
+ }
+ D(fprintf(stderr, "%*c> imaginary_number[%d-%d]: %s\n", p->level, ' ',
_mark, p->mark, "'+' NUMBER"));
+ Token * _literal;
+ expr_ty imag;
+ if (
+ (_literal = _PyPegen_expect_token(p, 14)) // token='+'
+ &&
+ (imag = _PyPegen_number_token(p)) // NUMBER
+ )
+ {
+ D(fprintf(stderr, "%*c+ imaginary_number[%d-%d]: %s succeeded!\n",
p->level, ' ', _mark, p->mark, "'+' NUMBER"));
+ _res = _PyPegen_ensure_imaginary ( p , imag );
+ if ((_res == NULL || p->error_indicator) && PyErr_Occurred()) {
+ p->error_indicator = 1;
+ p->level--;
+ return NULL;
+ }
+ goto done;
+ }
+ p->mark = _mark;
+ D(fprintf(stderr, "%*c%s imaginary_number[%d-%d]: %s failed!\n",
p->level, ' ',
+ p->error_indicator ? "ERROR!" : "-", _mark, p->mark, "'+'
NUMBER"));
+ }
_res = NULL;
done:
p->level--;
_______________________________________________
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]