Dependency Injection Improvements
This document explains recent refactors that introduce dependency injection (DI) for configuration and improve service injection patterns across the project. The primary goal is to reduce direct environment and concrete dependency coupling, improving testability, modularity, and maintainability.
What Changed
- Added
IConfigServiceinterface and a defaultEnvConfigServiceimplementation.- Location:
farm/core/services/interfaces.py,farm/core/services/implementations.py - Exported via
farm/core/services/__init__.py
- Location:
- Refactored modules that previously read environment variables directly to accept a
config_service(or useEnvConfigServiceby default):farm/charts/llm_client.pynow acceptsconfig_serviceand no longer readsOPENAI_API_KEYdirectly.farm/analysis/registry.register_modulesacceptsconfig_serviceand no longer readsFARM_ANALYSIS_MODULESdirectly.farm/analysis/service.AnalysisServiceinjectsEnvConfigServiceintoregister_modules.scripts/analysis_config.pyinjectsEnvConfigServiceintoregister_modules.farm/utils/run_analysis.setup_environmentusesIConfigServiceto validate existence ofOPENAI_API_KEY.
- A lightweight singleton
farm.analysis.null_module.null_modulewas exposed to make testing the registry easier.
These changes align with SRP, DIP, and KISS principles and prefactor the codebase for broader DI adoption.
New Interfaces and Implementations
IConfigService- Methods:
get(key: str, default: Optional[str] = None) -> Optional[str]get_analysis_module_paths(env_var: str = "FARM_ANALYSIS_MODULES") -> List[str]get_openai_api_key() -> Optional[str]
- Methods:
EnvConfigService(default)- Reads from environment variables via
os.getenvand implementsIConfigService.
- Reads from environment variables via
Updated APIs
LLMClient- Before:
LLMClient(api_key: Optional[str] = None)reads env directly whenapi_keynot provided. - Now:
LLMClient(api_key: Optional[str] = None, config_service: Optional[IConfigService] = None)- If
api_keyisNone, the client usesconfig_service.get_openai_api_key(). - Defaults to
EnvConfigService()when not provided.
- If
- Before:
farm.analysis.registry.register_modules- Before:
register_modules(config_env_var: str = "FARM_ANALYSIS_MODULES")reads env directly. - Now:
register_modules(config_env_var: str = "FARM_ANALYSIS_MODULES", *, config_service: Optional[IConfigService] = None)- Uses
config_service.get_analysis_module_paths(config_env_var). - Defaults to
EnvConfigService()when not provided. - Will attempt to register all configured modules; if none successfully register, it falls back to the built-in dominance module.
- Uses
- Before:
AnalysisServiceandscripts/analysis_config.py- Both now call
register_modules(config_service=EnvConfigService()).
- Both now call
utils/run_analysis.setup_environment- Before: validated
OPENAI_API_KEYviaos.getenv. - Now: validates via
config_service.get_openai_api_key()and defaults toEnvConfigService().
- Before: validated
Example Usage
Injecting a custom config service for testing or alternative configuration sources:
from farm.core.services import IConfigService
from farm.charts.llm_client import LLMClient
from farm.analysis.registry import register_modules
class StaticConfig(IConfigService):
def __init__(self, api_key: str, modules: list[str] = None):
self._api_key = api_key
self._modules = modules or []
def get(self, key: str, default: str | None = None) -> str | None:
return default
def get_analysis_module_paths(self, env_var: str = "FARM_ANALYSIS_MODULES") -> list[str]:
return list(self._modules)
def get_openai_api_key(self) -> str | None:
return self._api_key
cfg = StaticConfig(api_key="sk-test", modules=["farm.analysis.null_module.null_module"])
register_modules(config_service=cfg)
client = LLMClient(config_service=cfg)
Backward Compatibility
- Backward-compatibility fallbacks have been removed to enforce explicit DI usage:
LLMClientnow requires anIConfigService(and optional explicitapi_key).register_modulesrequires anIConfigServiceand no longer falls back to a built-in module.
Testing
New tests validate the DI behavior:
tests/test_config_service.pycoversEnvConfigServicebehaviors.tests/test_analysis_registry_di.pyensures the registry uses an injected module list provider.
To run only the new tests:
python3 -m pytest -q tests/test_config_service.py tests/test_analysis_registry_di.py
Note: The full test suite depends on additional packages (e.g., pydantic, psutil, stable_baselines3). Those are unrelated to these DI changes.
Migration Notes
- Code must now inject
IConfigServiceexplicitly:- For
LLMClient, passconfig_serviceand optionallyapi_key. - For analysis registration, call
register_modules(config_service=...)with module paths supplied by your service. - Replace any remaining direct env reads with
IConfigServiceaccessors.
- For
Related Files
farm/core/services/interfaces.pyfarm/core/services/implementations.pyfarm/core/services/__init__.pyfarm/charts/llm_client.pyfarm/charts/chart_analyzer.pyfarm/analysis/registry.pyfarm/analysis/service.pyfarm/utils/run_analysis.pyscripts/analysis_config.py