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:
objectBatch 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:
EnumHow 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.Noneyields nothing, which is how an unset optional dependency stays legal.
- cinnamon.utility.dependencies.dependency_shape(field_name, annotation, value)[source]#
Classify field_name as a dependency field, or
Noneif 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:
- cinnamon.utility.dependencies.iter_dependency_keys(value)[source]#
Yield every
RegistrationKeyheld 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 forConfigurationinstances) iterates cleanly.- Return type:
- cinnamon.utility.dependencies.map_dependency_keys(value, function)[source]#
Rebuild value with function applied to every
RegistrationKeyin it.The container shape is preserved, so a
dict[str, RegistrationKey]maps to adict[str, Configuration]under the same string labels. Members that are not keys are passed through untouched.- Return type:
cinnamon.utility.exceptions#
- exception cinnamon.utility.exceptions.AlreadyRegisteredException(registration_key)[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.NotRegisteredException(registration_key, suggestions=None)[source]#
Bases:
Exception
- exception cinnamon.utility.exceptions.UnserializableRuntimeException(registration_key, field, reason)[source]#
Bases:
ExceptionA registered value that cannot travel to another process.
Raised by
Registry.freeze_runtimerather 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:
ExceptionA
Configurationfield 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:
objectStores conditions evaluation result (see
Configuration.validate()).- Parameters:
- property stack_trace#
- exception cinnamon.utility.exceptions.VariantKeyCollisionException(key, variant_key, values, previous=None)[source]#
Bases:
ExceptionTwo 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
RegistrationKeyinstances contribute the child’s tags and nothing else, so two children that differ only by name –loader-csvandloader-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.
cinnamon.utility.inquirer#
- cinnamon.utility.inquirer.filter_keys(keys)[source]#
Narrow keys down interactively by namespace, then name, then tags.
- Return type:
- Returns:
The selected keys,
[]if the filters matched nothing, orNoneif the user cancelled. The caller must distinguish the last two: an empty match is worth re-prompting, a cancellation is not.
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:
objectOne problem, with enough context to act on it.
- key: RegistrationKey | None = None#
- referenced_by: List[RegistrationKey]#
- suggestions: List[KeySuggestion]#
- class cinnamon.utility.key_analyzer.Severity(*values)[source]#
-
- 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.loadto catch broken references:dag_resolutionstops at the first one, so the whole picture is only available beforehand. Calling it after a successfulbuildstill reports tag problems.- Return type:
- 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_resolutionhas run.
- cinnamon.utility.key_analyzer.format_findings(findings)[source]#
Render findings as a report, or a single line when there are none.
- Return type:
- cinnamon.utility.key_analyzer.format_variant_explanations(explanations)[source]#
Render
explain_variant_tags()output, or nothing when there is none.- Return type:
cinnamon.utility.registration#
- class cinnamon.utility.registration.NamespaceExtractor[source]#
Bases:
NodeVisitorFinds the namespaces a configuration module registers into, without running it.
Registry.buildneeds 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_methodon a method,@register_classon a configuration class, and aregister_configurationcall inside a@registerfunction.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'})#
- visit_AsyncFunctionDef(node)#
- 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 importpkg.module.Outer.- Return 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 byorigin. 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_speccannot be used for this. Resolving a dotted path there imports the parent packages to read their__path__– forsklearn.svmthat 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’ssubmodule_search_locationsinto the next lookup keepsPathFinderon the filesystem, where it never executes module code.
cinnamon.utility.sanity#
- 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_directoriescallsPath()on each entry, so a JSON object reached the user asTypeError: 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 raisesInvalidDirectoryExceptionnaming the directory.- Return type:
- Returns:
The list of directory paths, unchanged.
- Raises:
FileNotFoundError – if jsonpath does not exist.
TypeError – if jsonpath is not a
.jsonfile, 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.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). *okisTruewhen 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) – whenTrue(the default) each component is imported so its__init__can be checked against the configuration’s fields. WhenFalsethe 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:
- cinnamon.utility.static_analyzer.quick_validate(directory, *, external_dirs=None)[source]#
Build the registry for directory and immediately run the analyzer.
NOTE:
Registry.buildalready expands the DAG, so we must NOT callRegistry.dag_resolution()again here (it would raiseAlreadyExpandedException).
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:
objectA candidate key, how close it is, and why it differs.
- key: RegistrationKey#
- cinnamon.utility.suggestions.closest_string(value, options)[source]#
The most similar option, or
Nonewhen nothing is close enough.
- 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:
- 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
SequenceMatchertreats ‘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:
- 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: