-
-
Notifications
You must be signed in to change notification settings - Fork 2.9k
Simplify and tighten type aliases #3524
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
Conversation
Marked this as WIP, since I have found another place (in |
OK, I fixed remaining points (also this PR now may help with #3355) |
I fear that @JukkaL will have to review this, but I ran this against our Dropbox codebase, and found some occurrences of the following pattern: class A: ...
class B: ...
def f(a: bool) -> None:
cls = A
if a:
cls = B We get several errors on the last line:
Previously this code passed. |
@gvanrossum
probably But after you mention it is actually used like this, I think maybe it makes sense. If Jukka also agrees, then I will change this back (it is only one line). To me both behaviours make sense, on one hand explicit is better than explicit plus there will be less exceptions to explain, on another hand, it would be boring and probably un-Pythonic to put explicit annotations everywhere. |
You're right, in the actual code this has
The solution would be to start with |
OK, I will revert this soon. (But we need to document that type aliases should be defined at the module level, at a class and function scope they are always automatically variables with type While we are waiting for review by @JukkaL, could you please test whether this PR helps with #3355? |
I wish I had a repro for #3355, but it's worse -- it occasionally happens to other users when they use --quick mode, but I haven't found a way to trigger it reliably myself yet. :-( |
@gvanrossum As promised, I removed creation of aliases at function scope. |
@JukkaL I fixed the merge problem, this should be ready for review again. |
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.
Mostly looks good, just a bunch of minor things.
from typing import Tuple | ||
from lib import Transform | ||
|
||
def int_tf(m: int) -> Transform[int, 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.
Also use reveal_type
to check that the declared type resulting from Transform[x, y]
is as expected.
mypy/semanal.py
Outdated
# 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): |
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 comment that describes the purpose of this special case.
@@ -1643,26 +1623,59 @@ def alias_fallback(self, tp: Type) -> Instance: | |||
fb_info.mro = [fb_info, self.object_type().type] | |||
return Instance(fb_info, []) | |||
|
|||
def analyze_alias(self, rvalue: Expression, | |||
allow_unnormalized: bool) -> Tuple[Optional[Type], List[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.
Add docstring. Explain arguments and the return value.
mypy/nodes.py
Outdated
@@ -2267,6 +2268,7 @@ class SymbolTableNode: | |||
mod_id = '' # type: Optional[str] | |||
# If this not None, override the type of the 'node' attribute. | |||
type_override = None # type: Optional[mypy.types.Type] | |||
alias_tvars = None # type: Optional[List[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.
Add comment.
Are these short or full names? If short ones, it would be more correct to have full names. For example, a.T
and b.T
could refer to different type variables -- this is very unlikely, though.
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.
It looks like these are qualified names. I added a test for this, and a comment here.
mypy/fixup.py
Outdated
@@ -88,6 +88,9 @@ def visit_symbol_table(self, symtab: SymbolTable) -> None: | |||
if stnode is not None: | |||
value.node = stnode.node | |||
value.type_override = stnode.type_override | |||
if not value.type_override and self.quick_and_dirty: |
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 understand this -- add a comment. Is there a test case that tests this change?
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.
Now I am also not sure it is needed. It was an attempt to fix #3355, but now I have a better idea. I removed this for now, and will make a separate PR later.
@@ -2342,6 +2346,7 @@ def serialize(self, prefix: str, name: str) -> JsonDict: | |||
data['node'] = self.node.serialize() | |||
if self.type_override is not None: | |||
data['type_override'] = self.type_override.serialize() | |||
data['alias_tvars'] = self.alias_tvars |
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.
Is there a test case that triggers this (and the corresponding deserialization bit)?
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.
Good catch! Added an incremental test.
mypy/semanal.py
Outdated
node.kind = TYPE_ALIAS | ||
node.type_override = res | ||
node.alias_tvars = alias_tvars | ||
if isinstance(s.rvalue, IndexExpr): |
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 comment that explains the motivation of this if statement.
from typing import TypeVar, Sequence, Type | ||
T = TypeVar('T') | ||
|
||
A: Type[float] = 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.
Also test assignment after initialization (it should be fine since there is an explicit annotation)?
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.
Yes, added it below.
|
||
[case testGenericTypeAliasesImportingWithoutTypeVarError] | ||
from a import Alias | ||
x: Alias[int, str] # E: Bad number of arguments for type alias, expected: 1, given: 2 |
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 reveal_type(x)
to see what type gets assigned in this case.
mypy/semanal.py
Outdated
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. | ||
# Type aliases are created only at module scope, at class and function scopes | ||
# assignments create class valued members and local variables with type object types. |
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 caused an error in an internal Dropbox codebase. We have code that looks like this:
from typing import Union
class A:
B = Union[int, str]
def f(self) -> B: # this PR makes this an invalid type
pass
Maybe type aliases in the class body should still be supported? Alternatively, maybe anything where the rvalue is a valid type that is an IndexExpr
should still be considered as a type alias, i.e. only restrict simple cases like X = Y
.
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.
OK, now the rules a following: explicit annotation -- always a variable, otherwise:
- module scope -- always an alias
- class scope -- an alias for subscripted, a variable for simple class objects
- function scope -- always a variable
@JukkaL Thanks for review! All comments should be addressed now. |
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.
Thanks, now looks good!
Fixes #2887
Fixes #3191
In addition this prohibits reassigning aliases, before something like this was allowed:
Now this will say:
Cannot assign multiple types to name "Alias" without an explicit "Type[...]" annotation
. See #3494 for background.Finally, this simplifies the logic in
semanal.py
, so that most processing of type aliases happens in one function.