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

Support for functions producing generic functions #3113

Merged
merged 40 commits into from
Apr 21, 2017

Conversation

sixolet
Copy link
Collaborator

@sixolet sixolet commented Apr 3, 2017

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:

def deco(x: int) -> Callable[[T], T]: ...

@deco(4)
def f(stuff): ...

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 from semanal.py. I refactored type variable tracking and binding into its own class, that's used by both semanal.py and typeanal.py to keep track of its type variables. I also, in the process, nixed the thing where we're mutating TypeVarExprs 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:

sixolet added 28 commits April 2, 2017 16:23
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.
Copy link
Collaborator Author

@sixolet sixolet left a 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.

@@ -406,7 +406,9 @@ def analyze_class_attribute_access(itype: Instance,
return AnyType()

if isinstance(node.node, TypeVarExpr):
Copy link
Collaborator Author

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


Copy link
Collaborator Author

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():
Copy link
Collaborator Author

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.

Copy link
Collaborator

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)):
Copy link
Collaborator Author

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

Copy link
Collaborator

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.

@sixolet
Copy link
Collaborator Author

sixolet commented Apr 12, 2017

Ok. Feels ready for review now.

@sixolet sixolet changed the title Support for functions producing generic functions (WIP) Support for functions producing generic functions Apr 12, 2017
T = TypeVar('T')
class A(Generic[T]):
class B(Generic[T]): pass \
# E: Free type variable expected in Generic[...]
Copy link
Member

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.

Copy link
Collaborator Author

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.

@sixolet
Copy link
Collaborator Author

sixolet commented Apr 14, 2017

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.

@sixolet
Copy link
Collaborator Author

sixolet commented Apr 14, 2017

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.

@gvanrossum
Copy link
Member

Separate PRs, please.

sixolet added 2 commits April 17, 2017 16:23
… into TypeVarScope

This allows us to be more consistent about which type variables we allow and
which we don't.
@gvanrossum
Copy link
Member

FWIW I've ran this against our internal codebase and found no issues.

@gvanrossum
Copy link
Member

I'm hoping that @JukkaL can do the detailed review though.

Copy link
Collaborator

@JukkaL JukkaL left a 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.

"""Initializer for TypeVarScope

Parameters:
parent: the outer scope for this scope
Copy link
Collaborator

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

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:
Copy link
Collaborator

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

Copy link
Collaborator Author

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)
Copy link
Collaborator

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?

Copy link
Collaborator Author

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():
Copy link
Collaborator

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:
Copy link
Collaborator

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)):
Copy link
Collaborator

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.
Copy link
Collaborator

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)]
Copy link
Collaborator

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:
Copy link
Collaborator

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.

Copy link
Collaborator Author

@sixolet sixolet left a 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

@sixolet
Copy link
Collaborator Author

sixolet commented Apr 20, 2017

I think everything addressed now!

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.

4 participants