Skip to content

Commit aaec124

Browse files
committed
Add env_nested_depth option
1 parent 65929cd commit aaec124

File tree

3 files changed

+48
-6
lines changed

3 files changed

+48
-6
lines changed

pydantic_settings/main.py

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -38,6 +38,7 @@ class SettingsConfigDict(ConfigDict, total=False):
3838
env_file_encoding: str | None
3939
env_ignore_empty: bool
4040
env_nested_delimiter: str | None
41+
env_nested_depth: int
4142
env_parse_none_str: str | None
4243
env_parse_enums: bool | None
4344
cli_prog_name: str | None
@@ -112,6 +113,7 @@ class BaseSettings(BaseModel):
112113
_env_file_encoding: The env file encoding, e.g. `'latin-1'`. Defaults to `None`.
113114
_env_ignore_empty: Ignore environment variables where the value is an empty string. Default to `False`.
114115
_env_nested_delimiter: The nested env values delimiter. Defaults to `None`.
116+
_env_nested_depth: The nested env values maximum nesting. Defaults to `-1`, which means no limit.
115117
_env_parse_none_str: The env string value that should be parsed (e.g. "null", "void", "None", etc.)
116118
into `None` type(None). Defaults to `None` type(None), which means no parsing should occur.
117119
_env_parse_enums: Parse enum field names to values. Defaults to `None.`, which means no parsing should occur.
@@ -148,6 +150,7 @@ def __init__(
148150
_env_file_encoding: str | None = None,
149151
_env_ignore_empty: bool | None = None,
150152
_env_nested_delimiter: str | None = None,
153+
_env_nested_depth: int | None = None,
151154
_env_parse_none_str: str | None = None,
152155
_env_parse_enums: bool | None = None,
153156
_cli_prog_name: str | None = None,
@@ -178,6 +181,7 @@ def __init__(
178181
_env_file_encoding=_env_file_encoding,
179182
_env_ignore_empty=_env_ignore_empty,
180183
_env_nested_delimiter=_env_nested_delimiter,
184+
_env_nested_depth=_env_nested_depth,
181185
_env_parse_none_str=_env_parse_none_str,
182186
_env_parse_enums=_env_parse_enums,
183187
_cli_prog_name=_cli_prog_name,
@@ -232,6 +236,7 @@ def _settings_build_values(
232236
_env_file_encoding: str | None = None,
233237
_env_ignore_empty: bool | None = None,
234238
_env_nested_delimiter: str | None = None,
239+
_env_nested_depth: int | None = None,
235240
_env_parse_none_str: str | None = None,
236241
_env_parse_enums: bool | None = None,
237242
_cli_prog_name: str | None = None,
@@ -270,6 +275,9 @@ def _settings_build_values(
270275
if _env_nested_delimiter is not None
271276
else self.model_config.get('env_nested_delimiter')
272277
)
278+
env_nested_depth = (
279+
_env_nested_depth if _env_nested_depth is not None else self.model_config.get('env_nested_depth')
280+
)
273281
env_parse_none_str = (
274282
_env_parse_none_str if _env_parse_none_str is not None else self.model_config.get('env_parse_none_str')
275283
)
@@ -333,6 +341,7 @@ def _settings_build_values(
333341
case_sensitive=case_sensitive,
334342
env_prefix=env_prefix,
335343
env_nested_delimiter=env_nested_delimiter,
344+
env_nested_depth=env_nested_depth,
336345
env_ignore_empty=env_ignore_empty,
337346
env_parse_none_str=env_parse_none_str,
338347
env_parse_enums=env_parse_enums,
@@ -344,6 +353,7 @@ def _settings_build_values(
344353
case_sensitive=case_sensitive,
345354
env_prefix=env_prefix,
346355
env_nested_delimiter=env_nested_delimiter,
356+
env_nested_depth=env_nested_depth,
347357
env_ignore_empty=env_ignore_empty,
348358
env_parse_none_str=env_parse_none_str,
349359
env_parse_enums=env_parse_enums,
@@ -412,6 +422,7 @@ def _settings_build_values(
412422
env_file_encoding=None,
413423
env_ignore_empty=False,
414424
env_nested_delimiter=None,
425+
env_nested_depth=-1,
415426
env_parse_none_str=None,
416427
env_parse_enums=None,
417428
cli_prog_name=None,

pydantic_settings/sources.py

Lines changed: 10 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -735,6 +735,7 @@ def __init__(
735735
case_sensitive: bool | None = None,
736736
env_prefix: str | None = None,
737737
env_nested_delimiter: str | None = None,
738+
env_nested_depth: int | None = None,
738739
env_ignore_empty: bool | None = None,
739740
env_parse_none_str: str | None = None,
740741
env_parse_enums: bool | None = None,
@@ -745,6 +746,9 @@ def __init__(
745746
self.env_nested_delimiter = (
746747
env_nested_delimiter if env_nested_delimiter is not None else self.config.get('env_nested_delimiter')
747748
)
749+
self.env_nested_depth = (
750+
env_nested_depth if env_nested_depth is not None else self.config.get('env_nested_depth', -1)
751+
)
748752
self.env_prefix_len = len(self.env_prefix)
749753

750754
self.env_vars = self._load_env_vars()
@@ -914,7 +918,7 @@ def explode_env_vars(self, field_name: str, field: FieldInfo, env_vars: Mapping[
914918
continue
915919
# we remove the prefix before splitting in case the prefix has characters in common with the delimiter
916920
env_name_without_prefix = env_name[self.env_prefix_len :]
917-
_, *keys, last_key = env_name_without_prefix.split(self.env_nested_delimiter)
921+
_, *keys, last_key = env_name_without_prefix.split(self.env_nested_delimiter, self.env_nested_depth)
918922
env_var = result
919923
target_field: FieldInfo | None = field
920924
for key in keys:
@@ -947,7 +951,7 @@ def explode_env_vars(self, field_name: str, field: FieldInfo, env_vars: Mapping[
947951
def __repr__(self) -> str:
948952
return (
949953
f'{self.__class__.__name__}(env_nested_delimiter={self.env_nested_delimiter!r}, '
950-
f'env_prefix_len={self.env_prefix_len!r})'
954+
f'env_nested_depth={self.env_nested_depth}, env_prefix_len={self.env_prefix_len!r})'
951955
)
952956

953957

@@ -964,6 +968,7 @@ def __init__(
964968
case_sensitive: bool | None = None,
965969
env_prefix: str | None = None,
966970
env_nested_delimiter: str | None = None,
971+
env_nested_depth: int | None = None,
967972
env_ignore_empty: bool | None = None,
968973
env_parse_none_str: str | None = None,
969974
env_parse_enums: bool | None = None,
@@ -977,6 +982,7 @@ def __init__(
977982
case_sensitive,
978983
env_prefix,
979984
env_nested_delimiter,
985+
env_nested_depth,
980986
env_ignore_empty,
981987
env_parse_none_str,
982988
env_parse_enums,
@@ -1063,7 +1069,8 @@ def __call__(self) -> dict[str, Any]:
10631069
def __repr__(self) -> str:
10641070
return (
10651071
f'{self.__class__.__name__}(env_file={self.env_file!r}, env_file_encoding={self.env_file_encoding!r}, '
1066-
f'env_nested_delimiter={self.env_nested_delimiter!r}, env_prefix_len={self.env_prefix_len!r})'
1072+
f'env_nested_delimiter={self.env_nested_delimiter!r}, env_nested_depth={self.env_nested_depth}, '
1073+
f'env_prefix_len={self.env_prefix_len!r})'
10671074
)
10681075

10691076

tests/test_settings.py

Lines changed: 27 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,7 @@
44
import pathlib
55
import sys
66
import uuid
7-
from datetime import datetime, timezone
7+
from datetime import date, datetime, timezone
88
from enum import IntEnum
99
from pathlib import Path
1010
from typing import Any, Callable, Dict, Generic, Hashable, List, Optional, Set, Tuple, Type, TypeVar, Union
@@ -398,6 +398,30 @@ class Cfg(BaseSettings):
398398
assert Cfg().model_dump() == {'sub_model': {'v1': '-1-', 'v2': '-2-'}}
399399

400400

401+
@pytest.mark.parametrize('env_prefix', [None, 'prefix_', 'prefix__'])
402+
def test_nested_env_depth(env, env_prefix):
403+
class Person(BaseModel):
404+
sex: Literal['M', 'F']
405+
first_name: str
406+
date_of_birth: date
407+
408+
class Cfg(BaseSettings):
409+
caregiver: Person
410+
411+
model_config = SettingsConfigDict(env_nested_delimiter='_', env_nested_depth=1)
412+
if env_prefix is not None:
413+
model_config['env_prefix'] = env_prefix
414+
415+
env_prefix = env_prefix or ''
416+
env.set(env_prefix + 'caregiver_sex', 'M')
417+
env.set(env_prefix + 'caregiver_first_name', 'Joe')
418+
env.set(env_prefix + 'caregiver_date_of_birth', '1975-09-12')
419+
420+
assert Cfg().model_dump() == {
421+
'caregiver': {'sex': 'M', 'first_name': 'Joe', 'date_of_birth': date(1975, 9, 12)},
422+
}
423+
424+
401425
class DateModel(BaseModel):
402426
pips: bool = False
403427

@@ -1823,11 +1847,11 @@ def test_builtins_settings_source_repr():
18231847
)
18241848
assert (
18251849
repr(EnvSettingsSource(BaseSettings, env_nested_delimiter='__'))
1826-
== "EnvSettingsSource(env_nested_delimiter='__', env_prefix_len=0)"
1850+
== "EnvSettingsSource(env_nested_delimiter='__', env_nested_depth=-1, env_prefix_len=0)"
18271851
)
18281852
assert repr(DotEnvSettingsSource(BaseSettings, env_file='.env', env_file_encoding='utf-8')) == (
18291853
"DotEnvSettingsSource(env_file='.env', env_file_encoding='utf-8', "
1830-
'env_nested_delimiter=None, env_prefix_len=0)'
1854+
'env_nested_delimiter=None, env_nested_depth=-1, env_prefix_len=0)'
18311855
)
18321856
assert (
18331857
repr(SecretsSettingsSource(BaseSettings, secrets_dir='/secrets'))

0 commit comments

Comments
 (0)