Skip to main content

Dependency Injection framework for Python

Project description

InjectionKit

Dependency Injection framework for Python.

This project is in development and issues and PRs are welcome. Please feel free to share if you've got any ideas or encountered with any problems.

Installing

If you're using pip, you can install this package by

pip install injectionkit

you can also install it with your favourable package manager. For example, with uv:

uv add injectionkit

at least Python 3.10 is required.

Usage

This package is inspired by Uber's fx library in Go, and it adopts similar usage. For example, you can use Provider, Supplier and Consumer to define dependencies and how to inject them, and use App to wire things up.

Simple usage

from injectionkit import App, Consumer, Provider


def test_simple() -> None:
    """
    The simplest usage of this library.
    """

    # A function returning a value can be used as a provider.
    #
    # If the function receives parameters, they will be analyzed and injected automatically, based on their type
    # annotations. The returned value will be registered as a dependency and resolved when needed.
    def message() -> str:
        return "Hello, world!"

    # A function receiving a value or some values can be used as a consumer.
    #
    # The parameters are analyzed and injected automatically, based on their type annotations. The returned value will
    # be ignored, since we don't expect a Consumer to provide something.
    def check(message: str) -> None:
        assert message == "Hello, world!"

    # The `App` class wires up all the options, including Providers, Suppliers and Consumers.
    #
    # The Providers and Suppliers are registered, and will only be called when needed. Contrararily, the Consumers are
    # called immediately when the App is `run`.
    #
    # The order of the Providers and Suppliers is not guaranteed because of the resolution procedure, but the Consumers
    # is called in the order they were passed into the App. To be more precise, even if the Consumers are passed in with
    # some Providers and Suppliers in between of them, the Providers and Suppliers will not be called until resolution,
    # while the Consumers will be called when `App.run()`.
    App(
        Provider(message),
        Consumer(check),
    ).run()

Compositions

Compositions between dependencies are must-have:

from dataclasses import dataclass

from injectionkit import App, Consumer, Provider, Supplier


# Here we define a dataclass, whose constructor (`__init__()`) is automatically created, by the `@dataclass` decorator.
#
# To be more precise, the constructor signature is: `def __init__(self, name: str, age: int) -> None`.
@dataclass(frozen=True)
class Person(object):
    name: str
    age: int


def test_composition() -> None:
    """
    A usecase which shows how the framework wires things up.
    """

    # A simple function factory, which produces a `str`.
    def name() -> str:
        return "Cylix"

    # A simple consumer function, which requires a `Person`.
    def check(person: Person) -> None:
        assert person.name == "Cylix"
        assert person.age == 23

    App(
        # Provides a `str` to the framework through the function.
        Provider(name),
        # Supplies an `int` to the framework.
        #
        # Suppliers receives specific values (instances) rather than functions or classes. It just inject a value into
        # the framework.
        Supplier(23),
        # Provides a `Person` to the framework through the class name.
        #
        # Internally, InjectionKit will use the constuctor of this class to analyze and inject the dependencies. The
        # `Person` constructor requires a `str` and an `int` as arguments, the former is provided by the
        # `Provider(name)`, and the latter is supplied by `Supplier(23)`.
        #
        # When `singleton=True`, the `Person` instance will be cached in the framework after the first initialization.
        # When another consumer tries to consume a `Person`, the instance will be reused and the constructor will NOT be
        # called again.
        Provider(Person, singleton=True),
        # Consumes a `Person` from the framework.
        Consumer(check),
    ).run()

Multivalues

You can inject values of the same type one-by-one, and resolve them at once.

from injectionkit import App, Consumer, Provider, Supplier


def test_multivalues() -> None:
    """
    Demonstrates the multivalue resolution.
    """

    # A simple provider that produces a `str`.
    def last_name() -> str:
        return "Lee"

    # A consumer that consumes a list of `str`s.
    #
    # When no `list[str]` is provided, the framework tries to resolve `str`s, and make them a list. If there's no more
    # than 2 instances of `str`, the framework will raise a `MissingDependencyError`.
    #
    # This behavior is not performed if a `list[str]` dependency is provided.
    def check(name: list[str]) -> None:
        # NOTICE: "Cylix" supplied by `Supplier` is before the "Lee" provided by `Provider`.
        #
        # This is because the framework first searches the instances, which is supplied by the `Supplier`s. It will
        # continue to search the `Provider`s to avoid unexpected shadowing. If there's multiple values after searching,
        # they'll be stored into a list. This is the behavior of the `DependencyContainer`.
        assert name == ["Cylix", "Lee"]

    App(
        Provider(last_name),
        Supplier("Cylix"),
        Consumer(check),
    ).run()

Labels

And, to distinguish values of the same type, you can use Annotated from stdlib typing module, and labels of str type.

from dataclasses import dataclass
from typing import Annotated

from injectionkit import App, Consumer, Provider, Supplier


@dataclass(frozen=True)
class Person(object):
    name: str
    age: int


def test_labels() -> None:
    """
    Demonstrating how to use labels to distinguish dependencies of the same type.
    """

    # A provider of `Person`.
    #
    # This function requires two annotated arguments: a `str` with label "name", and an `int` with label "age".
    # InjectionKit allows functions and constructors to depend on annotated parameters, when multiple `str`s are
    # provided, only those with the label "name" will be passed into this function.
    #
    # When multiple labels are required, for example, an `Annotated[str, "a", "b"]` is required, only those instances
    # CONTAINING the labels will be passed in. For example, an `Annotated[str, "a", "b", "c"]` will be passed in because
    # it CONTAINS "a" and "b".
    def born(name: Annotated[str, "name"], age: Annotated[int, "age"]) -> Person:
        return Person(name, age)

    # A provider of `str` with label "name".
    #
    # A dependency can declare the label of the returned value using an `Annotated` return type. The `Annotated` is from
    # the stdlib `typing` module, and it's compatible with the underlying type while making the type checker happy.
    #
    # This design is intuitive and makes minimal changes to the provider, while maintaining readability.
    def name() -> Annotated[str, "name"]:
        return "Cylix"

    # A consumer.
    #
    # It just checks the fields of the `Person` passed in.
    def check(person: Person) -> None:
        assert person.name == "Cylix"
        assert person.age == 23

    App(
        # Provides `Annotated[str, "name"]`
        Provider(name),
        # Supplies a `str`, which will NOT be used by `born`.
        Supplier("unrelated str"),
        # Supplies an `int`, which will NOT be used by `born`.
        Supplier(42),
        # Supplies an `Annotated[int, "age"]`.
        #
        # `Provider`s and `Supplier`s can specify the `regard=type`, to achieve polymorphism. The framework will see the
        # factory or instance as the way to retrieve the `regard` type instead of the concrete type.
        Supplier(23, regard=Annotated[int, "age"]),
        # A provider of `Person`, which requires two annotated arguments.
        #
        # `Provider(name)` and `Supplier(23, regard=Annotated[int, "age"])` will be passed in.
        Provider(born),
        # The check consumer.
        Consumer(check),
    ).run()


def test_multi_labels() -> None:
    """
    Demonstating how to use multiple labels.
    """

    # A consumer requiring multiple `str`s with label "a" and "b".
    #
    # Because we won't inject `list[str]` directly, the framework will try to resolve `str`s with the labels instead. As
    # we mentioned before, the `str`s CONTAINING "a" and "b" will be injected, which means a `str` with "a", "b", "c"
    # will be passed in, while one with only "a" won't.
    #
    # BTW, the `list[Annotated[...]]` form is not accepted by this framework. The `Annotated` annotation must be placed
    # at the very outside.
    def hello(names: Annotated[list[str], "a", "b"]) -> None:
        assert names == ["Cylix", "Lee"]

    App(
        Supplier("some random str", regard=Annotated[str, "a"]),  # does NOT contain "b". unused.
        Supplier("Cylix", regard=Annotated[str, "a", "b"]),  # CONTAINS "a" and "b", passed in.
        Supplier("Lee", regard=Annotated[str, "a", "b", "c"]),  # CONTAINS "a" and "b", passed in.
        Consumer(hello),
    ).run()

For more examples, see the tests folder.

Project details


Download files

Download the file for your platform. If you're not sure which to choose, learn more about installing packages.

Source Distribution

injectionkit-1.3.3.tar.gz (17.6 kB view details)

Uploaded Source

Built Distribution

If you're not sure about the file name format, learn more about wheel file names.

injectionkit-1.3.3-py3-none-any.whl (18.3 kB view details)

Uploaded Python 3

File details

Details for the file injectionkit-1.3.3.tar.gz.

File metadata

  • Download URL: injectionkit-1.3.3.tar.gz
  • Upload date:
  • Size: 17.6 kB
  • Tags: Source
  • Uploaded using Trusted Publishing? Yes
  • Uploaded via: uv/0.9.2

File hashes

Hashes for injectionkit-1.3.3.tar.gz
Algorithm Hash digest
SHA256 e65eee3d9d79e5ace0e5b254129794767a35f02dff5b240ad47ad63a6fb1cf54
MD5 b8b0b311683ad7586c0ce18980e2d907
BLAKE2b-256 aea7c647d896e729d163dee2d1f5b263d653ed1541957aa9c62ad13b966b02c1

See more details on using hashes here.

File details

Details for the file injectionkit-1.3.3-py3-none-any.whl.

File metadata

File hashes

Hashes for injectionkit-1.3.3-py3-none-any.whl
Algorithm Hash digest
SHA256 3ec90f7acac25a0441778e276a5c3e6f890530f3a646e41f927513e3b05624fd
MD5 8713c0035d640305edb4a2a633e082d3
BLAKE2b-256 c57253f63bb7b3d335f3b648f77615180afa25c8aab9cfc5fb570fdeb4d91d08

See more details on using hashes here.

Supported by

AWS Cloud computing and Security Sponsor Datadog Monitoring Depot Continuous Integration Fastly CDN Google Download Analytics Pingdom Monitoring Sentry Error logging StatusPage Status page