environment.py¶
This library provides parsing and validation of environment variables.
Installation¶
environment
is available on GitHub and on PyPI:
$ pip install environment
We test against Python 2.6, 2.7, 3.2, and 3.3.
environment
is MIT-licensed.
Rationale¶
Configuration via environment variables has become popular with the rise of twelve-factor apps, yet few Python libraries exist to help with it (despite the abundance of libraries for command line and file configuration).
When I looked around, most of the solutions I found involved using
os.environ
directly, or overloading somewhat related libraries such
as argparse
or formencode
. The former are not robust enough
with regards to typecasting and error handling. The latter are inappropriate
and overengineered: the reason to prefer envvar configuration in the first
place is to reduce complexity, not compound it. We need something designed
specifically and solely for taking configuration from environment variables.
The one library I found is python-decouple, which does indeed rationalize
typecasting of environment variables. However, it also handles file
configuration, which adds unwanted complexity and (ironically) muddying of
concerns. Additionally, it doesn’t enable robust error messaging. The problem
with error handling in decouple
and in ad-hoc usage of
os.environ
is that if you have four environment variables wrong, you
only find out about them one at a time. We want to find out about all problems
with our configuration at once, so that we can solve them all at once instead
of playing configuration roulette (“Will it work this time? No! How about
now?”).
This present library is designed to be small in scope, limited to environment variables only, and to support robust error messaging. Look into foreman and honcho for process management tools to complement this library.
Tutorial¶
First let’s pretend that this is our os.environ
:
>>> pretend_os_environ = { 'FOO': '42'
... , 'BAR_BAZ': 'buz'
... , 'BAR_BLOO_BLOO': 'yes'
... , 'BAD': 'to the bone'
... }
And let’s import our stuff:
>>> from environment import Environment, is_yesish
The way the environment
library works is you instantiate an
Environment
class like so:
>>> env = Environment( FOO=int
... , BLAH=str
... , BAD=int
... , environ=pretend_os_environ
... )
Keyword arguments specify which variables to look for and how to typecast them. Since a process environment contains a lot of crap you don’t care about, we only parse out variables that you explicitly specify in the keyword arguments.
The resulting object has lowercase attributes for all variables that were asked for and found:
>>> env.foo
42
There are also missing
and
malformed
attributes for variables that weren’t found
or couldn’t be typecast:
>>> env.missing
['BLAH']
>>> env.malformed
[('BAD', "ValueError: invalid literal for int() with base 10: 'to the bone'")]
You’re expected to inspect the contents of missing
and
malformed
and do your own error reporting. You’re also
expected to handle defaults yourself at a higher level—this is not a
general-purpose configuration library—though the
parsed
dictionary should help with that:
>>> env.parsed
{'foo': 42}
If all of the environment variables you care about share a common prefix, you can specify this to the constructor to save yourself some clutter:
>>> bar = Environment( 'BAR_'
... , BAZ=str
... , BLOO_BLOO=is_yesish
... , environ=pretend_os_environ
... )
>>> bar.baz
'buz'
>>> bar.bloo_bloo
True
API Reference¶
-
class
environment.
Environment
(prefix='', spec=None, environ=None, **kw)¶ Represent a whitelisted, parsed subset of a process environment.
Parameters: - prefix (string) – If all of the environment variables of interest to you share a common prefix, you can specify that here. We will use this prefix when pulling values out of the environment, and the attribute names you end up with won’t include the prefix.
- spec (mapping) – A mapping of environment variable names to typecasters.
- environ (mapping) – By default we look at
os.environ
, of course, but you can override that with this. We operate on a shallow copy of this mapping (though it’s effectively a deep copy in the normal case where all values are strings, since strings are immutable). - kw – Keyword arguments are folded into
spec
.
The constructor for this class loops through the items in
environ
, skipping those variables not also named inspec
, and parsing those that are, using thetype
specified. Under Python 2, we harmonize with Python 3’s behavior by decoding environment variable values tounicode
using the result ofsys.getfilesystemencoding
before typecasting. The upshot is that if you want typecasting to be a pass-through for a particular variable, you should specify the Python-version-appropriate string type:str
for Python 3,unicode
for Python 2. We store variables using lowercased names, soMYVAR
would end up atenv.myvar
:>>> env = Environment(MYVAR=int, environ={'MYVAR': 42}) >>> env.myvar 42
If a variable is mentioned in
spec
but is not inenviron
, the variable name is recorded in themissing
list. If typecasting a variable raises an exception, the variable name and an error message are recorded in themalformed
list:>>> env = Environment(MYVAR=int, OTHER=str, environ={'MYVAR': 'blah'}) >>> env.missing ['OTHER'] >>> env.malformed [('MYVAR', "ValueError: invalid literal for int() with base 10: 'blah'")]
If
prefix
is provided, then we’ll add that to the variable names inspec
when reading the environment:>>> foo = Environment('FOO_', BAR=int, environ={'FOO_BAR': '42'}) >>> foo.prefix 'FOO_' >>> foo.bar 42
The copy of
environ
that we act on is stored atenviron
:>>> foo.environ {'FOO_BAR': '42'}
All parsed variables are stored in the dictionary at
parsed
:>>> foo.parsed {'bar': 42}
Use the
parsed
dictionary, for example, to fold configuration from the environment together with configuration from other sources (command line, config files, defaults) in higher-order data structures. Attribute access for non-class attributes onEnvironment
instances usesparsed
rather than__dict__
, which means that you can set attributes on the instance and they’re reflected inparsed
:>>> foo.bar = 537 >>> foo.parsed {'bar': 537}
But setting attributes doesn’t modify
environ
:>>> foo.environ {'FOO_BAR': '42'}
-
static
parse
(prefix, spec, environ, encoding)¶ Heavy lifting, with no side-effects on
self
.Parameters: - prefix (string) – The string to prefix to variable names when
looking them up in
environ
. - spec (mapping) – A mapping of environment variable names to typecasters.
- environ (mapping) – A mapping of environment variable names to values.
- encoding (string) – The encoding with which to decode environment
variable values before typecasting them, or
None
to suppress decoding.
Returns: A three-tuple, corresponding to
missing
,malformed
, andparsed
.- prefix (string) – The string to prefix to variable names when
looking them up in
-
environment.
is_yesish
(value)¶ Typecast booleanish environment variables to
bool
.Parameters: value (string) – An environment variable value. Returns: True
ifvalue
is1
,true
, oryes
(case-insensitive);False
otherwise.