From 5a15e7378996358848394930343e9633b6fec8a9 Mon Sep 17 00:00:00 2001 From: Denis Sergeev Date: Thu, 18 Sep 2025 00:45:52 +0300 Subject: [PATCH 1/3] gh-138813: Fix mutable default kwargs={} in multiprocessing BaseProcess and DummyProcess to use None (GH-138814) * gh-138813: Default `BaseProcess` `kwargs` to `None` (#138814) Set `BaseProcess.__init__(..., kwargs=None)` and initialize `kwargs` with `dict(kwargs) if kwargs else {}`. This avoids a shared mutable default and matches threading.Thread behavior. Co-authored-by: Dmitrii Chuprov * DummyProcess kwargs=None (which threading.Thread accepts properly) Co-authored-by: Gregory P. Smith --- Lib/multiprocessing/dummy/__init__.py | 2 +- Lib/multiprocessing/process.py | 4 ++-- Lib/test/_test_multiprocessing.py | 17 +++++++++++++++++ ...25-09-17-08-32-43.gh-issue-138813.LHkHjX.rst | 1 + 4 files changed, 21 insertions(+), 3 deletions(-) create mode 100644 Misc/NEWS.d/next/Library/2025-09-17-08-32-43.gh-issue-138813.LHkHjX.rst diff --git a/Lib/multiprocessing/dummy/__init__.py b/Lib/multiprocessing/dummy/__init__.py index 6a1468609e347b..7dc5d1c8dde848 100644 --- a/Lib/multiprocessing/dummy/__init__.py +++ b/Lib/multiprocessing/dummy/__init__.py @@ -33,7 +33,7 @@ class DummyProcess(threading.Thread): - def __init__(self, group=None, target=None, name=None, args=(), kwargs={}): + def __init__(self, group=None, target=None, name=None, args=(), kwargs=None): threading.Thread.__init__(self, group, target, name, args, kwargs) self._pid = None self._children = weakref.WeakKeyDictionary() diff --git a/Lib/multiprocessing/process.py b/Lib/multiprocessing/process.py index 9db322be1aa6d6..262513f295fde5 100644 --- a/Lib/multiprocessing/process.py +++ b/Lib/multiprocessing/process.py @@ -77,7 +77,7 @@ class BaseProcess(object): def _Popen(self): raise NotImplementedError - def __init__(self, group=None, target=None, name=None, args=(), kwargs={}, + def __init__(self, group=None, target=None, name=None, args=(), kwargs=None, *, daemon=None): assert group is None, 'group argument must be None for now' count = next(_process_counter) @@ -89,7 +89,7 @@ def __init__(self, group=None, target=None, name=None, args=(), kwargs={}, self._closed = False self._target = target self._args = tuple(args) - self._kwargs = dict(kwargs) + self._kwargs = dict(kwargs) if kwargs else {} self._name = name or type(self).__name__ + '-' + \ ':'.join(str(i) for i in self._identity) if daemon is not None: diff --git a/Lib/test/_test_multiprocessing.py b/Lib/test/_test_multiprocessing.py index d9e572961152b3..850744e47d0e0b 100644 --- a/Lib/test/_test_multiprocessing.py +++ b/Lib/test/_test_multiprocessing.py @@ -5278,6 +5278,23 @@ def test_invalid_handles(self): multiprocessing.connection.Connection, -1) +# +# Regression tests for BaseProcess kwargs handling +# + +class TestBaseProcessKwargs(unittest.TestCase): + def test_default_kwargs_not_shared_between_instances(self): + # Creating multiple Process instances without passing kwargs + # must create independent empty dicts (no shared state). + p1 = multiprocessing.Process(target=lambda: None) + p2 = multiprocessing.Process(target=lambda: None) + self.assertIsInstance(p1._kwargs, dict) + self.assertIsInstance(p2._kwargs, dict) + self.assertIsNot(p1._kwargs, p2._kwargs) + # Mutating one should not affect the other + p1._kwargs['x'] = 1 + self.assertNotIn('x', p2._kwargs) + @hashlib_helper.requires_hashdigest('sha256') class OtherTest(unittest.TestCase): diff --git a/Misc/NEWS.d/next/Library/2025-09-17-08-32-43.gh-issue-138813.LHkHjX.rst b/Misc/NEWS.d/next/Library/2025-09-17-08-32-43.gh-issue-138813.LHkHjX.rst new file mode 100644 index 00000000000000..97f4d76bb2f454 --- /dev/null +++ b/Misc/NEWS.d/next/Library/2025-09-17-08-32-43.gh-issue-138813.LHkHjX.rst @@ -0,0 +1 @@ +:class:`!multiprocessing.BaseProcess` defaults ``kwargs`` to ``None`` instead of a shared dictionary. From b485e50fde3be08d796a2dac66cb822da1226bb3 Mon Sep 17 00:00:00 2001 From: Raymond Hettinger Date: Wed, 17 Sep 2025 16:50:15 -0500 Subject: [PATCH 2/3] gh-139074: Fix missing high precision case in sumprod() (gh-139075) --- Lib/test/test_math.py | 4 ++- ...-09-17-13-21-26.gh-issue-139074.dVZO5F.rst | 2 ++ Modules/mathmodule.c | 29 +++++++++---------- 3 files changed, 19 insertions(+), 16 deletions(-) create mode 100644 Misc/NEWS.d/next/Core_and_Builtins/2025-09-17-13-21-26.gh-issue-139074.dVZO5F.rst diff --git a/Lib/test/test_math.py b/Lib/test/test_math.py index e3b0d4fa9eeeb3..92326a46c33963 100644 --- a/Lib/test/test_math.py +++ b/Lib/test/test_math.py @@ -1485,7 +1485,9 @@ def __rmul__(self, other): # Error cases that arose during development args = ((-5, -5, 10), (1.5, 4611686018427387904, 2305843009213693952)) - self.assertEqual(sumprod(*args), 0.0) + self.assertEqual(sumprod(*args), -7.5) + self.assertEqual(sumprod([-0.01, 1, -1, 0.01], [1, 1, 1, 1]), 0.0) + self.assertEqual(sumprod([1, 1, 1, 1], [-0.01, 1, -1, 0.01], ), 0.0) @requires_IEEE_754 @unittest.skipIf(HAVE_DOUBLE_ROUNDING, diff --git a/Misc/NEWS.d/next/Core_and_Builtins/2025-09-17-13-21-26.gh-issue-139074.dVZO5F.rst b/Misc/NEWS.d/next/Core_and_Builtins/2025-09-17-13-21-26.gh-issue-139074.dVZO5F.rst new file mode 100644 index 00000000000000..56c9f21296d3ac --- /dev/null +++ b/Misc/NEWS.d/next/Core_and_Builtins/2025-09-17-13-21-26.gh-issue-139074.dVZO5F.rst @@ -0,0 +1,2 @@ +Fixed a missing case in :func:`math.sumprod` where a low precision path was +taken when an int/int input pair followed a float input. diff --git a/Modules/mathmodule.c b/Modules/mathmodule.c index c631beb9ce5477..d8bf9e76cdd082 100644 --- a/Modules/mathmodule.c +++ b/Modules/mathmodule.c @@ -2937,32 +2937,31 @@ math_sumprod_impl(PyObject *module, PyObject *p, PyObject *q) if (!finished) { double flt_p, flt_q; - bool p_type_float = PyFloat_CheckExact(p_i); - bool q_type_float = PyFloat_CheckExact(q_i); - if (p_type_float && q_type_float) { - flt_p = PyFloat_AS_DOUBLE(p_i); - flt_q = PyFloat_AS_DOUBLE(q_i); - } else if (p_type_float && (PyLong_CheckExact(q_i) || PyBool_Check(q_i))) { - /* We care about float/int pairs and int/float pairs because - they arise naturally in several use cases such as price - times quantity, measurements with integer weights, or - data selected by a vector of bools. */ + + if (PyFloat_CheckExact(p_i)) { flt_p = PyFloat_AS_DOUBLE(p_i); - flt_q = PyLong_AsDouble(q_i); - if (flt_q == -1.0 && PyErr_Occurred()) { + } else if (PyLong_CheckExact(p_i) || PyBool_Check(p_i)) { + flt_p = PyLong_AsDouble(p_i); + if (flt_p == -1.0 && PyErr_Occurred()) { PyErr_Clear(); goto finalize_flt_path; } - } else if (q_type_float && (PyLong_CheckExact(p_i) || PyBool_Check(p_i))) { + } else { + goto finalize_flt_path; + } + + if (PyFloat_CheckExact(q_i)) { flt_q = PyFloat_AS_DOUBLE(q_i); - flt_p = PyLong_AsDouble(p_i); - if (flt_p == -1.0 && PyErr_Occurred()) { + } else if (PyLong_CheckExact(q_i) || PyBool_Check(q_i)) { + flt_q = PyLong_AsDouble(q_i); + if (flt_q == -1.0 && PyErr_Occurred()) { PyErr_Clear(); goto finalize_flt_path; } } else { goto finalize_flt_path; } + TripleLength new_flt_total = tl_fma(flt_p, flt_q, flt_total); if (isfinite(new_flt_total.hi)) { flt_total = new_flt_total; From 0ce9fb7e3b192aefdb55e86f3e4c0e504445812d Mon Sep 17 00:00:00 2001 From: Pablo Galindo Salgado Date: Thu, 18 Sep 2025 02:17:04 +0100 Subject: [PATCH 3/3] gh-138970: Add general metadata system to the peg generator (#138971) --- Lib/test/test_peg_generator/test_pegen.py | 46 ++++++++++++++ Tools/peg_generator/pegen/c_generator.py | 2 +- Tools/peg_generator/pegen/grammar.py | 6 +- Tools/peg_generator/pegen/grammar_parser.py | 67 +++++++++++++++++---- Tools/peg_generator/pegen/metagrammar.gram | 18 +++--- 5 files changed, 113 insertions(+), 26 deletions(-) diff --git a/Lib/test/test_peg_generator/test_pegen.py b/Lib/test/test_peg_generator/test_pegen.py index d912c55812397d..0387b9395611b0 100644 --- a/Lib/test/test_peg_generator/test_pegen.py +++ b/Lib/test/test_peg_generator/test_pegen.py @@ -1106,3 +1106,49 @@ def test_deep_nested_rule(self) -> None: ) self.assertEqual(output, expected_output) + + def test_rule_flags(self) -> None: + """Test the new rule flags syntax that accepts arbitrary lists of flags.""" + # Test grammar with various flag combinations + grammar_source = """ + start: simple_rule + + simple_rule (memo): + | "hello" + + multi_flag_rule (memo, custom, test): + | "world" + + single_custom_flag (custom): + | "test" + + no_flags_rule: + | "plain" + """ + + grammar: Grammar = parse_string(grammar_source, GrammarParser) + rules = grammar.rules + + # Test memo-only rule + simple_rule = rules['simple_rule'] + self.assertTrue(simple_rule.memo, "simple_rule should have memo=True") + self.assertEqual(simple_rule.flags, frozenset(['memo']), + f"simple_rule flags should be {'memo'}, got {simple_rule.flags}") + + # Test multi-flag rule + multi_flag_rule = rules['multi_flag_rule'] + self.assertTrue(multi_flag_rule.memo, "multi_flag_rule should have memo=True") + self.assertEqual(multi_flag_rule.flags, frozenset({'memo', 'custom', 'test'}), + f"multi_flag_rule flags should contain memo, custom, test, got {multi_flag_rule.flags}") + + # Test single custom flag rule + single_custom_rule = rules['single_custom_flag'] + self.assertFalse(single_custom_rule.memo, "single_custom_flag should have memo=False") + self.assertEqual(single_custom_rule.flags, frozenset(['custom']), + f"single_custom_flag flags should be {'custom'}, got {single_custom_rule.flags}") + + # Test no flags rule + no_flags_rule = rules['no_flags_rule'] + self.assertFalse(no_flags_rule.memo, "no_flags_rule should have memo=False") + self.assertEqual(no_flags_rule.flags, [], + f"no_flags_rule flags should be the empty set, got {no_flags_rule.flags}") diff --git a/Tools/peg_generator/pegen/c_generator.py b/Tools/peg_generator/pegen/c_generator.py index fa75174ea0d59d..ffa73a64f21cfe 100644 --- a/Tools/peg_generator/pegen/c_generator.py +++ b/Tools/peg_generator/pegen/c_generator.py @@ -595,7 +595,7 @@ def _set_up_rule_memoization(self, node: Rule, result_type: str) -> None: self.print(f"{node.name}_raw(Parser *p)") def _should_memoize(self, node: Rule) -> bool: - return node.memo and not node.left_recursive + return "memo" in node.flags and not node.left_recursive def _handle_default_rule_body(self, node: Rule, rhs: Rhs, result_type: str) -> None: memoize = self._should_memoize(node) diff --git a/Tools/peg_generator/pegen/grammar.py b/Tools/peg_generator/pegen/grammar.py index cca8584a632071..d3c2eca6615a9f 100644 --- a/Tools/peg_generator/pegen/grammar.py +++ b/Tools/peg_generator/pegen/grammar.py @@ -58,11 +58,11 @@ def __iter__(self) -> Iterator[Rule]: class Rule: - def __init__(self, name: str, type: str | None, rhs: Rhs, memo: object | None = None): + def __init__(self, name: str, type: str | None, rhs: Rhs, flags: frozenset[str] | None = None): self.name = name self.type = type self.rhs = rhs - self.memo = bool(memo) + self.flags = flags or frozenset() self.left_recursive = False self.leader = False @@ -135,7 +135,6 @@ def __repr__(self) -> str: class Rhs: def __init__(self, alts: list[Alt]): self.alts = alts - self.memo: tuple[str | None, str] | None = None def __str__(self) -> str: return " | ".join(str(alt) for alt in self.alts) @@ -263,7 +262,6 @@ class Repeat: def __init__(self, node: Plain): self.node = node - self.memo: tuple[str | None, str] | None = None def __iter__(self) -> Iterator[Plain]: yield self.node diff --git a/Tools/peg_generator/pegen/grammar_parser.py b/Tools/peg_generator/pegen/grammar_parser.py index 2e3a607f7209b0..4fa2739270773f 100644 --- a/Tools/peg_generator/pegen/grammar_parser.py +++ b/Tools/peg_generator/pegen/grammar_parser.py @@ -147,12 +147,12 @@ def rules(self) -> Optional[RuleList]: @memoize def rule(self) -> Optional[Rule]: - # rule: rulename memoflag? ":" alts NEWLINE INDENT more_alts DEDENT | rulename memoflag? ":" NEWLINE INDENT more_alts DEDENT | rulename memoflag? ":" alts NEWLINE + # rule: rulename flags? ":" alts NEWLINE INDENT more_alts DEDENT | rulename flags? ":" NEWLINE INDENT more_alts DEDENT | rulename flags? ":" alts NEWLINE mark = self._mark() if ( (rulename := self.rulename()) and - (opt := self.memoflag(),) + (flags := self.flags(),) and (literal := self.expect(":")) and @@ -166,12 +166,12 @@ def rule(self) -> Optional[Rule]: and (_dedent := self.expect('DEDENT')) ): - return Rule ( rulename [0] , rulename [1] , Rhs ( alts . alts + more_alts . alts ) , memo = opt ) + return Rule ( rulename [0] , rulename [1] , Rhs ( alts . alts + more_alts . alts ) , flags = flags ) self._reset(mark) if ( (rulename := self.rulename()) and - (opt := self.memoflag(),) + (flags := self.flags(),) and (literal := self.expect(":")) and @@ -183,12 +183,12 @@ def rule(self) -> Optional[Rule]: and (_dedent := self.expect('DEDENT')) ): - return Rule ( rulename [0] , rulename [1] , more_alts , memo = opt ) + return Rule ( rulename [0] , rulename [1] , more_alts , flags = flags ) self._reset(mark) if ( (rulename := self.rulename()) and - (opt := self.memoflag(),) + (flags := self.flags(),) and (literal := self.expect(":")) and @@ -196,7 +196,7 @@ def rule(self) -> Optional[Rule]: and (_newline := self.expect('NEWLINE')) ): - return Rule ( rulename [0] , rulename [1] , alts , memo = opt ) + return Rule ( rulename [0] , rulename [1] , alts , flags = flags ) self._reset(mark) return None @@ -219,17 +219,28 @@ def rulename(self) -> Optional[RuleName]: return None @memoize - def memoflag(self) -> Optional[str]: - # memoflag: '(' "memo" ')' + def flags(self) -> Optional[frozenset [str]]: + # flags: '(' ','.flag+ ')' mark = self._mark() if ( (literal := self.expect('(')) and - (literal_1 := self.expect("memo")) + (a := self._gather_2()) and - (literal_2 := self.expect(')')) + (literal_1 := self.expect(')')) + ): + return frozenset ( a ) + self._reset(mark) + return None + + @memoize + def flag(self) -> Optional[str]: + # flag: NAME + mark = self._mark() + if ( + (name := self.name()) ): - return "memo" + return name . string self._reset(mark) return None @@ -661,8 +672,38 @@ def target_atom(self) -> Optional[str]: self._reset(mark) return None + @memoize + def _loop0_1(self) -> Optional[Any]: + # _loop0_1: ',' flag + mark = self._mark() + children = [] + while ( + (literal := self.expect(',')) + and + (elem := self.flag()) + ): + children.append(elem) + mark = self._mark() + self._reset(mark) + return children + + @memoize + def _gather_2(self) -> Optional[Any]: + # _gather_2: flag _loop0_1 + mark = self._mark() + if ( + (elem := self.flag()) + is not None + and + (seq := self._loop0_1()) + is not None + ): + return [elem] + seq + self._reset(mark) + return None + KEYWORDS = () - SOFT_KEYWORDS = ('memo',) + SOFT_KEYWORDS = () if __name__ == '__main__': diff --git a/Tools/peg_generator/pegen/metagrammar.gram b/Tools/peg_generator/pegen/metagrammar.gram index f484c4781823bc..cae91ab9c4165b 100644 --- a/Tools/peg_generator/pegen/metagrammar.gram +++ b/Tools/peg_generator/pegen/metagrammar.gram @@ -50,19 +50,21 @@ rules[RuleList]: | rule { [rule] } rule[Rule]: - | rulename memoflag? ":" alts NEWLINE INDENT more_alts DEDENT { - Rule(rulename[0], rulename[1], Rhs(alts.alts + more_alts.alts), memo=opt) } - | rulename memoflag? ":" NEWLINE INDENT more_alts DEDENT { - Rule(rulename[0], rulename[1], more_alts, memo=opt) } - | rulename memoflag? ":" alts NEWLINE { Rule(rulename[0], rulename[1], alts, memo=opt) } + | rulename flags=flags? ":" alts NEWLINE INDENT more_alts DEDENT { + Rule(rulename[0], rulename[1], Rhs(alts.alts + more_alts.alts), flags=flags) } + | rulename flags=flags? ":" NEWLINE INDENT more_alts DEDENT { + Rule(rulename[0], rulename[1], more_alts, flags=flags) } + | rulename flags=flags? ":" alts NEWLINE { Rule(rulename[0], rulename[1], alts, flags=flags) } rulename[RuleName]: | NAME annotation { (name.string, annotation) } | NAME { (name.string, None) } -# In the future this may return something more complicated -memoflag[str]: - | '(' "memo" ')' { "memo" } +flags[frozenset[str]]: + | '(' a=','.flag+ ')' { frozenset(a) } + +flag[str]: + | NAME { name.string } alts[Rhs]: | alt "|" alts { Rhs([alt] + alts.alts)}