This issue tracker has been migrated to GitHub, and is currently read-only.
For more information, see the GitHub FAQs in the Python's Developer Guide.

classification
Title: Type annotations lost when using wraps by default
Type: behavior Stage: patch review
Components: Library (Lib) Versions: Python 3.10, Python 3.9, Python 3.8
process
Status: open Resolution:
Dependencies: Superseder:
Assigned To: Nosy List: David Caro, Terry Davis, terry.reedy
Priority: normal Keywords: patch

Created on 2020-07-07 17:29 by David Caro, last changed 2022-04-11 14:59 by admin.

Pull Requests
URL Status Linked Edit
PR 21392 open David Caro, 2020-07-08 09:16
Messages (6)
msg373233 - (view) Author: David Caro (David Caro) * Date: 2020-07-07 17:29
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.
msg373238 - (view) Author: Terry Davis (Terry Davis) Date: 2020-07-07 18:37
I don't understand this use-case, but would it make sense to `ChainMap` the wrapper's __annotations__ on top of the wrapped __annotations__?
msg373258 - (view) Author: David Caro (David Caro) * Date: 2020-07-07 22:24
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.
msg373299 - (view) Author: David Caro (David Caro) * Date: 2020-07-08 10:14
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
msg373303 - (view) Author: David Caro (David Caro) * Date: 2020-07-08 10:31
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.
msg373497 - (view) Author: Terry J. Reedy (terry.reedy) * (Python committer) Date: 2020-07-11 01:56
Only 3.8+ for bug fixes.
History
Date User Action Args
2022-04-11 14:59:33adminsetgithub: 85403
2020-07-11 01:56:16terry.reedysetnosy: + terry.reedy

messages: + msg373497
versions: - Python 3.5, Python 3.6, Python 3.7
2020-07-08 10:31:08David Carosetmessages: + msg373303
2020-07-08 10:14:33David Carosetmessages: + msg373299
2020-07-08 09:16:01David Carosetkeywords: + patch
stage: patch review
pull_requests: + pull_request20538
2020-07-07 22:24:01David Carosetmessages: + msg373258
2020-07-07 18:37:11Terry Davissetnosy: + Terry Davis
messages: + msg373238
2020-07-07 17:29:15David Carocreate