Open e508c9b1-f137-4a74-8873-03ddfcb92496 opened 4 years ago
In version 3.2, bpo-8814 introduced copying the __annotations__ property from the wrapped function to the wrapper by default.
That would be the desired behavior when your wrapper function has the same signature than the function it wraps, but in some cases (for example, with the contextlib.asynccontextmanager function) the return value is different, and then the __annotations__ property will have invalid information:
In [2]: from contextlib import asynccontextmanager
In [3]: @asynccontextmanager ...: async def mytest() -> int: ...: return 1 ...:
In [4]: mytest.__annotations__
Out[4]: {'return': int}
I propose changing the behavior of wraps, to only assign the __annotations by default if there's no __annotations already in the wrapper function, that would fit most default cases, but would allow to preserve the __annotations of the wrapper function when the types are explicitly specified, allowing now to change the contextlib.asynccontextmanager function with the proper types (returning now an AsyncContextManager) and keep the __annotation valid.
I'll try to get a POC and attach to the issue, but please comment with your ideas too.
I don't understand this use-case, but would it make sense to ChainMap
the wrapper's annotations on top of the wrapped annotations?
Hi Terry,
That would not work in this case, as I'd want to override all annotations with the wrapper function ones if there's any, instead of merging them.
The specific use case, is a type checker (part of TestSlide testing framework), to verify that if there's any type annotations, the parameters mocked and passed to it are the expected types.
For example, the contextmanager decorator returns an actual ContextManager, wrapping whatever the wrapped function returned, so if the wrapped function annotations prevail, then there's no way if verifying that the returned type is correct.
Thanks for the ChainMap pointer though, I'll use it for sure somewhere else.
As a note, mypy does not tpyecheck the wrapper functions, probably because it would not be possible with the current code (as the typing hints get lost):
https://mypy.readthedocs.io/en/latest/generics.html?highlight=wrapper#declaring-decorators
Elaborating on the last message, given the following code:
1 #!/usr/bin/env python3
2
3 from functools import wraps
4
5
6 def return_string(wrapped):
7 @wraps(wrapped)
8 def wrapper(an_int: int) -> str:
9 return str(wrapped(an_int))
10
11 return wrapper
12
13
14 @return_string
15 def identity(an_int: int) -> int:
16 return an_int
17
18 def print_bool(a_bool: bool) -> None:
19 print(a_bool)
20
21 def identity_nonwrapped(an_int: int) -> int:
22 return an_int
23
24
25 print_bool(a_bool=identity(7))
26 print_bool(a_bool=identity_nonwrapped(7))
mypy will complain only on the last line, being unable to check properly the line 25.
I'll investigate a bit more on why mypy skips that.
Only 3.8+ for bug fixes.
This is fixed by #21392
It will need rebasing though :)
Note: these values reflect the state of the issue at the time it was migrated and might not reflect the current state.
Show more details
GitHub fields: ```python assignee = None closed_at = None created_at =
labels = ['3.8', 'type-bug', 'library', '3.9', '3.10']
title = 'Type annotations lost when using wraps by default'
updated_at =
user = 'https://github.com/david-caro'
```
bugs.python.org fields:
```python
activity =
actor = 'terry.reedy'
assignee = 'none'
closed = False
closed_date = None
closer = None
components = ['Library (Lib)']
creation =
creator = 'David Caro'
dependencies = []
files = []
hgrepos = []
issue_num = 41231
keywords = ['patch']
message_count = 6.0
messages = ['373233', '373238', '373258', '373299', '373303', '373497']
nosy_count = 3.0
nosy_names = ['terry.reedy', 'Terry Davis', 'David Caro']
pr_nums = ['21392']
priority = 'normal'
resolution = None
stage = 'patch review'
status = 'open'
superseder = None
type = 'behavior'
url = 'https://bugs.python.org/issue41231'
versions = ['Python 3.8', 'Python 3.9', 'Python 3.10']
```