Skip to content

How It Works

While the best way to understand how spec-classes works is to look at the code, this section of the documentation provides a high-level overview of how things fit together so that looking at the code is less daunting.

At the end of the day, all that spec-classes does is add a few extra methods to a class to make it behave in a consistent and convenient manner. Exactly which methods get added depends on what is already implemented by the user (it never clobbers user-specified methods), and the type of attribute for which methods are being added (e.g. collection attributes). The functionality of these methods can be broken down into two categories:

  1. Magic methods that perform type-checking and value preparation/type-casting.
  2. Regular methods that help with copy-on-write/builder-pattern mutations.

We'll deal with these separately below, but all methods added by spec-classes are defined in spec_classes.methods. Rather than directly adding functions to the class, we add MethodDescriptor instances that lazily generate the helper methods as they are required, and then replace themselves with the actual implementation (except for magic methods which we pre-materialize from the descriptors), reducing unnecessary overhead and allowing the code to be more modular and extensible. Most methods are generated by spec_classes.utils.method_builder.MethodBuilder, which allows us to build methods with useful generated documentation (including type-annotations) that users can introspect using standard help() calls.

Apart from the methods themselves, you should also know that all spec-class specific metadata is stored in a SpecClassMetadata instance as the __spec_class__ attribute of a spec-class. This is regularly used by methods to tailor their behaviour to the method being mutated.

Magic Methods

The Python data model is very rich and allows for any class to customize all aspects of a class' life-cycle using double-underscore (aka. "dunder") or "magic" methods. Spec-classes leverages this data-model to intercept certain class instance operations. All magic methods are implemented in spec_classes.methods.core

The most important magic method is __setattr__, which is exploited to validate incoming values at run-time. In practice this method is defined such that calls to __setattr__ are redirected to the equivalent of calling the .with_<attr> helper method so that direct attribute mutation follows the same logic as the helper methods (except that mutations are done in-place by default). This results in all values being type-checked (unless disabled explicitly by some parameterization of the code pathway). Attributes that are not managed by spec-classes are passed through to the default implementation of __setattr__.

Another very important magic method is __init__, which is the default instance constructor for Python classes. The implementation provided by spec-classes ensures that every attribute is set correctly and is mutation safe, and respects any constructor implementations defined on super classes.

The other methods implemented by spec-classes are:

  • __eq__: Checks for equality of two instances of a spec-classes.
  • __repr__: Provides a human-friendly representation of the spec-class state.
  • __getattr__: A light-weight implementation that raises a user-friendly error if a managed spec-class attribute does not yet have a value, but otherwise introduces no new functionality.
  • __delattr__: Deleting an attribute on a class instance should remove local attributes and unmask the base class attribute (if it exists). In spec-classes this is achieved by resetting the attribute to its original default value.
  • __deepcopy__: During non-inplace mutations, spec-classes copies the entire class using copy.deepcopy. This magic method customizes the copying behavior to respect the options set on the spec-class.

Helper methods

The helper method implementations are pretty straight-forward. The scalar methods are defined in spec_classes.methods.scalar, and the collection-specific methods in spec_classes.methods.collections. All of these methods use the low level mutate_attr and mutate_value methods defined in spec_classes.utils.mutation.

Stitching it all together

The main logic that stitches together the right metadata and methods is contained within the spec_class decorator. It is implemented as a class so that users can subclass it downsteam, and to keep the logic in smaller understandable chunks. This body of code is also responsible for setting up the deferred bootstrapping (if necessary).

Back to top