python-attrs / python-attrs/attrs

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

Open
#424 19 comments 0 reactions 0 assignees View on GitHub

Nobody has claimed this yet.

Feature
Dominant language
Python
Stars
5.8k
Forks
480
Avg merge
2h 15m
Merged PRs (30d)
2

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 cls

and write:

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

Contributor guide

Open the contributing guide

First steps

  1. Read the whole issue, then the project's contributing guide.
  2. Comment on the issue to say you are picking it up — it saves two people doing the same work.
  3. Fork the repository and make your change on a branch.
  4. Open a pull request that references the issue number.

Research direction

Start in src/attr/_make.py at the annotation-handling logic in _transform_attrs. Trace how annotated fields become _CountingAttr instances and how explicit attr.ib declarations receive their types. Done means an idempotent public function can perform that pre-attr.s conversion, including the examples described in the issue, with behavior that supports subsequent attribute customization.

Written by the indexing model from the issue text.

Assessment

Tech stack
python
Domain
developer-experience
Issue type
Feature
Difficulty
5/5
Estimated time
Over a week
Activity status
Stale
Clarity
Mostly clear
Newbie friendliness
35/100

Get new issues in your inbox

A short digest of beginner-friendly GitHub issues.