Skip to content

Add scoped singleton registries to SingletonConfigurable - #953

Draft
Carreau wants to merge 5 commits into
ipython:mainfrom
Carreau:multiton
Draft

Add scoped singleton registries to SingletonConfigurable#953
Carreau wants to merge 5 commits 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

claude and others added 5 commits July 17, 2026 11:31
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
On the free-threaded build (e.g. 3.14t) sys.flags.thread_inherit_context
defaults to true, so a new threading.Thread starts with a copy of the
parent's contextvars.Context. The active SingletonScope therefore
propagates into child threads and MyApp.instance() resolves within the
scope instead of falling through to the process-global registry, which
made test_thread_isolation fail on all 3.14t jobs.

Skip the test whenever thread_inherit_context is enabled (the default on
free-threaded builds, opt-in elsewhere) so CI stays green; the classic
GIL-build behavior remains covered.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_0159wyFjYsXoVKyUN9NkNxzw
…erit_context

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.

- Docs: add a warning admonition to the "Singleton scopes" section and the
  SingletonScope docstring spelling out the build-dependent behavior, and
  correct the propagation bullet; reframe the per-thread example as working
  on both builds.
- Emit a RuntimeWarning when a scope is activated while
  thread_inherit_context is enabled, pointing users at per-thread scopes.
- Test the warning fires when the flag is enabled and stays silent when not.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_0159wyFjYsXoVKyUN9NkNxzw
The test mocked the whole sys.flags structseq with a Mock, so other code
that reads integer fields off sys.flags during the scope block (surfaced
on pypy-3.11) failed with "expected integer, got Mock object".

Read the flag through a small _threads_inherit_context() helper and patch
that in the test instead of replacing sys.flags wholesale.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_0159wyFjYsXoVKyUN9NkNxzw
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.

2 participants