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 registered Configuration.

  • namespace: a high-level grouping, useful for distinguishing between user groups, frameworks, or macro categories. Follows the convention user/namespace (e.g. "huggingface/transformers"), though any string is valid. Defaults to "default" if not provided.

  • tags: an optional set of strings that disambiguates keys sharing the same name and namespace (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:

  1. Stores a ConfigurationInfo entry keyed by RegistrationKey(name='test', tags={'default'}, namespace='testing').

  2. Records that CustomConfig.default() is the constructor to call.

  3. 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 Configuration in the Registry via a RegistrationKey.

  • A RegistrationKey is a (name, tags, namespace) compound identifier.

  • Build Configuration instances via the RegistrationKey.

  • Build Component instances via the RegistrationKey.

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.