Skip to content

Commit

Permalink
tests: added for service injection
Browse files Browse the repository at this point in the history
  • Loading branch information
tristiisch committed Oct 3, 2024
1 parent df33bd3 commit 848fb3a
Show file tree
Hide file tree
Showing 7 changed files with 453 additions and 57 deletions.
6 changes: 5 additions & 1 deletion src/pyramid/api/services/tools/exceptions.py
Original file line number Diff line number Diff line change
Expand Up @@ -4,8 +4,12 @@ class ServiceRegisterException(Exception):
class ServiceAlreadyRegisterException(ServiceRegisterException):
pass

class ServiceNotRegisterException(ServiceRegisterException):
class ServiceNotRegisteredException(ServiceRegisterException):
pass

class ServiceCicularDependencyException(ServiceRegisterException):
pass

class ServiceWasNotOrdedException(ServiceRegisterException):
pass

122 changes: 74 additions & 48 deletions src/pyramid/api/services/tools/register.py
Original file line number Diff line number Diff line change
Expand Up @@ -3,25 +3,17 @@
import importlib
import inspect
import pkgutil
from typing import Any, Type, TypeVar
from pyramid.api.services.tools.exceptions import ServiceNotRegisterException, ServiceAlreadyRegisterException, ServiceCicularDependencyException
from typing import Any, Type, TypeVar, get_type_hints
from pyramid.api.services.tools.exceptions import ServiceNotRegisteredException, ServiceAlreadyRegisterException, ServiceCicularDependencyException, ServiceWasNotOrdedException
from pyramid.api.services.tools.injector import ServiceInjector

T = TypeVar('T')

class ServiceRegister:

__SERVICES_REGISTRED: dict[str, type[ServiceInjector]] = {}
__SERVICES_INSTANCES: dict[str, ServiceInjector] = {}
__ORDERED_SERVICES: list[str] | None = None

@classmethod
def enable(cls):
cls.import_services()
cls.__create_services()
cls.__determine_service_order()
cls.__inject_services()
cls.__start_services()
SERVICES_REGISTRED: dict[str, type[ServiceInjector]] = {}
SERVICES_INSTANCES: dict[str, ServiceInjector] = {}
ORDERED_SERVICES: list[str] | None = None

@classmethod
def import_services(cls):
Expand All @@ -37,32 +29,45 @@ def register_service(cls, interface_name: str, type: type[object]):
type_name = type.__name__
if not issubclass(type, ServiceInjector):
raise TypeError("Service %s is not a subclass of ServiceInjector and cannot be initialized." % type_name)
if interface_name in cls.__SERVICES_REGISTRED:
already_class_name = cls.__SERVICES_REGISTRED[interface_name].__name__
if interface_name in cls.SERVICES_REGISTRED:
already_class_name = cls.SERVICES_REGISTRED[interface_name].__module__ + ' ' + cls.SERVICES_REGISTRED[interface_name].__name__
raise ServiceAlreadyRegisterException(
"Cannot register service %s with %s, it is already registered with the class %s."
% (interface_name, type_name, already_class_name)
)
cls.__SERVICES_REGISTRED[interface_name] = type
cls.SERVICES_REGISTRED[interface_name] = type

@classmethod
def enable(cls):
cls.create_services()
cls.determine_service_order()
cls.inject_services()
cls.start_services()

@classmethod
def __determine_service_order(cls):
def determine_service_order(cls):
"""This method is not recommended.
Please call the `enable` method instead, which takes care of performing
the actions in the correct order.
"""
# Step 1: Create a graph of dependencies
dependency_graph = defaultdict(list)
indegree = defaultdict(int) # To track the number of dependencies

# Parse dependencies but delay injecting
for name, service_type in cls.__SERVICES_REGISTRED.items():
class_instance = cls.__SERVICES_INSTANCES[name]
for name, service_type in cls.SERVICES_REGISTRED.items():
class_instance = cls.SERVICES_INSTANCES[name]

# Step 2: Parse dependencies for each service
signature = inspect.signature(class_instance.injectService)
method_parameters = list(signature.parameters.values())
type_hints = get_type_hints(class_instance.injectService)

for method_parameter in method_parameters:
dependency_name = method_parameter.annotation.__name__
if dependency_name not in cls.__SERVICES_INSTANCES:
raise ServiceNotRegisterException(
dependency_name = type_hints[method_parameter.name].__name__
if dependency_name not in cls.SERVICES_INSTANCES:
raise ServiceNotRegisteredException(
f"Cannot register {dependency_name} as a dependency for {name} because the dependency is not registered."
)
# Add an edge in the dependency graph
Expand All @@ -71,7 +76,7 @@ def __determine_service_order(cls):

# Step 3: Perform a topological sort to determine the order of instantiation
sorted_services = []
queue = deque([service for service in cls.__SERVICES_REGISTRED if indegree[service] == 0])
queue = deque([service for service in cls.SERVICES_REGISTRED if indegree[service] == 0])

while queue:
service = queue.popleft()
Expand All @@ -82,59 +87,76 @@ def __determine_service_order(cls):
if indegree[dependent] == 0:
queue.append(dependent)

if len(sorted_services) != len(cls.__SERVICES_REGISTRED):
unresolved_services = set(cls.__SERVICES_REGISTRED) - set(sorted_services)
if len(sorted_services) != len(cls.SERVICES_REGISTRED):
unresolved_services = set(cls.SERVICES_REGISTRED) - set(sorted_services)
raise ServiceCicularDependencyException(
f"Circular dependency detected! The following services are involved in a circular dependency: {', '.join(unresolved_services)}"
)

cls.__ORDERED_SERVICES = sorted_services
cls.ORDERED_SERVICES = sorted_services

@classmethod
def __inject_services(cls):
if not cls.__ORDERED_SERVICES:
raise Exception("Failed to determine service startup order.")
def inject_services(cls):
"""This method is not recommended.
Please call the `enable` method instead, which takes care of performing
the actions in the correct order.
"""
if not cls.ORDERED_SERVICES:
raise ServiceWasNotOrdedException("Failed to determine service startup order.")

Check warning on line 106 in src/pyramid/api/services/tools/register.py

View check run for this annotation

Codecov / codecov/patch

src/pyramid/api/services/tools/register.py#L106

Added line #L106 was not covered by tests

# Inject dependencies in the correct order
for service_name in cls.__ORDERED_SERVICES:
class_instance = cls.__SERVICES_INSTANCES[service_name]
for service_name in cls.ORDERED_SERVICES:
class_instance = cls.SERVICES_INSTANCES[service_name]
signature = inspect.signature(class_instance.injectService)
method_parameters = list(signature.parameters.values())
type_hints = get_type_hints(class_instance.injectService)

services_dependencies = []
for method_parameter in method_parameters:
dependency_name = method_parameter.annotation.__name__
dependency_instance = cls.__SERVICES_INSTANCES[dependency_name]
dependency_name = type_hints[method_parameter.name].__name__
dependency_instance = cls.SERVICES_INSTANCES[dependency_name]
services_dependencies.append(dependency_instance)

class_instance.injectService(*services_dependencies)

@classmethod
def __create_services(cls):
for name, service_type in cls.__SERVICES_REGISTRED.items():
def create_services(cls):
"""This method is not recommended.
Please call the `enable` method instead, which takes care of performing
the actions in the correct order.
"""
for name, service_type in cls.SERVICES_REGISTRED.items():
class_instance = service_type()
cls.__SERVICES_INSTANCES[name] = class_instance
cls.SERVICES_INSTANCES[name] = class_instance

@classmethod
def __start_services(cls):
if not cls.__ORDERED_SERVICES:
raise Exception("Failed to determine service startup order.")
def start_services(cls):
"""This method is not recommended.
for service_name in cls.__ORDERED_SERVICES:
class_instance = cls.__SERVICES_INSTANCES[service_name]
Please call the `enable` method instead, which takes care of performing
the actions in the correct order.
"""
if not cls.ORDERED_SERVICES:
raise ServiceWasNotOrdedException("Failed to determine service startup order.")

Check warning on line 142 in src/pyramid/api/services/tools/register.py

View check run for this annotation

Codecov / codecov/patch

src/pyramid/api/services/tools/register.py#L142

Added line #L142 was not covered by tests

for service_name in cls.ORDERED_SERVICES:
class_instance = cls.SERVICES_INSTANCES[service_name]
class_instance.start()

@classmethod
def get_dependency_tree(cls):
# Step 1: Build dependency graph
dependency_graph = defaultdict(list)
for name, class_instance in cls.__SERVICES_INSTANCES.items():
for name, class_instance in cls.SERVICES_INSTANCES.items():

Check warning on line 152 in src/pyramid/api/services/tools/register.py

View check run for this annotation

Codecov / codecov/patch

src/pyramid/api/services/tools/register.py#L151-L152

Added lines #L151 - L152 were not covered by tests

signature = inspect.signature(class_instance.injectService)
method_parameters = list(signature.parameters.values())
type_hints = get_type_hints(class_instance.injectService)

Check warning on line 156 in src/pyramid/api/services/tools/register.py

View check run for this annotation

Codecov / codecov/patch

src/pyramid/api/services/tools/register.py#L154-L156

Added lines #L154 - L156 were not covered by tests

for method_parameter in method_parameters:
dependency_name = method_parameter.annotation.__name__
dependency_name = type_hints[method_parameter.name].__name__
dependency_graph[dependency_name].append(name)

Check warning on line 160 in src/pyramid/api/services/tools/register.py

View check run for this annotation

Codecov / codecov/patch

src/pyramid/api/services/tools/register.py#L158-L160

Added lines #L158 - L160 were not covered by tests

# Step 2: Internal buffer for storing the tree structure
Expand All @@ -155,7 +177,7 @@ def build_tree(node, prefix="", last=True):
build_tree(child, prefix, i == len(children) - 1)

Check warning on line 177 in src/pyramid/api/services/tools/register.py

View check run for this annotation

Codecov / codecov/patch

src/pyramid/api/services/tools/register.py#L174-L177

Added lines #L174 - L177 were not covered by tests

# Step 4: Find root services (those with no dependencies)
all_services = set(cls.__SERVICES_REGISTRED.keys())
all_services = set(cls.SERVICES_REGISTRED.keys())
dependent_services = set(dep for deps in dependency_graph.values() for dep in deps)
root_services = all_services - dependent_services

Check warning on line 182 in src/pyramid/api/services/tools/register.py

View check run for this annotation

Codecov / codecov/patch

src/pyramid/api/services/tools/register.py#L180-L182

Added lines #L180 - L182 were not covered by tests

Expand All @@ -170,13 +192,17 @@ def build_tree(node, prefix="", last=True):

@classmethod
def get_service_registred(cls, class_name: str) -> type[ServiceInjector]:
if class_name not in cls.__SERVICES_REGISTRED:
raise ServiceNotRegisterException(
if class_name not in cls.SERVICES_REGISTRED:
raise ServiceNotRegisteredException(
"Cannot get %s because the service is not registered." % (class_name)
)
return cls.__SERVICES_REGISTRED[class_name]
return cls.SERVICES_REGISTRED[class_name]

@classmethod
def get_service(cls, class_type: Type[T]) -> T:
class_name = class_type.__name__
return cls.__SERVICES_INSTANCES[class_name]
if class_name not in cls.SERVICES_INSTANCES:
raise ServiceNotRegisteredException(
"Cannot get %s because the service is not started." % (class_name)
)
return cls.SERVICES_INSTANCES[class_name] # type: ignore
16 changes: 8 additions & 8 deletions src/pyramid/api/services/tools/tester.py
Original file line number Diff line number Diff line change
@@ -1,13 +1,12 @@
import inspect
from typing import Optional, Type, TypeVar, cast
from pyramid.api.services.tools.exceptions import ServiceNotRegisterException
from typing import Type, TypeVar, cast, get_type_hints
from pyramid.api.services.tools.register import ServiceRegister

T = TypeVar('T')

class ServiceStandalone:

__SERVICE_REGISTERED: dict[str, object] = {}
SERVICE_REGISTERED: dict[str, object] = {}

@classmethod
def import_services(cls):
Expand All @@ -16,31 +15,32 @@ def import_services(cls):
@classmethod
def set_service(cls, service_interface: Type[T], service_instance: object):
service_name = service_interface.__name__
cls.__SERVICE_REGISTERED[service_name] = service_instance
cls.SERVICE_REGISTERED[service_name] = service_instance

@classmethod
def get_service(cls, service_interface: Type[T]) -> T:
service_name = service_interface.__name__

if service_name in cls.__SERVICE_REGISTERED:
return cast(T, cls.__SERVICE_REGISTERED[service_name])
if service_name in cls.SERVICE_REGISTERED:
return cast(T, cls.SERVICE_REGISTERED[service_name])

service_type = ServiceRegister.get_service_registred(service_name)
class_instance = service_type()

signature = inspect.signature(class_instance.injectService)
method_parameters = list(signature.parameters.values())
type_hints = get_type_hints(class_instance.injectService)

services_dependencies = []
for method_parameter in method_parameters:
dependency = method_parameter.annotation
dependency = type_hints[method_parameter.name]
dependency_instance = cls.get_service(dependency)

services_dependencies.append(dependency_instance)

class_instance.injectService(*services_dependencies)
class_instance.start()

cls.__SERVICE_REGISTERED[service_name] = class_instance
cls.SERVICE_REGISTERED[service_name] = class_instance

return cast(T, class_instance)
1 change: 1 addition & 0 deletions src/pyramid/data/functional/main.py
Original file line number Diff line number Diff line change
Expand Up @@ -30,6 +30,7 @@ def args(self):
sys.exit(0)

def start(self):
ServiceRegister.import_services()
ServiceRegister.enable()

MainQueue.init()
Expand Down
Loading

0 comments on commit 848fb3a

Please sign in to comment.