larryhastings / co_annotations

Other
9 stars 5 forks source link

`functools.update_wrapper` will require changes for PEP 649 compatibility #21

Open ncoghlan opened 6 months ago

ncoghlan commented 6 months ago

(Adding the issue here rather than the CPython repo since the PEP 649 implementation hasn't landed yet)

Since Python 3.2, functools.update_wrapper has eagerly copied o.__annotations__ from the wrapped object to the wrapper object.

This will do the wrong thing under PEP 649, since it will trigger early evaluation of the annotations on wrapped functions (and hence potentially fail if the annotations contain forward references).

I think the following changes will be sufficient to resolve the incompatibility:

For maximum compatibility, I think the wrapper annotations will need to be retrieved as follows:

    def __annotate__(format):
        if format == 1:
            return wrapped.__annotations__ # Ensure previously cached annotations are respected
        import inspect # Ugh, maybe the guts of `get_annotations` could live down in `functools`?
        return inspect.get_annotations(wrapped, format)
    wrapper.__annotate__ = __annotate__

I initially thought we could just add __annotate__ to WRAPPER_ASSIGNMENTS, but I realised that doesn't play nice if a previous decorator has already eagerly evaluated the annotations, and perhaps modified them (in which case, wrapped.__annotate__ may even be None if the other decorator has been updated to behave as PEP 649 recommends).

The comment in the implementation sketch relates to the fact that inspect imports functools, so having functools depend on an inspect API introduces a circular dependency. Perhaps the piece that update_wrapper needs could be factored out to a lower level functools._get_annotations helper API that inspect.get_annotations also invokes? If we did that, the PEP 649 compatible update_wrapper code would become:

    def __annotate__(format):
        return _get_annotations(wrapped, format)
    wrapper.__annotate__ = __annotate__
ncoghlan commented 6 months ago

inspect also pulls in many more modules as dependencies than functools does:

$ python3 -c 'import sys; print(len(sys.modules))'
32
$ python3 -c 'import sys; import functools; print(len(sys.modules))'
42
$ python3 -c 'import sys; import inspect; print(len(sys.modules))'
67