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 recent fixes to Protocol from 3.12 #218

Merged
merged 5 commits into from
Jun 5, 2023
Merged
Show file tree
Hide file tree
Changes from 3 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__, {})
AlexWaygood marked this conversation as resolved.
Show resolved Hide resolved
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