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

gh-85403: Make wraps retain type annotations #21392

Open
wants to merge 3 commits into
base: main
Choose a base branch
from

Conversation

david-caro
Copy link

@david-caro david-caro commented Jul 8, 2020

When decorating a function with wraps, retain the type annotations
of the wrapper if there's any, and use the wrapped function ones to
complete the annotations by default.

This changes the update_wrapper function to use the wrapper function
annotations if any, and fall back to the wrapped function annotations if the
wrapper one did not have them. For the return type annotations, and the
parameter types annotations independently.

The annotations overriding behavior was introduced in bpo-8814.

https://bugs.python.org/issue41231

When decorating a function with ``wraps``, retain the type annotations
of the wrapper if there's any, and use the wrapped function ones to
complete the annotations by default.

This changes the ``update_wrapper`` function to use the wrapper function
annotations if any, and fall back to the wrapped function annotations if the
wrapper one did not have them. For the return type annotations, and the
parameter types annotations independently.

The annotations overriding behavior was introduced in bpo-8814.
Signed-off-by: David Caro <david@dcaro.es>
@rhettinger rhettinger removed their request for review May 3, 2022
@python python deleted a comment from the-knights-who-say-ni Jul 11, 2022
@terryjreedy terryjreedy changed the title bpo-41231: Make wraps retain type annotations gh-85403: Make wraps retain type annotations Jul 11, 2022
@terryjreedy
Copy link
Member

@terryjreedy terryjreedy commented Jul 11, 2022

Conflict was trivial. Do a simple update merge of main into the branch rather than a rebase.

@hmc-cs-mdrissi
Copy link

@hmc-cs-mdrissi hmc-cs-mdrissi commented Jul 12, 2022

Hmm, how does this interact with pep 612?

For toy example,

from functools import wraps

def log_info(func: Callable[P, R]) -> Callable[P, R]:
  @wraps
  def _log_func(*args: P.args, **kwargs: P.kwargs) -> R:
    print("stuff")
    return func(*args, **kwargs)

  return _log_func

@log_info
def foo(x: int) -> str:
  ...

The current behavior of wrap is very different here than new behavior. Paramspecs are intended to preserve original function signature and not overrwrite it. I know that code that uses decorators with beam library's runtime time introspection would be affected by this change in a way that can cause it to fail to work. beam inspects function signature types and doesn't handle paramspecs. If both original function (foo) and inner wrapper function (_log_func) have all type hints available still at runtime then I think this is possible to solve, but a very easy edge case bug for any runtime type library.

Copy link
Member

@gvanrossum gvanrossum left a comment

I was quite confused by the docs (which make things much more complicated than the code).

``assigned`` list parameter), it will be populated only if missing some
annotations as follows:

* If the wrapper function defines it's own return type and parameter
Copy link
Member

@gvanrossum gvanrossum Jul 12, 2022

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
* If the wrapper function defines it's own return type and parameter
* If the wrapper function defines its own return type and parameter

annotations, those will be preserved.

* If the wrapper function does not annotate the return type, it will be
copied from the wrapped if any.
Copy link
Member

@gvanrossum gvanrossum Jul 12, 2022

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
copied from the wrapped if any.
copied from the wrapped function, if it specifies a return type.

When assigning the ``__annotations__`` attribute (when it's part of the
``assigned`` list parameter), it will be populated only if missing some
Copy link
Member

@gvanrossum gvanrossum Jul 12, 2022

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Is it not part of WRAPPER_ASSIGNMENTS?

copied from the wrapped if any.

* If the wrapped function does not annotate any parameters, the annotations
for them will be copied over from the wrapped if any.
Copy link
Member

@gvanrossum gvanrossum Jul 12, 2022

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
for them will be copied over from the wrapped if any.
for them will be copied over from the wrapped function, if it has any.

* If the wrapper function defines it's own return type and parameter
annotations, those will be preserved.
Copy link
Member

@gvanrossum gvanrossum Jul 12, 2022

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

It is not very clear from this paragraph what this actually does.

Suppose the wrapper has annotations for 'x' and 'y' and the wrapped function has annotations for 'z'. Does 'z' get added to the wrapper's annotations?

Suppose the wrapper has no annotations, so it receives annotations for 'x', 'y' and 'z' from the wrapped function. What happens if the wrapper doesn't have arguments by those names? That is going to be confusing in some cases of further introspection.

It almost sounds like __annotations__ should be listed under updated instead of assigned, there are so many special cases.

That ensures that any custom annotations from the wrapper will not be
overridden by the wrapped, allowing to change the annotated types of
the wrapped function.

.. versionadded:: 3.2
Copy link
Member

@gvanrossum gvanrossum Jul 12, 2022

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Clearly you need a versionadded:: 3.12 block for the new treatment of __annotations__.


if '__annotations__' in assigned:
try:
# Issue #41231: copy the annotations from wrapped, only if they are
Copy link
Member

@gvanrossum gvanrossum Jul 12, 2022

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Update the bug number using the GH-NNN or bpo-NNN format.

# if the wrapper does not annotate the return type, copy the
# annotation from the wrapped
elif ('return' not in wrapper.__annotations__
and 'return' in wrapped.__annotations__):
wrapper.__annotations__['return'] = wrapped.__annotations__['return']
Copy link
Member

@gvanrossum gvanrossum Jul 12, 2022

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Oh this is quite different from what I had understood from the docs (which I read before reading the code). I recommend changing the docs to more clearly describe what this does.

@gvanrossum
Copy link
Member

@gvanrossum gvanrossum commented Jul 12, 2022

Hmm, how does this interact with pep 612?

That's a really good point. I'll leave it to the patch author to respond, but without a good answer (and backwards compatible behavior, given that ParamSpec is in 3.10 and 3.11) this PR can't go through.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Projects
None yet
Development

Successfully merging this pull request may close these issues.

None yet

7 participants