Skip to content

Use inspect.getattr_static in _ProtocolMeta.__instancecheck__ #140

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 1 commit into from
Apr 12, 2023
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
10 changes: 10 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,16 @@
`isinstance()` checks comparing objects to the protocol. See
["What's New in Python 3.12"](https://docs.python.org/3.12/whatsnew/3.12.html#typing)
for more details.
- `isinstance()` checks against runtime-checkable protocols now use
`inspect.getattr_static()` rather than `hasattr()` to lookup whether
attributes exist (backporting https://github.com/python/cpython/pull/103034).
This means that descriptors and `__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 on older versions of
`typing_extensions` may no longer be considered instances of that protocol
using the new release, and vice versa. Most users are unlikely to be affected
by this change. Patch by Alex Waygood.

# Release 4.5.0 (February 14, 2023)

Expand Down
93 changes: 91 additions & 2 deletions src/test_typing_extensions.py
Original file line number Diff line number Diff line change
Expand Up @@ -1658,7 +1658,15 @@ def attr(self): ...
class PG1(Protocol[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:
with self.subTest(
klass=klass.__name__,
Expand All @@ -1683,7 +1691,12 @@ def attr(self): ...
class BadPG1(Protocol[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:
with self.subTest(klass=klass.__name__, obj=obj):
with self.assertRaises(TypeError):
Expand All @@ -1706,6 +1719,82 @@ def __dir__(self):
self.assertIsInstance(CustomDirWithX(), 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):
class APoint:
def __init__(self, x, y, label):
Expand Down
2 changes: 1 addition & 1 deletion src/typing_extensions.py
Original file line number Diff line number Diff line change
Expand Up @@ -524,7 +524,7 @@ def __instancecheck__(cls, instance):
if is_protocol_cls:
for attr in cls.__protocol_attrs__:
try:
val = getattr(instance, attr)
val = inspect.getattr_static(instance, attr)
except AttributeError:
break
if val is None and callable(getattr(cls, attr, None)):
Expand Down