Skip to content

Commit 6dbd2d7

Browse files
committed
Fix mypy type handling in partial function and add regression tests
1 parent ff4b2d4 commit 6dbd2d7

File tree

4 files changed

+129
-79
lines changed

4 files changed

+129
-79
lines changed

returns/contrib/mypy/_features/partial.py

Lines changed: 26 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -5,11 +5,13 @@
55
from mypy.nodes import ARG_STAR, ARG_STAR2
66
from mypy.plugin import FunctionContext
77
from mypy.types import (
8+
AnyType,
89
CallableType,
910
FunctionLike,
1011
Instance,
1112
Overloaded,
1213
ProperType,
14+
TypeOfAny,
1315
TypeType,
1416
get_proper_type,
1517
)
@@ -51,17 +53,25 @@ def analyze(ctx: FunctionContext) -> ProperType:
5153
default_return = get_proper_type(ctx.default_return_type)
5254
if not isinstance(default_return, CallableType):
5355
return default_return
56+
return _analyze_partial(ctx, default_return)
57+
58+
59+
def _analyze_partial(
60+
ctx: FunctionContext,
61+
default_return: CallableType,
62+
) -> ProperType:
63+
if not ctx.arg_types or not ctx.arg_types[0]:
64+
# No function passed: treat as decorator factory and fallback to Any.
65+
return AnyType(TypeOfAny.implementation_artifact)
5466

5567
function_def = get_proper_type(ctx.arg_types[0][0])
5668
func_args = _AppliedArgs(ctx)
5769

5870
if len(list(filter(len, ctx.arg_types))) == 1:
5971
return function_def # this means, that `partial(func)` is called
60-
if not isinstance(function_def, _SUPPORTED_TYPES):
72+
function_def = _coerce_to_callable(function_def, func_args)
73+
if function_def is None:
6174
return default_return
62-
if isinstance(function_def, Instance | TypeType):
63-
# We force `Instance` and similar types to coercse to callable:
64-
function_def = func_args.get_callable_from_context()
6575

6676
is_valid, applied_args = func_args.build_from_context()
6777
if not isinstance(function_def, CallableType | Overloaded) or not is_valid:
@@ -75,6 +85,18 @@ def analyze(ctx: FunctionContext) -> ProperType:
7585
).new_partial()
7686

7787

88+
def _coerce_to_callable(
89+
function_def: ProperType,
90+
func_args: '_AppliedArgs',
91+
) -> CallableType | Overloaded | None:
92+
if not isinstance(function_def, _SUPPORTED_TYPES):
93+
return None
94+
if isinstance(function_def, Instance | TypeType):
95+
# We force `Instance` and similar types to coerce to callable:
96+
return func_args.get_callable_from_context()
97+
return function_def
98+
99+
78100
@final
79101
class _PartialFunctionReducer:
80102
"""

returns/curry.py

Lines changed: 9 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -8,7 +8,7 @@
88

99

1010
def partial(
11-
func: Callable[..., _ReturnType],
11+
func: Callable[..., _ReturnType] | None = None,
1212
*args: Any,
1313
**kwargs: Any,
1414
) -> Callable[..., _ReturnType]:
@@ -35,6 +35,14 @@ def partial(
3535
- https://docs.python.org/3/library/functools.html#functools.partial
3636
3737
"""
38+
if func is None:
39+
40+
def _decorator( # type: ignore[return-type]
41+
inner: Callable[..., _ReturnType],
42+
) -> Callable[..., _ReturnType]:
43+
return _partial(inner, *args, **kwargs)
44+
45+
return _decorator
3846
return _partial(func, *args, **kwargs)
3947

4048

typesafety/test_curry/test_partial/test_partial.py

Lines changed: 0 additions & 74 deletions
This file was deleted.

typesafety/test_curry/test_partial/test_partial.yml

Lines changed: 94 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -150,3 +150,97 @@
150150
function: Callable[[_SecondType, _FirstType], _SecondType],
151151
):
152152
reveal_type(partial(function, default)) # N: Revealed type is "def (_FirstType`-2) -> _SecondType`-1"
153+
154+
155+
- case: partial_regression1711
156+
disable_cache: false
157+
main: |
158+
from returns.curry import partial
159+
160+
def foo(x: int, y: int, z: int) -> int:
161+
...
162+
163+
def bar(x: int) -> int:
164+
...
165+
166+
baz = partial(foo, bar(1))
167+
reveal_type(baz) # N: Revealed type is "def (y: builtins.int, z: builtins.int) -> builtins.int"
168+
169+
170+
- case: partial_optional_arg
171+
disable_cache: false
172+
main: |
173+
from returns.curry import partial
174+
175+
def test_partial_fn(
176+
first_arg: int,
177+
optional_arg: str | None,
178+
) -> tuple[int, str | None]:
179+
...
180+
181+
bound = partial(test_partial_fn, 1)
182+
reveal_type(bound) # N: Revealed type is "def (optional_arg: builtins.str | None) -> tuple[builtins.int, builtins.str | None]"
183+
184+
185+
- case: partial_decorator
186+
disable_cache: false
187+
main: |
188+
from returns.curry import partial
189+
190+
@partial(first=1)
191+
def _decorated(first: int, second: str) -> float:
192+
...
193+
194+
reveal_type(_decorated) # N: Revealed type is "Any"
195+
out: |
196+
main:3: error: Untyped decorator makes function "_decorated" untyped [misc]
197+
198+
199+
- case: partial_keyword_arg
200+
disable_cache: false
201+
main: |
202+
from returns.curry import partial
203+
204+
def test_partial_fn(
205+
first_arg: int,
206+
optional_arg: str | None,
207+
) -> tuple[int, str | None]:
208+
...
209+
210+
bound = partial(test_partial_fn, optional_arg='a')
211+
reveal_type(bound) # N: Revealed type is "def (first_arg: builtins.int) -> tuple[builtins.int, builtins.str | None]"
212+
213+
214+
- case: partial_keyword_only
215+
disable_cache: false
216+
main: |
217+
from returns.curry import partial
218+
219+
def _target(*, arg: int) -> int:
220+
...
221+
222+
bound = partial(_target, arg=1)
223+
reveal_type(bound) # N: Revealed type is "def () -> builtins.int"
224+
225+
226+
- case: partial_keyword_mixed
227+
disable_cache: false
228+
main: |
229+
from returns.curry import partial
230+
231+
def _target(arg1: int, *, arg2: int) -> int:
232+
...
233+
234+
bound = partial(_target, arg2=1)
235+
reveal_type(bound) # N: Revealed type is "def (arg1: builtins.int) -> builtins.int"
236+
237+
238+
- case: partial_wrong_signature_any
239+
disable_cache: false
240+
main: |
241+
from returns.curry import partial
242+
243+
reveal_type(partial(len, 1))
244+
out: |
245+
main:3: error: Argument 1 to "len" has incompatible type "int"; expected "Sized" [arg-type]
246+
main:3: note: Revealed type is "def (*Any, **Any) -> builtins.int"

0 commit comments

Comments
 (0)