Description
Good afternoon,
First off - great job with this library. I'm really excited to be using it. I'm trying to use it to support dependency injection for Clean Architecture. I keep coming up against a perceived barrier for what I want to accomplish with this library, but I can't tell if I'm using the library wrong or if the library does not (should not?) support my use-case, and, in the latter case, what the right approach is.
BTW, I'm coming from Java land with Spring DI and Google Guice. This may have coloured my understanding significantly.
The project has this overall structure:
├── .....
├── __init__.py
├── adapter
│ ├── __init__.py
│ ├── byproduct_archives.py
│ ├── ...
│ ├── correction_parameter_caches.py
├── cli
│ ├── __init__.py
│ ├── cli.py
│ └── command_context.py
├── miniservice
│ ├── __init__.py
│ ├── cleanup
│ │ ├── __init__.py
│ │ ├── api.py
│ │ └── cleanup_usecase.py
│ ├── initialize
│ │ ├── __init__.py
│ │ ├── api.py
│ │ └── initialize_usecase.py
│ └── submit
│ ├── __init__.py
│ ├── api.py
│ ├── parameter_submission_usecase.py
├── .....
As you can see above, we have adapters
, usecase
, api
modules. api
classes have dependencies on usecase
classes, and usecase
classes have dependencies on adapter
classes. Kind of like this
byproduct_archives.py:
...
class SpecialByproductArchive:
...
def __init__(self, default_config_value: str):
self.default_config_value = default_config_value
def store(self, workflow_id):
print(f"do some stuff with {workflow_id} and {default_config_value}")
miniservice/initialize/initialize_usecase.py:
class InitializeUsecase:
def __init__(self, prelim_cache_port: SpecialByproductArchive)
self.prelim_cache_port = prelim_cache_port
def execute(workflow_id: str):
self.prelim_cache_port.store(workflow_id)
...
miniservice/initialize/api.py:
class InitializeApi:
def __init__(self, initialize_usecase: InitializeUsecase):
self.initialize_usecase: InitializeUsecase = initialize_usecase
def execute_default_usecase(self, workflow_id: str) -> None:
self.initialize_usecase.execute(workflow_id)
...
We also have a cli
module. __main__
is in the cli
module.
What I wanted to do was to 'wire up' (I use this term generally, but in a similar sense as to what is meant by this library's API) my application to 'inject' the dependencies of the different classes i.e. inject the adapter into the usecase and the usecase into the api, and then to make some function call in the cli
module with the wired up api
class by getting it pre-configured from the DI container.
@app.command() # some CLI library annotations
@click.pass_obj # some CLI library annotations
def initialize(command_context: CommandContext):
logger.info(f'{command_context.workflow_id}: initialize')
initialize_api: InitializeApi = ApiContainer.initialize_api()
initialize_api.execute(command_context.workflow_id)
You have some good examples of similar looking code, and they all accomplish what I am describing above by setting up their containers in a similar fashion. For example, in your multiple-containers example (https://github.com/ets-labs/python-dependency-injector/tree/master/examples/miniapps/application-multiple-containers/example);
class Services(containers.DeclarativeContainer):
config = providers.Configuration()
gateways = providers.DependenciesContainer()
user = providers.Factory(
services.UserService,
db=gateways.database_client,
)
.....
the db
dependency of the user is retrieved from the gateways
dependencies container which you are providing it from a different container:
class Application(containers.DeclarativeContainer):
config = providers.Configuration(yaml_files=["config.yml"])
core = providers.Container(
Core,
config=config.core,
)
gateways = providers.Container(
Gateways,
config=config.gateways,
)
services = providers.Container(
Services,
config=config.services,
gateways=gateways,
)
But I'm not sure I want to do all that. I would have an AdaptersContainer
, a UsecaseContainer
, and an ApiContainer
and I guess they would look something like this:
class AdapterContainer(containers.DeclarativeContainer):
# byproduct caches
byproduct_archive = providers.Singleton(
SpecialByproductArchive,
"config_val")
class UsecaseContainer(containers.DeclarativeContainer):
adapters = providers.DependencyContainer()
initialize_usecase = providers.Factory(
InitializeUsecase,
adapters.byproduct_archive)
class ApiContainer(containers.DeclarativeContainer):
usecases = providers.DependencyContainer()
initialize_api = providers.Singleton(
InitializeApi,
usecases.initialize_usecase)
if __name__ == "__main__":
apiContainer = ApiContainer(usecases=UsecaseContainer(adapters=AdapterContainer())
apiContainer.initializeApi().execute_default_usecase(workflow_id="some_id")
And as far as I know, something like that would work. I even figured out that I could get rid of the ApiContainer by updating the usecase with Markers and wiring the Api classes with the UsecaseContainer e.g.
miniservice/initialize/api.py:
class InitializeApi:
@inject
def __init__(self, initialize_usecase: InitializeUsecase = Provide["initialize_usecase"]):
self.initialize_usecase: InitializeUsecase = initialize_usecase
def execute_default_usecase(self, workflow_id: str) -> None:
self.initialize_usecase.execute(workflow_id)
...
if __name__ == "__main__":
usecaseContainer = UsecaseContainer(adapters=AdapterContainer())
usecaseContainer.wire("miniservice")
initialize_api = InitializeApi()
initialize_api.execute_default_usecase(workflow_id="some_id")
But if I wired the api objects, I could not figure out how to also wire the usecase objects. It feels like it should be possible to get by with just the AdapterContainer, because everything else has a... 'mapping' of sorts. Like... if I want to wire up the usecase classes with the adapter container, I know how to do that, too... but then I can't also wire up the api classes - or, if I can, there's not point, because I would still need to create the container and provide the correct... providers, before actually doing the wiring. You sort of need to have a container to wire something with this library, you know? Like... Is it possible to wire the *Api
classes and the *Usecase
classes with just an AdapterContainer
?
I'm not sure this made a lot of sense. I'm having some trouble describing my confusion very well. Do I sound like I'm totally off base here?