Skip to content

Commit 15b66df

Browse files
authored
Fix dotenv source extra values parsing provided in dotenv file (#221)
1 parent 8b92f61 commit 15b66df

File tree

2 files changed

+59
-23
lines changed

2 files changed

+59
-23
lines changed

pydantic_settings/sources.py

Lines changed: 10 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -684,27 +684,19 @@ def _read_env_files(self) -> Mapping[str, str | None]:
684684
def __call__(self) -> dict[str, Any]:
685685
data: dict[str, Any] = super().__call__()
686686

687-
data_lower_keys: list[str] = []
688-
is_extra_allowed = self.config.get('extra') != 'forbid'
689-
if not self.case_sensitive:
690-
data_lower_keys = [x.lower() for x in data.keys()]
691687
# As `extra` config is allowed in dotenv settings source, We have to
692688
# update data with extra env variabels from dotenv file.
693689
for env_name, env_value in self.env_vars.items():
694-
if not is_extra_allowed and not env_name.startswith(self.env_prefix):
695-
raise SettingsError(
696-
"unable to load environment variables from dotenv file "
697-
f"due to the presence of variables without the specified prefix - '{self.env_prefix}'"
698-
)
699-
if env_name.startswith(self.env_prefix) and env_value is not None:
700-
env_name_without_prefix = env_name[self.env_prefix_len :]
701-
first_key, *_ = env_name_without_prefix.split(self.env_nested_delimiter)
702-
703-
if (data_lower_keys and first_key not in data_lower_keys) or (
704-
not data_lower_keys and first_key not in data
705-
):
706-
data[first_key] = env_value
707-
690+
if not env_value:
691+
continue
692+
env_used = False
693+
for field_name, field in self.settings_cls.model_fields.items():
694+
for _, field_env_name, _ in self._extract_field_info(field, field_name):
695+
if env_name.startswith(field_env_name):
696+
env_used = True
697+
break
698+
if not env_used:
699+
data[env_name] = env_value
708700
return data
709701

710702
def __repr__(self) -> str:

tests/test_settings.py

Lines changed: 49 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -839,12 +839,11 @@ class Settings(BaseSettings):
839839

840840
model_config = SettingsConfigDict(env_file=p, env_prefix='prefix_')
841841

842-
err_msg = (
843-
"unable to load environment variables from dotenv file "
844-
"due to the presence of variables without the specified prefix - 'prefix_'"
845-
)
846-
with pytest.raises(SettingsError, match=err_msg):
842+
with pytest.raises(ValidationError) as exc_info:
847843
Settings()
844+
assert exc_info.value.errors(include_url=False) == [
845+
{'type': 'extra_forbidden', 'loc': ('f',), 'msg': 'Extra inputs are not permitted', 'input': 'random value'}
846+
]
848847

849848

850849
def test_ignore_env_file_with_env_prefix_invalid(tmp_path):
@@ -2310,3 +2309,48 @@ def settings_customise_sources(
23102309

23112310
s = Settings()
23122311
assert s.model_dump() == {'json5': 5, 'json6': 6}
2312+
2313+
2314+
def test_dotenv_with_alias_and_env_prefix(tmp_path):
2315+
p = tmp_path / '.env'
2316+
p.write_text('xxx__foo=1\nxxx__bar=2')
2317+
2318+
class Settings(BaseSettings):
2319+
model_config = SettingsConfigDict(env_file=p, env_prefix='xxx__')
2320+
2321+
foo: str = ''
2322+
bar_alias: str = Field('', validation_alias='xxx__bar')
2323+
2324+
s = Settings()
2325+
assert s.model_dump() == {'foo': '1', 'bar_alias': '2'}
2326+
2327+
class Settings1(BaseSettings):
2328+
model_config = SettingsConfigDict(env_file=p, env_prefix='xxx__')
2329+
2330+
foo: str = ''
2331+
bar_alias: str = Field('', alias='bar')
2332+
2333+
with pytest.raises(ValidationError) as exc_info:
2334+
Settings1()
2335+
assert exc_info.value.errors(include_url=False) == [
2336+
{'type': 'extra_forbidden', 'loc': ('xxx__bar',), 'msg': 'Extra inputs are not permitted', 'input': '2'}
2337+
]
2338+
2339+
2340+
def test_dotenv_with_alias_and_env_prefix_nested(tmp_path):
2341+
p = tmp_path / '.env'
2342+
p.write_text('xxx__bar=0\nxxx__nested__a=1\nxxx__nested__b=2')
2343+
2344+
class NestedSettings(BaseModel):
2345+
a: str = 'a'
2346+
b: str = 'b'
2347+
2348+
class Settings(BaseSettings):
2349+
model_config = SettingsConfigDict(env_prefix='xxx__', env_nested_delimiter='__', env_file=p)
2350+
2351+
foo: str = ''
2352+
bar_alias: str = Field('', alias='xxx__bar')
2353+
nested_alias: NestedSettings = Field(default_factory=NestedSettings, alias='xxx__nested')
2354+
2355+
s = Settings()
2356+
assert s.model_dump() == {'foo': '', 'bar_alias': '0', 'nested_alias': {'a': '1', 'b': '2'}}

0 commit comments

Comments
 (0)