-
-
Notifications
You must be signed in to change notification settings - Fork 2.9k
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Generic type aliases #2378
Generic type aliases #2378
Changes from 13 commits
5dc0090
f6d862b
b0790d4
7ddd6ab
633e3d1
07baa51
56b7ae7
cbcb2c0
5c92b12
13e1fff
ee1c25c
2edcf48
64fc96e
8c64111
c106204
feb9413
d402a68
8cea156
382c53e
863c576
544ff66
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -426,9 +426,65 @@ assigning the type to a variable: | |
def f() -> AliasType: | ||
... | ||
|
||
A type alias does not create a new type. It's just a shorthand notation | ||
for another type -- it's equivalent to the target type. Type aliases | ||
can be imported from modules like any names. | ||
Type aliases can be generic, in this case they could be used in two variants: | ||
Subscribed aliases are equivalent to original types with substituted type variables, | ||
number of type arguments must match the number of free type variables | ||
in generic type alias. Unsubscribed aliases are treated as original types with free | ||
vaiables replacec with ``Any``. Examples (following `PEP 484 | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Typoooos |
||
<https://www.python.org/dev/peps/pep-0484/#type-aliases>`_): | ||
|
||
.. code-block:: python | ||
|
||
from typing import TypeVar, Iterable, Tuple, Union, Callable | ||
T = TypeVar('T', int, float, complex) | ||
|
||
TInt = Tuple[T, int] | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. There seem to be no examples using TInt, UInt, CBack? |
||
UInt = Union[T, int] | ||
CBack = Callable[..., T] | ||
Vec = Iterable[Tuple[T, T]] | ||
|
||
def inproduct(v: Vec[T]) -> T: | ||
return sum(x*y for x, y in v) | ||
|
||
def dilate(v: Vec[T], scale: T) -> Vec[T]: | ||
return ((x * scale, y * scale) for x, y in v) | ||
|
||
v1: Vec[int] = [] # Same as Iterable[Tuple[int, int]] | ||
v2: Vec = [] # Same as Iterable[Tuple[Any, Any]] | ||
v3: Vec[int, int] = [] # Error: Invalid alias, too many type arguments! | ||
|
||
Type aliases can be imported from modules like any names. Aliases can target another | ||
aliases (although building complex chains of aliases is not recommended, this | ||
impedes code readability, thus defeating the purpose of using aliases). | ||
Following previous examples: | ||
|
||
.. code-block:: python | ||
|
||
from typing import TypeVar, Generic, Optional | ||
from first_example import AliasType | ||
from second_example import Vec | ||
|
||
def fun() -> AliasType: | ||
... | ||
|
||
T = TypeVar('T') | ||
class NewVec(Generic[T], Vec[T]): | ||
... | ||
for i, j in NewVec[int](): | ||
... | ||
|
||
OIntVec = Optional[Vec[int]] | ||
|
||
.. note:: | ||
|
||
A type alias does not create a new type. It's just a shorthand notation for | ||
another type -- it's equivalent to the target type. For generic type aliases | ||
this means that variance of type variables used for alias definition does not | ||
allpy to aliases. Parameterized generic alias is treated simply as an original | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. allpy?? |
||
type with corresponding type variables substituted. Accordingly, type checking | ||
happens when a type alias is used. Invalid aliases (like e.g. | ||
``Callable[..., List[T, T]]``) might not always be flagged by mypy if they are | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. That would be a regression IMO -- while this is currently just illegal, There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. @gvanrossum I don't think so. I have just checked and |
||
left unused. | ||
|
||
.. _newtypes: | ||
|
||
|
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -6,7 +6,9 @@ | |
Type, AnyType, CallableType, Overloaded, NoneTyp, Void, TypeVarDef, | ||
TupleType, Instance, TypeVarId, TypeVarType, ErasedType, UnionType, | ||
PartialType, DeletedType, UnboundType, UninhabitedType, TypeType, | ||
true_only, false_only, is_named_instance, function_type | ||
true_only, false_only, is_named_instance, function_type, | ||
get_typ_args, set_typ_args | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Add trailing comma, remove following blank line. |
||
|
||
) | ||
from mypy.nodes import ( | ||
NameExpr, RefExpr, Var, FuncDef, OverloadedFuncDef, TypeInfo, CallExpr, | ||
|
@@ -17,6 +19,7 @@ | |
ConditionalExpr, ComparisonExpr, TempNode, SetComprehension, | ||
DictionaryComprehension, ComplexExpr, EllipsisExpr, StarExpr, | ||
TypeAliasExpr, BackquoteExpr, ARG_POS, ARG_NAMED, ARG_STAR2, MODULE_REF, | ||
UNBOUND_TVAR, BOUND_TVAR, | ||
) | ||
from mypy import nodes | ||
import mypy.checker | ||
|
@@ -1375,8 +1378,50 @@ def visit_type_application(self, tapp: TypeApplication) -> Type: | |
return AnyType() | ||
|
||
def visit_type_alias_expr(self, alias: TypeAliasExpr) -> Type: | ||
""" Get type of a type alias (could be generic) in a runtime expression.""" | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. No space before first word of sentence. |
||
item = alias.type | ||
if not alias.runtime: | ||
item = self.replace_tvars_any(item) | ||
if isinstance(item, Instance): | ||
tp = type_object_type(item.type, self.named_type) | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. I'd like a comment here explaining that we'll generally get a callable type (or overloaded) with .is_type_obj() true, representing the class's constructor. This would explain that below we really just check for callable and overloaded. |
||
else: | ||
return alias.fback | ||
if isinstance(tp, CallableType): | ||
if len(tp.variables) != len(item.args): | ||
self.msg.incompatible_type_application(len(tp.variables), | ||
len(item.args), item) | ||
return AnyType() | ||
return self.apply_generic_arguments(tp, item.args, item) | ||
elif isinstance(tp, Overloaded): | ||
for it in tp.items(): | ||
if len(it.variables) != len(item.args): | ||
self.msg.incompatible_type_application(len(it.variables), | ||
len(item.args), item) | ||
return AnyType() | ||
return Overloaded([self.apply_generic_arguments(it, item.args, item) | ||
for it in tp.items()]) | ||
return AnyType() | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. It's not an error because we can only get here when there already was a previous error, right? There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. @gvanrossum Yes, I think so. At least I don't see a way to get there without previously having an error. |
||
|
||
def replace_tvars_any(self, tp: Type) -> Type: | ||
""" Replace all unbound type variables with Any if an alias is used in | ||
a runtime expression. Basically, this function finishes what could not be done | ||
in similar funtion from typeanal.py. | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Mind naming that similar function by name? (Also, typo -- funtion.) There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. @gvanrossum Yes, sure :-) |
||
""" | ||
typ_args = get_typ_args(tp) | ||
new_args = typ_args[:] | ||
for i, arg in enumerate(typ_args): | ||
if isinstance(arg, UnboundType): | ||
sym = None | ||
try: | ||
sym = self.chk.lookup_qualified(arg.name) | ||
except KeyError: | ||
pass | ||
if sym and (sym.kind == UNBOUND_TVAR or sym.kind == BOUND_TVAR): | ||
new_args[i] = AnyType() | ||
else: | ||
new_args[i] = self.replace_tvars_any(arg) | ||
return set_typ_args(tp, new_args) | ||
|
||
def visit_list_expr(self, e: ListExpr) -> Type: | ||
"""Type check a list expression [...].""" | ||
return self.check_lst_expr(e.items, 'builtins.list', '<list>', e) | ||
|
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -1732,9 +1732,14 @@ class TypeAliasExpr(Expression): | |
"""Type alias expression (rvalue).""" | ||
|
||
type = None # type: mypy.types.Type | ||
fback = None # type: mypy.types.Type | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Would be nice to have a comment that clarifies what "fback" means. (Apparently fallback? Why not spell it in full?) There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. @gvanrossum OK, changed to |
||
runtime = False # type: bool | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. I'm beginning to wonder what 'runtime' really means. I notice you have 'runtime expression' in an error message. I think it's unclear what makes some expressions more "runtime" than other. Can you think of a better term? There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. @gvanrossum I changed this to |
||
|
||
def __init__(self, type: 'mypy.types.Type') -> None: | ||
def __init__(self, type: 'mypy.types.Type', fback: 'mypy.types.Type' = None, | ||
runtime: bool = False) -> None: | ||
self.type = type | ||
self.fback = fback | ||
self.runtime = runtime | ||
|
||
def accept(self, visitor: NodeVisitor[T]) -> T: | ||
return visitor.visit_type_alias_expr(self) | ||
|
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -1126,7 +1126,8 @@ def visit_block_maybe(self, b: Block) -> None: | |
if b: | ||
self.visit_block(b) | ||
|
||
def anal_type(self, t: Type, allow_tuple_literal: bool = False) -> Type: | ||
def anal_type(self, t: Type, allow_tuple_literal: bool = False, | ||
aliasing: bool = False) -> Type: | ||
if t: | ||
if allow_tuple_literal: | ||
# Types such as (t1, t2, ...) only allowed in assignment statements. They'll | ||
|
@@ -1143,7 +1144,8 @@ def anal_type(self, t: Type, allow_tuple_literal: bool = False) -> Type: | |
return TupleType(items, self.builtin_type('builtins.tuple'), t.line) | ||
a = TypeAnalyser(self.lookup_qualified, | ||
self.lookup_fully_qualified, | ||
self.fail) | ||
self.fail, | ||
aliasing=aliasing) | ||
return t.accept(a) | ||
else: | ||
return None | ||
|
@@ -1173,7 +1175,7 @@ def visit_assignment_stmt(self, s: AssignmentStmt) -> None: | |
node.kind = TYPE_ALIAS | ||
node.type_override = res | ||
if isinstance(s.rvalue, IndexExpr): | ||
s.rvalue.analyzed = TypeAliasExpr(res) | ||
s.rvalue.analyzed = TypeAliasExpr(res, fback=self.alias_fallback(res)) | ||
if s.type: | ||
# Store type into nodes. | ||
for lvalue in s.lvalues: | ||
|
@@ -1211,6 +1213,20 @@ def analyze_simple_literal_type(self, rvalue: Expression) -> Optional[Type]: | |
return self.named_type_or_none('builtins.unicode') | ||
return None | ||
|
||
def alias_fallback(self, tp: Type) -> Instance: | ||
"""Make a dummy Instance with no methods. It is used as a fallback type | ||
to detect errors for non-Instance aliases (i.e. Unions, Tuples, Callables). | ||
""" | ||
|
||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. You don't need a blank line here (the stand-alone |
||
kind = (' to Callable' if isinstance(tp, CallableType) else | ||
' to Tuple' if isinstance(tp, TupleType) else | ||
' to Union' if isinstance(tp, UnionType) else '') | ||
cdef = ClassDef('Type alias{}'.format(kind), Block([])) | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. You can just use |
||
fb_info = TypeInfo(SymbolTable(), cdef, self.cur_mod_id) | ||
fb_info.bases = [self.object_type()] | ||
fb_info.mro = [fb_info, self.object_type().type] | ||
return Instance(fb_info, []) | ||
|
||
def check_and_set_up_type_alias(self, s: AssignmentStmt) -> None: | ||
"""Check if assignment creates a type alias and set it up as needed.""" | ||
# For now, type aliases only work at the top level of a module. | ||
|
@@ -2361,7 +2377,15 @@ def visit_unary_expr(self, expr: UnaryExpr) -> None: | |
|
||
def visit_index_expr(self, expr: IndexExpr) -> None: | ||
expr.base.accept(self) | ||
if refers_to_class_or_function(expr.base): | ||
if isinstance(expr.base, RefExpr) and expr.base.kind == TYPE_ALIAS: | ||
# Special form -- subcribing a generic type alias. | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. subscripting |
||
# Perform the type substitution and create a new alias. | ||
res = analyze_type_alias(expr, | ||
self.lookup_qualified, | ||
self.lookup_fully_qualified, | ||
self.fail) | ||
expr.analyzed = TypeAliasExpr(res, fback=self.alias_fallback(res), runtime=True) | ||
elif refers_to_class_or_function(expr.base): | ||
# Special form -- type application. | ||
# Translate index to an unanalyzed type. | ||
types = [] # type: List[Type] | ||
|
@@ -2375,7 +2399,7 @@ def visit_index_expr(self, expr: IndexExpr) -> None: | |
except TypeTranslationError: | ||
self.fail('Type expected within [...]', expr) | ||
return | ||
typearg = self.anal_type(typearg) | ||
typearg = self.anal_type(typearg, aliasing=True) | ||
types.append(typearg) | ||
expr.analyzed = TypeApplication(expr.base, types) | ||
expr.analyzed.line = expr.line | ||
|
@@ -3051,6 +3075,8 @@ def visit_decorator(self, dec: Decorator) -> None: | |
|
||
def visit_assignment_stmt(self, s: AssignmentStmt) -> None: | ||
self.analyze(s.type) | ||
if isinstance(s.rvalue, IndexExpr) and isinstance(s.rvalue.analyzed, TypeAliasExpr): | ||
self.analyze(s.rvalue.analyzed.type) | ||
super().visit_assignment_stmt(s) | ||
|
||
def visit_cast_expr(self, e: CastExpr) -> None: | ||
|
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -1,14 +1,14 @@ | ||
"""Semantic analysis of types""" | ||
|
||
from typing import Callable, cast, List | ||
from typing import Callable, cast, List, Optional | ||
|
||
from mypy.types import ( | ||
Type, UnboundType, TypeVarType, TupleType, UnionType, Instance, | ||
AnyType, CallableType, Void, NoneTyp, DeletedType, TypeList, TypeVarDef, TypeVisitor, | ||
StarType, PartialType, EllipsisType, UninhabitedType, TypeType | ||
StarType, PartialType, EllipsisType, UninhabitedType, TypeType, get_typ_args, set_typ_args | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Trailing comma. |
||
) | ||
from mypy.nodes import ( | ||
BOUND_TVAR, TYPE_ALIAS, UNBOUND_IMPORTED, | ||
BOUND_TVAR, UNBOUND_TVAR, TYPE_ALIAS, UNBOUND_IMPORTED, | ||
TypeInfo, Context, SymbolTableNode, Var, Expression, | ||
IndexExpr, RefExpr | ||
) | ||
|
@@ -40,6 +40,9 @@ def analyze_type_alias(node: Expression, | |
# that we don't support straight string literals as type aliases | ||
# (only string literals within index expressions). | ||
if isinstance(node, RefExpr): | ||
if node.kind == UNBOUND_TVAR or node.kind == BOUND_TVAR: | ||
fail_func('Invalid type "{}" for aliasing'.format(node.fullname), node) | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. I couldn't find any tests revealing this message? There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. @gvanrossum Good catch! Added a test. There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Could you make the error message clearer? I think it should explicitly state that you cannot create an alias for a type variable. There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. @gvanrossum OK, now the error explicitly says that type variable is invalid as target for type alias. |
||
return None | ||
if not (isinstance(node.node, TypeInfo) or | ||
node.fullname == 'typing.Any' or | ||
node.kind == TYPE_ALIAS): | ||
|
@@ -48,7 +51,8 @@ def analyze_type_alias(node: Expression, | |
base = node.base | ||
if isinstance(base, RefExpr): | ||
if not (isinstance(base.node, TypeInfo) or | ||
base.fullname in type_constructors): | ||
base.fullname in type_constructors or | ||
base.kind == TYPE_ALIAS): | ||
return None | ||
else: | ||
return None | ||
|
@@ -61,7 +65,7 @@ def analyze_type_alias(node: Expression, | |
except TypeTranslationError: | ||
fail_func('Invalid type alias', node) | ||
return None | ||
analyzer = TypeAnalyser(lookup_func, lookup_fqn_func, fail_func) | ||
analyzer = TypeAnalyser(lookup_func, lookup_fqn_func, fail_func, aliasing=True) | ||
return type.accept(analyzer) | ||
|
||
|
||
|
@@ -74,10 +78,12 @@ class TypeAnalyser(TypeVisitor[Type]): | |
def __init__(self, | ||
lookup_func: Callable[[str, Context], SymbolTableNode], | ||
lookup_fqn_func: Callable[[str], SymbolTableNode], | ||
fail_func: Callable[[str, Context], None]) -> None: | ||
fail_func: Callable[[str, Context], None], *, | ||
aliasing = False) -> None: | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Missing |
||
self.lookup = lookup_func | ||
self.lookup_fqn_func = lookup_fqn_func | ||
self.fail = fail_func | ||
self.aliasing = aliasing | ||
|
||
def visit_unbound_type(self, t: UnboundType) -> Type: | ||
if t.optional: | ||
|
@@ -141,8 +147,23 @@ def visit_unbound_type(self, t: UnboundType) -> Type: | |
item = items[0] | ||
return TypeType(item, line=t.line) | ||
elif sym.kind == TYPE_ALIAS: | ||
# TODO: Generic type aliases. | ||
return sym.type_override | ||
override = sym.type_override | ||
an_args = self.anal_array(t.args) | ||
all_vars = self.get_type_var_names(override) | ||
exp_len = len(all_vars) | ||
act_len = len(an_args) | ||
if exp_len > 0 and act_len == 0: | ||
# Interpret bare Alias same as normal generic, i.e., Alias[Any, Any, ...] | ||
return self.replace_alias_tvars(override, all_vars, [AnyType()] * exp_len) | ||
if exp_len == 0 and act_len == 0: | ||
return override | ||
if act_len != exp_len: | ||
# TODO: Detect wrong type variable numer for unused aliases | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Typo: numer. |
||
# (it is difficult at this stage, see comment below, line 187 atm) | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. I'm not crazy about the line number reference... :-) |
||
self.fail('Bad number of arguments for type alias, expected: %s, given: %s' | ||
% (exp_len, act_len), t) | ||
return t | ||
return self.replace_alias_tvars(override, all_vars, an_args) | ||
elif not isinstance(sym.node, TypeInfo): | ||
name = sym.fullname | ||
if name is None: | ||
|
@@ -153,7 +174,9 @@ def visit_unbound_type(self, t: UnboundType) -> Type: | |
# as a base class -- however, this will fail soon at runtime so the problem | ||
# is pretty minor. | ||
return AnyType() | ||
self.fail('Invalid type "{}"'.format(name), t) | ||
# Allow unbount type variables when defining an alias | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Typo: unbount. |
||
if not (self.aliasing and sym.kind == UNBOUND_TVAR): | ||
self.fail('Invalid type "{}"'.format(name), t) | ||
return t | ||
info = sym.node # type: TypeInfo | ||
if len(t.args) > 0 and info.fullname() == 'builtins.tuple': | ||
|
@@ -181,6 +204,53 @@ def visit_unbound_type(self, t: UnboundType) -> Type: | |
else: | ||
return AnyType() | ||
|
||
def get_type_var_names(self, tp: Type) -> List[str]: | ||
""" Get all type variable names that are present in a generic type alias | ||
in order of textual appearance (recursively, if needed). | ||
""" | ||
tvars = [] # type: List[str] | ||
typ_args = get_typ_args(tp) | ||
for arg in typ_args: | ||
tvar = self.get_tvar_name(arg) | ||
if tvar: | ||
tvars.append(tvar) | ||
else: | ||
subvars = self.get_type_var_names(arg) | ||
if subvars: | ||
tvars.extend(subvars) | ||
# Get unique type variables in order of appearance | ||
all_tvars = set(tvars) | ||
new_tvars = [] | ||
for t in tvars: | ||
if t in all_tvars: | ||
new_tvars.append(t) | ||
all_tvars.remove(t) | ||
return new_tvars | ||
|
||
def get_tvar_name(self, t: Type) -> Optional[str]: | ||
if not isinstance(t, UnboundType): | ||
return None | ||
sym = self.lookup(t.name, t) | ||
if sym is not None and (sym.kind == UNBOUND_TVAR or sym.kind == BOUND_TVAR): | ||
return t.name | ||
return None | ||
|
||
def replace_alias_tvars(self, tp: Type, vars: List[str], subs: List[Type]) -> Type: | ||
""" Replace type variables in a generic type alias tp with substitutions subs. | ||
Length of subs should be already checked. | ||
""" | ||
typ_args = get_typ_args(tp) | ||
new_args = typ_args[:] | ||
for i, arg in enumerate(typ_args): | ||
tvar = self.get_tvar_name(arg) | ||
if tvar and tvar in vars: | ||
# Perform actual substitution... | ||
new_args[i] = subs[vars.index(tvar)] | ||
else: | ||
# ...recursively, if needed. | ||
new_args[i] = self.replace_alias_tvars(arg, vars, subs) | ||
return set_typ_args(tp, new_args) | ||
|
||
def visit_any(self, t: AnyType) -> Type: | ||
return t | ||
|
||
|
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Subscripted