Skip to content

Add scoped singleton registries to SingletonConfigurable - #953

Merged
Carreau merged 1 commit into
ipython:mainfrom
Carreau:multiton
Aug 4, 2026
Merged

Add scoped singleton registries to SingletonConfigurable#953
Carreau merged 1 commit into
ipython:mainfrom
Carreau:multiton

Conversation

@Carreau

@Carreau Carreau commented Jul 17, 2026

Copy link
Copy Markdown
Member

Introduce SingletonScope, an isolated singleton registry activated for a dynamic extent (current thread / async task) via a contextvars.ContextVar. While active, instance() on any subclass of the scope's base resolves within the scope -- creating fresh instances on first use -- without ever reading, creating, or mutating the process-global _instance. The classic (no-scope) instance()/initialized()/clear_instance() paths are left textually unchanged; scoped resolution is prepended as an early branch.

  • SingletonScope with covers()/get() (MRO-walk parity)/add() (write-through pre-seeding) and a re-enterable call context manager
  • SingletonConfigurable.scope() factory and _current_scope() helper
  • Export SingletonScope from traitlets.config
  • Tests covering isolation, lazy creation by uncontrolled code, no global side effects, initialized() semantics, re-entry, pre-seeding, blast radius, nesting/LIFO restore, MRO sibling MultipleInstanceError parity, in-scope clear_instance, thread isolation, and asyncio task propagation
  • Docs: "Singleton scopes" section with usage/semantics and a per-thread singleton example

@codecov

codecov Bot commented Aug 4, 2026

Copy link
Copy Markdown

Welcome to Codecov 🎉

Once you merge this PR into your default branch, you're all set! Codecov will compare coverage reports and display results in all future pull requests.

Thanks for integrating Codecov - We've got you covered ☂️

@Carreau Carreau added this to the 5.17 milestone Aug 4, 2026
Introduce SingletonScope, an isolated singleton registry activated for a
dynamic extent (current thread / async task) via a contextvars.ContextVar.
While active, instance() on any subclass of the scope's base resolves within
the scope -- creating fresh instances on first use -- without ever reading,
creating, or mutating the process-global _instance. The classic (no-scope)
instance()/initialized()/clear_instance() paths are left textually unchanged;
scoped resolution is prepended as an early branch.

- SingletonScope with covers()/get() (MRO-walk parity)/add() (write-through
  pre-seeding) and a re-enterable __call__ context manager
- SingletonConfigurable.scope() factory and _current_scope() helper
- Export SingletonScope from traitlets.config
- Docs: "Singleton scopes" section with usage and semantics, a per-thread
  singleton example, a "complete blank slate" section covering every
  singleton at once via SingletonConfigurable.scope(), and an
  "injecting a specific instance" section motivating add()
- versionadded/versionchanged:: 5.17 directives on the new API and on the
  instance()/initialized()/clear_instance() behavior change
- Tests covering isolation, lazy creation by uncontrolled code, no global
  side effects, initialized() semantics, re-entry, pre-seeding, blast radius,
  nesting/LIFO restore, MRO sibling MultipleInstanceError parity,
  in-scope clear_instance, thread isolation, asyncio task propagation, and
  the process-wide blank-slate scope

Because coverage is just issubclass(cls, base), scoping SingletonConfigurable
itself yields a clean-room registry in which no singleton has been created
yet. add() is the only way to control *which* object in-scope .instance()
calls receive, since they otherwise construct a fresh one; it writes through
to singleton ancestors so a pre-seeded subclass instance is also returned
when resolving via a parent class.

The "threads don't inherit the scope" guarantee only holds when
sys.flags.thread_inherit_context is false (the default GIL build). On the
free-threaded build (e.g. 3.14t) it defaults to true, so a child thread
starts with a copy of the parent context and resolves .instance() within the
scope. Activating a scope under that flag emits a RuntimeWarning, the docs
carry a matching caveat, and the thread-isolation test is skipped there. The
flag is read through a small _threads_inherit_context() helper so tests can
patch it without replacing the sys.flags structseq wholesale (which broke on
pypy-3.11).

Also pin the prettier pre-commit hook to language_version: system, so it uses
the system node instead of a downloaded nodeenv build.
@Carreau
Carreau force-pushed the multiton branch 2 times, most recently from 1ae2b75 to 9133ced Compare August 4, 2026 08:07
@Carreau
Carreau marked this pull request as ready for review August 4, 2026 08:25
@Carreau
Carreau merged commit 58f4301 into ipython:main Aug 4, 2026
33 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant