Skip to content

Commit

Permalink
Backport CPython PR 105152 (#208)
Browse files Browse the repository at this point in the history
  • Loading branch information
AlexWaygood authored and JelleZijlstra committed Jun 1, 2023
1 parent b8a2ece commit 22c0e70
Show file tree
Hide file tree
Showing 3 changed files with 120 additions and 39 deletions.
7 changes: 7 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -1,5 +1,12 @@
# Unreleased

- Fix a regression introduced in v4.6.0 in the implementation of
runtime-checkable protocols. The regression meant
that doing `class Foo(X, typing_extensions.Protocol)`, where `X` was a class that
had `abc.ABCMeta` as its metaclass, would then cause subsequent
`isinstance(1, X)` calls to erroneously raise `TypeError`. Patch by
Alex Waygood (backporting the CPython PR
https://github.com/python/cpython/pull/105152).
- Sync the repository's LICENSE file with that of CPython.
`typing_extensions` is distributed under the same license as
CPython itself.
Expand Down
116 changes: 98 additions & 18 deletions src/test_typing_extensions.py
Original file line number Diff line number Diff line change
Expand Up @@ -1698,7 +1698,7 @@ class NT(NamedTuple):

skip_if_py312b1 = skipIf(
sys.version_info == (3, 12, 0, 'beta', 1),
"CPython had a bug in 3.12.0b1"
"CPython had bugs in 3.12.0b1"
)


Expand Down Expand Up @@ -1902,40 +1902,75 @@ def x(self): ...
self.assertIsSubclass(C, P)
self.assertIsSubclass(C, PG)
self.assertIsSubclass(BadP, PG)
with self.assertRaises(TypeError):

no_subscripted_generics = (
"Subscripted generics cannot be used with class and instance checks"
)

with self.assertRaisesRegex(TypeError, no_subscripted_generics):
issubclass(C, PG[T])
with self.assertRaises(TypeError):
with self.assertRaisesRegex(TypeError, no_subscripted_generics):
issubclass(C, PG[C])
with self.assertRaises(TypeError):

only_runtime_checkable_protocols = (
"Instance and class checks can only be used with "
"@runtime_checkable protocols"
)

with self.assertRaisesRegex(TypeError, only_runtime_checkable_protocols):
issubclass(C, BadP)
with self.assertRaises(TypeError):
with self.assertRaisesRegex(TypeError, only_runtime_checkable_protocols):
issubclass(C, BadPG)
with self.assertRaises(TypeError):

with self.assertRaisesRegex(TypeError, no_subscripted_generics):
issubclass(P, PG[T])
with self.assertRaises(TypeError):
with self.assertRaisesRegex(TypeError, no_subscripted_generics):
issubclass(PG, PG[int])

only_classes_allowed = r"issubclass\(\) arg 1 must be a class"

with self.assertRaisesRegex(TypeError, only_classes_allowed):
issubclass(1, P)
with self.assertRaisesRegex(TypeError, only_classes_allowed):
issubclass(1, PG)
with self.assertRaisesRegex(TypeError, only_classes_allowed):
issubclass(1, BadP)
with self.assertRaisesRegex(TypeError, only_classes_allowed):
issubclass(1, BadPG)

def test_protocols_issubclass_non_callable(self):
class C:
x = 1

@runtime_checkable
class PNonCall(Protocol):
x = 1
with self.assertRaises(TypeError):

non_callable_members_illegal = (
"Protocols with non-method members don't support issubclass()"
)

with self.assertRaisesRegex(TypeError, non_callable_members_illegal):
issubclass(C, PNonCall)

self.assertIsInstance(C(), PNonCall)
PNonCall.register(C)
with self.assertRaises(TypeError):

with self.assertRaisesRegex(TypeError, non_callable_members_illegal):
issubclass(C, PNonCall)

self.assertIsInstance(C(), PNonCall)

# check that non-protocol subclasses are not affected
class D(PNonCall): ...

self.assertNotIsSubclass(C, D)
self.assertNotIsInstance(C(), D)
D.register(C)
self.assertIsSubclass(C, D)
self.assertIsInstance(C(), D)
with self.assertRaises(TypeError):

with self.assertRaisesRegex(TypeError, non_callable_members_illegal):
issubclass(D, PNonCall)

def test_no_weird_caching_with_issubclass_after_isinstance(self):
Expand All @@ -1954,7 +1989,10 @@ def __init__(self) -> None:
# as the cached result of the isinstance() check immediately above
# would mean the issubclass() call would short-circuit
# before we got to the "raise TypeError" line
with self.assertRaises(TypeError):
with self.assertRaisesRegex(
TypeError,
"Protocols with non-method members don't support issubclass()"
):
issubclass(Eggs, Spam)

def test_no_weird_caching_with_issubclass_after_isinstance_2(self):
Expand All @@ -1971,7 +2009,10 @@ class Eggs: ...
# as the cached result of the isinstance() check immediately above
# would mean the issubclass() call would short-circuit
# before we got to the "raise TypeError" line
with self.assertRaises(TypeError):
with self.assertRaisesRegex(
TypeError,
"Protocols with non-method members don't support issubclass()"
):
issubclass(Eggs, Spam)

def test_no_weird_caching_with_issubclass_after_isinstance_3(self):
Expand All @@ -1992,7 +2033,10 @@ def __getattr__(self, attr):
# as the cached result of the isinstance() check immediately above
# would mean the issubclass() call would short-circuit
# before we got to the "raise TypeError" line
with self.assertRaises(TypeError):
with self.assertRaisesRegex(
TypeError,
"Protocols with non-method members don't support issubclass()"
):
issubclass(Eggs, Spam)

def test_protocols_isinstance(self):
Expand Down Expand Up @@ -2028,13 +2072,24 @@ def __init__(self):
for proto in P, PG, WeirdProto, WeirdProto2, WeirderProto:
with self.subTest(klass=klass.__name__, proto=proto.__name__):
self.assertIsInstance(klass(), proto)
with self.assertRaises(TypeError):

no_subscripted_generics = (
"Subscripted generics cannot be used with class and instance checks"
)

with self.assertRaisesRegex(TypeError, no_subscripted_generics):
isinstance(C(), PG[T])
with self.assertRaises(TypeError):
with self.assertRaisesRegex(TypeError, no_subscripted_generics):
isinstance(C(), PG[C])
with self.assertRaises(TypeError):

only_runtime_checkable_msg = (
"Instance and class checks can only be used "
"with @runtime_checkable protocols"
)

with self.assertRaisesRegex(TypeError, only_runtime_checkable_msg):
isinstance(C(), BadP)
with self.assertRaises(TypeError):
with self.assertRaisesRegex(TypeError, only_runtime_checkable_msg):
isinstance(C(), BadPG)

def test_protocols_isinstance_properties_and_descriptors(self):
Expand Down Expand Up @@ -2435,12 +2490,13 @@ def __subclasshook__(cls, other):
self.assertIsSubclass(OKClass, C)
self.assertNotIsSubclass(BadClass, C)

@skip_if_py312b1
def test_issubclass_fails_correctly(self):
@runtime_checkable
class P(Protocol):
x = 1
class C: pass
with self.assertRaises(TypeError):
with self.assertRaisesRegex(TypeError, r"issubclass\(\) arg 1 must be a class"):
issubclass(C(), P)

def test_defining_generic_protocols(self):
Expand Down Expand Up @@ -2768,6 +2824,30 @@ def __call__(self, *args: Unpack[Ts]) -> T: ...
self.assertEqual(Y.__parameters__, ())
self.assertEqual(Y.__args__, (int, bytes, memoryview))

@skip_if_py312b1
def test_interaction_with_isinstance_checks_on_superclasses_with_ABCMeta(self):
# Ensure the cache is empty, or this test won't work correctly
collections.abc.Sized._abc_registry_clear()

class Foo(collections.abc.Sized, Protocol): pass

# CPython gh-105144: this previously raised TypeError
# if a Protocol subclass of Sized had been created
# before any isinstance() checks against Sized
self.assertNotIsInstance(1, collections.abc.Sized)

@skip_if_py312b1
def test_interaction_with_isinstance_checks_on_superclasses_with_ABCMeta_2(self):
# Ensure the cache is empty, or this test won't work correctly
collections.abc.Sized._abc_registry_clear()

class Foo(typing.Sized, Protocol): pass

# CPython gh-105144: this previously raised TypeError
# if a Protocol subclass of Sized had been created
# before any isinstance() checks against Sized
self.assertNotIsInstance(1, typing.Sized)


class Point2DGeneric(Generic[T], TypedDict):
a: T
Expand Down
36 changes: 15 additions & 21 deletions src/typing_extensions.py
Original file line number Diff line number Diff line change
Expand Up @@ -547,7 +547,7 @@ def _caller(depth=2):
Protocol = typing.Protocol
runtime_checkable = typing.runtime_checkable
else:
def _allow_reckless_class_checks(depth=4):
def _allow_reckless_class_checks(depth=3):
"""Allow instance and class checks for special stdlib modules.
The abc and functools modules indiscriminately call isinstance() and
issubclass() on the whole MRO of a user class, which may contain protocols.
Expand All @@ -572,14 +572,22 @@ def __init__(cls, *args, **kwargs):
)

def __subclasscheck__(cls, other):
if not isinstance(other, type):
# Same error message as for issubclass(1, int).
raise TypeError('issubclass() arg 1 must be a class')
if (
getattr(cls, '_is_protocol', False)
and not cls.__callable_proto_members_only__
and not _allow_reckless_class_checks(depth=3)
and not _allow_reckless_class_checks()
):
raise TypeError(
"Protocols with non-method members don't support issubclass()"
)
if not cls.__callable_proto_members_only__:
raise TypeError(
"Protocols with non-method members don't support issubclass()"
)
if not getattr(cls, '_is_runtime_protocol', False):
raise TypeError(
"Instance and class checks can only be used with "
"@runtime_checkable protocols"
)
return super().__subclasscheck__(other)

def __instancecheck__(cls, instance):
Expand All @@ -591,7 +599,7 @@ def __instancecheck__(cls, instance):

if (
not getattr(cls, '_is_runtime_protocol', False) and
not _allow_reckless_class_checks(depth=2)
not _allow_reckless_class_checks()
):
raise TypeError("Instance and class checks can only be used with"
" @runtime_checkable protocols")
Expand Down Expand Up @@ -632,18 +640,6 @@ def _proto_hook(cls, other):
if not cls.__dict__.get('_is_protocol', False):
return NotImplemented

# First, perform various sanity checks.
if not getattr(cls, '_is_runtime_protocol', False):
if _allow_reckless_class_checks():
return NotImplemented
raise TypeError("Instance and class checks can only be used with"
" @runtime_checkable protocols")

if not isinstance(other, type):
# Same error message as for issubclass(1, int).
raise TypeError('issubclass() arg 1 must be a class')

# Second, perform the actual structural compatibility check.
for attr in cls.__protocol_attrs__:
for base in other.__mro__:
# Check if the members appears in the class dictionary...
Expand All @@ -658,8 +654,6 @@ def _proto_hook(cls, other):
isinstance(annotations, collections.abc.Mapping)
and attr in annotations
and issubclass(other, (typing.Generic, _ProtocolMeta))
# All subclasses of Generic have an _is_proto attribute on 3.8+
# But not on 3.7
and getattr(other, "_is_protocol", False)
):
break
Expand Down

0 comments on commit 22c0e70

Please sign in to comment.