attrs
attrs copied to clipboard
[RFC] A shortcut to define a classvar with the same name as an attribute
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.
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
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). :(
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!
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. :)