svcs Documentation

repository·main·Indexed 19 days ago

https://github.com/hynek/svcs

A flexible dependency container for Python that implements Inversion of Control (IoC) through dependency injection or service location. It features automatic resource cleanup via context managers, static type safety for tools like mypy and pyright, and support for autowiring since version 26.1.0. svcs provides seamless integration for web frameworks including FastAPI, Flask, AIOHTTP, and Starlette, allowing for decoupled service acquisition without global state.

Tokens
19.1K
Snippets
56
Records
84
Agent score
64%

What's inside svcs

  1. What is svcs?

    main

    svcs (pronounced services) is a dependency container for Python designed to implement Inversion of Control (IoC) via dependency injection or service location. It provides a central registry for service factories, allowing you to acquire instances of specific types/interfaces with automatic cleanup and built-in health checks.

    Key features include:

    • Automatic Cleanup: Services are managed via context managers, ensuring they are cleaned up automatically when the acquisition context ends.
    • Static Type Safety: It provides full support for static type checkers (like mypy or pyright) by forwarding requested types, though it does not perform runtime type checking.
    • Loose Coupling: Simplifies testing by decoupling service acquisition from implementation.
    • Monitoring: Supports live introspection and monitoring through health checks.
    • No Global State: Avoids the use of global state, decorators, or function signature mangling.
  2. Explore svcs framework integrations

    main

    svcs provides out-of-the-box integrations for several popular Python web frameworks. These integrations allow you to use svcs services within the lifecycle and dependency injection patterns of your chosen framework.

    Currently supported frameworks include:

    • aiohttp
    • fastapi
    • flask
    • starlette
  3. What is a Service in svcs?

    main

    In the context of svcs, a Service is a local object managed by the library that is loosely coupled to your application code. Examples include database connections, web API clients, or caches.

    Key characteristics of a service:

    • Behavioral, not stateful: Services are "pure doers" (e.g., querying a DB, making HTTP requests, deleting files).
    • No business state: They should not hold state relevant to business logic; they simply pass data to and from the domain model.
    • Configuration required: They often require setup before use and should not be instantiated directly within business logic.
  4. What is svcs and how does it work?

    main

    Concept: Service Locator

    svcs is a service locator that provides a unified API for storing and retrieving objects (services) from your application's request objects or application state. It allows you to manage configurable dependencies in one central place, ensuring they are acquired consistently and cleaned up automatically.

    Key Mental Models:

    • Service: A configurable dependency (e.g., a database connection or a web API client).
    • Late Binding: You only ask for a service when you know you need it. This avoids wasteful pre-instantiation and simplifies resource management.
    • Dependency Inversion: You should register concrete factories for abstract interfaces (using Python Protocol or abc.ABC). In a Hexagonal Architecture, the registered types are ports and the factories produce adapters.
    • Composition Root: To maintain clean architecture, use svcs at your application's entry point (e.g., a web view) to look up a service and then pass that service into your business logic. This is Dependency Injection. If you call get() directly inside your business logic, you are performing Service Location, which is harder to test.
  5. What is autowiring and how does it work?

    main

    Autowiring is an optional technique introduced in version 26.1.0 that automatically resolves dependencies based on type annotations. Instead of manually calling svcs.Container.get() for every dependency in a factory, you can use svcs.autowire (or svcs.aautowire for async) to inject services directly into function or class parameters.

    Key Behaviors

    • Parameter Support: Handles regular, positional-only, and keyword-only parameters. It ignores variadic parameters (*args and **kwargs).
    • Missing Services: If a parameter's type cannot be resolved in the registry, the autowirer will use the parameter's defined default value.
    • Container Injection: If a parameter is annotated with svcs.Container, the autowirer injects the current container instance, allowing for dynamic lookups.
    • Async Support: svcs.aautowire is used for asynchronous resolution. It works with both synchronous and asynchronous callables and uses svcs.Container.aget() internally.
    >>> from dataclasses import dataclass
    >>> from typing import NewType
    >>> import svcs
    
    >>> @dataclass
    ... class Database: ...
    >>> @dataclass
    ... class Cache: ...
    
    >>> registry = svcs.Registry()
    >>> registry.register_factory(Database, Database)
    >>> registry.register_factory(Cache, Cache)
    
    >>> @dataclass
    ... class AppServices:
    ...     db: Database
    ...     cache: Cache
    
    >>> # Using autowire to register the factory
    >>> AfterAppServices = NewType("AfterAppServices", AppServices)
    >>> registry.register_factory(AfterAppServices, svcs.autowire(AppServices))
  6. What are Registries and Containers in svcs?

    main

    To understand how svcs works, you need to understand two core concepts: registries and containers. They have different lifecycles and responsibilities.

    • Registries (svcs.Registry): These are responsible for storing and retrieving factories for specific types. A registry should typically live for the entire duration of your application, and there is usually only one per application.
    • Containers: (Details provided in subsequent documentation segments).
  7. How to register multiple factories for the same type

    main

    If you need multiple instances of the same type (e.g., multiple database connections or HTTP client pools), you must differentiate them so the registry can distinguish them by type. You can do this using one of the following methods:

    1. Subclassing: Create a new class that inherits from the base type. This is the most compatible method across all type checkers.
    2. typing.NewType: Create a distinct type alias. This works with Mypy, ty, and Pyrefly, but not with Pyright.
    3. typing.Annotated: Use metadata to differentiate types. This works with Mypy, ty, and Pyrefly, but not with Pyright.

    Note: The type keyword introduced in PEP 695 is currently not supported.

    Example of multiple connection engines:

    from typing import Annotated, NewType
    from sqlalchemy import Connection, create_engine
    
    # Setup engines
    primary_engine = create_engine("sqlite:///:memory:")
    secondary_engine = create_engine("sqlite:///:memory:")
    tertiary_engine = create_engine("sqlite:///:memory:")
    
    # 1. Subclassing (Universal compatibility)
    class PrimaryConnection(Connection):
        pass
    
    # 2. NewType (Works with Mypy/ty/Pyrefly, NOT Pyright)
    SecondaryConnection = NewType("SecondaryConnection", Connection)
    
    # 3. Annotated (Works with Mypy/ty/Pyrefly, NOT Pyright)
    TertiaryConnection = Annotated[Connection, "tertiary"]
    
    # Registering to the registry
    registry.register_factory(PrimaryConnection, primary_engine.connect)
    registry.register_factory(SecondaryConnection, secondary_engine.connect)
    registry.register_factory(TertiaryConnection, tertiary_engine.connect)
  8. How to use svcs: Service Location vs. Dependency Injection

    main

    svcs implements the Service Locator pattern. While you use the locator to find services, the recommended way to use the library is to combine it with Dependency Injection to keep your business logic clean and testable.

    1. Composition Root: Use your entry point (e.g., a web view, CLI command, or test fixture) as a composition root. Use svcs here to locate the required services.
    2. Dependency Injection: Pass those located services as arguments into your Service Layer functions. This ensures your business logic doesn't depend on the svcs registry itself.

    Avoid: Service Location in the Service Layer

    Do not call svcs directly inside your business logic/service layer. This makes the code harder to test and reason about because the dependencies are hidden inside the function body rather than being explicitly declared in the signature.

    # RECOMMENDED: View acts as Composition Root, injecting services into the Service Layer
    def view(request):
        """View and composition root."""
        # Locate the service using svcs
        db = svcs_from(request).get(Database)
        # Inject the service into the business logic
        return do_something(db)
    
    
    def do_something(db):
        """Service layer (Business Logic)."""
        # This function is easy to test because 'db' is just a parameter
        db.do_database_stuff()
  9. Understand the purpose of type hint tests

    main
    The files in the typing_tests/ directory are not intended to be executed as runnable code. Instead, they serve as a suite of type-checking exercises designed to validate the project's type interfaces. They are meant to be consumed by supported type checkers (like Mypy or Pyright) to ensure type correctness across the library's surface.
  10. Understand Late Binding and ServiceNotFoundError

    main

    svcs uses Late Binding, meaning the concrete instance of a service is only determined at runtime when svcs.Container.get() is called.

    Pros:

    • High testability: You can easily swap real services for mocks/test doubles in your composition root.

    Cons/Risks:

    • If the registry is not configured to provide the requested type, a svcs.exceptions.ServiceNotFoundError will be raised at runtime.
  11. Managing service cleanup and lifecycles

    main

    The container automatically handles cleanup for services that require it:

    1. Context Managers: If a factory returns a context manager, it is immediately entered, and the instance is added to the container's cleanup list.
    2. Generators: If a factory is a generator that yields the instance, it is automatically wrapped in a context manager.
    3. Async Support: Async context managers and async generators are supported similarly.

    To trigger cleanup (e.g., closing files or database connections), call svcs.Container.close(). You can also use the container itself as an (async) context manager to ensure automatic cleanup upon exit.

    >>> reg = svcs.Registry()
    >>> def clean_factory() -> str:
    ...     yield "Hello World"
    ...     print("Cleaned up!")
    
    >>> reg.register_factory(str, clean_factory)
    >>> with svcs.Container(reg) as con:
    ...     _ = con.get(str)
    Cleaned up!
  12. How svcs.Container works

    main

    A svcs.Container uses a svcs.Registry to look up registered types and manage their lifecycles. When you call get() or aget(), the container creates instances and caches them. Subsequent calls for the same type return the exact same instance (singleton behavior within the container's lifetime).

    A container typically lives for the duration of a specific scope, such as a single web request.

    Note on Integrations: Many integrations provide svcs_from() to extract the current container from the environment and a get()/aget() helper to retrieve services transparently. Depending on the framework, you may need to pass the current request object to these functions.

    >>> container = svcs.Container(registry)
    >>> u = container.get(uuid.UUID)
    >>> # Calling get() again returns the SAME instance!
    >>> u is container.get(uuid.UUID)
    True