Skip to content

Commit 8e14ace

Browse files
authored
Use inspect.getattr_static in _ProtocolMeta.__instancecheck__ (#140)
1 parent 6c93956 commit 8e14ace

File tree

3 files changed

+102
-3
lines changed

3 files changed

+102
-3
lines changed

CHANGELOG.md

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -19,6 +19,16 @@
1919
`isinstance()` checks comparing objects to the protocol. See
2020
["What's New in Python 3.12"](https://docs.python.org/3.12/whatsnew/3.12.html#typing)
2121
for more details.
22+
- `isinstance()` checks against runtime-checkable protocols now use
23+
`inspect.getattr_static()` rather than `hasattr()` to lookup whether
24+
attributes exist (backporting https://github.com/python/cpython/pull/103034).
25+
This means that descriptors and `__getattr__` methods are no longer
26+
unexpectedly evaluated during `isinstance()` checks against runtime-checkable
27+
protocols. However, it may also mean that some objects which used to be
28+
considered instances of a runtime-checkable protocol on older versions of
29+
`typing_extensions` may no longer be considered instances of that protocol
30+
using the new release, and vice versa. Most users are unlikely to be affected
31+
by this change. Patch by Alex Waygood.
2232

2333
# Release 4.5.0 (February 14, 2023)
2434

src/test_typing_extensions.py

Lines changed: 91 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1658,7 +1658,15 @@ def attr(self): ...
16581658
class PG1(Protocol[T]):
16591659
attr: T
16601660

1661-
for protocol_class in P, P1, PG, PG1:
1661+
@runtime_checkable
1662+
class MethodP(Protocol):
1663+
def attr(self): ...
1664+
1665+
@runtime_checkable
1666+
class MethodPG(Protocol[T]):
1667+
def attr(self) -> T: ...
1668+
1669+
for protocol_class in P, P1, PG, PG1, MethodP, MethodPG:
16621670
for klass in C, D, E, F:
16631671
with self.subTest(
16641672
klass=klass.__name__,
@@ -1683,7 +1691,12 @@ def attr(self): ...
16831691
class BadPG1(Protocol[T]):
16841692
attr: T
16851693

1686-
for obj in PG[T], PG[C], PG1[T], PG1[C], BadP, BadP1, BadPG, BadPG1:
1694+
cases = (
1695+
PG[T], PG[C], PG1[T], PG1[C], MethodPG[T],
1696+
MethodPG[C], BadP, BadP1, BadPG, BadPG1
1697+
)
1698+
1699+
for obj in cases:
16871700
for klass in C, D, E, F, Empty:
16881701
with self.subTest(klass=klass.__name__, obj=obj):
16891702
with self.assertRaises(TypeError):
@@ -1706,6 +1719,82 @@ def __dir__(self):
17061719
self.assertIsInstance(CustomDirWithX(), HasX)
17071720
self.assertNotIsInstance(CustomDirWithoutX(), HasX)
17081721

1722+
def test_protocols_isinstance_attribute_access_with_side_effects(self):
1723+
class C:
1724+
@property
1725+
def attr(self):
1726+
raise AttributeError('no')
1727+
1728+
class CustomDescriptor:
1729+
def __get__(self, obj, objtype=None):
1730+
raise RuntimeError("NO")
1731+
1732+
class D:
1733+
attr = CustomDescriptor()
1734+
1735+
# Check that properties set on superclasses
1736+
# are still found by the isinstance() logic
1737+
class E(C): ...
1738+
class F(D): ...
1739+
1740+
class WhyWouldYouDoThis:
1741+
def __getattr__(self, name):
1742+
raise RuntimeError("wut")
1743+
1744+
T = TypeVar('T')
1745+
1746+
@runtime_checkable
1747+
class P(Protocol):
1748+
@property
1749+
def attr(self): ...
1750+
1751+
@runtime_checkable
1752+
class P1(Protocol):
1753+
attr: int
1754+
1755+
@runtime_checkable
1756+
class PG(Protocol[T]):
1757+
@property
1758+
def attr(self): ...
1759+
1760+
@runtime_checkable
1761+
class PG1(Protocol[T]):
1762+
attr: T
1763+
1764+
@runtime_checkable
1765+
class MethodP(Protocol):
1766+
def attr(self): ...
1767+
1768+
@runtime_checkable
1769+
class MethodPG(Protocol[T]):
1770+
def attr(self) -> T: ...
1771+
1772+
for protocol_class in P, P1, PG, PG1, MethodP, MethodPG:
1773+
for klass in C, D, E, F:
1774+
with self.subTest(
1775+
klass=klass.__name__,
1776+
protocol_class=protocol_class.__name__
1777+
):
1778+
self.assertIsInstance(klass(), protocol_class)
1779+
1780+
with self.subTest(
1781+
klass="WhyWouldYouDoThis",
1782+
protocol_class=protocol_class.__name__
1783+
):
1784+
self.assertNotIsInstance(WhyWouldYouDoThis(), protocol_class)
1785+
1786+
def test_protocols_isinstance___slots__(self):
1787+
# As per the consensus in https://github.com/python/typing/issues/1367,
1788+
# this is desirable behaviour
1789+
@runtime_checkable
1790+
class HasX(Protocol):
1791+
x: int
1792+
1793+
class HasNothingButSlots:
1794+
__slots__ = ("x",)
1795+
1796+
self.assertIsInstance(HasNothingButSlots(), HasX)
1797+
17091798
def test_protocols_isinstance_py36(self):
17101799
class APoint:
17111800
def __init__(self, x, y, label):

src/typing_extensions.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -524,7 +524,7 @@ def __instancecheck__(cls, instance):
524524
if is_protocol_cls:
525525
for attr in cls.__protocol_attrs__:
526526
try:
527-
val = getattr(instance, attr)
527+
val = inspect.getattr_static(instance, attr)
528528
except AttributeError:
529529
break
530530
if val is None and callable(getattr(cls, attr, None)):

0 commit comments

Comments
 (0)