-
-
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
Conversation
@gvanrossum @ddfisher Can one of you review this? |
I can, and will, in due time.
…--Guido (mobile)
|
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.
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, |
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
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 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] |
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.
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 |
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.
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 |
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.
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.
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.
@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) |
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.
I couldn't find any tests revealing this message?
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.
@gvanrossum Good catch! Added a test.
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.
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 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 |
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.
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 |
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.
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) |
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.
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)): |
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.
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.
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.
@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
.
…eport same error as without alias
e58eff7
to
feb9413
Compare
@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! |
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.
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 |
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.
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 |
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.
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 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([])) |
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.
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). | ||
""" | ||
|
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.
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 |
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.
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) |
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.
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 |
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.
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. |
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.
...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 |
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.
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
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.
@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" |
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.
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.)
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.
@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.
@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" |
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.
@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" |
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.
@gvanrossum The column is not shown here because the type is inside a comment. For type annotation everything works.
Woot! Thanks so much! |
Sadly this broke something. I can work around it but it would be good if we fixed it before the next public release. |
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:
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: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
frompython/typing
to work at runtime (see #2382). This feature could be used with older versions oftyping.py
by using type comments or "forward references".