Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat: add support for case insensitive env names #313

1 change: 1 addition & 0 deletions HISTORY.rst
Expand Up @@ -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)
....................
Expand Down
8 changes: 8 additions & 0 deletions 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
8 changes: 7 additions & 1 deletion docs/index.rst
Expand Up @@ -231,7 +231,7 @@ Outputs:

The generated schemas are compliant with the specifications:
`JSON Schema Core <https://json-schema.org/latest/json-schema-core.html>`__,
`JSON Schema Validation <https://json-schema.org/latest/json-schema-validation.html>`__ and
`JSON Schema Validation <https://json-schema.org/latest/json-schema-validation.html>`__ and
`OpenAPI <https://github.com/OAI/OpenAPI-Specification>`__.

``BaseModel.schema`` will return a dict of the schema, while ``BaseModel.schema_json`` will return a JSON string
Expand Down Expand Up @@ -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
......................

Expand Down
25 changes: 19 additions & 6 deletions pydantic/env_settings.py
Expand Up @@ -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.
Expand All @@ -36,23 +38,34 @@ def _substitute_environ(self):
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)
Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I came to the conclusion that env_val was a better ident name now that we have env_vars above. env_var in context of env_vars sounds like its an item from the list. Buts its not. It is a value from the environ dict while env_vars is said environ dict.

If you prefer to not touch env_var ident name, I would suggest using a different name than env_vars above. Maybe normalized_environ?


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
15 changes: 15 additions & 0 deletions tests/test_settings.py
Expand Up @@ -110,3 +110,18 @@ class Config:
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'