Registration Dependencies
In registration, we saw that
cinnamon pairs Configuration to Component via RegistrationKey.
Moreover, Configuration instances can nest other Configuration instances to compose
more sophisticated ones (see configuration).
What remains is the question of how to organise code so that cinnamon can find and wire everything together automatically.
Code organisation
Registration functions (either @classmethod decorators or ad-hoc @register functions)
can technically be written anywhere.
However, cinnamon’s Registry only scans files inside a folder named configurations.
This constraint is intentional: it avoids accidentally executing unrelated code during
the registration scan.
The recommended project layout is:
project_folder/
configurations/
data_loader.py
components/
data_loader.py
A components folder is not required, but pairing component and configuration files
by name makes it easy to navigate the project.
For the above example the files would look like:
components/data_loader.py
from cinnamon.component import Component
class DataLoader(Component):
def __init__(self, folder_name: str):
self.folder_name = folder_name
def load(self):
...
configurations/data_loader.py
from pathlib import Path
from cinnamon.configuration import Configuration, Param
from cinnamon.registry import Registry, RegistrationKey, register_method
class DataLoaderConfig(Configuration):
folder_name: str = Param('my_custom_folder', description='folder to load data from')
@classmethod
@register_method(name='loader', tags={'default'}, namespace='testing',
component='components.DataLoader')
def default(cls) -> 'DataLoaderConfig':
return super().default()
Note
Defining a components folder is not mandatory, but it improves readability
by allowing users to quickly pair components and configurations.
Resolving dependencies
Registering and nesting Configuration can quickly lead to dependency ordering problems.
The addition of Configuration variants can further complicate this.
To avoid requiring users to manually order registrations, cinnamon builds a dependency graph automatically — independently of the registration order.
Consider the following two nested configurations:
from cinnamon.configuration import Configuration, Param
from cinnamon.registry import RegistrationKey
class NestedChild(Configuration):
x: int = Param(42)
class ParentConfig(Configuration):
param_1: bool = Param(True)
param_2: bool = Param(False)
child: RegistrationKey = Param(
RegistrationKey(name='test', tags={'nested'}, namespace='testing')
)
The two registration functions below produce identical dependency graphs, regardless of the order in which they register the parent and child:
from cinnamon.registry import Registry, register
@register
def custom_registration():
Registry.register_configuration(
config=ParentConfig.default(),
name='test', tags={'parent'}, namespace='testing'
)
Registry.register_configuration(
config=NestedChild.default(),
name='test', tags={'nested'}, namespace='testing'
)
@register
def custom_registration():
# Order reversed — the result is identical
Registry.register_configuration(
config=NestedChild.default(),
name='test', tags={'nested'}, namespace='testing'
)
Registry.register_configuration(
config=ParentConfig.default(),
name='test', tags={'parent'}, namespace='testing'
)
Note
The same ordering independence applies to @register_method decorators.
This is possible because the Registry builds a directed acyclic graph (DAG) of
dependencies and resolves them bottom-up — children before parents — regardless of
the order they were registered.
To trigger registration and resolution, call Registry.build():
from pathlib import Path
from cinnamon.registry import Registry
Registry.build(directory=Path('.'))
This instructs the Registry to scan all configurations folders under the
current working directory, execute every @register and @register_method
decorator it finds, and then resolve the full dependency graph.
Note
Registry.build() searches recursively — nested configurations folders
within subdirectories are also picked up automatically.
External dependencies
Cinnamon is designed to be a community framework. You may need to import
Configuration and Component definitions written by others and build on top of them.
The Registry supports loading registrations from directories outside your own project.
You can also define Configuration fields that point to externally registered keys.
For example, suppose a DataLoaderConfig variant depends on an external preprocessor:
from cinnamon.configuration import Configuration, Param
from cinnamon.registry import RegistrationKey, register_method
class DataLoaderConfig(Configuration):
folder_name: str = Param('my_custom_folder')
@classmethod
@register_method(name='loader', tags={'default'}, namespace='testing',
component='components.DataLoader')
def default(cls) -> 'DataLoaderConfig':
return super().default()
@classmethod
@register_method(name='loader', tags={'external'}, namespace='testing',
component='components.DataLoader')
def external_variant(cls) -> 'DataLoaderConfig':
config = cls()
# processor is defined in an external project
config = config.model_copy(update={
'processor': RegistrationKey(name='processor', namespace='external')
})
return config
Note
To use model_copy to add a new field, the field must already be declared
on the class. If processor is not declared in DataLoaderConfig, add it
as an optional field:
from typing import Optional
class DataLoaderConfig(Configuration):
folder_name: str = Param('my_custom_folder')
processor: Optional[RegistrationKey] = Param(None)
To avoid a NamespaceNotFoundException when the external key is resolved, inform
the Registry where that namespace was declared by passing external_directories
to Registry.build():
Registry.build(
directory=Path('.'),
external_directories=[Path('path/to/external/project')]
)
The Registry will scan the external project’s configurations folder,
register its keys, and make them available for dependency resolution alongside
your own.