8000 Generic type aliases by ilevkivskyi · Pull Request #2378 · python/mypy · GitHub
[go: up one dir, main page]

Skip to content

Generic type aliases #2378

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 21 commits into from
Nov 3, 2016
Merged
Show file tree
Hide file tree
Changes from 1 commit
Commits
Show all changes
21 commits
Select commit Hold shift + click to select a range
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
Add tests, comments, and doctrings
  • Loading branch information
ilevkivskyi committed Nov 1, 2016
commit 56b7ae75963caeb45f87e6d44a98e9e76dc6b258
7 changes: 7 additions & 0 deletions mypy/checkexpr.py
Original file line number Diff line number Diff line change
Expand Up @@ -1376,12 +1376,15 @@ def visit_type_application(self, tapp: TypeApplication) -> Type:
return AnyType()

def visit_type_alias_expr(self, alias: TypeAliasExpr) -> Type:
""" Get type of a type alias (could be generic) in a runtime expression."""
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

No space before first word of sentence.

item = alias.type
if isinstance(item, (Instance, TupleType, UnionType, CallableType)):
item = self.replace_tvars_any(item)
if isinstance(item, Instance):
tp = type_object_type(item.type, self.named_type)
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I'd like a comment here explaining that we'll generally get a callable type (or overloaded) with .is_type_obj() true, representing the class's constructor. This would explain that below we really just check for callable and overloaded.

else:
# TODO: Better error reporting, need to find line for
# unsubscribed generic aliases
if alias.line > 0:
self.chk.fail('Invalid type alias in runtime expression: {}'
.format(item), alias)
Expand All @@ -1393,6 +1396,10 @@ def visit_type_alias_expr(self, alias: TypeAliasExpr) -> Type:
return AnyType()
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

It's not an error because we can only get here when there already was a previous error, right?

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@gvanrossum Yes, I think so. At least I don't see a way to get there without previously having an error.


def replace_tvars_any(self, tp: Type) -> Type:
""" Replace all unbound type variables with Any if an alias is used in
a runtime expression. Basically, this function finishes what could not be done
in similar funtion from typeanal.py.
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Mind naming that similar function by name? (Also, typo -- funtion.)

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@gvanrossum Yes, sure :-)

"""
if not isinstance(tp, (Instance, UnionType, TupleType, CallableType)):
Copy link
Member

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.

Copy link
Member Author

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.

return tp
typ_args = (tp.args if isinstance(tp, Instance) else
Expand Down
11 changes: 11 additions & 0 deletions mypy/typeanal.py
Original file line number Diff line number Diff line change
Expand Up @@ -158,6 +158,8 @@ def visit_unbound_type(self, t: UnboundType) -> Type:
if exp_len == 0 and act_len == 0:
return override
if act_len != exp_len:
# TODO: Detect wrong type variable numer for unused aliases
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Typo: numer.

# (although it could be difficult at this stage, see comment below)
self.fail('Bad number of arguments for type alias, expected: %s, given: %s'
% (exp_len, act_len), t)
return t
Expand Down Expand Up @@ -203,6 +205,9 @@ def visit_unbound_type(self, t: UnboundType) -> Type:
return AnyType()

def get_type_var_names(self, tp: Type) -> List[str]:
""" Get all type variable names that are present in a generic type alias
in order of textual appearance (recursively, if needed).
"""
tvars = [] # type: List[str]
if not isinstance(tp, (Instance, UnionType, TupleType, CallableType)):
return tvars
Expand Down Expand Up @@ -235,6 +240,9 @@ def get_tvar_name(self, t: Type) -> Optional[str]:
return None

def replace_alias_tvars(self, tp: Type, vars: List[str], subs: List[Type]) -> Type:
""" Replace type variables in a generic type alias tp with substitutions subs.
Length of subs should be already checked.
"""
if not isinstance(tp, (Instance, UnionType, TupleType, CallableType)) or not subs:
return tp
typ_args = (tp.args if isinstance(tp, Instance) else
Expand All @@ -244,9 +252,12 @@ def replace_alias_tvars(self, tp: Type, vars: List[str], subs: List[Type]) -> Ty
for i, arg in enumerate(typ_args):
tvar = self.get_tvar_name(arg)
if tvar and tvar in vars:
# Perform actual substitution...
new_args[i] = subs[vars.index(tvar)]
else:
# ...recursively, if needed.
new_args[i] = self.replace_alias_tvars(arg, vars, subs)
# Create a copy with type vars replaced.
if isinstance(tp, Instance):
return Instance(tp.type, new_args, tp.line)
if isinstance(tp, TupleType):
Expand Down
31 changes: 26 additions & 5 deletions test-data/unit/check-generics.test
Original file line number Diff line number Diff line change
Expand Up @@ -794,13 +794,34 @@ fun2([('x', 'x')], 'x') # E: Type argument 1 of "fun2" has incompatible value "s
[builtins fixtures/list.pyi]

[case testGenericTypeAliasesImporting]
from typing import TypeVar, Generic
from typing import TypeVar
from a import Node, TupledNode
T = TypeVar('T')
class Node(Generic[T]):
def __init__(self, x: T) -> None:
...

[out]
n = None # type: TupledNode[int]
n.x = 1
n.y = (1, 1)
n.y = 'x' # E: Bad type

def f(x: Node[T, T]) -> TupledNode[T]:
return Node(x.x, (x.x, x.x))

f(1) # E: Argument 1 to "f" has incompatible type "int"; expected Node[None, None]
f(Node(1, 'x')) # E: Cannot infer type argument 1 of "f"
reveal_type(Node('x', 'x')) # E: Revealed type is 'a.Node[builtins.str*, builtins.str*]'

[file a.py]
from typing import TypeVar, Generic, Tuple
T = TypeVar('T')
S = TypeVar('S')
class Node(Generic[T, S]):
def __init__(self, x: T, y: S) -> None:
self.x = x
self.y = y

TupledNode = Node[T, Tuple[T, T]]

[builtins fixtures/list.pyi]

[case testGenericTypeAliasesRuntimeExpressionsInstance]
from typing import TypeVar, Generic
Expand Down
0