pydantic-settings icon indicating copy to clipboard operation
pydantic-settings copied to clipboard

More ``__init__`` Overwrite Support

Open acederberg opened this issue 7 months ago • 2 comments

Hello Everybody!

My apologies if this is a bit long.

The problem

Support for passing additional overwriting keyword arguments for additional sources to __init__ is confusing. I would like to overwrite some values in one of my sources, but it looks like I'd have to create my source in the settings_customize_sources classmethod to get it into _settings_build_values, which means I'd have to overwrite the _settings_build_values method somehow. This is less than desirable, since I have no means to inject those sources using super.

My Use Case

I have a bit of an odd use case for a library I'm writing. This library supports loading YAML from many sources optionally with reloading of the files or not when creating new instances. I know that there is a YAML source defined here, but my use case is different enough that I want to write my own source.

Essentially, I want to inject sources on an instance level when overwrites (_yaml_files and _yaml_settings) are provided, otherwise it uses the source created with the subclass hook:

class BaseYamlSettings(BaseSettings):
    __yaml_settings_cls__: ClassVar[CreateYamlSettings]
    __yaml_settings_self__: CreateYamlSettings | None
    __yaml_exclude__: bool

    if TYPE_CHECKING:
        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))

The main problem is that _settings_build_values uses the source customization hook settings_customize_sources which is a classmethod - there is no means to pass instance data to the hook.

I don't want to copy and paste the definition of _settings_build_values from here and maintain it in my library, that is not an elegant or easy to maintain solution. I also want to keep all of the functionality of BaseSettings without overwriting signatures ideally. I would like to do something like

def _settings_build_values(self, **kwargs):
    kwargs["sources_extra"] = [self.__yaml_settings_self__]
    super()._settings_build_values(self, **kwargs)

and have sources_extra concatenation to the local sources.

Is there an obvious way to do this I am not noticing? Should I just give up and not use BaseSettings and write an equivalent? If not, should I make a pull request doing roughly that which is specified in the block of code here?

acederberg avatar Jul 19 '24 16:07 acederberg