From 0df53bd62778523437922a87d21de8726e98135a Mon Sep 17 00:00:00 2001 From: Ryan Grout Date: Thu, 18 Aug 2022 00:37:24 -0500 Subject: [PATCH 01/10] Add functoolz.reorder_args --- toolz/functoolz.py | 32 +++++++++++++++++++++++++++++--- 1 file changed, 29 insertions(+), 3 deletions(-) diff --git a/toolz/functoolz.py b/toolz/functoolz.py index 2c75d3a4..68efd6ee 100644 --- a/toolz/functoolz.py +++ b/toolz/functoolz.py @@ -1,7 +1,7 @@ -from functools import reduce, partial +from functools import reduce, partial, wraps import inspect import sys -from operator import attrgetter, not_ +from operator import itemgetter, attrgetter, not_ from importlib import import_module from types import MethodType @@ -12,7 +12,7 @@ __all__ = ('identity', 'apply', 'thread_first', 'thread_last', 'memoize', 'compose', 'compose_left', 'pipe', 'complement', 'juxt', 'do', - 'curry', 'flip', 'excepts') + 'curry', 'flip', 'excepts', 'reorder_args') PYPY = hasattr(sys, 'pypy_version_info') @@ -731,6 +731,32 @@ def flip(func, a, b): return func(b, a) +def reorder_args(func, old_args, new_args): + """ Returns a new function with a desired argument order. + + >>> def op(a, b, c): + ... return a // (b - c) + ... + >>> new_op = reorder_args(op, ('a', 'b', 'c'), ('c', 'a', 'b')) + >>> new_op(1, 2, 3) == op(2, 3, 1) + True + """ + assert len(old_args) == num_required_args(func) + assert set(old_args) == set(new_args) + arg_map = itemgetter(*(new_args.index(v) for v in old_args)) + + @wraps(func) + def wrapper(*args, **kwargs): + return func(*arg_map(args), **kwargs) + + wrapper.__code__ = wrapper.__code__.replace( + co_name=f"{func.__name__}{tuple(old_args)}->{tuple(new_args)}" + ) + wrapper.__dict__['argorder'] = tuple(new_args) + return wrapper + + + def return_none(exc): """ Returns None. """ From 61b260c6199130b21b90f15ac0950e509ac783cd Mon Sep 17 00:00:00 2001 From: Ryan Grout Date: Thu, 18 Aug 2022 16:44:58 -0500 Subject: [PATCH 02/10] Add tests. --- toolz/tests/test_functoolz.py | 18 ++++++++++++++++-- 1 file changed, 16 insertions(+), 2 deletions(-) diff --git a/toolz/tests/test_functoolz.py b/toolz/tests/test_functoolz.py index 555cf48d..fcb9fe4b 100644 --- a/toolz/tests/test_functoolz.py +++ b/toolz/tests/test_functoolz.py @@ -2,8 +2,8 @@ import toolz from toolz.functoolz import (thread_first, thread_last, memoize, curry, compose, compose_left, pipe, complement, do, juxt, - flip, excepts, apply) -from operator import add, mul, itemgetter + flip, excepts, apply, reorder_args) +from operator import add, mul, getitem, itemgetter from toolz.utils import raises from functools import partial @@ -794,3 +794,17 @@ def raise_(a): excepting = excepts(object(), object(), object()) assert excepting.__name__ == 'excepting' assert excepting.__doc__ == excepts.__doc__ + + +def test_reorder_args(): + def op(a, b, c, d=1): + return a // (b - c) + d + + new_op = reorder_args(op, ('a', 'b', 'c'), ('c', 'a', 'b')) + assert new_op(1, 2, 3, d=1) == op(2, 3, 1, d=1) + + # test builtin functions (ie C functions) + getflip = reorder_args(getitem, ('a', 'b'), ('b', 'a')) + get1 = curry(getflip, 1) + assert get1([1, 2, 3, 1, 1]) == 2 + \ No newline at end of file From ab8f3d328949a0d708a377d790d7b3c9d301477c Mon Sep 17 00:00:00 2001 From: Ryan Grout Date: Thu, 18 Aug 2022 16:45:37 -0500 Subject: [PATCH 03/10] Add curried version of reorder_args. --- toolz/curried/__init__.py | 1 + 1 file changed, 1 insertion(+) diff --git a/toolz/curried/__init__.py b/toolz/curried/__init__.py index 356eddbd..2ecaa045 100644 --- a/toolz/curried/__init__.py +++ b/toolz/curried/__init__.py @@ -88,6 +88,7 @@ reduce = toolz.curry(toolz.reduce) reduceby = toolz.curry(toolz.reduceby) remove = toolz.curry(toolz.remove) +reorder_args = toolz.curry(toolz.reorder_args) sliding_window = toolz.curry(toolz.sliding_window) sorted = toolz.curry(toolz.sorted) tail = toolz.curry(toolz.tail) From 36cf635fa9c1e63234b789282b0944cff14920e6 Mon Sep 17 00:00:00 2001 From: Ryan Grout Date: Thu, 18 Aug 2022 16:48:43 -0500 Subject: [PATCH 04/10] Remove extra newline. --- toolz/functoolz.py | 1 - 1 file changed, 1 deletion(-) diff --git a/toolz/functoolz.py b/toolz/functoolz.py index 68efd6ee..47330d25 100644 --- a/toolz/functoolz.py +++ b/toolz/functoolz.py @@ -756,7 +756,6 @@ def wrapper(*args, **kwargs): return wrapper - def return_none(exc): """ Returns None. """ From 0f78af4fc69278b44c933be7a8abfbc7b215fcb4 Mon Sep 17 00:00:00 2001 From: Ryan Grout Date: Fri, 19 Aug 2022 11:33:30 -0500 Subject: [PATCH 05/10] Replace method for Python <3.8 --- toolz/functoolz.py | 32 +++++++++++++++++++++++++++++--- 1 file changed, 29 insertions(+), 3 deletions(-) diff --git a/toolz/functoolz.py b/toolz/functoolz.py index 47330d25..c19b28f7 100644 --- a/toolz/functoolz.py +++ b/toolz/functoolz.py @@ -749,9 +749,35 @@ def reorder_args(func, old_args, new_args): def wrapper(*args, **kwargs): return func(*arg_map(args), **kwargs) - wrapper.__code__ = wrapper.__code__.replace( - co_name=f"{func.__name__}{tuple(old_args)}->{tuple(new_args)}" - ) + def replace_co(co, **kwargs): + + arglist = ['co_argcount', + 'co_kwonlyargcount', + 'co_nlocals', + 'co_stacksize', + 'co_flags', + 'co_code', + 'co_consts', + 'co_names', + 'co_varnames', + 'co_filename', + 'co_name', + 'co_firstlineno', + 'co_lnotab', + 'co_freevars', + 'co_cellvars'] + + co_args = [] + for attr in arglist: + if attr in kwargs: + co_args.append(kwargs[attr]) + else: + co_args.append(getattr(co, attr)) + + return CodeType(*co_args) + + new_co_name = "{}{}->{}".format(func.__name__, tuple(old_args), tuple(new_args)) + wrapper.__code__ = replace_co(wrapper.__code__, co_name=new_co_name) wrapper.__dict__['argorder'] = tuple(new_args) return wrapper From d39ef0bc72ac4983960a7a670605d63a9d1411e4 Mon Sep 17 00:00:00 2001 From: Ryan Grout Date: Fri, 19 Aug 2022 11:49:29 -0500 Subject: [PATCH 06/10] Move CodeType import to top. --- toolz/functoolz.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/toolz/functoolz.py b/toolz/functoolz.py index c19b28f7..610106db 100644 --- a/toolz/functoolz.py +++ b/toolz/functoolz.py @@ -3,7 +3,7 @@ import sys from operator import itemgetter, attrgetter, not_ from importlib import import_module -from types import MethodType +from types import MethodType, CodeType from .utils import no_default From bfc3f964f1b20c90ccf203466b825fc0137d4785 Mon Sep 17 00:00:00 2001 From: Ryan Grout Date: Wed, 19 Jul 2023 23:36:08 -0600 Subject: [PATCH 07/10] Better implementation. --- toolz/functoolz.py | 47 ++++++++++++---------------------------------- 1 file changed, 12 insertions(+), 35 deletions(-) diff --git a/toolz/functoolz.py b/toolz/functoolz.py index 610106db..adffe901 100644 --- a/toolz/functoolz.py +++ b/toolz/functoolz.py @@ -731,7 +731,7 @@ def flip(func, a, b): return func(b, a) -def reorder_args(func, old_args, new_args): +def reorder_args(func, new_args): """ Returns a new function with a desired argument order. >>> def op(a, b, c): @@ -741,44 +741,21 @@ def reorder_args(func, old_args, new_args): >>> new_op(1, 2, 3) == op(2, 3, 1) True """ - assert len(old_args) == num_required_args(func) - assert set(old_args) == set(new_args) - arg_map = itemgetter(*(new_args.index(v) for v in old_args)) + arg_map = [] + func_sig = inspect.signature(func) + for arg in func_sig.parameters.values(): + try: + new_ind = new_args.index(arg.name) + arg_map.append(new_ind) + except ValueError: + raise ValueError(f"Unable to find argument `{arg.name}` in signature for `{func.__name__}`") + _mapper = itemgetter(*arg_map) @wraps(func) def wrapper(*args, **kwargs): - return func(*arg_map(args), **kwargs) - - def replace_co(co, **kwargs): - - arglist = ['co_argcount', - 'co_kwonlyargcount', - 'co_nlocals', - 'co_stacksize', - 'co_flags', - 'co_code', - 'co_consts', - 'co_names', - 'co_varnames', - 'co_filename', - 'co_name', - 'co_firstlineno', - 'co_lnotab', - 'co_freevars', - 'co_cellvars'] - - co_args = [] - for attr in arglist: - if attr in kwargs: - co_args.append(kwargs[attr]) - else: - co_args.append(getattr(co, attr)) - - return CodeType(*co_args) + return func(*_mapper(args), **kwargs) - new_co_name = "{}{}->{}".format(func.__name__, tuple(old_args), tuple(new_args)) - wrapper.__code__ = replace_co(wrapper.__code__, co_name=new_co_name) - wrapper.__dict__['argorder'] = tuple(new_args) + wrapper.__signature__ = func_sig.replace(parameters=_mapper(list(func_sig.parameters.values()))) return wrapper From acd218135793e4e52b6ecd35e828dffb490f445e Mon Sep 17 00:00:00 2001 From: Ryan Grout Date: Wed, 19 Jul 2023 23:40:07 -0600 Subject: [PATCH 08/10] Update tests. --- toolz/tests/test_functoolz.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/toolz/tests/test_functoolz.py b/toolz/tests/test_functoolz.py index fcb9fe4b..ce9e50b6 100644 --- a/toolz/tests/test_functoolz.py +++ b/toolz/tests/test_functoolz.py @@ -800,11 +800,11 @@ def test_reorder_args(): def op(a, b, c, d=1): return a // (b - c) + d - new_op = reorder_args(op, ('a', 'b', 'c'), ('c', 'a', 'b')) + new_op = reorder_args(op, ('c', 'a', 'b')) assert new_op(1, 2, 3, d=1) == op(2, 3, 1, d=1) # test builtin functions (ie C functions) - getflip = reorder_args(getitem, ('a', 'b'), ('b', 'a')) + getflip = reorder_args(getitem, ('b', 'a')) get1 = curry(getflip, 1) assert get1([1, 2, 3, 1, 1]) == 2 \ No newline at end of file From 99f39bb749909d991904ce2d8c2b99f38fd1c9af Mon Sep 17 00:00:00 2001 From: Ryan Grout Date: Thu, 20 Jul 2023 00:09:59 -0600 Subject: [PATCH 09/10] Add all parts of signature to wrapper. --- toolz/functoolz.py | 26 +++++++++++++++++--------- 1 file changed, 17 insertions(+), 9 deletions(-) diff --git a/toolz/functoolz.py b/toolz/functoolz.py index adffe901..a704460a 100644 --- a/toolz/functoolz.py +++ b/toolz/functoolz.py @@ -737,25 +737,33 @@ def reorder_args(func, new_args): >>> def op(a, b, c): ... return a // (b - c) ... - >>> new_op = reorder_args(op, ('a', 'b', 'c'), ('c', 'a', 'b')) + >>> new_op = reorder_args(op, ('c', 'a', 'b')) >>> new_op(1, 2, 3) == op(2, 3, 1) True """ - arg_map = [] func_sig = inspect.signature(func) - for arg in func_sig.parameters.values(): - try: - new_ind = new_args.index(arg.name) - arg_map.append(new_ind) - except ValueError: - raise ValueError(f"Unable to find argument `{arg.name}` in signature for `{func.__name__}`") + arg_map = [] + parameters = [None] * len(func_sig.parameters) + for i, arg in enumerate(func_sig.parameters.values()): + if (arg.kind in {inspect.Parameter.POSITIONAL_ONLY, + inspect.Parameter.VAR_POSITIONAL, + inspect.Parameter.POSITIONAL_OR_KEYWORD} + and arg.default is inspect._empty): + try: + new_ind = new_args.index(arg.name) + arg_map.append(new_ind) + parameters[new_ind] = arg + except ValueError: + raise ValueError(f"Unable to find positional argument `{arg.name}` in signature for `{func.__name__}`") + else: + parameters[i] = arg _mapper = itemgetter(*arg_map) @wraps(func) def wrapper(*args, **kwargs): return func(*_mapper(args), **kwargs) - wrapper.__signature__ = func_sig.replace(parameters=_mapper(list(func_sig.parameters.values()))) + wrapper.__signature__ = func_sig.replace(parameters=parameters) return wrapper From f348d93a5fb1979784d562de7e4b6580d30be5d4 Mon Sep 17 00:00:00 2001 From: Ryan Grout Date: Thu, 20 Jul 2023 00:11:06 -0600 Subject: [PATCH 10/10] Remove unused import --- toolz/functoolz.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/toolz/functoolz.py b/toolz/functoolz.py index a704460a..09fec19e 100644 --- a/toolz/functoolz.py +++ b/toolz/functoolz.py @@ -3,7 +3,7 @@ import sys from operator import itemgetter, attrgetter, not_ from importlib import import_module -from types import MethodType, CodeType +from types import MethodType from .utils import no_default