Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Deprecate read_env_file and move it to DotEnvSettingsSource #318

Merged
merged 4 commits into from
Jul 2, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
47 changes: 36 additions & 11 deletions pydantic_settings/sources.py
Original file line number Diff line number Diff line change
Expand Up @@ -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(
WarpedPixel marked this conversation as resolved.
Show resolved Hide resolved
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:
Expand All @@ -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

Expand Down Expand Up @@ -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',
WarpedPixel marked this conversation as resolved.
Show resolved Hide resolved
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:
Expand Down
41 changes: 36 additions & 5 deletions tests/test_settings.py
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand All @@ -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
Expand Down Expand Up @@ -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):
Expand Down Expand Up @@ -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'
Expand Down
Loading