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:
- Magic methods that perform type-checking and value preparation/type-casting.
- 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 usingcopy.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).