Skip to content

Commit 9050569

Browse files
authored
Merge pull request #86 from lucaswiman/gh85-use-__signature__
Use `__signature__` and set `__wrapped__` attribute even on signature-changing decorators.
2 parents de9caee + 0fe6a7e commit 9050569

4 files changed

Lines changed: 45 additions & 15 deletions

File tree

docs/api_reference.md

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -166,7 +166,7 @@ Comparison with `@with_signature`: `@wraps(f)` is equivalent to
166166

167167
In other words, as opposed to `@with_signature`, the metadata (doc, module name, etc.) is provided by the wrapped `wrapped_fun`, so that the created function seems to be identical (except possiblyfor the signature). Note that all options in `with_signature` can still be overrided using parameters of `@wraps`.
168168

169-
If the signature is *not* modified through `new_sig`, `remove_args`, `append_args` or `prepend_args`, the additional `__wrapped__` attribute on the created function, to stay consistent with the `functools.wraps` behaviour.
169+
The additional `__wrapped__` attribute is added on the created function, to stay consistent with the `functools.wraps` behaviour. If the signature is modified through `new_sig`, `remove_args`, `append_args` or `prepend_args`, the `__signature__` attribute will be added per [PEP 362](https://peps.python.org/pep-0362/).
170170

171171
See also [python documentation on @wraps](https://docs.python.org/3/library/functools.html#functools.wraps)
172172

src/makefun/main.py

Lines changed: 13 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -278,6 +278,11 @@ def create_function(func_signature, # type: Union[str, Signature]
278278
else:
279279
raise TypeError("Invalid type for `func_signature`: %s" % type(func_signature))
280280

281+
if isinstance(attrs.get('__signature__'), str):
282+
# __signature__ must be a Signature object, so if it is a string,
283+
# we need to evaluate it.
284+
attrs['__signature__'] = get_signature_from_string(attrs['__signature__'], evaldict)[1]
285+
281286
# extract all information needed from the `Signature`
282287
params_to_kw_assignment_mode = get_signature_params(func_signature)
283288
params_names = list(params_to_kw_assignment_mode.keys())
@@ -819,9 +824,11 @@ def wraps(wrapped_fun,
819824
`wrapped_fun`, so that the created function seems to be identical (except possiblyfor the signature).
820825
Note that all options in `with_signature` can still be overrided using parameters of `@wraps`.
821826
822-
If the signature is *not* modified through `new_sig`, `remove_args`, `append_args` or `prepend_args`, the
823-
additional `__wrapped__` attribute on the created function, to stay consistent with the `functools.wraps`
824-
behaviour.
827+
The additional `__wrapped__` attribute is set on the created function, to stay consistent
828+
with the `functools.wraps` behaviour. If the signature is modified through `new_sig`,
829+
`remove_args`, `append_args` or `prepend_args`, the additional
830+
`__signature__` attribute will be set so that `inspect.signature` and related functionality
831+
works as expected. See PEP 362 for more detail on `__wrapped__` and `__signature__`.
825832
826833
See also [python documentation on @wraps](https://docs.python.org/3/library/functools.html#functools.wraps)
827834
@@ -960,15 +967,10 @@ def _get_args_for_wrapping(wrapped, new_sig, remove_args, prepend_args, append_a
960967

961968
# attributes: start from the wrapped dict, add '__wrapped__' if needed, and override with all attrs.
962969
all_attrs = copy(getattr_partial_aware(wrapped, '__dict__'))
970+
# PEP362: always set `__wrapped__`, and if signature was changed, set `__signature__` too
971+
all_attrs["__wrapped__"] = wrapped
963972
if has_new_sig:
964-
# change of signature: delete the __wrapped__ attribute if any
965-
try:
966-
del all_attrs['__wrapped__']
967-
except KeyError:
968-
pass
969-
else:
970-
# no change of signature: we can safely set the __wrapped__ attribute
971-
all_attrs['__wrapped__'] = wrapped
973+
all_attrs["__signature__"] = func_sig
972974
all_attrs.update(attrs)
973975

974976
return func_name, func_sig, doc, qualname, co_name, module_name, all_attrs

tests/_issue_85_module.py

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,7 @@
1+
def forwardref_method(foo: "ForwardRef", bar: str) -> "ForwardRef":
2+
return ForwardRef(foo.x + bar)
3+
4+
5+
class ForwardRef:
6+
def __init__(self, x="default"):
7+
self.x = x

tests/test_issues.py

Lines changed: 24 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -181,11 +181,10 @@ def wrapper(foo):
181181
def second_wrapper(foo, bar):
182182
return wrapper(foo) + bar
183183

184+
assert second_wrapper.__wrapped__ is wrapper
185+
assert "bar" in signature(second_wrapper).parameters
184186
assert second_wrapper(1, -1) == 0
185187

186-
with pytest.raises(AttributeError):
187-
second_wrapper.__wrapped__
188-
189188

190189
def test_issue_pr_67():
191190
"""Test handcrafted for https://github.com/smarie/python-makefun/pull/67"""
@@ -253,3 +252,25 @@ def test_issue_77_async_generator_partial():
253252
assert inspect.isasyncgenfunction(f_partial)
254253

255254
assert asyncio.get_event_loop().run_until_complete(asyncio.ensure_future(f_partial().__anext__())) == 1
255+
256+
257+
258+
@pytest.mark.skipif(sys.version_info < (3, 7, 6), reason="The __wrapped__ behavior in get_type_hints being tested was not added until python 3.7.6.")
259+
def test_issue_85_wrapped_forwardref_annotation():
260+
import typing
261+
from . import _issue_85_module
262+
263+
@wraps(_issue_85_module.forwardref_method, remove_args=["bar"])
264+
def wrapper(**kwargs):
265+
kwargs["bar"] = "x" # python 2 syntax to prevent syntax error.
266+
return _issue_85_module.forwardref_method(**kwargs)
267+
268+
# Make sure the wrapper function works as expected
269+
assert wrapper(_issue_85_module.ForwardRef()).x == "defaultx"
270+
271+
# Check that the type hints of the wrapper are ok with the forward reference correctly resolved
272+
expected_annotations = {
273+
"foo": _issue_85_module.ForwardRef,
274+
"return": _issue_85_module.ForwardRef,
275+
}
276+
assert typing.get_type_hints(wrapper) == expected_annotations

0 commit comments

Comments
 (0)