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

Conversation

ilevkivskyi
Copy link
Member

@ilevkivskyi ilevkivskyi commented Oct 29, 2016

Fixes #606 as per PEP 484.

Now type aliases could be generic, so that the example in PEP now works. Generic type aliases are allowed for generic classes, unions, callables, and tuples, for example:

Vec = Iterable[Tuple[T, T]]
TInt = Tuple[T, int]
UInt = Union[T, int]
CBack = Callable[..., T]

The aliases could be used as specified in PEP 484, e.g. either one specifies all free type variables, or if unspecified they are all substituted by Any, for example:

def fun(v: Vec[T]) -> Vec[T]: # Same as Iterable[Tuple[T, T]]
    ...
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!

Generic aliases could be used everywhere, where a normal generic type could be used (in annotations, generic base classes etc, and since recently in runtime expressions). Nested and chained aliases are allowed, but excessive use of those is discouraged in the docs. As ordinary (non-generic) aliases, generic ones could be imported from other modules.

I believe this is a useful feature (I found three issues on tracker requesting this). When someone wants precise types, they quickly become quite verbose, so that generic type aliases will improve readability. Please, don't be scared by the size of diff, the actual implementation is quite simple, 85% of changes are extensive tests.

NOTE: Many examples in the tests and in docs require a recent version of typing.py from python/typing to work at runtime (see #2382). This feature could be used with older versions of typing.py by using type comments or "forward references".

@JukkaL
Copy link
Collaborator

JukkaL commented Oct 31, 2016

@gvanrossum @ddfisher Can one of you review this?

@gvanrossum
Copy link
Member

gvanrossum commented Oct 31, 2016 via email

Copy link
Member

@gvanrossum gvanrossum left a comment

Choose a reason for hiding this comment

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

This is a great piece of work! I think I understand everything that's going on (except I haven't studied the new tests much). Here are a lot of comments, mostly nits. Thanks!

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

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

Choose a reason for hiding this comment

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

Typoooos

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?

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

allpy to aliases. Parameterized generic alias is treated simply as an original
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.

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

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.

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

return override
if act_len != exp_len:
# TODO: Detect wrong type variable numer for unused aliases
# (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... :-)

a runtime expression. Basically, this function finishes what could not be done
in similar funtion from typeanal.py.
"""
if not isinstance(tp, (Instance, UnionType, TupleType, CallableType)):
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 wondering if this might be a job for TypeTranslator... Or perhaps refactor the code that extracts the type variables from a type into a helper function? (I see you have the exact same idiom in three places now. If in the future we add a new parameterized Type subclass we'd be in a bit of pain finding all places where this is used.

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 refactored this to two helpers get_typ_args() and set_typ_args() in types.py. This indeed made everything more compact, those two are used in both typeanal.py and checkexpr.py.

@ilevkivskyi
Copy link
Member Author

@gvanrossum Thank you for a thorough review! And sorry for all the typos, I promise to use a spellchecker. I implemented requested changes in new commits (more details are in replies to individual comments). Please, take a look!

Copy link
Member

@gvanrossum gvanrossum left a comment

Choose a reason for hiding this comment

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

Approaching the limit! I started a bunch of reviews in multiple tabs, hopefully this is all of them.

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

@@ -1732,13 +1732,13 @@ class TypeAliasExpr(Expression):
"""Type alias expression (rvalue)."""

type = None # type: mypy.types.Type
line = None # type: int
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.

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). :-)

"""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).

@@ -5,7 +5,7 @@
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.

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

Could you make the error message clearer? I think it should explicitly state that you cannot create an alias for a type variable.

``Callable[..., List[T, T]]``) might not always be flagged by mypy if they are
left unused.
this means that variance or constraints of type variables used for alias
definition don't apply to aliases. Parameterized generic alias is treated
Copy link
Member

Choose a reason for hiding this comment

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

A parameterized generic alias ...

left unused.
this means that variance or constraints of type variables used for alias
definition don't apply to aliases. Parameterized generic alias is treated
simply as an original type with corresponding type variables substituted.
Copy link
Member

Choose a reason for hiding this comment

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

...with the corresponding ...

@@ -766,7 +766,7 @@ if not isinstance(s, str):

z = None # type: TNode # Same as TNode[Any]
z.x
z.foo() # E: Some element of union has no attribute "foo"
z.foo() # Any simplyfies Union to Any
Copy link
Member

Choose a reason for hiding this comment

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

Actually, Union[Any, int] is not the same as Any. This is subtle (I got it wrong in an early version of typing.py) but it's important, since when it's an int, it can't be used as a string. See PR #2197

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 remember our conversation on python/typing tracker. I just wanted to mark somehow this test, so that when it will start failing after Union[Any, ...] PR is merged it will be clear that nothing is special here. I have expanded this comment now to clarify.


[builtins fixtures/list.pyi]
[out]
main:8: error: Invalid type argument value for "A"
main:8: error: Type argument 1 of "A" has incompatible value "str"
Copy link
Member

Choose a reason for hiding this comment

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

I don't like getting this twice. The first error is too vague since it doesn't tell you which position has the error.

(Unless you turn on column numbers? Which reminds me, it would be nice to turn on --show-column-numbers for at least some of the tests, to verify that they're being passed along.)

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, I updated the error messages, only the more detailed one is shown wherever possible.

Concerning the column numbers I discovered that a lot of code does not copy them. For example, exprtotype.py was simply throwing them away (I fixed this) while this file is the key, if column for a type is ignored here it is lost forever. I investigated this more and I found that this is a limitation of old parser and type comments. I don't know how to fix this (and think it will be difficult), but for fast parser and type annotations it is just one line and we get column for free.

I used --show-column-numbers in two tests (one with old parser and one with fast parser). And indeed fast parser works better. So this is another argument to make the fast parser default.

@ilevkivskyi
Copy link
Member Author

@gvanrossum Thank you for review! I implemented the comments in new commits. I hope everything is fine now.

main:3: error: Argument 2 to NewType(...) must be subclassable (got T?)
main:3: error: Invalid type "__main__.T"
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 The order of these two messages is changed because now the column is correctly passed and errors are ordered by column.

[out]
main:9:7: error: Type argument 1 of "A" has incompatible value "str"
main:13: error: Type argument 1 of "A" has incompatible value "str"
main:13: error: Type argument 2 of "A" has incompatible value "str"
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 The column is not shown here because the type is inside a comment. For type annotation everything works.

@gvanrossum gvanrossum merged commit 57ff82b into python:master Nov 3, 2016
@gvanrossum
Copy link
Member

Woot! Thanks so much!

@gvanrossum
Copy link
Member

Sadly this broke something. I can work around it but it would be good if we fixed it before the next public release.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

Successfully merging this pull request may close these issues.

3 participants