Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Use __signature__ and set __wrapped__ attribute even on signature-changing decorators. #86

Merged
merged 12 commits into from
Sep 7, 2022
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"

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