From 529627f1f28173b0ba92148243647905e7da4e4e Mon Sep 17 00:00:00 2001 From: Ofer Koren Date: Tue, 4 Feb 2020 20:39:19 +0200 Subject: [PATCH] docs for typed-env (#488) (#493) Also created a custom exception class for missing env-var --- docs/index.rst | 1 + docs/typed_env.rst | 96 ++++++++++++++++++++++++++++++++++++++++++++ plumbum/typed_env.py | 25 +++++++++--- 3 files changed, 116 insertions(+), 6 deletions(-) create mode 100644 docs/typed_env.rst diff --git a/docs/index.rst b/docs/index.rst index c4143f88f..c8446af9a 100644 --- a/docs/index.rst +++ b/docs/index.rst @@ -118,6 +118,7 @@ you read it in order. A quick :ref:`reference guide is available >> env = MyEnv() + >>> env.username + 'ofer' + + >>> env.path + [, + , + , + , + , + , + ] + + >>> 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 `. diff --git a/plumbum/typed_env.py b/plumbum/typed_env.py index 9d9d12186..9c8cbf076 100644 --- a/plumbum/typed_env.py +++ b/plumbum/typed_env.py @@ -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: @@ -29,7 +34,7 @@ class MyEnv(TypedEnv): try: print(p.tmp) - except KeyError: + except EnvironmentVariableError: print("TMP/TEMP is not defined") else: assert False @@ -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 @@ -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() @@ -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) @@ -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 @@ -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): @@ -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):