Skip to content

Commit b08fa5b

Browse files
Viicoshramezani
andauthored
Tweak sources to have case_sensitive and env_prefix usable as args (#76)
Co-authored-by: Hasan Ramezani <[email protected]>
1 parent dba60fe commit b08fa5b

File tree

4 files changed

+94
-46
lines changed

4 files changed

+94
-46
lines changed

docs/index.md

Lines changed: 5 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -90,7 +90,8 @@ print(Settings().model_dump())
9090

9191
By default, the environment variable name is the same as the field name.
9292

93-
You can change the prefix for all environment variables by setting the `env_prefix` config setting:
93+
You can change the prefix for all environment variables by setting the `env_prefix` config setting,
94+
or via the `_env_prefix` keyword argument on instantiation:
9495

9596
```py
9697
from pydantic_settings import BaseSettings, SettingsConfigDict
@@ -135,6 +136,8 @@ so in this example `redis_host` could only be modified via `export redis_host`.
135136
all upper-case, you should name attribute all upper-case too. You can still name environment variables anything
136137
you like through `Field(validation_alias=...)`.
137138

139+
Case-sensitivity can also be set via the `_case_sensitive` keyword argument on instantiation.
140+
138141
In case of nested models, the `case_sensitive` setting will be applied to all nested models.
139142

140143
```py
@@ -316,7 +319,7 @@ in the `BaseSettings` class:
316319

317320
```py test="skip" lint="skip"
318321
class Settings(BaseSettings):
319-
model_config = SettingsConfigDict(env_file='.env', env_file_encoding = 'utf-8')
322+
model_config = SettingsConfigDict(env_file='.env', env_file_encoding='utf-8')
320323
```
321324

322325
2. Instantiating the `BaseSettings` derived class with the `_env_file` keyword argument

pydantic_settings/main.py

Lines changed: 32 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -38,14 +38,18 @@ class BaseSettings(BaseModel):
3838
All the bellow attributes can be set via `model_config`.
3939
4040
Args:
41+
_case_sensitive: Whether environment variables names should be read with case-sensitivity. Defaults to `None`.
42+
_env_prefix: Prefix for all environment variables. Defaults to `None`.
4143
_env_file: The env file(s) to load settings values from. Defaults to `Path('')`.
42-
_env_file_encoding: The env file encoding. e.g. `'latin-1'`. Defaults to `None`.
44+
_env_file_encoding: The env file encoding, e.g. `'latin-1'`. Defaults to `None`.
4345
_env_nested_delimiter: The nested env values delimiter. Defaults to `None`.
4446
_secrets_dir: The secret files directory. Defaults to `None`.
4547
"""
4648

4749
def __init__(
4850
__pydantic_self__,
51+
_case_sensitive: bool | None = None,
52+
_env_prefix: str | None = None,
4953
_env_file: DotenvType | None = env_file_sentinel,
5054
_env_file_encoding: str | None = None,
5155
_env_nested_delimiter: str | None = None,
@@ -56,6 +60,8 @@ def __init__(
5660
super().__init__(
5761
**__pydantic_self__._settings_build_values(
5862
values,
63+
_case_sensitive=_case_sensitive,
64+
_env_prefix=_env_prefix,
5965
_env_file=_env_file,
6066
_env_file_encoding=_env_file_encoding,
6167
_env_nested_delimiter=_env_nested_delimiter,
@@ -90,38 +96,46 @@ def settings_customise_sources(
9096
def _settings_build_values(
9197
self,
9298
init_kwargs: dict[str, Any],
99+
_case_sensitive: bool | None = None,
100+
_env_prefix: str | None = None,
93101
_env_file: DotenvType | None = None,
94102
_env_file_encoding: str | None = None,
95103
_env_nested_delimiter: str | None = None,
96104
_secrets_dir: str | Path | None = None,
97105
) -> dict[str, Any]:
106+
# Determine settings config values
107+
case_sensitive = _case_sensitive if _case_sensitive is not None else self.model_config.get('case_sensitive')
108+
env_prefix = _env_prefix if _env_prefix is not None else self.model_config.get('env_prefix')
109+
env_file = _env_file if _env_file != env_file_sentinel else self.model_config.get('env_file')
110+
env_file_encoding = (
111+
_env_file_encoding if _env_file_encoding is not None else self.model_config.get('env_file_encoding')
112+
)
113+
env_nested_delimiter = (
114+
_env_nested_delimiter
115+
if _env_nested_delimiter is not None
116+
else self.model_config.get('env_nested_delimiter')
117+
)
118+
secrets_dir = _secrets_dir if _secrets_dir is not None else self.model_config.get('secrets_dir')
119+
98120
# Configure built-in sources
99121
init_settings = InitSettingsSource(self.__class__, init_kwargs=init_kwargs)
100122
env_settings = EnvSettingsSource(
101123
self.__class__,
102-
env_nested_delimiter=(
103-
_env_nested_delimiter
104-
if _env_nested_delimiter is not None
105-
else self.model_config.get('env_nested_delimiter')
106-
),
107-
env_prefix_len=len(self.model_config.get('env_prefix', '')),
124+
case_sensitive=case_sensitive,
125+
env_prefix=env_prefix,
126+
env_nested_delimiter=env_nested_delimiter,
108127
)
109128
dotenv_settings = DotEnvSettingsSource(
110129
self.__class__,
111-
env_file=(_env_file if _env_file != env_file_sentinel else self.model_config.get('env_file')),
112-
env_file_encoding=(
113-
_env_file_encoding if _env_file_encoding is not None else self.model_config.get('env_file_encoding')
114-
),
115-
env_nested_delimiter=(
116-
_env_nested_delimiter
117-
if _env_nested_delimiter is not None
118-
else self.model_config.get('env_nested_delimiter')
119-
),
120-
env_prefix_len=len(self.model_config.get('env_prefix', '')),
130+
env_file=env_file,
131+
env_file_encoding=env_file_encoding,
132+
case_sensitive=case_sensitive,
133+
env_prefix=env_prefix,
134+
env_nested_delimiter=env_nested_delimiter,
121135
)
122136

123137
file_secret_settings = SecretsSettingsSource(
124-
self.__class__, secrets_dir=_secrets_dir or self.model_config.get('secrets_dir')
138+
self.__class__, secrets_dir=secrets_dir, case_sensitive=case_sensitive, env_prefix=env_prefix
125139
)
126140
# Provide a hook to set built-in sources priority and add / remove sources
127141
sources = self.settings_customise_sources(

pydantic_settings/sources.py

Lines changed: 35 additions & 26 deletions
Original file line numberDiff line numberDiff line change
@@ -108,8 +108,15 @@ def __repr__(self) -> str:
108108

109109

110110
class PydanticBaseEnvSettingsSource(PydanticBaseSettingsSource):
111+
def __init__(
112+
self, settings_cls: type[BaseSettings], case_sensitive: bool | None = None, env_prefix: str | None = None
113+
) -> None:
114+
super().__init__(settings_cls)
115+
self.case_sensitive = case_sensitive if case_sensitive is not None else False
116+
self.env_prefix = env_prefix if env_prefix is not None else ''
117+
111118
def _apply_case_sensitive(self, value: str) -> str:
112-
return value.lower() if not self.config.get('case_sensitive') else value
119+
return value.lower() if not self.case_sensitive else value
113120

114121
def _extract_field_info(self, field: FieldInfo, field_name: str) -> list[tuple[str, str, bool]]:
115122
"""
@@ -147,9 +154,7 @@ def _extract_field_info(self, field: FieldInfo, field_name: str) -> list[tuple[s
147154
else: # string validation alias
148155
field_info.append((v_alias, self._apply_case_sensitive(v_alias), False))
149156
else:
150-
field_info.append(
151-
(field_name, self._apply_case_sensitive(self.config.get('env_prefix', '') + field_name), False)
152-
)
157+
field_info.append((field_name, self._apply_case_sensitive(self.env_prefix + field_name), False))
153158

154159
return field_info
155160

@@ -231,7 +236,7 @@ def __call__(self) -> dict[str, Any]:
231236
) from e
232237

233238
if field_value is not None:
234-
if not self.config.get('case_sensitive', False) and lenient_issubclass(field.annotation, BaseModel):
239+
if not self.case_sensitive and lenient_issubclass(field.annotation, BaseModel):
235240
data[field_key] = self._replace_field_names_case_insensitively(field, field_value)
236241
else:
237242
data[field_key] = field_value
@@ -244,9 +249,15 @@ class SecretsSettingsSource(PydanticBaseEnvSettingsSource):
244249
Source class for loading settings values from secret files.
245250
"""
246251

247-
def __init__(self, settings_cls: type[BaseSettings], secrets_dir: str | Path | None):
252+
def __init__(
253+
self,
254+
settings_cls: type[BaseSettings],
255+
secrets_dir: str | Path | None,
256+
case_sensitive: bool | None = None,
257+
env_prefix: str | None = None,
258+
) -> None:
259+
super().__init__(settings_cls, case_sensitive, env_prefix)
248260
self.secrets_dir = secrets_dir
249-
super().__init__(settings_cls)
250261

251262
def __call__(self) -> dict[str, Any]:
252263
"""
@@ -302,9 +313,7 @@ def get_field_value(self, field: FieldInfo, field_name: str) -> tuple[Any, str,
302313
"""
303314

304315
for field_key, env_name, value_is_complex in self._extract_field_info(field, field_name):
305-
path = self.find_case_path(
306-
self.secrets_path, env_name, self.settings_cls.model_config.get('case_sensitive', False)
307-
)
316+
path = self.find_case_path(self.secrets_path, env_name, self.case_sensitive)
308317
if not path:
309318
# path does not exist, we curently don't return a warning for this
310319
continue
@@ -331,18 +340,18 @@ class EnvSettingsSource(PydanticBaseEnvSettingsSource):
331340
def __init__(
332341
self,
333342
settings_cls: type[BaseSettings],
343+
case_sensitive: bool | None = None,
344+
env_prefix: str | None = None,
334345
env_nested_delimiter: str | None = None,
335-
env_prefix_len: int = 0,
336-
):
337-
super().__init__(settings_cls)
338-
339-
self.env_nested_delimiter: str | None = env_nested_delimiter
340-
self.env_prefix_len: int = env_prefix_len
346+
) -> None:
347+
super().__init__(settings_cls, case_sensitive, env_prefix)
348+
self.env_nested_delimiter = env_nested_delimiter
349+
self.env_prefix_len = len(self.env_prefix)
341350

342-
self.env_vars: Mapping[str, str | None] = self._load_env_vars()
351+
self.env_vars = self._load_env_vars()
343352

344353
def _load_env_vars(self) -> Mapping[str, str | None]:
345-
if self.settings_cls.model_config.get('case_sensitive'):
354+
if self.case_sensitive:
346355
return os.environ
347356
return {k.lower(): v for k, v in os.environ.items()}
348357

@@ -521,16 +530,16 @@ def __init__(
521530
settings_cls: type[BaseSettings],
522531
env_file: DotenvType | None,
523532
env_file_encoding: str | None,
533+
case_sensitive: bool | None = None,
534+
env_prefix: str | None = None,
524535
env_nested_delimiter: str | None = None,
525-
env_prefix_len: int = 0,
526-
):
527-
self.env_file: DotenvType | None = env_file
528-
self.env_file_encoding: str | None = env_file_encoding
529-
530-
super().__init__(settings_cls, env_nested_delimiter, env_prefix_len)
536+
) -> None:
537+
self.env_file = env_file
538+
self.env_file_encoding = env_file_encoding
539+
super().__init__(settings_cls, case_sensitive, env_prefix, env_nested_delimiter)
531540

532541
def _load_env_vars(self) -> Mapping[str, str | None]:
533-
return self._read_env_files(self.settings_cls.model_config.get('case_sensitive', False))
542+
return self._read_env_files(self.case_sensitive)
534543

535544
def _read_env_files(self, case_sensitive: bool) -> Mapping[str, str | None]:
536545
env_files = self.env_file
@@ -554,7 +563,7 @@ def __call__(self) -> dict[str, Any]:
554563
data: dict[str, Any] = super().__call__()
555564

556565
data_lower_keys: list[str] = []
557-
if not self.settings_cls.model_config.get('case_sensitive', False):
566+
if not self.case_sensitive:
558567
data_lower_keys = [x.lower() for x in data.keys()]
559568

560569
# As `extra` config is allowed in dotenv settings source, We have to

tests/test_settings.py

Lines changed: 22 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1630,3 +1630,25 @@ class Model(BaseSettings):
16301630

16311631
class Model1(BaseSettings):
16321632
settings_prefixed_field: str
1633+
1634+
1635+
def test_case_sensitive_from_args(monkeypatch):
1636+
class Settings(BaseSettings):
1637+
foo: str
1638+
1639+
# Need to patch os.environ to get build to work on Windows, where os.environ is case insensitive
1640+
monkeypatch.setattr(os, 'environ', value={'Foo': 'foo'})
1641+
with pytest.raises(ValidationError) as exc_info:
1642+
Settings(_case_sensitive=True)
1643+
assert exc_info.value.errors(include_url=False) == [
1644+
{'type': 'missing', 'loc': ('foo',), 'msg': 'Field required', 'input': {}}
1645+
]
1646+
1647+
1648+
def test_env_prefix_from_args(env):
1649+
class Settings(BaseSettings):
1650+
apple: str
1651+
1652+
env.set('foobar_apple', 'has_prefix')
1653+
s = Settings(_env_prefix='foobar_')
1654+
assert s.apple == 'has_prefix'

0 commit comments

Comments
 (0)