Skip to content
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

Backport CPython PR 105152 #208

Merged
merged 4 commits into from
Jun 1, 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
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