From 2d317413109bd9f00ec6ae279ce9126f998bfbb8 Mon Sep 17 00:00:00 2001 From: Ivan Levkivskyi Date: Fri, 8 Nov 2019 19:55:14 +0000 Subject: [PATCH 01/12] Start working --- mypy/checkexpr.py | 15 +++++------ mypy/checkmember.py | 2 +- mypy/nodes.py | 9 ++++--- mypy/semanal.py | 27 ++++++++++--------- mypy/server/deps.py | 4 +-- mypy/treetransform.py | 2 +- mypy/type_visitor.py | 12 +++++---- mypy/typeanal.py | 36 +++++++++++--------------- mypy/types.py | 18 +++++++++++-- test-data/unit/check-incremental.test | 2 +- test-data/unit/check-namedtuple.test | 9 ++++--- test-data/unit/check-type-aliases.test | 7 +++-- 12 files changed, 78 insertions(+), 65 deletions(-) diff --git a/mypy/checkexpr.py b/mypy/checkexpr.py index b86801e25f1b..8ae0d5613f77 100644 --- a/mypy/checkexpr.py +++ b/mypy/checkexpr.py @@ -224,8 +224,7 @@ def analyze_ref_expr(self, e: RefExpr, lvalue: bool = False) -> Type: # Something that refers to a type alias appears in runtime context. # Note that we suppress bogus errors for alias redefinitions, # they are already reported in semanal.py. - result = self.alias_type_in_runtime_context(node.target, node.alias_tvars, - node.no_args, e, + result = self.alias_type_in_runtime_context(node, node.no_args, e, alias_definition=e.is_alias_rvalue or lvalue) else: @@ -2909,9 +2908,7 @@ def visit_type_application(self, tapp: TypeApplication) -> Type: """ if isinstance(tapp.expr, RefExpr) and isinstance(tapp.expr.node, TypeAlias): # Subscription of a (generic) alias in runtime context, expand the alias. - target = tapp.expr.node.target - all_vars = tapp.expr.node.alias_tvars - item = expand_type_alias(target, all_vars, tapp.types, self.chk.fail, + item = expand_type_alias(tapp.expr.node, tapp.types, self.chk.fail, tapp.expr.node.no_args, tapp) item = get_proper_type(item) if isinstance(item, Instance): @@ -2943,10 +2940,10 @@ def visit_type_alias_expr(self, alias: TypeAliasExpr) -> Type: both `reveal_type` instances will reveal the same type `def (...) -> builtins.list[Any]`. Note that type variables are implicitly substituted with `Any`. """ - return self.alias_type_in_runtime_context(alias.type, alias.tvars, alias.no_args, + return self.alias_type_in_runtime_context(alias.node, alias.no_args, alias, alias_definition=True) - def alias_type_in_runtime_context(self, target: Type, alias_tvars: List[str], + def alias_type_in_runtime_context(self, alias: TypeAlias, no_args: bool, ctx: Context, *, alias_definition: bool = False) -> Type: @@ -2963,14 +2960,14 @@ class LongName(Generic[T]): ... x = A() y = cast(A, ...) """ - if isinstance(target, Instance) and target.invalid: # type: ignore + if isinstance(alias.target, Instance) and alias.target.invalid: # type: ignore # An invalid alias, error already has been reported return AnyType(TypeOfAny.from_error) # If this is a generic alias, we set all variables to `Any`. # For example: # A = List[Tuple[T, T]] # x = A() <- same as List[Tuple[Any, Any]], see PEP 484. - item = get_proper_type(set_any_tvars(target, alias_tvars, ctx.line, ctx.column)) + item = get_proper_type(set_any_tvars(alias, ctx.line, ctx.column)) if isinstance(item, Instance): # Normally we get a callable type (or overloaded) with .is_type_obj() true # representing the class's constructor diff --git a/mypy/checkmember.py b/mypy/checkmember.py index 859bd6afcc6d..0f64b26b737f 100644 --- a/mypy/checkmember.py +++ b/mypy/checkmember.py @@ -500,7 +500,7 @@ def instance_alias_type(alias: TypeAlias, target = get_proper_type(alias.target) # type: Type assert isinstance(get_proper_type(target), Instance), "Must be called only with aliases to classes" - target = set_any_tvars(target, alias.alias_tvars, alias.line, alias.column) + target = set_any_tvars(alias, alias.line, alias.column) assert isinstance(target, Instance) # type: ignore[misc] tp = type_object_type(target.type, builtin_type) return expand_type_by_instance(tp, target) diff --git a/mypy/nodes.py b/mypy/nodes.py index f294705ada01..e610fec72762 100644 --- a/mypy/nodes.py +++ b/mypy/nodes.py @@ -2101,11 +2101,12 @@ class TypeAliasExpr(Expression): # A = List[Any] no_args = False # type: bool - def __init__(self, type: 'mypy.types.Type', tvars: List[str], no_args: bool) -> None: + def __init__(self, node: 'TypeAlias') -> None: super().__init__() - self.type = type - self.tvars = tvars - self.no_args = no_args + self.type = node.target + self.tvars = node.alias_tvars + self.no_args = node.no_args + self.node = node def accept(self, visitor: ExpressionVisitor[T]) -> T: return visitor.visit_type_alias_expr(self) diff --git a/mypy/semanal.py b/mypy/semanal.py index 18e45d684f64..0928aa3d5ca8 100644 --- a/mypy/semanal.py +++ b/mypy/semanal.py @@ -2478,9 +2478,7 @@ def check_and_set_up_type_alias(self, s: AssignmentStmt) -> bool: self.analyze_alias(rvalue, allow_placeholder=True) if not res: return False - # TODO: Maybe we only need to reject top-level placeholders, similar - # to base classes. - if self.found_incomplete_ref(tag) or has_placeholder(res): + if self.found_incomplete_ref(tag) or isinstance(res, PlaceholderType): # Since we have got here, we know this must be a type alias (incomplete refs # may appear in nested positions), therefore use becomes_typeinfo=True. self.mark_incomplete(lvalue.name, rvalue, becomes_typeinfo=True) @@ -2496,20 +2494,25 @@ def check_and_set_up_type_alias(self, s: AssignmentStmt) -> bool: context=s) # When this type alias gets "inlined", the Any is not explicit anymore, # so we need to replace it with non-explicit Anys. - res = make_any_non_explicit(res) + if not has_placeholder(res): + res = make_any_non_explicit(res) no_args = isinstance(res, Instance) and not res.args # type: ignore fix_instance_types(res, self.fail, self.note) + alias_node = TypeAlias(res, self.qualified_name(lvalue.name), s.line, s.column, + alias_tvars=alias_tvars, no_args=no_args) if isinstance(s.rvalue, (IndexExpr, CallExpr)): # CallExpr is for `void = type(None)` - s.rvalue.analyzed = TypeAliasExpr(res, alias_tvars, no_args) + s.rvalue.analyzed = TypeAliasExpr(alias_node) s.rvalue.analyzed.line = s.line # we use the column from resulting target, to get better location for errors s.rvalue.analyzed.column = res.column elif isinstance(s.rvalue, RefExpr): s.rvalue.is_alias_rvalue = True - alias_node = TypeAlias(res, self.qualified_name(lvalue.name), s.line, s.column, - alias_tvars=alias_tvars, no_args=no_args) + if existing: # An alias gets updated. + if self.final_iteration: + self.cannot_resolve_name(lvalue.name, 'name', s) + return True updated = False if isinstance(existing.node, TypeAlias): if existing.node.target != res: @@ -2524,13 +2527,9 @@ def check_and_set_up_type_alias(self, s: AssignmentStmt) -> bool: existing.node = alias_node updated = True if updated: - if self.final_iteration: - self.cannot_resolve_name(lvalue.name, 'name', s) - return True - else: - self.progress = True - # We need to defer so that this change can get propagated to base classes. - self.defer(s) + self.progress = True + # We need to defer so that this change can get propagated to base classes. + self.defer(s) else: self.add_symbol(lvalue.name, alias_node, s) if isinstance(rvalue, RefExpr) and isinstance(rvalue.node, TypeAlias): diff --git a/mypy/server/deps.py b/mypy/server/deps.py index 295b1bca266c..2f370d6587c6 100644 --- a/mypy/server/deps.py +++ b/mypy/server/deps.py @@ -880,10 +880,10 @@ def visit_instance(self, typ: Instance) -> List[str]: def visit_type_alias_type(self, typ: TypeAliasType) -> List[str]: assert typ.alias is not None - trigger = make_trigger(typ.alias.fullname()) - triggers = [trigger] + triggers = [] for arg in typ.args: triggers.extend(self.get_type_triggers(arg)) + triggers.extend(self.get_type_triggers(typ.alias.target)) return triggers def visit_any(self, typ: AnyType) -> List[str]: diff --git a/mypy/treetransform.py b/mypy/treetransform.py index 43fe7ae87fe8..1e7e8f49e33e 100644 --- a/mypy/treetransform.py +++ b/mypy/treetransform.py @@ -497,7 +497,7 @@ def visit_type_var_expr(self, node: TypeVarExpr) -> TypeVarExpr: self.type(node.upper_bound), variance=node.variance) def visit_type_alias_expr(self, node: TypeAliasExpr) -> TypeAliasExpr: - return TypeAliasExpr(node.type, node.tvars, node.no_args) + return TypeAliasExpr(node.node) def visit_newtype_expr(self, node: NewTypeExpr) -> NewTypeExpr: res = NewTypeExpr(node.name, node.old_type, line=node.line, column=node.column) diff --git a/mypy/type_visitor.py b/mypy/type_visitor.py index e2812364f25e..fe455a125d17 100644 --- a/mypy/type_visitor.py +++ b/mypy/type_visitor.py @@ -13,7 +13,7 @@ from abc import abstractmethod from collections import OrderedDict -from typing import Generic, TypeVar, cast, Any, List, Callable, Iterable, Optional +from typing import Generic, TypeVar, cast, Any, List, Callable, Iterable, Optional, Set from mypy_extensions import trait T = TypeVar('T') @@ -253,7 +253,7 @@ class TypeQuery(SyntheticTypeVisitor[T]): def __init__(self, strategy: Callable[[Iterable[T]], T]) -> None: self.strategy = strategy - self.seen = [] # type: List[Type] + self.seen_aliases = set() # type: Set[TypeAliasType] def visit_unbound_type(self, t: UnboundType) -> T: return self.query_types(t.args) @@ -335,8 +335,10 @@ def query_types(self, types: Iterable[Type]) -> T: """ res = [] # type: List[T] for t in types: - if any(t is s for s in self.seen): - continue - self.seen.append(t) + if isinstance(t, TypeAliasType): + # Avoid infinite recursion for recursive type aliases. + if t in self.seen_aliases: + continue + self.seen_aliases.add(t) res.append(t.accept(self)) return self.strategy(res) diff --git a/mypy/typeanal.py b/mypy/typeanal.py index e455f80dc12e..0f859b25a542 100644 --- a/mypy/typeanal.py +++ b/mypy/typeanal.py @@ -212,18 +212,13 @@ def visit_unbound_type_nonoptional(self, t: UnboundType, defining_literal: bool) return special if isinstance(node, TypeAlias): self.aliases_used.add(fullname) - all_vars = node.alias_tvars - target = node.target an_args = self.anal_array(t.args) disallow_any = self.options.disallow_any_generics and not self.is_typeshed_stub - res = expand_type_alias(target, all_vars, an_args, self.fail, node.no_args, t, + res = expand_type_alias(node, an_args, self.fail, node.no_args, t, unexpanded_type=t, disallow_any=disallow_any) # The only case where expand_type_alias() can return an incorrect instance is # when it is top-level instance, so no need to recurse. - # TODO: this is not really needed, since with the new logic we will not expand - # aliases immediately. - res = get_proper_type(res) if (isinstance(res, Instance) and len(res.args) != len(res.type.type_vars) and not self.defining_alias): fix_instance( @@ -483,6 +478,7 @@ def visit_instance(self, t: Instance) -> Type: return t def visit_type_alias_type(self, t: TypeAliasType) -> Type: + # TODO: should we do something here? return t def visit_type_var(self, t: TypeVarType) -> Type: @@ -973,7 +969,7 @@ def fix_instance(t: Instance, fail: MsgCallback, note: MsgCallback, t.invalid = True -def expand_type_alias(target: Type, alias_tvars: List[str], args: List[Type], +def expand_type_alias(node: TypeAlias, args: List[Type], fail: MsgCallback, no_args: bool, ctx: Context, *, unexpanded_type: Optional[Type] = None, disallow_any: bool = False) -> Type: @@ -987,30 +983,28 @@ def expand_type_alias(target: Type, alias_tvars: List[str], args: List[Type], no_args: whether original definition used a bare generic `A = List` ctx: context where expansion happens """ - exp_len = len(alias_tvars) + exp_len = len(node.alias_tvars) act_len = len(args) if exp_len > 0 and act_len == 0: # Interpret bare Alias same as normal generic, i.e., Alias[Any, Any, ...] - assert alias_tvars is not None - return set_any_tvars(target, alias_tvars, ctx.line, ctx.column, + return set_any_tvars(node, ctx.line, ctx.column, disallow_any=disallow_any, fail=fail, unexpanded_type=unexpanded_type) if exp_len == 0 and act_len == 0: if no_args: - assert isinstance(target, Instance) # type: ignore - return Instance(target.type, [], line=ctx.line, column=ctx.column) - return target - if exp_len == 0 and act_len > 0 and isinstance(target, Instance) and no_args: # type: ignore - tp = Instance(target.type, args) + assert isinstance(node.target, Instance) # type: ignore + return Instance(node.target.type, [], line=ctx.line, column=ctx.column) + return TypeAliasType(node, [], line=ctx.line, column=ctx.column) + if exp_len == 0 and act_len > 0 and isinstance(node.target, Instance) and no_args: # type: ignore + tp = Instance(node.target.type, args) tp.line = ctx.line tp.column = ctx.column return tp if act_len != exp_len: fail('Bad number of arguments for type alias, expected: %s, given: %s' % (exp_len, act_len), ctx) - return set_any_tvars(target, alias_tvars or [], - ctx.line, ctx.column, from_error=True) - typ = replace_alias_tvars(target, alias_tvars, args, ctx.line, ctx.column) # type: Type + return set_any_tvars(node, ctx.line, ctx.column, from_error=True) + typ = TypeAliasType(node, args, ctx.line, ctx.column) # type: Type # HACK: Implement FlexibleAlias[T, typ] by expanding it to typ here. if (isinstance(typ, Instance) # type: ignore and typ.type.fullname() == 'mypy_extensions.FlexibleAlias'): @@ -1018,7 +1012,7 @@ def expand_type_alias(target: Type, alias_tvars: List[str], args: List[Type], return typ -def set_any_tvars(tp: Type, vars: List[str], +def set_any_tvars(node: TypeAlias, newline: int, newcolumn: int, *, from_error: bool = False, disallow_any: bool = False, @@ -1030,13 +1024,13 @@ def set_any_tvars(tp: Type, vars: List[str], type_of_any = TypeOfAny.from_omitted_generics if disallow_any: assert fail is not None - otype = unexpanded_type or tp + otype = unexpanded_type or node.target type_str = otype.name if isinstance(otype, UnboundType) else format_type_bare(otype) fail(message_registry.BARE_GENERIC.format(quote_type_string(type_str)), Context(newline, newcolumn), code=codes.TYPE_ARG) any_type = AnyType(type_of_any, line=newline, column=newcolumn) - return replace_alias_tvars(tp, vars, [any_type] * len(vars), newline, newcolumn) + return TypeAliasType(node, [any_type] * len(node.alias_tvars), newline, newcolumn) def remove_dups(tvars: Iterable[T]) -> List[T]: diff --git a/mypy/types.py b/mypy/types.py index b2c689f537e2..9d69ff80526b 100644 --- a/mypy/types.py +++ b/mypy/types.py @@ -180,6 +180,10 @@ def _expand_once(self) -> Type: its public wrapper mypy.types.get_proper_type() is preferred. """ assert self.alias is not None + if self.alias.no_args: + assert isinstance(self.alias.target, ProperType) + assert isinstance(self.alias.target, Instance) + return self.alias.target.copy_modified(args=self.args) return replace_alias_tvars(self.alias.target, self.alias.alias_tvars, self.args, self.line, self.column) @@ -1936,6 +1940,7 @@ class TypeStrVisitor(SyntheticTypeVisitor[str]): def __init__(self, id_mapper: Optional[IdMapper] = None) -> None: self.id_mapper = id_mapper + self.any_as_dots = False def visit_unbound_type(self, t: UnboundType) -> str: s = t.name + '?' @@ -1954,6 +1959,8 @@ def visit_callable_argument(self, t: CallableArgument) -> str: return "{}({}, {})".format(t.constructor, typ, t.name) def visit_any(self, t: AnyType) -> str: + if self.any_as_dots and t.type_of_any == TypeOfAny.special_form: + return '...' return 'Any' def visit_none_type(self, t: NoneType) -> str: @@ -2093,7 +2100,11 @@ def visit_placeholder_type(self, t: PlaceholderType) -> str: def visit_type_alias_type(self, t: TypeAliasType) -> str: if t.alias is not None: - return ''.format(t.alias.fullname()) + unrolled, recursed = t._partial_expansion() + self.any_as_dots = recursed + type_str = unrolled.accept(self) + self.any_as_dots = False + return type_str return '' def list_str(self, a: Iterable[Type]) -> str: @@ -2153,7 +2164,7 @@ def copy_type(t: TP) -> TP: return copy.copy(t) -class InstantiateAliasVisitor(TypeTranslator): +class InstantiateAliasVisitor(TypeTranslator, SyntheticTypeVisitor[Type]): def __init__(self, vars: List[str], subs: List[Type]) -> None: self.replacements = {v: s for (v, s) in zip(vars, subs)} @@ -2174,6 +2185,9 @@ def visit_type_var(self, typ: TypeVarType) -> Type: return self.replacements[typ.name] return typ + def visit_placeholder_type(self, typ: PlaceholderType) -> Type: + return typ + def replace_alias_tvars(tp: Type, vars: List[str], subs: List[Type], newline: int, newcolumn: int) -> Type: diff --git a/test-data/unit/check-incremental.test b/test-data/unit/check-incremental.test index 7f68c3afeff1..5a811dc2fd10 100644 --- a/test-data/unit/check-incremental.test +++ b/test-data/unit/check-incremental.test @@ -2472,7 +2472,7 @@ A = Dict[str, int] [builtins fixtures/dict.pyi] [out] --- Some crazy selef-referential named tuples, types dicts, and aliases +-- Some crazy self-referential named tuples, types dicts, and aliases -- to be sure that everything can be _serialized_ (i.e. ForwardRef's are removed). -- For this reason errors are silenced (tests with # type: ignore have equivalents in other files) diff --git a/test-data/unit/check-namedtuple.test b/test-data/unit/check-namedtuple.test index ab5c7bd0b6bb..c03b6f5a695f 100644 --- a/test-data/unit/check-namedtuple.test +++ b/test-data/unit/check-namedtuple.test @@ -658,16 +658,19 @@ reveal_type(n) # N: Revealed type is 'Tuple[builtins.str, builtins.tuple[Any], f [case testSelfRefNT3] -from typing import NamedTuple, Tuple +from typing import NamedTuple, Tuple, Union -class B(NamedTuple): +class _B(NamedTuple): x: Tuple[A, int] # E: Cannot resolve name "A" (possible cyclic definition) y: int +B = Union[_B] -A = NamedTuple('A', [ +_A = NamedTuple('_A', [ ('x', str), ('y', 'B'), ]) +A = Union[_A] + n: B m: A reveal_type(n.x) # N: Revealed type is 'Tuple[Any, builtins.int]' diff --git a/test-data/unit/check-type-aliases.test b/test-data/unit/check-type-aliases.test index 4371200bc3d8..bd5915ac5011 100644 --- a/test-data/unit/check-type-aliases.test +++ b/test-data/unit/check-type-aliases.test @@ -187,9 +187,12 @@ Alias = Tuple[int, T] # Recursive aliases are not supported yet. from typing import Type, Callable, Union -A = Union[A, int] # E: Cannot resolve name "A" (possible cyclic definition) +# A = Union[A, int] # E: Cannot resolve name "A" (possible cyclic definition) B = Callable[[B], int] # E: Cannot resolve name "B" (possible cyclic definition) -C = Type[C] # E: Cannot resolve name "C" (possible cyclic definition) +# C = Type[C] # E: Cannot resolve name "C" (possible cyclic definition) + +x: B +reveal_type(x) [case testRecursiveAliasesErrors2] From ea02fc33caccbfdefc97205917a016a45fc6fcb6 Mon Sep 17 00:00:00 2001 From: Ivan Levkivskyi Date: Fri, 8 Nov 2019 20:27:22 +0000 Subject: [PATCH 02/12] Some debugging attempts --- mypy/semanal.py | 2 +- test-data/unit/check-type-aliases.test | 19 +++++++++++++++++++ 2 files changed, 20 insertions(+), 1 deletion(-) diff --git a/mypy/semanal.py b/mypy/semanal.py index 0928aa3d5ca8..5ec303b215ca 100644 --- a/mypy/semanal.py +++ b/mypy/semanal.py @@ -2511,7 +2511,7 @@ def check_and_set_up_type_alias(self, s: AssignmentStmt) -> bool: if existing: # An alias gets updated. if self.final_iteration: - self.cannot_resolve_name(lvalue.name, 'name', s) + self.cannot_resolve_name(lvalue.name, 'iiiname', s) return True updated = False if isinstance(existing.node, TypeAlias): diff --git a/test-data/unit/check-type-aliases.test b/test-data/unit/check-type-aliases.test index bd5915ac5011..5c65dbf7e2c2 100644 --- a/test-data/unit/check-type-aliases.test +++ b/test-data/unit/check-type-aliases.test @@ -635,3 +635,22 @@ except E as e: reveal_type(e) # N: Revealed type is '__main__.E' [builtins fixtures/exception.pyi] [out] + +[case testTryFancyAliases] +from typing import Sequence, TypeVar, List, Union, Tuple +dummy: C +T = TypeVar('T') + +Nested = Union[int, List[Nested]] + +x: Nested = [1, [2, [3]]] +reveal_type(x) + +B = Tuple[int, B] +y: B +reveal_type(y) +class C: + x: D + +class D: ... +[builtins fixtures/list.pyi] From 51a63a41a62c49c3912f8f4e7e9e63c7686ba9ff Mon Sep 17 00:00:00 2001 From: Ivan Levkivskyi Date: Sat, 9 Nov 2019 00:47:17 +0000 Subject: [PATCH 03/12] Some more ideas --- mypy/constraints.py | 6 +++--- mypy/messages.py | 4 +++- mypy/sametypes.py | 3 --- mypy/semanal.py | 21 ++++++++++++-------- mypy/subtypes.py | 27 ++++++++++++++++---------- mypy/type_visitor.py | 1 + mypy/typeops.py | 5 ++--- mypy/types.py | 15 ++++++++++++++ test-data/unit/check-type-aliases.test | 27 ++++++++++++++++---------- 9 files changed, 71 insertions(+), 38 deletions(-) diff --git a/mypy/constraints.py b/mypy/constraints.py index a078eb0b08b5..4762796c606b 100644 --- a/mypy/constraints.py +++ b/mypy/constraints.py @@ -92,8 +92,7 @@ def infer_constraints(template: Type, actual: Type, """ if any(get_proper_type(template) == get_proper_type(t) for t in TypeState._inferring): return [] - if (isinstance(template, TypeAliasType) and isinstance(actual, TypeAliasType) and - template.is_recursive and actual.is_recursive): + if isinstance(template, TypeAliasType) and template.is_recursive: # This case requires special care because it may cause infinite recursion. TypeState._inferring.append(template) res = _infer_constraints(template, actual, direction) @@ -105,6 +104,7 @@ def infer_constraints(template: Type, actual: Type, def _infer_constraints(template: Type, actual: Type, direction: int) -> List[Constraint]: + orig_template = template template = get_proper_type(template) actual = get_proper_type(actual) @@ -129,7 +129,7 @@ def _infer_constraints(template: Type, actual: Type, if direction == SUPERTYPE_OF and isinstance(actual, UnionType): res = [] for a_item in actual.items: - res.extend(infer_constraints(template, a_item, direction)) + res.extend(infer_constraints(orig_template, a_item, direction)) return res # Now the potential subtype is known not to be a Union or a type diff --git a/mypy/messages.py b/mypy/messages.py index 29b99352cfec..3333be11337d 100644 --- a/mypy/messages.py +++ b/mypy/messages.py @@ -23,7 +23,7 @@ Type, CallableType, Instance, TypeVarType, TupleType, TypedDictType, LiteralType, UnionType, NoneType, AnyType, Overloaded, FunctionLike, DeletedType, TypeType, UninhabitedType, TypeOfAny, UnboundType, PartialType, get_proper_type, ProperType, - get_proper_types + get_proper_types, TypeAliasType ) from mypy.typetraverser import TypeTraverserVisitor from mypy.nodes import ( @@ -1413,6 +1413,8 @@ def format(typ: Type) -> str: return format_type_inner(typ, verbosity, fullnames) # TODO: show type alias names in errors. + if isinstance(typ, TypeAliasType) and typ.is_recursive: + return str(typ) typ = get_proper_type(typ) if isinstance(typ, Instance): diff --git a/mypy/sametypes.py b/mypy/sametypes.py index 024333a13ec8..ce4120476a7d 100644 --- a/mypy/sametypes.py +++ b/mypy/sametypes.py @@ -10,8 +10,6 @@ def is_same_type(left: Type, right: Type) -> bool: """Is 'left' the same type as 'right'?""" - left = get_proper_type(left) - right = get_proper_type(right) if isinstance(right, UnboundType): # Make unbound types same as anything else to reduce the number of @@ -32,7 +30,6 @@ def is_same_type(left: Type, right: Type) -> bool: def simplify_union(t: Type) -> ProperType: - t = get_proper_type(t) if isinstance(t, UnionType): return make_simplified_union(t.items) return t diff --git a/mypy/semanal.py b/mypy/semanal.py index 5ec303b215ca..c48613b32be0 100644 --- a/mypy/semanal.py +++ b/mypy/semanal.py @@ -1911,14 +1911,18 @@ def visit_assignment_stmt(self, s: AssignmentStmt) -> None: return tag = self.track_incomplete_refs() + self.no_applications = True s.rvalue.accept(self) + self.no_applications = False if self.found_incomplete_ref(tag) or self.should_wait_rhs(s.rvalue): # Initializer couldn't be fully analyzed. Defer the current node and give up. # Make sure that if we skip the definition of some local names, they can't be # added later in this scope, since an earlier definition should take precedence. + s.rvalue.accept(self) for expr in names_modified_by_assignment(s): self.mark_incomplete(expr.name, expr) return + s.rvalue.accept(self) # The r.h.s. is now ready to be classified, first check if it is a special form: special_form = False @@ -2510,9 +2514,6 @@ def check_and_set_up_type_alias(self, s: AssignmentStmt) -> bool: if existing: # An alias gets updated. - if self.final_iteration: - self.cannot_resolve_name(lvalue.name, 'iiiname', s) - return True updated = False if isinstance(existing.node, TypeAlias): if existing.node.target != res: @@ -2527,9 +2528,13 @@ def check_and_set_up_type_alias(self, s: AssignmentStmt) -> bool: existing.node = alias_node updated = True if updated: - self.progress = True - # We need to defer so that this change can get propagated to base classes. - self.defer(s) + if self.final_iteration: + self.cannot_resolve_name(lvalue.name, 'name', s) + return True + else: + self.progress = True + # We need to defer so that this change can get propagated to base classes. + self.defer(s) else: self.add_symbol(lvalue.name, alias_node, s) if isinstance(rvalue, RefExpr) and isinstance(rvalue.node, TypeAlias): @@ -3645,8 +3650,8 @@ def visit_index_expr(self, expr: IndexExpr) -> None: and isinstance(base.node, TypeInfo) and not base.node.is_generic()): expr.index.accept(self) - elif ((isinstance(base, RefExpr) and isinstance(base.node, TypeAlias)) - or refers_to_class_or_function(base)): + elif (((isinstance(base, RefExpr) and isinstance(base.node, TypeAlias)) + or refers_to_class_or_function(base)) and not getattr(self, 'no_applications', False)): # We need to do full processing on every iteration, since some type # arguments may contain placeholder types. self.analyze_type_application(expr) diff --git a/mypy/subtypes.py b/mypy/subtypes.py index 1c5862471656..fd8dbf6ff868 100644 --- a/mypy/subtypes.py +++ b/mypy/subtypes.py @@ -103,6 +103,8 @@ def _is_subtype(left: Type, right: Type, ignore_pos_arg_names: bool = False, ignore_declared_variance: bool = False, ignore_promotions: bool = False) -> bool: + orig_right = right + orig_left = left left = get_proper_type(left) right = get_proper_type(right) @@ -113,7 +115,7 @@ def _is_subtype(left: Type, right: Type, # Normally, when 'left' is not itself a union, the only way # 'left' can be a subtype of the union 'right' is if it is a # subtype of one of the items making up the union. - is_subtype_of_item = any(is_subtype(left, item, + is_subtype_of_item = any(is_subtype(orig_left, item, ignore_type_params=ignore_type_params, ignore_pos_arg_names=ignore_pos_arg_names, ignore_declared_variance=ignore_declared_variance, @@ -130,7 +132,7 @@ def _is_subtype(left: Type, right: Type, elif is_subtype_of_item: return True # otherwise, fall through - return left.accept(SubtypeVisitor(right, + return left.accept(SubtypeVisitor(orig_right, ignore_type_params=ignore_type_params, ignore_pos_arg_names=ignore_pos_arg_names, ignore_declared_variance=ignore_declared_variance, @@ -155,13 +157,14 @@ def is_equivalent(a: Type, b: Type, class SubtypeVisitor(TypeVisitor[bool]): - def __init__(self, right: ProperType, + def __init__(self, right: Type, *, ignore_type_params: bool, ignore_pos_arg_names: bool = False, ignore_declared_variance: bool = False, ignore_promotions: bool = False) -> None: - self.right = right + self.right = get_proper_type(right) + self.orig_right = right self.ignore_type_params = ignore_type_params self.ignore_pos_arg_names = ignore_pos_arg_names self.ignore_declared_variance = ignore_declared_variance @@ -449,7 +452,7 @@ def visit_overloaded(self, left: Overloaded) -> bool: return False def visit_union_type(self, left: UnionType) -> bool: - return all(self._is_subtype(item, self.right) for item in left.items) + return all(self._is_subtype(item, self.orig_right) for item in left.items) def visit_partial_type(self, left: PartialType) -> bool: # This is indeterminate as we don't really know the complete type yet. @@ -1139,22 +1142,26 @@ def is_proper_subtype(left: Type, right: Type, *, ignore_promotions: bool = Fals def _is_proper_subtype(left: Type, right: Type, *, ignore_promotions: bool = False, erase_instances: bool = False) -> bool: + orig_left = left + orig_right = right left = get_proper_type(left) right = get_proper_type(right) if isinstance(right, UnionType) and not isinstance(left, UnionType): - return any([is_proper_subtype(left, item, ignore_promotions=ignore_promotions, + return any([is_proper_subtype(orig_left, item, ignore_promotions=ignore_promotions, erase_instances=erase_instances) for item in right.items]) - return left.accept(ProperSubtypeVisitor(right, ignore_promotions=ignore_promotions, + return left.accept(ProperSubtypeVisitor(orig_right, + ignore_promotions=ignore_promotions, erase_instances=erase_instances)) class ProperSubtypeVisitor(TypeVisitor[bool]): - def __init__(self, right: ProperType, *, + def __init__(self, right: Type, *, ignore_promotions: bool = False, erase_instances: bool = False) -> None: - self.right = right + self.right = get_proper_type(right) + self.orig_right = right self.ignore_promotions = ignore_promotions self.erase_instances = erase_instances self._subtype_kind = ProperSubtypeVisitor.build_subtype_kind( @@ -1313,7 +1320,7 @@ def visit_overloaded(self, left: Overloaded) -> bool: return False def visit_union_type(self, left: UnionType) -> bool: - return all([self._is_proper_subtype(item, self.right) for item in left.items]) + return all([self._is_proper_subtype(item, self.orig_right) for item in left.items]) def visit_partial_type(self, left: PartialType) -> bool: # TODO: What's the right thing to do here? diff --git a/mypy/type_visitor.py b/mypy/type_visitor.py index fe455a125d17..cd53a8d4288a 100644 --- a/mypy/type_visitor.py +++ b/mypy/type_visitor.py @@ -337,6 +337,7 @@ def query_types(self, types: Iterable[Type]) -> T: for t in types: if isinstance(t, TypeAliasType): # Avoid infinite recursion for recursive type aliases. + # TODO: Ideally we should fire subvisitors if we care about duplicates. if t in self.seen_aliases: continue self.seen_aliases.add(t) diff --git a/mypy/typeops.py b/mypy/typeops.py index 53b65fefcad2..073395565102 100644 --- a/mypy/typeops.py +++ b/mypy/typeops.py @@ -303,12 +303,11 @@ def make_simplified_union(items: Sequence[Type], Note: This must NOT be used during semantic analysis, since TypeInfos may not be fully initialized. """ - items = get_proper_types(items) while any(isinstance(typ, UnionType) for typ in items): - all_items = [] # type: List[ProperType] + all_items = [] # type: List[Type] for typ in items: if isinstance(typ, UnionType): - all_items.extend(get_proper_types(typ.items)) + all_items.extend(typ.items) else: all_items.append(typ) items = all_items diff --git a/mypy/types.py b/mypy/types.py index 9d69ff80526b..8be7aff7eaa1 100644 --- a/mypy/types.py +++ b/mypy/types.py @@ -2188,6 +2188,21 @@ def visit_type_var(self, typ: TypeVarType) -> Type: def visit_placeholder_type(self, typ: PlaceholderType) -> Type: return typ + def visit_callable_argument(self, t: CallableArgument) -> Type: + pass + + def visit_ellipsis_type(self, t: EllipsisType) -> Type: + pass + + def visit_raw_expression_type(self, t: RawExpressionType) -> Type: + pass + + def visit_star_type(self, t: StarType) -> Type: + pass + + def visit_type_list(self, t: TypeList) -> Type: + pass + def replace_alias_tvars(tp: Type, vars: List[str], subs: List[Type], newline: int, newcolumn: int) -> Type: diff --git a/test-data/unit/check-type-aliases.test b/test-data/unit/check-type-aliases.test index 5c65dbf7e2c2..dc2997d5c9de 100644 --- a/test-data/unit/check-type-aliases.test +++ b/test-data/unit/check-type-aliases.test @@ -638,19 +638,26 @@ except E as e: [case testTryFancyAliases] from typing import Sequence, TypeVar, List, Union, Tuple -dummy: C + T = TypeVar('T') -Nested = Union[int, List[Nested]] +NestedA = List[Union[T, NestedA[T]]] +NestedB = Union[T, List[NestedB[T]]] -x: Nested = [1, [2, [3]]] -reveal_type(x) +xa: NestedA[int] = [1, [2, [3]]] +xb: NestedB[int] = [1, [2, [3]]] +reveal_type(xa) +reveal_type(xb) -B = Tuple[int, B] -y: B -reveal_type(y) -class C: - x: D +def fun_a(x: NestedA[T]) -> T: ... +def fun_b(x: NestedB[T]) -> T: ... + +reveal_type(fun_a([1, 2, 3])) +reveal_type(fun_b([1, 2, 3])) + +reveal_type(fun_a(xa)) +reveal_type(fun_b(xb)) -class D: ... +xbf: NestedB[float] +reveal_type([xb, xbf]) [builtins fixtures/list.pyi] From 6bdb0bc36c311a4134ab0b610042995e8e97f140 Mon Sep 17 00:00:00 2001 From: Ivan Levkivskyi Date: Sun, 10 Nov 2019 22:47:03 +0000 Subject: [PATCH 04/12] Undo some experimental changes --- mypy/messages.py | 4 +--- mypy/sametypes.py | 3 +++ mypy/semanal.py | 10 ++++---- mypy/subtypes.py | 2 +- mypy/type_visitor.py | 7 +++--- mypy/typeanal.py | 11 +++++---- mypy/typeops.py | 5 ++-- mypy/types.py | 20 +--------------- test-data/unit/check-namedtuple.test | 9 +++---- test-data/unit/check-type-aliases.test | 33 ++------------------------ 10 files changed, 28 insertions(+), 76 deletions(-) diff --git a/mypy/messages.py b/mypy/messages.py index 3333be11337d..29b99352cfec 100644 --- a/mypy/messages.py +++ b/mypy/messages.py @@ -23,7 +23,7 @@ Type, CallableType, Instance, TypeVarType, TupleType, TypedDictType, LiteralType, UnionType, NoneType, AnyType, Overloaded, FunctionLike, DeletedType, TypeType, UninhabitedType, TypeOfAny, UnboundType, PartialType, get_proper_type, ProperType, - get_proper_types, TypeAliasType + get_proper_types ) from mypy.typetraverser import TypeTraverserVisitor from mypy.nodes import ( @@ -1413,8 +1413,6 @@ def format(typ: Type) -> str: return format_type_inner(typ, verbosity, fullnames) # TODO: show type alias names in errors. - if isinstance(typ, TypeAliasType) and typ.is_recursive: - return str(typ) typ = get_proper_type(typ) if isinstance(typ, Instance): diff --git a/mypy/sametypes.py b/mypy/sametypes.py index ce4120476a7d..024333a13ec8 100644 --- a/mypy/sametypes.py +++ b/mypy/sametypes.py @@ -10,6 +10,8 @@ def is_same_type(left: Type, right: Type) -> bool: """Is 'left' the same type as 'right'?""" + left = get_proper_type(left) + right = get_proper_type(right) if isinstance(right, UnboundType): # Make unbound types same as anything else to reduce the number of @@ -30,6 +32,7 @@ def is_same_type(left: Type, right: Type) -> bool: def simplify_union(t: Type) -> ProperType: + t = get_proper_type(t) if isinstance(t, UnionType): return make_simplified_union(t.items) return t diff --git a/mypy/semanal.py b/mypy/semanal.py index c48613b32be0..6f7192ee2650 100644 --- a/mypy/semanal.py +++ b/mypy/semanal.py @@ -1911,18 +1911,14 @@ def visit_assignment_stmt(self, s: AssignmentStmt) -> None: return tag = self.track_incomplete_refs() - self.no_applications = True s.rvalue.accept(self) - self.no_applications = False if self.found_incomplete_ref(tag) or self.should_wait_rhs(s.rvalue): # Initializer couldn't be fully analyzed. Defer the current node and give up. # Make sure that if we skip the definition of some local names, they can't be # added later in this scope, since an earlier definition should take precedence. - s.rvalue.accept(self) for expr in names_modified_by_assignment(s): self.mark_incomplete(expr.name, expr) return - s.rvalue.accept(self) # The r.h.s. is now ready to be classified, first check if it is a special form: special_form = False @@ -2482,7 +2478,9 @@ def check_and_set_up_type_alias(self, s: AssignmentStmt) -> bool: self.analyze_alias(rvalue, allow_placeholder=True) if not res: return False - if self.found_incomplete_ref(tag) or isinstance(res, PlaceholderType): + # TODO: Maybe we only need to reject top-level placeholders, similar + # to base classes. + if self.found_incomplete_ref(tag) or has_placeholder(res): # Since we have got here, we know this must be a type alias (incomplete refs # may appear in nested positions), therefore use becomes_typeinfo=True. self.mark_incomplete(lvalue.name, rvalue, becomes_typeinfo=True) @@ -3651,7 +3649,7 @@ def visit_index_expr(self, expr: IndexExpr) -> None: and not base.node.is_generic()): expr.index.accept(self) elif (((isinstance(base, RefExpr) and isinstance(base.node, TypeAlias)) - or refers_to_class_or_function(base)) and not getattr(self, 'no_applications', False)): + or refers_to_class_or_function(base))): # We need to do full processing on every iteration, since some type # arguments may contain placeholder types. self.analyze_type_application(expr) diff --git a/mypy/subtypes.py b/mypy/subtypes.py index fd8dbf6ff868..fa3d3f9dd3a1 100644 --- a/mypy/subtypes.py +++ b/mypy/subtypes.py @@ -7,7 +7,7 @@ Type, AnyType, UnboundType, TypeVisitor, FormalArgument, NoneType, Instance, TypeVarType, CallableType, TupleType, TypedDictType, UnionType, Overloaded, ErasedType, PartialType, DeletedType, UninhabitedType, TypeType, is_named_instance, - FunctionLike, TypeOfAny, LiteralType, ProperType, get_proper_type, TypeAliasType + FunctionLike, TypeOfAny, LiteralType, get_proper_type, TypeAliasType ) import mypy.applytype import mypy.constraints diff --git a/mypy/type_visitor.py b/mypy/type_visitor.py index cd53a8d4288a..89e76b3a3325 100644 --- a/mypy/type_visitor.py +++ b/mypy/type_visitor.py @@ -329,15 +329,14 @@ def query_types(self, types: Iterable[Type]) -> T: """Perform a query for a list of types. Use the strategy to combine the results. - Skip types already visited types to avoid infinite recursion. - Note: types can be recursive until they are fully analyzed and "unentangled" - in patches after the semantic analysis. + Skip type aliases already visited types to avoid infinite recursion. """ res = [] # type: List[T] for t in types: if isinstance(t, TypeAliasType): # Avoid infinite recursion for recursive type aliases. - # TODO: Ideally we should fire subvisitors if we care about duplicates. + # TODO: Ideally we should fire subvisitors here (or use caching) if we care + # about duplicates. if t in self.seen_aliases: continue self.seen_aliases.add(t) diff --git a/mypy/typeanal.py b/mypy/typeanal.py index 0f859b25a542..a33d3b61af34 100644 --- a/mypy/typeanal.py +++ b/mypy/typeanal.py @@ -14,7 +14,7 @@ from mypy.types import ( Type, UnboundType, TypeVarType, TupleType, TypedDictType, UnionType, Instance, AnyType, CallableType, NoneType, ErasedType, DeletedType, TypeList, TypeVarDef, SyntheticTypeVisitor, - StarType, PartialType, EllipsisType, UninhabitedType, TypeType, replace_alias_tvars, + StarType, PartialType, EllipsisType, UninhabitedType, TypeType, CallableArgument, TypeQuery, union_items, TypeOfAny, LiteralType, RawExpressionType, PlaceholderType, Overloaded, get_proper_type, TypeAliasType ) @@ -219,7 +219,8 @@ def visit_unbound_type_nonoptional(self, t: UnboundType, defining_literal: bool) disallow_any=disallow_any) # The only case where expand_type_alias() can return an incorrect instance is # when it is top-level instance, so no need to recurse. - if (isinstance(res, Instance) and len(res.args) != len(res.type.type_vars) and + if (isinstance(res, Instance) and # type: ignore[misc] + len(res.args) != len(res.type.type_vars) and not self.defining_alias): fix_instance( res, @@ -992,10 +993,12 @@ def expand_type_alias(node: TypeAlias, args: List[Type], unexpanded_type=unexpanded_type) if exp_len == 0 and act_len == 0: if no_args: - assert isinstance(node.target, Instance) # type: ignore + assert isinstance(node.target, Instance) # type: ignore[misc] return Instance(node.target.type, [], line=ctx.line, column=ctx.column) return TypeAliasType(node, [], line=ctx.line, column=ctx.column) - if exp_len == 0 and act_len > 0 and isinstance(node.target, Instance) and no_args: # type: ignore + if (exp_len == 0 and act_len > 0 + and isinstance(node.target, Instance) # type: ignore[misc] + and no_args): tp = Instance(node.target.type, args) tp.line = ctx.line tp.column = ctx.column diff --git a/mypy/typeops.py b/mypy/typeops.py index 073395565102..53b65fefcad2 100644 --- a/mypy/typeops.py +++ b/mypy/typeops.py @@ -303,11 +303,12 @@ def make_simplified_union(items: Sequence[Type], Note: This must NOT be used during semantic analysis, since TypeInfos may not be fully initialized. """ + items = get_proper_types(items) while any(isinstance(typ, UnionType) for typ in items): - all_items = [] # type: List[Type] + all_items = [] # type: List[ProperType] for typ in items: if isinstance(typ, UnionType): - all_items.extend(typ.items) + all_items.extend(get_proper_types(typ.items)) else: all_items.append(typ) items = all_items diff --git a/mypy/types.py b/mypy/types.py index 8be7aff7eaa1..85a788611f0e 100644 --- a/mypy/types.py +++ b/mypy/types.py @@ -2164,7 +2164,7 @@ def copy_type(t: TP) -> TP: return copy.copy(t) -class InstantiateAliasVisitor(TypeTranslator, SyntheticTypeVisitor[Type]): +class InstantiateAliasVisitor(TypeTranslator): def __init__(self, vars: List[str], subs: List[Type]) -> None: self.replacements = {v: s for (v, s) in zip(vars, subs)} @@ -2185,24 +2185,6 @@ def visit_type_var(self, typ: TypeVarType) -> Type: return self.replacements[typ.name] return typ - def visit_placeholder_type(self, typ: PlaceholderType) -> Type: - return typ - - def visit_callable_argument(self, t: CallableArgument) -> Type: - pass - - def visit_ellipsis_type(self, t: EllipsisType) -> Type: - pass - - def visit_raw_expression_type(self, t: RawExpressionType) -> Type: - pass - - def visit_star_type(self, t: StarType) -> Type: - pass - - def visit_type_list(self, t: TypeList) -> Type: - pass - def replace_alias_tvars(tp: Type, vars: List[str], subs: List[Type], newline: int, newcolumn: int) -> Type: diff --git a/test-data/unit/check-namedtuple.test b/test-data/unit/check-namedtuple.test index c03b6f5a695f..143a84496ae9 100644 --- a/test-data/unit/check-namedtuple.test +++ b/test-data/unit/check-namedtuple.test @@ -658,19 +658,16 @@ reveal_type(n) # N: Revealed type is 'Tuple[builtins.str, builtins.tuple[Any], f [case testSelfRefNT3] -from typing import NamedTuple, Tuple, Union +from typing import NamedTuple, Tuple -class _B(NamedTuple): +class B(NamedTuple): x: Tuple[A, int] # E: Cannot resolve name "A" (possible cyclic definition) y: int -B = Union[_B] -_A = NamedTuple('_A', [ +A = NamedTuple('_A', [ ('x', str), ('y', 'B'), ]) -A = Union[_A] - n: B m: A reveal_type(n.x) # N: Revealed type is 'Tuple[Any, builtins.int]' diff --git a/test-data/unit/check-type-aliases.test b/test-data/unit/check-type-aliases.test index dc2997d5c9de..4371200bc3d8 100644 --- a/test-data/unit/check-type-aliases.test +++ b/test-data/unit/check-type-aliases.test @@ -187,12 +187,9 @@ Alias = Tuple[int, T] # Recursive aliases are not supported yet. from typing import Type, Callable, Union -# A = Union[A, int] # E: Cannot resolve name "A" (possible cyclic definition) +A = Union[A, int] # E: Cannot resolve name "A" (possible cyclic definition) B = Callable[[B], int] # E: Cannot resolve name "B" (possible cyclic definition) -# C = Type[C] # E: Cannot resolve name "C" (possible cyclic definition) - -x: B -reveal_type(x) +C = Type[C] # E: Cannot resolve name "C" (possible cyclic definition) [case testRecursiveAliasesErrors2] @@ -635,29 +632,3 @@ except E as e: reveal_type(e) # N: Revealed type is '__main__.E' [builtins fixtures/exception.pyi] [out] - -[case testTryFancyAliases] -from typing import Sequence, TypeVar, List, Union, Tuple - -T = TypeVar('T') - -NestedA = List[Union[T, NestedA[T]]] -NestedB = Union[T, List[NestedB[T]]] - -xa: NestedA[int] = [1, [2, [3]]] -xb: NestedB[int] = [1, [2, [3]]] -reveal_type(xa) -reveal_type(xb) - -def fun_a(x: NestedA[T]) -> T: ... -def fun_b(x: NestedB[T]) -> T: ... - -reveal_type(fun_a([1, 2, 3])) -reveal_type(fun_b([1, 2, 3])) - -reveal_type(fun_a(xa)) -reveal_type(fun_b(xb)) - -xbf: NestedB[float] -reveal_type([xb, xbf]) -[builtins fixtures/list.pyi] From ad8f4b4ed40a4b7f525928be589b12465177cbe0 Mon Sep 17 00:00:00 2001 From: Ivan Levkivskyi Date: Sun, 10 Nov 2019 23:02:05 +0000 Subject: [PATCH 05/12] Fix some issues --- mypy/checkmember.py | 2 +- mypy/semanal_typeargs.py | 14 ++++++++++++-- mypy/typeanal.py | 11 +++++++---- 3 files changed, 20 insertions(+), 7 deletions(-) diff --git a/mypy/checkmember.py b/mypy/checkmember.py index 0f64b26b737f..ac932a86351d 100644 --- a/mypy/checkmember.py +++ b/mypy/checkmember.py @@ -500,7 +500,7 @@ def instance_alias_type(alias: TypeAlias, target = get_proper_type(alias.target) # type: Type assert isinstance(get_proper_type(target), Instance), "Must be called only with aliases to classes" - target = set_any_tvars(alias, alias.line, alias.column) + target = get_proper_type(set_any_tvars(alias, alias.line, alias.column)) assert isinstance(target, Instance) # type: ignore[misc] tp = type_object_type(target.type, builtin_type) return expand_type_by_instance(tp, target) diff --git a/mypy/semanal_typeargs.py b/mypy/semanal_typeargs.py index eceb5664d0e9..199456f29417 100644 --- a/mypy/semanal_typeargs.py +++ b/mypy/semanal_typeargs.py @@ -5,10 +5,12 @@ operations, including subtype checks. """ -from typing import List, Optional +from typing import List, Optional, Set from mypy.nodes import TypeInfo, Context, MypyFile, FuncItem, ClassDef, Block -from mypy.types import Type, Instance, TypeVarType, AnyType, get_proper_types +from mypy.types import ( + Type, Instance, TypeVarType, AnyType, get_proper_types, TypeAliasType, get_proper_type +) from mypy.mixedtraverser import MixedTraverserVisitor from mypy.subtypes import is_subtype from mypy.sametypes import is_same_type @@ -27,6 +29,7 @@ def __init__(self, errors: Errors, options: Options, is_typeshed_file: bool) -> self.scope = Scope() # Should we also analyze function definitions, or only module top-levels? self.recurse_into_functions = True + self.seen_aliases = set() # type: Set[TypeAliasType] def visit_mypy_file(self, o: MypyFile) -> None: self.errors.set_file(o.path, o.fullname(), scope=self.scope) @@ -48,6 +51,13 @@ def visit_block(self, o: Block) -> None: if not o.is_unreachable: super().visit_block(o) + def visit_type_alias_type(self, t: TypeAliasType) -> None: + super().visit_type_alias_type(t) + if t in self.seen_aliases: + return + self.seen_aliases.add(t) + get_proper_type(t).accept(self) + def visit_instance(self, t: Instance) -> None: # Type argument counts were checked in the main semantic analyzer pass. We assume # that the counts are correct here. diff --git a/mypy/typeanal.py b/mypy/typeanal.py index a33d3b61af34..aa67c5bbd3fd 100644 --- a/mypy/typeanal.py +++ b/mypy/typeanal.py @@ -1007,11 +1007,14 @@ def expand_type_alias(node: TypeAlias, args: List[Type], fail('Bad number of arguments for type alias, expected: %s, given: %s' % (exp_len, act_len), ctx) return set_any_tvars(node, ctx.line, ctx.column, from_error=True) - typ = TypeAliasType(node, args, ctx.line, ctx.column) # type: Type + typ = TypeAliasType(node, args, ctx.line, ctx.column) + assert typ.alias is not None # HACK: Implement FlexibleAlias[T, typ] by expanding it to typ here. - if (isinstance(typ, Instance) # type: ignore - and typ.type.fullname() == 'mypy_extensions.FlexibleAlias'): - typ = typ.args[-1] + if (isinstance(typ.alias.target, Instance) # type: ignore + and typ.alias.target.type.fullname() == 'mypy_extensions.FlexibleAlias'): + exp = get_proper_type(typ) + assert isinstance(exp, Instance) + return exp.args[-1] return typ From 88f8619fba51ee23f428aeb37583aa7dc7669d44 Mon Sep 17 00:00:00 2001 From: Ivan Levkivskyi Date: Sun, 10 Nov 2019 23:29:31 +0000 Subject: [PATCH 06/12] Make alias deps more like instance deps --- mypy/server/deps.py | 4 +++- test-data/unit/deps-types.test | 14 +++++++------- 2 files changed, 10 insertions(+), 8 deletions(-) diff --git a/mypy/server/deps.py b/mypy/server/deps.py index 2f370d6587c6..78944a85b9cb 100644 --- a/mypy/server/deps.py +++ b/mypy/server/deps.py @@ -880,9 +880,11 @@ def visit_instance(self, typ: Instance) -> List[str]: def visit_type_alias_type(self, typ: TypeAliasType) -> List[str]: assert typ.alias is not None - triggers = [] + trigger = make_trigger(typ.alias.fullname()) + triggers = [trigger] for arg in typ.args: triggers.extend(self.get_type_triggers(arg)) + # TODO: Add guard for infinite recursion here. triggers.extend(self.get_type_triggers(typ.alias.target)) return triggers diff --git a/test-data/unit/deps-types.test b/test-data/unit/deps-types.test index 869824f936c3..a5f5ce82fed1 100644 --- a/test-data/unit/deps-types.test +++ b/test-data/unit/deps-types.test @@ -444,7 +444,7 @@ class D(Generic[T, U]): pass class I: pass class S: pass [out] - -> m.f + -> , m.f -> m -> m -> , m, m.f @@ -467,7 +467,7 @@ class D(Generic[T, U]): pass class I: pass class S: pass [out] - -> m.f + -> , m.f -> m -> , m.f -> , m.f @@ -622,7 +622,7 @@ class I: pass class S: pass [out] -> m - -> m + -> , m -> m -> m -> m @@ -648,7 +648,7 @@ class D(Generic[T, U]): pass class I: pass class S: pass [out] - -> m + -> , m -> m -> m -> m @@ -675,8 +675,8 @@ class D(Generic[T, U]): pass class I: pass class S: pass [out] - -> m - -> m.f + -> , m + -> , m.f -> m -> m -> , m, m.f @@ -753,7 +753,7 @@ class D(Generic[T, U]): pass class I: pass class S: pass [out] - -> m + -> , m -> m -> m.C -> m From 0da1fb24f9f69fec69af1a18e2cfaf78616ee09f Mon Sep 17 00:00:00 2001 From: Ivan Levkivskyi Date: Sun, 10 Nov 2019 23:44:16 +0000 Subject: [PATCH 07/12] Make alias deps more like instance deps --- test-data/unit/deps-expressions.test | 2 +- test-data/unit/deps-types.test | 16 ++++++++-------- 2 files changed, 9 insertions(+), 9 deletions(-) diff --git a/test-data/unit/deps-expressions.test b/test-data/unit/deps-expressions.test index ec75ff0bff89..4c4d8af6d308 100644 --- a/test-data/unit/deps-expressions.test +++ b/test-data/unit/deps-expressions.test @@ -461,6 +461,6 @@ def f(x: Alias) -> None: pass def g() -> Literal[1]: return b [out] - -> m, m.f + -> , m, m.f -> m -> m, m.g diff --git a/test-data/unit/deps-types.test b/test-data/unit/deps-types.test index a5f5ce82fed1..36cffe2ec306 100644 --- a/test-data/unit/deps-types.test +++ b/test-data/unit/deps-types.test @@ -621,8 +621,8 @@ class D(Generic[T, U]): pass class I: pass class S: pass [out] - -> m - -> , m + -> , m + -> m -> m -> m -> m @@ -648,9 +648,9 @@ class D(Generic[T, U]): pass class I: pass class S: pass [out] - -> , m + -> m -> m - -> m + -> , m -> m -> m, a -> m, a @@ -675,7 +675,7 @@ class D(Generic[T, U]): pass class I: pass class S: pass [out] - -> , m + -> , m, m.f -> , m.f -> m -> m @@ -702,8 +702,8 @@ class D(Generic[T, U]): pass class I: pass class S: pass [out] - -> m.f - -> m + -> , m.f + -> , m, m.f -> m -> m, a -> m, a @@ -728,7 +728,7 @@ class D(Generic[T, U]): pass class I: pass class S: pass [out] - -> m.f + -> , m.f -> m.f -> m -> m From 9cdac2a08048a1962b587a317099359ad83091b3 Mon Sep 17 00:00:00 2001 From: Ivan Levkivskyi Date: Mon, 11 Nov 2019 16:20:25 +0000 Subject: [PATCH 08/12] Extend one TODO --- mypy/server/deps.py | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/mypy/server/deps.py b/mypy/server/deps.py index 78944a85b9cb..0ff256b57b98 100644 --- a/mypy/server/deps.py +++ b/mypy/server/deps.py @@ -884,7 +884,9 @@ def visit_type_alias_type(self, typ: TypeAliasType) -> List[str]: triggers = [trigger] for arg in typ.args: triggers.extend(self.get_type_triggers(arg)) - # TODO: Add guard for infinite recursion here. + # TODO: Add guard for infinite recursion here. Moreover, now that type aliases + # are its own kind of types we can simplify the logic to rely on intermediate + # dependencies (like for instance types). triggers.extend(self.get_type_triggers(typ.alias.target)) return triggers From 407bb5933db77b36a49b0c70938f769419fc0491 Mon Sep 17 00:00:00 2001 From: Ivan Levkivskyi Date: Mon, 11 Nov 2019 16:23:35 +0000 Subject: [PATCH 09/12] Remove redundant parentheses --- mypy/semanal.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/mypy/semanal.py b/mypy/semanal.py index 6f7192ee2650..765f3f7f9779 100644 --- a/mypy/semanal.py +++ b/mypy/semanal.py @@ -3648,8 +3648,8 @@ def visit_index_expr(self, expr: IndexExpr) -> None: and isinstance(base.node, TypeInfo) and not base.node.is_generic()): expr.index.accept(self) - elif (((isinstance(base, RefExpr) and isinstance(base.node, TypeAlias)) - or refers_to_class_or_function(base))): + elif ((isinstance(base, RefExpr) and isinstance(base.node, TypeAlias)) + or refers_to_class_or_function(base)): # We need to do full processing on every iteration, since some type # arguments may contain placeholder types. self.analyze_type_application(expr) From 26e92cf00ee82fa61c63697e1940933fc3e9d834 Mon Sep 17 00:00:00 2001 From: Ivan Levkivskyi Date: Mon, 11 Nov 2019 16:27:06 +0000 Subject: [PATCH 10/12] Undo unneeded test change --- test-data/unit/check-namedtuple.test | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/test-data/unit/check-namedtuple.test b/test-data/unit/check-namedtuple.test index 143a84496ae9..ab5c7bd0b6bb 100644 --- a/test-data/unit/check-namedtuple.test +++ b/test-data/unit/check-namedtuple.test @@ -664,7 +664,7 @@ class B(NamedTuple): x: Tuple[A, int] # E: Cannot resolve name "A" (possible cyclic definition) y: int -A = NamedTuple('_A', [ +A = NamedTuple('A', [ ('x', str), ('y', 'B'), ]) From 792d7da57981cd8ebc036fb71adc52d7b50abd2c Mon Sep 17 00:00:00 2001 From: Ivan Levkivskyi Date: Mon, 11 Nov 2019 17:54:12 +0000 Subject: [PATCH 11/12] Fix self-check --- mypy/checkmember.py | 2 +- mypy/subtypes.py | 3 ++- 2 files changed, 3 insertions(+), 2 deletions(-) diff --git a/mypy/checkmember.py b/mypy/checkmember.py index ac932a86351d..efe995d2e314 100644 --- a/mypy/checkmember.py +++ b/mypy/checkmember.py @@ -501,7 +501,7 @@ def instance_alias_type(alias: TypeAlias, assert isinstance(get_proper_type(target), Instance), "Must be called only with aliases to classes" target = get_proper_type(set_any_tvars(alias, alias.line, alias.column)) - assert isinstance(target, Instance) # type: ignore[misc] + assert isinstance(target, Instance) tp = type_object_type(target.type, builtin_type) return expand_type_by_instance(tp, target) diff --git a/mypy/subtypes.py b/mypy/subtypes.py index fa3d3f9dd3a1..4997f73dca68 100644 --- a/mypy/subtypes.py +++ b/mypy/subtypes.py @@ -1086,7 +1086,8 @@ def restrict_subtype_away(t: Type, s: Type, *, ignore_promotions: bool = False) s = get_proper_type(s) if isinstance(t, UnionType): - new_items = [item for item in t.relevant_items() + new_items = [restrict_subtype_away(item, s, ignore_promotions=ignore_promotions) + for item in t.relevant_items() if (isinstance(get_proper_type(item), AnyType) or not covers_at_runtime(item, s, ignore_promotions))] return UnionType.make_union(new_items) From 1c1ea2bd3d99481e790a6efdb04217a0cf848b1f Mon Sep 17 00:00:00 2001 From: Ivan Levkivskyi Date: Wed, 13 Nov 2019 15:58:40 +0000 Subject: [PATCH 12/12] Address CR --- mypy/semanal_typeargs.py | 5 +++++ mypy/type_visitor.py | 11 +++++++++-- mypy/typeanal.py | 2 ++ mypy/types.py | 5 +++-- 4 files changed, 19 insertions(+), 4 deletions(-) diff --git a/mypy/semanal_typeargs.py b/mypy/semanal_typeargs.py index 199456f29417..73a355d47193 100644 --- a/mypy/semanal_typeargs.py +++ b/mypy/semanal_typeargs.py @@ -29,6 +29,8 @@ def __init__(self, errors: Errors, options: Options, is_typeshed_file: bool) -> self.scope = Scope() # Should we also analyze function definitions, or only module top-levels? self.recurse_into_functions = True + # Keep track of the type aliases already visited. This is needed to avoid + # infinite recursion on types like A = Union[int, List[A]]. self.seen_aliases = set() # type: Set[TypeAliasType] def visit_mypy_file(self, o: MypyFile) -> None: @@ -54,6 +56,9 @@ def visit_block(self, o: Block) -> None: def visit_type_alias_type(self, t: TypeAliasType) -> None: super().visit_type_alias_type(t) if t in self.seen_aliases: + # Avoid infinite recursion on recursive type aliases. + # Note: it is fine to skip the aliases we have already seen in non-recursive types, + # since errors there have already already reported. return self.seen_aliases.add(t) get_proper_type(t).accept(self) diff --git a/mypy/type_visitor.py b/mypy/type_visitor.py index 89e76b3a3325..da630e1f176c 100644 --- a/mypy/type_visitor.py +++ b/mypy/type_visitor.py @@ -246,13 +246,20 @@ def visit_type_alias_type(self, t: TypeAliasType) -> Type: class TypeQuery(SyntheticTypeVisitor[T]): """Visitor for performing queries of types. - strategy is used to combine results for a series of types + strategy is used to combine results for a series of types, + common use cases involve a boolean query using `any` or `all`. - Common use cases involve a boolean query using `any` or `all` + Note: this visitor keeps an internal state (tracks type aliases to avoid + recursion), so it should *never* be re-used for querying different types, + create a new visitor instance instead. + + # TODO: check that we don't have existing violations of this rule. """ def __init__(self, strategy: Callable[[Iterable[T]], T]) -> None: self.strategy = strategy + # Keep track of the type aliases already visited. This is needed to avoid + # infinite recursion on types like A = Union[int, List[A]]. self.seen_aliases = set() # type: Set[TypeAliasType] def visit_unbound_type(self, t: UnboundType) -> T: diff --git a/mypy/typeanal.py b/mypy/typeanal.py index aa67c5bbd3fd..c4c05c21eea8 100644 --- a/mypy/typeanal.py +++ b/mypy/typeanal.py @@ -994,6 +994,8 @@ def expand_type_alias(node: TypeAlias, args: List[Type], if exp_len == 0 and act_len == 0: if no_args: assert isinstance(node.target, Instance) # type: ignore[misc] + # Note: this is the only case where we use an eager expansion. See more info about + # no_args aliases like L = List in the docstring for TypeAlias class. return Instance(node.target.type, [], line=ctx.line, column=ctx.column) return TypeAliasType(node, [], line=ctx.line, column=ctx.column) if (exp_len == 0 and act_len > 0 diff --git a/mypy/types.py b/mypy/types.py index 85a788611f0e..e94e2529456f 100644 --- a/mypy/types.py +++ b/mypy/types.py @@ -181,8 +181,9 @@ def _expand_once(self) -> Type: """ assert self.alias is not None if self.alias.no_args: - assert isinstance(self.alias.target, ProperType) - assert isinstance(self.alias.target, Instance) + # We know that no_args=True aliases like L = List must have an instance + # as their target. + assert isinstance(self.alias.target, Instance) # type: ignore[misc] return self.alias.target.copy_modified(args=self.args) return replace_alias_tvars(self.alias.target, self.alias.alias_tvars, self.args, self.line, self.column)