---
Doc/tools/templates/download.html | 6 +-----
1 file changed, 1 insertion(+), 5 deletions(-)
diff --git a/Doc/tools/templates/download.html b/Doc/tools/templates/download.html
index f914ad86211..c78c650b1cb 100644
--- a/Doc/tools/templates/download.html
+++ b/Doc/tools/templates/download.html
@@ -31,8 +31,7 @@ {% trans %}Download Python {{ dl_version }} documentation{% endtrans %}
{% if last_updated %}{% trans %}Last updated on: {{ last_updated }}.{% endtrans %}
{% endif %}
-{% trans %}To download an archive containing all the documents for this version of
-Python in one of various formats, follow one of links in this table.{% endtrans %}
+{% trans %}Download an archive containing all the documentation for this version of Python:{% endtrans %}
@@ -62,8 +61,6 @@ {% trans %}Download Python {{ dl_version }} documentation{% endtrans %}
-{% trans %}These archives contain all the content in the documentation.{% endtrans %}
-
{% trans %}
We no longer provide pre-built PDFs of the documentation.
To build a PDF archive, follow the instructions in the
@@ -75,7 +72,6 @@
{% trans %}Download Python {{ dl_version }} documentation{% endtrans %}
See the directory listing
for file sizes.{% endtrans %}
-
{% trans %}Problems{% endtrans %}
{% set bugs = pathto('bugs') %}
{% trans bugs = bugs %}Open an issue
From 13fa313bebed71d8bc64f1cfdaf4b2f1ddd3ce5f Mon Sep 17 00:00:00 2001
From: Stan Ulbrych <89152624+StanFromIreland@users.noreply.github.com>
Date: Mon, 10 Nov 2025 13:37:34 +0000
Subject: [PATCH 101/417] gh-139707: Specify `winreg`, `msvcrt` and `winsound`
module availability in docs (GH-140429)
---
Doc/library/msvcrt.rst | 2 ++
Doc/library/winreg.rst | 2 ++
Doc/library/winsound.rst | 2 ++
3 files changed, 6 insertions(+)
diff --git a/Doc/library/msvcrt.rst b/Doc/library/msvcrt.rst
index 327cc3602b1..a2c5e375d2c 100644
--- a/Doc/library/msvcrt.rst
+++ b/Doc/library/msvcrt.rst
@@ -22,6 +22,8 @@ api. The normal API deals only with ASCII characters and is of limited use
for internationalized applications. The wide char API should be used where
ever possible.
+.. availability:: Windows.
+
.. versionchanged:: 3.3
Operations in this module now raise :exc:`OSError` where :exc:`IOError`
was raised.
diff --git a/Doc/library/winreg.rst b/Doc/library/winreg.rst
index df8fb83a018..b150c53735d 100644
--- a/Doc/library/winreg.rst
+++ b/Doc/library/winreg.rst
@@ -14,6 +14,8 @@ integer as the registry handle, a :ref:`handle object ` is used
to ensure that the handles are closed correctly, even if the programmer neglects
to explicitly close them.
+.. availability:: Windows.
+
.. _exception-changed:
.. versionchanged:: 3.3
diff --git a/Doc/library/winsound.rst b/Doc/library/winsound.rst
index 925984c3cdb..93c0c025982 100644
--- a/Doc/library/winsound.rst
+++ b/Doc/library/winsound.rst
@@ -13,6 +13,8 @@
The :mod:`winsound` module provides access to the basic sound-playing machinery
provided by Windows platforms. It includes functions and several constants.
+.. availability:: Windows.
+
.. function:: Beep(frequency, duration)
From 9f5152441d32166134c3c64f56f974b9476f9478 Mon Sep 17 00:00:00 2001
From: Petr Viktorin
Date: Mon, 10 Nov 2025 14:42:18 +0100
Subject: [PATCH 102/417] gh-136702: Clear codec caches for refleak tests; use
test.support helpers (GH-141345)
This should fix refleak buildbots.
---
Lib/test/libregrtest/utils.py | 19 +++++++++++++++++++
Lib/test/test_codecs.py | 5 +++--
Lib/test/test_email/test_email.py | 3 ++-
Lib/test/test_email/test_headerregistry.py | 3 ++-
4 files changed, 26 insertions(+), 4 deletions(-)
diff --git a/Lib/test/libregrtest/utils.py b/Lib/test/libregrtest/utils.py
index d94fb84a743..cfb009c203e 100644
--- a/Lib/test/libregrtest/utils.py
+++ b/Lib/test/libregrtest/utils.py
@@ -294,6 +294,25 @@ def clear_caches():
else:
importlib_metadata.FastPath.__new__.cache_clear()
+ try:
+ encodings = sys.modules['encodings']
+ except KeyError:
+ pass
+ else:
+ encodings._cache.clear()
+
+ try:
+ codecs = sys.modules['codecs']
+ except KeyError:
+ pass
+ else:
+ # There's no direct API to clear the codecs search cache, but
+ # `unregister` clears it implicitly.
+ def noop_search_function(name):
+ return None
+ codecs.register(noop_search_function)
+ codecs.unregister(noop_search_function)
+
def get_build_info():
# Get most important configure and build options as a list of strings.
diff --git a/Lib/test/test_codecs.py b/Lib/test/test_codecs.py
index f1f0ac5ad36..c31faec9ee5 100644
--- a/Lib/test/test_codecs.py
+++ b/Lib/test/test_codecs.py
@@ -13,6 +13,7 @@
from test import support
from test.support import os_helper
+from test.support import warnings_helper
try:
import _testlimitedcapi
@@ -3902,8 +3903,8 @@ def test_encodings_normalize_encoding(self):
self.assertEqual(normalize('utf...8'), 'utf...8')
# Non-ASCII *encoding* is deprecated.
- with self.assertWarnsRegex(DeprecationWarning,
- "Support for non-ascii encoding names will be removed in 3.17"):
+ msg = "Support for non-ascii encoding names will be removed in 3.17"
+ with warnings_helper.check_warnings((msg, DeprecationWarning)):
self.assertEqual(normalize('utf\xE9\u20AC\U0010ffff-8'), 'utf_8')
diff --git a/Lib/test/test_email/test_email.py b/Lib/test/test_email/test_email.py
index 1900adf463b..4020f1041c4 100644
--- a/Lib/test/test_email/test_email.py
+++ b/Lib/test/test_email/test_email.py
@@ -41,6 +41,7 @@
from test import support
from test.support import threading_helper
+from test.support import warnings_helper
from test.support.os_helper import unlink
from test.test_email import openfile, TestEmailBase
@@ -5738,7 +5739,7 @@ def test_rfc2231_bad_character_in_encoding(self):
"""
msg = email.message_from_string(m)
- with self.assertWarns(DeprecationWarning):
+ with warnings_helper.check_warnings(('', DeprecationWarning)):
self.assertEqual(msg.get_filename(), 'myfile.txt')
def test_rfc2231_single_tick_in_filename_extended(self):
diff --git a/Lib/test/test_email/test_headerregistry.py b/Lib/test/test_email/test_headerregistry.py
index 1d0d0a49a82..7138aa4c556 100644
--- a/Lib/test/test_email/test_headerregistry.py
+++ b/Lib/test/test_email/test_headerregistry.py
@@ -8,6 +8,7 @@
from email import headerregistry
from email.headerregistry import Address, Group
from test.support import ALWAYS_EQ
+from test.support import warnings_helper
DITTO = object()
@@ -252,7 +253,7 @@ def content_type_as_value(self,
if 'utf-8%E2%80%9D' in source and 'ascii' not in source:
import encodings
encodings._cache.clear()
- with self.assertWarns(DeprecationWarning):
+ with warnings_helper.check_warnings(('', DeprecationWarning)):
h = self.make_header('Content-Type', source)
else:
h = self.make_header('Content-Type', source)
From 12837c63635559873a5abddf511d38456d69617b Mon Sep 17 00:00:00 2001
From: David Ellis
Date: Mon, 10 Nov 2025 13:57:11 +0000
Subject: [PATCH 103/417] gh-137530: generate an __annotate__ function for
dataclasses __init__ (GH-137711)
---
Doc/whatsnew/3.15.rst | 8 +
Lib/dataclasses.py | 94 ++++++++++--
Lib/test/test_dataclasses/__init__.py | 139 +++++++++++++++++-
...-10-21-15-54-13.gh-issue-137530.ZyIVUH.rst | 1 +
4 files changed, 227 insertions(+), 15 deletions(-)
create mode 100644 Misc/NEWS.d/next/Library/2025-10-21-15-54-13.gh-issue-137530.ZyIVUH.rst
diff --git a/Doc/whatsnew/3.15.rst b/Doc/whatsnew/3.15.rst
index 5379ac3abba..e0b0471567c 100644
--- a/Doc/whatsnew/3.15.rst
+++ b/Doc/whatsnew/3.15.rst
@@ -368,6 +368,14 @@ collections.abc
previously emitted if it was merely imported or accessed from the
:mod:`!collections.abc` module.
+
+dataclasses
+-----------
+
+* Annotations for generated ``__init__`` methods no longer include internal
+ type names.
+
+
dbm
---
diff --git a/Lib/dataclasses.py b/Lib/dataclasses.py
index b98f21dcbe9..3ccb7246928 100644
--- a/Lib/dataclasses.py
+++ b/Lib/dataclasses.py
@@ -441,9 +441,11 @@ def __init__(self, globals):
self.locals = {}
self.overwrite_errors = {}
self.unconditional_adds = {}
+ self.method_annotations = {}
def add_fn(self, name, args, body, *, locals=None, return_type=MISSING,
- overwrite_error=False, unconditional_add=False, decorator=None):
+ overwrite_error=False, unconditional_add=False, decorator=None,
+ annotation_fields=None):
if locals is not None:
self.locals.update(locals)
@@ -464,16 +466,14 @@ def add_fn(self, name, args, body, *, locals=None, return_type=MISSING,
self.names.append(name)
- if return_type is not MISSING:
- self.locals[f'__dataclass_{name}_return_type__'] = return_type
- return_annotation = f'->__dataclass_{name}_return_type__'
- else:
- return_annotation = ''
+ if annotation_fields is not None:
+ self.method_annotations[name] = (annotation_fields, return_type)
+
args = ','.join(args)
body = '\n'.join(body)
# Compute the text of the entire function, add it to the text we're generating.
- self.src.append(f'{f' {decorator}\n' if decorator else ''} def {name}({args}){return_annotation}:\n{body}')
+ self.src.append(f'{f' {decorator}\n' if decorator else ''} def {name}({args}):\n{body}')
def add_fns_to_class(self, cls):
# The source to all of the functions we're generating.
@@ -509,6 +509,15 @@ def add_fns_to_class(self, cls):
# Now that we've generated the functions, assign them into cls.
for name, fn in zip(self.names, fns):
fn.__qualname__ = f"{cls.__qualname__}.{fn.__name__}"
+
+ try:
+ annotation_fields, return_type = self.method_annotations[name]
+ except KeyError:
+ pass
+ else:
+ annotate_fn = _make_annotate_function(cls, name, annotation_fields, return_type)
+ fn.__annotate__ = annotate_fn
+
if self.unconditional_adds.get(name, False):
setattr(cls, name, fn)
else:
@@ -524,6 +533,44 @@ def add_fns_to_class(self, cls):
raise TypeError(error_msg)
+def _make_annotate_function(__class__, method_name, annotation_fields, return_type):
+ # Create an __annotate__ function for a dataclass
+ # Try to return annotations in the same format as they would be
+ # from a regular __init__ function
+
+ def __annotate__(format, /):
+ Format = annotationlib.Format
+ match format:
+ case Format.VALUE | Format.FORWARDREF | Format.STRING:
+ cls_annotations = {}
+ for base in reversed(__class__.__mro__):
+ cls_annotations.update(
+ annotationlib.get_annotations(base, format=format)
+ )
+
+ new_annotations = {}
+ for k in annotation_fields:
+ new_annotations[k] = cls_annotations[k]
+
+ if return_type is not MISSING:
+ if format == Format.STRING:
+ new_annotations["return"] = annotationlib.type_repr(return_type)
+ else:
+ new_annotations["return"] = return_type
+
+ return new_annotations
+
+ case _:
+ raise NotImplementedError(format)
+
+ # This is a flag for _add_slots to know it needs to regenerate this method
+ # In order to remove references to the original class when it is replaced
+ __annotate__.__generated_by_dataclasses__ = True
+ __annotate__.__qualname__ = f"{__class__.__qualname__}.{method_name}.__annotate__"
+
+ return __annotate__
+
+
def _field_assign(frozen, name, value, self_name):
# If we're a frozen class, then assign to our fields in __init__
# via object.__setattr__. Otherwise, just use a simple
@@ -612,7 +659,7 @@ def _init_param(f):
elif f.default_factory is not MISSING:
# There's a factory function. Set a marker.
default = '=__dataclass_HAS_DEFAULT_FACTORY__'
- return f'{f.name}:__dataclass_type_{f.name}__{default}'
+ return f'{f.name}{default}'
def _init_fn(fields, std_fields, kw_only_fields, frozen, has_post_init,
@@ -635,11 +682,10 @@ def _init_fn(fields, std_fields, kw_only_fields, frozen, has_post_init,
raise TypeError(f'non-default argument {f.name!r} '
f'follows default argument {seen_default.name!r}')
- locals = {**{f'__dataclass_type_{f.name}__': f.type for f in fields},
- **{'__dataclass_HAS_DEFAULT_FACTORY__': _HAS_DEFAULT_FACTORY,
- '__dataclass_builtins_object__': object,
- }
- }
+ annotation_fields = [f.name for f in fields if f.init]
+
+ locals = {'__dataclass_HAS_DEFAULT_FACTORY__': _HAS_DEFAULT_FACTORY,
+ '__dataclass_builtins_object__': object}
body_lines = []
for f in fields:
@@ -670,7 +716,8 @@ def _init_fn(fields, std_fields, kw_only_fields, frozen, has_post_init,
[self_name] + _init_params,
body_lines,
locals=locals,
- return_type=None)
+ return_type=None,
+ annotation_fields=annotation_fields)
def _frozen_get_del_attr(cls, fields, func_builder):
@@ -1337,6 +1384,25 @@ def _add_slots(cls, is_frozen, weakref_slot, defined_fields):
or _update_func_cell_for__class__(member.fdel, cls, newcls)):
break
+ # Get new annotations to remove references to the original class
+ # in forward references
+ newcls_ann = annotationlib.get_annotations(
+ newcls, format=annotationlib.Format.FORWARDREF)
+
+ # Fix references in dataclass Fields
+ for f in getattr(newcls, _FIELDS).values():
+ try:
+ ann = newcls_ann[f.name]
+ except KeyError:
+ pass
+ else:
+ f.type = ann
+
+ # Fix the class reference in the __annotate__ method
+ init_annotate = newcls.__init__.__annotate__
+ if getattr(init_annotate, "__generated_by_dataclasses__", False):
+ _update_func_cell_for__class__(init_annotate, cls, newcls)
+
return newcls
diff --git a/Lib/test/test_dataclasses/__init__.py b/Lib/test/test_dataclasses/__init__.py
index 6bf5e5b3e55..513dd78c438 100644
--- a/Lib/test/test_dataclasses/__init__.py
+++ b/Lib/test/test_dataclasses/__init__.py
@@ -2471,6 +2471,135 @@ def __init__(self, a):
self.assertEqual(D(5).a, 10)
+class TestInitAnnotate(unittest.TestCase):
+ # Tests for the generated __annotate__ function for __init__
+ # See: https://github.com/python/cpython/issues/137530
+
+ def test_annotate_function(self):
+ # No forward references
+ @dataclass
+ class A:
+ a: int
+
+ value_annos = annotationlib.get_annotations(A.__init__, format=annotationlib.Format.VALUE)
+ forwardref_annos = annotationlib.get_annotations(A.__init__, format=annotationlib.Format.FORWARDREF)
+ string_annos = annotationlib.get_annotations(A.__init__, format=annotationlib.Format.STRING)
+
+ self.assertEqual(value_annos, {'a': int, 'return': None})
+ self.assertEqual(forwardref_annos, {'a': int, 'return': None})
+ self.assertEqual(string_annos, {'a': 'int', 'return': 'None'})
+
+ self.assertTrue(getattr(A.__init__.__annotate__, "__generated_by_dataclasses__"))
+
+ def test_annotate_function_forwardref(self):
+ # With forward references
+ @dataclass
+ class B:
+ b: undefined
+
+ # VALUE annotations should raise while unresolvable
+ with self.assertRaises(NameError):
+ _ = annotationlib.get_annotations(B.__init__, format=annotationlib.Format.VALUE)
+
+ forwardref_annos = annotationlib.get_annotations(B.__init__, format=annotationlib.Format.FORWARDREF)
+ string_annos = annotationlib.get_annotations(B.__init__, format=annotationlib.Format.STRING)
+
+ self.assertEqual(forwardref_annos, {'b': support.EqualToForwardRef('undefined', owner=B, is_class=True), 'return': None})
+ self.assertEqual(string_annos, {'b': 'undefined', 'return': 'None'})
+
+ # Now VALUE and FORWARDREF should resolve, STRING should be unchanged
+ undefined = int
+
+ value_annos = annotationlib.get_annotations(B.__init__, format=annotationlib.Format.VALUE)
+ forwardref_annos = annotationlib.get_annotations(B.__init__, format=annotationlib.Format.FORWARDREF)
+ string_annos = annotationlib.get_annotations(B.__init__, format=annotationlib.Format.STRING)
+
+ self.assertEqual(value_annos, {'b': int, 'return': None})
+ self.assertEqual(forwardref_annos, {'b': int, 'return': None})
+ self.assertEqual(string_annos, {'b': 'undefined', 'return': 'None'})
+
+ def test_annotate_function_init_false(self):
+ # Check `init=False` attributes don't get into the annotations of the __init__ function
+ @dataclass
+ class C:
+ c: str = field(init=False)
+
+ self.assertEqual(annotationlib.get_annotations(C.__init__), {'return': None})
+
+ def test_annotate_function_contains_forwardref(self):
+ # Check string annotations on objects containing a ForwardRef
+ @dataclass
+ class D:
+ d: list[undefined]
+
+ with self.assertRaises(NameError):
+ annotationlib.get_annotations(D.__init__)
+
+ self.assertEqual(
+ annotationlib.get_annotations(D.__init__, format=annotationlib.Format.FORWARDREF),
+ {"d": list[support.EqualToForwardRef("undefined", is_class=True, owner=D)], "return": None}
+ )
+
+ self.assertEqual(
+ annotationlib.get_annotations(D.__init__, format=annotationlib.Format.STRING),
+ {"d": "list[undefined]", "return": "None"}
+ )
+
+ # Now test when it is defined
+ undefined = str
+
+ # VALUE should now resolve
+ self.assertEqual(
+ annotationlib.get_annotations(D.__init__),
+ {"d": list[str], "return": None}
+ )
+
+ self.assertEqual(
+ annotationlib.get_annotations(D.__init__, format=annotationlib.Format.FORWARDREF),
+ {"d": list[str], "return": None}
+ )
+
+ self.assertEqual(
+ annotationlib.get_annotations(D.__init__, format=annotationlib.Format.STRING),
+ {"d": "list[undefined]", "return": "None"}
+ )
+
+ def test_annotate_function_not_replaced(self):
+ # Check that __annotate__ is not replaced on non-generated __init__ functions
+ @dataclass(slots=True)
+ class E:
+ x: str
+ def __init__(self, x: int) -> None:
+ self.x = x
+
+ self.assertEqual(
+ annotationlib.get_annotations(E.__init__), {"x": int, "return": None}
+ )
+
+ self.assertFalse(hasattr(E.__init__.__annotate__, "__generated_by_dataclasses__"))
+
+ def test_init_false_forwardref(self):
+ # Test forward references in fields not required for __init__ annotations.
+
+ # At the moment this raises a NameError for VALUE annotations even though the
+ # undefined annotation is not required for the __init__ annotations.
+ # Ideally this will be fixed but currently there is no good way to resolve this
+
+ @dataclass
+ class F:
+ not_in_init: list[undefined] = field(init=False, default=None)
+ in_init: int
+
+ annos = annotationlib.get_annotations(F.__init__, format=annotationlib.Format.FORWARDREF)
+ self.assertEqual(
+ annos,
+ {"in_init": int, "return": None},
+ )
+
+ with self.assertRaises(NameError):
+ annos = annotationlib.get_annotations(F.__init__) # NameError on not_in_init
+
+
class TestRepr(unittest.TestCase):
def test_repr(self):
@dataclass
@@ -3831,7 +3960,15 @@ def method(self) -> int:
return SlotsTest
- for make in (make_simple, make_with_annotations, make_with_annotations_and_method):
+ def make_with_forwardref():
+ @dataclass(slots=True)
+ class SlotsTest:
+ x: undefined
+ y: list[undefined]
+
+ return SlotsTest
+
+ for make in (make_simple, make_with_annotations, make_with_annotations_and_method, make_with_forwardref):
with self.subTest(make=make):
C = make()
support.gc_collect()
diff --git a/Misc/NEWS.d/next/Library/2025-10-21-15-54-13.gh-issue-137530.ZyIVUH.rst b/Misc/NEWS.d/next/Library/2025-10-21-15-54-13.gh-issue-137530.ZyIVUH.rst
new file mode 100644
index 00000000000..4ff55b41dea
--- /dev/null
+++ b/Misc/NEWS.d/next/Library/2025-10-21-15-54-13.gh-issue-137530.ZyIVUH.rst
@@ -0,0 +1 @@
+:mod:`dataclasses` Fix annotations for generated ``__init__`` methods by replacing the annotations that were in-line in the generated source code with ``__annotate__`` functions attached to the methods.
From 06b62282c79dd69293a3eefb4c55f5acc6312cb2 Mon Sep 17 00:00:00 2001
From: dr-carlos <77367421+dr-carlos@users.noreply.github.com>
Date: Tue, 11 Nov 2025 01:15:22 +1030
Subject: [PATCH 104/417] gh-141174: Improve `annotationlib.get_annotations()`
test coverage (#141286)
* Test `get_annotations(format=Format.VALUE)` for stringized annotations on custom objects
* Test `get_annotations(format=Format.VALUE)` for stringized annotations on wrapped partial functions
* Update test_stringized_annotations_with_star_unpack() to actually test stringized annotations
* Test __annotate__ returning a non-dict
* Test passing globals and locals to stringized `get_annotations()`
---
Lib/test/test_annotationlib.py | 70 +++++++++++++++++++++++++++++++++-
1 file changed, 69 insertions(+), 1 deletion(-)
diff --git a/Lib/test/test_annotationlib.py b/Lib/test/test_annotationlib.py
index fd5d43b09b9..f1d32ab50cf 100644
--- a/Lib/test/test_annotationlib.py
+++ b/Lib/test/test_annotationlib.py
@@ -9,6 +9,7 @@
import pickle
from string.templatelib import Template, Interpolation
import typing
+import sys
import unittest
from annotationlib import (
Format,
@@ -755,6 +756,8 @@ def test_stringized_annotations_in_module(self):
for kwargs in [
{"eval_str": True},
+ {"eval_str": True, "globals": isa.__dict__, "locals": {}},
+ {"eval_str": True, "globals": {}, "locals": isa.__dict__},
{"format": Format.VALUE, "eval_str": True},
]:
with self.subTest(**kwargs):
@@ -788,7 +791,7 @@ def test_stringized_annotations_in_empty_module(self):
self.assertEqual(get_annotations(isa2, eval_str=False), {})
def test_stringized_annotations_with_star_unpack(self):
- def f(*args: *tuple[int, ...]): ...
+ def f(*args: "*tuple[int, ...]"): ...
self.assertEqual(get_annotations(f, eval_str=True),
{'args': (*tuple[int, ...],)[0]})
@@ -811,6 +814,44 @@ def test_stringized_annotations_on_wrapper(self):
{"a": "int", "b": "str", "return": "MyClass"},
)
+ def test_stringized_annotations_on_partial_wrapper(self):
+ isa = inspect_stringized_annotations
+
+ def times_three_str(fn: typing.Callable[[str], isa.MyClass]):
+ @functools.wraps(fn)
+ def wrapper(b: "str") -> "MyClass":
+ return fn(b * 3)
+
+ return wrapper
+
+ wrapped = times_three_str(functools.partial(isa.function, 1))
+ self.assertEqual(wrapped("x"), isa.MyClass(1, "xxx"))
+ self.assertIsNot(wrapped.__globals__, isa.function.__globals__)
+ self.assertEqual(
+ get_annotations(wrapped, eval_str=True),
+ {"b": str, "return": isa.MyClass},
+ )
+ self.assertEqual(
+ get_annotations(wrapped, eval_str=False),
+ {"b": "str", "return": "MyClass"},
+ )
+
+ # If functools is not loaded, names will be evaluated in the current
+ # module instead of being unwrapped to the original.
+ functools_mod = sys.modules["functools"]
+ del sys.modules["functools"]
+
+ self.assertEqual(
+ get_annotations(wrapped, eval_str=True),
+ {"b": str, "return": MyClass},
+ )
+ self.assertEqual(
+ get_annotations(wrapped, eval_str=False),
+ {"b": "str", "return": "MyClass"},
+ )
+
+ sys.modules["functools"] = functools_mod
+
def test_stringized_annotations_on_class(self):
isa = inspect_stringized_annotations
# test that local namespace lookups work
@@ -823,6 +864,16 @@ def test_stringized_annotations_on_class(self):
{"x": int},
)
+ def test_stringized_annotations_on_custom_object(self):
+ class HasAnnotations:
+ @property
+ def __annotations__(self):
+ return {"x": "int"}
+
+ ha = HasAnnotations()
+ self.assertEqual(get_annotations(ha), {"x": "int"})
+ self.assertEqual(get_annotations(ha, eval_str=True), {"x": int})
+
def test_stringized_annotation_permutations(self):
def define_class(name, has_future, has_annos, base_text, extra_names=None):
lines = []
@@ -990,6 +1041,23 @@ def __annotate__(self):
{"x": "int"},
)
+ def test_non_dict_annotate(self):
+ class WeirdAnnotate:
+ def __annotate__(self, *args, **kwargs):
+ return "not a dict"
+
+ wa = WeirdAnnotate()
+ for format in Format:
+ if format == Format.VALUE_WITH_FAKE_GLOBALS:
+ continue
+ with (
+ self.subTest(format=format),
+ self.assertRaisesRegex(
+ ValueError, r".*__annotate__ returned a non-dict"
+ ),
+ ):
+ get_annotations(wa, format=format)
+
def test_no_annotations(self):
class CustomClass:
pass
From 68266c1f01e5791558cb088dfb0e26ecd577295e Mon Sep 17 00:00:00 2001
From: Victor Stinner
Date: Mon, 10 Nov 2025 15:50:51 +0100
Subject: [PATCH 105/417] gh-141341: Rename COMPILER macro to _Py_COMPILER on
Windows (#141342)
---
...-11-10-11-26-26.gh-issue-141341.OsO6-y.rst | 2 ++
PC/pyconfig.h | 28 +++++++++----------
Python/getcompiler.c | 4 +++
3 files changed, 20 insertions(+), 14 deletions(-)
create mode 100644 Misc/NEWS.d/next/C_API/2025-11-10-11-26-26.gh-issue-141341.OsO6-y.rst
diff --git a/Misc/NEWS.d/next/C_API/2025-11-10-11-26-26.gh-issue-141341.OsO6-y.rst b/Misc/NEWS.d/next/C_API/2025-11-10-11-26-26.gh-issue-141341.OsO6-y.rst
new file mode 100644
index 00000000000..460923b4d62
--- /dev/null
+++ b/Misc/NEWS.d/next/C_API/2025-11-10-11-26-26.gh-issue-141341.OsO6-y.rst
@@ -0,0 +1,2 @@
+On Windows, rename the ``COMPILER`` macro to ``_Py_COMPILER`` to avoid name
+conflicts. Patch by Victor Stinner.
diff --git a/PC/pyconfig.h b/PC/pyconfig.h
index 0e8379387cd..a126fca6f5a 100644
--- a/PC/pyconfig.h
+++ b/PC/pyconfig.h
@@ -118,7 +118,7 @@ WIN32 is still required for the locale module.
/* Microsoft C defines _MSC_VER, as does clang-cl.exe */
#ifdef _MSC_VER
-/* We want COMPILER to expand to a string containing _MSC_VER's *value*.
+/* We want _Py_COMPILER to expand to a string containing _MSC_VER's *value*.
* This is horridly tricky, because the stringization operator only works
* on macro arguments, and doesn't evaluate macros passed *as* arguments.
*/
@@ -148,7 +148,7 @@ WIN32 is still required for the locale module.
#define MS_WIN64
#endif
-/* set the COMPILER and support tier
+/* set the _Py_COMPILER and support tier
*
* win_amd64 MSVC (x86_64-pc-windows-msvc): 1
* win32 MSVC (i686-pc-windows-msvc): 1
@@ -158,22 +158,22 @@ WIN32 is still required for the locale module.
#ifdef MS_WIN64
#if defined(_M_X64) || defined(_M_AMD64)
#if defined(__clang__)
-#define COMPILER ("[Clang " __clang_version__ "] 64 bit (AMD64) with MSC v." _Py_STRINGIZE(_MSC_VER) " CRT]")
+#define _Py_COMPILER ("[Clang " __clang_version__ "] 64 bit (AMD64) with MSC v." _Py_STRINGIZE(_MSC_VER) " CRT]")
#define PY_SUPPORT_TIER 0
#elif defined(__INTEL_COMPILER)
-#define COMPILER ("[ICC v." _Py_STRINGIZE(__INTEL_COMPILER) " 64 bit (amd64) with MSC v." _Py_STRINGIZE(_MSC_VER) " CRT]")
+#define _Py_COMPILER ("[ICC v." _Py_STRINGIZE(__INTEL_COMPILER) " 64 bit (amd64) with MSC v." _Py_STRINGIZE(_MSC_VER) " CRT]")
#define PY_SUPPORT_TIER 0
#else
-#define COMPILER _Py_PASTE_VERSION("64 bit (AMD64)")
+#define _Py_COMPILER _Py_PASTE_VERSION("64 bit (AMD64)")
#define PY_SUPPORT_TIER 1
#endif /* __clang__ */
#define PYD_PLATFORM_TAG "win_amd64"
#elif defined(_M_ARM64)
-#define COMPILER _Py_PASTE_VERSION("64 bit (ARM64)")
+#define _Py_COMPILER _Py_PASTE_VERSION("64 bit (ARM64)")
#define PY_SUPPORT_TIER 3
#define PYD_PLATFORM_TAG "win_arm64"
#else
-#define COMPILER _Py_PASTE_VERSION("64 bit (Unknown)")
+#define _Py_COMPILER _Py_PASTE_VERSION("64 bit (Unknown)")
#define PY_SUPPORT_TIER 0
#endif
#endif /* MS_WIN64 */
@@ -220,22 +220,22 @@ typedef _W64 int Py_ssize_t;
#if defined(MS_WIN32) && !defined(MS_WIN64)
#if defined(_M_IX86)
#if defined(__clang__)
-#define COMPILER ("[Clang " __clang_version__ "] 32 bit (Intel) with MSC v." _Py_STRINGIZE(_MSC_VER) " CRT]")
+#define _Py_COMPILER ("[Clang " __clang_version__ "] 32 bit (Intel) with MSC v." _Py_STRINGIZE(_MSC_VER) " CRT]")
#define PY_SUPPORT_TIER 0
#elif defined(__INTEL_COMPILER)
-#define COMPILER ("[ICC v." _Py_STRINGIZE(__INTEL_COMPILER) " 32 bit (Intel) with MSC v." _Py_STRINGIZE(_MSC_VER) " CRT]")
+#define _Py_COMPILER ("[ICC v." _Py_STRINGIZE(__INTEL_COMPILER) " 32 bit (Intel) with MSC v." _Py_STRINGIZE(_MSC_VER) " CRT]")
#define PY_SUPPORT_TIER 0
#else
-#define COMPILER _Py_PASTE_VERSION("32 bit (Intel)")
+#define _Py_COMPILER _Py_PASTE_VERSION("32 bit (Intel)")
#define PY_SUPPORT_TIER 1
#endif /* __clang__ */
#define PYD_PLATFORM_TAG "win32"
#elif defined(_M_ARM)
-#define COMPILER _Py_PASTE_VERSION("32 bit (ARM)")
+#define _Py_COMPILER _Py_PASTE_VERSION("32 bit (ARM)")
#define PYD_PLATFORM_TAG "win_arm32"
#define PY_SUPPORT_TIER 0
#else
-#define COMPILER _Py_PASTE_VERSION("32 bit (Unknown)")
+#define _Py_COMPILER _Py_PASTE_VERSION("32 bit (Unknown)")
#define PY_SUPPORT_TIER 0
#endif
#endif /* MS_WIN32 && !MS_WIN64 */
@@ -273,7 +273,7 @@ typedef int pid_t;
#warning "Please use an up-to-date version of gcc! (>2.91 recommended)"
#endif
-#define COMPILER "[gcc]"
+#define _Py_COMPILER "[gcc]"
#define PY_LONG_LONG long long
#define PY_LLONG_MIN LLONG_MIN
#define PY_LLONG_MAX LLONG_MAX
@@ -286,7 +286,7 @@ typedef int pid_t;
/* XXX These defines are likely incomplete, but should be easy to fix.
They should be complete enough to build extension modules. */
-#define COMPILER "[lcc-win32]"
+#define _Py_COMPILER "[lcc-win32]"
typedef int pid_t;
/* __declspec() is supported here too - do nothing to get the defaults */
diff --git a/Python/getcompiler.c b/Python/getcompiler.c
index a5d26239e87..cc56ad8c895 100644
--- a/Python/getcompiler.c
+++ b/Python/getcompiler.c
@@ -3,6 +3,10 @@
#include "Python.h"
+#ifdef _Py_COMPILER
+# define COMPILER _Py_COMPILER
+#endif
+
#ifndef COMPILER
// Note the __clang__ conditional has to come before the __GNUC__ one because
From 19b573025e0aa569e7a34081116280133e33979a Mon Sep 17 00:00:00 2001
From: dr-carlos <77367421+dr-carlos@users.noreply.github.com>
Date: Tue, 11 Nov 2025 01:23:40 +1030
Subject: [PATCH 106/417] gh-141174: Improve `ForwardRef` test coverage
(#141175)
* Test unsupported format in ForwardRef.evaluate()
* Test dict cell closure with multiple variables
* Test all options in ForwardRef repr
* Test ForwardRef being a final class
---
Lib/test/test_annotationlib.py | 50 ++++++++++++++++++++++++++++++++++
1 file changed, 50 insertions(+)
diff --git a/Lib/test/test_annotationlib.py b/Lib/test/test_annotationlib.py
index f1d32ab50cf..d196801eede 100644
--- a/Lib/test/test_annotationlib.py
+++ b/Lib/test/test_annotationlib.py
@@ -74,6 +74,30 @@ def inner(arg: x):
anno = get_annotations(inner, format=Format.FORWARDREF)
self.assertEqual(anno["arg"], x)
+ def test_multiple_closure(self):
+ def inner(arg: x[y]):
+ pass
+
+ fwdref = get_annotations(inner, format=Format.FORWARDREF)["arg"]
+ self.assertIsInstance(fwdref, ForwardRef)
+ self.assertEqual(fwdref.__forward_arg__, "x[y]")
+ with self.assertRaises(NameError):
+ fwdref.evaluate()
+
+ y = str
+ fwdref = get_annotations(inner, format=Format.FORWARDREF)["arg"]
+ self.assertIsInstance(fwdref, ForwardRef)
+ extra_name, extra_val = next(iter(fwdref.__extra_names__.items()))
+ self.assertEqual(fwdref.__forward_arg__.replace(extra_name, extra_val.__name__), "x[str]")
+ with self.assertRaises(NameError):
+ fwdref.evaluate()
+
+ x = list
+ self.assertEqual(fwdref.evaluate(), x[y])
+
+ fwdref = get_annotations(inner, format=Format.FORWARDREF)["arg"]
+ self.assertEqual(fwdref, x[y])
+
def test_function(self):
def f(x: int, y: doesntexist):
pass
@@ -1756,6 +1780,14 @@ def test_forward_repr(self):
repr(List[ForwardRef("int", module="mod")]),
"typing.List[ForwardRef('int', module='mod')]",
)
+ self.assertEqual(
+ repr(List[ForwardRef("int", module="mod", is_class=True)]),
+ "typing.List[ForwardRef('int', module='mod', is_class=True)]",
+ )
+ self.assertEqual(
+ repr(List[ForwardRef("int", owner="class")]),
+ "typing.List[ForwardRef('int', owner='class')]",
+ )
def test_forward_recursion_actually(self):
def namespace1():
@@ -1861,6 +1893,19 @@ def test_evaluate_forwardref_format(self):
support.EqualToForwardRef('"a" + 1'),
)
+ def test_evaluate_notimplemented_format(self):
+ class C:
+ x: alias
+
+ fwdref = get_annotations(C, format=Format.FORWARDREF)["x"]
+
+ with self.assertRaises(NotImplementedError):
+ fwdref.evaluate(format=Format.VALUE_WITH_FAKE_GLOBALS)
+
+ with self.assertRaises(NotImplementedError):
+ # Some other unsupported value
+ fwdref.evaluate(format=7)
+
def test_evaluate_with_type_params(self):
class Gen[T]:
alias = int
@@ -1994,6 +2039,11 @@ def test_fwdref_invalid_syntax(self):
with self.assertRaises(SyntaxError):
fr.evaluate()
+ def test_fwdref_final_class(self):
+ with self.assertRaises(TypeError):
+ class C(ForwardRef):
+ pass
+
class TestAnnotationLib(unittest.TestCase):
def test__all__(self):
From 1110e8f6a4a767f6d09b121017442528733b380b Mon Sep 17 00:00:00 2001
From: dr-carlos <77367421+dr-carlos@users.noreply.github.com>
Date: Tue, 11 Nov 2025 01:24:50 +1030
Subject: [PATCH 107/417] gh-141174: Improve
`annotationlib.call_annotate_function()` test coverage (#141176)
* Test passing unsupported Format values to call_annotate_function()
* Test call_evaluate_function with fake globals that raise errors
* Fix typo and comparison in test_fake_global_evaluation
---
Lib/test/test_annotationlib.py | 43 ++++++++++++++++++++++++++++++++++
1 file changed, 43 insertions(+)
diff --git a/Lib/test/test_annotationlib.py b/Lib/test/test_annotationlib.py
index d196801eede..0ae598b6839 100644
--- a/Lib/test/test_annotationlib.py
+++ b/Lib/test/test_annotationlib.py
@@ -1339,6 +1339,32 @@ def evaluate(format, exc=NotImplementedError):
"undefined",
)
+ def test_fake_global_evaluation(self):
+ # This will raise an AttributeError
+ def evaluate_union(format, exc=NotImplementedError):
+ if format == Format.VALUE_WITH_FAKE_GLOBALS:
+ # Return a ForwardRef
+ return builtins.undefined | list[int]
+ raise exc
+
+ self.assertEqual(
+ annotationlib.call_evaluate_function(evaluate_union, Format.FORWARDREF),
+ support.EqualToForwardRef("builtins.undefined | list[int]"),
+ )
+
+ # This will raise an AttributeError
+ def evaluate_intermediate(format, exc=NotImplementedError):
+ if format == Format.VALUE_WITH_FAKE_GLOBALS:
+ intermediate = builtins.undefined
+ # Return a literal
+ return intermediate is None
+ raise exc
+
+ self.assertIs(
+ annotationlib.call_evaluate_function(evaluate_intermediate, Format.FORWARDREF),
+ False,
+ )
+
class TestCallAnnotateFunction(unittest.TestCase):
# Tests for user defined annotate functions.
@@ -1480,6 +1506,23 @@ def annotate(format, /):
with self.assertRaises(NotImplementedError):
annotationlib.call_annotate_function(annotate, Format.STRING)
+ def test_unsupported_formats(self):
+ def annotate(format, /):
+ if format == Format.FORWARDREF:
+ return {"x": str}
+ else:
+ raise NotImplementedError(format)
+
+ with self.assertRaises(ValueError):
+ annotationlib.call_annotate_function(annotate, Format.VALUE_WITH_FAKE_GLOBALS)
+
+ with self.assertRaises(RuntimeError):
+ annotationlib.call_annotate_function(annotate, Format.VALUE)
+
+ with self.assertRaises(ValueError):
+ # Some non-Format value
+ annotationlib.call_annotate_function(annotate, 7)
+
def test_error_from_value_raised(self):
# Test that the error from format.VALUE is raised
# if all formats fail
From 59b793b0dd76d37229fe6d379cd5fe76023d15f1 Mon Sep 17 00:00:00 2001
From: Yongzi Li <204532581+Yzi-Li@users.noreply.github.com>
Date: Mon, 10 Nov 2025 22:55:15 +0800
Subject: [PATCH 108/417] gh-141343: Fix swapped words in `sorted` doc
(GH-141348)
---
Doc/library/functions.rst | 2 +-
1 file changed, 1 insertion(+), 1 deletion(-)
diff --git a/Doc/library/functions.rst b/Doc/library/functions.rst
index 61799e303a1..e9879397555 100644
--- a/Doc/library/functions.rst
+++ b/Doc/library/functions.rst
@@ -1859,7 +1859,7 @@ are always available. They are listed here in alphabetical order.
the same data with other ordering tools such as :func:`max` that rely
on a different underlying method. Implementing all six comparisons
also helps avoid confusion for mixed type comparisons which can call
- reflected the :meth:`~object.__gt__` method.
+ the reflected :meth:`~object.__gt__` method.
For sorting examples and a brief sorting tutorial, see :ref:`sortinghowto`.
From 55ea13231313a2133e6f5a6112409d349081f273 Mon Sep 17 00:00:00 2001
From: dr-carlos <77367421+dr-carlos@users.noreply.github.com>
Date: Tue, 11 Nov 2025 01:26:45 +1030
Subject: [PATCH 109/417] gh-141174: Improve `annotationlib._Stringifier` test
coverage (#141220)
* Test `_Stringifier.__convert_to_ast()` for containers
* Test partial evaluation of `ForwardRef`s in `_Stringifier`
---
Lib/test/test_annotationlib.py | 67 ++++++++++++++++++++++++++++++++++
1 file changed, 67 insertions(+)
diff --git a/Lib/test/test_annotationlib.py b/Lib/test/test_annotationlib.py
index 0ae598b6839..9f3275d5071 100644
--- a/Lib/test/test_annotationlib.py
+++ b/Lib/test/test_annotationlib.py
@@ -119,6 +119,10 @@ def f(
alpha: some | obj,
beta: +some,
gamma: some < obj,
+ delta: some | {obj: module},
+ epsilon: some | {obj, module},
+ zeta: some | [obj],
+ eta: some | (),
):
pass
@@ -147,6 +151,69 @@ def f(
self.assertIsInstance(gamma_anno, ForwardRef)
self.assertEqual(gamma_anno, support.EqualToForwardRef("some < obj", owner=f))
+ delta_anno = anno["delta"]
+ self.assertIsInstance(delta_anno, ForwardRef)
+ self.assertEqual(delta_anno, support.EqualToForwardRef("some | {obj: module}", owner=f))
+
+ epsilon_anno = anno["epsilon"]
+ self.assertIsInstance(epsilon_anno, ForwardRef)
+ self.assertEqual(epsilon_anno, support.EqualToForwardRef("some | {obj, module}", owner=f))
+
+ zeta_anno = anno["zeta"]
+ self.assertIsInstance(zeta_anno, ForwardRef)
+ self.assertEqual(zeta_anno, support.EqualToForwardRef("some | [obj]", owner=f))
+
+ eta_anno = anno["eta"]
+ self.assertIsInstance(eta_anno, ForwardRef)
+ self.assertEqual(eta_anno, support.EqualToForwardRef("some | ()", owner=f))
+
+ def test_partially_nonexistent(self):
+ # These annotations start with a non-existent variable and then use
+ # global types with defined values. This partially evaluates by putting
+ # those globals into `fwdref.__extra_names__`.
+ def f(
+ x: obj | int,
+ y: container[int:obj, int],
+ z: dict_val | {str: int},
+ alpha: set_val | {str, int},
+ beta: obj | bool | int,
+ gamma: obj | call_func(int, kwd=bool),
+ ):
+ pass
+
+ def func(*args, **kwargs):
+ return Union[*args, *(kwargs.values())]
+
+ anno = get_annotations(f, format=Format.FORWARDREF)
+ globals_ = {
+ "obj": str, "container": list, "dict_val": {1: 2}, "set_val": {1, 2},
+ "call_func": func
+ }
+
+ x_anno = anno["x"]
+ self.assertIsInstance(x_anno, ForwardRef)
+ self.assertEqual(x_anno.evaluate(globals=globals_), str | int)
+
+ y_anno = anno["y"]
+ self.assertIsInstance(y_anno, ForwardRef)
+ self.assertEqual(y_anno.evaluate(globals=globals_), list[int:str, int])
+
+ z_anno = anno["z"]
+ self.assertIsInstance(z_anno, ForwardRef)
+ self.assertEqual(z_anno.evaluate(globals=globals_), {1: 2} | {str: int})
+
+ alpha_anno = anno["alpha"]
+ self.assertIsInstance(alpha_anno, ForwardRef)
+ self.assertEqual(alpha_anno.evaluate(globals=globals_), {1, 2} | {str, int})
+
+ beta_anno = anno["beta"]
+ self.assertIsInstance(beta_anno, ForwardRef)
+ self.assertEqual(beta_anno.evaluate(globals=globals_), str | bool | int)
+
+ gamma_anno = anno["gamma"]
+ self.assertIsInstance(gamma_anno, ForwardRef)
+ self.assertEqual(gamma_anno.evaluate(globals=globals_), str | func(int, kwd=bool))
+
def test_partially_nonexistent_union(self):
# Test unions with '|' syntax equal unions with typing.Union[] with some forwardrefs
class UnionForwardrefs:
From 88953d5debf08dfaa1cdb314d62262f770addf5b Mon Sep 17 00:00:00 2001
From: Sergey B Kirpichev
Date: Mon, 10 Nov 2025 18:36:01 +0300
Subject: [PATCH 110/417] gh-141004: Deprecate Py_MATH_El and Py_MATH_PIl
macros (#141035)
Co-authored-by: Victor Stinner
---
Doc/c-api/float.rst | 14 ++++++++++++++
Doc/deprecations/c-api-pending-removal-in-3.20.rst | 2 ++
Doc/whatsnew/3.15.rst | 4 ++++
Include/pymath.h | 2 ++
.../2025-11-05-05-45-49.gh-issue-141004.N9Ooh9.rst | 1 +
5 files changed, 23 insertions(+)
create mode 100644 Misc/NEWS.d/next/C_API/2025-11-05-05-45-49.gh-issue-141004.N9Ooh9.rst
diff --git a/Doc/c-api/float.rst b/Doc/c-api/float.rst
index edee498a0b8..9e703a46445 100644
--- a/Doc/c-api/float.rst
+++ b/Doc/c-api/float.rst
@@ -78,6 +78,20 @@ Floating-Point Objects
Return the minimum normalized positive float *DBL_MIN* as C :c:expr:`double`.
+.. c:macro:: Py_MATH_El
+
+ High precision (long double) definition of :data:`~math.e` constant.
+
+ .. deprecated-removed:: 3.15 3.20
+
+
+.. c:macro:: Py_MATH_PIl
+
+ High precision (long double) definition of :data:`~math.pi` constant.
+
+ .. deprecated-removed:: 3.15 3.20
+
+
.. c:macro:: Py_RETURN_NAN
Return :data:`math.nan` from a function.
diff --git a/Doc/deprecations/c-api-pending-removal-in-3.20.rst b/Doc/deprecations/c-api-pending-removal-in-3.20.rst
index 82f975d6ed4..18623b19a2a 100644
--- a/Doc/deprecations/c-api-pending-removal-in-3.20.rst
+++ b/Doc/deprecations/c-api-pending-removal-in-3.20.rst
@@ -5,3 +5,5 @@ Pending removal in Python 3.20
Use :c:func:`PyComplex_AsCComplex` and :c:func:`PyComplex_FromCComplex`
to convert a Python complex number to/from the C :c:type:`Py_complex`
representation.
+
+* Macros :c:macro:`!Py_MATH_PIl` and :c:macro:`!Py_MATH_El`.
diff --git a/Doc/whatsnew/3.15.rst b/Doc/whatsnew/3.15.rst
index e0b0471567c..1ba394a1967 100644
--- a/Doc/whatsnew/3.15.rst
+++ b/Doc/whatsnew/3.15.rst
@@ -1076,6 +1076,10 @@ Deprecated C APIs
since 3.15 and will be removed in 3.17.
(Contributed by Nikita Sobolev in :gh:`136355`.)
+* :c:macro:`!Py_MATH_El` and :c:macro:`!Py_MATH_PIl` are deprecated
+ since 3.15 and will be removed in 3.20.
+ (Contributed by Sergey B Kirpichev in :gh:`141004`.)
+
.. Add C API deprecations above alphabetically, not here at the end.
diff --git a/Include/pymath.h b/Include/pymath.h
index e2919c7b527..0f9f0f3b299 100644
--- a/Include/pymath.h
+++ b/Include/pymath.h
@@ -7,6 +7,7 @@
/* High precision definition of pi and e (Euler)
* The values are taken from libc6's math.h.
*/
+// Deprecated since Python 3.15.
#ifndef Py_MATH_PIl
#define Py_MATH_PIl 3.1415926535897932384626433832795029L
#endif
@@ -14,6 +15,7 @@
#define Py_MATH_PI 3.14159265358979323846
#endif
+// Deprecated since Python 3.15.
#ifndef Py_MATH_El
#define Py_MATH_El 2.7182818284590452353602874713526625L
#endif
diff --git a/Misc/NEWS.d/next/C_API/2025-11-05-05-45-49.gh-issue-141004.N9Ooh9.rst b/Misc/NEWS.d/next/C_API/2025-11-05-05-45-49.gh-issue-141004.N9Ooh9.rst
new file mode 100644
index 00000000000..5f3ccd62016
--- /dev/null
+++ b/Misc/NEWS.d/next/C_API/2025-11-05-05-45-49.gh-issue-141004.N9Ooh9.rst
@@ -0,0 +1 @@
+:c:macro:`!Py_MATH_El` and :c:macro:`!Py_MATH_PIl` are deprecated.
From f835552946e29ec20144c359b8822f9e421d4d64 Mon Sep 17 00:00:00 2001
From: Sergey Miryanov
Date: Mon, 10 Nov 2025 21:19:13 +0500
Subject: [PATCH 111/417] GH-141212: Fix possible memory leak in
gc_mark_span_push (gh-141213)
---
Python/gc_free_threading.c | 5 +++--
1 file changed, 3 insertions(+), 2 deletions(-)
diff --git a/Python/gc_free_threading.c b/Python/gc_free_threading.c
index f39793c3eeb..b183062eff7 100644
--- a/Python/gc_free_threading.c
+++ b/Python/gc_free_threading.c
@@ -675,10 +675,11 @@ gc_mark_span_push(gc_span_stack_t *ss, PyObject **start, PyObject **end)
else {
ss->capacity *= 2;
}
- ss->stack = (gc_span_t *)PyMem_Realloc(ss->stack, ss->capacity * sizeof(gc_span_t));
- if (ss->stack == NULL) {
+ gc_span_t *new_stack = (gc_span_t *)PyMem_Realloc(ss->stack, ss->capacity * sizeof(gc_span_t));
+ if (new_stack == NULL) {
return -1;
}
+ ss->stack = new_stack;
}
assert(end > start);
ss->stack[ss->size].start = start;
From ed0a5fd8cacb1964111d03ff37627f6bea5e6026 Mon Sep 17 00:00:00 2001
From: Stan Ulbrych <89152624+StanFromIreland@users.noreply.github.com>
Date: Mon, 10 Nov 2025 17:46:41 +0000
Subject: [PATCH 112/417] gh-141004: Document `PyType_FastSubclass` (GH-141313)
Co-authored-by: Peter Bierma
---
Doc/c-api/type.rst | 12 ++++++++++++
Doc/c-api/typeobj.rst | 4 ++--
2 files changed, 14 insertions(+), 2 deletions(-)
diff --git a/Doc/c-api/type.rst b/Doc/c-api/type.rst
index 5bdbff4e0ad..479ede70b01 100644
--- a/Doc/c-api/type.rst
+++ b/Doc/c-api/type.rst
@@ -133,6 +133,18 @@ Type Objects
Type features are denoted by single bit flags.
+.. c:function:: int PyType_FastSubclass(PyTypeObject *type, int flag)
+
+ Return non-zero if the type object *type* sets the subclass flag *flag*.
+ Subclass flags are denoted by
+ :c:macro:`Py_TPFLAGS_*_SUBCLASS `.
+ This function is used by many ``_Check`` functions for common types.
+
+ .. seealso::
+ :c:func:`PyObject_TypeCheck`, which is used as a slower alternative in
+ ``_Check`` functions for types that don't come with subclass flags.
+
+
.. c:function:: int PyType_IS_GC(PyTypeObject *o)
Return true if the type object includes support for the cycle detector; this
diff --git a/Doc/c-api/typeobj.rst b/Doc/c-api/typeobj.rst
index 9d23aea5734..34d19acdf17 100644
--- a/Doc/c-api/typeobj.rst
+++ b/Doc/c-api/typeobj.rst
@@ -1351,8 +1351,8 @@ and :c:data:`PyType_Type` effectively act as defaults.)
.. c:macro:: Py_TPFLAGS_BASE_EXC_SUBCLASS
.. c:macro:: Py_TPFLAGS_TYPE_SUBCLASS
- These flags are used by functions such as
- :c:func:`PyLong_Check` to quickly determine if a type is a subclass
+ Functions such as :c:func:`PyLong_Check` will call :c:func:`PyType_FastSubclass`
+ with one of these flags to quickly determine if a type is a subclass
of a built-in type; such specific checks are faster than a generic
check, like :c:func:`PyObject_IsInstance`. Custom types that inherit
from built-ins should have their :c:member:`~PyTypeObject.tp_flags`
From 86513f6c2ebdd1fb692c39b84786ea41d88c84fd Mon Sep 17 00:00:00 2001
From: Peter Bierma
Date: Mon, 10 Nov 2025 16:35:47 -0500
Subject: [PATCH 113/417] gh-141004: Document missing frame APIs (GH-141189)
Co-authored-by: Stan Ulbrych <89152624+StanFromIreland@users.noreply.github.com>
---
Doc/c-api/frame.rst | 57 +++++++++++++++++++++++++++++++++++++++++++++
1 file changed, 57 insertions(+)
diff --git a/Doc/c-api/frame.rst b/Doc/c-api/frame.rst
index 1a52e146a69..fb17cf7f1da 100644
--- a/Doc/c-api/frame.rst
+++ b/Doc/c-api/frame.rst
@@ -29,6 +29,12 @@ See also :ref:`Reflection `.
Previously, this type was only available after including
````.
+.. c:function:: PyFrameObject *PyFrame_New(PyThreadState *tstate, PyCodeObject *code, PyObject *globals, PyObject *locals)
+
+ Create a new frame object. This function returns a :term:`strong reference`
+ to the new frame object on success, and returns ``NULL`` with an exception
+ set on failure.
+
.. c:function:: int PyFrame_Check(PyObject *obj)
Return non-zero if *obj* is a frame object.
@@ -161,6 +167,57 @@ See :pep:`667` for more information.
Return non-zero if *obj* is a frame :func:`locals` proxy.
+
+Legacy Local Variable APIs
+^^^^^^^^^^^^^^^^^^^^^^^^^^
+
+These APIs are :term:`soft deprecated`. As of Python 3.13, they do nothing.
+They exist solely for backwards compatibility.
+
+
+.. c:function:: void PyFrame_LocalsToFast(PyFrameObject *f, int clear)
+
+ This function is :term:`soft deprecated` and does nothing.
+
+ Prior to Python 3.13, this function would copy the :attr:`~frame.f_locals`
+ attribute of *f* to the internal "fast" array of local variables, allowing
+ changes in frame objects to be visible to the interpreter. If *clear* was
+ true, this function would process variables that were unset in the locals
+ dictionary.
+
+ .. versionchanged:: 3.13
+ This function now does nothing.
+
+
+.. c:function:: void PyFrame_FastToLocals(PyFrameObject *f)
+
+ This function is :term:`soft deprecated` and does nothing.
+
+ Prior to Python 3.13, this function would copy the internal "fast" array
+ of local variables (which is used by the interpreter) to the
+ :attr:`~frame.f_locals` attribute of *f*, allowing changes in local
+ variables to be visible to frame objects.
+
+ .. versionchanged:: 3.13
+ This function now does nothing.
+
+
+.. c:function:: int PyFrame_FastToLocalsWithError(PyFrameObject *f)
+
+ This function is :term:`soft deprecated` and does nothing.
+
+ Prior to Python 3.13, this function was similar to
+ :c:func:`PyFrame_FastToLocals`, but would return ``0`` on success, and
+ ``-1`` with an exception set on failure.
+
+ .. versionchanged:: 3.13
+ This function now does nothing.
+
+
+.. seealso::
+ :pep:`667`
+
+
Internal Frames
^^^^^^^^^^^^^^^
From 46b58e1bb9e1e17d855588935f5a259be960a3a1 Mon Sep 17 00:00:00 2001
From: Louis
Date: Tue, 11 Nov 2025 05:50:30 +0100
Subject: [PATCH 114/417] gh-140578: Doc: Remove sencence implying that
concurrent.futures.ThreadPoolExecutor does not exist (#140689)
* Doc: Remove sencence implying that concurrent.futures.ThreadPoolExecutor does not exist
Closes #140578
* Add NEWS.d entry for gh-140578
---------
Co-authored-by: Louis Paternault
---
Doc/library/multiprocessing.rst | 7 +++++--
.../2025-10-27-23-06-01.gh-issue-140578.FMBdEn.rst | 3 +++
2 files changed, 8 insertions(+), 2 deletions(-)
create mode 100644 Misc/NEWS.d/next/Documentation/2025-10-27-23-06-01.gh-issue-140578.FMBdEn.rst
diff --git a/Doc/library/multiprocessing.rst b/Doc/library/multiprocessing.rst
index d18ada3511d..714207cb0ae 100644
--- a/Doc/library/multiprocessing.rst
+++ b/Doc/library/multiprocessing.rst
@@ -22,8 +22,7 @@ to this, the :mod:`multiprocessing` module allows the programmer to fully
leverage multiple processors on a given machine. It runs on both POSIX and
Windows.
-The :mod:`multiprocessing` module also introduces APIs which do not have
-analogs in the :mod:`threading` module. A prime example of this is the
+The :mod:`multiprocessing` module also introduces the
:class:`~multiprocessing.pool.Pool` object which offers a convenient means of
parallelizing the execution of a function across multiple input values,
distributing the input data across processes (data parallelism). The following
@@ -44,6 +43,10 @@ will print to standard output ::
[1, 4, 9]
+The :mod:`multiprocessing` module also introduces APIs which do not have
+analogs in the :mod:`threading` module, like the ability to :meth:`terminate
+`, :meth:`interrupt ` or :meth:`kill
+` a running process.
.. seealso::
diff --git a/Misc/NEWS.d/next/Documentation/2025-10-27-23-06-01.gh-issue-140578.FMBdEn.rst b/Misc/NEWS.d/next/Documentation/2025-10-27-23-06-01.gh-issue-140578.FMBdEn.rst
new file mode 100644
index 00000000000..702d38d4d24
--- /dev/null
+++ b/Misc/NEWS.d/next/Documentation/2025-10-27-23-06-01.gh-issue-140578.FMBdEn.rst
@@ -0,0 +1,3 @@
+Remove outdated sencence in the documentation for :mod:`multiprocessing`,
+that implied that :class:`concurrent.futures.ThreadPoolExecutor` did not
+exist.
From 9cb8c52d5e9a83efe4fa3878db06befd9df52f54 Mon Sep 17 00:00:00 2001
From: Victor Stinner
Date: Tue, 11 Nov 2025 05:59:16 +0100
Subject: [PATCH 115/417] gh-140485: Catch ChildProcessError in multiprocessing
resource tracker (GH-141132)
---
Lib/multiprocessing/resource_tracker.py | 7 ++++++-
1 file changed, 6 insertions(+), 1 deletion(-)
diff --git a/Lib/multiprocessing/resource_tracker.py b/Lib/multiprocessing/resource_tracker.py
index c53092f6e34..38fcaed48fa 100644
--- a/Lib/multiprocessing/resource_tracker.py
+++ b/Lib/multiprocessing/resource_tracker.py
@@ -111,7 +111,12 @@ def _stop_locked(
close(self._fd)
self._fd = None
- _, status = waitpid(self._pid, 0)
+ try:
+ _, status = waitpid(self._pid, 0)
+ except ChildProcessError:
+ self._pid = None
+ self._exitcode = None
+ return
self._pid = None
From 92741c59f89e114474bdb2cb539107ef6bae0b9c Mon Sep 17 00:00:00 2001
From: Krishna Chaitanya <141550576+XChaitanyaX@users.noreply.github.com>
Date: Tue, 11 Nov 2025 11:32:46 +0530
Subject: [PATCH 116/417] gh-140379: add hyperlinks to list and set (GH-140399)
add hyperlinks to list and set
---
Doc/tutorial/datastructures.rst | 14 +++++++-------
1 file changed, 7 insertions(+), 7 deletions(-)
diff --git a/Doc/tutorial/datastructures.rst b/Doc/tutorial/datastructures.rst
index 1332c53f396..7e02e74177c 100644
--- a/Doc/tutorial/datastructures.rst
+++ b/Doc/tutorial/datastructures.rst
@@ -12,9 +12,8 @@ and adds some new things as well.
More on Lists
=============
-The list data type has some more methods. Here are all of the methods of list
-objects:
-
+The :ref:`list ` data type has some more methods. Here are all
+of the methods of list objects:
.. method:: list.append(x)
:noindex:
@@ -445,10 +444,11 @@ packing and sequence unpacking.
Sets
====
-Python also includes a data type for *sets*. A set is an unordered collection
-with no duplicate elements. Basic uses include membership testing and
-eliminating duplicate entries. Set objects also support mathematical operations
-like union, intersection, difference, and symmetric difference.
+Python also includes a data type for :ref:`sets `. A set is
+an unordered collection with no duplicate elements. Basic uses include
+membership testing and eliminating duplicate entries. Set objects also
+support mathematical operations like union, intersection, difference, and
+symmetric difference.
Curly braces or the :func:`set` function can be used to create sets. Note: to
create an empty set you have to use ``set()``, not ``{}``; the latter creates an
From 8435a2278f964f48d36edbc5092be5ebecfcb120 Mon Sep 17 00:00:00 2001
From: Victor Stinner
Date: Tue, 11 Nov 2025 09:21:24 +0100
Subject: [PATCH 117/417] gh-141376: Fix exported symbols (GH-141377)
* gh-141376: Fix exported symbols
* _io module: add "_Py_" prefix to "spec" variables. For example,
rename bufferedrandom_spec to _Py_bufferedrandom_spec.
* typevarobject.c: add "static" to "spec" and "slots" variables.
* import.c: add "static" to "pkgcontext" variable.
* No longer export textiowrapper_slots
---
Modules/_io/_iomodule.c | 30 +++++++++++++++---------------
Modules/_io/_iomodule.h | 30 +++++++++++++++---------------
Modules/_io/bufferedio.c | 10 +++++-----
Modules/_io/bytesio.c | 4 ++--
Modules/_io/fileio.c | 2 +-
Modules/_io/iobase.c | 4 ++--
Modules/_io/stringio.c | 2 +-
Modules/_io/textio.c | 8 ++++----
Modules/_io/winconsoleio.c | 2 +-
Objects/typevarobject.c | 16 ++++++++--------
Python/import.c | 2 +-
11 files changed, 55 insertions(+), 55 deletions(-)
diff --git a/Modules/_io/_iomodule.c b/Modules/_io/_iomodule.c
index 27483494559..433d68d515c 100644
--- a/Modules/_io/_iomodule.c
+++ b/Modules/_io/_iomodule.c
@@ -681,40 +681,40 @@ iomodule_exec(PyObject *m)
}
// Base classes
- ADD_TYPE(m, state->PyIncrementalNewlineDecoder_Type, &nldecoder_spec, NULL);
- ADD_TYPE(m, state->PyBytesIOBuffer_Type, &bytesiobuf_spec, NULL);
- ADD_TYPE(m, state->PyIOBase_Type, &iobase_spec, NULL);
+ ADD_TYPE(m, state->PyIncrementalNewlineDecoder_Type, &_Py_nldecoder_spec, NULL);
+ ADD_TYPE(m, state->PyBytesIOBuffer_Type, &_Py_bytesiobuf_spec, NULL);
+ ADD_TYPE(m, state->PyIOBase_Type, &_Py_iobase_spec, NULL);
// PyIOBase_Type subclasses
- ADD_TYPE(m, state->PyTextIOBase_Type, &textiobase_spec,
+ ADD_TYPE(m, state->PyTextIOBase_Type, &_Py_textiobase_spec,
state->PyIOBase_Type);
- ADD_TYPE(m, state->PyBufferedIOBase_Type, &bufferediobase_spec,
+ ADD_TYPE(m, state->PyBufferedIOBase_Type, &_Py_bufferediobase_spec,
state->PyIOBase_Type);
- ADD_TYPE(m, state->PyRawIOBase_Type, &rawiobase_spec,
+ ADD_TYPE(m, state->PyRawIOBase_Type, &_Py_rawiobase_spec,
state->PyIOBase_Type);
// PyBufferedIOBase_Type(PyIOBase_Type) subclasses
- ADD_TYPE(m, state->PyBytesIO_Type, &bytesio_spec, state->PyBufferedIOBase_Type);
- ADD_TYPE(m, state->PyBufferedWriter_Type, &bufferedwriter_spec,
+ ADD_TYPE(m, state->PyBytesIO_Type, &_Py_bytesio_spec, state->PyBufferedIOBase_Type);
+ ADD_TYPE(m, state->PyBufferedWriter_Type, &_Py_bufferedwriter_spec,
state->PyBufferedIOBase_Type);
- ADD_TYPE(m, state->PyBufferedReader_Type, &bufferedreader_spec,
+ ADD_TYPE(m, state->PyBufferedReader_Type, &_Py_bufferedreader_spec,
state->PyBufferedIOBase_Type);
- ADD_TYPE(m, state->PyBufferedRWPair_Type, &bufferedrwpair_spec,
+ ADD_TYPE(m, state->PyBufferedRWPair_Type, &_Py_bufferedrwpair_spec,
state->PyBufferedIOBase_Type);
- ADD_TYPE(m, state->PyBufferedRandom_Type, &bufferedrandom_spec,
+ ADD_TYPE(m, state->PyBufferedRandom_Type, &_Py_bufferedrandom_spec,
state->PyBufferedIOBase_Type);
// PyRawIOBase_Type(PyIOBase_Type) subclasses
- ADD_TYPE(m, state->PyFileIO_Type, &fileio_spec, state->PyRawIOBase_Type);
+ ADD_TYPE(m, state->PyFileIO_Type, &_Py_fileio_spec, state->PyRawIOBase_Type);
#ifdef HAVE_WINDOWS_CONSOLE_IO
- ADD_TYPE(m, state->PyWindowsConsoleIO_Type, &winconsoleio_spec,
+ ADD_TYPE(m, state->PyWindowsConsoleIO_Type, &_Py_winconsoleio_spec,
state->PyRawIOBase_Type);
#endif
// PyTextIOBase_Type(PyIOBase_Type) subclasses
- ADD_TYPE(m, state->PyStringIO_Type, &stringio_spec, state->PyTextIOBase_Type);
- ADD_TYPE(m, state->PyTextIOWrapper_Type, &textiowrapper_spec,
+ ADD_TYPE(m, state->PyStringIO_Type, &_Py_stringio_spec, state->PyTextIOBase_Type);
+ ADD_TYPE(m, state->PyTextIOWrapper_Type, &_Py_textiowrapper_spec,
state->PyTextIOBase_Type);
#undef ADD_TYPE
diff --git a/Modules/_io/_iomodule.h b/Modules/_io/_iomodule.h
index 18cf20edf26..4ae487c8e2a 100644
--- a/Modules/_io/_iomodule.h
+++ b/Modules/_io/_iomodule.h
@@ -9,23 +9,23 @@
#include "structmember.h"
/* Type specs */
-extern PyType_Spec bufferediobase_spec;
-extern PyType_Spec bufferedrandom_spec;
-extern PyType_Spec bufferedreader_spec;
-extern PyType_Spec bufferedrwpair_spec;
-extern PyType_Spec bufferedwriter_spec;
-extern PyType_Spec bytesio_spec;
-extern PyType_Spec bytesiobuf_spec;
-extern PyType_Spec fileio_spec;
-extern PyType_Spec iobase_spec;
-extern PyType_Spec nldecoder_spec;
-extern PyType_Spec rawiobase_spec;
-extern PyType_Spec stringio_spec;
-extern PyType_Spec textiobase_spec;
-extern PyType_Spec textiowrapper_spec;
+extern PyType_Spec _Py_bufferediobase_spec;
+extern PyType_Spec _Py_bufferedrandom_spec;
+extern PyType_Spec _Py_bufferedreader_spec;
+extern PyType_Spec _Py_bufferedrwpair_spec;
+extern PyType_Spec _Py_bufferedwriter_spec;
+extern PyType_Spec _Py_bytesio_spec;
+extern PyType_Spec _Py_bytesiobuf_spec;
+extern PyType_Spec _Py_fileio_spec;
+extern PyType_Spec _Py_iobase_spec;
+extern PyType_Spec _Py_nldecoder_spec;
+extern PyType_Spec _Py_rawiobase_spec;
+extern PyType_Spec _Py_stringio_spec;
+extern PyType_Spec _Py_textiobase_spec;
+extern PyType_Spec _Py_textiowrapper_spec;
#ifdef HAVE_WINDOWS_CONSOLE_IO
-extern PyType_Spec winconsoleio_spec;
+extern PyType_Spec _Py_winconsoleio_spec;
#endif
/* These functions are used as METH_NOARGS methods, are normally called
diff --git a/Modules/_io/bufferedio.c b/Modules/_io/bufferedio.c
index 0b4bc4c6b8a..4602f2b42a6 100644
--- a/Modules/_io/bufferedio.c
+++ b/Modules/_io/bufferedio.c
@@ -2537,7 +2537,7 @@ static PyType_Slot bufferediobase_slots[] = {
};
/* Do not set Py_TPFLAGS_HAVE_GC so that tp_traverse and tp_clear are inherited */
-PyType_Spec bufferediobase_spec = {
+PyType_Spec _Py_bufferediobase_spec = {
.name = "_io._BufferedIOBase",
.flags = (Py_TPFLAGS_DEFAULT | Py_TPFLAGS_BASETYPE |
Py_TPFLAGS_IMMUTABLETYPE),
@@ -2600,7 +2600,7 @@ static PyType_Slot bufferedreader_slots[] = {
{0, NULL},
};
-PyType_Spec bufferedreader_spec = {
+PyType_Spec _Py_bufferedreader_spec = {
.name = "_io.BufferedReader",
.basicsize = sizeof(buffered),
.flags = (Py_TPFLAGS_DEFAULT | Py_TPFLAGS_BASETYPE | Py_TPFLAGS_HAVE_GC |
@@ -2658,7 +2658,7 @@ static PyType_Slot bufferedwriter_slots[] = {
{0, NULL},
};
-PyType_Spec bufferedwriter_spec = {
+PyType_Spec _Py_bufferedwriter_spec = {
.name = "_io.BufferedWriter",
.basicsize = sizeof(buffered),
.flags = (Py_TPFLAGS_DEFAULT | Py_TPFLAGS_BASETYPE | Py_TPFLAGS_HAVE_GC |
@@ -2708,7 +2708,7 @@ static PyType_Slot bufferedrwpair_slots[] = {
{0, NULL},
};
-PyType_Spec bufferedrwpair_spec = {
+PyType_Spec _Py_bufferedrwpair_spec = {
.name = "_io.BufferedRWPair",
.basicsize = sizeof(rwpair),
.flags = (Py_TPFLAGS_DEFAULT | Py_TPFLAGS_BASETYPE | Py_TPFLAGS_HAVE_GC |
@@ -2776,7 +2776,7 @@ static PyType_Slot bufferedrandom_slots[] = {
{0, NULL},
};
-PyType_Spec bufferedrandom_spec = {
+PyType_Spec _Py_bufferedrandom_spec = {
.name = "_io.BufferedRandom",
.basicsize = sizeof(buffered),
.flags = (Py_TPFLAGS_DEFAULT | Py_TPFLAGS_BASETYPE | Py_TPFLAGS_HAVE_GC |
diff --git a/Modules/_io/bytesio.c b/Modules/_io/bytesio.c
index 30d61f9d68e..d6bfb93177c 100644
--- a/Modules/_io/bytesio.c
+++ b/Modules/_io/bytesio.c
@@ -1156,7 +1156,7 @@ static PyType_Slot bytesio_slots[] = {
{0, NULL},
};
-PyType_Spec bytesio_spec = {
+PyType_Spec _Py_bytesio_spec = {
.name = "_io.BytesIO",
.basicsize = sizeof(bytesio),
.flags = (Py_TPFLAGS_DEFAULT | Py_TPFLAGS_BASETYPE | Py_TPFLAGS_HAVE_GC |
@@ -1246,7 +1246,7 @@ static PyType_Slot bytesiobuf_slots[] = {
{0, NULL},
};
-PyType_Spec bytesiobuf_spec = {
+PyType_Spec _Py_bytesiobuf_spec = {
.name = "_io._BytesIOBuffer",
.basicsize = sizeof(bytesiobuf),
.flags = (Py_TPFLAGS_DEFAULT | Py_TPFLAGS_HAVE_GC |
diff --git a/Modules/_io/fileio.c b/Modules/_io/fileio.c
index b84c1bd3e22..2544ff4ea91 100644
--- a/Modules/_io/fileio.c
+++ b/Modules/_io/fileio.c
@@ -1329,7 +1329,7 @@ static PyType_Slot fileio_slots[] = {
{0, NULL},
};
-PyType_Spec fileio_spec = {
+PyType_Spec _Py_fileio_spec = {
.name = "_io.FileIO",
.basicsize = sizeof(fileio),
.flags = (Py_TPFLAGS_DEFAULT | Py_TPFLAGS_BASETYPE | Py_TPFLAGS_HAVE_GC |
diff --git a/Modules/_io/iobase.c b/Modules/_io/iobase.c
index e304fc8bee2..f1c2fe17801 100644
--- a/Modules/_io/iobase.c
+++ b/Modules/_io/iobase.c
@@ -885,7 +885,7 @@ static PyType_Slot iobase_slots[] = {
{0, NULL},
};
-PyType_Spec iobase_spec = {
+PyType_Spec _Py_iobase_spec = {
.name = "_io._IOBase",
.basicsize = sizeof(iobase),
.flags = (Py_TPFLAGS_DEFAULT | Py_TPFLAGS_BASETYPE | Py_TPFLAGS_HAVE_GC |
@@ -1046,7 +1046,7 @@ static PyType_Slot rawiobase_slots[] = {
};
/* Do not set Py_TPFLAGS_HAVE_GC so that tp_traverse and tp_clear are inherited */
-PyType_Spec rawiobase_spec = {
+PyType_Spec _Py_rawiobase_spec = {
.name = "_io._RawIOBase",
.flags = (Py_TPFLAGS_DEFAULT | Py_TPFLAGS_BASETYPE |
Py_TPFLAGS_IMMUTABLETYPE),
diff --git a/Modules/_io/stringio.c b/Modules/_io/stringio.c
index 20b7cfc0088..781ca4327f9 100644
--- a/Modules/_io/stringio.c
+++ b/Modules/_io/stringio.c
@@ -1094,7 +1094,7 @@ static PyType_Slot stringio_slots[] = {
{0, NULL},
};
-PyType_Spec stringio_spec = {
+PyType_Spec _Py_stringio_spec = {
.name = "_io.StringIO",
.basicsize = sizeof(stringio),
.flags = (Py_TPFLAGS_DEFAULT | Py_TPFLAGS_BASETYPE | Py_TPFLAGS_HAVE_GC |
diff --git a/Modules/_io/textio.c b/Modules/_io/textio.c
index c462bd2ac57..84b7d9df400 100644
--- a/Modules/_io/textio.c
+++ b/Modules/_io/textio.c
@@ -208,7 +208,7 @@ static PyType_Slot textiobase_slots[] = {
};
/* Do not set Py_TPFLAGS_HAVE_GC so that tp_traverse and tp_clear are inherited */
-PyType_Spec textiobase_spec = {
+PyType_Spec _Py_textiobase_spec = {
.name = "_io._TextIOBase",
.flags = (Py_TPFLAGS_DEFAULT | Py_TPFLAGS_BASETYPE |
Py_TPFLAGS_IMMUTABLETYPE),
@@ -3352,7 +3352,7 @@ static PyType_Slot nldecoder_slots[] = {
{0, NULL},
};
-PyType_Spec nldecoder_spec = {
+PyType_Spec _Py_nldecoder_spec = {
.name = "_io.IncrementalNewlineDecoder",
.basicsize = sizeof(nldecoder_object),
.flags = (Py_TPFLAGS_DEFAULT | Py_TPFLAGS_BASETYPE | Py_TPFLAGS_HAVE_GC |
@@ -3404,7 +3404,7 @@ static PyGetSetDef textiowrapper_getset[] = {
{NULL}
};
-PyType_Slot textiowrapper_slots[] = {
+static PyType_Slot textiowrapper_slots[] = {
{Py_tp_dealloc, textiowrapper_dealloc},
{Py_tp_repr, textiowrapper_repr},
{Py_tp_doc, (void *)_io_TextIOWrapper___init____doc__},
@@ -3418,7 +3418,7 @@ PyType_Slot textiowrapper_slots[] = {
{0, NULL},
};
-PyType_Spec textiowrapper_spec = {
+PyType_Spec _Py_textiowrapper_spec = {
.name = "_io.TextIOWrapper",
.basicsize = sizeof(textio),
.flags = (Py_TPFLAGS_DEFAULT | Py_TPFLAGS_BASETYPE | Py_TPFLAGS_HAVE_GC |
diff --git a/Modules/_io/winconsoleio.c b/Modules/_io/winconsoleio.c
index 950b7fe241c..677d7e85d4e 100644
--- a/Modules/_io/winconsoleio.c
+++ b/Modules/_io/winconsoleio.c
@@ -1253,7 +1253,7 @@ static PyType_Slot winconsoleio_slots[] = {
{0, NULL},
};
-PyType_Spec winconsoleio_spec = {
+PyType_Spec _Py_winconsoleio_spec = {
.name = "_io._WindowsConsoleIO",
.basicsize = sizeof(winconsoleio),
.flags = (Py_TPFLAGS_DEFAULT | Py_TPFLAGS_BASETYPE | Py_TPFLAGS_HAVE_GC |
diff --git a/Objects/typevarobject.c b/Objects/typevarobject.c
index 75a69d4bc3e..8e43962c7e3 100644
--- a/Objects/typevarobject.c
+++ b/Objects/typevarobject.c
@@ -251,7 +251,7 @@ static PyType_Slot constevaluator_slots[] = {
{0, NULL},
};
-PyType_Spec constevaluator_spec = {
+static PyType_Spec constevaluator_spec = {
.name = "_typing._ConstEvaluator",
.basicsize = sizeof(constevaluatorobject),
.flags = Py_TPFLAGS_DEFAULT | Py_TPFLAGS_HAVE_GC | Py_TPFLAGS_IMMUTABLETYPE
@@ -930,7 +930,7 @@ static PyType_Slot typevar_slots[] = {
{0, NULL},
};
-PyType_Spec typevar_spec = {
+static PyType_Spec typevar_spec = {
.name = "typing.TypeVar",
.basicsize = sizeof(typevarobject),
.flags = Py_TPFLAGS_DEFAULT | Py_TPFLAGS_HAVE_GC | Py_TPFLAGS_IMMUTABLETYPE
@@ -1078,7 +1078,7 @@ static PyType_Slot paramspecargs_slots[] = {
{0, NULL},
};
-PyType_Spec paramspecargs_spec = {
+static PyType_Spec paramspecargs_spec = {
.name = "typing.ParamSpecArgs",
.basicsize = sizeof(paramspecattrobject),
.flags = Py_TPFLAGS_DEFAULT | Py_TPFLAGS_HAVE_GC | Py_TPFLAGS_IMMUTABLETYPE
@@ -1158,7 +1158,7 @@ static PyType_Slot paramspeckwargs_slots[] = {
{0, NULL},
};
-PyType_Spec paramspeckwargs_spec = {
+static PyType_Spec paramspeckwargs_spec = {
.name = "typing.ParamSpecKwargs",
.basicsize = sizeof(paramspecattrobject),
.flags = Py_TPFLAGS_DEFAULT | Py_TPFLAGS_HAVE_GC | Py_TPFLAGS_IMMUTABLETYPE
@@ -1509,7 +1509,7 @@ static PyType_Slot paramspec_slots[] = {
{0, 0},
};
-PyType_Spec paramspec_spec = {
+static PyType_Spec paramspec_spec = {
.name = "typing.ParamSpec",
.basicsize = sizeof(paramspecobject),
.flags = Py_TPFLAGS_DEFAULT | Py_TPFLAGS_HAVE_GC | Py_TPFLAGS_IMMUTABLETYPE
@@ -1789,7 +1789,7 @@ Note that only TypeVarTuples defined in the global scope can be\n\
pickled.\n\
");
-PyType_Slot typevartuple_slots[] = {
+static PyType_Slot typevartuple_slots[] = {
{Py_tp_doc, (void *)typevartuple_doc},
{Py_tp_members, typevartuple_members},
{Py_tp_methods, typevartuple_methods},
@@ -1805,7 +1805,7 @@ PyType_Slot typevartuple_slots[] = {
{0, 0},
};
-PyType_Spec typevartuple_spec = {
+static PyType_Spec typevartuple_spec = {
.name = "typing.TypeVarTuple",
.basicsize = sizeof(typevartupleobject),
.flags = Py_TPFLAGS_DEFAULT | Py_TPFLAGS_IMMUTABLETYPE | Py_TPFLAGS_MANAGED_DICT
@@ -2347,7 +2347,7 @@ static PyType_Slot generic_slots[] = {
{0, NULL},
};
-PyType_Spec generic_spec = {
+static PyType_Spec generic_spec = {
.name = "typing.Generic",
.basicsize = sizeof(PyObject),
.flags = Py_TPFLAGS_DEFAULT | Py_TPFLAGS_BASETYPE | Py_TPFLAGS_HAVE_GC,
diff --git a/Python/import.c b/Python/import.c
index 6cf4a061ca6..2afa7c15e6a 100644
--- a/Python/import.c
+++ b/Python/import.c
@@ -804,7 +804,7 @@ _PyImport_ClearModulesByIndex(PyInterpreterState *interp)
substitute this (if the name actually matches).
*/
-_Py_thread_local const char *pkgcontext = NULL;
+static _Py_thread_local const char *pkgcontext = NULL;
# undef PKGCONTEXT
# define PKGCONTEXT pkgcontext
From d69447445cbacf7537bf59c5c683a3b17060312d Mon Sep 17 00:00:00 2001
From: Sergey B Kirpichev
Date: Tue, 11 Nov 2025 13:13:59 +0300
Subject: [PATCH 118/417] gh-141004: document Py_INFINITY and Py_NAN macros
(#141145)
Co-authored-by: Stan Ulbrych <89152624+StanFromIreland@users.noreply.github.com>
---
Doc/c-api/float.rst | 18 ++++++++++++++++++
1 file changed, 18 insertions(+)
diff --git a/Doc/c-api/float.rst b/Doc/c-api/float.rst
index 9e703a46445..eae4792af7d 100644
--- a/Doc/c-api/float.rst
+++ b/Doc/c-api/float.rst
@@ -78,6 +78,24 @@ Floating-Point Objects
Return the minimum normalized positive float *DBL_MIN* as C :c:expr:`double`.
+.. c:macro:: Py_INFINITY
+
+ This macro expands a to constant expression of type :c:expr:`double`, that
+ represents the positive infinity.
+
+ On most platforms, this is equivalent to the :c:macro:`!INFINITY` macro from
+ the C11 standard ```` header.
+
+
+.. c:macro:: Py_NAN
+
+ This macro expands a to constant expression of type :c:expr:`double`, that
+ represents a quiet not-a-number (qNaN) value.
+
+ On most platforms, this is equivalent to the :c:macro:`!NAN` macro from
+ the C11 standard ```` header.
+
+
.. c:macro:: Py_MATH_El
High precision (long double) definition of :data:`~math.e` constant.
From 799326b0a93ae6375f153d5a6607e7dc5e0690b2 Mon Sep 17 00:00:00 2001
From: Petr Viktorin
Date: Tue, 11 Nov 2025 13:52:13 +0100
Subject: [PATCH 119/417] gh-141169: Re-raise exception from findfuncptr
(GH-141349)
---
Include/internal/pycore_importdl.h | 32 ++++++++++++++++++++++++++----
Python/importdl.c | 24 +++-------------------
2 files changed, 31 insertions(+), 25 deletions(-)
diff --git a/Include/internal/pycore_importdl.h b/Include/internal/pycore_importdl.h
index 12a32a5f70e..f60c5510d20 100644
--- a/Include/internal/pycore_importdl.h
+++ b/Include/internal/pycore_importdl.h
@@ -14,6 +14,34 @@ extern "C" {
extern const char *_PyImport_DynLoadFiletab[];
+#ifdef HAVE_DYNAMIC_LOADING
+/* ./configure sets HAVE_DYNAMIC_LOADING if dynamic loading of modules is
+ supported on this platform. configure will then compile and link in one
+ of the dynload_*.c files, as appropriate. We will call a function in
+ those modules to get a function pointer to the module's init function.
+
+ The function should return:
+ - The function pointer on success
+ - NULL with exception set if the library cannot be loaded
+ - NULL *without* an extension set if the library could be loaded but the
+ function cannot be found in it.
+*/
+#ifdef MS_WINDOWS
+#include
+typedef FARPROC dl_funcptr;
+extern dl_funcptr _PyImport_FindSharedFuncptrWindows(const char *prefix,
+ const char *shortname,
+ PyObject *pathname,
+ FILE *fp);
+#else
+typedef void (*dl_funcptr)(void);
+extern dl_funcptr _PyImport_FindSharedFuncptr(const char *prefix,
+ const char *shortname,
+ const char *pathname, FILE *fp);
+#endif
+
+#endif /* HAVE_DYNAMIC_LOADING */
+
typedef enum ext_module_kind {
_Py_ext_module_kind_UNKNOWN = 0,
@@ -112,8 +140,6 @@ extern int _PyImport_RunModInitFunc(
#define MAXSUFFIXSIZE 12
#ifdef MS_WINDOWS
-#include
-typedef FARPROC dl_funcptr;
#ifdef Py_DEBUG
# define PYD_DEBUG_SUFFIX "_d"
@@ -136,8 +162,6 @@ typedef FARPROC dl_funcptr;
#define PYD_TAGGED_SUFFIX PYD_DEBUG_SUFFIX "." PYD_SOABI ".pyd"
#define PYD_UNTAGGED_SUFFIX PYD_DEBUG_SUFFIX ".pyd"
-#else
-typedef void (*dl_funcptr)(void);
#endif
diff --git a/Python/importdl.c b/Python/importdl.c
index 23a55c39677..61a9cdaf375 100644
--- a/Python/importdl.c
+++ b/Python/importdl.c
@@ -10,27 +10,6 @@
#include "pycore_runtime.h" // _Py_ID()
-/* ./configure sets HAVE_DYNAMIC_LOADING if dynamic loading of modules is
- supported on this platform. configure will then compile and link in one
- of the dynload_*.c files, as appropriate. We will call a function in
- those modules to get a function pointer to the module's init function.
-*/
-#ifdef HAVE_DYNAMIC_LOADING
-
-#ifdef MS_WINDOWS
-extern dl_funcptr _PyImport_FindSharedFuncptrWindows(const char *prefix,
- const char *shortname,
- PyObject *pathname,
- FILE *fp);
-#else
-extern dl_funcptr _PyImport_FindSharedFuncptr(const char *prefix,
- const char *shortname,
- const char *pathname, FILE *fp);
-#endif
-
-#endif /* HAVE_DYNAMIC_LOADING */
-
-
/***********************************/
/* module info to use when loading */
/***********************************/
@@ -414,6 +393,9 @@ _PyImport_GetModuleExportHooks(
*modexport = (PyModExportFunction)exportfunc;
return 2;
}
+ if (PyErr_Occurred()) {
+ return -1;
+ }
exportfunc = findfuncptr(
info->hook_prefixes->init_prefix,
From 7211a34fe1d9704935342af8c9b46725629f2d97 Mon Sep 17 00:00:00 2001
From: Kumar Aditya
Date: Tue, 11 Nov 2025 20:02:32 +0530
Subject: [PATCH 120/417] gh-132657: optimize `PySet_Contains` for `frozenset`
(#141183)
---
Objects/setobject.c | 4 +++-
1 file changed, 3 insertions(+), 1 deletion(-)
diff --git a/Objects/setobject.c b/Objects/setobject.c
index 213bd821d8a..2401176576e 100644
--- a/Objects/setobject.c
+++ b/Objects/setobject.c
@@ -2747,7 +2747,9 @@ PySet_Contains(PyObject *anyset, PyObject *key)
PyErr_BadInternalCall();
return -1;
}
-
+ if (PyFrozenSet_CheckExact(anyset)) {
+ return set_contains_key((PySetObject *)anyset, key);
+ }
int rv;
Py_BEGIN_CRITICAL_SECTION(anyset);
rv = set_contains_key((PySetObject *)anyset, key);
From d890aba748e5213585f9f906888999227dc3fa9c Mon Sep 17 00:00:00 2001
From: John Franey <1728528+johnfraney@users.noreply.github.com>
Date: Tue, 11 Nov 2025 10:33:56 -0400
Subject: [PATCH 121/417] gh-140942: Add MIME type for .cjs extension (#140937)
Co-authored-by: Hugo van Kemenade <1324225+hugovk@users.noreply.github.com>
---
Doc/whatsnew/3.15.rst | 1 +
Lib/mimetypes.py | 1 +
.../2025-11-04-12-18-06.gh-issue-140942.GYns6n.rst | 2 ++
3 files changed, 4 insertions(+)
create mode 100644 Misc/NEWS.d/next/Core_and_Builtins/2025-11-04-12-18-06.gh-issue-140942.GYns6n.rst
diff --git a/Doc/whatsnew/3.15.rst b/Doc/whatsnew/3.15.rst
index 1ba394a1967..ef18d36e4d4 100644
--- a/Doc/whatsnew/3.15.rst
+++ b/Doc/whatsnew/3.15.rst
@@ -451,6 +451,7 @@ math
mimetypes
---------
+* Add ``application/node`` MIME type for ``.cjs`` extension. (Contributed by John Franey in :gh:`140937`.)
* Add ``application/toml``. (Contributed by Gil Forcada in :gh:`139959`.)
* Rename ``application/x-texinfo`` to ``application/texinfo``.
(Contributed by Charlie Lin in :gh:`140165`)
diff --git a/Lib/mimetypes.py b/Lib/mimetypes.py
index 48a9f430d45..d6896fc4042 100644
--- a/Lib/mimetypes.py
+++ b/Lib/mimetypes.py
@@ -486,6 +486,7 @@ def _default_mime_types():
'.wiz' : 'application/msword',
'.nq' : 'application/n-quads',
'.nt' : 'application/n-triples',
+ '.cjs' : 'application/node',
'.bin' : 'application/octet-stream',
'.a' : 'application/octet-stream',
'.dll' : 'application/octet-stream',
diff --git a/Misc/NEWS.d/next/Core_and_Builtins/2025-11-04-12-18-06.gh-issue-140942.GYns6n.rst b/Misc/NEWS.d/next/Core_and_Builtins/2025-11-04-12-18-06.gh-issue-140942.GYns6n.rst
new file mode 100644
index 00000000000..20cfeca1e71
--- /dev/null
+++ b/Misc/NEWS.d/next/Core_and_Builtins/2025-11-04-12-18-06.gh-issue-140942.GYns6n.rst
@@ -0,0 +1,2 @@
+Add ``.cjs`` to :mod:`mimetypes` to give CommonJS modules a MIME type of
+``application/node``.
From 759a048d4bea522fda2fe929be0fba1650c62b0e Mon Sep 17 00:00:00 2001
From: Peter Bierma
Date: Tue, 11 Nov 2025 12:22:16 -0500
Subject: [PATCH 122/417] gh-141004: Document `PyType_Unwatch` (GH-141414)
---
Doc/c-api/type.rst | 14 ++++++++++++++
1 file changed, 14 insertions(+)
diff --git a/Doc/c-api/type.rst b/Doc/c-api/type.rst
index 479ede70b01..29ffeb7c483 100644
--- a/Doc/c-api/type.rst
+++ b/Doc/c-api/type.rst
@@ -116,6 +116,20 @@ Type Objects
.. versionadded:: 3.12
+.. c:function:: int PyType_Unwatch(int watcher_id, PyObject *type)
+
+ Mark *type* as not watched. This undoes a previous call to
+ :c:func:`PyType_Watch`. *type* must not be ``NULL``.
+
+ An extension should never call this function with a *watcher_id* that was
+ not returned to it by a previous call to :c:func:`PyType_AddWatcher`.
+
+ On success, this function returns ``0``. On failure, this function returns
+ ``-1`` with an exception set.
+
+ .. versionadded:: 3.12
+
+
.. c:type:: int (*PyType_WatchCallback)(PyObject *type)
Type of a type-watcher callback function.
From 713edbcebfdb5aa83e9bf376ebc40255ccacd235 Mon Sep 17 00:00:00 2001
From: Tan Long
Date: Wed, 12 Nov 2025 03:27:21 +0800
Subject: [PATCH 123/417] gh-141415: Remove unused variables and comment in
`_pyrepl.windows_console.py` (#141416)
---
Lib/_pyrepl/windows_console.py | 13 -------------
1 file changed, 13 deletions(-)
diff --git a/Lib/_pyrepl/windows_console.py b/Lib/_pyrepl/windows_console.py
index c56dcd6d7dd..f9f5988af0b 100644
--- a/Lib/_pyrepl/windows_console.py
+++ b/Lib/_pyrepl/windows_console.py
@@ -249,22 +249,10 @@ def input_hook(self):
def __write_changed_line(
self, y: int, oldline: str, newline: str, px_coord: int
) -> None:
- # this is frustrating; there's no reason to test (say)
- # self.dch1 inside the loop -- but alternative ways of
- # structuring this function are equally painful (I'm trying to
- # avoid writing code generators these days...)
minlen = min(wlen(oldline), wlen(newline))
x_pos = 0
x_coord = 0
- px_pos = 0
- j = 0
- for c in oldline:
- if j >= px_coord:
- break
- j += wlen(c)
- px_pos += 1
-
# reuse the oldline as much as possible, but stop as soon as we
# encounter an ESCAPE, because it might be the start of an escape
# sequence
@@ -358,7 +346,6 @@ def prepare(self) -> None:
self.height, self.width = self.getheightwidth()
self.posxy = 0, 0
- self.__gone_tall = 0
self.__offset = 0
if self.__vt_support:
From 0f09bda643d778fb20fb79fecdfd09f20f9d9717 Mon Sep 17 00:00:00 2001
From: yihong
Date: Wed, 12 Nov 2025 03:27:56 +0800
Subject: [PATCH 124/417] gh-140193: Forward port test_exec_set_nomemory_hang
from 3.13 (GH-140187)
* chore: test_exec_set_nomemory_hang from 3.13
Signed-off-by: yihong0618
* fix: apply comments
Signed-off-by: yihong0618
* Update Lib/test/test_exceptions.py
Co-authored-by: Peter Bierma
* Update Lib/test/test_exceptions.py
Co-authored-by: Peter Bierma
* fix: windows too long name 60 times is enough
Signed-off-by: yihong0618
---------
Signed-off-by: yihong0618
Co-authored-by: Peter Bierma
---
Lib/test/test_exceptions.py | 33 +++++++++++++++++++++++++++++++++
1 file changed, 33 insertions(+)
diff --git a/Lib/test/test_exceptions.py b/Lib/test/test_exceptions.py
index 5262b58908a..6f212d2f91e 100644
--- a/Lib/test/test_exceptions.py
+++ b/Lib/test/test_exceptions.py
@@ -1923,6 +1923,39 @@ def test_keyerror_context(self):
exc2 = None
+ @cpython_only
+ # Python built with Py_TRACE_REFS fail with a fatal error in
+ # _PyRefchain_Trace() on memory allocation error.
+ @unittest.skipIf(support.Py_TRACE_REFS, 'cannot test Py_TRACE_REFS build')
+ def test_exec_set_nomemory_hang(self):
+ import_module("_testcapi")
+ # gh-134163: A MemoryError inside code that was wrapped by a try/except
+ # block would lead to an infinite loop.
+
+ # The frame_lasti needs to be greater than 257 to prevent
+ # PyLong_FromLong() from returning cached integers, which
+ # don't require a memory allocation. Prepend some dummy code
+ # to artificially increase the instruction index.
+ warmup_code = "a = list(range(0, 1))\n" * 60
+ user_input = warmup_code + dedent("""
+ try:
+ import _testcapi
+ _testcapi.set_nomemory(0)
+ b = list(range(1000, 2000))
+ except Exception as e:
+ import traceback
+ traceback.print_exc()
+ """)
+ with SuppressCrashReport():
+ with script_helper.spawn_python('-c', user_input) as p:
+ p.wait()
+ output = p.stdout.read()
+
+ self.assertIn(p.returncode, (0, 1))
+ self.assertGreater(len(output), 0) # At minimum, should not hang
+ self.assertIn(b"MemoryError", output)
+
+
class NameErrorTests(unittest.TestCase):
def test_name_error_has_name(self):
try:
From c903d768322989e9f8ba79e38ee87e14c85c5430 Mon Sep 17 00:00:00 2001
From: Marco Barbosa
Date: Tue, 11 Nov 2025 16:35:55 -0300
Subject: [PATCH 125/417] gh-139533: fix refs to code without proper markups on
turtledemo doc (GH-139534)
gh-139533: fix refs to code without proper markups on turtledemo documentation
---
Doc/library/turtle.rst | 124 ++++++++++++++++++++---------------------
1 file changed, 62 insertions(+), 62 deletions(-)
diff --git a/Doc/library/turtle.rst b/Doc/library/turtle.rst
index 58b99e0d441..95a57c57e71 100644
--- a/Doc/library/turtle.rst
+++ b/Doc/library/turtle.rst
@@ -2801,68 +2801,68 @@ The demo scripts are:
.. tabularcolumns:: |l|L|L|
-+----------------+------------------------------+-----------------------+
-| Name | Description | Features |
-+================+==============================+=======================+
-| bytedesign | complex classical | :func:`tracer`, delay,|
-| | turtle graphics pattern | :func:`update` |
-+----------------+------------------------------+-----------------------+
-| chaos | graphs Verhulst dynamics, | world coordinates |
-| | shows that computer's | |
-| | computations can generate | |
-| | results sometimes against the| |
-| | common sense expectations | |
-+----------------+------------------------------+-----------------------+
-| clock | analog clock showing time | turtles as clock's |
-| | of your computer | hands, ontimer |
-+----------------+------------------------------+-----------------------+
-| colormixer | experiment with r, g, b | :func:`ondrag` |
-+----------------+------------------------------+-----------------------+
-| forest | 3 breadth-first trees | randomization |
-+----------------+------------------------------+-----------------------+
-| fractalcurves | Hilbert & Koch curves | recursion |
-+----------------+------------------------------+-----------------------+
-| lindenmayer | ethnomathematics | L-System |
-| | (indian kolams) | |
-+----------------+------------------------------+-----------------------+
-| minimal_hanoi | Towers of Hanoi | Rectangular Turtles |
-| | | as Hanoi discs |
-| | | (shape, shapesize) |
-+----------------+------------------------------+-----------------------+
-| nim | play the classical nim game | turtles as nimsticks, |
-| | with three heaps of sticks | event driven (mouse, |
-| | against the computer. | keyboard) |
-+----------------+------------------------------+-----------------------+
-| paint | super minimalistic | :func:`onclick` |
-| | drawing program | |
-+----------------+------------------------------+-----------------------+
-| peace | elementary | turtle: appearance |
-| | | and animation |
-+----------------+------------------------------+-----------------------+
-| penrose | aperiodic tiling with | :func:`stamp` |
-| | kites and darts | |
-+----------------+------------------------------+-----------------------+
-| planet_and_moon| simulation of | compound shapes, |
-| | gravitational system | :class:`Vec2D` |
-+----------------+------------------------------+-----------------------+
-| rosette | a pattern from the wikipedia | :func:`clone`, |
-| | article on turtle graphics | :func:`undo` |
-+----------------+------------------------------+-----------------------+
-| round_dance | dancing turtles rotating | compound shapes, clone|
-| | pairwise in opposite | shapesize, tilt, |
-| | direction | get_shapepoly, update |
-+----------------+------------------------------+-----------------------+
-| sorting_animate| visual demonstration of | simple alignment, |
-| | different sorting methods | randomization |
-+----------------+------------------------------+-----------------------+
-| tree | a (graphical) breadth | :func:`clone` |
-| | first tree (using generators)| |
-+----------------+------------------------------+-----------------------+
-| two_canvases | simple design | turtles on two |
-| | | canvases |
-+----------------+------------------------------+-----------------------+
-| yinyang | another elementary example | :func:`circle` |
-+----------------+------------------------------+-----------------------+
++------------------------+------------------------------+--------------------------------------+
+| Name | Description | Features |
++========================+==============================+======================================+
+| ``bytedesign`` | complex classical | :func:`tracer`, :func:`delay`, |
+| | turtle graphics pattern | :func:`update` |
++------------------------+------------------------------+--------------------------------------+
+| ``chaos`` | graphs Verhulst dynamics, | world coordinates |
+| | shows that computer's | |
+| | computations can generate | |
+| | results sometimes against the| |
+| | common sense expectations | |
++------------------------+------------------------------+--------------------------------------+
+| ``clock`` | analog clock showing time | turtles as clock's |
+| | of your computer | hands, :func:`ontimer` |
++------------------------+------------------------------+--------------------------------------+
+| ``colormixer`` | experiment with r, g, b | :func:`ondrag` |
++------------------------+------------------------------+--------------------------------------+
+| ``forest`` | 3 breadth-first trees | randomization |
++------------------------+------------------------------+--------------------------------------+
+| ``fractalcurves`` | Hilbert & Koch curves | recursion |
++------------------------+------------------------------+--------------------------------------+
+| ``lindenmayer`` | ethnomathematics | L-System |
+| | (indian kolams) | |
++------------------------+------------------------------+--------------------------------------+
+| ``minimal_hanoi`` | Towers of Hanoi | Rectangular Turtles |
+| | | as Hanoi discs |
+| | | (:func:`shape`, :func:`shapesize`) |
++------------------------+------------------------------+--------------------------------------+
+| ``nim`` | play the classical nim game | turtles as nimsticks, |
+| | with three heaps of sticks | event driven (mouse, |
+| | against the computer. | keyboard) |
++------------------------+------------------------------+--------------------------------------+
+| ``paint`` | super minimalistic | :func:`onclick` |
+| | drawing program | |
++------------------------+------------------------------+--------------------------------------+
+| ``peace`` | elementary | turtle: appearance |
+| | | and animation |
++------------------------+------------------------------+--------------------------------------+
+| ``penrose`` | aperiodic tiling with | :func:`stamp` |
+| | kites and darts | |
++------------------------+------------------------------+--------------------------------------+
+| ``planet_and_moon`` | simulation of | compound shapes, |
+| | gravitational system | :class:`Vec2D` |
++------------------------+------------------------------+--------------------------------------+
+| ``rosette`` | a pattern from the wikipedia | :func:`clone`, |
+| | article on turtle graphics | :func:`undo` |
++------------------------+------------------------------+--------------------------------------+
+| ``round_dance`` | dancing turtles rotating | compound shapes, :func:`clone` |
+| | pairwise in opposite | :func:`shapesize`, :func:`tilt`, |
+| | direction | :func:`get_shapepoly`, :func:`update`|
++------------------------+------------------------------+--------------------------------------+
+| ``sorting_animate`` | visual demonstration of | simple alignment, |
+| | different sorting methods | randomization |
++------------------------+------------------------------+--------------------------------------+
+| ``tree`` | a (graphical) breadth | :func:`clone` |
+| | first tree (using generators)| |
++------------------------+------------------------------+--------------------------------------+
+| ``two_canvases`` | simple design | turtles on two |
+| | | canvases |
++------------------------+------------------------------+--------------------------------------+
+| ``yinyang`` | another elementary example | :func:`circle` |
++------------------------+------------------------------+--------------------------------------+
Have fun!
From 336154f4b0dbcf1d9dbb461ae962d558ba60f452 Mon Sep 17 00:00:00 2001
From: Steve Dower
Date: Tue, 11 Nov 2025 20:02:49 +0000
Subject: [PATCH 126/417] Add documentation for Python install manager's
install_dir, global_dir and download_dir (GH-140223)
---
Doc/using/windows.rst | 19 +++++++++++++++++++
1 file changed, 19 insertions(+)
diff --git a/Doc/using/windows.rst b/Doc/using/windows.rst
index 0b98cfb8d27..e6619b73bd2 100644
--- a/Doc/using/windows.rst
+++ b/Doc/using/windows.rst
@@ -457,6 +457,25 @@ customization.
- Specify the default format used by the ``py list`` command.
By default, ``table``.
+ * - ``install_dir``
+ - (none)
+ - Specify the root directory that runtimes will be installed into.
+ If you change this setting, previously installed runtimes will not be
+ usable unless you move them to the new location.
+
+ * - ``global_dir``
+ - (none)
+ - Specify the directory where global commands (such as ``python3.14.exe``)
+ are stored.
+ This directory should be added to your :envvar:`PATH` to make the
+ commands available from your terminal.
+
+ * - ``download_dir``
+ - (none)
+ - Specify the directory where downloaded files are stored.
+ This directory is a temporary cache, and can be cleaned up from time to
+ time.
+
Dotted names should be nested inside JSON objects, for example, ``list.format``
would be specified as ``{"list": {"format": "table"}}``.
From 2fb2b82161c6df57c4a247cb743816b79134e932 Mon Sep 17 00:00:00 2001
From: Mikhail Efimov
Date: Tue, 11 Nov 2025 23:16:46 +0300
Subject: [PATCH 127/417] gh-141367: Use actual SPECIALIZATION_THRESHOLD value
in specialization related test (GH-141417)
---
Lib/test/test_list.py | 4 +++-
1 file changed, 3 insertions(+), 1 deletion(-)
diff --git a/Lib/test/test_list.py b/Lib/test/test_list.py
index 223f34fb696..642b54d3484 100644
--- a/Lib/test/test_list.py
+++ b/Lib/test/test_list.py
@@ -349,10 +349,12 @@ def test_deopt_from_append_list(self):
# gh-132011: it used to crash, because
# of `CALL_LIST_APPEND` specialization failure.
code = textwrap.dedent("""
+ import _testinternalcapi
+
l = []
def lappend(l, x, y):
l.append((x, y))
- for x in range(3):
+ for x in range(_testinternalcapi.SPECIALIZATION_THRESHOLD):
lappend(l, None, None)
try:
lappend(list, None, None)
From b5196fa15a6c5aaa90eafff06206f8e44a9da216 Mon Sep 17 00:00:00 2001
From: Aniket <148300120+Aniketsy@users.noreply.github.com>
Date: Wed, 12 Nov 2025 01:55:26 +0530
Subject: [PATCH 128/417] gh-137339: Clarify host and port parameter behavior
in smtplib.SMTP{_SSL} initialization (#137340)
This also documents the previously undocumented default_port parameter.
Co-authored-by: Stan Ulbrych <89152624+StanFromIreland@users.noreply.github.com>
---
Doc/library/smtplib.rst | 34 ++++++++++++++++++++++++++--------
1 file changed, 26 insertions(+), 8 deletions(-)
diff --git a/Doc/library/smtplib.rst b/Doc/library/smtplib.rst
index c5a3de52090..3ee8b82a188 100644
--- a/Doc/library/smtplib.rst
+++ b/Doc/library/smtplib.rst
@@ -24,10 +24,13 @@ Protocol) and :rfc:`1869` (SMTP Service Extensions).
.. class:: SMTP(host='', port=0, local_hostname=None[, timeout], source_address=None)
An :class:`SMTP` instance encapsulates an SMTP connection. It has methods
- that support a full repertoire of SMTP and ESMTP operations. If the optional
- *host* and *port* parameters are given, the SMTP :meth:`connect` method is
- called with those parameters during initialization. If specified,
- *local_hostname* is used as the FQDN of the local host in the HELO/EHLO
+ that support a full repertoire of SMTP and ESMTP operations.
+
+ If the host parameter is set to a truthy value, :meth:`SMTP.connect` is called with
+ host and port automatically when the object is created; otherwise, :meth:`!connect` must
+ be called manually.
+
+ If specified, *local_hostname* is used as the FQDN of the local host in the HELO/EHLO
command. Otherwise, the local hostname is found using
:func:`socket.getfqdn`. If the :meth:`connect` call returns anything other
than a success code, an :exc:`SMTPConnectError` is raised. The optional
@@ -62,6 +65,10 @@ Protocol) and :rfc:`1869` (SMTP Service Extensions).
``smtplib.SMTP.send`` with arguments ``self`` and ``data``,
where ``data`` is the bytes about to be sent to the remote host.
+ .. attribute:: SMTP.default_port
+
+ The default port used for SMTP connections (25).
+
.. versionchanged:: 3.3
Support for the :keyword:`with` statement was added.
@@ -80,15 +87,23 @@ Protocol) and :rfc:`1869` (SMTP Service Extensions).
An :class:`SMTP_SSL` instance behaves exactly the same as instances of
:class:`SMTP`. :class:`SMTP_SSL` should be used for situations where SSL is
- required from the beginning of the connection and using :meth:`~SMTP.starttls`
- is not appropriate. If *host* is not specified, the local host is used. If
- *port* is zero, the standard SMTP-over-SSL port (465) is used. The optional
- arguments *local_hostname*, *timeout* and *source_address* have the same
+ required from the beginning of the connection and using :meth:`SMTP.starttls` is
+ not appropriate.
+
+ If the host parameter is set to a truthy value, :meth:`SMTP.connect` is called with host
+ and port automatically when the object is created; otherwise, :meth:`!SMTP.connect` must
+ be called manually.
+
+ The optional arguments *local_hostname*, *timeout* and *source_address* have the same
meaning as they do in the :class:`SMTP` class. *context*, also optional,
can contain a :class:`~ssl.SSLContext` and allows configuring various
aspects of the secure connection. Please read :ref:`ssl-security` for
best practices.
+ .. attribute:: SMTP_SSL.default_port
+
+ The default port used for SMTP-over-SSL connections (465).
+
.. versionchanged:: 3.3
*context* was added.
@@ -259,6 +274,9 @@ An :class:`SMTP` instance has the following methods:
2-tuple of the response code and message sent by the server in its
connection response.
+ If port is not changed from its default value of 0, the value of the :attr:`default_port`
+ attribute is used.
+
.. audit-event:: smtplib.connect self,host,port smtplib.SMTP.connect
From 298e9074cdffb09d518e6aceea556e8f4a8a745d Mon Sep 17 00:00:00 2001
From: Alper
Date: Tue, 11 Nov 2025 12:27:21 -0800
Subject: [PATCH 129/417] gh-140476: optimize `PySet_Add` for `frozenset` in
free-threading (#140440)
Avoids critical section in `PySet_Add` when adding items to newly created frozensets.
Co-authored-by: Kumar Aditya
---
...-10-22-12-48-05.gh-issue-140476.F3-d1P.rst | 2 ++
Objects/setobject.c | 25 ++++++++++++-------
2 files changed, 18 insertions(+), 9 deletions(-)
create mode 100644 Misc/NEWS.d/next/Core_and_Builtins/2025-10-22-12-48-05.gh-issue-140476.F3-d1P.rst
diff --git a/Misc/NEWS.d/next/Core_and_Builtins/2025-10-22-12-48-05.gh-issue-140476.F3-d1P.rst b/Misc/NEWS.d/next/Core_and_Builtins/2025-10-22-12-48-05.gh-issue-140476.F3-d1P.rst
new file mode 100644
index 00000000000..a24033208c5
--- /dev/null
+++ b/Misc/NEWS.d/next/Core_and_Builtins/2025-10-22-12-48-05.gh-issue-140476.F3-d1P.rst
@@ -0,0 +1,2 @@
+Optimize :c:func:`PySet_Add` for :class:`frozenset` in :term:`free threaded
+` build.
diff --git a/Objects/setobject.c b/Objects/setobject.c
index 2401176576e..85f4d7d4031 100644
--- a/Objects/setobject.c
+++ b/Objects/setobject.c
@@ -2775,17 +2775,24 @@ PySet_Discard(PyObject *set, PyObject *key)
int
PySet_Add(PyObject *anyset, PyObject *key)
{
- if (!PySet_Check(anyset) &&
- (!PyFrozenSet_Check(anyset) || !_PyObject_IsUniquelyReferenced(anyset))) {
- PyErr_BadInternalCall();
- return -1;
+ if (PySet_Check(anyset)) {
+ int rv;
+ Py_BEGIN_CRITICAL_SECTION(anyset);
+ rv = set_add_key((PySetObject *)anyset, key);
+ Py_END_CRITICAL_SECTION();
+ return rv;
}
- int rv;
- Py_BEGIN_CRITICAL_SECTION(anyset);
- rv = set_add_key((PySetObject *)anyset, key);
- Py_END_CRITICAL_SECTION();
- return rv;
+ if (PyFrozenSet_Check(anyset) && _PyObject_IsUniquelyReferenced(anyset)) {
+ // We can only change frozensets if they are uniquely referenced. The
+ // API limits the usage of `PySet_Add` to "fill in the values of brand
+ // new frozensets before they are exposed to other code". In this case,
+ // this can be done without a lock.
+ return set_add_key((PySetObject *)anyset, key);
+ }
+
+ PyErr_BadInternalCall();
+ return -1;
}
int
From 2befce86e699fdbb6610949b029bad56a0d0780f Mon Sep 17 00:00:00 2001
From: Peter Bierma
Date: Tue, 11 Nov 2025 15:31:29 -0500
Subject: [PATCH 130/417] gh-141004: Document `PyFile_OpenCode` and
`PyFile_OpenCodeObject` (GH-141413)
Co-authored-by: Stan Ulbrych <89152624+StanFromIreland@users.noreply.github.com>
---
Doc/c-api/file.rst | 23 +++++++++++++++++++++++
1 file changed, 23 insertions(+)
diff --git a/Doc/c-api/file.rst b/Doc/c-api/file.rst
index e9019a0d500..9d01254ddb2 100644
--- a/Doc/c-api/file.rst
+++ b/Doc/c-api/file.rst
@@ -93,6 +93,29 @@ the :mod:`io` APIs instead.
.. versionadded:: 3.8
+.. c:function:: PyObject *PyFile_OpenCodeObject(PyObject *path)
+
+ Open *path* with the mode ``'rb'``. *path* must be a Python :class:`str`
+ object. The behavior of this function may be overridden by
+ :c:func:`PyFile_SetOpenCodeHook` to allow for some preprocessing of the
+ text.
+
+ This is analogous to :func:`io.open_code` in Python.
+
+ On success, this function returns a :term:`strong reference` to a Python
+ file object. On failure, this function returns ``NULL`` with an exception
+ set.
+
+ .. versionadded:: 3.8
+
+
+.. c:function:: PyObject *PyFile_OpenCode(const char *path)
+
+ Similar to :c:func:`PyFile_OpenCodeObject`, but *path* is a
+ UTF-8 encoded :c:expr:`const char*`.
+
+ .. versionadded:: 3.8
+
.. c:function:: int PyFile_WriteObject(PyObject *obj, PyObject *p, int flags)
From c13b59204af562bfb022eb8f6a5c03eb82659531 Mon Sep 17 00:00:00 2001
From: Alper
Date: Tue, 11 Nov 2025 12:31:55 -0800
Subject: [PATCH 131/417] gh-116738: use `PyMutex` in `lzma` module (#140711)
Co-authored-by: Kumar Aditya
---
Lib/test/test_free_threading/test_lzma.py | 56 +++++++++++++++++++++++
Modules/_lzmamodule.c | 46 ++++++-------------
2 files changed, 69 insertions(+), 33 deletions(-)
create mode 100644 Lib/test/test_free_threading/test_lzma.py
diff --git a/Lib/test/test_free_threading/test_lzma.py b/Lib/test/test_free_threading/test_lzma.py
new file mode 100644
index 00000000000..38d7e5db489
--- /dev/null
+++ b/Lib/test/test_free_threading/test_lzma.py
@@ -0,0 +1,56 @@
+import unittest
+
+from test.support import import_helper, threading_helper
+from test.support.threading_helper import run_concurrently
+
+lzma = import_helper.import_module("lzma")
+from lzma import LZMACompressor, LZMADecompressor
+
+from test.test_lzma import INPUT
+
+
+NTHREADS = 10
+
+
+@threading_helper.requires_working_threading()
+class TestLZMA(unittest.TestCase):
+ def test_compressor(self):
+ lzc = LZMACompressor()
+
+ # First compress() outputs LZMA header
+ header = lzc.compress(INPUT)
+ self.assertGreater(len(header), 0)
+
+ def worker():
+ # it should return empty bytes as it buffers data internally
+ data = lzc.compress(INPUT)
+ self.assertEqual(data, b"")
+
+ run_concurrently(worker_func=worker, nthreads=NTHREADS - 1)
+ full_compressed = header + lzc.flush()
+ decompressed = lzma.decompress(full_compressed)
+ # The decompressed data should be INPUT repeated NTHREADS times
+ self.assertEqual(decompressed, INPUT * NTHREADS)
+
+ def test_decompressor(self):
+ chunk_size = 128
+ chunks = [bytes([ord("a") + i]) * chunk_size for i in range(NTHREADS)]
+ input_data = b"".join(chunks)
+ compressed = lzma.compress(input_data)
+
+ lzd = LZMADecompressor()
+ output = []
+
+ def worker():
+ data = lzd.decompress(compressed, chunk_size)
+ self.assertEqual(len(data), chunk_size)
+ output.append(data)
+
+ run_concurrently(worker_func=worker, nthreads=NTHREADS)
+ self.assertEqual(len(output), NTHREADS)
+ # Verify the expected chunks (order doesn't matter due to append race)
+ self.assertSetEqual(set(output), set(chunks))
+
+
+if __name__ == "__main__":
+ unittest.main()
diff --git a/Modules/_lzmamodule.c b/Modules/_lzmamodule.c
index 6fc072f6d0a..58766233998 100644
--- a/Modules/_lzmamodule.c
+++ b/Modules/_lzmamodule.c
@@ -72,13 +72,6 @@ OutputBuffer_OnError(_BlocksOutputBuffer *buffer)
}
-#define ACQUIRE_LOCK(obj) do { \
- if (!PyThread_acquire_lock((obj)->lock, 0)) { \
- Py_BEGIN_ALLOW_THREADS \
- PyThread_acquire_lock((obj)->lock, 1); \
- Py_END_ALLOW_THREADS \
- } } while (0)
-#define RELEASE_LOCK(obj) PyThread_release_lock((obj)->lock)
typedef struct {
PyTypeObject *lzma_compressor_type;
@@ -111,7 +104,7 @@ typedef struct {
lzma_allocator alloc;
lzma_stream lzs;
int flushed;
- PyThread_type_lock lock;
+ PyMutex mutex;
} Compressor;
typedef struct {
@@ -124,7 +117,7 @@ typedef struct {
char needs_input;
uint8_t *input_buffer;
size_t input_buffer_size;
- PyThread_type_lock lock;
+ PyMutex mutex;
} Decompressor;
#define Compressor_CAST(op) ((Compressor *)(op))
@@ -617,14 +610,14 @@ _lzma_LZMACompressor_compress_impl(Compressor *self, Py_buffer *data)
{
PyObject *result = NULL;
- ACQUIRE_LOCK(self);
+ PyMutex_Lock(&self->mutex);
if (self->flushed) {
PyErr_SetString(PyExc_ValueError, "Compressor has been flushed");
}
else {
result = compress(self, data->buf, data->len, LZMA_RUN);
}
- RELEASE_LOCK(self);
+ PyMutex_Unlock(&self->mutex);
return result;
}
@@ -644,14 +637,14 @@ _lzma_LZMACompressor_flush_impl(Compressor *self)
{
PyObject *result = NULL;
- ACQUIRE_LOCK(self);
+ PyMutex_Lock(&self->mutex);
if (self->flushed) {
PyErr_SetString(PyExc_ValueError, "Repeated call to flush()");
} else {
self->flushed = 1;
result = compress(self, NULL, 0, LZMA_FINISH);
}
- RELEASE_LOCK(self);
+ PyMutex_Unlock(&self->mutex);
return result;
}
@@ -820,12 +813,7 @@ Compressor_new(PyTypeObject *type, PyObject *args, PyObject *kwargs)
self->alloc.free = PyLzma_Free;
self->lzs.allocator = &self->alloc;
- self->lock = PyThread_allocate_lock();
- if (self->lock == NULL) {
- Py_DECREF(self);
- PyErr_SetString(PyExc_MemoryError, "Unable to allocate lock");
- return NULL;
- }
+ self->mutex = (PyMutex){0};
self->flushed = 0;
switch (format) {
@@ -867,10 +855,8 @@ static void
Compressor_dealloc(PyObject *op)
{
Compressor *self = Compressor_CAST(op);
+ assert(!PyMutex_IsLocked(&self->mutex));
lzma_end(&self->lzs);
- if (self->lock != NULL) {
- PyThread_free_lock(self->lock);
- }
PyTypeObject *tp = Py_TYPE(self);
tp->tp_free(self);
Py_DECREF(tp);
@@ -1146,12 +1132,12 @@ _lzma_LZMADecompressor_decompress_impl(Decompressor *self, Py_buffer *data,
{
PyObject *result = NULL;
- ACQUIRE_LOCK(self);
+ PyMutex_Lock(&self->mutex);
if (self->eof)
PyErr_SetString(PyExc_EOFError, "Already at end of stream");
else
result = decompress(self, data->buf, data->len, max_length);
- RELEASE_LOCK(self);
+ PyMutex_Unlock(&self->mutex);
return result;
}
@@ -1244,12 +1230,7 @@ _lzma_LZMADecompressor_impl(PyTypeObject *type, int format,
self->lzs.allocator = &self->alloc;
self->lzs.next_in = NULL;
- self->lock = PyThread_allocate_lock();
- if (self->lock == NULL) {
- Py_DECREF(self);
- PyErr_SetString(PyExc_MemoryError, "Unable to allocate lock");
- return NULL;
- }
+ self->mutex = (PyMutex){0};
self->check = LZMA_CHECK_UNKNOWN;
self->needs_input = 1;
@@ -1304,14 +1285,13 @@ static void
Decompressor_dealloc(PyObject *op)
{
Decompressor *self = Decompressor_CAST(op);
+ assert(!PyMutex_IsLocked(&self->mutex));
+
if(self->input_buffer != NULL)
PyMem_Free(self->input_buffer);
lzma_end(&self->lzs);
Py_CLEAR(self->unused_data);
- if (self->lock != NULL) {
- PyThread_free_lock(self->lock);
- }
PyTypeObject *tp = Py_TYPE(self);
tp->tp_free(self);
Py_DECREF(tp);
From 37e2762ee12c2d7fc465938d7161a9a0640bd71f Mon Sep 17 00:00:00 2001
From: Peter Bierma
Date: Tue, 11 Nov 2025 15:32:54 -0500
Subject: [PATCH 132/417] gh-141004: Document `PyBytes_Repr` and
`PyBytes_DecodeEscape` (GH-141407)
Co-authored-by: Stan Ulbrych <89152624+StanFromIreland@users.noreply.github.com>
---
Doc/c-api/bytes.rst | 36 ++++++++++++++++++++++++++++++++++++
1 file changed, 36 insertions(+)
diff --git a/Doc/c-api/bytes.rst b/Doc/c-api/bytes.rst
index 865a9e5d2bf..82c25573683 100644
--- a/Doc/c-api/bytes.rst
+++ b/Doc/c-api/bytes.rst
@@ -228,6 +228,42 @@ called with a non-bytes parameter.
The function is :term:`soft deprecated`,
use the :c:type:`PyBytesWriter` API instead.
+
+.. c:function:: PyObject *PyBytes_Repr(PyObject *bytes, int smartquotes)
+
+ Get the string representation of *bytes*. This function is currently used to
+ implement :meth:`!bytes.__repr__` in Python.
+
+ This function does not do type checking; it is undefined behavior to pass
+ *bytes* as a non-bytes object or ``NULL``.
+
+ If *smartquotes* is true, the representation will use a double-quoted string
+ instead of single-quoted string when single-quotes are present in *bytes*.
+ For example, the byte string ``'Python'`` would be represented as
+ ``b"'Python'"`` when *smartquotes* is true, or ``b'\'Python\''`` when it is
+ false.
+
+ On success, this function returns a :term:`strong reference` to a
+ :class:`str` object containing the representation. On failure, this
+ returns ``NULL`` with an exception set.
+
+
+.. c:function:: PyObject *PyBytes_DecodeEscape(const char *s, Py_ssize_t len, const char *errors, Py_ssize_t unicode, const char *recode_encoding)
+
+ Unescape a backslash-escaped string *s*. *s* must not be ``NULL``.
+ *len* must be the size of *s*.
+
+ *errors* must be one of ``"strict"``, ``"replace"``, or ``"ignore"``. If
+ *errors* is ``NULL``, then ``"strict"`` is used by default.
+
+ On success, this function returns a :term:`strong reference` to a Python
+ :class:`bytes` object containing the unescaped string. On failure, this
+ function returns ``NULL`` with an exception set.
+
+ .. versionchanged:: 3.9
+ *unicode* and *recode_encoding* are now unused.
+
+
.. _pybyteswriter:
PyBytesWriter
From af80fac42548719ede7241bfbab3c2c0775b4760 Mon Sep 17 00:00:00 2001
From: Mohsin Mehmood <55545648+mohsinm-dev@users.noreply.github.com>
Date: Wed, 12 Nov 2025 02:49:54 +0500
Subject: [PATCH 133/417] gh-141314: Fix TextIOWrapper.tell() assertion failure
with standalone carriage return (GH-141331)
The assertion was checking wrong variable (skip_back vs skip_bytes).
---
Lib/test/test_io/test_textio.py | 19 +++++++++++++++++++
...-11-10-01-47-18.gh-issue-141314.baaa28.rst | 1 +
Modules/_io/textio.c | 2 +-
3 files changed, 21 insertions(+), 1 deletion(-)
create mode 100644 Misc/NEWS.d/next/Library/2025-11-10-01-47-18.gh-issue-141314.baaa28.rst
diff --git a/Lib/test/test_io/test_textio.py b/Lib/test/test_io/test_textio.py
index d8d0928b4ba..6331ed2b958 100644
--- a/Lib/test/test_io/test_textio.py
+++ b/Lib/test/test_io/test_textio.py
@@ -686,6 +686,25 @@ def test_multibyte_seek_and_tell(self):
self.assertEqual(f.tell(), p1)
f.close()
+ def test_tell_after_readline_with_cr(self):
+ # Test for gh-141314: TextIOWrapper.tell() assertion failure
+ # when dealing with standalone carriage returns
+ data = b'line1\r'
+ with self.open(os_helper.TESTFN, "wb") as f:
+ f.write(data)
+
+ with self.open(os_helper.TESTFN, "r") as f:
+ # Read line that ends with \r
+ line = f.readline()
+ self.assertEqual(line, "line1\n")
+ # This should not cause an assertion failure
+ pos = f.tell()
+ # Verify we can seek back to this position
+ f.seek(pos)
+ remaining = f.read()
+ self.assertEqual(remaining, "")
+
+
def test_seek_with_encoder_state(self):
f = self.open(os_helper.TESTFN, "w", encoding="euc_jis_2004")
f.write("\u00e6\u0300")
diff --git a/Misc/NEWS.d/next/Library/2025-11-10-01-47-18.gh-issue-141314.baaa28.rst b/Misc/NEWS.d/next/Library/2025-11-10-01-47-18.gh-issue-141314.baaa28.rst
new file mode 100644
index 00000000000..37acaabfa3e
--- /dev/null
+++ b/Misc/NEWS.d/next/Library/2025-11-10-01-47-18.gh-issue-141314.baaa28.rst
@@ -0,0 +1 @@
+Fix assertion failure in :meth:`io.TextIOWrapper.tell` when reading files with standalone carriage return (``\r``) line endings.
diff --git a/Modules/_io/textio.c b/Modules/_io/textio.c
index 84b7d9df400..65da300abcf 100644
--- a/Modules/_io/textio.c
+++ b/Modules/_io/textio.c
@@ -2845,7 +2845,7 @@ _io_TextIOWrapper_tell_impl(textio *self)
current pos */
skip_bytes = (Py_ssize_t) (self->b2cratio * chars_to_skip);
skip_back = 1;
- assert(skip_back <= PyBytes_GET_SIZE(next_input));
+ assert(skip_bytes <= PyBytes_GET_SIZE(next_input));
input = PyBytes_AS_STRING(next_input);
while (skip_bytes > 0) {
/* Decode up to temptative start point */
From c744ccb2c92746bc7be6316ab478dbc13e176e97 Mon Sep 17 00:00:00 2001
From: Adam Turner <9087854+AA-Turner@users.noreply.github.com>
Date: Tue, 11 Nov 2025 21:51:22 +0000
Subject: [PATCH 134/417] GH-139596: Cease caching config.cache & ccache in GH
Actions (GH-139623)
* Cease caching config.cache in GH Actions\
* Remove ccache action
---
.github/workflows/build.yml | 52 ---------------------------
.github/workflows/reusable-macos.yml | 5 ---
.github/workflows/reusable-san.yml | 10 ------
.github/workflows/reusable-ubuntu.yml | 10 ------
.github/workflows/reusable-wasi.yml | 21 +----------
5 files changed, 1 insertion(+), 97 deletions(-)
diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml
index 6aa99928278..a0f60c30ac8 100644
--- a/.github/workflows/build.yml
+++ b/.github/workflows/build.yml
@@ -109,20 +109,10 @@ jobs:
python-version: '3.x'
- name: Runner image version
run: echo "IMAGE_OS_VERSION=${ImageOS}-${ImageVersion}" >> "$GITHUB_ENV"
- - name: Restore config.cache
- uses: actions/cache@v4
- with:
- path: config.cache
- # Include env.pythonLocation in key to avoid changes in environment when setup-python updates Python
- key: ${{ github.job }}-${{ env.IMAGE_OS_VERSION }}-${{ needs.build-context.outputs.config-hash }}-${{ env.pythonLocation }}
- name: Install dependencies
run: sudo ./.github/workflows/posix-deps-apt.sh
- name: Add ccache to PATH
run: echo "PATH=/usr/lib/ccache:$PATH" >> "$GITHUB_ENV"
- - name: Configure ccache action
- uses: hendrikmuhs/ccache-action@v1.2
- with:
- save: false
- name: Configure CPython
run: |
# Build Python with the libpython dynamic library
@@ -278,11 +268,6 @@ jobs:
persist-credentials: false
- name: Runner image version
run: echo "IMAGE_OS_VERSION=${ImageOS}-${ImageVersion}" >> "$GITHUB_ENV"
- - name: Restore config.cache
- uses: actions/cache@v4
- with:
- path: config.cache
- key: ${{ github.job }}-${{ env.IMAGE_OS_VERSION }}-${{ needs.build-context.outputs.config-hash }}
- name: Register gcc problem matcher
run: echo "::add-matcher::.github/problem-matchers/gcc.json"
- name: Install dependencies
@@ -304,10 +289,6 @@ jobs:
- name: Add ccache to PATH
run: |
echo "PATH=/usr/lib/ccache:$PATH" >> "$GITHUB_ENV"
- - name: Configure ccache action
- uses: hendrikmuhs/ccache-action@v1.2
- with:
- save: false
- name: Configure CPython
run: ./configure CFLAGS="-fdiagnostics-format=json" --config-cache --enable-slower-safety --with-pydebug --with-openssl="$OPENSSL_DIR"
- name: Build CPython
@@ -339,11 +320,6 @@ jobs:
persist-credentials: false
- name: Runner image version
run: echo "IMAGE_OS_VERSION=${ImageOS}-${ImageVersion}" >> "$GITHUB_ENV"
- - name: Restore config.cache
- uses: actions/cache@v4
- with:
- path: config.cache
- key: ${{ github.job }}-${{ env.IMAGE_OS_VERSION }}-${{ needs.build-context.outputs.config-hash }}
- name: Register gcc problem matcher
run: echo "::add-matcher::.github/problem-matchers/gcc.json"
- name: Install dependencies
@@ -370,10 +346,6 @@ jobs:
- name: Add ccache to PATH
run: |
echo "PATH=/usr/lib/ccache:$PATH" >> "$GITHUB_ENV"
- - name: Configure ccache action
- uses: hendrikmuhs/ccache-action@v1.2
- with:
- save: false
- name: Configure CPython
run: |
./configure CFLAGS="-fdiagnostics-format=json" \
@@ -479,10 +451,6 @@ jobs:
- name: Add ccache to PATH
run: |
echo "PATH=/usr/lib/ccache:$PATH" >> "$GITHUB_ENV"
- - name: Configure ccache action
- uses: hendrikmuhs/ccache-action@v1.2
- with:
- save: false
- name: Setup directory envs for out-of-tree builds
run: |
echo "CPYTHON_RO_SRCDIR=$(realpath -m "${GITHUB_WORKSPACE}"/../cpython-ro-srcdir)" >> "$GITHUB_ENV"
@@ -493,11 +461,6 @@ jobs:
run: sudo mount --bind -o ro "$GITHUB_WORKSPACE" "$CPYTHON_RO_SRCDIR"
- name: Runner image version
run: echo "IMAGE_OS_VERSION=${ImageOS}-${ImageVersion}" >> "$GITHUB_ENV"
- - name: Restore config.cache
- uses: actions/cache@v4
- with:
- path: ${{ env.CPYTHON_BUILDDIR }}/config.cache
- key: ${{ github.job }}-${{ env.IMAGE_OS_VERSION }}-${{ needs.build-context.outputs.config-hash }}
- name: Configure CPython out-of-tree
working-directory: ${{ env.CPYTHON_BUILDDIR }}
run: |
@@ -581,11 +544,6 @@ jobs:
persist-credentials: false
- name: Runner image version
run: echo "IMAGE_OS_VERSION=${ImageOS}-${ImageVersion}" >> "$GITHUB_ENV"
- - name: Restore config.cache
- uses: actions/cache@v4
- with:
- path: config.cache
- key: ${{ github.job }}-${{ env.IMAGE_OS_VERSION }}-${{ needs.build-context.outputs.config-hash }}
- name: Register gcc problem matcher
run: echo "::add-matcher::.github/problem-matchers/gcc.json"
- name: Install dependencies
@@ -611,11 +569,6 @@ jobs:
- name: Add ccache to PATH
run: |
echo "PATH=/usr/lib/ccache:$PATH" >> "$GITHUB_ENV"
- - name: Configure ccache action
- uses: hendrikmuhs/ccache-action@v1.2
- with:
- save: ${{ github.event_name == 'push' }}
- max-size: "200M"
- name: Configure CPython
run: ./configure --config-cache --with-address-sanitizer --without-pymalloc
- name: Build CPython
@@ -662,11 +615,6 @@ jobs:
persist-credentials: false
- name: Runner image version
run: echo "IMAGE_OS_VERSION=${ImageOS}-${ImageVersion}" >> "$GITHUB_ENV"
- - name: Restore config.cache
- uses: actions/cache@v4
- with:
- path: config.cache
- key: ${{ github.job }}-${{ env.IMAGE_OS_VERSION }}-${{ needs.build-context.outputs.config-hash }}
- name: Register gcc problem matcher
run: echo "::add-matcher::.github/problem-matchers/gcc.json"
- name: Set build dir
diff --git a/.github/workflows/reusable-macos.yml b/.github/workflows/reusable-macos.yml
index 3d310ae695b..d85c46b96f8 100644
--- a/.github/workflows/reusable-macos.yml
+++ b/.github/workflows/reusable-macos.yml
@@ -36,11 +36,6 @@ jobs:
persist-credentials: false
- name: Runner image version
run: echo "IMAGE_OS_VERSION=${ImageOS}-${ImageVersion}" >> "$GITHUB_ENV"
- - name: Restore config.cache
- uses: actions/cache@v4
- with:
- path: config.cache
- key: ${{ github.job }}-${{ env.IMAGE_OS_VERSION }}-${{ inputs.config_hash }}
- name: Install Homebrew dependencies
run: |
brew install pkg-config openssl@3.0 xz gdbm tcl-tk@9 make
diff --git a/.github/workflows/reusable-san.yml b/.github/workflows/reusable-san.yml
index e6ff02e4838..7fe96d1b238 100644
--- a/.github/workflows/reusable-san.yml
+++ b/.github/workflows/reusable-san.yml
@@ -34,11 +34,6 @@ jobs:
persist-credentials: false
- name: Runner image version
run: echo "IMAGE_OS_VERSION=${ImageOS}-${ImageVersion}" >> "$GITHUB_ENV"
- - name: Restore config.cache
- uses: actions/cache@v4
- with:
- path: config.cache
- key: ${{ github.job }}-${{ env.IMAGE_OS_VERSION }}-${{ inputs.sanitizer }}-${{ inputs.config_hash }}
- name: Install dependencies
run: |
sudo ./.github/workflows/posix-deps-apt.sh
@@ -77,11 +72,6 @@ jobs:
- name: Add ccache to PATH
run: |
echo "PATH=/usr/lib/ccache:$PATH" >> "$GITHUB_ENV"
- - name: Configure ccache action
- uses: hendrikmuhs/ccache-action@v1.2
- with:
- save: ${{ github.event_name == 'push' }}
- max-size: "200M"
- name: Configure CPython
run: >-
./configure
diff --git a/.github/workflows/reusable-ubuntu.yml b/.github/workflows/reusable-ubuntu.yml
index 7f8b9fdf5d6..7b93b5f51b0 100644
--- a/.github/workflows/reusable-ubuntu.yml
+++ b/.github/workflows/reusable-ubuntu.yml
@@ -64,11 +64,6 @@ jobs:
- name: Add ccache to PATH
run: |
echo "PATH=/usr/lib/ccache:$PATH" >> "$GITHUB_ENV"
- - name: Configure ccache action
- uses: hendrikmuhs/ccache-action@v1.2
- with:
- save: ${{ github.event_name == 'push' }}
- max-size: "200M"
- name: Setup directory envs for out-of-tree builds
run: |
echo "CPYTHON_RO_SRCDIR=$(realpath -m "${GITHUB_WORKSPACE}"/../cpython-ro-srcdir)" >> "$GITHUB_ENV"
@@ -79,11 +74,6 @@ jobs:
run: sudo mount --bind -o ro "$GITHUB_WORKSPACE" "$CPYTHON_RO_SRCDIR"
- name: Runner image version
run: echo "IMAGE_OS_VERSION=${ImageOS}-${ImageVersion}" >> "$GITHUB_ENV"
- - name: Restore config.cache
- uses: actions/cache@v4
- with:
- path: ${{ env.CPYTHON_BUILDDIR }}/config.cache
- key: ${{ github.job }}-${{ env.IMAGE_OS_VERSION }}-${{ inputs.config_hash }}
- name: Configure CPython out-of-tree
working-directory: ${{ env.CPYTHON_BUILDDIR }}
# `test_unpickle_module_race` writes to the source directory, which is
diff --git a/.github/workflows/reusable-wasi.yml b/.github/workflows/reusable-wasi.yml
index 18feb564822..8f412288f53 100644
--- a/.github/workflows/reusable-wasi.yml
+++ b/.github/workflows/reusable-wasi.yml
@@ -42,11 +42,6 @@ jobs:
mkdir "${WASI_SDK_PATH}" && \
curl -s -S --location "https://github.com/WebAssembly/wasi-sdk/releases/download/wasi-sdk-${WASI_SDK_VERSION}/wasi-sdk-${WASI_SDK_VERSION}.0-arm64-linux.tar.gz" | \
tar --strip-components 1 --directory "${WASI_SDK_PATH}" --extract --gunzip
- - name: "Configure ccache action"
- uses: hendrikmuhs/ccache-action@v1.2
- with:
- save: ${{ github.event_name == 'push' }}
- max-size: "200M"
- name: "Add ccache to PATH"
run: echo "PATH=/usr/lib/ccache:$PATH" >> "$GITHUB_ENV"
- name: "Install Python"
@@ -55,24 +50,10 @@ jobs:
python-version: '3.x'
- name: "Runner image version"
run: echo "IMAGE_OS_VERSION=${ImageOS}-${ImageVersion}" >> "$GITHUB_ENV"
- - name: "Restore Python build config.cache"
- uses: actions/cache@v4
- with:
- path: ${{ env.CROSS_BUILD_PYTHON }}/config.cache
- # Include env.pythonLocation in key to avoid changes in environment when setup-python updates Python.
- # Include the hash of `Tools/wasm/wasi/__main__.py` as it may change the environment variables.
- # (Make sure to keep the key in sync with the other config.cache step below.)
- key: ${{ github.job }}-${{ env.IMAGE_OS_VERSION }}-${{ env.WASI_SDK_VERSION }}-${{ env.WASMTIME_VERSION }}-${{ inputs.config_hash }}-${{ hashFiles('Tools/wasm/wasi/__main__.py') }}-${{ env.pythonLocation }}
- name: "Configure build Python"
run: python3 Tools/wasm/wasi configure-build-python -- --config-cache --with-pydebug
- name: "Make build Python"
- run: python3 Tools/wasm/wasi make-build-python
- - name: "Restore host config.cache"
- uses: actions/cache@v4
- with:
- path: ${{ env.CROSS_BUILD_WASI }}/config.cache
- # Should be kept in sync with the other config.cache step above.
- key: ${{ github.job }}-${{ env.IMAGE_OS_VERSION }}-${{ env.WASI_SDK_VERSION }}-${{ env.WASMTIME_VERSION }}-${{ inputs.config_hash }}-${{ hashFiles('Tools/wasm/wasi/__main__.py') }}-${{ env.pythonLocation }}
+ run: python3 Tools/wasm/wasi.py make-build-python
- name: "Configure host"
# `--with-pydebug` inferred from configure-build-python
run: python3 Tools/wasm/wasi configure-host -- --config-cache
From 7906f4d96a8fffbee9f4d4991019595878ad54e9 Mon Sep 17 00:00:00 2001
From: Serhiy Storchaka
Date: Wed, 12 Nov 2025 00:01:25 +0200
Subject: [PATCH 135/417] gh-132686: Add parameters inherit_class_doc and
fallback_to_class_doc for inspect.getdoc() (GH-132691)
---
Doc/library/inspect.rst | 17 +++-
Doc/whatsnew/3.15.rst | 8 ++
Lib/inspect.py | 34 +++++--
Lib/pydoc.py | 92 +------------------
Lib/test/test_inspect/test_inspect.py | 27 ++++++
...-04-18-18-08-05.gh-issue-132686.6kV_Gs.rst | 2 +
6 files changed, 79 insertions(+), 101 deletions(-)
create mode 100644 Misc/NEWS.d/next/Library/2025-04-18-18-08-05.gh-issue-132686.6kV_Gs.rst
diff --git a/Doc/library/inspect.rst b/Doc/library/inspect.rst
index 2b3b294ff33..aff53b78c4a 100644
--- a/Doc/library/inspect.rst
+++ b/Doc/library/inspect.rst
@@ -619,17 +619,26 @@ attributes (see :ref:`import-mod-attrs` for module attributes):
Retrieving source code
----------------------
-.. function:: getdoc(object)
+.. function:: getdoc(object, *, inherit_class_doc=True, fallback_to_class_doc=True)
Get the documentation string for an object, cleaned up with :func:`cleandoc`.
- If the documentation string for an object is not provided and the object is
- a class, a method, a property or a descriptor, retrieve the documentation
- string from the inheritance hierarchy.
+ If the documentation string for an object is not provided:
+
+ * if the object is a class and *inherit_class_doc* is true (by default),
+ retrieve the documentation string from the inheritance hierarchy;
+ * if the object is a method, a property or a descriptor, retrieve
+ the documentation string from the inheritance hierarchy;
+ * otherwise, if *fallback_to_class_doc* is true (by default), retrieve
+ the documentation string from the class of the object.
+
Return ``None`` if the documentation string is invalid or missing.
.. versionchanged:: 3.5
Documentation strings are now inherited if not overridden.
+ .. versionchanged:: next
+ Added parameters *inherit_class_doc* and *fallback_to_class_doc*.
+
.. function:: getcomments(object)
diff --git a/Doc/whatsnew/3.15.rst b/Doc/whatsnew/3.15.rst
index ef18d36e4d4..ecab0d03e10 100644
--- a/Doc/whatsnew/3.15.rst
+++ b/Doc/whatsnew/3.15.rst
@@ -429,6 +429,14 @@ http.cookies
(Contributed by Nick Burns and Senthil Kumaran in :gh:`92936`.)
+inspect
+-------
+
+* Add parameters *inherit_class_doc* and *fallback_to_class_doc*
+ for :func:`~inspect.getdoc`.
+ (Contributed by Serhiy Storchaka in :gh:`132686`.)
+
+
locale
------
diff --git a/Lib/inspect.py b/Lib/inspect.py
index bb22bab3040..bb17848b444 100644
--- a/Lib/inspect.py
+++ b/Lib/inspect.py
@@ -706,8 +706,8 @@ def _findclass(func):
return None
return cls
-def _finddoc(obj):
- if isclass(obj):
+def _finddoc(obj, *, search_in_class=True):
+ if search_in_class and isclass(obj):
for base in obj.__mro__:
if base is not object:
try:
@@ -767,19 +767,37 @@ def _finddoc(obj):
return doc
return None
-def getdoc(object):
+def _getowndoc(obj):
+ """Get the documentation string for an object if it is not
+ inherited from its class."""
+ try:
+ doc = object.__getattribute__(obj, '__doc__')
+ if doc is None:
+ return None
+ if obj is not type:
+ typedoc = type(obj).__doc__
+ if isinstance(typedoc, str) and typedoc == doc:
+ return None
+ return doc
+ except AttributeError:
+ return None
+
+def getdoc(object, *, fallback_to_class_doc=True, inherit_class_doc=True):
"""Get the documentation string for an object.
All tabs are expanded to spaces. To clean up docstrings that are
indented to line up with blocks of code, any whitespace than can be
uniformly removed from the second line onwards is removed."""
- try:
- doc = object.__doc__
- except AttributeError:
- return None
+ if fallback_to_class_doc:
+ try:
+ doc = object.__doc__
+ except AttributeError:
+ return None
+ else:
+ doc = _getowndoc(object)
if doc is None:
try:
- doc = _finddoc(object)
+ doc = _finddoc(object, search_in_class=inherit_class_doc)
except (AttributeError, TypeError):
return None
if not isinstance(doc, str):
diff --git a/Lib/pydoc.py b/Lib/pydoc.py
index 989fbd517d8..45ff5fca308 100644
--- a/Lib/pydoc.py
+++ b/Lib/pydoc.py
@@ -108,96 +108,10 @@ def pathdirs():
normdirs.append(normdir)
return dirs
-def _findclass(func):
- cls = sys.modules.get(func.__module__)
- if cls is None:
- return None
- for name in func.__qualname__.split('.')[:-1]:
- cls = getattr(cls, name)
- if not inspect.isclass(cls):
- return None
- return cls
-
-def _finddoc(obj):
- if inspect.ismethod(obj):
- name = obj.__func__.__name__
- self = obj.__self__
- if (inspect.isclass(self) and
- getattr(getattr(self, name, None), '__func__') is obj.__func__):
- # classmethod
- cls = self
- else:
- cls = self.__class__
- elif inspect.isfunction(obj):
- name = obj.__name__
- cls = _findclass(obj)
- if cls is None or getattr(cls, name) is not obj:
- return None
- elif inspect.isbuiltin(obj):
- name = obj.__name__
- self = obj.__self__
- if (inspect.isclass(self) and
- self.__qualname__ + '.' + name == obj.__qualname__):
- # classmethod
- cls = self
- else:
- cls = self.__class__
- # Should be tested before isdatadescriptor().
- elif isinstance(obj, property):
- name = obj.__name__
- cls = _findclass(obj.fget)
- if cls is None or getattr(cls, name) is not obj:
- return None
- elif inspect.ismethoddescriptor(obj) or inspect.isdatadescriptor(obj):
- name = obj.__name__
- cls = obj.__objclass__
- if getattr(cls, name) is not obj:
- return None
- if inspect.ismemberdescriptor(obj):
- slots = getattr(cls, '__slots__', None)
- if isinstance(slots, dict) and name in slots:
- return slots[name]
- else:
- return None
- for base in cls.__mro__:
- try:
- doc = _getowndoc(getattr(base, name))
- except AttributeError:
- continue
- if doc is not None:
- return doc
- return None
-
-def _getowndoc(obj):
- """Get the documentation string for an object if it is not
- inherited from its class."""
- try:
- doc = object.__getattribute__(obj, '__doc__')
- if doc is None:
- return None
- if obj is not type:
- typedoc = type(obj).__doc__
- if isinstance(typedoc, str) and typedoc == doc:
- return None
- return doc
- except AttributeError:
- return None
-
def _getdoc(object):
- """Get the documentation string for an object.
-
- All tabs are expanded to spaces. To clean up docstrings that are
- indented to line up with blocks of code, any whitespace than can be
- uniformly removed from the second line onwards is removed."""
- doc = _getowndoc(object)
- if doc is None:
- try:
- doc = _finddoc(object)
- except (AttributeError, TypeError):
- return None
- if not isinstance(doc, str):
- return None
- return inspect.cleandoc(doc)
+ return inspect.getdoc(object,
+ fallback_to_class_doc=False,
+ inherit_class_doc=False)
def getdoc(object):
"""Get the doc string or comments for an object."""
diff --git a/Lib/test/test_inspect/test_inspect.py b/Lib/test/test_inspect/test_inspect.py
index d42f2dbff99..24fd4a2fa62 100644
--- a/Lib/test/test_inspect/test_inspect.py
+++ b/Lib/test/test_inspect/test_inspect.py
@@ -688,10 +688,37 @@ def test_getdoc_inherited(self):
self.assertEqual(inspect.getdoc(mod.FesteringGob.contradiction),
'The automatic gainsaying.')
+ @unittest.skipIf(sys.flags.optimize >= 2,
+ "Docstrings are omitted with -O2 and above")
+ def test_getdoc_inherited_class_doc(self):
+ class A:
+ """Common base class"""
+ class B(A):
+ pass
+
+ a = A()
+ self.assertEqual(inspect.getdoc(A), 'Common base class')
+ self.assertEqual(inspect.getdoc(A, inherit_class_doc=False),
+ 'Common base class')
+ self.assertEqual(inspect.getdoc(a), 'Common base class')
+ self.assertIsNone(inspect.getdoc(a, fallback_to_class_doc=False))
+ a.__doc__ = 'Instance'
+ self.assertEqual(inspect.getdoc(a, fallback_to_class_doc=False),
+ 'Instance')
+
+ b = B()
+ self.assertEqual(inspect.getdoc(B), 'Common base class')
+ self.assertIsNone(inspect.getdoc(B, inherit_class_doc=False))
+ self.assertIsNone(inspect.getdoc(b))
+ self.assertIsNone(inspect.getdoc(b, fallback_to_class_doc=False))
+ b.__doc__ = 'Instance'
+ self.assertEqual(inspect.getdoc(b, fallback_to_class_doc=False), 'Instance')
+
@unittest.skipIf(MISSING_C_DOCSTRINGS, "test requires docstrings")
def test_finddoc(self):
finddoc = inspect._finddoc
self.assertEqual(finddoc(int), int.__doc__)
+ self.assertIsNone(finddoc(int, search_in_class=False))
self.assertEqual(finddoc(int.to_bytes), int.to_bytes.__doc__)
self.assertEqual(finddoc(int().to_bytes), int.to_bytes.__doc__)
self.assertEqual(finddoc(int.from_bytes), int.from_bytes.__doc__)
diff --git a/Misc/NEWS.d/next/Library/2025-04-18-18-08-05.gh-issue-132686.6kV_Gs.rst b/Misc/NEWS.d/next/Library/2025-04-18-18-08-05.gh-issue-132686.6kV_Gs.rst
new file mode 100644
index 00000000000..d0c8e2d705c
--- /dev/null
+++ b/Misc/NEWS.d/next/Library/2025-04-18-18-08-05.gh-issue-132686.6kV_Gs.rst
@@ -0,0 +1,2 @@
+Add parameters *inherit_class_doc* and *fallback_to_class_doc* for
+:func:`inspect.getdoc`.
From 9e7340cd3b5531784291088b504882cfb4d4c78c Mon Sep 17 00:00:00 2001
From: J Berg
Date: Tue, 11 Nov 2025 22:09:58 +0000
Subject: [PATCH 136/417] gh-139462: Make the ProcessPoolExecutor
BrokenProcessPool exception report which child process terminated (GH-139486)
Report which process terminated as cause of BPE
---
Doc/whatsnew/3.15.rst | 10 ++++++++++
Lib/concurrent/futures/process.py | 18 ++++++++++++++++--
.../test_process_pool.py | 15 +++++++++++++++
...5-10-02-22-29-00.gh-issue-139462.VZXUHe.rst | 3 +++
4 files changed, 44 insertions(+), 2 deletions(-)
create mode 100644 Misc/NEWS.d/next/Library/2025-10-02-22-29-00.gh-issue-139462.VZXUHe.rst
diff --git a/Doc/whatsnew/3.15.rst b/Doc/whatsnew/3.15.rst
index ecab0d03e10..c543b6e6c2a 100644
--- a/Doc/whatsnew/3.15.rst
+++ b/Doc/whatsnew/3.15.rst
@@ -369,6 +369,16 @@ collections.abc
:mod:`!collections.abc` module.
+concurrent.futures
+------------------
+
+* Improved error reporting when a child process in a
+ :class:`concurrent.futures.ProcessPoolExecutor` terminates abruptly.
+ The resulting traceback will now tell you the PID and exit code of the
+ terminated process.
+ (Contributed by Jonathan Berg in :gh:`139486`.)
+
+
dataclasses
-----------
diff --git a/Lib/concurrent/futures/process.py b/Lib/concurrent/futures/process.py
index a14650bf5fa..a42afa68efc 100644
--- a/Lib/concurrent/futures/process.py
+++ b/Lib/concurrent/futures/process.py
@@ -474,9 +474,23 @@ def _terminate_broken(self, cause):
bpe = BrokenProcessPool("A process in the process pool was "
"terminated abruptly while the future was "
"running or pending.")
+ cause_str = None
if cause is not None:
- bpe.__cause__ = _RemoteTraceback(
- f"\n'''\n{''.join(cause)}'''")
+ cause_str = ''.join(cause)
+ else:
+ # No cause known, so report any processes that have
+ # terminated with nonzero exit codes, e.g. from a
+ # segfault. Multiple may terminate simultaneously,
+ # so include all of them in the traceback.
+ errors = []
+ for p in self.processes.values():
+ if p.exitcode is not None and p.exitcode != 0:
+ errors.append(f"Process {p.pid} terminated abruptly "
+ f"with exit code {p.exitcode}")
+ if errors:
+ cause_str = "\n".join(errors)
+ if cause_str:
+ bpe.__cause__ = _RemoteTraceback(f"\n'''\n{cause_str}'''")
# Mark pending tasks as failed.
for work_id, work_item in self.pending_work_items.items():
diff --git a/Lib/test/test_concurrent_futures/test_process_pool.py b/Lib/test/test_concurrent_futures/test_process_pool.py
index 9685f980119..731419a48bd 100644
--- a/Lib/test/test_concurrent_futures/test_process_pool.py
+++ b/Lib/test/test_concurrent_futures/test_process_pool.py
@@ -106,6 +106,21 @@ def test_traceback(self):
self.assertIn('raise RuntimeError(123) # some comment',
f1.getvalue())
+ def test_traceback_when_child_process_terminates_abruptly(self):
+ # gh-139462 enhancement - BrokenProcessPool exceptions
+ # should describe which process terminated.
+ exit_code = 99
+ with self.executor_type(max_workers=1) as executor:
+ future = executor.submit(os._exit, exit_code)
+ with self.assertRaises(BrokenProcessPool) as bpe:
+ future.result()
+
+ cause = bpe.exception.__cause__
+ self.assertIsInstance(cause, futures.process._RemoteTraceback)
+ self.assertIn(
+ f"terminated abruptly with exit code {exit_code}", cause.tb
+ )
+
@warnings_helper.ignore_fork_in_thread_deprecation_warnings()
@hashlib_helper.requires_hashdigest('md5')
def test_ressources_gced_in_workers(self):
diff --git a/Misc/NEWS.d/next/Library/2025-10-02-22-29-00.gh-issue-139462.VZXUHe.rst b/Misc/NEWS.d/next/Library/2025-10-02-22-29-00.gh-issue-139462.VZXUHe.rst
new file mode 100644
index 00000000000..390a6124386
--- /dev/null
+++ b/Misc/NEWS.d/next/Library/2025-10-02-22-29-00.gh-issue-139462.VZXUHe.rst
@@ -0,0 +1,3 @@
+When a child process in a :class:`concurrent.futures.ProcessPoolExecutor`
+terminates abruptly, the resulting traceback will now tell you the PID
+and exit code of the terminated process. Contributed by Jonathan Berg.
From 4359706ac8d5589fc37e2f1460a0d07a2319df15 Mon Sep 17 00:00:00 2001
From: Serhiy Storchaka
Date: Wed, 12 Nov 2025 00:27:13 +0200
Subject: [PATCH 137/417] gh-120950: Fix overflow in math.log() with large
int-like argument (GH-121011)
Handling of arbitrary large int-like argument is now consistent with
handling arbitrary large int arguments.
---
Lib/test/test_math.py | 59 ++++++++++++++
...-06-26-16-16-43.gh-issue-121011.qW54eh.rst | 2 +
Modules/mathmodule.c | 80 ++++++++++++-------
3 files changed, 111 insertions(+), 30 deletions(-)
create mode 100644 Misc/NEWS.d/next/Library/2024-06-26-16-16-43.gh-issue-121011.qW54eh.rst
diff --git a/Lib/test/test_math.py b/Lib/test/test_math.py
index ddeb8ad7cd6..68f41a2e620 100644
--- a/Lib/test/test_math.py
+++ b/Lib/test/test_math.py
@@ -189,6 +189,22 @@ def __init__(self, value):
def __index__(self):
return self.value
+class IndexableFloatLike:
+ def __init__(self, float_value, index_value):
+ self.float_value = float_value
+ self.index_value = index_value
+
+ def __float__(self):
+ if isinstance(self.float_value, BaseException):
+ raise self.float_value
+ return self.float_value
+
+ def __index__(self):
+ if isinstance(self.index_value, BaseException):
+ raise self.index_value
+ return self.index_value
+
+
class BadDescr:
def __get__(self, obj, objtype=None):
raise ValueError
@@ -1192,13 +1208,32 @@ def testLog(self):
self.ftest('log(10**40, 10**20)', math.log(10**40, 10**20), 2)
self.ftest('log(10**1000)', math.log(10**1000),
2302.5850929940457)
+ self.ftest('log(10**2000, 10**1000)', math.log(10**2000, 10**1000), 2)
+ self.ftest('log(MyIndexable(32), MyIndexable(2))',
+ math.log(MyIndexable(32), MyIndexable(2)), 5)
+ self.ftest('log(MyIndexable(10**1000))',
+ math.log(MyIndexable(10**1000)),
+ 2302.5850929940457)
+ self.ftest('log(MyIndexable(10**2000), MyIndexable(10**1000))',
+ math.log(MyIndexable(10**2000), MyIndexable(10**1000)),
+ 2)
+ self.assertRaises(ValueError, math.log, 0.0)
+ self.assertRaises(ValueError, math.log, 0)
+ self.assertRaises(ValueError, math.log, MyIndexable(0))
self.assertRaises(ValueError, math.log, -1.5)
+ self.assertRaises(ValueError, math.log, -1)
+ self.assertRaises(ValueError, math.log, MyIndexable(-1))
self.assertRaises(ValueError, math.log, -10**1000)
+ self.assertRaises(ValueError, math.log, MyIndexable(-10**1000))
self.assertRaises(ValueError, math.log, 10, -10)
self.assertRaises(ValueError, math.log, NINF)
self.assertEqual(math.log(INF), INF)
self.assertTrue(math.isnan(math.log(NAN)))
+ self.assertEqual(math.log(IndexableFloatLike(math.e, 10**1000)), 1.0)
+ self.assertAlmostEqual(math.log(IndexableFloatLike(OverflowError(), 10**1000)),
+ 2302.5850929940457)
+
def testLog1p(self):
self.assertRaises(TypeError, math.log1p)
for n in [2, 2**90, 2**300]:
@@ -1214,16 +1249,28 @@ def testLog2(self):
self.assertEqual(math.log2(1), 0.0)
self.assertEqual(math.log2(2), 1.0)
self.assertEqual(math.log2(4), 2.0)
+ self.assertEqual(math.log2(MyIndexable(4)), 2.0)
# Large integer values
self.assertEqual(math.log2(2**1023), 1023.0)
self.assertEqual(math.log2(2**1024), 1024.0)
self.assertEqual(math.log2(2**2000), 2000.0)
+ self.assertEqual(math.log2(MyIndexable(2**2000)), 2000.0)
+ self.assertRaises(ValueError, math.log2, 0.0)
+ self.assertRaises(ValueError, math.log2, 0)
+ self.assertRaises(ValueError, math.log2, MyIndexable(0))
self.assertRaises(ValueError, math.log2, -1.5)
+ self.assertRaises(ValueError, math.log2, -1)
+ self.assertRaises(ValueError, math.log2, MyIndexable(-1))
+ self.assertRaises(ValueError, math.log2, -2**2000)
+ self.assertRaises(ValueError, math.log2, MyIndexable(-2**2000))
self.assertRaises(ValueError, math.log2, NINF)
self.assertTrue(math.isnan(math.log2(NAN)))
+ self.assertEqual(math.log2(IndexableFloatLike(8.0, 2**2000)), 3.0)
+ self.assertEqual(math.log2(IndexableFloatLike(OverflowError(), 2**2000)), 2000.0)
+
@requires_IEEE_754
# log2() is not accurate enough on Mac OS X Tiger (10.4)
@support.requires_mac_ver(10, 5)
@@ -1239,12 +1286,24 @@ def testLog10(self):
self.ftest('log10(1)', math.log10(1), 0)
self.ftest('log10(10)', math.log10(10), 1)
self.ftest('log10(10**1000)', math.log10(10**1000), 1000.0)
+ self.ftest('log10(MyIndexable(10))', math.log10(MyIndexable(10)), 1)
+ self.ftest('log10(MyIndexable(10**1000))',
+ math.log10(MyIndexable(10**1000)), 1000.0)
+ self.assertRaises(ValueError, math.log10, 0.0)
+ self.assertRaises(ValueError, math.log10, 0)
+ self.assertRaises(ValueError, math.log10, MyIndexable(0))
self.assertRaises(ValueError, math.log10, -1.5)
+ self.assertRaises(ValueError, math.log10, -1)
+ self.assertRaises(ValueError, math.log10, MyIndexable(-1))
self.assertRaises(ValueError, math.log10, -10**1000)
+ self.assertRaises(ValueError, math.log10, MyIndexable(-10**1000))
self.assertRaises(ValueError, math.log10, NINF)
self.assertEqual(math.log(INF), INF)
self.assertTrue(math.isnan(math.log10(NAN)))
+ self.assertEqual(math.log10(IndexableFloatLike(100.0, 10**1000)), 2.0)
+ self.assertEqual(math.log10(IndexableFloatLike(OverflowError(), 10**1000)), 1000.0)
+
@support.bigmemtest(2**32, memuse=0.2)
def test_log_huge_integer(self, size):
v = 1 << size
diff --git a/Misc/NEWS.d/next/Library/2024-06-26-16-16-43.gh-issue-121011.qW54eh.rst b/Misc/NEWS.d/next/Library/2024-06-26-16-16-43.gh-issue-121011.qW54eh.rst
new file mode 100644
index 00000000000..aee7fe2bcb5
--- /dev/null
+++ b/Misc/NEWS.d/next/Library/2024-06-26-16-16-43.gh-issue-121011.qW54eh.rst
@@ -0,0 +1,2 @@
+:func:`math.log` now supports arbitrary large integer-like arguments in the
+same way as arbitrary large integer arguments.
diff --git a/Modules/mathmodule.c b/Modules/mathmodule.c
index 82846843cfb..de1886451ed 100644
--- a/Modules/mathmodule.c
+++ b/Modules/mathmodule.c
@@ -57,6 +57,7 @@ raised for division by zero and mod by zero.
#endif
#include "Python.h"
+#include "pycore_abstract.h" // _PyNumber_Index()
#include "pycore_bitutils.h" // _Py_bit_length()
#include "pycore_call.h" // _PyObject_CallNoArgs()
#include "pycore_import.h" // _PyImport_SetModuleString()
@@ -1577,44 +1578,63 @@ math_modf_impl(PyObject *module, double x)
However, intermediate overflow is possible for an int if the number of bits
in that int is larger than PY_SSIZE_T_MAX. */
+static PyObject*
+loghelper_int(PyObject* arg, double (*func)(double))
+{
+ /* If it is int, do it ourselves. */
+ double x, result;
+ int64_t e;
+
+ /* Negative or zero inputs give a ValueError. */
+ if (!_PyLong_IsPositive((PyLongObject *)arg)) {
+ PyErr_SetString(PyExc_ValueError,
+ "expected a positive input");
+ return NULL;
+ }
+
+ x = PyLong_AsDouble(arg);
+ if (x == -1.0 && PyErr_Occurred()) {
+ if (!PyErr_ExceptionMatches(PyExc_OverflowError))
+ return NULL;
+ /* Here the conversion to double overflowed, but it's possible
+ to compute the log anyway. Clear the exception and continue. */
+ PyErr_Clear();
+ x = _PyLong_Frexp((PyLongObject *)arg, &e);
+ assert(!PyErr_Occurred());
+ /* Value is ~= x * 2**e, so the log ~= log(x) + log(2) * e. */
+ result = fma(func(2.0), (double)e, func(x));
+ }
+ else
+ /* Successfully converted x to a double. */
+ result = func(x);
+ return PyFloat_FromDouble(result);
+}
+
static PyObject*
loghelper(PyObject* arg, double (*func)(double))
{
/* If it is int, do it ourselves. */
if (PyLong_Check(arg)) {
- double x, result;
- int64_t e;
-
- /* Negative or zero inputs give a ValueError. */
- if (!_PyLong_IsPositive((PyLongObject *)arg)) {
- /* The input can be an arbitrary large integer, so we
- don't include it's value in the error message. */
- PyErr_SetString(PyExc_ValueError,
- "expected a positive input");
+ return loghelper_int(arg, func);
+ }
+ /* Else let libm handle it by itself. */
+ PyObject *res = math_1(arg, func, 0, "expected a positive input, got %s");
+ if (res == NULL &&
+ PyErr_ExceptionMatches(PyExc_OverflowError) &&
+ PyIndex_Check(arg))
+ {
+ /* Here the conversion to double overflowed, but it's possible
+ to compute the log anyway. Clear the exception, convert to
+ integer and continue. */
+ PyErr_Clear();
+ arg = _PyNumber_Index(arg);
+ if (arg == NULL) {
return NULL;
}
-
- x = PyLong_AsDouble(arg);
- if (x == -1.0 && PyErr_Occurred()) {
- if (!PyErr_ExceptionMatches(PyExc_OverflowError))
- return NULL;
- /* Here the conversion to double overflowed, but it's possible
- to compute the log anyway. Clear the exception and continue. */
- PyErr_Clear();
- x = _PyLong_Frexp((PyLongObject *)arg, &e);
- assert(e >= 0);
- assert(!PyErr_Occurred());
- /* Value is ~= x * 2**e, so the log ~= log(x) + log(2) * e. */
- result = fma(func(2.0), (double)e, func(x));
- }
- else
- /* Successfully converted x to a double. */
- result = func(x);
- return PyFloat_FromDouble(result);
+ res = loghelper_int(arg, func);
+ Py_DECREF(arg);
}
-
- /* Else let libm handle it by itself. */
- return math_1(arg, func, 0, "expected a positive input, got %s");
+ return res;
}
From f5c2a41f9a6b3be95c5be9dbae0a4a3342d356dc Mon Sep 17 00:00:00 2001
From: yihong
Date: Wed, 12 Nov 2025 07:47:57 +0800
Subject: [PATCH 138/417] gh-138775: fix handle `python -m base64` stdin
correct with EOF signal (GH-138776)
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit
* fix: handle stdin correct with EOF single.
* fix: flollow the comments when pipe stdin use buffer
* Apply suggestions from code review
* fix: apply review comments in Lib/base64.py
* fix: address comments
* Reword comment and NEWS entry.
---------
Signed-off-by: yihong0618
Co-authored-by: Bénédikt Tran <10796600+picnixz@users.noreply.github.com>
Co-authored-by: Peter Bierma
Co-authored-by: Gregory P. Smith
---
Lib/base64.py | 9 ++++++++-
.../2025-09-11-15-03-37.gh-issue-138775.w7rnSx.rst | 2 ++
2 files changed, 10 insertions(+), 1 deletion(-)
create mode 100644 Misc/NEWS.d/next/Library/2025-09-11-15-03-37.gh-issue-138775.w7rnSx.rst
diff --git a/Lib/base64.py b/Lib/base64.py
index cfc57626c40..f95132a4274 100644
--- a/Lib/base64.py
+++ b/Lib/base64.py
@@ -604,7 +604,14 @@ def main():
with open(args[0], 'rb') as f:
func(f, sys.stdout.buffer)
else:
- func(sys.stdin.buffer, sys.stdout.buffer)
+ if sys.stdin.isatty():
+ # gh-138775: read terminal input data all at once to detect EOF
+ import io
+ data = sys.stdin.buffer.read()
+ buffer = io.BytesIO(data)
+ else:
+ buffer = sys.stdin.buffer
+ func(buffer, sys.stdout.buffer)
if __name__ == '__main__':
diff --git a/Misc/NEWS.d/next/Library/2025-09-11-15-03-37.gh-issue-138775.w7rnSx.rst b/Misc/NEWS.d/next/Library/2025-09-11-15-03-37.gh-issue-138775.w7rnSx.rst
new file mode 100644
index 00000000000..455c1a9925a
--- /dev/null
+++ b/Misc/NEWS.d/next/Library/2025-09-11-15-03-37.gh-issue-138775.w7rnSx.rst
@@ -0,0 +1,2 @@
+Use of ``python -m`` with :mod:`base64` has been fixed to detect input from a
+terminal so that it properly notices EOF.
From 0d7b48a8f5de5c1c6d57e1cf7194b6fb222d92e5 Mon Sep 17 00:00:00 2001
From: =?UTF-8?q?Maurycy=20Paw=C5=82owski-Wiero=C5=84ski?=
Date: Wed, 12 Nov 2025 01:03:14 +0100
Subject: [PATCH 139/417] gh-137952: update `csv.Sniffer().has_header()` docs
to describe the actual off-by-onish behavior (GH-137953)
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit
* checks 21, not 20
* Say "header" instead of "first row" to disambiguate per review.
---------
Co-authored-by: Stan Ulbrych <89152624+StanFromIreland@users.noreply.github.com>
Co-authored-by: Maurycy Pawłowski-Wieroński
---
Doc/library/csv.rst | 4 ++--
1 file changed, 2 insertions(+), 2 deletions(-)
diff --git a/Doc/library/csv.rst b/Doc/library/csv.rst
index 3ea7cd210f7..4a033d823e6 100644
--- a/Doc/library/csv.rst
+++ b/Doc/library/csv.rst
@@ -295,8 +295,8 @@ The :mod:`csv` module defines the following classes:
- the second through n-th rows contain strings where at least one value's
length differs from that of the putative header of that column.
- Twenty rows after the first row are sampled; if more than half of columns +
- rows meet the criteria, :const:`True` is returned.
+ Twenty-one rows after the header are sampled; if more than half of the
+ columns + rows meet the criteria, :const:`True` is returned.
.. note::
From 0e88be6f55f35ab045e57f9f869b893c15dcc099 Mon Sep 17 00:00:00 2001
From: Jan-Eric Nitschke <47750513+JanEricNitschke@users.noreply.github.com>
Date: Wed, 12 Nov 2025 01:32:26 +0100
Subject: [PATCH 140/417] gh-138621: Increase test coverage for csv.DictReader
and csv.Sniffer (GH-138622)
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit
* Increase test coverage for csv.DictReader and csv.Sniffer
Previously there were no tests for the DictReader fieldnames
setter, the case where a StopIteration was encountered when trying
to determine the fieldnames from the content or the case where
Sniffer could not find a delimiter.
* Revert whitespace change to comment
* Add a test that csv.Sniffer.has_header checks up to 20 rows
* Replace name and age with letter and offset
Co-authored-by: Bénédikt Tran <10796600+picnixz@users.noreply.github.com>
* Address review comment
---------
Co-authored-by: Bénédikt Tran <10796600+picnixz@users.noreply.github.com>
Co-authored-by: Gregory P. Smith <68491+gpshead@users.noreply.github.com>
---
Lib/test/test_csv.py | 31 +++++++++++++++++++++++++++++++
1 file changed, 31 insertions(+)
diff --git a/Lib/test/test_csv.py b/Lib/test/test_csv.py
index 6be6a7ae222..df79840088a 100644
--- a/Lib/test/test_csv.py
+++ b/Lib/test/test_csv.py
@@ -918,6 +918,14 @@ def test_dict_reader_fieldnames_accepts_list(self):
reader = csv.DictReader(f, fieldnames)
self.assertEqual(reader.fieldnames, fieldnames)
+ def test_dict_reader_set_fieldnames(self):
+ fieldnames = ["a", "b", "c"]
+ f = StringIO()
+ reader = csv.DictReader(f)
+ self.assertIsNone(reader.fieldnames)
+ reader.fieldnames = fieldnames
+ self.assertEqual(reader.fieldnames, fieldnames)
+
def test_dict_writer_fieldnames_rejects_iter(self):
fieldnames = ["a", "b", "c"]
f = StringIO()
@@ -933,6 +941,7 @@ def test_dict_writer_fieldnames_accepts_list(self):
def test_dict_reader_fieldnames_is_optional(self):
f = StringIO()
reader = csv.DictReader(f, fieldnames=None)
+ self.assertIsNone(reader.fieldnames)
def test_read_dict_fields(self):
with TemporaryFile("w+", encoding="utf-8") as fileobj:
@@ -1353,6 +1362,19 @@ class TestSniffer(unittest.TestCase):
ghi\0jkl
"""
+ sample15 = "\n\n\n"
+ sample16 = "abc\ndef\nghi"
+
+ sample17 = ["letter,offset"]
+ sample17.extend(f"{chr(ord('a') + i)},{i}" for i in range(20))
+ sample17.append("v,twenty_one") # 'u' was skipped
+ sample17 = '\n'.join(sample17)
+
+ sample18 = ["letter,offset"]
+ sample18.extend(f"{chr(ord('a') + i)},{i}" for i in range(21))
+ sample18.append("v,twenty_one") # 'u' was not skipped
+ sample18 = '\n'.join(sample18)
+
def test_issue43625(self):
sniffer = csv.Sniffer()
self.assertTrue(sniffer.has_header(self.sample12))
@@ -1374,6 +1396,11 @@ def test_has_header_regex_special_delimiter(self):
self.assertIs(sniffer.has_header(self.sample8), False)
self.assertIs(sniffer.has_header(self.header2 + self.sample8), True)
+ def test_has_header_checks_20_rows(self):
+ sniffer = csv.Sniffer()
+ self.assertFalse(sniffer.has_header(self.sample17))
+ self.assertTrue(sniffer.has_header(self.sample18))
+
def test_guess_quote_and_delimiter(self):
sniffer = csv.Sniffer()
for header in (";'123;4';", "'123;4';", ";'123;4'", "'123;4'"):
@@ -1423,6 +1450,10 @@ def test_delimiters(self):
self.assertEqual(dialect.quotechar, "'")
dialect = sniffer.sniff(self.sample14)
self.assertEqual(dialect.delimiter, '\0')
+ self.assertRaisesRegex(csv.Error, "Could not determine delimiter",
+ sniffer.sniff, self.sample15)
+ self.assertRaisesRegex(csv.Error, "Could not determine delimiter",
+ sniffer.sniff, self.sample16)
def test_doublequote(self):
sniffer = csv.Sniffer()
From df6676549cd67c7b83111c6fce7c546270604aa6 Mon Sep 17 00:00:00 2001
From: =?UTF-8?q?Alejandro=20Cabello=20Jim=C3=A9nez?=
<33024690+acabelloj@users.noreply.github.com>
Date: Wed, 12 Nov 2025 01:36:43 +0100
Subject: [PATCH 141/417] gh-137928: remove redundant size validation in
multiprocessing.heap (GH-137929)
remove redundant size check, malloc does it
---------
Co-authored-by: Gregory P. Smith
---
Lib/multiprocessing/heap.py | 4 ----
1 file changed, 4 deletions(-)
diff --git a/Lib/multiprocessing/heap.py b/Lib/multiprocessing/heap.py
index 6217dfe1268..5c835648395 100644
--- a/Lib/multiprocessing/heap.py
+++ b/Lib/multiprocessing/heap.py
@@ -324,10 +324,6 @@ class BufferWrapper(object):
_heap = Heap()
def __init__(self, size):
- if size < 0:
- raise ValueError("Size {0:n} out of range".format(size))
- if sys.maxsize <= size:
- raise OverflowError("Size {0:n} too large".format(size))
block = BufferWrapper._heap.malloc(size)
self._state = (block, size)
util.Finalize(self, BufferWrapper._heap.free, args=(block,))
From 9ce99c6c1901705238e4cb3ce81eb6f499e7b4f4 Mon Sep 17 00:00:00 2001
From: Adam Turner <9087854+AA-Turner@users.noreply.github.com>
Date: Wed, 12 Nov 2025 00:53:21 +0000
Subject: [PATCH 142/417] GH-137618: Require Python 3.10 to Python 3.15 for
PYTHON_FOR_REGEN (GH-137619)
* Require Python 3.11 to Python 3.15 for PYTHON_FOR_REGEN
* NEWS
* keep allowing python 3.10
---------
Co-authored-by: Gregory P. Smith
---
.../next/Build/2025-08-10-22-28-06.gh-issue-137618.FdNvIE.rst | 2 ++
configure | 2 +-
configure.ac | 2 +-
3 files changed, 4 insertions(+), 2 deletions(-)
create mode 100644 Misc/NEWS.d/next/Build/2025-08-10-22-28-06.gh-issue-137618.FdNvIE.rst
diff --git a/Misc/NEWS.d/next/Build/2025-08-10-22-28-06.gh-issue-137618.FdNvIE.rst b/Misc/NEWS.d/next/Build/2025-08-10-22-28-06.gh-issue-137618.FdNvIE.rst
new file mode 100644
index 00000000000..0b56c4c8f68
--- /dev/null
+++ b/Misc/NEWS.d/next/Build/2025-08-10-22-28-06.gh-issue-137618.FdNvIE.rst
@@ -0,0 +1,2 @@
+``PYTHON_FOR_REGEN`` now requires Python 3.10 to Python 3.15.
+Patch by Adam Turner.
diff --git a/configure b/configure
index 8463b5b5e4a..eeb24c1d844 100755
--- a/configure
+++ b/configure
@@ -3818,7 +3818,7 @@ fi
-for ac_prog in python$PACKAGE_VERSION python3.13 python3.12 python3.11 python3.10 python3 python
+for ac_prog in python$PACKAGE_VERSION python3.15 python3.14 python3.13 python3.12 python3.11 python3.10 python3 python
do
# Extract the first word of "$ac_prog", so it can be a program name with args.
set dummy $ac_prog; ac_word=$2
diff --git a/configure.ac b/configure.ac
index df94ae25e63..92adc44da0d 100644
--- a/configure.ac
+++ b/configure.ac
@@ -205,7 +205,7 @@ AC_SUBST([FREEZE_MODULE_DEPS])
AC_SUBST([PYTHON_FOR_BUILD_DEPS])
AC_CHECK_PROGS([PYTHON_FOR_REGEN],
- [python$PACKAGE_VERSION python3.13 python3.12 python3.11 python3.10 python3 python],
+ [python$PACKAGE_VERSION python3.15 python3.14 python3.13 python3.12 python3.11 python3.10 python3 python],
[python3])
AC_SUBST([PYTHON_FOR_REGEN])
From fbebca289d811669fc1980e3a135325b8542a846 Mon Sep 17 00:00:00 2001
From: Sergey Miryanov
Date: Wed, 12 Nov 2025 09:59:48 +0500
Subject: [PATCH 143/417] GH-116946: eliminate the need for the GC in the
`_thread.lock` and `_thread.RLock` (#141268)
---
Modules/_threadmodule.c | 22 +++++++++++++++-------
1 file changed, 15 insertions(+), 7 deletions(-)
diff --git a/Modules/_threadmodule.c b/Modules/_threadmodule.c
index cc8277c5783..0e22c7bd386 100644
--- a/Modules/_threadmodule.c
+++ b/Modules/_threadmodule.c
@@ -41,6 +41,7 @@ typedef struct {
typedef struct {
PyObject_HEAD
PyMutex lock;
+ PyObject *weakreflist; /* List of weak references */
} lockobject;
#define lockobject_CAST(op) ((lockobject *)(op))
@@ -48,6 +49,7 @@ typedef struct {
typedef struct {
PyObject_HEAD
_PyRecursiveMutex lock;
+ PyObject *weakreflist; /* List of weak references */
} rlockobject;
#define rlockobject_CAST(op) ((rlockobject *)(op))
@@ -767,7 +769,6 @@ static PyType_Spec ThreadHandle_Type_spec = {
static void
lock_dealloc(PyObject *self)
{
- PyObject_GC_UnTrack(self);
PyObject_ClearWeakRefs(self);
PyTypeObject *tp = Py_TYPE(self);
tp->tp_free(self);
@@ -999,6 +1000,10 @@ lock_new_impl(PyTypeObject *type)
return (PyObject *)self;
}
+static PyMemberDef lock_members[] = {
+ {"__weaklistoffset__", Py_T_PYSSIZET, offsetof(lockobject, weakreflist), Py_READONLY},
+ {NULL}
+};
static PyMethodDef lock_methods[] = {
_THREAD_LOCK_ACQUIRE_LOCK_METHODDEF
@@ -1034,8 +1039,8 @@ static PyType_Slot lock_type_slots[] = {
{Py_tp_dealloc, lock_dealloc},
{Py_tp_repr, lock_repr},
{Py_tp_doc, (void *)lock_doc},
+ {Py_tp_members, lock_members},
{Py_tp_methods, lock_methods},
- {Py_tp_traverse, _PyObject_VisitType},
{Py_tp_new, lock_new},
{0, 0}
};
@@ -1043,8 +1048,7 @@ static PyType_Slot lock_type_slots[] = {
static PyType_Spec lock_type_spec = {
.name = "_thread.lock",
.basicsize = sizeof(lockobject),
- .flags = (Py_TPFLAGS_DEFAULT | Py_TPFLAGS_HAVE_GC |
- Py_TPFLAGS_IMMUTABLETYPE | Py_TPFLAGS_MANAGED_WEAKREF),
+ .flags = (Py_TPFLAGS_DEFAULT | Py_TPFLAGS_IMMUTABLETYPE),
.slots = lock_type_slots,
};
@@ -1059,7 +1063,6 @@ rlock_locked_impl(rlockobject *self)
static void
rlock_dealloc(PyObject *self)
{
- PyObject_GC_UnTrack(self);
PyObject_ClearWeakRefs(self);
PyTypeObject *tp = Py_TYPE(self);
tp->tp_free(self);
@@ -1319,6 +1322,11 @@ _thread_RLock__at_fork_reinit_impl(rlockobject *self)
#endif /* HAVE_FORK */
+static PyMemberDef rlock_members[] = {
+ {"__weaklistoffset__", Py_T_PYSSIZET, offsetof(rlockobject, weakreflist), Py_READONLY},
+ {NULL}
+};
+
static PyMethodDef rlock_methods[] = {
_THREAD_RLOCK_ACQUIRE_METHODDEF
_THREAD_RLOCK_RELEASE_METHODDEF
@@ -1339,10 +1347,10 @@ static PyMethodDef rlock_methods[] = {
static PyType_Slot rlock_type_slots[] = {
{Py_tp_dealloc, rlock_dealloc},
{Py_tp_repr, rlock_repr},
+ {Py_tp_members, rlock_members},
{Py_tp_methods, rlock_methods},
{Py_tp_alloc, PyType_GenericAlloc},
{Py_tp_new, rlock_new},
- {Py_tp_traverse, _PyObject_VisitType},
{0, 0},
};
@@ -1350,7 +1358,7 @@ static PyType_Spec rlock_type_spec = {
.name = "_thread.RLock",
.basicsize = sizeof(rlockobject),
.flags = (Py_TPFLAGS_DEFAULT | Py_TPFLAGS_BASETYPE |
- Py_TPFLAGS_HAVE_GC | Py_TPFLAGS_IMMUTABLETYPE | Py_TPFLAGS_MANAGED_WEAKREF),
+ Py_TPFLAGS_IMMUTABLETYPE),
.slots = rlock_type_slots,
};
From ef474cfafbdf3aa383fb1334a7ab95cef9834ced Mon Sep 17 00:00:00 2001
From: Kumar Aditya
Date: Wed, 12 Nov 2025 10:47:38 +0530
Subject: [PATCH 144/417] gh-103847: fix cancellation safety of
`asyncio.create_subprocess_exec` (#140805)
---
Lib/asyncio/base_subprocess.py | 11 +++++
Lib/test/test_asyncio/test_subprocess.py | 40 ++++++++++++++++++-
...-10-31-13-57-55.gh-issue-103847.VM7TnW.rst | 1 +
3 files changed, 51 insertions(+), 1 deletion(-)
create mode 100644 Misc/NEWS.d/next/Library/2025-10-31-13-57-55.gh-issue-103847.VM7TnW.rst
diff --git a/Lib/asyncio/base_subprocess.py b/Lib/asyncio/base_subprocess.py
index d40af422e61..321a4e5d5d1 100644
--- a/Lib/asyncio/base_subprocess.py
+++ b/Lib/asyncio/base_subprocess.py
@@ -26,6 +26,7 @@ def __init__(self, loop, protocol, args, shell,
self._pending_calls = collections.deque()
self._pipes = {}
self._finished = False
+ self._pipes_connected = False
if stdin == subprocess.PIPE:
self._pipes[0] = None
@@ -213,6 +214,7 @@ async def _connect_pipes(self, waiter):
else:
if waiter is not None and not waiter.cancelled():
waiter.set_result(None)
+ self._pipes_connected = True
def _call(self, cb, *data):
if self._pending_calls is not None:
@@ -256,6 +258,15 @@ def _try_finish(self):
assert not self._finished
if self._returncode is None:
return
+ if not self._pipes_connected:
+ # self._pipes_connected can be False if not all pipes were connected
+ # because either the process failed to start or the self._connect_pipes task
+ # got cancelled. In this broken state we consider all pipes disconnected and
+ # to avoid hanging forever in self._wait as otherwise _exit_waiters
+ # would never be woken up, we wake them up here.
+ for waiter in self._exit_waiters:
+ if not waiter.cancelled():
+ waiter.set_result(self._returncode)
if all(p is not None and p.disconnected
for p in self._pipes.values()):
self._finished = True
diff --git a/Lib/test/test_asyncio/test_subprocess.py b/Lib/test/test_asyncio/test_subprocess.py
index 3a17c169c34..bf301740741 100644
--- a/Lib/test/test_asyncio/test_subprocess.py
+++ b/Lib/test/test_asyncio/test_subprocess.py
@@ -11,7 +11,7 @@
from asyncio import subprocess
from test.test_asyncio import utils as test_utils
from test import support
-from test.support import os_helper
+from test.support import os_helper, warnings_helper, gc_collect
if not support.has_subprocess_support:
raise unittest.SkipTest("test module requires subprocess")
@@ -879,6 +879,44 @@ async def main():
self.loop.run_until_complete(main())
+ @warnings_helper.ignore_warnings(category=ResourceWarning)
+ def test_subprocess_read_pipe_cancelled(self):
+ async def main():
+ loop = asyncio.get_running_loop()
+ loop.connect_read_pipe = mock.AsyncMock(side_effect=asyncio.CancelledError)
+ with self.assertRaises(asyncio.CancelledError):
+ await asyncio.create_subprocess_exec(*PROGRAM_BLOCKED, stderr=asyncio.subprocess.PIPE)
+
+ asyncio.run(main())
+ gc_collect()
+
+ @warnings_helper.ignore_warnings(category=ResourceWarning)
+ def test_subprocess_write_pipe_cancelled(self):
+ async def main():
+ loop = asyncio.get_running_loop()
+ loop.connect_write_pipe = mock.AsyncMock(side_effect=asyncio.CancelledError)
+ with self.assertRaises(asyncio.CancelledError):
+ await asyncio.create_subprocess_exec(*PROGRAM_BLOCKED, stdin=asyncio.subprocess.PIPE)
+
+ asyncio.run(main())
+ gc_collect()
+
+ @warnings_helper.ignore_warnings(category=ResourceWarning)
+ def test_subprocess_read_write_pipe_cancelled(self):
+ async def main():
+ loop = asyncio.get_running_loop()
+ loop.connect_read_pipe = mock.AsyncMock(side_effect=asyncio.CancelledError)
+ loop.connect_write_pipe = mock.AsyncMock(side_effect=asyncio.CancelledError)
+ with self.assertRaises(asyncio.CancelledError):
+ await asyncio.create_subprocess_exec(
+ *PROGRAM_BLOCKED,
+ stdin=asyncio.subprocess.PIPE,
+ stdout=asyncio.subprocess.PIPE,
+ stderr=asyncio.subprocess.PIPE,
+ )
+
+ asyncio.run(main())
+ gc_collect()
if sys.platform != 'win32':
# Unix
diff --git a/Misc/NEWS.d/next/Library/2025-10-31-13-57-55.gh-issue-103847.VM7TnW.rst b/Misc/NEWS.d/next/Library/2025-10-31-13-57-55.gh-issue-103847.VM7TnW.rst
new file mode 100644
index 00000000000..e14af7d9708
--- /dev/null
+++ b/Misc/NEWS.d/next/Library/2025-10-31-13-57-55.gh-issue-103847.VM7TnW.rst
@@ -0,0 +1 @@
+Fix hang when cancelling process created by :func:`asyncio.create_subprocess_exec` or :func:`asyncio.create_subprocess_shell`. Patch by Kumar Aditya.
From f1b7961ccfa050e9c80622fff1b3cdada46f9aab Mon Sep 17 00:00:00 2001
From: Kumar Aditya
Date: Wed, 12 Nov 2025 12:51:43 +0530
Subject: [PATCH 145/417] GH-116946: revert eliminate the need for the GC in
the `_thread.lock` and `_thread.RLock` (#141448)
Revert "GH-116946: eliminate the need for the GC in the `_thread.lock` and `_thread.RLock` (#141268)"
This reverts commit fbebca289d811669fc1980e3a135325b8542a846.
---
Modules/_threadmodule.c | 22 +++++++---------------
1 file changed, 7 insertions(+), 15 deletions(-)
diff --git a/Modules/_threadmodule.c b/Modules/_threadmodule.c
index 0e22c7bd386..cc8277c5783 100644
--- a/Modules/_threadmodule.c
+++ b/Modules/_threadmodule.c
@@ -41,7 +41,6 @@ typedef struct {
typedef struct {
PyObject_HEAD
PyMutex lock;
- PyObject *weakreflist; /* List of weak references */
} lockobject;
#define lockobject_CAST(op) ((lockobject *)(op))
@@ -49,7 +48,6 @@ typedef struct {
typedef struct {
PyObject_HEAD
_PyRecursiveMutex lock;
- PyObject *weakreflist; /* List of weak references */
} rlockobject;
#define rlockobject_CAST(op) ((rlockobject *)(op))
@@ -769,6 +767,7 @@ static PyType_Spec ThreadHandle_Type_spec = {
static void
lock_dealloc(PyObject *self)
{
+ PyObject_GC_UnTrack(self);
PyObject_ClearWeakRefs(self);
PyTypeObject *tp = Py_TYPE(self);
tp->tp_free(self);
@@ -1000,10 +999,6 @@ lock_new_impl(PyTypeObject *type)
return (PyObject *)self;
}
-static PyMemberDef lock_members[] = {
- {"__weaklistoffset__", Py_T_PYSSIZET, offsetof(lockobject, weakreflist), Py_READONLY},
- {NULL}
-};
static PyMethodDef lock_methods[] = {
_THREAD_LOCK_ACQUIRE_LOCK_METHODDEF
@@ -1039,8 +1034,8 @@ static PyType_Slot lock_type_slots[] = {
{Py_tp_dealloc, lock_dealloc},
{Py_tp_repr, lock_repr},
{Py_tp_doc, (void *)lock_doc},
- {Py_tp_members, lock_members},
{Py_tp_methods, lock_methods},
+ {Py_tp_traverse, _PyObject_VisitType},
{Py_tp_new, lock_new},
{0, 0}
};
@@ -1048,7 +1043,8 @@ static PyType_Slot lock_type_slots[] = {
static PyType_Spec lock_type_spec = {
.name = "_thread.lock",
.basicsize = sizeof(lockobject),
- .flags = (Py_TPFLAGS_DEFAULT | Py_TPFLAGS_IMMUTABLETYPE),
+ .flags = (Py_TPFLAGS_DEFAULT | Py_TPFLAGS_HAVE_GC |
+ Py_TPFLAGS_IMMUTABLETYPE | Py_TPFLAGS_MANAGED_WEAKREF),
.slots = lock_type_slots,
};
@@ -1063,6 +1059,7 @@ rlock_locked_impl(rlockobject *self)
static void
rlock_dealloc(PyObject *self)
{
+ PyObject_GC_UnTrack(self);
PyObject_ClearWeakRefs(self);
PyTypeObject *tp = Py_TYPE(self);
tp->tp_free(self);
@@ -1322,11 +1319,6 @@ _thread_RLock__at_fork_reinit_impl(rlockobject *self)
#endif /* HAVE_FORK */
-static PyMemberDef rlock_members[] = {
- {"__weaklistoffset__", Py_T_PYSSIZET, offsetof(rlockobject, weakreflist), Py_READONLY},
- {NULL}
-};
-
static PyMethodDef rlock_methods[] = {
_THREAD_RLOCK_ACQUIRE_METHODDEF
_THREAD_RLOCK_RELEASE_METHODDEF
@@ -1347,10 +1339,10 @@ static PyMethodDef rlock_methods[] = {
static PyType_Slot rlock_type_slots[] = {
{Py_tp_dealloc, rlock_dealloc},
{Py_tp_repr, rlock_repr},
- {Py_tp_members, rlock_members},
{Py_tp_methods, rlock_methods},
{Py_tp_alloc, PyType_GenericAlloc},
{Py_tp_new, rlock_new},
+ {Py_tp_traverse, _PyObject_VisitType},
{0, 0},
};
@@ -1358,7 +1350,7 @@ static PyType_Spec rlock_type_spec = {
.name = "_thread.RLock",
.basicsize = sizeof(rlockobject),
.flags = (Py_TPFLAGS_DEFAULT | Py_TPFLAGS_BASETYPE |
- Py_TPFLAGS_IMMUTABLETYPE),
+ Py_TPFLAGS_HAVE_GC | Py_TPFLAGS_IMMUTABLETYPE | Py_TPFLAGS_MANAGED_WEAKREF),
.slots = rlock_type_slots,
};
From 35908265b09ac39b67116bfdfe8a053be09e6d8f Mon Sep 17 00:00:00 2001
From: Mark Byrne <31762852+mbyrnepr2@users.noreply.github.com>
Date: Wed, 12 Nov 2025 09:20:55 +0100
Subject: [PATCH 146/417] gh-75593: Add support of bytes and path-like paths in
wave.open() (GH-140951)
---
Doc/library/wave.rst | 9 ++++++--
Lib/test/test_wave.py | 22 +++++++++++++++++++
Lib/wave.py | 5 +++--
...5-11-04-12-16-13.gh-issue-75593.EFVhKR.rst | 1 +
4 files changed, 33 insertions(+), 4 deletions(-)
create mode 100644 Misc/NEWS.d/next/Library/2025-11-04-12-16-13.gh-issue-75593.EFVhKR.rst
diff --git a/Doc/library/wave.rst b/Doc/library/wave.rst
index a3f5bfd5e2f..7ff2c97992c 100644
--- a/Doc/library/wave.rst
+++ b/Doc/library/wave.rst
@@ -25,8 +25,9 @@ The :mod:`wave` module defines the following function and exception:
.. function:: open(file, mode=None)
- If *file* is a string, open the file by that name, otherwise treat it as a
- file-like object. *mode* can be:
+ If *file* is a string, a :term:`path-like object` or a
+ :term:`bytes-like object` open the file by that name, otherwise treat it as
+ a file-like object. *mode* can be:
``'rb'``
Read only mode.
@@ -52,6 +53,10 @@ The :mod:`wave` module defines the following function and exception:
.. versionchanged:: 3.4
Added support for unseekable files.
+ .. versionchanged:: 3.15
+ Added support for :term:`path-like objects `
+ and :term:`bytes-like objects `.
+
.. exception:: Error
An error raised when something is impossible because it violates the WAV
diff --git a/Lib/test/test_wave.py b/Lib/test/test_wave.py
index 226b1aa84bd..4c21f165537 100644
--- a/Lib/test/test_wave.py
+++ b/Lib/test/test_wave.py
@@ -1,9 +1,11 @@
import unittest
from test import audiotests
from test import support
+from test.support.os_helper import FakePath
import io
import os
import struct
+import tempfile
import sys
import wave
@@ -206,5 +208,25 @@ def test_open_in_write_raises(self):
self.assertIsNone(cm.unraisable)
+class WaveOpen(unittest.TestCase):
+ def test_open_pathlike(self):
+ """It is possible to use `wave.read` and `wave.write` with a path-like object"""
+ with tempfile.NamedTemporaryFile(delete_on_close=False) as fp:
+ cases = (
+ FakePath(fp.name),
+ FakePath(os.fsencode(fp.name)),
+ os.fsencode(fp.name),
+ )
+ for fake_path in cases:
+ with self.subTest(fake_path):
+ with wave.open(fake_path, 'wb') as f:
+ f.setnchannels(1)
+ f.setsampwidth(2)
+ f.setframerate(44100)
+
+ with wave.open(fake_path, 'rb') as f:
+ pass
+
+
if __name__ == '__main__':
unittest.main()
diff --git a/Lib/wave.py b/Lib/wave.py
index 5af745e2217..056bd6aab7f 100644
--- a/Lib/wave.py
+++ b/Lib/wave.py
@@ -69,6 +69,7 @@
from collections import namedtuple
import builtins
+import os
import struct
import sys
@@ -274,7 +275,7 @@ def initfp(self, file):
def __init__(self, f):
self._i_opened_the_file = None
- if isinstance(f, str):
+ if isinstance(f, (bytes, str, os.PathLike)):
f = builtins.open(f, 'rb')
self._i_opened_the_file = f
# else, assume it is an open file object already
@@ -431,7 +432,7 @@ class Wave_write:
def __init__(self, f):
self._i_opened_the_file = None
- if isinstance(f, str):
+ if isinstance(f, (bytes, str, os.PathLike)):
f = builtins.open(f, 'wb')
self._i_opened_the_file = f
try:
diff --git a/Misc/NEWS.d/next/Library/2025-11-04-12-16-13.gh-issue-75593.EFVhKR.rst b/Misc/NEWS.d/next/Library/2025-11-04-12-16-13.gh-issue-75593.EFVhKR.rst
new file mode 100644
index 00000000000..9a31af9c110
--- /dev/null
+++ b/Misc/NEWS.d/next/Library/2025-11-04-12-16-13.gh-issue-75593.EFVhKR.rst
@@ -0,0 +1 @@
+Add support of :term:`path-like objects ` and :term:`bytes-like objects ` in :func:`wave.open`.
From 909f76dab91f028edd2ae7bd589d3975996de9e1 Mon Sep 17 00:00:00 2001
From: Petr Viktorin
Date: Wed, 12 Nov 2025 09:42:56 +0100
Subject: [PATCH 147/417] gh-141376: Rename _AsyncioDebug to _Py_AsyncioDebug
(GH-141391)
---
Modules/_asynciomodule.c | 4 ++--
Tools/c-analyzer/cpython/ignored.tsv | 2 +-
2 files changed, 3 insertions(+), 3 deletions(-)
diff --git a/Modules/_asynciomodule.c b/Modules/_asynciomodule.c
index 1f58b1fb350..9b2b7011244 100644
--- a/Modules/_asynciomodule.c
+++ b/Modules/_asynciomodule.c
@@ -119,7 +119,7 @@ typedef struct _Py_AsyncioModuleDebugOffsets {
} asyncio_thread_state;
} Py_AsyncioModuleDebugOffsets;
-GENERATE_DEBUG_SECTION(AsyncioDebug, Py_AsyncioModuleDebugOffsets _AsyncioDebug)
+GENERATE_DEBUG_SECTION(AsyncioDebug, Py_AsyncioModuleDebugOffsets _Py_AsyncioDebug)
= {.asyncio_task_object = {
.size = sizeof(TaskObj),
.task_name = offsetof(TaskObj, task_name),
@@ -4338,7 +4338,7 @@ module_init(asyncio_state *state)
goto fail;
}
- state->debug_offsets = &_AsyncioDebug;
+ state->debug_offsets = &_Py_AsyncioDebug;
Py_DECREF(module);
return 0;
diff --git a/Tools/c-analyzer/cpython/ignored.tsv b/Tools/c-analyzer/cpython/ignored.tsv
index 8b73189fb07..11a3cd794ff 100644
--- a/Tools/c-analyzer/cpython/ignored.tsv
+++ b/Tools/c-analyzer/cpython/ignored.tsv
@@ -56,7 +56,7 @@ Python/pyhash.c - _Py_HashSecret -
Python/parking_lot.c - buckets -
## data needed for introspecting asyncio state from debuggers and profilers
-Modules/_asynciomodule.c - _AsyncioDebug -
+Modules/_asynciomodule.c - _Py_AsyncioDebug -
##################################
From 6f988b08d122e44848e89c04ad1e10c25d072cc7 Mon Sep 17 00:00:00 2001
From: Cody Maloney
Date: Wed, 12 Nov 2025 01:37:48 -0800
Subject: [PATCH 148/417] gh-85524: Raise "UnsupportedOperation" on
FileIO.readall (#141214)
io.UnsupportedOperation is a subclass of OSError and recommended by
io.IOBase for this case; matches other read methods on io.FileIO.
---
Lib/test/test_io/test_general.py | 1 +
.../2025-11-07-12-25-46.gh-issue-85524.9SWFIC.rst | 3 +++
Modules/_io/clinic/fileio.c.h | 14 +++++++++-----
Modules/_io/fileio.c | 13 ++++++++++---
4 files changed, 23 insertions(+), 8 deletions(-)
create mode 100644 Misc/NEWS.d/next/Library/2025-11-07-12-25-46.gh-issue-85524.9SWFIC.rst
diff --git a/Lib/test/test_io/test_general.py b/Lib/test/test_io/test_general.py
index a1cdd6876c2..f0677b01ea5 100644
--- a/Lib/test/test_io/test_general.py
+++ b/Lib/test/test_io/test_general.py
@@ -125,6 +125,7 @@ def test_invalid_operations(self):
self.assertRaises(exc, fp.readline)
with self.open(os_helper.TESTFN, "wb", buffering=0) as fp:
self.assertRaises(exc, fp.read)
+ self.assertRaises(exc, fp.readall)
self.assertRaises(exc, fp.readline)
with self.open(os_helper.TESTFN, "rb", buffering=0) as fp:
self.assertRaises(exc, fp.write, b"blah")
diff --git a/Misc/NEWS.d/next/Library/2025-11-07-12-25-46.gh-issue-85524.9SWFIC.rst b/Misc/NEWS.d/next/Library/2025-11-07-12-25-46.gh-issue-85524.9SWFIC.rst
new file mode 100644
index 00000000000..3e4fd1a5897
--- /dev/null
+++ b/Misc/NEWS.d/next/Library/2025-11-07-12-25-46.gh-issue-85524.9SWFIC.rst
@@ -0,0 +1,3 @@
+Update ``io.FileIO.readall``, an implementation of :meth:`io.RawIOBase.readall`,
+to follow :class:`io.IOBase` guidelines and raise :exc:`io.UnsupportedOperation`
+when a file is in "w" mode rather than :exc:`OSError`
diff --git a/Modules/_io/clinic/fileio.c.h b/Modules/_io/clinic/fileio.c.h
index 04870b1c890..96c31ce8d6f 100644
--- a/Modules/_io/clinic/fileio.c.h
+++ b/Modules/_io/clinic/fileio.c.h
@@ -277,15 +277,19 @@ PyDoc_STRVAR(_io_FileIO_readall__doc__,
"data is available (EAGAIN is returned before bytes are read) returns None.");
#define _IO_FILEIO_READALL_METHODDEF \
- {"readall", (PyCFunction)_io_FileIO_readall, METH_NOARGS, _io_FileIO_readall__doc__},
+ {"readall", _PyCFunction_CAST(_io_FileIO_readall), METH_METHOD|METH_FASTCALL|METH_KEYWORDS, _io_FileIO_readall__doc__},
static PyObject *
-_io_FileIO_readall_impl(fileio *self);
+_io_FileIO_readall_impl(fileio *self, PyTypeObject *cls);
static PyObject *
-_io_FileIO_readall(PyObject *self, PyObject *Py_UNUSED(ignored))
+_io_FileIO_readall(PyObject *self, PyTypeObject *cls, PyObject *const *args, Py_ssize_t nargs, PyObject *kwnames)
{
- return _io_FileIO_readall_impl((fileio *)self);
+ if (nargs || (kwnames && PyTuple_GET_SIZE(kwnames))) {
+ PyErr_SetString(PyExc_TypeError, "readall() takes no arguments");
+ return NULL;
+ }
+ return _io_FileIO_readall_impl((fileio *)self, cls);
}
PyDoc_STRVAR(_io_FileIO_read__doc__,
@@ -543,4 +547,4 @@ _io_FileIO_isatty(PyObject *self, PyObject *Py_UNUSED(ignored))
#ifndef _IO_FILEIO_TRUNCATE_METHODDEF
#define _IO_FILEIO_TRUNCATE_METHODDEF
#endif /* !defined(_IO_FILEIO_TRUNCATE_METHODDEF) */
-/*[clinic end generated code: output=1902fac9e39358aa input=a9049054013a1b77]*/
+/*[clinic end generated code: output=2e48f3df2f189170 input=a9049054013a1b77]*/
diff --git a/Modules/_io/fileio.c b/Modules/_io/fileio.c
index 2544ff4ea91..5d7741fdd83 100644
--- a/Modules/_io/fileio.c
+++ b/Modules/_io/fileio.c
@@ -728,6 +728,9 @@ new_buffersize(fileio *self, size_t currentsize)
@permit_long_docstring_body
_io.FileIO.readall
+ cls: defining_class
+ /
+
Read all data from the file, returned as bytes.
Reads until either there is an error or read() returns size 0 (indicates EOF).
@@ -738,8 +741,8 @@ data is available (EAGAIN is returned before bytes are read) returns None.
[clinic start generated code]*/
static PyObject *
-_io_FileIO_readall_impl(fileio *self)
-/*[clinic end generated code: output=faa0292b213b4022 input=10d8b2ec403302dc]*/
+_io_FileIO_readall_impl(fileio *self, PyTypeObject *cls)
+/*[clinic end generated code: output=d546737ec895c462 input=cecda40bf9961299]*/
{
Py_off_t pos, end;
PyBytesWriter *writer;
@@ -750,6 +753,10 @@ _io_FileIO_readall_impl(fileio *self)
if (self->fd < 0) {
return err_closed();
}
+ if (!self->readable) {
+ _PyIO_State *state = get_io_state_by_cls(cls);
+ return err_mode(state, "reading");
+ }
if (self->stat_atopen != NULL && self->stat_atopen->st_size < _PY_READ_MAX) {
end = (Py_off_t)self->stat_atopen->st_size;
@@ -873,7 +880,7 @@ _io_FileIO_read_impl(fileio *self, PyTypeObject *cls, Py_ssize_t size)
}
if (size < 0)
- return _io_FileIO_readall_impl(self);
+ return _io_FileIO_readall_impl(self, cls);
if (size > _PY_READ_MAX) {
size = _PY_READ_MAX;
From 20f53df07d42c495a08c73a3d54b8dd9098a62f0 Mon Sep 17 00:00:00 2001
From: Sergey B Kirpichev
Date: Wed, 12 Nov 2025 12:50:44 +0300
Subject: [PATCH 149/417] gh-141370: document undefined behavior of Py_ABS()
(GH-141439)
---
Doc/c-api/intro.rst | 4 ++++
1 file changed, 4 insertions(+)
diff --git a/Doc/c-api/intro.rst b/Doc/c-api/intro.rst
index 6e1a9dcb355..c76cc2f70ec 100644
--- a/Doc/c-api/intro.rst
+++ b/Doc/c-api/intro.rst
@@ -121,6 +121,10 @@ complete listing.
Return the absolute value of ``x``.
+ If the result cannot be represented (for example, if ``x`` has
+ :c:macro:`!INT_MIN` value for :c:expr:`int` type), the behavior is
+ undefined.
+
.. versionadded:: 3.3
.. c:macro:: Py_ALWAYS_INLINE
From 7d54374f9c7d91e0ef90c4ad84baf10073cf1d8a Mon Sep 17 00:00:00 2001
From: Cody Maloney
Date: Wed, 12 Nov 2025 01:57:05 -0800
Subject: [PATCH 150/417] gh-141311: Avoid assertion in BytesIO.readinto()
(GH-141333)
Fix error in assertion which causes failure if pos is equal to PY_SSIZE_T_MAX.
Fix undefined behavior in read() and readinto() if pos is larger that the size
of the underlying buffer.
---
Lib/test/test_io/test_memoryio.py | 14 ++++++++++++++
...025-11-09-18-55-13.gh-issue-141311.qZ3swc.rst | 2 ++
Modules/_io/bytesio.c | 16 +++++++++++++---
3 files changed, 29 insertions(+), 3 deletions(-)
create mode 100644 Misc/NEWS.d/next/Library/2025-11-09-18-55-13.gh-issue-141311.qZ3swc.rst
diff --git a/Lib/test/test_io/test_memoryio.py b/Lib/test/test_io/test_memoryio.py
index 63998a86c45..bb023735e21 100644
--- a/Lib/test/test_io/test_memoryio.py
+++ b/Lib/test/test_io/test_memoryio.py
@@ -54,6 +54,12 @@ def testSeek(self):
self.assertEqual(buf[3:], bytesIo.read())
self.assertRaises(TypeError, bytesIo.seek, 0.0)
+ self.assertEqual(sys.maxsize, bytesIo.seek(sys.maxsize))
+ self.assertEqual(self.EOF, bytesIo.read(4))
+
+ self.assertEqual(sys.maxsize - 2, bytesIo.seek(sys.maxsize - 2))
+ self.assertEqual(self.EOF, bytesIo.read(4))
+
def testTell(self):
buf = self.buftype("1234567890")
bytesIo = self.ioclass(buf)
@@ -552,6 +558,14 @@ def test_relative_seek(self):
memio.seek(1, 1)
self.assertEqual(memio.read(), buf[1:])
+ def test_issue141311(self):
+ memio = self.ioclass()
+ # Seek allows PY_SSIZE_T_MAX, read should handle that.
+ # Past end of buffer read should always return 0 (EOF).
+ self.assertEqual(sys.maxsize, memio.seek(sys.maxsize))
+ buf = bytearray(2)
+ self.assertEqual(0, memio.readinto(buf))
+
def test_unicode(self):
memio = self.ioclass()
diff --git a/Misc/NEWS.d/next/Library/2025-11-09-18-55-13.gh-issue-141311.qZ3swc.rst b/Misc/NEWS.d/next/Library/2025-11-09-18-55-13.gh-issue-141311.qZ3swc.rst
new file mode 100644
index 00000000000..bb425ce5df3
--- /dev/null
+++ b/Misc/NEWS.d/next/Library/2025-11-09-18-55-13.gh-issue-141311.qZ3swc.rst
@@ -0,0 +1,2 @@
+Fix assertion failure in :func:`!io.BytesIO.readinto` and undefined behavior
+arising when read position is above capcity in :class:`io.BytesIO`.
diff --git a/Modules/_io/bytesio.c b/Modules/_io/bytesio.c
index d6bfb93177c..96611823ab6 100644
--- a/Modules/_io/bytesio.c
+++ b/Modules/_io/bytesio.c
@@ -436,6 +436,13 @@ read_bytes_lock_held(bytesio *self, Py_ssize_t size)
return Py_NewRef(self->buf);
}
+ /* gh-141311: Avoid undefined behavior when self->pos (limit PY_SSIZE_T_MAX)
+ is beyond the size of self->buf. Assert above validates size is always in
+ bounds. When self->pos is out of bounds calling code sets size to 0. */
+ if (size == 0) {
+ return PyBytes_FromStringAndSize(NULL, 0);
+ }
+
output = PyBytes_AS_STRING(self->buf) + self->pos;
self->pos += size;
return PyBytes_FromStringAndSize(output, size);
@@ -609,11 +616,14 @@ _io_BytesIO_readinto_impl(bytesio *self, Py_buffer *buffer)
n = self->string_size - self->pos;
if (len > n) {
len = n;
- if (len < 0)
- len = 0;
+ if (len < 0) {
+ /* gh-141311: Avoid undefined behavior when self->pos (limit
+ PY_SSIZE_T_MAX) points beyond the size of self->buf. */
+ return PyLong_FromSsize_t(0);
+ }
}
- assert(self->pos + len < PY_SSIZE_T_MAX);
+ assert(self->pos + len <= PY_SSIZE_T_MAX);
assert(len >= 0);
memcpy(buffer->buf, PyBytes_AS_STRING(self->buf) + self->pos, len);
self->pos += len;
From 23d85a2a3fb029172ea15c6e596f64f8c2868ed3 Mon Sep 17 00:00:00 2001
From: Sergey B Kirpichev
Date: Wed, 12 Nov 2025 13:06:29 +0300
Subject: [PATCH 151/417] gh-141042: fix sNaN's packing for mixed
floating-point formats (#141107)
---
Lib/test/test_capi/test_float.py | 54 +++++++++++++++----
...-11-06-06-28-14.gh-issue-141042.brOioJ.rst | 3 ++
Objects/floatobject.c | 16 ++++--
3 files changed, 59 insertions(+), 14 deletions(-)
create mode 100644 Misc/NEWS.d/next/C_API/2025-11-06-06-28-14.gh-issue-141042.brOioJ.rst
diff --git a/Lib/test/test_capi/test_float.py b/Lib/test/test_capi/test_float.py
index 983b991b4f1..df7017e6436 100644
--- a/Lib/test/test_capi/test_float.py
+++ b/Lib/test/test_capi/test_float.py
@@ -29,6 +29,23 @@
NAN = float("nan")
+def make_nan(size, sign, quiet, payload=None):
+ if size == 8:
+ payload_mask = 0x7ffffffffffff
+ i = (sign << 63) + (0x7ff << 52) + (quiet << 51)
+ elif size == 4:
+ payload_mask = 0x3fffff
+ i = (sign << 31) + (0xff << 23) + (quiet << 22)
+ elif size == 2:
+ payload_mask = 0x1ff
+ i = (sign << 15) + (0x1f << 10) + (quiet << 9)
+ else:
+ raise ValueError("size must be either 2, 4, or 8")
+ if payload is None:
+ payload = random.randint(not quiet, payload_mask)
+ return i + payload
+
+
class CAPIFloatTest(unittest.TestCase):
def test_check(self):
# Test PyFloat_Check()
@@ -202,16 +219,7 @@ def test_pack_unpack_roundtrip_for_nans(self):
# HP PA RISC uses 0 for quiet, see:
# https://en.wikipedia.org/wiki/NaN#Encoding
signaling = 1
- quiet = int(not signaling)
- if size == 8:
- payload = random.randint(signaling, 0x7ffffffffffff)
- i = (sign << 63) + (0x7ff << 52) + (quiet << 51) + payload
- elif size == 4:
- payload = random.randint(signaling, 0x3fffff)
- i = (sign << 31) + (0xff << 23) + (quiet << 22) + payload
- elif size == 2:
- payload = random.randint(signaling, 0x1ff)
- i = (sign << 15) + (0x1f << 10) + (quiet << 9) + payload
+ i = make_nan(size, sign, not signaling)
data = bytes.fromhex(f'{i:x}')
for endian in (BIG_ENDIAN, LITTLE_ENDIAN):
with self.subTest(data=data, size=size, endian=endian):
@@ -221,6 +229,32 @@ def test_pack_unpack_roundtrip_for_nans(self):
self.assertTrue(math.isnan(value))
self.assertEqual(data1, data2)
+ @unittest.skipUnless(HAVE_IEEE_754, "requires IEEE 754")
+ @unittest.skipUnless(sys.maxsize != 2147483647, "requires 64-bit mode")
+ def test_pack_unpack_nans_for_different_formats(self):
+ pack = _testcapi.float_pack
+ unpack = _testcapi.float_unpack
+
+ for endian in (BIG_ENDIAN, LITTLE_ENDIAN):
+ with self.subTest(endian=endian):
+ byteorder = "big" if endian == BIG_ENDIAN else "little"
+
+ # Convert sNaN to qNaN, if payload got truncated
+ data = make_nan(8, 0, False, 0x80001).to_bytes(8, byteorder)
+ snan_low = unpack(data, endian)
+ qnan4 = make_nan(4, 0, True, 0).to_bytes(4, byteorder)
+ qnan2 = make_nan(2, 0, True, 0).to_bytes(2, byteorder)
+ self.assertEqual(pack(4, snan_low, endian), qnan4)
+ self.assertEqual(pack(2, snan_low, endian), qnan2)
+
+ # Preserve NaN type, if payload not truncated
+ data = make_nan(8, 0, False, 0x80000000001).to_bytes(8, byteorder)
+ snan_high = unpack(data, endian)
+ snan4 = make_nan(4, 0, False, 16384).to_bytes(4, byteorder)
+ snan2 = make_nan(2, 0, False, 2).to_bytes(2, byteorder)
+ self.assertEqual(pack(4, snan_high, endian), snan4)
+ self.assertEqual(pack(2, snan_high, endian), snan2)
+
if __name__ == "__main__":
unittest.main()
diff --git a/Misc/NEWS.d/next/C_API/2025-11-06-06-28-14.gh-issue-141042.brOioJ.rst b/Misc/NEWS.d/next/C_API/2025-11-06-06-28-14.gh-issue-141042.brOioJ.rst
new file mode 100644
index 00000000000..22a1aa1f405
--- /dev/null
+++ b/Misc/NEWS.d/next/C_API/2025-11-06-06-28-14.gh-issue-141042.brOioJ.rst
@@ -0,0 +1,3 @@
+Make qNaN in :c:func:`PyFloat_Pack2` and :c:func:`PyFloat_Pack4`, if while
+conversion to a narrower precision floating-point format --- the remaining
+after truncation payload will be zero. Patch by Sergey B Kirpichev.
diff --git a/Objects/floatobject.c b/Objects/floatobject.c
index 1fefb12803e..ef613efe4e7 100644
--- a/Objects/floatobject.c
+++ b/Objects/floatobject.c
@@ -2030,6 +2030,10 @@ PyFloat_Pack2(double x, char *data, int le)
memcpy(&v, &x, sizeof(v));
v &= 0xffc0000000000ULL;
bits = (unsigned short)(v >> 42); /* NaN's type & payload */
+ /* set qNaN if no payload */
+ if (!bits) {
+ bits |= (1<<9);
+ }
}
else {
sign = (x < 0.0);
@@ -2202,16 +2206,16 @@ PyFloat_Pack4(double x, char *data, int le)
if ((v & (1ULL << 51)) == 0) {
uint32_t u32;
memcpy(&u32, &y, 4);
- u32 &= ~(1 << 22); /* make sNaN */
+ /* if have payload, make sNaN */
+ if (u32 & 0x3fffff) {
+ u32 &= ~(1 << 22);
+ }
memcpy(&y, &u32, 4);
}
#else
uint32_t u32;
memcpy(&u32, &y, 4);
- if ((v & (1ULL << 51)) == 0) {
- u32 &= ~(1 << 22);
- }
/* Workaround RISC-V: "If a NaN value is converted to a
* different floating-point type, the result is the
* canonical NaN of the new type". The canonical NaN here
@@ -2222,6 +2226,10 @@ PyFloat_Pack4(double x, char *data, int le)
/* add payload */
u32 -= (u32 & 0x3fffff);
u32 += (uint32_t)((v & 0x7ffffffffffffULL) >> 29);
+ /* if have payload, make sNaN */
+ if ((v & (1ULL << 51)) == 0 && (u32 & 0x3fffff)) {
+ u32 &= ~(1 << 22);
+ }
memcpy(&y, &u32, 4);
#endif
From 70748bdbea872a84dd8eadad9b48c73e218d2e1f Mon Sep 17 00:00:00 2001
From: Jacob Austin Lincoln <99031153+lincolnj1@users.noreply.github.com>
Date: Wed, 12 Nov 2025 02:07:21 -0800
Subject: [PATCH 152/417] gh-131116: Fix inspect.getdoc() to work with
cached_property objects (GH-131165)
---
Doc/library/inspect.rst | 3 ++
Lib/inspect.py | 6 +++
Lib/test/test_inspect/inspect_fodder3.py | 39 +++++++++++++++++++
Lib/test/test_inspect/test_inspect.py | 20 ++++++++++
...-03-12-18-57-10.gh-issue-131116.uTpwXZ.rst | 2 +
5 files changed, 70 insertions(+)
create mode 100644 Lib/test/test_inspect/inspect_fodder3.py
create mode 100644 Misc/NEWS.d/next/Library/2025-03-12-18-57-10.gh-issue-131116.uTpwXZ.rst
diff --git a/Doc/library/inspect.rst b/Doc/library/inspect.rst
index aff53b78c4a..13a352cbdb2 100644
--- a/Doc/library/inspect.rst
+++ b/Doc/library/inspect.rst
@@ -639,6 +639,9 @@ Retrieving source code
.. versionchanged:: next
Added parameters *inherit_class_doc* and *fallback_to_class_doc*.
+ Documentation strings on :class:`~functools.cached_property`
+ objects are now inherited if not overriden.
+
.. function:: getcomments(object)
diff --git a/Lib/inspect.py b/Lib/inspect.py
index bb17848b444..8e7511b3af0 100644
--- a/Lib/inspect.py
+++ b/Lib/inspect.py
@@ -747,6 +747,12 @@ def _finddoc(obj, *, search_in_class=True):
cls = _findclass(obj.fget)
if cls is None or getattr(cls, name) is not obj:
return None
+ # Should be tested before ismethoddescriptor()
+ elif isinstance(obj, functools.cached_property):
+ name = obj.attrname
+ cls = _findclass(obj.func)
+ if cls is None or getattr(cls, name) is not obj:
+ return None
elif ismethoddescriptor(obj) or isdatadescriptor(obj):
name = obj.__name__
cls = obj.__objclass__
diff --git a/Lib/test/test_inspect/inspect_fodder3.py b/Lib/test/test_inspect/inspect_fodder3.py
new file mode 100644
index 00000000000..ea2481edf93
--- /dev/null
+++ b/Lib/test/test_inspect/inspect_fodder3.py
@@ -0,0 +1,39 @@
+from functools import cached_property
+
+# docstring in parent, inherited in child
+class ParentInheritDoc:
+ @cached_property
+ def foo(self):
+ """docstring for foo defined in parent"""
+
+class ChildInheritDoc(ParentInheritDoc):
+ pass
+
+class ChildInheritDefineDoc(ParentInheritDoc):
+ @cached_property
+ def foo(self):
+ pass
+
+# Redefine foo as something other than cached_property
+class ChildPropertyFoo(ParentInheritDoc):
+ @property
+ def foo(self):
+ """docstring for the property foo"""
+
+class ChildMethodFoo(ParentInheritDoc):
+ def foo(self):
+ """docstring for the method foo"""
+
+# docstring in child but not parent
+class ParentNoDoc:
+ @cached_property
+ def foo(self):
+ pass
+
+class ChildNoDoc(ParentNoDoc):
+ pass
+
+class ChildDefineDoc(ParentNoDoc):
+ @cached_property
+ def foo(self):
+ """docstring for foo defined in child"""
diff --git a/Lib/test/test_inspect/test_inspect.py b/Lib/test/test_inspect/test_inspect.py
index 24fd4a2fa62..dd3b7d9c5b4 100644
--- a/Lib/test/test_inspect/test_inspect.py
+++ b/Lib/test/test_inspect/test_inspect.py
@@ -46,6 +46,7 @@
from test.test_inspect import inspect_fodder as mod
from test.test_inspect import inspect_fodder2 as mod2
+from test.test_inspect import inspect_fodder3 as mod3
from test.test_inspect import inspect_stringized_annotations
from test.test_inspect import inspect_deferred_annotations
@@ -714,6 +715,25 @@ class B(A):
b.__doc__ = 'Instance'
self.assertEqual(inspect.getdoc(b, fallback_to_class_doc=False), 'Instance')
+ def test_getdoc_inherited_cached_property(self):
+ doc = inspect.getdoc(mod3.ParentInheritDoc.foo)
+ self.assertEqual(doc, 'docstring for foo defined in parent')
+ self.assertEqual(inspect.getdoc(mod3.ChildInheritDoc.foo), doc)
+ self.assertEqual(inspect.getdoc(mod3.ChildInheritDefineDoc.foo), doc)
+
+ def test_getdoc_redefine_cached_property_as_other(self):
+ self.assertEqual(inspect.getdoc(mod3.ChildPropertyFoo.foo),
+ 'docstring for the property foo')
+ self.assertEqual(inspect.getdoc(mod3.ChildMethodFoo.foo),
+ 'docstring for the method foo')
+
+ def test_getdoc_define_cached_property(self):
+ self.assertEqual(inspect.getdoc(mod3.ChildDefineDoc.foo),
+ 'docstring for foo defined in child')
+
+ def test_getdoc_nodoc_inherited(self):
+ self.assertIsNone(inspect.getdoc(mod3.ChildNoDoc.foo))
+
@unittest.skipIf(MISSING_C_DOCSTRINGS, "test requires docstrings")
def test_finddoc(self):
finddoc = inspect._finddoc
diff --git a/Misc/NEWS.d/next/Library/2025-03-12-18-57-10.gh-issue-131116.uTpwXZ.rst b/Misc/NEWS.d/next/Library/2025-03-12-18-57-10.gh-issue-131116.uTpwXZ.rst
new file mode 100644
index 00000000000..f5e60ab6e8c
--- /dev/null
+++ b/Misc/NEWS.d/next/Library/2025-03-12-18-57-10.gh-issue-131116.uTpwXZ.rst
@@ -0,0 +1,2 @@
+:func:`inspect.getdoc` now correctly returns an inherited docstring on
+:class:`~functools.cached_property` objects if none is given in a subclass.
From c6f3dd6a506a9bb1808c070e5ef5cf345a3bedc8 Mon Sep 17 00:00:00 2001
From: Rani Pinchuk <33353578+rani-pinchuk@users.noreply.github.com>
Date: Wed, 12 Nov 2025 13:35:01 +0100
Subject: [PATCH 153/417] gh-98896: resource_tracker: use json&base64 to allow
arbitrary shared memory names (GH-138473)
---
Lib/multiprocessing/resource_tracker.py | 60 ++++++++++++++++---
Lib/test/_test_multiprocessing.py | 43 +++++++++++++
...5-09-03-20-18-39.gh-issue-98896.tjez89.rst | 2 +
3 files changed, 97 insertions(+), 8 deletions(-)
create mode 100644 Misc/NEWS.d/next/Library/2025-09-03-20-18-39.gh-issue-98896.tjez89.rst
diff --git a/Lib/multiprocessing/resource_tracker.py b/Lib/multiprocessing/resource_tracker.py
index 38fcaed48fa..b0f9099f4a5 100644
--- a/Lib/multiprocessing/resource_tracker.py
+++ b/Lib/multiprocessing/resource_tracker.py
@@ -15,6 +15,7 @@
# this resource tracker process, "killall python" would probably leave unlinked
# resources.
+import base64
import os
import signal
import sys
@@ -22,6 +23,8 @@
import warnings
from collections import deque
+import json
+
from . import spawn
from . import util
@@ -196,6 +199,17 @@ def _launch(self):
finally:
os.close(r)
+ def _make_probe_message(self):
+ """Return a JSON-encoded probe message."""
+ return (
+ json.dumps(
+ {"cmd": "PROBE", "rtype": "noop"},
+ ensure_ascii=True,
+ separators=(",", ":"),
+ )
+ + "\n"
+ ).encode("ascii")
+
def _ensure_running_and_write(self, msg=None):
with self._lock:
if self._lock._recursion_count() > 1:
@@ -207,7 +221,7 @@ def _ensure_running_and_write(self, msg=None):
if self._fd is not None:
# resource tracker was launched before, is it still running?
if msg is None:
- to_send = b'PROBE:0:noop\n'
+ to_send = self._make_probe_message()
else:
to_send = msg
try:
@@ -234,7 +248,7 @@ def _check_alive(self):
try:
# We cannot use send here as it calls ensure_running, creating
# a cycle.
- os.write(self._fd, b'PROBE:0:noop\n')
+ os.write(self._fd, self._make_probe_message())
except OSError:
return False
else:
@@ -253,11 +267,25 @@ def _write(self, msg):
assert nbytes == len(msg), f"{nbytes=} != {len(msg)=}"
def _send(self, cmd, name, rtype):
- msg = f"{cmd}:{name}:{rtype}\n".encode("ascii")
- if len(msg) > 512:
- # posix guarantees that writes to a pipe of less than PIPE_BUF
- # bytes are atomic, and that PIPE_BUF >= 512
- raise ValueError('msg too long')
+ # POSIX guarantees that writes to a pipe of less than PIPE_BUF (512 on Linux)
+ # bytes are atomic. Therefore, we want the message to be shorter than 512 bytes.
+ # POSIX shm_open() and sem_open() require the name, including its leading slash,
+ # to be at most NAME_MAX bytes (255 on Linux)
+ # With json.dump(..., ensure_ascii=True) every non-ASCII byte becomes a 6-char
+ # escape like \uDC80.
+ # As we want the overall message to be kept atomic and therefore smaller than 512,
+ # we encode encode the raw name bytes with URL-safe Base64 - so a 255 long name
+ # will not exceed 340 bytes.
+ b = name.encode('utf-8', 'surrogateescape')
+ if len(b) > 255:
+ raise ValueError('shared memory name too long (max 255 bytes)')
+ b64 = base64.urlsafe_b64encode(b).decode('ascii')
+
+ payload = {"cmd": cmd, "rtype": rtype, "base64_name": b64}
+ msg = (json.dumps(payload, ensure_ascii=True, separators=(",", ":")) + "\n").encode("ascii")
+
+ # The entire JSON message is guaranteed < PIPE_BUF (512 bytes) by construction.
+ assert len(msg) <= 512, f"internal error: message too long ({len(msg)} bytes)"
self._ensure_running_and_write(msg)
@@ -290,7 +318,23 @@ def main(fd):
with open(fd, 'rb') as f:
for line in f:
try:
- cmd, name, rtype = line.strip().decode('ascii').split(':')
+ try:
+ obj = json.loads(line.decode('ascii'))
+ except Exception as e:
+ raise ValueError("malformed resource_tracker message: %r" % (line,)) from e
+
+ cmd = obj["cmd"]
+ rtype = obj["rtype"]
+ b64 = obj.get("base64_name", "")
+
+ if not isinstance(cmd, str) or not isinstance(rtype, str) or not isinstance(b64, str):
+ raise ValueError("malformed resource_tracker fields: %r" % (obj,))
+
+ try:
+ name = base64.urlsafe_b64decode(b64).decode('utf-8', 'surrogateescape')
+ except ValueError as e:
+ raise ValueError("malformed resource_tracker base64_name: %r" % (b64,)) from e
+
cleanup_func = _CLEANUP_FUNCS.get(rtype, None)
if cleanup_func is None:
raise ValueError(
diff --git a/Lib/test/_test_multiprocessing.py b/Lib/test/_test_multiprocessing.py
index 850744e47d0..0f9c5c22225 100644
--- a/Lib/test/_test_multiprocessing.py
+++ b/Lib/test/_test_multiprocessing.py
@@ -7364,3 +7364,46 @@ def test_forkpty(self):
res = assert_python_failure("-c", code, PYTHONWARNINGS='error')
self.assertIn(b'DeprecationWarning', res.err)
self.assertIn(b'is multi-threaded, use of forkpty() may lead to deadlocks in the child', res.err)
+
+@unittest.skipUnless(HAS_SHMEM, "requires multiprocessing.shared_memory")
+class TestSharedMemoryNames(unittest.TestCase):
+ def test_that_shared_memory_name_with_colons_has_no_resource_tracker_errors(self):
+ # Test script that creates and cleans up shared memory with colon in name
+ test_script = textwrap.dedent("""
+ import sys
+ from multiprocessing import shared_memory
+ import time
+
+ # Test various patterns of colons in names
+ test_names = [
+ "a:b",
+ "a:b:c",
+ "test:name:with:many:colons",
+ ":starts:with:colon",
+ "ends:with:colon:",
+ "::double::colons::",
+ "name\\nwithnewline",
+ "name-with-trailing-newline\\n",
+ "\\nname-starts-with-newline",
+ "colons:and\\nnewlines:mix",
+ "multi\\nline\\nname",
+ ]
+
+ for name in test_names:
+ try:
+ shm = shared_memory.SharedMemory(create=True, size=100, name=name)
+ shm.buf[:5] = b'hello' # Write something to the shared memory
+ shm.close()
+ shm.unlink()
+
+ except Exception as e:
+ print(f"Error with name '{name}': {e}", file=sys.stderr)
+ sys.exit(1)
+
+ print("SUCCESS")
+ """)
+
+ rc, out, err = assert_python_ok("-c", test_script)
+ self.assertIn(b"SUCCESS", out)
+ self.assertNotIn(b"traceback", err.lower(), err)
+ self.assertNotIn(b"resource_tracker.py", err, err)
diff --git a/Misc/NEWS.d/next/Library/2025-09-03-20-18-39.gh-issue-98896.tjez89.rst b/Misc/NEWS.d/next/Library/2025-09-03-20-18-39.gh-issue-98896.tjez89.rst
new file mode 100644
index 00000000000..6831499c0af
--- /dev/null
+++ b/Misc/NEWS.d/next/Library/2025-09-03-20-18-39.gh-issue-98896.tjez89.rst
@@ -0,0 +1,2 @@
+Fix a failure in multiprocessing resource_tracker when SharedMemory names contain colons.
+Patch by Rani Pinchuk.
From e2026731f5680022bd016b8b5ca5841c82e9574c Mon Sep 17 00:00:00 2001
From: Sergey B Kirpichev
Date: Wed, 12 Nov 2025 15:44:49 +0300
Subject: [PATCH 154/417] gh-141004: soft-deprecate Py_INFINITY macro (#141033)
Co-authored-by: Victor Stinner
---
Doc/c-api/conversion.rst | 2 +-
Doc/c-api/float.rst | 7 +++++--
Doc/whatsnew/3.14.rst | 2 +-
Doc/whatsnew/3.15.rst | 4 ++++
Include/floatobject.h | 16 +++++++--------
Include/internal/pycore_pymath.h | 6 +++---
Include/pymath.h | 3 ++-
...-11-05-04-38-16.gh-issue-141004.rJL43P.rst | 1 +
Modules/cmathmodule.c | 6 +++---
Modules/mathmodule.c | 20 +++++++++----------
Objects/complexobject.c | 8 ++++----
Objects/floatobject.c | 2 +-
Python/pystrtod.c | 4 ++--
13 files changed, 45 insertions(+), 36 deletions(-)
create mode 100644 Misc/NEWS.d/next/C_API/2025-11-05-04-38-16.gh-issue-141004.rJL43P.rst
diff --git a/Doc/c-api/conversion.rst b/Doc/c-api/conversion.rst
index 533e5460da8..a18bbf4e0e3 100644
--- a/Doc/c-api/conversion.rst
+++ b/Doc/c-api/conversion.rst
@@ -105,7 +105,7 @@ The following functions provide locale-independent string to number conversions.
If ``s`` represents a value that is too large to store in a float
(for example, ``"1e500"`` is such a string on many platforms) then
- if ``overflow_exception`` is ``NULL`` return ``Py_INFINITY`` (with
+ if ``overflow_exception`` is ``NULL`` return :c:macro:`!INFINITY` (with
an appropriate sign) and don't set any exception. Otherwise,
``overflow_exception`` must point to a Python exception object;
raise that exception and return ``-1.0``. In both cases, set
diff --git a/Doc/c-api/float.rst b/Doc/c-api/float.rst
index eae4792af7d..b6020533a2b 100644
--- a/Doc/c-api/float.rst
+++ b/Doc/c-api/float.rst
@@ -83,8 +83,11 @@ Floating-Point Objects
This macro expands a to constant expression of type :c:expr:`double`, that
represents the positive infinity.
- On most platforms, this is equivalent to the :c:macro:`!INFINITY` macro from
- the C11 standard ```` header.
+ It is equivalent to the :c:macro:`!INFINITY` macro from the C11 standard
+ ```` header.
+
+ .. deprecated:: 3.15
+ The macro is soft deprecated.
.. c:macro:: Py_NAN
diff --git a/Doc/whatsnew/3.14.rst b/Doc/whatsnew/3.14.rst
index 1a2fbda0c4c..9459b73bcb5 100644
--- a/Doc/whatsnew/3.14.rst
+++ b/Doc/whatsnew/3.14.rst
@@ -3045,7 +3045,7 @@ Deprecated C APIs
-----------------
* The :c:macro:`!Py_HUGE_VAL` macro is now :term:`soft deprecated`.
- Use :c:macro:`!Py_INFINITY` instead.
+ Use :c:macro:`!INFINITY` instead.
(Contributed by Sergey B Kirpichev in :gh:`120026`.)
* The :c:macro:`!Py_IS_NAN`, :c:macro:`!Py_IS_INFINITY`,
diff --git a/Doc/whatsnew/3.15.rst b/Doc/whatsnew/3.15.rst
index c543b6e6c2a..f0fd49c9033 100644
--- a/Doc/whatsnew/3.15.rst
+++ b/Doc/whatsnew/3.15.rst
@@ -1095,6 +1095,10 @@ Deprecated C APIs
since 3.15 and will be removed in 3.17.
(Contributed by Nikita Sobolev in :gh:`136355`.)
+* :c:macro:`!Py_INFINITY` macro is :term:`soft deprecated`,
+ use the C11 standard ```` :c:macro:`!INFINITY` instead.
+ (Contributed by Sergey B Kirpichev in :gh:`141004`.)
+
* :c:macro:`!Py_MATH_El` and :c:macro:`!Py_MATH_PIl` are deprecated
since 3.15 and will be removed in 3.20.
(Contributed by Sergey B Kirpichev in :gh:`141004`.)
diff --git a/Include/floatobject.h b/Include/floatobject.h
index 4d24a76edd5..814337b070a 100644
--- a/Include/floatobject.h
+++ b/Include/floatobject.h
@@ -18,14 +18,14 @@ PyAPI_DATA(PyTypeObject) PyFloat_Type;
#define Py_RETURN_NAN return PyFloat_FromDouble(Py_NAN)
-#define Py_RETURN_INF(sign) \
- do { \
- if (copysign(1., sign) == 1.) { \
- return PyFloat_FromDouble(Py_INFINITY); \
- } \
- else { \
- return PyFloat_FromDouble(-Py_INFINITY); \
- } \
+#define Py_RETURN_INF(sign) \
+ do { \
+ if (copysign(1., sign) == 1.) { \
+ return PyFloat_FromDouble(INFINITY); \
+ } \
+ else { \
+ return PyFloat_FromDouble(-INFINITY); \
+ } \
} while(0)
PyAPI_FUNC(double) PyFloat_GetMax(void);
diff --git a/Include/internal/pycore_pymath.h b/Include/internal/pycore_pymath.h
index eea8996ba68..4fcac3aab8b 100644
--- a/Include/internal/pycore_pymath.h
+++ b/Include/internal/pycore_pymath.h
@@ -33,7 +33,7 @@ extern "C" {
static inline void _Py_ADJUST_ERANGE1(double x)
{
if (errno == 0) {
- if (x == Py_INFINITY || x == -Py_INFINITY) {
+ if (x == INFINITY || x == -INFINITY) {
errno = ERANGE;
}
}
@@ -44,8 +44,8 @@ static inline void _Py_ADJUST_ERANGE1(double x)
static inline void _Py_ADJUST_ERANGE2(double x, double y)
{
- if (x == Py_INFINITY || x == -Py_INFINITY ||
- y == Py_INFINITY || y == -Py_INFINITY)
+ if (x == INFINITY || x == -INFINITY ||
+ y == INFINITY || y == -INFINITY)
{
if (errno == 0) {
errno = ERANGE;
diff --git a/Include/pymath.h b/Include/pymath.h
index 0f9f0f3b299..7cfe441365d 100644
--- a/Include/pymath.h
+++ b/Include/pymath.h
@@ -45,13 +45,14 @@
#define Py_IS_FINITE(X) isfinite(X)
// Py_INFINITY: Value that evaluates to a positive double infinity.
+// Soft deprecated since Python 3.15, use INFINITY instead.
#ifndef Py_INFINITY
# define Py_INFINITY ((double)INFINITY)
#endif
/* Py_HUGE_VAL should always be the same as Py_INFINITY. But historically
* this was not reliable and Python did not require IEEE floats and C99
- * conformity. The macro was soft deprecated in Python 3.14, use Py_INFINITY instead.
+ * conformity. The macro was soft deprecated in Python 3.14, use INFINITY instead.
*/
#ifndef Py_HUGE_VAL
# define Py_HUGE_VAL HUGE_VAL
diff --git a/Misc/NEWS.d/next/C_API/2025-11-05-04-38-16.gh-issue-141004.rJL43P.rst b/Misc/NEWS.d/next/C_API/2025-11-05-04-38-16.gh-issue-141004.rJL43P.rst
new file mode 100644
index 00000000000..a054f8eda6f
--- /dev/null
+++ b/Misc/NEWS.d/next/C_API/2025-11-05-04-38-16.gh-issue-141004.rJL43P.rst
@@ -0,0 +1 @@
+The :c:macro:`!Py_INFINITY` macro is :term:`soft deprecated`.
diff --git a/Modules/cmathmodule.c b/Modules/cmathmodule.c
index a4ea5557a6a..aee3e4f343d 100644
--- a/Modules/cmathmodule.c
+++ b/Modules/cmathmodule.c
@@ -150,7 +150,7 @@ special_type(double d)
#define P14 0.25*Py_MATH_PI
#define P12 0.5*Py_MATH_PI
#define P34 0.75*Py_MATH_PI
-#define INF Py_INFINITY
+#define INF INFINITY
#define N Py_NAN
#define U -9.5426319407711027e33 /* unlikely value, used as placeholder */
@@ -1186,11 +1186,11 @@ cmath_exec(PyObject *mod)
if (PyModule_Add(mod, "tau", PyFloat_FromDouble(Py_MATH_TAU)) < 0) {
return -1;
}
- if (PyModule_Add(mod, "inf", PyFloat_FromDouble(Py_INFINITY)) < 0) {
+ if (PyModule_Add(mod, "inf", PyFloat_FromDouble(INFINITY)) < 0) {
return -1;
}
- Py_complex infj = {0.0, Py_INFINITY};
+ Py_complex infj = {0.0, INFINITY};
if (PyModule_Add(mod, "infj", PyComplex_FromCComplex(infj)) < 0) {
return -1;
}
diff --git a/Modules/mathmodule.c b/Modules/mathmodule.c
index de1886451ed..11c46c987e1 100644
--- a/Modules/mathmodule.c
+++ b/Modules/mathmodule.c
@@ -395,7 +395,7 @@ m_tgamma(double x)
if (x == 0.0) {
errno = EDOM;
/* tgamma(+-0.0) = +-inf, divide-by-zero */
- return copysign(Py_INFINITY, x);
+ return copysign(INFINITY, x);
}
/* integer arguments */
@@ -426,7 +426,7 @@ m_tgamma(double x)
}
else {
errno = ERANGE;
- return Py_INFINITY;
+ return INFINITY;
}
}
@@ -490,14 +490,14 @@ m_lgamma(double x)
if (isnan(x))
return x; /* lgamma(nan) = nan */
else
- return Py_INFINITY; /* lgamma(+-inf) = +inf */
+ return INFINITY; /* lgamma(+-inf) = +inf */
}
/* integer arguments */
if (x == floor(x) && x <= 2.0) {
if (x <= 0.0) {
errno = EDOM; /* lgamma(n) = inf, divide-by-zero for */
- return Py_INFINITY; /* integers n <= 0 */
+ return INFINITY; /* integers n <= 0 */
}
else {
return 0.0; /* lgamma(1) = lgamma(2) = 0.0 */
@@ -633,7 +633,7 @@ m_log(double x)
return log(x);
errno = EDOM;
if (x == 0.0)
- return -Py_INFINITY; /* log(0) = -inf */
+ return -INFINITY; /* log(0) = -inf */
else
return Py_NAN; /* log(-ve) = nan */
}
@@ -676,7 +676,7 @@ m_log2(double x)
}
else if (x == 0.0) {
errno = EDOM;
- return -Py_INFINITY; /* log2(0) = -inf, divide-by-zero */
+ return -INFINITY; /* log2(0) = -inf, divide-by-zero */
}
else {
errno = EDOM;
@@ -692,7 +692,7 @@ m_log10(double x)
return log10(x);
errno = EDOM;
if (x == 0.0)
- return -Py_INFINITY; /* log10(0) = -inf */
+ return -INFINITY; /* log10(0) = -inf */
else
return Py_NAN; /* log10(-ve) = nan */
}
@@ -1500,7 +1500,7 @@ math_ldexp_impl(PyObject *module, double x, PyObject *i)
errno = 0;
} else if (exp > INT_MAX) {
/* overflow */
- r = copysign(Py_INFINITY, x);
+ r = copysign(INFINITY, x);
errno = ERANGE;
} else if (exp < INT_MIN) {
/* underflow to +-0 */
@@ -2983,7 +2983,7 @@ math_ulp_impl(PyObject *module, double x)
if (isinf(x)) {
return x;
}
- double inf = Py_INFINITY;
+ double inf = INFINITY;
double x2 = nextafter(x, inf);
if (isinf(x2)) {
/* special case: x is the largest positive representable float */
@@ -3007,7 +3007,7 @@ math_exec(PyObject *module)
if (PyModule_Add(module, "tau", PyFloat_FromDouble(Py_MATH_TAU)) < 0) {
return -1;
}
- if (PyModule_Add(module, "inf", PyFloat_FromDouble(Py_INFINITY)) < 0) {
+ if (PyModule_Add(module, "inf", PyFloat_FromDouble(INFINITY)) < 0) {
return -1;
}
if (PyModule_Add(module, "nan", PyFloat_FromDouble(fabs(Py_NAN))) < 0) {
diff --git a/Objects/complexobject.c b/Objects/complexobject.c
index 6247376a0e6..3612c2699a5 100644
--- a/Objects/complexobject.c
+++ b/Objects/complexobject.c
@@ -139,8 +139,8 @@ _Py_c_prod(Py_complex z, Py_complex w)
recalc = 1;
}
if (recalc) {
- r.real = Py_INFINITY*(a*c - b*d);
- r.imag = Py_INFINITY*(a*d + b*c);
+ r.real = INFINITY*(a*c - b*d);
+ r.imag = INFINITY*(a*d + b*c);
}
}
@@ -229,8 +229,8 @@ _Py_c_quot(Py_complex a, Py_complex b)
{
const double x = copysign(isinf(a.real) ? 1.0 : 0.0, a.real);
const double y = copysign(isinf(a.imag) ? 1.0 : 0.0, a.imag);
- r.real = Py_INFINITY * (x*b.real + y*b.imag);
- r.imag = Py_INFINITY * (y*b.real - x*b.imag);
+ r.real = INFINITY * (x*b.real + y*b.imag);
+ r.imag = INFINITY * (y*b.real - x*b.imag);
}
else if ((isinf(abs_breal) || isinf(abs_bimag))
&& isfinite(a.real) && isfinite(a.imag))
diff --git a/Objects/floatobject.c b/Objects/floatobject.c
index ef613efe4e7..78006783c6e 100644
--- a/Objects/floatobject.c
+++ b/Objects/floatobject.c
@@ -2415,7 +2415,7 @@ PyFloat_Unpack2(const char *data, int le)
if (e == 0x1f) {
if (f == 0) {
/* Infinity */
- return sign ? -Py_INFINITY : Py_INFINITY;
+ return sign ? -INFINITY : INFINITY;
}
else {
/* NaN */
diff --git a/Python/pystrtod.c b/Python/pystrtod.c
index 7b74f613ed5..e8aca939d1f 100644
--- a/Python/pystrtod.c
+++ b/Python/pystrtod.c
@@ -43,7 +43,7 @@ _Py_parse_inf_or_nan(const char *p, char **endptr)
s += 3;
if (case_insensitive_match(s, "inity"))
s += 5;
- retval = negate ? -Py_INFINITY : Py_INFINITY;
+ retval = negate ? -INFINITY : INFINITY;
}
else if (case_insensitive_match(s, "nan")) {
s += 3;
@@ -286,7 +286,7 @@ _PyOS_ascii_strtod(const char *nptr, char **endptr)
string, -1.0 is returned and again ValueError is raised.
On overflow (e.g., when trying to convert '1e500' on an IEEE 754 machine),
- if overflow_exception is NULL then +-Py_INFINITY is returned, and no Python
+ if overflow_exception is NULL then +-INFINITY is returned, and no Python
exception is raised. Otherwise, overflow_exception should point to
a Python exception, this exception will be raised, -1.0 will be returned,
and *endptr will point just past the end of the converted value.
From f963864cb54c2e7364b2c850485c6bf25479f6f2 Mon Sep 17 00:00:00 2001
From: yihong
Date: Wed, 12 Nov 2025 20:45:43 +0800
Subject: [PATCH 155/417] gh-141464: a typo in profiling sampling when can not
run warning in linux (#141465)
---
Lib/profiling/sampling/__main__.py | 2 +-
1 file changed, 1 insertion(+), 1 deletion(-)
diff --git a/Lib/profiling/sampling/__main__.py b/Lib/profiling/sampling/__main__.py
index a76ca62e2cd..cd1425b8b9c 100644
--- a/Lib/profiling/sampling/__main__.py
+++ b/Lib/profiling/sampling/__main__.py
@@ -15,7 +15,7 @@
"""
LINUX_PERMISSION_ERROR = """
-🔒 Tachyon was unable to acess process memory. This could be because tachyon
+🔒 Tachyon was unable to access process memory. This could be because tachyon
has insufficient privileges (the required capability is CAP_SYS_PTRACE).
Unprivileged processes cannot trace processes that they cannot send signals
to or those running set-user-ID/set-group-ID programs, for security reasons.
From 88aeff8eabefdc13b6fb29edb3cde618f743a034 Mon Sep 17 00:00:00 2001
From: Stan Ulbrych <89152624+StanFromIreland@users.noreply.github.com>
Date: Wed, 12 Nov 2025 14:22:01 +0000
Subject: [PATCH 156/417] gh-87710: Update mime type for ``.ai`` (#141239)
---
Doc/whatsnew/3.15.rst | 4 +++-
Lib/mimetypes.py | 2 +-
Lib/test/test_mimetypes.py | 1 +
.../Library/2025-11-08-13-03-10.gh-issue-87710.XJeZlP.rst | 1 +
4 files changed, 6 insertions(+), 2 deletions(-)
create mode 100644 Misc/NEWS.d/next/Library/2025-11-08-13-03-10.gh-issue-87710.XJeZlP.rst
diff --git a/Doc/whatsnew/3.15.rst b/Doc/whatsnew/3.15.rst
index f0fd49c9033..c6089f63dee 100644
--- a/Doc/whatsnew/3.15.rst
+++ b/Doc/whatsnew/3.15.rst
@@ -472,7 +472,9 @@ mimetypes
* Add ``application/node`` MIME type for ``.cjs`` extension. (Contributed by John Franey in :gh:`140937`.)
* Add ``application/toml``. (Contributed by Gil Forcada in :gh:`139959`.)
* Rename ``application/x-texinfo`` to ``application/texinfo``.
- (Contributed by Charlie Lin in :gh:`140165`)
+ (Contributed by Charlie Lin in :gh:`140165`.)
+* Changed the MIME type for ``.ai`` files to ``application/pdf``.
+ (Contributed by Stan Ulbrych in :gh:`141239`.)
mmap
diff --git a/Lib/mimetypes.py b/Lib/mimetypes.py
index d6896fc4042..42477713c78 100644
--- a/Lib/mimetypes.py
+++ b/Lib/mimetypes.py
@@ -497,9 +497,9 @@ def _default_mime_types():
'.oda' : 'application/oda',
'.ogx' : 'application/ogg',
'.pdf' : 'application/pdf',
+ '.ai' : 'application/pdf',
'.p7c' : 'application/pkcs7-mime',
'.ps' : 'application/postscript',
- '.ai' : 'application/postscript',
'.eps' : 'application/postscript',
'.texi' : 'application/texinfo',
'.texinfo': 'application/texinfo',
diff --git a/Lib/test/test_mimetypes.py b/Lib/test/test_mimetypes.py
index 746984ec0ca..73414498359 100644
--- a/Lib/test/test_mimetypes.py
+++ b/Lib/test/test_mimetypes.py
@@ -229,6 +229,7 @@ def check_extensions():
("application/octet-stream", ".bin"),
("application/gzip", ".gz"),
("application/ogg", ".ogx"),
+ ("application/pdf", ".pdf"),
("application/postscript", ".ps"),
("application/texinfo", ".texi"),
("application/toml", ".toml"),
diff --git a/Misc/NEWS.d/next/Library/2025-11-08-13-03-10.gh-issue-87710.XJeZlP.rst b/Misc/NEWS.d/next/Library/2025-11-08-13-03-10.gh-issue-87710.XJeZlP.rst
new file mode 100644
index 00000000000..62073280e32
--- /dev/null
+++ b/Misc/NEWS.d/next/Library/2025-11-08-13-03-10.gh-issue-87710.XJeZlP.rst
@@ -0,0 +1 @@
+:mod:`mimetypes`: Update mime type for ``.ai`` files to ``application/pdf``.
From 2ac738d325a6934e39fecb097f43d4d4ed97a2b9 Mon Sep 17 00:00:00 2001
From: M Bussonnier
Date: Wed, 12 Nov 2025 16:20:08 +0100
Subject: [PATCH 157/417] gh-132657: add regression test for `PySet_Contains`
with unhashable type (#141411)
---
Modules/_testlimitedcapi/set.c | 47 ++++++++++++++++++++++++++++++++++
1 file changed, 47 insertions(+)
diff --git a/Modules/_testlimitedcapi/set.c b/Modules/_testlimitedcapi/set.c
index 35da5fa5f00..34ed6b1d60b 100644
--- a/Modules/_testlimitedcapi/set.c
+++ b/Modules/_testlimitedcapi/set.c
@@ -155,6 +155,51 @@ test_frozenset_add_in_capi(PyObject *self, PyObject *Py_UNUSED(obj))
return NULL;
}
+static PyObject *
+test_set_contains_does_not_convert_unhashable_key(PyObject *self, PyObject *Py_UNUSED(obj))
+{
+ // See https://docs.python.org/3/c-api/set.html#c.PySet_Contains
+ PyObject *outer_set = PySet_New(NULL);
+
+ PyObject *needle = PySet_New(NULL);
+ if (needle == NULL) {
+ Py_DECREF(outer_set);
+ return NULL;
+ }
+
+ PyObject *num = PyLong_FromLong(42);
+ if (num == NULL) {
+ Py_DECREF(outer_set);
+ Py_DECREF(needle);
+ return NULL;
+ }
+
+ if (PySet_Add(needle, num) < 0) {
+ Py_DECREF(outer_set);
+ Py_DECREF(needle);
+ Py_DECREF(num);
+ return NULL;
+ }
+
+ int result = PySet_Contains(outer_set, needle);
+
+ Py_DECREF(num);
+ Py_DECREF(needle);
+ Py_DECREF(outer_set);
+
+ if (result < 0) {
+ if (PyErr_ExceptionMatches(PyExc_TypeError)) {
+ PyErr_Clear();
+ Py_RETURN_NONE;
+ }
+ return NULL;
+ }
+
+ PyErr_SetString(PyExc_AssertionError,
+ "PySet_Contains should have raised TypeError for unhashable key");
+ return NULL;
+}
+
static PyMethodDef test_methods[] = {
{"set_check", set_check, METH_O},
{"set_checkexact", set_checkexact, METH_O},
@@ -174,6 +219,8 @@ static PyMethodDef test_methods[] = {
{"set_clear", set_clear, METH_O},
{"test_frozenset_add_in_capi", test_frozenset_add_in_capi, METH_NOARGS},
+ {"test_set_contains_does_not_convert_unhashable_key",
+ test_set_contains_does_not_convert_unhashable_key, METH_NOARGS},
{NULL},
};
From f1330b35b8eb43904dfed0656acde80c08d63176 Mon Sep 17 00:00:00 2001
From: Stan Ulbrych <89152624+StanFromIreland@users.noreply.github.com>
Date: Wed, 12 Nov 2025 16:37:54 +0000
Subject: [PATCH 158/417] gh-141004: Document `Py_MATH_{E, PI, TAU}` constants
(#141373)
---
Doc/c-api/float.rst | 17 +++++++++++++++++
1 file changed, 17 insertions(+)
diff --git a/Doc/c-api/float.rst b/Doc/c-api/float.rst
index b6020533a2b..79de5daaa90 100644
--- a/Doc/c-api/float.rst
+++ b/Doc/c-api/float.rst
@@ -99,6 +99,11 @@ Floating-Point Objects
the C11 standard ```` header.
+.. c:macro:: Py_MATH_E
+
+ The definition (accurate for a :c:expr:`double` type) of the :data:`math.e` constant.
+
+
.. c:macro:: Py_MATH_El
High precision (long double) definition of :data:`~math.e` constant.
@@ -106,6 +111,11 @@ Floating-Point Objects
.. deprecated-removed:: 3.15 3.20
+.. c:macro:: Py_MATH_PI
+
+ The definition (accurate for a :c:expr:`double` type) of the :data:`math.pi` constant.
+
+
.. c:macro:: Py_MATH_PIl
High precision (long double) definition of :data:`~math.pi` constant.
@@ -113,6 +123,13 @@ Floating-Point Objects
.. deprecated-removed:: 3.15 3.20
+.. c:macro:: Py_MATH_TAU
+
+ The definition (accurate for a :c:expr:`double` type) of the :data:`math.tau` constant.
+
+ .. versionadded:: 3.6
+
+
.. c:macro:: Py_RETURN_NAN
Return :data:`math.nan` from a function.
From 9cd5427d9619b96db20d0347a136b3d331af71ae Mon Sep 17 00:00:00 2001
From: Peter Bierma
Date: Wed, 12 Nov 2025 11:38:17 -0500
Subject: [PATCH 159/417] gh-141004: Document `PyType_SUPPORTS_WEAKREFS`
(GH-141408)
Co-authored-by: Stan Ulbrych <89152624+StanFromIreland@users.noreply.github.com>
---
Doc/c-api/type.rst | 26 ++++++++++++++++++++++++++
Doc/c-api/weakref.rst | 8 ++++++++
2 files changed, 34 insertions(+)
diff --git a/Doc/c-api/type.rst b/Doc/c-api/type.rst
index 29ffeb7c483..b608f815160 100644
--- a/Doc/c-api/type.rst
+++ b/Doc/c-api/type.rst
@@ -195,12 +195,14 @@ Type Objects
before initialization) and should be paired with :c:func:`PyObject_Free` in
:c:member:`~PyTypeObject.tp_free`.
+
.. c:function:: PyObject* PyType_GenericNew(PyTypeObject *type, PyObject *args, PyObject *kwds)
Generic handler for the :c:member:`~PyTypeObject.tp_new` slot of a type
object. Creates a new instance using the type's
:c:member:`~PyTypeObject.tp_alloc` slot and returns the resulting object.
+
.. c:function:: int PyType_Ready(PyTypeObject *type)
Finalize a type object. This should be called on all type objects to finish
@@ -217,6 +219,7 @@ Type Objects
GC protocol itself by at least implementing the
:c:member:`~PyTypeObject.tp_traverse` handle.
+
.. c:function:: PyObject* PyType_GetName(PyTypeObject *type)
Return the type's name. Equivalent to getting the type's
@@ -224,6 +227,7 @@ Type Objects
.. versionadded:: 3.11
+
.. c:function:: PyObject* PyType_GetQualName(PyTypeObject *type)
Return the type's qualified name. Equivalent to getting the
@@ -239,6 +243,7 @@ Type Objects
.. versionadded:: 3.13
+
.. c:function:: PyObject* PyType_GetModuleName(PyTypeObject *type)
Return the type's module name. Equivalent to getting the
@@ -246,6 +251,7 @@ Type Objects
.. versionadded:: 3.13
+
.. c:function:: void* PyType_GetSlot(PyTypeObject *type, int slot)
Return the function pointer stored in the given slot. If the
@@ -262,6 +268,7 @@ Type Objects
:c:func:`PyType_GetSlot` can now accept all types.
Previously, it was limited to :ref:`heap types `.
+
.. c:function:: PyObject* PyType_GetModule(PyTypeObject *type)
Return the module object associated with the given type when the type was
@@ -281,6 +288,7 @@ Type Objects
.. versionadded:: 3.9
+
.. c:function:: void* PyType_GetModuleState(PyTypeObject *type)
Return the state of the module object associated with the given type.
@@ -295,6 +303,7 @@ Type Objects
.. versionadded:: 3.9
+
.. c:function:: PyObject* PyType_GetModuleByDef(PyTypeObject *type, struct PyModuleDef *def)
Find the first superclass whose module was created from
@@ -314,6 +323,7 @@ Type Objects
.. versionadded:: 3.11
+
.. c:function:: int PyType_GetBaseByToken(PyTypeObject *type, void *token, PyTypeObject **result)
Find the first superclass in *type*'s :term:`method resolution order` whose
@@ -332,6 +342,7 @@ Type Objects
.. versionadded:: 3.14
+
.. c:function:: int PyUnstable_Type_AssignVersionTag(PyTypeObject *type)
Attempt to assign a version tag to the given type.
@@ -342,6 +353,16 @@ Type Objects
.. versionadded:: 3.12
+.. c:function:: int PyType_SUPPORTS_WEAKREFS(PyTypeObject *type)
+
+ Return true if instances of *type* support creating weak references, false
+ otherwise. This function always succeeds. *type* must not be ``NULL``.
+
+ .. seealso::
+ * :ref:`weakrefobjects`
+ * :py:mod:`weakref`
+
+
Creating Heap-Allocated Types
.............................
@@ -390,6 +411,7 @@ The following functions and structs are used to create
.. versionadded:: 3.12
+
.. c:function:: PyObject* PyType_FromModuleAndSpec(PyObject *module, PyType_Spec *spec, PyObject *bases)
Equivalent to ``PyType_FromMetaclass(NULL, module, spec, bases)``.
@@ -416,6 +438,7 @@ The following functions and structs are used to create
Creating classes whose metaclass overrides
:c:member:`~PyTypeObject.tp_new` is no longer allowed.
+
.. c:function:: PyObject* PyType_FromSpecWithBases(PyType_Spec *spec, PyObject *bases)
Equivalent to ``PyType_FromMetaclass(NULL, NULL, spec, bases)``.
@@ -437,6 +460,7 @@ The following functions and structs are used to create
Creating classes whose metaclass overrides
:c:member:`~PyTypeObject.tp_new` is no longer allowed.
+
.. c:function:: PyObject* PyType_FromSpec(PyType_Spec *spec)
Equivalent to ``PyType_FromMetaclass(NULL, NULL, spec, NULL)``.
@@ -457,6 +481,7 @@ The following functions and structs are used to create
Creating classes whose metaclass overrides
:c:member:`~PyTypeObject.tp_new` is no longer allowed.
+
.. c:function:: int PyType_Freeze(PyTypeObject *type)
Make a type immutable: set the :c:macro:`Py_TPFLAGS_IMMUTABLETYPE` flag.
@@ -628,6 +653,7 @@ The following functions and structs are used to create
* :c:data:`Py_tp_token` (for clarity, prefer :c:data:`Py_TP_USE_SPEC`
rather than ``NULL``)
+
.. c:macro:: Py_tp_token
A :c:member:`~PyType_Slot.slot` that records a static memory layout ID
diff --git a/Doc/c-api/weakref.rst b/Doc/c-api/weakref.rst
index 39e4febd3ef..db6ae0a9d4e 100644
--- a/Doc/c-api/weakref.rst
+++ b/Doc/c-api/weakref.rst
@@ -45,6 +45,10 @@ as much as it can.
weakly referenceable object, or if *callback* is not callable, ``None``, or
``NULL``, this will return ``NULL`` and raise :exc:`TypeError`.
+ .. seealso::
+ :c:func:`PyType_SUPPORTS_WEAKREFS` for checking if *ob* is weakly
+ referenceable.
+
.. c:function:: PyObject* PyWeakref_NewProxy(PyObject *ob, PyObject *callback)
@@ -57,6 +61,10 @@ as much as it can.
is not a weakly referenceable object, or if *callback* is not callable,
``None``, or ``NULL``, this will return ``NULL`` and raise :exc:`TypeError`.
+ .. seealso::
+ :c:func:`PyType_SUPPORTS_WEAKREFS` for checking if *ob* is weakly
+ referenceable.
+
.. c:function:: int PyWeakref_GetRef(PyObject *ref, PyObject **pobj)
From d162c427904e232fec52d8da759caa1bfa4c01b5 Mon Sep 17 00:00:00 2001
From: Savannah Ostrowski
Date: Wed, 12 Nov 2025 10:09:25 -0800
Subject: [PATCH 160/417] GH-140479: Update JIT builds to use LLVM 21 (#140973)
---
.github/workflows/jit.yml | 8 ++++----
...-11-04-04-57-24.gh-issue-140479.lwQ2v2.rst | 1 +
PCbuild/get_externals.bat | 4 ++--
Tools/jit/README.md | 20 +++++++++----------
Tools/jit/_llvm.py | 4 ++--
5 files changed, 19 insertions(+), 18 deletions(-)
create mode 100644 Misc/NEWS.d/next/Core_and_Builtins/2025-11-04-04-57-24.gh-issue-140479.lwQ2v2.rst
diff --git a/.github/workflows/jit.yml b/.github/workflows/jit.yml
index 69d900091a3..62325250bd3 100644
--- a/.github/workflows/jit.yml
+++ b/.github/workflows/jit.yml
@@ -68,7 +68,7 @@ jobs:
- true
- false
llvm:
- - 20
+ - 21
include:
- target: i686-pc-windows-msvc/msvc
architecture: Win32
@@ -138,7 +138,7 @@ jobs:
fail-fast: false
matrix:
llvm:
- - 20
+ - 21
steps:
- uses: actions/checkout@v4
with:
@@ -166,7 +166,7 @@ jobs:
fail-fast: false
matrix:
llvm:
- - 20
+ - 21
steps:
- uses: actions/checkout@v4
with:
@@ -193,7 +193,7 @@ jobs:
fail-fast: false
matrix:
llvm:
- - 20
+ - 21
steps:
- uses: actions/checkout@v4
with:
diff --git a/Misc/NEWS.d/next/Core_and_Builtins/2025-11-04-04-57-24.gh-issue-140479.lwQ2v2.rst b/Misc/NEWS.d/next/Core_and_Builtins/2025-11-04-04-57-24.gh-issue-140479.lwQ2v2.rst
new file mode 100644
index 00000000000..0a615ed1311
--- /dev/null
+++ b/Misc/NEWS.d/next/Core_and_Builtins/2025-11-04-04-57-24.gh-issue-140479.lwQ2v2.rst
@@ -0,0 +1 @@
+Update JIT compilation to use LLVM 21 at build time.
diff --git a/PCbuild/get_externals.bat b/PCbuild/get_externals.bat
index 115203cecc8..9d02e2121cc 100644
--- a/PCbuild/get_externals.bat
+++ b/PCbuild/get_externals.bat
@@ -82,7 +82,7 @@ if NOT "%IncludeLibffi%"=="false" set binaries=%binaries% libffi-3.4.4
if NOT "%IncludeSSL%"=="false" set binaries=%binaries% openssl-bin-3.0.18
if NOT "%IncludeTkinter%"=="false" set binaries=%binaries% tcltk-8.6.15.0
if NOT "%IncludeSSLSrc%"=="false" set binaries=%binaries% nasm-2.11.06
-if NOT "%IncludeLLVM%"=="false" set binaries=%binaries% llvm-20.1.8.0
+if NOT "%IncludeLLVM%"=="false" set binaries=%binaries% llvm-21.1.4.0
for %%b in (%binaries%) do (
if exist "%EXTERNALS_DIR%\%%b" (
@@ -92,7 +92,7 @@ for %%b in (%binaries%) do (
git clone --depth 1 https://github.com/%ORG%/cpython-bin-deps --branch %%b "%EXTERNALS_DIR%\%%b"
) else (
echo.Fetching %%b...
- if "%%b"=="llvm-20.1.8.0" (
+ if "%%b"=="llvm-21.1.4.0" (
%PYTHON% -E "%PCBUILD%\get_external.py" --release --organization %ORG% --externals-dir "%EXTERNALS_DIR%" %%b
) else (
%PYTHON% -E "%PCBUILD%\get_external.py" --binary --organization %ORG% --externals-dir "%EXTERNALS_DIR%" %%b
diff --git a/Tools/jit/README.md b/Tools/jit/README.md
index d83b09aab59..dd7deb7b256 100644
--- a/Tools/jit/README.md
+++ b/Tools/jit/README.md
@@ -9,32 +9,32 @@ ## Installing LLVM
The JIT compiler does not require end users to install any third-party dependencies, but part of it must be *built* using LLVM[^why-llvm]. You are *not* required to build the rest of CPython using LLVM, or even the same version of LLVM (in fact, this is uncommon).
-LLVM version 20 is the officially supported version. You can modify if needed using the `LLVM_VERSION` env var during configure. Both `clang` and `llvm-readobj` need to be installed and discoverable (version suffixes, like `clang-19`, are okay). It's highly recommended that you also have `llvm-objdump` available, since this allows the build script to dump human-readable assembly for the generated code.
+LLVM version 21 is the officially supported version. You can modify if needed using the `LLVM_VERSION` env var during configure. Both `clang` and `llvm-readobj` need to be installed and discoverable (version suffixes, like `clang-19`, are okay). It's highly recommended that you also have `llvm-objdump` available, since this allows the build script to dump human-readable assembly for the generated code.
It's easy to install all of the required tools:
### Linux
-Install LLVM 20 on Ubuntu/Debian:
+Install LLVM 21 on Ubuntu/Debian:
```sh
wget https://apt.llvm.org/llvm.sh
chmod +x llvm.sh
-sudo ./llvm.sh 20
+sudo ./llvm.sh 21
```
-Install LLVM 20 on Fedora Linux 40 or newer:
+Install LLVM 21 on Fedora Linux 40 or newer:
```sh
-sudo dnf install 'clang(major) = 20' 'llvm(major) = 20'
+sudo dnf install 'clang(major) = 21' 'llvm(major) = 21'
```
### macOS
-Install LLVM 20 with [Homebrew](https://brew.sh):
+Install LLVM 21 with [Homebrew](https://brew.sh):
```sh
-brew install llvm@20
+brew install llvm@21
```
Homebrew won't add any of the tools to your `$PATH`. That's okay; the build script knows how to find them.
@@ -43,18 +43,18 @@ ### Windows
LLVM is downloaded automatically (along with other external binary dependencies) by `PCbuild\build.bat`.
-Otherwise, you can install LLVM 20 [by searching for it on LLVM's GitHub releases page](https://github.com/llvm/llvm-project/releases?q=20), clicking on "Assets", downloading the appropriate Windows installer for your platform (likely the file ending with `-win64.exe`), and running it. **When installing, be sure to select the option labeled "Add LLVM to the system PATH".**
+Otherwise, you can install LLVM 21 [by searching for it on LLVM's GitHub releases page](https://github.com/llvm/llvm-project/releases?q=21), clicking on "Assets", downloading the appropriate Windows installer for your platform (likely the file ending with `-win64.exe`), and running it. **When installing, be sure to select the option labeled "Add LLVM to the system PATH".**
Alternatively, you can use [chocolatey](https://chocolatey.org):
```sh
-choco install llvm --version=20.1.8
+choco install llvm --version=21.1.0
```
### Dev Containers
If you are working on CPython in a [Codespaces instance](https://devguide.python.org/getting-started/setup-building/#using-codespaces), there's no
-need to install LLVM as the Fedora 42 base image includes LLVM 20 out of the box.
+need to install LLVM as the Fedora 43 base image includes LLVM 21 out of the box.
## Building
diff --git a/Tools/jit/_llvm.py b/Tools/jit/_llvm.py
index f1b0ad3f5db..0b9cb5192f1 100644
--- a/Tools/jit/_llvm.py
+++ b/Tools/jit/_llvm.py
@@ -11,8 +11,8 @@
import _targets
-_LLVM_VERSION = "20"
-_EXTERNALS_LLVM_TAG = "llvm-20.1.8.0"
+_LLVM_VERSION = "21"
+_EXTERNALS_LLVM_TAG = "llvm-21.1.4.0"
_P = typing.ParamSpec("_P")
_R = typing.TypeVar("_R")
From fbcac799518e0cb29fcf5f84ed1fa001010b9073 Mon Sep 17 00:00:00 2001
From: Bob Kline
Date: Wed, 12 Nov 2025 13:25:23 -0500
Subject: [PATCH 161/417] gh-141412: Use reliable target URL for urllib example
(GH-141428)
The endpoint used for demonstrating reading URLs is no longer
stable. This change substitutes a target over which we have more
control.
---
Doc/tutorial/stdlib.rst | 6 +++---
1 file changed, 3 insertions(+), 3 deletions(-)
diff --git a/Doc/tutorial/stdlib.rst b/Doc/tutorial/stdlib.rst
index 49a3e370a4c..342c1a00193 100644
--- a/Doc/tutorial/stdlib.rst
+++ b/Doc/tutorial/stdlib.rst
@@ -183,13 +183,13 @@ protocols. Two of the simplest are :mod:`urllib.request` for retrieving data
from URLs and :mod:`smtplib` for sending mail::
>>> from urllib.request import urlopen
- >>> with urlopen('http://worldtimeapi.org/api/timezone/etc/UTC.txt') as response:
+ >>> with urlopen('https://docs.python.org/3/') as response:
... for line in response:
... line = line.decode() # Convert bytes to a str
- ... if line.startswith('datetime'):
+ ... if 'updated' in line:
... print(line.rstrip()) # Remove trailing newline
...
- datetime: 2022-01-01T01:36:47.689215+00:00
+ Last updated on Nov 11, 2025 (20:11 UTC).
>>> import smtplib
>>> server = smtplib.SMTP('localhost')
From 1f381a579cc50aa82838de84c2294b4979586bd9 Mon Sep 17 00:00:00 2001
From: Savannah Ostrowski
Date: Wed, 12 Nov 2025 10:26:50 -0800
Subject: [PATCH 162/417] Add details about JIT build infrastructure and
updating dependencies to `Tools/jit` (#141167)
---
Tools/jit/README.md | 3 +++
Tools/jit/jit_infra.md | 28 ++++++++++++++++++++++++++++
2 files changed, 31 insertions(+)
create mode 100644 Tools/jit/jit_infra.md
diff --git a/Tools/jit/README.md b/Tools/jit/README.md
index dd7deb7b256..c70c0c47d94 100644
--- a/Tools/jit/README.md
+++ b/Tools/jit/README.md
@@ -66,6 +66,9 @@ ## Building
The JIT can also be enabled or disabled using the `PYTHON_JIT` environment variable, even on builds where it is enabled or disabled by default. More details about configuring CPython with the JIT and optional values for `--enable-experimental-jit` can be found [here](https://docs.python.org/dev/using/configure.html#cmdoption-enable-experimental-jit).
+## Miscellaneous
+If you're looking for information on how to update the JIT build dependencies, see [JIT Build Infrastructure](jit_infra.md).
+
[^pep-744]: [PEP 744](https://peps.python.org/pep-0744/)
[^why-llvm]: Clang is specifically needed because it's the only C compiler with support for guaranteed tail calls (`musttail`), which are required by CPython's continuation-passing-style approach to JIT compilation. Since LLVM also includes other functionalities we need (namely, object file parsing and disassembly), it's convenient to only support one toolchain at this time.
diff --git a/Tools/jit/jit_infra.md b/Tools/jit/jit_infra.md
new file mode 100644
index 00000000000..1a954755611
--- /dev/null
+++ b/Tools/jit/jit_infra.md
@@ -0,0 +1,28 @@
+# JIT Build Infrastructure
+
+This document includes details about the intricacies of the JIT build infrastructure.
+
+## Updating LLVM
+
+When we update LLVM, we need to also update the LLVM release artifact for Windows builds. This is because Windows builds automatically pull prebuilt LLVM binaries in our pipelines (e.g. notice that `.github/workflows/jit.yml` does not explicitly download LLVM or build it from source).
+
+To update the LLVM release artifact for Windows builds, follow these steps:
+1. Go to the [LLVM releases page](https://github.com/llvm/llvm-project/releases).
+1. Download x86_64 Windows artifact for the desired LLVM version (e.g. `clang+llvm-21.1.4-x86_64-pc-windows-msvc.tar.xz`).
+1. Extract and repackage the tarball with the correct directory structure. For example:
+ ```bash
+ tar -xf clang+llvm-21.1.4-x86_64-pc-windows-msvc.tar.xz
+ mv clang+llvm-21.1.4-x86_64-pc-windows-msvc llvm-21.1.4.0
+ tar -cf - llvm-21.1.4.0 | pv | xz > llvm-21.1.4.0.tar.xz
+ ```
+ The tarball must contain a top-level directory named `llvm-{version}.0/`.
+1. Go to [cpython-bin-deps](https://github.com/python/cpython-bin-deps).
+1. Create a new release with the updated LLVM artifact.
+ - Create a new tag to match the LLVM version (e.g. `llvm-21.1.4.0`).
+ - Specify the release title (e.g. `LLVM 21.1.4 for x86_64 Windows`).
+ - Upload the asset (you can leave all other fields the same).
+
+### Other notes
+- You must make sure that the name of the artifact matches exactly what is expected in `Tools/jit/_llvm.py` and `PCbuild/get_externals.py`.
+- We don't need multiple release artifacts for each architecture because LLVM can cross-compile for different architectures on Windows; x86_64 is sufficient.
+- You must have permissions to create releases in the `cpython-bin-deps` repository. If you don't have permissions, you should contact one of the organization admins.
\ No newline at end of file
From 35ed3e4cedc8aef3936da81a6b64e90374532b13 Mon Sep 17 00:00:00 2001
From: Mikhail Efimov
Date: Wed, 12 Nov 2025 22:04:02 +0300
Subject: [PATCH 163/417] gh-140936: Fix JIT assertion crash at finalization if
some generator is alive (GH-140969)
---
Lib/test/test_capi/test_opt.py | 19 +++++++++++++++++++
Python/optimizer.c | 8 +++++++-
2 files changed, 26 insertions(+), 1 deletion(-)
diff --git a/Lib/test/test_capi/test_opt.py b/Lib/test/test_capi/test_opt.py
index 4e94f62d35e..e65556fb28f 100644
--- a/Lib/test/test_capi/test_opt.py
+++ b/Lib/test/test_capi/test_opt.py
@@ -2660,6 +2660,25 @@ def f():
f()
+ def test_interpreter_finalization_with_generator_alive(self):
+ script_helper.assert_python_ok("-c", textwrap.dedent("""
+ import sys
+ t = tuple(range(%d))
+ def simple_for():
+ for x in t:
+ x
+
+ def gen():
+ try:
+ yield
+ except:
+ simple_for()
+
+ sys.settrace(lambda *args: None)
+ simple_for()
+ g = gen()
+ next(g)
+ """ % _testinternalcapi.SPECIALIZATION_THRESHOLD))
def global_identity(x):
diff --git a/Python/optimizer.c b/Python/optimizer.c
index f44f8a9614b..3b7e2dafab8 100644
--- a/Python/optimizer.c
+++ b/Python/optimizer.c
@@ -118,7 +118,13 @@ _PyOptimizer_Optimize(
{
_PyStackRef *stack_pointer = frame->stackpointer;
PyInterpreterState *interp = _PyInterpreterState_GET();
- assert(interp->jit);
+ if (!interp->jit) {
+ // gh-140936: It is possible that interp->jit will become false during
+ // interpreter finalization. However, the specialized JUMP_BACKWARD_JIT
+ // instruction may still be present. In this case, we should
+ // return immediately without optimization.
+ return 0;
+ }
assert(!interp->compiling);
#ifndef Py_GIL_DISABLED
interp->compiling = true;
From 558936bec1f1e0f8346063a8cb2b2782d085178e Mon Sep 17 00:00:00 2001
From: Russell Keith-Magee
Date: Thu, 13 Nov 2025 05:41:26 +0800
Subject: [PATCH 164/417] gh-141442: Add escaping to iOS testbed arguments
(#141443)
Xcode concatenates the test argument array, losing quoting in the process.
---
Apple/testbed/__main__.py | 3 ++-
.../Tools-Demos/2025-11-12-12-54-28.gh-issue-141442.50dS3P.rst | 1 +
2 files changed, 3 insertions(+), 1 deletion(-)
create mode 100644 Misc/NEWS.d/next/Tools-Demos/2025-11-12-12-54-28.gh-issue-141442.50dS3P.rst
diff --git a/Apple/testbed/__main__.py b/Apple/testbed/__main__.py
index 42eb60a4c8d..49974cb1428 100644
--- a/Apple/testbed/__main__.py
+++ b/Apple/testbed/__main__.py
@@ -2,6 +2,7 @@
import json
import os
import re
+import shlex
import shutil
import subprocess
import sys
@@ -252,7 +253,7 @@ def update_test_plan(testbed_path, platform, args):
test_plan = json.load(f)
test_plan["defaultOptions"]["commandLineArgumentEntries"] = [
- {"argument": arg} for arg in args
+ {"argument": shlex.quote(arg)} for arg in args
]
with test_plan_path.open("w", encoding="utf-8") as f:
diff --git a/Misc/NEWS.d/next/Tools-Demos/2025-11-12-12-54-28.gh-issue-141442.50dS3P.rst b/Misc/NEWS.d/next/Tools-Demos/2025-11-12-12-54-28.gh-issue-141442.50dS3P.rst
new file mode 100644
index 00000000000..073c070413f
--- /dev/null
+++ b/Misc/NEWS.d/next/Tools-Demos/2025-11-12-12-54-28.gh-issue-141442.50dS3P.rst
@@ -0,0 +1 @@
+The iOS testbed now correctly handles test arguments that contain spaces.
From dc0987080ed66c662e8e0b24cdb8c179817bd697 Mon Sep 17 00:00:00 2001
From: Michael Cho
Date: Wed, 12 Nov 2025 17:16:58 -0500
Subject: [PATCH 165/417] gh-124111: Fix TCL 9 thread detection (GH-128103)
---
.../Library/2025-11-12-15-42-47.gh-issue-124111.hTw4OE.rst | 2 ++
Modules/_tkinter.c | 4 ++++
2 files changed, 6 insertions(+)
create mode 100644 Misc/NEWS.d/next/Library/2025-11-12-15-42-47.gh-issue-124111.hTw4OE.rst
diff --git a/Misc/NEWS.d/next/Library/2025-11-12-15-42-47.gh-issue-124111.hTw4OE.rst b/Misc/NEWS.d/next/Library/2025-11-12-15-42-47.gh-issue-124111.hTw4OE.rst
new file mode 100644
index 00000000000..8436cd2415d
--- /dev/null
+++ b/Misc/NEWS.d/next/Library/2025-11-12-15-42-47.gh-issue-124111.hTw4OE.rst
@@ -0,0 +1,2 @@
+Updated Tcl threading configuration in :mod:`_tkinter` to assume that
+threads are always available in Tcl 9 and later.
diff --git a/Modules/_tkinter.c b/Modules/_tkinter.c
index c0ed8977d8f..8cea7b59fe7 100644
--- a/Modules/_tkinter.c
+++ b/Modules/_tkinter.c
@@ -575,8 +575,12 @@ Tkapp_New(const char *screenName, const char *className,
v->interp = Tcl_CreateInterp();
v->wantobjects = wantobjects;
+#if TCL_MAJOR_VERSION >= 9
+ v->threaded = 1;
+#else
v->threaded = Tcl_GetVar2Ex(v->interp, "tcl_platform", "threaded",
TCL_GLOBAL_ONLY) != NULL;
+#endif
v->thread_id = Tcl_GetCurrentThread();
v->dispatching = 0;
v->trace = NULL;
From 26b7df2430cd5a9ee772bfa6ee03a73bd0b11619 Mon Sep 17 00:00:00 2001
From: Peter Bierma
Date: Wed, 12 Nov 2025 17:52:56 -0500
Subject: [PATCH 166/417] gh-141004: Document `PyRun_InteractiveOneObject`
(GH-141405)
---
Doc/c-api/veryhigh.rst | 25 ++++++++++++++++---------
1 file changed, 16 insertions(+), 9 deletions(-)
diff --git a/Doc/c-api/veryhigh.rst b/Doc/c-api/veryhigh.rst
index 916c616dfee..3b07b5fbed5 100644
--- a/Doc/c-api/veryhigh.rst
+++ b/Doc/c-api/veryhigh.rst
@@ -100,6 +100,20 @@ the same library that the Python runtime is using.
Otherwise, Python may not handle script file with LF line ending correctly.
+.. c:function:: int PyRun_InteractiveOneObject(FILE *fp, PyObject *filename, PyCompilerFlags *flags)
+
+ Read and execute a single statement from a file associated with an
+ interactive device according to the *flags* argument. The user will be
+ prompted using ``sys.ps1`` and ``sys.ps2``. *filename* must be a Python
+ :class:`str` object.
+
+ Returns ``0`` when the input was
+ executed successfully, ``-1`` if there was an exception, or an error code
+ from the :file:`errcode.h` include file distributed as part of Python if
+ there was a parse error. (Note that :file:`errcode.h` is not included by
+ :file:`Python.h`, so must be included specifically if needed.)
+
+
.. c:function:: int PyRun_InteractiveOne(FILE *fp, const char *filename)
This is a simplified interface to :c:func:`PyRun_InteractiveOneFlags` below,
@@ -108,17 +122,10 @@ the same library that the Python runtime is using.
.. c:function:: int PyRun_InteractiveOneFlags(FILE *fp, const char *filename, PyCompilerFlags *flags)
- Read and execute a single statement from a file associated with an
- interactive device according to the *flags* argument. The user will be
- prompted using ``sys.ps1`` and ``sys.ps2``. *filename* is decoded from the
+ Similar to :c:func:`PyRun_InteractiveOneObject`, but *filename* is a
+ :c:expr:`const char*`, which is decoded from the
:term:`filesystem encoding and error handler`.
- Returns ``0`` when the input was
- executed successfully, ``-1`` if there was an exception, or an error code
- from the :file:`errcode.h` include file distributed as part of Python if
- there was a parse error. (Note that :file:`errcode.h` is not included by
- :file:`Python.h`, so must be included specifically if needed.)
-
.. c:function:: int PyRun_InteractiveLoop(FILE *fp, const char *filename)
From 781cc68c3c814e46e6a74c3a6a32e0f9f8f7eb11 Mon Sep 17 00:00:00 2001
From: "Gregory P. Smith" <68491+gpshead@users.noreply.github.com>
Date: Wed, 12 Nov 2025 18:15:16 -0800
Subject: [PATCH 167/417] gh-137109: refactor warning about threads when
forking (#141438)
* gh-137109: refactor warning about threads when forking
This splits the OS API specific functionality to get the number of threads out
from the fallback Python method and warning raising code itself. This way the
OS APIs can be queried before we've run
`os.register_at_fork(after_in_parent=...)` registered functions which
themselves may (re)start threads that would otherwise be detected.
This is best effort. If the OS APIs are either unavailable or fail, the
warning generating code still falls back to looking at the Python threading
state after the CPython interpreter world has been restarted and the
after_in_parent calls have been made. The common case for most Linux and macOS
environments should work today.
This also lines up with the existing TODO refactoring, we may choose to expose
this API to get the number of OS threads in the `os` module in the future.
* NEWS entry
* avoid "function-prototype" compiler warning?
---
...-11-12-01-49-03.gh-issue-137109.D6sq2B.rst | 5 +
Modules/posixmodule.c | 103 ++++++++++--------
2 files changed, 65 insertions(+), 43 deletions(-)
create mode 100644 Misc/NEWS.d/next/Library/2025-11-12-01-49-03.gh-issue-137109.D6sq2B.rst
diff --git a/Misc/NEWS.d/next/Library/2025-11-12-01-49-03.gh-issue-137109.D6sq2B.rst b/Misc/NEWS.d/next/Library/2025-11-12-01-49-03.gh-issue-137109.D6sq2B.rst
new file mode 100644
index 00000000000..32f4e39f6d5
--- /dev/null
+++ b/Misc/NEWS.d/next/Library/2025-11-12-01-49-03.gh-issue-137109.D6sq2B.rst
@@ -0,0 +1,5 @@
+The :mod:`os.fork` and related forking APIs will no longer warn in the
+common case where Linux or macOS platform APIs return the number of threads
+in a process and find the answer to be 1 even when a
+:func:`os.register_at_fork` ``after_in_parent=`` callback (re)starts a
+thread.
diff --git a/Modules/posixmodule.c b/Modules/posixmodule.c
index 6390f1fc5fe..fc609b2707c 100644
--- a/Modules/posixmodule.c
+++ b/Modules/posixmodule.c
@@ -8431,53 +8431,19 @@ os_register_at_fork_impl(PyObject *module, PyObject *before,
// running in the process. Best effort, silent if unable to count threads.
// Constraint: Quick. Never overcounts. Never leaves an error set.
//
-// This should only be called from the parent process after
+// This MUST only be called from the parent process after
// PyOS_AfterFork_Parent().
static int
-warn_about_fork_with_threads(const char* name)
+warn_about_fork_with_threads(
+ const char* name, // Name of the API to use in the warning message.
+ const Py_ssize_t num_os_threads // Only trusted when >= 1.
+)
{
// It's not safe to issue the warning while the world is stopped, because
// other threads might be holding locks that we need, which would deadlock.
assert(!_PyRuntime.stoptheworld.world_stopped);
- // TODO: Consider making an `os` module API to return the current number
- // of threads in the process. That'd presumably use this platform code but
- // raise an error rather than using the inaccurate fallback.
- Py_ssize_t num_python_threads = 0;
-#if defined(__APPLE__) && defined(HAVE_GETPID)
- mach_port_t macos_self = mach_task_self();
- mach_port_t macos_task;
- if (task_for_pid(macos_self, getpid(), &macos_task) == KERN_SUCCESS) {
- thread_array_t macos_threads;
- mach_msg_type_number_t macos_n_threads;
- if (task_threads(macos_task, &macos_threads,
- &macos_n_threads) == KERN_SUCCESS) {
- num_python_threads = macos_n_threads;
- }
- }
-#elif defined(__linux__)
- // Linux /proc/self/stat 20th field is the number of threads.
- FILE* proc_stat = fopen("/proc/self/stat", "r");
- if (proc_stat) {
- size_t n;
- // Size chosen arbitrarily. ~60% more bytes than a 20th column index
- // observed on the author's workstation.
- char stat_line[160];
- n = fread(&stat_line, 1, 159, proc_stat);
- stat_line[n] = '\0';
- fclose(proc_stat);
-
- char *saveptr = NULL;
- char *field = strtok_r(stat_line, " ", &saveptr);
- unsigned int idx;
- for (idx = 19; idx && field; --idx) {
- field = strtok_r(NULL, " ", &saveptr);
- }
- if (idx == 0 && field) { // found the 20th field
- num_python_threads = atoi(field); // 0 on error
- }
- }
-#endif
+ Py_ssize_t num_python_threads = num_os_threads;
if (num_python_threads <= 0) {
// Fall back to just the number our threading module knows about.
// An incomplete view of the world, but better than nothing.
@@ -8530,6 +8496,51 @@ warn_about_fork_with_threads(const char* name)
}
return 0;
}
+
+// If this returns <= 0, we were unable to successfully use any OS APIs.
+// Returns a positive number of threads otherwise.
+static Py_ssize_t get_number_of_os_threads(void)
+{
+ // TODO: Consider making an `os` module API to return the current number
+ // of threads in the process. That'd presumably use this platform code but
+ // raise an error rather than using the inaccurate fallback.
+ Py_ssize_t num_python_threads = 0;
+#if defined(__APPLE__) && defined(HAVE_GETPID)
+ mach_port_t macos_self = mach_task_self();
+ mach_port_t macos_task;
+ if (task_for_pid(macos_self, getpid(), &macos_task) == KERN_SUCCESS) {
+ thread_array_t macos_threads;
+ mach_msg_type_number_t macos_n_threads;
+ if (task_threads(macos_task, &macos_threads,
+ &macos_n_threads) == KERN_SUCCESS) {
+ num_python_threads = macos_n_threads;
+ }
+ }
+#elif defined(__linux__)
+ // Linux /proc/self/stat 20th field is the number of threads.
+ FILE* proc_stat = fopen("/proc/self/stat", "r");
+ if (proc_stat) {
+ size_t n;
+ // Size chosen arbitrarily. ~60% more bytes than a 20th column index
+ // observed on the author's workstation.
+ char stat_line[160];
+ n = fread(&stat_line, 1, 159, proc_stat);
+ stat_line[n] = '\0';
+ fclose(proc_stat);
+
+ char *saveptr = NULL;
+ char *field = strtok_r(stat_line, " ", &saveptr);
+ unsigned int idx;
+ for (idx = 19; idx && field; --idx) {
+ field = strtok_r(NULL, " ", &saveptr);
+ }
+ if (idx == 0 && field) { // found the 20th field
+ num_python_threads = atoi(field); // 0 on error
+ }
+ }
+#endif
+ return num_python_threads;
+}
#endif // HAVE_FORK1 || HAVE_FORKPTY || HAVE_FORK
#ifdef HAVE_FORK1
@@ -8564,10 +8575,12 @@ os_fork1_impl(PyObject *module)
/* child: this clobbers and resets the import lock. */
PyOS_AfterFork_Child();
} else {
+ // Called before AfterFork_Parent in case those hooks start threads.
+ Py_ssize_t num_os_threads = get_number_of_os_threads();
/* parent: release the import lock. */
PyOS_AfterFork_Parent();
// After PyOS_AfterFork_Parent() starts the world to avoid deadlock.
- if (warn_about_fork_with_threads("fork1") < 0) {
+ if (warn_about_fork_with_threads("fork1", num_os_threads) < 0) {
return NULL;
}
}
@@ -8615,10 +8628,12 @@ os_fork_impl(PyObject *module)
/* child: this clobbers and resets the import lock. */
PyOS_AfterFork_Child();
} else {
+ // Called before AfterFork_Parent in case those hooks start threads.
+ Py_ssize_t num_os_threads = get_number_of_os_threads();
/* parent: release the import lock. */
PyOS_AfterFork_Parent();
// After PyOS_AfterFork_Parent() starts the world to avoid deadlock.
- if (warn_about_fork_with_threads("fork") < 0)
+ if (warn_about_fork_with_threads("fork", num_os_threads) < 0)
return NULL;
}
if (pid == -1) {
@@ -9476,6 +9491,8 @@ os_forkpty_impl(PyObject *module)
/* child: this clobbers and resets the import lock. */
PyOS_AfterFork_Child();
} else {
+ // Called before AfterFork_Parent in case those hooks start threads.
+ Py_ssize_t num_os_threads = get_number_of_os_threads();
/* parent: release the import lock. */
PyOS_AfterFork_Parent();
/* set O_CLOEXEC on master_fd */
@@ -9485,7 +9502,7 @@ os_forkpty_impl(PyObject *module)
}
// After PyOS_AfterFork_Parent() starts the world to avoid deadlock.
- if (warn_about_fork_with_threads("forkpty") < 0)
+ if (warn_about_fork_with_threads("forkpty", num_os_threads) < 0)
return NULL;
}
if (pid == -1) {
From 63548b36998e7f7cd5c7c28b53b348a93f836737 Mon Sep 17 00:00:00 2001
From: Shamil
Date: Thu, 13 Nov 2025 14:01:31 +0300
Subject: [PATCH 168/417] gh-140260: fix data race in `_struct` module
initialization with subinterpreters (#140909)
---
Lib/test/test_struct.py | 17 ++++
...-11-02-15-28-33.gh-issue-140260.JNzlGz.rst | 2 +
Modules/_struct.c | 91 ++++++++++---------
Tools/c-analyzer/cpython/ignored.tsv | 1 +
4 files changed, 70 insertions(+), 41 deletions(-)
create mode 100644 Misc/NEWS.d/next/Core_and_Builtins/2025-11-02-15-28-33.gh-issue-140260.JNzlGz.rst
diff --git a/Lib/test/test_struct.py b/Lib/test/test_struct.py
index 75c76a36ee9..cceecdd526c 100644
--- a/Lib/test/test_struct.py
+++ b/Lib/test/test_struct.py
@@ -800,6 +800,23 @@ def test_c_complex_round_trip(self):
round_trip = struct.unpack(f, struct.pack(f, z))[0]
self.assertComplexesAreIdentical(z, round_trip)
+ @unittest.skipIf(
+ support.is_android or support.is_apple_mobile,
+ "Subinterpreters are not supported on Android and iOS"
+ )
+ def test_endian_table_init_subinterpreters(self):
+ # Verify that the _struct extension module can be initialized
+ # concurrently in subinterpreters (gh-140260).
+ try:
+ from concurrent.futures import InterpreterPoolExecutor
+ except ImportError:
+ raise unittest.SkipTest("InterpreterPoolExecutor not available")
+
+ code = "import struct"
+ with InterpreterPoolExecutor(max_workers=5) as executor:
+ results = executor.map(exec, [code] * 5)
+ self.assertListEqual(list(results), [None] * 5)
+
class UnpackIteratorTest(unittest.TestCase):
"""
diff --git a/Misc/NEWS.d/next/Core_and_Builtins/2025-11-02-15-28-33.gh-issue-140260.JNzlGz.rst b/Misc/NEWS.d/next/Core_and_Builtins/2025-11-02-15-28-33.gh-issue-140260.JNzlGz.rst
new file mode 100644
index 00000000000..96bf9b51e48
--- /dev/null
+++ b/Misc/NEWS.d/next/Core_and_Builtins/2025-11-02-15-28-33.gh-issue-140260.JNzlGz.rst
@@ -0,0 +1,2 @@
+Fix :mod:`struct` data race in endian table initialization with
+subinterpreters. Patch by Shamil Abdulaev.
diff --git a/Modules/_struct.c b/Modules/_struct.c
index f09252e82c3..2acb3df3a30 100644
--- a/Modules/_struct.c
+++ b/Modules/_struct.c
@@ -9,6 +9,7 @@
#include "Python.h"
#include "pycore_bytesobject.h" // _PyBytesWriter
+#include "pycore_lock.h" // _PyOnceFlag_CallOnce()
#include "pycore_long.h" // _PyLong_AsByteArray()
#include "pycore_moduleobject.h" // _PyModule_GetState()
#include "pycore_weakref.h" // FT_CLEAR_WEAKREFS()
@@ -1505,6 +1506,53 @@ static formatdef lilendian_table[] = {
{0}
};
+/* Ensure endian table optimization happens exactly once across all interpreters */
+static _PyOnceFlag endian_tables_init_once = {0};
+
+static int
+init_endian_tables(void *Py_UNUSED(arg))
+{
+ const formatdef *native = native_table;
+ formatdef *other, *ptr;
+#if PY_LITTLE_ENDIAN
+ other = lilendian_table;
+#else
+ other = bigendian_table;
+#endif
+ /* Scan through the native table, find a matching
+ entry in the endian table and swap in the
+ native implementations whenever possible
+ (64-bit platforms may not have "standard" sizes) */
+ while (native->format != '\0' && other->format != '\0') {
+ ptr = other;
+ while (ptr->format != '\0') {
+ if (ptr->format == native->format) {
+ /* Match faster when formats are
+ listed in the same order */
+ if (ptr == other)
+ other++;
+ /* Only use the trick if the
+ size matches */
+ if (ptr->size != native->size)
+ break;
+ /* Skip float and double, could be
+ "unknown" float format */
+ if (ptr->format == 'd' || ptr->format == 'f')
+ break;
+ /* Skip _Bool, semantics are different for standard size */
+ if (ptr->format == '?')
+ break;
+ ptr->pack = native->pack;
+ ptr->unpack = native->unpack;
+ break;
+ }
+ ptr++;
+ }
+ native++;
+ }
+ return 0;
+}
+
static const formatdef *
whichtable(const char **pfmt)
@@ -2710,47 +2758,8 @@ _structmodule_exec(PyObject *m)
return -1;
}
- /* Check endian and swap in faster functions */
- {
- const formatdef *native = native_table;
- formatdef *other, *ptr;
-#if PY_LITTLE_ENDIAN
- other = lilendian_table;
-#else
- other = bigendian_table;
-#endif
- /* Scan through the native table, find a matching
- entry in the endian table and swap in the
- native implementations whenever possible
- (64-bit platforms may not have "standard" sizes) */
- while (native->format != '\0' && other->format != '\0') {
- ptr = other;
- while (ptr->format != '\0') {
- if (ptr->format == native->format) {
- /* Match faster when formats are
- listed in the same order */
- if (ptr == other)
- other++;
- /* Only use the trick if the
- size matches */
- if (ptr->size != native->size)
- break;
- /* Skip float and double, could be
- "unknown" float format */
- if (ptr->format == 'd' || ptr->format == 'f')
- break;
- /* Skip _Bool, semantics are different for standard size */
- if (ptr->format == '?')
- break;
- ptr->pack = native->pack;
- ptr->unpack = native->unpack;
- break;
- }
- ptr++;
- }
- native++;
- }
- }
+ /* init cannot fail */
+ (void)_PyOnceFlag_CallOnce(&endian_tables_init_once, init_endian_tables, NULL);
/* Add some symbolic constants to the module */
state->StructError = PyErr_NewException("struct.error", NULL, NULL);
diff --git a/Tools/c-analyzer/cpython/ignored.tsv b/Tools/c-analyzer/cpython/ignored.tsv
index 11a3cd794ff..4621ad250f4 100644
--- a/Tools/c-analyzer/cpython/ignored.tsv
+++ b/Tools/c-analyzer/cpython/ignored.tsv
@@ -24,6 +24,7 @@ Modules/posixmodule.c os_dup2_impl dup3_works -
## guards around resource init
Python/thread_pthread.h PyThread__init_thread lib_initialized -
+Modules/_struct.c - endian_tables_init_once -
##-----------------------
## other values (not Python-specific)
From d8e6bdc0d083f4e76ac49574544555ad91257592 Mon Sep 17 00:00:00 2001
From: Serhiy Storchaka
Date: Thu, 13 Nov 2025 13:21:32 +0200
Subject: [PATCH 169/417] gh-135801: Add the module parameter to compile() etc
(GH-139652)
Many functions related to compiling or parsing Python code, such as
compile(), ast.parse(), symtable.symtable(),
and importlib.abc.InspectLoader.source_to_code() now allow to pass
the module name used when filtering syntax warnings.
---
Doc/library/ast.rst | 7 ++-
Doc/library/functions.rst | 11 +++-
Doc/library/importlib.rst | 10 +++-
Doc/library/symtable.rst | 8 ++-
Doc/whatsnew/3.15.rst | 7 +++
Include/internal/pycore_compile.h | 9 ++-
Include/internal/pycore_parser.h | 3 +-
Include/internal/pycore_pyerrors.h | 3 +-
Include/internal/pycore_pythonrun.h | 6 ++
Include/internal/pycore_symtable.h | 3 +-
Lib/ast.py | 5 +-
Lib/importlib/_bootstrap_external.py | 9 +--
Lib/importlib/abc.py | 11 ++--
Lib/modulefinder.py | 2 +-
Lib/profiling/sampling/_sync_coordinator.py | 2 +-
Lib/profiling/tracing/__init__.py | 2 +-
Lib/runpy.py | 6 +-
Lib/symtable.py | 4 +-
Lib/test/test_ast/test_ast.py | 10 ++++
Lib/test/test_builtin.py | 3 +-
Lib/test/test_cmd_line_script.py | 23 ++++++--
Lib/test/test_compile.py | 10 ++++
Lib/test/test_import/__init__.py | 15 +----
Lib/test/test_runpy.py | 43 ++++++++++++++
Lib/test/test_symtable.py | 10 ++++
Lib/test/test_zipimport_support.py | 23 ++++++++
Lib/zipimport.py | 6 +-
...-10-06-14-19-47.gh-issue-135801.OhxEZS.rst | 6 ++
Modules/clinic/symtablemodule.c.h | 58 ++++++++++++++++---
Modules/symtablemodule.c | 19 +++++-
Parser/lexer/state.c | 2 +
Parser/lexer/state.h | 1 +
Parser/peg_api.c | 6 +-
Parser/pegen.c | 8 ++-
Parser/pegen.h | 2 +-
Parser/string_parser.c | 2 +-
Parser/tokenizer/helpers.c | 4 +-
Programs/_freeze_module.py | 2 +-
Programs/freeze_test_frozenmain.py | 2 +-
Python/ast_preprocess.c | 8 ++-
Python/bltinmodule.c | 25 ++++++--
Python/clinic/bltinmodule.c.h | 26 ++++++---
Python/compile.c | 30 ++++++----
Python/errors.c | 7 ++-
Python/pythonrun.c | 38 ++++++++++--
Python/symtable.c | 4 +-
.../peg_extension/peg_extension.c | 4 +-
47 files changed, 390 insertions(+), 115 deletions(-)
create mode 100644 Misc/NEWS.d/next/Core_and_Builtins/2025-10-06-14-19-47.gh-issue-135801.OhxEZS.rst
diff --git a/Doc/library/ast.rst b/Doc/library/ast.rst
index 49462167217..0ea3c3c59a6 100644
--- a/Doc/library/ast.rst
+++ b/Doc/library/ast.rst
@@ -2205,10 +2205,10 @@ Async and await
Apart from the node classes, the :mod:`ast` module defines these utility functions
and classes for traversing abstract syntax trees:
-.. function:: parse(source, filename='', mode='exec', *, type_comments=False, feature_version=None, optimize=-1)
+.. function:: parse(source, filename='', mode='exec', *, type_comments=False, feature_version=None, optimize=-1, module=None)
Parse the source into an AST node. Equivalent to ``compile(source,
- filename, mode, flags=FLAGS_VALUE, optimize=optimize)``,
+ filename, mode, flags=FLAGS_VALUE, optimize=optimize, module=module)``,
where ``FLAGS_VALUE`` is ``ast.PyCF_ONLY_AST`` if ``optimize <= 0``
and ``ast.PyCF_OPTIMIZED_AST`` otherwise.
@@ -2261,6 +2261,9 @@ and classes for traversing abstract syntax trees:
The minimum supported version for ``feature_version`` is now ``(3, 7)``.
The ``optimize`` argument was added.
+ .. versionadded:: next
+ Added the *module* parameter.
+
.. function:: unparse(ast_obj)
diff --git a/Doc/library/functions.rst b/Doc/library/functions.rst
index e9879397555..3257daf89d3 100644
--- a/Doc/library/functions.rst
+++ b/Doc/library/functions.rst
@@ -292,7 +292,9 @@ are always available. They are listed here in alphabetical order.
:func:`property`.
-.. function:: compile(source, filename, mode, flags=0, dont_inherit=False, optimize=-1)
+.. function:: compile(source, filename, mode, flags=0, \
+ dont_inherit=False, optimize=-1, \
+ *, module=None)
Compile the *source* into a code or AST object. Code objects can be executed
by :func:`exec` or :func:`eval`. *source* can either be a normal string, a
@@ -334,6 +336,10 @@ are always available. They are listed here in alphabetical order.
``__debug__`` is true), ``1`` (asserts are removed, ``__debug__`` is false)
or ``2`` (docstrings are removed too).
+ The optional argument *module* specifies the module name.
+ It is needed to unambiguous :ref:`filter ` syntax warnings
+ by module name.
+
This function raises :exc:`SyntaxError` if the compiled source is invalid,
and :exc:`ValueError` if the source contains null bytes.
@@ -371,6 +377,9 @@ are always available. They are listed here in alphabetical order.
``ast.PyCF_ALLOW_TOP_LEVEL_AWAIT`` can now be passed in flags to enable
support for top-level ``await``, ``async for``, and ``async with``.
+ .. versionadded:: next
+ Added the *module* parameter.
+
.. class:: complex(number=0, /)
complex(string, /)
diff --git a/Doc/library/importlib.rst b/Doc/library/importlib.rst
index 602a7100a12..03ba23b6216 100644
--- a/Doc/library/importlib.rst
+++ b/Doc/library/importlib.rst
@@ -459,7 +459,7 @@ ABC hierarchy::
.. versionchanged:: 3.4
Raises :exc:`ImportError` instead of :exc:`NotImplementedError`.
- .. staticmethod:: source_to_code(data, path='')
+ .. staticmethod:: source_to_code(data, path='', fullname=None)
Create a code object from Python source.
@@ -471,11 +471,19 @@ ABC hierarchy::
With the subsequent code object one can execute it in a module by
running ``exec(code, module.__dict__)``.
+ The optional argument *fullname* specifies the module name.
+ It is needed to unambiguous :ref:`filter ` syntax
+ warnings by module name.
+
.. versionadded:: 3.4
.. versionchanged:: 3.5
Made the method static.
+ .. versionadded:: next
+ Added the *fullname* parameter.
+
+
.. method:: exec_module(module)
Implementation of :meth:`Loader.exec_module`.
diff --git a/Doc/library/symtable.rst b/Doc/library/symtable.rst
index 54e19af4bd6..c0d9e79197d 100644
--- a/Doc/library/symtable.rst
+++ b/Doc/library/symtable.rst
@@ -21,11 +21,17 @@ tables.
Generating Symbol Tables
------------------------
-.. function:: symtable(code, filename, compile_type)
+.. function:: symtable(code, filename, compile_type, *, module=None)
Return the toplevel :class:`SymbolTable` for the Python source *code*.
*filename* is the name of the file containing the code. *compile_type* is
like the *mode* argument to :func:`compile`.
+ The optional argument *module* specifies the module name.
+ It is needed to unambiguous :ref:`filter ` syntax warnings
+ by module name.
+
+ .. versionadded:: next
+ Added the *module* parameter.
Examining Symbol Tables
diff --git a/Doc/whatsnew/3.15.rst b/Doc/whatsnew/3.15.rst
index c6089f63dee..3cb766978a7 100644
--- a/Doc/whatsnew/3.15.rst
+++ b/Doc/whatsnew/3.15.rst
@@ -307,6 +307,13 @@ Other language changes
not only integers or floats, although this does not improve precision.
(Contributed by Serhiy Storchaka in :gh:`67795`.)
+* Many functions related to compiling or parsing Python code, such as
+ :func:`compile`, :func:`ast.parse`, :func:`symtable.symtable`,
+ and :func:`importlib.abc.InspectLoader.source_to_code`, now allow to pass
+ the module name. It is needed to unambiguous :ref:`filter `
+ syntax warnings by module name.
+ (Contributed by Serhiy Storchaka in :gh:`135801`.)
+
New modules
===========
diff --git a/Include/internal/pycore_compile.h b/Include/internal/pycore_compile.h
index 1c60834fa20..527141b54d0 100644
--- a/Include/internal/pycore_compile.h
+++ b/Include/internal/pycore_compile.h
@@ -32,7 +32,8 @@ PyAPI_FUNC(PyCodeObject*) _PyAST_Compile(
PyObject *filename,
PyCompilerFlags *flags,
int optimize,
- struct _arena *arena);
+ struct _arena *arena,
+ PyObject *module);
/* AST preprocessing */
extern int _PyCompile_AstPreprocess(
@@ -41,7 +42,8 @@ extern int _PyCompile_AstPreprocess(
PyCompilerFlags *flags,
int optimize,
struct _arena *arena,
- int syntax_check_only);
+ int syntax_check_only,
+ PyObject *module);
extern int _PyAST_Preprocess(
struct _mod *,
@@ -50,7 +52,8 @@ extern int _PyAST_Preprocess(
int optimize,
int ff_features,
int syntax_check_only,
- int enable_warnings);
+ int enable_warnings,
+ PyObject *module);
typedef struct {
diff --git a/Include/internal/pycore_parser.h b/Include/internal/pycore_parser.h
index 2885dee63dc..2c46f59ab7d 100644
--- a/Include/internal/pycore_parser.h
+++ b/Include/internal/pycore_parser.h
@@ -48,7 +48,8 @@ extern struct _mod* _PyParser_ASTFromString(
PyObject* filename,
int mode,
PyCompilerFlags *flags,
- PyArena *arena);
+ PyArena *arena,
+ PyObject *module);
extern struct _mod* _PyParser_ASTFromFile(
FILE *fp,
diff --git a/Include/internal/pycore_pyerrors.h b/Include/internal/pycore_pyerrors.h
index 2c2048f7e12..f80808fcc8c 100644
--- a/Include/internal/pycore_pyerrors.h
+++ b/Include/internal/pycore_pyerrors.h
@@ -123,7 +123,8 @@ extern void _PyErr_SetNone(PyThreadState *tstate, PyObject *exception);
extern PyObject* _PyErr_NoMemory(PyThreadState *tstate);
extern int _PyErr_EmitSyntaxWarning(PyObject *msg, PyObject *filename, int lineno, int col_offset,
- int end_lineno, int end_col_offset);
+ int end_lineno, int end_col_offset,
+ PyObject *module);
extern void _PyErr_RaiseSyntaxError(PyObject *msg, PyObject *filename, int lineno, int col_offset,
int end_lineno, int end_col_offset);
diff --git a/Include/internal/pycore_pythonrun.h b/Include/internal/pycore_pythonrun.h
index c2832098ddb..f954f1b63ef 100644
--- a/Include/internal/pycore_pythonrun.h
+++ b/Include/internal/pycore_pythonrun.h
@@ -33,6 +33,12 @@ extern const char* _Py_SourceAsString(
PyCompilerFlags *cf,
PyObject **cmd_copy);
+extern PyObject * _Py_CompileStringObjectWithModule(
+ const char *str,
+ PyObject *filename, int start,
+ PyCompilerFlags *flags, int optimize,
+ PyObject *module);
+
/* Stack size, in "pointers". This must be large enough, so
* no two calls to check recursion depth are more than this far
diff --git a/Include/internal/pycore_symtable.h b/Include/internal/pycore_symtable.h
index 98099b4a497..9dbfa913219 100644
--- a/Include/internal/pycore_symtable.h
+++ b/Include/internal/pycore_symtable.h
@@ -188,7 +188,8 @@ extern struct symtable* _Py_SymtableStringObjectFlags(
const char *str,
PyObject *filename,
int start,
- PyCompilerFlags *flags);
+ PyCompilerFlags *flags,
+ PyObject *module);
int _PyFuture_FromAST(
struct _mod * mod,
diff --git a/Lib/ast.py b/Lib/ast.py
index 983ac1710d0..d9743ba7ab4 100644
--- a/Lib/ast.py
+++ b/Lib/ast.py
@@ -24,7 +24,7 @@
def parse(source, filename='', mode='exec', *,
- type_comments=False, feature_version=None, optimize=-1):
+ type_comments=False, feature_version=None, optimize=-1, module=None):
"""
Parse the source into an AST node.
Equivalent to compile(source, filename, mode, PyCF_ONLY_AST).
@@ -44,7 +44,8 @@ def parse(source, filename='', mode='exec', *,
feature_version = minor
# Else it should be an int giving the minor version for 3.x.
return compile(source, filename, mode, flags,
- _feature_version=feature_version, optimize=optimize)
+ _feature_version=feature_version, optimize=optimize,
+ module=module)
def literal_eval(node_or_string):
diff --git a/Lib/importlib/_bootstrap_external.py b/Lib/importlib/_bootstrap_external.py
index 035ae0fcae1..4ab0e79ea6e 100644
--- a/Lib/importlib/_bootstrap_external.py
+++ b/Lib/importlib/_bootstrap_external.py
@@ -819,13 +819,14 @@ def get_source(self, fullname):
name=fullname) from exc
return decode_source(source_bytes)
- def source_to_code(self, data, path, *, _optimize=-1):
+ def source_to_code(self, data, path, fullname=None, *, _optimize=-1):
"""Return the code object compiled from source.
The 'data' argument can be any object type that compile() supports.
"""
return _bootstrap._call_with_frames_removed(compile, data, path, 'exec',
- dont_inherit=True, optimize=_optimize)
+ dont_inherit=True, optimize=_optimize,
+ module=fullname)
def get_code(self, fullname):
"""Concrete implementation of InspectLoader.get_code.
@@ -894,7 +895,7 @@ def get_code(self, fullname):
source_path=source_path)
if source_bytes is None:
source_bytes = self.get_data(source_path)
- code_object = self.source_to_code(source_bytes, source_path)
+ code_object = self.source_to_code(source_bytes, source_path, fullname)
_bootstrap._verbose_message('code object from {}', source_path)
if (not sys.dont_write_bytecode and bytecode_path is not None and
source_mtime is not None):
@@ -1186,7 +1187,7 @@ def get_source(self, fullname):
return ''
def get_code(self, fullname):
- return compile('', '', 'exec', dont_inherit=True)
+ return compile('', '', 'exec', dont_inherit=True, module=fullname)
def create_module(self, spec):
"""Use default semantics for module creation."""
diff --git a/Lib/importlib/abc.py b/Lib/importlib/abc.py
index 1e47495f65f..5c13432b5bd 100644
--- a/Lib/importlib/abc.py
+++ b/Lib/importlib/abc.py
@@ -108,7 +108,7 @@ def get_code(self, fullname):
source = self.get_source(fullname)
if source is None:
return None
- return self.source_to_code(source)
+ return self.source_to_code(source, '', fullname)
@abc.abstractmethod
def get_source(self, fullname):
@@ -120,12 +120,12 @@ def get_source(self, fullname):
raise ImportError
@staticmethod
- def source_to_code(data, path=''):
+ def source_to_code(data, path='', fullname=None):
"""Compile 'data' into a code object.
The 'data' argument can be anything that compile() can handle. The'path'
argument should be where the data was retrieved (when applicable)."""
- return compile(data, path, 'exec', dont_inherit=True)
+ return compile(data, path, 'exec', dont_inherit=True, module=fullname)
exec_module = _bootstrap_external._LoaderBasics.exec_module
load_module = _bootstrap_external._LoaderBasics.load_module
@@ -163,9 +163,8 @@ def get_code(self, fullname):
try:
path = self.get_filename(fullname)
except ImportError:
- return self.source_to_code(source)
- else:
- return self.source_to_code(source, path)
+ path = ''
+ return self.source_to_code(source, path, fullname)
_register(
ExecutionLoader,
diff --git a/Lib/modulefinder.py b/Lib/modulefinder.py
index ac478ee7f51..b115d99ab30 100644
--- a/Lib/modulefinder.py
+++ b/Lib/modulefinder.py
@@ -334,7 +334,7 @@ def load_module(self, fqname, fp, pathname, file_info):
self.msgout(2, "load_module ->", m)
return m
if type == _PY_SOURCE:
- co = compile(fp.read(), pathname, 'exec')
+ co = compile(fp.read(), pathname, 'exec', module=fqname)
elif type == _PY_COMPILED:
try:
data = fp.read()
diff --git a/Lib/profiling/sampling/_sync_coordinator.py b/Lib/profiling/sampling/_sync_coordinator.py
index 8716e654104..adb040e89cc 100644
--- a/Lib/profiling/sampling/_sync_coordinator.py
+++ b/Lib/profiling/sampling/_sync_coordinator.py
@@ -182,7 +182,7 @@ def _execute_script(script_path: str, script_args: List[str], cwd: str) -> None:
try:
# Compile and execute the script
- code = compile(source_code, script_path, 'exec')
+ code = compile(source_code, script_path, 'exec', module='__main__')
exec(code, {'__name__': '__main__', '__file__': script_path})
except SyntaxError as e:
raise TargetError(f"Syntax error in script {script_path}: {e}") from e
diff --git a/Lib/profiling/tracing/__init__.py b/Lib/profiling/tracing/__init__.py
index 2dc7ea92c8c..a6b8edf7216 100644
--- a/Lib/profiling/tracing/__init__.py
+++ b/Lib/profiling/tracing/__init__.py
@@ -185,7 +185,7 @@ def main():
progname = args[0]
sys.path.insert(0, os.path.dirname(progname))
with io.open_code(progname) as fp:
- code = compile(fp.read(), progname, 'exec')
+ code = compile(fp.read(), progname, 'exec', module='__main__')
spec = importlib.machinery.ModuleSpec(name='__main__', loader=None,
origin=progname)
module = importlib.util.module_from_spec(spec)
diff --git a/Lib/runpy.py b/Lib/runpy.py
index ef54d3282ee..f072498f6cb 100644
--- a/Lib/runpy.py
+++ b/Lib/runpy.py
@@ -247,7 +247,7 @@ def _get_main_module_details(error=ImportError):
sys.modules[main_name] = saved_main
-def _get_code_from_file(fname):
+def _get_code_from_file(fname, module):
# Check for a compiled file first
from pkgutil import read_code
code_path = os.path.abspath(fname)
@@ -256,7 +256,7 @@ def _get_code_from_file(fname):
if code is None:
# That didn't work, so try it as normal source code
with io.open_code(code_path) as f:
- code = compile(f.read(), fname, 'exec')
+ code = compile(f.read(), fname, 'exec', module=module)
return code
def run_path(path_name, init_globals=None, run_name=None):
@@ -283,7 +283,7 @@ def run_path(path_name, init_globals=None, run_name=None):
if isinstance(importer, type(None)):
# Not a valid sys.path entry, so run the code directly
# execfile() doesn't help as we want to allow compiled files
- code = _get_code_from_file(path_name)
+ code = _get_code_from_file(path_name, run_name)
return _run_module_code(code, init_globals, run_name,
pkg_name=pkg_name, script_name=path_name)
else:
diff --git a/Lib/symtable.py b/Lib/symtable.py
index 77475c3ffd9..4c832e68f94 100644
--- a/Lib/symtable.py
+++ b/Lib/symtable.py
@@ -17,13 +17,13 @@
__all__ = ["symtable", "SymbolTableType", "SymbolTable", "Class", "Function", "Symbol"]
-def symtable(code, filename, compile_type):
+def symtable(code, filename, compile_type, *, module=None):
""" Return the toplevel *SymbolTable* for the source code.
*filename* is the name of the file with the code
and *compile_type* is the *compile()* mode argument.
"""
- top = _symtable.symtable(code, filename, compile_type)
+ top = _symtable.symtable(code, filename, compile_type, module=module)
return _newSymbolTable(top, filename)
class SymbolTableFactory:
diff --git a/Lib/test/test_ast/test_ast.py b/Lib/test/test_ast/test_ast.py
index 551de5851da..fb4a441ca64 100644
--- a/Lib/test/test_ast/test_ast.py
+++ b/Lib/test/test_ast/test_ast.py
@@ -1083,6 +1083,16 @@ def test_filter_syntax_warnings_by_module(self):
self.assertEqual(wm.filename, '')
self.assertIs(wm.category, SyntaxWarning)
+ with warnings.catch_warnings(record=True) as wlog:
+ warnings.simplefilter('error')
+ warnings.filterwarnings('always', module=r'package\.module\z')
+ warnings.filterwarnings('error', module=r'')
+ ast.parse(source, filename, module='package.module')
+ self.assertEqual(sorted(wm.lineno for wm in wlog), [4, 7, 10])
+ for wm in wlog:
+ self.assertEqual(wm.filename, filename)
+ self.assertIs(wm.category, SyntaxWarning)
+
class CopyTests(unittest.TestCase):
"""Test copying and pickling AST nodes."""
diff --git a/Lib/test/test_builtin.py b/Lib/test/test_builtin.py
index fba46af6617..ce60a5d095d 100644
--- a/Lib/test/test_builtin.py
+++ b/Lib/test/test_builtin.py
@@ -1103,7 +1103,8 @@ def test_exec_filter_syntax_warnings_by_module(self):
with warnings.catch_warnings(record=True) as wlog:
warnings.simplefilter('error')
- warnings.filterwarnings('always', module=r'\z')
+ warnings.filterwarnings('always', module=r'package.module\z')
+ warnings.filterwarnings('error', module=r'')
exec(source, {'__name__': 'package.module', '__file__': filename})
self.assertEqual(sorted(wm.lineno for wm in wlog), [4, 7, 10, 13, 14, 21])
for wm in wlog:
diff --git a/Lib/test/test_cmd_line_script.py b/Lib/test/test_cmd_line_script.py
index f8115cc8300..cc1a625a509 100644
--- a/Lib/test/test_cmd_line_script.py
+++ b/Lib/test/test_cmd_line_script.py
@@ -814,15 +814,26 @@ def test_filter_syntax_warnings_by_module(self):
filename = support.findfile('test_import/data/syntax_warnings.py')
rc, out, err = assert_python_ok(
'-Werror',
- '-Walways:::test.test_import.data.syntax_warnings',
+ '-Walways:::__main__',
+ '-Werror:::test.test_import.data.syntax_warnings',
+ '-Werror:::syntax_warnings',
filename)
self.assertEqual(err.count(b': SyntaxWarning: '), 6)
- rc, out, err = assert_python_ok(
- '-Werror',
- '-Walways:::syntax_warnings',
- filename)
- self.assertEqual(err.count(b': SyntaxWarning: '), 6)
+ def test_zipfile_run_filter_syntax_warnings_by_module(self):
+ filename = support.findfile('test_import/data/syntax_warnings.py')
+ with open(filename, 'rb') as f:
+ source = f.read()
+ with os_helper.temp_dir() as script_dir:
+ zip_name, _ = make_zip_pkg(
+ script_dir, 'test_zip', 'test_pkg', '__main__', source)
+ rc, out, err = assert_python_ok(
+ '-Werror',
+ '-Walways:::__main__',
+ '-Werror:::test_pkg.__main__',
+ os.path.join(zip_name, 'test_pkg')
+ )
+ self.assertEqual(err.count(b': SyntaxWarning: '), 12)
def tearDownModule():
diff --git a/Lib/test/test_compile.py b/Lib/test/test_compile.py
index 9c2364491fe..30f21875b22 100644
--- a/Lib/test/test_compile.py
+++ b/Lib/test/test_compile.py
@@ -1759,6 +1759,16 @@ def test_filter_syntax_warnings_by_module(self):
self.assertEqual(wm.filename, filename)
self.assertIs(wm.category, SyntaxWarning)
+ with warnings.catch_warnings(record=True) as wlog:
+ warnings.simplefilter('error')
+ warnings.filterwarnings('always', module=r'package\.module\z')
+ warnings.filterwarnings('error', module=module_re)
+ compile(source, filename, 'exec', module='package.module')
+ self.assertEqual(sorted(wm.lineno for wm in wlog), [4, 7, 10, 13, 14, 21])
+ for wm in wlog:
+ self.assertEqual(wm.filename, filename)
+ self.assertIs(wm.category, SyntaxWarning)
+
@support.subTests('src', [
textwrap.dedent("""
def f():
diff --git a/Lib/test/test_import/__init__.py b/Lib/test/test_import/__init__.py
index e87d8b7e7bb..fe669bb04df 100644
--- a/Lib/test/test_import/__init__.py
+++ b/Lib/test/test_import/__init__.py
@@ -1259,20 +1259,7 @@ def test_filter_syntax_warnings_by_module(self):
warnings.catch_warnings(record=True) as wlog):
warnings.simplefilter('error')
warnings.filterwarnings('always', module=module_re)
- import test.test_import.data.syntax_warnings
- self.assertEqual(sorted(wm.lineno for wm in wlog), [4, 7, 10, 13, 14, 21])
- filename = test.test_import.data.syntax_warnings.__file__
- for wm in wlog:
- self.assertEqual(wm.filename, filename)
- self.assertIs(wm.category, SyntaxWarning)
-
- module_re = r'syntax_warnings\z'
- unload('test.test_import.data.syntax_warnings')
- with (os_helper.temp_dir() as tmpdir,
- temporary_pycache_prefix(tmpdir),
- warnings.catch_warnings(record=True) as wlog):
- warnings.simplefilter('error')
- warnings.filterwarnings('always', module=module_re)
+ warnings.filterwarnings('error', module='syntax_warnings')
import test.test_import.data.syntax_warnings
self.assertEqual(sorted(wm.lineno for wm in wlog), [4, 7, 10, 13, 14, 21])
filename = test.test_import.data.syntax_warnings.__file__
diff --git a/Lib/test/test_runpy.py b/Lib/test/test_runpy.py
index a2a07c04f58..cc76b72b963 100644
--- a/Lib/test/test_runpy.py
+++ b/Lib/test/test_runpy.py
@@ -20,9 +20,11 @@
requires_subprocess,
verbose,
)
+from test import support
from test.support.import_helper import forget, make_legacy_pyc, unload
from test.support.os_helper import create_empty_file, temp_dir, FakePath
from test.support.script_helper import make_script, make_zip_script
+from test.test_importlib.util import temporary_pycache_prefix
import runpy
@@ -763,6 +765,47 @@ def test_encoding(self):
result = run_path(filename)
self.assertEqual(result['s'], "non-ASCII: h\xe9")
+ def test_run_module_filter_syntax_warnings_by_module(self):
+ module_re = r'test\.test_import\.data\.syntax_warnings\z'
+ with (temp_dir() as tmpdir,
+ temporary_pycache_prefix(tmpdir),
+ warnings.catch_warnings(record=True) as wlog):
+ warnings.simplefilter('error')
+ warnings.filterwarnings('always', module=module_re)
+ warnings.filterwarnings('error', module='syntax_warnings')
+ ns = run_module('test.test_import.data.syntax_warnings')
+ self.assertEqual(sorted(wm.lineno for wm in wlog), [4, 7, 10, 13, 14, 21])
+ filename = ns['__file__']
+ for wm in wlog:
+ self.assertEqual(wm.filename, filename)
+ self.assertIs(wm.category, SyntaxWarning)
+
+ def test_run_path_filter_syntax_warnings_by_module(self):
+ filename = support.findfile('test_import/data/syntax_warnings.py')
+ with warnings.catch_warnings(record=True) as wlog:
+ warnings.simplefilter('error')
+ warnings.filterwarnings('always', module=r'\z')
+ warnings.filterwarnings('error', module='test')
+ warnings.filterwarnings('error', module='syntax_warnings')
+ warnings.filterwarnings('error',
+ module=r'test\.test_import\.data\.syntax_warnings')
+ run_path(filename)
+ self.assertEqual(sorted(wm.lineno for wm in wlog), [4, 7, 10, 13, 14, 21])
+ for wm in wlog:
+ self.assertEqual(wm.filename, filename)
+ self.assertIs(wm.category, SyntaxWarning)
+
+ with warnings.catch_warnings(record=True) as wlog:
+ warnings.simplefilter('error')
+ warnings.filterwarnings('always', module=r'package\.script\z')
+ warnings.filterwarnings('error', module='')
+ warnings.filterwarnings('error', module='test')
+ warnings.filterwarnings('error', module='syntax_warnings')
+ warnings.filterwarnings('error',
+ module=r'test\.test_import\.data\.syntax_warnings')
+ run_path(filename, run_name='package.script')
+ self.assertEqual(sorted(wm.lineno for wm in wlog), [4, 7, 10, 13, 14, 21])
+
@force_not_colorized_test_class
class TestExit(unittest.TestCase):
diff --git a/Lib/test/test_symtable.py b/Lib/test/test_symtable.py
index ef2c00e04b8..094ab8f573e 100644
--- a/Lib/test/test_symtable.py
+++ b/Lib/test/test_symtable.py
@@ -601,6 +601,16 @@ def test_filter_syntax_warnings_by_module(self):
self.assertEqual(wm.filename, filename)
self.assertIs(wm.category, SyntaxWarning)
+ with warnings.catch_warnings(record=True) as wlog:
+ warnings.simplefilter('error')
+ warnings.filterwarnings('always', module=r'package\.module\z')
+ warnings.filterwarnings('error', module=module_re)
+ symtable.symtable(source, filename, 'exec', module='package.module')
+ self.assertEqual(sorted(wm.lineno for wm in wlog), [4, 7, 10])
+ for wm in wlog:
+ self.assertEqual(wm.filename, filename)
+ self.assertIs(wm.category, SyntaxWarning)
+
class ComprehensionTests(unittest.TestCase):
def get_identifiers_recursive(self, st, res):
diff --git a/Lib/test/test_zipimport_support.py b/Lib/test/test_zipimport_support.py
index ae8a8c99762..2b28f46149b 100644
--- a/Lib/test/test_zipimport_support.py
+++ b/Lib/test/test_zipimport_support.py
@@ -13,9 +13,12 @@
import inspect
import linecache
import unittest
+import warnings
+from test import support
from test.support import os_helper
from test.support.script_helper import (spawn_python, kill_python, assert_python_ok,
make_script, make_zip_script)
+from test.support import import_helper
verbose = test.support.verbose
@@ -236,6 +239,26 @@ def f():
# bdb/pdb applies normcase to its filename before displaying
self.assertIn(os.path.normcase(run_name.encode('utf-8')), data)
+ def test_import_filter_syntax_warnings_by_module(self):
+ filename = support.findfile('test_import/data/syntax_warnings.py')
+ with (os_helper.temp_dir() as tmpdir,
+ import_helper.DirsOnSysPath()):
+ zip_name, _ = make_zip_script(tmpdir, "test_zip",
+ filename, 'test_pkg/test_mod.py')
+ sys.path.insert(0, zip_name)
+ import_helper.unload('test_pkg.test_mod')
+ with warnings.catch_warnings(record=True) as wlog:
+ warnings.simplefilter('error')
+ warnings.filterwarnings('always', module=r'test_pkg\.test_mod\z')
+ warnings.filterwarnings('error', module='test_mod')
+ import test_pkg.test_mod
+ self.assertEqual(sorted(wm.lineno for wm in wlog),
+ sorted([4, 7, 10, 13, 14, 21]*2))
+ filename = test_pkg.test_mod.__file__
+ for wm in wlog:
+ self.assertEqual(wm.filename, filename)
+ self.assertIs(wm.category, SyntaxWarning)
+
def tearDownModule():
test.support.reap_children()
diff --git a/Lib/zipimport.py b/Lib/zipimport.py
index 340a7e07112..19279d1c2be 100644
--- a/Lib/zipimport.py
+++ b/Lib/zipimport.py
@@ -742,9 +742,9 @@ def _normalize_line_endings(source):
# Given a string buffer containing Python source code, compile it
# and return a code object.
-def _compile_source(pathname, source):
+def _compile_source(pathname, source, module):
source = _normalize_line_endings(source)
- return compile(source, pathname, 'exec', dont_inherit=True)
+ return compile(source, pathname, 'exec', dont_inherit=True, module=module)
# Convert the date/time values found in the Zip archive to a value
# that's compatible with the time stamp stored in .pyc files.
@@ -815,7 +815,7 @@ def _get_module_code(self, fullname):
except ImportError as exc:
import_error = exc
else:
- code = _compile_source(modpath, data)
+ code = _compile_source(modpath, data, fullname)
if code is None:
# bad magic number or non-matching mtime
# in byte code, try next
diff --git a/Misc/NEWS.d/next/Core_and_Builtins/2025-10-06-14-19-47.gh-issue-135801.OhxEZS.rst b/Misc/NEWS.d/next/Core_and_Builtins/2025-10-06-14-19-47.gh-issue-135801.OhxEZS.rst
new file mode 100644
index 00000000000..96226a7c525
--- /dev/null
+++ b/Misc/NEWS.d/next/Core_and_Builtins/2025-10-06-14-19-47.gh-issue-135801.OhxEZS.rst
@@ -0,0 +1,6 @@
+Many functions related to compiling or parsing Python code, such as
+:func:`compile`, :func:`ast.parse`, :func:`symtable.symtable`, and
+:func:`importlib.abc.InspectLoader.source_to_code` now allow to specify
+the module name.
+It is needed to unambiguous :ref:`filter ` syntax warnings
+by module name.
diff --git a/Modules/clinic/symtablemodule.c.h b/Modules/clinic/symtablemodule.c.h
index bd55d77c540..65352593f94 100644
--- a/Modules/clinic/symtablemodule.c.h
+++ b/Modules/clinic/symtablemodule.c.h
@@ -2,30 +2,67 @@
preserve
[clinic start generated code]*/
-#include "pycore_modsupport.h" // _PyArg_CheckPositional()
+#if defined(Py_BUILD_CORE) && !defined(Py_BUILD_CORE_MODULE)
+# include "pycore_gc.h" // PyGC_Head
+# include "pycore_runtime.h" // _Py_ID()
+#endif
+#include "pycore_modsupport.h" // _PyArg_UnpackKeywords()
PyDoc_STRVAR(_symtable_symtable__doc__,
-"symtable($module, source, filename, startstr, /)\n"
+"symtable($module, source, filename, startstr, /, *, module=None)\n"
"--\n"
"\n"
"Return symbol and scope dictionaries used internally by compiler.");
#define _SYMTABLE_SYMTABLE_METHODDEF \
- {"symtable", _PyCFunction_CAST(_symtable_symtable), METH_FASTCALL, _symtable_symtable__doc__},
+ {"symtable", _PyCFunction_CAST(_symtable_symtable), METH_FASTCALL|METH_KEYWORDS, _symtable_symtable__doc__},
static PyObject *
_symtable_symtable_impl(PyObject *module, PyObject *source,
- PyObject *filename, const char *startstr);
+ PyObject *filename, const char *startstr,
+ PyObject *modname);
static PyObject *
-_symtable_symtable(PyObject *module, PyObject *const *args, Py_ssize_t nargs)
+_symtable_symtable(PyObject *module, PyObject *const *args, Py_ssize_t nargs, PyObject *kwnames)
{
PyObject *return_value = NULL;
+ #if defined(Py_BUILD_CORE) && !defined(Py_BUILD_CORE_MODULE)
+
+ #define NUM_KEYWORDS 1
+ static struct {
+ PyGC_Head _this_is_not_used;
+ PyObject_VAR_HEAD
+ Py_hash_t ob_hash;
+ PyObject *ob_item[NUM_KEYWORDS];
+ } _kwtuple = {
+ .ob_base = PyVarObject_HEAD_INIT(&PyTuple_Type, NUM_KEYWORDS)
+ .ob_hash = -1,
+ .ob_item = { &_Py_ID(module), },
+ };
+ #undef NUM_KEYWORDS
+ #define KWTUPLE (&_kwtuple.ob_base.ob_base)
+
+ #else // !Py_BUILD_CORE
+ # define KWTUPLE NULL
+ #endif // !Py_BUILD_CORE
+
+ static const char * const _keywords[] = {"", "", "", "module", NULL};
+ static _PyArg_Parser _parser = {
+ .keywords = _keywords,
+ .fname = "symtable",
+ .kwtuple = KWTUPLE,
+ };
+ #undef KWTUPLE
+ PyObject *argsbuf[4];
+ Py_ssize_t noptargs = nargs + (kwnames ? PyTuple_GET_SIZE(kwnames) : 0) - 3;
PyObject *source;
PyObject *filename = NULL;
const char *startstr;
+ PyObject *modname = Py_None;
- if (!_PyArg_CheckPositional("symtable", nargs, 3, 3)) {
+ args = _PyArg_UnpackKeywords(args, nargs, NULL, kwnames, &_parser,
+ /*minpos*/ 3, /*maxpos*/ 3, /*minkw*/ 0, /*varpos*/ 0, argsbuf);
+ if (!args) {
goto exit;
}
source = args[0];
@@ -45,7 +82,12 @@ _symtable_symtable(PyObject *module, PyObject *const *args, Py_ssize_t nargs)
PyErr_SetString(PyExc_ValueError, "embedded null character");
goto exit;
}
- return_value = _symtable_symtable_impl(module, source, filename, startstr);
+ if (!noptargs) {
+ goto skip_optional_kwonly;
+ }
+ modname = args[3];
+skip_optional_kwonly:
+ return_value = _symtable_symtable_impl(module, source, filename, startstr, modname);
exit:
/* Cleanup for filename */
@@ -53,4 +95,4 @@ exit:
return return_value;
}
-/*[clinic end generated code: output=7a8545d9a1efe837 input=a9049054013a1b77]*/
+/*[clinic end generated code: output=0137be60c487c841 input=a9049054013a1b77]*/
diff --git a/Modules/symtablemodule.c b/Modules/symtablemodule.c
index d353f406831..a24927a9db6 100644
--- a/Modules/symtablemodule.c
+++ b/Modules/symtablemodule.c
@@ -16,14 +16,17 @@ _symtable.symtable
filename: unicode_fs_decoded
startstr: str
/
+ *
+ module as modname: object = None
Return symbol and scope dictionaries used internally by compiler.
[clinic start generated code]*/
static PyObject *
_symtable_symtable_impl(PyObject *module, PyObject *source,
- PyObject *filename, const char *startstr)
-/*[clinic end generated code: output=59eb0d5fc7285ac4 input=436ffff90d02e4f6]*/
+ PyObject *filename, const char *startstr,
+ PyObject *modname)
+/*[clinic end generated code: output=235ec5a87a9ce178 input=fbf9adaa33c7070d]*/
{
struct symtable *st;
PyObject *t;
@@ -50,7 +53,17 @@ _symtable_symtable_impl(PyObject *module, PyObject *source,
Py_XDECREF(source_copy);
return NULL;
}
- st = _Py_SymtableStringObjectFlags(str, filename, start, &cf);
+ if (modname == Py_None) {
+ modname = NULL;
+ }
+ else if (!PyUnicode_Check(modname)) {
+ PyErr_Format(PyExc_TypeError,
+ "symtable() argument 'module' must be str or None, not %T",
+ modname);
+ Py_XDECREF(source_copy);
+ return NULL;
+ }
+ st = _Py_SymtableStringObjectFlags(str, filename, start, &cf, modname);
Py_XDECREF(source_copy);
if (st == NULL) {
return NULL;
diff --git a/Parser/lexer/state.c b/Parser/lexer/state.c
index 2de9004fe08..3663dc3eb7f 100644
--- a/Parser/lexer/state.c
+++ b/Parser/lexer/state.c
@@ -43,6 +43,7 @@ _PyTokenizer_tok_new(void)
tok->encoding = NULL;
tok->cont_line = 0;
tok->filename = NULL;
+ tok->module = NULL;
tok->decoding_readline = NULL;
tok->decoding_buffer = NULL;
tok->readline = NULL;
@@ -91,6 +92,7 @@ _PyTokenizer_Free(struct tok_state *tok)
Py_XDECREF(tok->decoding_buffer);
Py_XDECREF(tok->readline);
Py_XDECREF(tok->filename);
+ Py_XDECREF(tok->module);
if ((tok->readline != NULL || tok->fp != NULL ) && tok->buf != NULL) {
PyMem_Free(tok->buf);
}
diff --git a/Parser/lexer/state.h b/Parser/lexer/state.h
index 877127125a7..9cd196a114c 100644
--- a/Parser/lexer/state.h
+++ b/Parser/lexer/state.h
@@ -102,6 +102,7 @@ struct tok_state {
int parenlinenostack[MAXLEVEL];
int parencolstack[MAXLEVEL];
PyObject *filename;
+ PyObject *module;
/* Stuff for checking on different tab sizes */
int altindstack[MAXINDENT]; /* Stack of alternate indents */
/* Stuff for PEP 0263 */
diff --git a/Parser/peg_api.c b/Parser/peg_api.c
index d4acc3e4935..e30ca0453bd 100644
--- a/Parser/peg_api.c
+++ b/Parser/peg_api.c
@@ -4,13 +4,15 @@
mod_ty
_PyParser_ASTFromString(const char *str, PyObject* filename, int mode,
- PyCompilerFlags *flags, PyArena *arena)
+ PyCompilerFlags *flags, PyArena *arena,
+ PyObject *module)
{
if (PySys_Audit("compile", "yO", str, filename) < 0) {
return NULL;
}
- mod_ty result = _PyPegen_run_parser_from_string(str, mode, filename, flags, arena);
+ mod_ty result = _PyPegen_run_parser_from_string(str, mode, filename, flags,
+ arena, module);
return result;
}
diff --git a/Parser/pegen.c b/Parser/pegen.c
index 70493031656..a38e973b3f6 100644
--- a/Parser/pegen.c
+++ b/Parser/pegen.c
@@ -1010,6 +1010,11 @@ _PyPegen_run_parser_from_file_pointer(FILE *fp, int start_rule, PyObject *filena
// From here on we need to clean up even if there's an error
mod_ty result = NULL;
+ tok->module = PyUnicode_FromString("__main__");
+ if (tok->module == NULL) {
+ goto error;
+ }
+
int parser_flags = compute_parser_flags(flags);
Parser *p = _PyPegen_Parser_New(tok, start_rule, parser_flags, PY_MINOR_VERSION,
errcode, NULL, arena);
@@ -1036,7 +1041,7 @@ _PyPegen_run_parser_from_file_pointer(FILE *fp, int start_rule, PyObject *filena
mod_ty
_PyPegen_run_parser_from_string(const char *str, int start_rule, PyObject *filename_ob,
- PyCompilerFlags *flags, PyArena *arena)
+ PyCompilerFlags *flags, PyArena *arena, PyObject *module)
{
int exec_input = start_rule == Py_file_input;
@@ -1054,6 +1059,7 @@ _PyPegen_run_parser_from_string(const char *str, int start_rule, PyObject *filen
}
// This transfers the ownership to the tokenizer
tok->filename = Py_NewRef(filename_ob);
+ tok->module = Py_XNewRef(module);
// We need to clear up from here on
mod_ty result = NULL;
diff --git a/Parser/pegen.h b/Parser/pegen.h
index 6b49b3537a0..b8f887608b1 100644
--- a/Parser/pegen.h
+++ b/Parser/pegen.h
@@ -378,7 +378,7 @@ mod_ty _PyPegen_run_parser_from_file_pointer(FILE *, int, PyObject *, const char
const char *, const char *, PyCompilerFlags *, int *, PyObject **,
PyArena *);
void *_PyPegen_run_parser(Parser *);
-mod_ty _PyPegen_run_parser_from_string(const char *, int, PyObject *, PyCompilerFlags *, PyArena *);
+mod_ty _PyPegen_run_parser_from_string(const char *, int, PyObject *, PyCompilerFlags *, PyArena *, PyObject *);
asdl_stmt_seq *_PyPegen_interactive_exit(Parser *);
// Generated function in parse.c - function definition in python.gram
diff --git a/Parser/string_parser.c b/Parser/string_parser.c
index ebe68989d1a..b164dfbc81a 100644
--- a/Parser/string_parser.c
+++ b/Parser/string_parser.c
@@ -88,7 +88,7 @@ warn_invalid_escape_sequence(Parser *p, const char* buffer, const char *first_in
}
if (PyErr_WarnExplicitObject(category, msg, p->tok->filename,
- lineno, NULL, NULL) < 0) {
+ lineno, p->tok->module, NULL) < 0) {
if (PyErr_ExceptionMatches(category)) {
/* Replace the Syntax/DeprecationWarning exception with a SyntaxError
to get a more accurate error report */
diff --git a/Parser/tokenizer/helpers.c b/Parser/tokenizer/helpers.c
index e5e2eed2d34..a03531a7441 100644
--- a/Parser/tokenizer/helpers.c
+++ b/Parser/tokenizer/helpers.c
@@ -127,7 +127,7 @@ _PyTokenizer_warn_invalid_escape_sequence(struct tok_state *tok, int first_inval
}
if (PyErr_WarnExplicitObject(PyExc_SyntaxWarning, msg, tok->filename,
- tok->lineno, NULL, NULL) < 0) {
+ tok->lineno, tok->module, NULL) < 0) {
Py_DECREF(msg);
if (PyErr_ExceptionMatches(PyExc_SyntaxWarning)) {
@@ -166,7 +166,7 @@ _PyTokenizer_parser_warn(struct tok_state *tok, PyObject *category, const char *
}
if (PyErr_WarnExplicitObject(category, errmsg, tok->filename,
- tok->lineno, NULL, NULL) < 0) {
+ tok->lineno, tok->module, NULL) < 0) {
if (PyErr_ExceptionMatches(category)) {
/* Replace the DeprecationWarning exception with a SyntaxError
to get a more accurate error report */
diff --git a/Programs/_freeze_module.py b/Programs/_freeze_module.py
index ba638eef6c4..62274e4aa9c 100644
--- a/Programs/_freeze_module.py
+++ b/Programs/_freeze_module.py
@@ -23,7 +23,7 @@ def read_text(inpath: str) -> bytes:
def compile_and_marshal(name: str, text: bytes) -> bytes:
filename = f""
# exec == Py_file_input
- code = compile(text, filename, "exec", optimize=0, dont_inherit=True)
+ code = compile(text, filename, "exec", optimize=0, dont_inherit=True, module=name)
return marshal.dumps(code)
diff --git a/Programs/freeze_test_frozenmain.py b/Programs/freeze_test_frozenmain.py
index 848fc31b3d6..1a986bbac2a 100644
--- a/Programs/freeze_test_frozenmain.py
+++ b/Programs/freeze_test_frozenmain.py
@@ -24,7 +24,7 @@ def dump(fp, filename, name):
with tokenize.open(filename) as source_fp:
source = source_fp.read()
- code = compile(source, code_filename, 'exec')
+ code = compile(source, code_filename, 'exec', module=name)
data = marshal.dumps(code)
writecode(fp, name, data)
diff --git a/Python/ast_preprocess.c b/Python/ast_preprocess.c
index fe6fd9479d1..d45435257cc 100644
--- a/Python/ast_preprocess.c
+++ b/Python/ast_preprocess.c
@@ -16,6 +16,7 @@ typedef struct {
typedef struct {
PyObject *filename;
+ PyObject *module;
int optimize;
int ff_features;
int syntax_check_only;
@@ -71,7 +72,8 @@ control_flow_in_finally_warning(const char *kw, stmt_ty n, _PyASTPreprocessState
}
int ret = _PyErr_EmitSyntaxWarning(msg, state->filename, n->lineno,
n->col_offset + 1, n->end_lineno,
- n->end_col_offset + 1);
+ n->end_col_offset + 1,
+ state->module);
Py_DECREF(msg);
return ret < 0 ? 0 : 1;
}
@@ -969,11 +971,13 @@ astfold_type_param(type_param_ty node_, PyArena *ctx_, _PyASTPreprocessState *st
int
_PyAST_Preprocess(mod_ty mod, PyArena *arena, PyObject *filename, int optimize,
- int ff_features, int syntax_check_only, int enable_warnings)
+ int ff_features, int syntax_check_only, int enable_warnings,
+ PyObject *module)
{
_PyASTPreprocessState state;
memset(&state, 0, sizeof(_PyASTPreprocessState));
state.filename = filename;
+ state.module = module;
state.optimize = optimize;
state.ff_features = ff_features;
state.syntax_check_only = syntax_check_only;
diff --git a/Python/bltinmodule.c b/Python/bltinmodule.c
index f6fadd936bb..c2d780ac9b9 100644
--- a/Python/bltinmodule.c
+++ b/Python/bltinmodule.c
@@ -751,6 +751,7 @@ compile as builtin_compile
dont_inherit: bool = False
optimize: int = -1
*
+ module as modname: object = None
_feature_version as feature_version: int = -1
Compile source into a code object that can be executed by exec() or eval().
@@ -770,8 +771,8 @@ in addition to any features explicitly specified.
static PyObject *
builtin_compile_impl(PyObject *module, PyObject *source, PyObject *filename,
const char *mode, int flags, int dont_inherit,
- int optimize, int feature_version)
-/*[clinic end generated code: output=b0c09c84f116d3d7 input=8f0069edbdac381b]*/
+ int optimize, PyObject *modname, int feature_version)
+/*[clinic end generated code: output=9a0dce1945917a86 input=ddeae1e0253459dc]*/
{
PyObject *source_copy;
const char *str;
@@ -800,6 +801,15 @@ builtin_compile_impl(PyObject *module, PyObject *source, PyObject *filename,
"compile(): invalid optimize value");
goto error;
}
+ if (modname == Py_None) {
+ modname = NULL;
+ }
+ else if (!PyUnicode_Check(modname)) {
+ PyErr_Format(PyExc_TypeError,
+ "compile() argument 'module' must be str or None, not %T",
+ modname);
+ goto error;
+ }
if (!dont_inherit) {
PyEval_MergeCompilerFlags(&cf);
@@ -845,8 +855,9 @@ builtin_compile_impl(PyObject *module, PyObject *source, PyObject *filename,
goto error;
}
int syntax_check_only = ((flags & PyCF_OPTIMIZED_AST) == PyCF_ONLY_AST); /* unoptiomized AST */
- if (_PyCompile_AstPreprocess(mod, filename, &cf, optimize,
- arena, syntax_check_only) < 0) {
+ if (_PyCompile_AstPreprocess(mod, filename, &cf, optimize, arena,
+ syntax_check_only, modname) < 0)
+ {
_PyArena_Free(arena);
goto error;
}
@@ -859,7 +870,7 @@ builtin_compile_impl(PyObject *module, PyObject *source, PyObject *filename,
goto error;
}
result = (PyObject*)_PyAST_Compile(mod, filename,
- &cf, optimize, arena);
+ &cf, optimize, arena, modname);
}
_PyArena_Free(arena);
goto finally;
@@ -877,7 +888,9 @@ builtin_compile_impl(PyObject *module, PyObject *source, PyObject *filename,
tstate->suppress_co_const_immortalization++;
#endif
- result = Py_CompileStringObject(str, filename, start[compile_mode], &cf, optimize);
+ result = _Py_CompileStringObjectWithModule(str, filename,
+ start[compile_mode], &cf,
+ optimize, modname);
#ifdef Py_GIL_DISABLED
tstate->suppress_co_const_immortalization--;
diff --git a/Python/clinic/bltinmodule.c.h b/Python/clinic/bltinmodule.c.h
index adb82f45c25..f08e5847abe 100644
--- a/Python/clinic/bltinmodule.c.h
+++ b/Python/clinic/bltinmodule.c.h
@@ -238,7 +238,8 @@ PyDoc_STRVAR(builtin_chr__doc__,
PyDoc_STRVAR(builtin_compile__doc__,
"compile($module, /, source, filename, mode, flags=0,\n"
-" dont_inherit=False, optimize=-1, *, _feature_version=-1)\n"
+" dont_inherit=False, optimize=-1, *, module=None,\n"
+" _feature_version=-1)\n"
"--\n"
"\n"
"Compile source into a code object that can be executed by exec() or eval().\n"
@@ -260,7 +261,7 @@ PyDoc_STRVAR(builtin_compile__doc__,
static PyObject *
builtin_compile_impl(PyObject *module, PyObject *source, PyObject *filename,
const char *mode, int flags, int dont_inherit,
- int optimize, int feature_version);
+ int optimize, PyObject *modname, int feature_version);
static PyObject *
builtin_compile(PyObject *module, PyObject *const *args, Py_ssize_t nargs, PyObject *kwnames)
@@ -268,7 +269,7 @@ builtin_compile(PyObject *module, PyObject *const *args, Py_ssize_t nargs, PyObj
PyObject *return_value = NULL;
#if defined(Py_BUILD_CORE) && !defined(Py_BUILD_CORE_MODULE)
- #define NUM_KEYWORDS 7
+ #define NUM_KEYWORDS 8
static struct {
PyGC_Head _this_is_not_used;
PyObject_VAR_HEAD
@@ -277,7 +278,7 @@ builtin_compile(PyObject *module, PyObject *const *args, Py_ssize_t nargs, PyObj
} _kwtuple = {
.ob_base = PyVarObject_HEAD_INIT(&PyTuple_Type, NUM_KEYWORDS)
.ob_hash = -1,
- .ob_item = { &_Py_ID(source), &_Py_ID(filename), &_Py_ID(mode), &_Py_ID(flags), &_Py_ID(dont_inherit), &_Py_ID(optimize), &_Py_ID(_feature_version), },
+ .ob_item = { &_Py_ID(source), &_Py_ID(filename), &_Py_ID(mode), &_Py_ID(flags), &_Py_ID(dont_inherit), &_Py_ID(optimize), &_Py_ID(module), &_Py_ID(_feature_version), },
};
#undef NUM_KEYWORDS
#define KWTUPLE (&_kwtuple.ob_base.ob_base)
@@ -286,14 +287,14 @@ builtin_compile(PyObject *module, PyObject *const *args, Py_ssize_t nargs, PyObj
# define KWTUPLE NULL
#endif // !Py_BUILD_CORE
- static const char * const _keywords[] = {"source", "filename", "mode", "flags", "dont_inherit", "optimize", "_feature_version", NULL};
+ static const char * const _keywords[] = {"source", "filename", "mode", "flags", "dont_inherit", "optimize", "module", "_feature_version", NULL};
static _PyArg_Parser _parser = {
.keywords = _keywords,
.fname = "compile",
.kwtuple = KWTUPLE,
};
#undef KWTUPLE
- PyObject *argsbuf[7];
+ PyObject *argsbuf[8];
Py_ssize_t noptargs = nargs + (kwnames ? PyTuple_GET_SIZE(kwnames) : 0) - 3;
PyObject *source;
PyObject *filename = NULL;
@@ -301,6 +302,7 @@ builtin_compile(PyObject *module, PyObject *const *args, Py_ssize_t nargs, PyObj
int flags = 0;
int dont_inherit = 0;
int optimize = -1;
+ PyObject *modname = Py_None;
int feature_version = -1;
args = _PyArg_UnpackKeywords(args, nargs, NULL, kwnames, &_parser,
@@ -359,12 +361,18 @@ skip_optional_pos:
if (!noptargs) {
goto skip_optional_kwonly;
}
- feature_version = PyLong_AsInt(args[6]);
+ if (args[6]) {
+ modname = args[6];
+ if (!--noptargs) {
+ goto skip_optional_kwonly;
+ }
+ }
+ feature_version = PyLong_AsInt(args[7]);
if (feature_version == -1 && PyErr_Occurred()) {
goto exit;
}
skip_optional_kwonly:
- return_value = builtin_compile_impl(module, source, filename, mode, flags, dont_inherit, optimize, feature_version);
+ return_value = builtin_compile_impl(module, source, filename, mode, flags, dont_inherit, optimize, modname, feature_version);
exit:
/* Cleanup for filename */
@@ -1277,4 +1285,4 @@ builtin_issubclass(PyObject *module, PyObject *const *args, Py_ssize_t nargs)
exit:
return return_value;
}
-/*[clinic end generated code: output=7eada753dc2e046f input=a9049054013a1b77]*/
+/*[clinic end generated code: output=06500bcc9a341e68 input=a9049054013a1b77]*/
diff --git a/Python/compile.c b/Python/compile.c
index e2f1c7e8eb5..6951c98500d 100644
--- a/Python/compile.c
+++ b/Python/compile.c
@@ -104,11 +104,13 @@ typedef struct _PyCompiler {
* (including instructions for nested code objects)
*/
int c_disable_warning;
+ PyObject *c_module;
} compiler;
static int
compiler_setup(compiler *c, mod_ty mod, PyObject *filename,
- PyCompilerFlags *flags, int optimize, PyArena *arena)
+ PyCompilerFlags *flags, int optimize, PyArena *arena,
+ PyObject *module)
{
PyCompilerFlags local_flags = _PyCompilerFlags_INIT;
@@ -126,6 +128,7 @@ compiler_setup(compiler *c, mod_ty mod, PyObject *filename,
if (!_PyFuture_FromAST(mod, filename, &c->c_future)) {
return ERROR;
}
+ c->c_module = Py_XNewRef(module);
if (!flags) {
flags = &local_flags;
}
@@ -136,7 +139,9 @@ compiler_setup(compiler *c, mod_ty mod, PyObject *filename,
c->c_optimize = (optimize == -1) ? _Py_GetConfig()->optimization_level : optimize;
c->c_save_nested_seqs = false;
- if (!_PyAST_Preprocess(mod, arena, filename, c->c_optimize, merged, 0, 1)) {
+ if (!_PyAST_Preprocess(mod, arena, filename, c->c_optimize, merged,
+ 0, 1, module))
+ {
return ERROR;
}
c->c_st = _PySymtable_Build(mod, filename, &c->c_future);
@@ -156,6 +161,7 @@ compiler_free(compiler *c)
_PySymtable_Free(c->c_st);
}
Py_XDECREF(c->c_filename);
+ Py_XDECREF(c->c_module);
Py_XDECREF(c->c_const_cache);
Py_XDECREF(c->c_stack);
PyMem_Free(c);
@@ -163,13 +169,13 @@ compiler_free(compiler *c)
static compiler*
new_compiler(mod_ty mod, PyObject *filename, PyCompilerFlags *pflags,
- int optimize, PyArena *arena)
+ int optimize, PyArena *arena, PyObject *module)
{
compiler *c = PyMem_Calloc(1, sizeof(compiler));
if (c == NULL) {
return NULL;
}
- if (compiler_setup(c, mod, filename, pflags, optimize, arena) < 0) {
+ if (compiler_setup(c, mod, filename, pflags, optimize, arena, module) < 0) {
compiler_free(c);
return NULL;
}
@@ -1221,7 +1227,8 @@ _PyCompile_Warn(compiler *c, location loc, const char *format, ...)
return ERROR;
}
int ret = _PyErr_EmitSyntaxWarning(msg, c->c_filename, loc.lineno, loc.col_offset + 1,
- loc.end_lineno, loc.end_col_offset + 1);
+ loc.end_lineno, loc.end_col_offset + 1,
+ c->c_module);
Py_DECREF(msg);
return ret;
}
@@ -1476,10 +1483,10 @@ _PyCompile_OptimizeAndAssemble(compiler *c, int addNone)
PyCodeObject *
_PyAST_Compile(mod_ty mod, PyObject *filename, PyCompilerFlags *pflags,
- int optimize, PyArena *arena)
+ int optimize, PyArena *arena, PyObject *module)
{
assert(!PyErr_Occurred());
- compiler *c = new_compiler(mod, filename, pflags, optimize, arena);
+ compiler *c = new_compiler(mod, filename, pflags, optimize, arena, module);
if (c == NULL) {
return NULL;
}
@@ -1492,7 +1499,8 @@ _PyAST_Compile(mod_ty mod, PyObject *filename, PyCompilerFlags *pflags,
int
_PyCompile_AstPreprocess(mod_ty mod, PyObject *filename, PyCompilerFlags *cf,
- int optimize, PyArena *arena, int no_const_folding)
+ int optimize, PyArena *arena, int no_const_folding,
+ PyObject *module)
{
_PyFutureFeatures future;
if (!_PyFuture_FromAST(mod, filename, &future)) {
@@ -1502,7 +1510,9 @@ _PyCompile_AstPreprocess(mod_ty mod, PyObject *filename, PyCompilerFlags *cf,
if (optimize == -1) {
optimize = _Py_GetConfig()->optimization_level;
}
- if (!_PyAST_Preprocess(mod, arena, filename, optimize, flags, no_const_folding, 0)) {
+ if (!_PyAST_Preprocess(mod, arena, filename, optimize, flags,
+ no_const_folding, 0, module))
+ {
return -1;
}
return 0;
@@ -1627,7 +1637,7 @@ _PyCompile_CodeGen(PyObject *ast, PyObject *filename, PyCompilerFlags *pflags,
return NULL;
}
- compiler *c = new_compiler(mod, filename, pflags, optimize, arena);
+ compiler *c = new_compiler(mod, filename, pflags, optimize, arena, NULL);
if (c == NULL) {
_PyArena_Free(arena);
return NULL;
diff --git a/Python/errors.c b/Python/errors.c
index 9fe95cec0ab..5c6ac48371a 100644
--- a/Python/errors.c
+++ b/Python/errors.c
@@ -1960,10 +1960,11 @@ _PyErr_RaiseSyntaxError(PyObject *msg, PyObject *filename, int lineno, int col_o
*/
int
_PyErr_EmitSyntaxWarning(PyObject *msg, PyObject *filename, int lineno, int col_offset,
- int end_lineno, int end_col_offset)
+ int end_lineno, int end_col_offset,
+ PyObject *module)
{
- if (PyErr_WarnExplicitObject(PyExc_SyntaxWarning, msg,
- filename, lineno, NULL, NULL) < 0)
+ if (PyErr_WarnExplicitObject(PyExc_SyntaxWarning, msg, filename, lineno,
+ module, NULL) < 0)
{
if (PyErr_ExceptionMatches(PyExc_SyntaxWarning)) {
/* Replace the SyntaxWarning exception with a SyntaxError
diff --git a/Python/pythonrun.c b/Python/pythonrun.c
index 45211e1b075..49ce0a97d47 100644
--- a/Python/pythonrun.c
+++ b/Python/pythonrun.c
@@ -1252,12 +1252,19 @@ _PyRun_StringFlagsWithName(const char *str, PyObject* name, int start,
} else {
name = &_Py_STR(anon_string);
}
+ PyObject *module = NULL;
+ if (globals && PyDict_GetItemStringRef(globals, "__name__", &module) < 0) {
+ goto done;
+ }
- mod = _PyParser_ASTFromString(str, name, start, flags, arena);
+ mod = _PyParser_ASTFromString(str, name, start, flags, arena, module);
+ Py_XDECREF(module);
- if (mod != NULL) {
+ if (mod != NULL) {
ret = run_mod(mod, name, globals, locals, flags, arena, source, generate_new_source);
}
+
+done:
Py_XDECREF(source);
_PyArena_Free(arena);
return ret;
@@ -1407,8 +1414,17 @@ run_mod(mod_ty mod, PyObject *filename, PyObject *globals, PyObject *locals,
return NULL;
}
}
+ PyObject *module = NULL;
+ if (globals && PyDict_GetItemStringRef(globals, "__name__", &module) < 0) {
+ if (interactive_src) {
+ Py_DECREF(interactive_filename);
+ }
+ return NULL;
+ }
- PyCodeObject *co = _PyAST_Compile(mod, interactive_filename, flags, -1, arena);
+ PyCodeObject *co = _PyAST_Compile(mod, interactive_filename, flags, -1,
+ arena, module);
+ Py_XDECREF(module);
if (co == NULL) {
if (interactive_src) {
Py_DECREF(interactive_filename);
@@ -1507,6 +1523,14 @@ run_pyc_file(FILE *fp, PyObject *globals, PyObject *locals,
PyObject *
Py_CompileStringObject(const char *str, PyObject *filename, int start,
PyCompilerFlags *flags, int optimize)
+{
+ return _Py_CompileStringObjectWithModule(str, filename, start,
+ flags, optimize, NULL);
+}
+
+PyObject *
+_Py_CompileStringObjectWithModule(const char *str, PyObject *filename, int start,
+ PyCompilerFlags *flags, int optimize, PyObject *module)
{
PyCodeObject *co;
mod_ty mod;
@@ -1514,14 +1538,16 @@ Py_CompileStringObject(const char *str, PyObject *filename, int start,
if (arena == NULL)
return NULL;
- mod = _PyParser_ASTFromString(str, filename, start, flags, arena);
+ mod = _PyParser_ASTFromString(str, filename, start, flags, arena, module);
if (mod == NULL) {
_PyArena_Free(arena);
return NULL;
}
if (flags && (flags->cf_flags & PyCF_ONLY_AST)) {
int syntax_check_only = ((flags->cf_flags & PyCF_OPTIMIZED_AST) == PyCF_ONLY_AST); /* unoptiomized AST */
- if (_PyCompile_AstPreprocess(mod, filename, flags, optimize, arena, syntax_check_only) < 0) {
+ if (_PyCompile_AstPreprocess(mod, filename, flags, optimize, arena,
+ syntax_check_only, module) < 0)
+ {
_PyArena_Free(arena);
return NULL;
}
@@ -1529,7 +1555,7 @@ Py_CompileStringObject(const char *str, PyObject *filename, int start,
_PyArena_Free(arena);
return result;
}
- co = _PyAST_Compile(mod, filename, flags, optimize, arena);
+ co = _PyAST_Compile(mod, filename, flags, optimize, arena, module);
_PyArena_Free(arena);
return (PyObject *)co;
}
diff --git a/Python/symtable.c b/Python/symtable.c
index bcd7365f8e1..29cf9190a4e 100644
--- a/Python/symtable.c
+++ b/Python/symtable.c
@@ -3137,7 +3137,7 @@ symtable_raise_if_not_coroutine(struct symtable *st, const char *msg, _Py_Source
struct symtable *
_Py_SymtableStringObjectFlags(const char *str, PyObject *filename,
- int start, PyCompilerFlags *flags)
+ int start, PyCompilerFlags *flags, PyObject *module)
{
struct symtable *st;
mod_ty mod;
@@ -3147,7 +3147,7 @@ _Py_SymtableStringObjectFlags(const char *str, PyObject *filename,
if (arena == NULL)
return NULL;
- mod = _PyParser_ASTFromString(str, filename, start, flags, arena);
+ mod = _PyParser_ASTFromString(str, filename, start, flags, arena, module);
if (mod == NULL) {
_PyArena_Free(arena);
return NULL;
diff --git a/Tools/peg_generator/peg_extension/peg_extension.c b/Tools/peg_generator/peg_extension/peg_extension.c
index 1587d53d594..2fec5b05129 100644
--- a/Tools/peg_generator/peg_extension/peg_extension.c
+++ b/Tools/peg_generator/peg_extension/peg_extension.c
@@ -8,7 +8,7 @@ _build_return_object(mod_ty module, int mode, PyObject *filename_ob, PyArena *ar
PyObject *result = NULL;
if (mode == 2) {
- result = (PyObject *)_PyAST_Compile(module, filename_ob, NULL, -1, arena);
+ result = (PyObject *)_PyAST_Compile(module, filename_ob, NULL, -1, arena, NULL);
} else if (mode == 1) {
result = PyAST_mod2obj(module);
} else {
@@ -93,7 +93,7 @@ parse_string(PyObject *self, PyObject *args, PyObject *kwds)
PyCompilerFlags flags = _PyCompilerFlags_INIT;
mod_ty res = _PyPegen_run_parser_from_string(the_string, Py_file_input, filename_ob,
- &flags, arena);
+ &flags, arena, NULL);
if (res == NULL) {
goto error;
}
From 2fbd39666663cb5ca1c0e3021ce2e7bc72331020 Mon Sep 17 00:00:00 2001
From: Serhiy Storchaka
Date: Thu, 13 Nov 2025 13:37:01 +0200
Subject: [PATCH 170/417] gh-140601: Refactor ElementTree.iterparse() tests
(GH-141499)
Split existing tests on smaller methods and move them to separate class.
Rename variable "content" to "it".
Use BytesIO instead of StringIO.
Add few more tests.
---
Lib/test/test_xml_etree.py | 430 ++++++++++++++++++++-----------------
1 file changed, 228 insertions(+), 202 deletions(-)
diff --git a/Lib/test/test_xml_etree.py b/Lib/test/test_xml_etree.py
index f65baa0cfae..25c084c8b9c 100644
--- a/Lib/test/test_xml_etree.py
+++ b/Lib/test/test_xml_etree.py
@@ -574,208 +574,6 @@ def test_parseliteral(self):
self.assertEqual(len(ids), 1)
self.assertEqual(ids["body"].tag, 'body')
- def test_iterparse(self):
- # Test iterparse interface.
-
- iterparse = ET.iterparse
-
- context = iterparse(SIMPLE_XMLFILE)
- self.assertIsNone(context.root)
- action, elem = next(context)
- self.assertIsNone(context.root)
- self.assertEqual((action, elem.tag), ('end', 'element'))
- self.assertEqual([(action, elem.tag) for action, elem in context], [
- ('end', 'element'),
- ('end', 'empty-element'),
- ('end', 'root'),
- ])
- self.assertEqual(context.root.tag, 'root')
-
- context = iterparse(SIMPLE_NS_XMLFILE)
- self.assertEqual([(action, elem.tag) for action, elem in context], [
- ('end', '{namespace}element'),
- ('end', '{namespace}element'),
- ('end', '{namespace}empty-element'),
- ('end', '{namespace}root'),
- ])
-
- with open(SIMPLE_XMLFILE, 'rb') as source:
- context = iterparse(source)
- action, elem = next(context)
- self.assertEqual((action, elem.tag), ('end', 'element'))
- self.assertEqual([(action, elem.tag) for action, elem in context], [
- ('end', 'element'),
- ('end', 'empty-element'),
- ('end', 'root'),
- ])
- self.assertEqual(context.root.tag, 'root')
-
- events = ()
- context = iterparse(SIMPLE_XMLFILE, events)
- self.assertEqual([(action, elem.tag) for action, elem in context], [])
-
- events = ()
- context = iterparse(SIMPLE_XMLFILE, events=events)
- self.assertEqual([(action, elem.tag) for action, elem in context], [])
-
- events = ("start", "end")
- context = iterparse(SIMPLE_XMLFILE, events)
- self.assertEqual([(action, elem.tag) for action, elem in context], [
- ('start', 'root'),
- ('start', 'element'),
- ('end', 'element'),
- ('start', 'element'),
- ('end', 'element'),
- ('start', 'empty-element'),
- ('end', 'empty-element'),
- ('end', 'root'),
- ])
-
- events = ("start", "end", "start-ns", "end-ns")
- context = iterparse(SIMPLE_NS_XMLFILE, events)
- self.assertEqual([(action, elem.tag) if action in ("start", "end")
- else (action, elem)
- for action, elem in context], [
- ('start-ns', ('', 'namespace')),
- ('start', '{namespace}root'),
- ('start', '{namespace}element'),
- ('end', '{namespace}element'),
- ('start', '{namespace}element'),
- ('end', '{namespace}element'),
- ('start', '{namespace}empty-element'),
- ('end', '{namespace}empty-element'),
- ('end', '{namespace}root'),
- ('end-ns', None),
- ])
-
- events = ('start-ns', 'end-ns')
- context = iterparse(io.StringIO(r""), events)
- res = [action for action, elem in context]
- self.assertEqual(res, ['start-ns', 'end-ns'])
-
- events = ("start", "end", "bogus")
- with open(SIMPLE_XMLFILE, "rb") as f:
- with self.assertRaises(ValueError) as cm:
- iterparse(f, events)
- self.assertFalse(f.closed)
- self.assertEqual(str(cm.exception), "unknown event 'bogus'")
-
- with warnings_helper.check_no_resource_warning(self):
- with self.assertRaises(ValueError) as cm:
- iterparse(SIMPLE_XMLFILE, events)
- self.assertEqual(str(cm.exception), "unknown event 'bogus'")
- del cm
-
- source = io.BytesIO(
- b"\n"
- b"text\n")
- events = ("start-ns",)
- context = iterparse(source, events)
- self.assertEqual([(action, elem) for action, elem in context], [
- ('start-ns', ('', 'http://\xe9ffbot.org/ns')),
- ('start-ns', ('cl\xe9', 'http://effbot.org/ns')),
- ])
-
- source = io.StringIO("junk")
- it = iterparse(source)
- action, elem = next(it)
- self.assertEqual((action, elem.tag), ('end', 'document'))
- with self.assertRaises(ET.ParseError) as cm:
- next(it)
- self.assertEqual(str(cm.exception),
- 'junk after document element: line 1, column 12')
-
- self.addCleanup(os_helper.unlink, TESTFN)
- with open(TESTFN, "wb") as f:
- f.write(b"junk")
- it = iterparse(TESTFN)
- action, elem = next(it)
- self.assertEqual((action, elem.tag), ('end', 'document'))
- with warnings_helper.check_no_resource_warning(self):
- with self.assertRaises(ET.ParseError) as cm:
- next(it)
- self.assertEqual(str(cm.exception),
- 'junk after document element: line 1, column 12')
- del cm, it
-
- # Not exhausting the iterator still closes the resource (bpo-43292)
- with warnings_helper.check_no_resource_warning(self):
- it = iterparse(SIMPLE_XMLFILE)
- del it
-
- with warnings_helper.check_no_resource_warning(self):
- it = iterparse(SIMPLE_XMLFILE)
- it.close()
- del it
-
- with warnings_helper.check_no_resource_warning(self):
- it = iterparse(SIMPLE_XMLFILE)
- action, elem = next(it)
- self.assertEqual((action, elem.tag), ('end', 'element'))
- del it, elem
-
- with warnings_helper.check_no_resource_warning(self):
- it = iterparse(SIMPLE_XMLFILE)
- action, elem = next(it)
- it.close()
- self.assertEqual((action, elem.tag), ('end', 'element'))
- del it, elem
-
- with self.assertRaises(FileNotFoundError):
- iterparse("nonexistent")
-
- def test_iterparse_close(self):
- iterparse = ET.iterparse
-
- it = iterparse(SIMPLE_XMLFILE)
- it.close()
- with self.assertRaises(StopIteration):
- next(it)
- it.close() # idempotent
-
- with open(SIMPLE_XMLFILE, 'rb') as source:
- it = iterparse(source)
- it.close()
- self.assertFalse(source.closed)
- with self.assertRaises(StopIteration):
- next(it)
- it.close() # idempotent
-
- it = iterparse(SIMPLE_XMLFILE)
- action, elem = next(it)
- self.assertEqual((action, elem.tag), ('end', 'element'))
- it.close()
- with self.assertRaises(StopIteration):
- next(it)
- it.close() # idempotent
-
- with open(SIMPLE_XMLFILE, 'rb') as source:
- it = iterparse(source)
- action, elem = next(it)
- self.assertEqual((action, elem.tag), ('end', 'element'))
- it.close()
- self.assertFalse(source.closed)
- with self.assertRaises(StopIteration):
- next(it)
- it.close() # idempotent
-
- it = iterparse(SIMPLE_XMLFILE)
- list(it)
- it.close()
- with self.assertRaises(StopIteration):
- next(it)
- it.close() # idempotent
-
- with open(SIMPLE_XMLFILE, 'rb') as source:
- it = iterparse(source)
- list(it)
- it.close()
- self.assertFalse(source.closed)
- with self.assertRaises(StopIteration):
- next(it)
- it.close() # idempotent
-
def test_writefile(self):
elem = ET.Element("tag")
elem.text = "text"
@@ -1499,6 +1297,234 @@ def test_attlist_default(self):
{'{http://www.w3.org/XML/1998/namespace}lang': 'eng'})
+class IterparseTest(unittest.TestCase):
+ # Test iterparse interface.
+
+ def test_basic(self):
+ iterparse = ET.iterparse
+
+ it = iterparse(SIMPLE_XMLFILE)
+ self.assertIsNone(it.root)
+ action, elem = next(it)
+ self.assertIsNone(it.root)
+ self.assertEqual((action, elem.tag), ('end', 'element'))
+ self.assertEqual([(action, elem.tag) for action, elem in it], [
+ ('end', 'element'),
+ ('end', 'empty-element'),
+ ('end', 'root'),
+ ])
+ self.assertEqual(it.root.tag, 'root')
+ it.close()
+
+ it = iterparse(SIMPLE_NS_XMLFILE)
+ self.assertEqual([(action, elem.tag) for action, elem in it], [
+ ('end', '{namespace}element'),
+ ('end', '{namespace}element'),
+ ('end', '{namespace}empty-element'),
+ ('end', '{namespace}root'),
+ ])
+ it.close()
+
+ def test_external_file(self):
+ with open(SIMPLE_XMLFILE, 'rb') as source:
+ it = ET.iterparse(source)
+ action, elem = next(it)
+ self.assertEqual((action, elem.tag), ('end', 'element'))
+ self.assertEqual([(action, elem.tag) for action, elem in it], [
+ ('end', 'element'),
+ ('end', 'empty-element'),
+ ('end', 'root'),
+ ])
+ self.assertEqual(it.root.tag, 'root')
+
+ def test_events(self):
+ iterparse = ET.iterparse
+
+ events = ()
+ it = iterparse(SIMPLE_XMLFILE, events)
+ self.assertEqual([(action, elem.tag) for action, elem in it], [])
+ it.close()
+
+ events = ()
+ it = iterparse(SIMPLE_XMLFILE, events=events)
+ self.assertEqual([(action, elem.tag) for action, elem in it], [])
+ it.close()
+
+ events = ("start", "end")
+ it = iterparse(SIMPLE_XMLFILE, events)
+ self.assertEqual([(action, elem.tag) for action, elem in it], [
+ ('start', 'root'),
+ ('start', 'element'),
+ ('end', 'element'),
+ ('start', 'element'),
+ ('end', 'element'),
+ ('start', 'empty-element'),
+ ('end', 'empty-element'),
+ ('end', 'root'),
+ ])
+ it.close()
+
+ def test_namespace_events(self):
+ iterparse = ET.iterparse
+
+ events = ("start", "end", "start-ns", "end-ns")
+ it = iterparse(SIMPLE_NS_XMLFILE, events)
+ self.assertEqual([(action, elem.tag) if action in ("start", "end")
+ else (action, elem)
+ for action, elem in it], [
+ ('start-ns', ('', 'namespace')),
+ ('start', '{namespace}root'),
+ ('start', '{namespace}element'),
+ ('end', '{namespace}element'),
+ ('start', '{namespace}element'),
+ ('end', '{namespace}element'),
+ ('start', '{namespace}empty-element'),
+ ('end', '{namespace}empty-element'),
+ ('end', '{namespace}root'),
+ ('end-ns', None),
+ ])
+ it.close()
+
+ events = ('start-ns', 'end-ns')
+ it = iterparse(io.BytesIO(br""), events)
+ res = [action for action, elem in it]
+ self.assertEqual(res, ['start-ns', 'end-ns'])
+ it.close()
+
+ def test_unknown_events(self):
+ iterparse = ET.iterparse
+
+ events = ("start", "end", "bogus")
+ with open(SIMPLE_XMLFILE, "rb") as f:
+ with self.assertRaises(ValueError) as cm:
+ iterparse(f, events)
+ self.assertFalse(f.closed)
+ self.assertEqual(str(cm.exception), "unknown event 'bogus'")
+
+ with warnings_helper.check_no_resource_warning(self):
+ with self.assertRaises(ValueError) as cm:
+ iterparse(SIMPLE_XMLFILE, events)
+ self.assertEqual(str(cm.exception), "unknown event 'bogus'")
+ del cm
+ gc_collect()
+
+ def test_non_utf8(self):
+ source = io.BytesIO(
+ b"\n"
+ b"text\n")
+ events = ("start-ns",)
+ it = ET.iterparse(source, events)
+ self.assertEqual([(action, elem) for action, elem in it], [
+ ('start-ns', ('', 'http://\xe9ffbot.org/ns')),
+ ('start-ns', ('cl\xe9', 'http://effbot.org/ns')),
+ ])
+
+ def test_parsing_error(self):
+ source = io.BytesIO(b"junk")
+ it = ET.iterparse(source)
+ action, elem = next(it)
+ self.assertEqual((action, elem.tag), ('end', 'document'))
+ with self.assertRaises(ET.ParseError) as cm:
+ next(it)
+ self.assertEqual(str(cm.exception),
+ 'junk after document element: line 1, column 12')
+
+ def test_nonexistent_file(self):
+ with self.assertRaises(FileNotFoundError):
+ ET.iterparse("nonexistent")
+
+ def test_resource_warnings_not_exhausted(self):
+ # Not exhausting the iterator still closes the underlying file (bpo-43292)
+ it = ET.iterparse(SIMPLE_XMLFILE)
+ with warnings_helper.check_no_resource_warning(self):
+ del it
+ gc_collect()
+
+ it = ET.iterparse(SIMPLE_XMLFILE)
+ with warnings_helper.check_no_resource_warning(self):
+ action, elem = next(it)
+ self.assertEqual((action, elem.tag), ('end', 'element'))
+ del it, elem
+ gc_collect()
+
+ def test_resource_warnings_failed_iteration(self):
+ self.addCleanup(os_helper.unlink, TESTFN)
+ with open(TESTFN, "wb") as f:
+ f.write(b"junk")
+
+ it = ET.iterparse(TESTFN)
+ action, elem = next(it)
+ self.assertEqual((action, elem.tag), ('end', 'document'))
+ with warnings_helper.check_no_resource_warning(self):
+ with self.assertRaises(ET.ParseError) as cm:
+ next(it)
+ self.assertEqual(str(cm.exception),
+ 'junk after document element: line 1, column 12')
+ del cm, it
+ gc_collect()
+
+ def test_resource_warnings_exhausted(self):
+ it = ET.iterparse(SIMPLE_XMLFILE)
+ with warnings_helper.check_no_resource_warning(self):
+ list(it)
+ del it
+ gc_collect()
+
+ def test_close_not_exhausted(self):
+ iterparse = ET.iterparse
+
+ it = iterparse(SIMPLE_XMLFILE)
+ it.close()
+ with self.assertRaises(StopIteration):
+ next(it)
+ it.close() # idempotent
+
+ with open(SIMPLE_XMLFILE, 'rb') as source:
+ it = iterparse(source)
+ it.close()
+ self.assertFalse(source.closed)
+ with self.assertRaises(StopIteration):
+ next(it)
+ it.close() # idempotent
+
+ it = iterparse(SIMPLE_XMLFILE)
+ action, elem = next(it)
+ self.assertEqual((action, elem.tag), ('end', 'element'))
+ it.close()
+ with self.assertRaises(StopIteration):
+ next(it)
+ it.close() # idempotent
+
+ with open(SIMPLE_XMLFILE, 'rb') as source:
+ it = iterparse(source)
+ action, elem = next(it)
+ self.assertEqual((action, elem.tag), ('end', 'element'))
+ it.close()
+ self.assertFalse(source.closed)
+ with self.assertRaises(StopIteration):
+ next(it)
+ it.close() # idempotent
+
+ def test_close_exhausted(self):
+ iterparse = ET.iterparse
+ it = iterparse(SIMPLE_XMLFILE)
+ list(it)
+ it.close()
+ with self.assertRaises(StopIteration):
+ next(it)
+ it.close() # idempotent
+
+ with open(SIMPLE_XMLFILE, 'rb') as source:
+ it = iterparse(source)
+ list(it)
+ it.close()
+ self.assertFalse(source.closed)
+ with self.assertRaises(StopIteration):
+ next(it)
+ it.close() # idempotent
+
+
class XMLPullParserTest(unittest.TestCase):
def _feed(self, parser, data, chunk_size=None, flush=False):
From 732224e1139f7ed4fe0259a2dad900f84910949e Mon Sep 17 00:00:00 2001
From: Cody Maloney
Date: Thu, 13 Nov 2025 05:19:44 -0800
Subject: [PATCH 171/417] gh-139871: Add `bytearray.take_bytes([n])` to
efficiently extract `bytes` (GH-140128)
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit
Update `bytearray` to contain a `bytes` and provide a zero-copy path to
"extract" the `bytes`. This allows making several code paths more efficient.
This does not move any codepaths to make use of this new API. The documentation
changes include common code patterns which can be made more efficient with
this API.
---
When just changing `bytearray` to contain `bytes` I ran pyperformance on a
`--with-lto --enable-optimizations --with-static-libpython` build and don't see
any major speedups or slowdowns with this; all seems to be in the noise of
my machine (Generally changes under 5% or benchmarks that don't touch
bytes/bytearray).
Co-authored-by: Victor Stinner
Co-authored-by: Maurycy Pawłowski-Wieroński <5383+maurycy@users.noreply.github.com>
---
Doc/library/stdtypes.rst | 24 ++
Doc/whatsnew/3.15.rst | 80 ++++++
Include/cpython/bytearrayobject.h | 16 +-
Include/internal/pycore_bytesobject.h | 8 +
Lib/test/test_bytes.py | 81 ++++++
Lib/test/test_capi/test_bytearray.py | 5 +-
Lib/test/test_sys.py | 2 +-
...-10-14-18-24-16.gh-issue-139871.SWtuUz.rst | 2 +
Objects/bytearrayobject.c | 238 ++++++++++++------
Objects/bytesobject.c | 8 +-
Objects/clinic/bytearrayobject.c.h | 39 ++-
11 files changed, 407 insertions(+), 96 deletions(-)
create mode 100644 Misc/NEWS.d/next/Core_and_Builtins/2025-10-14-18-24-16.gh-issue-139871.SWtuUz.rst
diff --git a/Doc/library/stdtypes.rst b/Doc/library/stdtypes.rst
index 97e7e08364e..c539345e598 100644
--- a/Doc/library/stdtypes.rst
+++ b/Doc/library/stdtypes.rst
@@ -3173,6 +3173,30 @@ objects.
.. versionadded:: 3.14
+ .. method:: take_bytes(n=None, /)
+
+ Remove the first *n* bytes from the bytearray and return them as an immutable
+ :class:`bytes`.
+ By default (if *n* is ``None``), return all bytes and clear the bytearray.
+
+ If *n* is negative, index from the end and take the first :func:`len`
+ plus *n* bytes. If *n* is out of bounds, raise :exc:`IndexError`.
+
+ Taking less than the full length will leave remaining bytes in the
+ :class:`bytearray`, which requires a copy. If the remaining bytes should be
+ discarded, use :func:`~bytearray.resize` or :keyword:`del` to truncate
+ then :func:`~bytearray.take_bytes` without a size.
+
+ .. impl-detail::
+
+ Taking all bytes is a zero-copy operation.
+
+ .. versionadded:: next
+
+ See the :ref:`What's New ` entry for
+ common code patterns which can be optimized with
+ :func:`bytearray.take_bytes`.
+
Since bytearray objects are sequences of integers (akin to a list), for a
bytearray object *b*, ``b[0]`` will be an integer, while ``b[0:1]`` will be
a bytearray object of length 1. (This contrasts with text strings, where
diff --git a/Doc/whatsnew/3.15.rst b/Doc/whatsnew/3.15.rst
index 3cb766978a7..d7c9a41eeb2 100644
--- a/Doc/whatsnew/3.15.rst
+++ b/Doc/whatsnew/3.15.rst
@@ -307,6 +307,86 @@ Other language changes
not only integers or floats, although this does not improve precision.
(Contributed by Serhiy Storchaka in :gh:`67795`.)
+.. _whatsnew315-bytearray-take-bytes:
+
+* Added :meth:`bytearray.take_bytes(n=None, /) ` to take
+ bytes out of a :class:`bytearray` without copying. This enables optimizing code
+ which must return :class:`bytes` after working with a mutable buffer of bytes
+ such as data buffering, network protocol parsing, encoding, decoding,
+ and compression. Common code patterns which can be optimized with
+ :func:`~bytearray.take_bytes` are listed below.
+
+ (Contributed by Cody Maloney in :gh:`139871`.)
+
+ .. list-table:: Suggested Optimizing Refactors
+ :header-rows: 1
+
+ * - Description
+ - Old
+ - New
+
+ * - Return :class:`bytes` after working with :class:`bytearray`
+ - .. code:: python
+
+ def read() -> bytes:
+ buffer = bytearray(1024)
+ ...
+ return bytes(buffer)
+
+ - .. code:: python
+
+ def read() -> bytes:
+ buffer = bytearray(1024)
+ ...
+ return buffer.take_bytes()
+
+ * - Empty a buffer getting the bytes
+ - .. code:: python
+
+ buffer = bytearray(1024)
+ ...
+ data = bytes(buffer)
+ buffer.clear()
+
+ - .. code:: python
+
+ buffer = bytearray(1024)
+ ...
+ data = buffer.take_bytes()
+
+ * - Split a buffer at a specific separator
+ - .. code:: python
+
+ buffer = bytearray(b'abc\ndef')
+ n = buffer.find(b'\n')
+ data = bytes(buffer[:n + 1])
+ del buffer[:n + 1]
+ assert data == b'abc'
+ assert buffer == bytearray(b'def')
+
+ - .. code:: python
+
+ buffer = bytearray(b'abc\ndef')
+ n = buffer.find(b'\n')
+ data = buffer.take_bytes(n + 1)
+
+ * - Split a buffer at a specific separator; discard after the separator
+ - .. code:: python
+
+ buffer = bytearray(b'abc\ndef')
+ n = buffer.find(b'\n')
+ data = bytes(buffer[:n])
+ buffer.clear()
+ assert data == b'abc'
+ assert len(buffer) == 0
+
+ - .. code:: python
+
+ buffer = bytearray(b'abc\ndef')
+ n = buffer.find(b'\n')
+ buffer.resize(n)
+ data = buffer.take_bytes()
+
* Many functions related to compiling or parsing Python code, such as
:func:`compile`, :func:`ast.parse`, :func:`symtable.symtable`,
and :func:`importlib.abc.InspectLoader.source_to_code`, now allow to pass
diff --git a/Include/cpython/bytearrayobject.h b/Include/cpython/bytearrayobject.h
index 4dddef713ce..1edd0820742 100644
--- a/Include/cpython/bytearrayobject.h
+++ b/Include/cpython/bytearrayobject.h
@@ -5,25 +5,25 @@
/* Object layout */
typedef struct {
PyObject_VAR_HEAD
- Py_ssize_t ob_alloc; /* How many bytes allocated in ob_bytes */
+ /* How many bytes allocated in ob_bytes
+
+ In the current implementation this is equivalent to Py_SIZE(ob_bytes_object).
+ The value is always loaded and stored atomically for thread safety.
+ There are API compatibilty concerns with removing so keeping for now. */
+ Py_ssize_t ob_alloc;
char *ob_bytes; /* Physical backing buffer */
char *ob_start; /* Logical start inside ob_bytes */
Py_ssize_t ob_exports; /* How many buffer exports */
+ PyObject *ob_bytes_object; /* PyBytes for zero-copy bytes conversion */
} PyByteArrayObject;
-PyAPI_DATA(char) _PyByteArray_empty_string[];
-
/* Macros and static inline functions, trading safety for speed */
#define _PyByteArray_CAST(op) \
(assert(PyByteArray_Check(op)), _Py_CAST(PyByteArrayObject*, op))
static inline char* PyByteArray_AS_STRING(PyObject *op)
{
- PyByteArrayObject *self = _PyByteArray_CAST(op);
- if (Py_SIZE(self)) {
- return self->ob_start;
- }
- return _PyByteArray_empty_string;
+ return _PyByteArray_CAST(op)->ob_start;
}
#define PyByteArray_AS_STRING(self) PyByteArray_AS_STRING(_PyObject_CAST(self))
diff --git a/Include/internal/pycore_bytesobject.h b/Include/internal/pycore_bytesobject.h
index c7bc53b6073..8e8fa696ee0 100644
--- a/Include/internal/pycore_bytesobject.h
+++ b/Include/internal/pycore_bytesobject.h
@@ -60,6 +60,14 @@ PyAPI_FUNC(void)
_PyBytes_Repeat(char* dest, Py_ssize_t len_dest,
const char* src, Py_ssize_t len_src);
+/* _PyBytesObject_SIZE gives the basic size of a bytes object; any memory allocation
+ for a bytes object of length n should request PyBytesObject_SIZE + n bytes.
+
+ Using _PyBytesObject_SIZE instead of sizeof(PyBytesObject) saves
+ 3 or 7 bytes per bytes object allocation on a typical system.
+*/
+#define _PyBytesObject_SIZE (offsetof(PyBytesObject, ob_sval) + 1)
+
/* --- PyBytesWriter ------------------------------------------------------ */
struct PyBytesWriter {
diff --git a/Lib/test/test_bytes.py b/Lib/test/test_bytes.py
index e012042159d..86898bfcab9 100644
--- a/Lib/test/test_bytes.py
+++ b/Lib/test/test_bytes.py
@@ -1397,6 +1397,16 @@ def test_clear(self):
b.append(ord('p'))
self.assertEqual(b, b'p')
+ # Cleared object should be empty.
+ b = bytearray(b'abc')
+ b.clear()
+ self.assertEqual(b.__alloc__(), 0)
+ base_size = sys.getsizeof(bytearray())
+ self.assertEqual(sys.getsizeof(b), base_size)
+ c = b.copy()
+ self.assertEqual(c.__alloc__(), 0)
+ self.assertEqual(sys.getsizeof(c), base_size)
+
def test_copy(self):
b = bytearray(b'abc')
bb = b.copy()
@@ -1458,6 +1468,61 @@ def test_resize(self):
self.assertRaises(MemoryError, bytearray().resize, sys.maxsize)
self.assertRaises(MemoryError, bytearray(1000).resize, sys.maxsize)
+ def test_take_bytes(self):
+ ba = bytearray(b'ab')
+ self.assertEqual(ba.take_bytes(), b'ab')
+ self.assertEqual(len(ba), 0)
+ self.assertEqual(ba, bytearray(b''))
+ self.assertEqual(ba.__alloc__(), 0)
+ base_size = sys.getsizeof(bytearray())
+ self.assertEqual(sys.getsizeof(ba), base_size)
+
+ # Positive and negative slicing.
+ ba = bytearray(b'abcdef')
+ self.assertEqual(ba.take_bytes(1), b'a')
+ self.assertEqual(ba, bytearray(b'bcdef'))
+ self.assertEqual(len(ba), 5)
+ self.assertEqual(ba.take_bytes(-5), b'')
+ self.assertEqual(ba, bytearray(b'bcdef'))
+ self.assertEqual(len(ba), 5)
+ self.assertEqual(ba.take_bytes(-3), b'bc')
+ self.assertEqual(ba, bytearray(b'def'))
+ self.assertEqual(len(ba), 3)
+ self.assertEqual(ba.take_bytes(3), b'def')
+ self.assertEqual(ba, bytearray(b''))
+ self.assertEqual(len(ba), 0)
+
+ # Take nothing from emptiness.
+ self.assertEqual(ba.take_bytes(0), b'')
+ self.assertEqual(ba.take_bytes(), b'')
+ self.assertEqual(ba.take_bytes(None), b'')
+
+ # Out of bounds, bad take value.
+ self.assertRaises(IndexError, ba.take_bytes, -1)
+ self.assertRaises(TypeError, ba.take_bytes, 3.14)
+ ba = bytearray(b'abcdef')
+ self.assertRaises(IndexError, ba.take_bytes, 7)
+
+ # Offset between physical and logical start (ob_bytes != ob_start).
+ ba = bytearray(b'abcde')
+ del ba[:2]
+ self.assertEqual(ba, bytearray(b'cde'))
+ self.assertEqual(ba.take_bytes(), b'cde')
+
+ # Overallocation at end.
+ ba = bytearray(b'abcde')
+ del ba[-2:]
+ self.assertEqual(ba, bytearray(b'abc'))
+ self.assertEqual(ba.take_bytes(), b'abc')
+ ba = bytearray(b'abcde')
+ ba.resize(4)
+ self.assertEqual(ba.take_bytes(), b'abcd')
+
+ # Take of a bytearray with references should fail.
+ ba = bytearray(b'abc')
+ with memoryview(ba) as mv:
+ self.assertRaises(BufferError, ba.take_bytes)
+ self.assertEqual(ba.take_bytes(), b'abc')
def test_setitem(self):
def setitem_as_mapping(b, i, val):
@@ -2564,6 +2629,18 @@ def zfill(b, a):
c = a.zfill(0x400000)
assert not c or c[-1] not in (0xdd, 0xcd)
+ def take_bytes(b, a): # MODIFIES!
+ b.wait()
+ c = a.take_bytes()
+ assert not c or c[0] == 48 # '0'
+
+ def take_bytes_n(b, a): # MODIFIES!
+ b.wait()
+ try:
+ c = a.take_bytes(10)
+ assert c == b'0123456789'
+ except IndexError: pass
+
def check(funcs, a=None, *args):
if a is None:
a = bytearray(b'0' * 0x400000)
@@ -2625,6 +2702,10 @@ def check(funcs, a=None, *args):
check([clear] + [startswith] * 10)
check([clear] + [strip] * 10)
+ check([clear] + [take_bytes] * 10)
+ check([take_bytes_n] * 10, bytearray(b'0123456789' * 0x400))
+ check([take_bytes_n] * 10, bytearray(b'0123456789' * 5))
+
check([clear] + [contains] * 10)
check([clear] + [subscript] * 10)
check([clear2] + [ass_subscript2] * 10, None, bytearray(b'0' * 0x400000))
diff --git a/Lib/test/test_capi/test_bytearray.py b/Lib/test/test_capi/test_bytearray.py
index 52565ea34c6..cb7ad8b2225 100644
--- a/Lib/test/test_capi/test_bytearray.py
+++ b/Lib/test/test_capi/test_bytearray.py
@@ -1,3 +1,4 @@
+import sys
import unittest
from test.support import import_helper
@@ -55,7 +56,9 @@ def test_fromstringandsize(self):
self.assertEqual(fromstringandsize(b'', 0), bytearray())
self.assertEqual(fromstringandsize(NULL, 0), bytearray())
self.assertEqual(len(fromstringandsize(NULL, 3)), 3)
- self.assertRaises(MemoryError, fromstringandsize, NULL, PY_SSIZE_T_MAX)
+ self.assertRaises(OverflowError, fromstringandsize, NULL, PY_SSIZE_T_MAX)
+ self.assertRaises(OverflowError, fromstringandsize, NULL,
+ PY_SSIZE_T_MAX-sys.getsizeof(b'') + 1)
self.assertRaises(SystemError, fromstringandsize, b'abc', -1)
self.assertRaises(SystemError, fromstringandsize, b'abc', PY_SSIZE_T_MIN)
diff --git a/Lib/test/test_sys.py b/Lib/test/test_sys.py
index 3ceed019ac4..9d3248d972e 100644
--- a/Lib/test/test_sys.py
+++ b/Lib/test/test_sys.py
@@ -1583,7 +1583,7 @@ def test_objecttypes(self):
samples = [b'', b'u'*100000]
for sample in samples:
x = bytearray(sample)
- check(x, vsize('n2Pi') + x.__alloc__())
+ check(x, vsize('n2PiP') + x.__alloc__())
# bytearray_iterator
check(iter(bytearray()), size('nP'))
# bytes
diff --git a/Misc/NEWS.d/next/Core_and_Builtins/2025-10-14-18-24-16.gh-issue-139871.SWtuUz.rst b/Misc/NEWS.d/next/Core_and_Builtins/2025-10-14-18-24-16.gh-issue-139871.SWtuUz.rst
new file mode 100644
index 00000000000..d4b8578afe3
--- /dev/null
+++ b/Misc/NEWS.d/next/Core_and_Builtins/2025-10-14-18-24-16.gh-issue-139871.SWtuUz.rst
@@ -0,0 +1,2 @@
+Update :class:`bytearray` to use a :class:`bytes` under the hood as its buffer
+and add :func:`bytearray.take_bytes` to take it out.
diff --git a/Objects/bytearrayobject.c b/Objects/bytearrayobject.c
index a73bfff340c..99bfdec89f6 100644
--- a/Objects/bytearrayobject.c
+++ b/Objects/bytearrayobject.c
@@ -17,8 +17,8 @@ class bytearray "PyByteArrayObject *" "&PyByteArray_Type"
[clinic start generated code]*/
/*[clinic end generated code: output=da39a3ee5e6b4b0d input=5535b77c37a119e0]*/
-/* For PyByteArray_AS_STRING(). */
-char _PyByteArray_empty_string[] = "";
+/* Max number of bytes a bytearray can contain */
+#define PyByteArray_SIZE_MAX ((Py_ssize_t)(PY_SSIZE_T_MAX - _PyBytesObject_SIZE))
/* Helpers */
@@ -43,6 +43,14 @@ _getbytevalue(PyObject* arg, int *value)
return 1;
}
+static void
+bytearray_reinit_from_bytes(PyByteArrayObject *self, Py_ssize_t size,
+ Py_ssize_t alloc) {
+ self->ob_bytes = self->ob_start = PyBytes_AS_STRING(self->ob_bytes_object);
+ Py_SET_SIZE(self, size);
+ FT_ATOMIC_STORE_SSIZE_RELAXED(self->ob_alloc, alloc);
+}
+
static int
bytearray_getbuffer_lock_held(PyObject *self, Py_buffer *view, int flags)
{
@@ -127,7 +135,6 @@ PyObject *
PyByteArray_FromStringAndSize(const char *bytes, Py_ssize_t size)
{
PyByteArrayObject *new;
- Py_ssize_t alloc;
if (size < 0) {
PyErr_SetString(PyExc_SystemError,
@@ -135,35 +142,32 @@ PyByteArray_FromStringAndSize(const char *bytes, Py_ssize_t size)
return NULL;
}
- /* Prevent buffer overflow when setting alloc to size+1. */
- if (size == PY_SSIZE_T_MAX) {
- return PyErr_NoMemory();
- }
-
new = PyObject_New(PyByteArrayObject, &PyByteArray_Type);
- if (new == NULL)
+ if (new == NULL) {
return NULL;
+ }
- if (size == 0) {
- new->ob_bytes = NULL;
- alloc = 0;
- }
- else {
- alloc = size + 1;
- new->ob_bytes = PyMem_Malloc(alloc);
- if (new->ob_bytes == NULL) {
- Py_DECREF(new);
- return PyErr_NoMemory();
- }
- if (bytes != NULL && size > 0)
- memcpy(new->ob_bytes, bytes, size);
- new->ob_bytes[size] = '\0'; /* Trailing null byte */
- }
- Py_SET_SIZE(new, size);
- new->ob_alloc = alloc;
- new->ob_start = new->ob_bytes;
+ /* Fill values used in bytearray_dealloc.
+
+ In an optimized build the memory isn't zeroed and ob_exports would be
+ uninitialized when when PyBytes_FromStringAndSize errored leading to
+ intermittent test failures. */
new->ob_exports = 0;
+ /* Optimization: size=0 bytearray should not allocate space
+
+ PyBytes_FromStringAndSize returns the empty bytes global when size=0 so
+ no allocation occurs. */
+ new->ob_bytes_object = PyBytes_FromStringAndSize(NULL, size);
+ if (new->ob_bytes_object == NULL) {
+ Py_DECREF(new);
+ return NULL;
+ }
+ bytearray_reinit_from_bytes(new, size, size);
+ if (bytes != NULL && size > 0) {
+ memcpy(new->ob_bytes, bytes, size);
+ }
+
return (PyObject *)new;
}
@@ -189,7 +193,6 @@ static int
bytearray_resize_lock_held(PyObject *self, Py_ssize_t requested_size)
{
_Py_CRITICAL_SECTION_ASSERT_OBJECT_LOCKED(self);
- void *sval;
PyByteArrayObject *obj = ((PyByteArrayObject *)self);
/* All computations are done unsigned to avoid integer overflows
(see issue #22335). */
@@ -214,16 +217,17 @@ bytearray_resize_lock_held(PyObject *self, Py_ssize_t requested_size)
return -1;
}
- if (size + logical_offset + 1 <= alloc) {
+ if (size + logical_offset <= alloc) {
/* Current buffer is large enough to host the requested size,
decide on a strategy. */
if (size < alloc / 2) {
/* Major downsize; resize down to exact size */
- alloc = size + 1;
+ alloc = size;
}
else {
/* Minor downsize; quick exit */
Py_SET_SIZE(self, size);
+ /* Add mid-buffer null; end provided by bytes. */
PyByteArray_AS_STRING(self)[size] = '\0'; /* Trailing null */
return 0;
}
@@ -236,38 +240,36 @@ bytearray_resize_lock_held(PyObject *self, Py_ssize_t requested_size)
}
else {
/* Major upsize; resize up to exact size */
- alloc = size + 1;
+ alloc = size;
}
}
- if (alloc > PY_SSIZE_T_MAX) {
+ if (alloc > PyByteArray_SIZE_MAX) {
PyErr_NoMemory();
return -1;
}
+ /* Re-align data to the start of the allocation. */
if (logical_offset > 0) {
- sval = PyMem_Malloc(alloc);
- if (sval == NULL) {
- PyErr_NoMemory();
- return -1;
- }
- memcpy(sval, PyByteArray_AS_STRING(self),
- Py_MIN((size_t)requested_size, (size_t)Py_SIZE(self)));
- PyMem_Free(obj->ob_bytes);
- }
- else {
- sval = PyMem_Realloc(obj->ob_bytes, alloc);
- if (sval == NULL) {
- PyErr_NoMemory();
- return -1;
- }
+ /* optimization tradeoff: This is faster than a new allocation when
+ the number of bytes being removed in a resize is small; for large
+ size changes it may be better to just make a new bytes object as
+ _PyBytes_Resize will do a malloc + memcpy internally. */
+ memmove(obj->ob_bytes, obj->ob_start,
+ Py_MIN(requested_size, Py_SIZE(self)));
}
- obj->ob_bytes = obj->ob_start = sval;
- Py_SET_SIZE(self, size);
- FT_ATOMIC_STORE_SSIZE_RELAXED(obj->ob_alloc, alloc);
- obj->ob_bytes[size] = '\0'; /* Trailing null byte */
+ int ret = _PyBytes_Resize(&obj->ob_bytes_object, alloc);
+ if (ret == -1) {
+ obj->ob_bytes_object = Py_GetConstant(Py_CONSTANT_EMPTY_BYTES);
+ size = alloc = 0;
+ }
+ bytearray_reinit_from_bytes(obj, size, alloc);
+ if (alloc != size) {
+ /* Add mid-buffer null; end provided by bytes. */
+ obj->ob_bytes[size] = '\0';
+ }
- return 0;
+ return ret;
}
int
@@ -295,7 +297,7 @@ PyByteArray_Concat(PyObject *a, PyObject *b)
goto done;
}
- if (va.len > PY_SSIZE_T_MAX - vb.len) {
+ if (va.len > PyByteArray_SIZE_MAX - vb.len) {
PyErr_NoMemory();
goto done;
}
@@ -339,7 +341,7 @@ bytearray_iconcat_lock_held(PyObject *op, PyObject *other)
}
Py_ssize_t size = Py_SIZE(self);
- if (size > PY_SSIZE_T_MAX - vo.len) {
+ if (size > PyByteArray_SIZE_MAX - vo.len) {
PyBuffer_Release(&vo);
return PyErr_NoMemory();
}
@@ -373,7 +375,7 @@ bytearray_repeat_lock_held(PyObject *op, Py_ssize_t count)
count = 0;
}
const Py_ssize_t mysize = Py_SIZE(self);
- if (count > 0 && mysize > PY_SSIZE_T_MAX / count) {
+ if (count > 0 && mysize > PyByteArray_SIZE_MAX / count) {
return PyErr_NoMemory();
}
Py_ssize_t size = mysize * count;
@@ -409,7 +411,7 @@ bytearray_irepeat_lock_held(PyObject *op, Py_ssize_t count)
}
const Py_ssize_t mysize = Py_SIZE(self);
- if (count > 0 && mysize > PY_SSIZE_T_MAX / count) {
+ if (count > 0 && mysize > PyByteArray_SIZE_MAX / count) {
return PyErr_NoMemory();
}
const Py_ssize_t size = mysize * count;
@@ -585,7 +587,7 @@ bytearray_setslice_linear(PyByteArrayObject *self,
buf = PyByteArray_AS_STRING(self);
}
else if (growth > 0) {
- if (Py_SIZE(self) > (Py_ssize_t)PY_SSIZE_T_MAX - growth) {
+ if (Py_SIZE(self) > PyByteArray_SIZE_MAX - growth) {
PyErr_NoMemory();
return -1;
}
@@ -899,6 +901,13 @@ bytearray___init___impl(PyByteArrayObject *self, PyObject *arg,
PyObject *it;
PyObject *(*iternext)(PyObject *);
+ /* First __init__; set ob_bytes_object so ob_bytes is always non-null. */
+ if (self->ob_bytes_object == NULL) {
+ self->ob_bytes_object = Py_GetConstant(Py_CONSTANT_EMPTY_BYTES);
+ bytearray_reinit_from_bytes(self, 0, 0);
+ self->ob_exports = 0;
+ }
+
if (Py_SIZE(self) != 0) {
/* Empty previous contents (yes, do this first of all!) */
if (PyByteArray_Resize((PyObject *)self, 0) < 0)
@@ -1169,9 +1178,7 @@ bytearray_dealloc(PyObject *op)
"deallocated bytearray object has exported buffers");
PyErr_Print();
}
- if (self->ob_bytes != 0) {
- PyMem_Free(self->ob_bytes);
- }
+ Py_XDECREF(self->ob_bytes_object);
Py_TYPE(self)->tp_free((PyObject *)self);
}
@@ -1491,6 +1498,82 @@ bytearray_resize_impl(PyByteArrayObject *self, Py_ssize_t size)
}
+/*[clinic input]
+@critical_section
+bytearray.take_bytes
+ n: object = None
+ Bytes to take, negative indexes from end. None indicates all bytes.
+ /
+Take *n* bytes from the bytearray and return them as a bytes object.
+[clinic start generated code]*/
+
+static PyObject *
+bytearray_take_bytes_impl(PyByteArrayObject *self, PyObject *n)
+/*[clinic end generated code: output=3147fbc0bbbe8d94 input=b15b5172cdc6deda]*/
+{
+ Py_ssize_t to_take;
+ Py_ssize_t size = Py_SIZE(self);
+ if (Py_IsNone(n)) {
+ to_take = size;
+ }
+ // Integer index, from start (zero, positive) or end (negative).
+ else if (_PyIndex_Check(n)) {
+ to_take = PyNumber_AsSsize_t(n, PyExc_IndexError);
+ if (to_take == -1 && PyErr_Occurred()) {
+ return NULL;
+ }
+ if (to_take < 0) {
+ to_take += size;
+ }
+ }
+ else {
+ PyErr_SetString(PyExc_TypeError, "n must be an integer or None");
+ return NULL;
+ }
+
+ if (to_take < 0 || to_take > size) {
+ PyErr_Format(PyExc_IndexError,
+ "can't take %zd bytes outside size %zd",
+ to_take, size);
+ return NULL;
+ }
+
+ // Exports may change the contents. No mutable bytes allowed.
+ if (!_canresize(self)) {
+ return NULL;
+ }
+
+ if (to_take == 0 || size == 0) {
+ return Py_GetConstant(Py_CONSTANT_EMPTY_BYTES);
+ }
+
+ // Copy remaining bytes to a new bytes.
+ Py_ssize_t remaining_length = size - to_take;
+ PyObject *remaining = PyBytes_FromStringAndSize(self->ob_start + to_take,
+ remaining_length);
+ if (remaining == NULL) {
+ return NULL;
+ }
+
+ // If the bytes are offset inside the buffer must first align.
+ if (self->ob_start != self->ob_bytes) {
+ memmove(self->ob_bytes, self->ob_start, to_take);
+ self->ob_start = self->ob_bytes;
+ }
+
+ if (_PyBytes_Resize(&self->ob_bytes_object, to_take) == -1) {
+ Py_DECREF(remaining);
+ return NULL;
+ }
+
+ // Point the bytearray towards the buffer with the remaining data.
+ PyObject *result = self->ob_bytes_object;
+ self->ob_bytes_object = remaining;
+ bytearray_reinit_from_bytes(self, remaining_length, remaining_length);
+ return result;
+}
+
+
/*[clinic input]
@critical_section
bytearray.translate
@@ -1868,11 +1951,6 @@ bytearray_insert_impl(PyByteArrayObject *self, Py_ssize_t index, int item)
Py_ssize_t n = Py_SIZE(self);
char *buf;
- if (n == PY_SSIZE_T_MAX) {
- PyErr_SetString(PyExc_OverflowError,
- "cannot add more objects to bytearray");
- return NULL;
- }
if (bytearray_resize_lock_held((PyObject *)self, n + 1) < 0)
return NULL;
buf = PyByteArray_AS_STRING(self);
@@ -1987,11 +2065,6 @@ bytearray_append_impl(PyByteArrayObject *self, int item)
{
Py_ssize_t n = Py_SIZE(self);
- if (n == PY_SSIZE_T_MAX) {
- PyErr_SetString(PyExc_OverflowError,
- "cannot add more objects to bytearray");
- return NULL;
- }
if (bytearray_resize_lock_held((PyObject *)self, n + 1) < 0)
return NULL;
@@ -2099,16 +2172,16 @@ bytearray_extend_impl(PyByteArrayObject *self, PyObject *iterable_of_ints)
if (len >= buf_size) {
Py_ssize_t addition;
- if (len == PY_SSIZE_T_MAX) {
+ if (len == PyByteArray_SIZE_MAX) {
Py_DECREF(it);
Py_DECREF(bytearray_obj);
return PyErr_NoMemory();
}
addition = len >> 1;
- if (addition > PY_SSIZE_T_MAX - len - 1)
- buf_size = PY_SSIZE_T_MAX;
+ if (addition > PyByteArray_SIZE_MAX - len)
+ buf_size = PyByteArray_SIZE_MAX;
else
- buf_size = len + addition + 1;
+ buf_size = len + addition;
if (bytearray_resize_lock_held((PyObject *)bytearray_obj, buf_size) < 0) {
Py_DECREF(it);
Py_DECREF(bytearray_obj);
@@ -2405,7 +2478,11 @@ static PyObject *
bytearray_alloc(PyObject *op, PyObject *Py_UNUSED(ignored))
{
PyByteArrayObject *self = _PyByteArray_CAST(op);
- return PyLong_FromSsize_t(FT_ATOMIC_LOAD_SSIZE_RELAXED(self->ob_alloc));
+ Py_ssize_t alloc = FT_ATOMIC_LOAD_SSIZE_RELAXED(self->ob_alloc);
+ if (alloc > 0) {
+ alloc += _PyBytesObject_SIZE;
+ }
+ return PyLong_FromSsize_t(alloc);
}
/*[clinic input]
@@ -2601,9 +2678,13 @@ static PyObject *
bytearray_sizeof_impl(PyByteArrayObject *self)
/*[clinic end generated code: output=738abdd17951c427 input=e27320fd98a4bc5a]*/
{
- size_t res = _PyObject_SIZE(Py_TYPE(self));
- res += (size_t)FT_ATOMIC_LOAD_SSIZE_RELAXED(self->ob_alloc) * sizeof(char);
- return PyLong_FromSize_t(res);
+ Py_ssize_t res = _PyObject_SIZE(Py_TYPE(self));
+ Py_ssize_t alloc = FT_ATOMIC_LOAD_SSIZE_RELAXED(self->ob_alloc);
+ if (alloc > 0) {
+ res += _PyBytesObject_SIZE + alloc;
+ }
+
+ return PyLong_FromSsize_t(res);
}
static PySequenceMethods bytearray_as_sequence = {
@@ -2686,6 +2767,7 @@ static PyMethodDef bytearray_methods[] = {
BYTEARRAY_STARTSWITH_METHODDEF
BYTEARRAY_STRIP_METHODDEF
{"swapcase", bytearray_swapcase, METH_NOARGS, _Py_swapcase__doc__},
+ BYTEARRAY_TAKE_BYTES_METHODDEF
{"title", bytearray_title, METH_NOARGS, _Py_title__doc__},
BYTEARRAY_TRANSLATE_METHODDEF
{"upper", bytearray_upper, METH_NOARGS, _Py_upper__doc__},
diff --git a/Objects/bytesobject.c b/Objects/bytesobject.c
index 2b9513abe91..2b0925017f2 100644
--- a/Objects/bytesobject.c
+++ b/Objects/bytesobject.c
@@ -25,13 +25,7 @@ class bytes "PyBytesObject *" "&PyBytes_Type"
#include "clinic/bytesobject.c.h"
-/* PyBytesObject_SIZE gives the basic size of a bytes object; any memory allocation
- for a bytes object of length n should request PyBytesObject_SIZE + n bytes.
-
- Using PyBytesObject_SIZE instead of sizeof(PyBytesObject) saves
- 3 or 7 bytes per bytes object allocation on a typical system.
-*/
-#define PyBytesObject_SIZE (offsetof(PyBytesObject, ob_sval) + 1)
+#define PyBytesObject_SIZE _PyBytesObject_SIZE
/* Forward declaration */
static void* _PyBytesWriter_ResizeAndUpdatePointer(PyBytesWriter *writer,
diff --git a/Objects/clinic/bytearrayobject.c.h b/Objects/clinic/bytearrayobject.c.h
index 6f13865177d..be704ccf68f 100644
--- a/Objects/clinic/bytearrayobject.c.h
+++ b/Objects/clinic/bytearrayobject.c.h
@@ -631,6 +631,43 @@ exit:
return return_value;
}
+PyDoc_STRVAR(bytearray_take_bytes__doc__,
+"take_bytes($self, n=None, /)\n"
+"--\n"
+"\n"
+"Take *n* bytes from the bytearray and return them as a bytes object.\n"
+"\n"
+" n\n"
+" Bytes to take, negative indexes from end. None indicates all bytes.");
+
+#define BYTEARRAY_TAKE_BYTES_METHODDEF \
+ {"take_bytes", _PyCFunction_CAST(bytearray_take_bytes), METH_FASTCALL, bytearray_take_bytes__doc__},
+
+static PyObject *
+bytearray_take_bytes_impl(PyByteArrayObject *self, PyObject *n);
+
+static PyObject *
+bytearray_take_bytes(PyObject *self, PyObject *const *args, Py_ssize_t nargs)
+{
+ PyObject *return_value = NULL;
+ PyObject *n = Py_None;
+
+ if (!_PyArg_CheckPositional("take_bytes", nargs, 0, 1)) {
+ goto exit;
+ }
+ if (nargs < 1) {
+ goto skip_optional;
+ }
+ n = args[0];
+skip_optional:
+ Py_BEGIN_CRITICAL_SECTION(self);
+ return_value = bytearray_take_bytes_impl((PyByteArrayObject *)self, n);
+ Py_END_CRITICAL_SECTION();
+
+exit:
+ return return_value;
+}
+
PyDoc_STRVAR(bytearray_translate__doc__,
"translate($self, table, /, delete=b\'\')\n"
"--\n"
@@ -1796,4 +1833,4 @@ bytearray_sizeof(PyObject *self, PyObject *Py_UNUSED(ignored))
{
return bytearray_sizeof_impl((PyByteArrayObject *)self);
}
-/*[clinic end generated code: output=fdfe41139c91e409 input=a9049054013a1b77]*/
+/*[clinic end generated code: output=5eddefde2a001ceb input=a9049054013a1b77]*/
From c2470b39fa21f355f811419f1b3d1c776c36fb10 Mon Sep 17 00:00:00 2001
From: Mikhail Efimov
Date: Thu, 13 Nov 2025 17:44:40 +0300
Subject: [PATCH 172/417] gh-137959: Fix `TIER1_TO_TIER2` macro name in JIT
InternalDocs (GH-141496)
JIT InternalDocs fix
---
InternalDocs/jit.md | 2 +-
1 file changed, 1 insertion(+), 1 deletion(-)
diff --git a/InternalDocs/jit.md b/InternalDocs/jit.md
index 09585380737..1740b22b85f 100644
--- a/InternalDocs/jit.md
+++ b/InternalDocs/jit.md
@@ -53,7 +53,7 @@ ## The micro-op optimizer
## The JIT interpreter
After a `JUMP_BACKWARD` instruction invokes the uop optimizer to create a uop
-executor, it transfers control to this executor via the `GOTO_TIER_TWO` macro.
+executor, it transfers control to this executor via the `TIER1_TO_TIER2` macro.
CPython implements two executors. Here we describe the JIT interpreter,
which is the simpler of them and is therefore useful for debugging and analyzing
From f72768f30e6ed9253eb3b6374b4395dfcaf4842a Mon Sep 17 00:00:00 2001
From: Peter Bierma
Date: Thu, 13 Nov 2025 10:02:21 -0500
Subject: [PATCH 173/417] gh-141004: Document C APIs for dictionary keys,
values, and items (GH-141009)
Co-authored-by: Petr Viktorin
---
Doc/c-api/dict.rst | 46 ++++++++++++++++++++++++++++++++++++++++++++++
1 file changed, 46 insertions(+)
diff --git a/Doc/c-api/dict.rst b/Doc/c-api/dict.rst
index 246ce5391e1..b7f201811aa 100644
--- a/Doc/c-api/dict.rst
+++ b/Doc/c-api/dict.rst
@@ -431,3 +431,49 @@ Dictionary Objects
it before returning.
.. versionadded:: 3.12
+
+
+Dictionary View Objects
+^^^^^^^^^^^^^^^^^^^^^^^
+
+.. c:function:: int PyDictViewSet_Check(PyObject *op)
+
+ Return true if *op* is a view of a set inside a dictionary. This is currently
+ equivalent to :c:expr:`PyDictKeys_Check(op) || PyDictItems_Check(op)`. This
+ function always succeeds.
+
+
+.. c:var:: PyTypeObject PyDictKeys_Type
+
+ Type object for a view of dictionary keys. In Python, this is the type of
+ the object returned by :meth:`dict.keys`.
+
+
+.. c:function:: int PyDictKeys_Check(PyObject *op)
+
+ Return true if *op* is an instance of a dictionary keys view. This function
+ always succeeds.
+
+
+.. c:var:: PyTypeObject PyDictValues_Type
+
+ Type object for a view of dictionary values. In Python, this is the type of
+ the object returned by :meth:`dict.values`.
+
+
+.. c:function:: int PyDictValues_Check(PyObject *op)
+
+ Return true if *op* is an instance of a dictionary values view. This function
+ always succeeds.
+
+
+.. c:var:: PyTypeObject PyDictItems_Type
+
+ Type object for a view of dictionary items. In Python, this is the type of
+ the object returned by :meth:`dict.items`.
+
+
+.. c:function:: int PyDictItems_Check(PyObject *op)
+
+ Return true if *op* is an instance of a dictionary items view. This function
+ always succeeds.
From d7862e9b1bd8f82e41c4f2c4dad31e15707d856f Mon Sep 17 00:00:00 2001
From: Peter Bierma
Date: Thu, 13 Nov 2025 10:07:57 -0500
Subject: [PATCH 174/417] gh-141004: Document `PyCode_Optimize` (GH-141378)
---
Doc/c-api/code.rst | 11 +++++++++++
1 file changed, 11 insertions(+)
diff --git a/Doc/c-api/code.rst b/Doc/c-api/code.rst
index c9741b61254..45f5e83adc4 100644
--- a/Doc/c-api/code.rst
+++ b/Doc/c-api/code.rst
@@ -211,6 +211,17 @@ bound into a function.
.. versionadded:: 3.12
+.. c:function:: PyObject *PyCode_Optimize(PyObject *code, PyObject *consts, PyObject *names, PyObject *lnotab_obj)
+
+ This is a :term:`soft deprecated` function that does nothing.
+
+ Prior to Python 3.10, this function would perform basic optimizations to a
+ code object.
+
+ .. versionchanged:: 3.10
+ This function now does nothing.
+
+
.. _c_codeobject_flags:
Code Object Flags
From b99db92dde38b17c3fba3b5db76a383ceddfce49 Mon Sep 17 00:00:00 2001
From: Victor Stinner
Date: Thu, 13 Nov 2025 17:30:50 +0100
Subject: [PATCH 175/417] gh-139653: Add
PyUnstable_ThreadState_SetStackProtection() (#139668)
Add PyUnstable_ThreadState_SetStackProtection() and
PyUnstable_ThreadState_ResetStackProtection() functions
to set the stack base address and stack size of a Python
thread state.
Co-authored-by: Petr Viktorin
---
Doc/c-api/exceptions.rst | 3 +
Doc/c-api/init.rst | 37 +++++++++
Doc/whatsnew/3.15.rst | 6 ++
Include/cpython/pystate.h | 12 +++
Include/internal/pycore_pythonrun.h | 6 ++
Include/internal/pycore_tstate.h | 4 +
...-10-06-22-17-47.gh-issue-139653.6-1MOd.rst | 4 +
Modules/_testinternalcapi.c | 54 +++++++++++++
Python/ceval.c | 77 +++++++++++++++++--
Python/pystate.c | 3 +
10 files changed, 199 insertions(+), 7 deletions(-)
create mode 100644 Misc/NEWS.d/next/C_API/2025-10-06-22-17-47.gh-issue-139653.6-1MOd.rst
diff --git a/Doc/c-api/exceptions.rst b/Doc/c-api/exceptions.rst
index 5241533e112..0ee595a07ac 100644
--- a/Doc/c-api/exceptions.rst
+++ b/Doc/c-api/exceptions.rst
@@ -976,6 +976,9 @@ because the :ref:`call protocol ` takes care of recursion handling.
be concatenated to the :exc:`RecursionError` message caused by the recursion
depth limit.
+ .. seealso::
+ The :c:func:`PyUnstable_ThreadState_SetStackProtection` function.
+
.. versionchanged:: 3.9
This function is now also available in the :ref:`limited API `.
diff --git a/Doc/c-api/init.rst b/Doc/c-api/init.rst
index 49ffeab5585..18ee1611807 100644
--- a/Doc/c-api/init.rst
+++ b/Doc/c-api/init.rst
@@ -1366,6 +1366,43 @@ All of the following functions must be called after :c:func:`Py_Initialize`.
.. versionadded:: 3.11
+.. c:function:: int PyUnstable_ThreadState_SetStackProtection(PyThreadState *tstate, void *stack_start_addr, size_t stack_size)
+
+ Set the stack protection start address and stack protection size
+ of a Python thread state.
+
+ On success, return ``0``.
+ On failure, set an exception and return ``-1``.
+
+ CPython implements :ref:`recursion control ` for C code by raising
+ :py:exc:`RecursionError` when it notices that the machine execution stack is close
+ to overflow. See for example the :c:func:`Py_EnterRecursiveCall` function.
+ For this, it needs to know the location of the current thread's stack, which it
+ normally gets from the operating system.
+ When the stack is changed, for example using context switching techniques like the
+ Boost library's ``boost::context``, you must call
+ :c:func:`~PyUnstable_ThreadState_SetStackProtection` to inform CPython of the change.
+
+ Call :c:func:`~PyUnstable_ThreadState_SetStackProtection` either before
+ or after changing the stack.
+ Do not call any other Python C API between the call and the stack
+ change.
+
+ See :c:func:`PyUnstable_ThreadState_ResetStackProtection` for undoing this operation.
+
+ .. versionadded:: next
+
+
+.. c:function:: void PyUnstable_ThreadState_ResetStackProtection(PyThreadState *tstate)
+
+ Reset the stack protection start address and stack protection size
+ of a Python thread state to the operating system defaults.
+
+ See :c:func:`PyUnstable_ThreadState_SetStackProtection` for an explanation.
+
+ .. versionadded:: next
+
+
.. c:function:: PyInterpreterState* PyInterpreterState_Get(void)
Get the current interpreter.
diff --git a/Doc/whatsnew/3.15.rst b/Doc/whatsnew/3.15.rst
index d7c9a41eeb2..b360ad964cf 100644
--- a/Doc/whatsnew/3.15.rst
+++ b/Doc/whatsnew/3.15.rst
@@ -1066,6 +1066,12 @@ New features
* Add :c:func:`PyTuple_FromArray` to create a :class:`tuple` from an array.
(Contributed by Victor Stinner in :gh:`111489`.)
+* Add :c:func:`PyUnstable_ThreadState_SetStackProtection` and
+ :c:func:`PyUnstable_ThreadState_ResetStackProtection` functions to set
+ the stack protection base address and stack protection size of a Python
+ thread state.
+ (Contributed by Victor Stinner in :gh:`139653`.)
+
Changed C APIs
--------------
diff --git a/Include/cpython/pystate.h b/Include/cpython/pystate.h
index dd2ea1202b3..c53abe43ebe 100644
--- a/Include/cpython/pystate.h
+++ b/Include/cpython/pystate.h
@@ -276,6 +276,18 @@ PyAPI_FUNC(int) PyGILState_Check(void);
*/
PyAPI_FUNC(PyObject*) _PyThread_CurrentFrames(void);
+// Set the stack protection start address and stack protection size
+// of a Python thread state
+PyAPI_FUNC(int) PyUnstable_ThreadState_SetStackProtection(
+ PyThreadState *tstate,
+ void *stack_start_addr, // Stack start address
+ size_t stack_size); // Stack size (in bytes)
+
+// Reset the stack protection start address and stack protection size
+// of a Python thread state
+PyAPI_FUNC(void) PyUnstable_ThreadState_ResetStackProtection(
+ PyThreadState *tstate);
+
/* Routines for advanced debuggers, requested by David Beazley.
Don't use unless you know what you are doing! */
PyAPI_FUNC(PyInterpreterState *) PyInterpreterState_Main(void);
diff --git a/Include/internal/pycore_pythonrun.h b/Include/internal/pycore_pythonrun.h
index f954f1b63ef..04a557e1204 100644
--- a/Include/internal/pycore_pythonrun.h
+++ b/Include/internal/pycore_pythonrun.h
@@ -60,6 +60,12 @@ extern PyObject * _Py_CompileStringObjectWithModule(
# define _PyOS_STACK_MARGIN_SHIFT (_PyOS_LOG2_STACK_MARGIN + 2)
#endif
+#ifdef _Py_THREAD_SANITIZER
+# define _PyOS_MIN_STACK_SIZE (_PyOS_STACK_MARGIN_BYTES * 6)
+#else
+# define _PyOS_MIN_STACK_SIZE (_PyOS_STACK_MARGIN_BYTES * 3)
+#endif
+
#ifdef __cplusplus
}
diff --git a/Include/internal/pycore_tstate.h b/Include/internal/pycore_tstate.h
index 29ebdfd7e01..a44c523e202 100644
--- a/Include/internal/pycore_tstate.h
+++ b/Include/internal/pycore_tstate.h
@@ -37,6 +37,10 @@ typedef struct _PyThreadStateImpl {
uintptr_t c_stack_soft_limit;
uintptr_t c_stack_hard_limit;
+ // PyUnstable_ThreadState_ResetStackProtection() values
+ uintptr_t c_stack_init_base;
+ uintptr_t c_stack_init_top;
+
PyObject *asyncio_running_loop; // Strong reference
PyObject *asyncio_running_task; // Strong reference
diff --git a/Misc/NEWS.d/next/C_API/2025-10-06-22-17-47.gh-issue-139653.6-1MOd.rst b/Misc/NEWS.d/next/C_API/2025-10-06-22-17-47.gh-issue-139653.6-1MOd.rst
new file mode 100644
index 00000000000..cd3d5262fa0
--- /dev/null
+++ b/Misc/NEWS.d/next/C_API/2025-10-06-22-17-47.gh-issue-139653.6-1MOd.rst
@@ -0,0 +1,4 @@
+Add :c:func:`PyUnstable_ThreadState_SetStackProtection` and
+:c:func:`PyUnstable_ThreadState_ResetStackProtection` functions to set the
+stack protection base address and stack protection size of a Python thread
+state. Patch by Victor Stinner.
diff --git a/Modules/_testinternalcapi.c b/Modules/_testinternalcapi.c
index dede05960d7..6514ca7f3cd 100644
--- a/Modules/_testinternalcapi.c
+++ b/Modules/_testinternalcapi.c
@@ -2446,6 +2446,58 @@ module_get_gc_hooks(PyObject *self, PyObject *arg)
return result;
}
+
+static void
+check_threadstate_set_stack_protection(PyThreadState *tstate,
+ void *start, size_t size)
+{
+ assert(PyUnstable_ThreadState_SetStackProtection(tstate, start, size) == 0);
+ assert(!PyErr_Occurred());
+
+ _PyThreadStateImpl *ts = (_PyThreadStateImpl *)tstate;
+ assert(ts->c_stack_top == (uintptr_t)start + size);
+ assert(ts->c_stack_hard_limit <= ts->c_stack_soft_limit);
+ assert(ts->c_stack_soft_limit < ts->c_stack_top);
+}
+
+
+static PyObject *
+test_threadstate_set_stack_protection(PyObject *self, PyObject *Py_UNUSED(args))
+{
+ PyThreadState *tstate = PyThreadState_GET();
+ _PyThreadStateImpl *ts = (_PyThreadStateImpl *)tstate;
+ assert(!PyErr_Occurred());
+
+ uintptr_t init_base = ts->c_stack_init_base;
+ size_t init_top = ts->c_stack_init_top;
+
+ // Test the minimum stack size
+ size_t size = _PyOS_MIN_STACK_SIZE;
+ void *start = (void*)(_Py_get_machine_stack_pointer() - size);
+ check_threadstate_set_stack_protection(tstate, start, size);
+
+ // Test a larger size
+ size = 7654321;
+ assert(size > _PyOS_MIN_STACK_SIZE);
+ start = (void*)(_Py_get_machine_stack_pointer() - size);
+ check_threadstate_set_stack_protection(tstate, start, size);
+
+ // Test invalid size (too small)
+ size = 5;
+ start = (void*)(_Py_get_machine_stack_pointer() - size);
+ assert(PyUnstable_ThreadState_SetStackProtection(tstate, start, size) == -1);
+ assert(PyErr_ExceptionMatches(PyExc_ValueError));
+ PyErr_Clear();
+
+ // Test PyUnstable_ThreadState_ResetStackProtection()
+ PyUnstable_ThreadState_ResetStackProtection(tstate);
+ assert(ts->c_stack_init_base == init_base);
+ assert(ts->c_stack_init_top == init_top);
+
+ Py_RETURN_NONE;
+}
+
+
static PyMethodDef module_functions[] = {
{"get_configs", get_configs, METH_NOARGS},
{"get_recursion_depth", get_recursion_depth, METH_NOARGS},
@@ -2556,6 +2608,8 @@ static PyMethodDef module_functions[] = {
{"simple_pending_call", simple_pending_call, METH_O},
{"set_vectorcall_nop", set_vectorcall_nop, METH_O},
{"module_get_gc_hooks", module_get_gc_hooks, METH_O},
+ {"test_threadstate_set_stack_protection",
+ test_threadstate_set_stack_protection, METH_NOARGS},
{NULL, NULL} /* sentinel */
};
diff --git a/Python/ceval.c b/Python/ceval.c
index 43e8ee71206..07d21575e3a 100644
--- a/Python/ceval.c
+++ b/Python/ceval.c
@@ -443,7 +443,7 @@ int pthread_attr_destroy(pthread_attr_t *a)
#endif
static void
-hardware_stack_limits(uintptr_t *top, uintptr_t *base)
+hardware_stack_limits(uintptr_t *base, uintptr_t *top)
{
#ifdef WIN32
ULONG_PTR low, high;
@@ -486,23 +486,86 @@ hardware_stack_limits(uintptr_t *top, uintptr_t *base)
#endif
}
-void
-_Py_InitializeRecursionLimits(PyThreadState *tstate)
+static void
+tstate_set_stack(PyThreadState *tstate,
+ uintptr_t base, uintptr_t top)
{
- uintptr_t top;
- uintptr_t base;
- hardware_stack_limits(&top, &base);
+ assert(base < top);
+ assert((top - base) >= _PyOS_MIN_STACK_SIZE);
+
#ifdef _Py_THREAD_SANITIZER
// Thread sanitizer crashes if we use more than half the stack.
uintptr_t stacksize = top - base;
- base += stacksize/2;
+ base += stacksize / 2;
#endif
_PyThreadStateImpl *_tstate = (_PyThreadStateImpl *)tstate;
_tstate->c_stack_top = top;
_tstate->c_stack_hard_limit = base + _PyOS_STACK_MARGIN_BYTES;
_tstate->c_stack_soft_limit = base + _PyOS_STACK_MARGIN_BYTES * 2;
+
+#ifndef NDEBUG
+ // Sanity checks
+ _PyThreadStateImpl *ts = (_PyThreadStateImpl *)tstate;
+ assert(ts->c_stack_hard_limit <= ts->c_stack_soft_limit);
+ assert(ts->c_stack_soft_limit < ts->c_stack_top);
+#endif
}
+
+void
+_Py_InitializeRecursionLimits(PyThreadState *tstate)
+{
+ uintptr_t base, top;
+ hardware_stack_limits(&base, &top);
+ assert(top != 0);
+
+ tstate_set_stack(tstate, base, top);
+ _PyThreadStateImpl *ts = (_PyThreadStateImpl *)tstate;
+ ts->c_stack_init_base = base;
+ ts->c_stack_init_top = top;
+
+ // Test the stack pointer
+#if !defined(NDEBUG) && !defined(__wasi__)
+ uintptr_t here_addr = _Py_get_machine_stack_pointer();
+ assert(ts->c_stack_soft_limit < here_addr);
+ assert(here_addr < ts->c_stack_top);
+#endif
+}
+
+
+int
+PyUnstable_ThreadState_SetStackProtection(PyThreadState *tstate,
+ void *stack_start_addr, size_t stack_size)
+{
+ if (stack_size < _PyOS_MIN_STACK_SIZE) {
+ PyErr_Format(PyExc_ValueError,
+ "stack_size must be at least %zu bytes",
+ _PyOS_MIN_STACK_SIZE);
+ return -1;
+ }
+
+ uintptr_t base = (uintptr_t)stack_start_addr;
+ uintptr_t top = base + stack_size;
+ tstate_set_stack(tstate, base, top);
+ return 0;
+}
+
+
+void
+PyUnstable_ThreadState_ResetStackProtection(PyThreadState *tstate)
+{
+ _PyThreadStateImpl *ts = (_PyThreadStateImpl *)tstate;
+ if (ts->c_stack_init_top != 0) {
+ tstate_set_stack(tstate,
+ ts->c_stack_init_base,
+ ts->c_stack_init_top);
+ return;
+ }
+
+ _Py_InitializeRecursionLimits(tstate);
+}
+
+
/* The function _Py_EnterRecursiveCallTstate() only calls _Py_CheckRecursiveCall()
if the recursion_depth reaches recursion_limit. */
int
diff --git a/Python/pystate.c b/Python/pystate.c
index cf251c120d7..341c680a403 100644
--- a/Python/pystate.c
+++ b/Python/pystate.c
@@ -1495,6 +1495,9 @@ init_threadstate(_PyThreadStateImpl *_tstate,
_tstate->c_stack_top = 0;
_tstate->c_stack_hard_limit = 0;
+ _tstate->c_stack_init_base = 0;
+ _tstate->c_stack_init_top = 0;
+
_tstate->asyncio_running_loop = NULL;
_tstate->asyncio_running_task = NULL;
From b2b68d40f887c8a9583a9b48babc40f25bc5e0e2 Mon Sep 17 00:00:00 2001
From: Serhiy Storchaka
Date: Thu, 13 Nov 2025 19:48:52 +0200
Subject: [PATCH 176/417] gh-140873: Add support of non-descriptor callables in
functools.singledispatchmethod() (GH-140884)
---
Doc/library/functools.rst | 5 ++-
Doc/whatsnew/3.15.rst | 8 +++++
Lib/functools.py | 5 ++-
Lib/test/test_functools.py | 35 ++++++++++++++++++-
...-11-01-14-44-09.gh-issue-140873.kfuc9B.rst | 2 ++
5 files changed, 52 insertions(+), 3 deletions(-)
create mode 100644 Misc/NEWS.d/next/Library/2025-11-01-14-44-09.gh-issue-140873.kfuc9B.rst
diff --git a/Doc/library/functools.rst b/Doc/library/functools.rst
index 1d9ac328f32..b2e2e11c0dc 100644
--- a/Doc/library/functools.rst
+++ b/Doc/library/functools.rst
@@ -672,7 +672,7 @@ The :mod:`functools` module defines the following functions:
dispatch>` :term:`generic function`.
To define a generic method, decorate it with the ``@singledispatchmethod``
- decorator. When defining a function using ``@singledispatchmethod``, note
+ decorator. When defining a method using ``@singledispatchmethod``, note
that the dispatch happens on the type of the first non-*self* or non-*cls*
argument::
@@ -716,6 +716,9 @@ The :mod:`functools` module defines the following functions:
.. versionadded:: 3.8
+ .. versionchanged:: next
+ Added support of non-:term:`descriptor` callables.
+
.. function:: update_wrapper(wrapper, wrapped, assigned=WRAPPER_ASSIGNMENTS, updated=WRAPPER_UPDATES)
diff --git a/Doc/whatsnew/3.15.rst b/Doc/whatsnew/3.15.rst
index b360ad964cf..895616e3049 100644
--- a/Doc/whatsnew/3.15.rst
+++ b/Doc/whatsnew/3.15.rst
@@ -498,6 +498,14 @@ difflib
(Contributed by Jiahao Li in :gh:`134580`.)
+functools
+---------
+
+* :func:`~functools.singledispatchmethod` now supports non-:term:`descriptor`
+ callables.
+ (Contributed by Serhiy Storchaka in :gh:`140873`.)
+
+
hashlib
-------
diff --git a/Lib/functools.py b/Lib/functools.py
index a92844ba722..8063eb5ffc3 100644
--- a/Lib/functools.py
+++ b/Lib/functools.py
@@ -1083,7 +1083,10 @@ def __call__(self, /, *args, **kwargs):
'singledispatchmethod method')
raise TypeError(f'{funcname} requires at least '
'1 positional argument')
- return self._dispatch(args[0].__class__).__get__(self._obj, self._cls)(*args, **kwargs)
+ method = self._dispatch(args[0].__class__)
+ if hasattr(method, "__get__"):
+ method = method.__get__(self._obj, self._cls)
+ return method(*args, **kwargs)
def __getattr__(self, name):
# Resolve these attributes lazily to speed up creation of
diff --git a/Lib/test/test_functools.py b/Lib/test/test_functools.py
index ce9e7f6d57d..090926fd8d8 100644
--- a/Lib/test/test_functools.py
+++ b/Lib/test/test_functools.py
@@ -2785,7 +2785,7 @@ class Slot:
@functools.singledispatchmethod
@classmethod
def go(cls, item, arg):
- pass
+ return item - arg
@go.register
@classmethod
@@ -2794,7 +2794,9 @@ def _(cls, item: int, arg):
s = Slot()
self.assertEqual(s.go(1, 1), 2)
+ self.assertEqual(s.go(1.5, 1), 0.5)
self.assertEqual(Slot.go(1, 1), 2)
+ self.assertEqual(Slot.go(1.5, 1), 0.5)
def test_staticmethod_slotted_class(self):
class A:
@@ -3485,6 +3487,37 @@ def _(item, arg: bytes) -> str:
self.assertEqual(str(Signature.from_callable(A.static_func)),
'(item, arg: int) -> str')
+ def test_method_non_descriptor(self):
+ class Callable:
+ def __init__(self, value):
+ self.value = value
+ def __call__(self, arg):
+ return self.value, arg
+
+ class A:
+ t = functools.singledispatchmethod(Callable('general'))
+ t.register(int, Callable('special'))
+
+ @functools.singledispatchmethod
+ def u(self, arg):
+ return 'general', arg
+ u.register(int, Callable('special'))
+
+ v = functools.singledispatchmethod(Callable('general'))
+ @v.register(int)
+ def _(self, arg):
+ return 'special', arg
+
+ a = A()
+ self.assertEqual(a.t(0), ('special', 0))
+ self.assertEqual(a.t(2.5), ('general', 2.5))
+ self.assertEqual(A.t(0), ('special', 0))
+ self.assertEqual(A.t(2.5), ('general', 2.5))
+ self.assertEqual(a.u(0), ('special', 0))
+ self.assertEqual(a.u(2.5), ('general', 2.5))
+ self.assertEqual(a.v(0), ('special', 0))
+ self.assertEqual(a.v(2.5), ('general', 2.5))
+
class CachedCostItem:
_cost = 1
diff --git a/Misc/NEWS.d/next/Library/2025-11-01-14-44-09.gh-issue-140873.kfuc9B.rst b/Misc/NEWS.d/next/Library/2025-11-01-14-44-09.gh-issue-140873.kfuc9B.rst
new file mode 100644
index 00000000000..e1505764064
--- /dev/null
+++ b/Misc/NEWS.d/next/Library/2025-11-01-14-44-09.gh-issue-140873.kfuc9B.rst
@@ -0,0 +1,2 @@
+Add support of non-:term:`descriptor` callables in
+:func:`functools.singledispatchmethod`.
From 196f1519cd2d8134d7643536f13f2b2844bea65d Mon Sep 17 00:00:00 2001
From: Stan Ulbrych <89152624+StanFromIreland@users.noreply.github.com>
Date: Thu, 13 Nov 2025 17:58:47 +0000
Subject: [PATCH 177/417] gh-141004: Document
`PyErr_RangedSyntaxLocationObject` (#141521)
PyErr_RangedSyntaxLocationObject
---
Doc/c-api/exceptions.rst | 8 ++++++++
1 file changed, 8 insertions(+)
diff --git a/Doc/c-api/exceptions.rst b/Doc/c-api/exceptions.rst
index 0ee595a07ac..d7fe9e2c9ec 100644
--- a/Doc/c-api/exceptions.rst
+++ b/Doc/c-api/exceptions.rst
@@ -309,6 +309,14 @@ For convenience, some of these functions will always return a
.. versionadded:: 3.4
+.. c:function:: void PyErr_RangedSyntaxLocationObject(PyObject *filename, int lineno, int col_offset, int end_lineno, int end_col_offset)
+
+ Similar to :c:func:`PyErr_SyntaxLocationObject`, but also sets the
+ *end_lineno* and *end_col_offset* information for the current exception.
+
+ .. versionadded:: 3.10
+
+
.. c:function:: void PyErr_SyntaxLocationEx(const char *filename, int lineno, int col_offset)
Like :c:func:`PyErr_SyntaxLocationObject`, but *filename* is a byte string
From 4fa80ce74c6d9f5159bdc5ec3596a194f0391e21 Mon Sep 17 00:00:00 2001
From: Ken Jin
Date: Fri, 14 Nov 2025 02:08:32 +0800
Subject: [PATCH 178/417] gh-139109: A new tracing JIT compiler frontend for
CPython (GH-140310)
This PR changes the current JIT model from trace projection to trace recording. Benchmarking: better pyperformance (about 1.7% overall) geomean versus current https://raw.githubusercontent.com/facebookexperimental/free-threading-benchmarking/refs/heads/main/results/bm-20251108-3.15.0a1%2B-7e2bc1d-JIT/bm-20251108-vultr-x86_64-Fidget%252dSpinner-tracing_jit-3.15.0a1%2B-7e2bc1d-vs-base.svg, 100% faster Richards on the most improved benchmark versus the current JIT. Slowdown of about 10-15% on the worst benchmark versus the current JIT. **Note: the fastest version isn't the one merged, as it relies on fixing bugs in the specializing interpreter, which is left to another PR**. The speedup in the merged version is about 1.1%. https://raw.githubusercontent.com/facebookexperimental/free-threading-benchmarking/refs/heads/main/results/bm-20251112-3.15.0a1%2B-f8a764a-JIT/bm-20251112-vultr-x86_64-Fidget%252dSpinner-tracing_jit-3.15.0a1%2B-f8a764a-vs-base.svg
Stats: 50% more uops executed, 30% more traces entered the last time we ran them. It also suggests our trace lengths for a real trace recording JIT are too short, as a lot of trace too long aborts https://github.com/facebookexperimental/free-threading-benchmarking/blob/main/results/bm-20251023-3.15.0a1%2B-eb73378-CLANG%2CJIT/bm-20251023-vultr-x86_64-Fidget%252dSpinner-tracing_jit-3.15.0a1%2B-eb73378-pystats-vs-base.md .
This new JIT frontend is already able to record/execute significantly more instructions than the previous JIT frontend. In this PR, we are now able to record through custom dunders, simple object creation, generators, etc. None of these were done by the old JIT frontend. Some custom dunders uops were discovered to be broken as part of this work gh-140277
The optimizer stack space check is disabled, as it's no longer valid to deal with underflow.
Pros:
* Ignoring the generated tracer code as it's automatically created, this is only additional 1k lines of code. The maintenance burden is handled by the DSL and code generator.
* `optimizer.c` is now significantly simpler, as we don't have to do strange things to recover the bytecode from a trace.
* The new JIT frontend is able to handle a lot more control-flow than the old one.
* Tracing is very low overhead. We use the tail calling interpreter/computed goto interpreter to switch between tracing mode and non-tracing mode. I call this mechanism dual dispatch, as we have two dispatch tables dispatching to each other. Specialization is still enabled while tracing.
* Better handling of polymorphism. We leverage the specializing interpreter for this.
Cons:
* (For now) requires tail calling interpreter or computed gotos. This means no Windows JIT for now :(. Not to fret, tail calling is coming soon to Windows though https://github.com/python/cpython/pull/139962
Design:
* After each instruction, the `record_previous_inst` function/label is executed. This does as the name suggests.
* The tracing interpreter lowers bytecode to uops directly so that it can obtain "fresh" values at the point of lowering.
* The tracing version behaves nearly identical to the normal interpreter, in fact it even has specialization! This allows it to run without much of a slowdown when tracing. The actual cost of tracing is only a function call and writes to memory.
* The tracing interpreter uses the specializing interpreter's deopt to naturally form the side exit chains. This allows it to side exit chain effectively, without repeating much code. We force a re-specializing when tracing a deopt.
* The tracing interpreter can even handle goto errors/exceptions, but I chose to disable them for now as it's not tested.
* Because we do not share interpreter dispatch, there is should be no significant slowdown to the original specializing interpreter on tailcall and computed got with JIT disabled. With JIT enabled, there might be a slowdown in the form of the JIT trying to trace.
* Things that could have dynamic instruction pointer effects are guarded on. The guard deopts to a new instruction --- `_DYNAMIC_EXIT`.
---
.github/workflows/jit.yml | 26 +-
Include/cpython/pystats.h | 2 +
Include/internal/pycore_backoff.h | 17 +-
Include/internal/pycore_ceval.h | 2 +
Include/internal/pycore_interp_structs.h | 4 +-
Include/internal/pycore_opcode_metadata.h | 71 +-
Include/internal/pycore_optimizer.h | 41 +-
Include/internal/pycore_tstate.h | 39 +-
Include/internal/pycore_uop.h | 12 +-
Include/internal/pycore_uop_ids.h | 389 +++---
Include/internal/pycore_uop_metadata.h | 38 +-
Lib/test/test_ast/test_ast.py | 4 +-
Lib/test/test_capi/test_opt.py | 65 +-
Lib/test/test_sys.py | 5 +-
...-10-18-21-50-44.gh-issue-139109.9QQOzN.rst | 1 +
Modules/_testinternalcapi.c | 3 +-
Objects/codeobject.c | 1 +
Objects/frameobject.c | 6 +-
Objects/funcobject.c | 6 +-
Python/bytecodes.c | 194 ++-
Python/ceval.c | 55 +-
Python/ceval_macros.h | 67 +-
Python/executor_cases.c.h | 139 ++-
Python/generated_cases.c.h | 104 +-
Python/instrumentation.c | 2 +
Python/jit.c | 2 +-
Python/opcode_targets.h | 526 +++++++-
Python/optimizer.c | 1063 +++++++++--------
Python/optimizer_analysis.c | 54 +-
Python/optimizer_bytecodes.c | 137 ++-
Python/optimizer_cases.c.h | 153 ++-
Python/optimizer_symbols.c | 44 +-
Python/pystate.c | 27 +-
Tools/c-analyzer/cpython/ignored.tsv | 1 +
Tools/cases_generator/analyzer.py | 58 +
Tools/cases_generator/generators_common.py | 17 +-
.../opcode_metadata_generator.py | 4 +-
Tools/cases_generator/target_generator.py | 26 +-
Tools/cases_generator/tier2_generator.py | 54 +-
.../cases_generator/uop_metadata_generator.py | 4 +-
Tools/jit/template.c | 11 +-
41 files changed, 2409 insertions(+), 1065 deletions(-)
create mode 100644 Misc/NEWS.d/next/Core_and_Builtins/2025-10-18-21-50-44.gh-issue-139109.9QQOzN.rst
diff --git a/.github/workflows/jit.yml b/.github/workflows/jit.yml
index 62325250bd3..3349eb04242 100644
--- a/.github/workflows/jit.yml
+++ b/.github/workflows/jit.yml
@@ -57,9 +57,10 @@ jobs:
fail-fast: false
matrix:
target:
- - i686-pc-windows-msvc/msvc
- - x86_64-pc-windows-msvc/msvc
- - aarch64-pc-windows-msvc/msvc
+# To re-enable later when we support these.
+# - i686-pc-windows-msvc/msvc
+# - x86_64-pc-windows-msvc/msvc
+# - aarch64-pc-windows-msvc/msvc
- x86_64-apple-darwin/clang
- aarch64-apple-darwin/clang
- x86_64-unknown-linux-gnu/gcc
@@ -70,15 +71,16 @@ jobs:
llvm:
- 21
include:
- - target: i686-pc-windows-msvc/msvc
- architecture: Win32
- runner: windows-2022
- - target: x86_64-pc-windows-msvc/msvc
- architecture: x64
- runner: windows-2022
- - target: aarch64-pc-windows-msvc/msvc
- architecture: ARM64
- runner: windows-11-arm
+# To re-enable later when we support these.
+# - target: i686-pc-windows-msvc/msvc
+# architecture: Win32
+# runner: windows-2022
+# - target: x86_64-pc-windows-msvc/msvc
+# architecture: x64
+# runner: windows-2022
+# - target: aarch64-pc-windows-msvc/msvc
+# architecture: ARM64
+# runner: windows-11-arm
- target: x86_64-apple-darwin/clang
architecture: x86_64
runner: macos-15-intel
diff --git a/Include/cpython/pystats.h b/Include/cpython/pystats.h
index d0a925a3055..1c94603c08b 100644
--- a/Include/cpython/pystats.h
+++ b/Include/cpython/pystats.h
@@ -150,6 +150,8 @@ typedef struct _optimization_stats {
uint64_t optimized_trace_length_hist[_Py_UOP_HIST_SIZE];
uint64_t optimizer_attempts;
uint64_t optimizer_successes;
+ uint64_t optimizer_contradiction;
+ uint64_t optimizer_frame_overflow;
uint64_t optimizer_failure_reason_no_memory;
uint64_t remove_globals_builtins_changed;
uint64_t remove_globals_incorrect_keys;
diff --git a/Include/internal/pycore_backoff.h b/Include/internal/pycore_backoff.h
index 454c8dde031..71066f1bd9f 100644
--- a/Include/internal/pycore_backoff.h
+++ b/Include/internal/pycore_backoff.h
@@ -95,11 +95,24 @@ backoff_counter_triggers(_Py_BackoffCounter counter)
return counter.value_and_backoff < UNREACHABLE_BACKOFF;
}
+static inline _Py_BackoffCounter
+trigger_backoff_counter(void)
+{
+ _Py_BackoffCounter result;
+ result.value_and_backoff = 0;
+ return result;
+}
+
// Initial JUMP_BACKWARD counter.
// Must be larger than ADAPTIVE_COOLDOWN_VALUE, otherwise when JIT code is
// invalidated we may construct a new trace before the bytecode has properly
// re-specialized:
-#define JUMP_BACKWARD_INITIAL_VALUE 4095
+// Note: this should be a prime number-1. This increases the likelihood of
+// finding a "good" loop iteration to trace.
+// For example, 4095 does not work for the nqueens benchmark on pyperformance
+// as we always end up tracing the loop iteration's
+// exhaustion iteration. Which aborts our current tracer.
+#define JUMP_BACKWARD_INITIAL_VALUE 4000
#define JUMP_BACKWARD_INITIAL_BACKOFF 12
static inline _Py_BackoffCounter
initial_jump_backoff_counter(void)
@@ -112,7 +125,7 @@ initial_jump_backoff_counter(void)
* Must be larger than ADAPTIVE_COOLDOWN_VALUE,
* otherwise when a side exit warms up we may construct
* a new trace before the Tier 1 code has properly re-specialized. */
-#define SIDE_EXIT_INITIAL_VALUE 4095
+#define SIDE_EXIT_INITIAL_VALUE 4000
#define SIDE_EXIT_INITIAL_BACKOFF 12
static inline _Py_BackoffCounter
diff --git a/Include/internal/pycore_ceval.h b/Include/internal/pycore_ceval.h
index fe72a0123eb..33b9fd053f7 100644
--- a/Include/internal/pycore_ceval.h
+++ b/Include/internal/pycore_ceval.h
@@ -392,6 +392,8 @@ _PyForIter_VirtualIteratorNext(PyThreadState* tstate, struct _PyInterpreterFrame
#define SPECIAL___AEXIT__ 3
#define SPECIAL_MAX 3
+PyAPI_DATA(const _Py_CODEUNIT *) _Py_INTERPRETER_TRAMPOLINE_INSTRUCTIONS_PTR;
+
#ifdef __cplusplus
}
#endif
diff --git a/Include/internal/pycore_interp_structs.h b/Include/internal/pycore_interp_structs.h
index e8cbe9d894e..9e4504479cd 100644
--- a/Include/internal/pycore_interp_structs.h
+++ b/Include/internal/pycore_interp_structs.h
@@ -14,8 +14,6 @@ extern "C" {
#include "pycore_structs.h" // PyHamtObject
#include "pycore_tstate.h" // _PyThreadStateImpl
#include "pycore_typedefs.h" // _PyRuntimeState
-#include "pycore_uop.h" // struct _PyUOpInstruction
-
#define CODE_MAX_WATCHERS 8
#define CONTEXT_MAX_WATCHERS 8
@@ -934,10 +932,10 @@ struct _is {
PyObject *common_consts[NUM_COMMON_CONSTANTS];
bool jit;
bool compiling;
- struct _PyUOpInstruction *jit_uop_buffer;
struct _PyExecutorObject *executor_list_head;
struct _PyExecutorObject *executor_deletion_list_head;
struct _PyExecutorObject *cold_executor;
+ struct _PyExecutorObject *cold_dynamic_executor;
int executor_deletion_list_remaining_capacity;
size_t executor_creation_counter;
_rare_events rare_events;
diff --git a/Include/internal/pycore_opcode_metadata.h b/Include/internal/pycore_opcode_metadata.h
index bd6b84ec7fd..548627dc798 100644
--- a/Include/internal/pycore_opcode_metadata.h
+++ b/Include/internal/pycore_opcode_metadata.h
@@ -1031,6 +1031,8 @@ enum InstructionFormat {
#define HAS_ERROR_NO_POP_FLAG (4096)
#define HAS_NO_SAVE_IP_FLAG (8192)
#define HAS_PERIODIC_FLAG (16384)
+#define HAS_UNPREDICTABLE_JUMP_FLAG (32768)
+#define HAS_NEEDS_GUARD_IP_FLAG (65536)
#define OPCODE_HAS_ARG(OP) (_PyOpcode_opcode_metadata[OP].flags & (HAS_ARG_FLAG))
#define OPCODE_HAS_CONST(OP) (_PyOpcode_opcode_metadata[OP].flags & (HAS_CONST_FLAG))
#define OPCODE_HAS_NAME(OP) (_PyOpcode_opcode_metadata[OP].flags & (HAS_NAME_FLAG))
@@ -1046,6 +1048,8 @@ enum InstructionFormat {
#define OPCODE_HAS_ERROR_NO_POP(OP) (_PyOpcode_opcode_metadata[OP].flags & (HAS_ERROR_NO_POP_FLAG))
#define OPCODE_HAS_NO_SAVE_IP(OP) (_PyOpcode_opcode_metadata[OP].flags & (HAS_NO_SAVE_IP_FLAG))
#define OPCODE_HAS_PERIODIC(OP) (_PyOpcode_opcode_metadata[OP].flags & (HAS_PERIODIC_FLAG))
+#define OPCODE_HAS_UNPREDICTABLE_JUMP(OP) (_PyOpcode_opcode_metadata[OP].flags & (HAS_UNPREDICTABLE_JUMP_FLAG))
+#define OPCODE_HAS_NEEDS_GUARD_IP(OP) (_PyOpcode_opcode_metadata[OP].flags & (HAS_NEEDS_GUARD_IP_FLAG))
#define OPARG_SIMPLE 0
#define OPARG_CACHE_1 1
@@ -1062,7 +1066,7 @@ enum InstructionFormat {
struct opcode_metadata {
uint8_t valid_entry;
uint8_t instr_format;
- uint16_t flags;
+ uint32_t flags;
};
extern const struct opcode_metadata _PyOpcode_opcode_metadata[267];
@@ -1077,7 +1081,7 @@ const struct opcode_metadata _PyOpcode_opcode_metadata[267] = {
[BINARY_OP_MULTIPLY_FLOAT] = { true, INSTR_FMT_IXC0000, HAS_EXIT_FLAG | HAS_ERROR_FLAG },
[BINARY_OP_MULTIPLY_INT] = { true, INSTR_FMT_IXC0000, HAS_EXIT_FLAG },
[BINARY_OP_SUBSCR_DICT] = { true, INSTR_FMT_IXC0000, HAS_EXIT_FLAG | HAS_ERROR_FLAG | HAS_ESCAPES_FLAG },
- [BINARY_OP_SUBSCR_GETITEM] = { true, INSTR_FMT_IXC0000, HAS_DEOPT_FLAG },
+ [BINARY_OP_SUBSCR_GETITEM] = { true, INSTR_FMT_IXC0000, HAS_DEOPT_FLAG | HAS_NEEDS_GUARD_IP_FLAG },
[BINARY_OP_SUBSCR_LIST_INT] = { true, INSTR_FMT_IXC0000, HAS_DEOPT_FLAG | HAS_EXIT_FLAG | HAS_ESCAPES_FLAG },
[BINARY_OP_SUBSCR_LIST_SLICE] = { true, INSTR_FMT_IXC0000, HAS_EXIT_FLAG | HAS_ERROR_FLAG | HAS_ESCAPES_FLAG },
[BINARY_OP_SUBSCR_STR_INT] = { true, INSTR_FMT_IXC0000, HAS_DEOPT_FLAG | HAS_EXIT_FLAG | HAS_ESCAPES_FLAG },
@@ -1094,22 +1098,22 @@ const struct opcode_metadata _PyOpcode_opcode_metadata[267] = {
[BUILD_TEMPLATE] = { true, INSTR_FMT_IX, HAS_ERROR_FLAG | HAS_ESCAPES_FLAG },
[BUILD_TUPLE] = { true, INSTR_FMT_IB, HAS_ARG_FLAG | HAS_ERROR_FLAG | HAS_ERROR_NO_POP_FLAG },
[CACHE] = { true, INSTR_FMT_IX, 0 },
- [CALL] = { true, INSTR_FMT_IBC00, HAS_ARG_FLAG | HAS_EVAL_BREAK_FLAG | HAS_ERROR_FLAG | HAS_ERROR_NO_POP_FLAG | HAS_ESCAPES_FLAG },
- [CALL_ALLOC_AND_ENTER_INIT] = { true, INSTR_FMT_IBC00, HAS_ARG_FLAG | HAS_DEOPT_FLAG | HAS_ERROR_FLAG | HAS_ERROR_NO_POP_FLAG | HAS_ESCAPES_FLAG },
- [CALL_BOUND_METHOD_EXACT_ARGS] = { true, INSTR_FMT_IBC00, HAS_ARG_FLAG | HAS_DEOPT_FLAG | HAS_EXIT_FLAG | HAS_ESCAPES_FLAG },
- [CALL_BOUND_METHOD_GENERAL] = { true, INSTR_FMT_IBC00, HAS_ARG_FLAG | HAS_DEOPT_FLAG | HAS_EXIT_FLAG | HAS_ERROR_FLAG | HAS_ERROR_NO_POP_FLAG | HAS_ESCAPES_FLAG },
+ [CALL] = { true, INSTR_FMT_IBC00, HAS_ARG_FLAG | HAS_EVAL_BREAK_FLAG | HAS_ERROR_FLAG | HAS_ERROR_NO_POP_FLAG | HAS_ESCAPES_FLAG | HAS_NEEDS_GUARD_IP_FLAG },
+ [CALL_ALLOC_AND_ENTER_INIT] = { true, INSTR_FMT_IBC00, HAS_ARG_FLAG | HAS_DEOPT_FLAG | HAS_ERROR_FLAG | HAS_ERROR_NO_POP_FLAG | HAS_ESCAPES_FLAG | HAS_NEEDS_GUARD_IP_FLAG },
+ [CALL_BOUND_METHOD_EXACT_ARGS] = { true, INSTR_FMT_IBC00, HAS_ARG_FLAG | HAS_DEOPT_FLAG | HAS_EXIT_FLAG | HAS_ESCAPES_FLAG | HAS_NEEDS_GUARD_IP_FLAG },
+ [CALL_BOUND_METHOD_GENERAL] = { true, INSTR_FMT_IBC00, HAS_ARG_FLAG | HAS_DEOPT_FLAG | HAS_EXIT_FLAG | HAS_ERROR_FLAG | HAS_ERROR_NO_POP_FLAG | HAS_ESCAPES_FLAG | HAS_NEEDS_GUARD_IP_FLAG },
[CALL_BUILTIN_CLASS] = { true, INSTR_FMT_IBC00, HAS_ARG_FLAG | HAS_EVAL_BREAK_FLAG | HAS_DEOPT_FLAG | HAS_ERROR_FLAG | HAS_ESCAPES_FLAG },
[CALL_BUILTIN_FAST] = { true, INSTR_FMT_IBC00, HAS_ARG_FLAG | HAS_EVAL_BREAK_FLAG | HAS_DEOPT_FLAG | HAS_ERROR_FLAG | HAS_ESCAPES_FLAG },
[CALL_BUILTIN_FAST_WITH_KEYWORDS] = { true, INSTR_FMT_IBC00, HAS_ARG_FLAG | HAS_EVAL_BREAK_FLAG | HAS_DEOPT_FLAG | HAS_ERROR_FLAG | HAS_ESCAPES_FLAG },
[CALL_BUILTIN_O] = { true, INSTR_FMT_IBC00, HAS_ARG_FLAG | HAS_EVAL_BREAK_FLAG | HAS_EXIT_FLAG | HAS_ERROR_FLAG | HAS_ESCAPES_FLAG },
- [CALL_FUNCTION_EX] = { true, INSTR_FMT_IX, HAS_EVAL_BREAK_FLAG | HAS_ERROR_FLAG | HAS_ERROR_NO_POP_FLAG | HAS_ESCAPES_FLAG },
+ [CALL_FUNCTION_EX] = { true, INSTR_FMT_IX, HAS_EVAL_BREAK_FLAG | HAS_ERROR_FLAG | HAS_ERROR_NO_POP_FLAG | HAS_ESCAPES_FLAG | HAS_NEEDS_GUARD_IP_FLAG },
[CALL_INTRINSIC_1] = { true, INSTR_FMT_IB, HAS_ARG_FLAG | HAS_ERROR_FLAG | HAS_ESCAPES_FLAG },
[CALL_INTRINSIC_2] = { true, INSTR_FMT_IB, HAS_ARG_FLAG | HAS_ERROR_FLAG | HAS_ESCAPES_FLAG },
[CALL_ISINSTANCE] = { true, INSTR_FMT_IXC00, HAS_DEOPT_FLAG | HAS_ERROR_FLAG | HAS_ERROR_NO_POP_FLAG | HAS_ESCAPES_FLAG },
- [CALL_KW] = { true, INSTR_FMT_IBC00, HAS_ARG_FLAG | HAS_ERROR_FLAG | HAS_ERROR_NO_POP_FLAG | HAS_ESCAPES_FLAG },
- [CALL_KW_BOUND_METHOD] = { true, INSTR_FMT_IBC00, HAS_ARG_FLAG | HAS_DEOPT_FLAG | HAS_EXIT_FLAG | HAS_ERROR_FLAG | HAS_ESCAPES_FLAG },
+ [CALL_KW] = { true, INSTR_FMT_IBC00, HAS_ARG_FLAG | HAS_ERROR_FLAG | HAS_ERROR_NO_POP_FLAG | HAS_ESCAPES_FLAG | HAS_NEEDS_GUARD_IP_FLAG },
+ [CALL_KW_BOUND_METHOD] = { true, INSTR_FMT_IBC00, HAS_ARG_FLAG | HAS_DEOPT_FLAG | HAS_EXIT_FLAG | HAS_ERROR_FLAG | HAS_ESCAPES_FLAG | HAS_NEEDS_GUARD_IP_FLAG },
[CALL_KW_NON_PY] = { true, INSTR_FMT_IBC00, HAS_ARG_FLAG | HAS_EVAL_BREAK_FLAG | HAS_EXIT_FLAG | HAS_ERROR_FLAG | HAS_ESCAPES_FLAG },
- [CALL_KW_PY] = { true, INSTR_FMT_IBC00, HAS_ARG_FLAG | HAS_DEOPT_FLAG | HAS_EXIT_FLAG | HAS_ERROR_FLAG | HAS_ESCAPES_FLAG },
+ [CALL_KW_PY] = { true, INSTR_FMT_IBC00, HAS_ARG_FLAG | HAS_DEOPT_FLAG | HAS_EXIT_FLAG | HAS_ERROR_FLAG | HAS_ESCAPES_FLAG | HAS_NEEDS_GUARD_IP_FLAG },
[CALL_LEN] = { true, INSTR_FMT_IXC00, HAS_DEOPT_FLAG | HAS_ERROR_FLAG | HAS_ERROR_NO_POP_FLAG | HAS_ESCAPES_FLAG },
[CALL_LIST_APPEND] = { true, INSTR_FMT_IBC00, HAS_ARG_FLAG | HAS_DEOPT_FLAG | HAS_EXIT_FLAG | HAS_ERROR_FLAG | HAS_ESCAPES_FLAG },
[CALL_METHOD_DESCRIPTOR_FAST] = { true, INSTR_FMT_IBC00, HAS_ARG_FLAG | HAS_EVAL_BREAK_FLAG | HAS_EXIT_FLAG | HAS_ERROR_FLAG | HAS_ESCAPES_FLAG },
@@ -1117,8 +1121,8 @@ const struct opcode_metadata _PyOpcode_opcode_metadata[267] = {
[CALL_METHOD_DESCRIPTOR_NOARGS] = { true, INSTR_FMT_IBC00, HAS_ARG_FLAG | HAS_EVAL_BREAK_FLAG | HAS_EXIT_FLAG | HAS_ERROR_FLAG | HAS_ESCAPES_FLAG },
[CALL_METHOD_DESCRIPTOR_O] = { true, INSTR_FMT_IBC00, HAS_ARG_FLAG | HAS_EVAL_BREAK_FLAG | HAS_EXIT_FLAG | HAS_ERROR_FLAG | HAS_ESCAPES_FLAG },
[CALL_NON_PY_GENERAL] = { true, INSTR_FMT_IBC00, HAS_ARG_FLAG | HAS_EVAL_BREAK_FLAG | HAS_EXIT_FLAG | HAS_ERROR_FLAG | HAS_ESCAPES_FLAG },
- [CALL_PY_EXACT_ARGS] = { true, INSTR_FMT_IBC00, HAS_ARG_FLAG | HAS_DEOPT_FLAG | HAS_EXIT_FLAG },
- [CALL_PY_GENERAL] = { true, INSTR_FMT_IBC00, HAS_ARG_FLAG | HAS_DEOPT_FLAG | HAS_EXIT_FLAG | HAS_ERROR_FLAG | HAS_ERROR_NO_POP_FLAG | HAS_ESCAPES_FLAG },
+ [CALL_PY_EXACT_ARGS] = { true, INSTR_FMT_IBC00, HAS_ARG_FLAG | HAS_DEOPT_FLAG | HAS_EXIT_FLAG | HAS_NEEDS_GUARD_IP_FLAG },
+ [CALL_PY_GENERAL] = { true, INSTR_FMT_IBC00, HAS_ARG_FLAG | HAS_DEOPT_FLAG | HAS_EXIT_FLAG | HAS_ERROR_FLAG | HAS_ERROR_NO_POP_FLAG | HAS_ESCAPES_FLAG | HAS_NEEDS_GUARD_IP_FLAG },
[CALL_STR_1] = { true, INSTR_FMT_IBC00, HAS_ARG_FLAG | HAS_EVAL_BREAK_FLAG | HAS_DEOPT_FLAG | HAS_ERROR_FLAG | HAS_ESCAPES_FLAG },
[CALL_TUPLE_1] = { true, INSTR_FMT_IBC00, HAS_ARG_FLAG | HAS_EVAL_BREAK_FLAG | HAS_DEOPT_FLAG | HAS_ERROR_FLAG | HAS_ESCAPES_FLAG },
[CALL_TYPE_1] = { true, INSTR_FMT_IBC00, HAS_ARG_FLAG | HAS_DEOPT_FLAG | HAS_ESCAPES_FLAG },
@@ -1143,7 +1147,7 @@ const struct opcode_metadata _PyOpcode_opcode_metadata[267] = {
[DELETE_SUBSCR] = { true, INSTR_FMT_IX, HAS_ERROR_FLAG | HAS_ESCAPES_FLAG },
[DICT_MERGE] = { true, INSTR_FMT_IB, HAS_ARG_FLAG | HAS_ERROR_FLAG | HAS_ESCAPES_FLAG },
[DICT_UPDATE] = { true, INSTR_FMT_IB, HAS_ARG_FLAG | HAS_ERROR_FLAG | HAS_ESCAPES_FLAG },
- [END_ASYNC_FOR] = { true, INSTR_FMT_IB, HAS_ARG_FLAG | HAS_JUMP_FLAG | HAS_ERROR_FLAG | HAS_ERROR_NO_POP_FLAG | HAS_ESCAPES_FLAG },
+ [END_ASYNC_FOR] = { true, INSTR_FMT_IB, HAS_ARG_FLAG | HAS_JUMP_FLAG | HAS_ERROR_FLAG | HAS_ERROR_NO_POP_FLAG | HAS_ESCAPES_FLAG | HAS_UNPREDICTABLE_JUMP_FLAG | HAS_NEEDS_GUARD_IP_FLAG },
[END_FOR] = { true, INSTR_FMT_IX, HAS_ESCAPES_FLAG | HAS_NO_SAVE_IP_FLAG },
[END_SEND] = { true, INSTR_FMT_IX, HAS_ESCAPES_FLAG | HAS_PURE_FLAG },
[ENTER_EXECUTOR] = { true, INSTR_FMT_IB, HAS_ARG_FLAG },
@@ -1151,11 +1155,11 @@ const struct opcode_metadata _PyOpcode_opcode_metadata[267] = {
[EXTENDED_ARG] = { true, INSTR_FMT_IB, HAS_ARG_FLAG },
[FORMAT_SIMPLE] = { true, INSTR_FMT_IX, HAS_ERROR_FLAG | HAS_ESCAPES_FLAG },
[FORMAT_WITH_SPEC] = { true, INSTR_FMT_IX, HAS_ERROR_FLAG | HAS_ESCAPES_FLAG },
- [FOR_ITER] = { true, INSTR_FMT_IBC, HAS_ARG_FLAG | HAS_JUMP_FLAG | HAS_ERROR_FLAG | HAS_ERROR_NO_POP_FLAG | HAS_ESCAPES_FLAG },
- [FOR_ITER_GEN] = { true, INSTR_FMT_IBC, HAS_ARG_FLAG | HAS_DEOPT_FLAG },
- [FOR_ITER_LIST] = { true, INSTR_FMT_IBC, HAS_ARG_FLAG | HAS_JUMP_FLAG | HAS_DEOPT_FLAG | HAS_EXIT_FLAG | HAS_ESCAPES_FLAG },
- [FOR_ITER_RANGE] = { true, INSTR_FMT_IBC, HAS_ARG_FLAG | HAS_JUMP_FLAG | HAS_EXIT_FLAG | HAS_ERROR_FLAG },
- [FOR_ITER_TUPLE] = { true, INSTR_FMT_IBC, HAS_ARG_FLAG | HAS_JUMP_FLAG | HAS_EXIT_FLAG },
+ [FOR_ITER] = { true, INSTR_FMT_IBC, HAS_ARG_FLAG | HAS_JUMP_FLAG | HAS_ERROR_FLAG | HAS_ERROR_NO_POP_FLAG | HAS_ESCAPES_FLAG | HAS_UNPREDICTABLE_JUMP_FLAG },
+ [FOR_ITER_GEN] = { true, INSTR_FMT_IBC, HAS_ARG_FLAG | HAS_DEOPT_FLAG | HAS_NEEDS_GUARD_IP_FLAG },
+ [FOR_ITER_LIST] = { true, INSTR_FMT_IBC, HAS_ARG_FLAG | HAS_JUMP_FLAG | HAS_DEOPT_FLAG | HAS_EXIT_FLAG | HAS_ESCAPES_FLAG | HAS_UNPREDICTABLE_JUMP_FLAG },
+ [FOR_ITER_RANGE] = { true, INSTR_FMT_IBC, HAS_ARG_FLAG | HAS_JUMP_FLAG | HAS_EXIT_FLAG | HAS_ERROR_FLAG | HAS_UNPREDICTABLE_JUMP_FLAG },
+ [FOR_ITER_TUPLE] = { true, INSTR_FMT_IBC, HAS_ARG_FLAG | HAS_JUMP_FLAG | HAS_EXIT_FLAG | HAS_UNPREDICTABLE_JUMP_FLAG },
[GET_AITER] = { true, INSTR_FMT_IX, HAS_ERROR_FLAG | HAS_ESCAPES_FLAG },
[GET_ANEXT] = { true, INSTR_FMT_IX, HAS_ERROR_FLAG | HAS_ERROR_NO_POP_FLAG | HAS_ESCAPES_FLAG },
[GET_AWAITABLE] = { true, INSTR_FMT_IB, HAS_ARG_FLAG | HAS_ERROR_FLAG | HAS_ESCAPES_FLAG },
@@ -1164,13 +1168,13 @@ const struct opcode_metadata _PyOpcode_opcode_metadata[267] = {
[GET_YIELD_FROM_ITER] = { true, INSTR_FMT_IX, HAS_ERROR_FLAG | HAS_ERROR_NO_POP_FLAG | HAS_ESCAPES_FLAG },
[IMPORT_FROM] = { true, INSTR_FMT_IB, HAS_ARG_FLAG | HAS_NAME_FLAG | HAS_ERROR_FLAG | HAS_ESCAPES_FLAG },
[IMPORT_NAME] = { true, INSTR_FMT_IB, HAS_ARG_FLAG | HAS_NAME_FLAG | HAS_ERROR_FLAG | HAS_ESCAPES_FLAG },
- [INSTRUMENTED_CALL] = { true, INSTR_FMT_IBC00, HAS_ARG_FLAG | HAS_EVAL_BREAK_FLAG | HAS_ERROR_FLAG | HAS_ERROR_NO_POP_FLAG | HAS_ESCAPES_FLAG },
- [INSTRUMENTED_CALL_FUNCTION_EX] = { true, INSTR_FMT_IX, HAS_EVAL_BREAK_FLAG | HAS_ERROR_FLAG | HAS_ERROR_NO_POP_FLAG | HAS_ESCAPES_FLAG },
- [INSTRUMENTED_CALL_KW] = { true, INSTR_FMT_IBC00, HAS_ARG_FLAG | HAS_ERROR_FLAG | HAS_ERROR_NO_POP_FLAG | HAS_ESCAPES_FLAG },
- [INSTRUMENTED_END_ASYNC_FOR] = { true, INSTR_FMT_IB, HAS_ARG_FLAG | HAS_JUMP_FLAG | HAS_ERROR_FLAG | HAS_ERROR_NO_POP_FLAG | HAS_ESCAPES_FLAG },
+ [INSTRUMENTED_CALL] = { true, INSTR_FMT_IBC00, HAS_ARG_FLAG | HAS_EVAL_BREAK_FLAG | HAS_ERROR_FLAG | HAS_ERROR_NO_POP_FLAG | HAS_ESCAPES_FLAG | HAS_NEEDS_GUARD_IP_FLAG },
+ [INSTRUMENTED_CALL_FUNCTION_EX] = { true, INSTR_FMT_IX, HAS_EVAL_BREAK_FLAG | HAS_ERROR_FLAG | HAS_ERROR_NO_POP_FLAG | HAS_ESCAPES_FLAG | HAS_NEEDS_GUARD_IP_FLAG },
+ [INSTRUMENTED_CALL_KW] = { true, INSTR_FMT_IBC00, HAS_ARG_FLAG | HAS_ERROR_FLAG | HAS_ERROR_NO_POP_FLAG | HAS_ESCAPES_FLAG | HAS_NEEDS_GUARD_IP_FLAG },
+ [INSTRUMENTED_END_ASYNC_FOR] = { true, INSTR_FMT_IB, HAS_ARG_FLAG | HAS_JUMP_FLAG | HAS_ERROR_FLAG | HAS_ERROR_NO_POP_FLAG | HAS_ESCAPES_FLAG | HAS_UNPREDICTABLE_JUMP_FLAG | HAS_NEEDS_GUARD_IP_FLAG },
[INSTRUMENTED_END_FOR] = { true, INSTR_FMT_IX, HAS_ERROR_FLAG | HAS_ERROR_NO_POP_FLAG | HAS_ESCAPES_FLAG | HAS_NO_SAVE_IP_FLAG },
[INSTRUMENTED_END_SEND] = { true, INSTR_FMT_IX, HAS_ERROR_FLAG | HAS_ERROR_NO_POP_FLAG | HAS_ESCAPES_FLAG },
- [INSTRUMENTED_FOR_ITER] = { true, INSTR_FMT_IBC, HAS_ARG_FLAG | HAS_JUMP_FLAG | HAS_ERROR_FLAG | HAS_ERROR_NO_POP_FLAG | HAS_ESCAPES_FLAG },
+ [INSTRUMENTED_FOR_ITER] = { true, INSTR_FMT_IBC, HAS_ARG_FLAG | HAS_JUMP_FLAG | HAS_ERROR_FLAG | HAS_ERROR_NO_POP_FLAG | HAS_ESCAPES_FLAG | HAS_UNPREDICTABLE_JUMP_FLAG | HAS_NEEDS_GUARD_IP_FLAG },
[INSTRUMENTED_INSTRUCTION] = { true, INSTR_FMT_IX, HAS_ERROR_FLAG | HAS_ESCAPES_FLAG },
[INSTRUMENTED_JUMP_BACKWARD] = { true, INSTR_FMT_IBC, HAS_ARG_FLAG | HAS_EVAL_BREAK_FLAG | HAS_ERROR_FLAG | HAS_ESCAPES_FLAG },
[INSTRUMENTED_JUMP_FORWARD] = { true, INSTR_FMT_IB, HAS_ARG_FLAG },
@@ -1183,8 +1187,8 @@ const struct opcode_metadata _PyOpcode_opcode_metadata[267] = {
[INSTRUMENTED_POP_JUMP_IF_NOT_NONE] = { true, INSTR_FMT_IBC, HAS_ARG_FLAG | HAS_ESCAPES_FLAG },
[INSTRUMENTED_POP_JUMP_IF_TRUE] = { true, INSTR_FMT_IBC, HAS_ARG_FLAG },
[INSTRUMENTED_RESUME] = { true, INSTR_FMT_IB, HAS_ARG_FLAG | HAS_EVAL_BREAK_FLAG | HAS_ERROR_FLAG | HAS_ERROR_NO_POP_FLAG | HAS_ESCAPES_FLAG },
- [INSTRUMENTED_RETURN_VALUE] = { true, INSTR_FMT_IX, HAS_ERROR_FLAG | HAS_ESCAPES_FLAG },
- [INSTRUMENTED_YIELD_VALUE] = { true, INSTR_FMT_IB, HAS_ARG_FLAG | HAS_ERROR_FLAG | HAS_ERROR_NO_POP_FLAG | HAS_ESCAPES_FLAG },
+ [INSTRUMENTED_RETURN_VALUE] = { true, INSTR_FMT_IX, HAS_ERROR_FLAG | HAS_ESCAPES_FLAG | HAS_NEEDS_GUARD_IP_FLAG },
+ [INSTRUMENTED_YIELD_VALUE] = { true, INSTR_FMT_IB, HAS_ARG_FLAG | HAS_ERROR_FLAG | HAS_ERROR_NO_POP_FLAG | HAS_ESCAPES_FLAG | HAS_NEEDS_GUARD_IP_FLAG },
[INTERPRETER_EXIT] = { true, INSTR_FMT_IX, HAS_ESCAPES_FLAG },
[IS_OP] = { true, INSTR_FMT_IB, HAS_ARG_FLAG | HAS_ESCAPES_FLAG },
[JUMP_BACKWARD] = { true, INSTR_FMT_IBC, HAS_ARG_FLAG | HAS_JUMP_FLAG | HAS_EVAL_BREAK_FLAG | HAS_ERROR_FLAG | HAS_ESCAPES_FLAG },
@@ -1197,7 +1201,7 @@ const struct opcode_metadata _PyOpcode_opcode_metadata[267] = {
[LOAD_ATTR] = { true, INSTR_FMT_IBC00000000, HAS_ARG_FLAG | HAS_NAME_FLAG | HAS_ERROR_FLAG | HAS_ESCAPES_FLAG },
[LOAD_ATTR_CLASS] = { true, INSTR_FMT_IBC00000000, HAS_ARG_FLAG | HAS_EXIT_FLAG | HAS_ESCAPES_FLAG },
[LOAD_ATTR_CLASS_WITH_METACLASS_CHECK] = { true, INSTR_FMT_IBC00000000, HAS_ARG_FLAG | HAS_EXIT_FLAG | HAS_ESCAPES_FLAG },
- [LOAD_ATTR_GETATTRIBUTE_OVERRIDDEN] = { true, INSTR_FMT_IBC00000000, HAS_ARG_FLAG | HAS_NAME_FLAG | HAS_DEOPT_FLAG },
+ [LOAD_ATTR_GETATTRIBUTE_OVERRIDDEN] = { true, INSTR_FMT_IBC00000000, HAS_ARG_FLAG | HAS_NAME_FLAG | HAS_DEOPT_FLAG | HAS_NEEDS_GUARD_IP_FLAG },
[LOAD_ATTR_INSTANCE_VALUE] = { true, INSTR_FMT_IBC00000000, HAS_ARG_FLAG | HAS_DEOPT_FLAG | HAS_EXIT_FLAG | HAS_ESCAPES_FLAG },
[LOAD_ATTR_METHOD_LAZY_DICT] = { true, INSTR_FMT_IBC00000000, HAS_ARG_FLAG | HAS_DEOPT_FLAG | HAS_EXIT_FLAG },
[LOAD_ATTR_METHOD_NO_DICT] = { true, INSTR_FMT_IBC00000000, HAS_ARG_FLAG | HAS_EXIT_FLAG },
@@ -1205,7 +1209,7 @@ const struct opcode_metadata _PyOpcode_opcode_metadata[267] = {
[LOAD_ATTR_MODULE] = { true, INSTR_FMT_IBC00000000, HAS_ARG_FLAG | HAS_DEOPT_FLAG | HAS_ESCAPES_FLAG },
[LOAD_ATTR_NONDESCRIPTOR_NO_DICT] = { true, INSTR_FMT_IBC00000000, HAS_ARG_FLAG | HAS_EXIT_FLAG | HAS_ESCAPES_FLAG },
[LOAD_ATTR_NONDESCRIPTOR_WITH_VALUES] = { true, INSTR_FMT_IBC00000000, HAS_ARG_FLAG | HAS_DEOPT_FLAG | HAS_EXIT_FLAG | HAS_ESCAPES_FLAG },
- [LOAD_ATTR_PROPERTY] = { true, INSTR_FMT_IBC00000000, HAS_ARG_FLAG | HAS_DEOPT_FLAG | HAS_EXIT_FLAG },
+ [LOAD_ATTR_PROPERTY] = { true, INSTR_FMT_IBC00000000, HAS_ARG_FLAG | HAS_DEOPT_FLAG | HAS_EXIT_FLAG | HAS_NEEDS_GUARD_IP_FLAG },
[LOAD_ATTR_SLOT] = { true, INSTR_FMT_IBC00000000, HAS_ARG_FLAG | HAS_DEOPT_FLAG | HAS_EXIT_FLAG | HAS_ESCAPES_FLAG },
[LOAD_ATTR_WITH_HINT] = { true, INSTR_FMT_IBC00000000, HAS_ARG_FLAG | HAS_NAME_FLAG | HAS_DEOPT_FLAG | HAS_EXIT_FLAG | HAS_ESCAPES_FLAG },
[LOAD_BUILD_CLASS] = { true, INSTR_FMT_IX, HAS_ERROR_FLAG | HAS_ESCAPES_FLAG },
@@ -1253,10 +1257,10 @@ const struct opcode_metadata _PyOpcode_opcode_metadata[267] = {
[RESERVED] = { true, INSTR_FMT_IX, 0 },
[RESUME] = { true, INSTR_FMT_IB, HAS_ARG_FLAG | HAS_EVAL_BREAK_FLAG | HAS_ERROR_FLAG | HAS_ERROR_NO_POP_FLAG | HAS_ESCAPES_FLAG },
[RESUME_CHECK] = { true, INSTR_FMT_IX, HAS_DEOPT_FLAG },
- [RETURN_GENERATOR] = { true, INSTR_FMT_IX, HAS_ERROR_FLAG | HAS_ESCAPES_FLAG },
- [RETURN_VALUE] = { true, INSTR_FMT_IX, HAS_ESCAPES_FLAG },
- [SEND] = { true, INSTR_FMT_IBC, HAS_ARG_FLAG | HAS_JUMP_FLAG | HAS_ERROR_FLAG | HAS_ESCAPES_FLAG },
- [SEND_GEN] = { true, INSTR_FMT_IBC, HAS_ARG_FLAG | HAS_DEOPT_FLAG },
+ [RETURN_GENERATOR] = { true, INSTR_FMT_IX, HAS_ERROR_FLAG | HAS_ESCAPES_FLAG | HAS_NEEDS_GUARD_IP_FLAG },
+ [RETURN_VALUE] = { true, INSTR_FMT_IX, HAS_ESCAPES_FLAG | HAS_NEEDS_GUARD_IP_FLAG },
+ [SEND] = { true, INSTR_FMT_IBC, HAS_ARG_FLAG | HAS_JUMP_FLAG | HAS_ERROR_FLAG | HAS_ESCAPES_FLAG | HAS_UNPREDICTABLE_JUMP_FLAG | HAS_NEEDS_GUARD_IP_FLAG },
+ [SEND_GEN] = { true, INSTR_FMT_IBC, HAS_ARG_FLAG | HAS_DEOPT_FLAG | HAS_NEEDS_GUARD_IP_FLAG },
[SETUP_ANNOTATIONS] = { true, INSTR_FMT_IX, HAS_ERROR_FLAG | HAS_ESCAPES_FLAG },
[SET_ADD] = { true, INSTR_FMT_IB, HAS_ARG_FLAG | HAS_ERROR_FLAG | HAS_ESCAPES_FLAG },
[SET_FUNCTION_ATTRIBUTE] = { true, INSTR_FMT_IB, HAS_ARG_FLAG },
@@ -1292,7 +1296,7 @@ const struct opcode_metadata _PyOpcode_opcode_metadata[267] = {
[UNPACK_SEQUENCE_TUPLE] = { true, INSTR_FMT_IBC, HAS_ARG_FLAG | HAS_DEOPT_FLAG | HAS_EXIT_FLAG | HAS_ESCAPES_FLAG },
[UNPACK_SEQUENCE_TWO_TUPLE] = { true, INSTR_FMT_IBC, HAS_ARG_FLAG | HAS_DEOPT_FLAG | HAS_EXIT_FLAG | HAS_ESCAPES_FLAG },
[WITH_EXCEPT_START] = { true, INSTR_FMT_IX, HAS_ERROR_FLAG | HAS_ESCAPES_FLAG },
- [YIELD_VALUE] = { true, INSTR_FMT_IB, HAS_ARG_FLAG },
+ [YIELD_VALUE] = { true, INSTR_FMT_IB, HAS_ARG_FLAG | HAS_NEEDS_GUARD_IP_FLAG },
[ANNOTATIONS_PLACEHOLDER] = { true, -1, HAS_PURE_FLAG },
[JUMP] = { true, -1, HAS_ARG_FLAG | HAS_JUMP_FLAG | HAS_EVAL_BREAK_FLAG | HAS_ERROR_FLAG | HAS_ESCAPES_FLAG },
[JUMP_IF_FALSE] = { true, -1, HAS_ARG_FLAG | HAS_JUMP_FLAG | HAS_ERROR_FLAG | HAS_ESCAPES_FLAG },
@@ -1406,6 +1410,9 @@ _PyOpcode_macro_expansion[256] = {
[IMPORT_FROM] = { .nuops = 1, .uops = { { _IMPORT_FROM, OPARG_SIMPLE, 0 } } },
[IMPORT_NAME] = { .nuops = 1, .uops = { { _IMPORT_NAME, OPARG_SIMPLE, 0 } } },
[IS_OP] = { .nuops = 1, .uops = { { _IS_OP, OPARG_SIMPLE, 0 } } },
+ [JUMP_BACKWARD] = { .nuops = 2, .uops = { { _CHECK_PERIODIC, OPARG_SIMPLE, 1 }, { _JUMP_BACKWARD_NO_INTERRUPT, OPARG_REPLACED, 1 } } },
+ [JUMP_BACKWARD_NO_INTERRUPT] = { .nuops = 1, .uops = { { _JUMP_BACKWARD_NO_INTERRUPT, OPARG_REPLACED, 0 } } },
+ [JUMP_BACKWARD_NO_JIT] = { .nuops = 2, .uops = { { _CHECK_PERIODIC, OPARG_SIMPLE, 1 }, { _JUMP_BACKWARD_NO_INTERRUPT, OPARG_REPLACED, 1 } } },
[LIST_APPEND] = { .nuops = 1, .uops = { { _LIST_APPEND, OPARG_SIMPLE, 0 } } },
[LIST_EXTEND] = { .nuops = 1, .uops = { { _LIST_EXTEND, OPARG_SIMPLE, 0 } } },
[LOAD_ATTR] = { .nuops = 1, .uops = { { _LOAD_ATTR, OPARG_SIMPLE, 8 } } },
diff --git a/Include/internal/pycore_optimizer.h b/Include/internal/pycore_optimizer.h
index 8ed5436eb68..653285a2c6b 100644
--- a/Include/internal/pycore_optimizer.h
+++ b/Include/internal/pycore_optimizer.h
@@ -21,14 +21,6 @@ typedef struct _PyExecutorLinkListNode {
} _PyExecutorLinkListNode;
-/* Bloom filter with m = 256
- * https://en.wikipedia.org/wiki/Bloom_filter */
-#define _Py_BLOOM_FILTER_WORDS 8
-
-typedef struct {
- uint32_t bits[_Py_BLOOM_FILTER_WORDS];
-} _PyBloomFilter;
-
typedef struct {
uint8_t opcode;
uint8_t oparg;
@@ -44,7 +36,9 @@ typedef struct {
typedef struct _PyExitData {
uint32_t target;
- uint16_t index;
+ uint16_t index:14;
+ uint16_t is_dynamic:1;
+ uint16_t is_control_flow:1;
_Py_BackoffCounter temperature;
struct _PyExecutorObject *executor;
} _PyExitData;
@@ -94,9 +88,8 @@ PyAPI_FUNC(void) _Py_Executors_InvalidateCold(PyInterpreterState *interp);
// This value is arbitrary and was not optimized.
#define JIT_CLEANUP_THRESHOLD 1000
-#define TRACE_STACK_SIZE 5
-
-int _Py_uop_analyze_and_optimize(_PyInterpreterFrame *frame,
+int _Py_uop_analyze_and_optimize(
+ PyFunctionObject *func,
_PyUOpInstruction *trace, int trace_len, int curr_stackentries,
_PyBloomFilter *dependencies);
@@ -130,7 +123,7 @@ static inline uint16_t uop_get_error_target(const _PyUOpInstruction *inst)
#define TY_ARENA_SIZE (UOP_MAX_TRACE_LENGTH * 5)
// Need extras for root frame and for overflow frame (see TRACE_STACK_PUSH())
-#define MAX_ABSTRACT_FRAME_DEPTH (TRACE_STACK_SIZE + 2)
+#define MAX_ABSTRACT_FRAME_DEPTH (16)
// The maximum number of side exits that we can take before requiring forward
// progress (and inserting a new ENTER_EXECUTOR instruction). In practice, this
@@ -258,6 +251,7 @@ struct _Py_UOpsAbstractFrame {
int stack_len;
int locals_len;
PyFunctionObject *func;
+ PyCodeObject *code;
JitOptRef *stack_pointer;
JitOptRef *stack;
@@ -333,11 +327,11 @@ extern _Py_UOpsAbstractFrame *_Py_uop_frame_new(
int curr_stackentries,
JitOptRef *args,
int arg_len);
-extern int _Py_uop_frame_pop(JitOptContext *ctx);
+extern int _Py_uop_frame_pop(JitOptContext *ctx, PyCodeObject *co, int curr_stackentries);
PyAPI_FUNC(PyObject *) _Py_uop_symbols_test(PyObject *self, PyObject *ignored);
-PyAPI_FUNC(int) _PyOptimizer_Optimize(_PyInterpreterFrame *frame, _Py_CODEUNIT *start, _PyExecutorObject **exec_ptr, int chain_depth);
+PyAPI_FUNC(int) _PyOptimizer_Optimize(_PyInterpreterFrame *frame, PyThreadState *tstate);
static inline _PyExecutorObject *_PyExecutor_FromExit(_PyExitData *exit)
{
@@ -346,6 +340,7 @@ static inline _PyExecutorObject *_PyExecutor_FromExit(_PyExitData *exit)
}
extern _PyExecutorObject *_PyExecutor_GetColdExecutor(void);
+extern _PyExecutorObject *_PyExecutor_GetColdDynamicExecutor(void);
PyAPI_FUNC(void) _PyExecutor_ClearExit(_PyExitData *exit);
@@ -354,7 +349,9 @@ static inline int is_terminator(const _PyUOpInstruction *uop)
int opcode = uop->opcode;
return (
opcode == _EXIT_TRACE ||
- opcode == _JUMP_TO_TOP
+ opcode == _DEOPT ||
+ opcode == _JUMP_TO_TOP ||
+ opcode == _DYNAMIC_EXIT
);
}
@@ -365,6 +362,18 @@ PyAPI_FUNC(int) _PyDumpExecutors(FILE *out);
extern void _Py_ClearExecutorDeletionList(PyInterpreterState *interp);
#endif
+int _PyJit_translate_single_bytecode_to_trace(PyThreadState *tstate, _PyInterpreterFrame *frame, _Py_CODEUNIT *next_instr, bool stop_tracing);
+
+int
+_PyJit_TryInitializeTracing(PyThreadState *tstate, _PyInterpreterFrame *frame,
+ _Py_CODEUNIT *curr_instr, _Py_CODEUNIT *start_instr,
+ _Py_CODEUNIT *close_loop_instr, int curr_stackdepth, int chain_depth, _PyExitData *exit,
+ int oparg);
+
+void _PyJit_FinalizeTracing(PyThreadState *tstate);
+
+void _PyJit_Tracer_InvalidateDependency(PyThreadState *old_tstate, void *obj);
+
#ifdef __cplusplus
}
#endif
diff --git a/Include/internal/pycore_tstate.h b/Include/internal/pycore_tstate.h
index a44c523e202..50048801b2e 100644
--- a/Include/internal/pycore_tstate.h
+++ b/Include/internal/pycore_tstate.h
@@ -12,7 +12,8 @@ extern "C" {
#include "pycore_freelist_state.h" // struct _Py_freelists
#include "pycore_mimalloc.h" // struct _mimalloc_thread_state
#include "pycore_qsbr.h" // struct qsbr
-
+#include "pycore_uop.h" // struct _PyUOpInstruction
+#include "pycore_structs.h"
#ifdef Py_GIL_DISABLED
struct _gc_thread_state {
@@ -21,6 +22,38 @@ struct _gc_thread_state {
};
#endif
+#if _Py_TIER2
+typedef struct _PyJitTracerInitialState {
+ int stack_depth;
+ int chain_depth;
+ struct _PyExitData *exit;
+ PyCodeObject *code; // Strong
+ PyFunctionObject *func; // Strong
+ _Py_CODEUNIT *start_instr;
+ _Py_CODEUNIT *close_loop_instr;
+ _Py_CODEUNIT *jump_backward_instr;
+} _PyJitTracerInitialState;
+
+typedef struct _PyJitTracerPreviousState {
+ bool dependencies_still_valid;
+ bool instr_is_super;
+ int code_max_size;
+ int code_curr_size;
+ int instr_oparg;
+ int instr_stacklevel;
+ _Py_CODEUNIT *instr;
+ PyCodeObject *instr_code; // Strong
+ struct _PyInterpreterFrame *instr_frame;
+ _PyBloomFilter dependencies;
+} _PyJitTracerPreviousState;
+
+typedef struct _PyJitTracerState {
+ _PyUOpInstruction *code_buffer;
+ _PyJitTracerInitialState initial_state;
+ _PyJitTracerPreviousState prev_state;
+} _PyJitTracerState;
+#endif
+
// Every PyThreadState is actually allocated as a _PyThreadStateImpl. The
// PyThreadState fields are exposed as part of the C API, although most fields
// are intended to be private. The _PyThreadStateImpl fields not exposed.
@@ -85,7 +118,9 @@ typedef struct _PyThreadStateImpl {
#if defined(Py_REF_DEBUG) && defined(Py_GIL_DISABLED)
Py_ssize_t reftotal; // this thread's total refcount operations
#endif
-
+#if _Py_TIER2
+ _PyJitTracerState jit_tracer_state;
+#endif
} _PyThreadStateImpl;
#ifdef __cplusplus
diff --git a/Include/internal/pycore_uop.h b/Include/internal/pycore_uop.h
index 4abefd3b95d..4e1b15af42c 100644
--- a/Include/internal/pycore_uop.h
+++ b/Include/internal/pycore_uop.h
@@ -35,10 +35,18 @@ typedef struct _PyUOpInstruction{
#endif
} _PyUOpInstruction;
-// This is the length of the trace we project initially.
-#define UOP_MAX_TRACE_LENGTH 1200
+// This is the length of the trace we translate initially.
+#define UOP_MAX_TRACE_LENGTH 3000
#define UOP_BUFFER_SIZE (UOP_MAX_TRACE_LENGTH * sizeof(_PyUOpInstruction))
+/* Bloom filter with m = 256
+ * https://en.wikipedia.org/wiki/Bloom_filter */
+#define _Py_BLOOM_FILTER_WORDS 8
+
+typedef struct {
+ uint32_t bits[_Py_BLOOM_FILTER_WORDS];
+} _PyBloomFilter;
+
#ifdef __cplusplus
}
#endif
diff --git a/Include/internal/pycore_uop_ids.h b/Include/internal/pycore_uop_ids.h
index ff1d75c0cb1..7a33a5b84fd 100644
--- a/Include/internal/pycore_uop_ids.h
+++ b/Include/internal/pycore_uop_ids.h
@@ -81,101 +81,107 @@ extern "C" {
#define _CHECK_STACK_SPACE 357
#define _CHECK_STACK_SPACE_OPERAND 358
#define _CHECK_VALIDITY 359
-#define _COLD_EXIT 360
-#define _COMPARE_OP 361
-#define _COMPARE_OP_FLOAT 362
-#define _COMPARE_OP_INT 363
-#define _COMPARE_OP_STR 364
-#define _CONTAINS_OP 365
-#define _CONTAINS_OP_DICT 366
-#define _CONTAINS_OP_SET 367
+#define _COLD_DYNAMIC_EXIT 360
+#define _COLD_EXIT 361
+#define _COMPARE_OP 362
+#define _COMPARE_OP_FLOAT 363
+#define _COMPARE_OP_INT 364
+#define _COMPARE_OP_STR 365
+#define _CONTAINS_OP 366
+#define _CONTAINS_OP_DICT 367
+#define _CONTAINS_OP_SET 368
#define _CONVERT_VALUE CONVERT_VALUE
-#define _COPY 368
-#define _COPY_1 369
-#define _COPY_2 370
-#define _COPY_3 371
+#define _COPY 369
+#define _COPY_1 370
+#define _COPY_2 371
+#define _COPY_3 372
#define _COPY_FREE_VARS COPY_FREE_VARS
-#define _CREATE_INIT_FRAME 372
+#define _CREATE_INIT_FRAME 373
#define _DELETE_ATTR DELETE_ATTR
#define _DELETE_DEREF DELETE_DEREF
#define _DELETE_FAST DELETE_FAST
#define _DELETE_GLOBAL DELETE_GLOBAL
#define _DELETE_NAME DELETE_NAME
#define _DELETE_SUBSCR DELETE_SUBSCR
-#define _DEOPT 373
+#define _DEOPT 374
#define _DICT_MERGE DICT_MERGE
#define _DICT_UPDATE DICT_UPDATE
-#define _DO_CALL 374
-#define _DO_CALL_FUNCTION_EX 375
-#define _DO_CALL_KW 376
+#define _DO_CALL 375
+#define _DO_CALL_FUNCTION_EX 376
+#define _DO_CALL_KW 377
+#define _DYNAMIC_EXIT 378
#define _END_FOR END_FOR
#define _END_SEND END_SEND
-#define _ERROR_POP_N 377
+#define _ERROR_POP_N 379
#define _EXIT_INIT_CHECK EXIT_INIT_CHECK
-#define _EXPAND_METHOD 378
-#define _EXPAND_METHOD_KW 379
-#define _FATAL_ERROR 380
+#define _EXPAND_METHOD 380
+#define _EXPAND_METHOD_KW 381
+#define _FATAL_ERROR 382
#define _FORMAT_SIMPLE FORMAT_SIMPLE
#define _FORMAT_WITH_SPEC FORMAT_WITH_SPEC
-#define _FOR_ITER 381
-#define _FOR_ITER_GEN_FRAME 382
-#define _FOR_ITER_TIER_TWO 383
+#define _FOR_ITER 383
+#define _FOR_ITER_GEN_FRAME 384
+#define _FOR_ITER_TIER_TWO 385
#define _GET_AITER GET_AITER
#define _GET_ANEXT GET_ANEXT
#define _GET_AWAITABLE GET_AWAITABLE
#define _GET_ITER GET_ITER
#define _GET_LEN GET_LEN
#define _GET_YIELD_FROM_ITER GET_YIELD_FROM_ITER
-#define _GUARD_BINARY_OP_EXTEND 384
-#define _GUARD_CALLABLE_ISINSTANCE 385
-#define _GUARD_CALLABLE_LEN 386
-#define _GUARD_CALLABLE_LIST_APPEND 387
-#define _GUARD_CALLABLE_STR_1 388
-#define _GUARD_CALLABLE_TUPLE_1 389
-#define _GUARD_CALLABLE_TYPE_1 390
-#define _GUARD_DORV_NO_DICT 391
-#define _GUARD_DORV_VALUES_INST_ATTR_FROM_DICT 392
-#define _GUARD_GLOBALS_VERSION 393
-#define _GUARD_IS_FALSE_POP 394
-#define _GUARD_IS_NONE_POP 395
-#define _GUARD_IS_NOT_NONE_POP 396
-#define _GUARD_IS_TRUE_POP 397
-#define _GUARD_KEYS_VERSION 398
-#define _GUARD_NOS_DICT 399
-#define _GUARD_NOS_FLOAT 400
-#define _GUARD_NOS_INT 401
-#define _GUARD_NOS_LIST 402
-#define _GUARD_NOS_NOT_NULL 403
-#define _GUARD_NOS_NULL 404
-#define _GUARD_NOS_OVERFLOWED 405
-#define _GUARD_NOS_TUPLE 406
-#define _GUARD_NOS_UNICODE 407
-#define _GUARD_NOT_EXHAUSTED_LIST 408
-#define _GUARD_NOT_EXHAUSTED_RANGE 409
-#define _GUARD_NOT_EXHAUSTED_TUPLE 410
-#define _GUARD_THIRD_NULL 411
-#define _GUARD_TOS_ANY_SET 412
-#define _GUARD_TOS_DICT 413
-#define _GUARD_TOS_FLOAT 414
-#define _GUARD_TOS_INT 415
-#define _GUARD_TOS_LIST 416
-#define _GUARD_TOS_OVERFLOWED 417
-#define _GUARD_TOS_SLICE 418
-#define _GUARD_TOS_TUPLE 419
-#define _GUARD_TOS_UNICODE 420
-#define _GUARD_TYPE_VERSION 421
-#define _GUARD_TYPE_VERSION_AND_LOCK 422
-#define _HANDLE_PENDING_AND_DEOPT 423
+#define _GUARD_BINARY_OP_EXTEND 386
+#define _GUARD_CALLABLE_ISINSTANCE 387
+#define _GUARD_CALLABLE_LEN 388
+#define _GUARD_CALLABLE_LIST_APPEND 389
+#define _GUARD_CALLABLE_STR_1 390
+#define _GUARD_CALLABLE_TUPLE_1 391
+#define _GUARD_CALLABLE_TYPE_1 392
+#define _GUARD_DORV_NO_DICT 393
+#define _GUARD_DORV_VALUES_INST_ATTR_FROM_DICT 394
+#define _GUARD_GLOBALS_VERSION 395
+#define _GUARD_IP_RETURN_GENERATOR 396
+#define _GUARD_IP_RETURN_VALUE 397
+#define _GUARD_IP_YIELD_VALUE 398
+#define _GUARD_IP__PUSH_FRAME 399
+#define _GUARD_IS_FALSE_POP 400
+#define _GUARD_IS_NONE_POP 401
+#define _GUARD_IS_NOT_NONE_POP 402
+#define _GUARD_IS_TRUE_POP 403
+#define _GUARD_KEYS_VERSION 404
+#define _GUARD_NOS_DICT 405
+#define _GUARD_NOS_FLOAT 406
+#define _GUARD_NOS_INT 407
+#define _GUARD_NOS_LIST 408
+#define _GUARD_NOS_NOT_NULL 409
+#define _GUARD_NOS_NULL 410
+#define _GUARD_NOS_OVERFLOWED 411
+#define _GUARD_NOS_TUPLE 412
+#define _GUARD_NOS_UNICODE 413
+#define _GUARD_NOT_EXHAUSTED_LIST 414
+#define _GUARD_NOT_EXHAUSTED_RANGE 415
+#define _GUARD_NOT_EXHAUSTED_TUPLE 416
+#define _GUARD_THIRD_NULL 417
+#define _GUARD_TOS_ANY_SET 418
+#define _GUARD_TOS_DICT 419
+#define _GUARD_TOS_FLOAT 420
+#define _GUARD_TOS_INT 421
+#define _GUARD_TOS_LIST 422
+#define _GUARD_TOS_OVERFLOWED 423
+#define _GUARD_TOS_SLICE 424
+#define _GUARD_TOS_TUPLE 425
+#define _GUARD_TOS_UNICODE 426
+#define _GUARD_TYPE_VERSION 427
+#define _GUARD_TYPE_VERSION_AND_LOCK 428
+#define _HANDLE_PENDING_AND_DEOPT 429
#define _IMPORT_FROM IMPORT_FROM
#define _IMPORT_NAME IMPORT_NAME
-#define _INIT_CALL_BOUND_METHOD_EXACT_ARGS 424
-#define _INIT_CALL_PY_EXACT_ARGS 425
-#define _INIT_CALL_PY_EXACT_ARGS_0 426
-#define _INIT_CALL_PY_EXACT_ARGS_1 427
-#define _INIT_CALL_PY_EXACT_ARGS_2 428
-#define _INIT_CALL_PY_EXACT_ARGS_3 429
-#define _INIT_CALL_PY_EXACT_ARGS_4 430
-#define _INSERT_NULL 431
+#define _INIT_CALL_BOUND_METHOD_EXACT_ARGS 430
+#define _INIT_CALL_PY_EXACT_ARGS 431
+#define _INIT_CALL_PY_EXACT_ARGS_0 432
+#define _INIT_CALL_PY_EXACT_ARGS_1 433
+#define _INIT_CALL_PY_EXACT_ARGS_2 434
+#define _INIT_CALL_PY_EXACT_ARGS_3 435
+#define _INIT_CALL_PY_EXACT_ARGS_4 436
+#define _INSERT_NULL 437
#define _INSTRUMENTED_FOR_ITER INSTRUMENTED_FOR_ITER
#define _INSTRUMENTED_INSTRUCTION INSTRUMENTED_INSTRUCTION
#define _INSTRUMENTED_JUMP_FORWARD INSTRUMENTED_JUMP_FORWARD
@@ -185,177 +191,178 @@ extern "C" {
#define _INSTRUMENTED_POP_JUMP_IF_NONE INSTRUMENTED_POP_JUMP_IF_NONE
#define _INSTRUMENTED_POP_JUMP_IF_NOT_NONE INSTRUMENTED_POP_JUMP_IF_NOT_NONE
#define _INSTRUMENTED_POP_JUMP_IF_TRUE INSTRUMENTED_POP_JUMP_IF_TRUE
-#define _IS_NONE 432
+#define _IS_NONE 438
#define _IS_OP IS_OP
-#define _ITER_CHECK_LIST 433
-#define _ITER_CHECK_RANGE 434
-#define _ITER_CHECK_TUPLE 435
-#define _ITER_JUMP_LIST 436
-#define _ITER_JUMP_RANGE 437
-#define _ITER_JUMP_TUPLE 438
-#define _ITER_NEXT_LIST 439
-#define _ITER_NEXT_LIST_TIER_TWO 440
-#define _ITER_NEXT_RANGE 441
-#define _ITER_NEXT_TUPLE 442
-#define _JUMP_TO_TOP 443
+#define _ITER_CHECK_LIST 439
+#define _ITER_CHECK_RANGE 440
+#define _ITER_CHECK_TUPLE 441
+#define _ITER_JUMP_LIST 442
+#define _ITER_JUMP_RANGE 443
+#define _ITER_JUMP_TUPLE 444
+#define _ITER_NEXT_LIST 445
+#define _ITER_NEXT_LIST_TIER_TWO 446
+#define _ITER_NEXT_RANGE 447
+#define _ITER_NEXT_TUPLE 448
+#define _JUMP_BACKWARD_NO_INTERRUPT JUMP_BACKWARD_NO_INTERRUPT
+#define _JUMP_TO_TOP 449
#define _LIST_APPEND LIST_APPEND
#define _LIST_EXTEND LIST_EXTEND
-#define _LOAD_ATTR 444
-#define _LOAD_ATTR_CLASS 445
+#define _LOAD_ATTR 450
+#define _LOAD_ATTR_CLASS 451
#define _LOAD_ATTR_GETATTRIBUTE_OVERRIDDEN LOAD_ATTR_GETATTRIBUTE_OVERRIDDEN
-#define _LOAD_ATTR_INSTANCE_VALUE 446
-#define _LOAD_ATTR_METHOD_LAZY_DICT 447
-#define _LOAD_ATTR_METHOD_NO_DICT 448
-#define _LOAD_ATTR_METHOD_WITH_VALUES 449
-#define _LOAD_ATTR_MODULE 450
-#define _LOAD_ATTR_NONDESCRIPTOR_NO_DICT 451
-#define _LOAD_ATTR_NONDESCRIPTOR_WITH_VALUES 452
-#define _LOAD_ATTR_PROPERTY_FRAME 453
-#define _LOAD_ATTR_SLOT 454
-#define _LOAD_ATTR_WITH_HINT 455
+#define _LOAD_ATTR_INSTANCE_VALUE 452
+#define _LOAD_ATTR_METHOD_LAZY_DICT 453
+#define _LOAD_ATTR_METHOD_NO_DICT 454
+#define _LOAD_ATTR_METHOD_WITH_VALUES 455
+#define _LOAD_ATTR_MODULE 456
+#define _LOAD_ATTR_NONDESCRIPTOR_NO_DICT 457
+#define _LOAD_ATTR_NONDESCRIPTOR_WITH_VALUES 458
+#define _LOAD_ATTR_PROPERTY_FRAME 459
+#define _LOAD_ATTR_SLOT 460
+#define _LOAD_ATTR_WITH_HINT 461
#define _LOAD_BUILD_CLASS LOAD_BUILD_CLASS
-#define _LOAD_BYTECODE 456
+#define _LOAD_BYTECODE 462
#define _LOAD_COMMON_CONSTANT LOAD_COMMON_CONSTANT
#define _LOAD_CONST LOAD_CONST
-#define _LOAD_CONST_INLINE 457
-#define _LOAD_CONST_INLINE_BORROW 458
-#define _LOAD_CONST_UNDER_INLINE 459
-#define _LOAD_CONST_UNDER_INLINE_BORROW 460
+#define _LOAD_CONST_INLINE 463
+#define _LOAD_CONST_INLINE_BORROW 464
+#define _LOAD_CONST_UNDER_INLINE 465
+#define _LOAD_CONST_UNDER_INLINE_BORROW 466
#define _LOAD_DEREF LOAD_DEREF
-#define _LOAD_FAST 461
-#define _LOAD_FAST_0 462
-#define _LOAD_FAST_1 463
-#define _LOAD_FAST_2 464
-#define _LOAD_FAST_3 465
-#define _LOAD_FAST_4 466
-#define _LOAD_FAST_5 467
-#define _LOAD_FAST_6 468
-#define _LOAD_FAST_7 469
+#define _LOAD_FAST 467
+#define _LOAD_FAST_0 468
+#define _LOAD_FAST_1 469
+#define _LOAD_FAST_2 470
+#define _LOAD_FAST_3 471
+#define _LOAD_FAST_4 472
+#define _LOAD_FAST_5 473
+#define _LOAD_FAST_6 474
+#define _LOAD_FAST_7 475
#define _LOAD_FAST_AND_CLEAR LOAD_FAST_AND_CLEAR
-#define _LOAD_FAST_BORROW 470
-#define _LOAD_FAST_BORROW_0 471
-#define _LOAD_FAST_BORROW_1 472
-#define _LOAD_FAST_BORROW_2 473
-#define _LOAD_FAST_BORROW_3 474
-#define _LOAD_FAST_BORROW_4 475
-#define _LOAD_FAST_BORROW_5 476
-#define _LOAD_FAST_BORROW_6 477
-#define _LOAD_FAST_BORROW_7 478
+#define _LOAD_FAST_BORROW 476
+#define _LOAD_FAST_BORROW_0 477
+#define _LOAD_FAST_BORROW_1 478
+#define _LOAD_FAST_BORROW_2 479
+#define _LOAD_FAST_BORROW_3 480
+#define _LOAD_FAST_BORROW_4 481
+#define _LOAD_FAST_BORROW_5 482
+#define _LOAD_FAST_BORROW_6 483
+#define _LOAD_FAST_BORROW_7 484
#define _LOAD_FAST_BORROW_LOAD_FAST_BORROW LOAD_FAST_BORROW_LOAD_FAST_BORROW
#define _LOAD_FAST_CHECK LOAD_FAST_CHECK
#define _LOAD_FAST_LOAD_FAST LOAD_FAST_LOAD_FAST
#define _LOAD_FROM_DICT_OR_DEREF LOAD_FROM_DICT_OR_DEREF
#define _LOAD_FROM_DICT_OR_GLOBALS LOAD_FROM_DICT_OR_GLOBALS
-#define _LOAD_GLOBAL 479
-#define _LOAD_GLOBAL_BUILTINS 480
-#define _LOAD_GLOBAL_MODULE 481
+#define _LOAD_GLOBAL 485
+#define _LOAD_GLOBAL_BUILTINS 486
+#define _LOAD_GLOBAL_MODULE 487
#define _LOAD_LOCALS LOAD_LOCALS
#define _LOAD_NAME LOAD_NAME
-#define _LOAD_SMALL_INT 482
-#define _LOAD_SMALL_INT_0 483
-#define _LOAD_SMALL_INT_1 484
-#define _LOAD_SMALL_INT_2 485
-#define _LOAD_SMALL_INT_3 486
-#define _LOAD_SPECIAL 487
+#define _LOAD_SMALL_INT 488
+#define _LOAD_SMALL_INT_0 489
+#define _LOAD_SMALL_INT_1 490
+#define _LOAD_SMALL_INT_2 491
+#define _LOAD_SMALL_INT_3 492
+#define _LOAD_SPECIAL 493
#define _LOAD_SUPER_ATTR_ATTR LOAD_SUPER_ATTR_ATTR
#define _LOAD_SUPER_ATTR_METHOD LOAD_SUPER_ATTR_METHOD
-#define _MAKE_CALLARGS_A_TUPLE 488
+#define _MAKE_CALLARGS_A_TUPLE 494
#define _MAKE_CELL MAKE_CELL
#define _MAKE_FUNCTION MAKE_FUNCTION
-#define _MAKE_WARM 489
+#define _MAKE_WARM 495
#define _MAP_ADD MAP_ADD
#define _MATCH_CLASS MATCH_CLASS
#define _MATCH_KEYS MATCH_KEYS
#define _MATCH_MAPPING MATCH_MAPPING
#define _MATCH_SEQUENCE MATCH_SEQUENCE
-#define _MAYBE_EXPAND_METHOD 490
-#define _MAYBE_EXPAND_METHOD_KW 491
-#define _MONITOR_CALL 492
-#define _MONITOR_CALL_KW 493
-#define _MONITOR_JUMP_BACKWARD 494
-#define _MONITOR_RESUME 495
+#define _MAYBE_EXPAND_METHOD 496
+#define _MAYBE_EXPAND_METHOD_KW 497
+#define _MONITOR_CALL 498
+#define _MONITOR_CALL_KW 499
+#define _MONITOR_JUMP_BACKWARD 500
+#define _MONITOR_RESUME 501
#define _NOP NOP
-#define _POP_CALL 496
-#define _POP_CALL_LOAD_CONST_INLINE_BORROW 497
-#define _POP_CALL_ONE 498
-#define _POP_CALL_ONE_LOAD_CONST_INLINE_BORROW 499
-#define _POP_CALL_TWO 500
-#define _POP_CALL_TWO_LOAD_CONST_INLINE_BORROW 501
+#define _POP_CALL 502
+#define _POP_CALL_LOAD_CONST_INLINE_BORROW 503
+#define _POP_CALL_ONE 504
+#define _POP_CALL_ONE_LOAD_CONST_INLINE_BORROW 505
+#define _POP_CALL_TWO 506
+#define _POP_CALL_TWO_LOAD_CONST_INLINE_BORROW 507
#define _POP_EXCEPT POP_EXCEPT
#define _POP_ITER POP_ITER
-#define _POP_JUMP_IF_FALSE 502
-#define _POP_JUMP_IF_TRUE 503
+#define _POP_JUMP_IF_FALSE 508
+#define _POP_JUMP_IF_TRUE 509
#define _POP_TOP POP_TOP
-#define _POP_TOP_FLOAT 504
-#define _POP_TOP_INT 505
-#define _POP_TOP_LOAD_CONST_INLINE 506
-#define _POP_TOP_LOAD_CONST_INLINE_BORROW 507
-#define _POP_TOP_NOP 508
-#define _POP_TOP_UNICODE 509
-#define _POP_TWO 510
-#define _POP_TWO_LOAD_CONST_INLINE_BORROW 511
+#define _POP_TOP_FLOAT 510
+#define _POP_TOP_INT 511
+#define _POP_TOP_LOAD_CONST_INLINE 512
+#define _POP_TOP_LOAD_CONST_INLINE_BORROW 513
+#define _POP_TOP_NOP 514
+#define _POP_TOP_UNICODE 515
+#define _POP_TWO 516
+#define _POP_TWO_LOAD_CONST_INLINE_BORROW 517
#define _PUSH_EXC_INFO PUSH_EXC_INFO
-#define _PUSH_FRAME 512
+#define _PUSH_FRAME 518
#define _PUSH_NULL PUSH_NULL
-#define _PUSH_NULL_CONDITIONAL 513
-#define _PY_FRAME_GENERAL 514
-#define _PY_FRAME_KW 515
-#define _QUICKEN_RESUME 516
-#define _REPLACE_WITH_TRUE 517
+#define _PUSH_NULL_CONDITIONAL 519
+#define _PY_FRAME_GENERAL 520
+#define _PY_FRAME_KW 521
+#define _QUICKEN_RESUME 522
+#define _REPLACE_WITH_TRUE 523
#define _RESUME_CHECK RESUME_CHECK
#define _RETURN_GENERATOR RETURN_GENERATOR
#define _RETURN_VALUE RETURN_VALUE
-#define _SAVE_RETURN_OFFSET 518
-#define _SEND 519
-#define _SEND_GEN_FRAME 520
+#define _SAVE_RETURN_OFFSET 524
+#define _SEND 525
+#define _SEND_GEN_FRAME 526
#define _SETUP_ANNOTATIONS SETUP_ANNOTATIONS
#define _SET_ADD SET_ADD
#define _SET_FUNCTION_ATTRIBUTE SET_FUNCTION_ATTRIBUTE
#define _SET_UPDATE SET_UPDATE
-#define _START_EXECUTOR 521
-#define _STORE_ATTR 522
-#define _STORE_ATTR_INSTANCE_VALUE 523
-#define _STORE_ATTR_SLOT 524
-#define _STORE_ATTR_WITH_HINT 525
+#define _START_EXECUTOR 527
+#define _STORE_ATTR 528
+#define _STORE_ATTR_INSTANCE_VALUE 529
+#define _STORE_ATTR_SLOT 530
+#define _STORE_ATTR_WITH_HINT 531
#define _STORE_DEREF STORE_DEREF
-#define _STORE_FAST 526
-#define _STORE_FAST_0 527
-#define _STORE_FAST_1 528
-#define _STORE_FAST_2 529
-#define _STORE_FAST_3 530
-#define _STORE_FAST_4 531
-#define _STORE_FAST_5 532
-#define _STORE_FAST_6 533
-#define _STORE_FAST_7 534
+#define _STORE_FAST 532
+#define _STORE_FAST_0 533
+#define _STORE_FAST_1 534
+#define _STORE_FAST_2 535
+#define _STORE_FAST_3 536
+#define _STORE_FAST_4 537
+#define _STORE_FAST_5 538
+#define _STORE_FAST_6 539
+#define _STORE_FAST_7 540
#define _STORE_FAST_LOAD_FAST STORE_FAST_LOAD_FAST
#define _STORE_FAST_STORE_FAST STORE_FAST_STORE_FAST
#define _STORE_GLOBAL STORE_GLOBAL
#define _STORE_NAME STORE_NAME
-#define _STORE_SLICE 535
-#define _STORE_SUBSCR 536
-#define _STORE_SUBSCR_DICT 537
-#define _STORE_SUBSCR_LIST_INT 538
-#define _SWAP 539
-#define _SWAP_2 540
-#define _SWAP_3 541
-#define _TIER2_RESUME_CHECK 542
-#define _TO_BOOL 543
+#define _STORE_SLICE 541
+#define _STORE_SUBSCR 542
+#define _STORE_SUBSCR_DICT 543
+#define _STORE_SUBSCR_LIST_INT 544
+#define _SWAP 545
+#define _SWAP_2 546
+#define _SWAP_3 547
+#define _TIER2_RESUME_CHECK 548
+#define _TO_BOOL 549
#define _TO_BOOL_BOOL TO_BOOL_BOOL
#define _TO_BOOL_INT TO_BOOL_INT
-#define _TO_BOOL_LIST 544
+#define _TO_BOOL_LIST 550
#define _TO_BOOL_NONE TO_BOOL_NONE
-#define _TO_BOOL_STR 545
+#define _TO_BOOL_STR 551
#define _UNARY_INVERT UNARY_INVERT
#define _UNARY_NEGATIVE UNARY_NEGATIVE
#define _UNARY_NOT UNARY_NOT
#define _UNPACK_EX UNPACK_EX
-#define _UNPACK_SEQUENCE 546
-#define _UNPACK_SEQUENCE_LIST 547
-#define _UNPACK_SEQUENCE_TUPLE 548
-#define _UNPACK_SEQUENCE_TWO_TUPLE 549
+#define _UNPACK_SEQUENCE 552
+#define _UNPACK_SEQUENCE_LIST 553
+#define _UNPACK_SEQUENCE_TUPLE 554
+#define _UNPACK_SEQUENCE_TWO_TUPLE 555
#define _WITH_EXCEPT_START WITH_EXCEPT_START
#define _YIELD_VALUE YIELD_VALUE
-#define MAX_UOP_ID 549
+#define MAX_UOP_ID 555
#ifdef __cplusplus
}
diff --git a/Include/internal/pycore_uop_metadata.h b/Include/internal/pycore_uop_metadata.h
index 12487719969..d5a3c362d87 100644
--- a/Include/internal/pycore_uop_metadata.h
+++ b/Include/internal/pycore_uop_metadata.h
@@ -11,7 +11,7 @@ extern "C" {
#include
#include "pycore_uop_ids.h"
-extern const uint16_t _PyUop_Flags[MAX_UOP_ID+1];
+extern const uint32_t _PyUop_Flags[MAX_UOP_ID+1];
typedef struct _rep_range { uint8_t start; uint8_t stop; } ReplicationRange;
extern const ReplicationRange _PyUop_Replication[MAX_UOP_ID+1];
extern const char * const _PyOpcode_uop_name[MAX_UOP_ID+1];
@@ -19,7 +19,7 @@ extern const char * const _PyOpcode_uop_name[MAX_UOP_ID+1];
extern int _PyUop_num_popped(int opcode, int oparg);
#ifdef NEED_OPCODE_METADATA
-const uint16_t _PyUop_Flags[MAX_UOP_ID+1] = {
+const uint32_t _PyUop_Flags[MAX_UOP_ID+1] = {
[_NOP] = HAS_PURE_FLAG,
[_CHECK_PERIODIC] = HAS_EVAL_BREAK_FLAG | HAS_ERROR_FLAG | HAS_ESCAPES_FLAG,
[_CHECK_PERIODIC_IF_NOT_YIELD_FROM] = HAS_ARG_FLAG | HAS_EVAL_BREAK_FLAG | HAS_ERROR_FLAG | HAS_ESCAPES_FLAG,
@@ -128,12 +128,12 @@ const uint16_t _PyUop_Flags[MAX_UOP_ID+1] = {
[_DELETE_SUBSCR] = HAS_ERROR_FLAG | HAS_ESCAPES_FLAG,
[_CALL_INTRINSIC_1] = HAS_ARG_FLAG | HAS_ERROR_FLAG | HAS_ESCAPES_FLAG,
[_CALL_INTRINSIC_2] = HAS_ARG_FLAG | HAS_ERROR_FLAG | HAS_ESCAPES_FLAG,
- [_RETURN_VALUE] = HAS_ESCAPES_FLAG,
+ [_RETURN_VALUE] = HAS_ESCAPES_FLAG | HAS_NEEDS_GUARD_IP_FLAG,
[_GET_AITER] = HAS_ERROR_FLAG | HAS_ESCAPES_FLAG,
[_GET_ANEXT] = HAS_ERROR_FLAG | HAS_ERROR_NO_POP_FLAG | HAS_ESCAPES_FLAG,
[_GET_AWAITABLE] = HAS_ARG_FLAG | HAS_ERROR_FLAG | HAS_ESCAPES_FLAG,
[_SEND_GEN_FRAME] = HAS_ARG_FLAG | HAS_DEOPT_FLAG,
- [_YIELD_VALUE] = HAS_ARG_FLAG,
+ [_YIELD_VALUE] = HAS_ARG_FLAG | HAS_NEEDS_GUARD_IP_FLAG,
[_POP_EXCEPT] = HAS_ESCAPES_FLAG,
[_LOAD_COMMON_CONSTANT] = HAS_ARG_FLAG,
[_LOAD_BUILD_CLASS] = HAS_ERROR_FLAG | HAS_ESCAPES_FLAG,
@@ -256,7 +256,7 @@ const uint16_t _PyUop_Flags[MAX_UOP_ID+1] = {
[_INIT_CALL_PY_EXACT_ARGS_3] = HAS_PURE_FLAG,
[_INIT_CALL_PY_EXACT_ARGS_4] = HAS_PURE_FLAG,
[_INIT_CALL_PY_EXACT_ARGS] = HAS_ARG_FLAG | HAS_PURE_FLAG,
- [_PUSH_FRAME] = 0,
+ [_PUSH_FRAME] = HAS_NEEDS_GUARD_IP_FLAG,
[_GUARD_NOS_NULL] = HAS_DEOPT_FLAG,
[_GUARD_NOS_NOT_NULL] = HAS_EXIT_FLAG,
[_GUARD_THIRD_NULL] = HAS_DEOPT_FLAG,
@@ -293,7 +293,7 @@ const uint16_t _PyUop_Flags[MAX_UOP_ID+1] = {
[_MAKE_CALLARGS_A_TUPLE] = HAS_ERROR_FLAG | HAS_ERROR_NO_POP_FLAG | HAS_ESCAPES_FLAG,
[_MAKE_FUNCTION] = HAS_ERROR_FLAG | HAS_ESCAPES_FLAG,
[_SET_FUNCTION_ATTRIBUTE] = HAS_ARG_FLAG,
- [_RETURN_GENERATOR] = HAS_ERROR_FLAG | HAS_ESCAPES_FLAG,
+ [_RETURN_GENERATOR] = HAS_ERROR_FLAG | HAS_ESCAPES_FLAG | HAS_NEEDS_GUARD_IP_FLAG,
[_BUILD_SLICE] = HAS_ARG_FLAG | HAS_ERROR_FLAG | HAS_ESCAPES_FLAG,
[_CONVERT_VALUE] = HAS_ARG_FLAG | HAS_ERROR_FLAG | HAS_ESCAPES_FLAG,
[_FORMAT_SIMPLE] = HAS_ERROR_FLAG | HAS_ESCAPES_FLAG,
@@ -315,6 +315,7 @@ const uint16_t _PyUop_Flags[MAX_UOP_ID+1] = {
[_CHECK_STACK_SPACE_OPERAND] = HAS_DEOPT_FLAG,
[_SAVE_RETURN_OFFSET] = HAS_ARG_FLAG,
[_EXIT_TRACE] = HAS_ESCAPES_FLAG,
+ [_DYNAMIC_EXIT] = HAS_ESCAPES_FLAG,
[_CHECK_VALIDITY] = HAS_DEOPT_FLAG,
[_LOAD_CONST_INLINE] = HAS_PURE_FLAG,
[_POP_TOP_LOAD_CONST_INLINE] = HAS_ESCAPES_FLAG | HAS_PURE_FLAG,
@@ -336,7 +337,12 @@ const uint16_t _PyUop_Flags[MAX_UOP_ID+1] = {
[_HANDLE_PENDING_AND_DEOPT] = HAS_ESCAPES_FLAG,
[_ERROR_POP_N] = HAS_ARG_FLAG,
[_TIER2_RESUME_CHECK] = HAS_PERIODIC_FLAG,
- [_COLD_EXIT] = HAS_ESCAPES_FLAG,
+ [_COLD_EXIT] = 0,
+ [_COLD_DYNAMIC_EXIT] = 0,
+ [_GUARD_IP__PUSH_FRAME] = HAS_EXIT_FLAG,
+ [_GUARD_IP_YIELD_VALUE] = HAS_EXIT_FLAG,
+ [_GUARD_IP_RETURN_VALUE] = HAS_EXIT_FLAG,
+ [_GUARD_IP_RETURN_GENERATOR] = HAS_EXIT_FLAG,
};
const ReplicationRange _PyUop_Replication[MAX_UOP_ID+1] = {
@@ -419,6 +425,7 @@ const char *const _PyOpcode_uop_name[MAX_UOP_ID+1] = {
[_CHECK_STACK_SPACE] = "_CHECK_STACK_SPACE",
[_CHECK_STACK_SPACE_OPERAND] = "_CHECK_STACK_SPACE_OPERAND",
[_CHECK_VALIDITY] = "_CHECK_VALIDITY",
+ [_COLD_DYNAMIC_EXIT] = "_COLD_DYNAMIC_EXIT",
[_COLD_EXIT] = "_COLD_EXIT",
[_COMPARE_OP] = "_COMPARE_OP",
[_COMPARE_OP_FLOAT] = "_COMPARE_OP_FLOAT",
@@ -443,6 +450,7 @@ const char *const _PyOpcode_uop_name[MAX_UOP_ID+1] = {
[_DEOPT] = "_DEOPT",
[_DICT_MERGE] = "_DICT_MERGE",
[_DICT_UPDATE] = "_DICT_UPDATE",
+ [_DYNAMIC_EXIT] = "_DYNAMIC_EXIT",
[_END_FOR] = "_END_FOR",
[_END_SEND] = "_END_SEND",
[_ERROR_POP_N] = "_ERROR_POP_N",
@@ -471,6 +479,10 @@ const char *const _PyOpcode_uop_name[MAX_UOP_ID+1] = {
[_GUARD_DORV_NO_DICT] = "_GUARD_DORV_NO_DICT",
[_GUARD_DORV_VALUES_INST_ATTR_FROM_DICT] = "_GUARD_DORV_VALUES_INST_ATTR_FROM_DICT",
[_GUARD_GLOBALS_VERSION] = "_GUARD_GLOBALS_VERSION",
+ [_GUARD_IP_RETURN_GENERATOR] = "_GUARD_IP_RETURN_GENERATOR",
+ [_GUARD_IP_RETURN_VALUE] = "_GUARD_IP_RETURN_VALUE",
+ [_GUARD_IP_YIELD_VALUE] = "_GUARD_IP_YIELD_VALUE",
+ [_GUARD_IP__PUSH_FRAME] = "_GUARD_IP__PUSH_FRAME",
[_GUARD_IS_FALSE_POP] = "_GUARD_IS_FALSE_POP",
[_GUARD_IS_NONE_POP] = "_GUARD_IS_NONE_POP",
[_GUARD_IS_NOT_NONE_POP] = "_GUARD_IS_NOT_NONE_POP",
@@ -1261,6 +1273,8 @@ int _PyUop_num_popped(int opcode, int oparg)
return 0;
case _EXIT_TRACE:
return 0;
+ case _DYNAMIC_EXIT:
+ return 0;
case _CHECK_VALIDITY:
return 0;
case _LOAD_CONST_INLINE:
@@ -1305,6 +1319,16 @@ int _PyUop_num_popped(int opcode, int oparg)
return 0;
case _COLD_EXIT:
return 0;
+ case _COLD_DYNAMIC_EXIT:
+ return 0;
+ case _GUARD_IP__PUSH_FRAME:
+ return 0;
+ case _GUARD_IP_YIELD_VALUE:
+ return 0;
+ case _GUARD_IP_RETURN_VALUE:
+ return 0;
+ case _GUARD_IP_RETURN_GENERATOR:
+ return 0;
default:
return -1;
}
diff --git a/Lib/test/test_ast/test_ast.py b/Lib/test/test_ast/test_ast.py
index fb4a441ca64..608ffdfad12 100644
--- a/Lib/test/test_ast/test_ast.py
+++ b/Lib/test/test_ast/test_ast.py
@@ -3057,8 +3057,8 @@ def test_source_segment_missing_info(self):
class NodeTransformerTests(ASTTestMixin, unittest.TestCase):
def assertASTTransformation(self, transformer_class,
- initial_code, expected_code):
- initial_ast = ast.parse(dedent(initial_code))
+ code, expected_code):
+ initial_ast = ast.parse(dedent(code))
expected_ast = ast.parse(dedent(expected_code))
transformer = transformer_class()
diff --git a/Lib/test/test_capi/test_opt.py b/Lib/test/test_capi/test_opt.py
index e65556fb28f..f06c6cbda29 100644
--- a/Lib/test/test_capi/test_opt.py
+++ b/Lib/test/test_capi/test_opt.py
@@ -422,32 +422,6 @@ def testfunc(n, m):
uops = get_opnames(ex)
self.assertIn("_FOR_ITER_TIER_TWO", uops)
- def test_confidence_score(self):
- def testfunc(n):
- bits = 0
- for i in range(n):
- if i & 0x01:
- bits += 1
- if i & 0x02:
- bits += 1
- if i&0x04:
- bits += 1
- if i&0x08:
- bits += 1
- if i&0x10:
- bits += 1
- return bits
-
- x = testfunc(TIER2_THRESHOLD * 2)
-
- self.assertEqual(x, TIER2_THRESHOLD * 5)
- ex = get_first_executor(testfunc)
- self.assertIsNotNone(ex)
- ops = list(iter_opnames(ex))
- #Since branch is 50/50 the trace could go either way.
- count = ops.count("_GUARD_IS_TRUE_POP") + ops.count("_GUARD_IS_FALSE_POP")
- self.assertLessEqual(count, 2)
-
@requires_specialization
@unittest.skipIf(Py_GIL_DISABLED, "optimizer not yet supported in free-threaded builds")
@@ -847,38 +821,7 @@ def testfunc(n):
self.assertLessEqual(len(guard_nos_unicode_count), 1)
self.assertIn("_COMPARE_OP_STR", uops)
- def test_type_inconsistency(self):
- ns = {}
- src = textwrap.dedent("""
- def testfunc(n):
- for i in range(n):
- x = _test_global + _test_global
- """)
- exec(src, ns, ns)
- testfunc = ns['testfunc']
- ns['_test_global'] = 0
- _, ex = self._run_with_optimizer(testfunc, TIER2_THRESHOLD - 1)
- self.assertIsNone(ex)
- ns['_test_global'] = 1
- _, ex = self._run_with_optimizer(testfunc, TIER2_THRESHOLD - 1)
- self.assertIsNotNone(ex)
- uops = get_opnames(ex)
- self.assertNotIn("_GUARD_TOS_INT", uops)
- self.assertNotIn("_GUARD_NOS_INT", uops)
- self.assertNotIn("_BINARY_OP_ADD_INT", uops)
- self.assertNotIn("_POP_TWO_LOAD_CONST_INLINE_BORROW", uops)
- # Try again, but between the runs, set the global to a float.
- # This should result in no executor the second time.
- ns = {}
- exec(src, ns, ns)
- testfunc = ns['testfunc']
- ns['_test_global'] = 0
- _, ex = self._run_with_optimizer(testfunc, TIER2_THRESHOLD - 1)
- self.assertIsNone(ex)
- ns['_test_global'] = 3.14
- _, ex = self._run_with_optimizer(testfunc, TIER2_THRESHOLD - 1)
- self.assertIsNone(ex)
-
+ @unittest.skip("gh-139109 WIP")
def test_combine_stack_space_checks_sequential(self):
def dummy12(x):
return x - 1
@@ -907,6 +850,7 @@ def testfunc(n):
largest_stack = _testinternalcapi.get_co_framesize(dummy13.__code__)
self.assertIn(("_CHECK_STACK_SPACE_OPERAND", largest_stack), uops_and_operands)
+ @unittest.skip("gh-139109 WIP")
def test_combine_stack_space_checks_nested(self):
def dummy12(x):
return x + 3
@@ -937,6 +881,7 @@ def testfunc(n):
)
self.assertIn(("_CHECK_STACK_SPACE_OPERAND", largest_stack), uops_and_operands)
+ @unittest.skip("gh-139109 WIP")
def test_combine_stack_space_checks_several_calls(self):
def dummy12(x):
return x + 3
@@ -972,6 +917,7 @@ def testfunc(n):
)
self.assertIn(("_CHECK_STACK_SPACE_OPERAND", largest_stack), uops_and_operands)
+ @unittest.skip("gh-139109 WIP")
def test_combine_stack_space_checks_several_calls_different_order(self):
# same as `several_calls` but with top-level calls reversed
def dummy12(x):
@@ -1008,6 +954,7 @@ def testfunc(n):
)
self.assertIn(("_CHECK_STACK_SPACE_OPERAND", largest_stack), uops_and_operands)
+ @unittest.skip("gh-139109 WIP")
def test_combine_stack_space_complex(self):
def dummy0(x):
return x
@@ -1057,6 +1004,7 @@ def testfunc(n):
("_CHECK_STACK_SPACE_OPERAND", largest_stack), uops_and_operands
)
+ @unittest.skip("gh-139109 WIP")
def test_combine_stack_space_checks_large_framesize(self):
# Create a function with a large framesize. This ensures _CHECK_STACK_SPACE is
# actually doing its job. Note that the resulting trace hits
@@ -1118,6 +1066,7 @@ def testfunc(n):
("_CHECK_STACK_SPACE_OPERAND", largest_stack), uops_and_operands
)
+ @unittest.skip("gh-139109 WIP")
def test_combine_stack_space_checks_recursion(self):
def dummy15(x):
while x > 0:
diff --git a/Lib/test/test_sys.py b/Lib/test/test_sys.py
index 9d3248d972e..798f58737b1 100644
--- a/Lib/test/test_sys.py
+++ b/Lib/test/test_sys.py
@@ -2253,9 +2253,10 @@ def frame_2_jit(expected: bool) -> None:
def frame_3_jit() -> None:
# JITs just before the last loop:
- for i in range(_testinternalcapi.TIER2_THRESHOLD + 1):
+ # 1 extra iteration for tracing.
+ for i in range(_testinternalcapi.TIER2_THRESHOLD + 2):
# Careful, doing this in the reverse order breaks tracing:
- expected = {enabled} and i == _testinternalcapi.TIER2_THRESHOLD
+ expected = {enabled} and i >= _testinternalcapi.TIER2_THRESHOLD + 1
assert sys._jit.is_active() is expected
frame_2_jit(expected)
assert sys._jit.is_active() is expected
diff --git a/Misc/NEWS.d/next/Core_and_Builtins/2025-10-18-21-50-44.gh-issue-139109.9QQOzN.rst b/Misc/NEWS.d/next/Core_and_Builtins/2025-10-18-21-50-44.gh-issue-139109.9QQOzN.rst
new file mode 100644
index 00000000000..40b9d19ee42
--- /dev/null
+++ b/Misc/NEWS.d/next/Core_and_Builtins/2025-10-18-21-50-44.gh-issue-139109.9QQOzN.rst
@@ -0,0 +1 @@
+A new tracing frontend for the JIT compiler has been implemented. Patch by Ken Jin. Design for CPython by Ken Jin, Mark Shannon and Brandt Bucher.
diff --git a/Modules/_testinternalcapi.c b/Modules/_testinternalcapi.c
index 6514ca7f3cd..89e558b0fe8 100644
--- a/Modules/_testinternalcapi.c
+++ b/Modules/_testinternalcapi.c
@@ -2661,7 +2661,8 @@ module_exec(PyObject *module)
}
if (PyModule_Add(module, "TIER2_THRESHOLD",
- PyLong_FromLong(JUMP_BACKWARD_INITIAL_VALUE + 1)) < 0) {
+ // + 1 more due to one loop spent on tracing.
+ PyLong_FromLong(JUMP_BACKWARD_INITIAL_VALUE + 2)) < 0) {
return 1;
}
diff --git a/Objects/codeobject.c b/Objects/codeobject.c
index fc3f5d9dde0..3aea2038fd1 100644
--- a/Objects/codeobject.c
+++ b/Objects/codeobject.c
@@ -2432,6 +2432,7 @@ code_dealloc(PyObject *self)
PyMem_Free(co_extra);
}
#ifdef _Py_TIER2
+ _PyJit_Tracer_InvalidateDependency(tstate, self);
if (co->co_executors != NULL) {
clear_executors(co);
}
diff --git a/Objects/frameobject.c b/Objects/frameobject.c
index 0cae3703d1d..b652973600c 100644
--- a/Objects/frameobject.c
+++ b/Objects/frameobject.c
@@ -17,6 +17,7 @@
#include "frameobject.h" // PyFrameLocalsProxyObject
#include "opcode.h" // EXTENDED_ARG
+#include "pycore_optimizer.h"
#include "clinic/frameobject.c.h"
@@ -260,7 +261,10 @@ framelocalsproxy_setitem(PyObject *self, PyObject *key, PyObject *value)
return -1;
}
- _Py_Executors_InvalidateDependency(PyInterpreterState_Get(), co, 1);
+#if _Py_TIER2
+ _Py_Executors_InvalidateDependency(_PyInterpreterState_GET(), co, 1);
+ _PyJit_Tracer_InvalidateDependency(_PyThreadState_GET(), co);
+#endif
_PyLocals_Kind kind = _PyLocals_GetKind(co->co_localspluskinds, i);
_PyStackRef oldvalue = fast[i];
diff --git a/Objects/funcobject.c b/Objects/funcobject.c
index 43198aaf8a7..b659ac80233 100644
--- a/Objects/funcobject.c
+++ b/Objects/funcobject.c
@@ -11,7 +11,7 @@
#include "pycore_setobject.h" // _PySet_NextEntry()
#include "pycore_stats.h"
#include "pycore_weakref.h" // FT_CLEAR_WEAKREFS()
-
+#include "pycore_optimizer.h" // _PyJit_Tracer_InvalidateDependency
static const char *
func_event_name(PyFunction_WatchEvent event) {
@@ -1151,6 +1151,10 @@ func_dealloc(PyObject *self)
if (_PyObject_ResurrectEnd(self)) {
return;
}
+#if _Py_TIER2
+ _Py_Executors_InvalidateDependency(_PyInterpreterState_GET(), self, 1);
+ _PyJit_Tracer_InvalidateDependency(_PyThreadState_GET(), self);
+#endif
_PyObject_GC_UNTRACK(op);
FT_CLEAR_WEAKREFS(self, op->func_weakreflist);
(void)func_clear((PyObject*)op);
diff --git a/Python/bytecodes.c b/Python/bytecodes.c
index 6ebd9ebdfce..2c798855a71 100644
--- a/Python/bytecodes.c
+++ b/Python/bytecodes.c
@@ -2938,8 +2938,8 @@ dummy_func(
JUMP_BACKWARD_JIT,
};
- tier1 op(_SPECIALIZE_JUMP_BACKWARD, (--)) {
- #if ENABLE_SPECIALIZATION_FT
+ specializing tier1 op(_SPECIALIZE_JUMP_BACKWARD, (--)) {
+ #if ENABLE_SPECIALIZATION
if (this_instr->op.code == JUMP_BACKWARD) {
uint8_t desired = tstate->interp->jit ? JUMP_BACKWARD_JIT : JUMP_BACKWARD_NO_JIT;
FT_ATOMIC_STORE_UINT8_RELAXED(this_instr->op.code, desired);
@@ -2953,25 +2953,21 @@ dummy_func(
tier1 op(_JIT, (--)) {
#ifdef _Py_TIER2
_Py_BackoffCounter counter = this_instr[1].counter;
- if (backoff_counter_triggers(counter) && this_instr->op.code == JUMP_BACKWARD_JIT) {
- _Py_CODEUNIT *start = this_instr;
- /* Back up over EXTENDED_ARGs so optimizer sees the whole instruction */
+ if (!IS_JIT_TRACING() && backoff_counter_triggers(counter) &&
+ this_instr->op.code == JUMP_BACKWARD_JIT &&
+ next_instr->op.code != ENTER_EXECUTOR) {
+ /* Back up over EXTENDED_ARGs so executor is inserted at the correct place */
+ _Py_CODEUNIT *insert_exec_at = this_instr;
while (oparg > 255) {
oparg >>= 8;
- start--;
+ insert_exec_at--;
}
- _PyExecutorObject *executor;
- int optimized = _PyOptimizer_Optimize(frame, start, &executor, 0);
- if (optimized <= 0) {
- this_instr[1].counter = restart_backoff_counter(counter);
- ERROR_IF(optimized < 0);
+ int succ = _PyJit_TryInitializeTracing(tstate, frame, this_instr, insert_exec_at, next_instr, STACK_LEVEL(), 0, NULL, oparg);
+ if (succ) {
+ ENTER_TRACING();
}
else {
- this_instr[1].counter = initial_jump_backoff_counter();
- assert(tstate->current_executor == NULL);
- assert(executor != tstate->interp->cold_executor);
- tstate->jit_exit = NULL;
- TIER1_TO_TIER2(executor);
+ this_instr[1].counter = restart_backoff_counter(counter);
}
}
else {
@@ -3017,6 +3013,10 @@ dummy_func(
tier1 inst(ENTER_EXECUTOR, (--)) {
#ifdef _Py_TIER2
+ if (IS_JIT_TRACING()) {
+ next_instr = this_instr;
+ goto stop_tracing;
+ }
PyCodeObject *code = _PyFrame_GetCode(frame);
_PyExecutorObject *executor = code->co_executors->executors[oparg & 255];
assert(executor->vm_data.index == INSTR_OFFSET() - 1);
@@ -3078,7 +3078,7 @@ dummy_func(
macro(POP_JUMP_IF_NOT_NONE) = unused/1 + _IS_NONE + _POP_JUMP_IF_FALSE;
- tier1 inst(JUMP_BACKWARD_NO_INTERRUPT, (--)) {
+ replaced inst(JUMP_BACKWARD_NO_INTERRUPT, (--)) {
/* This bytecode is used in the `yield from` or `await` loop.
* If there is an interrupt, we want it handled in the innermost
* generator or coroutine, so we deliberately do not check it here.
@@ -5245,19 +5245,40 @@ dummy_func(
tier2 op(_EXIT_TRACE, (exit_p/4 --)) {
_PyExitData *exit = (_PyExitData *)exit_p;
#if defined(Py_DEBUG) && !defined(_Py_JIT)
- _Py_CODEUNIT *target = _PyFrame_GetBytecode(frame) + exit->target;
+ const _Py_CODEUNIT *target = ((frame->owner == FRAME_OWNED_BY_INTERPRETER)
+ ? _Py_INTERPRETER_TRAMPOLINE_INSTRUCTIONS_PTR : _PyFrame_GetBytecode(frame))
+ + exit->target;
OPT_HIST(trace_uop_execution_counter, trace_run_length_hist);
- if (frame->lltrace >= 2) {
+ if (frame->lltrace >= 3) {
printf("SIDE EXIT: [UOp ");
_PyUOpPrint(&next_uop[-1]);
+ printf(", exit %tu, temp %d, target %d -> %s, is_control_flow %d]\n",
+ exit - current_executor->exits, exit->temperature.value_and_backoff,
+ (int)(target - _PyFrame_GetBytecode(frame)),
+ _PyOpcode_OpName[target->op.code], exit->is_control_flow);
+ }
+ #endif
+ tstate->jit_exit = exit;
+ TIER2_TO_TIER2(exit->executor);
+ }
+
+ tier2 op(_DYNAMIC_EXIT, (exit_p/4 --)) {
+ #if defined(Py_DEBUG) && !defined(_Py_JIT)
+ _PyExitData *exit = (_PyExitData *)exit_p;
+ _Py_CODEUNIT *target = frame->instr_ptr;
+ OPT_HIST(trace_uop_execution_counter, trace_run_length_hist);
+ if (frame->lltrace >= 3) {
+ printf("DYNAMIC EXIT: [UOp ");
+ _PyUOpPrint(&next_uop[-1]);
printf(", exit %tu, temp %d, target %d -> %s]\n",
exit - current_executor->exits, exit->temperature.value_and_backoff,
(int)(target - _PyFrame_GetBytecode(frame)),
_PyOpcode_OpName[target->op.code]);
}
- #endif
- tstate->jit_exit = exit;
- TIER2_TO_TIER2(exit->executor);
+ #endif
+ // Disabled for now (gh-139109) as it slows down dynamic code tremendously.
+ // Compile and jump to the cold dynamic executors in the future.
+ GOTO_TIER_ONE(frame->instr_ptr);
}
tier2 op(_CHECK_VALIDITY, (--)) {
@@ -5369,7 +5390,8 @@ dummy_func(
}
tier2 op(_DEOPT, (--)) {
- GOTO_TIER_ONE(_PyFrame_GetBytecode(frame) + CURRENT_TARGET());
+ GOTO_TIER_ONE((frame->owner == FRAME_OWNED_BY_INTERPRETER)
+ ? _Py_INTERPRETER_TRAMPOLINE_INSTRUCTIONS_PTR : _PyFrame_GetBytecode(frame) + CURRENT_TARGET());
}
tier2 op(_HANDLE_PENDING_AND_DEOPT, (--)) {
@@ -5399,32 +5421,76 @@ dummy_func(
tier2 op(_COLD_EXIT, ( -- )) {
_PyExitData *exit = tstate->jit_exit;
assert(exit != NULL);
+ assert(frame->owner < FRAME_OWNED_BY_INTERPRETER);
_Py_CODEUNIT *target = _PyFrame_GetBytecode(frame) + exit->target;
_Py_BackoffCounter temperature = exit->temperature;
- if (!backoff_counter_triggers(temperature)) {
- exit->temperature = advance_backoff_counter(temperature);
- GOTO_TIER_ONE(target);
- }
_PyExecutorObject *executor;
if (target->op.code == ENTER_EXECUTOR) {
PyCodeObject *code = _PyFrame_GetCode(frame);
executor = code->co_executors->executors[target->op.arg];
Py_INCREF(executor);
+ assert(tstate->jit_exit == exit);
+ exit->executor = executor;
+ TIER2_TO_TIER2(exit->executor);
}
else {
+ if (!backoff_counter_triggers(temperature)) {
+ exit->temperature = advance_backoff_counter(temperature);
+ GOTO_TIER_ONE(target);
+ }
_PyExecutorObject *previous_executor = _PyExecutor_FromExit(exit);
assert(tstate->current_executor == (PyObject *)previous_executor);
- int chain_depth = previous_executor->vm_data.chain_depth + 1;
- int optimized = _PyOptimizer_Optimize(frame, target, &executor, chain_depth);
- if (optimized <= 0) {
- exit->temperature = restart_backoff_counter(temperature);
- GOTO_TIER_ONE(optimized < 0 ? NULL : target);
+ // For control-flow guards, we don't want to increase the chain depth, as those don't actually
+ // represent deopts but rather just normal programs!
+ int chain_depth = previous_executor->vm_data.chain_depth + !exit->is_control_flow;
+ // Note: it's safe to use target->op.arg here instead of the oparg given by EXTENDED_ARG.
+ // The invariant in the optimizer is the deopt target always points back to the first EXTENDED_ARG.
+ // So setting it to anything else is wrong.
+ int succ = _PyJit_TryInitializeTracing(tstate, frame, target, target, target, STACK_LEVEL(), chain_depth, exit, target->op.arg);
+ exit->temperature = restart_backoff_counter(exit->temperature);
+ if (succ) {
+ GOTO_TIER_ONE_CONTINUE_TRACING(target);
}
- exit->temperature = initial_temperature_backoff_counter();
+ GOTO_TIER_ONE(target);
+ }
+ }
+
+ tier2 op(_COLD_DYNAMIC_EXIT, ( -- )) {
+ // TODO (gh-139109): This should be similar to _COLD_EXIT in the future.
+ _Py_CODEUNIT *target = frame->instr_ptr;
+ GOTO_TIER_ONE(target);
+ }
+
+ tier2 op(_GUARD_IP__PUSH_FRAME, (ip/4 --)) {
+ _Py_CODEUNIT *target = frame->instr_ptr + IP_OFFSET_OF(_PUSH_FRAME);
+ if (target != (_Py_CODEUNIT *)ip) {
+ frame->instr_ptr += IP_OFFSET_OF(_PUSH_FRAME);
+ EXIT_IF(true);
+ }
+ }
+
+ tier2 op(_GUARD_IP_YIELD_VALUE, (ip/4 --)) {
+ _Py_CODEUNIT *target = frame->instr_ptr + IP_OFFSET_OF(YIELD_VALUE);
+ if (target != (_Py_CODEUNIT *)ip) {
+ frame->instr_ptr += IP_OFFSET_OF(YIELD_VALUE);
+ EXIT_IF(true);
+ }
+ }
+
+ tier2 op(_GUARD_IP_RETURN_VALUE, (ip/4 --)) {
+ _Py_CODEUNIT *target = frame->instr_ptr + IP_OFFSET_OF(RETURN_VALUE);
+ if (target != (_Py_CODEUNIT *)ip) {
+ frame->instr_ptr += IP_OFFSET_OF(RETURN_VALUE);
+ EXIT_IF(true);
+ }
+ }
+
+ tier2 op(_GUARD_IP_RETURN_GENERATOR, (ip/4 --)) {
+ _Py_CODEUNIT *target = frame->instr_ptr + IP_OFFSET_OF(RETURN_GENERATOR);
+ if (target != (_Py_CODEUNIT *)ip) {
+ frame->instr_ptr += IP_OFFSET_OF(RETURN_GENERATOR);
+ EXIT_IF(true);
}
- assert(tstate->jit_exit == exit);
- exit->executor = executor;
- TIER2_TO_TIER2(exit->executor);
}
label(pop_2_error) {
@@ -5571,6 +5637,62 @@ dummy_func(
DISPATCH();
}
+ label(record_previous_inst) {
+#if _Py_TIER2
+ assert(IS_JIT_TRACING());
+ int opcode = next_instr->op.code;
+ bool stop_tracing = (opcode == WITH_EXCEPT_START ||
+ opcode == RERAISE || opcode == CLEANUP_THROW ||
+ opcode == PUSH_EXC_INFO || opcode == INTERPRETER_EXIT);
+ int full = !_PyJit_translate_single_bytecode_to_trace(tstate, frame, next_instr, stop_tracing);
+ if (full) {
+ LEAVE_TRACING();
+ int err = stop_tracing_and_jit(tstate, frame);
+ ERROR_IF(err < 0);
+ DISPATCH_GOTO_NON_TRACING();
+ }
+ // Super instructions. Instruction deopted. There's a mismatch in what the stack expects
+ // in the optimizer. So we have to reflect in the trace correctly.
+ _PyThreadStateImpl *_tstate = (_PyThreadStateImpl *)tstate;
+ if ((_tstate->jit_tracer_state.prev_state.instr->op.code == CALL_LIST_APPEND &&
+ opcode == POP_TOP) ||
+ (_tstate->jit_tracer_state.prev_state.instr->op.code == BINARY_OP_INPLACE_ADD_UNICODE &&
+ opcode == STORE_FAST)) {
+ _tstate->jit_tracer_state.prev_state.instr_is_super = true;
+ }
+ else {
+ _tstate->jit_tracer_state.prev_state.instr = next_instr;
+ }
+ PyObject *prev_code = PyStackRef_AsPyObjectBorrow(frame->f_executable);
+ if (_tstate->jit_tracer_state.prev_state.instr_code != (PyCodeObject *)prev_code) {
+ Py_SETREF(_tstate->jit_tracer_state.prev_state.instr_code, (PyCodeObject*)Py_NewRef((prev_code)));
+ }
+
+ _tstate->jit_tracer_state.prev_state.instr_frame = frame;
+ _tstate->jit_tracer_state.prev_state.instr_oparg = oparg;
+ _tstate->jit_tracer_state.prev_state.instr_stacklevel = PyStackRef_IsNone(frame->f_executable) ? 2 : STACK_LEVEL();
+ if (_PyOpcode_Caches[_PyOpcode_Deopt[opcode]]) {
+ (&next_instr[1])->counter = trigger_backoff_counter();
+ }
+ DISPATCH_GOTO_NON_TRACING();
+#else
+ Py_FatalError("JIT label executed in non-jit build.");
+#endif
+ }
+
+ label(stop_tracing) {
+#if _Py_TIER2
+ assert(IS_JIT_TRACING());
+ int opcode = next_instr->op.code;
+ _PyJit_translate_single_bytecode_to_trace(tstate, frame, NULL, true);
+ LEAVE_TRACING();
+ int err = stop_tracing_and_jit(tstate, frame);
+ ERROR_IF(err < 0);
+ DISPATCH_GOTO_NON_TRACING();
+#else
+ Py_FatalError("JIT label executed in non-jit build.");
+#endif
+ }
// END BYTECODES //
diff --git a/Python/ceval.c b/Python/ceval.c
index 07d21575e3a..b76c9ec2811 100644
--- a/Python/ceval.c
+++ b/Python/ceval.c
@@ -1004,6 +1004,8 @@ static const _Py_CODEUNIT _Py_INTERPRETER_TRAMPOLINE_INSTRUCTIONS[] = {
{ .op.code = RESUME, .op.arg = RESUME_OPARG_DEPTH1_MASK | RESUME_AT_FUNC_START }
};
+const _Py_CODEUNIT *_Py_INTERPRETER_TRAMPOLINE_INSTRUCTIONS_PTR = (_Py_CODEUNIT*)&_Py_INTERPRETER_TRAMPOLINE_INSTRUCTIONS;
+
#ifdef Py_DEBUG
extern void _PyUOpPrint(const _PyUOpInstruction *uop);
#endif
@@ -1051,6 +1053,43 @@ _PyObjectArray_Free(PyObject **array, PyObject **scratch)
}
}
+#if _Py_TIER2
+// 0 for success, -1 for error.
+static int
+stop_tracing_and_jit(PyThreadState *tstate, _PyInterpreterFrame *frame)
+{
+ int _is_sys_tracing = (tstate->c_tracefunc != NULL) || (tstate->c_profilefunc != NULL);
+ int err = 0;
+ if (!_PyErr_Occurred(tstate) && !_is_sys_tracing) {
+ err = _PyOptimizer_Optimize(frame, tstate);
+ }
+ _PyThreadStateImpl *_tstate = (_PyThreadStateImpl *)tstate;
+ // Deal with backoffs
+ _PyExitData *exit = _tstate->jit_tracer_state.initial_state.exit;
+ if (exit == NULL) {
+ // We hold a strong reference to the code object, so the instruction won't be freed.
+ if (err <= 0) {
+ _Py_BackoffCounter counter = _tstate->jit_tracer_state.initial_state.jump_backward_instr[1].counter;
+ _tstate->jit_tracer_state.initial_state.jump_backward_instr[1].counter = restart_backoff_counter(counter);
+ }
+ else {
+ _tstate->jit_tracer_state.initial_state.jump_backward_instr[1].counter = initial_jump_backoff_counter();
+ }
+ }
+ else {
+ // Likewise, we hold a strong reference to the executor containing this exit, so the exit is guaranteed
+ // to be valid to access.
+ if (err <= 0) {
+ exit->temperature = restart_backoff_counter(exit->temperature);
+ }
+ else {
+ exit->temperature = initial_temperature_backoff_counter();
+ }
+ }
+ _PyJit_FinalizeTracing(tstate);
+ return err;
+}
+#endif
/* _PyEval_EvalFrameDefault is too large to optimize for speed with PGO on MSVC.
*/
@@ -1180,9 +1219,9 @@ _PyEval_EvalFrameDefault(PyThreadState *tstate, _PyInterpreterFrame *frame, int
stack_pointer = _PyFrame_GetStackPointer(frame);
#if _Py_TAIL_CALL_INTERP
# if Py_STATS
- return _TAIL_CALL_error(frame, stack_pointer, tstate, next_instr, instruction_funcptr_table, 0, lastopcode);
+ return _TAIL_CALL_error(frame, stack_pointer, tstate, next_instr, instruction_funcptr_handler_table, 0, lastopcode);
# else
- return _TAIL_CALL_error(frame, stack_pointer, tstate, next_instr, instruction_funcptr_table, 0);
+ return _TAIL_CALL_error(frame, stack_pointer, tstate, next_instr, instruction_funcptr_handler_table, 0);
# endif
#else
goto error;
@@ -1191,9 +1230,9 @@ _PyEval_EvalFrameDefault(PyThreadState *tstate, _PyInterpreterFrame *frame, int
#if _Py_TAIL_CALL_INTERP
# if Py_STATS
- return _TAIL_CALL_start_frame(frame, NULL, tstate, NULL, instruction_funcptr_table, 0, lastopcode);
+ return _TAIL_CALL_start_frame(frame, NULL, tstate, NULL, instruction_funcptr_handler_table, 0, lastopcode);
# else
- return _TAIL_CALL_start_frame(frame, NULL, tstate, NULL, instruction_funcptr_table, 0);
+ return _TAIL_CALL_start_frame(frame, NULL, tstate, NULL, instruction_funcptr_handler_table, 0);
# endif
#else
goto start_frame;
@@ -1235,7 +1274,9 @@ _PyTier2Interpreter(
tier2_start:
next_uop = current_executor->trace;
- assert(next_uop->opcode == _START_EXECUTOR || next_uop->opcode == _COLD_EXIT);
+ assert(next_uop->opcode == _START_EXECUTOR ||
+ next_uop->opcode == _COLD_EXIT ||
+ next_uop->opcode == _COLD_DYNAMIC_EXIT);
#undef LOAD_IP
#define LOAD_IP(UNUSED) (void)0
@@ -1259,7 +1300,9 @@ _PyTier2Interpreter(
uint64_t trace_uop_execution_counter = 0;
#endif
- assert(next_uop->opcode == _START_EXECUTOR || next_uop->opcode == _COLD_EXIT);
+ assert(next_uop->opcode == _START_EXECUTOR ||
+ next_uop->opcode == _COLD_EXIT ||
+ next_uop->opcode == _COLD_DYNAMIC_EXIT);
tier2_dispatch:
for (;;) {
uopcode = next_uop->opcode;
diff --git a/Python/ceval_macros.h b/Python/ceval_macros.h
index afdcbc563b2..05a2760671e 100644
--- a/Python/ceval_macros.h
+++ b/Python/ceval_macros.h
@@ -93,11 +93,19 @@
# define Py_PRESERVE_NONE_CC __attribute__((preserve_none))
Py_PRESERVE_NONE_CC typedef PyObject* (*py_tail_call_funcptr)(TAIL_CALL_PARAMS);
+# define DISPATCH_TABLE_VAR instruction_funcptr_table
+# define DISPATCH_TABLE instruction_funcptr_handler_table
+# define TRACING_DISPATCH_TABLE instruction_funcptr_tracing_table
# define TARGET(op) Py_PRESERVE_NONE_CC PyObject *_TAIL_CALL_##op(TAIL_CALL_PARAMS)
+
# define DISPATCH_GOTO() \
do { \
Py_MUSTTAIL return (((py_tail_call_funcptr *)instruction_funcptr_table)[opcode])(TAIL_CALL_ARGS); \
} while (0)
+# define DISPATCH_GOTO_NON_TRACING() \
+ do { \
+ Py_MUSTTAIL return (((py_tail_call_funcptr *)DISPATCH_TABLE)[opcode])(TAIL_CALL_ARGS); \
+ } while (0)
# define JUMP_TO_LABEL(name) \
do { \
Py_MUSTTAIL return (_TAIL_CALL_##name)(TAIL_CALL_ARGS); \
@@ -115,19 +123,36 @@
# endif
# define LABEL(name) TARGET(name)
#elif USE_COMPUTED_GOTOS
+# define DISPATCH_TABLE_VAR opcode_targets
+# define DISPATCH_TABLE opcode_targets_table
+# define TRACING_DISPATCH_TABLE opcode_tracing_targets_table
# define TARGET(op) TARGET_##op:
# define DISPATCH_GOTO() goto *opcode_targets[opcode]
+# define DISPATCH_GOTO_NON_TRACING() goto *DISPATCH_TABLE[opcode];
# define JUMP_TO_LABEL(name) goto name;
# define JUMP_TO_PREDICTED(name) goto PREDICTED_##name;
# define LABEL(name) name:
#else
# define TARGET(op) case op: TARGET_##op:
# define DISPATCH_GOTO() goto dispatch_opcode
+# define DISPATCH_GOTO_NON_TRACING() goto dispatch_opcode
# define JUMP_TO_LABEL(name) goto name;
# define JUMP_TO_PREDICTED(name) goto PREDICTED_##name;
# define LABEL(name) name:
#endif
+#if (_Py_TAIL_CALL_INTERP || USE_COMPUTED_GOTOS) && _Py_TIER2
+# define IS_JIT_TRACING() (DISPATCH_TABLE_VAR == TRACING_DISPATCH_TABLE)
+# define ENTER_TRACING() \
+ DISPATCH_TABLE_VAR = TRACING_DISPATCH_TABLE;
+# define LEAVE_TRACING() \
+ DISPATCH_TABLE_VAR = DISPATCH_TABLE;
+#else
+# define IS_JIT_TRACING() (0)
+# define ENTER_TRACING()
+# define LEAVE_TRACING()
+#endif
+
/* PRE_DISPATCH_GOTO() does lltrace if enabled. Normally a no-op */
#ifdef Py_DEBUG
#define PRE_DISPATCH_GOTO() if (frame->lltrace >= 5) { \
@@ -164,11 +189,19 @@ do { \
DISPATCH_GOTO(); \
}
+#define DISPATCH_NON_TRACING() \
+ { \
+ assert(frame->stackpointer == NULL); \
+ NEXTOPARG(); \
+ PRE_DISPATCH_GOTO(); \
+ DISPATCH_GOTO_NON_TRACING(); \
+ }
+
#define DISPATCH_SAME_OPARG() \
{ \
opcode = next_instr->op.code; \
PRE_DISPATCH_GOTO(); \
- DISPATCH_GOTO(); \
+ DISPATCH_GOTO_NON_TRACING(); \
}
#define DISPATCH_INLINED(NEW_FRAME) \
@@ -280,6 +313,7 @@ GETITEM(PyObject *v, Py_ssize_t i) {
/* This takes a uint16_t instead of a _Py_BackoffCounter,
* because it is used directly on the cache entry in generated code,
* which is always an integral type. */
+// Force re-specialization when tracing a side exit to get good side exits.
#define ADAPTIVE_COUNTER_TRIGGERS(COUNTER) \
backoff_counter_triggers(forge_backoff_counter((COUNTER)))
@@ -366,12 +400,19 @@ do { \
next_instr = _Py_jit_entry((EXECUTOR), frame, stack_pointer, tstate); \
frame = tstate->current_frame; \
stack_pointer = _PyFrame_GetStackPointer(frame); \
+ int keep_tracing_bit = (uintptr_t)next_instr & 1; \
+ next_instr = (_Py_CODEUNIT *)(((uintptr_t)next_instr) & (~1)); \
if (next_instr == NULL) { \
/* gh-140104: The exception handler expects frame->instr_ptr
to after this_instr, not this_instr! */ \
next_instr = frame->instr_ptr + 1; \
JUMP_TO_LABEL(error); \
} \
+ if (keep_tracing_bit) { \
+ assert(((_PyThreadStateImpl *)tstate)->jit_tracer_state.prev_state.code_curr_size == 2); \
+ ENTER_TRACING(); \
+ DISPATCH_NON_TRACING(); \
+ } \
DISPATCH(); \
} while (0)
@@ -382,13 +423,23 @@ do { \
goto tier2_start; \
} while (0)
-#define GOTO_TIER_ONE(TARGET) \
- do \
- { \
- tstate->current_executor = NULL; \
- OPT_HIST(trace_uop_execution_counter, trace_run_length_hist); \
- _PyFrame_SetStackPointer(frame, stack_pointer); \
- return TARGET; \
+#define GOTO_TIER_ONE_SETUP \
+ tstate->current_executor = NULL; \
+ OPT_HIST(trace_uop_execution_counter, trace_run_length_hist); \
+ _PyFrame_SetStackPointer(frame, stack_pointer);
+
+#define GOTO_TIER_ONE(TARGET) \
+ do \
+ { \
+ GOTO_TIER_ONE_SETUP \
+ return (_Py_CODEUNIT *)(TARGET); \
+ } while (0)
+
+#define GOTO_TIER_ONE_CONTINUE_TRACING(TARGET) \
+ do \
+ { \
+ GOTO_TIER_ONE_SETUP \
+ return (_Py_CODEUNIT *)(((uintptr_t)(TARGET))| 1); \
} while (0)
#define CURRENT_OPARG() (next_uop[-1].oparg)
diff --git a/Python/executor_cases.c.h b/Python/executor_cases.c.h
index 9ce0a9f8a4d..7ba2e9d0d92 100644
--- a/Python/executor_cases.c.h
+++ b/Python/executor_cases.c.h
@@ -4189,6 +4189,8 @@
break;
}
+ /* _JUMP_BACKWARD_NO_INTERRUPT is not a viable micro-op for tier 2 because it is replaced */
+
case _GET_LEN: {
_PyStackRef obj;
_PyStackRef len;
@@ -7108,12 +7110,36 @@
PyObject *exit_p = (PyObject *)CURRENT_OPERAND0();
_PyExitData *exit = (_PyExitData *)exit_p;
#if defined(Py_DEBUG) && !defined(_Py_JIT)
- _Py_CODEUNIT *target = _PyFrame_GetBytecode(frame) + exit->target;
+ const _Py_CODEUNIT *target = ((frame->owner == FRAME_OWNED_BY_INTERPRETER)
+ ? _Py_INTERPRETER_TRAMPOLINE_INSTRUCTIONS_PTR : _PyFrame_GetBytecode(frame))
+ + exit->target;
OPT_HIST(trace_uop_execution_counter, trace_run_length_hist);
- if (frame->lltrace >= 2) {
+ if (frame->lltrace >= 3) {
_PyFrame_SetStackPointer(frame, stack_pointer);
printf("SIDE EXIT: [UOp ");
_PyUOpPrint(&next_uop[-1]);
+ printf(", exit %tu, temp %d, target %d -> %s, is_control_flow %d]\n",
+ exit - current_executor->exits, exit->temperature.value_and_backoff,
+ (int)(target - _PyFrame_GetBytecode(frame)),
+ _PyOpcode_OpName[target->op.code], exit->is_control_flow);
+ stack_pointer = _PyFrame_GetStackPointer(frame);
+ }
+ #endif
+ tstate->jit_exit = exit;
+ TIER2_TO_TIER2(exit->executor);
+ break;
+ }
+
+ case _DYNAMIC_EXIT: {
+ PyObject *exit_p = (PyObject *)CURRENT_OPERAND0();
+ #if defined(Py_DEBUG) && !defined(_Py_JIT)
+ _PyExitData *exit = (_PyExitData *)exit_p;
+ _Py_CODEUNIT *target = frame->instr_ptr;
+ OPT_HIST(trace_uop_execution_counter, trace_run_length_hist);
+ if (frame->lltrace >= 3) {
+ _PyFrame_SetStackPointer(frame, stack_pointer);
+ printf("DYNAMIC EXIT: [UOp ");
+ _PyUOpPrint(&next_uop[-1]);
printf(", exit %tu, temp %d, target %d -> %s]\n",
exit - current_executor->exits, exit->temperature.value_and_backoff,
(int)(target - _PyFrame_GetBytecode(frame)),
@@ -7121,8 +7147,8 @@
stack_pointer = _PyFrame_GetStackPointer(frame);
}
#endif
- tstate->jit_exit = exit;
- TIER2_TO_TIER2(exit->executor);
+
+ GOTO_TIER_ONE(frame->instr_ptr);
break;
}
@@ -7419,7 +7445,8 @@
}
case _DEOPT: {
- GOTO_TIER_ONE(_PyFrame_GetBytecode(frame) + CURRENT_TARGET());
+ GOTO_TIER_ONE((frame->owner == FRAME_OWNED_BY_INTERPRETER)
+ ? _Py_INTERPRETER_TRAMPOLINE_INSTRUCTIONS_PTR : _PyFrame_GetBytecode(frame) + CURRENT_TARGET());
break;
}
@@ -7460,37 +7487,101 @@
case _COLD_EXIT: {
_PyExitData *exit = tstate->jit_exit;
assert(exit != NULL);
+ assert(frame->owner < FRAME_OWNED_BY_INTERPRETER);
_Py_CODEUNIT *target = _PyFrame_GetBytecode(frame) + exit->target;
_Py_BackoffCounter temperature = exit->temperature;
- if (!backoff_counter_triggers(temperature)) {
- exit->temperature = advance_backoff_counter(temperature);
- GOTO_TIER_ONE(target);
- }
_PyExecutorObject *executor;
if (target->op.code == ENTER_EXECUTOR) {
PyCodeObject *code = _PyFrame_GetCode(frame);
executor = code->co_executors->executors[target->op.arg];
Py_INCREF(executor);
+ assert(tstate->jit_exit == exit);
+ exit->executor = executor;
+ TIER2_TO_TIER2(exit->executor);
}
else {
- _PyFrame_SetStackPointer(frame, stack_pointer);
- _PyExecutorObject *previous_executor = _PyExecutor_FromExit(exit);
- stack_pointer = _PyFrame_GetStackPointer(frame);
- assert(tstate->current_executor == (PyObject *)previous_executor);
- int chain_depth = previous_executor->vm_data.chain_depth + 1;
- _PyFrame_SetStackPointer(frame, stack_pointer);
- int optimized = _PyOptimizer_Optimize(frame, target, &executor, chain_depth);
- stack_pointer = _PyFrame_GetStackPointer(frame);
- if (optimized <= 0) {
- exit->temperature = restart_backoff_counter(temperature);
- GOTO_TIER_ONE(optimized < 0 ? NULL : target);
+ if (!backoff_counter_triggers(temperature)) {
+ exit->temperature = advance_backoff_counter(temperature);
+ GOTO_TIER_ONE(target);
}
- exit->temperature = initial_temperature_backoff_counter();
+ _PyExecutorObject *previous_executor = _PyExecutor_FromExit(exit);
+ assert(tstate->current_executor == (PyObject *)previous_executor);
+ int chain_depth = previous_executor->vm_data.chain_depth + !exit->is_control_flow;
+ int succ = _PyJit_TryInitializeTracing(tstate, frame, target, target, target, STACK_LEVEL(), chain_depth, exit, target->op.arg);
+ exit->temperature = restart_backoff_counter(exit->temperature);
+ if (succ) {
+ GOTO_TIER_ONE_CONTINUE_TRACING(target);
+ }
+ GOTO_TIER_ONE(target);
}
- assert(tstate->jit_exit == exit);
- exit->executor = executor;
- TIER2_TO_TIER2(exit->executor);
break;
}
+ case _COLD_DYNAMIC_EXIT: {
+ _Py_CODEUNIT *target = frame->instr_ptr;
+ GOTO_TIER_ONE(target);
+ break;
+ }
+
+ case _GUARD_IP__PUSH_FRAME: {
+ #define OFFSET_OF__PUSH_FRAME ((0))
+ PyObject *ip = (PyObject *)CURRENT_OPERAND0();
+ _Py_CODEUNIT *target = frame->instr_ptr + OFFSET_OF__PUSH_FRAME;
+ if (target != (_Py_CODEUNIT *)ip) {
+ frame->instr_ptr += OFFSET_OF__PUSH_FRAME;
+ if (true) {
+ UOP_STAT_INC(uopcode, miss);
+ JUMP_TO_JUMP_TARGET();
+ }
+ }
+ #undef OFFSET_OF__PUSH_FRAME
+ break;
+ }
+
+ case _GUARD_IP_YIELD_VALUE: {
+ #define OFFSET_OF_YIELD_VALUE ((1+INLINE_CACHE_ENTRIES_SEND))
+ PyObject *ip = (PyObject *)CURRENT_OPERAND0();
+ _Py_CODEUNIT *target = frame->instr_ptr + OFFSET_OF_YIELD_VALUE;
+ if (target != (_Py_CODEUNIT *)ip) {
+ frame->instr_ptr += OFFSET_OF_YIELD_VALUE;
+ if (true) {
+ UOP_STAT_INC(uopcode, miss);
+ JUMP_TO_JUMP_TARGET();
+ }
+ }
+ #undef OFFSET_OF_YIELD_VALUE
+ break;
+ }
+
+ case _GUARD_IP_RETURN_VALUE: {
+ #define OFFSET_OF_RETURN_VALUE ((frame->return_offset))
+ PyObject *ip = (PyObject *)CURRENT_OPERAND0();
+ _Py_CODEUNIT *target = frame->instr_ptr + OFFSET_OF_RETURN_VALUE;
+ if (target != (_Py_CODEUNIT *)ip) {
+ frame->instr_ptr += OFFSET_OF_RETURN_VALUE;
+ if (true) {
+ UOP_STAT_INC(uopcode, miss);
+ JUMP_TO_JUMP_TARGET();
+ }
+ }
+ #undef OFFSET_OF_RETURN_VALUE
+ break;
+ }
+
+ case _GUARD_IP_RETURN_GENERATOR: {
+ #define OFFSET_OF_RETURN_GENERATOR ((frame->return_offset))
+ PyObject *ip = (PyObject *)CURRENT_OPERAND0();
+ _Py_CODEUNIT *target = frame->instr_ptr + OFFSET_OF_RETURN_GENERATOR;
+ if (target != (_Py_CODEUNIT *)ip) {
+ frame->instr_ptr += OFFSET_OF_RETURN_GENERATOR;
+ if (true) {
+ UOP_STAT_INC(uopcode, miss);
+ JUMP_TO_JUMP_TARGET();
+ }
+ }
+ #undef OFFSET_OF_RETURN_GENERATOR
+ break;
+ }
+
+
#undef TIER_TWO
diff --git a/Python/generated_cases.c.h b/Python/generated_cases.c.h
index 79328a7b725..a984da6dc91 100644
--- a/Python/generated_cases.c.h
+++ b/Python/generated_cases.c.h
@@ -5476,6 +5476,10 @@
INSTRUCTION_STATS(ENTER_EXECUTOR);
opcode = ENTER_EXECUTOR;
#ifdef _Py_TIER2
+ if (IS_JIT_TRACING()) {
+ next_instr = this_instr;
+ JUMP_TO_LABEL(stop_tracing);
+ }
PyCodeObject *code = _PyFrame_GetCode(frame);
_PyExecutorObject *executor = code->co_executors->executors[oparg & 255];
assert(executor->vm_data.index == INSTR_OFFSET() - 1);
@@ -7589,7 +7593,7 @@
/* Skip 1 cache entry */
// _SPECIALIZE_JUMP_BACKWARD
{
- #if ENABLE_SPECIALIZATION_FT
+ #if ENABLE_SPECIALIZATION
if (this_instr->op.code == JUMP_BACKWARD) {
uint8_t desired = tstate->interp->jit ? JUMP_BACKWARD_JIT : JUMP_BACKWARD_NO_JIT;
FT_ATOMIC_STORE_UINT8_RELAXED(this_instr->op.code, desired);
@@ -7645,30 +7649,20 @@
{
#ifdef _Py_TIER2
_Py_BackoffCounter counter = this_instr[1].counter;
- if (backoff_counter_triggers(counter) && this_instr->op.code == JUMP_BACKWARD_JIT) {
- _Py_CODEUNIT *start = this_instr;
+ if (!IS_JIT_TRACING() && backoff_counter_triggers(counter) &&
+ this_instr->op.code == JUMP_BACKWARD_JIT &&
+ next_instr->op.code != ENTER_EXECUTOR) {
+ _Py_CODEUNIT *insert_exec_at = this_instr;
while (oparg > 255) {
oparg >>= 8;
- start--;
+ insert_exec_at--;
}
- _PyExecutorObject *executor;
- _PyFrame_SetStackPointer(frame, stack_pointer);
- int optimized = _PyOptimizer_Optimize(frame, start, &executor, 0);
- stack_pointer = _PyFrame_GetStackPointer(frame);
- if (optimized <= 0) {
- this_instr[1].counter = restart_backoff_counter(counter);
- if (optimized < 0) {
- JUMP_TO_LABEL(error);
- }
+ int succ = _PyJit_TryInitializeTracing(tstate, frame, this_instr, insert_exec_at, next_instr, STACK_LEVEL(), 0, NULL, oparg);
+ if (succ) {
+ ENTER_TRACING();
}
else {
- _PyFrame_SetStackPointer(frame, stack_pointer);
- this_instr[1].counter = initial_jump_backoff_counter();
- stack_pointer = _PyFrame_GetStackPointer(frame);
- assert(tstate->current_executor == NULL);
- assert(executor != tstate->interp->cold_executor);
- tstate->jit_exit = NULL;
- TIER1_TO_TIER2(executor);
+ this_instr[1].counter = restart_backoff_counter(counter);
}
}
else {
@@ -12265,5 +12259,75 @@ JUMP_TO_LABEL(error);
DISPATCH();
}
+ LABEL(record_previous_inst)
+ {
+ #if _Py_TIER2
+ assert(IS_JIT_TRACING());
+ int opcode = next_instr->op.code;
+ bool stop_tracing = (opcode == WITH_EXCEPT_START ||
+ opcode == RERAISE || opcode == CLEANUP_THROW ||
+ opcode == PUSH_EXC_INFO || opcode == INTERPRETER_EXIT);
+ _PyFrame_SetStackPointer(frame, stack_pointer);
+ int full = !_PyJit_translate_single_bytecode_to_trace(tstate, frame, next_instr, stop_tracing);
+ stack_pointer = _PyFrame_GetStackPointer(frame);
+ if (full) {
+ LEAVE_TRACING();
+ _PyFrame_SetStackPointer(frame, stack_pointer);
+ int err = stop_tracing_and_jit(tstate, frame);
+ stack_pointer = _PyFrame_GetStackPointer(frame);
+ if (err < 0) {
+ JUMP_TO_LABEL(error);
+ }
+ DISPATCH_GOTO_NON_TRACING();
+ }
+ _PyThreadStateImpl *_tstate = (_PyThreadStateImpl *)tstate;
+ if ((_tstate->jit_tracer_state.prev_state.instr->op.code == CALL_LIST_APPEND &&
+ opcode == POP_TOP) ||
+ (_tstate->jit_tracer_state.prev_state.instr->op.code == BINARY_OP_INPLACE_ADD_UNICODE &&
+ opcode == STORE_FAST)) {
+ _tstate->jit_tracer_state.prev_state.instr_is_super = true;
+ }
+ else {
+ _tstate->jit_tracer_state.prev_state.instr = next_instr;
+ }
+ PyObject *prev_code = PyStackRef_AsPyObjectBorrow(frame->f_executable);
+ if (_tstate->jit_tracer_state.prev_state.instr_code != (PyCodeObject *)prev_code) {
+ _PyFrame_SetStackPointer(frame, stack_pointer);
+ Py_SETREF(_tstate->jit_tracer_state.prev_state.instr_code, (PyCodeObject*)Py_NewRef((prev_code)));
+ stack_pointer = _PyFrame_GetStackPointer(frame);
+ }
+ _tstate->jit_tracer_state.prev_state.instr_frame = frame;
+ _tstate->jit_tracer_state.prev_state.instr_oparg = oparg;
+ _tstate->jit_tracer_state.prev_state.instr_stacklevel = PyStackRef_IsNone(frame->f_executable) ? 2 : STACK_LEVEL();
+ if (_PyOpcode_Caches[_PyOpcode_Deopt[opcode]]) {
+ (&next_instr[1])->counter = trigger_backoff_counter();
+ }
+ DISPATCH_GOTO_NON_TRACING();
+ #else
+ Py_FatalError("JIT label executed in non-jit build.");
+ #endif
+ }
+
+ LABEL(stop_tracing)
+ {
+ #if _Py_TIER2
+ assert(IS_JIT_TRACING());
+ int opcode = next_instr->op.code;
+ _PyFrame_SetStackPointer(frame, stack_pointer);
+ _PyJit_translate_single_bytecode_to_trace(tstate, frame, NULL, true);
+ stack_pointer = _PyFrame_GetStackPointer(frame);
+ LEAVE_TRACING();
+ _PyFrame_SetStackPointer(frame, stack_pointer);
+ int err = stop_tracing_and_jit(tstate, frame);
+ stack_pointer = _PyFrame_GetStackPointer(frame);
+ if (err < 0) {
+ JUMP_TO_LABEL(error);
+ }
+ DISPATCH_GOTO_NON_TRACING();
+ #else
+ Py_FatalError("JIT label executed in non-jit build.");
+ #endif
+ }
+
/* END LABELS */
#undef TIER_ONE
diff --git a/Python/instrumentation.c b/Python/instrumentation.c
index b4b2bc5dc69..81e46a331e0 100644
--- a/Python/instrumentation.c
+++ b/Python/instrumentation.c
@@ -18,6 +18,7 @@
#include "pycore_tuple.h" // _PyTuple_FromArraySteal()
#include "opcode_ids.h"
+#include "pycore_optimizer.h"
/* Uncomment this to dump debugging output when assertions fail */
@@ -1785,6 +1786,7 @@ force_instrument_lock_held(PyCodeObject *code, PyInterpreterState *interp)
_PyCode_Clear_Executors(code);
}
_Py_Executors_InvalidateDependency(interp, code, 1);
+ _PyJit_Tracer_InvalidateDependency(PyThreadState_GET(), code);
#endif
int code_len = (int)Py_SIZE(code);
/* Exit early to avoid creating instrumentation
diff --git a/Python/jit.c b/Python/jit.c
index 279e1ce6a0d..7ab0f8ddd43 100644
--- a/Python/jit.c
+++ b/Python/jit.c
@@ -604,7 +604,7 @@ _PyJIT_Compile(_PyExecutorObject *executor, const _PyUOpInstruction trace[], siz
unsigned char *code = memory;
state.trampolines.mem = memory + code_size;
unsigned char *data = memory + code_size + state.trampolines.size + code_padding;
- assert(trace[0].opcode == _START_EXECUTOR || trace[0].opcode == _COLD_EXIT);
+ assert(trace[0].opcode == _START_EXECUTOR || trace[0].opcode == _COLD_EXIT || trace[0].opcode == _COLD_DYNAMIC_EXIT);
for (size_t i = 0; i < length; i++) {
const _PyUOpInstruction *instruction = &trace[i];
group = &stencil_groups[instruction->opcode];
diff --git a/Python/opcode_targets.h b/Python/opcode_targets.h
index 6dd443e1655..1b9196503b5 100644
--- a/Python/opcode_targets.h
+++ b/Python/opcode_targets.h
@@ -257,8 +257,270 @@ static void *opcode_targets_table[256] = {
&&TARGET_INSTRUMENTED_LINE,
&&TARGET_ENTER_EXECUTOR,
};
+#if _Py_TIER2
+static void *opcode_tracing_targets_table[256] = {
+ &&record_previous_inst,
+ &&record_previous_inst,
+ &&record_previous_inst,
+ &&record_previous_inst,
+ &&record_previous_inst,
+ &&record_previous_inst,
+ &&record_previous_inst,
+ &&record_previous_inst,
+ &&record_previous_inst,
+ &&record_previous_inst,
+ &&record_previous_inst,
+ &&record_previous_inst,
+ &&record_previous_inst,
+ &&record_previous_inst,
+ &&record_previous_inst,
+ &&record_previous_inst,
+ &&record_previous_inst,
+ &&record_previous_inst,
+ &&record_previous_inst,
+ &&record_previous_inst,
+ &&record_previous_inst,
+ &&record_previous_inst,
+ &&record_previous_inst,
+ &&record_previous_inst,
+ &&record_previous_inst,
+ &&record_previous_inst,
+ &&record_previous_inst,
+ &&record_previous_inst,
+ &&record_previous_inst,
+ &&record_previous_inst,
+ &&record_previous_inst,
+ &&record_previous_inst,
+ &&record_previous_inst,
+ &&record_previous_inst,
+ &&record_previous_inst,
+ &&record_previous_inst,
+ &&record_previous_inst,
+ &&record_previous_inst,
+ &&record_previous_inst,
+ &&record_previous_inst,
+ &&record_previous_inst,
+ &&record_previous_inst,
+ &&record_previous_inst,
+ &&record_previous_inst,
+ &&record_previous_inst,
+ &&record_previous_inst,
+ &&record_previous_inst,
+ &&record_previous_inst,
+ &&record_previous_inst,
+ &&record_previous_inst,
+ &&record_previous_inst,
+ &&record_previous_inst,
+ &&record_previous_inst,
+ &&record_previous_inst,
+ &&record_previous_inst,
+ &&record_previous_inst,
+ &&record_previous_inst,
+ &&record_previous_inst,
+ &&record_previous_inst,
+ &&record_previous_inst,
+ &&record_previous_inst,
+ &&record_previous_inst,
+ &&record_previous_inst,
+ &&record_previous_inst,
+ &&record_previous_inst,
+ &&record_previous_inst,
+ &&record_previous_inst,
+ &&record_previous_inst,
+ &&record_previous_inst,
+ &&record_previous_inst,
+ &&record_previous_inst,
+ &&record_previous_inst,
+ &&record_previous_inst,
+ &&record_previous_inst,
+ &&record_previous_inst,
+ &&record_previous_inst,
+ &&record_previous_inst,
+ &&record_previous_inst,
+ &&record_previous_inst,
+ &&record_previous_inst,
+ &&record_previous_inst,
+ &&record_previous_inst,
+ &&record_previous_inst,
+ &&record_previous_inst,
+ &&record_previous_inst,
+ &&record_previous_inst,
+ &&record_previous_inst,
+ &&record_previous_inst,
+ &&record_previous_inst,
+ &&record_previous_inst,
+ &&record_previous_inst,
+ &&record_previous_inst,
+ &&record_previous_inst,
+ &&record_previous_inst,
+ &&record_previous_inst,
+ &&record_previous_inst,
+ &&record_previous_inst,
+ &&record_previous_inst,
+ &&record_previous_inst,
+ &&record_previous_inst,
+ &&record_previous_inst,
+ &&record_previous_inst,
+ &&record_previous_inst,
+ &&record_previous_inst,
+ &&record_previous_inst,
+ &&record_previous_inst,
+ &&record_previous_inst,
+ &&record_previous_inst,
+ &&record_previous_inst,
+ &&record_previous_inst,
+ &&record_previous_inst,
+ &&record_previous_inst,
+ &&record_previous_inst,
+ &&record_previous_inst,
+ &&record_previous_inst,
+ &&record_previous_inst,
+ &&record_previous_inst,
+ &&record_previous_inst,
+ &&record_previous_inst,
+ &&record_previous_inst,
+ &&record_previous_inst,
+ &&_unknown_opcode,
+ &&_unknown_opcode,
+ &&_unknown_opcode,
+ &&_unknown_opcode,
+ &&_unknown_opcode,
+ &&_unknown_opcode,
+ &&_unknown_opcode,
+ &&record_previous_inst,
+ &&record_previous_inst,
+ &&record_previous_inst,
+ &&record_previous_inst,
+ &&record_previous_inst,
+ &&record_previous_inst,
+ &&record_previous_inst,
+ &&record_previous_inst,
+ &&record_previous_inst,
+ &&record_previous_inst,
+ &&record_previous_inst,
+ &&record_previous_inst,
+ &&record_previous_inst,
+ &&record_previous_inst,
+ &&record_previous_inst,
+ &&record_previous_inst,
+ &&record_previous_inst,
+ &&record_previous_inst,
+ &&record_previous_inst,
+ &&record_previous_inst,
+ &&record_previous_inst,
+ &&record_previous_inst,
+ &&record_previous_inst,
+ &&record_previous_inst,
+ &&record_previous_inst,
+ &&record_previous_inst,
+ &&record_previous_inst,
+ &&record_previous_inst,
+ &&record_previous_inst,
+ &&record_previous_inst,
+ &&record_previous_inst,
+ &&record_previous_inst,
+ &&record_previous_inst,
+ &&record_previous_inst,
+ &&record_previous_inst,
+ &&record_previous_inst,
+ &&record_previous_inst,
+ &&record_previous_inst,
+ &&record_previous_inst,
+ &&record_previous_inst,
+ &&record_previous_inst,
+ &&record_previous_inst,
+ &&record_previous_inst,
+ &&record_previous_inst,
+ &&record_previous_inst,
+ &&record_previous_inst,
+ &&record_previous_inst,
+ &&record_previous_inst,
+ &&record_previous_inst,
+ &&record_previous_inst,
+ &&record_previous_inst,
+ &&record_previous_inst,
+ &&record_previous_inst,
+ &&record_previous_inst,
+ &&record_previous_inst,
+ &&record_previous_inst,
+ &&record_previous_inst,
+ &&record_previous_inst,
+ &&record_previous_inst,
+ &&record_previous_inst,
+ &&record_previous_inst,
+ &&record_previous_inst,
+ &&record_previous_inst,
+ &&record_previous_inst,
+ &&record_previous_inst,
+ &&record_previous_inst,
+ &&record_previous_inst,
+ &&record_previous_inst,
+ &&record_previous_inst,
+ &&record_previous_inst,
+ &&record_previous_inst,
+ &&record_previous_inst,
+ &&record_previous_inst,
+ &&record_previous_inst,
+ &&record_previous_inst,
+ &&record_previous_inst,
+ &&record_previous_inst,
+ &&record_previous_inst,
+ &&record_previous_inst,
+ &&record_previous_inst,
+ &&record_previous_inst,
+ &&record_previous_inst,
+ &&_unknown_opcode,
+ &&_unknown_opcode,
+ &&_unknown_opcode,
+ &&_unknown_opcode,
+ &&_unknown_opcode,
+ &&_unknown_opcode,
+ &&_unknown_opcode,
+ &&_unknown_opcode,
+ &&_unknown_opcode,
+ &&_unknown_opcode,
+ &&_unknown_opcode,
+ &&_unknown_opcode,
+ &&_unknown_opcode,
+ &&_unknown_opcode,
+ &&_unknown_opcode,
+ &&_unknown_opcode,
+ &&_unknown_opcode,
+ &&_unknown_opcode,
+ &&_unknown_opcode,
+ &&_unknown_opcode,
+ &&_unknown_opcode,
+ &&_unknown_opcode,
+ &&_unknown_opcode,
+ &&_unknown_opcode,
+ &&record_previous_inst,
+ &&record_previous_inst,
+ &&record_previous_inst,
+ &&record_previous_inst,
+ &&record_previous_inst,
+ &&record_previous_inst,
+ &&record_previous_inst,
+ &&record_previous_inst,
+ &&record_previous_inst,
+ &&record_previous_inst,
+ &&record_previous_inst,
+ &&record_previous_inst,
+ &&record_previous_inst,
+ &&record_previous_inst,
+ &&record_previous_inst,
+ &&record_previous_inst,
+ &&record_previous_inst,
+ &&record_previous_inst,
+ &&record_previous_inst,
+ &&record_previous_inst,
+ &&record_previous_inst,
+ &&record_previous_inst,
+};
+#endif
#else /* _Py_TAIL_CALL_INTERP */
-static py_tail_call_funcptr instruction_funcptr_table[256];
+static py_tail_call_funcptr instruction_funcptr_handler_table[256];
+
+static py_tail_call_funcptr instruction_funcptr_tracing_table[256];
Py_PRESERVE_NONE_CC static PyObject *_TAIL_CALL_pop_2_error(TAIL_CALL_PARAMS);
Py_PRESERVE_NONE_CC static PyObject *_TAIL_CALL_pop_1_error(TAIL_CALL_PARAMS);
@@ -266,6 +528,8 @@ Py_PRESERVE_NONE_CC static PyObject *_TAIL_CALL_error(TAIL_CALL_PARAMS);
Py_PRESERVE_NONE_CC static PyObject *_TAIL_CALL_exception_unwind(TAIL_CALL_PARAMS);
Py_PRESERVE_NONE_CC static PyObject *_TAIL_CALL_exit_unwind(TAIL_CALL_PARAMS);
Py_PRESERVE_NONE_CC static PyObject *_TAIL_CALL_start_frame(TAIL_CALL_PARAMS);
+Py_PRESERVE_NONE_CC static PyObject *_TAIL_CALL_record_previous_inst(TAIL_CALL_PARAMS);
+Py_PRESERVE_NONE_CC static PyObject *_TAIL_CALL_stop_tracing(TAIL_CALL_PARAMS);
Py_PRESERVE_NONE_CC static PyObject *_TAIL_CALL_BINARY_OP(TAIL_CALL_PARAMS);
Py_PRESERVE_NONE_CC static PyObject *_TAIL_CALL_BINARY_OP_ADD_FLOAT(TAIL_CALL_PARAMS);
@@ -503,7 +767,7 @@ Py_PRESERVE_NONE_CC static PyObject *_TAIL_CALL_UNKNOWN_OPCODE(TAIL_CALL_PARAMS)
JUMP_TO_LABEL(error);
}
-static py_tail_call_funcptr instruction_funcptr_table[256] = {
+static py_tail_call_funcptr instruction_funcptr_handler_table[256] = {
[BINARY_OP] = _TAIL_CALL_BINARY_OP,
[BINARY_OP_ADD_FLOAT] = _TAIL_CALL_BINARY_OP_ADD_FLOAT,
[BINARY_OP_ADD_INT] = _TAIL_CALL_BINARY_OP_ADD_INT,
@@ -761,4 +1025,262 @@ static py_tail_call_funcptr instruction_funcptr_table[256] = {
[232] = _TAIL_CALL_UNKNOWN_OPCODE,
[233] = _TAIL_CALL_UNKNOWN_OPCODE,
};
+static py_tail_call_funcptr instruction_funcptr_tracing_table[256] = {
+ [BINARY_OP] = _TAIL_CALL_record_previous_inst,
+ [BINARY_OP_ADD_FLOAT] = _TAIL_CALL_record_previous_inst,
+ [BINARY_OP_ADD_INT] = _TAIL_CALL_record_previous_inst,
+ [BINARY_OP_ADD_UNICODE] = _TAIL_CALL_record_previous_inst,
+ [BINARY_OP_EXTEND] = _TAIL_CALL_record_previous_inst,
+ [BINARY_OP_INPLACE_ADD_UNICODE] = _TAIL_CALL_record_previous_inst,
+ [BINARY_OP_MULTIPLY_FLOAT] = _TAIL_CALL_record_previous_inst,
+ [BINARY_OP_MULTIPLY_INT] = _TAIL_CALL_record_previous_inst,
+ [BINARY_OP_SUBSCR_DICT] = _TAIL_CALL_record_previous_inst,
+ [BINARY_OP_SUBSCR_GETITEM] = _TAIL_CALL_record_previous_inst,
+ [BINARY_OP_SUBSCR_LIST_INT] = _TAIL_CALL_record_previous_inst,
+ [BINARY_OP_SUBSCR_LIST_SLICE] = _TAIL_CALL_record_previous_inst,
+ [BINARY_OP_SUBSCR_STR_INT] = _TAIL_CALL_record_previous_inst,
+ [BINARY_OP_SUBSCR_TUPLE_INT] = _TAIL_CALL_record_previous_inst,
+ [BINARY_OP_SUBTRACT_FLOAT] = _TAIL_CALL_record_previous_inst,
+ [BINARY_OP_SUBTRACT_INT] = _TAIL_CALL_record_previous_inst,
+ [BINARY_SLICE] = _TAIL_CALL_record_previous_inst,
+ [BUILD_INTERPOLATION] = _TAIL_CALL_record_previous_inst,
+ [BUILD_LIST] = _TAIL_CALL_record_previous_inst,
+ [BUILD_MAP] = _TAIL_CALL_record_previous_inst,
+ [BUILD_SET] = _TAIL_CALL_record_previous_inst,
+ [BUILD_SLICE] = _TAIL_CALL_record_previous_inst,
+ [BUILD_STRING] = _TAIL_CALL_record_previous_inst,
+ [BUILD_TEMPLATE] = _TAIL_CALL_record_previous_inst,
+ [BUILD_TUPLE] = _TAIL_CALL_record_previous_inst,
+ [CACHE] = _TAIL_CALL_record_previous_inst,
+ [CALL] = _TAIL_CALL_record_previous_inst,
+ [CALL_ALLOC_AND_ENTER_INIT] = _TAIL_CALL_record_previous_inst,
+ [CALL_BOUND_METHOD_EXACT_ARGS] = _TAIL_CALL_record_previous_inst,
+ [CALL_BOUND_METHOD_GENERAL] = _TAIL_CALL_record_previous_inst,
+ [CALL_BUILTIN_CLASS] = _TAIL_CALL_record_previous_inst,
+ [CALL_BUILTIN_FAST] = _TAIL_CALL_record_previous_inst,
+ [CALL_BUILTIN_FAST_WITH_KEYWORDS] = _TAIL_CALL_record_previous_inst,
+ [CALL_BUILTIN_O] = _TAIL_CALL_record_previous_inst,
+ [CALL_FUNCTION_EX] = _TAIL_CALL_record_previous_inst,
+ [CALL_INTRINSIC_1] = _TAIL_CALL_record_previous_inst,
+ [CALL_INTRINSIC_2] = _TAIL_CALL_record_previous_inst,
+ [CALL_ISINSTANCE] = _TAIL_CALL_record_previous_inst,
+ [CALL_KW] = _TAIL_CALL_record_previous_inst,
+ [CALL_KW_BOUND_METHOD] = _TAIL_CALL_record_previous_inst,
+ [CALL_KW_NON_PY] = _TAIL_CALL_record_previous_inst,
+ [CALL_KW_PY] = _TAIL_CALL_record_previous_inst,
+ [CALL_LEN] = _TAIL_CALL_record_previous_inst,
+ [CALL_LIST_APPEND] = _TAIL_CALL_record_previous_inst,
+ [CALL_METHOD_DESCRIPTOR_FAST] = _TAIL_CALL_record_previous_inst,
+ [CALL_METHOD_DESCRIPTOR_FAST_WITH_KEYWORDS] = _TAIL_CALL_record_previous_inst,
+ [CALL_METHOD_DESCRIPTOR_NOARGS] = _TAIL_CALL_record_previous_inst,
+ [CALL_METHOD_DESCRIPTOR_O] = _TAIL_CALL_record_previous_inst,
+ [CALL_NON_PY_GENERAL] = _TAIL_CALL_record_previous_inst,
+ [CALL_PY_EXACT_ARGS] = _TAIL_CALL_record_previous_inst,
+ [CALL_PY_GENERAL] = _TAIL_CALL_record_previous_inst,
+ [CALL_STR_1] = _TAIL_CALL_record_previous_inst,
+ [CALL_TUPLE_1] = _TAIL_CALL_record_previous_inst,
+ [CALL_TYPE_1] = _TAIL_CALL_record_previous_inst,
+ [CHECK_EG_MATCH] = _TAIL_CALL_record_previous_inst,
+ [CHECK_EXC_MATCH] = _TAIL_CALL_record_previous_inst,
+ [CLEANUP_THROW] = _TAIL_CALL_record_previous_inst,
+ [COMPARE_OP] = _TAIL_CALL_record_previous_inst,
+ [COMPARE_OP_FLOAT] = _TAIL_CALL_record_previous_inst,
+ [COMPARE_OP_INT] = _TAIL_CALL_record_previous_inst,
+ [COMPARE_OP_STR] = _TAIL_CALL_record_previous_inst,
+ [CONTAINS_OP] = _TAIL_CALL_record_previous_inst,
+ [CONTAINS_OP_DICT] = _TAIL_CALL_record_previous_inst,
+ [CONTAINS_OP_SET] = _TAIL_CALL_record_previous_inst,
+ [CONVERT_VALUE] = _TAIL_CALL_record_previous_inst,
+ [COPY] = _TAIL_CALL_record_previous_inst,
+ [COPY_FREE_VARS] = _TAIL_CALL_record_previous_inst,
+ [DELETE_ATTR] = _TAIL_CALL_record_previous_inst,
+ [DELETE_DEREF] = _TAIL_CALL_record_previous_inst,
+ [DELETE_FAST] = _TAIL_CALL_record_previous_inst,
+ [DELETE_GLOBAL] = _TAIL_CALL_record_previous_inst,
+ [DELETE_NAME] = _TAIL_CALL_record_previous_inst,
+ [DELETE_SUBSCR] = _TAIL_CALL_record_previous_inst,
+ [DICT_MERGE] = _TAIL_CALL_record_previous_inst,
+ [DICT_UPDATE] = _TAIL_CALL_record_previous_inst,
+ [END_ASYNC_FOR] = _TAIL_CALL_record_previous_inst,
+ [END_FOR] = _TAIL_CALL_record_previous_inst,
+ [END_SEND] = _TAIL_CALL_record_previous_inst,
+ [ENTER_EXECUTOR] = _TAIL_CALL_record_previous_inst,
+ [EXIT_INIT_CHECK] = _TAIL_CALL_record_previous_inst,
+ [EXTENDED_ARG] = _TAIL_CALL_record_previous_inst,
+ [FORMAT_SIMPLE] = _TAIL_CALL_record_previous_inst,
+ [FORMAT_WITH_SPEC] = _TAIL_CALL_record_previous_inst,
+ [FOR_ITER] = _TAIL_CALL_record_previous_inst,
+ [FOR_ITER_GEN] = _TAIL_CALL_record_previous_inst,
+ [FOR_ITER_LIST] = _TAIL_CALL_record_previous_inst,
+ [FOR_ITER_RANGE] = _TAIL_CALL_record_previous_inst,
+ [FOR_ITER_TUPLE] = _TAIL_CALL_record_previous_inst,
+ [GET_AITER] = _TAIL_CALL_record_previous_inst,
+ [GET_ANEXT] = _TAIL_CALL_record_previous_inst,
+ [GET_AWAITABLE] = _TAIL_CALL_record_previous_inst,
+ [GET_ITER] = _TAIL_CALL_record_previous_inst,
+ [GET_LEN] = _TAIL_CALL_record_previous_inst,
+ [GET_YIELD_FROM_ITER] = _TAIL_CALL_record_previous_inst,
+ [IMPORT_FROM] = _TAIL_CALL_record_previous_inst,
+ [IMPORT_NAME] = _TAIL_CALL_record_previous_inst,
+ [INSTRUMENTED_CALL] = _TAIL_CALL_record_previous_inst,
+ [INSTRUMENTED_CALL_FUNCTION_EX] = _TAIL_CALL_record_previous_inst,
+ [INSTRUMENTED_CALL_KW] = _TAIL_CALL_record_previous_inst,
+ [INSTRUMENTED_END_ASYNC_FOR] = _TAIL_CALL_record_previous_inst,
+ [INSTRUMENTED_END_FOR] = _TAIL_CALL_record_previous_inst,
+ [INSTRUMENTED_END_SEND] = _TAIL_CALL_record_previous_inst,
+ [INSTRUMENTED_FOR_ITER] = _TAIL_CALL_record_previous_inst,
+ [INSTRUMENTED_INSTRUCTION] = _TAIL_CALL_record_previous_inst,
+ [INSTRUMENTED_JUMP_BACKWARD] = _TAIL_CALL_record_previous_inst,
+ [INSTRUMENTED_JUMP_FORWARD] = _TAIL_CALL_record_previous_inst,
+ [INSTRUMENTED_LINE] = _TAIL_CALL_record_previous_inst,
+ [INSTRUMENTED_LOAD_SUPER_ATTR] = _TAIL_CALL_record_previous_inst,
+ [INSTRUMENTED_NOT_TAKEN] = _TAIL_CALL_record_previous_inst,
+ [INSTRUMENTED_POP_ITER] = _TAIL_CALL_record_previous_inst,
+ [INSTRUMENTED_POP_JUMP_IF_FALSE] = _TAIL_CALL_record_previous_inst,
+ [INSTRUMENTED_POP_JUMP_IF_NONE] = _TAIL_CALL_record_previous_inst,
+ [INSTRUMENTED_POP_JUMP_IF_NOT_NONE] = _TAIL_CALL_record_previous_inst,
+ [INSTRUMENTED_POP_JUMP_IF_TRUE] = _TAIL_CALL_record_previous_inst,
+ [INSTRUMENTED_RESUME] = _TAIL_CALL_record_previous_inst,
+ [INSTRUMENTED_RETURN_VALUE] = _TAIL_CALL_record_previous_inst,
+ [INSTRUMENTED_YIELD_VALUE] = _TAIL_CALL_record_previous_inst,
+ [INTERPRETER_EXIT] = _TAIL_CALL_record_previous_inst,
+ [IS_OP] = _TAIL_CALL_record_previous_inst,
+ [JUMP_BACKWARD] = _TAIL_CALL_record_previous_inst,
+ [JUMP_BACKWARD_JIT] = _TAIL_CALL_record_previous_inst,
+ [JUMP_BACKWARD_NO_INTERRUPT] = _TAIL_CALL_record_previous_inst,
+ [JUMP_BACKWARD_NO_JIT] = _TAIL_CALL_record_previous_inst,
+ [JUMP_FORWARD] = _TAIL_CALL_record_previous_inst,
+ [LIST_APPEND] = _TAIL_CALL_record_previous_inst,
+ [LIST_EXTEND] = _TAIL_CALL_record_previous_inst,
+ [LOAD_ATTR] = _TAIL_CALL_record_previous_inst,
+ [LOAD_ATTR_CLASS] = _TAIL_CALL_record_previous_inst,
+ [LOAD_ATTR_CLASS_WITH_METACLASS_CHECK] = _TAIL_CALL_record_previous_inst,
+ [LOAD_ATTR_GETATTRIBUTE_OVERRIDDEN] = _TAIL_CALL_record_previous_inst,
+ [LOAD_ATTR_INSTANCE_VALUE] = _TAIL_CALL_record_previous_inst,
+ [LOAD_ATTR_METHOD_LAZY_DICT] = _TAIL_CALL_record_previous_inst,
+ [LOAD_ATTR_METHOD_NO_DICT] = _TAIL_CALL_record_previous_inst,
+ [LOAD_ATTR_METHOD_WITH_VALUES] = _TAIL_CALL_record_previous_inst,
+ [LOAD_ATTR_MODULE] = _TAIL_CALL_record_previous_inst,
+ [LOAD_ATTR_NONDESCRIPTOR_NO_DICT] = _TAIL_CALL_record_previous_inst,
+ [LOAD_ATTR_NONDESCRIPTOR_WITH_VALUES] = _TAIL_CALL_record_previous_inst,
+ [LOAD_ATTR_PROPERTY] = _TAIL_CALL_record_previous_inst,
+ [LOAD_ATTR_SLOT] = _TAIL_CALL_record_previous_inst,
+ [LOAD_ATTR_WITH_HINT] = _TAIL_CALL_record_previous_inst,
+ [LOAD_BUILD_CLASS] = _TAIL_CALL_record_previous_inst,
+ [LOAD_COMMON_CONSTANT] = _TAIL_CALL_record_previous_inst,
+ [LOAD_CONST] = _TAIL_CALL_record_previous_inst,
+ [LOAD_DEREF] = _TAIL_CALL_record_previous_inst,
+ [LOAD_FAST] = _TAIL_CALL_record_previous_inst,
+ [LOAD_FAST_AND_CLEAR] = _TAIL_CALL_record_previous_inst,
+ [LOAD_FAST_BORROW] = _TAIL_CALL_record_previous_inst,
+ [LOAD_FAST_BORROW_LOAD_FAST_BORROW] = _TAIL_CALL_record_previous_inst,
+ [LOAD_FAST_CHECK] = _TAIL_CALL_record_previous_inst,
+ [LOAD_FAST_LOAD_FAST] = _TAIL_CALL_record_previous_inst,
+ [LOAD_FROM_DICT_OR_DEREF] = _TAIL_CALL_record_previous_inst,
+ [LOAD_FROM_DICT_OR_GLOBALS] = _TAIL_CALL_record_previous_inst,
+ [LOAD_GLOBAL] = _TAIL_CALL_record_previous_inst,
+ [LOAD_GLOBAL_BUILTIN] = _TAIL_CALL_record_previous_inst,
+ [LOAD_GLOBAL_MODULE] = _TAIL_CALL_record_previous_inst,
+ [LOAD_LOCALS] = _TAIL_CALL_record_previous_inst,
+ [LOAD_NAME] = _TAIL_CALL_record_previous_inst,
+ [LOAD_SMALL_INT] = _TAIL_CALL_record_previous_inst,
+ [LOAD_SPECIAL] = _TAIL_CALL_record_previous_inst,
+ [LOAD_SUPER_ATTR] = _TAIL_CALL_record_previous_inst,
+ [LOAD_SUPER_ATTR_ATTR] = _TAIL_CALL_record_previous_inst,
+ [LOAD_SUPER_ATTR_METHOD] = _TAIL_CALL_record_previous_inst,
+ [MAKE_CELL] = _TAIL_CALL_record_previous_inst,
+ [MAKE_FUNCTION] = _TAIL_CALL_record_previous_inst,
+ [MAP_ADD] = _TAIL_CALL_record_previous_inst,
+ [MATCH_CLASS] = _TAIL_CALL_record_previous_inst,
+ [MATCH_KEYS] = _TAIL_CALL_record_previous_inst,
+ [MATCH_MAPPING] = _TAIL_CALL_record_previous_inst,
+ [MATCH_SEQUENCE] = _TAIL_CALL_record_previous_inst,
+ [NOP] = _TAIL_CALL_record_previous_inst,
+ [NOT_TAKEN] = _TAIL_CALL_record_previous_inst,
+ [POP_EXCEPT] = _TAIL_CALL_record_previous_inst,
+ [POP_ITER] = _TAIL_CALL_record_previous_inst,
+ [POP_JUMP_IF_FALSE] = _TAIL_CALL_record_previous_inst,
+ [POP_JUMP_IF_NONE] = _TAIL_CALL_record_previous_inst,
+ [POP_JUMP_IF_NOT_NONE] = _TAIL_CALL_record_previous_inst,
+ [POP_JUMP_IF_TRUE] = _TAIL_CALL_record_previous_inst,
+ [POP_TOP] = _TAIL_CALL_record_previous_inst,
+ [PUSH_EXC_INFO] = _TAIL_CALL_record_previous_inst,
+ [PUSH_NULL] = _TAIL_CALL_record_previous_inst,
+ [RAISE_VARARGS] = _TAIL_CALL_record_previous_inst,
+ [RERAISE] = _TAIL_CALL_record_previous_inst,
+ [RESERVED] = _TAIL_CALL_record_previous_inst,
+ [RESUME] = _TAIL_CALL_record_previous_inst,
+ [RESUME_CHECK] = _TAIL_CALL_record_previous_inst,
+ [RETURN_GENERATOR] = _TAIL_CALL_record_previous_inst,
+ [RETURN_VALUE] = _TAIL_CALL_record_previous_inst,
+ [SEND] = _TAIL_CALL_record_previous_inst,
+ [SEND_GEN] = _TAIL_CALL_record_previous_inst,
+ [SETUP_ANNOTATIONS] = _TAIL_CALL_record_previous_inst,
+ [SET_ADD] = _TAIL_CALL_record_previous_inst,
+ [SET_FUNCTION_ATTRIBUTE] = _TAIL_CALL_record_previous_inst,
+ [SET_UPDATE] = _TAIL_CALL_record_previous_inst,
+ [STORE_ATTR] = _TAIL_CALL_record_previous_inst,
+ [STORE_ATTR_INSTANCE_VALUE] = _TAIL_CALL_record_previous_inst,
+ [STORE_ATTR_SLOT] = _TAIL_CALL_record_previous_inst,
+ [STORE_ATTR_WITH_HINT] = _TAIL_CALL_record_previous_inst,
+ [STORE_DEREF] = _TAIL_CALL_record_previous_inst,
+ [STORE_FAST] = _TAIL_CALL_record_previous_inst,
+ [STORE_FAST_LOAD_FAST] = _TAIL_CALL_record_previous_inst,
+ [STORE_FAST_STORE_FAST] = _TAIL_CALL_record_previous_inst,
+ [STORE_GLOBAL] = _TAIL_CALL_record_previous_inst,
+ [STORE_NAME] = _TAIL_CALL_record_previous_inst,
+ [STORE_SLICE] = _TAIL_CALL_record_previous_inst,
+ [STORE_SUBSCR] = _TAIL_CALL_record_previous_inst,
+ [STORE_SUBSCR_DICT] = _TAIL_CALL_record_previous_inst,
+ [STORE_SUBSCR_LIST_INT] = _TAIL_CALL_record_previous_inst,
+ [SWAP] = _TAIL_CALL_record_previous_inst,
+ [TO_BOOL] = _TAIL_CALL_record_previous_inst,
+ [TO_BOOL_ALWAYS_TRUE] = _TAIL_CALL_record_previous_inst,
+ [TO_BOOL_BOOL] = _TAIL_CALL_record_previous_inst,
+ [TO_BOOL_INT] = _TAIL_CALL_record_previous_inst,
+ [TO_BOOL_LIST] = _TAIL_CALL_record_previous_inst,
+ [TO_BOOL_NONE] = _TAIL_CALL_record_previous_inst,
+ [TO_BOOL_STR] = _TAIL_CALL_record_previous_inst,
+ [UNARY_INVERT] = _TAIL_CALL_record_previous_inst,
+ [UNARY_NEGATIVE] = _TAIL_CALL_record_previous_inst,
+ [UNARY_NOT] = _TAIL_CALL_record_previous_inst,
+ [UNPACK_EX] = _TAIL_CALL_record_previous_inst,
+ [UNPACK_SEQUENCE] = _TAIL_CALL_record_previous_inst,
+ [UNPACK_SEQUENCE_LIST] = _TAIL_CALL_record_previous_inst,
+ [UNPACK_SEQUENCE_TUPLE] = _TAIL_CALL_record_previous_inst,
+ [UNPACK_SEQUENCE_TWO_TUPLE] = _TAIL_CALL_record_previous_inst,
+ [WITH_EXCEPT_START] = _TAIL_CALL_record_previous_inst,
+ [YIELD_VALUE] = _TAIL_CALL_record_previous_inst,
+ [121] = _TAIL_CALL_UNKNOWN_OPCODE,
+ [122] = _TAIL_CALL_UNKNOWN_OPCODE,
+ [123] = _TAIL_CALL_UNKNOWN_OPCODE,
+ [124] = _TAIL_CALL_UNKNOWN_OPCODE,
+ [125] = _TAIL_CALL_UNKNOWN_OPCODE,
+ [126] = _TAIL_CALL_UNKNOWN_OPCODE,
+ [127] = _TAIL_CALL_UNKNOWN_OPCODE,
+ [210] = _TAIL_CALL_UNKNOWN_OPCODE,
+ [211] = _TAIL_CALL_UNKNOWN_OPCODE,
+ [212] = _TAIL_CALL_UNKNOWN_OPCODE,
+ [213] = _TAIL_CALL_UNKNOWN_OPCODE,
+ [214] = _TAIL_CALL_UNKNOWN_OPCODE,
+ [215] = _TAIL_CALL_UNKNOWN_OPCODE,
+ [216] = _TAIL_CALL_UNKNOWN_OPCODE,
+ [217] = _TAIL_CALL_UNKNOWN_OPCODE,
+ [218] = _TAIL_CALL_UNKNOWN_OPCODE,
+ [219] = _TAIL_CALL_UNKNOWN_OPCODE,
+ [220] = _TAIL_CALL_UNKNOWN_OPCODE,
+ [221] = _TAIL_CALL_UNKNOWN_OPCODE,
+ [222] = _TAIL_CALL_UNKNOWN_OPCODE,
+ [223] = _TAIL_CALL_UNKNOWN_OPCODE,
+ [224] = _TAIL_CALL_UNKNOWN_OPCODE,
+ [225] = _TAIL_CALL_UNKNOWN_OPCODE,
+ [226] = _TAIL_CALL_UNKNOWN_OPCODE,
+ [227] = _TAIL_CALL_UNKNOWN_OPCODE,
+ [228] = _TAIL_CALL_UNKNOWN_OPCODE,
+ [229] = _TAIL_CALL_UNKNOWN_OPCODE,
+ [230] = _TAIL_CALL_UNKNOWN_OPCODE,
+ [231] = _TAIL_CALL_UNKNOWN_OPCODE,
+ [232] = _TAIL_CALL_UNKNOWN_OPCODE,
+ [233] = _TAIL_CALL_UNKNOWN_OPCODE,
+};
#endif /* _Py_TAIL_CALL_INTERP */
diff --git a/Python/optimizer.c b/Python/optimizer.c
index 3b7e2dafab8..65007a256d0 100644
--- a/Python/optimizer.c
+++ b/Python/optimizer.c
@@ -29,11 +29,24 @@
#define MAX_EXECUTORS_SIZE 256
+// Trace too short, no progress:
+// _START_EXECUTOR
+// _MAKE_WARM
+// _CHECK_VALIDITY
+// _SET_IP
+// is 4-5 instructions.
+#define CODE_SIZE_NO_PROGRESS 5
+// We start with _START_EXECUTOR, _MAKE_WARM
+#define CODE_SIZE_EMPTY 2
+
#define _PyExecutorObject_CAST(op) ((_PyExecutorObject *)(op))
static bool
has_space_for_executor(PyCodeObject *code, _Py_CODEUNIT *instr)
{
+ if (code == (PyCodeObject *)&_Py_InitCleanup) {
+ return false;
+ }
if (instr->op.code == ENTER_EXECUTOR) {
return true;
}
@@ -100,11 +113,11 @@ insert_executor(PyCodeObject *code, _Py_CODEUNIT *instr, int index, _PyExecutorO
}
static _PyExecutorObject *
-make_executor_from_uops(_PyUOpInstruction *buffer, int length, const _PyBloomFilter *dependencies);
+make_executor_from_uops(_PyUOpInstruction *buffer, int length, const _PyBloomFilter *dependencies, int chain_depth);
static int
-uop_optimize(_PyInterpreterFrame *frame, _Py_CODEUNIT *instr,
- _PyExecutorObject **exec_ptr, int curr_stackentries,
+uop_optimize(_PyInterpreterFrame *frame, PyThreadState *tstate,
+ _PyExecutorObject **exec_ptr,
bool progress_needed);
/* Returns 1 if optimized, 0 if not optimized, and -1 for an error.
@@ -113,10 +126,10 @@ uop_optimize(_PyInterpreterFrame *frame, _Py_CODEUNIT *instr,
// gh-137573: inlining this function causes stack overflows
Py_NO_INLINE int
_PyOptimizer_Optimize(
- _PyInterpreterFrame *frame, _Py_CODEUNIT *start,
- _PyExecutorObject **executor_ptr, int chain_depth)
+ _PyInterpreterFrame *frame, PyThreadState *tstate)
{
- _PyStackRef *stack_pointer = frame->stackpointer;
+ _PyThreadStateImpl *_tstate = (_PyThreadStateImpl *)tstate;
+ int chain_depth = _tstate->jit_tracer_state.initial_state.chain_depth;
PyInterpreterState *interp = _PyInterpreterState_GET();
if (!interp->jit) {
// gh-140936: It is possible that interp->jit will become false during
@@ -126,7 +139,9 @@ _PyOptimizer_Optimize(
return 0;
}
assert(!interp->compiling);
+ assert(_tstate->jit_tracer_state.initial_state.stack_depth >= 0);
#ifndef Py_GIL_DISABLED
+ assert(_tstate->jit_tracer_state.initial_state.func != NULL);
interp->compiling = true;
// The first executor in a chain and the MAX_CHAIN_DEPTH'th executor *must*
// make progress in order to avoid infinite loops or excessively-long
@@ -134,18 +149,24 @@ _PyOptimizer_Optimize(
// this is true, since a deopt won't infinitely re-enter the executor:
chain_depth %= MAX_CHAIN_DEPTH;
bool progress_needed = chain_depth == 0;
- PyCodeObject *code = _PyFrame_GetCode(frame);
- assert(PyCode_Check(code));
+ PyCodeObject *code = (PyCodeObject *)_tstate->jit_tracer_state.initial_state.code;
+ _Py_CODEUNIT *start = _tstate->jit_tracer_state.initial_state.start_instr;
if (progress_needed && !has_space_for_executor(code, start)) {
interp->compiling = false;
return 0;
}
- int err = uop_optimize(frame, start, executor_ptr, (int)(stack_pointer - _PyFrame_Stackbase(frame)), progress_needed);
+ // One of our dependencies while tracing was invalidated. Not worth compiling.
+ if (!_tstate->jit_tracer_state.prev_state.dependencies_still_valid) {
+ interp->compiling = false;
+ return 0;
+ }
+ _PyExecutorObject *executor;
+ int err = uop_optimize(frame, tstate, &executor, progress_needed);
if (err <= 0) {
interp->compiling = false;
return err;
}
- assert(*executor_ptr != NULL);
+ assert(executor != NULL);
if (progress_needed) {
int index = get_index_for_executor(code, start);
if (index < 0) {
@@ -155,17 +176,21 @@ _PyOptimizer_Optimize(
* If an optimizer has already produced an executor,
* it might get confused by the executor disappearing,
* but there is not much we can do about that here. */
- Py_DECREF(*executor_ptr);
+ Py_DECREF(executor);
interp->compiling = false;
return 0;
}
- insert_executor(code, start, index, *executor_ptr);
+ insert_executor(code, start, index, executor);
}
else {
- (*executor_ptr)->vm_data.code = NULL;
+ executor->vm_data.code = NULL;
}
- (*executor_ptr)->vm_data.chain_depth = chain_depth;
- assert((*executor_ptr)->vm_data.valid);
+ _PyExitData *exit = _tstate->jit_tracer_state.initial_state.exit;
+ if (exit != NULL) {
+ exit->executor = executor;
+ }
+ executor->vm_data.chain_depth = chain_depth;
+ assert(executor->vm_data.valid);
interp->compiling = false;
return 1;
#else
@@ -474,6 +499,14 @@ BRANCH_TO_GUARD[4][2] = {
[POP_JUMP_IF_NOT_NONE - POP_JUMP_IF_FALSE][1] = _GUARD_IS_NOT_NONE_POP,
};
+static const uint16_t
+guard_ip_uop[MAX_UOP_ID + 1] = {
+ [_PUSH_FRAME] = _GUARD_IP__PUSH_FRAME,
+ [_RETURN_GENERATOR] = _GUARD_IP_RETURN_GENERATOR,
+ [_RETURN_VALUE] = _GUARD_IP_RETURN_VALUE,
+ [_YIELD_VALUE] = _GUARD_IP_YIELD_VALUE,
+};
+
#define CONFIDENCE_RANGE 1000
#define CONFIDENCE_CUTOFF 333
@@ -530,64 +563,19 @@ add_to_trace(
DPRINTF(2, "No room for %s (need %d, got %d)\n", \
(opname), (n), max_length - trace_length); \
OPT_STAT_INC(trace_too_long); \
- goto done; \
+ goto full; \
}
-// Reserve space for N uops, plus 3 for _SET_IP, _CHECK_VALIDITY and _EXIT_TRACE
-#define RESERVE(needed) RESERVE_RAW((needed) + 3, _PyUOpName(opcode))
-// Trace stack operations (used by _PUSH_FRAME, _RETURN_VALUE)
-#define TRACE_STACK_PUSH() \
- if (trace_stack_depth >= TRACE_STACK_SIZE) { \
- DPRINTF(2, "Trace stack overflow\n"); \
- OPT_STAT_INC(trace_stack_overflow); \
- return 0; \
- } \
- assert(func == NULL || func->func_code == (PyObject *)code); \
- trace_stack[trace_stack_depth].func = func; \
- trace_stack[trace_stack_depth].code = code; \
- trace_stack[trace_stack_depth].instr = instr; \
- trace_stack_depth++;
-#define TRACE_STACK_POP() \
- if (trace_stack_depth <= 0) { \
- Py_FatalError("Trace stack underflow\n"); \
- } \
- trace_stack_depth--; \
- func = trace_stack[trace_stack_depth].func; \
- code = trace_stack[trace_stack_depth].code; \
- assert(func == NULL || func->func_code == (PyObject *)code); \
- instr = trace_stack[trace_stack_depth].instr;
-
-/* Returns the length of the trace on success,
- * 0 if it failed to produce a worthwhile trace,
- * and -1 on an error.
+/* Returns 1 on success (added to trace), 0 on trace end.
*/
-static int
-translate_bytecode_to_trace(
+int
+_PyJit_translate_single_bytecode_to_trace(
+ PyThreadState *tstate,
_PyInterpreterFrame *frame,
- _Py_CODEUNIT *instr,
- _PyUOpInstruction *trace,
- int buffer_size,
- _PyBloomFilter *dependencies, bool progress_needed)
+ _Py_CODEUNIT *next_instr,
+ bool stop_tracing)
{
- bool first = true;
- PyCodeObject *code = _PyFrame_GetCode(frame);
- PyFunctionObject *func = _PyFrame_GetFunction(frame);
- assert(PyFunction_Check(func));
- PyCodeObject *initial_code = code;
- _Py_BloomFilter_Add(dependencies, initial_code);
- _Py_CODEUNIT *initial_instr = instr;
- int trace_length = 0;
- // Leave space for possible trailing _EXIT_TRACE
- int max_length = buffer_size-2;
- struct {
- PyFunctionObject *func;
- PyCodeObject *code;
- _Py_CODEUNIT *instr;
- } trace_stack[TRACE_STACK_SIZE];
- int trace_stack_depth = 0;
- int confidence = CONFIDENCE_RANGE; // Adjusted by branch instructions
- bool jump_seen = false;
#ifdef Py_DEBUG
char *python_lltrace = Py_GETENV("PYTHON_LLTRACE");
@@ -596,410 +584,468 @@ translate_bytecode_to_trace(
lltrace = *python_lltrace - '0'; // TODO: Parse an int and all that
}
#endif
+ _PyThreadStateImpl *_tstate = (_PyThreadStateImpl *)tstate;
+ PyCodeObject *old_code = _tstate->jit_tracer_state.prev_state.instr_code;
+ bool progress_needed = (_tstate->jit_tracer_state.initial_state.chain_depth % MAX_CHAIN_DEPTH) == 0;
+ _PyBloomFilter *dependencies = &_tstate->jit_tracer_state.prev_state.dependencies;
+ int trace_length = _tstate->jit_tracer_state.prev_state.code_curr_size;
+ _PyUOpInstruction *trace = _tstate->jit_tracer_state.code_buffer;
+ int max_length = _tstate->jit_tracer_state.prev_state.code_max_size;
- DPRINTF(2,
- "Optimizing %s (%s:%d) at byte offset %d\n",
- PyUnicode_AsUTF8(code->co_qualname),
- PyUnicode_AsUTF8(code->co_filename),
- code->co_firstlineno,
- 2 * INSTR_IP(initial_instr, code));
- ADD_TO_TRACE(_START_EXECUTOR, 0, (uintptr_t)instr, INSTR_IP(instr, code));
- ADD_TO_TRACE(_MAKE_WARM, 0, 0, 0);
+ _Py_CODEUNIT *this_instr = _tstate->jit_tracer_state.prev_state.instr;
+ _Py_CODEUNIT *target_instr = this_instr;
uint32_t target = 0;
- for (;;) {
- target = INSTR_IP(instr, code);
- // One for possible _DEOPT, one because _CHECK_VALIDITY itself might _DEOPT
- max_length-=2;
- uint32_t opcode = instr->op.code;
- uint32_t oparg = instr->op.arg;
+ target = Py_IsNone((PyObject *)old_code)
+ ? (int)(target_instr - _Py_INTERPRETER_TRAMPOLINE_INSTRUCTIONS_PTR)
+ : INSTR_IP(target_instr, old_code);
- if (!first && instr == initial_instr) {
- // We have looped around to the start:
- RESERVE(1);
- ADD_TO_TRACE(_JUMP_TO_TOP, 0, 0, 0);
+ // Rewind EXTENDED_ARG so that we see the whole thing.
+ // We must point to the first EXTENDED_ARG when deopting.
+ int oparg = _tstate->jit_tracer_state.prev_state.instr_oparg;
+ int opcode = this_instr->op.code;
+ int rewind_oparg = oparg;
+ while (rewind_oparg > 255) {
+ rewind_oparg >>= 8;
+ target--;
+ }
+
+ int old_stack_level = _tstate->jit_tracer_state.prev_state.instr_stacklevel;
+
+ // Strange control-flow
+ bool has_dynamic_jump_taken = OPCODE_HAS_UNPREDICTABLE_JUMP(opcode) &&
+ (next_instr != this_instr + 1 + _PyOpcode_Caches[_PyOpcode_Deopt[opcode]]);
+
+ /* Special case the first instruction,
+ * so that we can guarantee forward progress */
+ if (progress_needed && _tstate->jit_tracer_state.prev_state.code_curr_size < CODE_SIZE_NO_PROGRESS) {
+ if (OPCODE_HAS_EXIT(opcode) || OPCODE_HAS_DEOPT(opcode)) {
+ opcode = _PyOpcode_Deopt[opcode];
+ }
+ assert(!OPCODE_HAS_EXIT(opcode));
+ assert(!OPCODE_HAS_DEOPT(opcode));
+ }
+
+ bool needs_guard_ip = OPCODE_HAS_NEEDS_GUARD_IP(opcode);
+ if (has_dynamic_jump_taken && !needs_guard_ip) {
+ DPRINTF(2, "Unsupported: dynamic jump taken %s\n", _PyOpcode_OpName[opcode]);
+ goto unsupported;
+ }
+
+ int is_sys_tracing = (tstate->c_tracefunc != NULL) || (tstate->c_profilefunc != NULL);
+ if (is_sys_tracing) {
+ goto full;
+ }
+
+ if (stop_tracing) {
+ ADD_TO_TRACE(_DEOPT, 0, 0, target);
+ goto done;
+ }
+
+ DPRINTF(2, "%p %d: %s(%d) %d %d\n", old_code, target, _PyOpcode_OpName[opcode], oparg, needs_guard_ip, old_stack_level);
+
+#ifdef Py_DEBUG
+ if (oparg > 255) {
+ assert(_Py_GetBaseCodeUnit(old_code, target).op.code == EXTENDED_ARG);
+ }
+#endif
+
+ // Skip over super instructions.
+ if (_tstate->jit_tracer_state.prev_state.instr_is_super) {
+ _tstate->jit_tracer_state.prev_state.instr_is_super = false;
+ return 1;
+ }
+
+ if (opcode == ENTER_EXECUTOR) {
+ goto full;
+ }
+
+ if (!_tstate->jit_tracer_state.prev_state.dependencies_still_valid) {
+ goto done;
+ }
+
+ // This happens when a recursive call happens that we can't trace. Such as Python -> C -> Python calls
+ // If we haven't guarded the IP, then it's untraceable.
+ if (frame != _tstate->jit_tracer_state.prev_state.instr_frame && !needs_guard_ip) {
+ DPRINTF(2, "Unsupported: unguardable jump taken\n");
+ goto unsupported;
+ }
+
+ if (oparg > 0xFFFF) {
+ DPRINTF(2, "Unsupported: oparg too large\n");
+ goto unsupported;
+ }
+
+ // TODO (gh-140277): The constituent use one extra stack slot. So we need to check for headroom.
+ if (opcode == BINARY_OP_SUBSCR_GETITEM && old_stack_level + 1 > old_code->co_stacksize) {
+ unsupported:
+ {
+ // Rewind to previous instruction and replace with _EXIT_TRACE.
+ _PyUOpInstruction *curr = &trace[trace_length-1];
+ while (curr->opcode != _SET_IP && trace_length > 2) {
+ trace_length--;
+ curr = &trace[trace_length-1];
+ }
+ assert(curr->opcode == _SET_IP || trace_length == 2);
+ if (curr->opcode == _SET_IP) {
+ int32_t old_target = (int32_t)uop_get_target(curr);
+ curr++;
+ trace_length++;
+ curr->opcode = _EXIT_TRACE;
+ curr->format = UOP_FORMAT_TARGET;
+ curr->target = old_target;
+ }
goto done;
}
+ }
- DPRINTF(2, "%d: %s(%d)\n", target, _PyOpcode_OpName[opcode], oparg);
+ if (opcode == NOP) {
+ return 1;
+ }
- if (opcode == EXTENDED_ARG) {
- instr++;
- opcode = instr->op.code;
- oparg = (oparg << 8) | instr->op.arg;
- if (opcode == EXTENDED_ARG) {
- instr--;
+ if (opcode == JUMP_FORWARD) {
+ return 1;
+ }
+
+ if (opcode == EXTENDED_ARG) {
+ return 1;
+ }
+
+ // One for possible _DEOPT, one because _CHECK_VALIDITY itself might _DEOPT
+ max_length -= 2;
+
+ const struct opcode_macro_expansion *expansion = &_PyOpcode_macro_expansion[opcode];
+
+ assert(opcode != ENTER_EXECUTOR && opcode != EXTENDED_ARG);
+ assert(!_PyErr_Occurred(tstate));
+
+
+ if (OPCODE_HAS_EXIT(opcode)) {
+ // Make space for side exit and final _EXIT_TRACE:
+ max_length--;
+ }
+ if (OPCODE_HAS_ERROR(opcode)) {
+ // Make space for error stub and final _EXIT_TRACE:
+ max_length--;
+ }
+
+ // _GUARD_IP leads to an exit.
+ max_length -= needs_guard_ip;
+
+ RESERVE_RAW(expansion->nuops + needs_guard_ip + 2 + (!OPCODE_HAS_NO_SAVE_IP(opcode)), "uop and various checks");
+
+ ADD_TO_TRACE(_CHECK_VALIDITY, 0, 0, target);
+
+ if (!OPCODE_HAS_NO_SAVE_IP(opcode)) {
+ ADD_TO_TRACE(_SET_IP, 0, (uintptr_t)target_instr, target);
+ }
+
+ // Can be NULL for the entry frame.
+ if (old_code != NULL) {
+ _Py_BloomFilter_Add(dependencies, old_code);
+ }
+
+ switch (opcode) {
+ case POP_JUMP_IF_NONE:
+ case POP_JUMP_IF_NOT_NONE:
+ case POP_JUMP_IF_FALSE:
+ case POP_JUMP_IF_TRUE:
+ {
+ _Py_CODEUNIT *computed_next_instr_without_modifiers = target_instr + 1 + _PyOpcode_Caches[_PyOpcode_Deopt[opcode]];
+ _Py_CODEUNIT *computed_next_instr = computed_next_instr_without_modifiers + (computed_next_instr_without_modifiers->op.code == NOT_TAKEN);
+ _Py_CODEUNIT *computed_jump_instr = computed_next_instr_without_modifiers + oparg;
+ assert(next_instr == computed_next_instr || next_instr == computed_jump_instr);
+ int jump_happened = computed_jump_instr == next_instr;
+ assert(jump_happened == (target_instr[1].cache & 1));
+ uint32_t uopcode = BRANCH_TO_GUARD[opcode - POP_JUMP_IF_FALSE][jump_happened];
+ ADD_TO_TRACE(uopcode, 0, 0, INSTR_IP(jump_happened ? computed_next_instr : computed_jump_instr, old_code));
+ break;
+ }
+ case JUMP_BACKWARD_JIT:
+ // This is possible as the JIT might have re-activated after it was disabled
+ case JUMP_BACKWARD_NO_JIT:
+ case JUMP_BACKWARD:
+ ADD_TO_TRACE(_CHECK_PERIODIC, 0, 0, target);
+ _Py_FALLTHROUGH;
+ case JUMP_BACKWARD_NO_INTERRUPT:
+ {
+ if ((next_instr != _tstate->jit_tracer_state.initial_state.close_loop_instr) &&
+ (next_instr != _tstate->jit_tracer_state.initial_state.start_instr) &&
+ _tstate->jit_tracer_state.prev_state.code_curr_size > CODE_SIZE_NO_PROGRESS &&
+ // For side exits, we don't want to terminate them early.
+ _tstate->jit_tracer_state.initial_state.exit == NULL &&
+ // These are coroutines, and we want to unroll those usually.
+ opcode != JUMP_BACKWARD_NO_INTERRUPT) {
+ // We encountered a JUMP_BACKWARD but not to the top of our own loop.
+ // We don't want to continue tracing as we might get stuck in the
+ // inner loop. Instead, end the trace where the executor of the
+ // inner loop might start and let the traces rejoin.
+ OPT_STAT_INC(inner_loop);
+ ADD_TO_TRACE(_EXIT_TRACE, 0, 0, target);
+ trace[trace_length-1].operand1 = true; // is_control_flow
+ DPRINTF(2, "JUMP_BACKWARD not to top ends trace %p %p %p\n", next_instr,
+ _tstate->jit_tracer_state.initial_state.close_loop_instr, _tstate->jit_tracer_state.initial_state.start_instr);
goto done;
}
- }
- if (opcode == ENTER_EXECUTOR) {
- // We have a couple of options here. We *could* peek "underneath"
- // this executor and continue tracing, which could give us a longer,
- // more optimizeable trace (at the expense of lots of duplicated
- // tier two code). Instead, we choose to just end here and stitch to
- // the other trace, which allows a side-exit traces to rejoin the
- // "main" trace periodically (and also helps protect us against
- // pathological behavior where the amount of tier two code explodes
- // for a medium-length, branchy code path). This seems to work
- // better in practice, but in the future we could be smarter about
- // what we do here:
- goto done;
- }
- assert(opcode != ENTER_EXECUTOR && opcode != EXTENDED_ARG);
- RESERVE_RAW(2, "_CHECK_VALIDITY");
- ADD_TO_TRACE(_CHECK_VALIDITY, 0, 0, target);
- if (!OPCODE_HAS_NO_SAVE_IP(opcode)) {
- RESERVE_RAW(2, "_SET_IP");
- ADD_TO_TRACE(_SET_IP, 0, (uintptr_t)instr, target);
+ break;
}
- /* Special case the first instruction,
- * so that we can guarantee forward progress */
- if (first && progress_needed) {
- assert(first);
- if (OPCODE_HAS_EXIT(opcode) || OPCODE_HAS_DEOPT(opcode)) {
- opcode = _PyOpcode_Deopt[opcode];
+ case RESUME:
+ case RESUME_CHECK:
+ /* Use a special tier 2 version of RESUME_CHECK to allow traces to
+ * start with RESUME_CHECK */
+ ADD_TO_TRACE(_TIER2_RESUME_CHECK, 0, 0, target);
+ break;
+ default:
+ {
+ const struct opcode_macro_expansion *expansion = &_PyOpcode_macro_expansion[opcode];
+ // Reserve space for nuops (+ _SET_IP + _EXIT_TRACE)
+ int nuops = expansion->nuops;
+ if (nuops == 0) {
+ DPRINTF(2, "Unsupported opcode %s\n", _PyOpcode_OpName[opcode]);
+ goto unsupported;
}
- assert(!OPCODE_HAS_EXIT(opcode));
- assert(!OPCODE_HAS_DEOPT(opcode));
- }
+ assert(nuops > 0);
+ uint32_t orig_oparg = oparg; // For OPARG_TOP/BOTTOM
+ uint32_t orig_target = target;
+ for (int i = 0; i < nuops; i++) {
+ oparg = orig_oparg;
+ target = orig_target;
+ uint32_t uop = expansion->uops[i].uop;
+ uint64_t operand = 0;
+ // Add one to account for the actual opcode/oparg pair:
+ int offset = expansion->uops[i].offset + 1;
+ switch (expansion->uops[i].size) {
+ case OPARG_SIMPLE:
+ assert(opcode != _JUMP_BACKWARD_NO_INTERRUPT && opcode != JUMP_BACKWARD);
+ break;
+ case OPARG_CACHE_1:
+ operand = read_u16(&this_instr[offset].cache);
+ break;
+ case OPARG_CACHE_2:
+ operand = read_u32(&this_instr[offset].cache);
+ break;
+ case OPARG_CACHE_4:
+ operand = read_u64(&this_instr[offset].cache);
+ break;
+ case OPARG_TOP: // First half of super-instr
+ assert(orig_oparg <= 255);
+ oparg = orig_oparg >> 4;
+ break;
+ case OPARG_BOTTOM: // Second half of super-instr
+ assert(orig_oparg <= 255);
+ oparg = orig_oparg & 0xF;
+ break;
+ case OPARG_SAVE_RETURN_OFFSET: // op=_SAVE_RETURN_OFFSET; oparg=return_offset
+ oparg = offset;
+ assert(uop == _SAVE_RETURN_OFFSET);
+ break;
+ case OPARG_REPLACED:
+ uop = _PyUOp_Replacements[uop];
+ assert(uop != 0);
- if (OPCODE_HAS_EXIT(opcode)) {
- // Make space for side exit and final _EXIT_TRACE:
- RESERVE_RAW(2, "_EXIT_TRACE");
- max_length--;
- }
- if (OPCODE_HAS_ERROR(opcode)) {
- // Make space for error stub and final _EXIT_TRACE:
- RESERVE_RAW(2, "_ERROR_POP_N");
- max_length--;
- }
- switch (opcode) {
- case POP_JUMP_IF_NONE:
- case POP_JUMP_IF_NOT_NONE:
- case POP_JUMP_IF_FALSE:
- case POP_JUMP_IF_TRUE:
- {
- RESERVE(1);
- int counter = instr[1].cache;
- int bitcount = _Py_popcount32(counter);
- int jump_likely = bitcount > 8;
- /* If bitcount is 8 (half the jumps were taken), adjust confidence by 50%.
- For values in between, adjust proportionally. */
- if (jump_likely) {
- confidence = confidence * bitcount / 16;
+ uint32_t next_inst = target + 1 + _PyOpcode_Caches[_PyOpcode_Deopt[opcode]];
+ if (uop == _TIER2_RESUME_CHECK) {
+ target = next_inst;
+ }
+ else {
+ int extended_arg = orig_oparg > 255;
+ uint32_t jump_target = next_inst + orig_oparg + extended_arg;
+ assert(_Py_GetBaseCodeUnit(old_code, jump_target).op.code == END_FOR);
+ assert(_Py_GetBaseCodeUnit(old_code, jump_target+1).op.code == POP_ITER);
+ if (is_for_iter_test[uop]) {
+ target = jump_target + 1;
+ }
+ }
+ break;
+ case OPERAND1_1:
+ assert(trace[trace_length-1].opcode == uop);
+ operand = read_u16(&this_instr[offset].cache);
+ trace[trace_length-1].operand1 = operand;
+ continue;
+ case OPERAND1_2:
+ assert(trace[trace_length-1].opcode == uop);
+ operand = read_u32(&this_instr[offset].cache);
+ trace[trace_length-1].operand1 = operand;
+ continue;
+ case OPERAND1_4:
+ assert(trace[trace_length-1].opcode == uop);
+ operand = read_u64(&this_instr[offset].cache);
+ trace[trace_length-1].operand1 = operand;
+ continue;
+ default:
+ fprintf(stderr,
+ "opcode=%d, oparg=%d; nuops=%d, i=%d; size=%d, offset=%d\n",
+ opcode, oparg, nuops, i,
+ expansion->uops[i].size,
+ expansion->uops[i].offset);
+ Py_FatalError("garbled expansion");
}
- else {
- confidence = confidence * (16 - bitcount) / 16;
- }
- uint32_t uopcode = BRANCH_TO_GUARD[opcode - POP_JUMP_IF_FALSE][jump_likely];
- DPRINTF(2, "%d: %s(%d): counter=%04x, bitcount=%d, likely=%d, confidence=%d, uopcode=%s\n",
- target, _PyOpcode_OpName[opcode], oparg,
- counter, bitcount, jump_likely, confidence, _PyUOpName(uopcode));
- if (confidence < CONFIDENCE_CUTOFF) {
- DPRINTF(2, "Confidence too low (%d < %d)\n", confidence, CONFIDENCE_CUTOFF);
- OPT_STAT_INC(low_confidence);
- goto done;
- }
- _Py_CODEUNIT *next_instr = instr + 1 + _PyOpcode_Caches[_PyOpcode_Deopt[opcode]];
- _Py_CODEUNIT *target_instr = next_instr + oparg;
- if (jump_likely) {
- DPRINTF(2, "Jump likely (%04x = %d bits), continue at byte offset %d\n",
- instr[1].cache, bitcount, 2 * INSTR_IP(target_instr, code));
- instr = target_instr;
- ADD_TO_TRACE(uopcode, 0, 0, INSTR_IP(next_instr, code));
- goto top;
- }
- ADD_TO_TRACE(uopcode, 0, 0, INSTR_IP(target_instr, code));
- break;
- }
+ if (uop == _PUSH_FRAME || uop == _RETURN_VALUE || uop == _RETURN_GENERATOR || uop == _YIELD_VALUE) {
+ PyCodeObject *new_code = (PyCodeObject *)PyStackRef_AsPyObjectBorrow(frame->f_executable);
+ PyFunctionObject *new_func = (PyFunctionObject *)PyStackRef_AsPyObjectBorrow(frame->f_funcobj);
- case JUMP_BACKWARD:
- case JUMP_BACKWARD_JIT:
- ADD_TO_TRACE(_CHECK_PERIODIC, 0, 0, target);
- _Py_FALLTHROUGH;
- case JUMP_BACKWARD_NO_INTERRUPT:
- {
- instr += 1 + _PyOpcode_Caches[_PyOpcode_Deopt[opcode]] - (int)oparg;
- if (jump_seen) {
- OPT_STAT_INC(inner_loop);
- DPRINTF(2, "JUMP_BACKWARD not to top ends trace\n");
- goto done;
- }
- jump_seen = true;
- goto top;
- }
-
- case JUMP_FORWARD:
- {
- RESERVE(0);
- // This will emit two _SET_IP instructions; leave it to the optimizer
- instr += oparg;
- break;
- }
-
- case RESUME:
- /* Use a special tier 2 version of RESUME_CHECK to allow traces to
- * start with RESUME_CHECK */
- ADD_TO_TRACE(_TIER2_RESUME_CHECK, 0, 0, target);
- break;
-
- default:
- {
- const struct opcode_macro_expansion *expansion = &_PyOpcode_macro_expansion[opcode];
- if (expansion->nuops > 0) {
- // Reserve space for nuops (+ _SET_IP + _EXIT_TRACE)
- int nuops = expansion->nuops;
- RESERVE(nuops + 1); /* One extra for exit */
- int16_t last_op = expansion->uops[nuops-1].uop;
- if (last_op == _RETURN_VALUE || last_op == _RETURN_GENERATOR || last_op == _YIELD_VALUE) {
- // Check for trace stack underflow now:
- // We can't bail e.g. in the middle of
- // LOAD_CONST + _RETURN_VALUE.
- if (trace_stack_depth == 0) {
- DPRINTF(2, "Trace stack underflow\n");
- OPT_STAT_INC(trace_stack_underflow);
- return 0;
+ operand = 0;
+ if (frame->owner < FRAME_OWNED_BY_INTERPRETER) {
+ // Don't add nested code objects to the dependency.
+ // It causes endless re-traces.
+ if (new_func != NULL && !Py_IsNone((PyObject*)new_func) && !(new_code->co_flags & CO_NESTED)) {
+ operand = (uintptr_t)new_func;
+ DPRINTF(2, "Adding %p func to op\n", (void *)operand);
+ _Py_BloomFilter_Add(dependencies, new_func);
+ }
+ else if (new_code != NULL && !Py_IsNone((PyObject*)new_code)) {
+ operand = (uintptr_t)new_code | 1;
+ DPRINTF(2, "Adding %p code to op\n", (void *)operand);
+ _Py_BloomFilter_Add(dependencies, new_code);
}
}
- uint32_t orig_oparg = oparg; // For OPARG_TOP/BOTTOM
- for (int i = 0; i < nuops; i++) {
- oparg = orig_oparg;
- uint32_t uop = expansion->uops[i].uop;
- uint64_t operand = 0;
- // Add one to account for the actual opcode/oparg pair:
- int offset = expansion->uops[i].offset + 1;
- switch (expansion->uops[i].size) {
- case OPARG_SIMPLE:
- assert(opcode != JUMP_BACKWARD_NO_INTERRUPT && opcode != JUMP_BACKWARD);
- break;
- case OPARG_CACHE_1:
- operand = read_u16(&instr[offset].cache);
- break;
- case OPARG_CACHE_2:
- operand = read_u32(&instr[offset].cache);
- break;
- case OPARG_CACHE_4:
- operand = read_u64(&instr[offset].cache);
- break;
- case OPARG_TOP: // First half of super-instr
- oparg = orig_oparg >> 4;
- break;
- case OPARG_BOTTOM: // Second half of super-instr
- oparg = orig_oparg & 0xF;
- break;
- case OPARG_SAVE_RETURN_OFFSET: // op=_SAVE_RETURN_OFFSET; oparg=return_offset
- oparg = offset;
- assert(uop == _SAVE_RETURN_OFFSET);
- break;
- case OPARG_REPLACED:
- uop = _PyUOp_Replacements[uop];
- assert(uop != 0);
- uint32_t next_inst = target + 1 + _PyOpcode_Caches[_PyOpcode_Deopt[opcode]] + (oparg > 255);
- if (uop == _TIER2_RESUME_CHECK) {
- target = next_inst;
- }
-#ifdef Py_DEBUG
- else {
- uint32_t jump_target = next_inst + oparg;
- assert(_Py_GetBaseCodeUnit(code, jump_target).op.code == END_FOR);
- assert(_Py_GetBaseCodeUnit(code, jump_target+1).op.code == POP_ITER);
- }
-#endif
- break;
- case OPERAND1_1:
- assert(trace[trace_length-1].opcode == uop);
- operand = read_u16(&instr[offset].cache);
- trace[trace_length-1].operand1 = operand;
- continue;
- case OPERAND1_2:
- assert(trace[trace_length-1].opcode == uop);
- operand = read_u32(&instr[offset].cache);
- trace[trace_length-1].operand1 = operand;
- continue;
- case OPERAND1_4:
- assert(trace[trace_length-1].opcode == uop);
- operand = read_u64(&instr[offset].cache);
- trace[trace_length-1].operand1 = operand;
- continue;
- default:
- fprintf(stderr,
- "opcode=%d, oparg=%d; nuops=%d, i=%d; size=%d, offset=%d\n",
- opcode, oparg, nuops, i,
- expansion->uops[i].size,
- expansion->uops[i].offset);
- Py_FatalError("garbled expansion");
- }
-
- if (uop == _RETURN_VALUE || uop == _RETURN_GENERATOR || uop == _YIELD_VALUE) {
- TRACE_STACK_POP();
- /* Set the operand to the function or code object returned to,
- * to assist optimization passes. (See _PUSH_FRAME below.)
- */
- if (func != NULL) {
- operand = (uintptr_t)func;
- }
- else if (code != NULL) {
- operand = (uintptr_t)code | 1;
- }
- else {
- operand = 0;
- }
- ADD_TO_TRACE(uop, oparg, operand, target);
- DPRINTF(2,
- "Returning to %s (%s:%d) at byte offset %d\n",
- PyUnicode_AsUTF8(code->co_qualname),
- PyUnicode_AsUTF8(code->co_filename),
- code->co_firstlineno,
- 2 * INSTR_IP(instr, code));
- goto top;
- }
-
- if (uop == _PUSH_FRAME) {
- assert(i + 1 == nuops);
- if (opcode == FOR_ITER_GEN ||
- opcode == LOAD_ATTR_PROPERTY ||
- opcode == BINARY_OP_SUBSCR_GETITEM ||
- opcode == SEND_GEN)
- {
- DPRINTF(2, "Bailing due to dynamic target\n");
- OPT_STAT_INC(unknown_callee);
- return 0;
- }
- assert(_PyOpcode_Deopt[opcode] == CALL || _PyOpcode_Deopt[opcode] == CALL_KW);
- int func_version_offset =
- offsetof(_PyCallCache, func_version)/sizeof(_Py_CODEUNIT)
- // Add one to account for the actual opcode/oparg pair:
- + 1;
- uint32_t func_version = read_u32(&instr[func_version_offset].cache);
- PyCodeObject *new_code = NULL;
- PyFunctionObject *new_func =
- _PyFunction_LookupByVersion(func_version, (PyObject **) &new_code);
- DPRINTF(2, "Function: version=%#x; new_func=%p, new_code=%p\n",
- (int)func_version, new_func, new_code);
- if (new_code != NULL) {
- if (new_code == code) {
- // Recursive call, bail (we could be here forever).
- DPRINTF(2, "Bailing on recursive call to %s (%s:%d)\n",
- PyUnicode_AsUTF8(new_code->co_qualname),
- PyUnicode_AsUTF8(new_code->co_filename),
- new_code->co_firstlineno);
- OPT_STAT_INC(recursive_call);
- ADD_TO_TRACE(uop, oparg, 0, target);
- ADD_TO_TRACE(_EXIT_TRACE, 0, 0, 0);
- goto done;
- }
- if (new_code->co_version != func_version) {
- // func.__code__ was updated.
- // Perhaps it may happen again, so don't bother tracing.
- // TODO: Reason about this -- is it better to bail or not?
- DPRINTF(2, "Bailing because co_version != func_version\n");
- ADD_TO_TRACE(uop, oparg, 0, target);
- ADD_TO_TRACE(_EXIT_TRACE, 0, 0, 0);
- goto done;
- }
- // Increment IP to the return address
- instr += _PyOpcode_Caches[_PyOpcode_Deopt[opcode]] + 1;
- TRACE_STACK_PUSH();
- _Py_BloomFilter_Add(dependencies, new_code);
- /* Set the operand to the callee's function or code object,
- * to assist optimization passes.
- * We prefer setting it to the function
- * but if that's not available but the code is available,
- * use the code, setting the low bit so the optimizer knows.
- */
- if (new_func != NULL) {
- operand = (uintptr_t)new_func;
- }
- else if (new_code != NULL) {
- operand = (uintptr_t)new_code | 1;
- }
- else {
- operand = 0;
- }
- ADD_TO_TRACE(uop, oparg, operand, target);
- code = new_code;
- func = new_func;
- instr = _PyCode_CODE(code);
- DPRINTF(2,
- "Continuing in %s (%s:%d) at byte offset %d\n",
- PyUnicode_AsUTF8(code->co_qualname),
- PyUnicode_AsUTF8(code->co_filename),
- code->co_firstlineno,
- 2 * INSTR_IP(instr, code));
- goto top;
- }
- DPRINTF(2, "Bail, new_code == NULL\n");
- OPT_STAT_INC(unknown_callee);
- return 0;
- }
-
- if (uop == _BINARY_OP_INPLACE_ADD_UNICODE) {
- assert(i + 1 == nuops);
- _Py_CODEUNIT *next_instr = instr + 1 + _PyOpcode_Caches[_PyOpcode_Deopt[opcode]];
- assert(next_instr->op.code == STORE_FAST);
- operand = next_instr->op.arg;
- // Skip the STORE_FAST:
- instr++;
- }
-
- // All other instructions
- ADD_TO_TRACE(uop, oparg, operand, target);
- }
+ ADD_TO_TRACE(uop, oparg, operand, target);
+ trace[trace_length - 1].operand1 = PyStackRef_IsNone(frame->f_executable) ? 2 : ((int)(frame->stackpointer - _PyFrame_Stackbase(frame)));
break;
}
- DPRINTF(2, "Unsupported opcode %s\n", _PyOpcode_OpName[opcode]);
- OPT_UNSUPPORTED_OPCODE(opcode);
- goto done; // Break out of loop
- } // End default
+ if (uop == _BINARY_OP_INPLACE_ADD_UNICODE) {
+ assert(i + 1 == nuops);
+ _Py_CODEUNIT *next = target_instr + 1 + _PyOpcode_Caches[_PyOpcode_Deopt[opcode]];
+ assert(next->op.code == STORE_FAST);
+ operand = next->op.arg;
+ }
+ // All other instructions
+ ADD_TO_TRACE(uop, oparg, operand, target);
+ }
+ break;
+ } // End default
- } // End switch (opcode)
+ } // End switch (opcode)
- instr++;
- // Add cache size for opcode
- instr += _PyOpcode_Caches[_PyOpcode_Deopt[opcode]];
-
- if (opcode == CALL_LIST_APPEND) {
- assert(instr->op.code == POP_TOP);
- instr++;
+ if (needs_guard_ip) {
+ uint16_t guard_ip = guard_ip_uop[trace[trace_length-1].opcode];
+ if (guard_ip == 0) {
+ DPRINTF(1, "Unknown uop needing guard ip %s\n", _PyOpcode_uop_name[trace[trace_length-1].opcode]);
+ Py_UNREACHABLE();
}
- top:
- // Jump here after _PUSH_FRAME or likely branches.
- first = false;
- } // End for (;;)
-
-done:
- while (trace_stack_depth > 0) {
- TRACE_STACK_POP();
+ ADD_TO_TRACE(guard_ip, 0, (uintptr_t)next_instr, 0);
}
- assert(code == initial_code);
- // Skip short traces where we can't even translate a single instruction:
- if (first) {
- OPT_STAT_INC(trace_too_short);
- DPRINTF(2,
- "No trace for %s (%s:%d) at byte offset %d (no progress)\n",
- PyUnicode_AsUTF8(code->co_qualname),
- PyUnicode_AsUTF8(code->co_filename),
- code->co_firstlineno,
- 2 * INSTR_IP(initial_instr, code));
+ // Loop back to the start
+ int is_first_instr = _tstate->jit_tracer_state.initial_state.close_loop_instr == next_instr ||
+ _tstate->jit_tracer_state.initial_state.start_instr == next_instr;
+ if (is_first_instr && _tstate->jit_tracer_state.prev_state.code_curr_size > CODE_SIZE_NO_PROGRESS) {
+ if (needs_guard_ip) {
+ ADD_TO_TRACE(_SET_IP, 0, (uintptr_t)next_instr, 0);
+ }
+ ADD_TO_TRACE(_JUMP_TO_TOP, 0, 0, 0);
+ goto done;
+ }
+ DPRINTF(2, "Trace continuing\n");
+ _tstate->jit_tracer_state.prev_state.code_curr_size = trace_length;
+ _tstate->jit_tracer_state.prev_state.code_max_size = max_length;
+ return 1;
+done:
+ DPRINTF(2, "Trace done\n");
+ _tstate->jit_tracer_state.prev_state.code_curr_size = trace_length;
+ _tstate->jit_tracer_state.prev_state.code_max_size = max_length;
+ return 0;
+full:
+ DPRINTF(2, "Trace full\n");
+ if (!is_terminator(&_tstate->jit_tracer_state.code_buffer[trace_length-1])) {
+ // Undo the last few instructions.
+ trace_length = _tstate->jit_tracer_state.prev_state.code_curr_size;
+ max_length = _tstate->jit_tracer_state.prev_state.code_max_size;
+ // We previously reversed one.
+ max_length += 1;
+ ADD_TO_TRACE(_EXIT_TRACE, 0, 0, target);
+ trace[trace_length-1].operand1 = true; // is_control_flow
+ }
+ _tstate->jit_tracer_state.prev_state.code_curr_size = trace_length;
+ _tstate->jit_tracer_state.prev_state.code_max_size = max_length;
+ return 0;
+}
+
+// Returns 0 for do not enter tracing, 1 on enter tracing.
+int
+_PyJit_TryInitializeTracing(
+ PyThreadState *tstate, _PyInterpreterFrame *frame, _Py_CODEUNIT *curr_instr,
+ _Py_CODEUNIT *start_instr, _Py_CODEUNIT *close_loop_instr, int curr_stackdepth, int chain_depth,
+ _PyExitData *exit, int oparg)
+{
+ _PyThreadStateImpl *_tstate = (_PyThreadStateImpl *)tstate;
+ // A recursive trace.
+ // Don't trace into the inner call because it will stomp on the previous trace, causing endless retraces.
+ if (_tstate->jit_tracer_state.prev_state.code_curr_size > CODE_SIZE_EMPTY) {
return 0;
}
- if (!is_terminator(&trace[trace_length-1])) {
- /* Allow space for _EXIT_TRACE */
- max_length += 2;
- ADD_TO_TRACE(_EXIT_TRACE, 0, 0, target);
+ if (oparg > 0xFFFF) {
+ return 0;
}
- DPRINTF(1,
- "Created a proto-trace for %s (%s:%d) at byte offset %d -- length %d\n",
- PyUnicode_AsUTF8(code->co_qualname),
- PyUnicode_AsUTF8(code->co_filename),
- code->co_firstlineno,
- 2 * INSTR_IP(initial_instr, code),
- trace_length);
- OPT_HIST(trace_length, trace_length_hist);
- return trace_length;
+ if (_tstate->jit_tracer_state.code_buffer == NULL) {
+ _tstate->jit_tracer_state.code_buffer = (_PyUOpInstruction *)_PyObject_VirtualAlloc(UOP_BUFFER_SIZE);
+ if (_tstate->jit_tracer_state.code_buffer == NULL) {
+ // Don't error, just go to next instruction.
+ return 0;
+ }
+ }
+ PyObject *func = PyStackRef_AsPyObjectBorrow(frame->f_funcobj);
+ if (func == NULL) {
+ return 0;
+ }
+ PyCodeObject *code = _PyFrame_GetCode(frame);
+#ifdef Py_DEBUG
+ char *python_lltrace = Py_GETENV("PYTHON_LLTRACE");
+ int lltrace = 0;
+ if (python_lltrace != NULL && *python_lltrace >= '0') {
+ lltrace = *python_lltrace - '0'; // TODO: Parse an int and all that
+ }
+ DPRINTF(2,
+ "Tracing %s (%s:%d) at byte offset %d at chain depth %d\n",
+ PyUnicode_AsUTF8(code->co_qualname),
+ PyUnicode_AsUTF8(code->co_filename),
+ code->co_firstlineno,
+ 2 * INSTR_IP(close_loop_instr, code),
+ chain_depth);
+#endif
+
+ add_to_trace(_tstate->jit_tracer_state.code_buffer, 0, _START_EXECUTOR, 0, (uintptr_t)start_instr, INSTR_IP(start_instr, code));
+ add_to_trace(_tstate->jit_tracer_state.code_buffer, 1, _MAKE_WARM, 0, 0, 0);
+ _tstate->jit_tracer_state.prev_state.code_curr_size = CODE_SIZE_EMPTY;
+
+ _tstate->jit_tracer_state.prev_state.code_max_size = UOP_MAX_TRACE_LENGTH;
+ _tstate->jit_tracer_state.initial_state.start_instr = start_instr;
+ _tstate->jit_tracer_state.initial_state.close_loop_instr = close_loop_instr;
+ _tstate->jit_tracer_state.initial_state.code = (PyCodeObject *)Py_NewRef(code);
+ _tstate->jit_tracer_state.initial_state.func = (PyFunctionObject *)Py_NewRef(func);
+ _tstate->jit_tracer_state.initial_state.exit = exit;
+ _tstate->jit_tracer_state.initial_state.stack_depth = curr_stackdepth;
+ _tstate->jit_tracer_state.initial_state.chain_depth = chain_depth;
+ _tstate->jit_tracer_state.prev_state.instr_frame = frame;
+ _tstate->jit_tracer_state.prev_state.dependencies_still_valid = true;
+ _tstate->jit_tracer_state.prev_state.instr_code = (PyCodeObject *)Py_NewRef(_PyFrame_GetCode(frame));
+ _tstate->jit_tracer_state.prev_state.instr = curr_instr;
+ _tstate->jit_tracer_state.prev_state.instr_frame = frame;
+ _tstate->jit_tracer_state.prev_state.instr_oparg = oparg;
+ _tstate->jit_tracer_state.prev_state.instr_stacklevel = curr_stackdepth;
+ _tstate->jit_tracer_state.prev_state.instr_is_super = false;
+ assert(curr_instr->op.code == JUMP_BACKWARD_JIT || (exit != NULL));
+ _tstate->jit_tracer_state.initial_state.jump_backward_instr = curr_instr;
+
+ if (_PyOpcode_Caches[_PyOpcode_Deopt[close_loop_instr->op.code]]) {
+ close_loop_instr[1].counter = trigger_backoff_counter();
+ }
+ _Py_BloomFilter_Init(&_tstate->jit_tracer_state.prev_state.dependencies);
+ return 1;
}
+void
+_PyJit_FinalizeTracing(PyThreadState *tstate)
+{
+ _PyThreadStateImpl *_tstate = (_PyThreadStateImpl *)tstate;
+ Py_CLEAR(_tstate->jit_tracer_state.initial_state.code);
+ Py_CLEAR(_tstate->jit_tracer_state.initial_state.func);
+ Py_CLEAR(_tstate->jit_tracer_state.prev_state.instr_code);
+ _tstate->jit_tracer_state.prev_state.code_curr_size = CODE_SIZE_EMPTY;
+ _tstate->jit_tracer_state.prev_state.code_max_size = UOP_MAX_TRACE_LENGTH - 1;
+}
+
+
#undef RESERVE
#undef RESERVE_RAW
#undef INSTR_IP
@@ -1018,20 +1064,21 @@ count_exits(_PyUOpInstruction *buffer, int length)
int exit_count = 0;
for (int i = 0; i < length; i++) {
int opcode = buffer[i].opcode;
- if (opcode == _EXIT_TRACE) {
+ if (opcode == _EXIT_TRACE || opcode == _DYNAMIC_EXIT) {
exit_count++;
}
}
return exit_count;
}
-static void make_exit(_PyUOpInstruction *inst, int opcode, int target)
+static void make_exit(_PyUOpInstruction *inst, int opcode, int target, bool is_control_flow)
{
inst->opcode = opcode;
inst->oparg = 0;
inst->operand0 = 0;
inst->format = UOP_FORMAT_TARGET;
inst->target = target;
+ inst->operand1 = is_control_flow;
#ifdef Py_STATS
inst->execution_count = 0;
#endif
@@ -1075,15 +1122,17 @@ prepare_for_execution(_PyUOpInstruction *buffer, int length)
exit_op = _HANDLE_PENDING_AND_DEOPT;
}
int32_t jump_target = target;
- if (is_for_iter_test[opcode]) {
- /* Target the POP_TOP immediately after the END_FOR,
- * leaving only the iterator on the stack. */
- int extended_arg = inst->oparg > 255;
- int32_t next_inst = target + 1 + INLINE_CACHE_ENTRIES_FOR_ITER + extended_arg;
- jump_target = next_inst + inst->oparg + 1;
+ if (
+ opcode == _GUARD_IP__PUSH_FRAME ||
+ opcode == _GUARD_IP_RETURN_VALUE ||
+ opcode == _GUARD_IP_YIELD_VALUE ||
+ opcode == _GUARD_IP_RETURN_GENERATOR
+ ) {
+ exit_op = _DYNAMIC_EXIT;
}
+ bool is_control_flow = (opcode == _GUARD_IS_FALSE_POP || opcode == _GUARD_IS_TRUE_POP || is_for_iter_test[opcode]);
if (jump_target != current_jump_target || current_exit_op != exit_op) {
- make_exit(&buffer[next_spare], exit_op, jump_target);
+ make_exit(&buffer[next_spare], exit_op, jump_target, is_control_flow);
current_exit_op = exit_op;
current_jump_target = jump_target;
current_jump = next_spare;
@@ -1099,7 +1148,7 @@ prepare_for_execution(_PyUOpInstruction *buffer, int length)
current_popped = popped;
current_error = next_spare;
current_error_target = target;
- make_exit(&buffer[next_spare], _ERROR_POP_N, 0);
+ make_exit(&buffer[next_spare], _ERROR_POP_N, 0, false);
buffer[next_spare].operand0 = target;
next_spare++;
}
@@ -1157,7 +1206,9 @@ sanity_check(_PyExecutorObject *executor)
}
bool ended = false;
uint32_t i = 0;
- CHECK(executor->trace[0].opcode == _START_EXECUTOR || executor->trace[0].opcode == _COLD_EXIT);
+ CHECK(executor->trace[0].opcode == _START_EXECUTOR ||
+ executor->trace[0].opcode == _COLD_EXIT ||
+ executor->trace[0].opcode == _COLD_DYNAMIC_EXIT);
for (; i < executor->code_size; i++) {
const _PyUOpInstruction *inst = &executor->trace[i];
uint16_t opcode = inst->opcode;
@@ -1189,7 +1240,8 @@ sanity_check(_PyExecutorObject *executor)
opcode == _DEOPT ||
opcode == _HANDLE_PENDING_AND_DEOPT ||
opcode == _EXIT_TRACE ||
- opcode == _ERROR_POP_N);
+ opcode == _ERROR_POP_N ||
+ opcode == _DYNAMIC_EXIT);
}
}
@@ -1202,7 +1254,7 @@ sanity_check(_PyExecutorObject *executor)
* and not a NOP.
*/
static _PyExecutorObject *
-make_executor_from_uops(_PyUOpInstruction *buffer, int length, const _PyBloomFilter *dependencies)
+make_executor_from_uops(_PyUOpInstruction *buffer, int length, const _PyBloomFilter *dependencies, int chain_depth)
{
int exit_count = count_exits(buffer, length);
_PyExecutorObject *executor = allocate_executor(exit_count, length);
@@ -1212,10 +1264,11 @@ make_executor_from_uops(_PyUOpInstruction *buffer, int length, const _PyBloomFil
/* Initialize exits */
_PyExecutorObject *cold = _PyExecutor_GetColdExecutor();
+ _PyExecutorObject *cold_dynamic = _PyExecutor_GetColdDynamicExecutor();
+ cold->vm_data.chain_depth = chain_depth;
for (int i = 0; i < exit_count; i++) {
executor->exits[i].index = i;
executor->exits[i].temperature = initial_temperature_backoff_counter();
- executor->exits[i].executor = cold;
}
int next_exit = exit_count-1;
_PyUOpInstruction *dest = (_PyUOpInstruction *)&executor->trace[length];
@@ -1225,11 +1278,13 @@ make_executor_from_uops(_PyUOpInstruction *buffer, int length, const _PyBloomFil
int opcode = buffer[i].opcode;
dest--;
*dest = buffer[i];
- assert(opcode != _POP_JUMP_IF_FALSE && opcode != _POP_JUMP_IF_TRUE);
- if (opcode == _EXIT_TRACE) {
+ if (opcode == _EXIT_TRACE || opcode == _DYNAMIC_EXIT) {
_PyExitData *exit = &executor->exits[next_exit];
exit->target = buffer[i].target;
dest->operand0 = (uint64_t)exit;
+ exit->executor = opcode == _EXIT_TRACE ? cold : cold_dynamic;
+ exit->is_dynamic = (char)(opcode == _DYNAMIC_EXIT);
+ exit->is_control_flow = (char)buffer[i].operand1;
next_exit--;
}
}
@@ -1291,38 +1346,32 @@ int effective_trace_length(_PyUOpInstruction *buffer, int length)
static int
uop_optimize(
_PyInterpreterFrame *frame,
- _Py_CODEUNIT *instr,
+ PyThreadState *tstate,
_PyExecutorObject **exec_ptr,
- int curr_stackentries,
bool progress_needed)
{
- _PyBloomFilter dependencies;
- _Py_BloomFilter_Init(&dependencies);
- PyInterpreterState *interp = _PyInterpreterState_GET();
- if (interp->jit_uop_buffer == NULL) {
- interp->jit_uop_buffer = (_PyUOpInstruction *)_PyObject_VirtualAlloc(UOP_BUFFER_SIZE);
- if (interp->jit_uop_buffer == NULL) {
- return 0;
- }
- }
- _PyUOpInstruction *buffer = interp->jit_uop_buffer;
+ _PyThreadStateImpl *_tstate = (_PyThreadStateImpl *)tstate;
+ _PyBloomFilter *dependencies = &_tstate->jit_tracer_state.prev_state.dependencies;
+ _PyUOpInstruction *buffer = _tstate->jit_tracer_state.code_buffer;
OPT_STAT_INC(attempts);
char *env_var = Py_GETENV("PYTHON_UOPS_OPTIMIZE");
bool is_noopt = true;
if (env_var == NULL || *env_var == '\0' || *env_var > '0') {
is_noopt = false;
}
- int length = translate_bytecode_to_trace(frame, instr, buffer, UOP_MAX_TRACE_LENGTH, &dependencies, progress_needed);
- if (length <= 0) {
- // Error or nothing translated
- return length;
+ int curr_stackentries = _tstate->jit_tracer_state.initial_state.stack_depth;
+ int length = _tstate->jit_tracer_state.prev_state.code_curr_size;
+ if (length <= CODE_SIZE_NO_PROGRESS) {
+ return 0;
}
+ assert(length > 0);
assert(length < UOP_MAX_TRACE_LENGTH);
OPT_STAT_INC(traces_created);
if (!is_noopt) {
- length = _Py_uop_analyze_and_optimize(frame, buffer,
- length,
- curr_stackentries, &dependencies);
+ length = _Py_uop_analyze_and_optimize(
+ _tstate->jit_tracer_state.initial_state.func,
+ buffer,length,
+ curr_stackentries, dependencies);
if (length <= 0) {
return length;
}
@@ -1345,14 +1394,14 @@ uop_optimize(
OPT_HIST(effective_trace_length(buffer, length), optimized_trace_length_hist);
length = prepare_for_execution(buffer, length);
assert(length <= UOP_MAX_TRACE_LENGTH);
- _PyExecutorObject *executor = make_executor_from_uops(buffer, length, &dependencies);
+ _PyExecutorObject *executor = make_executor_from_uops(
+ buffer, length, dependencies, _tstate->jit_tracer_state.initial_state.chain_depth);
if (executor == NULL) {
return -1;
}
assert(length <= UOP_MAX_TRACE_LENGTH);
// Check executor coldness
- PyThreadState *tstate = PyThreadState_Get();
// It's okay if this ends up going negative.
if (--tstate->interp->executor_creation_counter == 0) {
_Py_set_eval_breaker_bit(tstate, _PY_EVAL_JIT_INVALIDATE_COLD_BIT);
@@ -1539,6 +1588,35 @@ _PyExecutor_GetColdExecutor(void)
return cold;
}
+_PyExecutorObject *
+_PyExecutor_GetColdDynamicExecutor(void)
+{
+ PyInterpreterState *interp = _PyInterpreterState_GET();
+ if (interp->cold_dynamic_executor != NULL) {
+ assert(interp->cold_dynamic_executor->trace[0].opcode == _COLD_DYNAMIC_EXIT);
+ return interp->cold_dynamic_executor;
+ }
+ _PyExecutorObject *cold = allocate_executor(0, 1);
+ if (cold == NULL) {
+ Py_FatalError("Cannot allocate core JIT code");
+ }
+ ((_PyUOpInstruction *)cold->trace)->opcode = _COLD_DYNAMIC_EXIT;
+#ifdef _Py_JIT
+ cold->jit_code = NULL;
+ cold->jit_size = 0;
+ // This is initialized to true so we can prevent the executor
+ // from being immediately detected as cold and invalidated.
+ cold->vm_data.warm = true;
+ if (_PyJIT_Compile(cold, cold->trace, 1)) {
+ Py_DECREF(cold);
+ Py_FatalError("Cannot allocate core JIT code");
+ }
+#endif
+ _Py_SetImmortal((PyObject *)cold);
+ interp->cold_dynamic_executor = cold;
+ return cold;
+}
+
void
_PyExecutor_ClearExit(_PyExitData *exit)
{
@@ -1546,7 +1624,12 @@ _PyExecutor_ClearExit(_PyExitData *exit)
return;
}
_PyExecutorObject *old = exit->executor;
- exit->executor = _PyExecutor_GetColdExecutor();
+ if (exit->is_dynamic) {
+ exit->executor = _PyExecutor_GetColdDynamicExecutor();
+ }
+ else {
+ exit->executor = _PyExecutor_GetColdExecutor();
+ }
Py_DECREF(old);
}
@@ -1648,6 +1731,18 @@ _Py_Executors_InvalidateDependency(PyInterpreterState *interp, void *obj, int is
_Py_Executors_InvalidateAll(interp, is_invalidation);
}
+void
+_PyJit_Tracer_InvalidateDependency(PyThreadState *tstate, void *obj)
+{
+ _PyBloomFilter obj_filter;
+ _Py_BloomFilter_Init(&obj_filter);
+ _Py_BloomFilter_Add(&obj_filter, obj);
+ _PyThreadStateImpl *_tstate = (_PyThreadStateImpl *)tstate;
+ if (bloom_filter_may_contain(&_tstate->jit_tracer_state.prev_state.dependencies, &obj_filter))
+ {
+ _tstate->jit_tracer_state.prev_state.dependencies_still_valid = false;
+ }
+}
/* Invalidate all executors */
void
_Py_Executors_InvalidateAll(PyInterpreterState *interp, int is_invalidation)
@@ -1777,7 +1872,7 @@ executor_to_gv(_PyExecutorObject *executor, FILE *out)
#ifdef Py_STATS
fprintf(out, " | %s -- %" PRIu64 " |
\n", i, opname, inst->execution_count);
#else
- fprintf(out, " | %s |
\n", i, opname);
+ fprintf(out, " | %s op0=%" PRIu64 " |
\n", i, opname, inst->operand0);
#endif
if (inst->opcode == _EXIT_TRACE || inst->opcode == _JUMP_TO_TOP) {
break;
@@ -1787,6 +1882,8 @@ executor_to_gv(_PyExecutorObject *executor, FILE *out)
fprintf(out, "]\n\n");
/* Write all the outgoing edges */
+ _PyExecutorObject *cold = _PyExecutor_GetColdExecutor();
+ _PyExecutorObject *cold_dynamic = _PyExecutor_GetColdDynamicExecutor();
for (uint32_t i = 0; i < executor->code_size; i++) {
_PyUOpInstruction const *inst = &executor->trace[i];
uint16_t flags = _PyUop_Flags[inst->opcode];
@@ -1797,10 +1894,10 @@ executor_to_gv(_PyExecutorObject *executor, FILE *out)
else if (flags & HAS_EXIT_FLAG) {
assert(inst->format == UOP_FORMAT_JUMP);
_PyUOpInstruction const *exit_inst = &executor->trace[inst->jump_target];
- assert(exit_inst->opcode == _EXIT_TRACE);
+ assert(exit_inst->opcode == _EXIT_TRACE || exit_inst->opcode == _DYNAMIC_EXIT);
exit = (_PyExitData *)exit_inst->operand0;
}
- if (exit != NULL && exit->executor != NULL) {
+ if (exit != NULL && exit->executor != cold && exit->executor != cold_dynamic) {
fprintf(out, "executor_%p:i%d -> executor_%p:start\n", executor, i, exit->executor);
}
if (inst->opcode == _EXIT_TRACE || inst->opcode == _JUMP_TO_TOP) {
diff --git a/Python/optimizer_analysis.c b/Python/optimizer_analysis.c
index a6add301ccb..8d7b734e17c 100644
--- a/Python/optimizer_analysis.c
+++ b/Python/optimizer_analysis.c
@@ -142,8 +142,10 @@ incorrect_keys(PyObject *obj, uint32_t version)
#define STACK_LEVEL() ((int)(stack_pointer - ctx->frame->stack))
#define STACK_SIZE() ((int)(ctx->frame->stack_len))
+#define CURRENT_FRAME_IS_INIT_SHIM() (ctx->frame->code == ((PyCodeObject *)&_Py_InitCleanup))
+
#define WITHIN_STACK_BOUNDS() \
- (STACK_LEVEL() >= 0 && STACK_LEVEL() <= STACK_SIZE())
+ (CURRENT_FRAME_IS_INIT_SHIM() || (STACK_LEVEL() >= 0 && STACK_LEVEL() <= STACK_SIZE()))
#define GETLOCAL(idx) ((ctx->frame->locals[idx]))
@@ -267,7 +269,7 @@ static
PyCodeObject *
get_current_code_object(JitOptContext *ctx)
{
- return (PyCodeObject *)ctx->frame->func->func_code;
+ return (PyCodeObject *)ctx->frame->code;
}
static PyObject *
@@ -298,10 +300,6 @@ optimize_uops(
JitOptContext context;
JitOptContext *ctx = &context;
uint32_t opcode = UINT16_MAX;
- int curr_space = 0;
- int max_space = 0;
- _PyUOpInstruction *first_valid_check_stack = NULL;
- _PyUOpInstruction *corresponding_check_stack = NULL;
// Make sure that watchers are set up
PyInterpreterState *interp = _PyInterpreterState_GET();
@@ -320,13 +318,18 @@ optimize_uops(
ctx->frame = frame;
_PyUOpInstruction *this_instr = NULL;
+ JitOptRef *stack_pointer = ctx->frame->stack_pointer;
+
for (int i = 0; !ctx->done; i++) {
assert(i < trace_len);
this_instr = &trace[i];
int oparg = this_instr->oparg;
opcode = this_instr->opcode;
- JitOptRef *stack_pointer = ctx->frame->stack_pointer;
+
+ if (!CURRENT_FRAME_IS_INIT_SHIM()) {
+ stack_pointer = ctx->frame->stack_pointer;
+ }
#ifdef Py_DEBUG
if (get_lltrace() >= 3) {
@@ -345,9 +348,11 @@ optimize_uops(
Py_UNREACHABLE();
}
assert(ctx->frame != NULL);
- DPRINTF(3, " stack_level %d\n", STACK_LEVEL());
- ctx->frame->stack_pointer = stack_pointer;
- assert(STACK_LEVEL() >= 0);
+ if (!CURRENT_FRAME_IS_INIT_SHIM()) {
+ DPRINTF(3, " stack_level %d\n", STACK_LEVEL());
+ ctx->frame->stack_pointer = stack_pointer;
+ assert(STACK_LEVEL() >= 0);
+ }
}
if (ctx->out_of_space) {
DPRINTF(3, "\n");
@@ -355,27 +360,21 @@ optimize_uops(
}
if (ctx->contradiction) {
// Attempted to push a "bottom" (contradiction) symbol onto the stack.
- // This means that the abstract interpreter has hit unreachable code.
+ // This means that the abstract interpreter has optimized to trace
+ // to an unreachable estate.
// We *could* generate an _EXIT_TRACE or _FATAL_ERROR here, but hitting
- // bottom indicates type instability, so we are probably better off
+ // bottom usually indicates an optimizer bug, so we are probably better off
// retrying later.
DPRINTF(3, "\n");
DPRINTF(1, "Hit bottom in abstract interpreter\n");
_Py_uop_abstractcontext_fini(ctx);
+ OPT_STAT_INC(optimizer_contradiction);
return 0;
}
/* Either reached the end or cannot optimize further, but there
* would be no benefit in retrying later */
_Py_uop_abstractcontext_fini(ctx);
- if (first_valid_check_stack != NULL) {
- assert(first_valid_check_stack->opcode == _CHECK_STACK_SPACE);
- assert(max_space > 0);
- assert(max_space <= INT_MAX);
- assert(max_space <= INT32_MAX);
- first_valid_check_stack->opcode = _CHECK_STACK_SPACE_OPERAND;
- first_valid_check_stack->operand0 = max_space;
- }
return trace_len;
error:
@@ -460,6 +459,7 @@ remove_unneeded_uops(_PyUOpInstruction *buffer, int buffer_size)
buffer[pc].opcode = _NOP;
}
break;
+ case _EXIT_TRACE:
default:
{
// Cancel out pushes and pops, repeatedly. So:
@@ -493,7 +493,7 @@ remove_unneeded_uops(_PyUOpInstruction *buffer, int buffer_size)
}
/* _PUSH_FRAME doesn't escape or error, but it
* does need the IP for the return address */
- bool needs_ip = opcode == _PUSH_FRAME;
+ bool needs_ip = (opcode == _PUSH_FRAME || opcode == _YIELD_VALUE || opcode == _DYNAMIC_EXIT || opcode == _EXIT_TRACE);
if (_PyUop_Flags[opcode] & HAS_ESCAPES_FLAG) {
needs_ip = true;
may_have_escaped = true;
@@ -503,10 +503,14 @@ remove_unneeded_uops(_PyUOpInstruction *buffer, int buffer_size)
buffer[last_set_ip].opcode = _SET_IP;
last_set_ip = -1;
}
+ if (opcode == _EXIT_TRACE) {
+ return pc + 1;
+ }
break;
}
case _JUMP_TO_TOP:
- case _EXIT_TRACE:
+ case _DYNAMIC_EXIT:
+ case _DEOPT:
return pc + 1;
}
}
@@ -518,7 +522,7 @@ remove_unneeded_uops(_PyUOpInstruction *buffer, int buffer_size)
// > 0 - length of optimized trace
int
_Py_uop_analyze_and_optimize(
- _PyInterpreterFrame *frame,
+ PyFunctionObject *func,
_PyUOpInstruction *buffer,
int length,
int curr_stacklen,
@@ -528,8 +532,8 @@ _Py_uop_analyze_and_optimize(
OPT_STAT_INC(optimizer_attempts);
length = optimize_uops(
- _PyFrame_GetFunction(frame), buffer,
- length, curr_stacklen, dependencies);
+ func, buffer,
+ length, curr_stacklen, dependencies);
if (length == 0) {
return length;
diff --git a/Python/optimizer_bytecodes.c b/Python/optimizer_bytecodes.c
index da3d3c96bc1..06fa8a4522a 100644
--- a/Python/optimizer_bytecodes.c
+++ b/Python/optimizer_bytecodes.c
@@ -342,7 +342,6 @@ dummy_func(void) {
int already_bool = optimize_to_bool(this_instr, ctx, value, &value);
if (!already_bool) {
sym_set_type(value, &PyBool_Type);
- value = sym_new_truthiness(ctx, value, true);
}
}
@@ -752,8 +751,14 @@ dummy_func(void) {
}
op(_PY_FRAME_KW, (callable, self_or_null, args[oparg], kwnames -- new_frame)) {
- new_frame = PyJitRef_NULL;
- ctx->done = true;
+ assert((this_instr + 2)->opcode == _PUSH_FRAME);
+ PyCodeObject *co = get_code_with_logging((this_instr + 2));
+ if (co == NULL) {
+ ctx->done = true;
+ break;
+ }
+
+ new_frame = PyJitRef_Wrap((JitOptSymbol *)frame_new(ctx, co, 0, NULL, 0));
}
op(_CHECK_AND_ALLOCATE_OBJECT, (type_version/2, callable, self_or_null, args[oparg] -- callable, self_or_null, args[oparg])) {
@@ -764,8 +769,20 @@ dummy_func(void) {
}
op(_CREATE_INIT_FRAME, (init, self, args[oparg] -- init_frame)) {
- init_frame = PyJitRef_NULL;
- ctx->done = true;
+ ctx->frame->stack_pointer = stack_pointer - oparg - 2;
+ _Py_UOpsAbstractFrame *shim = frame_new(ctx, (PyCodeObject *)&_Py_InitCleanup, 0, NULL, 0);
+ if (shim == NULL) {
+ break;
+ }
+ /* Push self onto stack of shim */
+ shim->stack[0] = self;
+ shim->stack_pointer++;
+ assert((int)(shim->stack_pointer - shim->stack) == 1);
+ ctx->frame = shim;
+ ctx->curr_frame_depth++;
+ assert((this_instr + 1)->opcode == _PUSH_FRAME);
+ PyCodeObject *co = get_code_with_logging((this_instr + 1));
+ init_frame = PyJitRef_Wrap((JitOptSymbol *)frame_new(ctx, co, 0, args-1, oparg+1));
}
op(_RETURN_VALUE, (retval -- res)) {
@@ -773,42 +790,65 @@ dummy_func(void) {
JitOptRef temp = PyJitRef_StripReferenceInfo(retval);
DEAD(retval);
SAVE_STACK();
- PyCodeObject *co = get_current_code_object(ctx);
ctx->frame->stack_pointer = stack_pointer;
- frame_pop(ctx);
+ PyCodeObject *returning_code = get_code_with_logging(this_instr);
+ if (returning_code == NULL) {
+ ctx->done = true;
+ break;
+ }
+ int returning_stacklevel = this_instr->operand1;
+ if (ctx->curr_frame_depth >= 2) {
+ PyCodeObject *expected_code = ctx->frames[ctx->curr_frame_depth - 2].code;
+ if (expected_code == returning_code) {
+ assert((this_instr + 1)->opcode == _GUARD_IP_RETURN_VALUE);
+ REPLACE_OP((this_instr + 1), _NOP, 0, 0);
+ }
+ }
+ if (frame_pop(ctx, returning_code, returning_stacklevel)) {
+ break;
+ }
stack_pointer = ctx->frame->stack_pointer;
- /* Stack space handling */
- assert(corresponding_check_stack == NULL);
- assert(co != NULL);
- int framesize = co->co_framesize;
- assert(framesize > 0);
- assert(framesize <= curr_space);
- curr_space -= framesize;
-
RELOAD_STACK();
res = temp;
}
op(_RETURN_GENERATOR, ( -- res)) {
SYNC_SP();
- PyCodeObject *co = get_current_code_object(ctx);
ctx->frame->stack_pointer = stack_pointer;
- frame_pop(ctx);
+ PyCodeObject *returning_code = get_code_with_logging(this_instr);
+ if (returning_code == NULL) {
+ ctx->done = true;
+ break;
+ }
+ _Py_BloomFilter_Add(dependencies, returning_code);
+ int returning_stacklevel = this_instr->operand1;
+ if (frame_pop(ctx, returning_code, returning_stacklevel)) {
+ break;
+ }
stack_pointer = ctx->frame->stack_pointer;
res = sym_new_unknown(ctx);
-
- /* Stack space handling */
- assert(corresponding_check_stack == NULL);
- assert(co != NULL);
- int framesize = co->co_framesize;
- assert(framesize > 0);
- assert(framesize <= curr_space);
- curr_space -= framesize;
}
- op(_YIELD_VALUE, (unused -- value)) {
- value = sym_new_unknown(ctx);
+ op(_YIELD_VALUE, (retval -- value)) {
+ // Mimics PyStackRef_MakeHeapSafe in the interpreter.
+ JitOptRef temp = PyJitRef_StripReferenceInfo(retval);
+ DEAD(retval);
+ SAVE_STACK();
+ ctx->frame->stack_pointer = stack_pointer;
+ PyCodeObject *returning_code = get_code_with_logging(this_instr);
+ if (returning_code == NULL) {
+ ctx->done = true;
+ break;
+ }
+ _Py_BloomFilter_Add(dependencies, returning_code);
+ int returning_stacklevel = this_instr->operand1;
+ if (frame_pop(ctx, returning_code, returning_stacklevel)) {
+ break;
+ }
+ stack_pointer = ctx->frame->stack_pointer;
+ RELOAD_STACK();
+ value = temp;
}
op(_GET_ITER, (iterable -- iter, index_or_null)) {
@@ -835,8 +875,6 @@ dummy_func(void) {
}
op(_CHECK_STACK_SPACE, (unused, unused, unused[oparg] -- unused, unused, unused[oparg])) {
- assert(corresponding_check_stack == NULL);
- corresponding_check_stack = this_instr;
}
op (_CHECK_STACK_SPACE_OPERAND, (framesize/2 -- )) {
@@ -848,38 +886,29 @@ dummy_func(void) {
op(_PUSH_FRAME, (new_frame -- )) {
SYNC_SP();
- ctx->frame->stack_pointer = stack_pointer;
+ if (!CURRENT_FRAME_IS_INIT_SHIM()) {
+ ctx->frame->stack_pointer = stack_pointer;
+ }
ctx->frame = (_Py_UOpsAbstractFrame *)PyJitRef_Unwrap(new_frame);
ctx->curr_frame_depth++;
stack_pointer = ctx->frame->stack_pointer;
uint64_t operand = this_instr->operand0;
- if (operand == 0 || (operand & 1)) {
- // It's either a code object or NULL
+ if (operand == 0) {
ctx->done = true;
break;
}
- PyFunctionObject *func = (PyFunctionObject *)operand;
- PyCodeObject *co = (PyCodeObject *)func->func_code;
- assert(PyFunction_Check(func));
- ctx->frame->func = func;
- /* Stack space handling */
- int framesize = co->co_framesize;
- assert(framesize > 0);
- curr_space += framesize;
- if (curr_space < 0 || curr_space > INT32_MAX) {
- // won't fit in signed 32-bit int
- ctx->done = true;
- break;
+ if (!(operand & 1)) {
+ PyFunctionObject *func = (PyFunctionObject *)operand;
+ // No need to re-add to dependencies here. Already
+ // handled by the tracer.
+ ctx->frame->func = func;
}
- max_space = curr_space > max_space ? curr_space : max_space;
- if (first_valid_check_stack == NULL) {
- first_valid_check_stack = corresponding_check_stack;
+ // Fixed calls don't need IP guards.
+ if ((this_instr-1)->opcode == _SAVE_RETURN_OFFSET ||
+ (this_instr-1)->opcode == _CREATE_INIT_FRAME) {
+ assert((this_instr+1)->opcode == _GUARD_IP__PUSH_FRAME);
+ REPLACE_OP(this_instr+1, _NOP, 0, 0);
}
- else if (corresponding_check_stack) {
- // delete all but the first valid _CHECK_STACK_SPACE
- corresponding_check_stack->opcode = _NOP;
- }
- corresponding_check_stack = NULL;
}
op(_UNPACK_SEQUENCE, (seq -- values[oparg], top[0])) {
@@ -1024,6 +1053,10 @@ dummy_func(void) {
ctx->done = true;
}
+ op(_DEOPT, (--)) {
+ ctx->done = true;
+ }
+
op(_REPLACE_WITH_TRUE, (value -- res)) {
REPLACE_OP(this_instr, _POP_TOP_LOAD_CONST_INLINE_BORROW, 0, (uintptr_t)Py_True);
res = sym_new_const(ctx, Py_True);
diff --git a/Python/optimizer_cases.c.h b/Python/optimizer_cases.c.h
index b08099d8e2f..01263fe8c7a 100644
--- a/Python/optimizer_cases.c.h
+++ b/Python/optimizer_cases.c.h
@@ -280,7 +280,6 @@
int already_bool = optimize_to_bool(this_instr, ctx, value, &value);
if (!already_bool) {
sym_set_type(value, &PyBool_Type);
- value = sym_new_truthiness(ctx, value, true);
}
stack_pointer[-1] = value;
break;
@@ -1116,16 +1115,24 @@
JitOptRef temp = PyJitRef_StripReferenceInfo(retval);
stack_pointer += -1;
assert(WITHIN_STACK_BOUNDS());
- PyCodeObject *co = get_current_code_object(ctx);
ctx->frame->stack_pointer = stack_pointer;
- frame_pop(ctx);
+ PyCodeObject *returning_code = get_code_with_logging(this_instr);
+ if (returning_code == NULL) {
+ ctx->done = true;
+ break;
+ }
+ int returning_stacklevel = this_instr->operand1;
+ if (ctx->curr_frame_depth >= 2) {
+ PyCodeObject *expected_code = ctx->frames[ctx->curr_frame_depth - 2].code;
+ if (expected_code == returning_code) {
+ assert((this_instr + 1)->opcode == _GUARD_IP_RETURN_VALUE);
+ REPLACE_OP((this_instr + 1), _NOP, 0, 0);
+ }
+ }
+ if (frame_pop(ctx, returning_code, returning_stacklevel)) {
+ break;
+ }
stack_pointer = ctx->frame->stack_pointer;
- assert(corresponding_check_stack == NULL);
- assert(co != NULL);
- int framesize = co->co_framesize;
- assert(framesize > 0);
- assert(framesize <= curr_space);
- curr_space -= framesize;
res = temp;
stack_pointer[0] = res;
stack_pointer += 1;
@@ -1167,9 +1174,28 @@
}
case _YIELD_VALUE: {
+ JitOptRef retval;
JitOptRef value;
- value = sym_new_unknown(ctx);
- stack_pointer[-1] = value;
+ retval = stack_pointer[-1];
+ JitOptRef temp = PyJitRef_StripReferenceInfo(retval);
+ stack_pointer += -1;
+ assert(WITHIN_STACK_BOUNDS());
+ ctx->frame->stack_pointer = stack_pointer;
+ PyCodeObject *returning_code = get_code_with_logging(this_instr);
+ if (returning_code == NULL) {
+ ctx->done = true;
+ break;
+ }
+ _Py_BloomFilter_Add(dependencies, returning_code);
+ int returning_stacklevel = this_instr->operand1;
+ if (frame_pop(ctx, returning_code, returning_stacklevel)) {
+ break;
+ }
+ stack_pointer = ctx->frame->stack_pointer;
+ value = temp;
+ stack_pointer[0] = value;
+ stack_pointer += 1;
+ assert(WITHIN_STACK_BOUNDS());
break;
}
@@ -2103,6 +2129,8 @@
break;
}
+ /* _JUMP_BACKWARD_NO_INTERRUPT is not a viable micro-op for tier 2 */
+
case _GET_LEN: {
JitOptRef obj;
JitOptRef len;
@@ -2557,8 +2585,6 @@
}
case _CHECK_STACK_SPACE: {
- assert(corresponding_check_stack == NULL);
- corresponding_check_stack = this_instr;
break;
}
@@ -2601,34 +2627,26 @@
new_frame = stack_pointer[-1];
stack_pointer += -1;
assert(WITHIN_STACK_BOUNDS());
- ctx->frame->stack_pointer = stack_pointer;
+ if (!CURRENT_FRAME_IS_INIT_SHIM()) {
+ ctx->frame->stack_pointer = stack_pointer;
+ }
ctx->frame = (_Py_UOpsAbstractFrame *)PyJitRef_Unwrap(new_frame);
ctx->curr_frame_depth++;
stack_pointer = ctx->frame->stack_pointer;
uint64_t operand = this_instr->operand0;
- if (operand == 0 || (operand & 1)) {
+ if (operand == 0) {
ctx->done = true;
break;
}
- PyFunctionObject *func = (PyFunctionObject *)operand;
- PyCodeObject *co = (PyCodeObject *)func->func_code;
- assert(PyFunction_Check(func));
- ctx->frame->func = func;
- int framesize = co->co_framesize;
- assert(framesize > 0);
- curr_space += framesize;
- if (curr_space < 0 || curr_space > INT32_MAX) {
- ctx->done = true;
- break;
+ if (!(operand & 1)) {
+ PyFunctionObject *func = (PyFunctionObject *)operand;
+ ctx->frame->func = func;
}
- max_space = curr_space > max_space ? curr_space : max_space;
- if (first_valid_check_stack == NULL) {
- first_valid_check_stack = corresponding_check_stack;
+ if ((this_instr-1)->opcode == _SAVE_RETURN_OFFSET ||
+ (this_instr-1)->opcode == _CREATE_INIT_FRAME) {
+ assert((this_instr+1)->opcode == _GUARD_IP__PUSH_FRAME);
+ REPLACE_OP(this_instr+1, _NOP, 0, 0);
}
- else if (corresponding_check_stack) {
- corresponding_check_stack->opcode = _NOP;
- }
- corresponding_check_stack = NULL;
break;
}
@@ -2761,9 +2779,24 @@
}
case _CREATE_INIT_FRAME: {
+ JitOptRef *args;
+ JitOptRef self;
JitOptRef init_frame;
- init_frame = PyJitRef_NULL;
- ctx->done = true;
+ args = &stack_pointer[-oparg];
+ self = stack_pointer[-1 - oparg];
+ ctx->frame->stack_pointer = stack_pointer - oparg - 2;
+ _Py_UOpsAbstractFrame *shim = frame_new(ctx, (PyCodeObject *)&_Py_InitCleanup, 0, NULL, 0);
+ if (shim == NULL) {
+ break;
+ }
+ shim->stack[0] = self;
+ shim->stack_pointer++;
+ assert((int)(shim->stack_pointer - shim->stack) == 1);
+ ctx->frame = shim;
+ ctx->curr_frame_depth++;
+ assert((this_instr + 1)->opcode == _PUSH_FRAME);
+ PyCodeObject *co = get_code_with_logging((this_instr + 1));
+ init_frame = PyJitRef_Wrap((JitOptSymbol *)frame_new(ctx, co, 0, args-1, oparg+1));
stack_pointer[-2 - oparg] = init_frame;
stack_pointer += -1 - oparg;
assert(WITHIN_STACK_BOUNDS());
@@ -2948,8 +2981,13 @@
case _PY_FRAME_KW: {
JitOptRef new_frame;
- new_frame = PyJitRef_NULL;
- ctx->done = true;
+ assert((this_instr + 2)->opcode == _PUSH_FRAME);
+ PyCodeObject *co = get_code_with_logging((this_instr + 2));
+ if (co == NULL) {
+ ctx->done = true;
+ break;
+ }
+ new_frame = PyJitRef_Wrap((JitOptSymbol *)frame_new(ctx, co, 0, NULL, 0));
stack_pointer[-3 - oparg] = new_frame;
stack_pointer += -2 - oparg;
assert(WITHIN_STACK_BOUNDS());
@@ -3005,17 +3043,19 @@
case _RETURN_GENERATOR: {
JitOptRef res;
- PyCodeObject *co = get_current_code_object(ctx);
ctx->frame->stack_pointer = stack_pointer;
- frame_pop(ctx);
+ PyCodeObject *returning_code = get_code_with_logging(this_instr);
+ if (returning_code == NULL) {
+ ctx->done = true;
+ break;
+ }
+ _Py_BloomFilter_Add(dependencies, returning_code);
+ int returning_stacklevel = this_instr->operand1;
+ if (frame_pop(ctx, returning_code, returning_stacklevel)) {
+ break;
+ }
stack_pointer = ctx->frame->stack_pointer;
res = sym_new_unknown(ctx);
- assert(corresponding_check_stack == NULL);
- assert(co != NULL);
- int framesize = co->co_framesize;
- assert(framesize > 0);
- assert(framesize <= curr_space);
- curr_space -= framesize;
stack_pointer[0] = res;
stack_pointer += 1;
assert(WITHIN_STACK_BOUNDS());
@@ -3265,6 +3305,10 @@
break;
}
+ case _DYNAMIC_EXIT: {
+ break;
+ }
+
case _CHECK_VALIDITY: {
break;
}
@@ -3399,6 +3443,7 @@
}
case _DEOPT: {
+ ctx->done = true;
break;
}
@@ -3418,3 +3463,23 @@
break;
}
+ case _COLD_DYNAMIC_EXIT: {
+ break;
+ }
+
+ case _GUARD_IP__PUSH_FRAME: {
+ break;
+ }
+
+ case _GUARD_IP_YIELD_VALUE: {
+ break;
+ }
+
+ case _GUARD_IP_RETURN_VALUE: {
+ break;
+ }
+
+ case _GUARD_IP_RETURN_GENERATOR: {
+ break;
+ }
+
diff --git a/Python/optimizer_symbols.c b/Python/optimizer_symbols.c
index 01cff0b014c..8a71eff465e 100644
--- a/Python/optimizer_symbols.c
+++ b/Python/optimizer_symbols.c
@@ -817,9 +817,14 @@ _Py_uop_frame_new(
JitOptRef *args,
int arg_len)
{
- assert(ctx->curr_frame_depth < MAX_ABSTRACT_FRAME_DEPTH);
+ if (ctx->curr_frame_depth >= MAX_ABSTRACT_FRAME_DEPTH) {
+ ctx->done = true;
+ ctx->out_of_space = true;
+ OPT_STAT_INC(optimizer_frame_overflow);
+ return NULL;
+ }
_Py_UOpsAbstractFrame *frame = &ctx->frames[ctx->curr_frame_depth];
-
+ frame->code = co;
frame->stack_len = co->co_stacksize;
frame->locals_len = co->co_nlocalsplus;
@@ -901,13 +906,42 @@ _Py_uop_abstractcontext_init(JitOptContext *ctx)
}
int
-_Py_uop_frame_pop(JitOptContext *ctx)
+_Py_uop_frame_pop(JitOptContext *ctx, PyCodeObject *co, int curr_stackentries)
{
_Py_UOpsAbstractFrame *frame = ctx->frame;
ctx->n_consumed = frame->locals;
+
ctx->curr_frame_depth--;
- assert(ctx->curr_frame_depth >= 1);
- ctx->frame = &ctx->frames[ctx->curr_frame_depth - 1];
+
+ if (ctx->curr_frame_depth >= 1) {
+ ctx->frame = &ctx->frames[ctx->curr_frame_depth - 1];
+
+ // We returned to the correct code. Nothing to do here.
+ if (co == ctx->frame->code) {
+ return 0;
+ }
+ // Else: the code we recorded doesn't match the code we *think* we're
+ // returning to. We could trace anything, we can't just return to the
+ // old frame. We have to restore what the tracer recorded
+ // as the traced next frame.
+ // Remove the current frame, and later swap it out with the right one.
+ else {
+ ctx->curr_frame_depth--;
+ }
+ }
+ // Else: trace stack underflow.
+
+ // This handles swapping out frames.
+ assert(curr_stackentries >= 1);
+ // -1 to stackentries as we push to the stack our return value after this.
+ _Py_UOpsAbstractFrame *new_frame = _Py_uop_frame_new(ctx, co, curr_stackentries - 1, NULL, 0);
+ if (new_frame == NULL) {
+ ctx->done = true;
+ return 1;
+ }
+
+ ctx->curr_frame_depth++;
+ ctx->frame = new_frame;
return 0;
}
diff --git a/Python/pystate.c b/Python/pystate.c
index 341c680a403..c12a1418e74 100644
--- a/Python/pystate.c
+++ b/Python/pystate.c
@@ -552,10 +552,6 @@ init_interpreter(PyInterpreterState *interp,
_Py_brc_init_state(interp);
#endif
-#ifdef _Py_TIER2
- // Ensure the buffer is to be set as NULL.
- interp->jit_uop_buffer = NULL;
-#endif
llist_init(&interp->mem_free_queue.head);
llist_init(&interp->asyncio_tasks_head);
interp->asyncio_tasks_lock = (PyMutex){0};
@@ -805,10 +801,6 @@ interpreter_clear(PyInterpreterState *interp, PyThreadState *tstate)
#ifdef _Py_TIER2
_Py_ClearExecutorDeletionList(interp);
- if (interp->jit_uop_buffer != NULL) {
- _PyObject_VirtualFree(interp->jit_uop_buffer, UOP_BUFFER_SIZE);
- interp->jit_uop_buffer = NULL;
- }
#endif
_PyAST_Fini(interp);
_PyAtExit_Fini(interp);
@@ -831,6 +823,14 @@ interpreter_clear(PyInterpreterState *interp, PyThreadState *tstate)
assert(cold->vm_data.warm);
_PyExecutor_Free(cold);
}
+
+ struct _PyExecutorObject *cold_dynamic = interp->cold_dynamic_executor;
+ if (cold_dynamic != NULL) {
+ interp->cold_dynamic_executor = NULL;
+ assert(cold_dynamic->vm_data.valid);
+ assert(cold_dynamic->vm_data.warm);
+ _PyExecutor_Free(cold_dynamic);
+ }
/* We don't clear sysdict and builtins until the end of this function.
Because clearing other attributes can execute arbitrary Python code
which requires sysdict and builtins. */
@@ -1501,6 +1501,9 @@ init_threadstate(_PyThreadStateImpl *_tstate,
_tstate->asyncio_running_loop = NULL;
_tstate->asyncio_running_task = NULL;
+#ifdef _Py_TIER2
+ _tstate->jit_tracer_state.code_buffer = NULL;
+#endif
tstate->delete_later = NULL;
llist_init(&_tstate->mem_free_queue);
@@ -1807,6 +1810,14 @@ tstate_delete_common(PyThreadState *tstate, int release_gil)
assert(tstate_impl->refcounts.values == NULL);
#endif
+#if _Py_TIER2
+ _PyThreadStateImpl *_tstate = (_PyThreadStateImpl *)tstate;
+ if (_tstate->jit_tracer_state.code_buffer != NULL) {
+ _PyObject_VirtualFree(_tstate->jit_tracer_state.code_buffer, UOP_BUFFER_SIZE);
+ _tstate->jit_tracer_state.code_buffer = NULL;
+ }
+#endif
+
HEAD_UNLOCK(runtime);
// XXX Unbind in PyThreadState_Clear(), or earlier
diff --git a/Tools/c-analyzer/cpython/ignored.tsv b/Tools/c-analyzer/cpython/ignored.tsv
index 4621ad250f4..bd4a8cf0d3e 100644
--- a/Tools/c-analyzer/cpython/ignored.tsv
+++ b/Tools/c-analyzer/cpython/ignored.tsv
@@ -359,6 +359,7 @@ Parser/parser.c - soft_keywords -
Parser/lexer/lexer.c - type_comment_prefix -
Python/ceval.c - _PyEval_BinaryOps -
Python/ceval.c - _Py_INTERPRETER_TRAMPOLINE_INSTRUCTIONS -
+Python/ceval.c - _Py_INTERPRETER_TRAMPOLINE_INSTRUCTIONS_PTR -
Python/codecs.c - Py_hexdigits -
Python/codecs.c - codecs_builtin_error_handlers -
Python/codecs.c - ucnhash_capi -
diff --git a/Tools/cases_generator/analyzer.py b/Tools/cases_generator/analyzer.py
index 9dd7e5dbfba..d39013db4f7 100644
--- a/Tools/cases_generator/analyzer.py
+++ b/Tools/cases_generator/analyzer.py
@@ -34,6 +34,8 @@ class Properties:
side_exit: bool
pure: bool
uses_opcode: bool
+ needs_guard_ip: bool
+ unpredictable_jump: bool
tier: int | None = None
const_oparg: int = -1
needs_prev: bool = False
@@ -75,6 +77,8 @@ def from_list(properties: list["Properties"]) -> "Properties":
pure=all(p.pure for p in properties),
needs_prev=any(p.needs_prev for p in properties),
no_save_ip=all(p.no_save_ip for p in properties),
+ needs_guard_ip=any(p.needs_guard_ip for p in properties),
+ unpredictable_jump=any(p.unpredictable_jump for p in properties),
)
@property
@@ -102,6 +106,8 @@ def infallible(self) -> bool:
side_exit=False,
pure=True,
no_save_ip=False,
+ needs_guard_ip=False,
+ unpredictable_jump=False,
)
@@ -692,6 +698,11 @@ def has_error_without_pop(op: parser.CodeDef) -> bool:
"PyStackRef_Wrap",
"PyStackRef_Unwrap",
"_PyLong_CheckExactAndCompact",
+ "_PyExecutor_FromExit",
+ "_PyJit_TryInitializeTracing",
+ "_Py_unset_eval_breaker_bit",
+ "_Py_set_eval_breaker_bit",
+ "trigger_backoff_counter",
)
@@ -882,6 +893,46 @@ def stmt_escapes(stmt: Stmt) -> bool:
else:
assert False, "Unexpected statement type"
+def stmt_has_jump_on_unpredictable_path_body(stmts: list[Stmt] | None, branches_seen: int) -> tuple[bool, int]:
+ if not stmts:
+ return False, branches_seen
+ predict = False
+ seen = 0
+ for st in stmts:
+ predict_body, seen_body = stmt_has_jump_on_unpredictable_path(st, branches_seen)
+ predict = predict or predict_body
+ seen += seen_body
+ return predict, seen
+
+def stmt_has_jump_on_unpredictable_path(stmt: Stmt, branches_seen: int) -> tuple[bool, int]:
+ if isinstance(stmt, BlockStmt):
+ return stmt_has_jump_on_unpredictable_path_body(stmt.body, branches_seen)
+ elif isinstance(stmt, SimpleStmt):
+ for tkn in stmt.contents:
+ if tkn.text == "JUMPBY":
+ return True, branches_seen
+ return False, branches_seen
+ elif isinstance(stmt, IfStmt):
+ predict, seen = stmt_has_jump_on_unpredictable_path(stmt.body, branches_seen)
+ if stmt.else_body:
+ predict_else, seen_else = stmt_has_jump_on_unpredictable_path(stmt.else_body, branches_seen)
+ return predict != predict_else, seen + seen_else + 1
+ return predict, seen + 1
+ elif isinstance(stmt, MacroIfStmt):
+ predict, seen = stmt_has_jump_on_unpredictable_path_body(stmt.body, branches_seen)
+ if stmt.else_body:
+ predict_else, seen_else = stmt_has_jump_on_unpredictable_path_body(stmt.else_body, branches_seen)
+ return predict != predict_else, seen + seen_else
+ return predict, seen
+ elif isinstance(stmt, ForStmt):
+ unpredictable, branches_seen = stmt_has_jump_on_unpredictable_path(stmt.body, branches_seen)
+ return unpredictable, branches_seen + 1
+ elif isinstance(stmt, WhileStmt):
+ unpredictable, branches_seen = stmt_has_jump_on_unpredictable_path(stmt.body, branches_seen)
+ return unpredictable, branches_seen + 1
+ else:
+ assert False, f"Unexpected statement type {stmt}"
+
def compute_properties(op: parser.CodeDef) -> Properties:
escaping_calls = find_escaping_api_calls(op)
@@ -909,6 +960,8 @@ def compute_properties(op: parser.CodeDef) -> Properties:
escapes = stmt_escapes(op.block)
pure = False if isinstance(op, parser.LabelDef) else "pure" in op.annotations
no_save_ip = False if isinstance(op, parser.LabelDef) else "no_save_ip" in op.annotations
+ unpredictable, branches_seen = stmt_has_jump_on_unpredictable_path(op.block, 0)
+ unpredictable_jump = False if isinstance(op, parser.LabelDef) else (unpredictable and branches_seen > 0)
return Properties(
escaping_calls=escaping_calls,
escapes=escapes,
@@ -932,6 +985,11 @@ def compute_properties(op: parser.CodeDef) -> Properties:
no_save_ip=no_save_ip,
tier=tier_variable(op),
needs_prev=variable_used(op, "prev_instr"),
+ needs_guard_ip=(isinstance(op, parser.InstDef)
+ and (unpredictable_jump and "replaced" not in op.annotations))
+ or variable_used(op, "LOAD_IP")
+ or variable_used(op, "DISPATCH_INLINED"),
+ unpredictable_jump=unpredictable_jump,
)
def expand(items: list[StackItem], oparg: int) -> list[StackItem]:
diff --git a/Tools/cases_generator/generators_common.py b/Tools/cases_generator/generators_common.py
index 61e855eb003..0b5f764ec52 100644
--- a/Tools/cases_generator/generators_common.py
+++ b/Tools/cases_generator/generators_common.py
@@ -7,6 +7,7 @@
analysis_error,
Label,
CodeSection,
+ Uop,
)
from cwriter import CWriter
from typing import Callable, TextIO, Iterator, Iterable
@@ -107,8 +108,9 @@ class Emitter:
labels: dict[str, Label]
_replacers: dict[str, ReplacementFunctionType]
cannot_escape: bool
+ jump_prefix: str
- def __init__(self, out: CWriter, labels: dict[str, Label], cannot_escape: bool = False):
+ def __init__(self, out: CWriter, labels: dict[str, Label], cannot_escape: bool = False, jump_prefix: str = ""):
self._replacers = {
"EXIT_IF": self.exit_if,
"AT_END_EXIT_IF": self.exit_if_after,
@@ -131,6 +133,7 @@ def __init__(self, out: CWriter, labels: dict[str, Label], cannot_escape: bool =
self.out = out
self.labels = labels
self.cannot_escape = cannot_escape
+ self.jump_prefix = jump_prefix
def dispatch(
self,
@@ -167,7 +170,7 @@ def deopt_if(
family_name = inst.family.name
self.emit(f"UPDATE_MISS_STATS({family_name});\n")
self.emit(f"assert(_PyOpcode_Deopt[opcode] == ({family_name}));\n")
- self.emit(f"JUMP_TO_PREDICTED({family_name});\n")
+ self.emit(f"JUMP_TO_PREDICTED({self.jump_prefix}{family_name});\n")
self.emit("}\n")
return not always_true(first_tkn)
@@ -198,10 +201,10 @@ def exit_if_after(
def goto_error(self, offset: int, storage: Storage) -> str:
if offset > 0:
- return f"JUMP_TO_LABEL(pop_{offset}_error);"
+ return f"{self.jump_prefix}JUMP_TO_LABEL(pop_{offset}_error);"
if offset < 0:
storage.copy().flush(self.out)
- return f"JUMP_TO_LABEL(error);"
+ return f"{self.jump_prefix}JUMP_TO_LABEL(error);"
def error_if(
self,
@@ -421,7 +424,7 @@ def goto_label(self, goto: Token, label: Token, storage: Storage) -> None:
elif storage.spilled:
raise analysis_error("Cannot jump from spilled label without reloading the stack pointer", goto)
self.out.start_line()
- self.out.emit("JUMP_TO_LABEL(")
+ self.out.emit(f"{self.jump_prefix}JUMP_TO_LABEL(")
self.out.emit(label)
self.out.emit(")")
@@ -731,6 +734,10 @@ def cflags(p: Properties) -> str:
flags.append("HAS_PURE_FLAG")
if p.no_save_ip:
flags.append("HAS_NO_SAVE_IP_FLAG")
+ if p.unpredictable_jump:
+ flags.append("HAS_UNPREDICTABLE_JUMP_FLAG")
+ if p.needs_guard_ip:
+ flags.append("HAS_NEEDS_GUARD_IP_FLAG")
if flags:
return " | ".join(flags)
else:
diff --git a/Tools/cases_generator/opcode_metadata_generator.py b/Tools/cases_generator/opcode_metadata_generator.py
index b649b381233..21ae785a0ec 100644
--- a/Tools/cases_generator/opcode_metadata_generator.py
+++ b/Tools/cases_generator/opcode_metadata_generator.py
@@ -56,6 +56,8 @@
"ERROR_NO_POP",
"NO_SAVE_IP",
"PERIODIC",
+ "UNPREDICTABLE_JUMP",
+ "NEEDS_GUARD_IP",
]
@@ -201,7 +203,7 @@ def generate_metadata_table(analysis: Analysis, out: CWriter) -> None:
out.emit("struct opcode_metadata {\n")
out.emit("uint8_t valid_entry;\n")
out.emit("uint8_t instr_format;\n")
- out.emit("uint16_t flags;\n")
+ out.emit("uint32_t flags;\n")
out.emit("};\n\n")
out.emit(
f"extern const struct opcode_metadata _PyOpcode_opcode_metadata[{table_size}];\n"
diff --git a/Tools/cases_generator/target_generator.py b/Tools/cases_generator/target_generator.py
index 324ef2773ab..36fa1d7fa49 100644
--- a/Tools/cases_generator/target_generator.py
+++ b/Tools/cases_generator/target_generator.py
@@ -31,6 +31,16 @@ def write_opcode_targets(analysis: Analysis, out: CWriter) -> None:
for target in targets:
out.emit(target)
out.emit("};\n")
+ targets = ["&&_unknown_opcode,\n"] * 256
+ for name, op in analysis.opmap.items():
+ if op < 256:
+ targets[op] = f"&&record_previous_inst,\n"
+ out.emit("#if _Py_TIER2\n")
+ out.emit("static void *opcode_tracing_targets_table[256] = {\n")
+ for target in targets:
+ out.emit(target)
+ out.emit("};\n")
+ out.emit(f"#endif\n")
out.emit("#else /* _Py_TAIL_CALL_INTERP */\n")
def function_proto(name: str) -> str:
@@ -38,7 +48,9 @@ def function_proto(name: str) -> str:
def write_tailcall_dispatch_table(analysis: Analysis, out: CWriter) -> None:
- out.emit("static py_tail_call_funcptr instruction_funcptr_table[256];\n")
+ out.emit("static py_tail_call_funcptr instruction_funcptr_handler_table[256];\n")
+ out.emit("\n")
+ out.emit("static py_tail_call_funcptr instruction_funcptr_tracing_table[256];\n")
out.emit("\n")
# Emit function prototypes for labels.
@@ -60,7 +72,7 @@ def write_tailcall_dispatch_table(analysis: Analysis, out: CWriter) -> None:
out.emit("\n")
# Emit the dispatch table.
- out.emit("static py_tail_call_funcptr instruction_funcptr_table[256] = {\n")
+ out.emit("static py_tail_call_funcptr instruction_funcptr_handler_table[256] = {\n")
for name in sorted(analysis.instructions.keys()):
out.emit(f"[{name}] = _TAIL_CALL_{name},\n")
named_values = analysis.opmap.values()
@@ -68,6 +80,16 @@ def write_tailcall_dispatch_table(analysis: Analysis, out: CWriter) -> None:
if rest not in named_values:
out.emit(f"[{rest}] = _TAIL_CALL_UNKNOWN_OPCODE,\n")
out.emit("};\n")
+
+ # Emit the tracing dispatch table.
+ out.emit("static py_tail_call_funcptr instruction_funcptr_tracing_table[256] = {\n")
+ for name in sorted(analysis.instructions.keys()):
+ out.emit(f"[{name}] = _TAIL_CALL_record_previous_inst,\n")
+ named_values = analysis.opmap.values()
+ for rest in range(256):
+ if rest not in named_values:
+ out.emit(f"[{rest}] = _TAIL_CALL_UNKNOWN_OPCODE,\n")
+ out.emit("};\n")
outfile.write("#endif /* _Py_TAIL_CALL_INTERP */\n")
arg_parser = argparse.ArgumentParser(
diff --git a/Tools/cases_generator/tier2_generator.py b/Tools/cases_generator/tier2_generator.py
index 1bb5f48658d..ac3e6b94afe 100644
--- a/Tools/cases_generator/tier2_generator.py
+++ b/Tools/cases_generator/tier2_generator.py
@@ -63,6 +63,7 @@ class Tier2Emitter(Emitter):
def __init__(self, out: CWriter, labels: dict[str, Label]):
super().__init__(out, labels)
self._replacers["oparg"] = self.oparg
+ self._replacers["IP_OFFSET_OF"] = self.ip_offset_of
def goto_error(self, offset: int, storage: Storage) -> str:
# To do: Add jump targets for popping values.
@@ -134,10 +135,30 @@ def oparg(
self.out.emit_at(uop.name[-1], tkn)
return True
+ def ip_offset_of(
+ self,
+ tkn: Token,
+ tkn_iter: TokenIterator,
+ uop: CodeSection,
+ storage: Storage,
+ inst: Instruction | None,
+ ) -> bool:
+ assert uop.name.startswith("_GUARD_IP")
+ # LPAREN
+ next(tkn_iter)
+ tok = next(tkn_iter)
+ self.emit(f" OFFSET_OF_{tok.text};\n")
+ # RPAREN
+ next(tkn_iter)
+ # SEMI
+ next(tkn_iter)
+ return True
-def write_uop(uop: Uop, emitter: Emitter, stack: Stack) -> Stack:
+def write_uop(uop: Uop, emitter: Emitter, stack: Stack, offset_strs: dict[str, tuple[str, str]]) -> Stack:
locals: dict[str, Local] = {}
try:
+ if name_offset_pair := offset_strs.get(uop.name):
+ emitter.emit(f"#define OFFSET_OF_{name_offset_pair[0]} ({name_offset_pair[1]})\n")
emitter.out.start_line()
if uop.properties.oparg:
emitter.emit("oparg = CURRENT_OPARG();\n")
@@ -158,6 +179,8 @@ def write_uop(uop: Uop, emitter: Emitter, stack: Stack) -> Stack:
idx += 1
_, storage = emitter.emit_tokens(uop, storage, None, False)
storage.flush(emitter.out)
+ if name_offset_pair:
+ emitter.emit(f"#undef OFFSET_OF_{name_offset_pair[0]}\n")
except StackError as ex:
raise analysis_error(ex.args[0], uop.body.open) from None
return storage.stack
@@ -165,6 +188,29 @@ def write_uop(uop: Uop, emitter: Emitter, stack: Stack) -> Stack:
SKIPS = ("_EXTENDED_ARG",)
+def populate_offset_strs(analysis: Analysis) -> dict[str, tuple[str, str]]:
+ offset_strs: dict[str, tuple[str, str]] = {}
+ for name, uop in analysis.uops.items():
+ if not f"_GUARD_IP_{name}" in analysis.uops:
+ continue
+ tkn_iter = uop.body.tokens()
+ found = False
+ offset_str = ""
+ for token in tkn_iter:
+ if token.kind == "IDENTIFIER" and token.text == "LOAD_IP":
+ if found:
+ raise analysis_error("Cannot have two LOAD_IP in a guarded single uop.", uop.body.open)
+ offset = []
+ while token.kind != "SEMI":
+ offset.append(token.text)
+ token = next(tkn_iter)
+ # 1: to remove the LOAD_IP text
+ offset_str = "".join(offset[1:])
+ found = True
+ assert offset_str
+ offset_strs[f"_GUARD_IP_{name}"] = (name, offset_str)
+ return offset_strs
+
def generate_tier2(
filenames: list[str], analysis: Analysis, outfile: TextIO, lines: bool
) -> None:
@@ -179,7 +225,9 @@ def generate_tier2(
)
out = CWriter(outfile, 2, lines)
emitter = Tier2Emitter(out, analysis.labels)
+ offset_strs = populate_offset_strs(analysis)
out.emit("\n")
+
for name, uop in analysis.uops.items():
if uop.properties.tier == 1:
continue
@@ -194,13 +242,15 @@ def generate_tier2(
out.emit(f"case {uop.name}: {{\n")
declare_variables(uop, out)
stack = Stack()
- stack = write_uop(uop, emitter, stack)
+ stack = write_uop(uop, emitter, stack, offset_strs)
out.start_line()
if not uop.properties.always_exits:
out.emit("break;\n")
out.start_line()
out.emit("}")
out.emit("\n\n")
+
+ out.emit("\n")
outfile.write("#undef TIER_TWO\n")
diff --git a/Tools/cases_generator/uop_metadata_generator.py b/Tools/cases_generator/uop_metadata_generator.py
index 1cc23837a72..0e0396e5143 100644
--- a/Tools/cases_generator/uop_metadata_generator.py
+++ b/Tools/cases_generator/uop_metadata_generator.py
@@ -23,13 +23,13 @@
def generate_names_and_flags(analysis: Analysis, out: CWriter) -> None:
- out.emit("extern const uint16_t _PyUop_Flags[MAX_UOP_ID+1];\n")
+ out.emit("extern const uint32_t _PyUop_Flags[MAX_UOP_ID+1];\n")
out.emit("typedef struct _rep_range { uint8_t start; uint8_t stop; } ReplicationRange;\n")
out.emit("extern const ReplicationRange _PyUop_Replication[MAX_UOP_ID+1];\n")
out.emit("extern const char * const _PyOpcode_uop_name[MAX_UOP_ID+1];\n\n")
out.emit("extern int _PyUop_num_popped(int opcode, int oparg);\n\n")
out.emit("#ifdef NEED_OPCODE_METADATA\n")
- out.emit("const uint16_t _PyUop_Flags[MAX_UOP_ID+1] = {\n")
+ out.emit("const uint32_t _PyUop_Flags[MAX_UOP_ID+1] = {\n")
for uop in analysis.uops.values():
if uop.is_viable() and uop.properties.tier != 1:
out.emit(f"[{uop.name}] = {cflags(uop.properties)},\n")
diff --git a/Tools/jit/template.c b/Tools/jit/template.c
index 2f146014a1c..857e926d119 100644
--- a/Tools/jit/template.c
+++ b/Tools/jit/template.c
@@ -55,13 +55,10 @@ do { \
__attribute__((musttail)) return jitted(frame, stack_pointer, tstate); \
} while (0)
-#undef GOTO_TIER_ONE
-#define GOTO_TIER_ONE(TARGET) \
-do { \
- tstate->current_executor = NULL; \
- _PyFrame_SetStackPointer(frame, stack_pointer); \
- return TARGET; \
-} while (0)
+#undef GOTO_TIER_ONE_SETUP
+#define GOTO_TIER_ONE_SETUP \
+ tstate->current_executor = NULL; \
+ _PyFrame_SetStackPointer(frame, stack_pointer);
#undef LOAD_IP
#define LOAD_IP(UNUSED) \
From 209eaff68c3b241c01aece14182cb9ced51526fc Mon Sep 17 00:00:00 2001
From: dr-carlos <77367421+dr-carlos@users.noreply.github.com>
Date: Fri, 14 Nov 2025 04:47:17 +1030
Subject: [PATCH 179/417] gh-137969: Fix double evaluation of `ForwardRef`s
which rely on globals (#140974)
---
Lib/annotationlib.py | 39 +++++++++-------
Lib/test/test_annotationlib.py | 45 +++++++++++++++++++
...-11-04-15-40-35.gh-issue-137969.9VZQVt.rst | 3 ++
3 files changed, 72 insertions(+), 15 deletions(-)
create mode 100644 Misc/NEWS.d/next/Library/2025-11-04-15-40-35.gh-issue-137969.9VZQVt.rst
diff --git a/Lib/annotationlib.py b/Lib/annotationlib.py
index 2166dbff0ee..33907b1fc2a 100644
--- a/Lib/annotationlib.py
+++ b/Lib/annotationlib.py
@@ -150,33 +150,42 @@ def evaluate(
if globals is None:
globals = {}
+ if type_params is None and owner is not None:
+ type_params = getattr(owner, "__type_params__", None)
+
if locals is None:
locals = {}
if isinstance(owner, type):
locals.update(vars(owner))
+ elif (
+ type_params is not None
+ or isinstance(self.__cell__, dict)
+ or self.__extra_names__
+ ):
+ # Create a new locals dict if necessary,
+ # to avoid mutating the argument.
+ locals = dict(locals)
- if type_params is None and owner is not None:
- # "Inject" type parameters into the local namespace
- # (unless they are shadowed by assignments *in* the local namespace),
- # as a way of emulating annotation scopes when calling `eval()`
- type_params = getattr(owner, "__type_params__", None)
-
- # Type parameters exist in their own scope, which is logically
- # between the locals and the globals. We simulate this by adding
- # them to the globals. Similar reasoning applies to nonlocals stored in cells.
- if type_params is not None or isinstance(self.__cell__, dict):
- globals = dict(globals)
+ # "Inject" type parameters into the local namespace
+ # (unless they are shadowed by assignments *in* the local namespace),
+ # as a way of emulating annotation scopes when calling `eval()`
if type_params is not None:
for param in type_params:
- globals[param.__name__] = param
+ locals.setdefault(param.__name__, param)
+
+ # Similar logic can be used for nonlocals, which should not
+ # override locals.
if isinstance(self.__cell__, dict):
- for cell_name, cell_value in self.__cell__.items():
+ for cell_name, cell in self.__cell__.items():
try:
- globals[cell_name] = cell_value.cell_contents
+ cell_value = cell.cell_contents
except ValueError:
pass
+ else:
+ locals.setdefault(cell_name, cell_value)
+
if self.__extra_names__:
- locals = {**locals, **self.__extra_names__}
+ locals.update(self.__extra_names__)
arg = self.__forward_arg__
if arg.isidentifier() and not keyword.iskeyword(arg):
diff --git a/Lib/test/test_annotationlib.py b/Lib/test/test_annotationlib.py
index 9f3275d5071..8208d0e9c94 100644
--- a/Lib/test/test_annotationlib.py
+++ b/Lib/test/test_annotationlib.py
@@ -2149,6 +2149,51 @@ def test_fwdref_invalid_syntax(self):
with self.assertRaises(SyntaxError):
fr.evaluate()
+ def test_re_evaluate_generics(self):
+ global global_alias
+
+ # If we've already run this test before,
+ # ensure the variable is still undefined
+ if "global_alias" in globals():
+ del global_alias
+
+ class C:
+ x: global_alias[int]
+
+ # Evaluate the ForwardRef once
+ evaluated = get_annotations(C, format=Format.FORWARDREF)["x"].evaluate(
+ format=Format.FORWARDREF
+ )
+
+ # Now define the global and ensure that the ForwardRef evaluates
+ global_alias = list
+ self.assertEqual(evaluated.evaluate(), list[int])
+
+ def test_fwdref_evaluate_argument_mutation(self):
+ class C[T]:
+ nonlocal alias
+ x: alias[T]
+
+ # Mutable arguments
+ globals_ = globals()
+ globals_copy = globals_.copy()
+ locals_ = locals()
+ locals_copy = locals_.copy()
+
+ # Evaluate the ForwardRef, ensuring we use __cell__ and type params
+ get_annotations(C, format=Format.FORWARDREF)["x"].evaluate(
+ globals=globals_,
+ locals=locals_,
+ type_params=C.__type_params__,
+ format=Format.FORWARDREF,
+ )
+
+ # Check if the passed in mutable arguments equal the originals
+ self.assertEqual(globals_, globals_copy)
+ self.assertEqual(locals_, locals_copy)
+
+ alias = list
+
def test_fwdref_final_class(self):
with self.assertRaises(TypeError):
class C(ForwardRef):
diff --git a/Misc/NEWS.d/next/Library/2025-11-04-15-40-35.gh-issue-137969.9VZQVt.rst b/Misc/NEWS.d/next/Library/2025-11-04-15-40-35.gh-issue-137969.9VZQVt.rst
new file mode 100644
index 00000000000..dfa582bdbc8
--- /dev/null
+++ b/Misc/NEWS.d/next/Library/2025-11-04-15-40-35.gh-issue-137969.9VZQVt.rst
@@ -0,0 +1,3 @@
+Fix :meth:`annotationlib.ForwardRef.evaluate` returning
+:class:`~annotationlib.ForwardRef` objects which don't update with new
+globals.
From a486d452c78a7dfcd42561f6c151bf1fef0a756e Mon Sep 17 00:00:00 2001
From: Osama Abdelkader <78818069+osamakader@users.noreply.github.com>
Date: Thu, 13 Nov 2025 20:05:28 +0100
Subject: [PATCH 180/417] gh-140601: Add ResourceWarning to iterparse when not
closed (GH-140603)
When iterparse() opens a file by filename and is not explicitly closed,
emit a ResourceWarning to alert developers of the resource leak.
Signed-off-by: Osama Abdelkader
Co-authored-by: Serhiy Storchaka
---
Doc/library/xml.etree.elementtree.rst | 4 ++
Doc/whatsnew/3.15.rst | 6 +++
Lib/test/test_xml_etree.py | 47 +++++++++++++++++++
Lib/xml/etree/ElementTree.py | 12 +++--
...-10-25-22-55-07.gh-issue-140601.In3MlS.rst | 4 ++
5 files changed, 69 insertions(+), 4 deletions(-)
create mode 100644 Misc/NEWS.d/next/Library/2025-10-25-22-55-07.gh-issue-140601.In3MlS.rst
diff --git a/Doc/library/xml.etree.elementtree.rst b/Doc/library/xml.etree.elementtree.rst
index 881708a4dd7..cbbc87b4721 100644
--- a/Doc/library/xml.etree.elementtree.rst
+++ b/Doc/library/xml.etree.elementtree.rst
@@ -656,6 +656,10 @@ Functions
.. versionchanged:: 3.13
Added the :meth:`!close` method.
+ .. versionchanged:: next
+ A :exc:`ResourceWarning` is now emitted if the iterator opened a file
+ and is not explicitly closed.
+
.. function:: parse(source, parser=None)
diff --git a/Doc/whatsnew/3.15.rst b/Doc/whatsnew/3.15.rst
index 895616e3049..31594a2e70b 100644
--- a/Doc/whatsnew/3.15.rst
+++ b/Doc/whatsnew/3.15.rst
@@ -1244,3 +1244,9 @@ that may require changes to your code.
* :meth:`~mmap.mmap.resize` has been removed on platforms that don't support the
underlying syscall, instead of raising a :exc:`SystemError`.
+
+* Resource warning is now emitted for unclosed
+ :func:`xml.etree.ElementTree.iterparse` iterator if it opened a file.
+ Use its :meth:`!close` method or the :func:`contextlib.closing` context
+ manager to close it.
+ (Contributed by Osama Abdelkader and Serhiy Storchaka in :gh:`140601`.)
diff --git a/Lib/test/test_xml_etree.py b/Lib/test/test_xml_etree.py
index 25c084c8b9c..87811199706 100644
--- a/Lib/test/test_xml_etree.py
+++ b/Lib/test/test_xml_etree.py
@@ -1436,17 +1436,39 @@ def test_nonexistent_file(self):
def test_resource_warnings_not_exhausted(self):
# Not exhausting the iterator still closes the underlying file (bpo-43292)
+ # Not closing before del should emit ResourceWarning
it = ET.iterparse(SIMPLE_XMLFILE)
with warnings_helper.check_no_resource_warning(self):
+ it.close()
del it
gc_collect()
+ it = ET.iterparse(SIMPLE_XMLFILE)
+ with self.assertWarns(ResourceWarning) as wm:
+ del it
+ gc_collect()
+ # Not 'unclosed file'.
+ self.assertIn('unclosed iterparse iterator', str(wm.warning))
+ self.assertIn(repr(SIMPLE_XMLFILE), str(wm.warning))
+ self.assertEqual(wm.filename, __file__)
+
it = ET.iterparse(SIMPLE_XMLFILE)
with warnings_helper.check_no_resource_warning(self):
+ action, elem = next(it)
+ it.close()
+ self.assertEqual((action, elem.tag), ('end', 'element'))
+ del it, elem
+ gc_collect()
+
+ it = ET.iterparse(SIMPLE_XMLFILE)
+ with self.assertWarns(ResourceWarning) as wm:
action, elem = next(it)
self.assertEqual((action, elem.tag), ('end', 'element'))
del it, elem
gc_collect()
+ self.assertIn('unclosed iterparse iterator', str(wm.warning))
+ self.assertIn(repr(SIMPLE_XMLFILE), str(wm.warning))
+ self.assertEqual(wm.filename, __file__)
def test_resource_warnings_failed_iteration(self):
self.addCleanup(os_helper.unlink, TESTFN)
@@ -1461,16 +1483,41 @@ def test_resource_warnings_failed_iteration(self):
next(it)
self.assertEqual(str(cm.exception),
'junk after document element: line 1, column 12')
+ it.close()
del cm, it
gc_collect()
+ it = ET.iterparse(TESTFN)
+ action, elem = next(it)
+ self.assertEqual((action, elem.tag), ('end', 'document'))
+ with self.assertWarns(ResourceWarning) as wm:
+ with self.assertRaises(ET.ParseError) as cm:
+ next(it)
+ self.assertEqual(str(cm.exception),
+ 'junk after document element: line 1, column 12')
+ del cm, it
+ gc_collect()
+ self.assertIn('unclosed iterparse iterator', str(wm.warning))
+ self.assertIn(repr(TESTFN), str(wm.warning))
+ self.assertEqual(wm.filename, __file__)
+
def test_resource_warnings_exhausted(self):
it = ET.iterparse(SIMPLE_XMLFILE)
with warnings_helper.check_no_resource_warning(self):
list(it)
+ it.close()
del it
gc_collect()
+ it = ET.iterparse(SIMPLE_XMLFILE)
+ with self.assertWarns(ResourceWarning) as wm:
+ list(it)
+ del it
+ gc_collect()
+ self.assertIn('unclosed iterparse iterator', str(wm.warning))
+ self.assertIn(repr(SIMPLE_XMLFILE), str(wm.warning))
+ self.assertEqual(wm.filename, __file__)
+
def test_close_not_exhausted(self):
iterparse = ET.iterparse
diff --git a/Lib/xml/etree/ElementTree.py b/Lib/xml/etree/ElementTree.py
index dafe5b1b8a0..d8c0b1b6216 100644
--- a/Lib/xml/etree/ElementTree.py
+++ b/Lib/xml/etree/ElementTree.py
@@ -1261,16 +1261,20 @@ def iterator(source):
gen = iterator(source)
class IterParseIterator(collections.abc.Iterator):
__next__ = gen.__next__
+
def close(self):
+ nonlocal close_source
if close_source:
source.close()
+ close_source = False
gen.close()
- def __del__(self):
- # TODO: Emit a ResourceWarning if it was not explicitly closed.
- # (When the close() method will be supported in all maintained Python versions.)
+ def __del__(self, _warn=warnings.warn):
if close_source:
- source.close()
+ try:
+ _warn(f"unclosed iterparse iterator {source.name!r}", ResourceWarning, stacklevel=2)
+ finally:
+ source.close()
it = IterParseIterator()
it.root = None
diff --git a/Misc/NEWS.d/next/Library/2025-10-25-22-55-07.gh-issue-140601.In3MlS.rst b/Misc/NEWS.d/next/Library/2025-10-25-22-55-07.gh-issue-140601.In3MlS.rst
new file mode 100644
index 00000000000..72666bb8224
--- /dev/null
+++ b/Misc/NEWS.d/next/Library/2025-10-25-22-55-07.gh-issue-140601.In3MlS.rst
@@ -0,0 +1,4 @@
+:func:`xml.etree.ElementTree.iterparse` now emits a :exc:`ResourceWarning`
+when the iterator is not explicitly closed and was opened with a filename.
+This helps developers identify and fix resource leaks. Patch by Osama
+Abdelkader.
From 4885ecfbda4cc792691e5d488ef6cb09727eb417 Mon Sep 17 00:00:00 2001
From: M Bussonnier
Date: Fri, 14 Nov 2025 04:18:54 +0100
Subject: [PATCH 181/417] gh-140790: pdb: Initialize instance variables in
Pdb.__init__ (#140791)
Initialize lineno, stack, curindex, curframe, currentbp, and _user_requested_quit attributes in `Pdb.__init__``.
---
Lib/pdb.py | 10 ++++++++--
.../2025-10-30-12-36-19.gh-issue-140790._3T6-N.rst | 1 +
2 files changed, 9 insertions(+), 2 deletions(-)
create mode 100644 Misc/NEWS.d/next/Library/2025-10-30-12-36-19.gh-issue-140790._3T6-N.rst
diff --git a/Lib/pdb.py b/Lib/pdb.py
index fdc74198582..b799a113503 100644
--- a/Lib/pdb.py
+++ b/Lib/pdb.py
@@ -398,6 +398,12 @@ def __init__(self, completekey='tab', stdin=None, stdout=None, skip=None,
self._current_task = None
+ self.lineno = None
+ self.stack = []
+ self.curindex = 0
+ self.curframe = None
+ self._user_requested_quit = False
+
def set_trace(self, frame=None, *, commands=None):
Pdb._last_pdb_instance = self
if frame is None:
@@ -474,7 +480,7 @@ def forget(self):
self.lineno = None
self.stack = []
self.curindex = 0
- if hasattr(self, 'curframe') and self.curframe:
+ if self.curframe:
self.curframe.f_globals.pop('__pdb_convenience_variables', None)
self.curframe = None
self.tb_lineno.clear()
@@ -1493,7 +1499,7 @@ def checkline(self, filename, lineno, module_globals=None):
"""
# this method should be callable before starting debugging, so default
# to "no globals" if there is no current frame
- frame = getattr(self, 'curframe', None)
+ frame = self.curframe
if module_globals is None:
module_globals = frame.f_globals if frame else None
line = linecache.getline(filename, lineno, module_globals)
diff --git a/Misc/NEWS.d/next/Library/2025-10-30-12-36-19.gh-issue-140790._3T6-N.rst b/Misc/NEWS.d/next/Library/2025-10-30-12-36-19.gh-issue-140790._3T6-N.rst
new file mode 100644
index 00000000000..03856f0b9b6
--- /dev/null
+++ b/Misc/NEWS.d/next/Library/2025-10-30-12-36-19.gh-issue-140790._3T6-N.rst
@@ -0,0 +1 @@
+Initialize all Pdb's instance variables in ``__init__``, remove some hasattr/getattr
From a4dd66275b62453bec055d730a8ce7173e519b6d Mon Sep 17 00:00:00 2001
From: Petr Viktorin
Date: Fri, 14 Nov 2025 10:38:49 +0100
Subject: [PATCH 182/417] gh-140550: Use a bool for the Py_mod_gil value
(GH-141519)
This needs a single bit, but was stored as a void* in the module
struct. This didn't matter due to packing, but now that there's
another bool in the struct, we can save a bit of memory by
making md_gil a bool.
Variables that changed type are renamed, to detect conflicts.
---
Include/internal/pycore_moduleobject.h | 2 +-
Lib/test/test_sys.py | 2 +-
Objects/moduleobject.c | 15 ++++++++-------
Python/import.c | 26 ++++++++++++++------------
4 files changed, 24 insertions(+), 21 deletions(-)
diff --git a/Include/internal/pycore_moduleobject.h b/Include/internal/pycore_moduleobject.h
index 6eef6eaa5df..9a62daf6621 100644
--- a/Include/internal/pycore_moduleobject.h
+++ b/Include/internal/pycore_moduleobject.h
@@ -30,7 +30,7 @@ typedef struct {
PyObject *md_name;
bool md_token_is_def; /* if true, `md_token` is the PyModuleDef */
#ifdef Py_GIL_DISABLED
- void *md_gil;
+ bool md_requires_gil;
#endif
Py_ssize_t md_state_size;
traverseproc md_state_traverse;
diff --git a/Lib/test/test_sys.py b/Lib/test/test_sys.py
index 798f58737b1..2f169c1165d 100644
--- a/Lib/test/test_sys.py
+++ b/Lib/test/test_sys.py
@@ -1725,7 +1725,7 @@ def get_gen(): yield 1
check(int(PyLong_BASE**2), vsize('') + 3*self.longdigit)
# module
if support.Py_GIL_DISABLED:
- md_gil = 'P'
+ md_gil = '?'
else:
md_gil = ''
check(unittest, size('PPPP?' + md_gil + 'NPPPPP'))
diff --git a/Objects/moduleobject.c b/Objects/moduleobject.c
index 9dee03bdb5e..6c1c5f5eb89 100644
--- a/Objects/moduleobject.c
+++ b/Objects/moduleobject.c
@@ -178,7 +178,7 @@ new_module_notrack(PyTypeObject *mt)
m->md_name = NULL;
m->md_token_is_def = false;
#ifdef Py_GIL_DISABLED
- m->md_gil = Py_MOD_GIL_USED;
+ m->md_requires_gil = true;
#endif
m->md_state_size = 0;
m->md_state_traverse = NULL;
@@ -361,7 +361,7 @@ _PyModule_CreateInitialized(PyModuleDef* module, int module_api_version)
m->md_token_is_def = true;
module_copy_members_from_deflike(m, module);
#ifdef Py_GIL_DISABLED
- m->md_gil = Py_MOD_GIL_USED;
+ m->md_requires_gil = true;
#endif
return (PyObject*)m;
}
@@ -380,7 +380,7 @@ module_from_def_and_spec(
int has_multiple_interpreters_slot = 0;
void *multiple_interpreters = (void *)0;
int has_gil_slot = 0;
- void *gil_slot = Py_MOD_GIL_USED;
+ bool requires_gil = true;
int has_execution_slots = 0;
const char *name;
int ret;
@@ -474,7 +474,7 @@ module_from_def_and_spec(
name);
goto error;
}
- gil_slot = cur_slot->value;
+ requires_gil = (cur_slot->value != Py_MOD_GIL_NOT_USED);
has_gil_slot = 1;
break;
case Py_mod_abi:
@@ -581,9 +581,9 @@ module_from_def_and_spec(
mod->md_token = token;
}
#ifdef Py_GIL_DISABLED
- mod->md_gil = gil_slot;
+ mod->md_requires_gil = requires_gil;
#else
- (void)gil_slot;
+ (void)requires_gil;
#endif
mod->md_exec = m_exec;
} else {
@@ -664,11 +664,12 @@ PyModule_FromSlotsAndSpec(const PyModuleDef_Slot *slots, PyObject *spec)
int
PyUnstable_Module_SetGIL(PyObject *module, void *gil)
{
+ bool requires_gil = (gil != Py_MOD_GIL_NOT_USED);
if (!PyModule_Check(module)) {
PyErr_BadInternalCall();
return -1;
}
- ((PyModuleObject *)module)->md_gil = gil;
+ ((PyModuleObject *)module)->md_requires_gil = requires_gil;
return 0;
}
#endif
diff --git a/Python/import.c b/Python/import.c
index 2afa7c15e6a..b05b40448d0 100644
--- a/Python/import.c
+++ b/Python/import.c
@@ -1017,9 +1017,10 @@ struct extensions_cache_value {
_Py_ext_module_origin origin;
#ifdef Py_GIL_DISABLED
- /* The module's md_gil slot, for legacy modules that are reinitialized from
- m_dict rather than calling their initialization function again. */
- void *md_gil;
+ /* The module's md_requires_gil member, for legacy modules that are
+ * reinitialized from m_dict rather than calling their initialization
+ * function again. */
+ bool md_requires_gil;
#endif
};
@@ -1350,7 +1351,7 @@ static struct extensions_cache_value *
_extensions_cache_set(PyObject *path, PyObject *name,
PyModuleDef *def, PyModInitFunction m_init,
Py_ssize_t m_index, PyObject *m_dict,
- _Py_ext_module_origin origin, void *md_gil)
+ _Py_ext_module_origin origin, bool requires_gil)
{
struct extensions_cache_value *value = NULL;
void *key = NULL;
@@ -1405,11 +1406,11 @@ _extensions_cache_set(PyObject *path, PyObject *name,
/* m_dict is set by set_cached_m_dict(). */
.origin=origin,
#ifdef Py_GIL_DISABLED
- .md_gil=md_gil,
+ .md_requires_gil=requires_gil,
#endif
};
#ifndef Py_GIL_DISABLED
- (void)md_gil;
+ (void)requires_gil;
#endif
if (init_cached_m_dict(newvalue, m_dict) < 0) {
goto finally;
@@ -1547,7 +1548,8 @@ _PyImport_CheckGILForModule(PyObject* module, PyObject *module_name)
}
if (!PyModule_Check(module) ||
- ((PyModuleObject *)module)->md_gil == Py_MOD_GIL_USED) {
+ ((PyModuleObject *)module)->md_requires_gil)
+ {
if (_PyEval_EnableGILPermanent(tstate)) {
int warn_result = PyErr_WarnFormat(
PyExc_RuntimeWarning,
@@ -1725,7 +1727,7 @@ struct singlephase_global_update {
Py_ssize_t m_index;
PyObject *m_dict;
_Py_ext_module_origin origin;
- void *md_gil;
+ bool md_requires_gil;
};
static struct extensions_cache_value *
@@ -1784,7 +1786,7 @@ update_global_state_for_extension(PyThreadState *tstate,
#endif
cached = _extensions_cache_set(
path, name, def, m_init, singlephase->m_index, m_dict,
- singlephase->origin, singlephase->md_gil);
+ singlephase->origin, singlephase->md_requires_gil);
if (cached == NULL) {
// XXX Ignore this error? Doing so would effectively
// mark the module as not loadable.
@@ -1873,7 +1875,7 @@ reload_singlephase_extension(PyThreadState *tstate,
if (def->m_base.m_copy != NULL) {
// For non-core modules, fetch the GIL slot that was stored by
// import_run_extension().
- ((PyModuleObject *)mod)->md_gil = cached->md_gil;
+ ((PyModuleObject *)mod)->md_requires_gil = cached->md_requires_gil;
}
#endif
/* We can't set mod->md_def if it's missing,
@@ -2128,7 +2130,7 @@ import_run_extension(PyThreadState *tstate, PyModInitFunction p0,
.m_index=def->m_base.m_index,
.origin=info->origin,
#ifdef Py_GIL_DISABLED
- .md_gil=((PyModuleObject *)mod)->md_gil,
+ .md_requires_gil=((PyModuleObject *)mod)->md_requires_gil,
#endif
};
// gh-88216: Extensions and def->m_base.m_copy can be updated
@@ -2323,7 +2325,7 @@ _PyImport_FixupBuiltin(PyThreadState *tstate, PyObject *mod, const char *name,
.origin=_Py_ext_module_origin_CORE,
#ifdef Py_GIL_DISABLED
/* Unused when m_dict == NULL. */
- .md_gil=NULL,
+ .md_requires_gil=false,
#endif
};
cached = update_global_state_for_extension(
From 1e4e59bb3714ba7c6b6297f1a74e231b056f004c Mon Sep 17 00:00:00 2001
From: Itamar Oren
Date: Fri, 14 Nov 2025 01:43:25 -0800
Subject: [PATCH 183/417] gh-116146: Add C-API to create module from spec and
initfunc (GH-139196)
Co-authored-by: Kumar Aditya
Co-authored-by: Petr Viktorin
Co-authored-by: Victor Stinner
---
Doc/c-api/import.rst | 21 ++++
Doc/whatsnew/3.15.rst | 4 +
Include/cpython/import.h | 7 ++
Lib/test/test_embed.py | 25 ++++
...-11-08-10-51-50.gh-issue-116146.pCmx6L.rst | 2 +
Programs/_testembed.c | 111 ++++++++++++++++++
Python/import.c | 74 ++++++++----
7 files changed, 223 insertions(+), 21 deletions(-)
create mode 100644 Misc/NEWS.d/next/C_API/2025-11-08-10-51-50.gh-issue-116146.pCmx6L.rst
diff --git a/Doc/c-api/import.rst b/Doc/c-api/import.rst
index 8eabc0406b1..24e673d3d13 100644
--- a/Doc/c-api/import.rst
+++ b/Doc/c-api/import.rst
@@ -333,3 +333,24 @@ Importing Modules
strings instead of Python :class:`str` objects.
.. versionadded:: 3.14
+
+.. c:function:: PyObject* PyImport_CreateModuleFromInitfunc(PyObject *spec, PyObject* (*initfunc)(void))
+
+ This function is a building block that enables embedders to implement
+ the :py:meth:`~importlib.abc.Loader.create_module` step of custom
+ static extension importers (e.g. of statically-linked extensions).
+
+ *spec* must be a :class:`~importlib.machinery.ModuleSpec` object.
+
+ *initfunc* must be an :ref:`initialization function `,
+ the same as for :c:func:`PyImport_AppendInittab`.
+
+ On success, create and return a module object.
+ This module will not be initialized; call :c:func:`!PyModule_Exec`
+ to initialize it.
+ (Custom importers should do this in their
+ :py:meth:`~importlib.abc.Loader.exec_module` method.)
+
+ On error, return NULL with an exception set.
+
+ .. versionadded:: next
diff --git a/Doc/whatsnew/3.15.rst b/Doc/whatsnew/3.15.rst
index 31594a2e70b..9393b65ed8e 100644
--- a/Doc/whatsnew/3.15.rst
+++ b/Doc/whatsnew/3.15.rst
@@ -1080,6 +1080,10 @@ New features
thread state.
(Contributed by Victor Stinner in :gh:`139653`.)
+* Add a new :c:func:`PyImport_CreateModuleFromInitfunc` C-API for creating
+ a module from a *spec* and *initfunc*.
+ (Contributed by Itamar Oren in :gh:`116146`.)
+
Changed C APIs
--------------
diff --git a/Include/cpython/import.h b/Include/cpython/import.h
index 0ce0b1ee6cc..149a20af8b9 100644
--- a/Include/cpython/import.h
+++ b/Include/cpython/import.h
@@ -10,6 +10,13 @@ struct _inittab {
PyAPI_DATA(struct _inittab *) PyImport_Inittab;
PyAPI_FUNC(int) PyImport_ExtendInittab(struct _inittab *newtab);
+// Custom importers may use this API to initialize statically linked
+// extension modules directly from a spec and init function,
+// without needing to go through inittab
+PyAPI_FUNC(PyObject *) PyImport_CreateModuleFromInitfunc(
+ PyObject *spec,
+ PyObject *(*initfunc)(void));
+
struct _frozen {
const char *name; /* ASCII encoded string */
const unsigned char *code;
diff --git a/Lib/test/test_embed.py b/Lib/test/test_embed.py
index 1933f691a78..1078796eae8 100644
--- a/Lib/test/test_embed.py
+++ b/Lib/test/test_embed.py
@@ -239,6 +239,31 @@ def test_repeated_init_and_inittab(self):
lines = "\n".join(lines) + "\n"
self.assertEqual(out, lines)
+ def test_create_module_from_initfunc(self):
+ out, err = self.run_embedded_interpreter("test_create_module_from_initfunc")
+ if support.Py_GIL_DISABLED:
+ # the test imports a singlephase init extension, so it emits a warning
+ # under the free-threaded build
+ expected_runtime_warning = (
+ "RuntimeWarning: The global interpreter lock (GIL)"
+ " has been enabled to load module 'embedded_ext'"
+ )
+ filtered_err_lines = [
+ line
+ for line in err.strip().splitlines()
+ if expected_runtime_warning not in line
+ ]
+ self.assertEqual(filtered_err_lines, [])
+ else:
+ self.assertEqual(err, "")
+ self.assertEqual(out,
+ "\n"
+ "my_test_extension.executed='yes'\n"
+ "my_test_extension.exec_slot_ran='yes'\n"
+ "\n"
+ "embedded_ext.executed='yes'\n"
+ )
+
def test_forced_io_encoding(self):
# Checks forced configuration of embedded interpreter IO streams
env = dict(os.environ, PYTHONIOENCODING="utf-8:surrogateescape")
diff --git a/Misc/NEWS.d/next/C_API/2025-11-08-10-51-50.gh-issue-116146.pCmx6L.rst b/Misc/NEWS.d/next/C_API/2025-11-08-10-51-50.gh-issue-116146.pCmx6L.rst
new file mode 100644
index 00000000000..be8043e26dd
--- /dev/null
+++ b/Misc/NEWS.d/next/C_API/2025-11-08-10-51-50.gh-issue-116146.pCmx6L.rst
@@ -0,0 +1,2 @@
+Add a new :c:func:`PyImport_CreateModuleFromInitfunc` C-API for creating a
+module from a *spec* and *initfunc*. Patch by Itamar Oren.
diff --git a/Programs/_testembed.c b/Programs/_testembed.c
index d3600fecbe2..27224e508bd 100644
--- a/Programs/_testembed.c
+++ b/Programs/_testembed.c
@@ -166,6 +166,8 @@ static PyModuleDef embedded_ext = {
static PyObject*
PyInit_embedded_ext(void)
{
+ // keep this as a single-phase initialization module;
+ // see test_create_module_from_initfunc
return PyModule_Create(&embedded_ext);
}
@@ -1894,8 +1896,16 @@ static int test_initconfig_exit(void)
}
+int
+extension_module_exec(PyObject *mod)
+{
+ return PyModule_AddStringConstant(mod, "exec_slot_ran", "yes");
+}
+
+
static PyModuleDef_Slot extension_slots[] = {
{Py_mod_gil, Py_MOD_GIL_NOT_USED},
+ {Py_mod_exec, extension_module_exec},
{0, NULL}
};
@@ -2213,6 +2223,106 @@ static int test_repeated_init_and_inittab(void)
return 0;
}
+static PyObject*
+create_module(PyObject* self, PyObject* spec)
+{
+ PyObject *name = PyObject_GetAttrString(spec, "name");
+ if (!name) {
+ return NULL;
+ }
+ if (PyUnicode_EqualToUTF8(name, "my_test_extension")) {
+ Py_DECREF(name);
+ return PyImport_CreateModuleFromInitfunc(spec, init_my_test_extension);
+ }
+ if (PyUnicode_EqualToUTF8(name, "embedded_ext")) {
+ Py_DECREF(name);
+ return PyImport_CreateModuleFromInitfunc(spec, PyInit_embedded_ext);
+ }
+ PyErr_Format(PyExc_LookupError, "static module %R not found", name);
+ Py_DECREF(name);
+ return NULL;
+}
+
+static PyObject*
+exec_module(PyObject* self, PyObject* mod)
+{
+ if (PyModule_Exec(mod) < 0) {
+ return NULL;
+ }
+ Py_RETURN_NONE;
+}
+
+static PyMethodDef create_static_module_methods[] = {
+ {"create_module", create_module, METH_O, NULL},
+ {"exec_module", exec_module, METH_O, NULL},
+ {}
+};
+
+static struct PyModuleDef create_static_module_def = {
+ PyModuleDef_HEAD_INIT,
+ .m_name = "create_static_module",
+ .m_size = 0,
+ .m_methods = create_static_module_methods,
+ .m_slots = extension_slots,
+};
+
+PyMODINIT_FUNC PyInit_create_static_module(void) {
+ return PyModuleDef_Init(&create_static_module_def);
+}
+
+static int
+test_create_module_from_initfunc(void)
+{
+ wchar_t* argv[] = {
+ PROGRAM_NAME,
+ L"-c",
+ // Multi-phase initialization
+ L"import my_test_extension;"
+ L"print(my_test_extension);"
+ L"print(f'{my_test_extension.executed=}');"
+ L"print(f'{my_test_extension.exec_slot_ran=}');"
+ // Single-phase initialization
+ L"import embedded_ext;"
+ L"print(embedded_ext);"
+ L"print(f'{embedded_ext.executed=}');"
+ };
+ PyConfig config;
+ if (PyImport_AppendInittab("create_static_module",
+ &PyInit_create_static_module) != 0) {
+ fprintf(stderr, "PyImport_AppendInittab() failed\n");
+ return 1;
+ }
+ PyConfig_InitPythonConfig(&config);
+ config.isolated = 1;
+ config_set_argv(&config, Py_ARRAY_LENGTH(argv), argv);
+ init_from_config_clear(&config);
+ int result = PyRun_SimpleString(
+ "import sys\n"
+ "from importlib.util import spec_from_loader\n"
+ "import create_static_module\n"
+ "class StaticExtensionImporter:\n"
+ " _ORIGIN = \"static-extension\"\n"
+ " @classmethod\n"
+ " def find_spec(cls, fullname, path, target=None):\n"
+ " if fullname in {'my_test_extension', 'embedded_ext'}:\n"
+ " return spec_from_loader(fullname, cls, origin=cls._ORIGIN)\n"
+ " return None\n"
+ " @staticmethod\n"
+ " def create_module(spec):\n"
+ " return create_static_module.create_module(spec)\n"
+ " @staticmethod\n"
+ " def exec_module(module):\n"
+ " create_static_module.exec_module(module)\n"
+ " module.executed = 'yes'\n"
+ "sys.meta_path.append(StaticExtensionImporter)\n"
+ );
+ if (result < 0) {
+ fprintf(stderr, "PyRun_SimpleString() failed\n");
+ return 1;
+ }
+ return Py_RunMain();
+}
+
static void wrap_allocator(PyMemAllocatorEx *allocator);
static void unwrap_allocator(PyMemAllocatorEx *allocator);
@@ -2396,6 +2506,7 @@ static struct TestCase TestCases[] = {
#endif
{"test_get_incomplete_frame", test_get_incomplete_frame},
{"test_gilstate_after_finalization", test_gilstate_after_finalization},
+ {"test_create_module_from_initfunc", test_create_module_from_initfunc},
{NULL, NULL}
};
diff --git a/Python/import.c b/Python/import.c
index b05b40448d0..9ab2d3b3552 100644
--- a/Python/import.c
+++ b/Python/import.c
@@ -2364,8 +2364,23 @@ is_builtin(PyObject *name)
return 0;
}
+static PyModInitFunction
+lookup_inittab_initfunc(const struct _Py_ext_module_loader_info* info)
+{
+ for (struct _inittab *p = INITTAB; p->name != NULL; p++) {
+ if (_PyUnicode_EqualToASCIIString(info->name, p->name)) {
+ return (PyModInitFunction)p->initfunc;
+ }
+ }
+ // not found
+ return NULL;
+}
+
static PyObject*
-create_builtin(PyThreadState *tstate, PyObject *name, PyObject *spec)
+create_builtin(
+ PyThreadState *tstate, PyObject *name,
+ PyObject *spec,
+ PyModInitFunction initfunc)
{
struct _Py_ext_module_loader_info info;
if (_Py_ext_module_loader_info_init_for_builtin(&info, name) < 0) {
@@ -2396,25 +2411,15 @@ create_builtin(PyThreadState *tstate, PyObject *name, PyObject *spec)
_extensions_cache_delete(info.path, info.name);
}
- struct _inittab *found = NULL;
- for (struct _inittab *p = INITTAB; p->name != NULL; p++) {
- if (_PyUnicode_EqualToASCIIString(info.name, p->name)) {
- found = p;
- break;
- }
- }
- if (found == NULL) {
- // not found
- mod = Py_NewRef(Py_None);
- goto finally;
- }
-
- PyModInitFunction p0 = (PyModInitFunction)found->initfunc;
+ PyModInitFunction p0 = initfunc;
if (p0 == NULL) {
- /* Cannot re-init internal module ("sys" or "builtins") */
- assert(is_core_module(tstate->interp, info.name, info.path));
- mod = import_add_module(tstate, info.name);
- goto finally;
+ p0 = lookup_inittab_initfunc(&info);
+ if (p0 == NULL) {
+ /* Cannot re-init internal module ("sys" or "builtins") */
+ assert(is_core_module(tstate->interp, info.name, info.path));
+ mod = import_add_module(tstate, info.name);
+ goto finally;
+ }
}
#ifdef Py_GIL_DISABLED
@@ -2440,6 +2445,33 @@ create_builtin(PyThreadState *tstate, PyObject *name, PyObject *spec)
return mod;
}
+PyObject*
+PyImport_CreateModuleFromInitfunc(
+ PyObject *spec, PyObject *(*initfunc)(void))
+{
+ if (initfunc == NULL) {
+ PyErr_BadInternalCall();
+ return NULL;
+ }
+
+ PyThreadState *tstate = _PyThreadState_GET();
+
+ PyObject *name = PyObject_GetAttr(spec, &_Py_ID(name));
+ if (name == NULL) {
+ return NULL;
+ }
+
+ if (!PyUnicode_Check(name)) {
+ PyErr_Format(PyExc_TypeError,
+ "spec name must be string, not %T", name);
+ Py_DECREF(name);
+ return NULL;
+ }
+
+ PyObject *mod = create_builtin(tstate, name, spec, initfunc);
+ Py_DECREF(name);
+ return mod;
+}
/*****************************/
/* the builtin modules table */
@@ -3209,7 +3241,7 @@ bootstrap_imp(PyThreadState *tstate)
}
// Create the _imp module from its definition.
- PyObject *mod = create_builtin(tstate, name, spec);
+ PyObject *mod = create_builtin(tstate, name, spec, NULL);
Py_CLEAR(name);
Py_DECREF(spec);
if (mod == NULL) {
@@ -4369,7 +4401,7 @@ _imp_create_builtin(PyObject *module, PyObject *spec)
return NULL;
}
- PyObject *mod = create_builtin(tstate, name, spec);
+ PyObject *mod = create_builtin(tstate, name, spec, NULL);
Py_DECREF(name);
return mod;
}
From 181a2f4f2e3bed8dc6be5630e9bfb3362194ab3a Mon Sep 17 00:00:00 2001
From: Hugo van Kemenade <1324225+hugovk@users.noreply.github.com>
Date: Fri, 14 Nov 2025 11:59:19 +0200
Subject: [PATCH 184/417] gh-139596: Cease caching config.cache & ccache in GH
Actions (#141451)
---
.github/workflows/build.yml | 5 -----
.github/workflows/reusable-context.yml | 9 ---------
.github/workflows/reusable-macos.yml | 3 ---
.github/workflows/reusable-san.yml | 3 ---
.github/workflows/reusable-ubuntu.yml | 3 ---
.github/workflows/reusable-wasi.yml | 6 +-----
.gitignore | 1 -
7 files changed, 1 insertion(+), 29 deletions(-)
diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml
index a0f60c30ac8..8e15400e497 100644
--- a/.github/workflows/build.yml
+++ b/.github/workflows/build.yml
@@ -205,7 +205,6 @@ jobs:
free-threading: true
uses: ./.github/workflows/reusable-macos.yml
with:
- config_hash: ${{ needs.build-context.outputs.config-hash }}
free-threading: ${{ matrix.free-threading }}
os: ${{ matrix.os }}
@@ -237,7 +236,6 @@ jobs:
bolt: true
uses: ./.github/workflows/reusable-ubuntu.yml
with:
- config_hash: ${{ needs.build-context.outputs.config-hash }}
bolt-optimizations: ${{ matrix.bolt }}
free-threading: ${{ matrix.free-threading }}
os: ${{ matrix.os }}
@@ -414,8 +412,6 @@ jobs:
needs: build-context
if: needs.build-context.outputs.run-tests == 'true'
uses: ./.github/workflows/reusable-wasi.yml
- with:
- config_hash: ${{ needs.build-context.outputs.config-hash }}
test-hypothesis:
name: "Hypothesis tests on Ubuntu"
@@ -600,7 +596,6 @@ jobs:
uses: ./.github/workflows/reusable-san.yml
with:
sanitizer: ${{ matrix.sanitizer }}
- config_hash: ${{ needs.build-context.outputs.config-hash }}
free-threading: ${{ matrix.free-threading }}
cross-build-linux:
diff --git a/.github/workflows/reusable-context.yml b/.github/workflows/reusable-context.yml
index d2668ddcac1..66c7cc47de0 100644
--- a/.github/workflows/reusable-context.yml
+++ b/.github/workflows/reusable-context.yml
@@ -17,9 +17,6 @@ on: # yamllint disable-line rule:truthy
# || 'falsy-branch'
# }}
#
- config-hash:
- description: Config hash value for use in cache keys
- value: ${{ jobs.compute-changes.outputs.config-hash }} # str
run-docs:
description: Whether to build the docs
value: ${{ jobs.compute-changes.outputs.run-docs }} # bool
@@ -42,7 +39,6 @@ jobs:
runs-on: ubuntu-latest
timeout-minutes: 10
outputs:
- config-hash: ${{ steps.config-hash.outputs.hash }}
run-ci-fuzz: ${{ steps.changes.outputs.run-ci-fuzz }}
run-docs: ${{ steps.changes.outputs.run-docs }}
run-tests: ${{ steps.changes.outputs.run-tests }}
@@ -100,8 +96,3 @@ jobs:
GITHUB_EVENT_NAME: ${{ github.event_name }}
CCF_TARGET_REF: ${{ github.base_ref || github.event.repository.default_branch }}
CCF_HEAD_REF: ${{ github.event.pull_request.head.sha || github.sha }}
-
- - name: Compute hash for config cache key
- id: config-hash
- run: |
- echo "hash=${{ hashFiles('configure', 'configure.ac', '.github/workflows/build.yml') }}" >> "$GITHUB_OUTPUT"
diff --git a/.github/workflows/reusable-macos.yml b/.github/workflows/reusable-macos.yml
index d85c46b96f8..98d557ba1ea 100644
--- a/.github/workflows/reusable-macos.yml
+++ b/.github/workflows/reusable-macos.yml
@@ -3,9 +3,6 @@ name: Reusable macOS
on:
workflow_call:
inputs:
- config_hash:
- required: true
- type: string
free-threading:
required: false
type: boolean
diff --git a/.github/workflows/reusable-san.yml b/.github/workflows/reusable-san.yml
index 7fe96d1b238..c601d0b7338 100644
--- a/.github/workflows/reusable-san.yml
+++ b/.github/workflows/reusable-san.yml
@@ -6,9 +6,6 @@ on:
sanitizer:
required: true
type: string
- config_hash:
- required: true
- type: string
free-threading:
description: Whether to use free-threaded mode
required: false
diff --git a/.github/workflows/reusable-ubuntu.yml b/.github/workflows/reusable-ubuntu.yml
index 7b93b5f51b0..0c1ebe29ae3 100644
--- a/.github/workflows/reusable-ubuntu.yml
+++ b/.github/workflows/reusable-ubuntu.yml
@@ -3,9 +3,6 @@ name: Reusable Ubuntu
on:
workflow_call:
inputs:
- config_hash:
- required: true
- type: string
bolt-optimizations:
description: Whether to enable BOLT optimizations
required: false
diff --git a/.github/workflows/reusable-wasi.yml b/.github/workflows/reusable-wasi.yml
index 8f412288f53..a309ef4e7f4 100644
--- a/.github/workflows/reusable-wasi.yml
+++ b/.github/workflows/reusable-wasi.yml
@@ -2,10 +2,6 @@ name: Reusable WASI
on:
workflow_call:
- inputs:
- config_hash:
- required: true
- type: string
env:
FORCE_COLOR: 1
@@ -53,7 +49,7 @@ jobs:
- name: "Configure build Python"
run: python3 Tools/wasm/wasi configure-build-python -- --config-cache --with-pydebug
- name: "Make build Python"
- run: python3 Tools/wasm/wasi.py make-build-python
+ run: python3 Tools/wasm/wasi make-build-python
- name: "Configure host"
# `--with-pydebug` inferred from configure-build-python
run: python3 Tools/wasm/wasi configure-host -- --config-cache
diff --git a/.gitignore b/.gitignore
index 2bf4925647d..4ea2fd96554 100644
--- a/.gitignore
+++ b/.gitignore
@@ -135,7 +135,6 @@ Tools/unicode/data/
/config.log
/config.status
/config.status.lineno
-# hendrikmuhs/ccache-action@v1
/.ccache
/cross-build/
/jit_stencils*.h
From 3bacae55980561cb99095a20a70c45d6174e056d Mon Sep 17 00:00:00 2001
From: Victor Stinner
Date: Fri, 14 Nov 2025 11:13:24 +0100
Subject: [PATCH 185/417] gh-131510: Use PyUnstable_Unicode_GET_CACHED_HASH()
(GH-141520)
Replace code that directly accesses PyASCIIObject.hash with
PyUnstable_Unicode_GET_CACHED_HASH().
Remove redundant "assert(PyUnicode_Check(op))" from
PyUnstable_Unicode_GET_CACHED_HASH(), _PyASCIIObject_CAST() already
implements the check.
---
Include/cpython/unicodeobject.h | 1 -
Include/internal/pycore_object.h | 3 +--
Objects/dictobject.c | 3 +--
Objects/typeobject.c | 2 +-
4 files changed, 3 insertions(+), 6 deletions(-)
diff --git a/Include/cpython/unicodeobject.h b/Include/cpython/unicodeobject.h
index 73e3bc44d6c..2853d24c34b 100644
--- a/Include/cpython/unicodeobject.h
+++ b/Include/cpython/unicodeobject.h
@@ -301,7 +301,6 @@ static inline Py_ssize_t PyUnicode_GET_LENGTH(PyObject *op) {
/* Returns the cached hash, or -1 if not cached yet. */
static inline Py_hash_t
PyUnstable_Unicode_GET_CACHED_HASH(PyObject *op) {
- assert(PyUnicode_Check(op));
#ifdef Py_GIL_DISABLED
return _Py_atomic_load_ssize_relaxed(&_PyASCIIObject_CAST(op)->hash);
#else
diff --git a/Include/internal/pycore_object.h b/Include/internal/pycore_object.h
index 980d6d7764b..fb50acd62da 100644
--- a/Include/internal/pycore_object.h
+++ b/Include/internal/pycore_object.h
@@ -863,8 +863,7 @@ static inline Py_hash_t
_PyObject_HashFast(PyObject *op)
{
if (PyUnicode_CheckExact(op)) {
- Py_hash_t hash = FT_ATOMIC_LOAD_SSIZE_RELAXED(
- _PyASCIIObject_CAST(op)->hash);
+ Py_hash_t hash = PyUnstable_Unicode_GET_CACHED_HASH(op);
if (hash != -1) {
return hash;
}
diff --git a/Objects/dictobject.c b/Objects/dictobject.c
index 65eed151c28..14de21f3c67 100644
--- a/Objects/dictobject.c
+++ b/Objects/dictobject.c
@@ -400,8 +400,7 @@ static int _PyObject_InlineValuesConsistencyCheck(PyObject *obj);
static inline Py_hash_t
unicode_get_hash(PyObject *o)
{
- assert(PyUnicode_CheckExact(o));
- return FT_ATOMIC_LOAD_SSIZE_RELAXED(_PyASCIIObject_CAST(o)->hash);
+ return PyUnstable_Unicode_GET_CACHED_HASH(o);
}
/* Print summary info about the state of the optimized allocator */
diff --git a/Objects/typeobject.c b/Objects/typeobject.c
index 58228d62485..61bcc21ce13 100644
--- a/Objects/typeobject.c
+++ b/Objects/typeobject.c
@@ -6036,7 +6036,7 @@ static PyObject *
update_cache(struct type_cache_entry *entry, PyObject *name, unsigned int version_tag, PyObject *value)
{
_Py_atomic_store_ptr_relaxed(&entry->value, value); /* borrowed */
- assert(_PyASCIIObject_CAST(name)->hash != -1);
+ assert(PyUnstable_Unicode_GET_CACHED_HASH(name) != -1);
OBJECT_STAT_INC_COND(type_cache_collisions, entry->name != Py_None && entry->name != name);
// We're releasing this under the lock for simplicity sake because it's always a
// exact unicode object or Py_None so it's safe to do so.
From 5ac0b55ebc792936184f8e08697e60d5b3f8b946 Mon Sep 17 00:00:00 2001
From: Petr Viktorin
Date: Fri, 14 Nov 2025 11:22:18 +0100
Subject: [PATCH 186/417] gh-141376: Remove exceptions from `make smelly`
(GH-141392)
* Don't ignore initialized data and BSS
* Remove exceptions for _init and _fini
---
Tools/build/smelly.py | 11 -----------
1 file changed, 11 deletions(-)
diff --git a/Tools/build/smelly.py b/Tools/build/smelly.py
index 9a360412a73..424fa6ad4a1 100755
--- a/Tools/build/smelly.py
+++ b/Tools/build/smelly.py
@@ -21,8 +21,6 @@
})
IGNORED_EXTENSION = "_ctypes_test"
-# Ignore constructor and destructor functions
-IGNORED_SYMBOLS = {'_init', '_fini'}
def is_local_symbol_type(symtype):
@@ -34,19 +32,12 @@ def is_local_symbol_type(symtype):
if symtype.islower() and symtype not in "uvw":
return True
- # Ignore the initialized data section (d and D) and the BSS data
- # section. For example, ignore "__bss_start (type: B)"
- # and "_edata (type: D)".
- if symtype in "bBdD":
- return True
-
return False
def get_exported_symbols(library, dynamic=False):
print(f"Check that {library} only exports symbols starting with Py or _Py")
- # Only look at dynamic symbols
args = ['nm', '--no-sort']
if dynamic:
args.append('--dynamic')
@@ -89,8 +80,6 @@ def get_smelly_symbols(stdout, dynamic=False):
if is_local_symbol_type(symtype):
local_symbols.append(result)
- elif symbol in IGNORED_SYMBOLS:
- local_symbols.append(result)
else:
smelly_symbols.append(result)
From ef90261be508b97d682589aac8f00065a9585683 Mon Sep 17 00:00:00 2001
From: Stan Ulbrych <89152624+StanFromIreland@users.noreply.github.com>
Date: Fri, 14 Nov 2025 11:20:36 +0000
Subject: [PATCH 187/417] gh-141004: Document `PyOS_InterruptOccurred`
(GH-141526)
---
Doc/c-api/sys.rst | 18 ++++++++++++++++++
1 file changed, 18 insertions(+)
diff --git a/Doc/c-api/sys.rst b/Doc/c-api/sys.rst
index 336e3ef9640..ee73c1c8ada 100644
--- a/Doc/c-api/sys.rst
+++ b/Doc/c-api/sys.rst
@@ -123,6 +123,24 @@ Operating System Utilities
This is a thin wrapper around either :c:func:`!sigaction` or :c:func:`!signal`. Do
not call those functions directly!
+
+.. c:function:: int PyOS_InterruptOccurred(void)
+
+ Check if a :c:macro:`!SIGINT` signal has been received.
+
+ Returns ``1`` if a :c:macro:`!SIGINT` has occurred and clears the signal flag,
+ or ``0`` otherwise.
+
+ In most cases, you should prefer :c:func:`PyErr_CheckSignals` over this function.
+ :c:func:`!PyErr_CheckSignals` invokes the appropriate signal handlers
+ for all pending signals, allowing Python code to handle the signal properly.
+ This function only detects :c:macro:`!SIGINT` and does not invoke any Python
+ signal handlers.
+
+ This function is async-signal-safe and this function cannot fail.
+ The caller must hold an :term:`attached thread state`.
+
+
.. c:function:: wchar_t* Py_DecodeLocale(const char* arg, size_t *size)
.. warning::
From c10fa5be6167b1338ad194f9fe4be4782e025175 Mon Sep 17 00:00:00 2001
From: Peter Bierma
Date: Fri, 14 Nov 2025 09:22:36 -0500
Subject: [PATCH 188/417] gh-131229: Temporarily skip
`test_basic_multiple_interpreters_deleted_no_reset` (GH-141552)
This is a temporary band-aid to unblock other PRs.
Co-authored-by: Kumar Aditya
---
Lib/test/test_import/__init__.py | 1 +
1 file changed, 1 insertion(+)
diff --git a/Lib/test/test_import/__init__.py b/Lib/test/test_import/__init__.py
index fe669bb04df..fd9750eae80 100644
--- a/Lib/test/test_import/__init__.py
+++ b/Lib/test/test_import/__init__.py
@@ -3261,6 +3261,7 @@ def test_basic_multiple_interpreters_main_no_reset(self):
# * m_copy was copied from interp2 (was from interp1)
# * module's global state was updated, not reset
+ @unittest.skip("gh-131229: This is suddenly very flaky")
@no_rerun(reason="rerun not possible; module state is never cleared (see gh-102251)")
@requires_subinterpreters
def test_basic_multiple_interpreters_deleted_no_reset(self):
From 8deaa9393eadf84e6e571be611e0c5a377abf7cd Mon Sep 17 00:00:00 2001
From: Serhiy Storchaka
Date: Fri, 14 Nov 2025 16:49:28 +0200
Subject: [PATCH 189/417] gh-122255: Synchronize warnings in C and Python
implementations of the warnings module (GH-122824)
In the linecache module and in the Python implementation of the
warnings module, a DeprecationWarning is issued when
m.__loader__ differs from m.__spec__.loader (like in the C
implementation of the warnings module).
---
Lib/linecache.py | 63 +++++++++++++++----
Lib/test/test_linecache.py | 32 ++++++++--
Lib/test/test_warnings/__init__.py | 5 +-
...-08-08-12-39-36.gh-issue-122255.J_gU8Y.rst | 4 ++
4 files changed, 82 insertions(+), 22 deletions(-)
create mode 100644 Misc/NEWS.d/next/Library/2024-08-08-12-39-36.gh-issue-122255.J_gU8Y.rst
diff --git a/Lib/linecache.py b/Lib/linecache.py
index ef3b2d9136b..b5bf9dbdd3c 100644
--- a/Lib/linecache.py
+++ b/Lib/linecache.py
@@ -224,21 +224,58 @@ def lazycache(filename, module_globals):
def _make_lazycache_entry(filename, module_globals):
if not filename or (filename.startswith('<') and filename.endswith('>')):
return None
- # Try for a __loader__, if available
- if module_globals and '__name__' in module_globals:
- spec = module_globals.get('__spec__')
- name = getattr(spec, 'name', None) or module_globals['__name__']
- loader = getattr(spec, 'loader', None)
- if loader is None:
- loader = module_globals.get('__loader__')
- get_source = getattr(loader, 'get_source', None)
- if name and get_source:
- def get_lines(name=name, *args, **kwargs):
- return get_source(name, *args, **kwargs)
- return (get_lines,)
- return None
+ if module_globals is not None and not isinstance(module_globals, dict):
+ raise TypeError(f'module_globals must be a dict, not {type(module_globals).__qualname__}')
+ if not module_globals or '__name__' not in module_globals:
+ return None
+ spec = module_globals.get('__spec__')
+ name = getattr(spec, 'name', None) or module_globals['__name__']
+ if name is None:
+ return None
+
+ loader = _bless_my_loader(module_globals)
+ if loader is None:
+ return None
+
+ get_source = getattr(loader, 'get_source', None)
+ if get_source is None:
+ return None
+
+ def get_lines(name=name, *args, **kwargs):
+ return get_source(name, *args, **kwargs)
+ return (get_lines,)
+
+def _bless_my_loader(module_globals):
+ # Similar to _bless_my_loader() in importlib._bootstrap_external,
+ # but always emits warnings instead of errors.
+ loader = module_globals.get('__loader__')
+ if loader is None and '__spec__' not in module_globals:
+ return None
+ spec = module_globals.get('__spec__')
+
+ # The __main__ module has __spec__ = None.
+ if spec is None and module_globals.get('__name__') == '__main__':
+ return loader
+
+ spec_loader = getattr(spec, 'loader', None)
+ if spec_loader is None:
+ import warnings
+ warnings.warn(
+ 'Module globals is missing a __spec__.loader',
+ DeprecationWarning)
+ return loader
+
+ assert spec_loader is not None
+ if loader is not None and loader != spec_loader:
+ import warnings
+ warnings.warn(
+ 'Module globals; __loader__ != __spec__.loader',
+ DeprecationWarning)
+ return loader
+
+ return spec_loader
def _register_code(code, string, name):
diff --git a/Lib/test/test_linecache.py b/Lib/test/test_linecache.py
index 02f65338428..fcd94edc611 100644
--- a/Lib/test/test_linecache.py
+++ b/Lib/test/test_linecache.py
@@ -259,22 +259,44 @@ def raise_memoryerror(*args, **kwargs):
def test_loader(self):
filename = 'scheme://path'
- for loader in (None, object(), NoSourceLoader()):
+ linecache.clearcache()
+ module_globals = {'__name__': 'a.b.c', '__loader__': None}
+ self.assertEqual(linecache.getlines(filename, module_globals), [])
+
+ for loader in object(), NoSourceLoader():
linecache.clearcache()
module_globals = {'__name__': 'a.b.c', '__loader__': loader}
- self.assertEqual(linecache.getlines(filename, module_globals), [])
+ with self.assertWarns(DeprecationWarning) as w:
+ self.assertEqual(linecache.getlines(filename, module_globals), [])
+ self.assertEqual(str(w.warning),
+ 'Module globals is missing a __spec__.loader')
linecache.clearcache()
module_globals = {'__name__': 'a.b.c', '__loader__': FakeLoader()}
- self.assertEqual(linecache.getlines(filename, module_globals),
- ['source for a.b.c\n'])
+ with self.assertWarns(DeprecationWarning) as w:
+ self.assertEqual(linecache.getlines(filename, module_globals),
+ ['source for a.b.c\n'])
+ self.assertEqual(str(w.warning),
+ 'Module globals is missing a __spec__.loader')
- for spec in (None, object(), ModuleSpec('', FakeLoader())):
+ for spec in None, object():
linecache.clearcache()
module_globals = {'__name__': 'a.b.c', '__loader__': FakeLoader(),
'__spec__': spec}
+ with self.assertWarns(DeprecationWarning) as w:
+ self.assertEqual(linecache.getlines(filename, module_globals),
+ ['source for a.b.c\n'])
+ self.assertEqual(str(w.warning),
+ 'Module globals is missing a __spec__.loader')
+
+ linecache.clearcache()
+ module_globals = {'__name__': 'a.b.c', '__loader__': FakeLoader(),
+ '__spec__': ModuleSpec('', FakeLoader())}
+ with self.assertWarns(DeprecationWarning) as w:
self.assertEqual(linecache.getlines(filename, module_globals),
['source for a.b.c\n'])
+ self.assertEqual(str(w.warning),
+ 'Module globals; __loader__ != __spec__.loader')
linecache.clearcache()
spec = ModuleSpec('x.y.z', FakeLoader())
diff --git a/Lib/test/test_warnings/__init__.py b/Lib/test/test_warnings/__init__.py
index e6666ddc638..a6af5057cc8 100644
--- a/Lib/test/test_warnings/__init__.py
+++ b/Lib/test/test_warnings/__init__.py
@@ -727,7 +727,7 @@ def check_module_globals(self, module_globals):
def check_module_globals_error(self, module_globals, errmsg, errtype=ValueError):
if self.module is py_warnings:
- self.check_module_globals(module_globals)
+ self.check_module_globals_deprecated(module_globals, errmsg)
return
with self.module.catch_warnings(record=True) as w:
self.module.filterwarnings('always')
@@ -738,9 +738,6 @@ def check_module_globals_error(self, module_globals, errmsg, errtype=ValueError)
self.assertEqual(len(w), 0)
def check_module_globals_deprecated(self, module_globals, msg):
- if self.module is py_warnings:
- self.check_module_globals(module_globals)
- return
with self.module.catch_warnings(record=True) as w:
self.module.filterwarnings('always')
self.module.warn_explicit(
diff --git a/Misc/NEWS.d/next/Library/2024-08-08-12-39-36.gh-issue-122255.J_gU8Y.rst b/Misc/NEWS.d/next/Library/2024-08-08-12-39-36.gh-issue-122255.J_gU8Y.rst
new file mode 100644
index 00000000000..63e71c19f8b
--- /dev/null
+++ b/Misc/NEWS.d/next/Library/2024-08-08-12-39-36.gh-issue-122255.J_gU8Y.rst
@@ -0,0 +1,4 @@
+In the :mod:`linecache` module and in the Python implementation of the
+:mod:`warnings` module, a ``DeprecationWarning`` is issued when
+``mod.__loader__`` differs from ``mod.__spec__.loader`` (like in the C
+implementation of the :mod:`!warnings` module).
From 49e74210cb652d8bd538a4cc887f507396cfc893 Mon Sep 17 00:00:00 2001
From: Petr Viktorin
Date: Fri, 14 Nov 2025 15:50:03 +0100
Subject: [PATCH 190/417] gh-139344: Remove pending removal notice for
undeprecated importlib.resources API (GH-141507)
---
Doc/deprecations/pending-removal-in-3.13.rst | 12 ------------
1 file changed, 12 deletions(-)
diff --git a/Doc/deprecations/pending-removal-in-3.13.rst b/Doc/deprecations/pending-removal-in-3.13.rst
index 2fd2f12cc6a..d5b8c80e8f9 100644
--- a/Doc/deprecations/pending-removal-in-3.13.rst
+++ b/Doc/deprecations/pending-removal-in-3.13.rst
@@ -38,15 +38,3 @@ APIs:
* :meth:`!unittest.TestProgram.usageExit` (:gh:`67048`)
* :class:`!webbrowser.MacOSX` (:gh:`86421`)
* :class:`classmethod` descriptor chaining (:gh:`89519`)
-* :mod:`importlib.resources` deprecated methods:
-
- * ``contents()``
- * ``is_resource()``
- * ``open_binary()``
- * ``open_text()``
- * ``path()``
- * ``read_binary()``
- * ``read_text()``
-
- Use :func:`importlib.resources.files` instead. Refer to `importlib-resources: Migrating from Legacy
- `_ (:gh:`106531`)
From 10bec7c1eb3ee27f490a067426eef452b15f78f9 Mon Sep 17 00:00:00 2001
From: Sergey Miryanov
Date: Fri, 14 Nov 2025 19:52:01 +0500
Subject: [PATCH 191/417] GH-141312: Allow only integers to
longrangeiter_setstate state (GH-141317)
This fixes an assertion error when the new computed start is not an integer.
---
Lib/test/test_range.py | 10 ++++++++++
.../2025-11-10-23-07-06.gh-issue-141312.H-58GB.rst | 2 ++
Objects/rangeobject.c | 5 +++++
3 files changed, 17 insertions(+)
create mode 100644 Misc/NEWS.d/next/Core_and_Builtins/2025-11-10-23-07-06.gh-issue-141312.H-58GB.rst
diff --git a/Lib/test/test_range.py b/Lib/test/test_range.py
index 3870b153688..2c9c290e890 100644
--- a/Lib/test/test_range.py
+++ b/Lib/test/test_range.py
@@ -470,6 +470,16 @@ def test_iterator_setstate(self):
it.__setstate__(2**64 - 7)
self.assertEqual(list(it), [12, 10])
+ def test_iterator_invalid_setstate(self):
+ for invalid_value in (1.0, ""):
+ ranges = (('rangeiter', range(10, 100, 2)),
+ ('longrangeiter', range(10, 2**65, 2)))
+ for rng_name, rng in ranges:
+ with self.subTest(invalid_value=invalid_value, range=rng_name):
+ it = iter(rng)
+ with self.assertRaises(TypeError):
+ it.__setstate__(invalid_value)
+
def test_odd_bug(self):
# This used to raise a "SystemError: NULL result without error"
# because the range validation step was eating the exception
diff --git a/Misc/NEWS.d/next/Core_and_Builtins/2025-11-10-23-07-06.gh-issue-141312.H-58GB.rst b/Misc/NEWS.d/next/Core_and_Builtins/2025-11-10-23-07-06.gh-issue-141312.H-58GB.rst
new file mode 100644
index 00000000000..fdb136cef3f
--- /dev/null
+++ b/Misc/NEWS.d/next/Core_and_Builtins/2025-11-10-23-07-06.gh-issue-141312.H-58GB.rst
@@ -0,0 +1,2 @@
+Fix the assertion failure in the ``__setstate__`` method of the range iterator
+when a non-integer argument is passed. Patch by Sergey Miryanov.
diff --git a/Objects/rangeobject.c b/Objects/rangeobject.c
index f8cdfe68a64..e93346fb277 100644
--- a/Objects/rangeobject.c
+++ b/Objects/rangeobject.c
@@ -1042,6 +1042,11 @@ longrangeiter_reduce(PyObject *op, PyObject *Py_UNUSED(ignored))
static PyObject *
longrangeiter_setstate(PyObject *op, PyObject *state)
{
+ if (!PyLong_CheckExact(state)) {
+ PyErr_Format(PyExc_TypeError, "state must be an int, not %T", state);
+ return NULL;
+ }
+
longrangeiterobject *r = (longrangeiterobject*)op;
PyObject *zero = _PyLong_GetZero(); // borrowed reference
int cmp;
From fa245df4a0848c15cf8d907c10fc92819994b866 Mon Sep 17 00:00:00 2001
From: Sergey Miryanov
Date: Fri, 14 Nov 2025 19:55:04 +0500
Subject: [PATCH 192/417] GH-141509: Fix warning about remaining
subinterpreters (GH-141528)
Co-authored-by: Peter Bierma
---
Lib/test/test_interpreters/test_api.py | 2 +-
.../2025-11-14-00-19-45.gh-issue-141528.VWdax1.rst | 3 +++
Python/pylifecycle.c | 2 +-
3 files changed, 5 insertions(+), 2 deletions(-)
create mode 100644 Misc/NEWS.d/next/Core_and_Builtins/2025-11-14-00-19-45.gh-issue-141528.VWdax1.rst
diff --git a/Lib/test/test_interpreters/test_api.py b/Lib/test/test_interpreters/test_api.py
index 9a5ee03e472..fd9e46bf335 100644
--- a/Lib/test/test_interpreters/test_api.py
+++ b/Lib/test/test_interpreters/test_api.py
@@ -432,7 +432,7 @@ def test_cleanup_in_repl(self):
exit()"""
stdout, stderr = repl.communicate(script)
self.assertIsNone(stderr)
- self.assertIn(b"remaining subinterpreters", stdout)
+ self.assertIn(b"Interpreter.close()", stdout)
self.assertNotIn(b"Traceback", stdout)
@support.requires_subprocess()
diff --git a/Misc/NEWS.d/next/Core_and_Builtins/2025-11-14-00-19-45.gh-issue-141528.VWdax1.rst b/Misc/NEWS.d/next/Core_and_Builtins/2025-11-14-00-19-45.gh-issue-141528.VWdax1.rst
new file mode 100644
index 00000000000..a51aa495228
--- /dev/null
+++ b/Misc/NEWS.d/next/Core_and_Builtins/2025-11-14-00-19-45.gh-issue-141528.VWdax1.rst
@@ -0,0 +1,3 @@
+Suggest using :meth:`concurrent.interpreters.Interpreter.close` instead of the
+private ``_interpreters.destroy`` function when warning about remaining subinterpreters.
+Patch by Sergey Miryanov.
diff --git a/Python/pylifecycle.c b/Python/pylifecycle.c
index 805805ef188..67368b5ce07 100644
--- a/Python/pylifecycle.c
+++ b/Python/pylifecycle.c
@@ -2643,7 +2643,7 @@ finalize_subinterpreters(void)
(void)PyErr_WarnEx(
PyExc_RuntimeWarning,
"remaining subinterpreters; "
- "destroy them with _interpreters.destroy()",
+ "close them with Interpreter.close()",
0);
/* Swap out the current tstate, which we know must belong
From a415a1812c4d7798131d077c8776503bb3e1844f Mon Sep 17 00:00:00 2001
From: Victor Stinner
Date: Fri, 14 Nov 2025 15:56:37 +0100
Subject: [PATCH 193/417] gh-139653: Remove assertions in
_Py_InitializeRecursionLimits() (#141551)
These checks were invalid and failed randomly on FreeBSD
and Alpine Linux.
---
Python/ceval.c | 7 -------
1 file changed, 7 deletions(-)
diff --git a/Python/ceval.c b/Python/ceval.c
index b76c9ec2811..31b81a37464 100644
--- a/Python/ceval.c
+++ b/Python/ceval.c
@@ -523,13 +523,6 @@ _Py_InitializeRecursionLimits(PyThreadState *tstate)
_PyThreadStateImpl *ts = (_PyThreadStateImpl *)tstate;
ts->c_stack_init_base = base;
ts->c_stack_init_top = top;
-
- // Test the stack pointer
-#if !defined(NDEBUG) && !defined(__wasi__)
- uintptr_t here_addr = _Py_get_machine_stack_pointer();
- assert(ts->c_stack_soft_limit < here_addr);
- assert(here_addr < ts->c_stack_top);
-#endif
}
From eab7385858025df9fcb0131f71ec4a46d44e3ae9 Mon Sep 17 00:00:00 2001
From: Petr Viktorin
Date: Fri, 14 Nov 2025 16:05:42 +0100
Subject: [PATCH 194/417] gh-116146: Avoid empty braces in _testembed.c
(GH-141556)
---
Programs/_testembed.c | 2 +-
1 file changed, 1 insertion(+), 1 deletion(-)
diff --git a/Programs/_testembed.c b/Programs/_testembed.c
index 27224e508bd..d0d7d5f03fb 100644
--- a/Programs/_testembed.c
+++ b/Programs/_testembed.c
@@ -2255,7 +2255,7 @@ exec_module(PyObject* self, PyObject* mod)
static PyMethodDef create_static_module_methods[] = {
{"create_module", create_module, METH_O, NULL},
{"exec_module", exec_module, METH_O, NULL},
- {}
+ {NULL}
};
static struct PyModuleDef create_static_module_def = {
From b101e9d36b1aed2bb4bca8aec3e1cc1d1df4f79e Mon Sep 17 00:00:00 2001
From: Steve Dower
Date: Fri, 14 Nov 2025 15:23:01 +0000
Subject: [PATCH 195/417] Add PyManager troubleshooting steps for direct launch
of script files (GH-141530)
---
Doc/using/windows.rst | 22 ++++++++++++++++------
1 file changed, 16 insertions(+), 6 deletions(-)
diff --git a/Doc/using/windows.rst b/Doc/using/windows.rst
index e6619b73bd2..ee182519199 100644
--- a/Doc/using/windows.rst
+++ b/Doc/using/windows.rst
@@ -4,6 +4,8 @@
.. _Microsoft Store app: https://apps.microsoft.com/detail/9NQ7512CXL7T
+.. _legacy launcher: https://www.python.org/ftp/python/3.14.0/win32/launcher.msi
+
.. _using-on-windows:
*************************
@@ -543,12 +545,9 @@ configuration option.
The behaviour of shebangs in the Python install manager is subtly different
from the previous ``py.exe`` launcher, and the old configuration options no
longer apply. If you are specifically reliant on the old behaviour or
- configuration, we recommend keeping the legacy launcher. It may be
- `downloaded independently `_
- and installed on its own. The legacy launcher's ``py`` command will override
- PyManager's one, and you will need to use ``pymanager`` commands for
- installing and uninstalling.
-
+ configuration, we recommend installing the `legacy launcher`_. The legacy
+ launcher's ``py`` command will override PyManager's one by default, and you
+ will need to use ``pymanager`` commands for installing and uninstalling.
.. _Add-AppxPackage: https://learn.microsoft.com/powershell/module/appx/add-appxpackage
@@ -859,6 +858,17 @@ default).
These scripts are separated for each runtime, and so you may need to
add multiple paths.
+ * - Typing ``script-name.py`` in the terminal opens in a new window.
+ - This is a known limitation of the operating system. Either specify ``py``
+ before the script name, create a batch file containing ``@py "%~dpn0.py" %*``
+ with the same name as the script, or install the `legacy launcher`_
+ and select it as the association for scripts.
+
+ * - Drag-dropping files onto a script doesn't work
+ - This is a known limitation of the operating system. It is supported with
+ the `legacy launcher`_, or with the Python install manager when installed
+ from the MSI.
+
.. _windows-embeddable:
From da7f4e4b22020cfc6c5b5918756e454ef281848d Mon Sep 17 00:00:00 2001
From: Locked-chess-official <13140752715@163.com>
Date: Fri, 14 Nov 2025 23:52:14 +0800
Subject: [PATCH 196/417] gh-141488: Add `Py_` prefix to Include/datetime.h
macros (#141493)
---
Include/datetime.h | 6 +++---
1 file changed, 3 insertions(+), 3 deletions(-)
diff --git a/Include/datetime.h b/Include/datetime.h
index b78cc0e8e2e..ed36e6e48c8 100644
--- a/Include/datetime.h
+++ b/Include/datetime.h
@@ -1,8 +1,8 @@
/* datetime.h
*/
#ifndef Py_LIMITED_API
-#ifndef DATETIME_H
-#define DATETIME_H
+#ifndef Py_DATETIME_H
+#define Py_DATETIME_H
#ifdef __cplusplus
extern "C" {
#endif
@@ -263,5 +263,5 @@ static PyDateTime_CAPI *PyDateTimeAPI = NULL;
#ifdef __cplusplus
}
#endif
-#endif
+#endif /* !Py_DATETIME_H */
#endif /* !Py_LIMITED_API */
From f26ed455d5582a7d66618acf2a93bc4b22a84b47 Mon Sep 17 00:00:00 2001
From: Kumar Aditya
Date: Fri, 14 Nov 2025 23:17:59 +0530
Subject: [PATCH 197/417] gh-114203: skip locking if object is already locked
by two-mutex critical section (#141476)
---
...-11-14-16-25-15.gh-issue-114203.n3tlQO.rst | 1 +
.../test_critical_sections.c | 101 ++++++++++++++++++
Python/critical_section.c | 23 +++-
3 files changed, 120 insertions(+), 5 deletions(-)
create mode 100644 Misc/NEWS.d/next/Core_and_Builtins/2025-11-14-16-25-15.gh-issue-114203.n3tlQO.rst
diff --git a/Misc/NEWS.d/next/Core_and_Builtins/2025-11-14-16-25-15.gh-issue-114203.n3tlQO.rst b/Misc/NEWS.d/next/Core_and_Builtins/2025-11-14-16-25-15.gh-issue-114203.n3tlQO.rst
new file mode 100644
index 00000000000..883f9333cae
--- /dev/null
+++ b/Misc/NEWS.d/next/Core_and_Builtins/2025-11-14-16-25-15.gh-issue-114203.n3tlQO.rst
@@ -0,0 +1 @@
+Skip locking if object is already locked by two-mutex critical section.
diff --git a/Modules/_testinternalcapi/test_critical_sections.c b/Modules/_testinternalcapi/test_critical_sections.c
index e0ba37abcdd..e3b2fe716d4 100644
--- a/Modules/_testinternalcapi/test_critical_sections.c
+++ b/Modules/_testinternalcapi/test_critical_sections.c
@@ -284,10 +284,111 @@ test_critical_sections_gc(PyObject *self, PyObject *Py_UNUSED(args))
#endif
+#ifdef Py_GIL_DISABLED
+
+static PyObject *
+test_critical_section1_reacquisition(PyObject *self, PyObject *Py_UNUSED(args))
+{
+ PyObject *a = PyDict_New();
+ assert(a != NULL);
+
+ PyCriticalSection cs1, cs2;
+ // First acquisition of critical section on object locks it
+ PyCriticalSection_Begin(&cs1, a);
+ assert(PyMutex_IsLocked(&a->ob_mutex));
+ assert(_PyCriticalSection_IsActive(PyThreadState_GET()->critical_section));
+ assert(_PyThreadState_GET()->critical_section == (uintptr_t)&cs1);
+ // Attempting to re-acquire critical section on same object which
+ // is already locked by top-most critical section is a no-op.
+ PyCriticalSection_Begin(&cs2, a);
+ assert(PyMutex_IsLocked(&a->ob_mutex));
+ assert(_PyCriticalSection_IsActive(PyThreadState_GET()->critical_section));
+ assert(_PyThreadState_GET()->critical_section == (uintptr_t)&cs1);
+ // Releasing second critical section is a no-op.
+ PyCriticalSection_End(&cs2);
+ assert(PyMutex_IsLocked(&a->ob_mutex));
+ assert(_PyCriticalSection_IsActive(PyThreadState_GET()->critical_section));
+ assert(_PyThreadState_GET()->critical_section == (uintptr_t)&cs1);
+ // Releasing first critical section unlocks the object
+ PyCriticalSection_End(&cs1);
+ assert(!PyMutex_IsLocked(&a->ob_mutex));
+
+ Py_DECREF(a);
+ Py_RETURN_NONE;
+}
+
+static PyObject *
+test_critical_section2_reacquisition(PyObject *self, PyObject *Py_UNUSED(args))
+{
+ PyObject *a = PyDict_New();
+ assert(a != NULL);
+ PyObject *b = PyDict_New();
+ assert(b != NULL);
+
+ PyCriticalSection2 cs;
+ // First acquisition of critical section on objects locks them
+ PyCriticalSection2_Begin(&cs, a, b);
+ assert(PyMutex_IsLocked(&a->ob_mutex));
+ assert(PyMutex_IsLocked(&b->ob_mutex));
+ assert(_PyCriticalSection_IsActive(PyThreadState_GET()->critical_section));
+ assert((_PyThreadState_GET()->critical_section &
+ ~_Py_CRITICAL_SECTION_MASK) == (uintptr_t)&cs);
+
+ // Attempting to re-acquire critical section on either of two
+ // objects already locked by top-most critical section is a no-op.
+
+ // Check re-acquiring on first object
+ PyCriticalSection a_cs;
+ PyCriticalSection_Begin(&a_cs, a);
+ assert(PyMutex_IsLocked(&a->ob_mutex));
+ assert(PyMutex_IsLocked(&b->ob_mutex));
+ assert(_PyCriticalSection_IsActive(PyThreadState_GET()->critical_section));
+ assert((_PyThreadState_GET()->critical_section &
+ ~_Py_CRITICAL_SECTION_MASK) == (uintptr_t)&cs);
+ // Releasing critical section on either object is a no-op.
+ PyCriticalSection_End(&a_cs);
+ assert(PyMutex_IsLocked(&a->ob_mutex));
+ assert(PyMutex_IsLocked(&b->ob_mutex));
+ assert(_PyCriticalSection_IsActive(PyThreadState_GET()->critical_section));
+ assert((_PyThreadState_GET()->critical_section &
+ ~_Py_CRITICAL_SECTION_MASK) == (uintptr_t)&cs);
+
+ // Check re-acquiring on second object
+ PyCriticalSection b_cs;
+ PyCriticalSection_Begin(&b_cs, b);
+ assert(PyMutex_IsLocked(&a->ob_mutex));
+ assert(PyMutex_IsLocked(&b->ob_mutex));
+ assert(_PyCriticalSection_IsActive(PyThreadState_GET()->critical_section));
+ assert((_PyThreadState_GET()->critical_section &
+ ~_Py_CRITICAL_SECTION_MASK) == (uintptr_t)&cs);
+ // Releasing critical section on either object is a no-op.
+ PyCriticalSection_End(&b_cs);
+ assert(PyMutex_IsLocked(&a->ob_mutex));
+ assert(PyMutex_IsLocked(&b->ob_mutex));
+ assert(_PyCriticalSection_IsActive(PyThreadState_GET()->critical_section));
+ assert((_PyThreadState_GET()->critical_section &
+ ~_Py_CRITICAL_SECTION_MASK) == (uintptr_t)&cs);
+
+ // Releasing critical section on both objects unlocks them
+ PyCriticalSection2_End(&cs);
+ assert(!PyMutex_IsLocked(&a->ob_mutex));
+ assert(!PyMutex_IsLocked(&b->ob_mutex));
+
+ Py_DECREF(a);
+ Py_DECREF(b);
+ Py_RETURN_NONE;
+}
+
+#endif // Py_GIL_DISABLED
+
static PyMethodDef test_methods[] = {
{"test_critical_sections", test_critical_sections, METH_NOARGS},
{"test_critical_sections_nest", test_critical_sections_nest, METH_NOARGS},
{"test_critical_sections_suspend", test_critical_sections_suspend, METH_NOARGS},
+#ifdef Py_GIL_DISABLED
+ {"test_critical_section1_reacquisition", test_critical_section1_reacquisition, METH_NOARGS},
+ {"test_critical_section2_reacquisition", test_critical_section2_reacquisition, METH_NOARGS},
+#endif
#ifdef Py_CAN_START_THREADS
{"test_critical_sections_threads", test_critical_sections_threads, METH_NOARGS},
{"test_critical_sections_gc", test_critical_sections_gc, METH_NOARGS},
diff --git a/Python/critical_section.c b/Python/critical_section.c
index e628ba2f6d1..218b580e951 100644
--- a/Python/critical_section.c
+++ b/Python/critical_section.c
@@ -24,11 +24,24 @@ _PyCriticalSection_BeginSlow(PyCriticalSection *c, PyMutex *m)
// As an optimisation for locking the same object recursively, skip
// locking if the mutex is currently locked by the top-most critical
// section.
- if (tstate->critical_section &&
- untag_critical_section(tstate->critical_section)->_cs_mutex == m) {
- c->_cs_mutex = NULL;
- c->_cs_prev = 0;
- return;
+ // If the top-most critical section is a two-mutex critical section,
+ // then locking is skipped if either mutex is m.
+ if (tstate->critical_section) {
+ PyCriticalSection *prev = untag_critical_section(tstate->critical_section);
+ if (prev->_cs_mutex == m) {
+ c->_cs_mutex = NULL;
+ c->_cs_prev = 0;
+ return;
+ }
+ if (tstate->critical_section & _Py_CRITICAL_SECTION_TWO_MUTEXES) {
+ PyCriticalSection2 *prev2 = (PyCriticalSection2 *)
+ untag_critical_section(tstate->critical_section);
+ if (prev2->_cs_mutex2 == m) {
+ c->_cs_mutex = NULL;
+ c->_cs_prev = 0;
+ return;
+ }
+ }
}
c->_cs_mutex = NULL;
c->_cs_prev = (uintptr_t)tstate->critical_section;
From 1281be1caf9357ee2a68f7370a88b5cff0110e15 Mon Sep 17 00:00:00 2001
From: Mikhail Efimov
Date: Sat, 15 Nov 2025 00:38:39 +0300
Subject: [PATCH 198/417] gh-141367: Use CALL_LIST_APPEND instruction only for
lists, not for list subclasses (GH-141398)
Co-authored-by: Ken Jin
---
Include/internal/pycore_code.h | 4 +--
Lib/test/test_opcache.py | 27 +++++++++++++++++++
...-11-11-13-40-45.gh-issue-141367.I5KY7F.rst | 2 ++
Python/bytecodes.c | 3 +--
Python/executor_cases.c.h | 4 ---
Python/generated_cases.c.h | 7 +----
Python/specialize.c | 17 +++++++-----
7 files changed, 44 insertions(+), 20 deletions(-)
create mode 100644 Misc/NEWS.d/next/Core_and_Builtins/2025-11-11-13-40-45.gh-issue-141367.I5KY7F.rst
diff --git a/Include/internal/pycore_code.h b/Include/internal/pycore_code.h
index 9748e036bf2..cb9c0aa27a1 100644
--- a/Include/internal/pycore_code.h
+++ b/Include/internal/pycore_code.h
@@ -311,8 +311,8 @@ PyAPI_FUNC(void) _Py_Specialize_LoadGlobal(PyObject *globals, PyObject *builtins
_Py_CODEUNIT *instr, PyObject *name);
PyAPI_FUNC(void) _Py_Specialize_StoreSubscr(_PyStackRef container, _PyStackRef sub,
_Py_CODEUNIT *instr);
-PyAPI_FUNC(void) _Py_Specialize_Call(_PyStackRef callable, _Py_CODEUNIT *instr,
- int nargs);
+PyAPI_FUNC(void) _Py_Specialize_Call(_PyStackRef callable, _PyStackRef self_or_null,
+ _Py_CODEUNIT *instr, int nargs);
PyAPI_FUNC(void) _Py_Specialize_CallKw(_PyStackRef callable, _Py_CODEUNIT *instr,
int nargs);
PyAPI_FUNC(void) _Py_Specialize_BinaryOp(_PyStackRef lhs, _PyStackRef rhs, _Py_CODEUNIT *instr,
diff --git a/Lib/test/test_opcache.py b/Lib/test/test_opcache.py
index f23f8c053e8..c7eea75117d 100644
--- a/Lib/test/test_opcache.py
+++ b/Lib/test/test_opcache.py
@@ -1872,6 +1872,33 @@ def for_iter_generator():
self.assert_specialized(for_iter_generator, "FOR_ITER_GEN")
self.assert_no_opcode(for_iter_generator, "FOR_ITER")
+ @cpython_only
+ @requires_specialization_ft
+ def test_call_list_append(self):
+ # gh-141367: only exact lists should use
+ # CALL_LIST_APPEND instruction after specialization.
+
+ r = range(_testinternalcapi.SPECIALIZATION_THRESHOLD)
+
+ def list_append(l):
+ for _ in r:
+ l.append(1)
+
+ list_append([])
+ self.assert_specialized(list_append, "CALL_LIST_APPEND")
+ self.assert_no_opcode(list_append, "CALL_METHOD_DESCRIPTOR_O")
+ self.assert_no_opcode(list_append, "CALL")
+
+ def my_list_append(l):
+ for _ in r:
+ l.append(1)
+
+ class MyList(list): pass
+ my_list_append(MyList())
+ self.assert_specialized(my_list_append, "CALL_METHOD_DESCRIPTOR_O")
+ self.assert_no_opcode(my_list_append, "CALL_LIST_APPEND")
+ self.assert_no_opcode(my_list_append, "CALL")
+
if __name__ == "__main__":
unittest.main()
diff --git a/Misc/NEWS.d/next/Core_and_Builtins/2025-11-11-13-40-45.gh-issue-141367.I5KY7F.rst b/Misc/NEWS.d/next/Core_and_Builtins/2025-11-11-13-40-45.gh-issue-141367.I5KY7F.rst
new file mode 100644
index 00000000000..cb830fcd9e1
--- /dev/null
+++ b/Misc/NEWS.d/next/Core_and_Builtins/2025-11-11-13-40-45.gh-issue-141367.I5KY7F.rst
@@ -0,0 +1,2 @@
+Specialize ``CALL_LIST_APPEND`` instruction only for lists, not for list
+subclasses, to avoid unnecessary deopt. Patch by Mikhail Efimov.
diff --git a/Python/bytecodes.c b/Python/bytecodes.c
index 2c798855a71..8a7b784bb9e 100644
--- a/Python/bytecodes.c
+++ b/Python/bytecodes.c
@@ -3689,7 +3689,7 @@ dummy_func(
#if ENABLE_SPECIALIZATION_FT
if (ADAPTIVE_COUNTER_TRIGGERS(counter)) {
next_instr = this_instr;
- _Py_Specialize_Call(callable, next_instr, oparg + !PyStackRef_IsNull(self_or_null));
+ _Py_Specialize_Call(callable, self_or_null, next_instr, oparg + !PyStackRef_IsNull(self_or_null));
DISPATCH_SAME_OPARG();
}
OPCODE_DEFERRED_INC(CALL);
@@ -4395,7 +4395,6 @@ dummy_func(
assert(oparg == 1);
PyObject *self_o = PyStackRef_AsPyObjectBorrow(self);
- DEOPT_IF(!PyList_CheckExact(self_o));
DEOPT_IF(!LOCK_OBJECT(self_o));
STAT_INC(CALL, hit);
int err = _PyList_AppendTakeRef((PyListObject *)self_o, PyStackRef_AsPyObjectSteal(arg));
diff --git a/Python/executor_cases.c.h b/Python/executor_cases.c.h
index 7ba2e9d0d92..6796abf84ac 100644
--- a/Python/executor_cases.c.h
+++ b/Python/executor_cases.c.h
@@ -6037,10 +6037,6 @@
callable = stack_pointer[-3];
assert(oparg == 1);
PyObject *self_o = PyStackRef_AsPyObjectBorrow(self);
- if (!PyList_CheckExact(self_o)) {
- UOP_STAT_INC(uopcode, miss);
- JUMP_TO_JUMP_TARGET();
- }
if (!LOCK_OBJECT(self_o)) {
UOP_STAT_INC(uopcode, miss);
JUMP_TO_JUMP_TARGET();
diff --git a/Python/generated_cases.c.h b/Python/generated_cases.c.h
index a984da6dc91..01f65d9dd37 100644
--- a/Python/generated_cases.c.h
+++ b/Python/generated_cases.c.h
@@ -1533,7 +1533,7 @@
if (ADAPTIVE_COUNTER_TRIGGERS(counter)) {
next_instr = this_instr;
_PyFrame_SetStackPointer(frame, stack_pointer);
- _Py_Specialize_Call(callable, next_instr, oparg + !PyStackRef_IsNull(self_or_null));
+ _Py_Specialize_Call(callable, self_or_null, next_instr, oparg + !PyStackRef_IsNull(self_or_null));
stack_pointer = _PyFrame_GetStackPointer(frame);
DISPATCH_SAME_OPARG();
}
@@ -3470,11 +3470,6 @@
self = nos;
assert(oparg == 1);
PyObject *self_o = PyStackRef_AsPyObjectBorrow(self);
- if (!PyList_CheckExact(self_o)) {
- UPDATE_MISS_STATS(CALL);
- assert(_PyOpcode_Deopt[opcode] == (CALL));
- JUMP_TO_PREDICTED(CALL);
- }
if (!LOCK_OBJECT(self_o)) {
UPDATE_MISS_STATS(CALL);
assert(_PyOpcode_Deopt[opcode] == (CALL));
diff --git a/Python/specialize.c b/Python/specialize.c
index 2193596a331..19433bc7a74 100644
--- a/Python/specialize.c
+++ b/Python/specialize.c
@@ -1602,8 +1602,8 @@ specialize_class_call(PyObject *callable, _Py_CODEUNIT *instr, int nargs)
}
static int
-specialize_method_descriptor(PyMethodDescrObject *descr, _Py_CODEUNIT *instr,
- int nargs)
+specialize_method_descriptor(PyMethodDescrObject *descr, PyObject *self_or_null,
+ _Py_CODEUNIT *instr, int nargs)
{
switch (descr->d_method->ml_flags &
(METH_VARARGS | METH_FASTCALL | METH_NOARGS | METH_O |
@@ -1627,8 +1627,11 @@ specialize_method_descriptor(PyMethodDescrObject *descr, _Py_CODEUNIT *instr,
bool pop = (next.op.code == POP_TOP);
int oparg = instr->op.arg;
if ((PyObject *)descr == list_append && oparg == 1 && pop) {
- specialize(instr, CALL_LIST_APPEND);
- return 0;
+ assert(self_or_null != NULL);
+ if (PyList_CheckExact(self_or_null)) {
+ specialize(instr, CALL_LIST_APPEND);
+ return 0;
+ }
}
specialize(instr, CALL_METHOD_DESCRIPTOR_O);
return 0;
@@ -1766,7 +1769,7 @@ specialize_c_call(PyObject *callable, _Py_CODEUNIT *instr, int nargs)
}
Py_NO_INLINE void
-_Py_Specialize_Call(_PyStackRef callable_st, _Py_CODEUNIT *instr, int nargs)
+_Py_Specialize_Call(_PyStackRef callable_st, _PyStackRef self_or_null_st, _Py_CODEUNIT *instr, int nargs)
{
PyObject *callable = PyStackRef_AsPyObjectBorrow(callable_st);
@@ -1784,7 +1787,9 @@ _Py_Specialize_Call(_PyStackRef callable_st, _Py_CODEUNIT *instr, int nargs)
fail = specialize_class_call(callable, instr, nargs);
}
else if (Py_IS_TYPE(callable, &PyMethodDescr_Type)) {
- fail = specialize_method_descriptor((PyMethodDescrObject *)callable, instr, nargs);
+ PyObject *self_or_null = PyStackRef_AsPyObjectBorrow(self_or_null_st);
+ fail = specialize_method_descriptor((PyMethodDescrObject *)callable,
+ self_or_null, instr, nargs);
}
else if (PyMethod_Check(callable)) {
PyObject *func = ((PyMethodObject *)callable)->im_func;
From f0a8bc737ab2f04d4196eee154cb1e17e26ad585 Mon Sep 17 00:00:00 2001
From: Raymond Hettinger
Date: Fri, 14 Nov 2025 17:25:45 -0600
Subject: [PATCH 199/417] gh-140938: Raise ValueError for infinite inputs to
stdev/pstdev (GH-141531)
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit
Raise ValueError for infinite inputs to stdev/pstdev
---
Co-authored-by: Gregory P. Smith <68491+gpshead@users.noreply.github.com>
Co-authored-by: Bénédikt Tran <10796600+picnixz@users.noreply.github.com>
---
Lib/statistics.py | 18 ++++++++++++++----
Lib/test/test_statistics.py | 9 ++++++++-
...5-11-13-14-51-30.gh-issue-140938.kXsHHv.rst | 2 ++
3 files changed, 24 insertions(+), 5 deletions(-)
create mode 100644 Misc/NEWS.d/next/Library/2025-11-13-14-51-30.gh-issue-140938.kXsHHv.rst
diff --git a/Lib/statistics.py b/Lib/statistics.py
index 3d805cb0739..26cf925529e 100644
--- a/Lib/statistics.py
+++ b/Lib/statistics.py
@@ -619,9 +619,14 @@ def stdev(data, xbar=None):
if n < 2:
raise StatisticsError('stdev requires at least two data points')
mss = ss / (n - 1)
+ try:
+ mss_numerator = mss.numerator
+ mss_denominator = mss.denominator
+ except AttributeError:
+ raise ValueError('inf or nan encountered in data')
if issubclass(T, Decimal):
- return _decimal_sqrt_of_frac(mss.numerator, mss.denominator)
- return _float_sqrt_of_frac(mss.numerator, mss.denominator)
+ return _decimal_sqrt_of_frac(mss_numerator, mss_denominator)
+ return _float_sqrt_of_frac(mss_numerator, mss_denominator)
def pstdev(data, mu=None):
@@ -637,9 +642,14 @@ def pstdev(data, mu=None):
if n < 1:
raise StatisticsError('pstdev requires at least one data point')
mss = ss / n
+ try:
+ mss_numerator = mss.numerator
+ mss_denominator = mss.denominator
+ except AttributeError:
+ raise ValueError('inf or nan encountered in data')
if issubclass(T, Decimal):
- return _decimal_sqrt_of_frac(mss.numerator, mss.denominator)
- return _float_sqrt_of_frac(mss.numerator, mss.denominator)
+ return _decimal_sqrt_of_frac(mss_numerator, mss_denominator)
+ return _float_sqrt_of_frac(mss_numerator, mss_denominator)
## Statistics for relations between two inputs #############################
diff --git a/Lib/test/test_statistics.py b/Lib/test/test_statistics.py
index 8250b0aef09..677a87b51b9 100644
--- a/Lib/test/test_statistics.py
+++ b/Lib/test/test_statistics.py
@@ -2005,7 +2005,6 @@ def test_iter_list_same(self):
expected = self.func(data)
self.assertEqual(self.func(iter(data)), expected)
-
class TestPVariance(VarianceStdevMixin, NumericTestCase, UnivariateTypeMixin):
# Tests for population variance.
def setUp(self):
@@ -2113,6 +2112,14 @@ def test_center_not_at_mean(self):
self.assertEqual(self.func(data), 2.5)
self.assertEqual(self.func(data, mu=0.5), 6.5)
+ def test_gh_140938(self):
+ # Inputs with inf/nan should raise a ValueError
+ with self.assertRaises(ValueError):
+ self.func([1.0, math.inf])
+ with self.assertRaises(ValueError):
+ self.func([1.0, math.nan])
+
+
class TestSqrtHelpers(unittest.TestCase):
def test_integer_sqrt_of_frac_rto(self):
diff --git a/Misc/NEWS.d/next/Library/2025-11-13-14-51-30.gh-issue-140938.kXsHHv.rst b/Misc/NEWS.d/next/Library/2025-11-13-14-51-30.gh-issue-140938.kXsHHv.rst
new file mode 100644
index 00000000000..bd3044002a2
--- /dev/null
+++ b/Misc/NEWS.d/next/Library/2025-11-13-14-51-30.gh-issue-140938.kXsHHv.rst
@@ -0,0 +1,2 @@
+The :func:`statistics.stdev` and :func:`statistics.pstdev` functions now raise a
+:exc:`ValueError` when the input contains an infinity or a NaN.
From 453d886f8592d2f4346d5621b1e4ff31c24338d5 Mon Sep 17 00:00:00 2001
From: Guo Ci
Date: Fri, 14 Nov 2025 19:13:37 -0500
Subject: [PATCH 200/417] GH-90344: replace single-call
`io.IncrementalNewlineDecoder` usage with non-incremental newline decoders
(GH-30276)
Co-authored-by: blurb-it[bot] <43283697+blurb-it[bot]@users.noreply.github.com>
Co-authored-by: Brett Cannon
---
Lib/doctest.py | 9 ++-------
Lib/importlib/_bootstrap_external.py | 3 +--
Lib/test/test_importlib/test_abc.py | 2 +-
.../2025-10-31-14-03-42.gh-issue-90344.gvZigO.rst | 1 +
4 files changed, 5 insertions(+), 10 deletions(-)
create mode 100644 Misc/NEWS.d/next/Core_and_Builtins/2025-10-31-14-03-42.gh-issue-90344.gvZigO.rst
diff --git a/Lib/doctest.py b/Lib/doctest.py
index 92a2ab4f7e6..ad8fb900f69 100644
--- a/Lib/doctest.py
+++ b/Lib/doctest.py
@@ -104,7 +104,7 @@ def _test():
import traceback
import types
import unittest
-from io import StringIO, IncrementalNewlineDecoder
+from io import StringIO, TextIOWrapper, BytesIO
from collections import namedtuple
import _colorize # Used in doctests
from _colorize import ANSIColors, can_colorize
@@ -237,10 +237,6 @@ def _normalize_module(module, depth=2):
else:
raise TypeError("Expected a module, string, or None")
-def _newline_convert(data):
- # The IO module provides a handy decoder for universal newline conversion
- return IncrementalNewlineDecoder(None, True).decode(data, True)
-
def _load_testfile(filename, package, module_relative, encoding):
if module_relative:
package = _normalize_module(package, 3)
@@ -252,10 +248,9 @@ def _load_testfile(filename, package, module_relative, encoding):
pass
if hasattr(loader, 'get_data'):
file_contents = loader.get_data(filename)
- file_contents = file_contents.decode(encoding)
# get_data() opens files as 'rb', so one must do the equivalent
# conversion as universal newlines would do.
- return _newline_convert(file_contents), filename
+ return TextIOWrapper(BytesIO(file_contents), encoding=encoding, newline=None).read(), filename
with open(filename, encoding=encoding) as f:
return f.read(), filename
diff --git a/Lib/importlib/_bootstrap_external.py b/Lib/importlib/_bootstrap_external.py
index 4ab0e79ea6e..192c0261408 100644
--- a/Lib/importlib/_bootstrap_external.py
+++ b/Lib/importlib/_bootstrap_external.py
@@ -552,8 +552,7 @@ def decode_source(source_bytes):
import tokenize # To avoid bootstrap issues.
source_bytes_readline = _io.BytesIO(source_bytes).readline
encoding = tokenize.detect_encoding(source_bytes_readline)
- newline_decoder = _io.IncrementalNewlineDecoder(None, True)
- return newline_decoder.decode(source_bytes.decode(encoding[0]))
+ return _io.TextIOWrapper(_io.BytesIO(source_bytes), encoding=encoding[0], newline=None).read()
# Module specifications #######################################################
diff --git a/Lib/test/test_importlib/test_abc.py b/Lib/test/test_importlib/test_abc.py
index dd943210ffc..bd1540ce403 100644
--- a/Lib/test/test_importlib/test_abc.py
+++ b/Lib/test/test_importlib/test_abc.py
@@ -904,7 +904,7 @@ def test_universal_newlines(self):
mock = self.SourceOnlyLoaderMock('mod.file')
source = "x = 42\r\ny = -13\r\n"
mock.source = source.encode('utf-8')
- expect = io.IncrementalNewlineDecoder(None, True).decode(source)
+ expect = io.StringIO(source, newline=None).getvalue()
self.assertEqual(mock.get_source(name), expect)
diff --git a/Misc/NEWS.d/next/Core_and_Builtins/2025-10-31-14-03-42.gh-issue-90344.gvZigO.rst b/Misc/NEWS.d/next/Core_and_Builtins/2025-10-31-14-03-42.gh-issue-90344.gvZigO.rst
new file mode 100644
index 00000000000..b1d05354f65
--- /dev/null
+++ b/Misc/NEWS.d/next/Core_and_Builtins/2025-10-31-14-03-42.gh-issue-90344.gvZigO.rst
@@ -0,0 +1 @@
+Replace :class:`io.IncrementalNewlineDecoder` with non incremental newline decoders in codebase where :meth:`!io.IncrementalNewlineDecoder.decode` was being called once.
From 53d65c840e038ce9a5782fbd3da963c7aba90570 Mon Sep 17 00:00:00 2001
From: Takuya UESHIN