From 58fe22e3d509aa1dd043c43aa098e6f23d4cd3b1 Mon Sep 17 00:00:00 2001 From: Artem Akymenko Date: Mon, 6 Jul 2026 09:01:16 +0000 Subject: [PATCH] feat: add TTSBackendRegistry for backend registration and creation - TTSBackendRegistry class with register(), list_backends(), get_metadata(), create_backend() - Global registry singleton with register_backend() and create_backend() convenience functions - Unit tests for registry operations --- abogen/tts_backend_registry.py | 71 ++++++++++++++++++++++++++++++++++ tests/test_tts_backend.py | 70 +++++++++++++++++++++++++++++++++ 2 files changed, 141 insertions(+) create mode 100644 abogen/tts_backend_registry.py diff --git a/abogen/tts_backend_registry.py b/abogen/tts_backend_registry.py new file mode 100644 index 0000000..4066537 --- /dev/null +++ b/abogen/tts_backend_registry.py @@ -0,0 +1,71 @@ +""" +TTS Backend Registry + +Provides a global registry for TTS backend factories. +Backends register themselves with metadata and a factory callable. +The registry is universal and does not know about backend constructors. +""" + +from typing import Callable, Any + +from abogen.tts_backend import TTSBackend, TTSBackendMetadata + + +class TTSBackendRegistry: + """Registry of TTS backend factories. + + Stores metadata and factory callables for registered backends. + """ + + def __init__(self) -> None: + self._backends: dict[str, TTSBackendMetadata] = {} + self._factories: dict[str, Callable[..., TTSBackend]] = {} + + def register( + self, + metadata: TTSBackendMetadata, + factory: Callable[..., TTSBackend], + ) -> None: + """Register a backend with its metadata and factory callable.""" + self._backends[metadata.id] = metadata + self._factories[metadata.id] = factory + + def list_backends(self) -> list[TTSBackendMetadata]: + """Return metadata for all registered backends.""" + return list(self._backends.values()) + + def get_metadata(self, backend_id: str) -> TTSBackendMetadata: + """Get metadata for a specific backend. + + Raises: + KeyError: If backend with given id is not registered. + """ + if backend_id not in self._backends: + raise KeyError(f"Unknown backend: {backend_id}") + return self._backends[backend_id] + + def create_backend(self, backend_id: str, **kwargs: Any) -> TTSBackend: + """Create a backend instance by id. + + Raises: + KeyError: If backend with given id is not registered. + """ + if backend_id not in self._factories: + raise KeyError(f"Unknown backend: {backend_id}") + return self._factories[backend_id](**kwargs) + + +_registry = TTSBackendRegistry() + + +def register_backend( + metadata: TTSBackendMetadata, + factory: Callable[..., TTSBackend], +) -> None: + """Register a TTS backend in the global registry.""" + _registry.register(metadata, factory) + + +def create_backend(backend_id: str, **kwargs: Any) -> TTSBackend: + """Create a TTS backend instance by provider id.""" + return _registry.create_backend(backend_id, **kwargs) diff --git a/tests/test_tts_backend.py b/tests/test_tts_backend.py index 0f1e532..fc777a4 100644 --- a/tests/test_tts_backend.py +++ b/tests/test_tts_backend.py @@ -1,6 +1,7 @@ from dataclasses import dataclass from abogen.tts_backend import TTSBackendMetadata +from abogen.tts_backend_registry import TTSBackendRegistry class TestTTSBackendMetadata: @@ -27,3 +28,72 @@ class TestTTSBackendMetadata: ) with pytest.raises(Exception): meta.id = "changed" + + +class TestTTSBackendRegistry: + def test_register_and_list(self): + registry = TTSBackendRegistry() + meta = TTSBackendMetadata(id="a", name="A", description="Backend A") + registry.register(metadata=meta, factory=lambda: None) + + backends = registry.list_backends() + assert len(backends) == 1 + assert backends[0].id == "a" + + def test_list_multiple(self): + registry = TTSBackendRegistry() + meta_a = TTSBackendMetadata(id="a", name="A", description="A") + meta_b = TTSBackendMetadata(id="b", name="B", description="B") + registry.register(metadata=meta_a, factory=lambda: None) + registry.register(metadata=meta_b, factory=lambda: None) + + backends = registry.list_backends() + ids = [b.id for b in backends] + assert "a" in ids + assert "b" in ids + + def test_get_metadata(self): + registry = TTSBackendRegistry() + meta = TTSBackendMetadata(id="x", name="X", description="X backend") + registry.register(metadata=meta, factory=lambda: None) + + result = registry.get_metadata("x") + assert result.id == "x" + assert result.name == "X" + + def test_get_metadata_unknown_raises(self): + import pytest + + registry = TTSBackendRegistry() + with pytest.raises(KeyError, match="Unknown backend: nope"): + registry.get_metadata("nope") + + def test_create_backend(self): + registry = TTSBackendRegistry() + meta = TTSBackendMetadata(id="test", name="Test", description="Test backend") + + def factory(**kwargs): + return {"created": True, "kwargs": kwargs} + + registry.register(metadata=meta, factory=factory) + result = registry.create_backend("test", foo="bar") + + assert result == {"created": True, "kwargs": {"foo": "bar"}} + + def test_create_backend_unknown_raises(self): + import pytest + + registry = TTSBackendRegistry() + with pytest.raises(KeyError, match="Unknown backend: missing"): + registry.create_backend("missing") + + def test_register_overwrites(self): + registry = TTSBackendRegistry() + meta1 = TTSBackendMetadata(id="x", name="V1", description="First") + meta2 = TTSBackendMetadata(id="x", name="V2", description="Second") + registry.register(metadata=meta1, factory=lambda: "v1") + registry.register(metadata=meta2, factory=lambda: "v2") + + result = registry.get_metadata("x") + assert result.name == "V2" + assert registry.create_backend("x") == "v2"