From a0aa9e78cd84863a8ea9e2446c75cb1127379caa Mon Sep 17 00:00:00 2001 From: Jason Kuhrt Date: Mon, 26 Nov 2018 09:57:09 -0500 Subject: [PATCH] feat: add support for case insensitive env names (#313) * feat: add support for case insensitive env names Closes #277 * feedback: just alias os.environ * doc: update history * doc: mention case_insensitive option * refactor: feedback if-else expression assignment * fix: formatting * chore: encode black configuration in file to support IDEs * docs: fix example * feedback: no suppport for IDEs in this PR * feedback: style --- HISTORY.rst | 1 + docs/examples/settings_case_insensitive.py | 8 +++++++ docs/index.rst | 8 ++++++- pydantic/env_settings.py | 25 ++++++++++++++++------ tests/test_settings.py | 15 +++++++++++++ 5 files changed, 50 insertions(+), 7 deletions(-) create mode 100644 docs/examples/settings_case_insensitive.py diff --git a/HISTORY.rst b/HISTORY.rst index ea982f9..fc37c34 100644 --- a/HISTORY.rst +++ b/HISTORY.rst @@ -8,6 +8,7 @@ v0.16.0 (2018-XX-XX) * refactor schema generation to be compatible with JSON Schema and OpenAPI specs, #308 by @tiangolo * add ``schema`` to ``schema`` module to generate top-level schemas from base models, #308 by @tiangolo +* add ``case_insensitive`` option to ``BaseSettings`` ``Config``, #277 by @jasonkuhrt v0.15.0 (2018-11-18) .................... diff --git a/docs/examples/settings_case_insensitive.py b/docs/examples/settings_case_insensitive.py new file mode 100644 index 0000000..6d28c70 --- /dev/null +++ b/docs/examples/settings_case_insensitive.py @@ -0,0 +1,8 @@ +from pydantic import BaseSettings + + +class Settings(BaseSettings): + redis_host = 'localhost' + + class Config: + case_insensitive = True diff --git a/docs/index.rst b/docs/index.rst index 095cbec..ce3ba47 100644 --- a/docs/index.rst +++ b/docs/index.rst @@ -231,7 +231,7 @@ Outputs: The generated schemas are compliant with the specifications: `JSON Schema Core `__, -`JSON Schema Validation `__ and +`JSON Schema Validation `__ and `OpenAPI `__. ``BaseModel.schema`` will return a dict of the schema, while ``BaseModel.schema_json`` will return a JSON string @@ -459,6 +459,12 @@ Here ``redis_port`` could be modified via ``export MY_PREFIX_REDIS_PORT=6380`` o 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: + +.. literalinclude:: examples/settings_case_insensitive.py + +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 fbd91b6..de1866a 100644 --- a/pydantic/env_settings.py +++ b/pydantic/env_settings.py @@ -21,7 +21,9 @@ class BaseSettings(BaseModel): """ Base class for settings, allowing values to be overridden by environment variables. - Environment variables must be upper case. Eg. to override foobar, `export APP_FOOBAR="whatever"`. + 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 places nicely with docker(-compose), Heroku and any 12 factor app design. @@ -36,23 +38,34 @@ class BaseSettings(BaseModel): Substitute environment variables into values. """ d = {} + + if self.__config__.case_insensitive: + env_vars = {k.lower(): v for (k, v) in os.environ.items()} + else: + env_vars = os.environ + for field in self.__fields__.values(): + if field.has_alias: env_name = field.alias else: env_name = self.__config__.env_prefix + field.name.upper() - env_var = os.getenv(env_name, None) - if env_var: + + env_name_ = env_name.lower() if self.__config__.case_insensitive else env_name + env_val = env_vars.get(env_name_, None) + + if env_val: if _complex_field(field): try: - env_var = json.loads(env_var) + env_val = json.loads(env_val) except ValueError as e: raise SettingsError(f'error parsing JSON for "{env_name}"') from e - d[field.alias] = env_var + d[field.alias] = env_val return d class Config: - env_prefix = 'APP_' + env_prefix = "APP_" validate_all = True ignore_extra = False arbitrary_types_allowed = True + case_insensitive = False diff --git a/tests/test_settings.py b/tests/test_settings.py index 68922e3..075cb0f 100644 --- a/tests/test_settings.py +++ b/tests/test_settings.py @@ -110,3 +110,18 @@ def test_alias_matches_name(env): env.set('foobar', 'xxx') s = Settings() assert s.foobar == 'xxx' + + +def test_case_insensitive(env): + class Settings(BaseSettings): + foo: str + bAR: str + + class Config: + case_insensitive = True + + env.set('apP_foO', 'foo') + env.set('app_bar', 'bar') + s = Settings() + assert s.foo == 'foo' + assert s.bAR == 'bar'