Skip to main content

Minimal dependency injection framework

Project description

luckydep: A minimal dependency injection framework

luckydep is a minimal dependency framework.

Full type-hint on public interface, provide type safety for library user.

There are two way to use this library

  • luckydep.Container: friendly, common style in the domain of DI.
  • luckydep.Value: no runtime-type and more flexible, just a little hassle.

Motivation

Most popular DI frameworks in Python ecosystem have some design choices:

  • Use decorator on injected function/class for object wiring.
  • Declare container by inheriting some base-container class, and the order of object creation become explicit.

Highly integrate with user code, core use-cases will depend on the framework

While these design choices make it easier to test code, and reduce main lines of code in main module/program, these do have some drawback.

  • Core library now depends on particular framework, add unwanted noises.
  • Declaring order of objects become important, which reduce the benefits of DI.

So comes this library. Inspired by golang library github.com/samber/do, this library require user to do a little more stuff in main program, for limiting the dependency of DI framework into one place.

Usage by Container

Assuming we have a Service, providing use-case greeting, which need user to inject a Store instance and a prefix config.

class Store:
    def get_name(self, user_id: int) -> str:
        raise NotImplementedError()

class FaKeStore(Store):
    def __init__(self, records: dict[int, str]):
        self._records = records

    def get_name(self, user_id: int) -> str:
        return self._records[user_id]

class Service:
    def __init__(self, store: Store, prefix: str):
        self._store = store
        self._prefix = prefix

    def greeting(self, user_id: int) -> str:
        name = self._store.get_name(user_id)
        return f"{self._prefix}, {name}"

With the help of Container, we can register factory function and later invoke the required instance.

import luckydep

container = luckydep.Container()
container.provide(
    Service,
    lambda c: Service(
        store=c.invoke(Store),
        prefix=c.invoke(str, name="hello-prefix"),
    ),
)
container.provide(
    Store,
    lambda c: FaKeStore({7: "Alice"}),
)
container.provide(
    str,
    lambda c: "Hi",
    name="hello-prefix",
)

service = container.invoke(Service)
assert service.greeting(7) == "Hi, Alice"

Notice that the registration order of Service, Store (and the str "hello-prefix") is not important here.

Since that all factory function is evaluate lazily, the factory function can alway use c.invoke to ask another object, for which the factory function are not registered already.

For custom types, we usually create only one instance, so we don't give a explicit name, the name of the provided factory is "default" by default.

class Obj: ...

container = luckydep.Container()

# both usage are equivalent
container.provide(Obj, lambda c: Obj())
container.provide(Obj, lambda c: Obj(), name="default")

Usage by Value

Another way to use this library is to keep interested objects in different Value separately. This usage does not evaluate any type in runtime. Also the generic Value class is enough to achieve type-safety.

import luckydep

value_service = luckydep.Value[Service](
    lambda: Service(
        value_store.invoke(),
        "Hi",
    )
)
value_store = luckydep.Value[Store](
    lambda: FaKeStore(
        {3: "Bob"},
    )
)

service = value_service.invoke()
sentence = service.greeting(3)

assert sentence == "Hi, Bob"

Again, the order of value_service and value_store is not important here, since that the factory function is invoked lazily.

Also, because all object is already stored in different Value, named-provide/invoke mechanism is not available in value-based usage,

Limitation

No dependency cycle detection, this library just explode the stack and crash immediately at runtime if there exists a dependency cycle.

Unlike some other framework, No config-file/environment-variable provider, with the *provide` interface, it's easy to integrate with other library with a simple lambda object.

import os
import luckydep

os.environ["API_TOKEN"] = "some-token"

c = luckydep.Container()
# can use any config-file/environ/argument library you like here
c.provide(str, lambda c: os.environ.get("API_TOKEN", ""), name="api-token")

api_token = c.invoke(str, name="api-token")
assert api_token == "some-token"

Only return a singleton by invoke interface, since personally I think that's the most important usage of dependency injection. To create a new instance every time, we can register "factory function of some factory function". Although the faction function need to be represent by some class, see next limitation.

For container-based usage, we can not provide a type which are not exists at runtime. For example, mypy will complain when we want to provide a Callable[[int, int], int] function to the container. Although this still work well in runtime since these subscripted generic type instance is comparable and hashable. Likewise, interface defined by typing.Protocol won't pass mypy check either.

User need to establish a explicit type inheritance.

import luckydep

class BinOp:
    def __call__(self, a: int, b: int) -> int:
        raise NotImplementedError()

class Add(BinOp):
    def __call__(self, a: int, b: int) -> int:
        return a + b

c = luckydep.Container()
c.provide(BinOp, lambda c: Add(), name="add-func")

operator = c.invoke(BinOp, name="add-func")
assert operator(2, 3) == 5

Prefer value-based style if that's a problem.

Related Work

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

luckydep-0.2.0.tar.gz (5.9 kB view details)

Uploaded Source

Built Distribution

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

luckydep-0.2.0-py3-none-any.whl (4.6 kB view details)

Uploaded Python 3

File details

Details for the file luckydep-0.2.0.tar.gz.

File metadata

  • Download URL: luckydep-0.2.0.tar.gz
  • Upload date:
  • Size: 5.9 kB
  • Tags: Source
  • Uploaded using Trusted Publishing? No
  • Uploaded via: twine/6.0.1 CPython/3.12.4

File hashes

Hashes for luckydep-0.2.0.tar.gz
Algorithm Hash digest
SHA256 dca3c7dbdbd27795401f46ea0c673d06702e9a6da522596cbc7b34d3826b2aeb
MD5 f14f6eff7d0f89069eb8e77575b91319
BLAKE2b-256 a3aa15fc8919aaf13351bfe1261ffead3d1630848d151e608fe24a04094e37ef

See more details on using hashes here.

File details

Details for the file luckydep-0.2.0-py3-none-any.whl.

File metadata

  • Download URL: luckydep-0.2.0-py3-none-any.whl
  • Upload date:
  • Size: 4.6 kB
  • Tags: Python 3
  • Uploaded using Trusted Publishing? No
  • Uploaded via: twine/6.0.1 CPython/3.12.4

File hashes

Hashes for luckydep-0.2.0-py3-none-any.whl
Algorithm Hash digest
SHA256 daef06d27428de2112b2e7440cb5c316a27bea71f340bc02c11051e4b676b5b4
MD5 dbe332645bdf37f440d66bafa1c166c5
BLAKE2b-256 c071d727b875d8c9adb49824e2115b4e1240e82c4adabbee59083d286ef3a4bf

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