adriangb / di

Pythonic dependency injection
https://www.adriangb.com/di/
MIT License
301 stars 13 forks source link

feat: Only autowire transitive edges between manually wired dependencies #88

Open adriangb opened 2 years ago

adriangb commented 2 years ago

di does auto wiring for dependencies which is super convenient to avoid boilerplate. But let's say you have something like:

class DBConnection:
    def __init__(self, host: str) -> None:
        ...

We (wrongly) assume that this can be constructed like DBConnection(host=str()). This is because we inspect the type annotation and autowire str itself!

We need some way of cutting off how deep we auto-wire. I think a sensible rule would be "all of the leaf dependencies (dependencies with no further dependencies) must be manually wired with:

mikedmcfarland commented 1 year ago

@adriangb some control of this would be nice, since I can imagine creating a class with no dependencies, using it within your project, and expecting it to get wired in for folks. I know I'd love this for builtin types, but maybe not so much my own.

I think a sensible default would be to check if its a leaf and if the type is part of builtins...

import inspect
import builtins
inspect.getmodule(str) == builtins    # True
inspect.getmodule(list) == builtins   # True
inspect.getmodule(dict) == builtins   # True

Here's example BindHook that helps a little bit here:

def match_all_builtins_and_error(
    param: Optional[inspect.Parameter], dependent: DependentBase[Any]
) -> Optional[DependentBase[Any]]:
    if (
        param is not None
        and param.default is param.empty
        and inspect.getmodule(param.annotation) is builtins
    ):
        return Dependent(wire=False, call=lambda: _raise(param=param, dependent=dependent))

    return None

def _raise(param: Optional[inspect.Parameter], dependent: DependentBase[Any]) -> Any:
    raise RuntimeError(
        f"The parameter {param} to {dependent.call} is a builtin which we don't want to autowire"
    )