Skip to content

Question: How to create subclass with slightly different attr.ib parameters? #698

Description

@blthayer

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?

Metadata

Metadata

Assignees

No one assigned

    Type

    No type

    Fields

    No fields configured for issues without a type.

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions