Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,7 @@
[![Context7](https://img.shields.io/badge/Context7-docs-blue)](https://context7.com/modern-python/that-depends)
[![llms.txt](https://img.shields.io/badge/llms.txt-green)](https://that-depends.modern-python.org/llms.txt)

Dependency injection framework for Python.
Simple, typed dependency injection framework for Python.

It is production-ready and gives you the following:
- Simple async-first DI framework with IOC-container.
Expand Down
13 changes: 9 additions & 4 deletions pyproject.toml
Original file line number Diff line number Diff line change
@@ -1,14 +1,16 @@
[project]
name = "that-depends"
description = "Simple Dependency Injection framework"
description = "Simple, typed dependency-injection framework for Python"
authors = [
{ name = "Artur Shiriev", email = "me@shiriev.ru" },
]
readme = "README.md"
requires-python = ">=3.10,<4"
license = "MIT"
keywords = ["di", "dependency injector", "ioc-container", "mocks", "python"]
keywords = ["dependency-injection", "di", "ioc-container", "scopes", "mocks", "python"]
classifiers = [
"Development Status :: 5 - Production/Stable",
"Intended Audience :: Developers",
"Programming Language :: Python :: 3.10",
"Programming Language :: Python :: 3.11",
"Programming Language :: Python :: 3.12",
Expand All @@ -28,8 +30,11 @@ faststream = [
]

[project.urls]
repository = "https://github.com/modern-python/that-depends"
docs = "https://that-depends.modern-python.org"
Homepage = "https://that-depends.modern-python.org"
Documentation = "https://that-depends.modern-python.org"
Repository = "https://github.com/modern-python/that-depends"
Issues = "https://github.com/modern-python/that-depends/issues"
Changelog = "https://github.com/modern-python/that-depends/releases"

[dependency-groups]
dev = [
Expand Down
25 changes: 20 additions & 5 deletions tests/providers/test_local_singleton.py
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
import asyncio
import itertools
import random
import threading
import time
Expand Down Expand Up @@ -71,11 +72,25 @@ async def test_thread_local_singleton_reuses_instance_created_while_waiting_on_l

def test_thread_local_singleton_different_threads() -> None:
"""Test that different threads receive different instances."""
provider = ThreadLocalSingleton(_factory)
results = []
# Each factory call returns a distinct value from an atomic counter, so the test is
# deterministic: the ThreadLocalSingleton calls the factory once per thread, yielding
# one unique value per thread. A random factory could collide and a thread-id factory
# is unreliable because thread ids are recycled once a thread exits; itertools.count
# avoids both. The sleep keeps the threads overlapping so this exercises real concurrency.
counter = itertools.count()

def factory() -> int:
time.sleep(0.01)
return next(counter)

provider = ThreadLocalSingleton(factory)
results: list[int] = []
results_lock = threading.Lock()

def resolve_in_thread() -> None:
results.append(provider.resolve_sync())
value = provider.resolve_sync()
with results_lock:
results.append(value)

number_of_threads = 10

Expand All @@ -86,8 +101,8 @@ def resolve_in_thread() -> None:
for thread in threads:
thread.join()

assert len(results) == number_of_threads, "Test failed: Expected results from two threads."
assert results[0] != results[1], "Thread-local failed: Instances across threads should differ."
assert len(results) == number_of_threads, "Expected one result per thread."
assert len(set(results)) == number_of_threads, "Thread-local failed: each thread should get its own instance."


def test_thread_local_singleton_override() -> None:
Expand Down
2 changes: 1 addition & 1 deletion that_depends/providers/base.py
Original file line number Diff line number Diff line change
Expand Up @@ -158,7 +158,7 @@ def _deregister(self, candidates: typing.Iterable[typing.Any]) -> None:
self._invalidate_scope_init_order()

def _invalidate_scope_init_order(self) -> None:
stack = [self]
stack: list[AbstractProvider[typing.Any]] = [self]
visited: set[AbstractProvider[typing.Any]] = set()

while stack:
Expand Down
Loading