-
-
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
More principled approach for callable vs callable inference #15910
Changes from all commits
13ff43d
3898f79
4fe5688
1f3af72
1dadd7d
cb35b17
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -590,6 +590,7 @@ def check_mixed( | |
): | ||
nominal = False | ||
else: | ||
# TODO: everywhere else ParamSpecs are handled as invariant. | ||
if not check_type_parameter( | ||
lefta, righta, COVARIANT, self.proper_subtype, self.subtype_context | ||
): | ||
|
@@ -666,13 +667,12 @@ def visit_unpack_type(self, left: UnpackType) -> bool: | |
return False | ||
|
||
def visit_parameters(self, left: Parameters) -> bool: | ||
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 commentThe 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 commentThe reason will be displayed to describe this comment to others. Learn more. We should normally never compare |
||
if isinstance(self.right, Parameters): | ||
# TODO: direction here should be opposite, this function expects | ||
# order of callables, while parameters are contravariant. | ||
return are_parameters_compatible( | ||
left, | ||
right, | ||
self.right, | ||
is_compat=self._is_subtype, | ||
ignore_pos_arg_names=self.subtype_context.ignore_pos_arg_names, | ||
) | ||
|
@@ -723,14 +723,6 @@ def visit_callable_type(self, left: CallableType) -> bool: | |
elif isinstance(right, TypeType): | ||
# This is unsound, we don't check the __init__ signature. | ||
return left.is_type_obj() and self._is_subtype(left.ret_type, right.item) | ||
elif isinstance(right, Parameters): | ||
# this doesn't check return types.... but is needed for is_equivalent | ||
return are_parameters_compatible( | ||
left.with_unpacked_kwargs(), | ||
right, | ||
is_compat=self._is_subtype, | ||
ignore_pos_arg_names=self.subtype_context.ignore_pos_arg_names, | ||
) | ||
else: | ||
return False | ||
|
||
|
@@ -1456,7 +1448,6 @@ def g(x: int) -> int: ... | |
right, | ||
is_compat=is_compat, | ||
ignore_pos_arg_names=ignore_pos_arg_names, | ||
check_args_covariantly=check_args_covariantly, | ||
allow_partial_overlap=allow_partial_overlap, | ||
strict_concatenate_check=strict_concatenate_check, | ||
) | ||
|
@@ -1480,7 +1471,6 @@ def are_parameters_compatible( | |
*, | ||
is_compat: Callable[[Type, Type], bool], | ||
ignore_pos_arg_names: bool = False, | ||
check_args_covariantly: bool = False, | ||
allow_partial_overlap: bool = False, | ||
strict_concatenate_check: bool = False, | ||
) -> bool: | ||
|
@@ -1534,7 +1524,7 @@ def _incompatible(left_arg: FormalArgument | None, right_arg: FormalArgument | N | |
|
||
# Phase 1b: Check non-star args: for every arg right can accept, left must | ||
# also accept. The only exception is if we are allowing partial | ||
# partial overlaps: in that case, we ignore optional args on the right. | ||
# overlaps: in that case, we ignore optional args on the right. | ||
for right_arg in right.formal_arguments(): | ||
left_arg = mypy.typeops.callable_corresponding_argument(left, right_arg) | ||
if left_arg is None: | ||
|
@@ -1548,7 +1538,7 @@ def _incompatible(left_arg: FormalArgument | None, right_arg: FormalArgument | N | |
|
||
# Phase 1c: Check var args. Right has an infinite series of optional positional | ||
# arguments. Get all further positional args of left, and make sure | ||
# they're more general then the corresponding member in right. | ||
# they're more general than the corresponding member in right. | ||
if right_star is not None: | ||
# Synthesize an anonymous formal argument for the right | ||
right_by_position = right.try_synthesizing_arg_from_vararg(None) | ||
|
@@ -1575,7 +1565,7 @@ def _incompatible(left_arg: FormalArgument | None, right_arg: FormalArgument | N | |
|
||
# Phase 1d: Check kw args. Right has an infinite series of optional named | ||
# arguments. Get all further named args of left, and make sure | ||
# they're more general then the corresponding member in right. | ||
# they're more general than the corresponding member in right. | ||
if right_star2 is not None: | ||
right_names = {name for name in right.arg_names if name is not None} | ||
left_only_names = set() | ||
|
@@ -1643,6 +1633,10 @@ def are_args_compatible( | |
allow_partial_overlap: bool, | ||
is_compat: Callable[[Type, Type], bool], | ||
) -> bool: | ||
if left.required and right.required: | ||
# If both arguments are required allow_partial_overlap has no effect. | ||
allow_partial_overlap = False | ||
|
||
def is_different(left_item: object | None, right_item: object | None) -> bool: | ||
"""Checks if the left and right items are different. | ||
|
||
|
@@ -1670,7 +1664,7 @@ def is_different(left_item: object | None, right_item: object | None) -> bool: | |
|
||
# If right's argument is optional, left's must also be | ||
# (unless we're relaxing the checks to allow potential | ||
# rather then definite compatibility). | ||
# rather than definite compatibility). | ||
if not allow_partial_overlap and not right.required and left.required: | ||
return False | ||
|
||
|
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -1545,9 +1545,6 @@ class FormalArgument(NamedTuple): | |
required: bool | ||
|
||
|
||
# TODO: should this take bound typevars too? what would this take? | ||
# ex: class Z(Generic[P, T]): ...; Z[[V], V] | ||
# What does a typevar even mean in this context? | ||
class Parameters(ProperType): | ||
"""Type that represents the parameters to a function. | ||
|
||
|
@@ -1559,6 +1556,8 @@ class Parameters(ProperType): | |
"arg_names", | ||
"min_args", | ||
"is_ellipsis_args", | ||
# TODO: variables don't really belong here, but they are used to allow hacky support | ||
# for forall . Foo[[x: T], T] by capturing generic callable with ParamSpec, see #15909 | ||
"variables", | ||
) | ||
|
||
|
@@ -1602,7 +1601,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 commentThe 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 commentThe reason will be displayed to describe this comment to others. Learn more. There are three possible ways to fix this:
Ideally I think we should try second option, measure performance, if it is OK, go with it, if it is bad, try option 3. |
||
def var_arg(self) -> FormalArgument | None: | ||
"""The formal argument for *args.""" | ||
for position, (type, kind) in enumerate(zip(self.arg_types, self.arg_kinds)): | ||
|
@@ -2046,7 +2045,6 @@ def param_spec(self) -> ParamSpecType | None: | |
return arg_type.copy_modified(flavor=ParamSpecFlavor.BARE, prefix=prefix) | ||
|
||
def expand_param_spec(self, c: Parameters) -> CallableType: | ||
# TODO: try deleting variables from Parameters after new type inference is default. | ||
variables = c.variables | ||
return self.copy_modified( | ||
arg_types=self.arg_types[:-2] + c.arg_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'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.