Skip to content

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

Closed
@blthayer

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

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions