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
6 changes: 6 additions & 0 deletions docs/changelog.md
Original file line number Diff line number Diff line change
@@ -1,5 +1,11 @@
# Changelog

### Unreleased

- Fixed `partial` so call-time keyword arguments and nested partials can override preset keyword values,
consistently with `functools.partial`. This also applies to generators and async generators.
Fixes [#95](https://github.com/smarie/python-makefun/issues/95).

### 1.16.0 - Support for 3.14, dropped support for < 3.9

- Removed official support for python versions `<3.9`. These versions will not run in CI anymore.
Expand Down
5 changes: 3 additions & 2 deletions src/makefun/_main_legacy_py.py
Original file line number Diff line number Diff line change
Expand Up @@ -21,8 +21,9 @@ def make_partial_using_yield(new_sig, f, *preset_pos_args, **preset_kwargs):
@wraps(f, new_sig=new_sig)
def partial_f(*args, **kwargs):
# since the signature does the checking for us, no need to check for redundancy.
kwargs.update(preset_kwargs)
gen = f(*chain(preset_pos_args, args), **kwargs)
final_kwargs = preset_kwargs.copy()
final_kwargs.update(kwargs)
gen = f(*chain(preset_pos_args, args), **final_kwargs)
_i = iter(gen) # initialize the generator
_y = next(_i) # first iteration
while 1:
Expand Down
5 changes: 3 additions & 2 deletions src/makefun/_main_py35_and_higher.py
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,7 @@ def make_partial_using_yield_from(new_sig, f, *preset_pos_args, **preset_kwargs)
@wraps(f, new_sig)
def partial_f(*args, **kwargs):
# since the signature does the checking for us, no need to check for redundancy.
kwargs.update(preset_kwargs) # for python 3.4: explicit dict update
yield from f(*chain(preset_pos_args, args), **kwargs)
final_kwargs = preset_kwargs.copy()
final_kwargs.update(kwargs)
yield from f(*chain(preset_pos_args, args), **final_kwargs)
return partial_f
5 changes: 3 additions & 2 deletions src/makefun/_main_py36_and_higher.py
Original file line number Diff line number Diff line change
Expand Up @@ -19,8 +19,9 @@ def make_partial_using_async_for_in_yield(new_sig, f, *preset_pos_args, **preset

@wraps(f, new_sig=new_sig)
async def partial_f(*args, **kwargs):
kwargs.update(preset_kwargs)
async for v in f(*chain(preset_pos_args, args), **kwargs):
final_kwargs = preset_kwargs.copy()
final_kwargs.update(kwargs)
async for v in f(*chain(preset_pos_args, args), **final_kwargs):
yield v

return partial_f
5 changes: 3 additions & 2 deletions src/makefun/main.py
Original file line number Diff line number Diff line change
Expand Up @@ -1248,8 +1248,9 @@ def partial(f, # type: Callable
@wraps(f, new_sig=new_sig)
def partial_f(*args, **kwargs):
# since the signature does the checking for us, no need to check for redundancy.
kwargs.update(preset_kwargs)
return f(*itertools.chain(preset_pos_args, args), **kwargs)
final_kwargs = preset_kwargs.copy()
final_kwargs.update(kwargs)
return f(*itertools.chain(preset_pos_args, args), **final_kwargs)

# update the doc.
# Note that partial_f is generated above with a proper __name__ and __doc__ identical to the wrapped ones
Expand Down
28 changes: 27 additions & 1 deletion tests/test_generators_coroutines.py
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@

import pytest

from makefun import with_signature, create_function
from makefun import with_signature, create_function, partial

try:
from inspect import iscoroutinefunction
Expand Down Expand Up @@ -115,3 +115,29 @@ async def test_issue_96():
# verify that the new function is a native coroutine and behaves correctly
out = await dynamic_fun(0.1)
assert out == 0.1


@pytest.mark.parametrize("is_async_generator", [False, True])
async def test_partial_async_keyword_overrides(is_async_generator):
if is_async_generator:
async def f(a):
yield a

async def invoke(partial_f, **kwargs):
values = [item async for item in partial_f(**kwargs)]
return values[0]
else:
async def f(a):
return a

async def invoke(partial_f, **kwargs):
return await partial_f(**kwargs)

inner = partial(f, a=1)
outer = partial(inner, a=2)

assert await invoke(outer) == 2
assert await invoke(inner, a=3) == 3
assert await invoke(outer, a=4) == 4
assert await invoke(inner) == 1
assert await invoke(outer) == 2
57 changes: 57 additions & 0 deletions tests/test_partial_and_macros.py
Original file line number Diff line number Diff line change
Expand Up @@ -211,3 +211,60 @@ def f1(a):
# the func attribute is there too
f4 = functools.partial(f1)
assert f2.func == f4.func


@pytest.mark.parametrize("is_generator", [False, True])
def test_partial_keyword_overrides(is_generator):
"""Call-time and nested keyword assignments override partial defaults."""
if is_generator:
def f(a, b):
yield a, b

def invoke(partial_f, **kwargs):
return next(partial_f(**kwargs))
else:
def f(a, b):
return a, b

def invoke(partial_f, **kwargs):
return partial_f(**kwargs)

inner = makefun.partial(f, a=1)
outer = makefun.partial(inner, a=2, b=3)
reference = functools.partial(functools.partial(f, a=1), a=2, b=3)

assert str(signature(outer)) == str(signature(reference))
assert invoke(outer) == (2, 3)
assert invoke(inner, b=4) == (1, 4)

call_kwargs = {"a": 5, "b": 6}
assert invoke(inner, **call_kwargs) == (5, 6)
assert invoke(outer, **call_kwargs) == (5, 6)
assert call_kwargs == {"a": 5, "b": 6}
assert invoke(outer) == (2, 3)


def test_partial_keyword_override_preserves_positional_arguments():
def f(a, b, c):
return a, b, c

inner = makefun.partial(f, 1, c=2)
outer = makefun.partial(inner, 3, c=4)

assert outer() == (1, 3, 4)
assert outer(c=5) == (1, 3, 5)
assert inner(6) == (1, 6, 2)


def test_legacy_generator_partial_keyword_override():
from makefun._main_legacy_py import make_partial_using_yield

def f(a):
yield a

inner = make_partial_using_yield(signature(makefun.partial(f, a=1)), f, a=1)
outer = makefun.partial(inner, a=2)

assert next(outer()) == 2
assert next(inner(a=3)) == 3
assert next(inner()) == 1