API: internals#

cinnamon.utility is where the registry’s own machinery lives: the dependency graph, the key parser, the static analyzer behind cmn-check, the exception types, and the suggestion engine that turns a typo into a “did you mean”. A project does not import these, but reading them is the fastest way to see how a build actually proceeds.

cinnamon.utility.configuration#

class cinnamon.utility.configuration.batched(iterable, n)#

Bases: object

Batch data into tuples of length n. The last batch may be shorter than n.

Loops over the input iterable and accumulates data into tuples up to size n. The input is consumed lazily, just enough to fill a batch. The result is yielded as soon as a batch is full or when the input iterable is exhausted.

>>> for batch in batched('ABCDEFG', 3):
...     print(batch)
...
('A', 'B', 'C')
('D', 'E', 'F')
('G',)

cinnamon.utility.dependencies#

Shape handling for Configuration dependency fields.

A Configuration declares a dependency on another registration by typing a field as a RegistrationKey (or a Configuration subclass). Three shapes are supported:

child: RegistrationKey -> SCALAR losses: list[RegistrationKey] -> LIST metrics: dict[str, RegistrationKey] -> DICT

each optionally wrapped in Optional[...], and each optionally parameterised (RegistrationKey[Loss]) for the benefit of readers and type checkers.

Nesting is deliberately unsupported. list[list[RegistrationKey]] and dict[str, list[RegistrationKey]] raise TypeError at detection time rather than failing somewhere deeper in registration. One level keeps the dependency DAG a graph over keys, with no need for a path language to address a key inside a container.

This module is the single place that knows about these shapes. registry.py walks dependencies through iter_dependency_keys() and rebuilds them through map_dependency_keys(), so registration, expansion and resolution stay shape-agnostic.

class cinnamon.utility.dependencies.DependencyShape(*values)[source]#

Bases: Enum

How the registration keys of a dependency field are laid out.

DICT = 'dict'#
LIST = 'list'#
SCALAR = 'scalar'#
cinnamon.utility.dependencies.dependency_members(value)[source]#

Yield the raw members of value, whatever their type.

Unlike iter_dependency_keys() nothing is filtered out, so callers can check that a dependency really holds registration keys and report the ones that do not. None yields nothing, which is how an unset optional dependency stays legal.

Return type:

Iterator[Any]

cinnamon.utility.dependencies.dependency_shape(field_name, annotation, value)[source]#

Classify field_name as a dependency field, or None if it is not one.

The runtime value is consulted first, so a union annotation such as RegistrationKey | list[RegistrationKey] resolves to whatever the field actually holds. The annotation is the fallback, which is what makes an unset optional dependency (dict[str, RegistrationKey] | None = None) still register as one.

Raises:

TypeError – if the field nests containers of registration keys.

Return type:

DependencyShape | None

cinnamon.utility.dependencies.iter_dependency_keys(value)[source]#

Yield every RegistrationKey held by value, whatever its shape.

Accepts a bare key, a list/tuple/set of keys, a mapping of them, or None. Non-key members are skipped, so a partially resolved container (keys already swapped for Configuration instances) iterates cleanly.

Return type:

Iterator[RegistrationKey]

cinnamon.utility.dependencies.map_dependency_keys(value, function)[source]#

Rebuild value with function applied to every RegistrationKey in it.

The container shape is preserved, so a dict[str, RegistrationKey] maps to a dict[str, Configuration] under the same string labels. Members that are not keys are passed through untouched.

Return type:

Any

cinnamon.utility.exceptions#

exception cinnamon.utility.exceptions.AlreadyExpandedException[source]#

Bases: Exception

exception cinnamon.utility.exceptions.AlreadyRegisteredException(registration_key)[source]#

Bases: Exception

exception cinnamon.utility.exceptions.DisconnectedGraphException(nodes)[source]#

Bases: Exception

exception cinnamon.utility.exceptions.InvalidDirectoryException(directory)[source]#

Bases: Exception

exception cinnamon.utility.exceptions.NamespaceNotFoundException(registration_key, namespaces, missing_namespace=None)[source]#

Bases: Exception

exception cinnamon.utility.exceptions.NotADAGException(edges)[source]#

Bases: Exception

build_edge_view(edges)[source]#
exception cinnamon.utility.exceptions.NotBoundException(registration_key)[source]#

Bases: Exception

exception cinnamon.utility.exceptions.NotExpandedException[source]#

Bases: Exception

exception cinnamon.utility.exceptions.NotRegisteredException(registration_key, suggestions=None)[source]#

Bases: Exception

exception cinnamon.utility.exceptions.UnserializableRuntimeException(registration_key, field, reason)[source]#

Bases: Exception

A registered value that cannot travel to another process.

Raised by Registry.freeze_runtime rather than by the worker that would have failed to unpickle it, so the offending key and field are named while there is still a stack that says which registration they came from.

exception cinnamon.utility.exceptions.UnsupportedFieldTypeException(configuration_name, field_type=None, field_name=None)[source]#

Bases: Exception

A Configuration field is typed as something too heavy to configure.

Raised in place of pydantic’s schema-generation error, whose advice – “set arbitrary_types_allowed=True” – is correct for pydantic and exactly wrong here: taking it merges the component and configuration concepts that cinnamon exists to keep apart.

exception cinnamon.utility.exceptions.ValidationFailureException(validation_result)[source]#

Bases: Exception

class cinnamon.utility.exceptions.ValidationResult(passed, source, error_message=None)[source]#

Bases: object

Stores conditions evaluation result (see Configuration.validate()).

Parameters:
  • passed (bool) – True if all conditions are True

  • error_message (Optional[str]) – describes which condition failed during the evaluation process.

error_message: str | None = None#
passed: bool#
source: str#
property stack_trace#
exception cinnamon.utility.exceptions.VariantKeyCollisionException(key, variant_key, values, previous=None)[source]#

Bases: Exception

Two variants of one configuration derive the same registration key.

A variant’s key is its parent’s key plus the tags the varied fields contribute. Fields whose alternatives are RegistrationKey instances contribute the child’s tags and nothing else, so two children that differ only by name – loader-csv and loader-json, both untagged – derive one key between them. The registry used to keep whichever arrived last, silently, and when the derived key equalled the parent’s own it also added a self-loop to the dependency graph.

Tag the alternatives so they are distinguishable, or vary a field whose values carry tags of their own.

static describe(values)[source]#
Return type:

str

cinnamon.utility.inquirer#

cinnamon.utility.inquirer.filter_keys(keys)[source]#

Narrow keys down interactively by namespace, then name, then tags.

Return type:

Optional[List[RegistrationKey]]

Returns:

The selected keys, [] if the filters matched nothing, or None if the user cancelled. The caller must distinguish the last two: an empty match is worth re-prompting, a cancellation is not.

cinnamon.utility.inquirer.select_keys(keys, selected_tags=None)[source]#
Return type:

List[RegistrationKey]

cinnamon.utility.inquirer.select_name(keys)[source]#
cinnamon.utility.inquirer.select_namespace(keys)[source]#
cinnamon.utility.inquirer.select_tags(keys)[source]#

cinnamon.utility.key_analyzer#

Static analysis of registration keys.

Where static_analyzer checks that a configuration matches the component it is bound to, this module checks the keys themselves: references that resolve to nothing, and tags that look like slips of the keyboard.

Both checks exist because a RegistrationKey is a compound of three loosely typed parts. Nothing stops you writing tags={'imbd'}, and the failure – when it comes – is a lookup miss that names the key you asked for but not the one you meant.

Run it through cmn-check, or call analyze_keys() directly.

class cinnamon.utility.key_analyzer.KeyFinding(severity, category, message, key=None, suggestions=<factory>, referenced_by=<factory>)[source]#

Bases: object

One problem, with enough context to act on it.

category: str#
key: RegistrationKey | None = None#
message: str#
referenced_by: List[RegistrationKey]#
severity: Severity#
suggestions: List[KeySuggestion]#
class cinnamon.utility.key_analyzer.Severity(*values)[source]#

Bases: str, Enum

ERROR = 'error'#
WARNING = 'warning'#
cinnamon.utility.key_analyzer.analyze_keys(registry=<class 'cinnamon.registry.Registry'>)[source]#

Check every registration key, returning findings worst-first.

Call after Registry.load to catch broken references: dag_resolution stops at the first one, so the whole picture is only available beforehand. Calling it after a successful build still reports tag problems.

Return type:

List[KeyFinding]

cinnamon.utility.key_analyzer.explain_variant_tags(registry=<class 'cinnamon.registry.Registry'>)[source]#

Say what each indexed variant tag actually holds.

A variant of a list, a dict, or any other value without a short stable rendering is tagged by position – losses=variant-1. The index is deterministic, so keys stay comparable across runs and machines, but it does not tell you which losses. This reads the value back off the registered configuration and renders it.

Reported once per configuration and tag, not once per key. A tag means the same thing wherever it appears, and a registry with four varying fields carries it on a dozen keys – repeating the explanation for each would bury it.

Requires a resolved registry: the variant configurations only exist once dag_resolution has run.

Return type:

List[Tuple[str, str, str, str]]

Returns:

(name, namespace, tag, rendering) tuples, sorted and deduplicated.

cinnamon.utility.key_analyzer.format_findings(findings)[source]#

Render findings as a report, or a single line when there are none.

Return type:

str

cinnamon.utility.key_analyzer.format_variant_explanations(explanations)[source]#

Render explain_variant_tags() output, or nothing when there is none.

Return type:

str

cinnamon.utility.registration#

class cinnamon.utility.registration.NamespaceExtractor[source]#

Bases: NodeVisitor

Finds the namespaces a configuration module registers into, without running it.

Registry.build needs to know which namespaces live in which directory before it imports anything, so this reads the decorators and registration calls straight from the AST.

All three ways of registering are read: @register_method on a method, @register_class on a configuration class, and a register_configuration call inside a @register function.

A namespace is discovered when it is a literal, a module-level constant bound to one – NAMESPACE = "myproject" at the top of the file is the common idiom – or such a constant imported from another module, which is how a project keeps one namespace for many configuration files. Imports are followed on the filesystem, by parsing the module they name; nothing is executed, and nothing needs to be importable yet.

Anything computed at runtime cannot be read without executing the module, and is skipped rather than guessed at: the previous implementation took the text after namespace= and would record the string "NAMESPACE" as though it were a real namespace.

REGISTER_CLASS_DECORATOR = 'register_class'#
REGISTER_DECORATOR = 'register'#
REGISTER_METHOD_DECORATOR = 'register_method'#
REGISTRATION_CALLS = frozenset({'register_configuration'})#
process(filename)[source]#
Return type:

List[str]

visit_AsyncFunctionDef(node)#
visit_Call(node)[source]#
visit_ClassDef(node)[source]#
visit_FunctionDef(node)[source]#
cinnamon.utility.registration.import_class_from_string(path)[source]#

Import the class named by a dotted path.

The split between module and attribute is found by trying the longest importable prefix, rather than assuming the last segment is the class. A nested class – pkg.module.Outer.Inner – has two attribute segments, and splitting once would try to import pkg.module.Outer.

Return type:

type

cinnamon.utility.registration.locate_module(module_path)[source]#

Find a module’s source file without importing anything.

Returns (origin, missing_segment): the file backing module_path, and the first dotted segment that could not be found.

Presence is signalled by missing_segment is None, not by origin. A namespace package – a directory with no __init__.py – resolves successfully and yet has no file of its own, so it comes back as (None, None).

importlib.util.find_spec cannot be used for this. Resolving a dotted path there imports the parent packages to read their __path__ – for sklearn.svm that costs 619 ms against 0.11 ms here, near enough the full import it was meant to avoid. Walking segment by segment and threading each package’s submodule_search_locations into the next lookup keeps PathFinder on the filesystem, where it never executes module code.

Return type:

Tuple[Optional[str], Optional[str]]

cinnamon.utility.registration.match_name(name, names=None)[source]#
cinnamon.utility.registration.match_namespace(namespace, namespaces=None)[source]#
cinnamon.utility.registration.match_tags(a_tags, b_tags)[source]#
Return type:

bool

cinnamon.utility.sanity#

cinnamon.utility.sanity.check_directory(directory_path=None)[source]#
Return type:

Path

cinnamon.utility.sanity.check_external_json_path(jsonpath)[source]#

Read and validate a JSON file listing external configuration directories.

The contract is a JSON array of directory paths, as strings:

["/path/to/external/project_a", "/path/to/external/project_b"]

The structure is checked here rather than downstream because this file is an input boundary: it is hand-written, and it is the file a future remote-source feature would grow into. Without the check, the shape only failed later – resolve_external_directories calls Path() on each entry, so a JSON object reached the user as TypeError: argument should be a str or an os.PathLike object where __fspath__ returns a str, not <class 'dict'>, which names neither the file nor the offending entry.

Existence and directory-ness are not checked here – Registry.resolve_external_directories() already does that, and raises InvalidDirectoryException naming the directory.

Return type:

List[str]

Returns:

The list of directory paths, unchanged.

Raises:
  • FileNotFoundError – if jsonpath does not exist.

  • TypeError – if jsonpath is not a .json file, if it does not contain a list, or if an entry is not a string.

  • ValueError – if an entry is empty or only whitespace.

cinnamon.utility.sanity.time_it(func)[source]#

cinnamon.utility.static_analyzer#

Static analyzer for cinnamon bound components.

Verifies that configurations are registered correctly with the components they are bound to. This framework does not require a Component class: components are plain Python classes referenced by a fully-qualified string path (see Registry.instantiate). The analyzer therefore checks:

  • that the component path imports to a class;

  • that the configuration’s fields are compatible with the component’s __init__;

  • that a configuration bound to no component is reported as a warning,

    not an error.

cinnamon.utility.static_analyzer.analyze_registry(registry=<class 'cinnamon.registry.Registry'>, *, raise_on_error=False, deep=True)[source]#

Analyze every registered configuration’s component binding.

Returns a mapping (name, namespace, tags) -> (ok, errors, warnings). * ok is True when there are no errors. * An unbound config (component is None) is a warning, not an error, since unbound configs are valid when used purely as dependencies.

Parameters:

deep (bool) – when True (the default) each component is imported so its __init__ can be checked against the configuration’s fields. When False the component path is only resolved on the filesystem – no imports, so no cost proportional to how heavy the components are, at the price of catching only path mistakes. Importing every component of a torch-based project to look for typos costs seconds; the shallow pass costs a tenth of a millisecond per component.

Return type:

Dict[Tuple[str, str, frozenset], Tuple[bool, List[str], List[str]]]

cinnamon.utility.static_analyzer.print_analysis_summary(results)[source]#

Print a human-readable summary of results.

Return type:

None

cinnamon.utility.static_analyzer.quick_validate(directory, *, external_dirs=None)[source]#

Build the registry for directory and immediately run the analyzer.

NOTE: Registry.build already expands the DAG, so we must NOT call Registry.dag_resolution() again here (it would raise AlreadyExpandedException).

Return type:

Dict[Tuple[str, str, frozenset], Tuple[bool, List[str], List[str]]]

cinnamon.utility.static_analyzer.reset_signature_cache()[source]#

Drop the memoized component signatures.

_get_component_signature caches by import path for the lifetime of the process. Call this after reloading or redefining component classes so the analyzer re-inspects them.

Return type:

None

cinnamon.utility.suggestions#

Ranked “did you mean …?” suggestions for registration keys.

A RegistrationKey is a compound of name, namespace and tags, so a typo in any one of the three produces the same failure: the key is simply not found. This module scores registered keys against the one that was asked for and explains which part differs, which is usually the whole answer.

class cinnamon.utility.suggestions.KeySuggestion(key, score, reason)[source]#

Bases: object

A candidate key, how close it is, and why it differs.

key: RegistrationKey#
reason: str#
score: float#
cinnamon.utility.suggestions.closest_string(value, options)[source]#

The most similar option, or None when nothing is close enough.

Return type:

str | None

cinnamon.utility.suggestions.describe_difference(target, candidate)[source]#

Say, in words, how candidate differs from the key that was asked for.

Naming the differing component is what turns a list of near-misses into an actionable message: “tags differ” points straight at the mistake, where the key’s string form leaves the reader to diff two lines of text by eye.

Return type:

str

cinnamon.utility.suggestions.similarity(left, right)[source]#

Ratio in [0, 1] of how alike two strings are, ignoring case mismatches.

Case is compared separately and the better of the two ratios wins, because SequenceMatcher treats ‘IMDB’ and ‘imdb’ as sharing no characters at all – scoring 0.0 for what is one of the most common tag mistakes there is.

Return type:

float

cinnamon.utility.suggestions.suggest_keys(target, candidates, limit=3)[source]#

Rank candidates by how likely each is to be what target meant.

A candidate whose name and namespace match exactly is always offered, no matter how far its tags are: it is the single most common mistake, and the tag score alone can be zero when a tag is misspelt rather than omitted.

Return type:

List[KeySuggestion]

cinnamon.utility#