30
30
ENV_FILE_SENTINEL : DotenvType = Path ('' )
31
31
32
32
33
+ class EnvNoneType (str ):
34
+ pass
35
+
36
+
33
37
class SettingsError (ValueError ):
34
38
pass
35
39
@@ -134,13 +138,17 @@ def __init__(
134
138
case_sensitive : bool | None = None ,
135
139
env_prefix : str | None = None ,
136
140
env_ignore_empty : bool | None = None ,
141
+ env_parse_none_str : str | None = None ,
137
142
) -> None :
138
143
super ().__init__ (settings_cls )
139
144
self .case_sensitive = case_sensitive if case_sensitive is not None else self .config .get ('case_sensitive' , False )
140
145
self .env_prefix = env_prefix if env_prefix is not None else self .config .get ('env_prefix' , '' )
141
146
self .env_ignore_empty = (
142
147
env_ignore_empty if env_ignore_empty is not None else self .config .get ('env_ignore_empty' , False )
143
148
)
149
+ self .env_parse_none_str = (
150
+ env_parse_none_str if env_parse_none_str is not None else self .config .get ('env_parse_none_str' )
151
+ )
144
152
145
153
def _apply_case_sensitive (self , value : str ) -> str :
146
154
return value .lower () if not self .case_sensitive else value
@@ -244,6 +252,20 @@ class Settings(BaseSettings):
244
252
245
253
return values
246
254
255
+ def _replace_env_none_type_values (self , field_value : dict [str , Any ]) -> dict [str , Any ]:
256
+ """
257
+ Recursively parse values that are of "None" type(EnvNoneType) to `None` type(None).
258
+ """
259
+ values : dict [str , Any ] = {}
260
+
261
+ for key , value in field_value .items ():
262
+ if not isinstance (value , EnvNoneType ):
263
+ values [key ] = value if not isinstance (value , dict ) else self ._replace_env_none_type_values (value )
264
+ else :
265
+ values [key ] = None
266
+
267
+ return values
268
+
247
269
def __call__ (self ) -> dict [str , Any ]:
248
270
data : dict [str , Any ] = {}
249
271
@@ -263,6 +285,11 @@ def __call__(self) -> dict[str, Any]:
263
285
) from e
264
286
265
287
if field_value is not None :
288
+ if self .env_parse_none_str is not None :
289
+ if isinstance (field_value , dict ):
290
+ field_value = self ._replace_env_none_type_values (field_value )
291
+ elif isinstance (field_value , EnvNoneType ):
292
+ field_value = None
266
293
if (
267
294
not self .case_sensitive
268
295
and lenient_issubclass (field .annotation , BaseModel )
@@ -287,8 +314,9 @@ def __init__(
287
314
case_sensitive : bool | None = None ,
288
315
env_prefix : str | None = None ,
289
316
env_ignore_empty : bool | None = None ,
317
+ env_parse_none_str : str | None = None ,
290
318
) -> None :
291
- super ().__init__ (settings_cls , case_sensitive , env_prefix , env_ignore_empty )
319
+ super ().__init__ (settings_cls , case_sensitive , env_prefix , env_ignore_empty , env_parse_none_str )
292
320
self .secrets_dir = secrets_dir if secrets_dir is not None else self .config .get ('secrets_dir' )
293
321
294
322
def __call__ (self ) -> dict [str , Any ]:
@@ -376,8 +404,9 @@ def __init__(
376
404
env_prefix : str | None = None ,
377
405
env_nested_delimiter : str | None = None ,
378
406
env_ignore_empty : bool | None = None ,
407
+ env_parse_none_str : str | None = None ,
379
408
) -> None :
380
- super ().__init__ (settings_cls , case_sensitive , env_prefix , env_ignore_empty )
409
+ super ().__init__ (settings_cls , case_sensitive , env_prefix , env_ignore_empty , env_parse_none_str )
381
410
self .env_nested_delimiter = (
382
411
env_nested_delimiter if env_nested_delimiter is not None else self .config .get ('env_nested_delimiter' )
383
412
)
@@ -386,7 +415,7 @@ def __init__(
386
415
self .env_vars = self ._load_env_vars ()
387
416
388
417
def _load_env_vars (self ) -> Mapping [str , str | None ]:
389
- return parse_env_vars (os .environ , self .case_sensitive , self .env_ignore_empty )
418
+ return parse_env_vars (os .environ , self .case_sensitive , self .env_ignore_empty , self . env_parse_none_str )
390
419
391
420
def get_field_value (self , field : FieldInfo , field_name : str ) -> tuple [Any , str , bool ]:
392
421
"""
@@ -570,12 +599,15 @@ def __init__(
570
599
env_prefix : str | None = None ,
571
600
env_nested_delimiter : str | None = None ,
572
601
env_ignore_empty : bool | None = None ,
602
+ env_parse_none_str : str | None = None ,
573
603
) -> None :
574
604
self .env_file = env_file if env_file != ENV_FILE_SENTINEL else settings_cls .model_config .get ('env_file' )
575
605
self .env_file_encoding = (
576
606
env_file_encoding if env_file_encoding is not None else settings_cls .model_config .get ('env_file_encoding' )
577
607
)
578
- super ().__init__ (settings_cls , case_sensitive , env_prefix , env_nested_delimiter , env_ignore_empty )
608
+ super ().__init__ (
609
+ settings_cls , case_sensitive , env_prefix , env_nested_delimiter , env_ignore_empty , env_parse_none_str
610
+ )
579
611
580
612
def _load_env_vars (self ) -> Mapping [str , str | None ]:
581
613
return self ._read_env_files ()
@@ -598,6 +630,7 @@ def _read_env_files(self) -> Mapping[str, str | None]:
598
630
encoding = self .env_file_encoding ,
599
631
case_sensitive = self .case_sensitive ,
600
632
ignore_empty = self .env_ignore_empty ,
633
+ parse_none_str = self .env_parse_none_str ,
601
634
)
602
635
)
603
636
@@ -635,10 +668,21 @@ def _get_env_var_key(key: str, case_sensitive: bool = False) -> str:
635
668
return key if case_sensitive else key .lower ()
636
669
637
670
671
+ def _parse_env_none_str (value : str | None , parse_none_str : str | None = None ) -> str | None | EnvNoneType :
672
+ return value if not (value == parse_none_str and parse_none_str is not None ) else EnvNoneType (value )
673
+
674
+
638
675
def parse_env_vars (
639
- env_vars : Mapping [str , str | None ], case_sensitive : bool = False , ignore_empty : bool = False
676
+ env_vars : Mapping [str , str | None ],
677
+ case_sensitive : bool = False ,
678
+ ignore_empty : bool = False ,
679
+ parse_none_str : str | None = None ,
640
680
) -> Mapping [str , str | None ]:
641
- return {_get_env_var_key (k , case_sensitive ): v for k , v in env_vars .items () if not (ignore_empty and v == '' )}
681
+ return {
682
+ _get_env_var_key (k , case_sensitive ): _parse_env_none_str (v , parse_none_str )
683
+ for k , v in env_vars .items ()
684
+ if not (ignore_empty and v == '' )
685
+ }
642
686
643
687
644
688
def read_env_file (
@@ -647,9 +691,10 @@ def read_env_file(
647
691
encoding : str | None = None ,
648
692
case_sensitive : bool = False ,
649
693
ignore_empty : bool = False ,
694
+ parse_none_str : str | None = None ,
650
695
) -> Mapping [str , str | None ]:
651
696
file_vars : dict [str , str | None ] = dotenv_values (file_path , encoding = encoding or 'utf8' )
652
- return parse_env_vars (file_vars , case_sensitive , ignore_empty )
697
+ return parse_env_vars (file_vars , case_sensitive , ignore_empty , parse_none_str )
653
698
654
699
655
700
def _annotation_is_complex (annotation : type [Any ] | None , metadata : list [Any ]) -> bool :
0 commit comments