Skip to main content

Very simple dependency injection

Project description

build workflow License: MIT uv-managed

INJECTINATOR

Very Simple Dependency Injection.

Here's the function, just paste it where you need it:

def injectinator(func):
    def wrapper(*args, **kwargs):
        replacements = dict(zip(
            func.__code__.co_varnames[:func.__code__.co_argcount],
            list(([None] * func.__code__.co_argcount) + list(func.__defaults__ or []))[-func.__code__.co_argcount:]
        ))
        for position, default in enumerate(replacements.keys()):
            if position >= len(args):
                if default not in kwargs and isinstance(replacements[default], type):
                    kwargs[default] = replacements[default]()
        return func(*args, **kwargs)
    return wrapper

Need some kind of dependency injection but don't want a massive library adding to your dependency graph that introduces potential supply chain attacks? Well, this is what you need.

This also doesn't need any imports to work. Just paste the script above here's an example of how to use it:

class BaseClass:
    # You don't really need this base class, but for the sake of method contracts it's here 
    def print(self):
        ...

class InjectedClass(BaseClass):
    # This will be the default injected class if nothing is provided in a call
    def print(self):
        print("Default class")


class SuppliedClass(BaseClass):
    # This will be a supplied class
    def print(self):
        print("Supplied class")


@injectinator
def test(injected=InjectedClass):
    # Can have as many parameters as you like, note that the class is a reference to the class, not an instance
    injected.print()

# Two calls, first creates an instance of InjectedClass and passes it to the function, second supplies an instance of SuppliedClass
test()
test(SuppliedClass())

Wait I hear you say: Can't we just create an instance of the class in the function specification like this?

def test(injected=InjectedClass()):
    ...

You can, but the instantiation is performed when Python creates the definition of the function, not at the time you run the function. Which means that the object it creates is re-used every time you call the function, which is not desirable.

Also, if you need to debug this, you might need to change it to:

import functools

def injectinator(func):
    @functools.wraps(func)
    def wrapper(*args, **kwargs):
        ...

This will retain the call stack and debugging easier. But as the script is below, you can add it without imports.

Can I Just Install This As A Dependency?

For sure, this is available on pypi:

pip install injectinator

And then in your code

from injectinator import injectinator

@injectinator
def myfun(...):
    ...

Notes on this script

  • It relies on dunder methods on func itself, so these could change in the future. They've all been clumped up together for this reason to make it obvious when it fails because the Python foundation changed their usage of dunders
  • There is no parameter support on the injected class, it could be adjusted to support them
  • All the config for the injection is in the function specification rather than the decorator
  • There's no types. It should have types I guess. Probably added to the two function specifications. Could be lazy and just make it all Any but then you'd have to drag in typing as an import for this gist
  • I have not timed this. It could be wildly inefficient. There's an iteration enumerating over the function parameter list so its O(n)
  • This script does work with arbitrary length argument specifications like adding *args to the end
  • Although it is DI in 11 lines, I've no interest in code golf.

Operation

  1. Figure out what the default values are for each argument, dump the results in replacements
  2. Skip supplied positional values
  3. For arguments with a default value that do not have a keyword supplied value and the default is a class type, create an instance and add to the keyword argument dictionary
  4. Invoke the wrapped function/method with positional and keyword values

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

injectinator-0.2.0.tar.gz (3.9 kB view details)

Uploaded Source

Built Distribution

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

injectinator-0.2.0-py3-none-any.whl (3.6 kB view details)

Uploaded Python 3

File details

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

File metadata

  • Download URL: injectinator-0.2.0.tar.gz
  • Upload date:
  • Size: 3.9 kB
  • Tags: Source
  • Uploaded using Trusted Publishing? No
  • Uploaded via: uv/0.11.8 {"installer":{"name":"uv","version":"0.11.8","subcommand":["publish"]},"python":null,"implementation":{"name":null,"version":null},"distro":{"name":"Ubuntu","version":"24.04","id":"noble","libc":null},"system":{"name":null,"release":null},"cpu":null,"openssl_version":null,"setuptools_version":null,"rustc_version":null,"ci":true}

File hashes

Hashes for injectinator-0.2.0.tar.gz
Algorithm Hash digest
SHA256 c4318d02b508f3f5bf96858ada7226b09f4ef83b7289822b968481f4b45e5177
MD5 90be8843d79e1da48d999796f9793bcb
BLAKE2b-256 d42c9ada8ca740fe3f8fc98243fa7cb88a95910bcc41968b00ab797d919492f8

See more details on using hashes here.

File details

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

File metadata

  • Download URL: injectinator-0.2.0-py3-none-any.whl
  • Upload date:
  • Size: 3.6 kB
  • Tags: Python 3
  • Uploaded using Trusted Publishing? No
  • Uploaded via: uv/0.11.8 {"installer":{"name":"uv","version":"0.11.8","subcommand":["publish"]},"python":null,"implementation":{"name":null,"version":null},"distro":{"name":"Ubuntu","version":"24.04","id":"noble","libc":null},"system":{"name":null,"release":null},"cpu":null,"openssl_version":null,"setuptools_version":null,"rustc_version":null,"ci":true}

File hashes

Hashes for injectinator-0.2.0-py3-none-any.whl
Algorithm Hash digest
SHA256 6aa26522157e8d05eed49e4971a48acb2358c9d0232f189b1c4130a02d09ee0b
MD5 b10bb939550c81c161504d084be9629c
BLAKE2b-256 d8c5fa3cf85f7ee9c7ec90248dfac26b53be6918ab7e865d3c420b77a6ee78f3

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