From 56a16cbeba0e8159850a05c46936ea73c83285f1 Mon Sep 17 00:00:00 2001 From: edraft Date: Fri, 26 Sep 2025 08:46:30 +0200 Subject: [PATCH] Module dependencies as static var --- example/api/src/main.py | 9 ++- src/cpl-api/cpl/api/api_module.py | 11 ++- src/cpl-auth/cpl/auth/auth_module.py | 8 +-- .../cpl/auth/permission/permission_module.py | 9 +-- src/cpl-core/cpl/core/errors.py | 2 +- .../cpl/database/database_module.py | 13 ++-- .../cpl/database/mysql/mysql_module.py | 6 +- .../cpl/database/postgres/postgres_module.py | 7 +- src/cpl-dependency/cpl/dependency/module.py | 20 ++++-- .../cpl/dependency/service_collection.py | 72 ++++++++++++++----- src/cpl-dependency/cpl/dependency/typing.py | 9 +++ src/cpl-mail/cpl/mail/__init__.py | 17 +---- src/cpl-mail/cpl/mail/mail_module.py | 6 +- .../cpl/translation/__init__.py | 18 ----- .../cpl/translation/translation_module.py | 8 +-- 15 files changed, 112 insertions(+), 103 deletions(-) create mode 100644 src/cpl-dependency/cpl/dependency/typing.py diff --git a/example/api/src/main.py b/example/api/src/main.py index b5593953..94ba5901 100644 --- a/example/api/src/main.py +++ b/example/api/src/main.py @@ -3,6 +3,7 @@ from starlette.responses import JSONResponse from cpl.api.api_module import ApiModule from cpl.api.application.web_app import WebApp from cpl.application.application_builder import ApplicationBuilder +from cpl.auth import AuthModule from cpl.auth.permission.permissions import Permissions from cpl.auth.schema import AuthUser, Role from cpl.core.configuration import Configuration @@ -39,7 +40,13 @@ def main(): app.with_authentication() app.with_authorization() - app.with_route(path="/route1", fn=lambda r: JSONResponse("route1"), method="GET", authentication=True, permissions=[Permissions.administrator]) + app.with_route( + path="/route1", + fn=lambda r: JSONResponse("route1"), + method="GET", + authentication=True, + permissions=[Permissions.administrator], + ) app.with_routes_directory("routes") provider = builder.service_provider diff --git a/src/cpl-api/cpl/api/api_module.py b/src/cpl-api/cpl/api/api_module.py index 95f41ba6..5c33410b 100644 --- a/src/cpl-api/cpl/api/api_module.py +++ b/src/cpl-api/cpl/api/api_module.py @@ -1,22 +1,19 @@ from cpl.auth.auth_module import AuthModule from cpl.auth.permission.permission_module import PermissionsModule from cpl.database.database_module import DatabaseModule -from cpl.dependency.module import Module, TModule +from cpl.dependency.module import Module +from cpl.dependency.service_collection import ServiceCollection class ApiModule(Module): + dependencies = [] @staticmethod - def dependencies() -> list[TModule]: - return [AuthModule, DatabaseModule, PermissionsModule] - - @staticmethod - def register(collection: "ServiceCollection"): + def register(collection: ServiceCollection): from cpl.api.registry.policy import PolicyRegistry from cpl.api.registry.route import RouteRegistry collection.add_module(DatabaseModule) - collection.add_module(AuthModule) collection.add_module(PermissionsModule) diff --git a/src/cpl-auth/cpl/auth/auth_module.py b/src/cpl-auth/cpl/auth/auth_module.py index 43971df4..e07d8c3d 100644 --- a/src/cpl-auth/cpl/auth/auth_module.py +++ b/src/cpl-auth/cpl/auth/auth_module.py @@ -2,15 +2,15 @@ import os from cpl.database.database_module import DatabaseModule from cpl.database.model.server_type import ServerType, ServerTypes +from cpl.database.mysql.mysql_module import MySQLModule +from cpl.database.postgres.postgres_module import PostgresModule from cpl.database.service.migration_service import MigrationService -from cpl.dependency.module import Module, TModule +from cpl.dependency.module import Module from cpl.dependency.service_collection import ServiceCollection class AuthModule(Module): - @staticmethod - def dependencies() -> list[TModule]: - return [DatabaseModule] + dependencies = [DatabaseModule, (MySQLModule, PostgresModule)] @staticmethod def register(collection: ServiceCollection): diff --git a/src/cpl-auth/cpl/auth/permission/permission_module.py b/src/cpl-auth/cpl/auth/permission/permission_module.py index f70aa7e0..a6866120 100644 --- a/src/cpl-auth/cpl/auth/permission/permission_module.py +++ b/src/cpl-auth/cpl/auth/permission/permission_module.py @@ -3,16 +3,13 @@ from cpl.auth.permission.permission_seeder import PermissionSeeder from cpl.auth.permission.permissions import Permissions from cpl.auth.permission.permissions_registry import PermissionsRegistry from cpl.database.abc.data_seeder_abc import DataSeederABC -from cpl.dependency.module import Module, TModule +from cpl.database.database_module import DatabaseModule +from cpl.dependency.module import Module from cpl.dependency.service_collection import ServiceCollection class PermissionsModule(Module): - @staticmethod - def dependencies() -> list[TModule]: - from cpl.database.database_module import DatabaseModule - - return [DatabaseModule, AuthModule] + dependencies = [DatabaseModule, AuthModule] @staticmethod def register(collection: ServiceCollection): diff --git a/src/cpl-core/cpl/core/errors.py b/src/cpl-core/cpl/core/errors.py index 9b12f36b..2fb0e199 100644 --- a/src/cpl-core/cpl/core/errors.py +++ b/src/cpl-core/cpl/core/errors.py @@ -16,7 +16,7 @@ def dependency_error(src: str, package_name: str, e: ImportError = None) -> None def module_dependency_error(src: str, module: str, e: ImportError = None) -> None: - Console.error(f"'{module}' is required to use feature: {src}. Please initialize it with `add_module({module})`.") + Console.error(f"'{module}' is required by '{src}'. Please initialize it with `add_module({module})`.") tb = traceback.format_exc() if not tb.startswith("NoneType: None"): Console.write_line("->", tb) diff --git a/src/cpl-database/cpl/database/database_module.py b/src/cpl-database/cpl/database/database_module.py index 75c28f41..01b23497 100644 --- a/src/cpl-database/cpl/database/database_module.py +++ b/src/cpl-database/cpl/database/database_module.py @@ -1,16 +1,11 @@ -from cpl.core.errors import module_dependency_error -from cpl.database.model.server_type import ServerType -from cpl.dependency.module import Module, TModule +from cpl.database.mysql.mysql_module import MySQLModule +from cpl.database.postgres.postgres_module import PostgresModule +from cpl.dependency.module import Module from cpl.dependency.service_collection import ServiceCollection class DatabaseModule(Module): - @staticmethod - def dependencies() -> list[TModule]: - if not ServerType.has_server_type: - module_dependency_error(__name__, "MySQLModule or PostgresModule") - - return [] + dependencies = [(MySQLModule, PostgresModule)] @staticmethod def register(collection: ServiceCollection): diff --git a/src/cpl-database/cpl/database/mysql/mysql_module.py b/src/cpl-database/cpl/database/mysql/mysql_module.py index df4e9841..3aff86c4 100644 --- a/src/cpl-database/cpl/database/mysql/mysql_module.py +++ b/src/cpl-database/cpl/database/mysql/mysql_module.py @@ -1,13 +1,11 @@ from cpl.core.configuration.configuration import Configuration from cpl.database.model.server_type import ServerTypes, ServerType -from cpl.dependency.module import Module, TModule +from cpl.dependency.module import Module from cpl.dependency.service_collection import ServiceCollection class MySQLModule(Module): - @staticmethod - def dependencies() -> list[TModule]: - return [] + dependencies = [] @staticmethod def register(collection: ServiceCollection): diff --git a/src/cpl-database/cpl/database/postgres/postgres_module.py b/src/cpl-database/cpl/database/postgres/postgres_module.py index 59a51d30..679a2b00 100644 --- a/src/cpl-database/cpl/database/postgres/postgres_module.py +++ b/src/cpl-database/cpl/database/postgres/postgres_module.py @@ -1,14 +1,11 @@ from cpl.core.configuration.configuration import Configuration -from cpl.database.database_module import DatabaseModule from cpl.database.model.server_type import ServerTypes, ServerType -from cpl.dependency.module import Module, TModule +from cpl.dependency.module import Module from cpl.dependency.service_collection import ServiceCollection class PostgresModule(Module): - @staticmethod - def dependencies() -> list[TModule]: - return [DatabaseModule] + dependencies = [] @staticmethod def register(collection: ServiceCollection): diff --git a/src/cpl-dependency/cpl/dependency/module.py b/src/cpl-dependency/cpl/dependency/module.py index 66d306c0..e6376638 100644 --- a/src/cpl-dependency/cpl/dependency/module.py +++ b/src/cpl-dependency/cpl/dependency/module.py @@ -1,14 +1,22 @@ from abc import abstractmethod, ABC -from typing import Type - -TModule = Type["Module"] +from inspect import isclass class Module(ABC): + __REQUIRED_VARS = ["dependencies"] - @staticmethod - @abstractmethod - def dependencies() -> list[TModule]: ... + def __init_subclass__(cls): + super().__init_subclass__() + for var in cls.__REQUIRED_VARS: + if not hasattr(cls, var): + raise TypeError(f"Can't instantiate abstract class {cls.__name__} without {var} attribute") + + if not isinstance(getattr(cls, var), list): + raise TypeError(f"Can't instantiate abstract class {cls.__name__} with non-list {var} attribute") + + for dep in getattr(cls, var): + if not isinstance(dep, (list, tuple)) and not isclass(dep): + raise TypeError(f"Can't instantiate abstract class {cls.__name__} with invalid dependency {dep}") @staticmethod @abstractmethod diff --git a/src/cpl-dependency/cpl/dependency/service_collection.py b/src/cpl-dependency/cpl/dependency/service_collection.py index c8ae7e75..8086ed5a 100644 --- a/src/cpl-dependency/cpl/dependency/service_collection.py +++ b/src/cpl-dependency/cpl/dependency/service_collection.py @@ -1,6 +1,7 @@ from inspect import isclass -from typing import Union, Type, Callable, Self +from typing import Union, Callable, Self, Type +from cpl.core.errors import module_dependency_error from cpl.core.log.logger_abc import LoggerABC from cpl.core.typing import T, Service from cpl.core.utils.cache import Cache @@ -9,6 +10,7 @@ from cpl.dependency.module import Module from cpl.dependency.service_descriptor import ServiceDescriptor from cpl.dependency.service_lifetime import ServiceLifetimeEnum from cpl.dependency.service_provider import ServiceProvider +from cpl.dependency.typing import TModule, TService, TStartupTask class ServiceCollection: @@ -16,11 +18,6 @@ class ServiceCollection: _modules: dict[str, Callable] = {} - @classmethod - def with_module(cls, func: Callable, name: str = None) -> type[Self]: - # cls._modules[func.__name__ if name is None else name] = func - return cls - def __init__(self): self._service_descriptors: list[ServiceDescriptor] = [] self._loaded_modules: set[str] = set() @@ -29,6 +26,47 @@ class ServiceCollection: def loaded_modules(self) -> set[str]: return self._loaded_modules + def _check_dependency(self, module: TModule, dependency: TModule | TService, optional: bool = False) -> bool: + if not issubclass(dependency, Module): + found_services = [ + x + for x in self._service_descriptors + if x.service_type == dependency or x.base_type == dependency or isinstance(x.implementation, dependency) + ] + + if len(found_services) > 0: + return True + + if optional: + return False + + module_dependency_error(module.__name__, dependency.__name__) + + if dependency.__name__ not in self._loaded_modules: + if optional: + return False + + module_dependency_error(module.__name__, dependency.__name__) + + return True + + def _check_dependencies(self, module: TModule): + dependencies: list[TModule | Type] = getattr(module, "dependencies", []) + for dependency in dependencies: + if isinstance(dependency, (list, tuple)): + deps_exists = [self._check_dependency(module, dep, optional=True) for dep in dependency] + + if not any(deps_exists): + if len(dependency) > 1: + names = ", ".join([dep.__name__ for dep in dependency[:-1]]) + f" or {dependency[-1].__name__}" + else: + names = dependency[0].__name__ + + module_dependency_error(module.__name__, names) + continue + + self._check_dependency(module, dependency) + def _add_descriptor(self, service: Union[type, object], lifetime: ServiceLifetimeEnum, base_type: Callable = None): found = False for descriptor in self._service_descriptors: @@ -44,7 +82,9 @@ class ServiceCollection: self._service_descriptors.append(ServiceDescriptor(service, lifetime, base_type)) - def _add_descriptor_by_lifetime(self, service_type: Type, lifetime: ServiceLifetimeEnum, service: Callable = None): + def _add_descriptor_by_lifetime( + self, service_type: TService | T, lifetime: ServiceLifetimeEnum, service: Callable = None + ): if service is not None: self._add_descriptor(service, lifetime, service_type) else: @@ -52,19 +92,19 @@ class ServiceCollection: return self - def add_singleton(self, service_type: T, service: Service = None) -> Self: + def add_singleton(self, service_type: TService | T, service: Service = None) -> Self: self._add_descriptor_by_lifetime(service_type, ServiceLifetimeEnum.singleton, service) return self - def add_scoped(self, service_type: T, service: Service = None) -> Self: + def add_scoped(self, service_type: TService | T, service: Service = None) -> Self: self._add_descriptor_by_lifetime(service_type, ServiceLifetimeEnum.scoped, service) return self - def add_transient(self, service_type: T, service: Service = None) -> Self: + def add_transient(self, service_type: TService | T, service: Service = None) -> Self: self._add_descriptor_by_lifetime(service_type, ServiceLifetimeEnum.transient, service) return self - def add_startup_task(self, task: Type[StartupTask]) -> Self: + def add_startup_task(self, task: TStartupTask) -> Self: self.add_singleton(StartupTask, task) return self @@ -76,17 +116,15 @@ class ServiceCollection: sp = ServiceProvider(self._service_descriptors) return sp - def add_module(self, module: Type[Module]) -> Self: + def add_module(self, module: TModule) -> Self: assert isclass(module), "Module must be a Module" assert issubclass(module, Module), f"Module must be subclass of {Module.__name__}" name = module.__name__ if module in self._modules: - raise ValueError(f"Module {module} not found") + raise ValueError(f"Module {name} is already registered") - for dependency in module.dependencies(): - if dependency.__name__ not in self._loaded_modules: - self.add_module(dependency) + self._check_dependencies(module) module().register(self) @@ -114,6 +152,6 @@ class ServiceCollection: self.add_transient(wrapper) return self - def add_cache(self, t: Type[T]): + def add_cache(self, t: TService): self._service_descriptors.append(ServiceDescriptor(Cache(t=t), ServiceLifetimeEnum.singleton, Cache[t])) return self diff --git a/src/cpl-dependency/cpl/dependency/typing.py b/src/cpl-dependency/cpl/dependency/typing.py new file mode 100644 index 00000000..78b64652 --- /dev/null +++ b/src/cpl-dependency/cpl/dependency/typing.py @@ -0,0 +1,9 @@ +from typing import Type + +from cpl.core.typing import T +from cpl.dependency.hosted import StartupTask +from cpl.dependency.module import Module + +TModule = Type[Module] +TService = Type[T] +TStartupTask = Type[StartupTask] diff --git a/src/cpl-mail/cpl/mail/__init__.py b/src/cpl-mail/cpl/mail/__init__.py index 846c4dfe..97336c8e 100644 --- a/src/cpl-mail/cpl/mail/__init__.py +++ b/src/cpl-mail/cpl/mail/__init__.py @@ -1,21 +1,6 @@ -from cpl.dependency import ServiceCollection as _ServiceCollection from .abc.email_client_abc import EMailClientABC from .email_client import EMailClient from .email_client_settings import EMailClientSettings from .email_model import EMail -from .mail_module import MailModule from .logger import MailLogger - - -def add_mail(collection: _ServiceCollection): - from cpl.core.console import Console - from cpl.core.log import LoggerABC - - try: - collection.add_singleton(EMailClientABC, EMailClient) - collection.add_transient(LoggerABC, MailLogger) - except ImportError as e: - Console.error("cpl-translation is not installed", str(e)) - - -_ServiceCollection.with_module(add_mail, __name__) +from .mail_module import MailModule diff --git a/src/cpl-mail/cpl/mail/mail_module.py b/src/cpl-mail/cpl/mail/mail_module.py index f0e22336..3594bbe0 100644 --- a/src/cpl-mail/cpl/mail/mail_module.py +++ b/src/cpl-mail/cpl/mail/mail_module.py @@ -1,11 +1,9 @@ -from cpl.dependency import ServiceCollection +from cpl.dependency.service_collection import ServiceCollection from cpl.dependency.module import Module, TModule class MailModule(Module): - @staticmethod - def dependencies() -> list[TModule]: - return [] + dependencies = [] @staticmethod def register(collection: ServiceCollection): diff --git a/src/cpl-translation/cpl/translation/__init__.py b/src/cpl-translation/cpl/translation/__init__.py index e0d00e28..8bcc4f38 100644 --- a/src/cpl-translation/cpl/translation/__init__.py +++ b/src/cpl-translation/cpl/translation/__init__.py @@ -1,23 +1,5 @@ -from cpl.dependency import ServiceCollection as _ServiceCollection from .translate_pipe import TranslatePipe from .translation_module import TranslationModule from .translation_service import TranslationService from .translation_service_abc import TranslationServiceABC from .translation_settings import TranslationSettings - - -def add_translation(collection: _ServiceCollection): - from cpl.core.console import Console - from cpl.core.pipes import PipeABC - from cpl.translation.translate_pipe import TranslatePipe - from cpl.translation.translation_service import TranslationService - from cpl.translation.translation_service_abc import TranslationServiceABC - - try: - collection.add_singleton(TranslationServiceABC, TranslationService) - collection.add_transient(PipeABC, TranslatePipe) - except ImportError as e: - Console.error("cpl-translation is not installed", str(e)) - - -_ServiceCollection.with_module(add_translation, __name__) diff --git a/src/cpl-translation/cpl/translation/translation_module.py b/src/cpl-translation/cpl/translation/translation_module.py index 2144d77d..19444fd4 100644 --- a/src/cpl-translation/cpl/translation/translation_module.py +++ b/src/cpl-translation/cpl/translation/translation_module.py @@ -1,12 +1,10 @@ -from cpl.dependency import ServiceCollection -from cpl.dependency.module import Module, TModule +from cpl.dependency.service_collection import ServiceCollection +from cpl.dependency.module import Module from cpl.translation.translation_service_abc import TranslationServiceABC class TranslationModule(Module): - @staticmethod - def dependencies() -> list[TModule]: - return [] + dependencies = [] @staticmethod def register(collection: ServiceCollection):