-
-
Notifications
You must be signed in to change notification settings - Fork 2.9k
[OLD] Add more robust support for detecting partially overlapping types #5280
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
[OLD] Add more robust support for detecting partially overlapping types #5280
Conversation
This pull request adds more robust support for detecting partially overlapping types. Specifically, it detects overlaps with... 1. TypedDicts 2. Tuples 3. Unions 4. Typevars 5. Generic types containing variations of the above
Some additional 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! I have some minor comments, and few larger comments:
- It seems to me there are too many functions:
is_overlapping
,is_partially_overlapping
, andis_overlapping_erased
. So the question do we need the one that checks for partial overlap? It seems to me the logic is as following: we first check for unsafe overlap because first arg types are subtypes of second arg types, then we check for "will never match" (the opposite), at the third step we can only get partial overlaps. So it is fine to have a simpler function that doesn't care whether overlap is partial or full. I think this can allow to simplify the code and also we will have only two functions: normal overlap and erased overlap. - You renamed existing function and added a new function with old name, I think it would be safer to keep the name of existing function, and choose a new name for the new function (
is_overlapping_nonerased
?). - It seems to me the erased one is not so erased, in the sense it doesn't match the comment that says it corresponds to runtime behavior (e.g. because of tuples). But it is probably not for this PR.
mypy/checker.py
Outdated
@@ -3630,37 +3633,119 @@ def are_argument_counts_overlapping(t: CallableType, s: CallableType) -> bool: | |||
|
|||
def is_unsafe_overlapping_overload_signatures(signature: CallableType, | |||
other: CallableType) -> bool: | |||
"""Check if two overloaded function signatures may be unsafely overlapping. | |||
"""Check if two overloaded signatures are unsafely overlapping, ignoring partial overlaps. | |||
|
|||
We consider two functions 's' and 't' to be unsafely overlapping both if |
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.
"both if" -> "if both"
mypy/checker.py
Outdated
other: CallableType) -> bool: | ||
"""Check if two overloaded signatures are unsafely overlapping, ignoring partial overlaps. | ||
|
||
We consider two functions 's' and 't' to be unsafely overlapping both if |
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.
"both if" -> "if both" here too.
mypy/checker.py
Outdated
return typ.copy_modified(variables=new_variables) | ||
|
||
|
||
class TypeVarExtractor(TypeQuery[Set[TypeVarType]]): |
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.
We already have TypeVariableQuery
in typeanal.py
, so I am thinking maybe we can combine these two (maybe there are other). Or at least move these visitors to a separate utility module.
mypy/meet.py
Outdated
@@ -49,41 +53,175 @@ def narrow_declared_type(declared: Type, narrowed: Type) -> Type: | |||
return narrowed | |||
|
|||
|
|||
def is_partially_overlapping_types(t: Type, s: Type) -> bool: | |||
"""Returns 'true' if the two types are partially, but not completely, overlapping. | |||
def get_possible_variants(typ: Type) -> List[Type]: |
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 function deserves a docstring (in particular it is not clear at this point why we need both typ.items
and typ
).
mypy/meet.py
Outdated
return typ.items + [typ] | ||
elif isinstance(typ, Overloaded): | ||
# Note: doing 'return typ.items() + [typ]' makes mypy | ||
# infer a too-specific return type of List[CallableType] |
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 a bug actually, mypy shuould have used the return context List[Type]
to correctly infer the type (or maybe the signature of list.__add__
is not good enough?) Could you please open an issue with a simple repro?
mypy/meet.py
Outdated
return isinstance(t, Instance) and t.type.fullname() == 'builtins.object' | ||
def is_partially_overlapping(left: Type, right: Type) -> bool: | ||
# We should never encounter these types | ||
illegal_types = (UnboundType, PartialType, ErasedType, DeletedType) |
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.
UnboundType
should be definitely not in this list. It is known that semanal.py
"leaks" some of those. Also I am not very keen on adding this assert. Maybe we can do this later when we are more sure about this code.
@@ -920,6 +920,7 @@ def unify_generic_callable(type: CallableType, target: CallableType, | |||
c = mypy.constraints.infer_constraints( | |||
type.ret_type, target.ret_type, return_constraint_direction) | |||
constraints.extend(c) | |||
|
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.
Stray whitespace?
# Same root issue: why does the additional constraint bound T <: T | ||
# cause the constraint solver to not infer T = object like it did in the | ||
# first example? | ||
@overload |
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 the root cause here is invariance, mypy adds two constrains from Dummy
: T' <: T
and T <: T'
(see code in ConstraintBuilder.visit_instance
), at least this is the only difference I see between two functions.
Note, I use prime to distinguish between two variables with the same name, assuming this comment in subtypes.py
:
# ...because generating and solving
# constraints for the variables of L to make L a subtype of R
# (below) treats type variables on the two sides as independent.
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.
@ilevkivskyi -- before I continue working on these TODOs, I just want to check in with you and see if this is something you think we should actually fix.
For context, here's a sample program -- we get the same output whether we're using mypy master or this PR:
from typing import *
T = TypeVar('T')
# Example 1: No error reported
@overload
def f1(x: int) -> int: ...
@overload
def f1(x: T) -> T: ...
def f1(*args, **kwargs): pass
# Example 2: Overload is reported as being unsafe
class Wrapper(Generic[T]):
@overload
def f3(self, x: int) -> int: ...
@overload
def f3(self, x: T) -> T: ...
def f3(self, *args, **kwargs): pass
# Example 3: Overload is reported as being unsafe
class Dummy(Generic[T]): pass
@overload
def f2(d: Dummy[T], x: int) -> int: ...
@overload
def f2(d: Dummy[T], x: T) -> T: ...
def f2(*args, **kwargs): pass
Basically, the issue is that example 1 typechecks (as expected) but example 2 doesn't, which seems like surprising and unintuitive behavior.
I think the root issue is that mypy internally represents example 2 in a way that's basically indistinguishable from example 3. Mypy then tries and fails to unify together the two generics due to the invariance issue you pointed out.
So, before I spend more time on this, I wanted to check in and ask a few questions:
-
Do you think example 3 is actually unsafely overlapping? I'm having some difficultly determining whether or not mypy is actually correct here.
(One thing to note is that the behavior of example 3 is technically undefined since the first overload is using just a single typevar. That said, modifying the signature to fix that doesn't change the error message.)
-
Do you think it's worth trying to modify mypy so it doesn't report an error on example 2?
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.
Here are my short opinions about two questions:
- I think the third example is fine (as well as other two).
- I think it is a bug, i.e. mypy should not report an error there, but it is probably a low-priority one. So I propose to just go ahead with whatever is easier and then we can try fixing this if someone will complain.
This commit is a WIP which I am pushing now for testing purposes. I will rebase/push another commit with more polishing/more exposition a bit later.
So, progress report:
|
I think this is fine, the next release is just 3-4 weeks away.
No problems at all! Thanks for all the work on overloads, this is already a big improvement. |
This is mostly so we can get python/typeshed#2338, which unblocks python#5370.
An unintended side-effect of silencing errors in site packages and typeshed by default is that our tests no longer flag changes to mypy that end up breaking typeshed in some way. (For example, see https://github.com/python/mypy/pulls/5280 which *really* should not be passing, at least as of time of writing.) This pull request will add the `--no-silence-site-packages` flag to any test suites that directly or indirectly test typeshed. Specifically, I believe this PR ought to add the flag to any tests triggered by `testsamples.py`, `testselfcheck.py`, and `testpythoneval.py`.
This PR gets a bit too large. Is it possible to split it in parts. Also I am not sure I understand how some changes are necessary (in particular all the changes to subtypes visitors). Is it possible to split this PR in parts (especially if it fixes unrelated bugs triggered by the main change)? Unfortunately it looks like I will not have time to review this before my vacation, but I will do this first thing I am back. |
@ilevkivskyi -- I can work on splitting up these changes, yeah. I agree that this is starting to become a bit large. That said, I don't think this is 100% ready to review yet. It looks like I need to fiddle around with operator overloads first in order to make these changes pass the tests, so I plan on submitting a separate PR for that first. Anyways, enjoy your vacation -- you definitely deserve one! And thank you for putting in the time to review/help me land these changes! |
Great idea!
Thanks! :-) (Although you did most of the hard work) |
Closing in favor of #5476 |
This pull request adds more robust support for detecting partially overlapping types. Specifically, it detects overlaps with...