Skip to content
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

Merged
merged 21 commits into from
Nov 3, 2016
Merged
Show file tree
Hide file tree
Changes from 13 commits
Commits
Show all changes
21 commits
Select commit Hold shift + click to select a range
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
62 changes: 59 additions & 3 deletions docs/source/kinds_of_types.rst
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Subscripted

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
Copy link
Member

Choose a reason for hiding this comment

The 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]
Copy link
Member

Choose a reason for hiding this comment

The 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
Copy link
Member

Choose a reason for hiding this comment

The 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
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

That would be a regression IMO -- while this is currently just illegal, List[int, int] in an alias is immediately flagged as an error ("Too many parameters for typing.List; actual 2, expected 1") and I'd be sad if that was no longer flagged on the alias line.

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@gvanrossum I don't think so. I have just checked and A = Callable[..., List[int, int]] passes without errors with mypy/master. But anyway, it looks like I fixed it, with this PR both the above and aliases likeUnion[int, List[T, T]] etc. should be flagged as errors even if they are left unused.

left unused.

.. _newtypes:

Expand Down
47 changes: 46 additions & 1 deletion mypy/checkexpr.py
Original file line number Diff line number Diff line change
Expand Up @@ -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
Copy link
Member

Choose a reason for hiding this comment

The 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,
Expand All @@ -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
Expand Down Expand Up @@ -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."""
Copy link
Member

Choose a reason for hiding this comment

The 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)
Copy link
Member

Choose a reason for hiding this comment

The 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()
Copy link
Member

Choose a reason for hiding this comment

The 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?

Copy link
Member Author

Choose a reason for hiding this comment

The 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.
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Mind naming that similar function by name? (Also, typo -- funtion.)

Copy link
Member Author

Choose a reason for hiding this comment

The 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)
Expand Down
7 changes: 6 additions & 1 deletion mypy/nodes.py
Original file line number Diff line number Diff line change
Expand Up @@ -1732,9 +1732,14 @@ class TypeAliasExpr(Expression):
"""Type alias expression (rvalue)."""

type = None # type: mypy.types.Type
fback = None # type: mypy.types.Type
Copy link
Member

Choose a reason for hiding this comment

The 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?)

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@gvanrossum OK, changed to fallback and added comment.

runtime = False # type: bool
Copy link
Member

Choose a reason for hiding this comment

The 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?

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@gvanrossum I changed this to in_runtime and added a detailed comment explaining that this marks things like Alias[T](42) in contrast to subscripting in type context x: Alias[T].


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)
Expand Down
36 changes: 31 additions & 5 deletions mypy/semanal.py
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand All @@ -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
Expand Down Expand Up @@ -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:
Expand Down Expand Up @@ -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).
"""

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

You don't need a blank line here (the stand-alone """ serves as enough of a visual separator).

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([]))
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

You can just use 'a' + b instead of 'a{}'.format(b). :-)

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.
Expand Down Expand Up @@ -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.
Copy link
Member

Choose a reason for hiding this comment

The 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]
Expand All @@ -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
Expand Down Expand Up @@ -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:
Expand Down
88 changes: 79 additions & 9 deletions mypy/typeanal.py
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
Copy link
Member

Choose a reason for hiding this comment

The 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
)
Expand Down Expand Up @@ -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)
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I couldn't find any tests revealing this message?

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@gvanrossum Good catch! Added a test.

Copy link
Member

Choose a reason for hiding this comment

The 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.

Copy link
Member Author

Choose a reason for hiding this comment

The 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):
Expand All @@ -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
Expand All @@ -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)


Expand All @@ -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:
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Missing : bool (even though it's implied).

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:
Expand Down Expand Up @@ -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
Copy link
Member

Choose a reason for hiding this comment

The 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)
Copy link
Member

Choose a reason for hiding this comment

The 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:
Expand All @@ -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
Copy link
Member

Choose a reason for hiding this comment

The 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':
Expand Down Expand Up @@ -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

Expand Down
Loading