Skip to content
Merged
Show file tree
Hide file tree
Changes from 4 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
4 changes: 1 addition & 3 deletions .github/workflows/third_party.yml
Original file line number Diff line number Diff line change
Expand Up @@ -265,9 +265,7 @@ jobs:
strategy:
fail-fast: false
matrix:
# TODO: add 3.7 back to this matrix when tests pass on 3.7 again
# (issue #213)
python-version: ["3.8", "3.9", "3.10", "3.11"]
python-version: ["3.7", "3.8", "3.9", "3.10", "3.11"]
runs-on: ubuntu-latest
timeout-minutes: 60
steps:
Expand Down
4 changes: 4 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,10 @@
- Fix tests on Python 3.13, which removes support for creating
`TypedDict` classes through the keyword-argument syntax. Patch by
Jelle Zijlstra.
- Fix a regression introduced in v4.6.3 that meant that
``issubclass(object, typing_extensions.Protocol)`` would erroneously raise
``TypeError``. Patch by Alex Waygood (backporting the CPython PR
https://github.com/python/cpython/pull/105239).

# Release 4.6.3 (June 1, 2023)

Expand Down
75 changes: 75 additions & 0 deletions src/test_typing_extensions.py
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
import sys
import os
import abc
import gc
import io
import contextlib
import collections
Expand Down Expand Up @@ -1938,6 +1939,80 @@ def x(self): ...
with self.assertRaisesRegex(TypeError, only_classes_allowed):
issubclass(1, BadPG)

@skip_if_py312b1
def test_issubclass_and_isinstance_on_Protocol_itself(self):
class C:
def x(self): pass

self.assertNotIsSubclass(object, Protocol)
self.assertNotIsInstance(object(), Protocol)

self.assertNotIsSubclass(str, Protocol)
self.assertNotIsInstance('foo', Protocol)

self.assertNotIsSubclass(C, Protocol)
self.assertNotIsInstance(C(), Protocol)

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

with self.assertRaisesRegex(TypeError, only_classes_allowed):
issubclass(1, Protocol)
with self.assertRaisesRegex(TypeError, only_classes_allowed):
issubclass('foo', Protocol)
with self.assertRaisesRegex(TypeError, only_classes_allowed):
issubclass(C(), Protocol)

T = TypeVar('T')

@runtime_checkable
class EmptyProtocol(Protocol): pass

@runtime_checkable
class SupportsStartsWith(Protocol):
def startswith(self, x: str) -> bool: ...

@runtime_checkable
class SupportsX(Protocol[T]):
def x(self): ...

for proto in EmptyProtocol, SupportsStartsWith, SupportsX:
with self.subTest(proto=proto.__name__):
self.assertIsSubclass(proto, Protocol)

# gh-105237 / PR #105239:
# check that the presence of Protocol subclasses
# where `issubclass(X, <subclass>)` evaluates to True
# doesn't influence the result of `issubclass(X, Protocol)`

self.assertIsSubclass(object, EmptyProtocol)
self.assertIsInstance(object(), EmptyProtocol)
self.assertNotIsSubclass(object, Protocol)
self.assertNotIsInstance(object(), Protocol)

self.assertIsSubclass(str, SupportsStartsWith)
self.assertIsInstance('foo', SupportsStartsWith)
self.assertNotIsSubclass(str, Protocol)
self.assertNotIsInstance('foo', Protocol)

self.assertIsSubclass(C, SupportsX)
self.assertIsInstance(C(), SupportsX)
self.assertNotIsSubclass(C, Protocol)
self.assertNotIsInstance(C(), Protocol)

@skip_if_py312b1
def test_isinstance_checks_not_at_whim_of_gc(self):
self.addCleanup(gc.enable)
gc.disable()

with self.assertRaisesRegex(
TypeError,
"Protocols can only inherit from other protocols"
):
class Foo(collections.abc.Mapping, Protocol):
pass

self.assertNotIsInstance([], collections.abc.Mapping)

def test_protocols_issubclass_non_callable(self):
class C:
x = 1
Expand Down
38 changes: 25 additions & 13 deletions src/typing_extensions.py
Original file line number Diff line number Diff line change
Expand Up @@ -561,6 +561,25 @@ def _no_init(self, *args, **kwargs):
class _ProtocolMeta(abc.ABCMeta):
# This metaclass is somewhat unfortunate,
# but is necessary for several reasons...
def __new__(mcls, name, bases, namespace, **kwargs):
if name == "Protocol" and len(bases) < 2:
pass
elif Protocol in bases:
for base in bases:
if not (
base in {object, typing.Generic}
or base.__name__ in _PROTO_ALLOWLIST.get(base.__module__, [])
or (
isinstance(base, _ProtocolMeta)
and getattr(base, "_is_protocol", False)
)
):
raise TypeError(
f"Protocols can only inherit from other protocols, "
f"got {base!r}"
)
return super().__new__(mcls, name, bases, namespace, **kwargs)

def __init__(cls, *args, **kwargs):
super().__init__(*args, **kwargs)
if getattr(cls, "_is_protocol", False):
Expand All @@ -572,6 +591,8 @@ def __init__(cls, *args, **kwargs):
)

def __subclasscheck__(cls, other):
if cls is Protocol:
return type.__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')
Expand All @@ -593,6 +614,8 @@ def __subclasscheck__(cls, other):
def __instancecheck__(cls, instance):
# We need this method for situations where attributes are
# assigned in __init__.
if cls is Protocol:
return type.__instancecheck__(cls, instance)
if not getattr(cls, "_is_protocol", False):
# i.e., it's a concrete subclass of a protocol
return super().__instancecheck__(instance)
Expand Down Expand Up @@ -661,15 +684,6 @@ def _proto_hook(cls, other):
return NotImplemented
return True

def _check_proto_bases(cls):
for base in cls.__bases__:
if not (base in (object, typing.Generic) or
base.__module__ in _PROTO_ALLOWLIST and
base.__name__ in _PROTO_ALLOWLIST[base.__module__] or
isinstance(base, _ProtocolMeta) and base._is_protocol):
raise TypeError('Protocols can only inherit from other'
f' protocols, got {repr(base)}')

if sys.version_info >= (3, 8):
class Protocol(typing.Generic, metaclass=_ProtocolMeta):
__doc__ = typing.Protocol.__doc__
Expand All @@ -692,8 +706,7 @@ def __init_subclass__(cls, *args, **kwargs):
if not cls._is_protocol:
return

# ... otherwise check consistency of bases, and prohibit instantiation.
_check_proto_bases(cls)
# ... otherwise prohibit instantiation.
if cls.__init__ is Protocol.__init__:
cls.__init__ = _no_init

Expand Down Expand Up @@ -788,8 +801,7 @@ def __init_subclass__(cls, *args, **kwargs):
if not cls._is_protocol:
return

# Check consistency of bases.
_check_proto_bases(cls)
# Prohibit instantiation
if cls.__init__ is Protocol.__init__:
cls.__init__ = _no_init

Expand Down