diff --git a/README.md b/README.md index 8a1c169..bbb3a89 100644 --- a/README.md +++ b/README.md @@ -12,7 +12,7 @@ > At this point, it's unclear whether this project will become a "proper Hynek project". > I will keep using it for my work projects, but whether this will grow beyond my personal needs depends on community interest. -*svc-reg* is a [service locator](https://en.wikipedia.org/wiki/Service_locator_pattern) for Python that lets you register factories for types/interfaces and then create instances of those types with unified life-cycle management and health checks. +*svc-reg* is a [service locator](https://en.wikipedia.org/wiki/Service_locator_pattern) for Python that lets you register factories for types/interfaces and then create instances of those types with **unified life-cycle management** and **health checks**. **This allows you to configure and manage resources in *one central place* and access them in a *consistent* way.** @@ -48,6 +48,20 @@ def view(): The latter already works with [Flask](#flask). +And you set it up like this: + +```python +engine = create_engine("postgresql://localhost") + +def engine_factory(): + with engine.connect() as conn: + yield conn + +svc_reg.register_factory(Database, create_database) +``` + +The generator-based setup and cleanup may remind you of [Pytest fixtures](https://docs.pytest.org/en/stable/explanation/fixtures.html). + @@ -97,11 +111,13 @@ True ``` A container lives as long as you want the instances to live -- e.g., as long as a request lives. -At the end, you run `container.close()` to clean up all instances that the container has created. + +If a factory is a generator and yields the instance, the generator will be remembered. +At the end, you run `container.close()` and all generators will be finished (i.e. called `next(g)`). You can use this to return database connections to a pool, et cetera. -If you have async cleanup functions, use `await container.aclose()` instead. -It will run both sync and async cleanup functions. +If you have async generators, use `await container.aclose()` instead which calls `await g.__anext__()` on all async generators. +It will run both sync and async cleanup functions by default. Failing cleanups are logged at `warning` level but otherwise ignored. @@ -162,17 +178,21 @@ def create_app(config_filename): app = svc_reg.flask.init_app(app) # Now, register a factory that calls `engine.connect()` if you ask for a - # Connections and `connection.close()` on cleanup. + # Connections. Since we use yield inside of a context manager, the + # connection gets cleaned up when the container is closed. # If you ask for a ping, it will run `SELECT 1` on a new connection and # clean up the connection behind itself. engine = create_engine("postgresql://localhost") + def engine_factory(): + with engine.connect() as conn: + yield conn + ping = text("SELECT 1") svc_reg_flask.register_factory( # The app argument makes it good for custom init_app() functions. app, Connection, - engine.connect, - cleanup=lambda conn: conn.close(), + engine_factory, ping=lambda conn: conn.execute(ping) ) @@ -180,6 +200,7 @@ def create_app(config_filename): svc_reg_flask.register_factory( app, # <--- AbstractRepository, + # No cleanup, so we just return an object using a lambda lambda: Repository.from_connection( svc_reg.flask.get(Connection) ), diff --git a/conftest.py b/conftest.py index e457f15..5e1448c 100644 --- a/conftest.py +++ b/conftest.py @@ -1,8 +1,12 @@ from doctest import ELLIPSIS +import pytest + from sybil import Sybil from sybil.parsers.rest import DocTestParser, PythonCodeBlockParser +import svc_reg + pytest_collect_file = Sybil( parsers=[ @@ -11,3 +15,13 @@ ], patterns=["*.md", "*.py"], ).pytest() + + +@pytest.fixture(name="registry") +def _registry(): + return svc_reg.Registry() + + +@pytest.fixture(name="container") +def _container(registry): + return svc_reg.Container(registry) diff --git a/pyproject.toml b/pyproject.toml index 14eed4b..2db1c06 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -157,6 +157,10 @@ ignore = [ "EM101", # no need for exception msg hygiene in tests ] +"conftest.py" = [ + "PT005", # we always add underscores and explicit name +] + [tool.ruff.isort] lines-between-types = 1 diff --git a/src/svc_reg/__init__.py b/src/svc_reg/__init__.py index b0d3c36..a8875be 100644 --- a/src/svc_reg/__init__.py +++ b/src/svc_reg/__init__.py @@ -1,10 +1,10 @@ from __future__ import annotations +from . import exceptions from ._core import ( Container, RegisteredService, Registry, - ServiceNotFoundError, ServicePing, ) @@ -15,6 +15,7 @@ "Registry", "ServiceNotFoundError", "ServicePing", + "exceptions", ] try: diff --git a/src/svc_reg/_core.py b/src/svc_reg/_core.py index 9935e3e..3d24f7e 100644 --- a/src/svc_reg/_core.py +++ b/src/svc_reg/_core.py @@ -1,22 +1,18 @@ from __future__ import annotations -import asyncio import logging +import warnings from collections.abc import Callable from contextlib import suppress -from typing import Any +from typing import Any, AsyncGenerator, Generator import attrs - -log = logging.getLogger(__name__) +from .exceptions import ServiceNotFoundError -class ServiceNotFoundError(Exception): - """ - Raised when a service type is not registered. - """ +log = logging.getLogger(__name__) @attrs.define @@ -27,16 +23,16 @@ class Container: registry: Registry instantiated: dict[type, object] = attrs.Factory(dict) - cleanups: list[tuple[RegisteredService, object]] = attrs.Factory(list) - async_cleanups: list[tuple[RegisteredService, object]] = attrs.Factory( - list - ) + cleanups: list[tuple[RegisteredService, Generator]] = attrs.Factory(list) + async_cleanups: list[ + tuple[RegisteredService, AsyncGenerator] + ] = attrs.Factory(list) def __repr__(self) -> str: return ( f"" + f"async_cleanups={len(self.async_cleanups)})>" ) def get(self, svc_type: type) -> Any: @@ -48,42 +44,44 @@ def get(self, svc_type: type) -> Any: Returns: Any until https://github.com/python/mypy/issues/4717 is fixed. """ - if (svc := self._get_instance(svc_type)) is not None: + if (svc := self.instantiated.get(svc_type)) is not None: return svc rs = self.registry.get_registered_service_for(svc_type) svc = rs.factory() - self._add_instance(rs, svc) - return svc + if isinstance(svc, Generator): + self.cleanups.append((rs, svc)) + svc = next(svc) - def _add_instance(self, rs: RegisteredService, svc: object) -> None: self.instantiated[rs.svc_type] = svc - self.add_cleanup(rs, svc) - def _get_instance(self, svc_type: type) -> object | None: - """ - If present, return the cached instance of *svc_type*. - """ - return self.instantiated.get(svc_type) + return svc - def add_cleanup(self, rs: RegisteredService, svc: object) -> bool: + async def aget(self, svc_type: type) -> Any: """ - Add a cleanup function for *svc* if *rs* has one without remembering - the service itself. + Get an instance of *svc_type*. + + Instantiate it asynchronously if necessary and register its cleanup. - Return: - True if a cleanup was added, False otherwise. + Returns: + Any until https://github.com/python/mypy/issues/4717 is fixed. """ - if not rs.cleanup: - return False + if (svc := self.instantiated.get(svc_type)) is not None: + return svc - if asyncio.iscoroutinefunction(rs.cleanup): + rs = self.registry.get_registered_service_for(svc_type) + svc = rs.factory() + + if isinstance(svc, AsyncGenerator): self.async_cleanups.append((rs, svc)) + svc = await svc.__anext__() else: - self.cleanups.append((rs, svc)) + svc = await svc # type: ignore[misc] - return True + self.instantiated[rs.svc_type] = svc + + return svc def forget_service_type(self, svc_type: type) -> None: """ @@ -97,10 +95,16 @@ def close(self) -> None: Run all synchronous registered cleanups. """ while self.cleanups: - rs, svc = self.cleanups.pop() + rs, gen = self.cleanups.pop() try: - rs.cleanup(svc) # type: ignore[misc] - except Exception: # noqa: PERF203, BLE001 + next(gen) + + warnings.warn( + f"clean up for {rs!r} didn't stop iterating", stacklevel=1 + ) + except StopIteration: # noqa: PERF203 + pass + except Exception: # noqa: BLE001 log.warning( "clean up failed", exc_info=True, @@ -114,10 +118,17 @@ async def aclose(self) -> None: self.close() while self.async_cleanups: - rs, svc = self.async_cleanups.pop() + rs, gen = self.async_cleanups.pop() try: - await rs.cleanup(svc) # type: ignore[misc] - except Exception: # noqa: PERF203, BLE001 + await gen.__anext__() + + warnings.warn( + f"clean up for {rs!r} didn't stop iterating", stacklevel=1 + ) + + except StopAsyncIteration: # noqa: PERF203 + pass + except Exception: # noqa: BLE001 log.warning( "clean up failed", exc_info=True, @@ -139,7 +150,6 @@ def get_pings(self) -> list[ServicePing]: class RegisteredService: svc_type: type factory: Callable = attrs.field(hash=False) - cleanup: Callable | None = attrs.field(hash=False) ping: Callable | None = attrs.field(hash=False) @property @@ -150,7 +160,6 @@ def __repr__(self) -> str: return ( f"" ) @@ -161,8 +170,7 @@ class ServicePing: _rs: RegisteredService def ping(self) -> None: - svc = self._rs.factory() - self._container.add_cleanup(self._rs, svc) + svc = self._container.get(self._rs.svc_type) self._rs.ping(svc) # type: ignore[misc] @property @@ -179,24 +187,18 @@ def register_factory( svc_type: type, factory: Callable, *, - cleanup: Callable | None = None, ping: Callable | None = None, ) -> None: - self.services[svc_type] = RegisteredService( - svc_type, factory, cleanup, ping - ) + self.services[svc_type] = RegisteredService(svc_type, factory, ping) def register_value( self, svc_type: type, instance: object, *, - cleanup: Callable | None = None, ping: Callable | None = None, ) -> None: - self.register_factory( - svc_type, lambda: instance, cleanup=cleanup, ping=ping - ) + self.register_factory(svc_type, lambda: instance, ping=ping) def get_registered_service_for(self, svc_type: type) -> RegisteredService: try: diff --git a/src/svc_reg/exceptions.py b/src/svc_reg/exceptions.py new file mode 100644 index 0000000..4b94711 --- /dev/null +++ b/src/svc_reg/exceptions.py @@ -0,0 +1,7 @@ +from __future__ import annotations + + +class ServiceNotFoundError(Exception): + """ + Raised when a service type is not registered. + """ diff --git a/src/svc_reg/flask.py b/src/svc_reg/flask.py index 1074396..97eb6e6 100644 --- a/src/svc_reg/flask.py +++ b/src/svc_reg/flask.py @@ -36,12 +36,9 @@ def register_factory( svc_type: type, factory: Callable, *, - cleanup: Callable | None = None, ping: Callable | None = None, ) -> None: - app.config["svc_registry"].register_factory( - svc_type, factory, cleanup=cleanup, ping=ping - ) + app.config["svc_registry"].register_factory(svc_type, factory, ping=ping) def register_value( @@ -49,38 +46,33 @@ def register_value( svc_type: type, instance: object, *, - cleanup: Callable | None = None, ping: Callable | None = None, ) -> None: - app.config["svc_registry"].register_value( - svc_type, instance, cleanup=cleanup, ping=ping - ) + app.config["svc_registry"].register_value(svc_type, instance, ping=ping) def replace_factory( svc_type: type, factory: Callable, *, - cleanup: Callable | None = None, ping: Callable | None = None, ) -> None: registry, container = _ensure_req_data() container.forget_service_type(svc_type) - registry.register_factory(svc_type, factory, cleanup=cleanup, ping=ping) + registry.register_factory(svc_type, factory, ping=ping) def replace_value( svc_type: type, instance: object, *, - cleanup: Callable | None = None, ping: Callable | None = None, ) -> None: registry, container = _ensure_req_data() container.forget_service_type(svc_type) - registry.register_value(svc_type, instance, cleanup=cleanup, ping=ping) + registry.register_value(svc_type, instance, ping=ping) def get_pings() -> list[ServicePing]: diff --git a/tests/test_async.py b/tests/test_async.py index de2c4ef..75d6fa8 100644 --- a/tests/test_async.py +++ b/tests/test_async.py @@ -4,8 +4,6 @@ import pytest -import svc_reg - @dataclass class Service: @@ -14,40 +12,69 @@ class Service: @pytest.mark.asyncio() class TestAsync: - async def test_async_factory(self): + async def test_async_factory(self, registry, container): """ A factory can be async. """ - reg = svc_reg.Registry() - container = svc_reg.Container(reg) async def factory(): await asyncio.sleep(0) return Service() - reg.register_factory(Service, factory) + registry.register_factory(Service, factory) - coro = container.get(Service) + svc = await container.aget(Service) - assert asyncio.iscoroutine(coro) - assert Service() == await coro + assert isinstance(svc, Service) + assert svc is (await container.aget(Service)) - async def test_async_cleanup(self): + async def test_async_cleanup(self, registry, container): """ Async cleanups are handled by acleanup. """ - reg = svc_reg.Registry() - container = svc_reg.Container(reg) + cleaned_up = False + + async def factory(): + nonlocal cleaned_up + await asyncio.sleep(0) + + yield Service() - async def cleanup(svc_): await asyncio.sleep(0) - assert svc is svc_ + cleaned_up = True - reg.register_factory(Service, Service, cleanup=cleanup) + registry.register_factory(Service, factory) - svc = container.get(Service) + svc = await container.aget(Service) assert 1 == len(container.async_cleanups) assert Service() == svc + assert not cleaned_up await container.aclose() + + assert cleaned_up + + async def test_warns_if_generator_does_not_stop_after_cleanup( + self, registry, container + ): + """ + If a generator doesn't stop after cleanup, a warning is emitted. + """ + + async def factory(): + yield Service() + yield 42 + + registry.register_factory(Service, factory) + + await container.aget(Service) + + with pytest.warns(UserWarning) as wi: + await container.aclose() + + assert ( + "clean up for " + "didn't stop iterating" == wi.pop().message.args[0] + ) diff --git a/tests/test_core.py b/tests/test_core.py index 9f683e6..0109d5a 100644 --- a/tests/test_core.py +++ b/tests/test_core.py @@ -1,4 +1,4 @@ -from unittest.mock import AsyncMock, Mock +from unittest.mock import Mock import pytest @@ -19,12 +19,7 @@ class YetAnotherService: @pytest.fixture(name="rs") def _rs(svc): - return svc_reg.RegisteredService(Service, Service, None, None) - - -@pytest.fixture(name="container") -def _container(registry): - return svc_reg.Container(registry) + return svc_reg.RegisteredService(Service, Service, None) @pytest.fixture(name="svc") @@ -32,18 +27,6 @@ def _svc(): return Service() -@pytest.fixture(name="registry") -def _registry(): - return svc_reg.Registry() - - -def nop_cleanup(svc): - pass - - -# class TestRegistry: - - class TestContainer: def test_register_factory_get(self, registry, container): """ @@ -71,7 +54,7 @@ def test_get_not_found(self, container): """ Asking for a service that isn't registered raises a ServiceNotFoundError. """ - with pytest.raises(svc_reg.ServiceNotFoundError) as ei: + with pytest.raises(svc_reg.exceptions.ServiceNotFoundError) as ei: container.get(Service) assert Service is ei.value.args[0] @@ -93,47 +76,6 @@ def test_get_pings(self, registry, container, svc): ping._rs.svc_type for ping in container.get_pings() ] - def test_add_get_instance_cleanup(self, container, svc): - """ - _add_instance adds the service with its cleanup to the container and - get_instance retrieves it. - """ - rs = svc_reg.RegisteredService(Service, Service, nop_cleanup, None) - - container._add_instance(rs, svc) - - assert svc is container._get_instance(rs.svc_type) - assert [(rs, svc)] == container.cleanups - - def test_add_get_instance_no_cleanup(self, container, svc, rs): - """ - _add_instance adds the service with its cleanup to the container and - get_instance retrieves it. - """ - container._add_instance(rs, svc) - - assert svc is container._get_instance(rs.svc_type) - assert [] == container.cleanups - - def test_add_cleanup_added(self, container, svc): - """ - If the registered service has a cleanup, it is added to the cleanup - and add_cleanup returns True. - """ - - rs = svc_reg.RegisteredService(Service, Service, nop_cleanup, None) - - assert container.add_cleanup(rs, svc) - assert [(rs, svc)] == container.cleanups - - def test_add_cleanup_not_added(self, container, rs, svc): - """ - If the registered service has no cleanup, it's not added and - add_cleanup returns False. - """ - assert False is container.add_cleanup(rs, svc) - assert [] == container.cleanups - def test_forget_service_type_nothing_registered(self, container): """ forget_service_type does nothing if nothing has been registered. @@ -144,87 +86,56 @@ def test_forget_service_type_no_cleanup(self, container, rs, svc): """ forget_service_type removes the registered service from the container. """ - container._add_instance(rs, svc) + container.instantiated[rs.svc_type] = (rs, svc) container.forget_service_type(Service) assert {} == container.instantiated assert [] == container.cleanups - def test_forget_service_type_with_cleanup(self, container, svc): - """ - forget_service_type removes the registered service from the container. - """ - rs = svc_reg.RegisteredService( - Service, svc, Mock(spec_set=["__call__"]), None - ) - container._add_instance(rs, svc) - - container.forget_service_type(Service) - - assert {} == container.instantiated - - def test_forget_service_type_is_ok_with_other_registrations( - self, container, registry - ): + @pytest.mark.asyncio() + async def test_repr(self, registry, container): """ - If other svc_reg are registered, they are ignored by the cleanup - purge. + The repr counts correctly. """ - registry.register_factory(Service, Service, cleanup=lambda _: None) - registry.register_factory( - AnotherService, AnotherService, cleanup=lambda _: None - ) - container.get(Service) - container.get(AnotherService) + def factory(): + yield 42 - assert 2 == len(container.cleanups) + async def async_factory(): + yield 42 - container.forget_service_type(Service) - registry.register_value(Service, object(), cleanup=nop_cleanup) + registry.register_factory(Service, factory) + registry.register_factory(AnotherService, async_factory) container.get(Service) - container.get(AnotherService) - - assert 3 == len(container.cleanups) - - def test_repr(self, container, rs, svc): - """ - The repr counts correctly. - """ - rs2 = svc_reg.RegisteredService( - AnotherService, svc, Mock(spec_set=["__call__"]), None - ) - rs3 = svc_reg.RegisteredService( - AnotherService, svc, AsyncMock(spec_set=["__call__"]), None - ) - - container._add_instance(rs, Service()) - container._add_instance(rs2, Service()) - container._add_instance(rs3, Service()) # overwrites rs2 + await container.aget(AnotherService) assert ( - # rs2 has been removed, but its cleanup is still there. - "" + "" == repr(container) ) - def test_cleanup_called(self, container, rs): + def test_cleanup_called(self, registry, container, rs): """ Services that have a cleanup have them called on cleanup. """ - container._add_instance(rs, Service()) + cleaned_up = False - svc = AnotherService() - rs_cleanup = svc_reg.RegisteredService( - AnotherService, AnotherService, Mock(spec_set=["__call__"]), None - ) - container._add_instance(rs_cleanup, svc) + def factory(): + nonlocal cleaned_up + yield 42 + cleaned_up = True + + registry.register_factory(Service, factory) + + container.get(Service) + + assert not cleaned_up container.close() - rs_cleanup.cleanup.assert_called_once_with(svc) + assert cleaned_up @pytest.mark.asyncio() async def test_clean_resilient(self, container, registry, caplog): @@ -232,33 +143,64 @@ async def test_clean_resilient(self, container, registry, caplog): Failing cleanups are logged and ignored. They do not break the cleanup process. """ - registry.register_factory( - Service, - Service, - cleanup=Mock(spec_set=["__call__"], side_effect=Exception), - ) - registry.register_factory( - AnotherService, - AnotherService, - cleanup=AsyncMock(spec_set=["__call__"], side_effect=Exception), - ) - last_cleanup = Mock(spec_set=["__call__"]) - registry.register_factory( - YetAnotherService, - YetAnotherService, - cleanup=last_cleanup, - ) - container.get(Service) - container.get(AnotherService) - container.get(YetAnotherService) + def factory(): + yield 1 + raise Exception + + async def async_factory(): + yield 2 + raise Exception + + cleaned_up = False + + async def factory_no_boom(): + nonlocal cleaned_up + + yield 3 + + cleaned_up = True + + registry.register_factory(Service, factory) + registry.register_factory(AnotherService, async_factory) + registry.register_factory(YetAnotherService, factory_no_boom) + + assert 1 == container.get(Service) + assert 2 == await container.aget(AnotherService) + assert 3 == await container.aget(YetAnotherService) + + assert not cleaned_up await container.aclose() # Sync cleanups are run first. assert "Service" == caplog.records[0].service assert "AnotherService" == caplog.records[1].service - last_cleanup.assert_called_once() + assert cleaned_up + + def test_warns_if_generator_does_not_stop_after_cleanup( + self, registry, container + ): + """ + If a generator doesn't stop after cleanup, a warning is emitted. + """ + + def factory(): + yield Service() + yield 42 + + registry.register_factory(Service, factory) + + container.get(Service) + + with pytest.warns(UserWarning) as wi: + container.close() + + assert ( + "clean up for " + "didn't stop iterating" == wi.pop().message.args[0] + ) class TestRegisteredService: @@ -268,7 +210,7 @@ def test_repr(self, rs): """ assert ( - "" + "" ) == repr(rs) def test_name(self, rs): @@ -287,23 +229,30 @@ def test_name(self, rs): assert "Service" == svc_reg.ServicePing(None, rs).name - def test_ping(self): + def test_ping(self, registry, container): """ Calling ping instantiates the service using its factory, appends it to the cleanup list, and calls the service's ping method. """ - svc = Service() - rs = svc_reg.RegisteredService( - Service, - lambda: svc, - Mock(spec_set=["__call__"]), - Mock(spec_set=["__call__"]), - ) - container = svc_reg.Container(svc_reg.Registry()) - svc_ping = svc_reg.ServicePing(container, rs) + cleaned_up = False + + def factory(): + nonlocal cleaned_up + yield Service() + cleaned_up = True + + ping = Mock(spec_set=["__call__"]) + registry.register_factory(Service, factory, ping=ping) + + (svc_ping,) = container.get_pings() svc_ping.ping() - rs.ping.assert_called_once_with(svc) - assert [(rs, svc)] == container.cleanups + ping.assert_called_once() + + assert not cleaned_up + + container.close() + + assert cleaned_up diff --git a/tests/test_flask.py b/tests/test_flask.py index 9f58b3e..de84058 100644 --- a/tests/test_flask.py +++ b/tests/test_flask.py @@ -25,11 +25,6 @@ def _clean_app_ctx(registry, app): yield ctx -@pytest.fixture(name="registry") -def _registry(): - return svc_reg.Registry() - - @pytest.fixture(name="container") def _container(clean_app_ctx): return svc_reg.flask._ensure_req_data()[1] @@ -54,35 +49,39 @@ def test_cleanup_added(self, registry): get() handles the case where there is already a cleanup registered. """ - cleanup1 = Mock(return_value=None) - cleanup2 = Mock(return_value=None) + cleanup1 = Mock() + cleanup2 = Mock() + + def factory1(): + yield Service1() + cleanup1() + + def factory2(): + yield Service2() + cleanup2() - registry.register_factory(Service1, Service1, cleanup=cleanup1) - svc_reg.flask.replace_factory(Service2, Service2, cleanup=cleanup2) + registry.register_factory(Service1, factory1) + svc_reg.flask.replace_factory(Service2, factory2) svc1 = svc_reg.flask.get(Service1) svc2 = svc_reg.flask.get(Service2) assert isinstance(svc1, Service1) assert isinstance(svc2, Service2) + assert 2 == len(flask.g.svc_container.cleanups) teardown(None) - cleanup1.assert_called_once_with(svc1) - cleanup2.assert_called_once_with(svc2) + cleanup1.assert_called_once_with() + cleanup2.assert_called_once_with() - def test_overwrite_value(self, registry, container): + def test_overwrite_value(self, registry): """ - It's possible to overwrite an already registered type. That type's - cleanup is removed. + It's possible to overwrite an already registered type. """ - - registry.register_value( - Interface, Interface(), cleanup=lambda _: None, ping=lambda _: None - ) + registry.register_value(Interface, Interface(), ping=lambda _: None) assert isinstance(svc_reg.flask.get(Interface), Interface) - assert [] != container.cleanups svc_reg.flask.replace_value(Interface, Service2()) @@ -93,13 +92,11 @@ def test_overwrite_factory(self, container): """ It's possible to overwrite an already registered type using a factory. """ - svc_reg.flask.replace_value( - Interface, Interface(), cleanup=lambda _: None, ping=lambda _: None + Interface, Interface(), ping=lambda _: None ) assert isinstance(svc_reg.flask.get(Interface), Interface) - assert [] != container.cleanups svc_reg.flask.replace_factory(Interface, Service2) @@ -120,7 +117,7 @@ def test_not_found(self): Trying to get a service that hasn't been registered raises a ServiceNotFoundError. """ - with pytest.raises(svc_reg.ServiceNotFoundError): + with pytest.raises(svc_reg.exceptions.ServiceNotFoundError): svc_reg.flask.get(Interface) def test_get_pingeable(self): @@ -139,12 +136,15 @@ def test_cleanup_purge_tolerant(self, container): If other svc_reg are registered, they are ignored by the cleanup purge. """ - svc_reg.flask.replace_factory( - Service1, Service1, cleanup=lambda _: None - ) - svc_reg.flask.replace_factory( - Service2, Service2, cleanup=lambda _: None - ) + + def factory1(): + yield Service1() + + def factory2(): + yield Service2() + + svc_reg.flask.replace_factory(Service1, factory1) + svc_reg.flask.replace_factory(Service2, factory2) svc_reg.flask.get(Service1) svc_reg.flask.get(Service2) @@ -160,19 +160,18 @@ def test_cleanup_purge_tolerant(self, container): assert 2 == len(container.cleanups) assert 0 == len(container.async_cleanups) - def test_teardown_warns_on_async_cleanups(self, container): + @pytest.mark.asyncio() + async def test_teardown_warns_on_async_cleanups(self, container): """ teardown() warns if there are async cleanups. """ - async def cleanup(svc): - pass + async def factory(): + yield Service1() - rs = svc_reg.RegisteredService( - Interface, lambda: Service1(), cleanup=cleanup, ping=None - ) + svc_reg.flask.replace_factory(Service1, factory) - container.add_cleanup(rs, rs.factory()) + await container.aget(Service1) with pytest.warns(UserWarning) as wi: teardown(None) diff --git a/tests/typing/core.py b/tests/typing/core.py index afdfee9..168124c 100644 --- a/tests/typing/core.py +++ b/tests/typing/core.py @@ -1,18 +1,34 @@ import contextlib import sys +from typing import AsyncGenerator, Generator + import svc_reg reg = svc_reg.Registry() + +def gen() -> Generator: + yield 42 + + +async def async_gen() -> AsyncGenerator: + yield 42 + + +def factory_with_cleanup() -> Generator[int, None, None]: + yield 1 + + reg.register_value(int, 1) reg.register_value(int, 1, ping=lambda: None) -reg.register_value(int, 1, cleanup=lambda _: None) +reg.register_value(int, gen) reg.register_factory(str, str) +reg.register_factory(int, factory_with_cleanup) reg.register_value(str, str, ping=lambda: None) -reg.register_value(str, str, cleanup=lambda _: None) +reg.register_value(str, async_gen) con = svc_reg.Container(reg) diff --git a/tests/typing/flask_.py b/tests/typing/flask_.py index 12ff938..cca02cf 100644 --- a/tests/typing/flask_.py +++ b/tests/typing/flask_.py @@ -1,8 +1,14 @@ +from typing import Generator + from flask import Flask import svc_reg +def factory_with_cleanup() -> Generator[int, None, None]: + yield 1 + + reg = svc_reg.Registry() app = Flask("tests") @@ -11,19 +17,16 @@ svc_reg.flask.replace_value(int, 1) svc_reg.flask.replace_value(int, 1, ping=lambda: None) -svc_reg.flask.replace_value(int, 1, cleanup=lambda _: None) svc_reg.flask.register_value(app, int, 1) svc_reg.flask.register_value(app, int, 1, ping=lambda: None) -svc_reg.flask.register_value(app, int, 1, cleanup=lambda _: None) svc_reg.flask.replace_factory(str, str) svc_reg.flask.replace_value(str, str, ping=lambda: None) -svc_reg.flask.replace_value(str, str, cleanup=lambda _: None) svc_reg.flask.register_factory(app, str, str) +svc_reg.flask.register_factory(app, int, factory_with_cleanup) svc_reg.flask.register_value(app, str, str, ping=lambda: None) -svc_reg.flask.register_value(app, str, str, cleanup=lambda _: None) # The type checker believes whatever we tell it. o1: object = svc_reg.flask.get(object)