Skip to content

Commit dfe4889

Browse files
authored
Backport some recent Protocol fixes from 3.12 (#161)
1 parent 7e6a4c0 commit dfe4889

File tree

2 files changed

+152
-28
lines changed

2 files changed

+152
-28
lines changed

src/test_typing_extensions.py

+116-3
Original file line numberDiff line numberDiff line change
@@ -1907,6 +1907,63 @@ class D(PNonCall): ...
19071907
with self.assertRaises(TypeError):
19081908
issubclass(D, PNonCall)
19091909

1910+
def test_no_weird_caching_with_issubclass_after_isinstance(self):
1911+
@runtime_checkable
1912+
class Spam(Protocol):
1913+
x: int
1914+
1915+
class Eggs:
1916+
def __init__(self) -> None:
1917+
self.x = 42
1918+
1919+
self.assertIsInstance(Eggs(), Spam)
1920+
1921+
# gh-104555: If we didn't override ABCMeta.__subclasscheck__ in _ProtocolMeta,
1922+
# TypeError wouldn't be raised here,
1923+
# as the cached result of the isinstance() check immediately above
1924+
# would mean the issubclass() call would short-circuit
1925+
# before we got to the "raise TypeError" line
1926+
with self.assertRaises(TypeError):
1927+
issubclass(Eggs, Spam)
1928+
1929+
def test_no_weird_caching_with_issubclass_after_isinstance_2(self):
1930+
@runtime_checkable
1931+
class Spam(Protocol):
1932+
x: int
1933+
1934+
class Eggs: ...
1935+
1936+
self.assertNotIsInstance(Eggs(), Spam)
1937+
1938+
# gh-104555: If we didn't override ABCMeta.__subclasscheck__ in _ProtocolMeta,
1939+
# TypeError wouldn't be raised here,
1940+
# as the cached result of the isinstance() check immediately above
1941+
# would mean the issubclass() call would short-circuit
1942+
# before we got to the "raise TypeError" line
1943+
with self.assertRaises(TypeError):
1944+
issubclass(Eggs, Spam)
1945+
1946+
def test_no_weird_caching_with_issubclass_after_isinstance_3(self):
1947+
@runtime_checkable
1948+
class Spam(Protocol):
1949+
x: int
1950+
1951+
class Eggs:
1952+
def __getattr__(self, attr):
1953+
if attr == "x":
1954+
return 42
1955+
raise AttributeError(attr)
1956+
1957+
self.assertNotIsInstance(Eggs(), Spam)
1958+
1959+
# gh-104555: If we didn't override ABCMeta.__subclasscheck__ in _ProtocolMeta,
1960+
# TypeError wouldn't be raised here,
1961+
# as the cached result of the isinstance() check immediately above
1962+
# would mean the issubclass() call would short-circuit
1963+
# before we got to the "raise TypeError" line
1964+
with self.assertRaises(TypeError):
1965+
issubclass(Eggs, Spam)
1966+
19101967
def test_protocols_isinstance(self):
19111968
T = TypeVar('T')
19121969
@runtime_checkable
@@ -2235,17 +2292,31 @@ def meth(self): pass
22352292
class NonP(P):
22362293
x = 1
22372294
class NonPR(PR): pass
2238-
class C:
2295+
class C(metaclass=abc.ABCMeta):
22392296
x = 1
2240-
class D:
2241-
def meth(self): pass
2297+
class D(metaclass=abc.ABCMeta): # noqa: B024
2298+
def meth(self): pass # noqa: B027
22422299
self.assertNotIsInstance(C(), NonP)
22432300
self.assertNotIsInstance(D(), NonPR)
22442301
self.assertNotIsSubclass(C, NonP)
22452302
self.assertNotIsSubclass(D, NonPR)
22462303
self.assertIsInstance(NonPR(), PR)
22472304
self.assertIsSubclass(NonPR, PR)
22482305

2306+
self.assertNotIn("__protocol_attrs__", vars(NonP))
2307+
self.assertNotIn("__protocol_attrs__", vars(NonPR))
2308+
self.assertNotIn("__callable_proto_members_only__", vars(NonP))
2309+
self.assertNotIn("__callable_proto_members_only__", vars(NonPR))
2310+
2311+
acceptable_extra_attrs = {
2312+
'_is_protocol', '_is_runtime_protocol', '__parameters__',
2313+
'__init__', '__annotations__', '__subclasshook__',
2314+
}
2315+
self.assertLessEqual(vars(NonP).keys(), vars(C).keys() | acceptable_extra_attrs)
2316+
self.assertLessEqual(
2317+
vars(NonPR).keys(), vars(D).keys() | acceptable_extra_attrs
2318+
)
2319+
22492320
def test_custom_subclasshook(self):
22502321
class P(Protocol):
22512322
x = 1
@@ -2325,6 +2396,48 @@ def bar(self, x: str) -> str:
23252396
with self.assertRaises(TypeError):
23262397
PR[int, ClassVar]
23272398

2399+
if sys.version_info >= (3, 12):
2400+
exec(textwrap.dedent(
2401+
"""
2402+
def test_pep695_generic_protocol_callable_members(self):
2403+
@runtime_checkable
2404+
class Foo[T](Protocol):
2405+
def meth(self, x: T) -> None: ...
2406+
2407+
class Bar[T]:
2408+
def meth(self, x: T) -> None: ...
2409+
2410+
self.assertIsInstance(Bar(), Foo)
2411+
self.assertIsSubclass(Bar, Foo)
2412+
2413+
@runtime_checkable
2414+
class SupportsTrunc[T](Protocol):
2415+
def __trunc__(self) -> T: ...
2416+
2417+
self.assertIsInstance(0.0, SupportsTrunc)
2418+
self.assertIsSubclass(float, SupportsTrunc)
2419+
2420+
def test_no_weird_caching_with_issubclass_after_isinstance_pep695(self):
2421+
@runtime_checkable
2422+
class Spam[T](Protocol):
2423+
x: T
2424+
2425+
class Eggs[T]:
2426+
def __init__(self, x: T) -> None:
2427+
self.x = x
2428+
2429+
self.assertIsInstance(Eggs(42), Spam)
2430+
2431+
# gh-104555: If we didn't override ABCMeta.__subclasscheck__ in _ProtocolMeta,
2432+
# TypeError wouldn't be raised here,
2433+
# as the cached result of the isinstance() check immediately above
2434+
# would mean the issubclass() call would short-circuit
2435+
# before we got to the "raise TypeError" line
2436+
with self.assertRaises(TypeError):
2437+
issubclass(Eggs, Spam)
2438+
"""
2439+
))
2440+
23282441
def test_init_called(self):
23292442
T = TypeVar('T')
23302443
class P(Protocol[T]): pass

src/typing_extensions.py

+36-25
Original file line numberDiff line numberDiff line change
@@ -470,6 +470,9 @@ def clear_overloads():
470470
if sys.version_info >= (3, 9):
471471
_EXCLUDED_ATTRS.add("__class_getitem__")
472472

473+
if sys.version_info >= (3, 12):
474+
_EXCLUDED_ATTRS.add("__type_params__")
475+
473476
_EXCLUDED_ATTRS = frozenset(_EXCLUDED_ATTRS)
474477

475478

@@ -550,23 +553,37 @@ def _no_init(self, *args, **kwargs):
550553
raise TypeError('Protocols cannot be instantiated')
551554

552555
class _ProtocolMeta(abc.ABCMeta):
553-
# This metaclass is a bit unfortunate and exists only because of the lack
554-
# of __instancehook__.
556+
# This metaclass is somewhat unfortunate,
557+
# but is necessary for several reasons...
555558
def __init__(cls, *args, **kwargs):
556559
super().__init__(*args, **kwargs)
557-
cls.__protocol_attrs__ = _get_protocol_attrs(cls)
558-
# PEP 544 prohibits using issubclass()
559-
# with protocols that have non-method members.
560-
cls.__callable_proto_members_only__ = all(
561-
callable(getattr(cls, attr, None)) for attr in cls.__protocol_attrs__
562-
)
560+
if getattr(cls, "_is_protocol", False):
561+
cls.__protocol_attrs__ = _get_protocol_attrs(cls)
562+
# PEP 544 prohibits using issubclass()
563+
# with protocols that have non-method members.
564+
cls.__callable_proto_members_only__ = all(
565+
callable(getattr(cls, attr, None)) for attr in cls.__protocol_attrs__
566+
)
567+
568+
def __subclasscheck__(cls, other):
569+
if (
570+
getattr(cls, '_is_protocol', False)
571+
and not cls.__callable_proto_members_only__
572+
and not _allow_reckless_class_checks(depth=3)
573+
):
574+
raise TypeError(
575+
"Protocols with non-method members don't support issubclass()"
576+
)
577+
return super().__subclasscheck__(other)
563578

564579
def __instancecheck__(cls, instance):
565580
# We need this method for situations where attributes are
566581
# assigned in __init__.
567-
is_protocol_cls = getattr(cls, "_is_protocol", False)
582+
if not getattr(cls, "_is_protocol", False):
583+
# i.e., it's a concrete subclass of a protocol
584+
return super().__instancecheck__(instance)
585+
568586
if (
569-
is_protocol_cls and
570587
not getattr(cls, '_is_runtime_protocol', False) and
571588
not _allow_reckless_class_checks(depth=2)
572589
):
@@ -576,16 +593,15 @@ def __instancecheck__(cls, instance):
576593
if super().__instancecheck__(instance):
577594
return True
578595

579-
if is_protocol_cls:
580-
for attr in cls.__protocol_attrs__:
581-
try:
582-
val = inspect.getattr_static(instance, attr)
583-
except AttributeError:
584-
break
585-
if val is None and callable(getattr(cls, attr, None)):
586-
break
587-
else:
588-
return True
596+
for attr in cls.__protocol_attrs__:
597+
try:
598+
val = inspect.getattr_static(instance, attr)
599+
except AttributeError:
600+
break
601+
if val is None and callable(getattr(cls, attr, None)):
602+
break
603+
else:
604+
return True
589605

590606
return False
591607

@@ -679,11 +695,6 @@ def _proto_hook(other):
679695
return NotImplemented
680696
raise TypeError("Instance and class checks can only be used with"
681697
" @runtime protocols")
682-
if not cls.__callable_proto_members_only__:
683-
if _allow_reckless_class_checks():
684-
return NotImplemented
685-
raise TypeError("Protocols with non-method members"
686-
" don't support issubclass()")
687698
if not isinstance(other, type):
688699
# Same error as for issubclass(1, int)
689700
raise TypeError('issubclass() arg 1 must be a class')

0 commit comments

Comments
 (0)