Skip to content
Next Next commit
Backport CPython PR 26067
  • Loading branch information
AlexWaygood committed Apr 9, 2023
commit 587d84341242c1abdb6ca69e2b94d8a54786916a
4 changes: 4 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,10 @@
- Add `typing_extensions.Buffer`, a marker class for buffer types, as proposed
by PEP 688. Equivalent to `collections.abc.Buffer` in Python 3.12. Patch by
Jelle Zijlstra.
- Backport [CPython PR 26067](https://github.com/python/cpython/pull/26067)
(originally by Yurii Karabas), ensuring that `isinstance()` calls on
protocols raise `TypeError` when the protocol is not decorated with
`@runtime_checkable`. Patch by Alex Waygood.

# Release 4.5.0 (February 14, 2023)

Expand Down
7 changes: 7 additions & 0 deletions src/test_typing_extensions.py
Original file line number Diff line number Diff line change
Expand Up @@ -1918,6 +1918,13 @@ class CustomProtocol(TestCase, Protocol):
class CustomContextManager(typing.ContextManager, Protocol):
pass

def test_non_runtime_protocol_isinstance_check(self):
class P(Protocol):
x: int

with self.assertRaisesRegex(TypeError, "@runtime_checkable"):
isinstance(1, P)

def test_no_init_same_for_different_protocol_implementations(self):
class CustomProtocolWithoutInitA(Protocol):
pass
Expand Down
28 changes: 21 additions & 7 deletions src/typing_extensions.py
Original file line number Diff line number Diff line change
Expand Up @@ -468,11 +468,17 @@ def _caller(depth=2):
return None


# 3.8+
if hasattr(typing, 'Protocol'):
# A bug in runtime-checkable protocols was fixed in 3.10+,
# but we backport it to all versions
if sys.version_info >= (3, 10):
Protocol = typing.Protocol
# 3.7
else:
def _allow_reckless_class_checks(depth=4):
"""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.
"""
return _caller(depth) in {'abc', 'functools', None}

def _no_init(self, *args, **kwargs):
if type(self)._is_protocol:
Expand All @@ -484,11 +490,19 @@ class _ProtocolMeta(abc.ABCMeta): # noqa: B024
def __instancecheck__(cls, instance):
# We need this method for situations where attributes are
# assigned in __init__.
if ((not getattr(cls, '_is_protocol', False) or
is_protocol_cls = getattr(cls, "_is_protocol", False)
if (
is_protocol_cls and
not getattr(cls, '_is_runtime_protocol', False) and
not _allow_reckless_class_checks(depth=2)
):
raise TypeError("Instance and class checks can only be used with"
" @runtime_checkable protocols")
if ((not is_protocol_cls or
_is_callable_members_only(cls)) and
issubclass(instance.__class__, cls)):
return True
if cls._is_protocol:
if is_protocol_cls:
if all(hasattr(instance, attr) and
(not callable(getattr(cls, attr, None)) or
getattr(instance, attr) is not None)
Expand Down Expand Up @@ -581,12 +595,12 @@ def _proto_hook(other):
if not cls.__dict__.get('_is_protocol', None):
return NotImplemented
if not getattr(cls, '_is_runtime_protocol', False):
if _caller(depth=3) in {'abc', 'functools'}:
if _allow_reckless_class_checks():

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Is it correct that the depth is changed from 3 to 4?

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yes, because by adding a new function that the code has to pass through before it gets to the sys._getframe call, the call stack becomes "another frame deep"

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I could just have looked at _caller and figured that out myself. 🤦

return NotImplemented
raise TypeError("Instance and class checks can only be used with"
" @runtime protocols")
if not _is_callable_members_only(cls):
if _caller(depth=3) in {'abc', 'functools'}:
if _allow_reckless_class_checks():

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Here as well?

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Same as above

return NotImplemented
raise TypeError("Protocols with non-method members"
" don't support issubclass()")
Expand Down