diff --git a/CHANGELOG.md b/CHANGELOG.md index be5e870..a65d307 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -4,8 +4,11 @@ We follow [Semantic Versions](https://semver.org/). ## Version next +## Version 0.27.0 + - Breaking changes: - - Argument `ignore_manual_init` of `@dependency` decorator by default is `True` now + - Argument `ignore_manual_init` of `@dependency` decorator renamed to `use_init_hook` + and its default value set to `False` ## Version 0.26.0 diff --git a/docs/knownissues.rst b/docs/knownissues.rst index 364ceef..c787735 100644 --- a/docs/knownissues.rst +++ b/docs/knownissues.rst @@ -7,7 +7,7 @@ Receiving a coroutine object instead of the actual value If you are trying to resolve async dependencies in sync functions, you will receive a coroutine object. For regular dependencies, this is intended behavior, so only use async dependencies in async functions. However, if your dependency uses a scope inherited from :class:`picodi.ManualScope`, -and used ``ignore_manual_init=False`` with :func:`dependency` decorator, +and used ``use_init_hook=True`` with :func:`dependency` decorator, you can use :func:`picodi.init_dependencies` on app startup to resolve dependencies, and then Picodi will use cached values, even in sync functions. @@ -15,7 +15,7 @@ Dependency not initialized with init_dependencies() ----------------------------------------------------- 1. Ensure that your dependency defined with scopes inherited from :class:`picodi.ManualScope`. -2. Ensure that your dependency is decorated with parameter ``ignore_manual_init=False`` of :func:`dependency` decorator +2. Ensure that your dependency is decorated with parameter ``use_init_hook=True`` of :func:`dependency` decorator 3. If you have async dependency, ensure that you are calling ``await init_dependencies()`` in an async context. 4. Ensure that modules with your dependencies are imported (e.g., registered) before calling :func:`picodi.init_dependencies()`. diff --git a/docs/scopes.rst b/docs/scopes.rst index 61833cb..4aa4c03 100644 --- a/docs/scopes.rst +++ b/docs/scopes.rst @@ -30,14 +30,15 @@ NullScope By default, all dependencies are created with the :class:`picodi.NullScope` scope. This means that a new instance is created every time the dependency is injected and closed immediately after root injection is done. + SingletonScope ************** The :class:`picodi.SingletonScope` scope creates a single instance of the dependency and reuses it every time the dependency is injected. The instance is created when the dependency is first injected or :func:`picodi.init_dependencies` is called. -In case of ```picodi.init_dependencies`` yor dependency need to be decorated with -parameter ``ignore_manual_init=False`` of :func:`dependency` decorator +In case of ``picodi.init_dependencies`` yor dependency need to be decorated with +parameter ``use_init_hook=True`` of :func:`picodi.dependency` decorator ``SingletonScope`` is the manual scope, so you need to call :func:`picodi.shutdown_dependencies` manually. Usually you want to call it when your application is shutting down. @@ -49,8 +50,8 @@ The :class:`picodi.ContextVarScope` uses the :class:`python:contextvars.ContextV to store the instance. The instance is created when the dependency is first injected or :func:`picodi.init_dependencies` with ``scope_class=ContextVarScope`` is called. -In case of ```picodi.init_dependencies`` yor dependency need to be decorated with -parameter ``ignore_manual_init=False`` of :func:`dependency` decorator +In case of ``picodi.init_dependencies`` yor dependency need to be decorated with +parameter ``use_init_hook=True`` of :func:`picodi.dependency` decorator ``ContextVarScope`` is the manual scope, so you need to call :func:`picodi.shutdown_dependencies` with ``scope_class=ContextVarScope`` manually. @@ -78,8 +79,8 @@ Lifecycle of manual scopes You can manually initialize your dependencies by calling :func:`picodi.init_dependencies` with the ``scope_class`` argument -(yor dependencies need to be decorated with parameter ``ignore_manual_init=False`` -of :func:`dependency` decorator). +(your dependencies need to be decorated with parameter ``use_init_hook=True`` +of :func:`picodi.dependency` decorator). Example: .. code-block:: python @@ -111,7 +112,7 @@ scope you can inject it in sync code. Example: from picodi import Provide, SingletonScope, dependency, init_dependencies, inject - @dependency(scope_class=SingletonScope, ignore_manual_init=False) + @dependency(scope_class=SingletonScope, use_init_hook=True) async def get_async_dependency(): return "from async" @@ -133,27 +134,6 @@ scope you can inject it in sync code. Example: Because ``get_async_dependency`` is ``SingletonScope`` scoped dependency and it's initialized on startup, while your app is running you can inject it in sync code. -Resolve dependencies on app startup -*********************************** - -If you want to automatically resolve dependencies on app startup -(or any other necessary time) you can set to ``True`` the -``ignore_manual_init`` argument of the :func:`picodi.dependency` decorator. - -.. testcode:: - - from picodi import SingletonScope, dependency, init_dependencies - - - # Try to set `ignore_manual_init` to `False` and see what happens - @dependency(scope_class=SingletonScope, ignore_manual_init=True) - def get_dependency(): - print("This will not be printed") - return "from async" - - - init_dependencies() # This will not initialize get_dependency - Managing scopes selectively *************************** @@ -206,7 +186,7 @@ It's convenient for using with workers or cli commands. from picodi.helpers import lifespan - @dependency(scope_class=SingletonScope, ignore_manual_init=False) + @dependency(scope_class=SingletonScope, use_init_hook=True) def get_singleton(): print("Creating singleton object") yield "singleton" diff --git a/picodi/_picodi.py b/picodi/_picodi.py index b6416e5..997f1b1 100644 --- a/picodi/_picodi.py +++ b/picodi/_picodi.py @@ -76,7 +76,7 @@ def add( dependency: DependencyCallable, scope_class: type[ScopeType] = NullScope, override_scope: bool = False, - ignore_manual_init: bool | Callable[[], bool] = False, + use_init_hook: bool | Callable[[], bool] = False, ) -> None: """ Add a dependency to the registry. @@ -93,7 +93,7 @@ def add( self._storage.deps[dependency] = Provider.from_dependency( dependency=dependency, scope_class=scope_class, - ignore_manual_init=ignore_manual_init, + use_init_hook=use_init_hook, ) def get(self, dependency: DependencyCallable) -> Provider: @@ -393,7 +393,7 @@ def gen_wrapper( def dependency( *, scope_class: type[ScopeType] = NullScope, - ignore_manual_init: bool | Callable[[], bool] = True, + use_init_hook: bool | Callable[[], bool] = False, ) -> Callable[[TC], TC]: """ Decorator to declare a dependency. You don't need to use it with default arguments, @@ -404,10 +404,11 @@ def dependency( :class:`NullScope`. Picodi additionally provides a few built-in scopes: :class:`SingletonScope`, :class:`ContextVarScope`. - :param ignore_manual_init: this parameter can be used to skip the dependency manual - initialization. It can be a boolean or a callable that returns a boolean. + :param use_init_hook: this parameter can be used to initialize dependency on + :func:`init_dependencies` call. + It can be a boolean or a callable that returns a boolean. If it's a callable, it will be called every time before the dependency is - initialized with :func:`init_dependencies`. If it returns True, the dependency + initialized with :func:`init_dependencies`. If it returns False, the dependency will be skipped. """ @@ -419,7 +420,7 @@ def decorator(fn: TC) -> TC: fn, scope_class=scope_class, override_scope=True, - ignore_manual_init=ignore_manual_init, + use_init_hook=use_init_hook, ) return fn @@ -502,14 +503,14 @@ class Provider: dependency: DependencyCallable is_async: bool scope_class: type[ScopeType] - ignore_manual_init: bool | Callable[[], bool] + use_init_hook: bool | Callable[[], bool] @classmethod def from_dependency( cls, dependency: DependencyCallable, scope_class: type[ScopeType], - ignore_manual_init: bool | Callable[[], bool] = False, + use_init_hook: bool | Callable[[], bool] = False, ) -> Provider: is_async = inspect.iscoroutinefunction( dependency @@ -518,13 +519,14 @@ def from_dependency( dependency=dependency, is_async=is_async, scope_class=scope_class, - ignore_manual_init=ignore_manual_init, + use_init_hook=use_init_hook, ) def is_ignored(self) -> bool: - if callable(self.ignore_manual_init): - return self.ignore_manual_init() - return self.ignore_manual_init + value = self.use_init_hook + if callable(value): + value = value() + return not value def replace(self, scope_class: type[ScopeType] | None = None) -> Provider: kwargs = asdict(self) diff --git a/pyproject.toml b/pyproject.toml index d1d1f63..cf03454 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -5,7 +5,7 @@ build-backend = "poetry.core.masonry.api" [tool.poetry] name = "picodi" description = "Simple Dependency Injection for Python" -version = "0.26.0" +version = "0.27.0" license = "MIT" authors = [ "yakimka" diff --git a/tests/test_complex_scopes_behaviours.py b/tests/test_complex_scopes_behaviours.py index d352c3f..0583600 100644 --- a/tests/test_complex_scopes_behaviours.py +++ b/tests/test_complex_scopes_behaviours.py @@ -11,7 +11,7 @@ async def test_transitive_dependency_injected_with_enter_closed_properly(closeable): # Arrange - @dependency(scope_class=SingletonScope, ignore_manual_init=True) + @dependency(scope_class=SingletonScope) async def get_dep_with_cleanup(): yield 42 closeable.close() @@ -76,7 +76,7 @@ async def service2(dep_with_cleanup: int = Provide(get_dep_with_cleanup)) -> int async def test_can_sync_enter_inited_async_singleton(): - @dependency(scope_class=SingletonScope, ignore_manual_init=False) + @dependency(scope_class=SingletonScope, use_init_hook=True) async def dep(): return 42 diff --git a/tests/test_dependency_decorator.py b/tests/test_dependency_decorator.py index 10722c8..3377b86 100644 --- a/tests/test_dependency_decorator.py +++ b/tests/test_dependency_decorator.py @@ -64,113 +64,77 @@ async def service(num: int = Provide(get_num)) -> int: assert result == 42 * 2 * 2 -def test_can_optionally_ignore_manual_initialization(): +def test_can_optionally_set_manual_initialization(): # Arrange inited = False - @dependency(scope_class=SingletonScope, ignore_manual_init=True) + @dependency(scope_class=SingletonScope, use_init_hook=True) def get_num(): nonlocal inited inited = True yield 42 - @inject - def service(num: int = Provide(get_num)) -> int: - return num - # Act init_dependencies() # Assert - assert inited is False - - assert service() == 42 assert inited is True -async def test_can_optionally_ignore_manual_initialization_async(): +async def test_can_optionally_set_manual_initialization_async(): # Arrange inited = False - @dependency(scope_class=SingletonScope, ignore_manual_init=True) + @dependency(scope_class=SingletonScope, use_init_hook=True) async def get_num(): nonlocal inited inited = True yield 42 - @inject - async def service(num: int = Provide(get_num)) -> int: - return num - # Act await init_dependencies() # Assert - assert inited is False - - assert await service() == 42 assert inited is True -def test_can_optionally_ignore_manual_initialization_with_callable(): +def test_can_optionally_set_manual_initialization_with_callable(): # Arrange inited = False - callable_called = False @inject def callable_func(flag: bool = Provide(lambda: True)): - nonlocal callable_called - callable_called = True return flag - @dependency(scope_class=SingletonScope, ignore_manual_init=callable_func) + @dependency(scope_class=SingletonScope, use_init_hook=callable_func) def get_num(): nonlocal inited inited = True yield 42 - @inject - def service(num: int = Provide(get_num)) -> int: - return num - # Act init_dependencies() # Assert - assert inited is False - assert callable_called is True - - assert service() == 42 assert inited is True -async def test_can_optionally_ignore_manual_initialization_with_callable_async(): +async def test_can_optionally_set_manual_initialization_with_callable_async(): # Arrange inited = False - callable_called = False @inject def callable_func(flag: bool = Provide(lambda: True)): - nonlocal callable_called - callable_called = True return flag - @dependency(scope_class=SingletonScope, ignore_manual_init=callable_func) + @dependency(scope_class=SingletonScope, use_init_hook=callable_func) async def get_num(): nonlocal inited inited = True yield 42 - @inject - async def service(num: int = Provide(get_num)) -> int: - return num - # Act await init_dependencies() # Assert - assert inited is False - assert callable_called is True - - assert await service() == 42 assert inited is True diff --git a/tests/test_integrations/test_fastapi_integration.py b/tests/test_integrations/test_fastapi_integration.py index 8879e66..73f89fa 100644 --- a/tests/test_integrations/test_fastapi_integration.py +++ b/tests/test_integrations/test_fastapi_integration.py @@ -142,7 +142,7 @@ async def test_middleware_init_and_shutdown_request_scope(app, asgi_client): init_counter = 0 closing_counter = 0 - @dependency(scope_class=RequestScope, ignore_manual_init=False) + @dependency(scope_class=RequestScope, use_init_hook=True) async def get_42(): nonlocal init_counter init_counter += 1 @@ -167,7 +167,7 @@ async def test_middleware_init_and_shutdown_request_scope_sync(app, asgi_client) init_counter = 0 closing_counter = 0 - @dependency(scope_class=RequestScope, ignore_manual_init=False) + @dependency(scope_class=RequestScope, use_init_hook=True) def get_42(): nonlocal init_counter init_counter += 1 diff --git a/tests/test_integrations/test_starlette_integration.py b/tests/test_integrations/test_starlette_integration.py index ce213b4..9ca7107 100644 --- a/tests/test_integrations/test_starlette_integration.py +++ b/tests/test_integrations/test_starlette_integration.py @@ -27,7 +27,7 @@ async def test_middleware_init_and_shutdown_request_scope(asgi_client): init_counter = 0 closing_counter = 0 - @dependency(scope_class=RequestScope, ignore_manual_init=False) + @dependency(scope_class=RequestScope, use_init_hook=True) async def get_42(): nonlocal init_counter init_counter += 1 @@ -45,7 +45,7 @@ async def test_middleware_init_and_shutdown_request_scope_sync(asgi_client): init_counter = 0 closing_counter = 0 - @dependency(scope_class=RequestScope, ignore_manual_init=False) + @dependency(scope_class=RequestScope, use_init_hook=True) def get_42(): nonlocal init_counter init_counter += 1 diff --git a/tests/test_lifespan.py b/tests/test_lifespan.py index 4a0641a..c8d1513 100644 --- a/tests/test_lifespan.py +++ b/tests/test_lifespan.py @@ -8,7 +8,7 @@ def resource(): state = {"inited": False, "closed": False} - @dependency(scope_class=SingletonScope, ignore_manual_init=False) + @dependency(scope_class=SingletonScope, use_init_hook=True) def my_resource(): state["inited"] = True yield state @@ -21,7 +21,7 @@ def my_resource(): def async_resource(): state = {"inited": False, "closed": False} - @dependency(scope_class=SingletonScope, ignore_manual_init=False) + @dependency(scope_class=SingletonScope, use_init_hook=True) async def my_resource(): state["inited"] = True yield state diff --git a/tests/test_override.py b/tests/test_override.py index 188704a..e4c2339 100644 --- a/tests/test_override.py +++ b/tests/test_override.py @@ -196,8 +196,8 @@ async def test_can_use_async_dep_with_not_default_scope_in_override_in_sync_cont def my_service(settings: dict = Provide(get_abc_settings)): return settings - @dependency(scope_class=SingletonScope) @registry.override(get_abc_settings) + @dependency(scope_class=SingletonScope, use_init_hook=True) async def real_settings(): return {"real": "settings"} diff --git a/tests/test_standard_dep_with_return.py b/tests/test_standard_dep_with_return.py index a6cdf43..ca90fb7 100644 --- a/tests/test_standard_dep_with_return.py +++ b/tests/test_standard_dep_with_return.py @@ -118,7 +118,7 @@ def _check_redis_string(redis_string): async def test_resolve_async_singleton_dependency_through_sync(): - @dependency(scope_class=SingletonScope, ignore_manual_init=False) + @dependency(scope_class=SingletonScope, use_init_hook=True) async def get_client(): return "my_client" diff --git a/tests/test_yield_dep.py b/tests/test_yield_dep.py index 8987f7e..84ff020 100644 --- a/tests/test_yield_dep.py +++ b/tests/test_yield_dep.py @@ -181,7 +181,7 @@ def get_async_dep(port: int = Provide(get_int_service_async)): async def test_resolve_async_yield_dep_from_sync_function_can_be_inited(): - @dependency(scope_class=SingletonScope, ignore_manual_init=False) + @dependency(scope_class=SingletonScope, use_init_hook=True) async def async_singleton_scope_dep(): int_service = IntService.create() yield int_service @@ -327,7 +327,7 @@ def test_can_init_injected_singleton_scope_dep(): def get_42(): return 42 - @dependency(scope_class=SingletonScope, ignore_manual_init=False) + @dependency(scope_class=SingletonScope, use_init_hook=True) @inject def my_singleton_scope_dep(number: int = Provide(get_42)): assert number == 42 @@ -346,7 +346,7 @@ async def test_can_init_injected_singleton_scope_dep_async(): def get_42(): return 42 - @dependency(scope_class=SingletonScope, ignore_manual_init=False) + @dependency(scope_class=SingletonScope, use_init_hook=True) @inject async def my_async_singleton_scope_dep(number: int = Provide(get_42)): assert number == 42