attrs icon indicating copy to clipboard operation
attrs copied to clipboard

[RFC] A shortcut to define a classvar with the same name as an attribute

Open Drino opened this issue 4 years ago • 4 comments

Hello!

Thanks again for the great library! :)

After answering in a couple of issues regarding default values on class-level and debugging another mess with self-using factory which was fixed by migration to @cached_property, I thought that it may be good to have a simple way to set a class-level attribute with the same name as an attr.ib on attrs class.

The issue can be illustrated by a following example:

import attr

@attr.dataclass
class BaseClass:
    value: int
    magic: str

@attr.s
class FixedMagicClass(BaseClass):
    magic = attr.ib(init=False, default='alohomora')

@attr.s
class _ReprMagicClass(BaseClass):
    # A proxy class to exclude 'magic' from init
    magic = attr.ib(init=False)

@attr.s
class ReprMagicClass(_ReprMagicClass):
    @cached_property
    def magic(self):
        return repr(self.value)

I'd like to add classvar parameter in attr.ib and decorator syntactic sugar like this:

@attr.s
class FixedMagicClass(BaseClass):
    magic = attr.ib(init=False, classvar='alohomora')  # Almost no difference, but it can be accessed on the class object

@attr.s
class ReprMagicClass(BaseClass):
    @attr.ib(init=False).classvar
    @cached_property  # Property is a descriptor and is set on class object itself
    def magic(self):
        return repr(self.value)

Alternative approach may be implementing it via metadata + field_transformer, but syntax becomes clumsy. From my perspective this seems more like a core feature than like an extension.

Though, it may be a non-desired feature as it wouldn't work with slotted classes - they use their own slot descriptors - and therefore may complicate migration from dict-classes to slot-classes.

Other possible related feature, which may blend in perfectly with property and class-level constants, is a way to make an attr.ib skippable during __init__ - e.g. if an attribute is init=True and has default=attr.UNSET the generated __init__ code will be like following:

def __init__(self, required_attribute, attribute=attr.UNSET):
    self.required_attribute = required_attribute  #  This attribute had `attr.NOTHING` default in `attr.ib`
    if attribute is not attr.UNSET:  # If it wasn't set, we'll take the classvar instead
        self.attribute = attribute

I'll be happy to implement this proposal (actually - both of them) if it would be considered useful.

Drino avatar Jan 07 '21 00:01 Drino

A little update here:

Alternative approach may be implementing it via metadata + field_transformer

I've finally tried to implement this in my project via field_transformer and found out that this approach doesn't work - field_transformer is applied before we remove all attr.ib fields from the class, so all added classvars are deleted :)

I've ended up with a simple class decorator:

def expand_classvars(cls):
    for field in attr.fields(cls):
        if not field.inherited and 'classvar' in field.metadata:
            setattr(cls, field.name, field.metadata['classvar'])
    return cls

Drino avatar Apr 21 '21 10:04 Drino

Hey Andrei, thanks for the update and sorry I still haven’t gotten back to you! The reason’s really just that I desperatere need to get 21.1.0 out ASAP (only the cmp helper PR left). :(

hynek avatar Apr 21 '21 11:04 hynek

Hi, @hynek ! No worries, thanks for the fast answer :)

I just wanted to note that this ticket is probably worth closing, as mentioned problem is solved in 5 lines of code + a field in metadata.

Thank you for the flexible attrs API once again!

Drino avatar Apr 21 '21 19:04 Drino

I just wanted to note that this ticket is probably worth closing, as mentioned problem is solved in 5 lines of code + a field in metadata.

The question then is if/how we can somehow "productionize" it? Add an API? Add a recipe?

Thank you for the flexible attrs API once again!

This is the biggest compliment – my design priority has always been to allow people to solve as many problems themselves. :)

hynek avatar Apr 24 '21 06:04 hynek