Skip to content
Open
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
7 changes: 7 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,13 @@ incremental in minor, bugfixes only are patches.
See [0Ver](https://0ver.org/).


## Unreleased

### Bugfixes

- Fixes the `curry.partial` compatibility with mypy 1.6.1+


## 0.26.0

### Features
Expand Down
1 change: 1 addition & 0 deletions pyproject.toml
Original file line number Diff line number Diff line change
Expand Up @@ -185,6 +185,7 @@ lint.per-file-ignores."tests/test_examples/test_result/test_result_pattern_match
"D103",
]
lint.per-file-ignores."tests/test_pattern_matching.py" = [ "S101" ]
lint.per-file-ignores."typesafety/test_curry/test_partial/test_partial.py" = [ "S101" ]
lint.external = [ "WPS" ]
lint.flake8-quotes.inline-quotes = "single"
lint.mccabe.max-complexity = 6
Expand Down
30 changes: 26 additions & 4 deletions returns/contrib/mypy/_features/partial.py
Original file line number Diff line number Diff line change
Expand Up @@ -5,11 +5,13 @@
from mypy.nodes import ARG_STAR, ARG_STAR2
from mypy.plugin import FunctionContext
from mypy.types import (
AnyType,
CallableType,
FunctionLike,
Instance,
Overloaded,
ProperType,
TypeOfAny,
TypeType,
get_proper_type,
)
Expand Down Expand Up @@ -51,17 +53,25 @@ def analyze(ctx: FunctionContext) -> ProperType:
default_return = get_proper_type(ctx.default_return_type)
if not isinstance(default_return, CallableType):
return default_return
return _analyze_partial(ctx, default_return)


def _analyze_partial(
ctx: FunctionContext,
default_return: CallableType,
) -> ProperType:
if not ctx.arg_types or not ctx.arg_types[0]:
# No function passed: treat as decorator factory and fallback to Any.
return AnyType(TypeOfAny.implementation_artifact)

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

if len(list(filter(len, ctx.arg_types))) == 1:
return function_def # this means, that `partial(func)` is called
if not isinstance(function_def, _SUPPORTED_TYPES):
function_def = _coerce_to_callable(function_def, func_args)
if function_def is None:
return default_return
if isinstance(function_def, Instance | TypeType):
# We force `Instance` and similar types to coercse to callable:
function_def = func_args.get_callable_from_context()

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


def _coerce_to_callable(
function_def: ProperType,
func_args: '_AppliedArgs',
) -> CallableType | Overloaded | None:
if not isinstance(function_def, _SUPPORTED_TYPES):
return None
if isinstance(function_def, Instance | TypeType):
# We force `Instance` and similar types to coerce to callable:
return func_args.get_callable_from_context()
return function_def


@final
class _PartialFunctionReducer:
"""
Expand Down
25 changes: 13 additions & 12 deletions returns/contrib/mypy/_typeops/inference.py
Original file line number Diff line number Diff line change
Expand Up @@ -73,25 +73,26 @@ def _infer_constraints(
"""Creates mapping of ``typevar`` to real type that we already know."""
checker = self._ctx.api.expr_checker # type: ignore
kinds = [arg.kind for arg in applied_args]
exprs = [arg.expression(self._ctx.context) for arg in applied_args]

formal_to_actual = map_actuals_to_formals(
kinds,
[arg.name for arg in applied_args],
self._fallback.arg_kinds,
self._fallback.arg_names,
lambda index: checker.accept(exprs[index]),
)
constraints = infer_constraints_for_callable(
self._fallback,
arg_types=[arg.type for arg in applied_args],
arg_kinds=kinds,
arg_names=[arg.name for arg in applied_args],
formal_to_actual=formal_to_actual,
context=checker.argument_infer_context(),
lambda index: checker.accept(
applied_args[index].expression(self._ctx.context),
),
)

return {
constraint.type_var: constraint.target for constraint in constraints
constraint.type_var: constraint.target
for constraint in infer_constraints_for_callable(
self._fallback,
arg_types=[arg.type for arg in applied_args],
arg_kinds=kinds,
arg_names=[arg.name for arg in applied_args],
formal_to_actual=formal_to_actual,
context=checker.argument_infer_context(),
)
}


Expand Down
10 changes: 9 additions & 1 deletion returns/curry.py
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,7 @@


def partial(
func: Callable[..., _ReturnType],
func: Callable[..., _ReturnType] | None = None,
*args: Any,
**kwargs: Any,
) -> Callable[..., _ReturnType]:
Expand All @@ -35,6 +35,14 @@ def partial(
- https://docs.python.org/3/library/functools.html#functools.partial

"""
if func is None:

def _decorator( # type: ignore[return-type]
inner: Callable[..., _ReturnType],
) -> Callable[..., _ReturnType]:
return _partial(inner, *args, **kwargs)

return _decorator
return _partial(func, *args, **kwargs)


Expand Down
3 changes: 3 additions & 0 deletions typesafety/test_curry/test_partial/mypy.ini
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
[mypy]
python_version = 3.11
plugins = returns.contrib.mypy.returns_plugin
94 changes: 94 additions & 0 deletions typesafety/test_curry/test_partial/test_partial.yml
Original file line number Diff line number Diff line change
Expand Up @@ -150,3 +150,97 @@
function: Callable[[_SecondType, _FirstType], _SecondType],
):
reveal_type(partial(function, default)) # N: Revealed type is "def (_FirstType`-2) -> _SecondType`-1"


- case: partial_regression1711
disable_cache: false
main: |
from returns.curry import partial

def foo(x: int, y: int, z: int) -> int:
...

def bar(x: int) -> int:
...

baz = partial(foo, bar(1))
reveal_type(baz) # N: Revealed type is "def (y: builtins.int, z: builtins.int) -> builtins.int"


- case: partial_optional_arg
disable_cache: false
main: |
from returns.curry import partial

def test_partial_fn(
first_arg: int,
optional_arg: str | None,
) -> tuple[int, str | None]:
...

bound = partial(test_partial_fn, 1)
reveal_type(bound) # N: Revealed type is "def (optional_arg: builtins.str | None) -> tuple[builtins.int, builtins.str | None]"


- case: partial_decorator
disable_cache: false
main: |
from returns.curry import partial

@partial(first=1)
def _decorated(first: int, second: str) -> float:
...

reveal_type(_decorated) # N: Revealed type is "Any"
out: |
main:3: error: Untyped decorator makes function "_decorated" untyped [misc]


- case: partial_keyword_arg
disable_cache: false
main: |
from returns.curry import partial

def test_partial_fn(
first_arg: int,
optional_arg: str | None,
) -> tuple[int, str | None]:
...

bound = partial(test_partial_fn, optional_arg='a')
reveal_type(bound) # N: Revealed type is "def (first_arg: builtins.int) -> tuple[builtins.int, builtins.str | None]"


- case: partial_keyword_only
disable_cache: false
main: |
from returns.curry import partial

def _target(*, arg: int) -> int:
...

bound = partial(_target, arg=1)
reveal_type(bound) # N: Revealed type is "def () -> builtins.int"


- case: partial_keyword_mixed
disable_cache: false
main: |
from returns.curry import partial

def _target(arg1: int, *, arg2: int) -> int:
...

bound = partial(_target, arg2=1)
reveal_type(bound) # N: Revealed type is "def (arg1: builtins.int) -> builtins.int"


- case: partial_wrong_signature_any
disable_cache: false
main: |
from returns.curry import partial

reveal_type(partial(len, 1))
out: |
main:3: error: Argument 1 to "len" has incompatible type "int"; expected "Sized" [arg-type]
main:3: note: Revealed type is "def (*Any, **Any) -> builtins.int"