Description
Hello, and thanks for the excellent package!
I've been spending a lot of time in attrs
documentation, and haven't seen a way to create a subclass of an attr.s
wrapped class that is identical to the parent but with different arguments to attr.ib
for one or more attributes. Does such a function exist? It's completely possible that I'm missing something :)
To illustrate what I'm getting at, here's an example. Imagine I have a parent class named ParentClass
:
import attr
@attr.s()
class ParentClass:
"""Simple class with no validators."""
x: int = attr.ib(default=42, repr=False, eq=False,
metadata={'add_validation': True})
y: str = attr.ib(default='hello there')
In this example, I intentionally want the ParentClass
to be as light as possible with no validators, converters, etc. Now, I'd like to create a subclass of ParentClass
that adds a validator to the x
attribute. Performing this manually looks like:
@attr.s()
class ManualChild(ParentClass):
"""Manually add a validator for x."""
x: int = attr.ib(default=42, repr=False, eq=False,
metadata={'add_validation': False},
validator=attr.validators.instance_of(int))
While this works, it becomes problematic from a maintenance perspective if I have a lot of classes. If I change the attr.ib
signature in ParentClass
(say I add a non-default argument or flip eq
to True
) I also have to change it in ManualChild
.
Here's my hacky cut at automating this, hard-coded for this specific example of adding an instance_of(int)
validator
:
def get_validated_class(cls, classname):
"""Add integer validators for each attribute with an
"add_validation" metadata flag and return a new class type.
"""
# Initialize return.
attr_dict = {}
# Loop over fields.
for this_field in attr.fields(cls):
# Test for specific metadata flag.
if this_field.metadata.get('add_validation', False):
# Collect all Attribute attributes in a dictionary.
# Looping over __slots__ seems hacky, but Attribute
# instances are not iterable.
field_dict = {slot: getattr(this_field, slot)
for slot in attr.Attribute.__slots__}
# Override validator.
field_dict['validator'] = attr.validators.instance_of(int)
# Flip the add_validation flag.
field_dict['metadata']['add_validation'] = False
# Remove 'name' from field_dict and add to our output
# dictionary.
name = field_dict.pop('name')
# "inherited" is not an attr.ib argument.
field_dict.pop('inherited')
# Add an Attribute to the dictionary.
attr_dict[name] = attr.ib(**field_dict)
# Create and return a new class.
return attr.make_class(name=classname, attrs=attr_dict, bases=(cls,))
Putting it all together in one module and running it:
if __name__ == '__main__':
parent = ParentClass(x=12, y='goodbye')
bad_parent = ParentClass(x='12', y='haha!')
print('ParentClass did not validate x, as expected.')
try:
bad_child1 = ManualChild(x='not an integer')
except TypeError:
print('Validation of x successful for ManualChild.')
# Create a new child class that validates the x attribute.
AutomaticChild = get_validated_class(
cls=ParentClass, classname='AutomaticChild')
try:
bad_child2 = AutomaticChild(x='wrong')
except TypeError:
print('Validation of x successful for AutomaticChild.')
results in the following (as expected):
ParentClass did not validate x, as expected.
Validation of x successful for ManualChild.
Validation of x successful for AutomaticChild.
Hopefully the example helped to illustrate what I'm trying to get at. Is there a supported way of creating a subclass of an attr.s
class where I can override only specific attr.ib
arguments from the parent to create subclass?