ets-labs / python-dependency-injector

Dependency injection framework for Python
https://python-dependency-injector.ets-labs.org/
BSD 3-Clause "New" or "Revised" License
3.89k stars 304 forks source link

Options pattern #582

Open panicoenlaxbox opened 2 years ago

panicoenlaxbox commented 2 years ago

Hello,

I'm a happy user of your package, it's awesome. I come from the .NET stack and find it very useful in Python.

I don't know if it is available or not in the package, but I miss a couple of things that I used a lot in .NET applications.

The first one is the possibility of loading configuration from a .json file. I know that I can use .ini, .yaml, and other formats, but I think it would be good to support .json also.

The second one, and the most important in my opinion, is don't have something similar to this out-of-the-box https://docs.microsoft.com/en-us/aspnet/core/fundamentals/configuration/options?view=aspnetcore-6.0

The idea behind this, it's to allow to inject "part" of the configuration like an object. For now, I'm using a workaround that works perfectly.

config.json

{
  "foo": {
    "bar": "bar",
    "baz": "baz"
  },
  "qux": "qux"
}

foo_options.py

from dataclasses import dataclass

@dataclass
class FooOptions:
    bar: str
    baz: str

containers.py

import json

from dacite import from_dict
from dependency_injector.containers import DeclarativeContainer, WiringConfiguration
from dependency_injector import providers

from foo_options import FooOptions

def _create_foo_options() -> FooOptions:
    with open("config.json") as f:
        return from_dict(FooOptions, json.loads(f.read())["foo"])  # using dacite package https://github.com/konradhalas/dacite

class Container(DeclarativeContainer):
    wiring_config = WiringConfiguration(
        modules=["__main__"]
    )

    foo_options = providers.Singleton(_create_foo_options)

main.py

from dependency_injector.wiring import inject, Provide

from containers import Container
from foo_options import FooOptions

@inject
def main(foo_options: FooOptions = Provide[Container.foo_options]):
    print(foo_options)  # FooOptions(bar='bar', baz='baz')

if __name__ == '__main__':
    container = Container()
    main()

I would like to know your opinion about this. What I want to avoid is having a big object with all my configuration when a class only needs to be aware of a little part. I know that I can inject concrete values of config using Configuration provider or even the whole config like a dict, but I want to use dataclasses with types safety and don't use primitive values.

Congrats for your great job, I couldn't live without this package :)

panicoenlaxbox commented 2 years ago

After writing my issue, I have been thinking about it and with a few changes, I can achieve the same result in a more best way, I think. With this solution, I am using the proposed configuration provider but also, I have an options pattern implementation. The best of both worlds.

containers.py

import json
from typing import Dict, Any

from dacite import from_dict
from dependency_injector import providers
from dependency_injector.containers import DeclarativeContainer, WiringConfiguration

from foo_options import FooOptions

def _from_json() -> Dict[str, Any]:
    with open("config.json") as f:
        return json.loads(f.read())

def _create_foo_options(config: Dict[str, Any]) -> FooOptions:
    return from_dict(FooOptions, config["foo"])

class Container(DeclarativeContainer):
    wiring_config = WiringConfiguration(
        modules=["__main__"]
    )
    config = providers.Configuration()
    config.from_dict(_from_json())
    # This works, but I think it's better to use the dacite library for complex objects
    # foo_options = providers.Singleton(FooOptions, bar=config.foo.bar, baz=config.foo.baz)
    foo_options = providers.Singleton(_create_foo_options, config)

main.py

from typing import Any, Dict

from dependency_injector.wiring import inject, Provide

from containers import Container
from foo_options import FooOptions

@inject
def main(config: Dict[str, Any] = Provide[Container.config], foo_options: FooOptions = Provide[Container.foo_options]):
    print(config)  # {'foo': {'bar': 'bar', 'baz': 'baz'}, 'qux': 'qux'}
    print(foo_options)  # FooOptions(bar='bar', baz='baz')

if __name__ == '__main__':
    container = Container()
    main()