From f2dd08086c9cc3448c9fe0b28bd337e485e268b5 Mon Sep 17 00:00:00 2001 From: Artur Shiriev Date: Fri, 1 May 2026 09:03:48 +0300 Subject: [PATCH 1/5] Short-circuit cached provider resolution before kwargs compilation Move the cache-hit check before kwargs resolution in Factory.resolve() so singleton providers return immediately without copying static_kwargs or recursively resolving dependency providers on every call. --- modern_di/providers/factory.py | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) diff --git a/modern_di/providers/factory.py b/modern_di/providers/factory.py index f04496a..4187c84 100644 --- a/modern_di/providers/factory.py +++ b/modern_di/providers/factory.py @@ -127,6 +127,10 @@ def validate(self, container: "Container") -> dict[str, typing.Any]: def resolve(self, container: "Container") -> types.T_co: container = container.find_container(self.scope) cache_item = container.cache_registry.fetch_cache_item(self) + + if self.cache_settings and cache_item.cache is not None: + return typing.cast(types.T_co, cache_item.cache) + provider_kwargs, static_kwargs = self._ensure_kwargs_cached(container, cache_item) resolved_kwargs = dict(static_kwargs) for k, v in provider_kwargs.items(): @@ -135,9 +139,6 @@ def resolve(self, container: "Container") -> types.T_co: if not self.cache_settings: return self._creator(**resolved_kwargs) - if cache_item.cache is not None: - return typing.cast(types.T_co, cache_item.cache) - if container.lock: container.lock.acquire() From b0717632789fceaa0081ffe9ddcbe4beebd1d474 Mon Sep 17 00:00:00 2001 From: Artur Shiriev Date: Fri, 1 May 2026 09:08:09 +0300 Subject: [PATCH 2/5] Avoid throwaway CacheItem construction on every resolve Replace dict.setdefault (which eagerly evaluates its default argument, constructing a CacheItem on every call even for cache hits) with a get-then-set pattern that only creates a CacheItem on first access. --- modern_di/registries/cache_registry.py | 8 +++++++- 1 file changed, 7 insertions(+), 1 deletion(-) diff --git a/modern_di/registries/cache_registry.py b/modern_di/registries/cache_registry.py index 1a0c40e..25fc874 100644 --- a/modern_di/registries/cache_registry.py +++ b/modern_di/registries/cache_registry.py @@ -49,7 +49,13 @@ def cached_count(self) -> int: return sum(1 for item in self._items.values() if item.cache is not None) def fetch_cache_item(self, provider: Factory[types.T_co]) -> CacheItem: - return self._items.setdefault(provider.provider_id, CacheItem(settings=provider.cache_settings)) + pid = provider.provider_id + item = self._items.get(pid) + if item is not None: + return item + item = CacheItem(settings=provider.cache_settings) + self._items[pid] = item + return item async def close_async(self) -> None: errors: list[BaseException] = [] From 9ed404071fa862a68d9d2809c77943ab1374456f Mon Sep 17 00:00:00 2001 From: Artur Shiriev Date: Fri, 1 May 2026 09:09:42 +0300 Subject: [PATCH 3/5] Revert "Avoid throwaway CacheItem construction on every resolve" setdefault is atomic in CPython, which prevents a race where two threads could each create a CacheItem and one overwrites the other, losing cached instances. The throwaway construction cost is the price of thread safety. --- modern_di/registries/cache_registry.py | 8 +------- 1 file changed, 1 insertion(+), 7 deletions(-) diff --git a/modern_di/registries/cache_registry.py b/modern_di/registries/cache_registry.py index 25fc874..1a0c40e 100644 --- a/modern_di/registries/cache_registry.py +++ b/modern_di/registries/cache_registry.py @@ -49,13 +49,7 @@ def cached_count(self) -> int: return sum(1 for item in self._items.values() if item.cache is not None) def fetch_cache_item(self, provider: Factory[types.T_co]) -> CacheItem: - pid = provider.provider_id - item = self._items.get(pid) - if item is not None: - return item - item = CacheItem(settings=provider.cache_settings) - self._items[pid] = item - return item + return self._items.setdefault(provider.provider_id, CacheItem(settings=provider.cache_settings)) async def close_async(self) -> None: errors: list[BaseException] = [] From 81895da94528bbfc046513d57fd683fd8b79638a Mon Sep 17 00:00:00 2001 From: Artur Shiriev Date: Fri, 1 May 2026 09:13:51 +0300 Subject: [PATCH 4/5] Remove typing.cast from hot resolve paths typing.cast is a no-op function call at runtime that still costs ~25-30ns per invocation. Remove it from resolve_provider and Factory.resolve where it runs on every resolution. --- modern_di/container.py | 4 ++-- modern_di/providers/factory.py | 4 ++-- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/modern_di/container.py b/modern_di/container.py index 65b5234..06ecafa 100644 --- a/modern_di/container.py +++ b/modern_di/container.py @@ -100,9 +100,9 @@ def resolve_provider(self, provider: "AbstractProvider[types.T]") -> types.T: self.overrides_registry.overrides and (override := self.overrides_registry.fetch_override(provider.provider_id)) is not types.UNSET ): - return typing.cast(types.T, override) + return override # ty: ignore[invalid-return-type] - return typing.cast(types.T, provider.resolve(self)) + return provider.resolve(self) def validate_provider(self, provider: "AbstractProvider[types.T]") -> types.T: return typing.cast(types.T, provider.validate(self)) diff --git a/modern_di/providers/factory.py b/modern_di/providers/factory.py index 4187c84..567e3c5 100644 --- a/modern_di/providers/factory.py +++ b/modern_di/providers/factory.py @@ -129,7 +129,7 @@ def resolve(self, container: "Container") -> types.T_co: cache_item = container.cache_registry.fetch_cache_item(self) if self.cache_settings and cache_item.cache is not None: - return typing.cast(types.T_co, cache_item.cache) + return cache_item.cache provider_kwargs, static_kwargs = self._ensure_kwargs_cached(container, cache_item) resolved_kwargs = dict(static_kwargs) @@ -144,7 +144,7 @@ def resolve(self, container: "Container") -> types.T_co: try: if cache_item.cache is not None: - return typing.cast(types.T_co, cache_item.cache) + return cache_item.cache instance = self._creator(**resolved_kwargs) cache_item.cache = instance From 81dba154ea4235f71fbfbc606764311da05b8a90 Mon Sep 17 00:00:00 2001 From: Artur Shiriev Date: Fri, 1 May 2026 09:16:49 +0300 Subject: [PATCH 5/5] Single-pass kwargs split in _ensure_kwargs_cached Replace two dict comprehensions (one filtering providers, one filtering statics) with a single loop that partitions into both dicts in one pass. --- modern_di/providers/factory.py | 11 +++++++++-- 1 file changed, 9 insertions(+), 2 deletions(-) diff --git a/modern_di/providers/factory.py b/modern_di/providers/factory.py index 567e3c5..cf33f88 100644 --- a/modern_di/providers/factory.py +++ b/modern_di/providers/factory.py @@ -105,8 +105,15 @@ def _ensure_kwargs_cached( ) -> tuple[dict[str, "AbstractProvider[typing.Any]"], dict[str, typing.Any]]: if not cache_item.kwargs_compiled: kwargs = self._compile_kwargs(container) - cache_item.provider_kwargs = {k: v for k, v in kwargs.items() if isinstance(v, AbstractProvider)} - cache_item.static_kwargs = {k: v for k, v in kwargs.items() if not isinstance(v, AbstractProvider)} + provider_kwargs: dict[str, AbstractProvider[typing.Any]] = {} + static_kwargs: dict[str, typing.Any] = {} + for k, v in kwargs.items(): + if isinstance(v, AbstractProvider): + provider_kwargs[k] = v + else: + static_kwargs[k] = v + cache_item.provider_kwargs = provider_kwargs + cache_item.static_kwargs = static_kwargs cache_item.kwargs_compiled = True return cache_item.provider_kwargs, cache_item.static_kwargs