From 004646e2ed6288ed839deee2e88ad5f3eaf2e738 Mon Sep 17 00:00:00 2001 From: johnslavik Date: Tue, 14 Apr 2026 00:50:39 +0200 Subject: [PATCH 1/8] gh-145239: Accept unary plus in match literal patterns Add '+' alternatives to signed_number and signed_real_number grammar rules, mirroring how unary minus is already handled. Unary plus is a no-op on numbers so the value is returned directly without wrapping in a UnaryOp node. --- Grammar/python.gram | 2 ++ Parser/parser.c | 58 +++++++++++++++++++++++++++++++++++++++++++-- 2 files changed, 58 insertions(+), 2 deletions(-) diff --git a/Grammar/python.gram b/Grammar/python.gram index 3a91d426c36501..1f749ca27cca8c 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]: diff --git a/Parser/parser.c b/Parser/parser.c index f853d309de9180..b12fb52be2445d 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--; From b30bc97c42ea8e4ff3e93fb0fe3ea5a9ff2fc3e9 Mon Sep 17 00:00:00 2001 From: johnslavik Date: Tue, 14 Apr 2026 00:53:45 +0200 Subject: [PATCH 2/8] gh-145239: Add tests for unary plus in match literal patterns Mirror the existing negative-number tests (095-110) for positive numbers: integers, floats, imaginary, and complex patterns. --- Lib/test/test_patma.py | 80 ++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 80 insertions(+) diff --git a/Lib/test/test_patma.py b/Lib/test/test_patma.py index 5d0857b059ea23..ed378b34deefe1 100644 --- a/Lib/test/test_patma.py +++ b/Lib/test/test_patma.py @@ -2762,6 +2762,86 @@ 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_runtime_checkable_protocol(self): # Runtime-checkable protocol from typing import Protocol, runtime_checkable From 5bb1a936bb5ec540697642e0061fac911563e61e Mon Sep 17 00:00:00 2001 From: johnslavik Date: Tue, 14 Apr 2026 01:21:58 +0200 Subject: [PATCH 3/8] gh-145239: Add NEWS entry --- .../2026-04-13-23-21-45.gh-issue-145239.pL8qRt.rst | 3 +++ 1 file changed, 3 insertions(+) create mode 100644 Misc/NEWS.d/next/Core_and_Builtins/2026-04-13-23-21-45.gh-issue-145239.pL8qRt.rst 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. From ea2a22421dd16f6a2c096e49a8c65526b92b21bb Mon Sep 17 00:00:00 2001 From: johnslavik Date: Tue, 14 Apr 2026 19:03:04 +0200 Subject: [PATCH 4/8] Add `1e1000` case --- Lib/test/test_patma.py | 10 ++++++++++ 1 file changed, 10 insertions(+) diff --git a/Lib/test/test_patma.py b/Lib/test/test_patma.py index ed378b34deefe1..0dc471a409beaf 100644 --- a/Lib/test/test_patma.py +++ b/Lib/test/test_patma.py @@ -2842,6 +2842,16 @@ def test_patma_265(self): 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 From a54b92d09270bea8a2fee4ab129f36e3ab0889d3 Mon Sep 17 00:00:00 2001 From: johnslavik Date: Tue, 14 Apr 2026 19:12:44 +0200 Subject: [PATCH 5/8] Add exclusion to `Lib/test/.ruff.toml` Assuming https://github.com/astral-sh/ruff/discussions/16394#discussioncomment-12492160 is still correct, there's no other way around --- Lib/test/.ruff.toml | 2 ++ 1 file changed, 2 insertions(+) diff --git a/Lib/test/.ruff.toml b/Lib/test/.ruff.toml index a960543f277935..f1dea05da0ac03 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 literal pattern is not yet supported by Ruff (GH-145239) + "test_patma.py", ] [lint] From 163998680a2ad50e355b06367feda78e229db48f Mon Sep 17 00:00:00 2001 From: johnslavik Date: Tue, 14 Apr 2026 19:14:04 +0200 Subject: [PATCH 6/8] Fix comment (unary plus literal, not just unary literal) --- Lib/test/.ruff.toml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Lib/test/.ruff.toml b/Lib/test/.ruff.toml index f1dea05da0ac03..dca74eb6e14bbd 100644 --- a/Lib/test/.ruff.toml +++ b/Lib/test/.ruff.toml @@ -18,7 +18,7 @@ extend-exclude = [ "test_lazy_import/__init__.py", "test_lazy_import/data/*.py", "test_lazy_import/data/**/*.py", - # Unary literal pattern is not yet supported by Ruff (GH-145239) + # Unary plus literal pattern is not yet supported by Ruff (GH-145239) "test_patma.py", ] From 52a973980399515d5aedefc1a08adb10f935184e Mon Sep 17 00:00:00 2001 From: johnslavik Date: Tue, 14 Apr 2026 19:30:23 +0200 Subject: [PATCH 7/8] Add What's New entry --- Doc/whatsnew/3.15.rst | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/Doc/whatsnew/3.15.rst b/Doc/whatsnew/3.15.rst index c754b634ecccfa..2ab7db8902a940 100644 --- a/Doc/whatsnew/3.15.rst +++ b/Doc/whatsnew/3.15.rst @@ -608,6 +608,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 =========== From 4ec49ce169e527874fa5e45531843711caae6286 Mon Sep 17 00:00:00 2001 From: johnslavik Date: Tue, 14 Apr 2026 20:15:11 +0200 Subject: [PATCH 8/8] Update the "Literal Patterns" section --- Doc/reference/compound_stmts.rst | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) 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