punq

repository·main·Indexed 19 days ago

https://github.com/bobthemighty/punq

An unintrusive IOC container for Python 3.10+ that provides dependency injection without global state, decorators, or complex syntax. It supports automatic recursive injection via constructor inspection, singleton and transient scopes, child containers for scoped overrides, and full compatibility with type checkers like mypy and pyright.

Tokens
3.7K
Snippets
13
Records
14
Agent score
15%

What's inside punq

  1. How Punq dependency injection works

    main

    Punq is an unintrusive dependency injection library that avoids global state, decorators, and special syntax on arguments.

    To use Punq, you must explicitly create a punq.Container at your application's entrypoint. You then register dependencies (either specific instances or classes) with the container and use resolve() to retrieve them. Punq automatically handles recursive dependency injection: if a requested class has dependencies in its __init__ method, Punq will resolve and inject those dependencies automatically.

    import punq
    
    # 1. Create the container
    container = punq.Container()
    
    # 2. Register dependencies
    container.register(BaseService, ConcreteService)
    
    # 3. Resolve dependencies
    service = container.resolve(BaseService)
  2. Register singleton instances

    main

    To ensure the container returns the exact same object every time (a singleton pattern), register a specific instance using the instance keyword argument.

    class FileWritingGreeter:
        def __init__(self, path: str, greeting: str) -> None:
            self.path = path
            self.message = greeting
    
    # Create the single instance manually
    one_true_greeter = FileWritingGreeter("/tmp/greetings", "Hello world")
    
    # Register that specific instance
    container.register(FileWritingGreeter, instance=one_true_greeter)
  3. Provide arguments during resolution or registration

    main

    You can provide arguments to a dependency at two different stages:

    1. At Resolution: If you don't know the arguments at registration time, pass them to resolve().
    2. At Registration: If you want to bake specific arguments into the dependency definition, pass them to register().
    # Option 1: Provide arguments at resolution time
    container.register(Greeter, FileWritingGreeter)
    greeter = container.resolve(Greeter, path="/tmp/foo", greeting="Hello world")
    
    # Option 2: Provide arguments at registration time
    container.register(Greeter, FileWritingGreeter, path="/tmp/foo", greeting="Hello world")
  4. Register and resolve simple dependencies

    main

    You can register arbitrary objects using a string key or register classes to map an interface (base class) to a concrete implementation.

    import punq
    
    container = punq.Container()
    
    # Registering an object with a string key
    container.register("connection_string", instance="postgresql://...")
    conn_str = container.resolve("connection_string")
    
    # Registering a concrete implementation for an interface/base class
    class ConfigReader:
        def get_config(self) -> dict[str, str]: pass
    
    class EnvironmentConfigReader(ConfigReader):
        def get_config(self) -> dict[str, str]: return {"greeting": "Hello"}
    
    container.register(ConfigReader, EnvironmentConfigReader)
    config = container.resolve(ConfigReader).get_config()
  5. Handle forward references in registrations

    main

    If a service uses a string type annotation (a forward reference) in its constructor, Punq may fail to resolve it with an InvalidForwardReferenceError.

    To resolve this, you can:

    1. Register the dependency type in the container before registering the service that depends on it.
    2. Register the dependency using the exact string name as the service key.
    from dataclasses import dataclass
    from punq import Container
    
    @dataclass
    class Client:
        dep: 'Dependency'  # Forward reference
    
    container = Container()
    
    # Option 1: Register the type first
    class Dependency: pass
    container.register(Dependency)
    container.register(Client)
    
    # Option 2: Register using the string key
    class AlternativeDependency: pass
    container.register('Dependency', AlternativeDependency)
    container.register(Client)
    
    instance = container.resolve(Client)
  6. Configure service lifetime with Scope

    main

    Punq uses the Scope enum to control the lifetime of resolved objects:

    • Scope.transient (default): A fresh instance is created every time the service is resolved.
    • Scope.singleton: The same instance is created once and re-used for every subsequent resolve call.
    from punq import Container, Scope
    
    container = Container()
    
    # This will always return a new instance
    container.register(MyService, scope=Scope.transient)
    
    # This will always return the same instance
    container.register(MyService, scope=Scope.singleton)
  7. Register and resolve dependencies with automatic injection

    main

    Punq automatically inspects the __init__ method of registered classes to inject required dependencies.

    import punq
    
    class ConfigReader:
        def get_config(self) -> dict[str, str]: return {"greeting": "Hello world"}
    
    class Greeter:
        def __init__(self, config_reader: ConfigReader) -> None:
            self.config = config_reader.get_config()
    
        def greet(self) -> None: 
            print(self.config['greeting'])
    
    container = punq.Container()
    container.register(ConfigReader)
    container.register(Greeter)
    
    # Punq injects ConfigReader into Greeter automatically
    container.resolve(Greeter).greet()
  8. Instantiate unregistered services with Container.instantiate()

    main

    If you want to create an instance of a class that has not been explicitly registered in the container, but you still want Punq to resolve its dependencies, use Container.instantiate(service_key, **kwargs). This treats the class as a transient service for that specific call.

    from punq import Container
    
    class BirthdayNotification:
        def __init__(self, sender: EmailSender) -> None:
            self.sender = sender
    
    container = Container()
    # ... register EmailSender ...
    
    # Instantiate without explicit registration
    instance = container.instantiate(BirthdayNotification)
  9. Create child containers for scoped overrides

    main

    Use Container.child() to create a new container that inherits all registrations from the parent. Child containers are useful for providing different implementations in specific contexts, such as overriding dependencies for unit tests or providing per-request data in a web application without affecting the global container.

    from typing import NamedTuple
    from punq import Container
    
    class RequestData(NamedTuple):
        user_id: int
        is_admin: bool
    
    class RequestHandler:
        def __init__(self, state: RequestData): self.state = state
    
    app_container = Container()
    app_container.register(RequestHandler)
    
    # Create a child container for a specific request
    request_container = app_container.child()
    request_container.register(RequestData, instance=RequestData(123, True))
    
    # Resolving from the child uses the child's RequestData
    handler = request_container.resolve(RequestHandler)
    print(handler.state)  # RequestData(user_id=123, is_admin=True)
  10. Register dependencies with Container.register()

    main

    Use Container.register() to add services to the container. You can register a service in three primary ways:

    1. As a concrete service: If you pass only the service type, Punq treats the type itself as the implementation.
    2. With a factory: Pass a callable (like a class or a function) as the factory argument to define how the service is created.
    3. With an existing instance: Pass an object to the instance argument to register it as a singleton implementation.

    Arguments:

    • service: The key used for resolving the dependency (can be a type or a string).
    • factory: A callable used to create the service. Defaults to empty (triggers concrete registration).
    • instance: A pre-constructed object. If provided, it is registered as a Scope.singleton.
    • scope: Controls lifetime. Use Scope.transient (default) for fresh instances or Scope.singleton for re-using the same instance.
    • cache: Boolean indicating if the resolution should be cached within the current resolution context.
    • **kwargs: Additional arguments passed to the factory/constructor.
    from punq import Container, Scope
    
    container = Container()
    
    # 1. Register as concrete service
    container.register(FileReader)
    
    # 2. Register with a factory (implementation class)
    container.register(EmailSender, SmtpEmailSender)
    
    # 3. Register a singleton instance
    dal = SqlAlchemyDataAccessLayer(engine)
    container.register(DataAccessLayer, instance=dal)
  11. Resolve multiple implementations with Container.resolve_all()

    main

    If you have registered multiple implementations for the same service key, use Container.resolve_all(service_type, **kwargs) to retrieve all of them as a list. This is useful for patterns like plugin systems or middleware chains where multiple authenticators or handlers need to be processed.

    from punq import Container
    
    class Authenticator:
        def matches(self, req): return False
        def authenticate(self, req): return False
    
    class BasicAuthenticator(Authenticator): ...
    class TokenAuthenticator(Authenticator): ...
    
    container = Container()
    container.register(Authenticator, BasicAuthenticator)
    container.register(Authenticator, TokenAuthenticator)
    
    def authenticate_request(container, req):
        # Returns all registered Authenticator instances
        for authn in container.resolve_all(Authenticator):
            if authn.matches(req):
                return authn.authenticate(req)