Allow typing_extensions.Protocol and typing.Protocol to mix (#237)
Fixes #236
Co-authored-by: Jelle Zijlstra <jelle.zijlstra@gmail.com>
diff --git a/CHANGELOG.md b/CHANGELOG.md
index 2df212b..191a583 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -24,6 +24,9 @@
- Allow `Protocol` classes to inherit from `typing_extensions.Buffer` or
`collections.abc.Buffer`. Patch by Alex Waygood (backporting
https://github.com/python/cpython/pull/104827, by Jelle Zijlstra).
+- Allow classes to inherit from both `typing.Protocol` and `typing_extensions.Protocol`
+ simultaneously. Since v4.6.0, this caused `TypeError` to be raised due to a
+ metaclass conflict. Patch by Alex Waygood.
# Release 4.6.3 (June 1, 2023)
diff --git a/doc/index.rst b/doc/index.rst
index 8d1f9a8..82109c6 100644
--- a/doc/index.rst
+++ b/doc/index.rst
@@ -282,6 +282,12 @@
Backported changes to runtime-checkable protocols from Python 3.12,
including :pr-cpy:`103034` and :pr-cpy:`26067`.
+ .. versionchanged:: 4.7.0
+
+ Classes can now inherit from both :py:class:`typing.Protocol` and
+ ``typing_extensions.Protocol`` simultaneously. Previously, this led to
+ :py:exc:`TypeError` being raised due to a metaclass conflict.
+
.. data:: Required
See :py:data:`typing.Required` and :pep:`655`. In ``typing`` since 3.11.
diff --git a/src/test_typing_extensions.py b/src/test_typing_extensions.py
index 102f0c1..bd1aa0f 100644
--- a/src/test_typing_extensions.py
+++ b/src/test_typing_extensions.py
@@ -1799,6 +1799,34 @@
self.assertIsInstance(Bar(), Foo)
self.assertNotIsInstance(object(), Foo)
+ @skipUnless(
+ hasattr(typing, "Protocol"),
+ "Test is only relevant if typing.Protocol exists"
+ )
+ def test_typing_Protocol_and_extensions_Protocol_can_mix(self):
+ class TypingProto(typing.Protocol):
+ x: int
+
+ class ExtensionsProto(Protocol):
+ y: int
+
+ class SubProto(TypingProto, ExtensionsProto, typing.Protocol):
+ z: int
+
+ class SubProto2(TypingProto, ExtensionsProto, Protocol):
+ z: int
+
+ class SubProto3(ExtensionsProto, TypingProto, typing.Protocol):
+ z: int
+
+ class SubProto4(ExtensionsProto, TypingProto, Protocol):
+ z: int
+
+ class Concrete(SubProto): pass
+ class Concrete2(SubProto2): pass
+ class Concrete3(SubProto3): pass
+ class Concrete4(SubProto4): pass
+
def test_no_instantiation(self):
class P(Protocol): pass
with self.assertRaises(TypeError):
diff --git a/src/typing_extensions.py b/src/typing_extensions.py
index 449ea5e..e6c1ca8 100644
--- a/src/typing_extensions.py
+++ b/src/typing_extensions.py
@@ -596,30 +596,54 @@
if type(self)._is_protocol:
raise TypeError('Protocols cannot be instantiated')
- class _ProtocolMeta(abc.ABCMeta):
+ if sys.version_info >= (3, 8):
+ # Inheriting from typing._ProtocolMeta isn't actually desirable,
+ # but is necessary to allow typing.Protocol and typing_extensions.Protocol
+ # to mix without getting TypeErrors about "metaclass conflict"
+ _typing_Protocol = typing.Protocol
+ _ProtocolMetaBase = type(_typing_Protocol)
+
+ def _is_protocol(cls):
+ return (
+ isinstance(cls, type)
+ and issubclass(cls, typing.Generic)
+ and getattr(cls, "_is_protocol", False)
+ )
+ else:
+ _typing_Protocol = _marker
+ _ProtocolMetaBase = abc.ABCMeta
+
+ def _is_protocol(cls):
+ return (
+ isinstance(cls, _ProtocolMeta)
+ and getattr(cls, "_is_protocol", False)
+ )
+
+ class _ProtocolMeta(_ProtocolMetaBase):
# This metaclass is somewhat unfortunate,
# but is necessary for several reasons...
+ #
+ # NOTE: DO NOT call super() in any methods in this class
+ # That would call the methods on typing._ProtocolMeta on Python 3.8-3.11
+ # and those are slow
def __new__(mcls, name, bases, namespace, **kwargs):
if name == "Protocol" and len(bases) < 2:
pass
- elif Protocol in bases:
+ elif {Protocol, _typing_Protocol} & set(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)
- )
+ or _is_protocol(base)
):
raise TypeError(
f"Protocols can only inherit from other protocols, "
f"got {base!r}"
)
- return super().__new__(mcls, name, bases, namespace, **kwargs)
+ return abc.ABCMeta.__new__(mcls, name, bases, namespace, **kwargs)
def __init__(cls, *args, **kwargs):
- super().__init__(*args, **kwargs)
+ abc.ABCMeta.__init__(cls, *args, **kwargs)
if getattr(cls, "_is_protocol", False):
cls.__protocol_attrs__ = _get_protocol_attrs(cls)
# PEP 544 prohibits using issubclass()
@@ -647,7 +671,7 @@
"Instance and class checks can only be used with "
"@runtime_checkable protocols"
)
- return super().__subclasscheck__(other)
+ return abc.ABCMeta.__subclasscheck__(cls, other)
def __instancecheck__(cls, instance):
# We need this method for situations where attributes are
@@ -656,7 +680,7 @@
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)
+ return abc.ABCMeta.__instancecheck__(cls, instance)
if (
not getattr(cls, '_is_runtime_protocol', False) and
@@ -665,7 +689,7 @@
raise TypeError("Instance and class checks can only be used with"
" @runtime_checkable protocols")
- if super().__instancecheck__(instance):
+ if abc.ABCMeta.__instancecheck__(cls, instance):
return True
for attr in cls.__protocol_attrs__:
@@ -684,7 +708,7 @@
# Hack so that typing.Generic.__class_getitem__
# treats typing_extensions.Protocol
# as equivalent to typing.Protocol on Python 3.8+
- if super().__eq__(other) is True:
+ if abc.ABCMeta.__eq__(cls, other) is True:
return True
return (
cls is Protocol and other is getattr(typing, "Protocol", object())