Skip to content

Commit

Permalink
Prepare 0.27 (#116)
Browse files Browse the repository at this point in the history
* Rename `ignore_manual_init` to `init_hook`

* Rename `init_hook` to `use_init_hook`

* Bump version

* Fix docs

* Fix docs
  • Loading branch information
yakimka authored Oct 7, 2024
1 parent b25272b commit 9018767
Show file tree
Hide file tree
Showing 13 changed files with 52 additions and 103 deletions.
5 changes: 4 additions & 1 deletion CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -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

Expand Down
4 changes: 2 additions & 2 deletions docs/knownissues.rst
Original file line number Diff line number Diff line change
Expand Up @@ -7,15 +7,15 @@ 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.

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()`.

Expand Down
38 changes: 9 additions & 29 deletions docs/scopes.rst
Original file line number Diff line number Diff line change
Expand Up @@ -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.
Expand All @@ -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.
Expand Down Expand Up @@ -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
Expand Down Expand Up @@ -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"
Expand All @@ -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
***************************

Expand Down Expand Up @@ -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"
Expand Down
28 changes: 15 additions & 13 deletions picodi/_picodi.py
Original file line number Diff line number Diff line change
Expand Up @@ -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.
Expand All @@ -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:
Expand Down Expand Up @@ -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,
Expand All @@ -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.
"""

Expand All @@ -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

Expand Down Expand Up @@ -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
Expand All @@ -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)
Expand Down
2 changes: 1 addition & 1 deletion pyproject.toml
Original file line number Diff line number Diff line change
Expand Up @@ -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"
Expand Down
4 changes: 2 additions & 2 deletions tests/test_complex_scopes_behaviours.py
Original file line number Diff line number Diff line change
Expand Up @@ -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()
Expand Down Expand Up @@ -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

Expand Down
52 changes: 8 additions & 44 deletions tests/test_dependency_decorator.py
Original file line number Diff line number Diff line change
Expand Up @@ -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
4 changes: 2 additions & 2 deletions tests/test_integrations/test_fastapi_integration.py
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand All @@ -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
Expand Down
4 changes: 2 additions & 2 deletions tests/test_integrations/test_starlette_integration.py
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand All @@ -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
Expand Down
Loading

0 comments on commit 9018767

Please sign in to comment.