-
-
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
More principled approach for callable vs callable inference #15910
More principled approach for callable vs callable inference #15910
Conversation
This comment has been minimized.
This comment has been minimized.
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.
Mostly questions!
mypy/constraints.py
Outdated
This function essentially extracts four steps from are_parameters_compatible() in | ||
subtypes.py that involve subtype checks between argument types. We keep the argument | ||
matching logic, but ignore various strictness flags present there, and checks that | ||
do not involve subtyping. Then in place of every subtype check we put an infer_constrains() |
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.
Typo
mypy/constraints.py
Outdated
# Numbering of steps below matches the one in are_parameters_compatible() for convenience. | ||
# Phase 1a: compare star vs star arguments. | ||
if left_star is not None and right_star is not None: | ||
res.extend(infer_directed_constraints(left_star.typ, right_star.typ, direction)) |
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 TODO in infer_directed_constraints
only applies here? (Unless TypeVarMapping or whatever gets added)
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.
Probably yes, but TBH I didn't think much about this.
"""Infer constraints between two arguments using direction between original callables.""" | ||
if isinstance(left, (ParamSpecType, UnpackType)) or isinstance( | ||
right, (ParamSpecType, UnpackType) | ||
): |
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'm not sure if this extra logic is necessary normally: the only place these can pop up is *args and **kwargs. Err well, maybe not given current lax validation 😅.
Also I think the function name should make incorporate "argument" somehow...?
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.
FWIW some tests failed without this, and we can never do anything useful anyway, so we can just always skip.
if isinstance(self.right, (Parameters, CallableType)): | ||
right = self.right | ||
if isinstance(right, CallableType): | ||
right = right.with_unpacked_kwargs() |
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 can't tell why you removed support for callable types here. I guess that might be handled elsewhere?
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 should normally never compare Callable
vs Parameters
, and if we do they can never be subtype of each other, in the same sense as int
is never a sub- (or super-) type of (int) -> int
.
@@ -1602,7 +1599,7 @@ def copy_modified( | |||
variables=variables if variables is not _dummy else self.variables, | |||
) | |||
|
|||
# the following are copied from CallableType. Is there a way to decrease code duplication? | |||
# TODO: here is a lot of code duplication with Callable type, fix this. |
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 guess I should write this historical note here in case it helps or something: I originally wanted to make every Callable have a Parameters attribute, meaning it doesn't hold its own args anymore. That seemed like too much work though! ... maybe these can inherit instead?
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 are three possible ways to fix this:
- Delete
Parameters
and useCallableType
instead. (But I don't like this way, because it would be a hack.) - Make
arg_types
etc. properties onCallableType
that forward values from itsparameters
attribute. This way I guess it will be much less work, but may have visible performance impact. - Indeed, both can inherit these methods from say
ArgumentHelpers
mixin.
Ideally I think we should try second option, measure performance, if it is OK, go with it, if it is bad, try option 3.
Looking at |
Yep, a simple repro not involving type vars: @overload
def d(x: int) -> int: ...
@overload
def d(f: int, *, x: int) -> str: ...
def d(x): ... Let's see if there is an easy fix to avoid regressions. |
This comment has been minimized.
This comment has been minimized.
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.
Great to have this finally working as expected! It's interesting how long we managed without proper types inference for callables. Looks good, just one comment about possible additional test cases.
# Note: order of names is different w.r.t. protocol | ||
def g(*, y: int, x: str) -> None: pass | ||
reveal_type(f(g)) # N: Revealed type is "Tuple[builtins.str, builtins.int]" | ||
[builtins fixtures/list.pyi] |
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.
Also test a few cases where we shouldn't be inferring constraints, e.g. positional-only vs keyword-only?
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, added such tests (note btw if there are no constraints, it now means they can never by subtypes, so there will be also an error in each test).
Diff from mypy_primer, showing the effect of this PR on open source code: steam.py (https://github.com/Gobot1234/steam.py)
+ steam/ext/commands/commands.py:909: error: Overloaded function implementation cannot satisfy signature 3 due to inconsistencies in how they use type variables [misc]
|
Fixes #702 (one of the oldest open issues)
The approach is quite simple, I essentially replicate the logic from subtyping check, while replacing each
is_subtype()
call withinfer_constraints()
call. Note that we don't have various options available inconstraints.py
so I use all checks, even those that may be skipped with some strictness flags (so we can infer as many constraints as possible). Depending on the output ofmypy_primer
we can try to tune this.Note that while I was looking at subtyping code, I noticed couple inconsistencies for ParamSpecs, I added TODOs for them (and updated some existing TODOs). I also deleted some code that should be dead code after my previous cleanup.
Among inconsistencies most notably, subtyping between
Parameters
uses wrong (opposite) direction. Normally,Parameters
entity behaves covariantly (w.r.t. types of individual arguments) as a single big argument, like a tuple plus a map. But then this entity appears in a contravariant position inCallable
. This is how we handle it inconstraints.py
,join.py
,meet.py
etc. I tried to fix the left/right order invisit_parameters()
, but then one test failed (and btw same test would also fail if I would try to fix variance invisit_instance()
). I decided to leave this for separate PR(s).cc @A5rocks