Skip to content

Commit

Permalink
Fix previous partial fix (#17429)
Browse files Browse the repository at this point in the history
This is a bit unfortunate, but the best we can probably do.

cc @hauntsaninja
  • Loading branch information
ilevkivskyi committed Jun 23, 2024
1 parent 1b116df commit 79b1c8d
Show file tree
Hide file tree
Showing 2 changed files with 39 additions and 8 deletions.
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]

0 comments on commit 79b1c8d

Please sign in to comment.