Skip to content

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

@Drino

Description

@Drino

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.

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