From e9dc660d00c2129d6056eb123ad7ca2b41c30a15 Mon Sep 17 00:00:00 2001 From: sobolevn Date: Fri, 5 Aug 2022 13:00:49 +0300 Subject: [PATCH 1/6] stubtest: analyze `metaclass` of types, refs #13327 --- mypy/stubtest.py | 34 ++++++++++++++++++++++++++ mypy/test/teststubtest.py | 50 +++++++++++++++++++++++++++++++++++++++ 2 files changed, 84 insertions(+) diff --git a/mypy/stubtest.py b/mypy/stubtest.py index ebc7fa12857d..7881386ab6e5 100644 --- a/mypy/stubtest.py +++ b/mypy/stubtest.py @@ -375,6 +375,40 @@ class SubClass(runtime): # type: ignore # Examples: ctypes.Array, ctypes._SimpleCData pass + # Check metaclass. We exclude protocols, because of how complex + # their implementation is in different versions of python. + # Enums are also hard, ignoring. + if not stub.is_protocol and not stub.is_enum: + runtime_metaclass = type(runtime) + if runtime_metaclass is not type and stub.metaclass_type is None: + # This means that runtime has a custom metaclass, but a stub does not. + yield Error( + object_path, + "metaclass missmatch", + stub, + runtime, + stub_desc="Missing metaclass", + runtime_desc=f"Exiting metaclass: {runtime_metaclass}", + ) + elif ( + runtime_metaclass is type + and stub.metaclass_type is not None + # We ignore extra `ABCMeta` metaclass on stubs, this might be typing hack. + # We also ignore `builtins.type` metaclass as an implementation detail in mypy. + and not mypy.types.is_named_instance( + stub.metaclass_type, ("abc.ABCMeta", "builtins.type") + ) + ): + # This means that our stub has a metaclass that is not present at runtime. + yield Error( + object_path, + "metaclass missmatch", + stub, + runtime, + stub_desc=f"Existing metaclass: {stub.metaclass_type.type.fullname}", + runtime_desc="Missing metaclass", + ) + # Check everything already defined on the stub class itself (i.e. not inherited) to_check = set(stub.names) # Check all public things on the runtime class diff --git a/mypy/test/teststubtest.py b/mypy/test/teststubtest.py index 3de0e3fd5fc6..6e7fc7ff699b 100644 --- a/mypy/test/teststubtest.py +++ b/mypy/test/teststubtest.py @@ -1264,6 +1264,56 @@ def test_type_var(self) -> Iterator[Case]: ) yield Case(stub="C = ParamSpec('C')", runtime="C = ParamSpec('C')", error=None) + @collect_cases + def test_metaclass_match(self) -> Iterator[Case]: + yield Case(stub="class Meta(type): ...", runtime="class Meta(type): ...", error=None) + yield Case(stub="class A0: ...", runtime="class A0: ...", error=None) + yield Case( + stub="class A1(metaclass=Meta): ...", + runtime="class A1(metaclass=Meta): ...", + error=None, + ) + yield Case(stub="class A2: ...", runtime="class A2(metaclass=Meta): ...", error="A2") + yield Case(stub="class A3(metaclass=Meta): ...", runtime="class A3: ...", error="A3") + + # With inheritance: + yield Case( + stub=""" + class I1(metaclass=Meta): ... + class S1(I1): ... + """, + runtime=""" + class I1(metaclass=Meta): ... + class S1(I1): ... + """, + error=None, + ) + yield Case( + stub=""" + class I2(metaclass=Meta): ... + class S2: ... # missing inheritance + """, + runtime=""" + class I2(metaclass=Meta): ... + class S2(I2): ... + """, + error="S2", + ) + + @collect_cases + def test_metaclass_abcmeta(self) -> Iterator[Case]: + # Handling abstract metaclasses is special: + yield Case(stub="from abc import ABCMeta", runtime="from abc import ABCMeta", error=None) + yield Case( + stub="class A1(metaclass=ABCMeta): ...", + runtime="class A1(metaclass=ABCMeta): ...", + error=None, + ) + # Stubs cannot miss abstract metaclass: + yield Case(stub="class A2: ...", runtime="class A2(metaclass=ABCMeta): ...", error="A2") + # But, stubs can add extra abstract metaclass, this might be a typing hack: + yield Case(stub="class A3(metaclass=ABCMeta): ...", runtime="class A3: ...", error=None) + def remove_color_code(s: str) -> str: return re.sub("\\x1b.*?m", "", s) # this works! From 99c404b678eca3c9b3715541af3589fe63609938 Mon Sep 17 00:00:00 2001 From: sobolevn Date: Fri, 5 Aug 2022 15:00:36 +0300 Subject: [PATCH 2/6] Add more tests --- mypy/stubtest.py | 5 +++-- mypy/test/teststubtest.py | 5 +++++ 2 files changed, 8 insertions(+), 2 deletions(-) diff --git a/mypy/stubtest.py b/mypy/stubtest.py index 7881386ab6e5..3db1514201d9 100644 --- a/mypy/stubtest.py +++ b/mypy/stubtest.py @@ -378,13 +378,14 @@ class SubClass(runtime): # type: ignore # Check metaclass. We exclude protocols, because of how complex # their implementation is in different versions of python. # Enums are also hard, ignoring. + # NOTE: we do not check that metaclasses are identical just yet. if not stub.is_protocol and not stub.is_enum: runtime_metaclass = type(runtime) if runtime_metaclass is not type and stub.metaclass_type is None: # This means that runtime has a custom metaclass, but a stub does not. yield Error( object_path, - "metaclass missmatch", + "metaclass mismatch", stub, runtime, stub_desc="Missing metaclass", @@ -402,7 +403,7 @@ class SubClass(runtime): # type: ignore # This means that our stub has a metaclass that is not present at runtime. yield Error( object_path, - "metaclass missmatch", + "metaclass mismatch", stub, runtime, stub_desc=f"Existing metaclass: {stub.metaclass_type.type.fullname}", diff --git a/mypy/test/teststubtest.py b/mypy/test/teststubtest.py index 6e7fc7ff699b..46b45ac6d0fa 100644 --- a/mypy/test/teststubtest.py +++ b/mypy/test/teststubtest.py @@ -1276,6 +1276,11 @@ def test_metaclass_match(self) -> Iterator[Case]: yield Case(stub="class A2: ...", runtime="class A2(metaclass=Meta): ...", error="A2") yield Case(stub="class A3(metaclass=Meta): ...", runtime="class A3: ...", error="A3") + # Explicit `type` metaclass can always be added in any part: + yield Case(stub="class T1(metaclass=type): ...", runtime="class T1(metaclass=type): ...", error=None) + yield Case(stub="class T2: ...", runtime="class T2(metaclass=type): ...", error=None) + yield Case(stub="class T3(metaclass=type): ...", runtime="class T3: ...", error=None) + # With inheritance: yield Case( stub=""" From 39c4388ea5cf075f56cbab424c94a5235a3f7f8a Mon Sep 17 00:00:00 2001 From: sobolevn Date: Fri, 5 Aug 2022 15:03:05 +0300 Subject: [PATCH 3/6] Add explicit protected name test --- mypy/test/teststubtest.py | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/mypy/test/teststubtest.py b/mypy/test/teststubtest.py index 46b45ac6d0fa..a4c9e5f80988 100644 --- a/mypy/test/teststubtest.py +++ b/mypy/test/teststubtest.py @@ -1281,6 +1281,10 @@ def test_metaclass_match(self) -> Iterator[Case]: yield Case(stub="class T2: ...", runtime="class T2(metaclass=type): ...", error=None) yield Case(stub="class T3(metaclass=type): ...", runtime="class T3: ...", error=None) + # Explicit check that `_protected` names are also supported: + yield Case(stub="class _P1(type): ...", runtime="class _P1(type): ...", error=None) + yield Case(stub="class P2: ...", runtime="class P2(metaclass=_P1): ...", error="P2") + # With inheritance: yield Case( stub=""" From 1700716acde64c5e4cc033cb00924a6c0ec0ca73 Mon Sep 17 00:00:00 2001 From: sobolevn Date: Fri, 5 Aug 2022 15:07:26 +0300 Subject: [PATCH 4/6] Fix black --- mypy/test/teststubtest.py | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/mypy/test/teststubtest.py b/mypy/test/teststubtest.py index a4c9e5f80988..4ed5d92e9698 100644 --- a/mypy/test/teststubtest.py +++ b/mypy/test/teststubtest.py @@ -1277,7 +1277,11 @@ def test_metaclass_match(self) -> Iterator[Case]: yield Case(stub="class A3(metaclass=Meta): ...", runtime="class A3: ...", error="A3") # Explicit `type` metaclass can always be added in any part: - yield Case(stub="class T1(metaclass=type): ...", runtime="class T1(metaclass=type): ...", error=None) + yield Case( + stub="class T1(metaclass=type): ...", + runtime="class T1(metaclass=type): ...", + error=None, + ) yield Case(stub="class T2: ...", runtime="class T2(metaclass=type): ...", error=None) yield Case(stub="class T3(metaclass=type): ...", runtime="class T3: ...", error=None) From a445bf30e3f6b5fa3d57fff96cf2b8d92f7dc391 Mon Sep 17 00:00:00 2001 From: hauntsaninja Date: Wed, 24 Aug 2022 20:01:50 -0700 Subject: [PATCH 5/6] tweak error message, refactor into functions --- mypy/stubtest.py | 48 +++++++++++++++++++++++++++++------------------- 1 file changed, 29 insertions(+), 19 deletions(-) diff --git a/mypy/stubtest.py b/mypy/stubtest.py index 0019fe198357..32c27f0e644e 100644 --- a/mypy/stubtest.py +++ b/mypy/stubtest.py @@ -349,17 +349,9 @@ def _belongs_to_runtime(r: types.ModuleType, attr: str) -> bool: yield from verify(stub_entry, runtime_entry, object_path + [entry]) -@verify.register(nodes.TypeInfo) -def verify_typeinfo( - stub: nodes.TypeInfo, runtime: MaybeMissing[type[Any]], object_path: list[str] +def _verify_final( + stub: nodes.TypeInfo, runtime: type[Any], object_path: list[str] ) -> Iterator[Error]: - if isinstance(runtime, Missing): - yield Error(object_path, "is not present at runtime", stub, runtime, stub_desc=repr(stub)) - return - if not isinstance(runtime, type): - yield Error(object_path, "is not a type", stub, runtime, stub_desc=repr(stub)) - return - try: class SubClass(runtime): # type: ignore @@ -380,21 +372,24 @@ class SubClass(runtime): # type: ignore # Examples: ctypes.Array, ctypes._SimpleCData pass - # Check metaclass. We exclude protocols, because of how complex - # their implementation is in different versions of python. - # Enums are also hard, ignoring. - # NOTE: we do not check that metaclasses are identical just yet. + +def _verify_metaclass( + stub: nodes.TypeInfo, runtime: type[Any], object_path: list[str] +) -> Iterator[Error]: + # We exclude protocols, because of how complex their implementation is in different versions of + # python. Enums are also hard, ignoring. + # TODO: check that metaclasses are identical? if not stub.is_protocol and not stub.is_enum: runtime_metaclass = type(runtime) if runtime_metaclass is not type and stub.metaclass_type is None: # This means that runtime has a custom metaclass, but a stub does not. yield Error( object_path, - "metaclass mismatch", + "is inconsistent, metaclass differs", stub, runtime, - stub_desc="Missing metaclass", - runtime_desc=f"Exiting metaclass: {runtime_metaclass}", + stub_desc="N/A", + runtime_desc=f"{runtime_metaclass}", ) elif ( runtime_metaclass is type @@ -411,10 +406,25 @@ class SubClass(runtime): # type: ignore "metaclass mismatch", stub, runtime, - stub_desc=f"Existing metaclass: {stub.metaclass_type.type.fullname}", - runtime_desc="Missing metaclass", + stub_desc=f"{stub.metaclass_type.type.fullname}", + runtime_desc=f"N/A", ) + +@verify.register(nodes.TypeInfo) +def verify_typeinfo( + stub: nodes.TypeInfo, runtime: MaybeMissing[type[Any]], object_path: list[str] +) -> Iterator[Error]: + if isinstance(runtime, Missing): + yield Error(object_path, "is not present at runtime", stub, runtime, stub_desc=repr(stub)) + return + if not isinstance(runtime, type): + yield Error(object_path, "is not a type", stub, runtime, stub_desc=repr(stub)) + return + + yield from _verify_final(stub, runtime, object_path) + yield from _verify_metaclass(stub, runtime, object_path) + # Check everything already defined on the stub class itself (i.e. not inherited) to_check = set(stub.names) # Check all public things on the runtime class From 9c59544183dc60b898ef0f14760b2e8a3905c9f8 Mon Sep 17 00:00:00 2001 From: hauntsaninja Date: Wed, 24 Aug 2022 20:50:37 -0700 Subject: [PATCH 6/6] fix lint --- mypy/stubtest.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/mypy/stubtest.py b/mypy/stubtest.py index 32c27f0e644e..31053f120931 100644 --- a/mypy/stubtest.py +++ b/mypy/stubtest.py @@ -407,7 +407,7 @@ def _verify_metaclass( stub, runtime, stub_desc=f"{stub.metaclass_type.type.fullname}", - runtime_desc=f"N/A", + runtime_desc="N/A", )