From 2966617f826e92a4c3134413dff50d6fc9dd4d87 Mon Sep 17 00:00:00 2001 From: Kyle Benesch <4b796c65+github@gmail.com> Date: Wed, 21 Jun 2023 09:36:52 -0700 Subject: [PATCH 1/6] Test `__slots__` used as a protocol member --- test-data/unit/check-protocols.test | 36 +++++++++++++++++++++++++++++ 1 file changed, 36 insertions(+) diff --git a/test-data/unit/check-protocols.test b/test-data/unit/check-protocols.test index 6976b8ee0a39..7b5215b76f16 100644 --- a/test-data/unit/check-protocols.test +++ b/test-data/unit/check-protocols.test @@ -2789,6 +2789,42 @@ class A(Protocol): [builtins fixtures/tuple.pyi] +[case testProtocolSlotsIsNotProtocolMember] +# https://github.com/python/mypy/issues/11884 +from typing import Protocol + +class Foo(Protocol): + __slots__ = () +class NoSlots: + pass +class EmptySlots: + __slots__ = () +class TupleSlots: + __slots__ = ('x', 'y') +class StringSlots: + __slots__ = 'x y' +def foo(f: Foo): + pass + +# All should pass: +foo(NoSlots()) +foo(EmptySlots()) +foo(TupleSlots()) +foo(StringSlots()) +[builtins fixtures/tuple.pyi] + +[case testProtocolSlotsAndRuntimeCheckable] +from typing import Protocol, runtime_checkable + +@runtime_checkable +class Foo(Protocol): + __slots__ = () +class Bar: + pass +issubclass(Bar, Foo) # Used to be an error, when `__slots__` counted as a protocol member +[builtins fixtures/isinstance.pyi] +[typing fixtures/typing-full.pyi] + [case testNoneVsProtocol] # mypy: strict-optional from typing_extensions import Protocol From 67872e81101781e8c1d73f346cd87963a075c071 Mon Sep 17 00:00:00 2001 From: Kyle Benesch <4b796c65+github@gmail.com> Date: Wed, 21 Jun 2023 09:43:47 -0700 Subject: [PATCH 2/6] Exclude special attributes not collected by Python --- mypy/nodes.py | 30 ++++++++++++++++++++++++++++++ 1 file changed, 30 insertions(+) diff --git a/mypy/nodes.py b/mypy/nodes.py index 4e29a434d7fe..6ef2d49a8888 100644 --- a/mypy/nodes.py +++ b/mypy/nodes.py @@ -2801,6 +2801,34 @@ def accept(self, visitor: ExpressionVisitor[T]) -> T: return visitor.visit_temp_node(self) +# Special attributes not collected as protocol members by Python 3.12 +# See typing.EXCLUDED_ATTRIBUTES and typing._get_protocol_attrs +EXCLUDED_ATTRIBUTES = frozenset( + { + "__parameters__", + "__orig_bases__", + "__orig_class__", + "_is_protocol", + "_is_runtime_protocol", + "__protocol_attrs__", + "__callable_proto_members_only__", + "__type_params__", + "__abstractmethods__", + "__annotations__", + "__dict__", + "__doc__", + "__init__", + "__module__", + "__new__", + "__slots__", + "__subclasshook__", + "__weakref__", + "__class_getitem__", + "_MutableMapping__marker", + } +) + + class TypeInfo(SymbolNode): """The type structure of a single class. @@ -3115,6 +3143,8 @@ def protocol_members(self) -> list[str]: if isinstance(node.node, (TypeAlias, TypeVarExpr, MypyFile)): # These are auxiliary definitions (and type aliases are prohibited). continue + if name in EXCLUDED_ATTRIBUTES: + continue members.add(name) return sorted(list(members)) From e2edbf93f2de5e2b9be610b38f952247af4be98f Mon Sep 17 00:00:00 2001 From: Kyle Benesch <4b796c65+github@gmail.com> Date: Wed, 21 Jun 2023 10:57:29 -0700 Subject: [PATCH 3/6] Add tests for `__class_getitem__` in protocols --- test-data/unit/check-protocols.test | 23 +++++++++++++++++++++++ 1 file changed, 23 insertions(+) diff --git a/test-data/unit/check-protocols.test b/test-data/unit/check-protocols.test index 7b5215b76f16..fffbf3df6e4b 100644 --- a/test-data/unit/check-protocols.test +++ b/test-data/unit/check-protocols.test @@ -2825,6 +2825,29 @@ issubclass(Bar, Foo) # Used to be an error, when `__slots__` counted as a proto [builtins fixtures/isinstance.pyi] [typing fixtures/typing-full.pyi] + +[case testProtocolWithClassGetItem] +# https://github.com/python/mypy/issues/11886 +from typing import Any, Iterable, Protocol, Union + +class B: + ... + +class C: + def __class_getitem__(cls, __item: Any) -> Any: + ... + +class SupportsClassGetItem(Protocol): + __slots__: Union[str, Iterable[str]] = () + def __class_getitem__(cls, __item: Any) -> Any: + ... + +b1: SupportsClassGetItem = B() +c1: SupportsClassGetItem = C() +[builtins fixtures/tuple.pyi] +[typing fixtures/typing-full.pyi] + + [case testNoneVsProtocol] # mypy: strict-optional from typing_extensions import Protocol From 87ddf420d3812ebb3a479a5e9c8d2b66e405d20d Mon Sep 17 00:00:00 2001 From: Kyle Benesch <4b796c65+github@gmail.com> Date: Wed, 21 Jun 2023 11:45:02 -0700 Subject: [PATCH 4/6] Make EXCLUDED_ATTRIBUTES a class constant Reduce the items to just `typing._SPECIAL_NAMES` --- mypy/nodes.py | 48 +++++++++++++++++++----------------------------- 1 file changed, 19 insertions(+), 29 deletions(-) diff --git a/mypy/nodes.py b/mypy/nodes.py index 6ef2d49a8888..d669f0a51bb4 100644 --- a/mypy/nodes.py +++ b/mypy/nodes.py @@ -2801,34 +2801,6 @@ def accept(self, visitor: ExpressionVisitor[T]) -> T: return visitor.visit_temp_node(self) -# Special attributes not collected as protocol members by Python 3.12 -# See typing.EXCLUDED_ATTRIBUTES and typing._get_protocol_attrs -EXCLUDED_ATTRIBUTES = frozenset( - { - "__parameters__", - "__orig_bases__", - "__orig_class__", - "_is_protocol", - "_is_runtime_protocol", - "__protocol_attrs__", - "__callable_proto_members_only__", - "__type_params__", - "__abstractmethods__", - "__annotations__", - "__dict__", - "__doc__", - "__init__", - "__module__", - "__new__", - "__slots__", - "__subclasshook__", - "__weakref__", - "__class_getitem__", - "_MutableMapping__marker", - } -) - - class TypeInfo(SymbolNode): """The type structure of a single class. @@ -3045,6 +3017,24 @@ class is generic then it will be a type constructor of higher kind. "is_intersection", ] + # Special attributes not collected as protocol members by Python 3.12 + # See typing._SPECIAL_NAMES + EXCLUDED_ATTRIBUTES: Final = frozenset( + { + "__abstractmethods__", + "__annotations__", + "__dict__", + "__doc__", + "__init__", + "__module__", + "__new__", + "__slots__", + "__subclasshook__", + "__weakref__", + "__class_getitem__", # Since Python 3.9 + } + ) + def __init__(self, names: SymbolTable, defn: ClassDef, module_name: str) -> None: """Initialize a TypeInfo.""" super().__init__() @@ -3143,7 +3133,7 @@ def protocol_members(self) -> list[str]: if isinstance(node.node, (TypeAlias, TypeVarExpr, MypyFile)): # These are auxiliary definitions (and type aliases are prohibited). continue - if name in EXCLUDED_ATTRIBUTES: + if name in self.EXCLUDED_ATTRIBUTES: continue members.add(name) return sorted(list(members)) From b348e714b98ded11ccdaac713898bf32d0056bcf Mon Sep 17 00:00:00 2001 From: Kyle Benesch <4b796c65+github@gmail.com> Date: Wed, 21 Jun 2023 12:43:37 -0700 Subject: [PATCH 5/6] Move excluded attributes back to the top-level --- mypy/nodes.py | 39 ++++++++++++++++++++------------------- 1 file changed, 20 insertions(+), 19 deletions(-) diff --git a/mypy/nodes.py b/mypy/nodes.py index d669f0a51bb4..360a29efe036 100644 --- a/mypy/nodes.py +++ b/mypy/nodes.py @@ -2801,6 +2801,25 @@ def accept(self, visitor: ExpressionVisitor[T]) -> T: return visitor.visit_temp_node(self) +# Special attributes not collected as protocol members by Python 3.12 +# See typing._SPECIAL_NAMES +EXCLUDED_PROTOCOL_ATTRIBUTES: Final = frozenset( + { + "__abstractmethods__", + "__annotations__", + "__dict__", + "__doc__", + "__init__", + "__module__", + "__new__", + "__slots__", + "__subclasshook__", + "__weakref__", + "__class_getitem__", # Since Python 3.9 + } +) + + class TypeInfo(SymbolNode): """The type structure of a single class. @@ -3017,24 +3036,6 @@ class is generic then it will be a type constructor of higher kind. "is_intersection", ] - # Special attributes not collected as protocol members by Python 3.12 - # See typing._SPECIAL_NAMES - EXCLUDED_ATTRIBUTES: Final = frozenset( - { - "__abstractmethods__", - "__annotations__", - "__dict__", - "__doc__", - "__init__", - "__module__", - "__new__", - "__slots__", - "__subclasshook__", - "__weakref__", - "__class_getitem__", # Since Python 3.9 - } - ) - def __init__(self, names: SymbolTable, defn: ClassDef, module_name: str) -> None: """Initialize a TypeInfo.""" super().__init__() @@ -3133,7 +3134,7 @@ def protocol_members(self) -> list[str]: if isinstance(node.node, (TypeAlias, TypeVarExpr, MypyFile)): # These are auxiliary definitions (and type aliases are prohibited). continue - if name in self.EXCLUDED_ATTRIBUTES: + if name in EXCLUDED_PROTOCOL_ATTRIBUTES: continue members.add(name) return sorted(list(members)) From f473ea0d6b088e83c6fa8fe716590bae42dae0d3 Mon Sep 17 00:00:00 2001 From: Kyle Benesch <4b796c65+github@gmail.com> Date: Wed, 21 Jun 2023 18:17:51 -0700 Subject: [PATCH 6/6] Add test for Protocol slots with init assignment Confirms and fixes #11013 --- test-data/unit/check-protocols.test | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/test-data/unit/check-protocols.test b/test-data/unit/check-protocols.test index fffbf3df6e4b..6ba1fde4d022 100644 --- a/test-data/unit/check-protocols.test +++ b/test-data/unit/check-protocols.test @@ -2803,6 +2803,10 @@ class TupleSlots: __slots__ = ('x', 'y') class StringSlots: __slots__ = 'x y' +class InitSlots: + __slots__ = ('x',) + def __init__(self) -> None: + self.x = None def foo(f: Foo): pass @@ -2811,6 +2815,7 @@ foo(NoSlots()) foo(EmptySlots()) foo(TupleSlots()) foo(StringSlots()) +foo(InitSlots()) [builtins fixtures/tuple.pyi] [case testProtocolSlotsAndRuntimeCheckable]