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.69k stars 295 forks source link

How to pass configuration dependency between containers. #746

Closed felixgao closed 9 months ago

felixgao commented 9 months ago

I am trying to pass a config object that I have between containers to bind the correct factory for each object types that I have.
when I run my code it get an AttributeError

❯ python containers.py
Traceback (most recent call last):
  File "/Users/ggao/github/experiments/extraction/extraction/containers.py", line 64, in <module>
    class GenOSOCRContainer(containers.DeclarativeContainer):  # pragma: no cover
  File "/Users/ggao/github/experiments/extraction/extraction/containers.py", line 68, in GenOSOCRContainer
    config.ocr.engine,
  File "src/dependency_injector/providers.pyx", line 847, in dependency_injector.providers.Dependency.__getattr__
AttributeError: Provider "Dependency" has no attribute "ocr"

my container.py has the following

class GenOSOCRContainer(containers.DeclarativeContainer):  # pragma: no cover
    config = providers.Dependency()
    ocr_client = providers.Selector(
        config.ocr.engine,
        paddle_ocr=providers.Factory(
            PaddleOCRProvider,
            parser=providers.Selector(
                config.ocr.parser,
                paddle_parser=providers.Factory(PaddleParser),
            ),
        ),
        url=config.ocr.url,
        timeout=config.ocr.timeout,
    )

class GenOSExtractionContainer(containers.DeclarativeContainer):  # pragma: no cover
    config = providers.Singleton(get_settings)

    ocr_container: GenOSOCRContainer = providers.Container(
        GenOSOCRContainer, config=config
    )

if __name__ == "__main__":
    container = GenOSExtractionContainer()
    container.wire(modules=[sys.modules[__name__]])
    print(container.config())
    print(container.ocr_container.ocr_client())

the get_settings function returns a pydantic setting object.

class OCRSettings(BaseSettings):
    engine: str = "Textract"
    parser: str = "PaddleWordBoxParser"
    url: AnyHttpUrl = "http://localhost:8000"
    timeout: int = 20

class Settings(BaseSettings):
    app_name: str = "OCR Extraction"
    ocr: OCRSettings = OCRSettings()

    model_config = SettingsConfigDict(
            env_file=THIS_DIR / ".env",
            env_file_encoding="utf-8",
            env_nested_delimiter="__",
            config_file=THIS_DIR / "config.yaml",
            frozen=True,
        )

    @classmethod
    def settings_customise_sources(
        cls,
        settings_cls: Type[BaseSettings],
        init_settings: PydanticBaseSettingsSource,
        env_settings: PydanticBaseSettingsSource,
        dotenv_settings: PydanticBaseSettingsSource,
        file_secret_settings: PydanticBaseSettingsSource,
    )-> tuple[PydanticBaseSettingsSource, ...]:
        return (
            init_settings,
            YamlConfigSettingsSource(settings_cls),
            env_settings,
            dotenv_settings,
            file_secret_settings,
        )

my yaml file configuration that loads into settings looks like

app_name: "Test Sample Extraction"
ocr:
  engine: "PaddleOCR"
  parser: "paddle_wordbox_parser"
  url: "http://0.0.0.0:8000/api/v1/ocr/extract/file"
  timeout: 60
felixgao commented 9 months ago

I think I figured it out, but it is not the most clean approach I think.

basically use the provided to get the actual value.

class GenOSOCRContainer(containers.DeclarativeContainer):  # pragma: no cover

    config = providers.Dependency(instance_of=OCRSettings)
    ocr_client = providers.Selector(
        config.provided.engine,
        paddle_ocr=providers.Factory(
            PaddleOCRProvider,
            parser=providers.Selector(
                config.provided.parser,
                paddle_wordbox_parser=providers.Factory(PaddleWordBoxParser),
            ),
            url=config.provided.url,
            timeout=config.provided.timeout,
        ),
    )