Thanks to visit codestin.com
Credit goes to github.com

Skip to content
Open
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
Empty file.
Original file line number Diff line number Diff line change
@@ -0,0 +1,115 @@
"""
In this example you'll learn different ways to select which injectables are
eligible to satisfy a dependency at injection time.

We will use the following selection mechanisms:

1. Groups on injectables: we assign a `group` label when declaring an
`@injectable` so that it can be included or excluded when autowiring.
2. Excluding groups when injecting: we use the `exclude_groups` parameter on
`Autowired` to filter out unwanted injectables.
3. Including groups when loading: we call
:meth:`load_injection_container <injectable.load_injection_container>` with the
`groups` argument so only injectables declared with those groups (plus those
without any group) are loaded for resolution across the whole application.

We declare an abstract base class `Processor` and three implementations:
`DefaultProcessor` (no group), `NewProcessor` (group "new"), and `OldProcessor`
(group "old"). In the example we:

- Load the injection container including only the "new" group globally.
- Inject a single `Processor` by selecting the "new" group explicitly.
- Inject all `Processor` implementations without any per-injection filter to show
the effect of the global "include groups" selection ("old" is filtered out).
- Inject all `Processor` implementations while excluding the "old" group to show
how per-injection filtering works as well.

.. note::

When loading with `groups=[...]`, injectables with `group=None` are still
eligible for injection. The global list works as an allowlist for labeled
groups but keeps unlabeled injectables available.
"""

# sphinx-start
from typing import Annotated, List
from abc import ABC, abstractmethod

from examples import Example
from injectable import injectable, autowired, Autowired, load_injection_container


class Processor(ABC):
@abstractmethod
def process(self, value: int) -> int: ...


@injectable # no group -> always eligible
class DefaultProcessor(Processor):
def process(self, value: int) -> int:
print("DefaultProcessor processing")
return value + 1


@injectable(group="new")
class NewProcessor(Processor):
def process(self, value: int) -> int:
print("NewProcessor processing")
return value * 2


@injectable(group="old")
class OldProcessor(Processor):
def process(self, value: int) -> int:
print("OldProcessor processing")
return value - 1

@injectable
class UnrelatedClass:
def run(self):
print("UnrelatedClass running")


class SelectingDependencies(Example):
@autowired
def __init__(
self,
# Explicitly pick the "new" group for a single instance
preferred: Annotated[Processor, Autowired(group="new")],
# Get all processors; global groups will filter out "old"
all_allowed: Annotated[List[Processor], Autowired],
# Per-injection filtering using exclude_groups
all_but_old: Annotated[List[Processor], Autowired(exclude_groups=["old"])],
# Unrelated injectable, not affected by the selection
unrelated: Annotated[UnrelatedClass, Autowired],
):
self.preferred = preferred
self.all_allowed = all_allowed
self.all_but_old = all_but_old
self.unrelated = unrelated

def run(self):
print(self.preferred.process(3))
# NewProcessor processing
# 6

print([type(p).__name__ for p in self.all_allowed])
# ['DefaultProcessor', 'NewProcessor']

print([type(p).__name__ for p in self.all_but_old])
# ['DefaultProcessor', 'NewProcessor']

self.unrelated.run()
# UnrelatedClass running


def run_example():
# Only injectables with group "new" (and with no group) will be considered
# across the whole application. This demonstrates the global selection toggle.
load_injection_container(groups=["new"])
example = SelectingDependencies()
example.run()


if __name__ == "__main__":
run_example()
10 changes: 8 additions & 2 deletions injectable/container/injection_container.py
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
import os
import warnings
from runpy import run_path, run_module
from typing import Dict, Optional, Callable
from typing import Dict, List, Optional, Callable
from typing import Set

from pycollect import PythonFileCollector, module_finder
Expand Down Expand Up @@ -36,6 +36,7 @@ class InjectionContainer:
LOADING_FILEPATH: Optional[str] = None
LOADED_FILEPATHS: Set[str] = set()
NAMESPACES: Dict[str, Namespace] = {}
GROUPS: Optional[List[str]] = None

def __new__(cls):
raise NotImplementedError("InjectionContainer must not be instantiated")
Expand Down Expand Up @@ -156,10 +157,15 @@ def _link_dependencies(cls, search_path: str):

@classmethod
def load_dependencies_from(
cls, absolute_search_path: str, default_namespace: str, encoding: str = "utf-8"
cls,
absolute_search_path: str,
default_namespace: str,
groups: Optional[list[str]] = None,
encoding: str = "utf-8",
):
files = cls._collect_python_files(absolute_search_path)
cls.LOADING_DEFAULT_NAMESPACE = default_namespace
cls.GROUPS = groups
if default_namespace not in cls.NAMESPACES:
cls.NAMESPACES[default_namespace] = Namespace()
for file in files:
Expand Down
7 changes: 6 additions & 1 deletion injectable/container/load_injection_container.py
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@ def load_injection_container(
search_path: str = None,
*,
default_namespace: str = DEFAULT_NAMESPACE,
groups: list[str] = None,
encoding: str = "utf-8",
):
"""
Expand All @@ -22,6 +23,8 @@ def load_injection_container(
injectables which does not explicitly request to be addressed in a
specific namespace. Defaults to
:const:`injectable.constants.DEFAULT_NAMESPACE`.
:param groups: (optional) list of groups to filter injectables.
Defaults to None.
:param encoding: (optional) defines which encoding to use when reading project files
to discover and register injectables. Defaults to ``utf-8``.

Expand All @@ -44,4 +47,6 @@ def load_injection_container(
elif not os.path.isabs(search_path):
caller_path = os.path.dirname(get_caller_filepath())
search_path = os.path.abspath(os.path.join(caller_path, search_path))
InjectionContainer.load_dependencies_from(search_path, default_namespace, encoding)
InjectionContainer.load_dependencies_from(
search_path, default_namespace, groups, encoding
)
30 changes: 14 additions & 16 deletions injectable/injection/inject.py
Original file line number Diff line number Diff line change
Expand Up @@ -81,12 +81,11 @@ def inject(
if not optional:
raise InjectionError(registry_type.value, dependency_name)
return None
if group is not None or exclude_groups is not None:
matches = filter_by_group(matches, group, exclude_groups)
if not matches:
if not optional:
raise InjectionError(registry_type.value, dependency_name)
return None
matches = filter_by_group(matches, group, exclude_groups)
if not matches:
if not optional:
raise InjectionError(registry_type.value, dependency_name)
return None
injectable = resolve_single_injectable(dependency_name, registry_type, matches)
return injectable.get_instance(lazy=lazy)

Expand Down Expand Up @@ -154,14 +153,13 @@ def inject_multiple(
if not optional:
raise InjectionError(registry_type.value, dependency_name)
return []
if group is not None or exclude_groups is not None:
matches = filter_by_group(
matches,
group,
exclude_groups,
)
if not matches:
if not optional:
raise InjectionError(registry_type.value, dependency_name)
return []
matches = filter_by_group(
matches,
group,
exclude_groups,
)
if not matches:
if not optional:
raise InjectionError(registry_type.value, dependency_name)
return []
return [inj.get_instance(lazy=lazy) for inj in matches]
24 changes: 22 additions & 2 deletions injectable/injection/injection_utils.py
Original file line number Diff line number Diff line change
Expand Up @@ -43,13 +43,33 @@ def filter_by_group(
group: str = None,
exclude_groups: Sequence[str] = None,
) -> Set[Injectable]:
matches = _filter_by_container_groups(matches)
matches = _filter_by_group_and_exclude(matches, group, exclude_groups)

return matches


def _filter_by_container_groups(matches: Set[Injectable]) -> Set[Injectable]:
container_groups = InjectionContainer.GROUPS or []
if not container_groups or not any(
[i for i in matches if i.group in container_groups]
):
return matches

container_matches = {
inj for inj in matches if inj.group is None or inj.group in container_groups
}

return container_matches


def _filter_by_group_and_exclude(matches, group, exclude_groups):
exclude = exclude_groups or []
matches = {
return {
inj
for inj in matches
if (group is None or inj.group == group) and inj.group not in exclude
}
return matches


def resolve_single_injectable(
Expand Down
31 changes: 31 additions & 0 deletions tests/unit/container/injection_container_unit_test.py
Original file line number Diff line number Diff line change
Expand Up @@ -383,6 +383,37 @@ def test__load_dependencies_from__with_specific_encoding(
assert run_module.call_count == 2
assert run_path.call_count == 0

def test__load_dependencies_from__with_groups(
self, patch_injection_container, patch_open
):
# given
root = "/" if os.name != "nt" else "C:\\"
search_path = os.path.join(root, "fake", "path")
namespace = DEFAULT_NAMESPACE
file_collector = MagicMock()
file_collector.collect.return_value = {
MagicMock(spec=os.DirEntry),
MagicMock(spec=os.DirEntry),
}
patch_injection_container(
"PythonFileCollector",
return_value=file_collector,
)
patch_open(
read_data="from injectable import injectable\n@injectable\nclass A: ..."
)
patch_injection_container("module_finder")
patch_injection_container("run_module")
patch_injection_container("run_path")

# when
InjectionContainer.load_dependencies_from(
search_path, namespace, groups=["group1", "group2"]
)

# then
assert len(InjectionContainer.GROUPS) == 2

def test__register_injectable__with_defaults(self, patch_injection_container):
# given
klass = TestInjectionContainer
Expand Down
16 changes: 16 additions & 0 deletions tests/unit/container/load_injection_container_unit_test.py
Original file line number Diff line number Diff line change
Expand Up @@ -93,3 +93,19 @@ def test__load_injection_container__with_explicit_namespace(
assert load.called is True
default_namespace_arg = load.call_args[0][1]
assert default_namespace_arg == default_namespace

def test__load_injection_container__with_explicit_groups(
self, get_caller_filepath_mock, injection_container_mock
):
# given
get_caller_filepath_mock.return_value = os.path.join("fake", "path", "file.py")
group_name = "group_name"

# when
load_injection_container(groups=[group_name])

# then
load = injection_container_mock.load_dependencies_from
assert load.called is True
group_name_arg = load.call_args[0][2]
assert group_name_arg == [group_name]
4 changes: 2 additions & 2 deletions tests/unit/injection/inject_unit_test.py
Original file line number Diff line number Diff line change
Expand Up @@ -54,6 +54,7 @@ def test__inject__with_default_values(
registry_type = RegistryType.CLASS
get_dependency_registry_type_mock.return_value = registry_type
get_namespace_injectables_mock.return_value = matches
filter_by_group_mock.return_value = matches
resolve_single_injectable_mock.return_value = injectable
dependency = "TEST"

Expand All @@ -70,7 +71,6 @@ def test__inject__with_default_values(
assert dependency_name_arg is dependency_name
assert registry_type_arg is registry_type
assert namespace_arg is DEFAULT_NAMESPACE
assert filter_by_group_mock.called is False
assert resolve_single_injectable_mock.called is True
(
dependency_name_arg,
Expand Down Expand Up @@ -272,6 +272,7 @@ def test__inject_multiple__with_default_values(
registry_type = RegistryType.CLASS
get_dependency_registry_type_mock.return_value = registry_type
get_namespace_injectables_mock.return_value = matches
filter_by_group_mock.return_value = matches
dependency = "TEST"

# when
Expand All @@ -287,7 +288,6 @@ def test__inject_multiple__with_default_values(
assert dependency_name_arg is dependency
assert registry_type_arg is registry_type
assert namespace_arg is DEFAULT_NAMESPACE
assert filter_by_group_mock.called is False
assert all(injectable.get_instance.called is True for injectable in injectables)
assert all(
injectable.get_instance.call_args[1]["lazy"] is False
Expand Down
Loading