attrs icon indicating copy to clipboard operation
attrs copied to clipboard

Using Annotated in attrs

Open Dr-ZeeD opened this issue 4 years ago • 9 comments

I have been playing with Annotated recently, and was wondering whether this is something that might be used within attrs. My first instinct was "maybe this could be used for validators?". Maybe even field()?

Dr-ZeeD avatar Mar 08 '21 17:03 Dr-ZeeD

Full disclosure I haven't had a chance to use Annotated yet, though I'm excited about it for my own projects.

Are you thinking of something like:

@attr.define
class MyClass:
   x: int
   y: Annotated[int, Field(validator=...)] = 15

My big concerns of doing it for validators are:

  1. Type checking those validators. I don't know that the validator can actually be type-checked by mypy against the int. Though maybe that doesn't matter too much.
  2. In 3.10 those will be "string annotations" and so you're more likely to have the get_type_hints issues.

We'd probably not just want to stick attr.ib as the annotation since we probably don't want people sticking default values etc in there.

Reading the Pep a little more it looks like one idea is to have a series of classes rather than one class for the Annotations.

So you could do something like this

But it could be nice to stick the bool arguments in there oh you can even do it as separate "Annotations"

from attr import NoInit, Comparable, define
@attr.define
class MyClass:
   x: Annotated[int, NoInit(), Comparable()]
   y: Annotated[str, Comparable()]  = "wheee"

I can't decide if that's worse or better though.

euresti avatar Mar 08 '21 18:03 euresti

Just wanted to chime-in that I have a use-case for adding field configuration in Annotated types. Currently I'm using Annotated to denote parental relationship between objects:

from typing import Annotated, TypeVar
import attr

T = TypeVar("T", bound="Driver")

Parent = Annotated[T, "parent"]

@attr.s
class Driver:
    @property
    def parents(self) -> list[Driver]:
        def _is_parent_type(field: attr.Attribute, value: Any) -> bool:
            return type(field.type) is type(Parent) and "parent" in getattr(field.type, "__metadata__")
        return list(attr.asdict(self, recurse=False, filter=_is_parent_type).values())

@attr.s
class Bus(Driver):
   pass
   
@attr.s
class Device(Driver):
   bus: Parent[Bus] = attr.ib()

bus = Bus()
dev = Device(bus)
assert dev.parents == [bus]

What would be nice is to be able to add extra metadata into Parent so that things like repr=False could be passed into the attr.ib() field:

Parent = Annotated[T, "parent", Field(repr=False)]

uSpike avatar Feb 14 '22 22:02 uSpike

That said, it could even be improved to allow insertion of field metadata via Annotated:

Parent = Annotated[T, Field(repr=False, metadata={"parent": True})]

@attr.s
class Driver:
    @property
    def parents(self) -> list[Driver]:
        def _is_parent_type(field: attr.Attribute, value: Any) -> bool:
            return field.metadata.get("parent", False)
        return list(attr.asdict(self, recurse=False, filter=_is_parent_type).values())

uSpike avatar Feb 14 '22 22:02 uSpike