Skip to main content

A dependency injection library for beginners and professionals

Project description

Junkie

Junkie is a Dependency Injection library for beginners and professionals.

Installation: pip install junkie

Example:

from junkie import Junkie


class App:
    def __init__(self, addressee):
        self.addressee = addressee

    def greets(self):
        return f"Hello {self.addressee}!"


context = {"addressee": "World"}

with Junkie(context).inject(App) as app:
    assert app.greets() == "Hello World!"

What is Dependency Injection, and why should we use it?

Dependency Injection is a design pattern in which all dependent objects are created separately and handed over from outside into the actual object. An object B depends on A if A calls a method of B. Don't worry - it sounds more complicated than it really is.

Example:
In traditional source code, object A creates B in the constructor or a method. That means it is hard to reuse B in other objects because the reference of B is only known by A. When using Dependency Injection, an independent software component creates B separately and hands it over to all objects which need it. This amazing software component is Junkie!

Finally, Dependency Injection helps you to implement highly decoupled and testable code.

How does Junkie work?

from junkie import Junkie

Before using Junkie you need to prepare the so-called context. This context is a Python dictionary, describing how objects get created or which pre-defined values to use. Every dictionary key represents an argument name. The corresponding value defines the constructor or function which assembles the requested object. A dictionary value can also provide a primitive value or a non-callable object.

Junkie also takes Python type hints into account. They are used if no mapping in the context for the argument name exists.

Additionally, Python lambdas can be used to adjust object construction.

from http.server import HTTPServer, SimpleHTTPRequestHandler

context = {
    "http_server": HTTPServer,  # constructor
    "server_address": ("0.0.0.0", 8080),  # pre-defined value
    "RequestHandlerClass": lambda: SimpleHTTPRequestHandler,  # pre-defined callable via lambda (special case)
}

Now, Junkie can create new objects and their dependencies. All dependencies are resolved via their argument name in the constructor. Only one object is created per argument name and is shared with all other objects.

with Junkie(context).inject(HTTPServer) as http_server:  # type: HTTPServer
    http_server.serve_forever()

Python context managers provide methods to prepare and finalize an object. All context managers are also handled in this way by Junkie.

Best practices

Use type hints for object construction

Junkie uses constructor-based dependency injection. The constructor gets all references to dependent objects, and saves them for later usage. The constructor should not do any work.

The argument names and their type hints are the easiest and recommended way to define object construction of dependencies. Junkie stores and reuses all objects by their argument name until the object is not required anymore. The context dictionary should be used to handle more complicated situations.

from junkie import Junkie


class Database:
    pass


class QueryHelper:
    def __init__(self, database: Database):
        self.database = database


class App:
    def __init__(self, database: Database, query_helper: QueryHelper):
        self.database = database
        self.query_helper = query_helper


with Junkie().inject(App) as app:  # type: App
    assert isinstance(app.database, Database)
    assert app.query_helper.database == app.database

Write integration tests with modified application context

After defining the application context it is very easy to replace individual objects with test doubles for integration tests.

import unittest

from junkie import Junkie

APPLICATION_CONTEXT = {
    "database_url": "postgresql://scott:tiger@localhost:5432/production",
}


class App:
    def __init__(self, database_url):
        self.database_url = database_url


def main():
    with Junkie(APPLICATION_CONTEXT).inject(App) as app:
        assert app.database_url.startswith("postgresql:")


class AppTest(unittest.TestCase):
    def test(self):
        test_context = APPLICATION_CONTEXT | {"database_url": "sqlite://"}

        with Junkie(test_context).inject(App) as app:
            self.assertEqual(app.database_url, "sqlite://")

Advanced usage

New object versus reuse an object

In general, all objects will be reused by their name. But, if we do not provide a name the object will not be reused.

from junkie import Junkie


class App:
    pass


context = {
    "app": App,
}

with Junkie(context).inject("app", App, "app") as (app1, app2, app3):
    assert app1 == app3
    assert app1 != app2 != app3

Adjust object construction via lambdas

The following example code shows various ways to adjust object construction via Python lambdas.

from junkie import Junkie


class App:
    def __init__(self, greeting: str):
        self.greeting = greeting


context = {
    # app1
    "app1": lambda: App("Hello Joe!"),
    # app2
    "greeting2": "Hello John!",
    "app2": lambda greeting2: App(greeting2),
    # app3
    "greeting3": lambda: "Hello Doe!",
    "app3": lambda greeting3: App(greeting3),
}

with Junkie(context).inject("app1", "app2", "app3") as (app1, app2, app3):
    assert app1.greeting == "Hello Joe!"
    assert app2.greeting == "Hello John!"
    assert app3.greeting == "Hello Doe!"

The _junkie argument name

If you need Junkie in one of your classes or functions, you can use the argument name _junkie. This argument name is reserved for the Junkie instance itself.

from contextlib import contextmanager

from junkie import Junkie


class SqlDatabase:
    pass


class FileDatabase:
    pass


class App:
    def __init__(self, database):
        self.database = database


@contextmanager
def provide_database(_junkie, url: str):
    if url.startswith("file:"):
        with _junkie.inject(FileDatabase) as database:
            yield database
    else:
        with _junkie.inject(SqlDatabase) as database:
            yield database


context = {
    "url": "file://local.db",
    "database": provide_database,
}

with Junkie(context).inject(App) as app:
    assert isinstance(app.database, FileDatabase)

Instantiate list items

Sometimes you need a list of objects. This list can be instantiated with the inject_list() helper function. It works similar to the Junkie.inject() method.

from junkie import Junkie, inject_list


class CustomerDataSource:
    def __init__(self, connection_string: str):
        pass


class ProductDataSource:
    pass


class SupplierDataSource:
    pass


class App:
    def __init__(self, data_sources):
        self.data_sources = data_sources


context = {
    "customer_ds": lambda: CustomerDataSource("sqlite://"),
    "data_sources": inject_list("customer_ds", ProductDataSource, SupplierDataSource),
}

with Junkie(context).inject(App) as app:
    assert isinstance(app.data_sources[0], CustomerDataSource)
    assert isinstance(app.data_sources[1], ProductDataSource)
    assert isinstance(app.data_sources[2], SupplierDataSource)

Callables as pre-defined context values

All requested context values are evaluated if they are callables. If you want to provide a callable object, wrap it via lambda expression.

from junkie import Junkie


class Database:
    def __call__(self, *args, **kwargs):
        return "called"


class App:
    def __init__(self, database):
        self.database = database


context = {
    "database": lambda: Database(),
}

with Junkie(context).inject(App) as app:
    assert app.database() == "called"

Built-in functions as context values are not supported

Unfortunately, built-in functions (implemented in C) like sqlite3.connect() can not be inspected. That's why they are not supported by Junkie as context values. Python lambdas help to work around this issue.

import sqlite3

from junkie import Junkie

context = {
    "database": ":memory:",
    "connection": sqlite3.connect,
    "working_connection": lambda database: sqlite3.connect(database)
}

# ValueError: no signature found for builtin <built-in function connect>
with Junkie(context).inject("connection") as connection:
    pass

with Junkie(context).inject("working_connection") as working_connection:
    pass

Collaboration

Get Involved

You are warmly welcome to contribute to Junkie. Just initiate a pull request or report an issue.

Authors

Junkie was written by Stefan Richter. Special thanks go to Erik Türke for his valuable feedback and many helpful code snippets.

Distribution

License

MIT License

See LICENSE for full text.

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

junkie-3.0.2.tar.gz (12.2 kB view details)

Uploaded Source

Built Distribution

junkie-3.0.2-py3-none-any.whl (14.9 kB view details)

Uploaded Python 3

File details

Details for the file junkie-3.0.2.tar.gz.

File metadata

  • Download URL: junkie-3.0.2.tar.gz
  • Upload date:
  • Size: 12.2 kB
  • Tags: Source
  • Uploaded using Trusted Publishing? Yes
  • Uploaded via: twine/5.0.0 CPython/3.12.3

File hashes

Hashes for junkie-3.0.2.tar.gz
Algorithm Hash digest
SHA256 3766aace648e9d9ce7f88553d236ea7347c504cb6fec93c5f02e12e597f631e2
MD5 51d94e2deea7c76991a65bd8d264aa28
BLAKE2b-256 67e19843610a77495ff1dc582f406597a6f45c4df85142c70162155368292818

See more details on using hashes here.

File details

Details for the file junkie-3.0.2-py3-none-any.whl.

File metadata

  • Download URL: junkie-3.0.2-py3-none-any.whl
  • Upload date:
  • Size: 14.9 kB
  • Tags: Python 3
  • Uploaded using Trusted Publishing? Yes
  • Uploaded via: twine/5.0.0 CPython/3.12.3

File hashes

Hashes for junkie-3.0.2-py3-none-any.whl
Algorithm Hash digest
SHA256 374d67298cb6759c3b8621c43a1a91e06a1777ac9158a206c73ec1cd3b6105bc
MD5 52cd5fcf0003e29bc1227df928b25228
BLAKE2b-256 929d4263cdd5e809b33a67a516e9ff43378a2e29ebd9e28ef0ccc35ffe05eb0a

See more details on using hashes here.

Supported by

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