Repository: hynek/svcs (language: python)

Issue:
Document & test multiple deps per type using Annotated
See discussion in #40


Discussion / hints:

Fix patch:
diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml
index 07c9f18..585b1ab 100644
--- a/.pre-commit-config.yaml
+++ b/.pre-commit-config.yaml
@@ -4,12 +4,12 @@ ci:
 
 repos:
   - repo: https://github.com/psf/black
-    rev: 23.7.0
+    rev: 23.9.1
     hooks:
       - id: black
 
   - repo: https://github.com/astral-sh/ruff-pre-commit
-    rev: v0.0.287
+    rev: v0.0.290
     hooks:
       - id: ruff
         args: [--fix, --exit-non-zero-on-fix]
diff --git a/docs/core-concepts.md b/docs/core-concepts.md
index 113116f..7428c1a 100644
--- a/docs/core-concepts.md
+++ b/docs/core-concepts.md
@@ -18,15 +18,15 @@ It is possible to register either factory callables or values:
 >>> import svcs
 >>> import uuid
 
->>> reg = svcs.Registry()
+>>> registry = svcs.Registry()
 
->>> reg.register_factory(uuid.UUID, uuid.uuid4)
->>> reg.register_value(str, "Hello World")
->>> uuid.UUID in reg
+>>> registry.register_factory(uuid.UUID, uuid.uuid4)
+>>> registry.register_value(str, "Hello World")
+>>> uuid.UUID in registry
 True
->>> str in reg
+>>> str in registry
 True
->>> int in reg
+>>> int in registry
 False
 ```
 
@@ -34,15 +34,54 @@ The values and return values of the factories don't have to be actual instances
 But the types must be *hashable* because they're used as keys in a lookup dictionary.
 
 
+### Multiple Factories for the Same Type
+
+Sometimes, it makes sense to have multiple instances of the same type.
+For example, you might have multiple HTTP client pools or more than one database connection.
+
+You can achieve this by using either {any}`typing.Annotated` (Python 3.9+, or in [*typing-extensions*](https://pypi.org/project/typing-extensions/)) or by using {any}`typing.NewType`.
+You can also mix and match the two.
+For instance, if you need a primary and a secondary database connection:
+
+% invisible-code-block: python
+%
+% primary_url = "sqlite:///:memory:"
+% secondary_url = "sqlite:///:memory:"
+
+```python
+from typing import Annotated, NewType
+
+from sqlalchemy import Connection, create_engine
+
+# Create the two connection engines
+primary_engine = create_engine(primary_url)
+secondary_engine = create_engine(secondary_url)
+
+# Create unique types for both with two different approaches
+PrimaryConnection = Annotated[Connection, "primary"]
+SecondaryConnection = NewType("SecondaryConnection", Connection)
+
+# Register the factories to the aliases
+registry.register_factory(PrimaryConnection, primary_engine.connect)
+registry.register_factory(SecondaryConnection, secondary_engine.connect)
+```
+
+The type and content of the annotated metadata ("primary") are not important to *svcs*, as long as the whole type is hashable.
+
+
 ### Cleanup
 
 It's possible to register a callback that is called when the *registry* is closed:
 
-% skip: next
+% invisible-code-block: python
+%
+% url = "sqlite:///:memory:"
 
 ```python
+engine = create_engine(url)
+
 registry.register_factory(
-    Connection, connection_factory, on_registry_close=engine.dispose
+    Connection, engine.connect, on_registry_close=engine.dispose
 )
 ```
 
@@ -59,7 +98,7 @@ You can also use a registry as an (async) context manager that (a)closes automat
 A **{class}`svcs.Container`** uses a {class}`svcs.Registry` to lookup registered types and uses that information to create instances and to take care of their life cycles:
 
 ```python
->>> container = svcs.Container(reg)
+>>> container = svcs.Container(registry)
 
 >>> uuid.UUID in container
 False
@@ -81,15 +120,15 @@ A container lives as long as you want the instances within to live -- for exampl
 If a factory takes a first argument called `svcs_container` or the first argument (of any name) is annotated as being {class}`svcs.Container`, the current container instance is passed into the factory as the first *positional* argument allowing for recursive service acquisition:
 
 ```python
->>> container = svcs.Container(reg)
+>>> container = svcs.Container(registry)
 
 # Let's make the UUID predictable for our test!
->>> reg.register_value(uuid.UUID, uuid.UUID('639c0a5c-8d93-4a67-8341-fe43367308a5'))
+>>> registry.register_value(uuid.UUID, uuid.UUID('639c0a5c-8d93-4a67-8341-fe43367308a5'))
 
 >>> def factory(svcs_container) -> str:
 ...     return svcs_container.get(uuid.UUID).hex  # get the UUID, then work on it
 
->>> reg.register_factory(str, factory)
+>>> registry.register_factory(str, factory)
 
 >>> container.get(str)
 '639c0a5c8d934a678341fe43367308a5'
diff --git a/pyproject.toml b/pyproject.toml
index 3d2e552..70aab6b 100644
--- a/pyproject.toml
+++ b/pyproject.toml
@@ -29,7 +29,12 @@ dependencies = [
 ]
 
 [project.optional-dependencies]
-tests = ["pytest", "pytest-asyncio", "sybil"]
+tests = [
+  "pytest",
+  "pytest-asyncio",
+  "sybil",
+  "typing-extensions; python_version<'3.9'",
+]
 typing = [
   "mypy>=1.4",
   "fastapi",


Test patch:
diff --git a/tests/helpers.py b/tests/helpers.py
index a484277..0539681 100644
--- a/tests/helpers.py
+++ b/tests/helpers.py
@@ -2,6 +2,14 @@
 #
 # SPDX-License-Identifier: MIT
 
+import sys
+
+
+if sys.version_info >= (3, 9):
+    from typing import Annotated
+else:
+    from typing_extensions import Annotated  # noqa: F401
+
 
 def nop(*_, **__):
     pass
diff --git a/tests/test_integration.py b/tests/test_integration.py
index b4ad49f..e6f3832 100644
--- a/tests/test_integration.py
+++ b/tests/test_integration.py
@@ -11,6 +11,7 @@
     asynccontextmanager,
     contextmanager,
 )
+from typing import NewType
 
 import pytest
 
@@ -21,7 +22,7 @@
     async_int_factory,
     async_str_gen_factory,
 )
-from .helpers import nop
+from .helpers import Annotated, nop
 from .ifaces import AnotherService, Interface, Service, YetAnotherService
 
 
@@ -65,6 +66,22 @@ def test_register_value_multiple(registry, container):
     assert [1, 2] == container.get(Service, AnotherService)
 
 
+S1 = Annotated[Interface, "s1"]
+S2 = NewType("S2", Interface)
+
+
+def test_get_annotated_multiple(registry, container):
+    """
+    It's possible to register multiple factories for the same type using
+    Annotated TypeAliases.
+    """
+    registry.register_factory(S1, Service)
+    registry.register_factory(S2, AnotherService)
+
+    assert isinstance(container.get(S1), Service)
+    assert isinstance(container.get(S2), AnotherService)
+
+
 def test_get_not_found(container):
     """
     Asking for a service that isn't registered raises a ServiceNotFoundError.
diff --git a/tests/typing/core.py b/tests/typing/core.py
index 477d87b..42f2139 100644
--- a/tests/typing/core.py
+++ b/tests/typing/core.py
@@ -5,11 +5,17 @@
 import contextlib
 import sys
 
-from typing import AsyncGenerator, Generator, Protocol
+from typing import AsyncGenerator, Generator, NewType, Protocol
 
 import svcs
 
 
+if sys.version_info >= (3, 9):
+    from typing import Annotated
+else:
+    from typing_extensions import Annotated
+
+
 reg = svcs.Registry()
 con = svcs.Container(reg)
 
@@ -130,3 +136,14 @@ async def ctx() -> None:
 
         async with contextlib.aclosing(svcs.Registry()):
             ...
+
+
+# Multiple factories for same type:
+S1 = Annotated[str, "s1"]
+S2 = NewType("S2", str)
+
+reg.register_value(S1, "foo")
+reg.register_value(S2, "bar")
+
+s1: str = con.get(S1)
+s2: str = con.get(S2)