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
20 changes: 9 additions & 11 deletions src/makefun/main.py
Original file line number Diff line number Diff line change
Expand Up @@ -246,6 +246,8 @@ def create_function(func_signature, # type: Union[str, Signature]
if isinstance(func_signature, str):
# transform the string into a Signature and make sure the string contains ":"
func_name_from_str, func_signature, func_signature_str = get_signature_from_string(func_signature, evaldict)
if '__signature__' in attrs:
lucaswiman marked this conversation as resolved.
Show resolved Hide resolved
attrs['__signature__'] = func_signature

# if not explicitly overridden using `func_name`, the name in the string takes over
if func_name_from_str is not None:
Expand Down Expand Up @@ -819,9 +821,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 +964,9 @@ 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__'))
all_attrs.setdefault("__wrapped__", wrapped)
lucaswiman marked this conversation as resolved.
Show resolved Hide resolved
lucaswiman marked this conversation as resolved.
Show resolved Hide resolved
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
10 changes: 10 additions & 0 deletions tests/issue_85_module.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
from dataclasses import dataclass
lucaswiman marked this conversation as resolved.
Show resolved Hide resolved


def forwardref_method(foo: "ForwardRef", bar: str) -> "ForwardRef":
return foo.x + bar


@dataclass
class ForwardRef:
x: str = "default"
21 changes: 18 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 a
assert "bar" in inspect.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,19 @@ 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


def test_issue_85_wrapped_forwardref_annotation():
import typing
from . import issue_85_module

@wraps(issue_85_module.forwardref_method, remove_args=["bar"])
def decorated(*args, **kwargs):
lucaswiman marked this conversation as resolved.
Show resolved Hide resolved
return issue_85_module.forwardref_method(*args, **kwargs, bar="x")

lucaswiman marked this conversation as resolved.
Show resolved Hide resolved
assert decorated(issue_85_module.ForwardRef()) == "defaultx"
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(decorated) == expected_annotations