diff --git a/yaml_settings_pydantic/loader.py b/yaml_settings_pydantic/loader.py index a219f14..d85a1b7 100644 --- a/yaml_settings_pydantic/loader.py +++ b/yaml_settings_pydantic/loader.py @@ -63,17 +63,21 @@ class YamlFileData(TypedDict): envvar=None, subpath=None, required=True ) +YamlSettingsFilesInput = ( + Sequence[str] + | Sequence[Path] + | str + | Path + | dict[str, YamlFileConfigDict] + | dict[Path, YamlFileConfigDict] + | set[str] + | set[Path] +) + class YamlSettingsConfigDict(SettingsConfigDict): yaml_files: Annotated[ - set[Path] - | Sequence[Path] - | dict[Path, YamlFileConfigDict] - | Path - | set[str] - | Sequence[str] - | dict[str, YamlFileConfigDict] - | str, + YamlSettingsFilesInput, Doc( "Files to load. This can be a ``str`` or ``Sequence`` of " "configuration paths, or a dictionary of file names mapping to " @@ -91,6 +95,10 @@ class YamlSettingsConfigDict(SettingsConfigDict): ] +YamlFilesData = dict[Path, YamlFileData] +YamlFilesConfigs = dict[Path, YamlFileConfigDict] + + def resolve_filepaths(fp: Path, fp_config: YamlFileConfigDict) -> Path: fp_from_env = None diff --git a/yaml_settings_pydantic/manifests.py b/yaml_settings_pydantic/manifests.py index 2a155f0..a9d4140 100644 --- a/yaml_settings_pydantic/manifests.py +++ b/yaml_settings_pydantic/manifests.py @@ -3,7 +3,8 @@ from __future__ import annotations -from typing import Any, Self +from pathlib import Path +from typing import Any, Self, Unpack from pydantic import BaseModel @@ -13,24 +14,27 @@ class BaseYaml(BaseModel): """Pydantic model loadable from yaml. See :meth:`load`.""" + _yaml_files_data: dict[Path, loader.YamlFileData] + _yaml_files_configs: dict[Path, loader.YamlSettingsConfigDict] + @classmethod def load( cls, *, - overwrite: dict[str, Any] | None = None, - exclude: dict[str, Any] | None = None, - **yaml_settings: loader.YamlSettingsConfigDict, + init_kwargs: dict[str, Any] | None = None, + yaml_exclude: dict[str, Any] | None = None, + **yaml_settings: Unpack[loader.YamlSettingsConfigDict], ) -> Self: - if (yaml_files := yaml_settings.get("yaml_files")) is None: - raise ValueError() + # if (yaml_files := yaml_settings.get("yaml_files")) is None: + # raise ValueError() - yaml_files_config = loader.validate_yaml_settings_config_files(yaml_files) + yaml_files_config = loader.validate_yaml_settings_config_files(yaml_settings) yaml_files_data = loader.load_yaml_data(yaml_files_config) data = loader.validate_yaml_data( yaml_files_data, - overwrite=overwrite, - exclude=exclude, + overwrite=init_kwargs, + exclude=yaml_exclude, ) return cls.model_validate(data) diff --git a/yaml_settings_pydantic/settings.py b/yaml_settings_pydantic/settings.py index e55bd46..18cc781 100644 --- a/yaml_settings_pydantic/settings.py +++ b/yaml_settings_pydantic/settings.py @@ -5,7 +5,7 @@ from pydantic.fields import FieldInfo from pydantic_settings import BaseSettings, PydanticBaseSettingsSource -from typing_extensions import Doc +from typing_extensions import Doc, override from yaml_settings_pydantic import loader, util @@ -124,11 +124,44 @@ class BaseYamlSettings(BaseSettings): `model_config["yaml_reload"]`. """ + __yaml_settings_cls__: ClassVar[CreateYamlSettings] + __yaml_settings_self__: CreateYamlSettings | None + __yaml_exclude__: bool + if TYPE_CHECKING: # NOTE: pydantic>=2.7 checks at load time for annotated fields, and # thinks that `model_config` is a model field name. model_config: ClassVar[loader.YamlSettingsConfigDict] + def __init_subclass__(cls, **kwargs): + """Create the default ``CreateYamlSettings`` instance.""" + super().__init_subclass__(**kwargs) + cls.__yaml_settings_cls__ = CreateYamlSettings(cls) + + def __init__( + self, + _yaml_files: loader.YamlSettingsFilesInput | None = None, + _yaml_reload: bool | None = None, + _yaml_exclude: bool = False, + **kwargs, + ): + # NOTE: If any overwrites are added, then do not use ``__yaml_settings_cls__``. + # This is a pain because the other option is to overwrite + # ``_settings_build_values``, which likely results in having + # to overwrite ``settings_customise_sources``. + if _yaml_files is not None: + yaml_settings = self.model_config.copy() + yaml_settings.update( + yaml_files=_yaml_files, + yaml_reload=_yaml_reload, + ) + self.__yaml_settings_self__ = CreateYamlSettings(self.__class__) + else: + self.__yaml_settings_self__ = self.__yaml_settings_cls__ + + self.__yaml_exclude__ = _yaml_exclude + super().__init__(**super()._settings_build_values(kwargs)) + @classmethod def settings_customise_sources( cls, @@ -142,10 +175,10 @@ def settings_customise_sources( # Look for YAML files. logger.debug("Creating YAML settings callable for `%s`.", cls.__name__) - yaml_settings = CreateYamlSettings(settings_cls) # The order in which these appear determines their precendence. So a # `.env` file could be added to # override the ``YAML`` configuration + return ( init_settings, env_settings,