-
-
Notifications
You must be signed in to change notification settings - Fork 408
Description
TLDR
It would be very useful to extract the __annotations__ handling logic currently in _transform_attrs into a standalone function that can be used to convert annotations into _CountingAttrs and _CountAttr.types. This would support attrs-based plugins that set or modify attribute validators/converters/metadata before attr.s-based class generation.
Current State
One major strength of attrs is the ability to layer metadata, validation and conversion logic onto attr classes to implement project-specific class declaration patterns.
As a toy example, consider the case of adding simple init-time type validation to a class:
@attr.s
class Foo:
bar = attr.ib(type=str, validator=attr.validators.instance_of(str))
bat = attr.ib(type=int, validator=attr.validators.instance_of(int))
baz = attr.ib(type=float, validator=attr.validators.instance_of(float))Very punchy, but we're spoiled. This can be dry-ed out with a trivial decorator:
def validate_attr_types(cls):
counting_attrs = [
v for v in cls.__dict__.values()
if isinstance(v, attr._make._CountingAttr)
]
for ca in counting_attrs:
if ca.type:
ca.validator(attr.validator.instance_of(ca.type))
return cls
@attr.s
@validate_attr_types
class Foo:
bar = attr.ib(type=str)
bat = attr.ib(type=int)
baz = attr.ib(type=float)As our decorator is in the business of modifying the declared attributes to add validation (or conversion, metadata, ...) it needs to run before attr.s, which freezes _CountingAttrs down into Attributes. This isn't an obstacle, as we're declaring all the attr.ibs and most of the time we'll define a nice wrapper around attr.ib if we need any non-trivial logic.
The Problem
It's 2018 and type annotations are in! We would really prefer to write something like:
@attr.s(auto_attribs=True)
@validate_attr_types
class Foo:
bar : str
bat : int
baz : float Unfortunately this gets us into a real sticky wicket. At the time our decorator fires Foo doesn't have any _CountingAttr instances available for us to update, just entries in the __annotations__ dict. The naive idea of swapping the decorator ordering is no good, as the generated attributes would be frozen by attr.s. We might be able to let attr.s run, hackily create an updated variant of the class, call attr.s again, and then return the second generated class but this just feels wrong.
Slightly subtly, this problem bites us even if we mandate that all attr.ibs are declared:
@attr.s(auto_attribs=True)
@validate_attr_types
class Foo:
bar : str = attr.ib()
bat : int = attr.ib()
baz : float = attr.ib()Our decorator will now fail because the type information stored in the __annotations__ hasn't yet been extracted into _CountingAttr.type.
A Solution
It would be ideal if attrs offered an idempotent function to convert all __annotations__ based attribute information into concrete, mutable _CountingAttr instances on a pre-attr.s class. This would convert:
class FooPre:
bar : str
bat : int = attr.ib()
baz = attr.ib(type=float)into _CountingAttr equivalents of:
class Foo:
bar = attr.ib(type=str)
bat = attr.ib(type=int)
baz = attr.ib(type=float)Given the straw-man name attr.transform_annotations, this would let us write our simple decorator as:
def validate_attr_types(cls):
# in-place update of cls from __annotations__
attr.transform_annotations(cls, auto_attribs=True)
counting_attrs = [
v for v in cls.__dict__.values()
if isinstance(v, attr._make._CountingAttr)
]
for ca in counting_attrs:
if ca.type:
ca.validator(attr.validator.instance_of(ca.type))
return clsand write:
@attr.s
@validate_attr_types
class Foo:
bar : str
bat : int
baz : float