mirror of
https://github.com/python/cpython.git
synced 2025-09-26 18:29:57 +00:00
gh-102433: Use inspect.getattr_static
in typing._ProtocolMeta.__instancecheck__
(#103034)
This commit is contained in:
parent
d828b35785
commit
6d59c9e32e
5 changed files with 142 additions and 7 deletions
|
@ -1598,6 +1598,15 @@ These are not used in annotations. They are building blocks for creating generic
|
||||||
import threading
|
import threading
|
||||||
assert isinstance(threading.Thread(name='Bob'), Named)
|
assert isinstance(threading.Thread(name='Bob'), Named)
|
||||||
|
|
||||||
|
.. versionchanged:: 3.12
|
||||||
|
The internal implementation of :func:`isinstance` checks against
|
||||||
|
runtime-checkable protocols now uses :func:`inspect.getattr_static`
|
||||||
|
to look up attributes (previously, :func:`hasattr` was used).
|
||||||
|
As a result, some objects which used to be considered instances
|
||||||
|
of a runtime-checkable protocol may no longer be considered instances
|
||||||
|
of that protocol on Python 3.12+, and vice versa.
|
||||||
|
Most users are unlikely to be affected by this change.
|
||||||
|
|
||||||
.. note::
|
.. note::
|
||||||
|
|
||||||
:func:`!runtime_checkable` will check only the presence of the required
|
:func:`!runtime_checkable` will check only the presence of the required
|
||||||
|
|
|
@ -391,6 +391,17 @@ typing
|
||||||
same name on a base class, as per :pep:`698`. (Contributed by Steven Troxler in
|
same name on a base class, as per :pep:`698`. (Contributed by Steven Troxler in
|
||||||
:gh:`101564`.)
|
:gh:`101564`.)
|
||||||
|
|
||||||
|
* :func:`isinstance` checks against
|
||||||
|
:func:`runtime-checkable protocols <typing.runtime_checkable>` now use
|
||||||
|
:func:`inspect.getattr_static` rather than :func:`hasattr` to lookup whether
|
||||||
|
attributes exist. This means that descriptors and :meth:`~object.__getattr__`
|
||||||
|
methods are no longer unexpectedly evaluated during ``isinstance()`` checks
|
||||||
|
against runtime-checkable protocols. However, it may also mean that some
|
||||||
|
objects which used to be considered instances of a runtime-checkable protocol
|
||||||
|
may no longer be considered instances of that protocol on Python 3.12+, and
|
||||||
|
vice versa. Most users are unlikely to be affected by this change.
|
||||||
|
(Contributed by Alex Waygood in :gh:`102433`.)
|
||||||
|
|
||||||
sys
|
sys
|
||||||
---
|
---
|
||||||
|
|
||||||
|
|
|
@ -2637,7 +2637,15 @@ class ProtocolTests(BaseTestCase):
|
||||||
class PG1(Protocol[T]):
|
class PG1(Protocol[T]):
|
||||||
attr: T
|
attr: T
|
||||||
|
|
||||||
for protocol_class in P, P1, PG, PG1:
|
@runtime_checkable
|
||||||
|
class MethodP(Protocol):
|
||||||
|
def attr(self): ...
|
||||||
|
|
||||||
|
@runtime_checkable
|
||||||
|
class MethodPG(Protocol[T]):
|
||||||
|
def attr(self) -> T: ...
|
||||||
|
|
||||||
|
for protocol_class in P, P1, PG, PG1, MethodP, MethodPG:
|
||||||
for klass in C, D, E, F:
|
for klass in C, D, E, F:
|
||||||
with self.subTest(
|
with self.subTest(
|
||||||
klass=klass.__name__,
|
klass=klass.__name__,
|
||||||
|
@ -2662,7 +2670,12 @@ class ProtocolTests(BaseTestCase):
|
||||||
class BadPG1(Protocol[T]):
|
class BadPG1(Protocol[T]):
|
||||||
attr: T
|
attr: T
|
||||||
|
|
||||||
for obj in PG[T], PG[C], PG1[T], PG1[C], BadP, BadP1, BadPG, BadPG1:
|
cases = (
|
||||||
|
PG[T], PG[C], PG1[T], PG1[C], MethodPG[T],
|
||||||
|
MethodPG[C], BadP, BadP1, BadPG, BadPG1
|
||||||
|
)
|
||||||
|
|
||||||
|
for obj in cases:
|
||||||
for klass in C, D, E, F, Empty:
|
for klass in C, D, E, F, Empty:
|
||||||
with self.subTest(klass=klass.__name__, obj=obj):
|
with self.subTest(klass=klass.__name__, obj=obj):
|
||||||
with self.assertRaises(TypeError):
|
with self.assertRaises(TypeError):
|
||||||
|
@ -2685,6 +2698,82 @@ class ProtocolTests(BaseTestCase):
|
||||||
self.assertIsInstance(CustomDirWithX(), HasX)
|
self.assertIsInstance(CustomDirWithX(), HasX)
|
||||||
self.assertNotIsInstance(CustomDirWithoutX(), HasX)
|
self.assertNotIsInstance(CustomDirWithoutX(), HasX)
|
||||||
|
|
||||||
|
def test_protocols_isinstance_attribute_access_with_side_effects(self):
|
||||||
|
class C:
|
||||||
|
@property
|
||||||
|
def attr(self):
|
||||||
|
raise AttributeError('no')
|
||||||
|
|
||||||
|
class CustomDescriptor:
|
||||||
|
def __get__(self, obj, objtype=None):
|
||||||
|
raise RuntimeError("NO")
|
||||||
|
|
||||||
|
class D:
|
||||||
|
attr = CustomDescriptor()
|
||||||
|
|
||||||
|
# Check that properties set on superclasses
|
||||||
|
# are still found by the isinstance() logic
|
||||||
|
class E(C): ...
|
||||||
|
class F(D): ...
|
||||||
|
|
||||||
|
class WhyWouldYouDoThis:
|
||||||
|
def __getattr__(self, name):
|
||||||
|
raise RuntimeError("wut")
|
||||||
|
|
||||||
|
T = TypeVar('T')
|
||||||
|
|
||||||
|
@runtime_checkable
|
||||||
|
class P(Protocol):
|
||||||
|
@property
|
||||||
|
def attr(self): ...
|
||||||
|
|
||||||
|
@runtime_checkable
|
||||||
|
class P1(Protocol):
|
||||||
|
attr: int
|
||||||
|
|
||||||
|
@runtime_checkable
|
||||||
|
class PG(Protocol[T]):
|
||||||
|
@property
|
||||||
|
def attr(self): ...
|
||||||
|
|
||||||
|
@runtime_checkable
|
||||||
|
class PG1(Protocol[T]):
|
||||||
|
attr: T
|
||||||
|
|
||||||
|
@runtime_checkable
|
||||||
|
class MethodP(Protocol):
|
||||||
|
def attr(self): ...
|
||||||
|
|
||||||
|
@runtime_checkable
|
||||||
|
class MethodPG(Protocol[T]):
|
||||||
|
def attr(self) -> T: ...
|
||||||
|
|
||||||
|
for protocol_class in P, P1, PG, PG1, MethodP, MethodPG:
|
||||||
|
for klass in C, D, E, F:
|
||||||
|
with self.subTest(
|
||||||
|
klass=klass.__name__,
|
||||||
|
protocol_class=protocol_class.__name__
|
||||||
|
):
|
||||||
|
self.assertIsInstance(klass(), protocol_class)
|
||||||
|
|
||||||
|
with self.subTest(
|
||||||
|
klass="WhyWouldYouDoThis",
|
||||||
|
protocol_class=protocol_class.__name__
|
||||||
|
):
|
||||||
|
self.assertNotIsInstance(WhyWouldYouDoThis(), protocol_class)
|
||||||
|
|
||||||
|
def test_protocols_isinstance___slots__(self):
|
||||||
|
# As per the consensus in https://github.com/python/typing/issues/1367,
|
||||||
|
# this is desirable behaviour
|
||||||
|
@runtime_checkable
|
||||||
|
class HasX(Protocol):
|
||||||
|
x: int
|
||||||
|
|
||||||
|
class HasNothingButSlots:
|
||||||
|
__slots__ = ("x",)
|
||||||
|
|
||||||
|
self.assertIsInstance(HasNothingButSlots(), HasX)
|
||||||
|
|
||||||
def test_protocols_isinstance_py36(self):
|
def test_protocols_isinstance_py36(self):
|
||||||
class APoint:
|
class APoint:
|
||||||
def __init__(self, x, y, label):
|
def __init__(self, x, y, label):
|
||||||
|
|
|
@ -1998,6 +1998,17 @@ _PROTO_ALLOWLIST = {
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
@functools.cache
|
||||||
|
def _lazy_load_getattr_static():
|
||||||
|
# Import getattr_static lazily so as not to slow down the import of typing.py
|
||||||
|
# Cache the result so we don't slow down _ProtocolMeta.__instancecheck__ unnecessarily
|
||||||
|
from inspect import getattr_static
|
||||||
|
return getattr_static
|
||||||
|
|
||||||
|
|
||||||
|
_cleanups.append(_lazy_load_getattr_static.cache_clear)
|
||||||
|
|
||||||
|
|
||||||
class _ProtocolMeta(ABCMeta):
|
class _ProtocolMeta(ABCMeta):
|
||||||
# This metaclass is really unfortunate and exists only because of
|
# This metaclass is really unfortunate and exists only because of
|
||||||
# the lack of __instancehook__.
|
# the lack of __instancehook__.
|
||||||
|
@ -2025,12 +2036,17 @@ class _ProtocolMeta(ABCMeta):
|
||||||
return True
|
return True
|
||||||
|
|
||||||
if is_protocol_cls:
|
if is_protocol_cls:
|
||||||
if all(hasattr(instance, attr) and
|
getattr_static = _lazy_load_getattr_static()
|
||||||
# All *methods* can be blocked by setting them to None.
|
for attr in protocol_attrs:
|
||||||
(not callable(getattr(cls, attr, None)) or
|
try:
|
||||||
getattr(instance, attr) is not None)
|
val = getattr_static(instance, attr)
|
||||||
for attr in protocol_attrs):
|
except AttributeError:
|
||||||
|
break
|
||||||
|
if callable(getattr(cls, attr, None)) and val is None:
|
||||||
|
break
|
||||||
|
else:
|
||||||
return True
|
return True
|
||||||
|
|
||||||
return super().__instancecheck__(instance)
|
return super().__instancecheck__(instance)
|
||||||
|
|
||||||
|
|
||||||
|
|
|
@ -0,0 +1,10 @@
|
||||||
|
:func:`isinstance` checks against :func:`runtime-checkable protocols
|
||||||
|
<typing.runtime_checkable>` now use :func:`inspect.getattr_static` rather
|
||||||
|
than :func:`hasattr` to lookup whether attributes exist. This means that
|
||||||
|
descriptors and :meth:`~object.__getattr__` methods are no longer
|
||||||
|
unexpectedly evaluated during ``isinstance()`` checks against
|
||||||
|
runtime-checkable protocols. However, it may also mean that some objects
|
||||||
|
which used to be considered instances of a runtime-checkable protocol may no
|
||||||
|
longer be considered instances of that protocol on Python 3.12+, and vice
|
||||||
|
versa. Most users are unlikely to be affected by this change. Patch by Alex
|
||||||
|
Waygood.
|
Loading…
Add table
Add a link
Reference in a new issue