From 70bd7759863b3c6a4c8bf0cbd9316fabe0a8b85c Mon Sep 17 00:00:00 2001 From: Ivan Levkivskyi Date: Wed, 6 Jan 2021 12:56:47 +0000 Subject: [PATCH 1/2] Do not expand references to global variables in bodies of functions with constrained type variables --- mypy/treetransform.py | 9 +++++++-- test-data/unit/check-generics.test | 28 ++++++++++++++++++++++++++++ 2 files changed, 35 insertions(+), 2 deletions(-) diff --git a/mypy/treetransform.py b/mypy/treetransform.py index 4191569995b0..8a025f0aba30 100644 --- a/mypy/treetransform.py +++ b/mypy/treetransform.py @@ -20,7 +20,7 @@ YieldFromExpr, NamedTupleExpr, TypedDictExpr, NonlocalDecl, SetComprehension, DictionaryComprehension, ComplexExpr, TypeAliasExpr, EllipsisExpr, YieldExpr, ExecStmt, Argument, BackquoteExpr, AwaitExpr, AssignmentExpr, - OverloadPart, EnumCallExpr, REVEAL_TYPE + OverloadPart, EnumCallExpr, REVEAL_TYPE, GDEF ) from mypy.types import Type, FunctionLike, ProperType from mypy.traverser import TraverserVisitor @@ -358,7 +358,12 @@ def copy_ref(self, new: RefExpr, original: RefExpr) -> None: new.fullname = original.fullname target = original.node if isinstance(target, Var): - target = self.visit_var(target) + # Do not transform references to global variables. + # TODO: is it really the best solution? Should we instead make + # a trivial copy, or do something completely different? See + # testGenericFunctionAliasExpand for an example where this is important. + if original.kind != GDEF: + target = self.visit_var(target) elif isinstance(target, Decorator): target = self.visit_var(target.var) elif isinstance(target, FuncDef): diff --git a/test-data/unit/check-generics.test b/test-data/unit/check-generics.test index c4807c8fe1e7..654f33c590c2 100644 --- a/test-data/unit/check-generics.test +++ b/test-data/unit/check-generics.test @@ -2402,3 +2402,31 @@ def func(tp: Type[C[S]]) -> S: else: return reg[tp.test].test[0] [builtins fixtures/dict.pyi] + +[case testGenericFunctionAliasExpand] +from typing import Optional, TypeVar + +T = TypeVar("T") +def gen(x: T) -> T: ... +gen_a = gen + +S = TypeVar("S", int, str) +class C: ... +def test() -> Optional[S]: + reveal_type(gen_a(C())) # N: Revealed type is '__main__.C*' + return None + +[case testGenericFunctionMemberExpand] +from typing import Optional, TypeVar, Callable + +T = TypeVar("T") + +class A: + def __init__(self) -> None: + self.gen: Callable[[T], T] + +S = TypeVar("S", int, str) +class C: ... +def test() -> Optional[S]: + reveal_type(A().gen(C())) # N: Revealed type is '__main__.C*' + return None From 44c3987c999189d708f2e44b5adba8253659bf52 Mon Sep 17 00:00:00 2001 From: Ivan Levkivskyi Date: Wed, 6 Jan 2021 15:52:39 +0000 Subject: [PATCH 2/2] Only use visi_mypy_file() for tests --- mypy/test/testtransform.py | 1 + mypy/treetransform.py | 10 +++++++--- 2 files changed, 8 insertions(+), 3 deletions(-) diff --git a/mypy/test/testtransform.py b/mypy/test/testtransform.py index 803f2dcd4035..d884fe9137ab 100644 --- a/mypy/test/testtransform.py +++ b/mypy/test/testtransform.py @@ -60,6 +60,7 @@ def test_transform(testcase: DataDrivenTestCase) -> None: and not os.path.splitext( os.path.basename(f.path))[0].endswith('_')): t = TypeAssertTransformVisitor() + t.test_only = True f = t.mypyfile(f) a += str(f).split('\n') except CompileError as e: diff --git a/mypy/treetransform.py b/mypy/treetransform.py index 8a025f0aba30..bd8a623455f7 100644 --- a/mypy/treetransform.py +++ b/mypy/treetransform.py @@ -37,6 +37,8 @@ class TransformVisitor(NodeVisitor[Node]): Notes: + * This can only be used to transform functions or classes, not top-level + statements, and/or modules as a whole. * Do not duplicate TypeInfo nodes. This would generally not be desirable. * Only update some name binding cross-references, but only those that refer to Var, Decorator or FuncDef nodes, not those targeting ClassDef or @@ -48,6 +50,9 @@ class TransformVisitor(NodeVisitor[Node]): """ def __init__(self) -> None: + # To simplify testing, set this flag to True if you want to transform + # all statements in a file (this is prohibited in normal mode). + self.test_only = False # There may be multiple references to a Var node. Keep track of # Var translations using a dictionary. self.var_map = {} # type: Dict[Var, Var] @@ -58,6 +63,7 @@ def __init__(self) -> None: self.func_placeholder_map = {} # type: Dict[FuncDef, FuncDef] def visit_mypy_file(self, node: MypyFile) -> MypyFile: + assert self.test_only, "This visitor should not be used for whole files." # NOTE: The 'names' and 'imports' instance variables will be empty! ignored_lines = {line: codes[:] for line, codes in node.ignored_lines.items()} @@ -358,9 +364,7 @@ def copy_ref(self, new: RefExpr, original: RefExpr) -> None: new.fullname = original.fullname target = original.node if isinstance(target, Var): - # Do not transform references to global variables. - # TODO: is it really the best solution? Should we instead make - # a trivial copy, or do something completely different? See + # Do not transform references to global variables. See # testGenericFunctionAliasExpand for an example where this is important. if original.kind != GDEF: target = self.visit_var(target)