Skip to content

Commit 4250119

Browse files
author
Sylvain MARIE
committed
New method partial that behaves like functools.partial, and equivalent decorator @with_partial. Fixes #30
1 parent 13360d0 commit 4250119

File tree

7 files changed

+387
-11
lines changed

7 files changed

+387
-11
lines changed

docs/index.md

Lines changed: 52 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -306,6 +306,58 @@ modified signature: (z, c, o=True)
306306
They might save you a few lines of code if your use-case is not too specific.
307307

308308

309+
#### Removing parameters easily
310+
311+
As goodies, `makefun` provides a `partial` function that are equivalent to `functools.partial`, except that it is fully signature-preserving and modifies the documentation with a nice helper message explaining that this is a partial view:
312+
313+
```python
314+
def foo(x, y):
315+
"""
316+
a `foo` function
317+
318+
:param x:
319+
:param y:
320+
:return:
321+
"""
322+
return x + y
323+
324+
from makefun import partial
325+
bar = partial(foo, x=12)
326+
```
327+
328+
we can test it:
329+
330+
```python
331+
>>> assert bar(1) == 13
332+
>>> help(bar)
333+
Help on function bar in module makefun.tests.test_partial_and_macros:
334+
335+
bar(y)
336+
<This function is equivalent to 'foo(y, x=12)', see original 'foo' doc below.>
337+
338+
a `foo` function
339+
340+
:param x:
341+
:param y:
342+
:return:
343+
```
344+
345+
A decorator is also available to create partial views easily for quick tests:
346+
347+
```python
348+
@with_partial(x=12)
349+
def foo(x, y):
350+
"""
351+
a `foo` function
352+
353+
:param x:
354+
:param y:
355+
:return:
356+
"""
357+
return x + y
358+
```
359+
360+
309361
### 3- Advanced topics
310362

311363
#### Generators and Coroutines

makefun/__init__.py

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,10 +1,10 @@
11
from makefun.main import create_function, with_signature, remove_signature_parameters, add_signature_parameters, \
2-
wraps, create_wrapper
2+
wraps, create_wrapper, partial, with_partial
33

44
__all__ = [
55
# submodules
66
'main',
77
# symbols
88
'create_function', 'with_signature', 'remove_signature_parameters', 'add_signature_parameters',
9-
'wraps', 'create_wrapper'
9+
'wraps', 'create_wrapper', 'partial', 'with_partial'
1010
]

makefun/_main_latest_py.py

Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,20 @@
1+
from itertools import chain
2+
3+
from makefun.main import wraps
4+
5+
6+
def make_partial_using_yield_from(new_sig, f, *preset_pos_args, **preset_kwargs):
7+
"""
8+
Makes a 'partial' when f is a generator and python is new enough to support `yield from`
9+
10+
:param new_sig:
11+
:param f:
12+
:param presets:
13+
:return:
14+
"""
15+
@wraps(f, new_sig)
16+
def partial_f(*args, **kwargs):
17+
# since the signature does the checking for us, no need to check for redundancy.
18+
kwargs.update(preset_kwargs) # for python 3.4: explicit dict update
19+
yield from f(*chain(preset_pos_args, args), **kwargs)
20+
return partial_f

makefun/_main_legacy_py.py

Lines changed: 50 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,53 @@
1+
import sys
2+
from itertools import chain
3+
4+
from makefun.main import wraps
5+
6+
7+
def make_partial_using_yield(new_sig, f, *preset_pos_args, **preset_kwargs):
8+
"""
9+
Makes a 'partial' when f is a generator and python is new enough to support `yield from`
10+
11+
:param new_sig:
12+
:param f:
13+
:param preset_pos_args:
14+
:param preset_kwargs:
15+
:return:
16+
"""
17+
@wraps(f, new_sig=new_sig)
18+
def partial_f(*args, **kwargs):
19+
# since the signature does the checking for us, no need to check for redundancy.
20+
kwargs.update(preset_kwargs)
21+
gen = f(*chain(preset_pos_args, args), **kwargs)
22+
_i = iter(gen) # initialize the generator
23+
_y = next(_i) # first iteration
24+
while 1:
25+
try:
26+
_s = yield _y # yield the first output and retrieve the new input
27+
except GeneratorExit as _e: # ---generator exit error---
28+
try:
29+
_m = _i.close # if there is a close method
30+
except AttributeError:
31+
pass
32+
else:
33+
_m() # use it first
34+
raise _e # then re-raise exception
35+
except BaseException as _e: # ---other exception
36+
_x = sys.exc_info() # if captured exception, grab info
37+
try:
38+
_m = _i.throw # if there is a throw method
39+
except AttributeError:
40+
raise _e # otherwise re-raise
41+
else:
42+
_y = _m(*_x) # use it
43+
else: # --- nominal case: the new input was received
44+
# if _s is None:
45+
# _y = next(_i)
46+
# else:
47+
_y = _i.send(_s) # let the implementation decide if None means "no new input" or "new input = None"
48+
return partial_f
49+
50+
151
def get_legacy_py_generator_body_template():
252
"""
353
In Python 2 we cannot use `yield from` in the generated function body.

makefun/main.py

Lines changed: 87 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -198,15 +198,7 @@ def create_function(func_signature, # type: Union[str, Signature]
198198
if inject_as_first_arg:
199199
params_str = "%s, %s" % (func_name, params_str)
200200

201-
if (3, 5) <= sys.version_info < (3, 6):
202-
# with Python 3.5 isgeneratorfunction returns True for all coroutines
203-
# however we know that it is NOT possible to have a generator
204-
# coroutine in python 3.5: PEP525 was not there yet
205-
generatorcaller = isgeneratorfunction(func_impl) and not iscoroutinefunction(func_impl)
206-
else:
207-
generatorcaller = isgeneratorfunction(func_impl)
208-
209-
if generatorcaller:
201+
if _is_generator_func(func_impl):
210202
if sys.version_info >= (3, 3):
211203
body = "def %s\n yield from _func_impl_(%s)\n" % (func_signature_str, params_str)
212204
else:
@@ -239,6 +231,21 @@ def create_function(func_signature, # type: Union[str, Signature]
239231
return f
240232

241233

234+
def _is_generator_func(func_impl):
235+
"""
236+
Return True if the func_impl is a generator
237+
:param func_impl:
238+
:return:
239+
"""
240+
if (3, 5) <= sys.version_info < (3, 6):
241+
# with Python 3.5 isgeneratorfunction returns True for all coroutines
242+
# however we know that it is NOT possible to have a generator
243+
# coroutine in python 3.5: PEP525 was not there yet
244+
return isgeneratorfunction(func_impl) and not iscoroutinefunction(func_impl)
245+
else:
246+
return isgeneratorfunction(func_impl)
247+
248+
242249
class DefaultHolder:
243250
__slots__ = 'varname'
244251

@@ -778,3 +785,74 @@ def add_signature_parameters(s, # type: Signature
778785
lst.append(last)
779786

780787
return s.replace(parameters=lst)
788+
789+
790+
def with_partial(*preset_pos_args, **preset_kwargs):
791+
"""
792+
Decorator to 'partialize' a function using `partial`
793+
794+
:param preset_pos_args:
795+
:param preset_kwargs:
796+
:return:
797+
"""
798+
def apply_decorator(f):
799+
return partial(f, *preset_pos_args, **preset_kwargs)
800+
return apply_decorator
801+
802+
803+
def partial(f, *preset_pos_args, **preset_kwargs):
804+
"""
805+
806+
:param preset_pos_args:
807+
:param preset_kwargs:
808+
:return:
809+
"""
810+
# TODO do we need to mimic `partial`'s behaviour concerning positional args?
811+
812+
# (1) remove all preset arguments from the signature
813+
orig_sig = signature(f)
814+
# first the first n positional
815+
if len(orig_sig.parameters) <= len(preset_pos_args):
816+
raise ValueError("Cannot preset %s positional args, function %s has only %s args."
817+
"" % (len(preset_pos_args), f.__name__, len(orig_sig.parameters)))
818+
new_sig = Signature(parameters=tuple(orig_sig.parameters.values())[len(preset_pos_args):],
819+
return_annotation=orig_sig.return_annotation)
820+
# then the keyword
821+
try:
822+
new_sig = remove_signature_parameters(new_sig, *preset_kwargs.keys())
823+
except KeyError as e:
824+
raise ValueError("Cannot preset keyword argument, it does not appear to be present in the signature of %s: %s"
825+
"" % (f.__name__, e))
826+
827+
if _is_generator_func(f):
828+
if sys.version_info >= (3, 3):
829+
from makefun._main_latest_py import make_partial_using_yield_from
830+
partial_f = make_partial_using_yield_from(new_sig, f, *preset_pos_args, **preset_kwargs)
831+
else:
832+
from makefun._main_legacy_py import make_partial_using_yield
833+
partial_f = make_partial_using_yield(new_sig, f, *preset_pos_args, **preset_kwargs)
834+
else:
835+
@wraps(f, new_sig=new_sig)
836+
def partial_f(*args, **kwargs):
837+
# since the signature does the checking for us, no need to check for redundancy.
838+
kwargs.update(preset_kwargs)
839+
return f(*itertools.chain(preset_pos_args, args), **kwargs)
840+
841+
# update the doc
842+
argstring = ', '.join([("%s" % a) for a in preset_pos_args])
843+
if len(argstring) > 0:
844+
argstring = argstring + ', '
845+
argstring = argstring + str(new_sig)[1:-1]
846+
if len(argstring) > 0:
847+
argstring = argstring + ', '
848+
argstring = argstring + ', '.join(["%s=%s" % (k, v) for k, v in preset_kwargs.items()])
849+
new_line = "<This function is equivalent to '%s(%s)', see original '%s' doc below.>\n" \
850+
"" % (partial_f.__name__, argstring, partial_f.__name__)
851+
# new_line = new_line + ("-" * (len(new_line) - 1)) + '\n'
852+
try:
853+
doc = getattr(partial_f, '__doc__')
854+
partial_f.__doc__ = new_line + doc
855+
except AttributeError:
856+
partial_f.__doc__ = new_line
857+
858+
return partial_f
Lines changed: 49 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,49 @@
1+
from makefun import with_partial
2+
3+
4+
def test_doc():
5+
def foo(x, y):
6+
"""
7+
a `foo` function
8+
9+
:param x:
10+
:param y:
11+
:return:
12+
"""
13+
return x + y
14+
15+
from makefun import partial
16+
bar = partial(foo, x=12)
17+
bar.__name__ = 'bar'
18+
help(bar)
19+
assert bar(1) == 13
20+
21+
22+
def test_partial():
23+
"""Tests that `with_partial` works"""
24+
25+
@with_partial(a='hello')
26+
def foo(x, y, a):
27+
"""
28+
a `foo` function
29+
30+
:param x:
31+
:param y:
32+
:param a:
33+
:return:
34+
"""
35+
print(a)
36+
print(x, y)
37+
38+
foo(1, 2)
39+
help(foo)
40+
41+
assert foo.__doc__ == """<This function is equivalent to 'foo(x, y, a=hello)', see original 'foo' doc below.>
42+
43+
a `foo` function
44+
45+
:param x:
46+
:param y:
47+
:param a:
48+
:return:
49+
"""

0 commit comments

Comments
 (0)