7. A real project#

Steps 1–6 registered everything by hand in a single file so each concept stayed readable in one screen. Real projects do not do that. They put registrations in a configurations/ package, let Registry.build discover them, and drive the whole thing from the command line.

examples/tutorial/07_project_layout/
├── components/
│   ├── __init__.py
│   └── summariser.py       # the logic: plain classes
└── configurations/
    ├── __init__.py
    └── summariser.py       # the registrations

Registry.build finds every configurations/ package beneath the directory it is given, imports the modules inside, and runs whatever the @register and @register_method decorators buffered. components/ is a convention, not a requirement — a component is found by the import path its registration names.

Those modules are ordinary Python. They may sit in subfolders of configurations/, they may import each other — a configuration subclassing one from a sibling module is the usual reason — and the namespace they register into may be a constant imported from a shared module rather than repeated as a literal in each file. Registry.build reads namespaces off the source before importing anything, and follows such an import on disk to find the literal behind it.

Building on another project’s registrations#

A second project may register configurations that depend on the first one’s – a package of published-experiment settings over a library of components, say. Point Registry.build at the project whose registrations you want run, and name the other under external_directories:

Registry.build(
    directory=Path(experiments.__file__).parent,
    external_directories=[Path(library.__file__).parent],
)

The distinction matters: directory is where registration scripts are executed, while external_directories are indexed so their namespaces can be referenced. A key from an external directory is loaded when something depends on it, not because the directory was listed.

The registrations#

 1"""
 2Registrations for the worked project.
 3
 4`Registry.build` finds every `configurations/` package under the directory it is
 5given, imports the modules inside, and runs whatever the `@register` and
 6`@register_method` decorators buffered. Nothing else about the layout matters --
 7`components/` is a convention, not a requirement.
 8"""
 9
10from cinnamon.configuration import Configuration, Param
11from cinnamon.registry import RegistrationKey, Registry, register, register_method
12
13NAMESPACE = "tutorial/summarisation"
14
15DOCUMENT = (
16    "cinnamon separates logic from configuration. Components carry the weight. "
17    "Configurations describe it. That split is what makes sweeps cheap."
18)
19
20
21class TruncatorConfig(Configuration):
22    sentences: int = Param(1, ge=1, variants=[2], description="How many to keep")
23
24    @classmethod
25    @register_method(
26        name="strategy",
27        tags={"truncate"},
28        namespace=NAMESPACE,
29        component="components.summariser.Truncator",
30    )
31    def default(cls):
32        return super().default()
33
34
35class SummariserConfig(Configuration):
36    strategy: RegistrationKey = Param(
37        RegistrationKey(name="strategy", tags={"truncate"}, namespace=NAMESPACE),
38        description="How to shorten the document",
39    )
40    document: str = Param(DOCUMENT, description="Text to summarise")
41
42
43# `@register` is the other entry point: a plain function that registers whatever
44# it likes. Use it when a `default()` classmethod would be contrived.
45@register
46def register_summarisers():
47    Registry.register_configuration(
48        config=SummariserConfig(),
49        name="summariser",
50        namespace=NAMESPACE,
51        component="components.summariser.Summariser",
52        run_method="run",  # makes it discoverable by `cmn-run`
53    )

Two entry points appear here, and they are equivalent:

  • @register_method decorates a default() classmethod on the configuration. Concise when the registration belongs naturally to the class.

  • @register decorates a plain function that registers whatever it likes. Use it when a default() classmethod would be contrived, or to register a configuration you did not write.

run_method="run" is what makes a registration discoverable by cmn-run.

The components#

 1"""Components for the worked project: plain classes, no cinnamon imports needed."""
 2
 3from cinnamon.registry import RegistrationKey, Registry
 4
 5
 6class Truncator:
 7    """Keeps the first `sentences` sentences."""
 8
 9    def __init__(self, sentences: int):
10        self.sentences = sentences
11
12    def summarise(self, text: str) -> str:
13        parts = [part.strip() for part in text.split(".") if part.strip()]
14        return ". ".join(parts[: self.sentences]) + "."
15
16
17class Summariser:
18    """A runnable component: `cmn-run` calls its `run` method."""
19
20    def __init__(self, strategy: RegistrationKey, document: str):
21        self.strategy = Registry.from_key(strategy)
22        self.document = document
23
24    def run(self) -> str:
25        summary = self.strategy.summarise(self.document)
26        print(f"summary: {summary}")
27        return summary

Driving it from the command line#

cd examples/tutorial/07_project_layout

cmn-check -dir .          # look for mistakes before running anything
cmn-build -dir .          # resolve, and write the key list to registrations/
cmn-run   -dir .          # pick a configuration interactively and run it

cmn-check and cmn-build need only pip install cinnamon-core. cmn-run prompts, so it also wants pip install "cinnamon-core[cli]". Cinnamon entry points covers all four commands in detail.

Four registrations come out of two declarations, because the strategy declares a variant and the summariser inherits it:

strategy--tags=['truncate']
strategy--tags=['sentences=2', 'truncate']
summariser
summariser--tags=['strategy.sentences=2', 'strategy.truncate']

That last key is worth a second look: it records which strategy the summariser was built against. Nobody wrote it down — resolution derived it, and it will be the same key on the next machine and in six months.

After the tutorial#

  • Worked pipeline — a full scikit-learn pipeline: loader, processors, model, benchmark. Needs pip install "cinnamon-core[examples]" and downloads the IMDB dataset on first run.

  • Concepts covers configuration, component, registration and dependencies in depth.

  • CONTRIBUTING.md — the design principle in more depth, and how to work on cinnamon itself.