Registration#
While configurations and components can be instantiated manually, cinnamon provides a
registration-based dependency system that links them together via unique identifiers
called RegistrationKey.
This system allows the Registry to build Configuration and Component
instances on demand, resolve nested dependencies automatically, and enumerate valid
parameter variants — all without the caller knowing which concrete classes are involved.
RegistrationKey#
flowchart LR
N["name<br/>'model'"] --> K
T["tags<br/>{'bert', 'large'}"] --> K
NS["namespace<br/>'nlp'"] --> K
K["RegistrationKey"] --> S["name=model--tags=['bert', 'large']--namespace=nlp"]
A RegistrationKey is a compound identifier made up of three fields:
name: a general identifier for the registeredConfiguration.namespace: a high-level grouping, useful for distinguishing between user groups, frameworks, or macro categories. Follows the conventionuser/namespace(e.g."huggingface/transformers"), though any string is valid. Defaults to"default"if not provided.tags: an optionalsetof strings that disambiguates keys sharing the samenameandnamespace(e.g. multiple model variants by the same author).
from cinnamon.registry import RegistrationKey
key = RegistrationKey(name='model', tags={'bert', 'large'}, namespace='nlp')
Two keys are equal if and only if their name, tags, and namespace all match:
key_a = RegistrationKey(name='model', tags={'bert'}, namespace='nlp')
key_b = RegistrationKey(name='model', tags={'bert'}, namespace='nlp')
key_c = RegistrationKey(name='model', tags={'gpt'}, namespace='nlp')
key_a == key_b # True
key_a == key_c # False
RegistrationKey has a canonical string representation and can be round-tripped
through it:
key = RegistrationKey(name='model', tags={'bert'}, namespace='nlp')
str(key)
# 'name=model--tags=['bert']--namespace=nlp'
restored = RegistrationKey.from_string(str(key))
restored == key # True
# parse() accepts a key object, its string form, or name/tags/namespace directly
RegistrationKey.parse(name='model', tags={'bert'}, namespace='nlp') == key # True
Serializing a key#
A key has two written forms.
The string form is what str(key) gives and what cmn-build writes into
registrations/valid_keys.json. It is compact and reads well in a log:
str(key)
# "name=loader--tags=['imdb', 'v2']--namespace=nlp"
RegistrationKey.from_string("name=loader--tags=['imdb']--namespace=nlp")
The mapping form is plain JSON data, and is the better choice for anything that has to survive a round trip:
key.to_dict()
# {'name': 'loader', 'namespace': 'nlp', 'tags': ['imdb', 'v2']}
RegistrationKey.from_dict({'name': 'loader', 'namespace': 'nlp'})
Tags are sorted, so the output is stable across runs and comparable byte for
byte. Only the three components that make up the key’s identity appear:
description and metadata annotate a key rather than identify it, and two
keys differing only in those are equal.
A class cannot make itself serializable to json.dumps() – the encoder
dispatches on a fixed set of types and consults default only for what it does
not recognise – so cinnamon provides the hook:
import json
from cinnamon.registry import json_default
json.dumps({'losses': [key_a, key_b]}, default=json_default)
Inside a Configuration none of this is needed. Pydantic already knows how to
serialize a key, so config.model_dump_json() writes its string form.
Registration#
Registration is the action of storing a Configuration instance in the Registry,
optionally binding it to a Component class so that component instances can be built
from it later.
There are three ways to register: via a decorator on the Configuration class,
via a decorated @classmethod, or via an ad-hoc function. All three end in the
same place, a ConfigurationInfo entry stored under a key:
flowchart LR
A["@register_class<br/>one key per class"] --> I
B["@register_method<br/>several keys per class"] --> I
C["@register<br/>an ad-hoc function"] --> I
I["ConfigurationInfo<br/>config, component path, run method"]
I --> R["Registry, under the key"]
Pick the first form that fits. @register_class covers the common case, a
configuration that describes one component under one key.
Class registration#
Most configurations describe one component under one key, and differ from their parent only in the parameters they set. Decorate the class:
from cinnamon.configuration import Configuration, Param
from cinnamon.registry import register_class
@register_class(
name='test',
tags={'default'},
namespace='testing',
component='components.CustomComponent'
)
class CustomConfig(Configuration):
x: int = Param(5, description='An example parameter')
The Registry builds the configuration by calling CustomConfig.default(),
which every Configuration already has. So a subclass that only overrides a
parameter writes nothing else:
@register_class(name='test', tags={'large'}, namespace='testing',
component='components.CustomComponent')
class LargeConfig(CustomConfig):
x: int = Param(100)
A configuration whose default() does real work – adding a condition, say –
writes it as an ordinary @classmethod, and that is the one the registration
builds from.
Note
Pass namespace as a keyword argument, named by a string literal or a
module-level constant. Registry.build reads namespaces out of your source
before importing anything, so a namespace it cannot read statically leaves
the directory looking as though it registers nothing – which only shows up
once a key from another directory has to resolve into it.
Class method registration#
Reach for this when one Configuration class registers under several keys,
which a class decorator cannot express.
Decorate a @classmethod of your Configuration with @register_method to
register it automatically when the Registry scans your configurations folder:
from cinnamon.configuration import Configuration, Param
from cinnamon.registry import register_method
class CustomConfig(Configuration):
x: int = Param(5, description='An example parameter')
@classmethod
@register_method(
name='test',
tags={'default'},
namespace='testing',
component='components.CustomComponent'
)
def default(cls) -> 'CustomConfig':
return super().default()
When the Registry processes this file, it:
Stores a
ConfigurationInfoentry keyed byRegistrationKey(name='test', tags={'default'}, namespace='testing').Records that
CustomConfig.default()is the constructor to call.Binds the result to
components.CustomComponent(the component’s module path as a string).
Any @classmethod can be decorated, not just default() – which is the
reason to prefer this form over @register_class: it registers multiple
templates from the same Configuration class.
class CustomConfig(Configuration):
x: int = Param(5)
y: float = Param(0.01)
@classmethod
@register_method(name='test', tags={'small'}, namespace='testing',
component='components.CustomComponent')
def default(cls) -> 'CustomConfig':
return super().default()
@classmethod
@register_method(name='test', tags={'large'}, namespace='testing',
component='components.CustomComponent')
def large(cls) -> 'CustomConfig':
return cls(x=100, y=0.001)
Ad-hoc registration#
When you want to register an existing Configuration without subclassing it,
use the @register decorator and call Registry.register_configuration() directly:
from cinnamon.registry import Registry, register
@register
def custom_registration():
Registry.register_configuration(
config=CustomConfig.default(),
name='test',
tags={'default'},
namespace='testing',
component='components.CustomComponent'
)
This is equivalent to @register_method but keeps the registration logic separate
from the Configuration class — useful when re-using an existing configuration
without modification.
If you already hold a RegistrationKey, register_configuration_from_key()
takes it directly rather than making you split it into three arguments:
key = RegistrationKey(name='test', tags={'default'}, namespace='testing')
Registry.register_configuration_from_key(
config=CustomConfig.default(),
registration_key=key,
component='components.CustomComponent'
)
The key is used as given, so it keeps its description and any special tags.
register_configuration() is the same method with the key assembled for you,
and resolution uses this form for the variant keys it derives.
Runnable components#
If a Component exposes a method that should be invoked automatically when run via
cmn-run, specify it with the run_method argument:
Registry.register_configuration(
config=CustomConfig.default(),
name='test',
tags={'runnable'},
namespace='testing',
component='components.CustomComponent',
run_method='train' # calls component.train() when run via cmn-run
)
# or via @register_method:
@register_method(name='test', tags={'runnable'}, namespace='testing',
component='components.CustomComponent', run_method='train')
def default(cls) -> 'CustomConfig':
return super().default()
Registered runnable keys carry the internal __runnable special tag and can be
retrieved with Registry.retrieve_runnable_keys().
Entry points#
Every script that uses cinnamon opens the same four lines:
if __name__ == '__main__':
directory = Path(__file__).parent.parent.resolve()
Registry.build(directory=directory)
logging.basicConfig(level=logging.INFO)
logger = getLogger(__name__)
@setup replaces them:
from pathlib import Path
from cinnamon.registry import Registry, setup
@setup(directory=Path(__file__).parent.parent)
def main():
benchmark = Registry.instantiate(name='benchmark', namespace='examples')
benchmark.run()
The decorated function runs immediately when its module is __main__ –
whether started as python main.py or as python -m main. That is what
removes the if __name__ guard, rather than merely moving the build call into
a decorator.
Imported from anywhere else it does not run, so a module keeps its ordinary
behaviour under import and under test. The function stays callable, and
calling it rebuilds the registry first.
Note
Because it runs at decoration time, put the decorated function where you
would have put the if __name__ block: after everything it refers to.
That is the same discipline, not a new one.
directory defaults to the working directory, matching the -dir flag of
the commands. logging_level defaults to logging.INFO; pass
None for a script that configures its own logging:
@setup(logging_level=None)
def main():
...
Retrieving registrations#
After Registry.build() (or Registry.dag_resolution() in manual workflows),
you can query the registry by key or by field filters.
Retrieve a configuration instance by its key:
config = Registry.retrieve_configuration(
name='test', tags={'default'}, namespace='testing'
)
Retrieve full registration info (configuration + bound component + run method):
info = Registry.retrieve_configuration_info(
name='test', tags={'default'}, namespace='testing'
)
info.config # the Configuration instance
info.component # 'components.CustomComponent' (string) or None
info.run_method # 'train' or None
Both methods also accept a RegistrationKey instance directly:
key = RegistrationKey(name='test', tags={'default'}, namespace='testing')
config = Registry.retrieve_configuration(registration_key=key)
Search for keys using retrieve_keys(), which supports filtering by name,
namespace, tags, or any combination:
# All keys in a namespace
keys = Registry.retrieve_keys(namespaces='testing')
# All keys matching a name and namespace
keys = Registry.retrieve_keys(names='test', namespaces='testing')
# All keys with specific tags
keys = Registry.retrieve_keys(tags={'default'})
# All runnable keys
keys = Registry.retrieve_runnable_keys()
Configuration also provides a retrieve() classmethod as syntactic sugar that
additionally type-checks the result:
config = CustomConfig.retrieve(name='test', tags={'default'}, namespace='testing')
# raises RuntimeError if the retrieved config is not a CustomConfig instance
Building instances from registrations#
Build a Configuration instance via its key:
config = Registry.retrieve_configuration(
name='test', tags={'default'}, namespace='testing'
)
config.x # >>> 5
Build the bound Component instance:
component = Registry.instantiate(
name='test', tags={'default'}, namespace='testing'
)
component.x # >>> 5
See Component for the full
details of how component construction works, including nested configurations and
build_args overrides.
Dependencies reach the component as keys#
Resolution replaces a dependency key with the child’s Configuration inside
the parent configuration, so conditions can be validated across the whole graph.
What the component receives is still the key.
class Pipeline:
def __init__(self, processor: RegistrationKey):
self.processor = Registry.from_key(processor) # built when it wants to
That is deliberate, and it is the only behaviour: a component decides when, and whether, each child is built. A child needed on one code path costs nothing on the others.
Note
Earlier releases had a resolve_automatically=False registration flag for
exactly this. It is gone, because handing components keys is now what always
happens, and a flag with one possible meaning is not a choice.
Tl;dr#
Define your
Component(code logic).Define its corresponding
Configuration(one or more).Register the
Configurationin theRegistryvia aRegistrationKey.A
RegistrationKeyis a(name, tags, namespace)compound identifier.Build
Configurationinstances via theRegistrationKey.Build
Componentinstances via theRegistrationKey.
Congrats! This is 99% of cinnamon.
How to use registration APIs#
You do not need to scatter registration calls throughout your codebase. Cinnamon supports a specific code organisation that handles all registration automatically while keeping things readable.
See Registration Dependencies for the
recommended project layout and a full walkthrough of how Registry.build() discovers
and executes registrations.