diff --git a/docs/pages/curry.rst b/docs/pages/curry.rst index 2b9bac1cd..3a58a148e 100644 --- a/docs/pages/curry.rst +++ b/docs/pages/curry.rst @@ -3,11 +3,16 @@ Curry ===== -Python already has a great tool to use partial application: +This module is dedicated to partial application. + +We support two types of partial application: ``@curry`` and ``partial``. + +``@curry`` is a new concept for most Python developers, +but Python already has a great tool to use partial application: `functools.partial `_ The only problem with it is the lack of typing. -Let's see what problems do we solve with our custom solution. +Let's see what problems do we solve with this module. .. warning:: @@ -145,39 +150,133 @@ From this return type you can see that we work with all matching cases and discriminate unmatching ones. -FAQ ---- +curry +----- -What is the difference between curring and partial? -~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ +``curry`` allows to provide only a subset of arguments to a function. +And it won't be called untill all the required arguments are provided. -This is a question a lot of Python developers ask. +In contrast to ``partial`` which works on the calling stage, +``@curry`` works best when defining a new function. -`Here are some great answers `_. +.. code:: python -.. warning:: + >>> from returns.curry import curry + + >>> @curry + ... def function(first: int, second: str) -> bool: + ... return len(second) > first + + >>> assert function(1)('a') is False + >>> assert function(1, 'a') is False + >>> assert function(2)('abc') is True + >>> assert function(2, 'abc') is True + +Take a note, that providing invalid arguments will raise ``TypeError``: + +.. code:: + + >>> function(1, 2, 3) + Traceback (most recent call last): + ... + TypeError: too many positional arguments + + >>> function(a=1) + Traceback (most recent call last): + ... + TypeError: got an unexpected keyword argument 'a' + +This is really helpful when working with ``.apply()`` method of containers. + +Typing +~~~~~~ + +``@curry`` functions are also fully typed with our custom ``mypy`` plugin. + +Let's see how types do look like for a curried function: + +.. code:: python - Python has a very limited support for real curring in a way like - ``(x, y, z) -> t`` => ``x -> y -> z -> t`` - works in languages like Haskell. + >>> from returns.curry import curry - This is actually a partial application, but that's the best we can do. + >>> @curry + ... def zero(a: int, b: float, *, kw: bool) -> str: + ... return str(a - b) if kw else '' + + >>> assert zero(1)(0.3)(kw=True) == '0.7' + >>> assert zero(1)(0.3, kw=False) == '' + + # If we will reveal the type it would be quite big: + + reveal_type(zero) + + # Overload( + # def (a: builtins.int) -> Overload( + # def (b: builtins.float, *, kw: builtins.bool) -> builtins.str, + # def (b: builtins.float) -> def (*, kw: builtins.bool) -> builtins.str + # ), + # def (a: builtins.int, b: builtins.float) -> def (*, kw: builtins.bool) + # -> builtins.str, + # def (a: builtins.int, b: builtins.float, *, kw: builtins.bool) + # -> builtins.str + # ) + +It reaveals to us that there are 4 possible way to call this function. +And we type all of them with +`overload `_ +type. + +When you provide any arguments, +you discriminate some overloads and choose more specific path: + +.. code:: python + + reveal_type(zero(1, 2.0)) + # By providing this set of arguments we have choosen this path: + # + # def (a: builtins.int, b: builtins.float) -> def (*, kw: builtins.bool) + # -> builtins.str, + # + # And the revealed type would be: + # def (*, kw: builtins.bool) -> builtins.str + # + +It works with functions, instance, class, +and static methods, including generics. +See ``Limitations`` in the API reference. + + +FAQ +--- Why don't you support `*` and `**` arguments? ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ When you use ``partial(some, *my_args)`` or ``partial(some, **my_args)`` or both of them at the same time, -we fallback to the default return type. Why? +we fallback to the default return type. The same happens with ``curry``. Why? + +There are several problems: + +- Because ``mypy`` cannot not infer what arguments are there + inside this ``my_args`` variable +- Because ``curry`` cannot know when + to stop accepting ``*args`` and ``**kwargs`` +- And there are possibly other problems! + +Our advice is not to use ``*args`` and ``*kwargs`` +with ``partial`` and ``curry``. + -Because ``mypy`` cannot not infer what arguments are there -inside this ``my_args`` variable. +Further reading +--------------- -Our advice is not to use ``*args`` and ``*kwargs`` in ``partial`` call. +- `functools.partial `_ +- `Currying `_ +- `@curry decorator `_ API Reference ------------- .. automodule:: returns.curry - :members: diff --git a/returns/context/__init__.py b/returns/context/__init__.py index 71b1583ff..37b9f64b9 100644 --- a/returns/context/__init__.py +++ b/returns/context/__init__.py @@ -1,5 +1,5 @@ """ -This module was quite big one, so we have split it. +This module was quite a big one, so we have split it. isort:skip_file """ @@ -7,6 +7,7 @@ from returns.context.requires_context import ( # noqa: F401 Context as Context, RequiresContext as RequiresContext, + Reader as Reader, NoDeps as NoDeps, ) from returns.context.requires_context_result import ( # noqa: F401 diff --git a/returns/context/requires_context.py b/returns/context/requires_context.py index 69363e809..bf07bfdbd 100644 --- a/returns/context/requires_context.py +++ b/returns/context/requires_context.py @@ -388,3 +388,9 @@ def ask(cls) -> RequiresContext[_EnvType, _EnvType]: """ return RequiresContext(identity) + + +# Aliases + +#: Sometimes `RequiresContext` is too long to type. +Reader = RequiresContext diff --git a/returns/context/requires_context_io_result.py b/returns/context/requires_context_io_result.py index c4143dd8a..1701056bb 100644 --- a/returns/context/requires_context_io_result.py +++ b/returns/context/requires_context_io_result.py @@ -947,9 +947,6 @@ def from_failure( """ return RequiresContextIOResult(lambda _: IOFailure(inner_value)) - # TODO: support from_successful_result_context - # TODO: support from_failed_result_context - @final class ContextIOResult(Immutable, Generic[_EnvType]): diff --git a/returns/contrib/mypy/_curry.py b/returns/contrib/mypy/_curry.py index 3b9600356..53fa8572e 100644 --- a/returns/contrib/mypy/_curry.py +++ b/returns/contrib/mypy/_curry.py @@ -1,4 +1,6 @@ from collections import namedtuple +from itertools import groupby, product +from operator import itemgetter from types import MappingProxyType from typing import ( ClassVar, @@ -25,9 +27,9 @@ TempNode, ) from mypy.plugin import FunctionContext -from mypy.types import CallableType, FunctionLike, Overloaded +from mypy.types import AnyType, CallableType, FunctionLike, Overloaded from mypy.types import Type as MypyType -from mypy.types import TypeVarId +from mypy.types import TypeOfAny, TypeVarId #: Mapping for better `call || function` argument compatibility. _KIND_MAPPING = MappingProxyType({ @@ -40,6 +42,9 @@ #: Mapping of `typevar` to real type. _Constraints = Mapping[TypeVarId, MypyType] +#: Raw material to build `_ArgTree`. +_RawArgTree = List[List[List['_FuncArg']]] + #: Basic struct to represent function arguments. _FuncArgStruct = namedtuple('_FuncArg', ('name', 'type', 'kind')) @@ -84,15 +89,20 @@ def build_callable_type(self, applied_args: List[_FuncArg]) -> CallableType: """ new_pos_args = self._applied_positional_args(applied_args) new_named_args = self._applied_named_args(applied_args) - return self._with_signature(new_pos_args + new_named_args) + return self.with_signature(new_pos_args + new_named_args) - def _with_signature(self, new_args: List[_FuncArg]) -> CallableType: + def with_signature(self, new_args: List[_FuncArg]) -> CallableType: + """Smartly creates a new callable from a given arguments.""" return detach_callable(self._case_function.copy_modified( arg_names=[arg.name for arg in new_args], arg_types=[arg.type for arg in new_args], arg_kinds=[arg.kind for arg in new_args], )) + def with_ret_type(self, ret_type: MypyType) -> CallableType: + """Smartly creates a new callable from a given return type.""" + return self._case_function.copy_modified(ret_type=ret_type) + def _applied_positional_args( self, applied_args: List[_FuncArg], @@ -140,6 +150,7 @@ def __init__( self._intermediate = intermediate def diff(self) -> CallableType: + """Finds a diff between two functions' arguments.""" intermediate_names = [ arg.name for arg in _FuncArg.from_callable(self._intermediate) @@ -164,7 +175,6 @@ def diff(self) -> CallableType: ) if should_be_copied: new_function_args.append(arg) - return _Intermediate(self._original).build_callable_type( new_function_args, ) @@ -192,7 +202,7 @@ def _analyze_function_call( return checked_function -class CurryFunctionReducer(object): +class PartialFunctionReducer(object): """ Helper object to work with curring. @@ -363,30 +373,164 @@ def _proper_type( return Overloaded(case_functions) -def get_callable_from_type(function_ctx: FunctionContext) -> MypyType: +class CurryFunctionOverloads(object): """ - Returns callable type from the context. + Implementation of ``@curry`` decorator typings. - There's why we need it: + Basically does just two things: - - We can use ``curry`` on real functions - - We can use ``curry`` on ``@overload`` functions - - We can use ``curry`` on instances with ``__call__`` - - We can use ``curry`` on ``Type`` types + 1. Creates all possible ordered combitations of arguments + 2. Creates ``Overload`` instances for functions' return types - This function allows us to unify this process. - We also need to disable errors, because we explicitly pass empty args. """ - checker = function_ctx.api.expr_checker # type: ignore - function_def = function_ctx.arg_types[0][0] - checker.msg.disable_errors() - _return_type, function_def = checker.check_call( - function_def, [], [], function_ctx.context, [], - ) - checker.msg.enable_errors() + class _ArgTree(object): # noqa: WPS431 + def __init__(self, case: Optional[CallableType]) -> None: + self.case = case + self.children: List['CurryFunctionOverloads._ArgTree'] = [] + + def __init__(self, original: CallableType, ctx: FunctionContext) -> None: + """ + Saving the things we need. + + Args: + original: original function that was passed to ``@curry``. + ctx: function context. + + """ + self._original = original + self._ctx = ctx + self._overloads: List[CallableType] = [] + self._args = _FuncArg.from_callable(self._original) + + # We need to get rid of generics here. + # Because, otherwise `detach_callable` with add + # unused variables to intermediate callables. + self._default = cast( + CallableType, self._ctx.default_return_type, + ).copy_modified( + ret_type=AnyType(TypeOfAny.implementation_artifact), + ) + + def build_overloads(self) -> MypyType: + """ + Builds lots of possible overloads for a given function. + + Inside we try to repsent all functions as sequence of arguments, + grouped by the similar ones and returning one more overload instance. + """ + if not self._args: # There's nothing to do, function has 0 args. + return self._original + + if any(arg.kind in {ARG_STAR, ARG_STAR2} for arg in self._args): + # We don't support `*args` and `**kwargs`. + # Because it is very complex. It might be fixes in the future. + return self._default.ret_type # Any + + argtree = self._build_argtree( + self._ArgTree(None), # starting from root node + list(self._slices(self._args)), + ) + self._build_overloads_from_argtree(argtree) + return self._proper_type(self._overloads) + + def _build_argtree( + self, + node: '_ArgTree', + source: _RawArgTree, + ) -> '_ArgTree': + """ + Builds argument tree. + + Each argument can point to zero, one, or more other nodes. + Arguments that have zero children are treated as bottom (last) ones. + Arguments that have just one child are meant to be regular functions. + Arguments that have more than one child are treated as overloads. + + """ + def factory( + args: _RawArgTree, + ) -> Iterator[Tuple[List[_FuncArg], _RawArgTree]]: + if not args or not args[0]: + return # we have reached an end of arguments + yield from ( + (case, [group[1:] for group in grouped]) + for case, grouped in groupby(args, itemgetter(0)) + ) - return function_def + for case, rest in factory(source): + new_node = self._ArgTree( + _Intermediate(self._default).with_signature(case), + ) + node.children.append(new_node) + self._build_argtree(source=rest, node=new_node) + return node + + def _build_overloads_from_argtree(self, argtree: _ArgTree) -> None: + """Generates functions from argument tree.""" + for child in argtree.children: + self._build_overloads_from_argtree(child) + assert child.case # mypy is not happy # noqa: S101 + + if not child.children: + child.case = _Intermediate(child.case).with_ret_type( + self._original.ret_type, + ) + + if argtree.case is not None: + # We need to go backwards and to replace the return types + # of the previous functions. Like so: + # 1. `def x -> A` + # 2. `def y -> A` + # Will take `2` and apply its type to the previous function `1`. + # Will result in `def x -> y -> A` + # We also overloadify existing return types. + ret_type = argtree.case.ret_type + temp_any = isinstance( + ret_type, AnyType, + ) and ret_type.type_of_any == TypeOfAny.implementation_artifact + argtree.case = _Intermediate(argtree.case).with_ret_type( + child.case if temp_any else Overloaded( + [child.case, *cast(FunctionLike, ret_type).items()], + ), + ) + else: # Root is reached, we need to save the result: + self._overloads.append(child.case) + + def _slices(self, source: List[_FuncArg]) -> Iterator[List[List[_FuncArg]]]: + """ + Generate all possible slices of a source list. + + Example:: + + _slices("AB") -> + "AB" + "A" "B" + + _slices("ABC") -> + "ABC" + "AB" "C" + "A" "BC" + "A" "B" "C" + + """ + for doslice in product([True, False], repeat=len(source) - 1): + slices = [] + start = 0 + for index, slicehere in enumerate(doslice, 1): + if slicehere: + slices.append(source[start:index]) + start = index + slices.append(source[start:]) + yield slices + + def _proper_type( + self, + case_functions: List[CallableType], + ) -> FunctionLike: + if len(case_functions) == 1: + return case_functions[0] + return Overloaded(case_functions) class AppliedArgs(object): @@ -406,6 +550,31 @@ def __init__(self, function_ctx: FunctionContext) -> None: self._function_ctx.arg_kinds[1:], ) + def get_callable_from_context(self) -> MypyType: + """ + Returns callable type from the context. + + There's why we need it: + + - We can use ``curry`` on real functions + - We can use ``curry`` on ``@overload`` functions + - We can use ``curry`` on instances with ``__call__`` + - We can use ``curry`` on ``Type`` types + + This function allows us to unify this process. + We also need to disable errors, because we explicitly pass empty args. + """ + checker = self._function_ctx.api.expr_checker # type: ignore + function_def = self._function_ctx.arg_types[0][0] + + checker.msg.disable_errors() + _return_type, function_def = checker.check_call( + function_def, [], [], self._function_ctx.context, [], + ) + checker.msg.enable_errors() + + return function_def + def build_from_context(self) -> Tuple[bool, List[_FuncArg]]: """ Builds handy arguments structures from the context. diff --git a/returns/contrib/mypy/decorator_plugin.py b/returns/contrib/mypy/decorator_plugin.py index 733996da4..8b3359fa0 100644 --- a/returns/contrib/mypy/decorator_plugin.py +++ b/returns/contrib/mypy/decorator_plugin.py @@ -22,12 +22,14 @@ from typing import Callable, Optional, Type from mypy.plugin import FunctionContext, Plugin -from mypy.types import CallableType, Instance, Overloaded, TypeType +from mypy.types import CallableType, Instance, Overloaded +from mypy.types import Type as MypyType +from mypy.types import TypeType from returns.contrib.mypy._curry import ( AppliedArgs, - CurryFunctionReducer, - get_callable_from_type, + CurryFunctionOverloads, + PartialFunctionReducer, ) #: Set of full names of our decorators. @@ -41,8 +43,11 @@ 'returns.functions.not_', )) -#: Used for typed curring. -_TYPED_CURRY_FUNCTION = 'returns.curry.partial' +#: Used for typed ``partial`` function. +_TYPED_PARTIAL_FUNCTION = 'returns.curry.partial' + +#: Used for typed ``curry`` decorator. +_TYPED_CURRY_FUNCTION = 'returns.curry.curry' def _change_decorator_function_type( @@ -59,7 +64,7 @@ def _change_decorator_function_type( ) -def _analyze_decorator(function_ctx: FunctionContext): +def _analyze_decorator(function_ctx: FunctionContext) -> MypyType: """Tells us what to do when one of the typed decorators is called.""" if not isinstance(function_ctx.arg_types[0][0], CallableType): return function_ctx.default_return_type @@ -71,7 +76,7 @@ def _analyze_decorator(function_ctx: FunctionContext): ) -def _analyze_curring(function_ctx: FunctionContext): +def _analyze_partial(function_ctx: FunctionContext) -> MypyType: """ This hook is used to make typed curring a thing in `returns` project. @@ -89,20 +94,20 @@ def _analyze_curring(function_ctx: FunctionContext): Overloaded, ) + func_args = AppliedArgs(function_ctx) if len(list(filter(len, function_ctx.arg_types))) == 1: return function_def # this means, that `partial(func)` is called elif not isinstance(function_def, supported_types): return function_ctx.default_return_type elif isinstance(function_def, (Instance, TypeType)): # We force `Instance` and similar types to coercse to callable: - function_def = get_callable_from_type(function_ctx) - - is_valid, applied_args = AppliedArgs(function_ctx).build_from_context() + 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: return function_ctx.default_return_type - return CurryFunctionReducer( + return PartialFunctionReducer( function_ctx.default_return_type, function_def, applied_args, @@ -110,10 +115,22 @@ def _analyze_curring(function_ctx: FunctionContext): ).new_partial() +def _analyze_curry(function_ctx: FunctionContext) -> MypyType: + if not isinstance(function_ctx.arg_types[0][0], CallableType): + return function_ctx.default_return_type + if not isinstance(function_ctx.default_return_type, CallableType): + return function_ctx.default_return_type + + return CurryFunctionOverloads( + function_ctx.arg_types[0][0], + function_ctx, + ).build_overloads() + + class _TypedDecoratorPlugin(Plugin): - def get_function_hook( # type: ignore + def get_function_hook( self, fullname: str, - ) -> Optional[Callable[[FunctionContext], Type]]: + ) -> Optional[Callable[[FunctionContext], MypyType]]: """ One of the specified ``mypy`` callbacks. @@ -123,8 +140,10 @@ def get_function_hook( # type: ignore Otherwise, we return ``None``. """ - if fullname == _TYPED_CURRY_FUNCTION: - return _analyze_curring + if fullname == _TYPED_PARTIAL_FUNCTION: + return _analyze_partial + elif fullname == _TYPED_CURRY_FUNCTION: + return _analyze_curry elif fullname in _TYPED_DECORATORS: return _analyze_decorator return None diff --git a/returns/curry.py b/returns/curry.py index c96900155..0d55bff40 100644 --- a/returns/curry.py +++ b/returns/curry.py @@ -1,5 +1,7 @@ from functools import partial as _partial -from typing import Any, Callable, TypeVar +from functools import wraps +from inspect import BoundArguments, Signature +from typing import Any, Callable, Tuple, TypeVar, Union _ReturnType = TypeVar('_ReturnType') @@ -12,22 +14,165 @@ def partial( It just ``functools.partial`` wrapper with better typing support. - We use custom ``mypy`` plugin to make types correct. + We use a custom ``mypy`` plugin to make types correct. Otherwise, it is currently impossible. .. code:: python - >>> from returns.curry import partial + >>> from returns.curry import partial - >>> def sum_two_numbers(first: int, second: int) -> int: - ... return first + second - ... - >>> sum_with_ten = partial(sum_two_numbers, 10) - >>> assert sum_with_ten(2) == 12 - >>> assert sum_with_ten(-5) == 5 + >>> def sum_two_numbers(first: int, second: int) -> int: + ... return first + second + + >>> sum_with_ten = partial(sum_two_numbers, 10) + >>> assert sum_with_ten(2) == 12 + >>> assert sum_with_ten(-5) == 5 See also: https://docs.python.org/3/library/functools.html#functools.partial """ return _partial(func, *args, **kwargs) + + +def curry(function: Callable[..., _ReturnType]) -> Callable[..., _ReturnType]: + """ + Typed currying decorator. + + Calls the wrapped function when there're enough arguments, + otherwise returns an intermediate function to + + Currying is a conception from functional languages that does partial + applying. That means that if we pass one argument in a function that + get 2 or more arguments, we'll get a new function that remembers all + previously passed arguments. Then we can pass remaining arguments, and + the function will be executed. + + :func`~partial` sunction does a similar thing, + but it does partial application exactly once. + ``curry`` is a bit smarter and will do parial + application until enough arguments passed. + + If wrong arguments are passed, ``TypeError`` will be raised immediately. + + .. code:: python + + >>> from returns.curry import curry + + >>> @curry + ... def divide(number: int, by: int) -> float: + ... return number / by + + >>> divide(1) # doesn't call the func and remembers arguments + + >>> assert divide(1)(by=10) == 0.1 # calls the func when possible + >>> assert divide(1, by=10) == 0.1 # or call the func like always + + Here are several examples with wrong arguments: + + .. code:: + + >>> divide(1, 2, 3) + Traceback (most recent call last): + ... + TypeError: too many positional arguments + + >>> divide(a=1) + Traceback (most recent call last): + ... + TypeError: got an unexpected keyword argument 'a' + + Limitations: + + - It is kinda slow. Like 100 times slower than a regular function call. + - It does not work with several builtins like ``str``, ``int``, + and possibly other ``C`` defined callables + - ``*args`` and ``**kwargs`` are not supported + and we use ``Any`` as a fallback + - Support of arguments with default values is very limited, + because we cannot be totally sure which case we are using: + with the default value or without it, be careful + - We use a custom ``mypy`` plugin to make types correct, + otherwise, it is currently impossible + - It might not work as expected with curried ``Klass().method``, + it might generate invalid method signrature + (looks like a bug in ``mypy``) + - It is probably a bad idea to ``curry`` a function with lots of arguments, + because you will end up with lots of overload functions, + that you won't be able to understand. + It might be also be slow during the typecheck + - Cyrrying of ``__init__`` does not work because of the bug in ``mypy``: + https://github.com/python/mypy/issues/8801 + + We expect people to use this tool responsibly + when they know that they are doing. + + See also: + https://en.wikipedia.org/wiki/Currying + https://stackoverflow.com/questions/218025/ + + """ + argspec = Signature.from_callable(function).bind_partial() + + def decorator(*args, **kwargs): + return _eager_curry(function, argspec, args, kwargs) + return wraps(function)(decorator) + + +def _eager_curry( + function, + argspec, + args: tuple, + kwargs: dict, +): + """Internal ``curry`` implementation.""" + intermediate, full_args = _intermediate_argspec(argspec, args, kwargs) + if full_args is not None: + return function(*full_args[0], **full_args[1]) + + # We use closures to avoid names conflict between + # the function args and args of the curry implementation. + def decorator(*inner_args, **inner_kwargs): + return _eager_curry(function, intermediate, inner_args, inner_kwargs) + return wraps(function)(decorator) + + +_ArgSpec = Union[ + # Case when all arguments are bound and function can be called: + Tuple[None, Tuple[tuple, dict]], + # Case when there are still unbound arguments: + Tuple[BoundArguments, None], +] + + +def _intermediate_argspec( + argspec: BoundArguments, + args: tuple, + kwargs: dict, +) -> _ArgSpec: + """ + That's where ``curry`` magic happens. + + We use ``Signature`` objects from ``inspect`` to bind existing arguments. + + If there's a ``TypeError`` while we ``bind`` the arguments we try again. + The second time we try to ``bind_partial`` arguments. It can fail too! + It fails when there are invalid arguments + or more arguments than we can fit in a function. + + This function is slow. Any optimization ideas are welcome! + """ + full_args = argspec.args + args + full_kwargs = {**argspec.kwargs, **kwargs} + + try: + argspec.signature.bind(*full_args, **full_kwargs) + except TypeError: + # Another option is to copy-paste and patch `getcallargs` func + # but in this case we get responsibility to maintain it over + # python releases. + # This place is also responsible for raising ``TypeError`` for cases: + # 1. When incorrect argument is provided + # 2. When too many arguments are provided + return argspec.signature.bind_partial(*full_args, **full_kwargs), None + return None, (full_args, full_kwargs) diff --git a/setup.cfg b/setup.cfg index d5a1bb5e5..13a3554cc 100644 --- a/setup.cfg +++ b/setup.cfg @@ -14,7 +14,6 @@ max-complexity = 6 max-line-length = 80 # wemake-python-styleguide -max-methods = 8 max-annotation-complexity = 4 i-control-code = False @@ -36,10 +35,11 @@ per-file-ignores = # We allow `futures` to do attribute access: returns/future.py: WPS437 returns/_generated/futures/*.py: WPS204, WPS433, WPS437 - # We allow a lot of durty hacks in our pytest plugin: + # We allow a lot of durty hacks in our plugins: + returns/contrib/mypy/*.py: WPS201 returns/contrib/pytest/plugin.py: WPS430, WPS433, WPS437, WPS609 # There are multiple assert's in tests: - tests/*.py: S101, WPS226, WPS432, WPS436 + tests/*.py: S101, WPS204, WPS218, WPS226, WPS432, WPS436 # Annotations: *.pyi: D103, WPS211, WPS428 diff --git a/tests/test_curry/test_curry.py b/tests/test_curry/test_curry.py new file mode 100644 index 000000000..3c63b36d1 --- /dev/null +++ b/tests/test_curry/test_curry.py @@ -0,0 +1,185 @@ +from inspect import getdoc +from typing import List, Tuple + +import pytest + +from returns.curry import curry + + +def test_docstring(): + """Ensures that we preserve docstrings from curried function.""" + + @curry + def factory(arg: int, other: int) -> None: + """Some docstring.""" + + assert getdoc(factory) == 'Some docstring.' + + +def test_immutable(): + """Check that arguments from previous calls are immutable.""" + + @curry + def factory(arg: int, other: int) -> Tuple[int, int]: + return (arg, other) + + cached = factory(arg=1) + assert cached(other=2) == (1, 2) + assert cached(other=3) == (1, 3) + + +def test_no_args(): + """Ensures that it is possible to curry a function with empty args.""" + + @curry + def factory() -> int: + return 1 + + assert factory() == 1 + + +def test_one_arg(): + """Ensures that it is possible to curry a function with one arg.""" + + @curry + def factory(arg: int) -> int: + return arg + + assert factory(1) == 1 + assert factory(arg=1) == 1 + with pytest.raises(TypeError): + factory(other=2) # type: ignore + with pytest.raises(TypeError): + factory(1, 2) # type: ignore + with pytest.raises(TypeError): + factory(1)(2) # type: ignore + + +def test_two_args(): + """Ensures that it is possible to curry a function with two args.""" + + @curry + def factory(arg: int, other: int) -> Tuple[int, int]: + return (arg, other) + + assert factory(1)(2) == (1, 2) + assert factory(1, 2) == (1, 2) + + assert factory(2, other=3) == (2, 3) + assert factory(arg=2, other=3) == (2, 3) + assert factory(other=3, arg=2) == (2, 3) + + assert factory(arg=0)(other=5) == (0, 5) + assert factory(0)(other=5) == (0, 5) + + with pytest.raises(TypeError): + factory(1, 2, 3) # type: ignore + with pytest.raises(TypeError): + factory(1, c=2) # type: ignore + with pytest.raises(TypeError): + factory(1)(c=2) # type: ignore + with pytest.raises(TypeError): + factory(1)(2)(3) # type: ignore + + +def test_star_args(): + """Ensures that it is possible to curry a function with ``*args``.""" + + @curry + def factory(*args: int) -> int: + return sum(args) + + assert factory() == 0 + assert factory(1) == 1 + assert factory(1, 2) == 3 + assert factory(1, 2, 3) == 6 + + with pytest.raises(TypeError): + factory(arg=1) + with pytest.raises(TypeError): + factory(1, other=2) + with pytest.raises(TypeError): + factory(1)(2) + + +def test_arg_and_star_args(): + """Ensures that it is possible to curry a function with ``*args``.""" + + @curry + def factory(arg: int, *args: int) -> int: + return arg + sum(args) + + assert factory(1) == 1 + assert factory(1, 2) == 3 + assert factory(1, 2, 3) == 6 + + with pytest.raises(TypeError): + assert factory(1)(2, 3) == 6 + + +def test_star_kwargs(): + """Ensures that it is possible to curry a function with ``**kwargs``.""" + + @curry + def factory(**kwargs: int) -> List[Tuple[str, int]]: + return sorted(kwargs.items()) + + assert not factory() + assert factory(arg=1) == [('arg', 1)] + assert factory(arg=1, other=2) == [('arg', 1), ('other', 2)] + + with pytest.raises(TypeError): + factory(1) + with pytest.raises(TypeError): + factory(1, other=2) + + +def test_arg_star_kwargs(): + """The decorator should work with ``kwargs``.""" + + @curry + def factory(first: int, **kwargs: int) -> List[Tuple[str, int]]: + return [('first', first)] + sorted(kwargs.items()) + + assert factory(1) == [('first', 1)] + assert factory(1, arg=2) == [('first', 1), ('arg', 2)] + assert factory(first=1, arg=2) == [('first', 1), ('arg', 2)] + assert factory(1, arg=2, other=3) == [ + ('first', 1), + ('arg', 2), + ('other', 3), + ] + + with pytest.raises(TypeError): + factory(1, 2) + with pytest.raises(TypeError): + factory(1, first=2) + with pytest.raises(TypeError): + factory(1, 2, c=2) + + +def test_kwonly(): + """The decorator should work with kw-only args.""" + + @curry + def factory(*args: int, by: int) -> Tuple[int, ...]: + return args + (by, ) + + assert factory(1, 2, 3)(by=10) == (1, 2, 3, 10) + assert factory(by=10) == (10, ) + + +def test_raises(): + """Exception raised from the function must not be intercepted.""" + + @curry + def factory(arg: int, other: int) -> None: + msg = "f() missing 2 required positional arguments: 'a' and 'b'" + raise TypeError(msg) + + with pytest.raises(TypeError): + factory(1)(2) + with pytest.raises(TypeError): + factory(1, 2) + with pytest.raises(TypeError): + factory(1, 2, 3) # type: ignore diff --git a/typesafety/test_curry/test_curry/test_curry.yml b/typesafety/test_curry/test_curry/test_curry.yml new file mode 100644 index 000000000..601605ea6 --- /dev/null +++ b/typesafety/test_curry/test_curry/test_curry.yml @@ -0,0 +1,115 @@ +- case: curry_zero_args + disable_cache: true + main: | + from returns.curry import curry + + @curry + def zero() -> str: + ... + + reveal_type(zero) # N: Revealed type is 'def () -> builtins.str' + reveal_type(zero()) # N: Revealed type is 'builtins.str' + + +- case: curry_single_arg + disable_cache: true + main: | + from returns.curry import curry + + @curry + def zero(arg: int) -> str: + ... + + reveal_type(zero) # N: Revealed type is 'def (arg: builtins.int) -> builtins.str' + reveal_type(zero(1)) # N: Revealed type is 'builtins.str' + + +- case: curry_two_args1 + disable_cache: true + main: | + from returns.curry import curry + + @curry + def zero(arg: int, other: float) -> str: + ... + + reveal_type(zero) # N: Revealed type is 'Overload(def (arg: builtins.int) -> def (other: builtins.float) -> builtins.str, def (arg: builtins.int, other: builtins.float) -> builtins.str)' + reveal_type(zero(1)) # N: Revealed type is 'def (other: builtins.float) -> builtins.str' + reveal_type(zero(1, 2.0)) # N: Revealed type is 'builtins.str' + reveal_type(zero(1)(2.0)) # N: Revealed type is 'builtins.str' + + +- case: curry_two_args2 + disable_cache: true + main: | + from returns.curry import curry + + def zero(arg: int, other: float) -> str: + ... + + reveal_type(curry(zero)) # N: Revealed type is 'Overload(def (arg: builtins.int) -> def (other: builtins.float) -> builtins.str, def (arg: builtins.int, other: builtins.float) -> builtins.str)' + reveal_type(curry(zero)(1)) # N: Revealed type is 'def (other: builtins.float) -> builtins.str' + reveal_type(curry(zero)(1, 2.0)) # N: Revealed type is 'builtins.str' + reveal_type(curry(zero)(1)(2.0)) # N: Revealed type is 'builtins.str' + + +- case: curry_two_args3 + disable_cache: true + main: | + from returns.curry import curry + + @curry + def zero(arg: int, other: float) -> str: + ... + + reveal_type(zero('a')) + reveal_type(zero(1.0, 1)) + reveal_type(zero(1, 2.0, 2.0)) + out: | + main:7: error: No overload variant of "zero" matches argument type "str" + main:7: note: <1 more non-matching overload not shown> + main:7: note: def zero(arg: int) -> Callable[..., str] + main:7: note: Possible overload variant: + main:7: note: Revealed type is 'Any' + main:8: error: No overload variant of "zero" matches argument types "float", "int" + main:8: note: <1 more non-matching overload not shown> + main:8: note: def zero(arg: int, other: float) -> str + main:8: note: Possible overload variant: + main:8: note: Revealed type is 'Any' + main:9: error: No overload variant of "zero" matches argument types "int", "float", "float" + main:9: note: def zero(arg: int) -> Callable[..., str] + main:9: note: def zero(arg: int, other: float) -> str + main:9: note: Possible overload variants: + main:9: note: Revealed type is 'Any' + +- case: curry_two_args_one_default + disable_cache: true + main: | + from returns.curry import curry + + @curry + def zero(arg: int, other: float = 1.0) -> str: + ... + + reveal_type(zero) # N: Revealed type is 'Overload(def (arg: builtins.int) -> def (other: builtins.float =) -> builtins.str, def (arg: builtins.int, other: builtins.float =) -> builtins.str)' + reveal_type(zero(1)) # N: Revealed type is 'def (other: builtins.float =) -> builtins.str' + reveal_type(zero(1, 2.0)) # N: Revealed type is 'builtins.str' + reveal_type(zero(1)(2.0)) # N: Revealed type is 'builtins.str' + + +- case: curry_three_args + disable_cache: true + main: | + from returns.curry import curry + + @curry + def zero(arg: int, other: float, *, kw: bool) -> str: + ... + + reveal_type(zero) # N: Revealed type is 'Overload(def (arg: builtins.int) -> Overload(def (other: builtins.float, *, kw: builtins.bool) -> builtins.str, def (other: builtins.float) -> def (*, kw: builtins.bool) -> builtins.str), def (arg: builtins.int, other: builtins.float) -> def (*, kw: builtins.bool) -> builtins.str, def (arg: builtins.int, other: builtins.float, *, kw: builtins.bool) -> builtins.str)' + reveal_type(zero(1)) # N: Revealed type is 'Overload(def (other: builtins.float, *, kw: builtins.bool) -> builtins.str, def (other: builtins.float) -> def (*, kw: builtins.bool) -> builtins.str)' + reveal_type(zero(1, 2.0)) # N: Revealed type is 'def (*, kw: builtins.bool) -> builtins.str' + reveal_type(zero(1)(2.0)) # N: Revealed type is 'def (*, kw: builtins.bool) -> builtins.str' + reveal_type(zero(1, 2.0)(kw=True)) # N: Revealed type is 'builtins.str' + reveal_type(zero(1)(2.0)(kw=True)) # N: Revealed type is 'builtins.str' + reveal_type(zero(1, 2.0, kw=True)) # N: Revealed type is 'builtins.str' diff --git a/typesafety/test_curry/test_curry/test_curry_args_kwargs.yml b/typesafety/test_curry/test_curry/test_curry_args_kwargs.yml new file mode 100644 index 000000000..a6ea043fb --- /dev/null +++ b/typesafety/test_curry/test_curry/test_curry_args_kwargs.yml @@ -0,0 +1,34 @@ +- case: curry_args + disable_cache: true + main: | + from returns.curry import curry + + @curry + def zero(*args) -> str: + ... + + reveal_type(zero) # N: Revealed type is 'Any' + + +- case: curry_kwargs + disable_cache: true + main: | + from returns.curry import curry + + @curry + def zero(**kwargs) -> str: + ... + + reveal_type(zero) # N: Revealed type is 'Any' + + +- case: curry_args_kwargs + disable_cache: true + main: | + from returns.curry import curry + + @curry + def zero(*args, **kwargs) -> str: + ... + + reveal_type(zero) # N: Revealed type is 'Any' diff --git a/typesafety/test_curry/test_curry/test_curry_arguments.yml b/typesafety/test_curry/test_curry/test_curry_arguments.yml new file mode 100644 index 000000000..980c9021c --- /dev/null +++ b/typesafety/test_curry/test_curry/test_curry_arguments.yml @@ -0,0 +1,202 @@ +- case: curry_pos_only_args + disable_cache: true + skip: sys.version_info < (3, 8) + main: | + from returns.curry import curry + + @curry + def multiple( + a: int, + b: int, + c: int, + /, + d: int, + ) -> str: + ... + + reveal_type(multiple) # N: Revealed type is 'Overload(def (builtins.int) -> Overload(def (builtins.int, builtins.int, d: builtins.int) -> builtins.str, def (builtins.int, builtins.int) -> def (d: builtins.int) -> builtins.str, def (builtins.int) -> Overload(def (builtins.int, d: builtins.int) -> builtins.str, def (builtins.int) -> def (d: builtins.int) -> builtins.str)), def (builtins.int, builtins.int) -> Overload(def (builtins.int, d: builtins.int) -> builtins.str, def (builtins.int) -> def (d: builtins.int) -> builtins.str), def (builtins.int, builtins.int, builtins.int) -> def (d: builtins.int) -> builtins.str, def (builtins.int, builtins.int, builtins.int, d: builtins.int) -> builtins.str)' + + +# TODO: remove skip after this bug in `mypy` is fixed: +# https://github.com/python/mypy/issues/8801 +- case: curry_init_magic_method + disable_cache: true + skip: true + main: | + from returns.curry import curry + + class Test(object): + @curry + def __init__(self, arg: int, other: str) -> None: + ... + + reveal_type(Test) # N: Revealed type is 'Overload(def (arg: builtins.int) -> def (other: builtins.str) -> ex.Test, def (arg: builtins.int, other: builtins.str) -> ex.Test)' + + +- case: curry_call_magic_method + disable_cache: true + main: | + from returns.curry import curry + + class Test(object): + @curry + def __call__(self, arg: int, other: float, last: str) -> str: + ... + + reveal_type(Test()(1)) # N: Revealed type is 'Overload(def (other: builtins.float, last: builtins.str) -> builtins.str, def (other: builtins.float) -> def (last: builtins.str) -> builtins.str)' + + +- case: curry_classmethod1 + disable_cache: true + main: | + from returns.curry import curry + + class Test(object): + @curry + @classmethod + def some(cls, arg: int, other: float, last: str) -> str: + ... + + reveal_type(Test.some) # N: Revealed type is 'Overload(def () -> Overload(def (arg: builtins.int, other: builtins.float, last: builtins.str) -> builtins.str, def (arg: builtins.int, other: builtins.float) -> def (last: builtins.str) -> builtins.str, def (arg: builtins.int) -> Overload(def (other: builtins.float, last: builtins.str) -> builtins.str, def (other: builtins.float) -> def (last: builtins.str) -> builtins.str)), def (arg: builtins.int) -> Overload(def (other: builtins.float, last: builtins.str) -> builtins.str, def (other: builtins.float) -> def (last: builtins.str) -> builtins.str), def (arg: builtins.int, other: builtins.float) -> def (last: builtins.str) -> builtins.str, def (arg: builtins.int, other: builtins.float, last: builtins.str) -> builtins.str)' + + reveal_type(Test.some(1)) # N: Revealed type is 'Overload(def (other: builtins.float, last: builtins.str) -> builtins.str, def (other: builtins.float) -> def (last: builtins.str) -> builtins.str)' + + reveal_type(Test.some(1, 2.0, 'a')) # N: Revealed type is 'builtins.str' + + +- case: curry_classmethod2 + disable_cache: true + main: | + from returns.curry import curry + from typing import Callable + + class Test(object): + @curry + @classmethod + def some(cls, arg: int, other: str) -> str: + ... + + def test(c: Callable[[int, str], str]) -> str: + return c(1, 'a') + + reveal_type(test(Test.some)) # N: Revealed type is 'builtins.str' + + +- case: curry_classmethod3 + disable_cache: true + main: | + from returns.curry import curry + from typing import Callable + + class Test(object): + @curry + @classmethod + def some(cls, first:str, arg: int, other: str) -> str: + ... + + def test(c: Callable[[int, str], str]) -> str: + return c(1, 'a') + + reveal_type(test(Test.some('a'))) # N: Revealed type is 'builtins.str' + + +- case: curry_staticmethod + disable_cache: true + main: | + from returns.curry import curry + + class Test(object): + @curry + @staticmethod + def some(arg: int, other: float, last: str) -> str: + ... + + reveal_type(Test.some) # N: Revealed type is 'Overload(def (arg: builtins.int) -> Overload(def (other: builtins.float, last: builtins.str) -> builtins.str, def (other: builtins.float) -> def (last: builtins.str) -> builtins.str), def (arg: builtins.int, other: builtins.float) -> def (last: builtins.str) -> builtins.str, def (arg: builtins.int, other: builtins.float, last: builtins.str) -> builtins.str)' + + +- case: curry_regular_method + disable_cache: true + main: | + from returns.curry import curry + + class Test(object): + @curry + def some(self, arg: int, other: float, last: str) -> str: + ... + + reveal_type(Test.some) # N: Revealed type is 'Overload(def (self: main.Test) -> Overload(def (arg: builtins.int, other: builtins.float, last: builtins.str) -> builtins.str, def (arg: builtins.int, other: builtins.float) -> def (last: builtins.str) -> builtins.str, def (arg: builtins.int) -> Overload(def (other: builtins.float, last: builtins.str) -> builtins.str, def (other: builtins.float) -> def (last: builtins.str) -> builtins.str)), def (self: main.Test, arg: builtins.int) -> Overload(def (other: builtins.float, last: builtins.str) -> builtins.str, def (other: builtins.float) -> def (last: builtins.str) -> builtins.str), def (self: main.Test, arg: builtins.int, other: builtins.float) -> def (last: builtins.str) -> builtins.str, def (self: main.Test, arg: builtins.int, other: builtins.float, last: builtins.str) -> builtins.str)' + + reveal_type(Test.some(Test(), 1)) # N: Revealed type is 'Overload(def (other: builtins.float, last: builtins.str) -> builtins.str, def (other: builtins.float) -> def (last: builtins.str) -> builtins.str)' + + reveal_type(Test().some) # N: Revealed type is 'Overload(def () -> Overload(def (arg: builtins.int, other: builtins.float, last: builtins.str) -> builtins.str, def (arg: builtins.int, other: builtins.float) -> def (last: builtins.str) -> builtins.str, def (arg: builtins.int) -> Overload(def (other: builtins.float, last: builtins.str) -> builtins.str, def (other: builtins.float) -> def (last: builtins.str) -> builtins.str)), def (arg: builtins.int) -> Overload(def (other: builtins.float, last: builtins.str) -> builtins.str, def (other: builtins.float) -> def (last: builtins.str) -> builtins.str), def (arg: builtins.int, other: builtins.float) -> def (last: builtins.str) -> builtins.str, def (arg: builtins.int, other: builtins.float, last: builtins.str) -> builtins.str)' + + reveal_type(Test().some(1)) # N: Revealed type is 'Overload(def (other: builtins.float, last: builtins.str) -> builtins.str, def (other: builtins.float) -> def (last: builtins.str) -> builtins.str)' + + +- case: curry_match_callable_protocol1 + disable_cache: true + main: | + from returns.curry import curry + from typing import Callable + + class Test(object): + @curry + def some(self, a: int, arg: int, other: str) -> str: + ... + + def test(c: Callable[[int, str], str]) -> str: + return c(1, 'a') + + reveal_type(test(Test().some(1))) # N: Revealed type is 'builtins.str' + + +- case: curry_match_callable_protocol2 + disable_cache: true + main: | + from returns.curry import curry + from typing import Callable + + class Test(object): + @curry + def some(self, arg: int, other: str) -> str: + ... + + def test(c: Callable[[int, str], str]) -> str: + return c(1, 'a') + + reveal_type(test(Test().some)) # N: Revealed type is 'builtins.str' + + +- case: curry_match_callable_protocol3 + disable_cache: true + main: | + from returns.curry import curry + from typing import Callable + + class Test(object): + @curry + def some(self, arg: int, other: float) -> str: + ... + + def test(c: Callable[[int], Callable[[float], str]]) -> str: + return c(1)(5.0) + + reveal_type(test(Test().some)) # N: Revealed type is 'builtins.str' + + +- case: curry_match_callable_protocol4 + disable_cache: true + main: | + from returns.curry import curry + from typing import Callable + + class Test(object): + @curry + @classmethod + def some(cls, arg: int, other: float) -> str: + ... + + def test(c: Callable[[int], Callable[[float], str]]) -> str: + return c(1)(5.0) + + reveal_type(test(Test.some)) # N: Revealed type is 'builtins.str' diff --git a/typesafety/test_curry/test_curry/test_curry_generics.yml b/typesafety/test_curry/test_curry/test_curry_generics.yml new file mode 100644 index 000000000..f1fbdbee2 --- /dev/null +++ b/typesafety/test_curry/test_curry/test_curry_generics.yml @@ -0,0 +1,76 @@ +- case: curry_single_generic_arg + disable_cache: true + main: | + from returns.curry import curry + from typing import List, TypeVar + + T = TypeVar('T') + + @curry + def zero(arg: List[T]) -> T: + ... + + x: List[int] + + reveal_type(zero) # N: Revealed type is 'def [T] (arg: builtins.list[T`-1]) -> T`-1' + reveal_type(zero(x)) # N: Revealed type is 'builtins.int*' + + +- case: curry_two_generic_args1 + disable_cache: true + main: | + from returns.curry import curry + from typing import List, TypeVar + + T = TypeVar('T') + + @curry + def zero(arg: List[T], other: int) -> T: + ... + + x: List[int] + + reveal_type(zero) # N: Revealed type is 'Overload(def [T] (arg: builtins.list[T`-1]) -> def (other: builtins.int) -> T`-1, def [T] (arg: builtins.list[T`-1], other: builtins.int) -> T`-1)' + reveal_type(zero(x)) # N: Revealed type is 'def (other: builtins.int) -> builtins.int*' + reveal_type(zero(x)(1)) # N: Revealed type is 'builtins.int*' + reveal_type(zero(x, 1)) # N: Revealed type is 'builtins.int*' + + +- case: curry_two_generic_args2 + disable_cache: true + main: | + from returns.curry import curry + from typing import List, TypeVar + + T = TypeVar('T') + + @curry + def zero(arg: int, other: List[T]) -> T: + ... + + x: List[int] + + reveal_type(zero) # N: Revealed type is 'Overload(def (arg: builtins.int) -> def [T] (other: builtins.list[T`-1]) -> T`-1, def [T] (arg: builtins.int, other: builtins.list[T`-1]) -> T`-1)' + reveal_type(zero(1)) # N: Revealed type is 'def [T] (other: builtins.list[T`-1]) -> T`-1' + reveal_type(zero(1)(x)) # N: Revealed type is 'builtins.int*' + reveal_type(zero(1, x)) # N: Revealed type is 'builtins.int*' + + +- case: curry_two_generic_args3 + disable_cache: true + main: | + from returns.curry import curry + from typing import List, TypeVar + + T = TypeVar('T') + + @curry + def zero(arg: T, other: List[T]) -> T: + ... + + x: List[int] + + reveal_type(zero) # N: Revealed type is 'Overload(def [T] (arg: T`-1) -> def [T] (other: builtins.list[T`-1]) -> T`-1, def [T] (arg: T`-1, other: builtins.list[T`-1]) -> T`-1)' + reveal_type(zero(1)) # N: Revealed type is 'def [T] (other: builtins.list[builtins.int*]) -> builtins.int*' + reveal_type(zero(1)(x)) # N: Revealed type is 'builtins.int' + reveal_type(zero(1, x)) # N: Revealed type is 'builtins.int*' diff --git a/typesafety/test_curry/test_curry.yml b/typesafety/test_curry/test_partial/test_partial.yml similarity index 90% rename from typesafety/test_curry/test_curry.yml rename to typesafety/test_curry/test_partial/test_partial.yml index dc37b039f..ca94b1862 100644 --- a/typesafety/test_curry/test_curry.yml +++ b/typesafety/test_curry/test_partial/test_partial.yml @@ -1,4 +1,4 @@ -- case: curry_zero_args +- case: partial_zero_args disable_cache: true main: | from returns.curry import partial @@ -9,7 +9,7 @@ reveal_type(partial(two_args)) # N: Revealed type is 'def (first: builtins.int, second: builtins.float) -> builtins.str' -- case: curry_single_arg +- case: partial_single_arg disable_cache: true main: | from returns.curry import partial @@ -20,7 +20,7 @@ reveal_type(partial(two_args, 1)) # N: Revealed type is 'def (second: builtins.float) -> builtins.str' -- case: curry_all_args +- case: partial_all_args disable_cache: true main: | from returns.curry import partial @@ -31,7 +31,7 @@ reveal_type(partial(two_args, 1, second=0.5)) # N: Revealed type is 'def () -> builtins.str' -- case: curry_single_named_arg +- case: partial_single_named_arg disable_cache: true main: | from returns.curry import partial @@ -42,7 +42,7 @@ reveal_type(partial(two_args, second=1.0)) # N: Revealed type is 'def (first: builtins.int) -> builtins.str' -- case: curry_multiple_args +- case: partial_multiple_args disable_cache: true main: | from returns.curry import partial @@ -60,7 +60,7 @@ reveal_type(partial(multiple, 1, 0.4, flag3=int, flag2=True)) # N: Revealed type is 'def (third: builtins.str, flag1: builtins.bool) -> builtins.str' -- case: curry_not_callable_type +- case: partial_not_callable_type disable_cache: true main: | from returns.curry import partial @@ -70,7 +70,7 @@ reveal_type(curried_int) # N: Revealed type is 'def () -> builtins.int' -- case: curry_explicit_noreturn +- case: partial_explicit_noreturn disable_cache: true main: | from returns.curry import partial @@ -82,7 +82,7 @@ reveal_type(partial(exit, 1)) # N: Revealed type is 'def () -> ' -- case: curry_wrong_argument_types +- case: partial_wrong_argument_types disable_cache: true main: | from returns.curry import partial @@ -101,7 +101,7 @@ main:11: error: Argument 2 to "multiple" has incompatible type "bool"; expected "Literal[True]" -- case: curry_too_many_positional_args +- case: partial_too_many_positional_args disable_cache: true main: | from returns.curry import partial @@ -118,7 +118,7 @@ main:10: error: Too many arguments for "multiple" -- case: curry_invalid_named_arg +- case: partial_invalid_named_arg disable_cache: true main: | from returns.curry import partial diff --git a/typesafety/test_curry/test_curry_arguments.yml b/typesafety/test_curry/test_partial/test_partial_arguments.yml similarity index 87% rename from typesafety/test_curry/test_curry_arguments.yml rename to typesafety/test_curry/test_partial/test_partial_arguments.yml index 3931d6b9a..ffe9271c4 100644 --- a/typesafety/test_curry/test_curry_arguments.yml +++ b/typesafety/test_curry/test_partial/test_partial_arguments.yml @@ -1,4 +1,4 @@ -- case: curry_complex_args +- case: partial_complex_args disable_cache: true main: | from returns.curry import partial @@ -37,7 +37,7 @@ main:23: note: Revealed type is 'def (*args: builtins.float, *, d: builtins.str, e: builtins.bool =, **kwargs: builtins.str) -> builtins.str' -- case: curry_args_kwargs +- case: partial_args_kwargs disable_cache: true main: | from returns.curry import partial @@ -51,7 +51,7 @@ reveal_type(partial(multiple, 1, 2, 3, x='x', y='y')(4, 5, z='z')) # N: Revealed type is 'builtins.str' -- case: curry_pos_only_args +- case: partial_pos_only_args disable_cache: true skip: sys.version_info < (3, 8) main: | @@ -73,7 +73,7 @@ reveal_type(partial(multiple, 1, 2, 3, d=4)) # N: Revealed type is 'def () -> builtins.str' -- case: curry_object +- case: partial_object disable_cache: true main: | from returns.curry import partial @@ -96,7 +96,33 @@ main:13: note: Revealed type is 'def () -> builtins.int' -- case: curry_union +- case: partial_classmethod + disable_cache: true + main: | + from returns.curry import partial + + class Test(object): + @classmethod + def some(cls, arg: int, other: str) -> float: + ... + + reveal_type(partial(Test.some, 1)) # N: Revealed type is 'def (other: builtins.str) -> builtins.float' + + +- case: partial_staticmethod + disable_cache: true + main: | + from returns.curry import partial + + class Test(object): + @staticmethod + def some(arg: int, other: str) -> float: + ... + + reveal_type(partial(Test.some, 1)) # N: Revealed type is 'def (other: builtins.str) -> builtins.float' + + +- case: partial_union disable_cache: true main: | from typing import Union @@ -119,7 +145,7 @@ main:15: note: Revealed type is 'def (*Any, **Any)' -- case: curry_type_var +- case: partial_type_var disable_cache: true main: | from typing import Callable, TypeVar @@ -139,7 +165,7 @@ test(first) -- case: curry_type_type +- case: partial_type_type disable_cache: true main: | from typing import Type, TypeVar @@ -158,7 +184,7 @@ return t(1) -- case: curry_star_arg +- case: partial_star_arg disable_cache: true main: | from returns.curry import partial @@ -169,7 +195,7 @@ reveal_type(partial(multiple, *(1, 2))) # N: Revealed type is 'def (*Any, **Any) -> builtins.int*' -- case: curry_star2_arg +- case: partial_star2_arg disable_cache: true main: | from returns.curry import partial @@ -180,7 +206,7 @@ reveal_type(partial(multiple, **{'a': 1, 'b': 2})) # N: Revealed type is 'def (*Any, **Any) -> builtins.int*' -- case: curry_lambda +- case: partial_lambda disable_cache: true main: | from returns.curry import partial diff --git a/typesafety/test_curry/test_curry_generic.yml b/typesafety/test_curry/test_partial/test_partial_generic.yml similarity index 96% rename from typesafety/test_curry/test_curry_generic.yml rename to typesafety/test_curry/test_partial/test_partial_generic.yml index d20db7bc3..5da3f57c5 100644 --- a/typesafety/test_curry/test_curry_generic.yml +++ b/typesafety/test_curry/test_partial/test_partial_generic.yml @@ -1,4 +1,4 @@ -- case: curry_wrong_generic +- case: partial_wrong_generic disable_cache: true main: | from returns.curry import partial @@ -21,7 +21,7 @@ main:15: note: Revealed type is 'builtins.int*' -- case: curry_correct_generic +- case: partial_correct_generic disable_cache: true main: | from returns.curry import partial @@ -43,7 +43,7 @@ main:15: note: Revealed type is 'builtins.int*' -- case: curry_single_generic +- case: partial_single_generic disable_cache: true main: | from returns.curry import partial @@ -79,7 +79,7 @@ main:22: note: Revealed type is 'builtins.int*' -- case: curry_double_generic_complex37 +- case: partial_double_generic_complex37 disable_cache: true skip: sys.version_info >= (3, 8) main: | @@ -116,7 +116,7 @@ # Python3.8+ sorts generic arguments differently: -- case: curry_double_generic_complex38 +- case: partial_double_generic_complex38 disable_cache: true skip: sys.version_info < (3, 8) main: | @@ -152,7 +152,7 @@ main:23: note: Revealed type is 'def [B] (*, b: builtins.list[B`-1]) -> Union[builtins.str*, B`-1]' -- case: curry_double_generic +- case: partial_double_generic disable_cache: true main: | from returns.curry import partial diff --git a/typesafety/test_curry/test_curry_overload.yml b/typesafety/test_curry/test_partial/test_partial_overload.yml similarity index 96% rename from typesafety/test_curry/test_curry_overload.yml rename to typesafety/test_curry/test_partial/test_partial_overload.yml index 2c51c20e1..35f9c9c67 100644 --- a/typesafety/test_curry/test_curry_overload.yml +++ b/typesafety/test_curry/test_partial/test_partial_overload.yml @@ -1,4 +1,4 @@ -- case: curry_wrong_overload1 +- case: partial_wrong_overload1 disable_cache: true main: | from typing import overload @@ -24,7 +24,7 @@ main:15: note: Revealed type is 'def (*Any, **Any) -> builtins.int*' -- case: curry_wrong_overload2 +- case: partial_wrong_overload2 disable_cache: true main: | from typing import overload @@ -50,7 +50,7 @@ main:15: note: Revealed type is 'Any' -- case: curry_regular_overload +- case: partial_regular_overload disable_cache: true main: | from typing import overload @@ -84,7 +84,7 @@ main:23: note: Revealed type is 'def (b: builtins.str) -> builtins.str' -- case: curry_generic_overload_kind1 +- case: partial_generic_overload_kind1 disable_cache: true main: | from typing import overload, TypeVar, List, Set @@ -125,7 +125,7 @@ main:29: note: Revealed type is 'def () -> builtins.float*' -- case: curry_generic_overload_kind2 +- case: partial_generic_overload_kind2 disable_cache: true main: | from typing import overload, TypeVar, List, Union