-
-
Notifications
You must be signed in to change notification settings - Fork 2.9k
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
Add support for operators with union operands #5545
Add support for operators with union operands #5545
Conversation
This pull request resolves python#2128 -- it modifies how we check operators to add support for operations like `Union[int, float] + Union[int, float]`. This approach basically iterates over all possible variations of the left and right operands when they're unions and uses the union of the resulting inferred type as the type of the overall expression. Some implementation notes: 1. I attempting "destructuring" just the left operand, which is basically the approach proposed here: python#2128 (comment) Unfortunately, I discovered it became necessary to also destructure the right operand to handle certain edge cases -- see the testOperatorDoubleUnionInterwovenUnionAdd test case. 2. This algorithm varies slightly from what we do for union math in that we don't attempt to "preserve" the union/we always destructure both operands. I'm fairly confident that this is type-safe; I plan on testing this pull request against some internal code bases to help us gain more confidence.
@ilevkivskyi -- this is ready for review whenever, I think. (No rush though -- I probably won't have time to follow-up on any feedback until this weekend anyways). I've also confirmed that this PR introduces no new errors in our internal codebases. |
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 looks very good. I have only few suggestions.
mypy/checkexpr.py
Outdated
# type inference errors -- e.g. see 'testOperatorDoubleUnionSum'. | ||
right_variants = [(right_type, arg)] | ||
if isinstance(right_type, UnionType): | ||
right_variants = [(item, TempNode(item)) for item in right_type.relevant_items()] |
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.
Could it be that all these things will be simplified if you use
with self.type_overrides_set(args, arg_types): ...
?
I remember similar situations from union overloads, where an argument expression once gets in type map, and then you see weird errors.
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.
Huh, that's a good point -- it didn't occur to me until you pointed it out that type_overrides_set
is trying to solve the same problem.
I think the overall level of complexity is going to be about the same though -- e.g. if I switch to using type_overrides_set
, I'd be able to simplify this list comprehension a bit but would need to add the with block to the doubly nested loop below.
I decided to keep the code the same for now mostly because I didn't want to have to worry about what would happen if you tried doing union math things on an overloaded operator. But if you think it's worth switching over to the other approach for consistency, LMK.
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 it is OK. It is probably safer because a more robust implementation of type_overrides_set
should allow "stacking" the overrides. But it is a separate topic, we can do this later if this pattern will be needed elsewhere.
@@ -755,7 +755,8 @@ class Node(Generic[T]): | |||
UNode = Union[int, Node[T]] | |||
x = 1 # type: UNode[int] | |||
|
|||
x + 1 # E: Unsupported left operand type for + (some union) | |||
x + 1 # E: Unsupported left operand type for + ("Node[int]") \ | |||
# N: Left operand is of type "Union[int, Node[int]]" |
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 love these new error messages!
mypy/checkexpr.py
Outdated
self.msg.warn_operand_was_from_union("Right", right_type, context) | ||
|
||
results_final = UnionType.make_simplified_union(all_results) | ||
inferred_final = UnionType.make_simplified_union(all_inferred) |
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 this may be technically not true (although I don't think it is ever used currently). See for example this comment:
# Note that we use `union_overload_matches` instead of just returning
# a union of inferred callables because for example a call
# Union[int -> int, str -> str](Union[int, str]) is invalid and
# we don't want to introduce internal inconsistencies.
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 switched over to using union_overload_matches
btw (and renamed the method, since we're now using it for things other than overloads).
@ilevkivskyi -- Sorry for the delay! This is ready for another look whenever. |
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, looks good now!
@Michael0x2a, @ilevkivskyi
running mypy results in We can use Union[int, float] as type instead of |
Your concrete example doesn't need typevars, because each of S and T is used only once. Why don't you just use aliases? |
@JelleZijlstra I've quoted the workaround as well. Edited the code above so return type is dependent on one of the the input types. The problem is in the line |
@appraveen -- you should file a new issue for this and link to this pull request rather then commenting here. This pull request has already been merged in some time ago, which means it's considered "done" or "finished": so any discussion that happens here will probably be lost or forgotten. Issues are the way we keep track of things that people want/that we need to do. |
This pull request resolves #2128 -- it modifies how we check operators to add support for operations like
Union[int, float] + Union[int, float]
.This approach basically iterates over all possible variations of the left and right operands when they're unions and uses the union of the resulting inferred type as the type of the overall expression.
A few implementation notes:
This PR starts by first destructuring only the left operand (and leaving the right operand alone) in a manner very similar to what's described here: Mypy fails on
Union[float, int] + Union[float, int]
#2128 (comment). If that triggers an error, it retries but also destructures the right operand.It unfortunately turned out that just attempting to do one or the other leads to failures in different edge cases -- destructuring just the left operand causes the testOperatorDoubleUnionInterwovenUnionAdd test case to fail; immediately jumping to destructuring both operands causes the testOperatorWithEmptyListAndSum test case to fail.
This will almost certainly cause some sort of performance regression, but I haven't yet benchmarked how severe it is.
This PR also makes the error messages when the left operand is a union more explicit (and a little more verbose). I hope that's ok -- I don't think there's an easy way of getting around that/keeping the old error messages.