ytptube/app/library/Services.py
2026-06-14 18:35:03 +03:00

179 lines
5.4 KiB
Python

import inspect
from collections.abc import Callable
from dataclasses import dataclass
from typing import Annotated, Any, TypeVar, get_args, get_origin, get_type_hints
from app.library.log import get_logger
from app.library.Singleton import Singleton
T = TypeVar("T")
LOG = get_logger()
def _unwrap_annotation(ann: Any) -> Any:
if ann is inspect._empty:
return inspect._empty
origin = get_origin(ann)
# Annotated[T, ...] -> T
if origin is Annotated:
args = get_args(ann)
return args[0] if args else ann
# Optional[T] / Union[T, None] / T | None -> T
if origin is None:
return ann
if str(origin) in ("typing.Union", "types.UnionType"):
args = [a for a in get_args(ann) if a is not type(None)]
if len(args) == 1:
return args[0]
return ann
return ann
@dataclass(frozen=True)
class ServiceEntry:
name: str
declared_type: type | None
instance: Any
class Services(metaclass=Singleton):
def __init__(self):
self._services: list[ServiceEntry] = []
@staticmethod
def get_instance() -> "Services":
return Services()
def add(self, name: str, service: Any, declared_type: type | None = None) -> "Services":
"""
Add a service by name.
Args:
name: The name of the service.
service: The service instance.
declared_type: The declared type of the service (optional).
Returns:
Services: The Services instance (for chaining).
"""
if declared_type is None and service is not None:
declared_type = type(service)
self.remove(name)
self._services.append(ServiceEntry(name=name, declared_type=declared_type, instance=service))
return self
def add_all(self, services: dict[str, Any]):
for name, svc in services.items():
self.add(name, svc)
def remove(self, name: str):
self._services = [e for e in self._services if e.name != name]
def clear(self):
self._services.clear()
def get(self, name: str) -> Any | None:
for e in reversed(self._services):
if e.name == name:
return e.instance
return None
def has(self, name: str) -> bool:
return any(e.name == name for e in self._services)
def get_all(self) -> list[ServiceEntry]:
return self._services.copy()
def get_by_type(self, expected_type: Any) -> Any | None:
expected_type = _unwrap_annotation(expected_type)
if expected_type is inspect._empty or not isinstance(expected_type, type):
return None
exact: list[Any] = [e.instance for e in self._services if e.declared_type is expected_type]
if len(exact) == 1:
return exact[0]
if len(exact) > 1:
msg: str = (
f"Ambiguous dependency for type {expected_type.__name__}: {len(exact)} exact matches. Resolve by name."
)
raise LookupError(msg)
candidates: list[Any] = [e.instance for e in self._services if isinstance(e.instance, expected_type)]
if len(candidates) == 0:
return None
if len(candidates) > 1:
msg: str = (
f"Ambiguous dependency for type {expected_type.__name__}: "
f"{len(candidates)} candidates. Resolve by name."
)
raise LookupError(msg)
return candidates[0]
def _build_call_args(self, handler: Callable[..., Any], overrides: dict[str, Any]) -> dict[str, Any]:
sig: inspect.Signature = inspect.signature(handler)
try:
type_hints: dict[str, Any] = get_type_hints(handler)
except Exception:
type_hints = {}
resolved: dict[str, Any] = {}
for name, param in sig.parameters.items():
if param.kind in (param.VAR_POSITIONAL, param.VAR_KEYWORD):
continue
if name in overrides:
resolved[name] = overrides[name]
continue
by_name: Any | None = self.get(name)
if by_name is not None:
resolved[name] = by_name
continue
ann: Any | None = type_hints.get(name, param.annotation)
by_type: Any | None = self.get_by_type(ann)
if by_type is not None:
resolved[name] = by_type
continue
if param.default is not inspect._empty:
continue
missing_required: list[str] = []
for name, param in sig.parameters.items():
if param.kind in (param.VAR_POSITIONAL, param.VAR_KEYWORD):
continue
if param.default is not inspect._empty:
continue
if name not in resolved:
missing_required.append(name)
if missing_required:
LOG.error(
"Missing arguments for handler '%s': %s",
getattr(handler, "__name__", str(handler)),
missing_required,
)
return resolved
async def handle_async(self, handler: Callable[..., Any], **kwargs) -> Any:
resolved: dict[str, Any] = self._build_call_args(handler, kwargs)
return await handler(**resolved)
def handle_sync(self, handler: Callable[..., Any], **kwargs) -> Any:
resolved: dict[str, Any] = self._build_call_args(handler, kwargs)
return handler(**resolved)