Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
29 changes: 17 additions & 12 deletions docs/profile-spec-pattern.md
Original file line number Diff line number Diff line change
Expand Up @@ -173,24 +173,29 @@ The pattern is not free. Compared to a plain subclass:

For a single application-specific settings class, none of this is worth it. For a library or any code where you manage more than two or three similar profiles, the structural guarantees pay for themselves quickly.

## Extending emission: `register_adapter`
## Extending emission

`emit()` targets are any `Hashable`, so other domains can add their own. To
register an `emit()` adapter for a target on a profile class after definition:
`emit()` targets are any `Hashable`, so other domains can add their own. The
sanctioned mechanism is the **inline** per-target adapter map declared on the
profile class:

```python
from mountainash_settings.profiles import emit_adapter
class PasswordAuthProfile(Profile):
__spec__ = ...
__adapters__ = {MyTarget.POSTGRES: _postgres}


@emit_adapter(PasswordAuthProfile, MyTarget.POSTGRES)
def _postgres(auth, base):
return {**base, "user": auth.USERNAME,
"password": auth.PASSWORD.get_secret_value()}
```

`Profile.register_adapter(target, adapter, *, overwrite=False)` is the
non-decorator form. It is copy-on-write-safe (never mutates a parent/shared
adapter map), idempotent for the same adapter object, and raises on a conflicting
re-registration. Register on the **concrete** class you mean (registering on a
base does not propagate to a child that already registered). Use a
package-namespaced target type (an `Enum` / frozen dataclass), never bare strings.
`registered_adapters()` returns a read-only copy for introspection.
Each adapter has the 2-arg compose signature `(profile, merged) -> dict`;
`instance.emit(target, base=...)` routes through the entry for `target`. Use a
package-namespaced target type (an `Enum` / frozen dataclass), never bare
strings.

A consumer that needs to extend emission for a profile it does not own builds a
**consumer-owned** dispatch table (`(provider_type, auth_class) -> fn`) and reads
the credential's data directly, rather than mutating a class it imports — see
`mountainash-auth-client/a.architecture/credentials-are-rendered-by-the-consumer.md`.
Original file line number Diff line number Diff line change
@@ -1,5 +1,7 @@
# Profile.register_adapter Implementation Plan

> **SUPERSEDED (2026-06-29):** The `register_adapter` registry this plan built was removed as built-but-unused and architecturally overkill (zero callers). See `mountainash-central/01.principles/mountainash-settings/h.backlog/remove-register-adapter-registry.md` and the removal PR. Retained for history.

> **For agentic workers:** REQUIRED SUB-SKILL: Use superpowers:subagent-driven-development (recommended) or superpowers:executing-plans to implement this plan task-by-task. Steps use checkbox (`- [ ]`) syntax for tracking.

**Goal:** Add a generic, copy-on-write-safe `Profile.register_adapter` primitive to mountainash-settings so downstream packages can register an `emit()` adapter for a `Hashable` target on an existing `Profile` subclass after class definition.
Expand Down
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
# Design Spec: `Profile.register_adapter` — post-hoc emit-adapter registration

**Date:** 2026-06-27
**Status:** Draft — for review
**Status:** SUPERSEDED (2026-06-29) — the `register_adapter` registry was removed as built-but-unused and architecturally overkill (zero callers; consumers use a consumer-owned dispatch table instead). See `mountainash-central/01.principles/mountainash-settings/h.backlog/remove-register-adapter-registry.md` and the removal PR. The original draft is retained below for history.
**Repo:** mountainash-settings
**Author:** Nathaniel Ramm (with Claude)

Expand Down
3 changes: 1 addition & 2 deletions src/mountainash_settings/profiles/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -8,13 +8,12 @@

from .lookup import lookup_class_var
from .invariants import spec_invariants_for
from .profile import Adapter, Profile, emit_adapter
from .profile import Adapter, Profile
from .registry import Registry
from .spec import MISSING, Missing, ParameterSpec, ProfileSpec

__all__ = [
"Adapter",
"emit_adapter",
"MISSING",
"Missing",
"ParameterSpec",
Expand Down
116 changes: 1 addition & 115 deletions src/mountainash_settings/profiles/profile.py
Original file line number Diff line number Diff line change
Expand Up @@ -13,8 +13,6 @@

from __future__ import annotations

import inspect
import threading
import typing as t
import warnings

Expand All @@ -26,7 +24,7 @@
from .lookup import lookup_class_var
from .spec import MISSING, ProfileSpec

__all__ = ["Adapter", "Profile", "emit_adapter"]
__all__ = ["Adapter", "Profile"]

# A target adapter composes credential/config kwargs: it receives the profile
# and the already-merged (base + driver_key renames) dict, and returns the final
Expand All @@ -38,29 +36,6 @@
# target, so ``emit()`` can fail closed on target-scoped profiles.
_UNSET: t.Any = object()

# Serializes register_adapter's copy-on-write + conflict-check + insert. Registration
# is import-time (already serialized by the import lock); this is defence-in-depth.
_REGISTER_LOCK = threading.RLock()


def _check_two_positional(adapter: t.Callable[..., t.Any]) -> None:
"""Raise ``TypeError`` unless ``adapter`` can be called with two positional args.

For C callables / builtins where ``inspect.signature`` is unavailable, accept
after the caller's ``callable()`` check rather than guess.
"""
try:
sig = inspect.signature(adapter)
except (ValueError, TypeError):
return # cannot introspect (C callable) — accept
try:
sig.bind(_UNSET, _UNSET)
except TypeError as exc:
raise TypeError(
"adapter must accept two positional args (profile, merged); "
f"{getattr(adapter, '__name__', adapter)!r} does not: {exc}"
) from None


def _resolve_spec(cls: type) -> ProfileSpec | None:
"""Resolve a class's bound spec from __spec__ (new) or __descriptor__ (old).
Expand Down Expand Up @@ -120,77 +95,6 @@ class Profile(MountainAshBaseSettings):
] = None
__adapters__: t.ClassVar[dict[t.Hashable, "Adapter"]] = {}

@classmethod
def register_adapter(
cls,
target: t.Hashable,
adapter: "Adapter",
*,
overwrite: bool = False,
) -> None:
"""Register a per-target ``emit()`` adapter on this Profile subclass.

``adapter`` has the 2-arg compose signature ``(profile, merged) -> dict``
(the same shape as inline ``__adapters__`` entries). After registration,
``instance.emit(target, base=...)`` routes through it.

Safe to call at import time from a downstream package: copies
``__adapters__`` onto ``cls`` first if ``cls`` is still inheriting an
ancestor's map, so registration never mutates a shared/parent dict.

Idempotent by identity: re-registering the *same* adapter object is a
no-op; a *different* adapter for an existing target raises unless
``overwrite=True``.

Raises:
TypeError: if called on ``Profile`` itself, if ``adapter`` is not a
two-positional-arg callable, or if ``target`` is unhashable.
ValueError: if ``target`` is already registered to a different
adapter and ``overwrite`` is False.
"""
if cls is Profile:
raise TypeError(
"register_adapter must be called on a concrete Profile subclass, "
"not Profile itself (would mutate the shared default adapter map)."
)
if not callable(adapter):
raise TypeError(
f"adapter must be callable, got {type(adapter).__name__}"
)
_check_two_positional(adapter)
try:
hash(target)
except TypeError as exc:
raise TypeError(f"target must be hashable, got {target!r}") from exc

with _REGISTER_LOCK:
# Build the new adapter map in full, then rebind in a single atomic
# assignment (last statement). Copy-on-write off the inherited/own map
# snapshots existing entries; the conflict check and insert run on the
# *copy*, so a lock-free emit() — whether it reads __adapters__.get() or
# iterates it in _known_targets() — only ever observes the old complete
# dict or the new complete dict, never a partially mutated one, even on
# re-registration. This also never touches Profile's shared default or a
# parent's map.
new_map = dict(cls.__adapters__)
existing = new_map.get(target, _UNSET)
if existing is not _UNSET and existing is not adapter and not overwrite:
raise ValueError(
f"{cls.__name__} already has an adapter for target {target!r}; "
f"pass overwrite=True to replace it."
)
new_map[target] = adapter
cls.__adapters__ = new_map

@classmethod
def registered_adapters(cls) -> dict[t.Hashable, "Adapter"]:
"""Return a copy of the effective ``__adapters__`` map for ``cls``.

Read-only snapshot (own or inherited entries); mutating it does not
affect the class.
"""
return dict(cls.__adapters__)

@classmethod
def __pydantic_init_subclass__(cls, **kwargs: t.Any) -> None:
"""Install fields described by ``__spec__`` on the subclass."""
Expand Down Expand Up @@ -397,21 +301,3 @@ def emit(
return type(self).__adapter__(self) # legacy 1-arg owns-pipeline
return merged


def emit_adapter(
profile_cls: type["Profile"],
target: t.Hashable,
*,
overwrite: bool = False,
) -> t.Callable[["Adapter"], "Adapter"]:
"""Decorator form of :meth:`Profile.register_adapter`.

Registers the decorated 2-arg adapter on ``profile_cls`` for ``target`` and
returns it unchanged.
"""
def _wrap(fn: "Adapter") -> "Adapter":
profile_cls.register_adapter(target, fn, overwrite=overwrite)
return fn

return _wrap

Loading
Loading