feat(profiles): add Profile.register_adapter emit() extension primitive - #47
Merged
Conversation
…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>
|
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.



Summary
Adds
Profile.register_adapter— a generic, copy-on-write-safe primitive letting downstream packages register anemit()adapter for anyHashabletarget on aProfilesubclass 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 aClassVar[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 existingemit()three-tier pipeline (driver_key→__adapters__2-arg compose → legacy__adapter__).emit()semantics are unchanged — it already readstype(self).__adapters__.What's added
Profile.register_adapter(target, adapter, *, overwrite=False)— copy-on-write registration under a modulethreading.RLock. Builds the new adapter map fully, then a single atomic rebind, so a lock-freeemit()(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 →ValueErrorunlessoverwrite=True.Profile.registered_adapters()— read-only copy of the effective map for introspection.emit_adapter(profile_cls, target, *, overwrite=False)— decorator form, re-exported frommountainash_settings.profiles.docs/profile-spec-pattern.md.Design notes
emit()does no MRO merge; both orderings are covered by tests.Testing
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-endemit()routing, fail-closed unknown target, and both inheritance orderings (with a live-MRO assertion).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