Skip to main content

Conject

This library automates DI - allowing to create dependencies by its declarative description. Features:

  • Creates application components hierarchy automatically.
  • Eliminates the need to implemenent config files support - allows you to configure any application component out of the box, select implementations and link them together.
  • Makes it easy to finalize components.
  • Supports synchronous and asynchronous interfaces.
  • Doesn't require components code modification (assuming they are written in DI-compatible style).

Example

Let's assume we have a simple http service:

class HttpClient(abc.ABC):
    @abc.abstractmethod
    def request(self, method: str, url: str):
        raise NotImplementedError


class MockHttpClient(HttpClient):
    def request(self, method: str, url: str):
        ...  # return fake response


class PooledHttpClient(HttpClient):
    def __init__(self, proxy: str, user_agent: str):
        ...

    def request(self, method: str, url: str):
        ...

    def __enter__(self):
        return self

    def __exit__(self, exc_type, exc_val, exc_tb):
        ...  # cleanup


class FbApiClient:
    def __init__(self, http_client: HttpClient, access_token: str, api_ver: str):
        ...

    def get_user_info(self, user_id: str) -> dict:
        ...  # use HttpClient to fetch user info


class FetchFbUserHandler:
    def __init__(self, fb_api_client: FbApiClient):
        ...

    def __call__(self, req):
        ...  # use FbApiClient.get_user_info and return


def create_http_app(fetch_fb_user_handler: FetchFbUserHandler):
    http_app = HttpApp([
        ('GET', '/fetch_fb_user/', fetch_fb_user_handler),
    ])
    return http_app

In order to fully construct our HttpApp we need to:

  • Add config option to choose HttpClient implementation.
  • Use ExitStack since PooledHttpClient is a context manager (while MockHttpClient isn't).
  • Add config options for proxy, user_agent, access_token, api_ver parameters.

conject lets you avoid doing it manually.

from conject import DepSpec, Impl


def run(config: dict) -> None:
    spec = DepSpec([
        Impl(Impl.Func,     'http_app',                 create_http_app),
        Impl(Impl.CtxMgr,   'pooled_http_client',       PooledHttpClient),
        Impl(Impl.Class,    'mock_http_client',         MockHttpClient),
        Impl(Impl.Class,    'fb_api_client',            FbApiClient),
        Impl(Impl.Class,    'fetch_fb_user_handler',    FetchFbUserHandler),
    ])

    with spec.start_container(config) as container:
        # get or create component with name 'http_app'
        #  expected type is optional, but allows type-checking
        http_app = container.get('http_app', HttpApp)

        ... # run_http_app(http_app)

Now we are able to control our components via configuration (formatted as toml for example):

[http_client]                 # component name, can be anything
-impl = 'pooled_http_client'  # use implementation named 'pooled_http_client'
user_agent = 'super-app/10'   # implementation params

[fb_api_client]
access_token = 'abcdefgh'
api_ver = '7.0'

Another possible config:

[proxied_http_client]
-impl = 'pooled_http_client'
proxy = 'my-squid:3128'

[proxied_fb_api_client]
-impl = 'fb_api_client'
http_client = {-ref = 'proxied_http_client'}  # use component named 'proxied_http_client'
access_token = 'fake_token'
api_ver = '7.0'

[http_app]
fb_api_client = {-ref = 'proxied_fb_api_client'}

conject allows some configuration parts to be omitted:

  • If your component name matches implementation name component's -impl property can be omitted.
  • You can omit implementation parameter. It will receive:
    • default value, if there is one in the factory signature;
    • component with the same name;
  • If there is no need to set any of component's properties you can omit it completely.

Registering implementations

Factory types:

  • FactoryType.Value - just value.
  • FactoryType.Func - function returning implementation.
  • FactoryType.Class - implementation-class.
  • FactoryType.GenFunc - function generating implementation that can finalize it afterwards.
  • FactoryType.CtxMgr - implementation context manager.
  • FactoryType.AFunc - like Func, but async.
  • FactoryType.AGenFunc - like GenFunc, but async.
  • FactoryType.ACtxMgr - like CtxMgr, but async.

Factory can receive any parameters except for variadic (*args, **kwargs) and positional-only.

To register your implementations you need to create DepSpec (or AsyncDepSpec) object.

Registration ways:

from conject import DepSpec, Impl

spec = DepSpec()  # you can pass impls to __init__

spec.add(Impl.CtxMgr,  'some_cls',  SomeCls)

spec.add_many([
    Impl(Impl.GenFunc,   'some_gen_func',   some_gen_func),
])

@spec.decorate(Impl.Func)
def some_func(some_cls: SomeCls):
    return 1

Creating a container

To create a container you need to enter context spec.start_container(config: Any), where config will be used to configure components.

Example:

def run(spec: DepSpec) -> None:
    config = {'auth_provider': {'-impl': 'google_auth_provider'}}
    with spec.start_container(config) as container:
        auth_provider = container.get('auth_provider')
        assert isinstance(auth_provider, GoogleAuthProvider)

Using a container

Dependency container lazily creates components and manages their lifetime.

def conject.Container.get(component_name: str) -> Any

async def conject.AsyncContainer.get(component_name: str) -> Any

You can fetch any component by calling get. It will create every component dependency during this call.

These created objects will be finalized after spec.start_container() context exit only. The order of finalization will be the reversed order of their creation.

Usually you will need only one container that runs for a whole program lifetime.

Components configuration

Config should be a dict in which every key describes one component. Value is also a dict with options:

  • -impl is a special option, it allows you to select implementation name for this component. It could be omitted.

  • All other options represents factory parameters. Besides simple values, you can use special ones:

    • {'-ref': '<component_name>'}

      Pass another component by its name.

    • {'-expr': '<expression>'}

      Pass result of evaluation of python-expression.

      The expression may depend on other components from refs namespace, e.g. refs.other_component.

      Example:

      import os
      spec.add(Impl.Value, 'environ', os.environ)
      spec.add(Impl.Class, 'fb_api_client', FbApiClient)
      

      Config (formatted as toml):

      [fb_api_client]
      access_token = {-expr = 'refs.environ["FB_API_ACCESS_TOKEN"]'}
      

Special options -ref/-expr can also be used inside lists and dicts (including nested).

Type checking

In case your factories are type-annotated, conject will check its parameters in runtime. This is done via pydantic library.

Also this allows you to pass e.g. datetime param from your toml configuration. To get the list of supported types see pydantic docs.

Caveat: your mutable parameter values (including dict/list/dataclass instances) will be rebuilt from scratch, so you may not receive the same instance you've expected.

Utils

def conject.utils.load_package_recursively(package_name: str) -> None

Recursively import all package's modules. Doesn't handle namespace packages for now.

def conject.utils.skip_type_check(value: Any) -> SkipTypeCheck

Ask conject to assume this value is correct as any param type.

Metadata

Release files for conject 0.4.0

For a detailed explanation of source distributions (sdists) and built distributions (wheels), please see the package formats documentation.

Source distribution (sdist)

Source distribution for conject 0.4.0
File Size Uploaded
conject-0.4.0.tar.gz 14.7 kB Details

Built distribution (wheel)

Table of built distributions (wheels) for conject 0.4.0
File Interpreter ABI Platform
conject-0.4.0-py3-none-any.whl Python 3 none any Details

Total release size: 29.4 kB

Release files / conject-0.4.0.tar.gz

Download URL conject-0.4.0.tar.gz
Size 14.7 kB
Tags Source
SHA-256 checksum
How to use checksums
050986a20fb49c692b07120974973f581c5129b1dbb48b5697e5268105ebef14
BLAKE2b-256 checksum
How to use checksums
b553b1ead0adb58a7225523d63295ea5b76f8a23e98a8348d3dc16e72d5dfabb
Upload date
Uploaded using Trusted Publishing?
What is trusted publishing?
No
Uploaded via twine/3.3.0 pkginfo/1.7.0 requests/2.25.1 setuptools/52.0.0 requests-toolbelt/0.9.1 tqdm/4.56.0 CPython/3.8.7

Release files / conject-0.4.0-py3-none-any.whl

Download URL conject-0.4.0-py3-none-any.whl
Size 14.6 kB
Tags Python 3
SHA-256 checksum
How to use checksums
958291129300468f3d7c650b9a8e6009221b314a830cb5723a4689ad97793443
BLAKE2b-256 checksum
How to use checksums
32827d9e5e8bb8dee9c82fcdd003c6330b4a6e3349bc77384769752881536cb7
Upload date
Uploaded using Trusted Publishing?
What is trusted publishing?
No
Uploaded via twine/3.3.0 pkginfo/1.7.0 requests/2.25.1 setuptools/52.0.0 requests-toolbelt/0.9.1 tqdm/4.56.0 CPython/3.8.7

Release history Release notifications | RSS feed

This release

0.4.0 This release

2 release files

Anthropic, PBC Visionary sponsor Bloomberg Visionary sponsor Hudson River Trading Visionary sponsor Meta Visionary sponsor NVIDIA Visionary sponsor Microsoft Sustainability sponsor Depot Continuous Integration AWS Cloud computing and Security Sponsor Datadog Monitoring Fastly CDN Google Download Analytics Sentry Error logging StatusPage Status page