diff --git a/Doc/library/typing.rst b/Doc/library/typing.rst index 92943c46ef5132..2c6fd3c2d7bcf5 100644 --- a/Doc/library/typing.rst +++ b/Doc/library/typing.rst @@ -2905,6 +2905,23 @@ Introspection helpers .. 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) + {'a', 'b'} + + Return None for arguments that are not Protocols. + + .. versionadded:: 3.13 + .. function:: is_typeddict(tp) Check if a type is a :class:`TypedDict`. diff --git a/Doc/whatsnew/3.13.rst b/Doc/whatsnew/3.13.rst index 1102225e50b658..64146f0b764352 100644 --- a/Doc/whatsnew/3.13.rst +++ b/Doc/whatsnew/3.13.rst @@ -87,6 +87,13 @@ New Modules Improved Modules ================ +typing +------ + +* Added :func:`typing.get_protocol_members` to return the set of members + defining a :class:`typing.Protocol`. (Contributed by Jelle Zijlstra in + :gh:`104873`.) + Optimizations ============= diff --git a/Lib/test/test_typing.py b/Lib/test/test_typing.py index 098933b7cb434f..d89b3a5045de83 100644 --- a/Lib/test/test_typing.py +++ b/Lib/test/test_typing.py @@ -22,7 +22,7 @@ from typing import Generic, ClassVar, Final, final, Protocol from typing import assert_type, cast, runtime_checkable 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 is_typeddict from typing import reveal_type @@ -3172,6 +3172,11 @@ def meth(self): pass self.assertNotIn("__callable_proto_members_only__", vars(NonP)) self.assertNotIn("__callable_proto_members_only__", vars(NonPR)) + self.assertIs(get_protocol_members(NonP), None) + self.assertIs(get_protocol_members(NonPR), None) + self.assertEqual(get_protocol_members(P), {"x"}) + self.assertEqual(get_protocol_members(PR), {"meth"}) + acceptable_extra_attrs = { '_is_protocol', '_is_runtime_protocol', '__parameters__', '__init__', '__annotations__', '__subclasshook__', @@ -3587,6 +3592,39 @@ def __init__(self): Foo() # Previously triggered RecursionError + def test_get_protocol_members(self): + self.assertIs(get_protocol_members(object), None) + self.assertIs(get_protocol_members(object()), None) + self.assertIs(get_protocol_members(Protocol), None) + self.assertIs(get_protocol_members(Generic), None) + + class P(Protocol): + a: int + def b(self) -> str: ... + @property + def c(self) -> int: ... + + self.assertEqual(get_protocol_members(P), {'a', 'b', 'c'}) + + class Concrete: + a: int + def b(self) -> str: return "capybara" + @property + def c(self) -> int: return 5 + + self.assertIs(get_protocol_members(Concrete), None) + self.assertIs(get_protocol_members(Concrete()), None) + + class ConcreteInherit(P): + a: int = 42 + def b(self) -> str: return "capybara" + @property + def c(self) -> int: return 5 + + # not a protocol + self.assertEqual(get_protocol_members(ConcreteInherit), None) + self.assertIs(get_protocol_members(ConcreteInherit()), None) + class GenericTests(BaseTestCase): diff --git a/Lib/typing.py b/Lib/typing.py index b32ff0c6ba4e25..8dc8b7686d8b09 100644 --- a/Lib/typing.py +++ b/Lib/typing.py @@ -132,6 +132,7 @@ 'get_args', 'get_origin', 'get_overloads', + 'get_protocol_members', 'get_type_hints', 'is_typeddict', 'LiteralString', @@ -3296,3 +3297,23 @@ def method(self) -> None: # read-only property, TypeError if it's a builtin class. pass return method + + +def get_protocol_members(tp: type, /) -> set[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) + {'a', 'b'} + + Return None for arguments that are not Protocols. + + """ + if not getattr(tp, '_is_protocol', False) or tp is Protocol: + return None + return tp.__protocol_attrs__ diff --git a/Misc/NEWS.d/next/Library/2023-05-24-09-55-33.gh-issue-104873.BKQ54y.rst b/Misc/NEWS.d/next/Library/2023-05-24-09-55-33.gh-issue-104873.BKQ54y.rst new file mode 100644 index 00000000000000..89f2b51e7a0759 --- /dev/null +++ b/Misc/NEWS.d/next/Library/2023-05-24-09-55-33.gh-issue-104873.BKQ54y.rst @@ -0,0 +1,2 @@ +Add :func:`typing.get_protocol_members` to return the set of members +defining a :class:`typing.Protocol`. Patch by Jelle Zijlstra.