diff --git a/qlib/config.py b/qlib/config.py index 4e5d62564f7..58954da846d 100644 --- a/qlib/config.py +++ b/qlib/config.py @@ -62,16 +62,33 @@ class QSettings(BaseSettings): class Config: def __init__(self, default_conf): - self.__dict__["_default_config"] = copy.deepcopy(default_conf) # avoiding conflicts with __getattr__ + self.__dict__["_default_config"] = copy.deepcopy(default_conf) self.reset() + # TODO: This validation logic is a temporary solution. + # The long-term goal is to migrate Qlib Config to a typed configuration + # system based on pydantic.BaseModel, with explicit schema and field validation. + def validate(self): + errors = [] + + if not self.get("provider_uri"): + errors.append("provider_uri must be set (e.g. ~/.qlib/qlib_data or a valid path)") + + if not self.get("region"): + errors.append("region must be specified (e.g. 'cn', 'us')") + + if errors: + raise ValueError( + "Invalid Qlib configuration (note: the global config has already been updated):\n" + "Invalid Qlib configuration:\n- " + "\n- ".join(errors) + ) + def __getitem__(self, key): return self.__dict__["_config"][key] def __getattr__(self, attr): if attr in self.__dict__["_config"]: return self.__dict__["_config"][attr] - raise AttributeError(f"No such `{attr}` in self._config") def get(self, key, default=None): @@ -115,8 +132,11 @@ def register_from_C(config, skip_register=True): return C.set_conf_from_C(config) + C.validate() + if C.logging_config: set_log_with_config(C.logging_config) + C.register() diff --git a/qlib/tests/test_config_validation.py b/qlib/tests/test_config_validation.py new file mode 100644 index 00000000000..60a9c89963e --- /dev/null +++ b/qlib/tests/test_config_validation.py @@ -0,0 +1,17 @@ +import pytest + +from qlib.config import Config + + +def test_missing_provider_uri_raises(): + default_conf = { + "provider_uri": None, + "region": "us", + } + + cfg = Config(default_conf) + + with pytest.raises(ValueError) as exc: + cfg.validate() + + assert "provider_uri must be set" in str(exc.value)