From 08477dbf300020cc67006c180917c02ee8a5cec8 Mon Sep 17 00:00:00 2001 From: "Andrew J. Hesford" Date: Sat, 22 Nov 2025 13:17:40 -0500 Subject: [PATCH 1/6] gh-141600: Fix musl version detection on Void Linux (GH-141602) --- Lib/platform.py | 4 ++-- Lib/test/test_platform.py | 6 ++++++ Lib/test/test_support.py | 4 ++-- .../Library/2025-11-15-14-58-12.gh-issue-141600.XY2BXg.rst | 1 + 4 files changed, 11 insertions(+), 4 deletions(-) create mode 100644 Misc/NEWS.d/next/Library/2025-11-15-14-58-12.gh-issue-141600.XY2BXg.rst diff --git a/Lib/platform.py b/Lib/platform.py index 4db93bea2a39e1..b5017dbdb02252 100644 --- a/Lib/platform.py +++ b/Lib/platform.py @@ -197,7 +197,7 @@ def libc_ver(executable=None, lib='', version='', chunksize=16384): | (GLIBC_([0-9.]+)) | (libc(_\w+)?\.so(?:\.(\d[0-9.]*))?) | (musl-([0-9.]+)) - | (libc.musl(?:-\w+)?.so(?:\.(\d[0-9.]*))?) + | ((?:libc\.|ld-)musl(?:-\w+)?.so(?:\.(\d[0-9.]*))?) """, re.ASCII | re.VERBOSE) @@ -236,7 +236,7 @@ def libc_ver(executable=None, lib='', version='', chunksize=16384): elif V(glibcversion) > V(ver): ver = glibcversion elif so: - if lib != 'glibc': + if lib not in ('glibc', 'musl'): lib = 'libc' if soversion and (not ver or V(soversion) > V(ver)): ver = soversion diff --git a/Lib/test/test_platform.py b/Lib/test/test_platform.py index c07f96aecf4a78..9ee97b922ad48e 100644 --- a/Lib/test/test_platform.py +++ b/Lib/test/test_platform.py @@ -569,6 +569,8 @@ def test_libc_ver(self): (b'/aports/main/musl/src/musl-1.2.5.7', ('musl', '1.2.5.7')), (b'libc.musl.so.1', ('musl', '1')), (b'libc.musl-x86_64.so.1.2.5', ('musl', '1.2.5')), + (b'ld-musl.so.1', ('musl', '1')), + (b'ld-musl-x86_64.so.1.2.5', ('musl', '1.2.5')), (b'', ('', '')), ): with open(filename, 'wb') as fp: @@ -591,6 +593,10 @@ def test_libc_ver(self): b'libc.musl-x86_64.so.1.4.1\0libc.musl-x86_64.so.2.1.1\0libc.musl-x86_64.so.2.0.1', ('musl', '2.1.1'), ), + ( + b'ld-musl-x86_64.so.1.4.1\0ld-musl-x86_64.so.2.1.1\0ld-musl-x86_64.so.2.0.1', + ('musl', '2.1.1'), + ), (b'no match here, so defaults are used', ('test', '100.1.0')), ): with open(filename, 'wb') as f: diff --git a/Lib/test/test_support.py b/Lib/test/test_support.py index d69328a6a6ac90..667fcc81d8e378 100644 --- a/Lib/test/test_support.py +++ b/Lib/test/test_support.py @@ -798,10 +798,10 @@ def test_linked_to_musl(self): self.assertTrue(linked) # The value is cached, so make sure it returns the same value again. self.assertIs(linked, support.linked_to_musl()) - # The unlike libc, the musl version is a triple. + # The musl version is either triple or just a major version number. if linked: self.assertIsInstance(linked, tuple) - self.assertEqual(3, len(linked)) + self.assertIn(len(linked), (1, 3)) for v in linked: self.assertIsInstance(v, int) diff --git a/Misc/NEWS.d/next/Library/2025-11-15-14-58-12.gh-issue-141600.XY2BXg.rst b/Misc/NEWS.d/next/Library/2025-11-15-14-58-12.gh-issue-141600.XY2BXg.rst new file mode 100644 index 00000000000000..8071246f130ace --- /dev/null +++ b/Misc/NEWS.d/next/Library/2025-11-15-14-58-12.gh-issue-141600.XY2BXg.rst @@ -0,0 +1 @@ +Fix musl version detection on Void Linux. From 58badb1711e12b6e8b5240ab96cdd01b95012de7 Mon Sep 17 00:00:00 2001 From: "Gregory P. Smith" <68491+gpshead@users.noreply.github.com> Date: Sat, 22 Nov 2025 10:29:51 -0800 Subject: [PATCH 2/6] gh-98552: flush std streams in the multiprocessing forkserver before fork (#141849) * flush std streams in the multiprocessing forkserver before fork * NEWS --- Lib/multiprocessing/forkserver.py | 1 + .../Library/2025-11-22-18-00-38.gh-issue-98552.d5KNy-.rst | 4 ++++ 2 files changed, 5 insertions(+) create mode 100644 Misc/NEWS.d/next/Library/2025-11-22-18-00-38.gh-issue-98552.d5KNy-.rst diff --git a/Lib/multiprocessing/forkserver.py b/Lib/multiprocessing/forkserver.py index cc8947c5e04fb1..8a4e8d835b0c91 100644 --- a/Lib/multiprocessing/forkserver.py +++ b/Lib/multiprocessing/forkserver.py @@ -326,6 +326,7 @@ def sigchld_handler(*_unused): len(fds))) child_r, child_w, *fds = fds s.close() + util._flush_std_streams() pid = os.fork() if pid == 0: # Child diff --git a/Misc/NEWS.d/next/Library/2025-11-22-18-00-38.gh-issue-98552.d5KNy-.rst b/Misc/NEWS.d/next/Library/2025-11-22-18-00-38.gh-issue-98552.d5KNy-.rst new file mode 100644 index 00000000000000..37a71ac1fff3f3 --- /dev/null +++ b/Misc/NEWS.d/next/Library/2025-11-22-18-00-38.gh-issue-98552.d5KNy-.rst @@ -0,0 +1,4 @@ +The :mod:`multiprocessing` forkserver process now flushes stdout and stderr +before it forks to avoid the confusion children inheriting any buffered but +not yet written output data. Normally there is none, but when using +:func:`multiprocessing.set_forkserver_preload` there *could* be. From d4e3829a74bc43e8f6ed6f56254c93a9ad3017b8 Mon Sep 17 00:00:00 2001 From: Weilin Du <108666168+LamentXU123@users.noreply.github.com> Date: Sun, 23 Nov 2025 03:16:09 +0800 Subject: [PATCH 3/6] gh-101100: Fix sphinx warnings in `library/unittest.rst` (#140109) Co-authored-by: Hugo van Kemenade <1324225+hugovk@users.noreply.github.com> --- Doc/library/unittest.rst | 50 ++++++++++++++++++++-------------------- Doc/tools/.nitignore | 1 - 2 files changed, 25 insertions(+), 26 deletions(-) diff --git a/Doc/library/unittest.rst b/Doc/library/unittest.rst index fe45becce2e5c3..0bc0a953fd921c 100644 --- a/Doc/library/unittest.rst +++ b/Doc/library/unittest.rst @@ -438,7 +438,7 @@ run whether the test method succeeded or not. Such a working environment for the testing code is called a :dfn:`test fixture`. A new TestCase instance is created as a unique test fixture used to execute each individual test method. Thus -:meth:`~TestCase.setUp`, :meth:`~TestCase.tearDown`, and :meth:`~TestCase.__init__` +:meth:`~TestCase.setUp`, :meth:`~TestCase.tearDown`, and :meth:`!TestCase.__init__` will be called once per test. It is recommended that you use TestCase implementations to group tests together @@ -518,7 +518,7 @@ set-up and tear-down methods:: subclasses will make future test refactorings infinitely easier. In some cases, the existing tests may have been written using the :mod:`doctest` -module. If so, :mod:`doctest` provides a :class:`DocTestSuite` class that can +module. If so, :mod:`doctest` provides a :class:`~doctest.DocTestSuite` class that can automatically build :class:`unittest.TestSuite` instances from the existing :mod:`doctest`\ -based tests. @@ -1023,7 +1023,7 @@ Test cases additional keyword argument *msg*. The context manager will store the caught exception object in its - :attr:`exception` attribute. This can be useful if the intention + :attr:`!exception` attribute. This can be useful if the intention is to perform additional checks on the exception raised:: with self.assertRaises(SomeException) as cm: @@ -1036,7 +1036,7 @@ Test cases Added the ability to use :meth:`assertRaises` as a context manager. .. versionchanged:: 3.2 - Added the :attr:`exception` attribute. + Added the :attr:`!exception` attribute. .. versionchanged:: 3.3 Added the *msg* keyword argument when used as a context manager. @@ -1089,8 +1089,8 @@ Test cases additional keyword argument *msg*. The context manager will store the caught warning object in its - :attr:`warning` attribute, and the source line which triggered the - warnings in the :attr:`filename` and :attr:`lineno` attributes. + :attr:`!warning` attribute, and the source line which triggered the + warnings in the :attr:`!filename` and :attr:`!lineno` attributes. This can be useful if the intention is to perform additional checks on the warning caught:: @@ -1437,7 +1437,7 @@ Test cases that lists the differences between the sets. This method is used by default when comparing sets or frozensets with :meth:`assertEqual`. - Fails if either of *first* or *second* does not have a :meth:`set.difference` + Fails if either of *first* or *second* does not have a :meth:`~frozenset.difference` method. .. versionadded:: 3.1 @@ -1645,7 +1645,7 @@ Test cases .. method:: asyncSetUp() :async: - Method called to prepare the test fixture. This is called after :meth:`setUp`. + Method called to prepare the test fixture. This is called after :meth:`TestCase.setUp`. This is called immediately before calling the test method; other than :exc:`AssertionError` or :exc:`SkipTest`, any exception raised by this method will be considered an error rather than a test failure. The default implementation @@ -1655,7 +1655,7 @@ Test cases :async: Method called immediately after the test method has been called and the - result recorded. This is called before :meth:`tearDown`. This is called even if + result recorded. This is called before :meth:`~TestCase.tearDown`. This is called even if the test method raised an exception, so the implementation in subclasses may need to be particularly careful about checking internal state. Any exception, other than :exc:`AssertionError` or :exc:`SkipTest`, raised by this method will be @@ -1684,7 +1684,7 @@ Test cases Sets up a new event loop to run the test, collecting the result into the :class:`TestResult` object passed as *result*. If *result* is omitted or ``None``, a temporary result object is created (by calling - the :meth:`defaultTestResult` method) and used. The result object is + the :meth:`~TestCase.defaultTestResult` method) and used. The result object is returned to :meth:`run`'s caller. At the end of the test all the tasks in the event loop are cancelled. @@ -1805,7 +1805,7 @@ Grouping tests returned by repeated iterations before :meth:`TestSuite.run` must be the same for each call iteration. After :meth:`TestSuite.run`, callers should not rely on the tests returned by this method unless the caller uses a - subclass that overrides :meth:`TestSuite._removeTestAtIndex` to preserve + subclass that overrides :meth:`!TestSuite._removeTestAtIndex` to preserve test references. .. versionchanged:: 3.2 @@ -1816,10 +1816,10 @@ Grouping tests .. versionchanged:: 3.4 In earlier versions the :class:`TestSuite` held references to each :class:`TestCase` after :meth:`TestSuite.run`. Subclasses can restore - that behavior by overriding :meth:`TestSuite._removeTestAtIndex`. + that behavior by overriding :meth:`!TestSuite._removeTestAtIndex`. In the typical usage of a :class:`TestSuite` object, the :meth:`run` method - is invoked by a :class:`TestRunner` rather than by the end-user test harness. + is invoked by a :class:`!TestRunner` rather than by the end-user test harness. Loading and running tests @@ -1853,12 +1853,12 @@ Loading and running tests .. method:: loadTestsFromTestCase(testCaseClass) Return a suite of all test cases contained in the :class:`TestCase`\ -derived - :class:`testCaseClass`. + :class:`!testCaseClass`. A test case instance is created for each method named by :meth:`getTestCaseNames`. By default these are the method names beginning with ``test``. If :meth:`getTestCaseNames` returns no - methods, but the :meth:`runTest` method is implemented, a single test + methods, but the :meth:`!runTest` method is implemented, a single test case is created for that method instead. @@ -1905,13 +1905,13 @@ Loading and running tests case class will be picked up as "a test method within a test case class", rather than "a callable object". - For example, if you have a module :mod:`SampleTests` containing a - :class:`TestCase`\ -derived class :class:`SampleTestCase` with three test - methods (:meth:`test_one`, :meth:`test_two`, and :meth:`test_three`), the + For example, if you have a module :mod:`!SampleTests` containing a + :class:`TestCase`\ -derived class :class:`!SampleTestCase` with three test + methods (:meth:`!test_one`, :meth:`!test_two`, and :meth:`!test_three`), the specifier ``'SampleTests.SampleTestCase'`` would cause this method to return a suite which will run all three test methods. Using the specifier ``'SampleTests.SampleTestCase.test_two'`` would cause it to return a test - suite which will run only the :meth:`test_two` test method. The specifier + suite which will run only the :meth:`!test_two` test method. The specifier can refer to modules and packages which have not been imported; they will be imported as a side-effect. @@ -2058,7 +2058,7 @@ Loading and running tests Testing frameworks built on top of :mod:`unittest` may want access to the :class:`TestResult` object generated by running a set of tests for reporting purposes; a :class:`TestResult` instance is returned by the - :meth:`TestRunner.run` method for this purpose. + :meth:`!TestRunner.run` method for this purpose. :class:`TestResult` instances have the following attributes that will be of interest when inspecting the results of running a set of tests: @@ -2144,12 +2144,12 @@ Loading and running tests This method can be called to signal that the set of tests being run should be aborted by setting the :attr:`shouldStop` attribute to ``True``. - :class:`TestRunner` objects should respect this flag and return without + :class:`!TestRunner` objects should respect this flag and return without running any additional tests. For example, this feature is used by the :class:`TextTestRunner` class to stop the test framework when the user signals an interrupt from the - keyboard. Interactive tools which provide :class:`TestRunner` + keyboard. Interactive tools which provide :class:`!TestRunner` implementations can use this in a similar manner. The following methods of the :class:`TestResult` class are used to maintain @@ -2469,9 +2469,9 @@ Class and Module Fixtures ------------------------- Class and module level fixtures are implemented in :class:`TestSuite`. When -the test suite encounters a test from a new class then :meth:`tearDownClass` -from the previous class (if there is one) is called, followed by -:meth:`setUpClass` from the new class. +the test suite encounters a test from a new class then +:meth:`~TestCase.tearDownClass` from the previous class (if there is one) +is called, followed by :meth:`~TestCase.setUpClass` from the new class. Similarly if a test is from a different module from the previous test then ``tearDownModule`` from the previous module is run, followed by diff --git a/Doc/tools/.nitignore b/Doc/tools/.nitignore index 04e8e5580fcd79..a431e0b32ad621 100644 --- a/Doc/tools/.nitignore +++ b/Doc/tools/.nitignore @@ -36,7 +36,6 @@ Doc/library/tkinter.rst Doc/library/tkinter.scrolledtext.rst Doc/library/tkinter.ttk.rst Doc/library/unittest.mock.rst -Doc/library/unittest.rst Doc/library/urllib.parse.rst Doc/library/urllib.request.rst Doc/library/wsgiref.rst From cde19e565cc9127fe5db38358ebf3bbd75a9d2cd Mon Sep 17 00:00:00 2001 From: Adam Turner <9087854+AA-Turner@users.noreply.github.com> Date: Sat, 22 Nov 2025 19:23:29 +0000 Subject: [PATCH 4/6] GH-101100: Resolve reference warnings in library/stdtypes.rst (#138420) --- Doc/library/stdtypes.rst | 11 ++++++----- Doc/tools/.nitignore | 1 - 2 files changed, 6 insertions(+), 6 deletions(-) diff --git a/Doc/library/stdtypes.rst b/Doc/library/stdtypes.rst index 3bcaba0b3e1eba..f3a99c4448b9f3 100644 --- a/Doc/library/stdtypes.rst +++ b/Doc/library/stdtypes.rst @@ -4755,11 +4755,12 @@ other sequence-like behavior. There are currently two built-in set types, :class:`set` and :class:`frozenset`. The :class:`set` type is mutable --- the contents can be changed using methods -like :meth:`~set.add` and :meth:`~set.remove`. Since it is mutable, it has no -hash value and cannot be used as either a dictionary key or as an element of -another set. The :class:`frozenset` type is immutable and :term:`hashable` --- -its contents cannot be altered after it is created; it can therefore be used as -a dictionary key or as an element of another set. +like :meth:`add ` and :meth:`remove `. +Since it is mutable, it has no hash value and cannot be used as +either a dictionary key or as an element of another set. +The :class:`frozenset` type is immutable and :term:`hashable` --- +its contents cannot be altered after it is created; +it can therefore be used as a dictionary key or as an element of another set. Non-empty sets (not frozensets) can be created by placing a comma-separated list of elements within braces, for example: ``{'jack', 'sjoerd'}``, in addition to the diff --git a/Doc/tools/.nitignore b/Doc/tools/.nitignore index a431e0b32ad621..520e64f5fe13b9 100644 --- a/Doc/tools/.nitignore +++ b/Doc/tools/.nitignore @@ -29,7 +29,6 @@ Doc/library/pyexpat.rst Doc/library/select.rst Doc/library/socket.rst Doc/library/ssl.rst -Doc/library/stdtypes.rst Doc/library/termios.rst Doc/library/test.rst Doc/library/tkinter.rst From 425fd85ca360a39a1a3fb16f09c448cb93ff794a Mon Sep 17 00:00:00 2001 From: Serhiy Storchaka Date: Sat, 22 Nov 2025 22:54:02 +0200 Subject: [PATCH 5/6] gh-138525: Support single-dash long options and prefix_chars in BooleanOptionalAction (GH-138692) -nofoo is generated for -foo. ++no-foo is generated for ++foo. /nofoo is generated for /foo. --- Doc/library/argparse.rst | 10 +++ Doc/whatsnew/3.15.rst | 4 ++ Lib/argparse.py | 20 ++++-- Lib/test/test_argparse.py | 70 +++++++++++++++++++ ...-09-09-10-13-24.gh-issue-138525.hDTaAM.rst | 2 + 5 files changed, 102 insertions(+), 4 deletions(-) create mode 100644 Misc/NEWS.d/next/Library/2025-09-09-10-13-24.gh-issue-138525.hDTaAM.rst diff --git a/Doc/library/argparse.rst b/Doc/library/argparse.rst index 30ddd849f3a2ef..2a39f248651936 100644 --- a/Doc/library/argparse.rst +++ b/Doc/library/argparse.rst @@ -1445,8 +1445,18 @@ this API may be passed as the ``action`` parameter to >>> parser.parse_args(['--no-foo']) Namespace(foo=False) + Single-dash long options are also supported. + For example, negative option ``-nofoo`` is automatically added for + positive option ``-foo``. + But no additional options are added for short options such as ``-f``. + .. versionadded:: 3.9 + .. versionchanged:: next + Added support for single-dash options. + + Added support for alternate prefix_chars_. + The parse_args() method ----------------------- diff --git a/Doc/whatsnew/3.15.rst b/Doc/whatsnew/3.15.rst index 8991584a9f22dd..4882ddb4310fc2 100644 --- a/Doc/whatsnew/3.15.rst +++ b/Doc/whatsnew/3.15.rst @@ -416,6 +416,10 @@ Improved modules argparse -------- +* The :class:`~argparse.BooleanOptionalAction` action supports now single-dash + long options and alternate prefix characters. + (Contributed by Serhiy Storchaka in :gh:`138525`.) + * Changed the *suggest_on_error* parameter of :class:`argparse.ArgumentParser` to default to ``True``. This enables suggestions for mistyped arguments by default. (Contributed by Jakob Schluse in :gh:`140450`.) diff --git a/Lib/argparse.py b/Lib/argparse.py index 5003927cb30485..02a17d93bdfcf5 100644 --- a/Lib/argparse.py +++ b/Lib/argparse.py @@ -932,15 +932,26 @@ def __init__(self, deprecated=False): _option_strings = [] + neg_option_strings = [] for option_string in option_strings: _option_strings.append(option_string) - if option_string.startswith('--'): - if option_string.startswith('--no-'): + if len(option_string) > 2 and option_string[0] == option_string[1]: + # two-dash long option: '--foo' -> '--no-foo' + if option_string.startswith('no-', 2): raise ValueError(f'invalid option name {option_string!r} ' f'for BooleanOptionalAction') - option_string = '--no-' + option_string[2:] + option_string = option_string[:2] + 'no-' + option_string[2:] _option_strings.append(option_string) + neg_option_strings.append(option_string) + elif len(option_string) > 2 and option_string[0] != option_string[1]: + # single-dash long option: '-foo' -> '-nofoo' + if option_string.startswith('no', 1): + raise ValueError(f'invalid option name {option_string!r} ' + f'for BooleanOptionalAction') + option_string = option_string[:1] + 'no' + option_string[1:] + _option_strings.append(option_string) + neg_option_strings.append(option_string) super().__init__( option_strings=_option_strings, @@ -950,11 +961,12 @@ def __init__(self, required=required, help=help, deprecated=deprecated) + self.neg_option_strings = neg_option_strings def __call__(self, parser, namespace, values, option_string=None): if option_string in self.option_strings: - setattr(namespace, self.dest, not option_string.startswith('--no-')) + setattr(namespace, self.dest, option_string not in self.neg_option_strings) def format_usage(self): return ' | '.join(self.option_strings) diff --git a/Lib/test/test_argparse.py b/Lib/test/test_argparse.py index b93502a74596df..8af51b1fc6eb26 100644 --- a/Lib/test/test_argparse.py +++ b/Lib/test/test_argparse.py @@ -805,6 +805,76 @@ def test_invalid_name(self): self.assertEqual(str(cm.exception), "invalid option name '--no-foo' for BooleanOptionalAction") +class TestBooleanOptionalActionSingleDash(ParserTestCase): + """Tests BooleanOptionalAction with single dash""" + + argument_signatures = [ + Sig('-foo', '-x', action=argparse.BooleanOptionalAction), + ] + failures = ['--foo', '--no-foo', '-no-foo', '-no-x', '-nox'] + successes = [ + ('', NS(foo=None)), + ('-foo', NS(foo=True)), + ('-nofoo', NS(foo=False)), + ('-x', NS(foo=True)), + ] + + def test_invalid_name(self): + parser = argparse.ArgumentParser() + with self.assertRaises(ValueError) as cm: + parser.add_argument('-nofoo', action=argparse.BooleanOptionalAction) + self.assertEqual(str(cm.exception), + "invalid option name '-nofoo' for BooleanOptionalAction") + +class TestBooleanOptionalActionAlternatePrefixChars(ParserTestCase): + """Tests BooleanOptionalAction with custom prefixes""" + + parser_signature = Sig(prefix_chars='+-', add_help=False) + argument_signatures = [Sig('++foo', action=argparse.BooleanOptionalAction)] + failures = ['--foo', '--no-foo'] + successes = [ + ('', NS(foo=None)), + ('++foo', NS(foo=True)), + ('++no-foo', NS(foo=False)), + ] + + def test_invalid_name(self): + parser = argparse.ArgumentParser(prefix_chars='+/') + with self.assertRaisesRegex(ValueError, + 'BooleanOptionalAction.*is not valid for positional arguments'): + parser.add_argument('--foo', action=argparse.BooleanOptionalAction) + with self.assertRaises(ValueError) as cm: + parser.add_argument('++no-foo', action=argparse.BooleanOptionalAction) + self.assertEqual(str(cm.exception), + "invalid option name '++no-foo' for BooleanOptionalAction") + +class TestBooleanOptionalActionSingleAlternatePrefixChar(ParserTestCase): + """Tests BooleanOptionalAction with single alternate prefix char""" + + parser_signature = Sig(prefix_chars='+/', add_help=False) + argument_signatures = [ + Sig('+foo', '+x', action=argparse.BooleanOptionalAction), + ] + failures = ['++foo', '++no-foo', '++nofoo', + '-no-foo', '-nofoo', '+no-foo', '-nofoo', + '+no-x', '+nox', '-no-x', '-nox'] + successes = [ + ('', NS(foo=None)), + ('+foo', NS(foo=True)), + ('+nofoo', NS(foo=False)), + ('+x', NS(foo=True)), + ] + + def test_invalid_name(self): + parser = argparse.ArgumentParser(prefix_chars='+/') + with self.assertRaisesRegex(ValueError, + 'BooleanOptionalAction.*is not valid for positional arguments'): + parser.add_argument('-foo', action=argparse.BooleanOptionalAction) + with self.assertRaises(ValueError) as cm: + parser.add_argument('+nofoo', action=argparse.BooleanOptionalAction) + self.assertEqual(str(cm.exception), + "invalid option name '+nofoo' for BooleanOptionalAction") + class TestBooleanOptionalActionRequired(ParserTestCase): """Tests BooleanOptionalAction required""" diff --git a/Misc/NEWS.d/next/Library/2025-09-09-10-13-24.gh-issue-138525.hDTaAM.rst b/Misc/NEWS.d/next/Library/2025-09-09-10-13-24.gh-issue-138525.hDTaAM.rst new file mode 100644 index 00000000000000..c4cea4b74b8a4c --- /dev/null +++ b/Misc/NEWS.d/next/Library/2025-09-09-10-13-24.gh-issue-138525.hDTaAM.rst @@ -0,0 +1,2 @@ +Add support for single-dash long options and alternate prefix characters in +:class:`argparse.BooleanOptionalAction`. From 227b9d326ec7eba35942a4eb451c7db244a33a6c Mon Sep 17 00:00:00 2001 From: Brandt Bucher Date: Sat, 22 Nov 2025 13:59:14 -0800 Subject: [PATCH 6/6] GH-140638: Add a GC "candidates" stat (GH-141814) --- Doc/library/gc.rst | 10 +++++++-- Include/internal/pycore_interp_structs.h | 4 ++++ Lib/test/test_gc.py | 12 ++++++---- ...-11-20-22-09-22.gh-issue-140638.f6btj0.rst | 2 ++ Modules/gcmodule.c | 3 ++- Python/gc.c | 17 +++++++++----- Python/gc_free_threading.c | 22 ++++++++++++------- 7 files changed, 50 insertions(+), 20 deletions(-) create mode 100644 Misc/NEWS.d/next/Core_and_Builtins/2025-11-20-22-09-22.gh-issue-140638.f6btj0.rst diff --git a/Doc/library/gc.rst b/Doc/library/gc.rst index 8e6f2342a2869a..79a8c38626f002 100644 --- a/Doc/library/gc.rst +++ b/Doc/library/gc.rst @@ -110,13 +110,16 @@ The :mod:`gc` module provides the following functions: to be uncollectable (and were therefore moved to the :data:`garbage` list) inside this generation; + * ``candidates`` is the total number of objects in this generation which were + considered for collection and traversed; + * ``duration`` is the total time in seconds spent in collections for this generation. .. versionadded:: 3.4 .. versionchanged:: next - Add ``duration``. + Add ``duration`` and ``candidates``. .. function:: set_threshold(threshold0, [threshold1, [threshold2]]) @@ -319,6 +322,9 @@ values but should not rebind them): "uncollectable": When *phase* is "stop", the number of objects that could not be collected and were put in :data:`garbage`. + "candidates": When *phase* is "stop", the total number of objects in this + generation which were considered for collection and traversed. + "duration": When *phase* is "stop", the time in seconds spent in the collection. @@ -335,7 +341,7 @@ values but should not rebind them): .. versionadded:: 3.3 .. versionchanged:: next - Add "duration". + Add "duration" and "candidates". The following constants are provided for use with :func:`set_debug`: diff --git a/Include/internal/pycore_interp_structs.h b/Include/internal/pycore_interp_structs.h index d9f5d444a2dc07..6b3d5711b92971 100644 --- a/Include/internal/pycore_interp_structs.h +++ b/Include/internal/pycore_interp_structs.h @@ -179,6 +179,8 @@ struct gc_collection_stats { Py_ssize_t collected; /* total number of uncollectable objects (put into gc.garbage) */ Py_ssize_t uncollectable; + // Total number of objects considered for collection and traversed: + Py_ssize_t candidates; // Duration of the collection in seconds: double duration; }; @@ -191,6 +193,8 @@ struct gc_generation_stats { Py_ssize_t collected; /* total number of uncollectable objects (put into gc.garbage) */ Py_ssize_t uncollectable; + // Total number of objects considered for collection and traversed: + Py_ssize_t candidates; // Duration of the collection in seconds: double duration; }; diff --git a/Lib/test/test_gc.py b/Lib/test/test_gc.py index e65da0f61d944f..ec5df4d20e7085 100644 --- a/Lib/test/test_gc.py +++ b/Lib/test/test_gc.py @@ -846,11 +846,14 @@ def test_get_stats(self): self.assertEqual(len(stats), 3) for st in stats: self.assertIsInstance(st, dict) - self.assertEqual(set(st), - {"collected", "collections", "uncollectable", "duration"}) + self.assertEqual( + set(st), + {"collected", "collections", "uncollectable", "candidates", "duration"} + ) self.assertGreaterEqual(st["collected"], 0) self.assertGreaterEqual(st["collections"], 0) self.assertGreaterEqual(st["uncollectable"], 0) + self.assertGreaterEqual(st["candidates"], 0) self.assertGreaterEqual(st["duration"], 0) # Check that collection counts are incremented correctly if gc.isenabled(): @@ -865,7 +868,7 @@ def test_get_stats(self): self.assertGreater(new[0]["duration"], old[0]["duration"]) self.assertEqual(new[1]["duration"], old[1]["duration"]) self.assertEqual(new[2]["duration"], old[2]["duration"]) - for stat in ["collected", "uncollectable"]: + for stat in ["collected", "uncollectable", "candidates"]: self.assertGreaterEqual(new[0][stat], old[0][stat]) self.assertEqual(new[1][stat], old[1][stat]) self.assertEqual(new[2][stat], old[2][stat]) @@ -877,7 +880,7 @@ def test_get_stats(self): self.assertEqual(new[0]["duration"], old[0]["duration"]) self.assertEqual(new[1]["duration"], old[1]["duration"]) self.assertGreater(new[2]["duration"], old[2]["duration"]) - for stat in ["collected", "uncollectable"]: + for stat in ["collected", "uncollectable", "candidates"]: self.assertEqual(new[0][stat], old[0][stat]) self.assertEqual(new[1][stat], old[1][stat]) self.assertGreaterEqual(new[2][stat], old[2][stat]) @@ -1316,6 +1319,7 @@ def test_collect(self): self.assertIn("generation", info) self.assertIn("collected", info) self.assertIn("uncollectable", info) + self.assertIn("candidates", info) self.assertIn("duration", info) def test_collect_generation(self): diff --git a/Misc/NEWS.d/next/Core_and_Builtins/2025-11-20-22-09-22.gh-issue-140638.f6btj0.rst b/Misc/NEWS.d/next/Core_and_Builtins/2025-11-20-22-09-22.gh-issue-140638.f6btj0.rst new file mode 100644 index 00000000000000..e3af941523cb75 --- /dev/null +++ b/Misc/NEWS.d/next/Core_and_Builtins/2025-11-20-22-09-22.gh-issue-140638.f6btj0.rst @@ -0,0 +1,2 @@ +Expose a ``"candidates"`` stat in :func:`gc.get_stats` and +:data:`gc.callbacks`. diff --git a/Modules/gcmodule.c b/Modules/gcmodule.c index 6a44d8a9d17aea..4c286f5c12cc7d 100644 --- a/Modules/gcmodule.c +++ b/Modules/gcmodule.c @@ -358,10 +358,11 @@ gc_get_stats_impl(PyObject *module) for (i = 0; i < NUM_GENERATIONS; i++) { PyObject *dict; st = &stats[i]; - dict = Py_BuildValue("{snsnsnsd}", + dict = Py_BuildValue("{snsnsnsnsd}", "collections", st->collections, "collected", st->collected, "uncollectable", st->uncollectable, + "candidates", st->candidates, "duration", st->duration ); if (dict == NULL) diff --git a/Python/gc.c b/Python/gc.c index 7e3e93e6e01be2..d067a6144b0763 100644 --- a/Python/gc.c +++ b/Python/gc.c @@ -483,11 +483,12 @@ validate_consistent_old_space(PyGC_Head *head) /* Set all gc_refs = ob_refcnt. After this, gc_refs is > 0 and * PREV_MASK_COLLECTING bit is set for all objects in containers. */ -static void +static Py_ssize_t update_refs(PyGC_Head *containers) { PyGC_Head *next; PyGC_Head *gc = GC_NEXT(containers); + Py_ssize_t candidates = 0; while (gc != containers) { next = GC_NEXT(gc); @@ -519,7 +520,9 @@ update_refs(PyGC_Head *containers) */ _PyObject_ASSERT(op, gc_get_refs(gc) != 0); gc = next; + candidates++; } + return candidates; } /* A traversal callback for subtract_refs. */ @@ -1240,7 +1243,7 @@ flag set but it does not clear it to skip unnecessary iteration. Before the flag is cleared (for example, by using 'clear_unreachable_mask' function or by a call to 'move_legacy_finalizers'), the 'unreachable' list is not a normal list and we can not use most gc_list_* functions for it. */ -static inline void +static inline Py_ssize_t deduce_unreachable(PyGC_Head *base, PyGC_Head *unreachable) { validate_list(base, collecting_clear_unreachable_clear); /* Using ob_refcnt and gc_refs, calculate which objects in the @@ -1248,7 +1251,7 @@ deduce_unreachable(PyGC_Head *base, PyGC_Head *unreachable) { * refcount greater than 0 when all the references within the * set are taken into account). */ - update_refs(base); // gc_prev is used for gc_refs + Py_ssize_t candidates = update_refs(base); // gc_prev is used for gc_refs subtract_refs(base); /* Leave everything reachable from outside base in base, and move @@ -1289,6 +1292,7 @@ deduce_unreachable(PyGC_Head *base, PyGC_Head *unreachable) { move_unreachable(base, unreachable); // gc_prev is pointer again validate_list(base, collecting_clear_unreachable_clear); validate_list(unreachable, collecting_set_unreachable_set); + return candidates; } /* Handle objects that may have resurrected after a call to 'finalize_garbage', moving @@ -1366,6 +1370,7 @@ add_stats(GCState *gcstate, int gen, struct gc_collection_stats *stats) gcstate->generation_stats[gen].duration += stats->duration; gcstate->generation_stats[gen].collected += stats->collected; gcstate->generation_stats[gen].uncollectable += stats->uncollectable; + gcstate->generation_stats[gen].candidates += stats->candidates; gcstate->generation_stats[gen].collections += 1; } @@ -1662,6 +1667,7 @@ gc_collect_increment(PyThreadState *tstate, struct gc_collection_stats *stats) Py_ssize_t objects_marked = mark_at_start(tstate); GC_STAT_ADD(1, objects_transitively_reachable, objects_marked); gcstate->work_to_do -= objects_marked; + stats->candidates += objects_marked; validate_spaces(gcstate); return; } @@ -1754,7 +1760,7 @@ gc_collect_region(PyThreadState *tstate, assert(!_PyErr_Occurred(tstate)); gc_list_init(&unreachable); - deduce_unreachable(from, &unreachable); + stats->candidates = deduce_unreachable(from, &unreachable); validate_consistent_old_space(from); untrack_tuples(from); @@ -1844,10 +1850,11 @@ do_gc_callback(GCState *gcstate, const char *phase, assert(PyList_CheckExact(gcstate->callbacks)); PyObject *info = NULL; if (PyList_GET_SIZE(gcstate->callbacks) != 0) { - info = Py_BuildValue("{sisnsnsd}", + info = Py_BuildValue("{sisnsnsnsd}", "generation", generation, "collected", stats->collected, "uncollectable", stats->uncollectable, + "candidates", stats->candidates, "duration", stats->duration); if (info == NULL) { PyErr_FormatUnraisable("Exception ignored while invoking gc callbacks"); diff --git a/Python/gc_free_threading.c b/Python/gc_free_threading.c index 9f424db8894524..1717603b947f90 100644 --- a/Python/gc_free_threading.c +++ b/Python/gc_free_threading.c @@ -100,6 +100,7 @@ struct collection_state { int skip_deferred_objects; Py_ssize_t collected; Py_ssize_t uncollectable; + Py_ssize_t candidates; Py_ssize_t long_lived_total; struct worklist unreachable; struct worklist legacy_finalizers; @@ -975,15 +976,12 @@ static bool update_refs(const mi_heap_t *heap, const mi_heap_area_t *area, void *block, size_t block_size, void *args) { + struct collection_state *state = (struct collection_state *)args; PyObject *op = op_from_block(block, args, false); if (op == NULL) { return true; } - if (gc_is_alive(op)) { - return true; - } - // Exclude immortal objects from garbage collection if (_Py_IsImmortal(op)) { op->ob_tid = 0; @@ -991,6 +989,11 @@ update_refs(const mi_heap_t *heap, const mi_heap_area_t *area, gc_clear_unreachable(op); return true; } + // Marked objects count as candidates, immortals don't: + state->candidates++; + if (gc_is_alive(op)) { + return true; + } Py_ssize_t refcount = Py_REFCNT(op); if (_PyObject_HasDeferredRefcount(op)) { @@ -1911,7 +1914,8 @@ handle_resurrected_objects(struct collection_state *state) static void invoke_gc_callback(PyThreadState *tstate, const char *phase, int generation, Py_ssize_t collected, - Py_ssize_t uncollectable, double duration) + Py_ssize_t uncollectable, Py_ssize_t candidates, + double duration) { assert(!_PyErr_Occurred(tstate)); @@ -1925,10 +1929,11 @@ invoke_gc_callback(PyThreadState *tstate, const char *phase, assert(PyList_CheckExact(gcstate->callbacks)); PyObject *info = NULL; if (PyList_GET_SIZE(gcstate->callbacks) != 0) { - info = Py_BuildValue("{sisnsnsd}", + info = Py_BuildValue("{sisnsnsnsd}", "generation", generation, "collected", collected, "uncollectable", uncollectable, + "candidates", candidates, "duration", duration); if (info == NULL) { PyErr_FormatUnraisable("Exception ignored while " @@ -2372,7 +2377,7 @@ gc_collect_main(PyThreadState *tstate, int generation, _PyGC_Reason reason) GC_STAT_ADD(generation, collections, 1); if (reason != _Py_GC_REASON_SHUTDOWN) { - invoke_gc_callback(tstate, "start", generation, 0, 0, 0); + invoke_gc_callback(tstate, "start", generation, 0, 0, 0, 0.0); } if (gcstate->debug & _PyGC_DEBUG_STATS) { @@ -2427,6 +2432,7 @@ gc_collect_main(PyThreadState *tstate, int generation, _PyGC_Reason reason) stats->collected += m; stats->uncollectable += n; stats->duration += duration; + stats->candidates += state.candidates; GC_STAT_ADD(generation, objects_collected, m); #ifdef Py_STATS @@ -2445,7 +2451,7 @@ gc_collect_main(PyThreadState *tstate, int generation, _PyGC_Reason reason) } if (reason != _Py_GC_REASON_SHUTDOWN) { - invoke_gc_callback(tstate, "stop", generation, m, n, duration); + invoke_gc_callback(tstate, "stop", generation, m, n, state.candidates, duration); } assert(!_PyErr_Occurred(tstate));