Skip to content

Commit

Permalink
Merge pull request #86 from lucaswiman/gh85-use-__signature__
Browse files Browse the repository at this point in the history
Use  `__signature__` and set `__wrapped__` attribute even on signature-changing decorators.
  • Loading branch information
smarie authored Sep 7, 2022
2 parents de9caee + 0fe6a7e commit 9050569
Show file tree
Hide file tree
Showing 4 changed files with 45 additions and 15 deletions.
2 changes: 1 addition & 1 deletion docs/api_reference.md
Original file line number Diff line number Diff line change
Expand Up @@ -166,7 +166,7 @@ Comparison with `@with_signature`: `@wraps(f)` is equivalent to

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`.

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.
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/).

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

Expand Down
24 changes: 13 additions & 11 deletions src/makefun/main.py
Original file line number Diff line number Diff line change
Expand Up @@ -278,6 +278,11 @@ def create_function(func_signature, # type: Union[str, Signature]
else:
raise TypeError("Invalid type for `func_signature`: %s" % type(func_signature))

if isinstance(attrs.get('__signature__'), str):
# __signature__ must be a Signature object, so if it is a string,
# we need to evaluate it.
attrs['__signature__'] = get_signature_from_string(attrs['__signature__'], evaldict)[1]

# extract all information needed from the `Signature`
params_to_kw_assignment_mode = get_signature_params(func_signature)
params_names = list(params_to_kw_assignment_mode.keys())
Expand Down Expand Up @@ -819,9 +824,11 @@ def wraps(wrapped_fun,
`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`.
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.
The additional `__wrapped__` attribute is set 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 additional
`__signature__` attribute will be set so that `inspect.signature` and related functionality
works as expected. See PEP 362 for more detail on `__wrapped__` and `__signature__`.
See also [python documentation on @wraps](https://docs.python.org/3/library/functools.html#functools.wraps)
Expand Down Expand Up @@ -960,15 +967,10 @@ def _get_args_for_wrapping(wrapped, new_sig, remove_args, prepend_args, append_a

# attributes: start from the wrapped dict, add '__wrapped__' if needed, and override with all attrs.
all_attrs = copy(getattr_partial_aware(wrapped, '__dict__'))
# PEP362: always set `__wrapped__`, and if signature was changed, set `__signature__` too
all_attrs["__wrapped__"] = wrapped
if has_new_sig:
# change of signature: delete the __wrapped__ attribute if any
try:
del all_attrs['__wrapped__']
except KeyError:
pass
else:
# no change of signature: we can safely set the __wrapped__ attribute
all_attrs['__wrapped__'] = wrapped
all_attrs["__signature__"] = func_sig
all_attrs.update(attrs)

return func_name, func_sig, doc, qualname, co_name, module_name, all_attrs
Expand Down
7 changes: 7 additions & 0 deletions tests/_issue_85_module.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
def forwardref_method(foo: "ForwardRef", bar: str) -> "ForwardRef":
return ForwardRef(foo.x + bar)


class ForwardRef:
def __init__(self, x="default"):
self.x = x
27 changes: 24 additions & 3 deletions tests/test_issues.py
Original file line number Diff line number Diff line change
Expand Up @@ -181,11 +181,10 @@ def wrapper(foo):
def second_wrapper(foo, bar):
return wrapper(foo) + bar

assert second_wrapper.__wrapped__ is wrapper
assert "bar" in signature(second_wrapper).parameters
assert second_wrapper(1, -1) == 0

with pytest.raises(AttributeError):
second_wrapper.__wrapped__


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

assert asyncio.get_event_loop().run_until_complete(asyncio.ensure_future(f_partial().__anext__())) == 1



@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.")
def test_issue_85_wrapped_forwardref_annotation():
import typing
from . import _issue_85_module

@wraps(_issue_85_module.forwardref_method, remove_args=["bar"])
def wrapper(**kwargs):
kwargs["bar"] = "x" # python 2 syntax to prevent syntax error.
return _issue_85_module.forwardref_method(**kwargs)

# Make sure the wrapper function works as expected
assert wrapper(_issue_85_module.ForwardRef()).x == "defaultx"

# Check that the type hints of the wrapper are ok with the forward reference correctly resolved
expected_annotations = {
"foo": _issue_85_module.ForwardRef,
"return": _issue_85_module.ForwardRef,
}
assert typing.get_type_hints(wrapper) == expected_annotations

0 comments on commit 9050569

Please sign in to comment.