-
-
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
Support for functions producing generic functions #3113
Conversation
Previously, e9d28a0 fixed a crash when you tried to access a class-attribute type variable. The test in that commit involved assigning a variable the value of the typevar. It resulted in no crash, but rather treating the variable as being an instance of the type the typevar bound to, later, which is incorrect. Instead, this PR treats such an assignment as an error, and gives you the same message as when you try to alias a typevar directly. Also test a *correct* alias with the typevar access method in question -- it works.
Instead of TypeQuery always returning a boolean and having the strategy be an enum, the strategy is now a Callable describing how to combine partial results, and the two default strategies are plain old funcitons. To preserve the short-circuiting behavior of the previous code, this PR uses an exception. This is a pure refactor that I am using in my experimentation regarding fixing python#1551. It should result in exactly no change to current behavior. It's separable from the other things I'm experimenting with, so I'm filing it as a separate pull request now. It enables me to rewrite the code that pulls type variables out of types as a TypeQuery. Consider waiting to merge this PR until I have some code that uses it ready for review. Or merge it now, if you think it's a pleasant cleanup instead of an ugly complication. I'm of two minds on that particular question.
…with a scope object
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 needs a little more love before it's mergeable, and it depends on two other PRs currently in flight, but I wanted to get this up for comment if people like.
mypy/checkmember.py
Outdated
@@ -406,7 +406,9 @@ def analyze_class_attribute_access(itype: Instance, | |||
return AnyType() | |||
|
|||
if isinstance(node.node, TypeVarExpr): |
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 the part that depends on #3105 -- the TypeVarExpr
is now only associated with its TypeVarDef
by scope, not direct reference.
mypy/types.py
Outdated
@@ -1500,112 +1500,112 @@ def keywords_str(self, a: Iterable[Tuple[str, Type]]) -> 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.
This business is #3084
mypy/typeanal.py
Outdated
fallback=t.fallback or self.builtin_type('builtins.function'), | ||
variables=self.anal_var_defs(t.variables)) | ||
def visit_callable_type(self, t: CallableType, nested: bool = True) -> Type: | ||
with self.tvar_scope_frame(): |
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.
The above line is the punchline of this diff. Make a layer of type variable scope when analyzing a callable type, so it can bind its own type variables if it needs to.
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 a code comment with contents similar to the second sentence in the above comment?
mypy/typeanal.py
Outdated
names.append(name) | ||
tvars.append(tvar_expr) | ||
for name, tvar_expr in type.ret_type.accept( | ||
TypeVariableQuery(self.lookup, self.tvar_scope, include_callables=False)): |
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.
Other punchline of this diff: when you're finding the type variables that a function has, don't include the tvars you only find in a Callable
in the return type. Those belong to that Callable
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 code comment mentioning what you said above.
Ok. Feels ready for review now. |
test-data/unit/semanal-errors.test
Outdated
T = TypeVar('T') | ||
class A(Generic[T]): | ||
class B(Generic[T]): pass \ | ||
# E: Free type variable expected in Generic[...] |
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 appears in PEP 484 as an example of an error. I think it should still be prohibited.
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.
Oh, yes, you're totally right. According to PEP484 you can neither make the inner class generic in the same tvar, nor use the outer tvar in the inner class. I'll adjust as necessary.
Bah. The difference between the type variable scope you get from a class and the one you get from a function is complicated, and I suspect it causes bugs in static methods and class methods. I'll write up a test for what happens with those, to see if there are things that don't make sense. |
Following up, here are the things that don't make sense: #3172 I can include a fix in this PR, but maybe I might prefer to make it in a smaller follow-on PR. This one is quite long enough already. |
Separate PRs, please. |
… into TypeVarScope This allows us to be more consistent about which type variables we allow and which we don't.
FWIW I've ran this against our internal codebase and found no issues. |
I'm hoping that @JukkaL can do the detailed review 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.
Looks good -- thanks for implementing this! A have just a few random nits, other than these this is ready to merge.
mypy/tvar_scope.py
Outdated
"""Initializer for TypeVarScope | ||
|
||
Parameters: | ||
parent: the outer scope for this scope |
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.
Style nit: indent the parameter descriptions like this (for better consistency):
Args:
parent: the outer scope for this cope
...
mypy/tvar_scope.py
Outdated
def get_function_scope(self) -> Optional['TypeVarScope']: | ||
"""Get the nearest parent that's a function scope, not a class scope""" | ||
it = self | ||
while it.is_class_scope: |
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.
Should the check be while it is not None and ...
?
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, that's safer. We have an invariant that the root is an empty function scope, with the shape of the code now, but that could change.
|
||
def class_frame(self) -> 'TypeVarScope': | ||
"""A new scope frame for binding a class. Prohibits *this* class's tvars""" | ||
return TypeVarScope(self.get_function_scope(), True, self) |
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.
Should we assert that the current scope is a class scope? If not, should we still prohibit the definitions in the current scope even if it's a function scope?
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 are always prohibited from re-binding an already-bound type variable. When you define a class in a function, this doesn't prohibit anything more -- your class type variables are also going to be prohibited by dint of being in your own parent.
mypy/typeanal.py
Outdated
fallback=t.fallback or self.builtin_type('builtins.function'), | ||
variables=self.anal_var_defs(t.variables)) | ||
def visit_callable_type(self, t: CallableType, nested: bool = True) -> Type: | ||
with self.tvar_scope_frame(): |
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 a code comment with contents similar to the second sentence in the above comment?
mypy/typeanal.py
Outdated
return ret.accept(self) | ||
|
||
@contextmanager | ||
def tvar_scope_frame(self) -> Generator: |
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.
Use Iterator[None]
as the return type to avoid implicit Any
types?
mypy/typeanal.py
Outdated
names.append(name) | ||
tvars.append(tvar_expr) | ||
for name, tvar_expr in type.ret_type.accept( | ||
TypeVariableQuery(self.lookup, self.tvar_scope, include_callables=False)): |
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 code comment mentioning what you said above.
mypy/typeanal.py
Outdated
|
||
def bind_function_type_variables(self, | ||
fun_type: CallableType, defn: Context) -> List[TypeVarDef]: | ||
"""Find the type variables of the function type and bind them in our tvar_scope. |
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.
Style nit: move the trailing """
to the end of the first docstring line, for consistency.
mypy/typeanal.py
Outdated
if node and node.kind == TVAR and ( | ||
self.include_bound or self.scope.get_binding(node) is None): | ||
assert isinstance(node.node, TypeVarExpr) | ||
return[(name, node.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.
Style nit: missing space after return
.
mypy/typeanal.py
Outdated
scope: 'TypeVarScope', | ||
*, | ||
include_callables: bool = True, | ||
include_bound: bool = False) -> None: |
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 was confused by this argument initially. What about renaming it to include_bound_tvars
, for example? 'Bound' is also used to refer to type variable bounds.
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.
Comments from being on the phone with Jukka
I think everything addressed now! |
Provides support for returning a generic
Callable
from a function, allowing you to write a function that produces a decorator, for example. This pattern is common:Details on why this touches so much code:
Returning a generic
Callable
from a function requires binding type variables while we're traversing the type analysis phase of the check. Previously, all type variable binding was done fromsemanal.py
. I refactored type variable tracking and binding into its own class, that's used by bothsemanal.py
andtypeanal.py
to keep track of its type variables. I also, in the process, nixed the thing where we're mutatingTypeVarExpr
s to bind them, instead keeping track of the bindings in the scope object I created. Seems more sustainable in a world where more than one class has to deal with typevar binding.Merge checklist: