Skip to content

Commit

Permalink
docs for typed-env (#488) (#493)
Browse files Browse the repository at this point in the history
Also created a custom exception class for missing env-var
  • Loading branch information
koreno authored Feb 4, 2020
1 parent 0d0f7bf commit 529627f
Show file tree
Hide file tree
Showing 3 changed files with 116 additions and 6 deletions.
1 change: 1 addition & 0 deletions docs/index.rst
Original file line number Diff line number Diff line change
Expand Up @@ -118,6 +118,7 @@ you read it in order. A quick :ref:`reference guide is available <guide-quickref
remote
utils
cli
typed_env
colors
changelog
quickref
Expand Down
96 changes: 96 additions & 0 deletions docs/typed_env.rst
Original file line number Diff line number Diff line change
@@ -0,0 +1,96 @@
.. _guide-typed-env:

TypedEnv
========
Plumbum provides this utility class to facilitate working with environment variables.
Similar to how :class:`plumbum.cli.Application` parses command line arguments into pythonic data types,
:class:`plumbum.typed_env.TypedEnv` parses environment variables:

class MyEnv(TypedEnv):
username = TypedEnv.Str("USER", default='anonymous')
path = TypedEnv.CSV("PATH", separator=":", type=local.path)
tmp = TypedEnv.Str(["TMP", "TEMP"]) # support 'fallback' var-names
is_travis = TypedEnv.Bool("TRAVIS", default=False) # True is 'yes/true/1' (case-insensitive)

We can now instantiate this class to access its attributes::

>>> env = MyEnv()
>>> env.username
'ofer'

>>> env.path
[<LocalPath /home/ofer/bin>,
<LocalPath /usr/local/bin>,
<LocalPath /usr/local/sbin>,
<LocalPath /usr/sbin>,
<LocalPath /usr/bin>,
<LocalPath /sbin>,
<LocalPath /bin>]

>>> env.tmp
Traceback (most recent call last):
[...]
KeyError: 'TMP'

>>> env.is_travis
False

Finally, our ``TypedEnv`` object allows us ad-hoc access to the rest of the environment variables, using dot-notation::

>>> env.HOME
'/home/ofer'

We can also update the environment via our ``TypedEnv`` object:

>>> env.tmp = "/tmp"
>>> env.tmp
'/tmp'

>>> from os import environ
>>> env.TMP
'/tmp'

>>> env.is_travis = True
>>> env.TRAVIS
'yes'

>>> env.path = [local.path("/a"), local.path("/b")]
>>> env.PATH
'/a:/b'


TypedEnv as an Abstraction Layer
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
The ``TypedEnv`` class is very useful for separating your application from the actual environment variables.
It provides a layer where parsing and normalizing can take place in a centralized fashion.

For example, you might start with this simple implementation::

class CiBuildEnv(TypedEnv):
job_id = TypedEnv.Str("BUILD_ID")


Later, as the application gets more complicated, you may expand your implementation like so::

class CiBuildEnv(TypedEnv):
is_travis = TypedEnv.Bool("TRAVIS", default=False)
_travis_job_id = TypedEnv.Str("TRAVIS_JOB_ID")
_jenkins_job_id = TypedEnv.Str("BUILD_ID")

@property
def job_id(self):
return self._travis_job_id if self.is_travis else self._jenkins_job_id



TypedEnv vs. local.env
^^^^^^^^^^^^^^^^^^^^^^

It is important to note that ``TypedEnv`` is separate and unrelated to the ``LocalEnv`` object that is provided via ``local.env``.

While ``TypedEnv`` reads and writes directly to ``os.environ``,
``local.env`` is a frozen copy taken at the start of the python session.

While ``TypedEnv`` is focused on parsing environment variables to be used by the current process,
``local.env``'s primary purpose is to manipulate the environment for child processes that are spawned
via plumbum's :ref:`local commands <guide-local-commands>`.
25 changes: 19 additions & 6 deletions plumbum/typed_env.py
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,11 @@
NO_DEFAULT = object()


# must not inherit from AttributeError, so not to mess with python's attribute-lookup flow
class EnvironmentVariableError(KeyError):
pass


class TypedEnv(MutableMapping):
"""
This object can be used in 'exploratory' mode:
Expand All @@ -29,7 +34,7 @@ class MyEnv(TypedEnv):
try:
print(p.tmp)
except KeyError:
except EnvironmentVariableError:
print("TMP/TEMP is not defined")
else:
assert False
Expand All @@ -52,7 +57,7 @@ def __get__(self, instance, owner):
return self
try:
return self.convert(instance._raw_get(*self.names))
except KeyError:
except EnvironmentVariableError:
if self.default is NO_DEFAULT:
raise
return self.default
Expand All @@ -64,6 +69,10 @@ class Str(_BaseVar):
pass

class Bool(_BaseVar):
"""
Converts 'yes|true|1|no|false|0' to the appropriate boolean value.
Case-insensitive. Throws a ``ValueError`` for any other value.
"""

def convert(self, s):
s = s.lower()
Expand All @@ -81,6 +90,10 @@ class Float(_BaseVar):
convert = staticmethod(float)

class CSV(_BaseVar):
"""
Comma-separated-strings get split using the ``separator`` (',' by default) into
a list of objects of type ``type`` (``str`` by default).
"""

def __init__(self, name, default=NO_DEFAULT, type=str, separator=","):
super(TypedEnv.CSV, self).__init__(name, default=default)
Expand Down Expand Up @@ -117,12 +130,12 @@ def _raw_get(self, *key_names):
if value is not NO_DEFAULT:
return value
else:
raise KeyError(key_names[0])
raise EnvironmentVariableError(key_names[0])

def __contains__(self, key):
try:
self._raw_get(key)
except KeyError:
except EnvironmentVariableError:
return False
else:
return True
Expand All @@ -131,7 +144,7 @@ def __getattr__(self, name):
# if we're here then there was no descriptor defined
try:
return self._raw_get(name)
except KeyError:
except EnvironmentVariableError:
raise AttributeError("%s has no attribute %r" % (self.__class__, name))

def __getitem__(self, key):
Expand All @@ -140,7 +153,7 @@ def __getitem__(self, key):
def get(self, key, default=None):
try:
return self[key]
except KeyError:
except EnvironmentVariableError:
return default

def __dir__(self):
Expand Down

0 comments on commit 529627f

Please sign in to comment.