From 15b58351edc5dc1779ce16055b183a1c512f6b58 Mon Sep 17 00:00:00 2001 From: AlexWaygood Date: Wed, 13 Jul 2022 20:08:21 +0100 Subject: [PATCH 01/18] stubtest: Reduce false-positive errors on runtime type aliases --- mypy/stubtest.py | 34 +++++++++++-- mypy/test/teststubtest.py | 102 +++++++++++++++++++++++++++++++++++++- 2 files changed, 132 insertions(+), 4 deletions(-) diff --git a/mypy/stubtest.py b/mypy/stubtest.py index 3928ee009f7f..478a3ae2259f 100644 --- a/mypy/stubtest.py +++ b/mypy/stubtest.py @@ -5,6 +5,7 @@ """ import argparse +import collections.abc import copy import enum import importlib @@ -22,7 +23,7 @@ from pathlib import Path from typing import Any, Dict, Generic, Iterator, List, Optional, Tuple, TypeVar, Union, cast -from typing_extensions import Type +from typing_extensions import Type, get_origin import mypy.build import mypy.modulefinder @@ -1017,15 +1018,34 @@ def verify_typealias( ) return if isinstance(stub_target, mypy.types.Instance): - yield from verify(stub_target.type, runtime, object_path) + runtime_type = get_origin(runtime) or runtime + if not isinstance(runtime_type, type): + yield Error(object_path, "is inconsistent: runtime is not a type", stub, runtime) + return + # Don't test the fullname, + # stubs can sometimes be in different modules to the runtime for various reasons + # (e.g. we want compatibility between collections.abc and typing, etc.) + try: + runtime_name = runtime_type.__name__ + except AttributeError: + return + stub_name = stub_target.type.name + if runtime_name != stub_name: + msg = ( + f"is inconsistent: runtime is an alias for {runtime_name} " + f"but stub is an alias for {stub_name}" + ) + yield Error(object_path, msg, stub, runtime) return if isinstance(stub_target, mypy.types.UnionType): + if sys.version_info >= (3, 10) and isinstance(runtime, types.UnionType): + return if not getattr(runtime, "__origin__", None) is Union: yield Error(object_path, "is not a Union", stub, runtime, stub_desc=str(stub_target)) # could check Union contents here... return if isinstance(stub_target, mypy.types.TupleType): - if tuple not in getattr(runtime, "__mro__", ()): + if tuple not in getattr(runtime, "__mro__", getattr(get_origin(runtime), "__mro__", ())): yield Error( object_path, "is not a subclass of tuple", stub, runtime, stub_desc=str(stub_target) @@ -1034,6 +1054,14 @@ def verify_typealias( return if isinstance(stub_target, mypy.types.AnyType): return + if isinstance(stub_target, mypy.types.CallableType): + callables = {typing.Callable, collections.abc.Callable} + if runtime in callables or get_origin(runtime) in callables: + return + yield Error( + object_path, "is not a type alias for Callable", stub, runtime, + stub_desc=str(stub_target) + ) yield Error( object_path, "is not a recognised type alias", stub, runtime, stub_desc=str(stub_target) ) diff --git a/mypy/test/teststubtest.py b/mypy/test/teststubtest.py index 72944f44414c..633ae7c15cec 100644 --- a/mypy/test/teststubtest.py +++ b/mypy/test/teststubtest.py @@ -44,6 +44,7 @@ def __getitem__(self, typeargs: Any) -> object: ... Callable: _SpecialForm = ... Generic: _SpecialForm = ... Protocol: _SpecialForm = ... +Union: _SpecialForm = ... class TypeVar: def __init__(self, name, covariant: bool = ..., contravariant: bool = ...) -> None: ... @@ -706,7 +707,7 @@ class X: def f(self) -> None: ... class Y: ... """, - error="Y.f", + error="Y", ) yield Case( stub=""" @@ -728,6 +729,105 @@ class Y: ... runtime="", error=None ) + yield Case( + stub=""" + from typing import Tuple + D = tuple[str, str] + E = Tuple[int, int, int] + F = Tuple[str, int] + """, + runtime=""" + from typing import List, Tuple + D = Tuple[str, str] + E = Tuple[int, int, int] + F = List[str] + """, + error="F" + ) + yield Case( + stub=""" + from typing import Union + G = str | int + H = Union[str, bool] + I = str | int + """, + runtime=""" + from typing import Union + G = Union[str, int] + H = Union[str, bool] + I = str + """, + error="I" + ) + yield Case( + stub=""" + import typing + from collections.abc import Iterable + from typing import Dict + K = dict[str, str] + L = Dict[int, int] + M = Dict[str, int] + KK = Iterable[str] + LL = typing.Iterable[str] + """, + runtime=""" + from typing import Iterable, Dict, List + K = Dict[str, str] + L = Dict[int, int] + M = List[int] + KK = Iterable[str] + LL = Iterable[str] + """, + error="M" + ) + yield Case(stub="MM = list[str]", runtime="['foo', 'bar']", error="MM") + yield Case( + stub=""" + import collections.abc + from typing import Callable + N = Callable[[str], bool] + O = collections.abc.Callable[[int], str] + P = Callable[[str], bool] + """, + runtime=""" + from typing import Callable + N = Callable[[str], bool] + O = Callable[[int], str] + P = int + """, + error="P" + ) + if sys.version_info >= (3, 10): + yield Case( + stub=""" + import collections.abc + from typing import Callable, Dict, Iterable, Tuple, Union + Q = Dict[str, str] + R = dict[int, int] + S = Tuple[int, int] + T = tuple[str, str] + U = int | str + V = Union[int, str] + W = Callable[[str], bool] + Z = collections.abc.Callable[[str], bool] + QQ = Iterable[str] + RR = collections.abc.Iterable[str] + """, + runtime=""" + from collections.abc import Callable, Iterable + Q = dict[str, str] + R = dict[int, int] + S = tuple[int, int] + T = tuple[str, str] + U = int | str + V = int | str + W = Callable[[str], bool] + Z = Callable[[str], bool] + QQ = Iterable[str] + RR = Iterable[str] + """, + error=None + ) @collect_cases def test_enum(self) -> Iterator[Case]: From 6f71ba41b0ba45c30cd88574fb6381c59dbca790 Mon Sep 17 00:00:00 2001 From: Alex Waygood Date: Thu, 14 Jul 2022 10:30:21 +0100 Subject: [PATCH 02/18] Apply suggestions from code review --- mypy/stubtest.py | 7 +++++-- 1 file changed, 5 insertions(+), 2 deletions(-) diff --git a/mypy/stubtest.py b/mypy/stubtest.py index 478a3ae2259f..025133bc3ac2 100644 --- a/mypy/stubtest.py +++ b/mypy/stubtest.py @@ -1020,7 +1020,10 @@ def verify_typealias( if isinstance(stub_target, mypy.types.Instance): runtime_type = get_origin(runtime) or runtime if not isinstance(runtime_type, type): - yield Error(object_path, "is inconsistent: runtime is not a type", stub, runtime) + yield Error( + object_path, "is inconsistent: runtime is not a type", stub, runtime, + stub_desc=str(stub_target) + ) return # Don't test the fullname, # stubs can sometimes be in different modules to the runtime for various reasons @@ -1035,7 +1038,7 @@ def verify_typealias( f"is inconsistent: runtime is an alias for {runtime_name} " f"but stub is an alias for {stub_name}" ) - yield Error(object_path, msg, stub, runtime) + yield Error(object_path, msg, stub, runtime, stub_desc=str(stub_target)) return if isinstance(stub_target, mypy.types.UnionType): if sys.version_info >= (3, 10) and isinstance(runtime, types.UnionType): From 5010db70fa3e4134b877ce1ad1229ad0299b6b91 Mon Sep 17 00:00:00 2001 From: Alex Waygood Date: Thu, 14 Jul 2022 15:07:05 +0100 Subject: [PATCH 03/18] More consistent error messages --- mypy/stubtest.py | 13 ++++++++----- 1 file changed, 8 insertions(+), 5 deletions(-) diff --git a/mypy/stubtest.py b/mypy/stubtest.py index 025133bc3ac2..0debd4c44c0d 100644 --- a/mypy/stubtest.py +++ b/mypy/stubtest.py @@ -1022,7 +1022,7 @@ def verify_typealias( if not isinstance(runtime_type, type): yield Error( object_path, "is inconsistent: runtime is not a type", stub, runtime, - stub_desc=str(stub_target) + stub_desc=f"Type alias for: {stub_target}" ) return # Don't test the fullname, @@ -1038,7 +1038,9 @@ def verify_typealias( f"is inconsistent: runtime is an alias for {runtime_name} " f"but stub is an alias for {stub_name}" ) - yield Error(object_path, msg, stub, runtime, stub_desc=str(stub_target)) + yield Error( + object_path, msg, stub, runtime, stub_desc=f"Type alias for: {stub_target}" + ) return if isinstance(stub_target, mypy.types.UnionType): if sys.version_info >= (3, 10) and isinstance(runtime, types.UnionType): @@ -1051,7 +1053,7 @@ def verify_typealias( if tuple not in getattr(runtime, "__mro__", getattr(get_origin(runtime), "__mro__", ())): yield Error( object_path, "is not a subclass of tuple", stub, runtime, - stub_desc=str(stub_target) + stub_desc=f"Type alias for: {stub_target}" ) # could check Tuple contents here... return @@ -1063,10 +1065,11 @@ def verify_typealias( return yield Error( object_path, "is not a type alias for Callable", stub, runtime, - stub_desc=str(stub_target) + stub_desc=f"Type alias for: {stub_target}" ) yield Error( - object_path, "is not a recognised type alias", stub, runtime, stub_desc=str(stub_target) + object_path, "is not a recognised type alias", stub, runtime, + stub_desc=f"Type alias for: {stub_target}" ) From df7e9ad5a9802ee047426561c6e5ef2be5957d51 Mon Sep 17 00:00:00 2001 From: AlexWaygood Date: Fri, 15 Jul 2022 16:15:33 +0100 Subject: [PATCH 04/18] Tidy up --- mypy/stubtest.py | 23 +++++++++++------------ 1 file changed, 11 insertions(+), 12 deletions(-) diff --git a/mypy/stubtest.py b/mypy/stubtest.py index 0debd4c44c0d..12c6a4dabf40 100644 --- a/mypy/stubtest.py +++ b/mypy/stubtest.py @@ -1017,15 +1017,15 @@ def verify_typealias( stub_desc=f"Type alias for: {stub_target}" ) return + runtime_type = get_origin(runtime) or runtime if isinstance(stub_target, mypy.types.Instance): - runtime_type = get_origin(runtime) or runtime if not isinstance(runtime_type, type): yield Error( object_path, "is inconsistent: runtime is not a type", stub, runtime, stub_desc=f"Type alias for: {stub_target}" ) return - # Don't test the fullname, + # Don't test the fullname; # stubs can sometimes be in different modules to the runtime for various reasons # (e.g. we want compatibility between collections.abc and typing, etc.) try: @@ -1043,14 +1043,14 @@ def verify_typealias( ) return if isinstance(stub_target, mypy.types.UnionType): + # could check Union contents here... if sys.version_info >= (3, 10) and isinstance(runtime, types.UnionType): return - if not getattr(runtime, "__origin__", None) is Union: + if runtime_type is not Union: yield Error(object_path, "is not a Union", stub, runtime, stub_desc=str(stub_target)) - # could check Union contents here... return if isinstance(stub_target, mypy.types.TupleType): - if tuple not in getattr(runtime, "__mro__", getattr(get_origin(runtime), "__mro__", ())): + if tuple not in getattr(runtime_type, "__mro__", ()): yield Error( object_path, "is not a subclass of tuple", stub, runtime, stub_desc=f"Type alias for: {stub_target}" @@ -1060,13 +1060,12 @@ def verify_typealias( if isinstance(stub_target, mypy.types.AnyType): return if isinstance(stub_target, mypy.types.CallableType): - callables = {typing.Callable, collections.abc.Callable} - if runtime in callables or get_origin(runtime) in callables: - return - yield Error( - object_path, "is not a type alias for Callable", stub, runtime, - stub_desc=f"Type alias for: {stub_target}" - ) + if runtime_type is not collections.abc.Callable: + yield Error( + object_path, "is not a type alias for Callable", stub, runtime, + stub_desc=f"Type alias for: {stub_target}" + ) + return yield Error( object_path, "is not a recognised type alias", stub, runtime, stub_desc=f"Type alias for: {stub_target}" From ad66ae17a88d64bb1b4d7168e66cef5022b47f53 Mon Sep 17 00:00:00 2001 From: hauntsaninja Date: Sat, 16 Jul 2022 15:58:17 -0700 Subject: [PATCH 05/18] reorder union check correctly for future contents checking --- mypy/stubtest.py | 7 +++++-- 1 file changed, 5 insertions(+), 2 deletions(-) diff --git a/mypy/stubtest.py b/mypy/stubtest.py index 12c6a4dabf40..56b9aae4c3e7 100644 --- a/mypy/stubtest.py +++ b/mypy/stubtest.py @@ -1043,11 +1043,14 @@ def verify_typealias( ) return if isinstance(stub_target, mypy.types.UnionType): - # could check Union contents here... if sys.version_info >= (3, 10) and isinstance(runtime, types.UnionType): return - if runtime_type is not Union: + # complain if runtime is not a Union or UnionType + if runtime_type is not Union and ( + not (sys.version_info >= (3, 10) and isinstance(runtime, types.UnionType)) + ): yield Error(object_path, "is not a Union", stub, runtime, stub_desc=str(stub_target)) + # could check Union contents here... return if isinstance(stub_target, mypy.types.TupleType): if tuple not in getattr(runtime_type, "__mro__", ()): From cb30b20fe140b1ed29e9af5cada08728e0bc78a2 Mon Sep 17 00:00:00 2001 From: hauntsaninja Date: Sat, 16 Jul 2022 16:01:05 -0700 Subject: [PATCH 06/18] rename runtime_type to runtime_origin --- mypy/stubtest.py | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/mypy/stubtest.py b/mypy/stubtest.py index 56b9aae4c3e7..fc9d43239281 100644 --- a/mypy/stubtest.py +++ b/mypy/stubtest.py @@ -1017,9 +1017,9 @@ def verify_typealias( stub_desc=f"Type alias for: {stub_target}" ) return - runtime_type = get_origin(runtime) or runtime + runtime_origin = get_origin(runtime) or runtime if isinstance(stub_target, mypy.types.Instance): - if not isinstance(runtime_type, type): + if not isinstance(runtime_origin, type): yield Error( object_path, "is inconsistent: runtime is not a type", stub, runtime, stub_desc=f"Type alias for: {stub_target}" @@ -1029,7 +1029,7 @@ def verify_typealias( # stubs can sometimes be in different modules to the runtime for various reasons # (e.g. we want compatibility between collections.abc and typing, etc.) try: - runtime_name = runtime_type.__name__ + runtime_name = runtime_origin.__name__ except AttributeError: return stub_name = stub_target.type.name @@ -1046,14 +1046,14 @@ def verify_typealias( if sys.version_info >= (3, 10) and isinstance(runtime, types.UnionType): return # complain if runtime is not a Union or UnionType - if runtime_type is not Union and ( + if runtime_origin is not Union and ( not (sys.version_info >= (3, 10) and isinstance(runtime, types.UnionType)) ): yield Error(object_path, "is not a Union", stub, runtime, stub_desc=str(stub_target)) # could check Union contents here... return if isinstance(stub_target, mypy.types.TupleType): - if tuple not in getattr(runtime_type, "__mro__", ()): + if tuple not in getattr(runtime_origin, "__mro__", ()): yield Error( object_path, "is not a subclass of tuple", stub, runtime, stub_desc=f"Type alias for: {stub_target}" @@ -1063,7 +1063,7 @@ def verify_typealias( if isinstance(stub_target, mypy.types.AnyType): return if isinstance(stub_target, mypy.types.CallableType): - if runtime_type is not collections.abc.Callable: + if runtime_origin is not collections.abc.Callable: yield Error( object_path, "is not a type alias for Callable", stub, runtime, stub_desc=f"Type alias for: {stub_target}" From 7e5adb49b72db289c840b6c28947dd89a223595d Mon Sep 17 00:00:00 2001 From: hauntsaninja Date: Sat, 16 Jul 2022 16:05:18 -0700 Subject: [PATCH 07/18] error messages more consistent with rest of stubtest --- mypy/stubtest.py | 30 ++++++++++++------------------ 1 file changed, 12 insertions(+), 18 deletions(-) diff --git a/mypy/stubtest.py b/mypy/stubtest.py index fc9d43239281..00dbee35d5c4 100644 --- a/mypy/stubtest.py +++ b/mypy/stubtest.py @@ -1011,18 +1011,19 @@ def verify_typealias( stub: nodes.TypeAlias, runtime: MaybeMissing[Any], object_path: List[str] ) -> Iterator[Error]: stub_target = mypy.types.get_proper_type(stub.target) + stub_desc = f"Type alias for {stub_target}" if isinstance(runtime, Missing): - yield Error( - object_path, "is not present at runtime", stub, runtime, - stub_desc=f"Type alias for: {stub_target}" - ) + yield Error(object_path, "is not present at runtime", stub, runtime, stub_desc=stub_desc) return runtime_origin = get_origin(runtime) or runtime if isinstance(stub_target, mypy.types.Instance): if not isinstance(runtime_origin, type): yield Error( - object_path, "is inconsistent: runtime is not a type", stub, runtime, - stub_desc=f"Type alias for: {stub_target}" + object_path, + "is inconsistent, runtime is not a type", + stub, + runtime, + stub_desc=stub_desc, ) return # Don't test the fullname; @@ -1035,12 +1036,10 @@ def verify_typealias( stub_name = stub_target.type.name if runtime_name != stub_name: msg = ( - f"is inconsistent: runtime is an alias for {runtime_name} " + f"is inconsistent, runtime is an alias for {runtime_name} " f"but stub is an alias for {stub_name}" ) - yield Error( - object_path, msg, stub, runtime, stub_desc=f"Type alias for: {stub_target}" - ) + yield Error(object_path, msg, stub, runtime, stub_desc=stub_desc) return if isinstance(stub_target, mypy.types.UnionType): if sys.version_info >= (3, 10) and isinstance(runtime, types.UnionType): @@ -1055,8 +1054,7 @@ def verify_typealias( if isinstance(stub_target, mypy.types.TupleType): if tuple not in getattr(runtime_origin, "__mro__", ()): yield Error( - object_path, "is not a subclass of tuple", stub, runtime, - stub_desc=f"Type alias for: {stub_target}" + object_path, "is not a subclass of tuple", stub, runtime, stub_desc=stub_desc ) # could check Tuple contents here... return @@ -1065,14 +1063,10 @@ def verify_typealias( if isinstance(stub_target, mypy.types.CallableType): if runtime_origin is not collections.abc.Callable: yield Error( - object_path, "is not a type alias for Callable", stub, runtime, - stub_desc=f"Type alias for: {stub_target}" + object_path, "is not a type alias for Callable", stub, runtime, stub_desc=stub_desc ) return - yield Error( - object_path, "is not a recognised type alias", stub, runtime, - stub_desc=f"Type alias for: {stub_target}" - ) + yield Error(object_path, "is not a recognised type alias", stub, runtime, stub_desc=stub_desc) # ==================== From 583687a0359985327febf6d20119436ad443a2eb Mon Sep 17 00:00:00 2001 From: hauntsaninja Date: Sat, 16 Jul 2022 16:16:20 -0700 Subject: [PATCH 08/18] minor rearrangement --- mypy/stubtest.py | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/mypy/stubtest.py b/mypy/stubtest.py index 00dbee35d5c4..4baec70861da 100644 --- a/mypy/stubtest.py +++ b/mypy/stubtest.py @@ -1058,13 +1058,14 @@ def verify_typealias( ) # could check Tuple contents here... return - if isinstance(stub_target, mypy.types.AnyType): - return if isinstance(stub_target, mypy.types.CallableType): if runtime_origin is not collections.abc.Callable: yield Error( object_path, "is not a type alias for Callable", stub, runtime, stub_desc=stub_desc ) + # could check Callable contents here... + return + if isinstance(stub_target, mypy.types.AnyType): return yield Error(object_path, "is not a recognised type alias", stub, runtime, stub_desc=stub_desc) From 90ec88e7aefe91bb82376cc356c03764aad27857 Mon Sep 17 00:00:00 2001 From: AlexWaygood Date: Mon, 18 Jul 2022 14:18:31 +0100 Subject: [PATCH 09/18] Address review --- mypy/stubtest.py | 33 ++++++++++------ mypy/test/teststubtest.py | 81 ++++++++++++++++++++++++++++++++++++--- 2 files changed, 96 insertions(+), 18 deletions(-) diff --git a/mypy/stubtest.py b/mypy/stubtest.py index 4baec70861da..65328dbfcccd 100644 --- a/mypy/stubtest.py +++ b/mypy/stubtest.py @@ -1026,20 +1026,29 @@ def verify_typealias( stub_desc=stub_desc, ) return - # Don't test the fullname; - # stubs can sometimes be in different modules to the runtime for various reasons - # (e.g. we want compatibility between collections.abc and typing, etc.) + + # Do our best to figure out the fullname of the runtime object... + runtime_name: Union[str, Missing] try: - runtime_name = runtime_origin.__name__ + runtime_name = runtime_origin.__qualname__ except AttributeError: - return - stub_name = stub_target.type.name - if runtime_name != stub_name: - msg = ( - f"is inconsistent, runtime is an alias for {runtime_name} " - f"but stub is an alias for {stub_name}" - ) - yield Error(object_path, msg, stub, runtime, stub_desc=stub_desc) + runtime_name = getattr(runtime_origin, "__name__", MISSING) + if not isinstance(runtime_name, Missing): + runtime_module: Union[str, Missing] = getattr(runtime_origin, "__module__", MISSING) + if not isinstance(runtime_module, Missing): + if runtime_module == "collections.abc" or ( + runtime_module == "re" and runtime_name in {"Match", "Pattern"} + ): + runtime_module = "typing" + runtime_fullname = f"{runtime_module}.{runtime_name}" + if re.match(fr"_?{stub_target.type.fullname}", runtime_fullname): + # Okay, we're probably fine. + return + + # Okay, either we couldn't construct a fullname + # or the fullname of the stub didn't match the fullname of the runtime. + # Fallback to a full structural check of the runtime vis-a-vis the stub. + yield from verify(stub_target.type, runtime_origin, object_path) return if isinstance(stub_target, mypy.types.UnionType): if sys.version_info >= (3, 10) and isinstance(runtime, types.UnionType): diff --git a/mypy/test/teststubtest.py b/mypy/test/teststubtest.py index 633ae7c15cec..1ce286ecddc1 100644 --- a/mypy/test/teststubtest.py +++ b/mypy/test/teststubtest.py @@ -62,6 +62,7 @@ def __init__(self, name: str) -> None: ... class Coroutine(Generic[_T_co, _S, _R]): ... class Iterable(Generic[_T_co]): ... class Mapping(Generic[_K, _V]): ... +class Match(Generic[_T]): ... class Sequence(Iterable[_T_co]): ... class Tuple(Sequence[_T_co]): ... def overload(func: _T) -> _T: ... @@ -707,7 +708,7 @@ class X: def f(self) -> None: ... class Y: ... """, - error="Y", + error="Y.f", ) yield Case( stub=""" @@ -766,7 +767,6 @@ class Y: ... from typing import Dict K = dict[str, str] L = Dict[int, int] - M = Dict[str, int] KK = Iterable[str] LL = typing.Iterable[str] """, @@ -774,13 +774,29 @@ class Y: ... from typing import Iterable, Dict, List K = Dict[str, str] L = Dict[int, int] - M = List[int] KK = Iterable[str] LL = Iterable[str] """, - error="M" + error=None ) - yield Case(stub="MM = list[str]", runtime="['foo', 'bar']", error="MM") + yield Case( + stub=""" + from typing import Generic, TypeVar + _T = TypeVar("_T") + class _Spam(Generic[_T]): + def foo(self): ... + IntFood = _Spam[int] + """, + runtime=""" + from typing import Generic, TypeVar + _T = TypeVar("_T") + class _Bacon(Generic[_T]): + def foo(self, arg): pass + IntFood = _Bacon[int] + """, + error="IntFood.foo" + ) + yield Case(stub="StrList = list[str]", runtime="StrList = ['foo', 'bar']", error="StrList") yield Case( stub=""" import collections.abc @@ -797,11 +813,59 @@ class Y: ... """, error="P" ) + yield Case( + stub=""" + class Foo: + class Bar: ... + BarAlias = Foo.Bar + """, + runtime=""" + class Foo: + class Bar: pass + BarAlias = Foo.Bar + """, + error=None + ) + yield Case( + stub=""" + from io import StringIO + StringIOAlias = StringIO + """, + runtime=""" + from _io import StringIO + StringIOAlias = StringIO + """, + error=None + ) + yield Case( + stub=""" + from typing import Match + M = Match[str] + """, + runtime=""" + from typing import Match + M = Match[str] + """, + error=None + ) + yield Case( + stub=""" + class Baz: ... + BazAlias = Baz + """, + runtime=""" + class Baz: ... + BazAlias = Baz + Baz.__name__ = Baz.__qualname__ = Baz.__module__ = "New" + """, + error=None + ) if sys.version_info >= (3, 10): yield Case( stub=""" import collections.abc - from typing import Callable, Dict, Iterable, Tuple, Union + import re + from typing import Callable, Dict, Match, Iterable, Tuple, Union Q = Dict[str, str] R = dict[int, int] S = Tuple[int, int] @@ -812,9 +876,12 @@ class Y: ... Z = collections.abc.Callable[[str], bool] QQ = Iterable[str] RR = collections.abc.Iterable[str] + MM = Match[str] + MMM = re.Match[str] """, runtime=""" from collections.abc import Callable, Iterable + from re import Match Q = dict[str, str] R = dict[int, int] S = tuple[int, int] @@ -825,6 +892,8 @@ class Y: ... Z = Callable[[str], bool] QQ = Iterable[str] RR = Iterable[str] + MM = Match[str] + MMM = Match[str] """, error=None ) From d613865434d6b5f85ee8b748815f1665fa54bc51 Mon Sep 17 00:00:00 2001 From: Alex Waygood Date: Mon, 18 Jul 2022 14:25:20 +0100 Subject: [PATCH 10/18] Unused import in test case --- mypy/test/teststubtest.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/mypy/test/teststubtest.py b/mypy/test/teststubtest.py index 1ce286ecddc1..8e1b1eaac3ae 100644 --- a/mypy/test/teststubtest.py +++ b/mypy/test/teststubtest.py @@ -771,7 +771,7 @@ class Y: ... LL = typing.Iterable[str] """, runtime=""" - from typing import Iterable, Dict, List + from typing import Iterable, Dict K = Dict[str, str] L = Dict[int, int] KK = Iterable[str] From 38aa229c9db437c550f787cc31bed44646ee8e7b Mon Sep 17 00:00:00 2001 From: AlexWaygood Date: Mon, 18 Jul 2022 14:28:40 +0100 Subject: [PATCH 11/18] `Union[str, Missing]` -> `MaybeMissing[str]` --- mypy/stubtest.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/mypy/stubtest.py b/mypy/stubtest.py index 65328dbfcccd..e7163b4d02d7 100644 --- a/mypy/stubtest.py +++ b/mypy/stubtest.py @@ -1028,13 +1028,13 @@ def verify_typealias( return # Do our best to figure out the fullname of the runtime object... - runtime_name: Union[str, Missing] + runtime_name: MaybeMissing[str] try: runtime_name = runtime_origin.__qualname__ except AttributeError: runtime_name = getattr(runtime_origin, "__name__", MISSING) if not isinstance(runtime_name, Missing): - runtime_module: Union[str, Missing] = getattr(runtime_origin, "__module__", MISSING) + runtime_module: MaybeMissing[str] = getattr(runtime_origin, "__module__", MISSING) if not isinstance(runtime_module, Missing): if runtime_module == "collections.abc" or ( runtime_module == "re" and runtime_name in {"Match", "Pattern"} From 6d14acab7d8edf3c1baaa64c8e622e831b82cd40 Mon Sep 17 00:00:00 2001 From: AlexWaygood Date: Mon, 18 Jul 2022 14:59:35 +0100 Subject: [PATCH 12/18] Improve --- mypy/stubtest.py | 4 ++-- mypy/test/teststubtest.py | 23 ++++++++++++++++++++--- 2 files changed, 22 insertions(+), 5 deletions(-) diff --git a/mypy/stubtest.py b/mypy/stubtest.py index e7163b4d02d7..e6b942840162 100644 --- a/mypy/stubtest.py +++ b/mypy/stubtest.py @@ -1033,9 +1033,9 @@ def verify_typealias( runtime_name = runtime_origin.__qualname__ except AttributeError: runtime_name = getattr(runtime_origin, "__name__", MISSING) - if not isinstance(runtime_name, Missing): + if isinstance(runtime_name, str): runtime_module: MaybeMissing[str] = getattr(runtime_origin, "__module__", MISSING) - if not isinstance(runtime_module, Missing): + if isinstance(runtime_module, str): if runtime_module == "collections.abc" or ( runtime_module == "re" and runtime_name in {"Match", "Pattern"} ): diff --git a/mypy/test/teststubtest.py b/mypy/test/teststubtest.py index 8e1b1eaac3ae..d8ac9f2346c0 100644 --- a/mypy/test/teststubtest.py +++ b/mypy/test/teststubtest.py @@ -784,7 +784,7 @@ class Y: ... from typing import Generic, TypeVar _T = TypeVar("_T") class _Spam(Generic[_T]): - def foo(self): ... + def foo(self) -> None: ... IntFood = _Spam[int] """, runtime=""" @@ -850,16 +850,33 @@ class Bar: pass ) yield Case( stub=""" - class Baz: ... + class Baz: + def fizz(self) -> None: ... BazAlias = Baz """, runtime=""" - class Baz: ... + class Baz: + def fizz(self): pass BazAlias = Baz Baz.__name__ = Baz.__qualname__ = Baz.__module__ = "New" """, error=None ) + yield Case( + stub=""" + class FooBar: + __module__: None # type: ignore + def fizz(self) -> None: ... + FooBarAlias = FooBar + """, + runtime=""" + class FooBar: + def fizz(self): pass + FooBarAlias = FooBar + FooBar.__module__ = None + """, + error=None + ) if sys.version_info >= (3, 10): yield Case( stub=""" From c763b989b82d35ee05108e2f0e1de9ddce96840b Mon Sep 17 00:00:00 2001 From: AlexWaygood Date: Mon, 18 Jul 2022 15:18:56 +0100 Subject: [PATCH 13/18] Fix mistake made in ad66ae17a88d64bb1b4d7168e66cef5022b47f53 --- mypy/stubtest.py | 2 -- 1 file changed, 2 deletions(-) diff --git a/mypy/stubtest.py b/mypy/stubtest.py index e6b942840162..4069dbd97fcd 100644 --- a/mypy/stubtest.py +++ b/mypy/stubtest.py @@ -1051,8 +1051,6 @@ def verify_typealias( yield from verify(stub_target.type, runtime_origin, object_path) return if isinstance(stub_target, mypy.types.UnionType): - if sys.version_info >= (3, 10) and isinstance(runtime, types.UnionType): - return # complain if runtime is not a Union or UnionType if runtime_origin is not Union and ( not (sys.version_info >= (3, 10) and isinstance(runtime, types.UnionType)) From 04a054b1adf5834d102b6b76e77555b663df3a19 Mon Sep 17 00:00:00 2001 From: Alex Waygood Date: Mon, 18 Jul 2022 15:44:31 +0100 Subject: [PATCH 14/18] Escape --- mypy/stubtest.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/mypy/stubtest.py b/mypy/stubtest.py index 4069dbd97fcd..2b751fc901ad 100644 --- a/mypy/stubtest.py +++ b/mypy/stubtest.py @@ -1041,7 +1041,7 @@ def verify_typealias( ): runtime_module = "typing" runtime_fullname = f"{runtime_module}.{runtime_name}" - if re.match(fr"_?{stub_target.type.fullname}", runtime_fullname): + if re.match(fr"_?{re.escape(stub_target.type.fullname)}", runtime_fullname): # Okay, we're probably fine. return From 0667cf74bbc5b2eeff589876f8b3bb60a4f6e1cd Mon Sep 17 00:00:00 2001 From: AlexWaygood Date: Mon, 18 Jul 2022 15:52:05 +0100 Subject: [PATCH 15/18] Tweak --- mypy/stubtest.py | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/mypy/stubtest.py b/mypy/stubtest.py index 2b751fc901ad..1037455ab1bb 100644 --- a/mypy/stubtest.py +++ b/mypy/stubtest.py @@ -1027,6 +1027,7 @@ def verify_typealias( ) return + stub_origin = stub_target.type # Do our best to figure out the fullname of the runtime object... runtime_name: MaybeMissing[str] try: @@ -1041,14 +1042,14 @@ def verify_typealias( ): runtime_module = "typing" runtime_fullname = f"{runtime_module}.{runtime_name}" - if re.match(fr"_?{re.escape(stub_target.type.fullname)}", runtime_fullname): + if re.match(fr"_?{re.escape(stub_origin.fullname)}", runtime_fullname): # Okay, we're probably fine. return # Okay, either we couldn't construct a fullname # or the fullname of the stub didn't match the fullname of the runtime. # Fallback to a full structural check of the runtime vis-a-vis the stub. - yield from verify(stub_target.type, runtime_origin, object_path) + yield from verify(stub_origin, runtime_origin, object_path) return if isinstance(stub_target, mypy.types.UnionType): # complain if runtime is not a Union or UnionType From 36ce3f8b5482223167d3b812910409ad06bf0cf3 Mon Sep 17 00:00:00 2001 From: AlexWaygood Date: Mon, 18 Jul 2022 15:58:37 +0100 Subject: [PATCH 16/18] More type safety --- mypy/stubtest.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/mypy/stubtest.py b/mypy/stubtest.py index 1037455ab1bb..326af60c56ab 100644 --- a/mypy/stubtest.py +++ b/mypy/stubtest.py @@ -1029,13 +1029,13 @@ def verify_typealias( stub_origin = stub_target.type # Do our best to figure out the fullname of the runtime object... - runtime_name: MaybeMissing[str] + runtime_name: object try: runtime_name = runtime_origin.__qualname__ except AttributeError: runtime_name = getattr(runtime_origin, "__name__", MISSING) if isinstance(runtime_name, str): - runtime_module: MaybeMissing[str] = getattr(runtime_origin, "__module__", MISSING) + runtime_module: object = getattr(runtime_origin, "__module__", MISSING) if isinstance(runtime_module, str): if runtime_module == "collections.abc" or ( runtime_module == "re" and runtime_name in {"Match", "Pattern"} From f5fe566426a0b691eb5532780efbaa9ab8e4f21c Mon Sep 17 00:00:00 2001 From: Alex Waygood Date: Tue, 19 Jul 2022 08:43:44 +0100 Subject: [PATCH 17/18] Update mypy/stubtest.py --- mypy/stubtest.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/mypy/stubtest.py b/mypy/stubtest.py index 326af60c56ab..88caa33b44ac 100644 --- a/mypy/stubtest.py +++ b/mypy/stubtest.py @@ -1042,7 +1042,7 @@ def verify_typealias( ): runtime_module = "typing" runtime_fullname = f"{runtime_module}.{runtime_name}" - if re.match(fr"_?{re.escape(stub_origin.fullname)}", runtime_fullname): + if re.fullmatch(fr"_?{re.escape(stub_origin.fullname)}", runtime_fullname): # Okay, we're probably fine. return From a77467622829364882419b2f8842dc9a98974218 Mon Sep 17 00:00:00 2001 From: AlexWaygood Date: Wed, 27 Jul 2022 15:38:47 +0100 Subject: [PATCH 18/18] Blacken --- mypy/stubtest.py | 2 +- mypy/test/teststubtest.py | 22 +++++++++++----------- 2 files changed, 12 insertions(+), 12 deletions(-) diff --git a/mypy/stubtest.py b/mypy/stubtest.py index ab1200e1470e..96f6aa5af96a 100644 --- a/mypy/stubtest.py +++ b/mypy/stubtest.py @@ -1063,7 +1063,7 @@ def verify_typealias( ): runtime_module = "typing" runtime_fullname = f"{runtime_module}.{runtime_name}" - if re.fullmatch(fr"_?{re.escape(stub_origin.fullname)}", runtime_fullname): + if re.fullmatch(rf"_?{re.escape(stub_origin.fullname)}", runtime_fullname): # Okay, we're probably fine. return diff --git a/mypy/test/teststubtest.py b/mypy/test/teststubtest.py index 370b3bb11f96..61c46ea01b91 100644 --- a/mypy/test/teststubtest.py +++ b/mypy/test/teststubtest.py @@ -718,7 +718,7 @@ class Y: ... E = Tuple[int, int, int] F = List[str] """, - error="F" + error="F", ) yield Case( stub=""" @@ -733,7 +733,7 @@ class Y: ... H = Union[str, bool] I = str """, - error="I" + error="I", ) yield Case( stub=""" @@ -752,7 +752,7 @@ class Y: ... KK = Iterable[str] LL = Iterable[str] """, - error=None + error=None, ) yield Case( stub=""" @@ -769,7 +769,7 @@ class _Bacon(Generic[_T]): def foo(self, arg): pass IntFood = _Bacon[int] """, - error="IntFood.foo" + error="IntFood.foo", ) yield Case(stub="StrList = list[str]", runtime="StrList = ['foo', 'bar']", error="StrList") yield Case( @@ -786,7 +786,7 @@ def foo(self, arg): pass O = Callable[[int], str] P = int """, - error="P" + error="P", ) yield Case( stub=""" @@ -799,7 +799,7 @@ class Foo: class Bar: pass BarAlias = Foo.Bar """, - error=None + error=None, ) yield Case( stub=""" @@ -810,7 +810,7 @@ class Bar: pass from _io import StringIO StringIOAlias = StringIO """, - error=None + error=None, ) yield Case( stub=""" @@ -821,7 +821,7 @@ class Bar: pass from typing import Match M = Match[str] """, - error=None + error=None, ) yield Case( stub=""" @@ -835,7 +835,7 @@ def fizz(self): pass BazAlias = Baz Baz.__name__ = Baz.__qualname__ = Baz.__module__ = "New" """, - error=None + error=None, ) yield Case( stub=""" @@ -850,7 +850,7 @@ def fizz(self): pass FooBarAlias = FooBar FooBar.__module__ = None """, - error=None + error=None, ) if sys.version_info >= (3, 10): yield Case( @@ -887,7 +887,7 @@ def fizz(self): pass MM = Match[str] MMM = Match[str] """, - error=None + error=None, ) @collect_cases