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/