larryhastings / co_annotations

Other
9 stars 5 forks source link

Accessing names in a class namespace #4

Closed larryhastings closed 3 years ago

larryhastings commented 3 years ago

I'm trying to get feature parity with existing (non-stringized) annotations. Consider this example:

# from __future__ import co_annotations

class C:
    abc = 'xyz'
    def method(self, a:abc=abc):
        ...

import inspect
print(inspect.signature(C.method))

That code works fine in Python 3.9. But if you try it in the co_annotations branch and uncomment the first line it no longer works.

I'm pretty sure it's because __co_annotations__ is a function, and the function is being defined inside a class, and Python doesn't let functions defined inside of classes see the class namespace. That's why it works if co_annotations isn't turned on; the __annotations__ dict is generated from within the class namespace, so the lookup works fine.

But, well. co_annotations functions are special, aren't they? And we want them to see fields defined in the class namespace. In fact we want them to be closures, keeping references to those values alive.

I'm posting this here in case anybody seeing this (Guido, Jelle) can suggest how to fix this. My best guess so far is that I need to add a fourth _Py_block_ty to symtable.h for co_annotations functions. This would be a nested block inside a ClassBlock that behaved like a FunctionBlock except it was allowed to see the definitions inside the ClassBlock. My fear is that there might be baked-in assumptions inside classes that e.g. a ClassBlock would never ever contain a... cell variable? Is that right? I'm still mostly working in the dark when it comes to how closures are implemented in Python.

JelleZijlstra commented 3 years ago

What if the annotation is being used inside a function:

class X:
    Alias = int
    def outer(self):
         x: Alias
         def inner(y: Alias): ...

On the one hand, maybe this should work because class-scoped type aliases are one of the main use cases for this sort of thing, and if you want to use the alias in the method's own annotation, you may also want to use it in a local variable. On the other hand, this doesn't work under current vanilla Python, so there's less of a need to support it.

Your sketch for how to fix the non-nested case was enough to get at least simple examples to work; I put up my code in #5. Undoubtedly there are edge cases that fail.

larryhastings commented 3 years ago

The desired semantics for PEP 649 should behave like vanilla (what I call "stock") semantics in Python 3.9, except for one super-power: an annotation can refer to things defined after it in the file. If you're defining an annotation on X, pretend that you could magically temporarily move X to the end of the scope it's defined in (module / class / function) just when evaluating its annotations.

So I don't think 649 should make the annotations on X.outer.x and X.outer.inner.y in your example viable.

JelleZijlstra commented 3 years ago

Makes sense. Here's another use case someone just brought up: https://bugs.python.org/issue43746.

from ... import losses

class A:
  # AttributeError: 'Losses' object has no attribute 'Losses' (under stock semantics)
  losses: losses.Losses = losses.Losses()

This makes sense looking at the disassembly: we first set losses in the class namespace, then evaluate the annotation. Under PEP 649 semantics that's also the sensible thing to do. However, type checkers currently accept this code.

larryhastings commented 3 years ago

Theoretically I should be interested in edge cases like this. But it's hard to see how defining an annotation in terms of the name it's annotating is valid Python.

ericvsmith commented 3 years ago

Theoretically I should be interested in edge cases like this. But it's hard to see how defining an annotation in terms of the name it's annotating is valid Python.

I think this is the crux of the problem. Are annotations supposed to be valid Python, or not? If not, then let's just admit it and say stringized annotations are the only way forward and that PEP 649 has no future.

Personally, I think annotations should be valid Python code and I'd like to see PEP 649 accepted.