-
-
Notifications
You must be signed in to change notification settings - Fork 2.8k
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
Refine 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 python#7905 for more details. To put it another way, this adds support for tagged unions, I guess. This more or less resolves python#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 python#7169 (which I resumed work on earlier this week).
A few more notes:
|
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.
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? |
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 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 |
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 assume you need this for a test like if isinstance(t[-1], int): ...
, but I can't find this test.
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 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 |
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.
What kind of problems does this cause?
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.
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.
OK, let's leave this out for a separate PR.
mypy/checker.py
Outdated
# 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? |
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 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).
@ilevkivskyi -- Ok, this should be ready for a second look! I added support for cycling up all parents, as suggested. |
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.
Thanks for updates! Here are few more suggestions.
mypy/checker.py
Outdated
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) |
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 don't think the new line above is needed.
mypy/checker.py
Outdated
|
||
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`. |
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 now outdated.
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] |
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.
Just in case I would add a test involving "double index" like if x['model']['kind'] is ...: ...
for nested typed dicts.
@Michael0x2a also note mypyc build failed. |
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).