diff --git a/pydantic_settings/sources.py b/pydantic_settings/sources.py index c7c019a4..23bbbf13 100644 --- a/pydantic_settings/sources.py +++ b/pydantic_settings/sources.py @@ -753,6 +753,30 @@ def __init__( def _load_env_vars(self) -> Mapping[str, str | None]: return self._read_env_files() + @staticmethod + def _static_read_env_file( + file_path: Path, + *, + encoding: str | None = None, + case_sensitive: bool = False, + ignore_empty: bool = False, + parse_none_str: str | None = None, + ) -> Mapping[str, str | None]: + file_vars: dict[str, str | None] = dotenv_values(file_path, encoding=encoding or 'utf8') + return parse_env_vars(file_vars, case_sensitive, ignore_empty, parse_none_str) + + def _read_env_file( + self, + file_path: Path, + ) -> Mapping[str, str | None]: + return self._static_read_env_file( + file_path, + encoding=self.env_file_encoding, + case_sensitive=self.case_sensitive, + ignore_empty=self.env_ignore_empty, + parse_none_str=self.env_parse_none_str, + ) + def _read_env_files(self) -> Mapping[str, str | None]: env_files = self.env_file if env_files is None: @@ -765,15 +789,7 @@ def _read_env_files(self) -> Mapping[str, str | None]: for env_file in env_files: env_path = Path(env_file).expanduser() if env_path.is_file(): - dotenv_vars.update( - read_env_file( - env_path, - encoding=self.env_file_encoding, - case_sensitive=self.case_sensitive, - ignore_empty=self.env_ignore_empty, - parse_none_str=self.env_parse_none_str, - ) - ) + dotenv_vars.update(self._read_env_file(env_path)) return dotenv_vars @@ -1708,8 +1724,17 @@ def read_env_file( ignore_empty: bool = False, parse_none_str: str | None = None, ) -> Mapping[str, str | None]: - file_vars: dict[str, str | None] = dotenv_values(file_path, encoding=encoding or 'utf8') - return parse_env_vars(file_vars, case_sensitive, ignore_empty, parse_none_str) + warnings.warn( + 'read_env_file will be removed in the next version, use DotEnvSettingsSource._static_read_env_file if you must', + DeprecationWarning, + ) + return DotEnvSettingsSource._static_read_env_file( + file_path, + encoding=encoding, + case_sensitive=case_sensitive, + ignore_empty=ignore_empty, + parse_none_str=parse_none_str, + ) def _annotation_is_complex(annotation: type[Any] | None, metadata: list[Any]) -> bool: diff --git a/tests/test_settings.py b/tests/test_settings.py index 7690e3c6..e812ffed 100644 --- a/tests/test_settings.py +++ b/tests/test_settings.py @@ -34,7 +34,7 @@ from pydantic._internal._repr import Representation from pydantic.fields import FieldInfo from pytest_mock import MockerFixture -from typing_extensions import Annotated, Literal +from typing_extensions import Annotated, Literal, override from pydantic_settings import ( BaseSettings, @@ -49,7 +49,7 @@ TomlConfigSettingsSource, YamlConfigSettingsSource, ) -from pydantic_settings.sources import CliPositionalArg, CliSettingsSource, CliSubCommand, SettingsError, read_env_file +from pydantic_settings.sources import CliPositionalArg, CliSettingsSource, CliSubCommand, SettingsError try: import dotenv @@ -1039,15 +1039,15 @@ def test_read_env_file_case_sensitive(tmp_path): p = tmp_path / '.env' p.write_text('a="test"\nB=123') - assert read_env_file(p) == {'a': 'test', 'b': '123'} - assert read_env_file(p, case_sensitive=True) == {'a': 'test', 'B': '123'} + assert DotEnvSettingsSource._static_read_env_file(p) == {'a': 'test', 'b': '123'} + assert DotEnvSettingsSource._static_read_env_file(p, case_sensitive=True) == {'a': 'test', 'B': '123'} def test_read_env_file_syntax_wrong(tmp_path): p = tmp_path / '.env' p.write_text('NOT_AN_ASSIGNMENT') - assert read_env_file(p, case_sensitive=True) == {'NOT_AN_ASSIGNMENT': None} + assert DotEnvSettingsSource._static_read_env_file(p, case_sensitive=True) == {'NOT_AN_ASSIGNMENT': None} def test_env_file_example(tmp_path): @@ -1188,6 +1188,37 @@ def test_read_dotenv_vars_when_env_file_is_none(): ) +def test_dotenvsource_override(env): + class StdinDotEnvSettingsSource(DotEnvSettingsSource): + @override + def _read_env_file(self, file_path: Path) -> Dict[str, str]: + assert str(file_path) == '-' + return {'foo': 'stdin_foo', 'bar': 'stdin_bar'} + + @override + def _read_env_files(self) -> Dict[str, str]: + return self._read_env_file(Path('-')) + + source = StdinDotEnvSettingsSource(BaseSettings()) + assert source._read_env_files() == {'foo': 'stdin_foo', 'bar': 'stdin_bar'} + + +# test that calling read_env_file issues a DeprecationWarning +# TODO: remove this test once read_env_file is removed +def test_read_env_file_deprecation(tmp_path): + from pydantic_settings.sources import read_env_file + + base_env = tmp_path / '.env' + base_env.write_text(test_default_env_file) + + with pytest.deprecated_call(): + assert read_env_file(base_env) == { + 'debug_mode': 'true', + 'host': 'localhost', + 'port': '8000', + } + + @pytest.mark.skipif(yaml, reason='PyYAML is installed') def test_yaml_not_installed(tmp_path): p = tmp_path / '.env'