Quickstart#
Let’s consider a data loader class that reads a CSV file from disk.
import pandas as pd
class DataLoader:
def __init__(self, df_path: str):
self.df_path = df_path
def load(self):
return pd.read_csv(self.df_path)
if __name__ == '__main__':
loader = DataLoader('path/to/data')
data = loader.load()
What if we want to run multiple DataLoader instances, each pointing to a
different df_path?
The issue is that df_path is mixed into the code logic itself.
Changing it means touching the class or its instantiation site — both of which tend
to spread as a project grows.
A cleaner approach is to separate code logic from configuration:
class DataLoaderConfig:
def __init__(self, df_path: str):
self.df_path = df_path
class DataLoader:
def __init__(self, config: DataLoaderConfig):
self.config = config
def load(self):
return pd.read_csv(self.config.df_path)
if __name__ == '__main__':
config = DataLoaderConfig(df_path='path/to/data')
loader = DataLoader(config)
data = loader.load()
We now rely on dependency injection — the DataLoader does not know or care
where its parameters come from.
Note
The DataLoader’s API does not change as we swap configurations.
Cinnamon#
Cinnamon formalises this pattern and adds validation, registration, and dependency resolution on top of it.
Define your configuration by subclassing Configuration and declaring each field
as a typed class annotation, optionally wrapped with Param:
from pathlib import Path
from cinnamon.configuration import Configuration, Param
class DataLoaderConfig(Configuration):
df_path: Path = Param(
'path/to/data',
description='Path to the CSV file to load'
)
Define your component as an ordinary class. There is no base class to inherit and nothing to import — cinnamon imposes no APIs on your code logic:
class DataLoader:
def __init__(self, df_path: Path):
self.df_path = df_path
def load(self):
return pd.read_csv(self.df_path)
To use both together without the registry:
if __name__ == '__main__':
config = DataLoaderConfig.default()
loader = DataLoader(**config.values)
data = loader.load()
config.values returns a plain {field_name: value} dictionary, which unpacks
directly into the component’s constructor.
Note
DataLoaderConfig.default() is equivalent to DataLoaderConfig().
It returns a DataLoaderConfig instance with all fields set to their defaults.
Registration#
In practice, cinnamon encourages a register, bind, and build workflow rather than directly instantiating configurations and components.
flowchart LR
A["register<br/>store the configuration under a key"]
B["bind<br/>name the component by import path"]
C["build<br/>Registry.build() resolves everything"]
D["from_key(key)<br/>a ready component"]
A --> B --> C --> D
Once you have defined a Configuration and its corresponding component,
you register the configuration in the Registry and bind it to the component.
This is done via a RegistrationKey: a compound identifier made up of a name,
an optional tags set, and a namespace.
The most concise way to register is the @register_class decorator on the
Configuration itself:
from cinnamon.configuration import Configuration, Param
from cinnamon.registry import register_class
@register_class(
name='data_loader',
tags={'test'},
namespace='showcasing',
component='components.DataLoader' # module path as a string
)
class DataLoaderConfig(Configuration):
df_path: Path = Param(
'path/to/data',
description='Path to the CSV file to load'
)
When one Configuration class has to register under several keys, decorate the
@classmethod that builds each of them with @register_method instead.
Alternatively, you can register programmatically using Registry.register_configuration()
inside a function decorated with @register:
from cinnamon.registry import Registry, register
@register
def register_data_loader():
Registry.register_configuration(
config=DataLoaderConfig.default(),
name='data_loader',
tags={'test'},
namespace='showcasing',
component='components.DataLoader'
)
Both approaches are equivalent. The @register_method style is more concise when
the registration lives naturally on the configuration class. The @register style
is useful when you want to re-use an existing Configuration without subclassing it.
Building#
The Registry does not execute registrations eagerly.
Instead, call Registry.build() to scan your project’s configurations folder,
run all @register and @register_method decorators it finds, and resolve the
full dependency graph:
from pathlib import Path
from cinnamon.registry import Registry
Registry.build(directory=Path('.'))
After a successful build, construct a DataLoader instance from its registered key:
from cinnamon.registry import RegistrationKey
# From a key
key = RegistrationKey(name='data_loader', tags={'test'}, namespace='showcasing')
loader = Registry.from_key(key)
# Or with the key spelled out, optionally checking the type of the result
loader = Registry.instantiate(
name='data_loader',
tags={'test'},
namespace='showcasing',
expected_type=DataLoader,
)
data = loader.load()
The Registry builds the DataLoaderConfig instance, resolves any dependencies,
validates all conditions, and passes config.values to DataLoader.__init__
automatically.
To swap the underlying implementation, you only need to change the RegistrationKey
— the calling code stays the same.
Beyond quickstart#
The register, bind, and build workflow unlocks a number of powerful features:
Nesting components and configurations to compose more sophisticated pipelines.
Automatically generating
Configurationvariants for hyperparameter search.Integrating external components and configurations written by other users.
Static and dynamic condition validation.
The tutorial is the natural next step: seven runnable files that introduce each of these one at a time, ending with a worked project laid out the way a real one is.
For reference depth, Concepts covers each idea in turn: parameters,
conditions and variants; how to structure registration code and the Registry
APIs; and how keys, containers and variants propagate through the dependency
graph. Cinnamon entry points covers the four cmn-* commands.
Before you run anything#
cmn-check resolves the whole registry and reports what is wrong: unresolved
keys, with a suggestion for what you probably meant, and components whose
__init__ does not match the configuration bound to them.
cmn-check -dir .
It imports none of your components to do it, so it stays fast on a project that takes a minute to import, and it exits non-zero when it finds errors — which makes it usable as a commit hook or a CI step.