Skip to content

Commit ff6fbe8

Browse files
stubtest: reduce false positives on runtime type aliases (#13116)
Co-authored-by: hauntsaninja <[email protected]>
1 parent 1ff79b6 commit ff6fbe8

File tree

2 files changed

+238
-19
lines changed

2 files changed

+238
-19
lines changed

mypy/stubtest.py

Lines changed: 52 additions & 19 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@
55
"""
66

77
import argparse
8+
import collections.abc
89
import copy
910
import enum
1011
import importlib
@@ -23,7 +24,7 @@
2324
from typing import Any, Dict, Generic, Iterator, List, Optional, Tuple, TypeVar, Union, cast
2425

2526
import typing_extensions
26-
from typing_extensions import Type
27+
from typing_extensions import Type, get_origin
2728

2829
import mypy.build
2930
import mypy.modulefinder
@@ -1031,39 +1032,71 @@ def verify_typealias(
10311032
stub: nodes.TypeAlias, runtime: MaybeMissing[Any], object_path: List[str]
10321033
) -> Iterator[Error]:
10331034
stub_target = mypy.types.get_proper_type(stub.target)
1035+
stub_desc = f"Type alias for {stub_target}"
10341036
if isinstance(runtime, Missing):
1035-
yield Error(
1036-
object_path,
1037-
"is not present at runtime",
1038-
stub,
1039-
runtime,
1040-
stub_desc=f"Type alias for: {stub_target}",
1041-
)
1037+
yield Error(object_path, "is not present at runtime", stub, runtime, stub_desc=stub_desc)
10421038
return
1039+
runtime_origin = get_origin(runtime) or runtime
10431040
if isinstance(stub_target, mypy.types.Instance):
1044-
yield from verify(stub_target.type, runtime, object_path)
1041+
if not isinstance(runtime_origin, type):
1042+
yield Error(
1043+
object_path,
1044+
"is inconsistent, runtime is not a type",
1045+
stub,
1046+
runtime,
1047+
stub_desc=stub_desc,
1048+
)
1049+
return
1050+
1051+
stub_origin = stub_target.type
1052+
# Do our best to figure out the fullname of the runtime object...
1053+
runtime_name: object
1054+
try:
1055+
runtime_name = runtime_origin.__qualname__
1056+
except AttributeError:
1057+
runtime_name = getattr(runtime_origin, "__name__", MISSING)
1058+
if isinstance(runtime_name, str):
1059+
runtime_module: object = getattr(runtime_origin, "__module__", MISSING)
1060+
if isinstance(runtime_module, str):
1061+
if runtime_module == "collections.abc" or (
1062+
runtime_module == "re" and runtime_name in {"Match", "Pattern"}
1063+
):
1064+
runtime_module = "typing"
1065+
runtime_fullname = f"{runtime_module}.{runtime_name}"
1066+
if re.fullmatch(rf"_?{re.escape(stub_origin.fullname)}", runtime_fullname):
1067+
# Okay, we're probably fine.
1068+
return
1069+
1070+
# Okay, either we couldn't construct a fullname
1071+
# or the fullname of the stub didn't match the fullname of the runtime.
1072+
# Fallback to a full structural check of the runtime vis-a-vis the stub.
1073+
yield from verify(stub_origin, runtime_origin, object_path)
10451074
return
10461075
if isinstance(stub_target, mypy.types.UnionType):
1047-
if not getattr(runtime, "__origin__", None) is Union:
1076+
# complain if runtime is not a Union or UnionType
1077+
if runtime_origin is not Union and (
1078+
not (sys.version_info >= (3, 10) and isinstance(runtime, types.UnionType))
1079+
):
10481080
yield Error(object_path, "is not a Union", stub, runtime, stub_desc=str(stub_target))
10491081
# could check Union contents here...
10501082
return
10511083
if isinstance(stub_target, mypy.types.TupleType):
1052-
if tuple not in getattr(runtime, "__mro__", ()):
1084+
if tuple not in getattr(runtime_origin, "__mro__", ()):
10531085
yield Error(
1054-
object_path,
1055-
"is not a subclass of tuple",
1056-
stub,
1057-
runtime,
1058-
stub_desc=str(stub_target),
1086+
object_path, "is not a subclass of tuple", stub, runtime, stub_desc=stub_desc
10591087
)
10601088
# could check Tuple contents here...
10611089
return
1090+
if isinstance(stub_target, mypy.types.CallableType):
1091+
if runtime_origin is not collections.abc.Callable:
1092+
yield Error(
1093+
object_path, "is not a type alias for Callable", stub, runtime, stub_desc=stub_desc
1094+
)
1095+
# could check Callable contents here...
1096+
return
10621097
if isinstance(stub_target, mypy.types.AnyType):
10631098
return
1064-
yield Error(
1065-
object_path, "is not a recognised type alias", stub, runtime, stub_desc=str(stub_target)
1066-
)
1099+
yield Error(object_path, "is not a recognised type alias", stub, runtime, stub_desc=stub_desc)
10671100

10681101

10691102
# ====================

mypy/test/teststubtest.py

Lines changed: 186 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -44,6 +44,7 @@ def __getitem__(self, typeargs: Any) -> object: ...
4444
Callable: _SpecialForm = ...
4545
Generic: _SpecialForm = ...
4646
Protocol: _SpecialForm = ...
47+
Union: _SpecialForm = ...
4748
4849
class TypeVar:
4950
def __init__(self, name, covariant: bool = ..., contravariant: bool = ...) -> None: ...
@@ -61,6 +62,7 @@ def __init__(self, name: str) -> None: ...
6162
class Coroutine(Generic[_T_co, _S, _R]): ...
6263
class Iterable(Generic[_T_co]): ...
6364
class Mapping(Generic[_K, _V]): ...
65+
class Match(Generic[_T]): ...
6466
class Sequence(Iterable[_T_co]): ...
6567
class Tuple(Sequence[_T_co]): ...
6668
def overload(func: _T) -> _T: ...
@@ -703,6 +705,190 @@ class Y: ...
703705
yield Case(stub="B = str", runtime="", error="B")
704706
# ... but only if the alias isn't private
705707
yield Case(stub="_C = int", runtime="", error=None)
708+
yield Case(
709+
stub="""
710+
from typing import Tuple
711+
D = tuple[str, str]
712+
E = Tuple[int, int, int]
713+
F = Tuple[str, int]
714+
""",
715+
runtime="""
716+
from typing import List, Tuple
717+
D = Tuple[str, str]
718+
E = Tuple[int, int, int]
719+
F = List[str]
720+
""",
721+
error="F",
722+
)
723+
yield Case(
724+
stub="""
725+
from typing import Union
726+
G = str | int
727+
H = Union[str, bool]
728+
I = str | int
729+
""",
730+
runtime="""
731+
from typing import Union
732+
G = Union[str, int]
733+
H = Union[str, bool]
734+
I = str
735+
""",
736+
error="I",
737+
)
738+
yield Case(
739+
stub="""
740+
import typing
741+
from collections.abc import Iterable
742+
from typing import Dict
743+
K = dict[str, str]
744+
L = Dict[int, int]
745+
KK = Iterable[str]
746+
LL = typing.Iterable[str]
747+
""",
748+
runtime="""
749+
from typing import Iterable, Dict
750+
K = Dict[str, str]
751+
L = Dict[int, int]
752+
KK = Iterable[str]
753+
LL = Iterable[str]
754+
""",
755+
error=None,
756+
)
757+
yield Case(
758+
stub="""
759+
from typing import Generic, TypeVar
760+
_T = TypeVar("_T")
761+
class _Spam(Generic[_T]):
762+
def foo(self) -> None: ...
763+
IntFood = _Spam[int]
764+
""",
765+
runtime="""
766+
from typing import Generic, TypeVar
767+
_T = TypeVar("_T")
768+
class _Bacon(Generic[_T]):
769+
def foo(self, arg): pass
770+
IntFood = _Bacon[int]
771+
""",
772+
error="IntFood.foo",
773+
)
774+
yield Case(stub="StrList = list[str]", runtime="StrList = ['foo', 'bar']", error="StrList")
775+
yield Case(
776+
stub="""
777+
import collections.abc
778+
from typing import Callable
779+
N = Callable[[str], bool]
780+
O = collections.abc.Callable[[int], str]
781+
P = Callable[[str], bool]
782+
""",
783+
runtime="""
784+
from typing import Callable
785+
N = Callable[[str], bool]
786+
O = Callable[[int], str]
787+
P = int
788+
""",
789+
error="P",
790+
)
791+
yield Case(
792+
stub="""
793+
class Foo:
794+
class Bar: ...
795+
BarAlias = Foo.Bar
796+
""",
797+
runtime="""
798+
class Foo:
799+
class Bar: pass
800+
BarAlias = Foo.Bar
801+
""",
802+
error=None,
803+
)
804+
yield Case(
805+
stub="""
806+
from io import StringIO
807+
StringIOAlias = StringIO
808+
""",
809+
runtime="""
810+
from _io import StringIO
811+
StringIOAlias = StringIO
812+
""",
813+
error=None,
814+
)
815+
yield Case(
816+
stub="""
817+
from typing import Match
818+
M = Match[str]
819+
""",
820+
runtime="""
821+
from typing import Match
822+
M = Match[str]
823+
""",
824+
error=None,
825+
)
826+
yield Case(
827+
stub="""
828+
class Baz:
829+
def fizz(self) -> None: ...
830+
BazAlias = Baz
831+
""",
832+
runtime="""
833+
class Baz:
834+
def fizz(self): pass
835+
BazAlias = Baz
836+
Baz.__name__ = Baz.__qualname__ = Baz.__module__ = "New"
837+
""",
838+
error=None,
839+
)
840+
yield Case(
841+
stub="""
842+
class FooBar:
843+
__module__: None # type: ignore
844+
def fizz(self) -> None: ...
845+
FooBarAlias = FooBar
846+
""",
847+
runtime="""
848+
class FooBar:
849+
def fizz(self): pass
850+
FooBarAlias = FooBar
851+
FooBar.__module__ = None
852+
""",
853+
error=None,
854+
)
855+
if sys.version_info >= (3, 10):
856+
yield Case(
857+
stub="""
858+
import collections.abc
859+
import re
860+
from typing import Callable, Dict, Match, Iterable, Tuple, Union
861+
Q = Dict[str, str]
862+
R = dict[int, int]
863+
S = Tuple[int, int]
864+
T = tuple[str, str]
865+
U = int | str
866+
V = Union[int, str]
867+
W = Callable[[str], bool]
868+
Z = collections.abc.Callable[[str], bool]
869+
QQ = Iterable[str]
870+
RR = collections.abc.Iterable[str]
871+
MM = Match[str]
872+
MMM = re.Match[str]
873+
""",
874+
runtime="""
875+
from collections.abc import Callable, Iterable
876+
from re import Match
877+
Q = dict[str, str]
878+
R = dict[int, int]
879+
S = tuple[int, int]
880+
T = tuple[str, str]
881+
U = int | str
882+
V = int | str
883+
W = Callable[[str], bool]
884+
Z = Callable[[str], bool]
885+
QQ = Iterable[str]
886+
RR = Iterable[str]
887+
MM = Match[str]
888+
MMM = Match[str]
889+
""",
890+
error=None,
891+
)
706892

707893
@collect_cases
708894
def test_enum(self) -> Iterator[Case]:

0 commit comments

Comments
 (0)