Skip to content

Fix previous partial fix #17429

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

Merged
merged 1 commit into from
Jun 23, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
34 changes: 26 additions & 8 deletions mypy/plugins/functools.py
Original file line number Diff line number Diff line change
Expand Up @@ -137,6 +137,20 @@ def partial_new_callback(ctx: mypy.plugin.FunctionContext) -> Type:
# We must normalize from the start to have coherent view together with TypeChecker.
fn_type = fn_type.with_unpacked_kwargs().with_normalized_var_args()

last_context = ctx.api.type_context[-1]
if not fn_type.is_type_obj():
# We wrap the return type to get use of a possible type context provided by caller.
# We cannot do this in case of class objects, since otherwise the plugin may get
# falsely triggered when evaluating the constructed call itself.
ret_type: Type = ctx.api.named_generic_type(PARTIAL, [fn_type.ret_type])
wrapped_return = True
else:
ret_type = fn_type.ret_type
# Instead, for class objects we ignore any type context to avoid spurious errors,
# since the type context will be partial[X] etc., not X.
ctx.api.type_context[-1] = None
wrapped_return = False

defaulted = fn_type.copy_modified(
arg_kinds=[
(
Expand All @@ -146,7 +160,7 @@ def partial_new_callback(ctx: mypy.plugin.FunctionContext) -> Type:
)
for k in fn_type.arg_kinds
],
ret_type=ctx.api.named_generic_type(PARTIAL, [fn_type.ret_type]),
ret_type=ret_type,
)
if defaulted.line < 0:
# Make up a line number if we don't have one
Expand Down Expand Up @@ -189,16 +203,20 @@ def partial_new_callback(ctx: mypy.plugin.FunctionContext) -> Type:
arg_names=actual_arg_names,
context=call_expr,
)
if not wrapped_return:
# Restore previously ignored context.
ctx.api.type_context[-1] = last_context

bound = get_proper_type(bound)
if not isinstance(bound, CallableType):
return ctx.default_return_type
wrapped_ret_type = get_proper_type(bound.ret_type)
if not isinstance(wrapped_ret_type, Instance) or wrapped_ret_type.type.fullname != PARTIAL:
return ctx.default_return_type
if not mypy.semanal.refers_to_fullname(ctx.args[0][0], PARTIAL):
# If the first argument is partial, above call will trigger the plugin
# again, in between the wrapping above an unwrapping here.
bound = bound.copy_modified(ret_type=wrapped_ret_type.args[0])

if wrapped_return:
# Reverse the wrapping we did above.
ret_type = get_proper_type(bound.ret_type)
if not isinstance(ret_type, Instance) or ret_type.type.fullname != PARTIAL:
return ctx.default_return_type
bound = bound.copy_modified(ret_type=ret_type.args[0])

formal_to_actual = map_actuals_to_formals(
actual_kinds=actual_arg_kinds,
Expand Down
13 changes: 13 additions & 0 deletions test-data/unit/check-functools.test
Original file line number Diff line number Diff line change
Expand Up @@ -455,3 +455,16 @@ first_kw([1]) # E: Too many positional arguments for "get" \
# E: Too few arguments for "get" \
# E: Argument 1 to "get" has incompatible type "List[int]"; expected "int"
[builtins fixtures/list.pyi]

[case testFunctoolsPartialClassObjectMatchingPartial]
from functools import partial

class A:
def __init__(self, var: int, b: int, c: int) -> None: ...

p = partial(A, 1)
reveal_type(p) # N: Revealed type is "functools.partial[__main__.A]"
p(1, "no") # E: Argument 2 to "A" has incompatible type "str"; expected "int"

q: partial[A] = partial(A, 1) # OK
[builtins fixtures/tuple.pyi]
Loading