Join GitHub today
GitHub is home to over 40 million developers working together to host and review code, manage projects, and build software together.
Sign upRefine parent type when narrowing "lookup" expressions #7917
Conversation
This diff adds support for the following pattern: ```python from typing import Enum, List from enum import Enum class Key(Enum): A = 1 B = 2 class Foo: key: Literal[Key.A] blah: List[int] class Bar: key: Literal[Key.B] something: List[str] x: Union[Foo, Bar] if x.key is Key.A: reveal_type(x) # Revealed type is 'Foo' else: reveal_type(x) # Revealed type is 'Bar' ``` In short, when we do `x.key is Key.A`, we "propagate" the information we discovered about `x.key` up one level to refine the type of `x`. We perform this propagation only when `x` is a Union and only when we are doing member or index lookups into instances, typeddicts, namedtuples, and tuples. For indexing operations, we have one additional limitation: we *must* use a literal expression in order for narrowing to work at all. Using Literal types or Final instances won't work; See #7905 for more details. To put it another way, this adds support for tagged unions, I guess. This more or less resolves #7344. We currently don't have support for narrowing based on string or int literals, but that's a separate issue and should be resolved by #7169 (which I resumed work on earlier this week).
This comment has been minimized.
This comment has been minimized.
A few more notes:
|
Thanks! This is a really great feature (and will be especially useful for people familiar with TypeScript). I just have one larger comment, and few small ones. |
reveal_type(x.key) # N: Revealed type is 'Literal[__main__.Key.A]' | ||
reveal_type(x) # N: Revealed type is 'Union[__main__.Object1, Any]' | ||
else: | ||
# TODO: Is this a bug? Should we skip inferring Any for singleton types? |
This comment has been minimized.
This comment has been minimized.
ilevkivskyi
Nov 11, 2019
Collaborator
I think we have some special-casing for None
, but generally yes, unions with Any
and singletons is indeed problematic.
@@ -21,7 +21,8 @@ class function: pass | |||
class ellipsis: pass | |||
|
|||
# We need int and slice for indexing tuples. | |||
class int: pass | |||
class int: | |||
def __neg__(self) -> 'int': pass |
This comment has been minimized.
This comment has been minimized.
ilevkivskyi
Nov 11, 2019
Collaborator
I assume you need this for a test like if isinstance(t[-1], int): ...
, but I can't find this test.
This comment has been minimized.
This comment has been minimized.
Michael0x2a
Nov 11, 2019
Author
Collaborator
This was because of the one-line change in checkexpr.py
: since we're deducing a type for index expressions now, a few unrelated tests about tuples and negative indices started failing.
if isinstance(expr, StrExpr): | ||
return [expr.value] | ||
|
||
# TODO: See if we can eliminate this function and call the below one directly |
This comment has been minimized.
This comment has been minimized.
This comment has been minimized.
This comment has been minimized.
This comment has been minimized.
This comment has been minimized.
# Next, try using this information to refine the parent type, if applicable. | ||
# Note that we currently refine just the immediate parent. | ||
# | ||
# TODO: Should we also try recursively refining any parents of the parents? |
This comment has been minimized.
This comment has been minimized.
ilevkivskyi
Nov 11, 2019
Collaborator
I think we should. This was one of the first things I thought of when looking at the PR. Supporting only immediate parents may look arbitrary to a user and potentially cause confusions. Imagine a simple situation:
class Model(Generic[T]):
attr: T
class A:
model: Model[int]
class B:
model: Model[str]
x: Union[A, B]:
if isinstance(x.model.attr, int):
... # I would want 'x' to be an 'A' here
I think we should just cycle up until the expression is NameExpr
(IIRC all bindable expressions root there). I don't think this will cause any performance impact (unless you have a proof of the opposite).
This comment has been minimized.
This comment has been minimized.
@ilevkivskyi -- Ok, this should be ready for a second look! I added support for cycling up all parents, as suggested. |
Thanks for updates! Here are few more suggestions. |
from mypy import state, errorcodes as codes | ||
from mypy.traverser import has_return_statement, all_return_statements | ||
from mypy.errorcodes import ErrorCode | ||
|
||
T = TypeVar('T') | ||
|
||
T_contra = TypeVar('T_contra', contravariant=True) |
This comment has been minimized.
This comment has been minimized.
We perform this kind of "parent narrowing" for member lookup expressions and indexing | ||
expressions into tuples, namedtuples, and typeddicts. This narrowing is also performed | ||
only once, for the immediate parents of any "lookup" expressions in `new_types`. |
This comment has been minimized.
This comment has been minimized.
reveal_type(x.key) # N: Revealed type is 'Union[Any, Literal[__main__.Key.B]]' | ||
reveal_type(x) # N: Revealed type is 'Union[__main__.Object1, __main__.Object2, Any]' | ||
|
||
[case testNarrowingParentsHierarchy] |
This comment has been minimized.
This comment has been minimized.
ilevkivskyi
Nov 11, 2019
Collaborator
Just in case I would add a test involving "double index" like if x['model']['kind'] is ...: ...
for nested typed dicts.
This comment has been minimized.
This comment has been minimized.
@Michael0x2a also note mypyc build failed. |
Michael0x2a commentedNov 9, 2019
This diff adds support for the following pattern:
In short, when we do
x.key is Key.A
, we "propagate" the information we discovered aboutx.key
up one level to refine the type ofx
.We perform this propagation only when
x
is a Union and only when we are doing member or index lookups into instances, typeddicts, namedtuples, and tuples. For indexing operations, we have one additional limitation: we must use a literal expression in order for narrowing to work at all. Using Literal types or Final instances won't work; See #7905 for more details.To put it another way, this adds support for tagged unions, I guess.
This more or less resolves #7344. We currently don't have support for narrowing based on string or int literals, but that's a separate issue and should be resolved by #7169 (which I resumed work on earlier this week).