Change defaults for BaseSettings (#747)

* Change defaults for BaseSettings

* Update docs and fix build

* Minor documentation fixes

* Fix lowercase issues

* Update docs and fix build

* Fix formatting

* Try with monkeypatched test

* Fix doublequotes

* Change case_insensitive to case_sensitive

* more change details.
This commit is contained in:
dmontagu
2019-08-21 04:48:28 -07:00
committed by Samuel Colvin
parent 5e8db161b8
commit 756454fb58
5 changed files with 56 additions and 24 deletions
+2
View File
@@ -0,0 +1,2 @@
**Breaking Change:** modify default config settings for ``BaseSettings``; ``case_insensitive`` renamed to ``case_sensitive``,
default changed to ``case_sensitive = False``, ``env_prefix`` default changed to ``''`` - e.g. no prefix.
@@ -5,4 +5,4 @@ class Settings(BaseSettings):
redis_host = 'localhost'
class Config:
case_insensitive = True
case_sensitive = True
+16 -7
View File
@@ -864,24 +864,33 @@ environment variables or keyword arguments (e.g. in unit tests).
(This script is complete, it should run "as is")
Here ``redis_port`` could be modified via ``export MY_PREFIX_REDIS_PORT=6380`` or ``auth_key`` by
``export my_api_key=6380``.
``export my_api_key=6380``. By default, environment variables are treated as case-insensitive, so
``export my_prefix_redis_port=6380`` would work as well.
(Aliases are always sensitive to case, so ``export MY_API_KEY=6380`` would not work.)
By default ``BaseSettings`` considers field values in the following priority (where 3. has the highest priority
and overrides the other two):
1. The default values set in your ``Settings`` class
2. Environment variables eg. ``MY_PREFIX_REDIS_PORT`` as described above.
3. Argument passed to the ``Settings`` class on initialisation.
1. The default values set in your ``Settings`` class.
2. Environment variables, e.g. ``MY_PREFIX_REDIS_PORT`` as described above.
3. Arguments passed to the ``Settings`` class on initialisation.
This behaviour can be changed by overriding the ``_build_values`` method on ``BaseSettings``.
Complex types like ``list``, ``set``, ``dict`` and submodels can be set by using JSON environment variables.
Environment variables can be read in a case insensitive manner:
Case-sensitivity can be turned on through the ``Config``:
.. literalinclude:: examples/settings_case_insensitive.py
.. literalinclude:: examples/settings_case_sensitive.py
When ``case_sensitive`` is ``True``, the environment variable must be in all-caps,
so in this example ``redis_host`` could only be modified via ``export REDIS_HOST``.
.. note::
On Windows, python's `os` module always treats environment variables as case-insensitive, so the
``case_sensitive`` config setting will have no effect -- settings will always be updated ignoring case.
Here ``redis_port`` could be modified via ``export APP_REDIS_HOST``, ``export app_redis_host``, ``export app_REDIS_host``, etc.
Dynamic model creation
......................
+6 -9
View File
@@ -13,9 +13,6 @@ class BaseSettings(BaseModel):
"""
Base class for settings, allowing values to be overridden by environment variables.
By default environment variables must be upper case and prefixed by APP_ by default. Eg. to override foobar,
`export APP_FOOBAR="whatever"`. To change this behaviour set Config options case_insensitive and env_prefix.
This is useful in production for secrets you do not wish to save in code, it plays nicely with docker(-compose),
Heroku and any 12 factor app design.
"""
@@ -33,10 +30,10 @@ class BaseSettings(BaseModel):
"""
d: Dict[str, Optional[str]] = {}
if self.__config__.case_insensitive:
env_vars = {k.lower(): v for k, v in os.environ.items()}
else:
if self.__config__.case_sensitive:
env_vars = cast(Dict[str, str], os.environ)
else:
env_vars = {k.lower(): v for k, v in os.environ.items()}
for field in self.__fields__.values():
if field.has_alias:
@@ -44,7 +41,7 @@ class BaseSettings(BaseModel):
else:
env_name = self.__config__.env_prefix + field.name.upper()
env_name_ = env_name.lower() if self.__config__.case_insensitive else env_name
env_name_ = env_name if self.__config__.case_sensitive else env_name.lower()
env_val = env_vars.get(env_name_, None)
if env_val:
@@ -57,10 +54,10 @@ class BaseSettings(BaseModel):
return d
class Config:
env_prefix = 'APP_'
env_prefix = ''
validate_all = True
extra = Extra.forbid
arbitrary_types_allowed = True
case_insensitive = False
case_sensitive = False
__config__: Config # type: ignore
+31 -7
View File
@@ -1,14 +1,19 @@
import os
from typing import List, Set
import pytest
from pydantic import BaseModel, BaseSettings, NoneStr, ValidationError, dataclasses
from pydantic import BaseModel, BaseSettings, NoneStr, Schema, ValidationError, dataclasses
from pydantic.env_settings import SettingsError
class SimpleSettings(BaseSettings):
apple: str
class Config:
env_prefix = 'APP_'
case_sensitive = True
def test_sub_env(env):
env.set('APP_APPLE', 'hello')
@@ -54,6 +59,10 @@ class ComplexSettings(BaseSettings):
carrots: dict = {}
date: DateModel = DateModel()
class Config:
env_prefix = 'APP_'
case_sensitive = True
def test_list(env):
env.set('APP_APPLES', '["russet", "granny smith"]')
@@ -84,7 +93,7 @@ def test_required_sub_model(env):
with pytest.raises(ValidationError):
Settings()
env.set('APP_FOOBAR', '{"pips": "TRUE"}')
env.set('FOOBAR', '{"pips": "TRUE"}')
s = Settings()
assert s.foobar.pips is True
@@ -93,7 +102,7 @@ def test_non_class(env):
class Settings(BaseSettings):
foobar: NoneStr
env.set('APP_FOOBAR', 'xxx')
env.set('FOOBAR', 'xxx')
s = Settings()
assert s.foobar == 'xxx'
@@ -116,7 +125,8 @@ def test_case_insensitive(env):
bAR: str
class Config:
case_insensitive = True
env_prefix = 'APP_'
case_sensitive = False
env.set('apP_foO', 'foo')
env.set('app_bar', 'bar')
@@ -125,6 +135,20 @@ def test_case_insensitive(env):
assert s.bAR == 'bar'
def test_case_sensitive(monkeypatch):
class Settings(BaseSettings):
foo: str = Schema(..., alias='foo')
class Config:
case_sensitive = True
# Need to patch os.environ to get build to work on Windows, where os.environ is case insensitive
monkeypatch.setattr(os, 'environ', value={'Foo': 'foo'})
with pytest.raises(ValidationError) as exc_info:
Settings()
assert exc_info.value.errors() == [{'loc': ('foo',), 'msg': 'field required', 'type': 'value_error.missing'}]
def test_nested_dataclass(env):
@dataclasses.dataclass
class MyDataclass:
@@ -134,7 +158,7 @@ def test_nested_dataclass(env):
class Settings(BaseSettings):
n: MyDataclass
env.set('APP_N', '[123, "bar value"]')
env.set('N', '[123, "bar value"]')
s = Settings()
assert isinstance(s.n, MyDataclass)
assert s.n.foo == 123
@@ -149,7 +173,7 @@ def test_config_file_settings(env):
def _build_values(self, init_kwargs):
return {**init_kwargs, **self._build_environ()}
env.set('APP_BAR', 'env setting')
env.set('BAR', 'env setting')
s = Settings(foo='123', bar='argument')
assert s.foo == 123
@@ -170,7 +194,7 @@ def test_config_file_settings_nornir(env):
config_settings = init_kwargs.pop('__config_settings__')
return {**config_settings, **init_kwargs, **self._build_environ()}
env.set('APP_C', 'env setting c')
env.set('C', 'env setting c')
config = {'a': 'config a', 'b': 'config b', 'c': 'config c'}
s = Settings(__config_settings__=config, b='argument b', c='argument c')