Skip to content

Commit

Permalink
Revert tags (#80)
Browse files Browse the repository at this point in the history
  • Loading branch information
yakimka authored Jul 3, 2024
1 parent 4e60851 commit a3a3caa
Show file tree
Hide file tree
Showing 7 changed files with 163 additions and 163 deletions.
4 changes: 4 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,10 @@ We follow [Semantic Versions](https://semver.org/).

## Version next

## Version 0.24.0

- revert tags

## Version 0.23.0

- Now you can mark you dependencies with tags for more granular control over their lifecycle
Expand Down
19 changes: 10 additions & 9 deletions docs/scopes.rst
Original file line number Diff line number Diff line change
Expand Up @@ -127,24 +127,25 @@ 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.

Initializing dependencies by tags
*********************************
Skipping manual initialization
******************************

You can tag your dependencies to initialize them selectively.
If you want to skip manual initialization of your dependency you can use the
``ignore_manual_init`` argument of the :func:`picodi.dependency` decorator.

.. testcode::

from picodi import SingletonScope, dependency, init_dependencies


@dependency(scope_class=SingletonScope, tags=["tag1"])
def get_tagged_dependency():
return "tagged dependency"
# 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(tags=["tag1"])
# or you can exclude dependencies by tags
init_dependencies(tags=["-tag1"])
init_dependencies() # This will not initialize get_dependency

Managing scopes selectively
***************************
Expand Down
50 changes: 19 additions & 31 deletions picodi/_picodi.py
Original file line number Diff line number Diff line change
Expand Up @@ -48,7 +48,6 @@

DependencyCallable = Callable[..., Any]
LifespanScopeClass = type[ManualScope] | tuple[type[ManualScope], ...]
Tags = list[str] | tuple[str, ...]
T = TypeVar("T")
P = ParamSpec("P")
TC = TypeVar("TC", bound=Callable)
Expand All @@ -75,7 +74,7 @@ def add(
dependency: DependencyCallable,
scope_class: type[ScopeType] = NullScope,
override_scope: bool = False,
tags: Tags = (),
ignore_manual_init: bool | Callable[[], bool] = False,
) -> None:
"""
Add a dependency to the registry.
Expand All @@ -92,7 +91,7 @@ def add(
self._storage.deps[dependency] = Provider.from_dependency(
dependency=dependency,
scope_class=scope_class,
tags=tags,
ignore_manual_init=ignore_manual_init,
)

def get(self, dependency: DependencyCallable) -> Provider:
Expand Down Expand Up @@ -356,7 +355,7 @@ def gen_wrapper(
def dependency(
*,
scope_class: type[ScopeType] = NullScope,
tags: Tags = (),
ignore_manual_init: bool | Callable[[], bool] = False,
) -> Callable[[TC], TC]:
"""
Decorator to declare a dependency. You don't need to use it with default arguments,
Expand All @@ -367,9 +366,11 @@ def dependency(
:class:`NullScope`.
Picodi additionally provides a few built-in scopes:
:class:`SingletonScope`, :class:`ContextVarScope`.
:param tags: list of tags to assign to the dependency. Tags can be used to select
which dependencies to initialize. They can be used to group
dependencies and control their lifecycle.
: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.
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
will be skipped.
"""

if scope_class not in _scopes:
Expand All @@ -380,17 +381,14 @@ def decorator(fn: TC) -> TC:
fn,
scope_class=scope_class,
override_scope=True,
tags=tags,
ignore_manual_init=ignore_manual_init,
)
return fn

return decorator


def init_dependencies(
scope_class: LifespanScopeClass = SingletonScope,
tags: Tags = (),
) -> Awaitable:
def init_dependencies(scope_class: LifespanScopeClass = SingletonScope) -> Awaitable:
"""
Call this function to close dependencies. Usually, it should be called
when your application is starting up.
Expand All @@ -403,18 +401,13 @@ def init_dependencies(
If you not pass any arguments, it will initialize only :class:`SingletonScope`
and its subclasses.
If you pass tags with scope_class ``and`` logic will be applied.
:param scope_class: you can specify the scope class to initialize. If passed -
only dependencies of this scope class and its subclasses will be initialized.
:param tags: you can specify the dependencies to initialize by tags. If passed -
only dependencies of this tags will be initialized. If you pass a tag with a
minus sign, it will exclude dependencies with this tag.
"""
async_deps = []
filtered_providers = _internal_registry.filter(
lambda p: p.match_tags(tags) and issubclass(p.scope_class, scope_class)
lambda p: not p.is_ignored() and issubclass(p.scope_class, scope_class)
)
async_deps = []
for provider in filtered_providers:
resolver = LazyResolver(provider)
value = resolver(provider.is_async)
Expand Down Expand Up @@ -471,34 +464,29 @@ class Provider:
dependency: DependencyCallable
is_async: bool
scope_class: type[ScopeType]
tags: set[str]
ignore_manual_init: bool | Callable[[], bool]

@classmethod
def from_dependency(
cls,
dependency: DependencyCallable,
scope_class: type[ScopeType],
tags: Tags,
ignore_manual_init: bool | Callable[[], bool] = False,
) -> Provider:
is_async = inspect.iscoroutinefunction(
dependency
) or inspect.isasyncgenfunction(dependency)

return cls(
dependency=dependency,
is_async=is_async,
scope_class=scope_class,
tags=set(tags),
ignore_manual_init=ignore_manual_init,
)

def match_tags(self, tags: Tags) -> bool:
include_tags = {tag for tag in tags if not tag.startswith("-")}
exclude_tags = {tag[1:] for tag in tags if tag.startswith("-")}

if exclude_tags.intersection(self.tags):
return False

return bool(not include_tags or include_tags.intersection(self.tags))
def is_ignored(self) -> bool:
if callable(self.ignore_manual_init):
return self.ignore_manual_init()
return self.ignore_manual_init

def replace(self, scope_class: type[ScopeType] | None = None) -> Provider:
kwargs = asdict(self)
Expand Down
15 changes: 3 additions & 12 deletions picodi/helpers.py
Original file line number Diff line number Diff line change
Expand Up @@ -19,7 +19,7 @@

import picodi
from picodi import ManualScope, SingletonScope
from picodi._picodi import LifespanScopeClass, Tags, _internal_registry
from picodi._picodi import LifespanScopeClass, _internal_registry
from picodi.support import nullcontext

if TYPE_CHECKING:
Expand Down Expand Up @@ -147,7 +147,6 @@ def __call__(
fn: None = None,
*,
init_scope_class: LifespanScopeClass | None = SingletonScope,
init_tags: Tags = (),
shutdown_scope_class: LifespanScopeClass | None = ManualScope,
) -> Callable[[Callable[P, T]], Callable[P, T]]:
"""Sync and Async context manager"""
Expand All @@ -157,7 +156,6 @@ def __call__(
fn: Callable[P, T] | None = None,
*,
init_scope_class: LifespanScopeClass | None = SingletonScope,
init_tags: Tags = (),
shutdown_scope_class: LifespanScopeClass | None = ManualScope,
) -> Callable[P, T] | Callable[[Callable[P, T]], Callable[P, T]]:
"""
Expand All @@ -166,7 +164,6 @@ def __call__(
:param fn: function to decorate (if used as a decorator).
:param init_scope_class: scope class for initialization
(can be omitted by passing None).
:param init_tags: list of tags to be initialized.
:param shutdown_scope_class: scope class for shutdown
(can be omitted by passing None).
:return: decorated function or decorator.
Expand All @@ -176,12 +173,10 @@ def decorator(fn: Callable[P, T]) -> Callable[P, T]:
if asyncio.iscoroutinefunction(fn):
return self.async_( # type: ignore[return-value]
init_scope_class=init_scope_class,
init_tags=init_tags,
shutdown_scope_class=shutdown_scope_class,
)(fn)
return self.sync(
init_scope_class=init_scope_class,
init_tags=init_tags,
shutdown_scope_class=shutdown_scope_class,
)(fn)

Expand All @@ -192,7 +187,6 @@ def sync(
self,
*,
init_scope_class: LifespanScopeClass | None = SingletonScope,
init_tags: Tags = (),
shutdown_scope_class: LifespanScopeClass | None = ManualScope,
) -> Generator[None, None, None]:
"""
Expand All @@ -201,12 +195,11 @@ def sync(
:param init_scope_class: scope class for initialization
(can be omitted by passing None).
:param init_tags: list of tags to be initialized.
:param shutdown_scope_class: scope class for shutdown
(can be omitted by passing None).
"""
if init_scope_class is not None:
picodi.init_dependencies(init_scope_class, tags=init_tags)
picodi.init_dependencies(init_scope_class)
try:
yield
finally:
Expand All @@ -218,7 +211,6 @@ async def async_(
self,
*,
init_scope_class: LifespanScopeClass | None = SingletonScope,
init_tags: Tags = (),
shutdown_scope_class: LifespanScopeClass | None = ManualScope,
) -> AsyncGenerator[None, None]:
"""
Expand All @@ -228,12 +220,11 @@ async def async_(
:param init_scope_class: scope class for initialization
(can be omitted by passing None).
:param init_tags: list of tags to be initialized.
:param shutdown_scope_class: scope class for shutdown
(can be omitted by passing None).
"""
if init_scope_class is not None:
await picodi.init_dependencies(init_scope_class, tags=init_tags)
await picodi.init_dependencies(init_scope_class)
try:
yield
finally:
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.23.0"
version = "0.24.0"
license = "MIT"
authors = [
"yakimka"
Expand Down
Loading

0 comments on commit a3a3caa

Please sign in to comment.