gh-104873: Add typing.get_protocol_members and typing.is_protocol (#104878)
Co-authored-by: Alex Waygood <Alex.Waygood@Gmail.com>
This commit is contained in:
parent
ba516e70c6
commit
fc8037d84c
@ -3388,6 +3388,38 @@ Introspection helpers
|
|||||||
|
|
||||||
.. versionadded:: 3.8
|
.. versionadded:: 3.8
|
||||||
|
|
||||||
|
.. function:: get_protocol_members(tp)
|
||||||
|
|
||||||
|
Return the set of members defined in a :class:`Protocol`.
|
||||||
|
|
||||||
|
::
|
||||||
|
|
||||||
|
>>> from typing import Protocol, get_protocol_members
|
||||||
|
>>> class P(Protocol):
|
||||||
|
... def a(self) -> str: ...
|
||||||
|
... b: int
|
||||||
|
>>> get_protocol_members(P)
|
||||||
|
frozenset({'a', 'b'})
|
||||||
|
|
||||||
|
Raise :exc:`TypeError` for arguments that are not Protocols.
|
||||||
|
|
||||||
|
.. versionadded:: 3.13
|
||||||
|
|
||||||
|
.. function:: is_protocol(tp)
|
||||||
|
|
||||||
|
Determine if a type is a :class:`Protocol`.
|
||||||
|
|
||||||
|
For example::
|
||||||
|
|
||||||
|
class P(Protocol):
|
||||||
|
def a(self) -> str: ...
|
||||||
|
b: int
|
||||||
|
|
||||||
|
is_protocol(P) # => True
|
||||||
|
is_protocol(int) # => False
|
||||||
|
|
||||||
|
.. versionadded:: 3.13
|
||||||
|
|
||||||
.. function:: is_typeddict(tp)
|
.. function:: is_typeddict(tp)
|
||||||
|
|
||||||
Check if a type is a :class:`TypedDict`.
|
Check if a type is a :class:`TypedDict`.
|
||||||
|
@ -120,6 +120,14 @@ traceback
|
|||||||
to format the nested exceptions of a :exc:`BaseExceptionGroup` instance, recursively.
|
to format the nested exceptions of a :exc:`BaseExceptionGroup` instance, recursively.
|
||||||
(Contributed by Irit Katriel in :gh:`105292`.)
|
(Contributed by Irit Katriel in :gh:`105292`.)
|
||||||
|
|
||||||
|
typing
|
||||||
|
------
|
||||||
|
|
||||||
|
* Add :func:`typing.get_protocol_members` to return the set of members
|
||||||
|
defining a :class:`typing.Protocol`. Add :func:`typing.is_protocol` to
|
||||||
|
check whether a class is a :class:`typing.Protocol`. (Contributed by Jelle Zijlstra in
|
||||||
|
:gh:`104873`.)
|
||||||
|
|
||||||
Optimizations
|
Optimizations
|
||||||
=============
|
=============
|
||||||
|
|
||||||
|
@ -24,9 +24,9 @@ from typing import Callable
|
|||||||
from typing import Generic, ClassVar, Final, final, Protocol
|
from typing import Generic, ClassVar, Final, final, Protocol
|
||||||
from typing import assert_type, cast, runtime_checkable
|
from typing import assert_type, cast, runtime_checkable
|
||||||
from typing import get_type_hints
|
from typing import get_type_hints
|
||||||
from typing import get_origin, get_args
|
from typing import get_origin, get_args, get_protocol_members
|
||||||
from typing import override
|
from typing import override
|
||||||
from typing import is_typeddict
|
from typing import is_typeddict, is_protocol
|
||||||
from typing import reveal_type
|
from typing import reveal_type
|
||||||
from typing import dataclass_transform
|
from typing import dataclass_transform
|
||||||
from typing import no_type_check, no_type_check_decorator
|
from typing import no_type_check, no_type_check_decorator
|
||||||
@ -3363,6 +3363,18 @@ class ProtocolTests(BaseTestCase):
|
|||||||
self.assertNotIn("__callable_proto_members_only__", vars(NonP))
|
self.assertNotIn("__callable_proto_members_only__", vars(NonP))
|
||||||
self.assertNotIn("__callable_proto_members_only__", vars(NonPR))
|
self.assertNotIn("__callable_proto_members_only__", vars(NonPR))
|
||||||
|
|
||||||
|
self.assertEqual(get_protocol_members(P), {"x"})
|
||||||
|
self.assertEqual(get_protocol_members(PR), {"meth"})
|
||||||
|
|
||||||
|
# the returned object should be immutable,
|
||||||
|
# and should be a different object to the original attribute
|
||||||
|
# to prevent users from (accidentally or deliberately)
|
||||||
|
# mutating the attribute on the original class
|
||||||
|
self.assertIsInstance(get_protocol_members(P), frozenset)
|
||||||
|
self.assertIsNot(get_protocol_members(P), P.__protocol_attrs__)
|
||||||
|
self.assertIsInstance(get_protocol_members(PR), frozenset)
|
||||||
|
self.assertIsNot(get_protocol_members(PR), P.__protocol_attrs__)
|
||||||
|
|
||||||
acceptable_extra_attrs = {
|
acceptable_extra_attrs = {
|
||||||
'_is_protocol', '_is_runtime_protocol', '__parameters__',
|
'_is_protocol', '_is_runtime_protocol', '__parameters__',
|
||||||
'__init__', '__annotations__', '__subclasshook__',
|
'__init__', '__annotations__', '__subclasshook__',
|
||||||
@ -3778,6 +3790,59 @@ class ProtocolTests(BaseTestCase):
|
|||||||
|
|
||||||
Foo() # Previously triggered RecursionError
|
Foo() # Previously triggered RecursionError
|
||||||
|
|
||||||
|
def test_get_protocol_members(self):
|
||||||
|
with self.assertRaisesRegex(TypeError, "not a Protocol"):
|
||||||
|
get_protocol_members(object)
|
||||||
|
with self.assertRaisesRegex(TypeError, "not a Protocol"):
|
||||||
|
get_protocol_members(object())
|
||||||
|
with self.assertRaisesRegex(TypeError, "not a Protocol"):
|
||||||
|
get_protocol_members(Protocol)
|
||||||
|
with self.assertRaisesRegex(TypeError, "not a Protocol"):
|
||||||
|
get_protocol_members(Generic)
|
||||||
|
|
||||||
|
class P(Protocol):
|
||||||
|
a: int
|
||||||
|
def b(self) -> str: ...
|
||||||
|
@property
|
||||||
|
def c(self) -> int: ...
|
||||||
|
|
||||||
|
self.assertEqual(get_protocol_members(P), {'a', 'b', 'c'})
|
||||||
|
self.assertIsInstance(get_protocol_members(P), frozenset)
|
||||||
|
self.assertIsNot(get_protocol_members(P), P.__protocol_attrs__)
|
||||||
|
|
||||||
|
class Concrete:
|
||||||
|
a: int
|
||||||
|
def b(self) -> str: return "capybara"
|
||||||
|
@property
|
||||||
|
def c(self) -> int: return 5
|
||||||
|
|
||||||
|
with self.assertRaisesRegex(TypeError, "not a Protocol"):
|
||||||
|
get_protocol_members(Concrete)
|
||||||
|
with self.assertRaisesRegex(TypeError, "not a Protocol"):
|
||||||
|
get_protocol_members(Concrete())
|
||||||
|
|
||||||
|
class ConcreteInherit(P):
|
||||||
|
a: int = 42
|
||||||
|
def b(self) -> str: return "capybara"
|
||||||
|
@property
|
||||||
|
def c(self) -> int: return 5
|
||||||
|
|
||||||
|
with self.assertRaisesRegex(TypeError, "not a Protocol"):
|
||||||
|
get_protocol_members(ConcreteInherit)
|
||||||
|
with self.assertRaisesRegex(TypeError, "not a Protocol"):
|
||||||
|
get_protocol_members(ConcreteInherit())
|
||||||
|
|
||||||
|
def test_is_protocol(self):
|
||||||
|
self.assertTrue(is_protocol(Proto))
|
||||||
|
self.assertTrue(is_protocol(Point))
|
||||||
|
self.assertFalse(is_protocol(Concrete))
|
||||||
|
self.assertFalse(is_protocol(Concrete()))
|
||||||
|
self.assertFalse(is_protocol(Generic))
|
||||||
|
self.assertFalse(is_protocol(object))
|
||||||
|
|
||||||
|
# Protocol is not itself a protocol
|
||||||
|
self.assertFalse(is_protocol(Protocol))
|
||||||
|
|
||||||
def test_interaction_with_isinstance_checks_on_superclasses_with_ABCMeta(self):
|
def test_interaction_with_isinstance_checks_on_superclasses_with_ABCMeta(self):
|
||||||
# Ensure the cache is empty, or this test won't work correctly
|
# Ensure the cache is empty, or this test won't work correctly
|
||||||
collections.abc.Sized._abc_registry_clear()
|
collections.abc.Sized._abc_registry_clear()
|
||||||
|
@ -131,7 +131,9 @@ __all__ = [
|
|||||||
'get_args',
|
'get_args',
|
||||||
'get_origin',
|
'get_origin',
|
||||||
'get_overloads',
|
'get_overloads',
|
||||||
|
'get_protocol_members',
|
||||||
'get_type_hints',
|
'get_type_hints',
|
||||||
|
'is_protocol',
|
||||||
'is_typeddict',
|
'is_typeddict',
|
||||||
'LiteralString',
|
'LiteralString',
|
||||||
'Never',
|
'Never',
|
||||||
@ -3337,3 +3339,43 @@ def override[F: _Func](method: F, /) -> F:
|
|||||||
# read-only property, TypeError if it's a builtin class.
|
# read-only property, TypeError if it's a builtin class.
|
||||||
pass
|
pass
|
||||||
return method
|
return method
|
||||||
|
|
||||||
|
|
||||||
|
def is_protocol(tp: type, /) -> bool:
|
||||||
|
"""Return True if the given type is a Protocol.
|
||||||
|
|
||||||
|
Example::
|
||||||
|
|
||||||
|
>>> from typing import Protocol, is_protocol
|
||||||
|
>>> class P(Protocol):
|
||||||
|
... def a(self) -> str: ...
|
||||||
|
... b: int
|
||||||
|
>>> is_protocol(P)
|
||||||
|
True
|
||||||
|
>>> is_protocol(int)
|
||||||
|
False
|
||||||
|
"""
|
||||||
|
return (
|
||||||
|
isinstance(tp, type)
|
||||||
|
and getattr(tp, '_is_protocol', False)
|
||||||
|
and tp != Protocol
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
def get_protocol_members(tp: type, /) -> frozenset[str]:
|
||||||
|
"""Return the set of members defined in a Protocol.
|
||||||
|
|
||||||
|
Example::
|
||||||
|
|
||||||
|
>>> from typing import Protocol, get_protocol_members
|
||||||
|
>>> class P(Protocol):
|
||||||
|
... def a(self) -> str: ...
|
||||||
|
... b: int
|
||||||
|
>>> get_protocol_members(P)
|
||||||
|
frozenset({'a', 'b'})
|
||||||
|
|
||||||
|
Raise a TypeError for arguments that are not Protocols.
|
||||||
|
"""
|
||||||
|
if not is_protocol(tp):
|
||||||
|
raise TypeError(f'{tp!r} is not a Protocol')
|
||||||
|
return frozenset(tp.__protocol_attrs__)
|
||||||
|
@ -0,0 +1,3 @@
|
|||||||
|
Add :func:`typing.get_protocol_members` to return the set of members
|
||||||
|
defining a :class:`typing.Protocol`. Add :func:`typing.is_protocol` to
|
||||||
|
check whether a class is a :class:`typing.Protocol`. Patch by Jelle Zijlstra.
|
Loading…
x
Reference in New Issue
Block a user