Quickstart

Let’s consider a data loader class that loads and returns some data.

class DataLoader:

    def __init__(df_path):
        self.df_path = df_path

    def load():
        df = pd.read_csv(self.df_path)
        return df

if __name__ == '__main__':
    loader = DataLoader('path/to/data')
    data = loader.load()

What if we want to define multiple DataLoader, each pointing to a different df_path?

We notice that we are mixing code logic (i.e., the DataLoader class) with its configuration (i.e., df_path).

We can separate code logic from configuration.

Instead of relying on additional data formats (e.g., JSON), we define a DataLoaderConfig in python.

class DataLoaderConfig:

    def __init__(df_path):
        self.df_path = df_path

class DataLoader:

    def __init__(config: DataLoaderConfig):
        self.config = config

    def load():
        df = pd.read_csv(self.config.df_path)
        return df

if __name__ = '__main__':
    config = DataLoaderConfig(df_path='path/to/data')
    loader = DataLoader(config)
    data = loader.load()

Now, we are relying on dependency injection to separate code logic and configuration.

Note

The DataLoader’s APIs do not change as we change the configuration.

Cinnamon

In cinnamon we follow the above paradigm.

from cinnamon.configuration import Configuration
from cinnamon.component import Component

class DataLoaderConfig(Configuration):

    @classmethod
    def default(cls):
        config = super().default()

        config.add(name='df_path',
                   value='path/to/data',
                   type_hint=Path,
                   description='path where to load data')

        return config


class DataLoader(Component):

    def __init__(self, df_path):
        self.df_path = df_path

    def load():
        df = pd.read_csv(self.df_path)
        return df

if __name__ = '__main__':
    config = DataLoaderConfig.default()
    loader = DataLoader(**config.values)
    data = loader.load()

Configurations are cinnamon.configuration.Configuration subclasses, where the default() method defines the standard template of the configuration.

You can add parameters to the configuration via add() method.

Each parameter is defined by a name, a value, and, optionally, info about its type, textual description, variants, allowed value range and more…

All this information allows cinnamon checking whether the defined Configuration is valid or not.

The code logic is a cinnamon.component.Component subclass and maintains the same code structure with no modifications.

In particular, components can be defined as you would normally define a standard python class.

Registration

In cinnamon, we usually don’t explicitly instantiate a Configuration and its corresponding Component as done in the previous section.

Instead, cinnamon supports a registration, bind, and build paradigm.

Once, we have defined the Configuration and its corresponding Component, we register the Configuration.

Registry.register_configuration(config_class=DataLoaderConfig,
                           name='data_loader',
                           tags={'test'},
                           namespace='showcasing',
                           component_class=DataLoader)

or

class DataLoaderConfig(Configuration):

    @classmethod
    @register_method(name='data_loader',
                     tags={'test'},
                     namespace='showcasing',
                     component_class=DataLoader)
    def default(cls):
        config = super().default()

        config.add(name='df_path',
                   value='path/to/data',
                   type_hint=Path,
                   description='path where to load data')

        return config

We do so by using a RegistrationKey defined as a (name, tags, namespace) tuple.

Additionally, we bind the Configuration to a Component so that cinnamon knows that we want to create DataLoader instances via DataLoaderConfig.

At this point, we only need to build our first instance via the RegistrationKey.

loader = DataLoader.build_component(name='data_loader',
                                    tags={'test'},
                                    namespace='showcasing')

to return a DataLoader.

Now, we can build DataLoader instances anywhere in our code by simply using the associated RegistrationKey.

Note

If you want to quickly change the Configuration of your DataLoader, you only need to change the key!

Beyond quickstart

cinnamon uses the registration, bind, and build to provide flexible, clean and easy to extend code.

The main code dependency are RegistrationKey instances. See Registration if you want to know more about how to set up your code with cinnamon.

Via this paradigm, cinnamon supports:

  • Nesting Component and Configuration to build more sophisticated ones.

  • Automatically generating Configuration variants.

  • Quick integration of external Component and Configuration (e.g., written by other users).

  • Static and dynamic code sanity check.

See Configuration for more details.