10000 Support for functions producing generic functions by sixolet · Pull Request #3113 · python/mypy · GitHub
[go: up one dir, main page]

Skip to content

Support for functions producing generic functions #3113

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

Merged
merged 40 commits into from
Apr 21, 2017
Merged
Show file tree
Hide file tree
Changes from 1 commit
Commits
Show all changes
40 commits
Select commit Hold shift + click to select a range
679a172
Make it an error to use a class-attribute type var outside a type
sixolet Apr 1, 2017
74e9262
Fix the error message to handle more cases
sixolet Apr 1, 2017
5470c77
oops accidentally changed a file
sixolet Apr 1, 2017
12def8b
Typo fix
sixolet Apr 1, 2017
52782fe
more checking things in tests
sixolet Apr 1, 2017
0184595
Change error message
sixolet Apr 1, 2017
757a6b8
Change error message
sixolet Apr 1, 2017
57bca93
Make TypeQuery more general, handling nonboolean queries.
sixolet Mar 29, 2017
3b8bc23
Remove redundant empty check
sixolet Mar 29, 2017
82da335
Some types in the visitor were missing recursion
sixolet Mar 30, 2017
9f12644
Add ellipsis type handler to TypeQuery
sixolet Mar 30, 2017
8cf4fa6
Typechecks, but gives spurious error. Need to move variable binding?
sixolet Mar 30, 2017
2634277
Small change: Move tvar id generation nearer type analysis
sixolet Mar 30, 2017
30dba10
Even nearer
sixolet Mar 30, 2017
7b4f9f2
Replace the concept of BOUND_TVAR and UNBOUND_TVAR as mutative flags …
sixolet Mar 30, 2017
daf9592
Mid debugging gotta push now
sixolet Mar 30, 2017
826b9cf
Checkpoint. Most tests pass, classvar type aliases not yet
sixolet Mar 31, 2017
4141f94
forgot this file
sixolet Mar 31, 2017
58e3a2a
Fix aliasing to not bind type vars within a callable while aliasing
sixolet Mar 31, 2017
c061a09
removed some now-unused code
sixolet Apr 1, 2017
603013d
my own code review, first pass
sixolet Apr 1, 2017
c6fec63
Use type var query instead of another query fn
sixolet Apr 2, 2017
f46d52a
Tighten code
sixolet Apr 2, 2017
ea21337
fix some types
sixolet Apr 2, 2017
80d62a7
Use same type alias throughout
sixolet Apr 2, 2017
eaf8c0d
Make semanal tests pass. Delete one I think is wrong
sixolet Apr 2, 2017
5009a2b
Generator types for generators
sixolet Apr 2, 2017
7c64464
Lint and cleanups
sixolet Apr 3, 2017
a866d0f
Test for using generic function return as a decorator
sixolet Apr 11, 2017
9d5c630
Merge upstream
sixolet Apr 12, 2017
4683d70
Merge lint
sixolet Apr 12, 2017
cbb3cb0
Merged master; adapted code to use better TypeQuery
sixolet Apr 12, 2017
da0d936
More tests for nested and multi-type-var situations
sixolet Apr 12, 2017
edbecb0
Fixed type variable scoping rules to conform to PEP484 better
sixolet Apr 14, 2017
21d8b13
Move the typevars that are prohibited due to being bound by the class…
sixolet Apr 17, 2017
214aebc
More doc
sixolet Apr 17, 2017
5405646
Jukka comments
sixolet Apr 20, 2017
a25e926
one more
sixolet Apr 20, 2017
4aaab6c
get imports right
sixolet Apr 20, 2017
fd153ad
Merge branch 'master' into second-order-decorator
sixolet Apr 20, 2017
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Prev Previous commit
Next Next commit
Move the typevars that are prohibited due to being bound by the class…
… into TypeVarScope

This allows us to be more consistent about which type variables we allow and
which we don't.
  • Loading branch information
sixolet committed Apr 17, 2017
commit 21d8b138dc0890e5ea29ad77b5aab6b6b688fdea
45 changes: 7 additions & 38 deletions mypy/semanal.py
Original file line number Diff line number Diff line change
Expand Up @@ -189,10 +189,6 @@ class SemanticAnalyzer(NodeVisitor):
bound_tvars = None # type: List[SymbolTableNode]
# Type variables bound by the current scope, be it class or function
tvar_scope = None # type: TypeVarScope
# Type variables bound by current function/method
function_tvar_scope = None # type: TypeVarScope
# Stack of type variable scopes bound by current class. Only the last is active.
classdef_tvar_scopes = None # type: List[TypeVarScope]
# Per-module options
options = None # type: Options

Expand Down Expand Up @@ -227,8 +223,6 @@ def __init__(self,
self.type = None
self.type_stack = []
self.tvar_scope = TypeVarScope()
self.function_tvar_scope = self.tvar_scope
self.classdef_tvar_scopes = []
self.function_stack = []
self.block_depth = [0]
self.loop_depth = 0
Expand Down Expand Up @@ -429,7 +423,7 @@ def update_function_type_variables(self, fun_type: CallableType, defn: FuncItem)
Update the signature of defn to contain type variable definitions
if defn is generic.
"""
with self.tvar_scope_frame():
with self.tvar_scope_frame(self.tvar_scope.method_frame()):
a = self.type_analyzer()
fun_type.variables = a.bind_function_type_variables(fun_type, defn)

Expand Down Expand Up @@ -547,7 +541,7 @@ def analyze_property_with_multi_part_definition(self, defn: OverloadedFuncDef) -

def analyze_function(self, defn: FuncItem) -> None:
is_method = self.is_class_scope()
with self.tvar_scope_frame():
with self.tvar_scope_frame(self.tvar_scope.method_frame()):
if defn.type:
self.check_classvar_in_signature(defn.type)
assert isinstance(defn.type, CallableType)
Expand Down Expand Up @@ -627,7 +621,7 @@ def visit_class_def(self, defn: ClassDef) -> None:

@contextmanager
def analyze_class_body(self, defn: ClassDef) -> Iterator[bool]:
with self.classdef_tvar_scope_frame():
with self.tvar_scope_frame(self.tvar_scope.class_frame()):
self.clean_up_bases_and_infer_type_variables(defn)
if self.analyze_typeddict_classdef(defn 8000 ):
yield False
Expand Down Expand Up @@ -762,7 +756,7 @@ def clean_up_bases_and_infer_type_variables(self, defn: ClassDef) -> None:
del defn.base_type_exprs[i]
tvar_defs = [] # type: List[TypeVarDef]
for name, tvar_expr in declared_tvars:
tvar_defs.append(self.tvar_scope.bind_class_tvar(name, tvar_expr))
tvar_defs.append(self.tvar_scope.bind(name, tvar_expr))
defn.type_vars = tvar_defs

def analyze_typevar_declaration(self, t: Type) -> Optional[TypeVarList]:
Expand Down Expand Up @@ -791,12 +785,9 @@ def analyze_unbound_tvar(self, t: Type) -> Tuple[str, TypeVarExpr]:
sym = self.lookup_qualified(unbound.name, unbound)
if sym is None or sym.kind != TVAR:
return None
elif self.tvar_scope.get_binding(sym):
elif not self.tvar_scope.allow_binding(sym.fullname):
# It's bound by our type variable scope
return None
elif any(s.get_binding(sym) for s in self.classdef_tvar_scopes):
# It's bound by some outer class's type variable scope
return None
else:
assert isinstance(sym.node, TypeVarExpr)
return unbound.name, sym.node
Expand Down Expand Up @@ -3032,33 +3023,11 @@ def visit_await_expr(self, expr: AwaitExpr) -> None:
#

@contextmanager
def tvar_scope_frame(self) -> Generator:
"""Prepare our TypeVarScope for a function or method"""
old_scope = self.tvar_scope
old_function_scope = self.function_tvar_scope
self.tvar_scope = TypeVarScope(self.tvar_scope)
self.function_tvar_scope = self.tvar_scope
yield
self.function_tvar_scope = old_function_scope
self.tvar_scope = old_scope

@contextmanager
def classdef_tvar_scope_frame(self) -> Generator:
"""Prepare our TypeVarScope stack for a newly-declared class.

The scope for an inner class is based on the surrounding function
TypeVarScope, not the surrounding class's -- but it's still an error to
try and bind the same variable as the surrounding class, so we keep a
stack of those around.
"""
old_class_scope = self.classdef_tvar_scopes
def tvar_scope_frame(self, frame: TypeVarScope) -> Generator:
old_scope = self.tvar_scope
new_scope = TypeVarScope(self.function_tvar_scope)
self.classdef_tvar_scopes = old_class_scope + [new_scope]
self.tvar_scope = new_scope
self.tvar_scope = frame
yield
self.tvar_scope = old_scope
self.classdef_tvar_scopes = old_class_scope

def lookup(self, name: str, ctx: Context) -> SymbolTableNode:
"""Look up an unqualified name in all active namespaces."""
Expand Down
50 changes: 40 additions & 10 deletions mypy/tvar_scope.py
Original file line number Diff line number Diff line change
Expand Up @@ -4,28 +4,58 @@


class TypeVarScope:
"""Scope that holds bindings for type variables. Node fullname -> TypeVarDef.
"""Scope that holds bindings for type variables. Node fullname -> TypeVarDef."""

Provide a parent on intitalization when you want to make a child scope."""
def __init__(self,
parent: Optional['TypeVarScope'] = None,
is_class_scope: bool = False,
prohibited: Optional['TypeVarScope'] = None) -> None:
"""Initializer for TypeVarScope

def __init__(self, parent: Optional['TypeVarScope'] = None) -> None:
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
  ...

is_class_scope: True if this represents a generic class
prohibited: Type variables that aren't strictly in scope exactly,
but can't be bound because they're part of an outer class's scope.
"""
self.scope = {} # type: Dict[str, TypeVarDef]
self.parent = parent
self.func_id = 0
self.class_id = 0
self.is_class_scope = is_class_scope
self.prohibited = prohibited
if parent is not None:
self.func_id = parent.func_id
self.class_id = parent.class_id

def bind_fun_tvar(self, name: str, tvar_expr: TypeVarExpr) -> TypeVarDef:
self.func_id -= 1
return self._bind(name, tvar_expr, self.func_id)
def get_function_scope(self) -> Optional['TypeVarScope']:
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.

it = it.parent
return it

def bind_class_tvar(self, name: str, tvar_expr: TypeVarExpr) -> TypeVarDef:
self.class_id += 1
return self._bind(name, tvar_expr, self.class_id)
def allow_binding(self, fullname: str) -> bool:
if fullname in self.scope:
return False
elif self.parent and not self.parent.allow_binding(fullname):
return False
elif self.prohibited and not self.prohibited.allow_binding(fullname):
return False
return True

def method_frame(self) -> 'TypeVarScope':
return TypeVarScope(self, False, None)

def _bind(self, name: str, tvar_expr: TypeVarExpr, i: int) -> TypeVarDef:
def class_frame(self) -> 'TypeVarScope':
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.


def bind(self, name: str, tvar_expr: TypeVarExpr) -> TypeVarDef:
if self.is_class_scope:
self.class_id += 1
i = self.class_id
else:
self.func_id -= 1
i = self.func_id
tvar_def = TypeVarDef(
name, i, values=tvar_expr.values,
upper_bound=tvar_expr.upper_bound, variance=tvar_expr.variance,
Expand Down
9 changes: 6 additions & 3 deletions mypy/typeanal.py
Original file line number Diff line number Diff line change
Expand Up @@ -408,7 +408,7 @@ def analyze_callable_type(self, t: UnboundType) -> Type:
@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?

old_scope = self.tvar_scope
self.tvar_scope = TypeVarScope(self.tvar_scope)
self.tvar_scope = self.tvar_scope.method_frame()
yield
self.tvar_scope = old_scope

Expand Down Expand Up @@ -437,16 +437,19 @@ def bind_function_type_variables(self,
for var in fun_type.variables:
var_expr = self.lookup(var.name, var).node
assert isinstance(var_expr, TypeVarExpr)
self.tvar_scope.bind_fun_tvar(var.name, var_expr)
self.tvar_scope.bind(var.name, var_expr)
return fun_type.variables
typevars = self.infer_type_variables(fun_type)
# Do not define a new type variable if already defined in scope.
typevars = [(name, tvar) for name, tvar in typevars
if not self.is_defined_type_var(name, defn)]
defs = [] # type: List[TypeVarDef]
for name, tvar in typevars:
self.tvar_scope.bind_fun_tvar(name, tvar)
if not self.tvar_scope.allow_binding(tvar.fullname()):
self.fail("Type variable '{}' is bound by an outer class".format(name), defn)
self.tvar_scope.bind(name, tvar)
defs.append(self.tvar_scope.get_binding(tvar.fullname()))

return defs

def is_defined_type_var(self, tvar: str, context: Context) -> bool:
Expand Down
12 changes: 11 additions & 1 deletion test-data/unit/check-generics.test
Original file line number Diff line number Diff line change
Expand Up @@ -1347,7 +1347,17 @@ class A(Generic[T]):
self.a = a
g(self.a)
g(n) # E: Argument 1 to "g" has incompatible type "int"; expected "T"
[out]

[case testFunctionInGenericInnerClassTypeVariable]
from typing import TypeVar, Generic

T = TypeVar('T')
class Outer(Generic[T]):
class Inner:
x: T # E: Invalid type "__main__.T"
def f(self, x: T) -> T: ... # E: Type variable 'T' is bound by an outer class
def g(self) -> None:
y: T # E: Invalid type "__main__.T"


-- Callable subtyping with generic functions
Expand Down
0