Skip to content

Looking for support with wiring #575

Open
@rowan-maclachlan

Description

@rowan-maclachlan

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?

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions