Skip to content

feat(profiles): add Profile.register_adapter emit() extension primitive - #47

Merged
discreteds merged 11 commits into
developfrom
feature/profile-register-adapter
Jun 27, 2026
Merged

feat(profiles): add Profile.register_adapter emit() extension primitive#47
discreteds merged 11 commits into
developfrom
feature/profile-register-adapter

Conversation

@discreteds

Copy link
Copy Markdown
Member

Summary

Adds Profile.register_adapter — a generic, copy-on-write-safe primitive letting downstream packages register an emit() adapter for any Hashable target on a Profile subclass after class definition. This is the leading package of a three-package auth-migration program (settings → auth-client docs → mountainash-data); the data migration consumes this primitive to register per-dialect DB driver adapters onto auth-client profile classes without auth-client ever importing DB drivers.

Why

Profile.__adapters__ is a ClassVar[dict] = {} shared default that many subclasses inherit. Naive post-definition mutation would pollute siblings. This primitive makes registration copy-on-write-safe and gives downstream a clean, fail-closed extension point on top of the existing emit() three-tier pipeline (driver_key__adapters__ 2-arg compose → legacy __adapter__). emit() semantics are unchanged — it already reads type(self).__adapters__.

What's added

  • Profile.register_adapter(target, adapter, *, overwrite=False) — copy-on-write registration under a module threading.RLock. Builds the new adapter map fully, then a single atomic rebind, so a lock-free emit() (read or iterate) never observes a partial dict. Rejects root-class registration, validates 2-positional-arg callables (accepting un-introspectable C callables), idempotent by object identity, conflict → ValueError unless overwrite=True.
  • Profile.registered_adapters() — read-only copy of the effective map for introspection.
  • emit_adapter(profile_cls, target, *, overwrite=False) — decorator form, re-exported from mountainash_settings.profiles.
  • Docs: new "Extending emission" subsection in docs/profile-spec-pattern.md.

Design notes

  • Child-first inheritance severance (documented, deliberate): the moment a subclass registers, it snapshots its own dict and severs live inheritance from ancestors — so register on the exact leaf class that should serve the adapter. emit() does no MRO merge; both orderings are covered by tests.
  • Concurrency: registration is import-time (already serialized by the import lock); the RLock is defence-in-depth and makes the read-check-write atomic.

Testing

  • New tests/unit/profiles/test_register_adapter.py — 20 tests: CoW isolation, root rejection, arity validation (incl. un-introspectable C callable), identity idempotence, conflict/overwrite, partial-identity caveat, lock serialization, end-to-end emit() routing, fail-closed unknown target, and both inheritance orderings (with a live-MRO assertion).
  • Full suite: 461 passed · ruff clean · mypy errors all pre-existing (unrelated to this branch).

Process

Built via spec → Codex adversarial review → plan → Codex review → subagent-driven execution (task-reviewed per task) → opus whole-branch review. Two review findings fixed: honest concurrency-test comment, and single-atomic-rebind realignment to the spec's stated guarantee.

🤖 Generated with Claude Code

discreteds and others added 11 commits June 27, 2026 21:47
…tion

Generic, copy-on-write-safe primitive to register an emit adapter for a Hashable
target on any Profile subclass after class definition. Realizes the opaque-target
extensibility already intended by emit(); removes the shared-__adapters__-dict
mutation hazard. Leads the 3-package auth-client migration (settings -> auth-client
docs -> mountainash-data).

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Resolve all 7 findings: reject root-class registration (shared-dict pollution);
trial-bind callable validation with C-callable fallback; document identity-based
idempotence (module-level singletons); make child-first inheritance severance an
explicit, tested semantic; add module RLock over copy-on-write+check+insert;
require package-namespaced target types; add registered_adapters() introspection +
record/restore test fixture. Codex confirmed pydantic v2 keeps __adapters__ in
cls.__dict__, so the copy-on-write check is reliable.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
5 TDD tasks covering register_adapter (copy-on-write, root reject, callable
validation, conflict policy, lock), registered_adapters introspection,
emit_adapter decorator + export, emit() routing + inheritance ordering, and
docs/quality gate.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
P-4 (blocker): Task 1 tests no longer forward-reference registered_adapters()
(Task 2's method) — inspect __adapters__ directly so each task runs in isolation.
P-1: add a two-thread concurrency test exercising the RLock. P-2: add an
uninspectable-callable (monkeypatched inspect.signature) test for the C-callable
fallback. P-3: formally retire the record/restore fixture in spec §6 (per-test
local subclasses give full isolation). Codex confirmed the pydantic v2 runtime
patterns, _UNSET bind-probe, and local-subclass isolation are sound.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
…apter

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
…concurrency-test comment

Build the new __adapters__ map fully and rebind in one assignment so a
lock-free emit() (read or iterate) never observes a partially mutated dict,
even on re-registration. Aligns code with spec 3.2 step 2's guarantee.
Correct the concurrency test comment: it asserts the one-winner contract,
not the lock's necessity (GIL serializes the short section regardless).

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
@discreteds
discreteds merged commit 2f06c06 into develop Jun 27, 2026
5 checks passed
@discreteds
discreteds deleted the feature/profile-register-adapter branch June 27, 2026 15:23
@sonarqubecloud

Copy link
Copy Markdown

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant