attrs icon indicating copy to clipboard operation
attrs copied to clipboard

Support 3.14 annotations in converter functions

Open DavidCEllis opened this issue 3 months ago • 2 comments

While #1451 added support for forward references in the classes themselves, forward references in converter functions still fail due to the use of inspect.signature which by default uses Format.VALUE when getting annotations.

Example:

from attrs import define, field

@define
class Works:
    x: unknown1

def converter(x: unknown2 | str) -> str:
    return str(x)

@define
class Fails:
    x: str = field(converter=converter)

Result:

NameError: name 'unknown2' is not defined

Cause: https://github.com/python-attrs/attrs/blob/1315e42fe51743fe1c3529da6792dcda2ef6cc1f/src/attr/_compat.py#L50

For Python 3.14 annotations you now need:

self.sig = inspect.signature(callable, annotation_format=annotationlib.Format.FORWARDREF)

DavidCEllis avatar Sep 10 '25 12:09 DavidCEllis

After talking to @Tinche, this seems to be a lot more complicated than it looks like.

We actually do use the results on class creation, so it might be that we'll have to special-case this entire thing and implement an __annotate__ on 3.14.

hynek avatar Sep 10 '25 13:09 hynek

You already kind of need to generate __annotate__ for 3.14 - at least for the __init__ function - but it's difficult with the current tools available from annotationlib. String annotations from attrs currently reveal the ForwardRef values.

from attrs import define
from annotationlib import get_annotations, Format

Vector = list[float]

@define
class Example:
    examples: list[Example]
    vectors: list[Vector]

# Format.STRING names show up in help()
print(get_annotations(Example.__init__, format=Format.STRING))
{
    'return': 'None', 
    # should be "list[Example]"
    'examples': "list[ForwardRef('Example', is_class=True, owner=<class 'ref_demo.Example'>)]", 
    # should be "list[Vector]"
    'vectors': 'list[list[float]]',
}

dataclasses has a related but different issue as it writes the types into the source code instead of generating __annotations__. See: https://github.com/python/cpython/issues/137530

In trying to solve this for dataclasses I've run into the issue that you can't easily use annotations collected in construction to generate a new __annotate__ function and recreating the logic used to gather the annotations in the first place is both awkward and problematic for VALUE annotations - do you leak forward references or do you fail for annotations on fields where init=False if they contain a forward reference?

I'd like a Format for annotations that deferred the evaluation further. Internally annotationlib already uses such a format when there's an exception in trying to get FORWARDREF annotations see here.

I think having these could make generating __annotate__ functions much closer to how you can currently create the __annotations__ dict. I've been kicking around a proposal for such a feature but I wasn't sure how broadly useful it would be - would you find this helpful?

DavidCEllis avatar Sep 10 '25 14:09 DavidCEllis