Profile Registry and Invariants¶
Summary¶
This chapter covers the profile registry that provides a centralized store for ProfileSpec instances. You will learn about its name-keyed store, bare class-decorator registration, duplicate prevention, lookup by name, spec invariants and automated invariant tests.
Why a Registry¶
When an application supports multiple connection backends -- PostgreSQL, MySQL, Redis, Snowflake, BigQuery -- each backend has its own ProfileSpec and Profile subclass. Something needs to collect these registrations, prevent naming collisions, and provide runtime lookup so that a configuration file specifying backend: postgresql can be resolved to the correct settings class. The Registry class fills this role.
Registries are domain-scoped. A database package has its own registry, a message broker package has another, and an API client package has a third. Each registry operates independently, with its own namespace and optional type constraints.
Registry Class¶
The Registry class is a mutable, name-keyed store that maps profile names to their specs and settings classes. It maintains two parallel dictionaries:
_descriptors-- maps name strings toProfileSpecinstances_classes-- maps name strings toProfilesubclasses
The constructor accepts a name (used in error messages and test IDs) plus two optional type constraints:
class Registry:
def __init__(
self,
name: str,
*,
spec_type: type[ProfileSpec] | None = None,
profile_type: type | None = None,
) -> None:
self.name = name
self._spec_type = spec_type
self._profile_type = profile_type
self._descriptors: dict[str, ProfileSpec] = {}
self._classes: dict[str, type[Profile]] = {}
When spec_type is provided, every registered spec must be an instance of that type (or a subclass). When profile_type is provided, every registered class must be a subclass. These constraints catch accidental cross-domain registrations at registration time rather than at runtime lookup.
# Create a typed registry
DATABASES_REGISTRY = Registry(
"databases",
spec_type=BackendSpec, # custom subclass of ProfileSpec
profile_type=ConnectionProfile # custom subclass of Profile
)
Name Keyed Store¶
The name-keyed store is the core data structure: two dictionaries keyed by the ProfileSpec.name string. The name serves as the primary identifier for all lookup and registration operations.
The store supports three access patterns:
- Registration --
register(spec, cls)adds a new entry - Lookup by name --
get_spec(name)andget_settings_class(name)retrieve entries - Containment check --
name in registrytests whether a name is registered
<!-- concept:73 -->
# After registration:
assert "postgresql" in DATABASES_REGISTRY
assert len(DATABASES_REGISTRY) == 1
spec = DATABASES_REGISTRY.get_spec("postgresql")
cls = DATABASES_REGISTRY.get_settings_class("postgresql")
The descriptors property returns a shallow copy of the specs dictionary, providing a read-only view that prevents external mutation of the registry's internal state.
Decorator Registration¶
Decorator registration is the primary mechanism for adding profiles to a registry. Instead of calling registry.register(spec, cls) imperatively, developers use the @register decorator on the class definition:
register = DATABASES_REGISTRY.decorator()
@register
class PostgreSQLProfile(ConnectionProfile):
__spec__ = POSTGRESQL_SPEC
The bare @register form (without arguments) is the canonical pattern as of version 26.5.0. The decorator reads cls.__spec__ from the class body and calls registry.register(spec, cls) internally. This ensures that the spec and class are always in sync -- the spec declared on the class is exactly the spec registered in the registry.
The bare decorator returns the Profile subclass unchanged, validates its class-body __spec__, and adds it to the registry's lookup tables. There is no descriptor mirror or decorator-with-spec form in 0.1.0.
Diagram: Decorator Registration Flow¶
Decorator Registration Flow
Type: workflow
sim-id: decorator-registration-flow
Library: vis-network
Status: Specified
A directed flow showing the @register decoration process: (1) Python evaluates class body (installs spec), (2) @register decorator invoked with the class, (3) decorator reads cls.spec, (4) type constraints validated (spec_type, profile_type), (5) duplicate check against _descriptors, (6) spec and class added to registry dictionaries, (7) class returned unchanged. Each step is clickable to show the data at that point. Error branches show what happens when type mismatch or duplicate is detected. Learning objective: Trace the registration lifecycle from class definition through decorator execution to registry storage (Bloom: Understand).
Registry Decorator Factory¶
The registry decorator factory is the decorator() method on Registry that returns the @register decorator bound to a specific registry instance. This factory pattern allows multiple registries to coexist, each with its own decorator:
# Each registry produces its own decorator
db_register = DATABASES_REGISTRY.decorator()
mq_register = MESSAGE_QUEUE_REGISTRY.decorator()
@db_register
class PostgreSQLProfile(ConnectionProfile):
__spec__ = POSTGRESQL_SPEC
@mq_register
class RabbitMQProfile(BrokerProfile):
__spec__ = RABBITMQ_SPEC
The factory also supports a deprecated with-argument form @register(spec) for backwards compatibility during the 26.5.x deprecation window. This form emits a DeprecationWarning and validates that the passed spec matches the class's __spec__ if both are present. The disambiguation between the two forms is based on type: if the argument is a type (a class), it is the bare form; if it is a ProfileSpec instance, it is the deprecated with-argument form.
Duplicate Prevention¶
Duplicate prevention ensures that no two profiles register under the same name in a single registry. The register() method checks _descriptors before adding a new entry:
def register(self, spec: ProfileSpec, cls: type[Profile]) -> None:
if spec.name in self._descriptors:
existing = self._classes.get(spec.name)
where = (
f"{existing.__module__}.{existing.__qualname__}"
if existing is not None
else "<unknown class>"
)
raise ValueError(
f"Profile {spec.name!r} is already registered "
f"in {self.name} registry by {where}"
)
The error message includes the fully qualified name of the class that already occupies the slot, making it easy to diagnose import-order conflicts where two modules both attempt to register under the same name. This is a common issue in plugin-style architectures where multiple packages contribute to the same registry.
Duplicate prevention applies per-registry. A name like "postgresql" can exist in both DATABASES_REGISTRY and ANALYTICS_REGISTRY without conflict, since they are separate Registry instances with independent namespaces.
Resolving duplicate registration errors
If you encounter a ValueError about duplicate registration, check whether the same module is being imported twice (common with relative vs absolute imports) or whether two specs accidentally share the same name field. The error message's module path helps identify the conflict source.
Registry Iteration¶
Registry iteration is supported through the descriptors property and the __len__ and __contains__ dunder methods:
# Check how many profiles are registered
print(len(DATABASES_REGISTRY)) # e.g., 5
# Iterate over all specs
for name, spec in DATABASES_REGISTRY.specs.items():
print(f"{name}: {len(spec.parameters)} parameters")
# Check membership
if "postgresql" in DATABASES_REGISTRY:
spec = DATABASES_REGISTRY.get_spec("postgresql")
The descriptors property returns a copy of the internal dictionary, which means iterating or modifying the returned dict does not affect the registry. The __contains__ method checks isinstance(name, str) before lookup, ensuring that non-string values always return False rather than raising a TypeError.
Registry Lookup By Name¶
Registry lookup by name is provided by two methods that mirror the two internal dictionaries:
get_spec(name)-- returns theProfileSpecfor the given nameget_settings_class(name)-- returns theProfilesubclass for the given name
Both methods raise KeyError with a helpful message that lists all known names in the registry:
def get_spec(self, name: str) -> ProfileSpec:
try:
return self._specs[name]
except KeyError:
known = ", ".join(sorted(self._specs)) or "<none>"
raise KeyError(
f"No profile registered under {name!r} in {self.name} "
f"registry. Known: {known}"
) from None
The from None suppression ensures a clean error message without the confusing "during handling of the above exception, another exception occurred" chain. The known-names hint is especially valuable in applications with many registered profiles, where a typo in the name (e.g., "postgresq" instead of "postgresql") can be immediately identified.
Diagram: Registry Internal Structure¶
Registry Internal Structure
Type: diagram
sim-id: registry-structure
Library: vis-network
Status: Specified
An interactive diagram showing a Registry instance with its two parallel dictionaries (_descriptors and _classes). Three sample profiles are registered (postgresql, mysql, redis). Clicking a profile name in either dictionary highlights the matching entry in the other dictionary and shows the spec details / class information in a side panel. The type constraint badges (spec_type, profile_type) are shown at the top with hover explanations. A search box allows filtering registered entries. Learning objective: Navigate the Registry's dual-dictionary structure and understand how specs and classes are co-indexed (Bloom: Understand).
Descriptor Invariants¶
Descriptor invariants are a set of validation rules that every registered ProfileSpec must satisfy. They encode the naming conventions, uniqueness constraints, and structural requirements that the profile system relies on:
| Invariant | Rule | Failure Meaning |
|---|---|---|
test_name_matches_registry_key |
spec.name == registry_key |
Spec name and registry key are out of sync |
test_name_lowercase_nonempty |
spec.name is lowercase and non-empty |
Naming convention violated |
test_parameter_names_unique |
No duplicate names in spec.parameters |
Ambiguous field definitions |
test_parameter_names_uppercase |
All parameter names are UPPERCASE and non-empty | Naming convention violated |
test_driver_keys_unique |
No duplicate driver_key values |
Ambiguous driver kwargs |
test_parameter_tiers_valid |
All tiers are "core" or "advanced" |
Invalid tier classification |
test_auth_modes_nonempty |
spec.auth_modes has at least one entry |
Missing authentication declaration |
test_provider_type_not_none |
spec.provider_type is not None |
Missing domain classification |
These invariants catch specification errors at test time, well before they could cause runtime failures. A typo in a parameter name, a forgotten auth mode declaration, or a missing provider type all produce clear test failures with descriptive messages.
Invariant Test Generator¶
The invariant test generator is the spec_invariants_for() function that takes a Registry instance and returns a pytest class parameterized over every registered spec. This function is the primary integration point between the invariant system and a project's test suite:
from mountainash_settings.profiles import spec_invariants_for
from my_package.settings import MY_REGISTRY
TestMyInvariants = spec_invariants_for(MY_REGISTRY)
This single line generates one test per invariant per registered spec. If the registry contains five profiles and there are eight invariants, the generated class produces forty parameterized test cases.
The generator uses pytest.mark.parametrize to create test cases named by the profile:
@pytest.mark.parametrize("name,spec", entries, ids=ids)
class TestSpecInvariants:
def test_name_matches_registry_key(self, name, spec):
assert spec.name == name
def test_parameter_names_unique(self, name, spec):
names = [p.name for p in spec.parameters]
assert len(names) == len(set(names))
# ... remaining invariants
The class is dynamically renamed to TestSpecInvariants_<registry_name> so that pytest output clearly identifies which registry's invariants are running. New profile registrations automatically get full invariant coverage -- there is no need to write individual tests for each new backend.
Diagram: Invariant Test Coverage Matrix¶
Invariant Test Coverage Matrix
Type: chart
sim-id: invariant-test-matrix
Library: Chart.js
Status: Specified
A heatmap-style matrix with invariant names as rows and registered profile names as columns. Each cell is green (pass), red (fail), or gray (not yet run). Clicking a cell shows the test assertion that passed or the error message that failed. Hovering over a row header shows the invariant's rule description. A "Run All" button animates the matrix filling in cell by cell. Users can add a "broken" spec via a form to see how invariant failures appear. Learning objective: Evaluate the completeness of invariant coverage across all registered profiles in a registry (Bloom: Evaluate).
Putting It All Together¶
The registry and invariant systems work together to create a reliable, discoverable ecosystem of connection profiles. The full workflow from profile creation to validated deployment is:
- Define a ProfileSpec with parameters, auth modes, and identity
- Create a Profile subclass with
__spec__ = YOUR_SPEC - Register with
@registeron the class definition - Add
TestInvariants = spec_invariants_for(REGISTRY)to your test suite - Look up at runtime via
REGISTRY.get_settings_class("name")
Each step in this workflow is supported by the framework with clear error messages and fail-fast behavior. Misspelled names produce KeyError with a list of known alternatives. Type constraint violations produce TypeError at registration time. Duplicate names produce ValueError with the conflicting class's location.
The test seam methods (_snapshot_for_tests and _reset_for_tests) allow test suites to register temporary profiles without permanently polluting the global registry, supporting isolated test scenarios.
Key Takeaways¶
- Registry Class provides a mutable, name-keyed store with optional type constraints for both specs and profile classes.
- Name Keyed Store maintains two parallel dictionaries (specs and classes) indexed by the profile name string.
- Decorator Registration uses bare
@registerto readcls.__spec__and add the class to the registry in a single declaration. - Registry Decorator Factory returns a bound decorator, enabling multiple independent registries with their own
@registerdecorators. - Duplicate Prevention raises
ValueErrorwith the conflicting class's module path when a name collision occurs. - Registry Iteration provides read-only access via the
descriptorsproperty,__len__, and__contains__. - Registry Lookup By Name raises
KeyErrorwith a list of known names when a lookup fails, aiding quick diagnosis of typos. - Invariant Test Generator produces parameterized pytest classes that automatically cover every registered spec, ensuring new profiles get full validation for free.