SerializableMetatype#
- class jangada.serialization.SerializableMetatype(name: str, bases: tuple[type, ...], namespace: dict[str, Any], **kwargs: Any)#
Bases:
ABCMetaMetaclass for automatic registration and introspection of Serializable classes.
This metaclass provides automatic registration of all Serializable subclasses, discovery of SerializableProperty descriptors, and management of type registries for serialization support.
See also
SerializableThe base class using this metaclass
SerializablePropertyProperty descriptor for serializable attributes
Notes
This metaclass extends ABCMeta to allow Serializable classes to also be abstract base classes if needed.
The metaclass automatically: 1. Registers each subclass in the global registry 2. Walks the MRO to collect all SerializableProperty descriptors 3. Creates the _serializable_properties dict on each subclass
Examples
Creating a Serializable class automatically registers it:
>>> class MyClass(Serializable): ... value = SerializableProperty(default=0) ... >>> get_full_qualified_name(MyClass) in Serializable True
Access classes by qualified name:
>>> qualname = get_full_qualified_name(MyClass) >>> retrieved = Serializable[qualname] >>> retrieved is MyClass True
Property discovery happens automatically:
>>> 'value' in MyClass._serializable_properties True
Register custom primitive types:
>>> class CustomType: ... pass >>> Serializable.register_primitive_type(CustomType) >>> Serializable.is_primitive_type(CustomType) True
Metaclass Protocol#
Special methods that implement the metaclass protocol.
|
Create a new Serializable class with automatic registration. |
|
Get a registered Serializable subclass by qualified name. |
|
Check if a class or qualified name is registered. |
Primitive Type Management#
Methods for registering and managing primitive types that serialize as-is.
Register a type as a primitive that can be serialized as-is. |
|
Remove a type from the primitive types registry. |
|
Check if a type is registered as a primitive. |
Dataset Type Management#
Methods for registering and managing types that require array conversion.
Register a type that requires special handling for serialization. |
|
Remove a dataset type registration. |
|
Check if a type is registered as a dataset type. |
Read-Only Properties#
Properties providing access to type and class registries.
Get a list of all registered Serializable subclasses. |
|
Get a list of all registered primitive types. |
|
Get a list of all registered dataset types. |
|
Get all SerializableProperty descriptors on this class. |
|
Get SerializableProperty descriptors marked as copiable. |
Overview#
SerializableMetatype is the metaclass that powers the Serializable system.
It provides automatic registration, property discovery, and type management without
requiring manual configuration.
Key Features#
- Automatic Registration
Every class that inherits from
Serializableis automatically registered in a global registry, indexed by its fully qualified name.- Property Discovery
The metaclass walks the Method Resolution Order (MRO) to discover all
SerializablePropertydescriptors, including those inherited from base classes.- Type Registries
Maintains separate registries for primitive types (serialized as-is) and dataset types (require conversion to/from arrays).
- Subscript Access
Classes can be retrieved by their qualified name using subscript notation:
Serializable['module.ClassName']- Membership Testing
Check if a class is registered using the
inoperator:'module.ClassName' in Serializable
How It Works#
Class Creation#
When a new Serializable subclass is defined, the metaclass:
Calls
__new__to create the class objectGenerates the fully qualified name
Registers the class in
Serializable._subclassesWalks the MRO to collect
SerializablePropertydescriptorsStores them in
_serializable_properties
Example:
# When this class is defined...
class MyClass(Serializable):
prop1 = SerializableProperty(default=0)
prop2 = SerializableProperty(default="")
# ...the metaclass automatically:
# 1. Registers it: Serializable._subclasses['__main__.MyClass'] = MyClass
# 2. Collects properties: MyClass._serializable_properties = {'prop1': ..., 'prop2': ...}
Property Collection#
The metaclass collects properties from all base classes in the MRO:
class Base(Serializable):
base_prop = SerializableProperty(default=0)
class Derived(Base):
derived_prop = SerializableProperty(default=1)
# Derived._serializable_properties includes both:
# {'base_prop': ..., 'derived_prop': ...}
Class Registry#
Access registered classes by name:
# Define a class
class MyClass(Serializable):
pass
# Access by qualified name
qualname = 'mymodule.MyClass'
retrieved_class = Serializable[qualname]
# Check registration
if qualname in Serializable:
print("Class is registered")
Type System#
Primitive Types#
Primitive types are serialized without transformation. By default, these types are registered:
- str
- numbers.Number (int, float, complex, Decimal, etc.)
- pathlib.Path
Register additional primitive types:
from decimal import Decimal
Serializable.register_primitive_type(Decimal)
# Now Decimal values serialize as-is
class FinancialData(Serializable):
amount = SerializableProperty(default=Decimal('0.00'))
Dataset Types#
Dataset types require conversion to/from NumPy arrays. These are typically used for types that will be stored as HDF5 datasets rather than attributes.
Built-in dataset types:
- numpy.ndarray
- pandas.Timestamp
- pandas.DatetimeIndex
Register custom dataset types:
import numpy as np
class CustomArray:
def __init__(self, data):
self.data = np.array(data)
def disassemble(obj):
"""Convert object to (array, attributes) tuple."""
return obj.data, {'dtype': str(obj.data.dtype)}
def assemble(array, attrs):
"""Reconstruct object from array and attributes."""
result = CustomArray(array)
# Could use attrs to restore additional state
return result
Serializable.register_dataset_type(
CustomArray,
disassemble=disassemble,
assemble=assemble
)
The disassemble/assemble pattern allows complex types to be stored efficiently in array-based storage systems like HDF5.
Properties Access#
All Properties#
Get all SerializableProperty descriptors on a class:
class MyClass(Serializable):
prop1 = SerializableProperty(default=0)
prop2 = SerializableProperty(default="")
regular_attr = 42 # Not a SerializableProperty
props = MyClass.serializable_properties
# Returns: {'prop1': <SerializableProperty>, 'prop2': <SerializableProperty>}
This includes properties from base classes.
Copiable Properties#
Get only properties marked as copiable:
class MyClass(Serializable):
data = SerializableProperty(default=None, copiable=True)
cache = SerializableProperty(default=None, copiable=False)
copiable = MyClass.copiable_properties
# Returns: {'data': <SerializableProperty>}
This is used internally by serialize(is_copy=True) and copy().
Advanced Usage#
Multiple Inheritance#
Properties from all base classes are collected:
class Mixin1(Serializable):
prop1 = SerializableProperty(default=0)
class Mixin2(Serializable):
prop2 = SerializableProperty(default=0)
class Combined(Mixin1, Mixin2):
prop3 = SerializableProperty(default=0)
# Combined has all three properties
assert 'prop1' in Combined.serializable_properties
assert 'prop2' in Combined.serializable_properties
assert 'prop3' in Combined.serializable_properties
Dynamic Class Creation#
The metaclass works with dynamically created classes:
# Create a class at runtime
DynamicClass = SerializableMetatype(
'DynamicClass',
(Serializable,),
{
'value': SerializableProperty(default=0)
}
)
# It's automatically registered
qualname = get_full_qualified_name(DynamicClass)
assert qualname in Serializable
Introspection#
Query the type registries:
# Get all registered Serializable classes
all_classes = Serializable.serializable_types
# Get all primitive types
primitives = Serializable.primitive_types
assert str in primitives
# Get all dataset types
datasets = Serializable.dataset_types
assert np.ndarray in datasets
# Check specific types
assert Serializable.is_primitive_type(str)
assert Serializable.is_dataset_type(np.ndarray)
Design Rationale#
Why a Metaclass?#
The metaclass approach provides several benefits:
Automatic registration: No manual registration code needed
Zero boilerplate: Properties are discovered automatically
Inheritance support: Base class properties are inherited correctly
Type safety: All subclasses share the same serialization machinery
Introspection: Easy to query what classes and types are available
Alternative approaches (decorators, manual registration) would require more code and be more error-prone.
Why Global Registries?#
Global registries enable:
Deserialization: Reconstruct objects from just a class name string
Cross-module serialization: Serialize objects from any module
Plugin architectures: Dynamically loaded modules auto-register
Introspection: Query all available Serializable types
The registries are class-level (not instance-level) to minimize memory overhead.
Thread Safety Considerations#
The current implementation does not guarantee thread safety for:
Concurrent class definition (rare in practice)
Concurrent type registration/removal
For most applications, this is not an issue since:
Classes are typically defined at module load time (single-threaded)
Type registration happens during initialization (before threads spawn)
If you need thread safety, add locking around type registration operations.
See Also#
serializable - The base class using this metaclass
serializable_property - Property descriptor documentation
helper_functions - Helper functions like get_full_qualified_name