mirror of
https://github.com/python/cpython.git
synced 2026-02-21 22:50:55 +00:00
gh-132604: Deprecate inherited runtime checkability of protocols (GH-143806)
Co-authored-by: Jelle Zijlstra <jelle.zijlstra@gmail.com>
This commit is contained in:
parent
a91b5c3fb5
commit
ff531f9005
4 changed files with 125 additions and 3 deletions
|
|
@ -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.
|
||||
|
|
@ -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::
|
||||
|
|
@ -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)
|
||||
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
|
|
@ -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)
|
||||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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__',
|
||||
})
|
||||
|
||||
|
|
@ -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__
|
||||
|
|
@ -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
|
||||
|
||||
|
|
@ -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
|
||||
|
|
@ -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,
|
||||
|
|
|
|||
|
|
@ -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.
|
||||
Loading…
Add table
Add a link
Reference in a new issue