attrs: don't use __annotations__

https://github.com/python-attrs/attrs/blob/aa501176fac1f12912f863fd2f5c4d1d08410322/src/attr/_make.py#L213 uses __annotations__

However, it looks as though an upcoming incompatible change to Python will change __annotations__ into a list of strings.

It seems like the correct public API to use to retrieve the information attrs wants would be get_type_hints.

As a bonus, fixing this might make #265 easier to implement.

About this issue

  • Original URL
  • State: closed
  • Created 7 years ago
  • Comments: 37 (34 by maintainers)

Most upvoted comments

So I played around with this for a while and came to the following conclusion. attrs should probably use __annotations__ but have a way to resolve the types to real types if needed.

The real issue is there’s no way to guarantee that typing.get_type_hints() won’t fail. And about 90% of the time you don’t need it. (Technically you need the type for ClassVar but I would just check for “ClassVar” or “typing.ClassVar”)

I found three places where you can call typing.get_type_hints()

  1. At class creation time. This will work find if there are no forward or self references on the class. Otherwise NameError. (I believe you can hack something to make self references work, by passing {cls.name: cls} to localns.)

  2. When a class is instantiated (for example by adding something to init) this works fine for self references and most forward references. Except it won’t work if those types are defined in if TYPE_CHECKING: sections.

Here’s the code to resolve_types: I believe caching is useful because typing.get_type_hints can be somewhat slow. I don’t know how y’all feel about hanging something off the class.

def resolve_types(cls, globalns=None, localns=None):
    """
    Resolve any strings and forward annotations in type annotations.

    :param type cls: Class to resolve.
    :param globalns:
    :param localns:

    :raise TypeError: If *cls* is not a class.
    :raise attr.exceptions.NotAnAttrsClassError: If *cls* is not an ``attrs``
        class.
    :raise NameError: If a cannot be resolved because of missing variables.

    :rtype: True
    """
    try:
        # Since getting type hints is expensive we cache whether we've done
        # it already.
        return cls.__attrs_types_resolved__
    except AttributeError:
        import typing
        hints = typing.get_type_hints(cls, globalns=globalns, localns=localns)
        for field in fields(cls):
            if field.name in hints:
                _obj_setattr(field, 'type', hints[field.name])
        cls.__attrs_types_resolved__ = True
        return cls.__attrs_types_resolved__

This was how I modified _get_annotations to get them at class creation time:

def _get_annotations(cls):
    """
    Get annotations for *cls*.
    """
    anns = getattr(cls, "__annotations__", None)
    if anns is None:
        return {}

    # Verify that the annotations aren't merely inherited.
    for super_cls in cls.__mro__[1:]:
        if anns is getattr(super_cls, "__annotations__", None):
            return {}

    if anns:
        import typing
        try:
            ret = typing.get_type_hints(cls)
            cls.__attrs_types_resolved__ = True
            return ret
        except NameError:
            pass

    return anns

And this is how I check them at instance creation time, by adding it to the generated init.

    if any(a.type for a in attrs):
        lines.append("attr_resolve_types(type(self))")
        names_for_globals["attr_resolve_types"] = resolve_types

If we don’t care then we can do nothing.

❤️❤️❤️

I’m very confused. They want to remove typing, but…we have to use typing.get_type_hints() so things keep working!?

Why am I already regretting ever supporting this half-baked bullshit? 😢

So I guess I should add support to cattrs for this? I wonder if there’s comprehensive docs anywhere for tools that use runtime types that would help me adjust. Are we just supposed to call get_type_hints() if we encounter a string type? Is that going to be super slow if we do it on every single use?

TBH I feel like the Python typing crowd kinda looks down on using types in runtime, which is amusing because runtime types are several orders of magnitude more useful to me at this point than running mypy over my codebases (which remains borderline useless). My codebases simply wouldn’t work at all without runtime types, and the alternatives are much, much uglier and less user-friendly.

If we don’t care then we can do nothing.

No open source maintenance best open source maintenance.

I think so.