diff --git a/libs/container/pyproject.toml b/libs/container/pyproject.toml new file mode 100644 index 0000000..880f412 --- /dev/null +++ b/libs/container/pyproject.toml @@ -0,0 +1,14 @@ +[build-system] +requires = ["setuptools>=61"] +build-backend = "setuptools.build_meta" + +[project] +name = "container" +version = "0.1.0" +description = "Dependency injection container for the BookSync applications." +requires-python = ">=3.12" + +[tool.setuptools.packages.find] +where = ["src"] +include = ["container*"] +namespaces = true diff --git a/libs/container/src/container/__init__.py b/libs/container/src/container/__init__.py new file mode 100644 index 0000000..1f2ada4 --- /dev/null +++ b/libs/container/src/container/__init__.py @@ -0,0 +1,6 @@ +"""Public API for the dependency injection container.""" + +from .container import Container +from .container_interface import IContainer + +__all__ = ["Container", "IContainer"] diff --git a/libs/container/src/container/container.py b/libs/container/src/container/container.py new file mode 100644 index 0000000..80843b9 --- /dev/null +++ b/libs/container/src/container/container.py @@ -0,0 +1,95 @@ +# __ _____ _ __ ____________________ __ _____________________ _____ _______________ +# / ___/ / /_ ____ ____ / /_ ___ ____ / __ \ ___ _ __/ +# \__ \ / __ \ / __ \ / __ \ / __/ / _ \ / __/ ____ / / / / / _ \ | | / / +# ___/ / / / / / / /_/ / / /_/ / / /_ / __/ / / /___/ / /_/ / / __/ | |/ / +# /____/ /_/ /_/ \____/ \____/ \__/ \___/ /_/ _______ /______/ \___/ |___/ +import importlib +import inspect +from typing import Any, TypeVar, cast + +from .container_interface import IContainer + +T = TypeVar("T") + + +class Container(IContainer): + """Conteneur léger de services avec résolution par type.""" + + def __init__(self) -> None: + """Initialise des registres indépendants pour ce conteneur.""" + self.__instances: dict[str, Any] = { + "container": self, + self.get_type(Container): self, + } + self.__alias: dict[str, Any] = {} + self.__parameters: dict[str, Any] = {} + self.alias(IContainer, Container) + + @staticmethod + def get_type(name: type[Any] | str) -> str: + """Retourner l'identifiant stable d'un type ou d'une chaîne.""" + if isinstance(name, str): + return name + module = inspect.getmodule(name) + if module is None: + raise TypeError(f"Impossible de déterminer le module de {name!r}.") + return f"{module.__name__}.{name.__name__}" + + @staticmethod + def get_instance(type_: str, args: list[Any]) -> object: + """Construire dynamiquement une instance à partir de son identifiant.""" + module = importlib.import_module(type_[0 : type_.rfind(".")]) + return getattr(module, type_[type_.rfind(".") + 1 :])(*args) + + def set(self, name: type[Any], obj: object) -> None: + """Enregistrer une instance pour un type.""" + self.__instances[Container.get_type(name)] = obj + + def __getitem__(self, name: type[T]) -> T: + """Résoudre un service avec la syntaxe d'indexation historique du sandbox.""" + return cast("T", self.get(name)) + + def __setitem__(self, name: type[T], obj: T) -> None: + """Enregistrer un service avec la syntaxe d'indexation historique du sandbox.""" + self.set(name, obj) + + def __contains__(self, name: type[Any] | str) -> bool: + """Indiquer si un service a déjà été enregistré dans le conteneur.""" + return self.get_type(name) in self.__instances + + def get(self, name: type[Any] | str) -> Any: + """Résoudre ou construire un service par son type ou son identifiant.""" + if name in self.__parameters: + return self.__parameters[name] + + service_id: str = self.get_type(name) + + if service_id in self.__alias: + return self.get(self.__alias[Container.get_type(name)]) + + if service_id not in self.__instances: + if isinstance(name, str): + raise KeyError(f"Service introuvable : {name}.") + args: list[Any] = [] + if len(inspect.signature(name.__init__).parameters) > 1: + for arg, parameter in inspect.signature(name.__init__).parameters.items(): + if arg in ["self", "args", "kwargs"]: + continue + if parameter.annotation != inspect.Parameter.empty: + try: + args.append(self.get(parameter.annotation)) + except (AttributeError, KeyError, TypeError): + args.append(self.get(arg)) + else: + args.append(self.get(arg)) + self.__instances[service_id] = Container.get_instance(service_id, args) + + return self.__instances[service_id] + + def alias(self, alias: type[Any] | str, name: type[Any] | str) -> None: + """Associer un identifiant à un autre service.""" + self.__alias[Container.get_type(alias)] = name + + def set_parameter(self, name: str, value: Any) -> None: + """Enregistrer un paramètre nommé.""" + self.__parameters[name] = value diff --git a/libs/container/src/container/container_interface.py b/libs/container/src/container/container_interface.py new file mode 100644 index 0000000..e1383d2 --- /dev/null +++ b/libs/container/src/container/container_interface.py @@ -0,0 +1,26 @@ +# __ _____ _ __ ____________________ __ _____________________ _____ _______________ +# / ___/ / /_ ____ ____ / /_ ___ ____ / __ \ ___ _ __/ +# \__ \ / __ \ / __ \ / __ \ / __/ / _ \ / __/ ____ / / / / / _ \ | | / / +# ___/ / / / / / / /_/ / / /_/ / / /_ / __/ / / /___/ / /_/ / / __/ | |/ / +# /____/ /_/ /_/ \____/ \____/ \__/ \___/ /_/ _______ /______/ \___/ |___/ +from typing import Any, Protocol + + +class IContainer(Protocol): + """Définir le port partagé de résolution des dépendances.""" + + def get(self, name: type[Any] | str) -> Any: + """Résoudre un service par son type ou son identifiant.""" + ... + + def set(self, name: type[Any], obj: object) -> None: + """Enregistrer une instance pour un type.""" + ... + + def alias(self, alias: type[Any] | str, name: type[Any] | str) -> None: + """Associer un identifiant à un autre service.""" + ... + + def set_parameter(self, name: str, value: Any) -> None: + """Enregistrer un paramètre nommé.""" + ... diff --git a/libs/container/tests/test_public_api.py b/libs/container/tests/test_public_api.py new file mode 100644 index 0000000..c02210f --- /dev/null +++ b/libs/container/tests/test_public_api.py @@ -0,0 +1,9 @@ +import unittest + + +class ContainerPublicApiTests(unittest.TestCase): + def test_public_types_are_importable(self) -> None: + from container import Container, IContainer + + self.assertEqual(Container.__name__, "Container") + self.assertEqual(IContainer.__name__, "IContainer")