diff --git a/README.md b/README.md index 1a1ce16..2ec5d06 100644 --- a/README.md +++ b/README.md @@ -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. diff --git a/pyproject.toml b/pyproject.toml index 0cee046..53926cf 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -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", @@ -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 = [ diff --git a/tests/providers/test_local_singleton.py b/tests/providers/test_local_singleton.py index 4f92dda..af9384d 100644 --- a/tests/providers/test_local_singleton.py +++ b/tests/providers/test_local_singleton.py @@ -1,4 +1,5 @@ import asyncio +import itertools import random import threading import time @@ -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 @@ -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: diff --git a/that_depends/providers/base.py b/that_depends/providers/base.py index 7adc02d..31f5982 100644 --- a/that_depends/providers/base.py +++ b/that_depends/providers/base.py @@ -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: