Skip to content
Merged
23 changes: 23 additions & 0 deletions Doc/library/typing.rst
Original file line number Diff line number Diff line change
Expand Up @@ -2527,6 +2527,12 @@ types.

.. versionadded:: 3.8

.. deprecated-removed:: 3.15 3.20
It is deprecated to call :func:`isinstance` and :func:`issubclass` checks on
protocol classes that were not explicitly decorated with :func:`!runtime_checkable`
but that inherit from a runtime-checkable protocol class. This will throw
a :exc:`TypeError` in Python 3.20.

.. decorator:: runtime_checkable

Mark a protocol class as a runtime protocol.
Expand All @@ -2548,6 +2554,18 @@ types.
import threading
assert isinstance(threading.Thread(name='Bob'), Named)

Runtime checkability of protocols is not inherited. A subclass of a runtime-checkable protocol
is only runtime-checkable if it is explicitly marked as such, regardless of class hierarchy::

@runtime_checkable
class Iterable(Protocol):
def __iter__(self): ...

# Without @runtime_checkable, Reversible would no longer be runtime-checkable.
@runtime_checkable
class Reversible(Iterable, Protocol):
def __reversed__(self): ...

This decorator raises :exc:`TypeError` when applied to a non-protocol class.

.. note::
Expand Down Expand Up @@ -2588,6 +2606,11 @@ types.
protocol. See :ref:`What's new in Python 3.12 <whatsnew-typing-py312>`
for more details.

.. deprecated-removed:: 3.15 3.20
It is deprecated to call :func:`isinstance` and :func:`issubclass` checks on
protocol classes that were not explicitly decorated with :func:`!runtime_checkable`
but that inherit from a runtime-checkable protocol class. This will throw
a :exc:`TypeError` in Python 3.20.

.. class:: TypedDict(dict)

Expand Down
19 changes: 17 additions & 2 deletions Include/datetime.h
Original file line number Diff line number Diff line change
Expand Up @@ -196,8 +196,23 @@ typedef struct {
/* Define global variable for the C API and a macro for setting it. */
static PyDateTime_CAPI *PyDateTimeAPI = NULL;

#define PyDateTime_IMPORT \
PyDateTimeAPI = (PyDateTime_CAPI *)PyCapsule_Import(PyDateTime_CAPSULE_NAME, 0)
static inline PyDateTime_CAPI *
_PyDateTime_IMPORT(void) {
PyDateTime_CAPI *val = _Py_atomic_load_ptr(&PyDateTimeAPI);
if (val == NULL) {
PyDateTime_CAPI *capi = (PyDateTime_CAPI *)PyCapsule_Import(
PyDateTime_CAPSULE_NAME, 0);
if (capi != NULL) {
/* if the compare exchange fails then in that case
another thread would have initialized it */
_Py_atomic_compare_exchange_ptr(&PyDateTimeAPI, &val, (void *)capi);
return capi;
}
}
return val;
}

#define PyDateTime_IMPORT _PyDateTime_IMPORT()

/* Macro for access to the UTC singleton */
#define PyDateTime_TimeZone_UTC PyDateTimeAPI->TimeZone_UTC
Expand Down
15 changes: 4 additions & 11 deletions Lib/test/test_cmd_line.py
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,6 @@
import tempfile
import textwrap
import unittest
import warnings
from test import support
from test.support import os_helper
from test.support import force_not_colorized
Expand Down Expand Up @@ -943,21 +942,15 @@ def test_python_asyncio_debug(self):

@unittest.skipUnless(sysconfig.get_config_var('Py_TRACE_REFS'), "Requires --with-trace-refs build option")
def test_python_dump_refs(self):
code = 'import sys; sys._clear_type_cache()'
# TODO: Remove warnings context manager once sys._clear_type_cache is removed
with warnings.catch_warnings():
warnings.simplefilter("ignore", DeprecationWarning)
rc, out, err = assert_python_ok('-c', code, PYTHONDUMPREFS='1')
code = 'import sys; sys._clear_internal_caches()'
rc, out, err = assert_python_ok('-c', code, PYTHONDUMPREFS='1')
self.assertEqual(rc, 0)

@unittest.skipUnless(sysconfig.get_config_var('Py_TRACE_REFS'), "Requires --with-trace-refs build option")
def test_python_dump_refs_file(self):
with tempfile.NamedTemporaryFile() as dump_file:
code = 'import sys; sys._clear_type_cache()'
# TODO: Remove warnings context manager once sys._clear_type_cache is removed
with warnings.catch_warnings():
warnings.simplefilter("ignore", DeprecationWarning)
rc, out, err = assert_python_ok('-c', code, PYTHONDUMPREFSFILE=dump_file.name)
code = 'import sys; sys._clear_internal_caches()'
rc, out, err = assert_python_ok('-c', code, PYTHONDUMPREFSFILE=dump_file.name)
self.assertEqual(rc, 0)
with open(dump_file.name, 'r') as file:
contents = file.read()
Expand Down
7 changes: 3 additions & 4 deletions Lib/test/test_cppext/extension.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -16,10 +16,9 @@
// gh-135906: Check for compiler warnings in the internal C API
# include "internal/pycore_frame.h"
// mimalloc emits many compiler warnings when Python is built in debug
// mode (when MI_DEBUG is not zero)
// mimalloc emits compiler warnings when Python is built on Windows
// in free-threaded mode.
# if !defined(Py_DEBUG) && !(defined(MS_WINDOWS) && defined(Py_GIL_DISABLED))
// mode (when MI_DEBUG is not zero).
// mimalloc emits compiler warnings when Python is built on Windows.
# if !defined(Py_DEBUG) && !defined(MS_WINDOWS)
# include "internal/pycore_backoff.h"
# include "internal/pycore_cell.h"
# endif
Expand Down
27 changes: 27 additions & 0 deletions Lib/test/test_import/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -45,6 +45,7 @@
Py_GIL_DISABLED,
no_rerun,
force_not_colorized_test_class,
catch_unraisable_exception
)
from test.support.import_helper import (
forget, make_legacy_pyc, unlink, unload, ready_to_import,
Expand Down Expand Up @@ -2517,6 +2518,32 @@ def test_disallowed_reimport(self):
excsnap = _interpreters.run_string(interpid, script)
self.assertIsNot(excsnap, None)

@requires_subinterpreters
def test_pyinit_function_raises_exception(self):
# gh-144601: PyInit functions that raised exceptions would cause a
# crash when imported from a subinterpreter.
import _testsinglephase
filename = _testsinglephase.__file__
script = f"""if True:
from test.test_import import import_extension_from_file

import_extension_from_file('_testsinglephase_raise_exception', {filename!r})"""

interp = _interpreters.create()
try:
with catch_unraisable_exception() as cm:
exception = _interpreters.run_string(interp, script)
unraisable = cm.unraisable
finally:
_interpreters.destroy(interp)

self.assertIsNotNone(exception)
self.assertIsNotNone(exception.type.__name__, "ImportError")
self.assertIsNotNone(exception.msg, "failed to import from subinterpreter due to exception")
self.assertIsNotNone(unraisable)
self.assertIs(unraisable.exc_type, RuntimeError)
self.assertEqual(str(unraisable.exc_value), "evil")


class TestSinglePhaseSnapshot(ModuleSnapshot):
"""A representation of a single-phase init module for testing.
Expand Down
21 changes: 21 additions & 0 deletions Lib/test/test_itertools.py
Original file line number Diff line number Diff line change
Expand Up @@ -733,6 +733,27 @@ def keyfunc(obj):
keyfunc.skip = 1
self.assertRaises(ExpectedError, gulp, [None, None], keyfunc)

def test_groupby_reentrant_eq_does_not_crash(self):
# regression test for gh-143543
class Key:
def __init__(self, do_advance):
self.do_advance = do_advance

def __eq__(self, other):
if self.do_advance:
self.do_advance = False
next(g)
return NotImplemented
return False

def keys():
yield Key(True)
yield Key(False)

g = itertools.groupby([None, None], keys().send)
next(g)
next(g) # must pass with address sanitizer

def test_filter(self):
self.assertEqual(list(filter(isEven, range(6))), [0,2,4])
self.assertEqual(list(filter(None, [0,1,0,2,0])), [1,2])
Expand Down
70 changes: 67 additions & 3 deletions Lib/test/test_typing.py
Original file line number Diff line number Diff line change
Expand Up @@ -51,7 +51,7 @@

from test.support import (
captured_stderr, cpython_only, requires_docstrings, import_helper, run_code,
EqualToForwardRef,
subTests, EqualToForwardRef,
)
from test.typinganndata import (
ann_module695, mod_generics_cache, _typed_dict_helper,
Expand Down Expand Up @@ -3885,8 +3885,8 @@ def meth(self): pass
self.assertIsNot(get_protocol_members(PR), P.__protocol_attrs__)

acceptable_extra_attrs = {
'_is_protocol', '_is_runtime_protocol', '__parameters__',
'__init__', '__annotations__', '__subclasshook__', '__annotate__',
'_is_protocol', '_is_runtime_protocol', '__typing_is_deprecated_inherited_runtime_protocol__',
'__parameters__', '__init__', '__annotations__', '__subclasshook__', '__annotate__',
'__annotations_cache__', '__annotate_func__',
}
self.assertLessEqual(vars(NonP).keys(), vars(C).keys() | acceptable_extra_attrs)
Expand Down Expand Up @@ -4458,6 +4458,70 @@ class P(Protocol):
with self.assertRaisesRegex(TypeError, "@runtime_checkable"):
isinstance(1, P)

@subTests(['check_obj', 'check_func'], ([42, isinstance], [frozenset, issubclass]))
def test_inherited_runtime_protocol_deprecated(self, check_obj, check_func):
"""See GH-132604."""

class BareProto(Protocol):
"""I am not runtime-checkable."""

@runtime_checkable
class RCProto1(Protocol):
"""I am runtime-checkable."""

class InheritedRCProto1(RCProto1, Protocol):
"""I am accidentally runtime-checkable (by inheritance)."""

@runtime_checkable
class RCProto2(InheritedRCProto1, Protocol):
"""Explicit RC -> inherited RC -> explicit RC."""
def spam(self): ...

@runtime_checkable
class RCProto3(BareProto, Protocol):
"""Not RC -> explicit RC."""

class InheritedRCProto2(RCProto3, Protocol):
"""Not RC -> explicit RC -> inherited RC."""
def eggs(self): ...

class InheritedRCProto3(RCProto2, Protocol):
"""Explicit RC -> inherited RC -> explicit RC -> inherited RC."""

class Concrete1(BareProto):
pass

class Concrete2(InheritedRCProto2):
pass

class Concrete3(InheritedRCProto3):
pass

depr_message_re = (
r"<class .+\.InheritedRCProto\d'> isn't explicitly decorated "
r"with @runtime_checkable but it is used in issubclass\(\) or "
r"isinstance\(\). Instance and class checks can only be used with "
r"@runtime_checkable protocols. This will raise a TypeError in Python 3.20."
)

for inherited_runtime_proto in InheritedRCProto1, InheritedRCProto2, InheritedRCProto3:
with self.assertWarnsRegex(DeprecationWarning, depr_message_re):
check_func(check_obj, inherited_runtime_proto)

# Don't warn for explicitly checkable protocols and concrete implementations.
with warnings.catch_warnings():
warnings.simplefilter("error", DeprecationWarning)

for checkable in RCProto1, RCProto2, RCProto3, Concrete1, Concrete2, Concrete3:
check_func(check_obj, checkable)

# Don't warn for uncheckable protocols.
with warnings.catch_warnings():
warnings.simplefilter("error", DeprecationWarning)

with self.assertRaises(TypeError): # Self-test. Protocol below can't be runtime-checkable.
check_func(check_obj, BareProto)

def test_super_call_init(self):
class P(Protocol):
x: int
Expand Down
31 changes: 31 additions & 0 deletions Lib/typing.py
Original file line number Diff line number Diff line change
Expand Up @@ -1826,6 +1826,7 @@ class _TypingEllipsis:
_TYPING_INTERNALS = frozenset({
'__parameters__', '__orig_bases__', '__orig_class__',
'_is_protocol', '_is_runtime_protocol', '__protocol_attrs__',
'__typing_is_deprecated_inherited_runtime_protocol__',
'__non_callable_proto_members__', '__type_params__',
})

Expand Down Expand Up @@ -2015,6 +2016,16 @@ def __subclasscheck__(cls, other):
"Instance and class checks can only be used with "
"@runtime_checkable protocols"
)
if getattr(cls, '__typing_is_deprecated_inherited_runtime_protocol__', False):
# See GH-132604.
import warnings
depr_message = (
f"{cls!r} isn't explicitly decorated with @runtime_checkable but "
"it is used in issubclass() or isinstance(). Instance and class "
"checks can only be used with @runtime_checkable protocols. "
"This will raise a TypeError in Python 3.20."
)
warnings.warn(depr_message, category=DeprecationWarning, stacklevel=2)
if (
# this attribute is set by @runtime_checkable:
cls.__non_callable_proto_members__
Expand Down Expand Up @@ -2044,6 +2055,18 @@ def __instancecheck__(cls, instance):
raise TypeError("Instance and class checks can only be used with"
" @runtime_checkable protocols")

if getattr(cls, '__typing_is_deprecated_inherited_runtime_protocol__', False):
# See GH-132604.
import warnings

depr_message = (
f"{cls!r} isn't explicitly decorated with @runtime_checkable but "
"it is used in issubclass() or isinstance(). Instance and class "
"checks can only be used with @runtime_checkable protocols. "
"This will raise a TypeError in Python 3.20."
)
warnings.warn(depr_message, category=DeprecationWarning, stacklevel=2)

if _abc_instancecheck(cls, instance):
return True

Expand Down Expand Up @@ -2136,6 +2159,11 @@ def __init_subclass__(cls, *args, **kwargs):
if not cls.__dict__.get('_is_protocol', False):
cls._is_protocol = any(b is Protocol for b in cls.__bases__)

# Mark inherited runtime checkability (deprecated). See GH-132604.
if cls._is_protocol and getattr(cls, '_is_runtime_protocol', False):
# This flag is set to False by @runtime_checkable.
cls.__typing_is_deprecated_inherited_runtime_protocol__ = True

# Set (or override) the protocol subclass hook.
if '__subclasshook__' not in cls.__dict__:
cls.__subclasshook__ = _proto_hook
Expand Down Expand Up @@ -2282,6 +2310,9 @@ def close(self): ...
raise TypeError('@runtime_checkable can be only applied to protocol classes,'
' got %r' % cls)
cls._is_runtime_protocol = True
# See GH-132604.
if hasattr(cls, '__typing_is_deprecated_inherited_runtime_protocol__'):
cls.__typing_is_deprecated_inherited_runtime_protocol__ = False
# PEP 544 prohibits using issubclass()
# with protocols that have non-method members.
# See gh-113320 for why we compute this attribute here,
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
Fix thread safety of :c:macro:`! PyDateTime_IMPORT`.
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
Fix crash when importing a module whose ``PyInit`` function raises an
exception from a subinterpreter.
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
Fix a crash in itertools.groupby that could occur when a user-defined
:meth:`~object.__eq__` method re-enters the iterator during key comparison.
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
Previously, :class:`~typing.Protocol` classes that were not decorated with :deco:`~typing.runtime_checkable`,
but that inherited from another ``Protocol`` class that did have this decorator, could be used in :func:`isinstance`
and :func:`issubclass` checks. This behavior is now deprecated and such checks will throw a :exc:`TypeError`
in Python 3.20. Patch by Bartosz Sławecki.
8 changes: 8 additions & 0 deletions Modules/_testsinglephase.c
Original file line number Diff line number Diff line change
Expand Up @@ -801,3 +801,11 @@ PyInit__testsinglephase_circular(void)
}
return Py_NewRef(static_module_circular);
}


PyMODINIT_FUNC
PyInit__testsinglephase_raise_exception(void)
{
PyErr_SetString(PyExc_RuntimeError, "evil");
return NULL;
}
14 changes: 12 additions & 2 deletions Modules/itertoolsmodule.c
Original file line number Diff line number Diff line change
Expand Up @@ -544,9 +544,19 @@ groupby_next(PyObject *op)
else if (gbo->tgtkey == NULL)
break;
else {
int rcmp;
/* A user-defined __eq__ can re-enter groupby and advance the iterator,
mutating gbo->tgtkey / gbo->currkey while we are comparing them.
Take local snapshots and hold strong references so INCREF/DECREF
apply to the same objects even under re-entrancy. */
PyObject *tgtkey = gbo->tgtkey;
PyObject *currkey = gbo->currkey;

Py_INCREF(tgtkey);
Py_INCREF(currkey);
int rcmp = PyObject_RichCompareBool(tgtkey, currkey, Py_EQ);
Py_DECREF(tgtkey);
Py_DECREF(currkey);

rcmp = PyObject_RichCompareBool(gbo->tgtkey, gbo->currkey, Py_EQ);
if (rcmp == -1)
return NULL;
else if (rcmp == 0)
Expand Down
Loading
Loading