On 14 September 2017 at 09:43, Lukasz Langa <luk...@langa.pl> wrote:
>> On Sep 13, 2017, at 6:37 PM, Nick Coghlan <ncogh...@gmail.com> wrote:
>> That way, during the "from __future__ import lazy_annotations" period,
>> folks will have clearer guidance on how to explicitly opt-in to eager
>> evaluation via function and class decorators.
>
> I like this idea! For classes it would have to be a function that you call 
> post factum. The way class decorators are implemented, they cannot evaluate 
> annotations that contain forward references. For example:
>
> class Tree:
>     left: Tree
>     right: Tree
>
>     def __init__(self, left: Tree, right: Tree):
>         self.left = left
>         self.right = right
>
> This is true today, get_type_hints() called from within a class decorator 
> will fail on this class. However, a function performing postponed evaluation 
> can do this without issue. If a class decorator knew what name a class is 
> about to get, that would help. But that's a different PEP and I'm not writing 
> that one ;-)

The class decorator case is indeed a bit more complicated, but there
are a few tricks available to create a forward-reference friendly
evaluation environment.

1. To get the right globals namespace, you can do:

    global_ns = sys.modules[cls.__module__].__dict__

2. Define the evaluation locals as follows:

    local_ns = collections.ChainMap({cls.__name__: cls}, cls.__dict__)

3. Evaluate the variable and method annotations using "eval(expr,
global_ns, local_ns)"

If you make the eager annotation evaluation recursive (so the
decorator can be applied to the outermost class, but also affects all
inner class definitions), then it would even be sufficient to allow
nested classes to refer to both the outer class as well as other inner
classes (regardless of definition order).

To prevent inadvertent eager evaluation of annotations on functions
and classes that are merely referenced from a class attribute, the
recursive descent would need to be conditional on "attr.__qualname__
== cls.__qualname__ + '.' + attr.__name__".

So something like:

    def eager_class_annotations(cls):
        global_ns = sys.modules[cls.__module__].__dict__
        local_ns = collections.ChainMap({cls.__name__: cls}, cls.__dict__)
        annotations = cls.__annotations__
        for k, v in annotations.items():
            annotations[k] = eval(v, global_ns, local_ns)
        for attr in cls.__dict__.values():
            name = getattr(attr, "__name__", None)
            if name is None:
                continue
            qualname = getattr(attr, "__qualname__", None)
            if qualname is None:
                continue
            if qualname != f"{cls.__qualname}.{name}":
                continue
            if isinstance(attr, type):
                eager_class_annotations(attr)
            else:
                eager_annotations(attr)
        return cls

You could also hide the difference between eager annotation evaluation
on a class or a function inside a single decorator:

    def eager_annotations(obj):
        if isinstance(obj, type):
            _eval_class_annotations(obj) # Class
        elif hasattr(obj, "__globals__"):
            _eval_annotations(obj, obj.__globals__) # Function
        else:
            _eval_annotations(obj, obj.__dict__) # Module
        return obj

Given the complexity of the class decorator variant, I now think it
would actually make sense for the PEP to propose *providing* these
decorators somewhere in the standard library (the lower level "types"
module seems like a reasonable candidate, but we've historically
avoided having that depend on the full collections module)

Cheers,
Nick.

-- 
Nick Coghlan   |   ncogh...@gmail.com   |   Brisbane, Australia
_______________________________________________
Python-ideas mailing list
Python-ideas@python.org
https://mail.python.org/mailman/listinfo/python-ideas
Code of Conduct: http://python.org/psf/codeofconduct/

Reply via email to