Skip to content

Add function to "instantiate" __annotations__-derived 'attr.ib's. #424

@asford

Description

@asford

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 cls

and write:

@attr.s
@validate_attr_types
class Foo:
    bar : str
    bat : int
    baz : float

Metadata

Metadata

Assignees

No one assigned

    Labels

    Type

    No type

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions