From 4c9b023b4926d3a2cfd543518df21f1010de7eff Mon Sep 17 00:00:00 2001 From: Ivan Levkivskyi Date: Mon, 25 Sep 2017 01:37:54 +0200 Subject: [PATCH 1/4] Allow subscripted aliases at function scope; prohibit bound typevar reuse --- mypy/checkexpr.py | 2 +- mypy/semanal.py | 15 +++++---- mypy/typeanal.py | 16 ++++++--- test-data/unit/check-classes.test | 8 ++--- test-data/unit/check-isinstance.test | 12 +++---- test-data/unit/check-type-aliases.test | 42 ++++++++++++++++++++++++ test-data/unit/check-typevar-values.test | 9 +++-- 7 files changed, 77 insertions(+), 27 deletions(-) diff --git a/mypy/checkexpr.py b/mypy/checkexpr.py index 36c89b3ceda4..898dfe568768 100644 --- a/mypy/checkexpr.py +++ b/mypy/checkexpr.py @@ -635,7 +635,7 @@ def analyze_type_type_callee(self, item: Type, context: Context) -> Type: res = type_object_type(item.type, self.named_type) if isinstance(res, CallableType): res = res.copy_modified(from_type_type=True) - return res + return expand_type_by_instance(res, item) if isinstance(item, UnionType): return UnionType([self.analyze_type_type_callee(item, context) for item in item.relevant_items()], item.line) diff --git a/mypy/semanal.py b/mypy/semanal.py index d0b301193697..e54d1e8c5225 100644 --- a/mypy/semanal.py +++ b/mypy/semanal.py @@ -1710,11 +1710,10 @@ def alias_fallback(self, tp: Type) -> Instance: return Instance(fb_info, []) def analyze_alias(self, rvalue: Expression, - allow_unnormalized: bool) -> Tuple[Optional[Type], List[str]]: + warn_bound_tvar: bool = False) -> Tuple[Optional[Type], List[str]]: """Check if 'rvalue' represents a valid type allowed for aliasing (e.g. not a type variable). If yes, return the corresponding type and a list of qualified type variable names for generic aliases. - If 'allow_unnormalized' is True, allow types like builtins.list[T]. """ res = analyze_type_alias(rvalue, self.lookup_qualified, @@ -1724,7 +1723,8 @@ def analyze_alias(self, rvalue: Expression, self.plugin, self.options, self.is_typeshed_stub_file, - allow_unnormalized=True) + allow_unnormalized=True, + warn_bound_tvar=warn_bound_tvar) if res: alias_tvars = [name for (name, _) in res.accept(TypeVariableQuery(self.lookup_qualified, self.tvar_scope))] @@ -1743,11 +1743,12 @@ def check_and_set_up_type_alias(self, s: AssignmentStmt) -> None: lvalue = s.lvalues[0] if not isinstance(lvalue, NameExpr): return - if (len(s.lvalues) == 1 and not self.is_func_scope() and - not (self.type and isinstance(s.rvalue, NameExpr) and lvalue.is_def) + if (len(s.lvalues) == 1 and + not ((self.type or self.is_func_scope()) and + isinstance(s.rvalue, NameExpr) and lvalue.is_def) and not s.type): rvalue = s.rvalue - res, alias_tvars = self.analyze_alias(rvalue, allow_unnormalized=True) + res, alias_tvars = self.analyze_alias(rvalue, warn_bound_tvar=True) if not res: return node = self.lookup(lvalue.name, lvalue) @@ -3339,7 +3340,7 @@ def visit_index_expr(self, expr: IndexExpr) -> None: elif isinstance(expr.base, RefExpr) and expr.base.kind == TYPE_ALIAS: # Special form -- subscripting a generic type alias. # Perform the type substitution and create a new alias. - res, alias_tvars = self.analyze_alias(expr, allow_unnormalized=self.is_stub_file) + res, alias_tvars = self.analyze_alias(expr) expr.analyzed = TypeAliasExpr(res, alias_tvars, fallback=self.alias_fallback(res), in_runtime=True) expr.analyzed.line = expr.line diff --git a/mypy/typeanal.py b/mypy/typeanal.py index 1a7f66d0ae22..3cf0b02617a4 100644 --- a/mypy/typeanal.py +++ b/mypy/typeanal.py @@ -59,7 +59,8 @@ def analyze_type_alias(node: Expression, plugin: Plugin, options: Options, is_typeshed_stub: bool, - allow_unnormalized: bool = False) -> Optional[Type]: + allow_unnormalized: bool = False, + warn_bound_tvar: bool = False) -> Optional[Type]: """Return type if node is valid as a type alias rvalue. Return None otherwise. 'node' must have been semantically analyzed. @@ -112,7 +113,8 @@ def analyze_type_alias(node: Expression, fail_func('Invalid type alias', node) return None analyzer = TypeAnalyser(lookup_func, lookup_fqn_func, tvar_scope, fail_func, plugin, options, - is_typeshed_stub, aliasing=True, allow_unnormalized=allow_unnormalized) + is_typeshed_stub, aliasing=True, allow_unnormalized=allow_unnormalized, + warn_bound_tvar=warn_bound_tvar) return type.accept(analyzer) @@ -140,7 +142,8 @@ def __init__(self, is_typeshed_stub: bool, *, aliasing: bool = False, allow_tuple_literal: bool = False, - allow_unnormalized: bool = False) -> None: + allow_unnormalized: bool = False, + warn_bound_tvar: bool = False) -> None: self.lookup = lookup_func self.lookup_fqn_func = lookup_fqn_func self.fail_func = fail_func @@ -153,6 +156,7 @@ def __init__(self, self.plugin = plugin self.options = options self.is_typeshed_stub = is_typeshed_stub + self.warn_bound_tvar = warn_bound_tvar def visit_unbound_type(self, t: UnboundType) -> Type: if t.optional: @@ -175,7 +179,11 @@ def visit_unbound_type(self, t: UnboundType) -> Type: not sym.normalized and not self.allow_unnormalized): self.fail(no_subscript_builtin_alias(fullname), t) tvar_def = self.tvar_scope.get_binding(sym) - if sym.kind == TVAR and tvar_def is not None: + if self.warn_bound_tvar and sym.kind == TVAR and tvar_def is not None: + self.fail('Can\'t use bound type variable "{}"' + ' to define generic alias'.format(t.name), t) + return AnyType(TypeOfAny.from_error) + elif sym.kind == TVAR and tvar_def is not None: if len(t.args) > 0: self.fail('Type variable "{}" used with arguments'.format( t.name), t) diff --git a/test-data/unit/check-classes.test b/test-data/unit/check-classes.test index 2ff0b8d84d78..1ae37e558e55 100644 --- a/test-data/unit/check-classes.test +++ b/test-data/unit/check-classes.test @@ -2119,17 +2119,17 @@ reveal_type(C().aa) # E: Revealed type is '__main__.A' [out] [case testClassValuedAttributesGeneric] -from typing import Generic, TypeVar +from typing import Generic, TypeVar, Type T = TypeVar('T') class A(Generic[T]): def __init__(self, x: T) -> None: self.x = x class B(Generic[T]): - a = A[T] + a: Type[A[T]] -reveal_type(B[int]().a) # E: Revealed type is 'def (x: builtins.int*) -> __main__.A[builtins.int*]' -B[int]().a('hi') # E: Argument 1 has incompatible type "str"; expected "int" +reveal_type(B[int]().a) # E: Revealed type is 'Type[__main__.A[builtins.int*]]' +B[int]().a('hi') # E: Argument 1 to "A" has incompatible type "str"; expected "int" class C(Generic[T]): a = A diff --git a/test-data/unit/check-isinstance.test b/test-data/unit/check-isinstance.test index 60eef4eb4555..7f8f17ff2652 100644 --- a/test-data/unit/check-isinstance.test +++ b/test-data/unit/check-isinstance.test @@ -1453,10 +1453,10 @@ def f(x: Union[Type[int], Type[str], Type[List]]) -> None: reveal_type(x()) # E: Revealed type is 'Union[builtins.int, builtins.str, builtins.list[]]' if issubclass(x, (str, (list,))): reveal_type(x) # E: Revealed type is 'Union[Type[builtins.str], Type[builtins.list[Any]]]' - reveal_type(x()) # E: Revealed type is 'Union[builtins.str, builtins.list[]]' + reveal_type(x()) # E: Revealed type is 'Union[builtins.str, builtins.list[Any]]' x()[1] reveal_type(x) # E: Revealed type is 'Union[Type[builtins.int], Type[builtins.str], Type[builtins.list[Any]]]' - reveal_type(x()) # E: Revealed type is 'Union[builtins.int, builtins.str, builtins.list[]]' + reveal_type(x()) # E: Revealed type is 'Union[builtins.int, builtins.str, builtins.list[Any]]' [builtins fixtures/isinstancelist.pyi] [case testIssubclasDestructuringUnions2] @@ -1475,10 +1475,10 @@ def f(x: Type[Union[int, str, List]]) -> None: reveal_type(x()) # E: Revealed type is 'Union[builtins.int, builtins.str, builtins.list[]]' if issubclass(x, (str, (list,))): reveal_type(x) # E: Revealed type is 'Union[Type[builtins.str], Type[builtins.list[Any]]]' - reveal_type(x()) # E: Revealed type is 'Union[builtins.str, builtins.list[]]' + reveal_type(x()) # E: Revealed type is 'Union[builtins.str, builtins.list[Any]]' x()[1] reveal_type(x) # E: Revealed type is 'Union[Type[builtins.int], Type[builtins.str], Type[builtins.list[Any]]]' - reveal_type(x()) # E: Revealed type is 'Union[builtins.int, builtins.str, builtins.list[]]' + reveal_type(x()) # E: Revealed type is 'Union[builtins.int, builtins.str, builtins.list[Any]]' [builtins fixtures/isinstancelist.pyi] [case testIssubclasDestructuringUnions3] @@ -1499,10 +1499,10 @@ def f(x: Type[Union[int, str, List]]) -> None: reveal_type(x()) # E: Revealed type is 'Union[builtins.int, builtins.str, builtins.list[]]' if issubclass(x, (str, (list,))): reveal_type(x) # E: Revealed type is 'Union[Type[builtins.str], Type[builtins.list[Any]]]' - reveal_type(x()) # E: Revealed type is 'Union[builtins.str, builtins.list[]]' + reveal_type(x()) # E: Revealed type is 'Union[builtins.str, builtins.list[Any]]' x()[1] reveal_type(x) # E: Revealed type is 'Union[Type[builtins.int], Type[builtins.str], Type[builtins.list[Any]]]' - reveal_type(x()) # E: Revealed type is 'Union[builtins.int, builtins.str, builtins.list[]]' + reveal_type(x()) # E: Revealed type is 'Union[builtins.int, builtins.str, builtins.list[Any]]' [builtins fixtures/isinstancelist.pyi] [case testIssubclass] diff --git a/test-data/unit/check-type-aliases.test b/test-data/unit/check-type-aliases.test index 2179eecd9c21..f05f887c64b2 100644 --- a/test-data/unit/check-type-aliases.test +++ b/test-data/unit/check-type-aliases.test @@ -96,6 +96,48 @@ GenAlias = Sequence[T] def fun(x: Alias) -> GenAlias[int]: pass [out] +[case testCorrectQualifiedAliasesAlsoInFunctions] +from typing import TypeVar, Generic +T = TypeVar('T') +S = TypeVar('S') + +class X(Generic[T]): + A = X[S] + def f(self) -> X[T]: + pass + + a: X[T] + b: A = a + c: A[T] = a + d: A[int] = a # E: Incompatible types in assignment (expression has type "X[T]", variable has type "X[int]") + + def g(self) -> None: + a: X[T] + b: X.A = a + c: X.A[T] = a + d: X.A[int] = a # E: Incompatible types in assignment (expression has type "X[T]", variable has type "X[int]") + +def g(arg: X[int]) -> None: + p: X[int] = arg.f() + q: X.A = arg.f() + r: X.A[str] = arg.f() # E: Incompatible types in assignment (expression has type "X[int]", variable has type "X[str]") +[out] + +[case testProhibitBoundTypeVariableReuseForAliases] +from typing import TypeVar, Generic, List +T = TypeVar('T') +class C(Generic[T]): + A = List[T] # E: Can't use bound type variable "T" to define generic alias + +x: C.A +reveal_type(x) # E: Revealed type is 'builtins.list[Any]' + +def f(x: T) -> T: + A = List[T] # E: Can't use bound type variable "T" to define generic alias + return x +[builtins fixtures/list.pyi] +[out] + [case testTypeAliasInBuiltins] def f(x: bytes): pass bytes diff --git a/test-data/unit/check-typevar-values.test b/test-data/unit/check-typevar-values.test index 1dc5d143790e..eab5e3f094a0 100644 --- a/test-data/unit/check-typevar-values.test +++ b/test-data/unit/check-typevar-values.test @@ -556,16 +556,15 @@ def outer(x: T) -> T: [case testClassMemberTypeVarInFunctionBody] from typing import TypeVar, List +S = TypeVar('S') class C: T = TypeVar('T', bound=int) def f(self, x: T) -> T: - L = List[C.T] # this creates a variable, not an alias - reveal_type(L) # E: Revealed type is 'Overload(def () -> builtins.list[T`-1], def (x: typing.Iterable[T`-1]) -> builtins.list[T`-1])' - y: C.T = x - L().append(x) + L = List[S] + y: L[C.T] = [x] C.T # E: Type variable "C.T" cannot be used as an expression A = C.T # E: Type variable "C.T" cannot be used as an expression - return L()[0] + return y[0] [builtins fixtures/list.pyi] From a464a53bff7640c5257dabe8e2dd6bce8c68ea58 Mon Sep 17 00:00:00 2001 From: Ivan Levkivskyi Date: Mon, 25 Sep 2017 01:55:07 +0200 Subject: [PATCH 2/4] Improve one test --- test-data/unit/check-classes.test | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/test-data/unit/check-classes.test b/test-data/unit/check-classes.test index 1ae37e558e55..506b8ecb3816 100644 --- a/test-data/unit/check-classes.test +++ b/test-data/unit/check-classes.test @@ -2126,7 +2126,7 @@ class A(Generic[T]): def __init__(self, x: T) -> None: self.x = x class B(Generic[T]): - a: Type[A[T]] + a: Type[A[T]] = A reveal_type(B[int]().a) # E: Revealed type is 'Type[__main__.A[builtins.int*]]' B[int]().a('hi') # E: Argument 1 to "A" has incompatible type "str"; expected "int" From 358ca9660e05f1299e46dd5c4712db492930b802 Mon Sep 17 00:00:00 2001 From: Ivan Levkivskyi Date: Fri, 6 Oct 2017 18:31:21 +0200 Subject: [PATCH 3/4] Update tests after merge --- test-data/unit/check-isinstance.test | 14 +++++++------- 1 file changed, 7 insertions(+), 7 deletions(-) diff --git a/test-data/unit/check-isinstance.test b/test-data/unit/check-isinstance.test index 2635ff219f44..a92204ca5878 100644 --- a/test-data/unit/check-isinstance.test +++ b/test-data/unit/check-isinstance.test @@ -1447,10 +1447,10 @@ def f(x: Union[Type[int], Type[str], Type[List]]) -> None: x()[1] # E: Value of type "Union[int, str]" is not indexable else: reveal_type(x) # E: Revealed type is 'Type[builtins.list[Any]]' - reveal_type(x()) # E: Revealed type is 'builtins.list[]' + reveal_type(x()) # E: Revealed type is 'builtins.list[Any]' x()[1] reveal_type(x) # E: Revealed type is 'Union[Type[builtins.int], Type[builtins.str], Type[builtins.list[Any]]]' - reveal_type(x()) # E: Revealed type is 'Union[builtins.int, builtins.str, builtins.list[]]' + reveal_type(x()) # E: Revealed type is 'Union[builtins.int, builtins.str, builtins.list[Any]]' if issubclass(x, (str, (list,))): reveal_type(x) # E: Revealed type is 'Union[Type[builtins.str], Type[builtins.list[Any]]]' reveal_type(x()) # E: Revealed type is 'Union[builtins.str, builtins.list[Any]]' @@ -1469,10 +1469,10 @@ def f(x: Type[Union[int, str, List]]) -> None: x()[1] # E: Value of type "Union[int, str]" is not indexable else: reveal_type(x) # E: Revealed type is 'Type[builtins.list[Any]]' - reveal_type(x()) # E: Revealed type is 'builtins.list[]' + reveal_type(x()) # E: Revealed type is 'builtins.list[Any]' x()[1] reveal_type(x) # E: Revealed type is 'Union[Type[builtins.int], Type[builtins.str], Type[builtins.list[Any]]]' - reveal_type(x()) # E: Revealed type is 'Union[builtins.int, builtins.str, builtins.list[]]' + reveal_type(x()) # E: Revealed type is 'Union[builtins.int, builtins.str, builtins.list[Any]]' if issubclass(x, (str, (list,))): reveal_type(x) # E: Revealed type is 'Union[Type[builtins.str], Type[builtins.list[Any]]]' reveal_type(x()) # E: Revealed type is 'Union[builtins.str, builtins.list[Any]]' @@ -1486,17 +1486,17 @@ from typing import Union, List, Tuple, Dict, Type def f(x: Type[Union[int, str, List]]) -> None: reveal_type(x) # E: Revealed type is 'Union[Type[builtins.int], Type[builtins.str], Type[builtins.list[Any]]]' - reveal_type(x()) # E: Revealed type is 'Union[builtins.int, builtins.str, builtins.list[]]' + reveal_type(x()) # E: Revealed type is 'Union[builtins.int, builtins.str, builtins.list[Any]]' if issubclass(x, (str, (int,))): reveal_type(x) # E: Revealed type is 'Union[Type[builtins.int], Type[builtins.str]]' reveal_type(x()) # E: Revealed type is 'Union[builtins.int, builtins.str]' x()[1] # E: Value of type "Union[int, str]" is not indexable else: reveal_type(x) # E: Revealed type is 'Type[builtins.list[Any]]' - reveal_type(x()) # E: Revealed type is 'builtins.list[]' + reveal_type(x()) # E: Revealed type is 'builtins.list[Any]' x()[1] reveal_type(x) # E: Revealed type is 'Union[Type[builtins.int], Type[builtins.str], Type[builtins.list[Any]]]' - reveal_type(x()) # E: Revealed type is 'Union[builtins.int, builtins.str, builtins.list[]]' + reveal_type(x()) # E: Revealed type is 'Union[builtins.int, builtins.str, builtins.list[Any]]' if issubclass(x, (str, (list,))): reveal_type(x) # E: Revealed type is 'Union[Type[builtins.str], Type[builtins.list[Any]]]' reveal_type(x()) # E: Revealed type is 'Union[builtins.str, builtins.list[Any]]' From 5cbe056192df20ee9ca5ae7cd535e1ebaf042b36 Mon Sep 17 00:00:00 2001 From: Ivan Levkivskyi Date: Mon, 9 Oct 2017 20:09:50 +0200 Subject: [PATCH 4/4] Address CR --- mypy/semanal.py | 97 +++++++++++++++++++++++++++---------------------- 1 file changed, 54 insertions(+), 43 deletions(-) diff --git a/mypy/semanal.py b/mypy/semanal.py index c6498b959b84..90731bf60e05 100644 --- a/mypy/semanal.py +++ b/mypy/semanal.py @@ -1765,51 +1765,62 @@ def check_and_set_up_type_alias(self, s: AssignmentStmt) -> None: For subscripted (including generic) aliases the resulting types are stored in rvalue.analyzed. """ - # Type aliases are created only at module scope and class scope (for subscripted types), - # at function scope assignments always create local variables with type object types. lvalue = s.lvalues[0] - if not isinstance(lvalue, NameExpr): + if len(s.lvalues) > 1 or not isinstance(lvalue, NameExpr): + # First rule: Only simple assignments like Alias = ... create aliases. return - if (len(s.lvalues) == 1 and - not ((self.type or self.is_func_scope()) and - isinstance(s.rvalue, NameExpr) and lvalue.is_def) - and not s.type): - rvalue = s.rvalue - res, alias_tvars = self.analyze_alias(rvalue, warn_bound_tvar=True) - if not res: - return - node = self.lookup(lvalue.name, lvalue) - if not lvalue.is_def: - # Only a definition can create a type alias, not regular assignment. - if node and node.kind == TYPE_ALIAS or isinstance(node.node, TypeInfo): - self.fail('Cannot assign multiple types to name "{}"' - ' without an explicit "Type[...]" annotation' - .format(lvalue.name), lvalue) - return - check_for_explicit_any(res, self.options, self.is_typeshed_stub_file, self.msg, - 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 isinstance(res, Instance) and not res.args and isinstance(rvalue, RefExpr): - # For simple (on-generic) aliases we use aliasing TypeInfo's - # to allow using them in runtime context where it makes sense. - node.node = res.type - if isinstance(rvalue, RefExpr): - sym = self.lookup_type_node(rvalue) - if sym: - node.normalized = sym.normalized - return - node.kind = TYPE_ALIAS - node.type_override = res - node.alias_tvars = alias_tvars - if isinstance(rvalue, (IndexExpr, CallExpr)): - # We only need this for subscripted aliases, since simple aliases - # are already processed using aliasing TypeInfo's above. - rvalue.analyzed = TypeAliasExpr(res, node.alias_tvars, - fallback=self.alias_fallback(res)) - rvalue.analyzed.line = rvalue.line - rvalue.analyzed.column = rvalue.column + if s.type: + # Second rule: Explicit type (cls: Type[A] = A) always creates variable, not alias. + return + non_global_scope = self.type or self.is_func_scope() + if isinstance(s.rvalue, NameExpr) and non_global_scope and lvalue.is_def: + # Third rule: Non-subscripted right hand side creates a variable + # at class and function scopes. For example: + # + # class Model: + # ... + # class C: + # model = Model # this is automatically a variable with type 'Type[Model]' + # + # without this rule, this typical use case will require a lot of explicit + # annotations (see the second rule). + return + rvalue = s.rvalue + res, alias_tvars = self.analyze_alias(rvalue, warn_bound_tvar=True) + if not res: + return + node = self.lookup(lvalue.name, lvalue) + if not lvalue.is_def: + # Type aliases can't be re-defined. + if node and (node.kind == TYPE_ALIAS or isinstance(node.node, TypeInfo)): + self.fail('Cannot assign multiple types to name "{}"' + ' without an explicit "Type[...]" annotation' + .format(lvalue.name), lvalue) + return + check_for_explicit_any(res, self.options, self.is_typeshed_stub_file, self.msg, + 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 isinstance(res, Instance) and not res.args and isinstance(rvalue, RefExpr): + # For simple (on-generic) aliases we use aliasing TypeInfo's + # to allow using them in runtime context where it makes sense. + node.node = res.type + if isinstance(rvalue, RefExpr): + sym = self.lookup_type_node(rvalue) + if sym: + node.normalized = sym.normalized + return + node.kind = TYPE_ALIAS + node.type_override = res + node.alias_tvars = alias_tvars + if isinstance(rvalue, (IndexExpr, CallExpr)): + # We only need this for subscripted aliases, since simple aliases + # are already processed using aliasing TypeInfo's above. + rvalue.analyzed = TypeAliasExpr(res, node.alias_tvars, + fallback=self.alias_fallback(res)) + rvalue.analyzed.line = rvalue.line + rvalue.analyzed.column = rvalue.column def analyze_lvalue(self, lval: Lvalue, nested: bool = False, add_global: bool = False,