From 756454fb58b470cabb8a7dcc6cf4d2353543d6dd Mon Sep 17 00:00:00 2001 From: dmontagu <35119617+dmontagu@users.noreply.github.com> Date: Wed, 21 Aug 2019 04:48:28 -0700 Subject: [PATCH] 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. --- changes/721-dmontagu.rst | 2 + ...ensitive.py => settings_case_sensitive.py} | 2 +- docs/index.rst | 23 +++++++---- pydantic/env_settings.py | 15 +++----- tests/test_settings.py | 38 +++++++++++++++---- 5 files changed, 56 insertions(+), 24 deletions(-) create mode 100644 changes/721-dmontagu.rst rename docs/examples/{settings_case_insensitive.py => settings_case_sensitive.py} (78%) diff --git a/changes/721-dmontagu.rst b/changes/721-dmontagu.rst new file mode 100644 index 0000000..94ada96 --- /dev/null +++ b/changes/721-dmontagu.rst @@ -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. diff --git a/docs/examples/settings_case_insensitive.py b/docs/examples/settings_case_sensitive.py similarity index 78% rename from docs/examples/settings_case_insensitive.py rename to docs/examples/settings_case_sensitive.py index 6d28c70..5030704 100644 --- a/docs/examples/settings_case_insensitive.py +++ b/docs/examples/settings_case_sensitive.py @@ -5,4 +5,4 @@ class Settings(BaseSettings): redis_host = 'localhost' class Config: - case_insensitive = True + case_sensitive = True diff --git a/docs/index.rst b/docs/index.rst index 385e608..c7e94af 100644 --- a/docs/index.rst +++ b/docs/index.rst @@ -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 ...................... diff --git a/pydantic/env_settings.py b/pydantic/env_settings.py index bf5b7a4..35b938d 100644 --- a/pydantic/env_settings.py +++ b/pydantic/env_settings.py @@ -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 diff --git a/tests/test_settings.py b/tests/test_settings.py index e4df476..bd4b267 100644 --- a/tests/test_settings.py +++ b/tests/test_settings.py @@ -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')