diff --git a/setup.cfg b/setup.cfg index 9018227..c85d9ca 100644 --- a/setup.cfg +++ b/setup.cfg @@ -9,12 +9,28 @@ commit = True [bumpversion:file:docs/conf.py] -[flake8] -max-line-length = 160 - [tool:pytest] norecursedirs = dist build .tox [bdist_wheel] universal = 1 +[flake8] +max-line-length = 120 +exclude = .tox,.git,*/migrations/*,*/static/CACHE/*,docs,node_modules + +[pycodestyle] +max-line-length = 120 +exclude = .tox,.git,*/migrations/*,*/static/CACHE/*,docs,node_modules + +[yapf] +based_on_style = pep8 +column_limit = 120 +dedent_closing_brackets = true +coalesce_brackets = false +each_dict_entry_on_separate_line = true +split_complex_comprehension = true + +[importmagic] +multiline = 'parentheses' +max_columns = 120 diff --git a/src/environment.py b/src/environment.py index cc620d1..ad86a44 100644 --- a/src/environment.py +++ b/src/environment.py @@ -1,10 +1,7 @@ -# -*- coding: utf-8 -*- -# forked adapted and merged from +# forked, adapted and merged from # https://github.com/theskumar/python-dotenv # https://github.com/mattseymour/python-env -from __future__ import absolute_import - import codecs import os import re @@ -31,7 +28,7 @@ def load_dotenv(dotenv_path, verbose=False): """ if not os.path.exists(dotenv_path): if verbose: - warnings.warn("Not loading {}, it doesn't exist.".format(dotenv_path)) + warnings.warn(f"Not loading {dotenv_path}, it doesn't exist.") return None for k, v in dotenv_values(dotenv_path).items(): os.environ.setdefault(k, v) @@ -51,16 +48,14 @@ def get_key(dotenv_path, key_to_get, verbose=False): key_to_get = str(key_to_get) if not os.path.exists(dotenv_path): if verbose: - warnings.warn("Can't read {}, it doesn't exist.".format(dotenv_path)) + warnings.warn(f"Can't read {dotenv_path}, it doesn't exist.") return None dotenv_as_dict = dotenv_values(dotenv_path) if key_to_get in dotenv_as_dict: return dotenv_as_dict[key_to_get] else: if verbose: - warnings.warn("key {key} not found in {path}.".format( - key=key_to_get, path=dotenv_path) - ) + warnings.warn(f"key {key_to_get} not found in {dotenv_path}.") return None @@ -69,7 +64,7 @@ def set_key(dotenv_path, key_to_set, value_to_set, quote_mode='always', verbose= Adds or Updates a key/value to the given .env If the .env path given doesn't exist, fails instead of risking creating - an orphan .env somewhere in the filesystem + an orphan .env somewhere in the file-system :param dotenv_path: env path :param key_to_set: key @@ -82,7 +77,7 @@ def set_key(dotenv_path, key_to_set, value_to_set, quote_mode='always', verbose= value_to_set = str(value_to_set).strip("'").strip('"') if not os.path.exists(dotenv_path): if verbose: - warnings.warn("Can't write to {}, it doesn't exist.".format(dotenv_path)) + warnings.warn(f"Can't write to {dotenv_path}, it doesn't exist.") return None, key_to_set, value_to_set dotenv_as_dict = OrderedDict(parse_dotenv(dotenv_path)) @@ -108,16 +103,14 @@ def unset_key(dotenv_path, key_to_unset, quote_mode='always', verbose=False): key_to_unset = str(key_to_unset) if not os.path.exists(dotenv_path): if verbose: - warnings.warn("Can't delete from {}, it doesn't exist.".format(dotenv_path)) + warnings.warn(f"Can't delete from {dotenv_path}, it doesn't exist.") return None, key_to_unset dotenv_as_dict = dotenv_values(dotenv_path) if key_to_unset in dotenv_as_dict: dotenv_as_dict.pop(key_to_unset, None) else: if verbose: - warnings.warn("Key {key} not removed from {path}, key doesn't exist.".format( - key=key_to_unset, path=dotenv_path) - ) + warnings.warn(f"Key {key_to_unset} not removed from {dotenv_path}, key doesn't exist.") return None, key_to_unset success = flatten_and_write(dotenv_path, dotenv_as_dict, quote_mode) @@ -190,20 +183,17 @@ def _get_format(value, quote_mode='always'): Returns the quote format depending on the quote_mode. This determines if the key value will be quoted when written to the env file. - - :param value: - :param quote_mode: + + :param value: + :param quote_mode: :return: str :raises: KeyError if the quote_mode is unknown """ - formats = { - 'always': '{key}="{value}"\n', - 'auto': '{key}={value}\n' - } + formats = {'always': '{key}="{value}"\n', 'auto': '{key}={value}\n'} if quote_mode not in formats.keys(): - return KeyError('quote_mode {} is invalid'.format(quote_mode)) + return KeyError(f'quote_mode {quote_mode} is invalid') _mode = quote_mode if quote_mode == 'auto' and ' ' in value: diff --git a/src/knobs.py b/src/knobs.py index 434e240..c8a7737 100644 --- a/src/knobs.py +++ b/src/knobs.py @@ -1,7 +1,6 @@ -from __future__ import absolute_import - import os import sys +import json import click import tabulate @@ -13,7 +12,7 @@ BOOLEAN_TRUE_STRINGS = ('true', 'on', 'ok', 'y', 'yes', '1') -class Knob(object): +class Knob: """ A knob can be tuned to satisfaction. Lookup and _cast environment variables to the required type. @@ -38,7 +37,14 @@ class Knob(object): _register = {} - def __init__(self, env_name, default, unit='', description='', validator=None): + def __init__( + self, + env_name: str, + default, + unit: str = '', + description: str = '', + validator=None, + ): """ :param env_name: Name of environment variable :param default: Default knob setting @@ -70,7 +76,7 @@ def help(self): """ :return: Description string with default appended """ - return '{}, Default: {}{}'.format(self.description, self.get(), self.unit) + return f'{self.description}, Default: {self.get()}{self.unit}' def rm(self): """ @@ -120,14 +126,13 @@ def get(self): return val def __repr__(self): - return "{_class}('{env_name}', {default}, unit={unit}, description='{desc}', validator={validator})".format( - _class=self.__class__.__name__, - env_name=self.env_name, - default=repr(self.default), - unit=repr(self.unit), - desc=self.description, - validator=repr(self.validator), - ) + _class = self.__class__.__name__ + env_name = self.env_name + default = repr(self.default) + unit = repr(self.unit) + desc = self.description + validator = repr(self.validator) + return f"{_class}('{env_name}', {default}, unit={unit}, description='{desc}', validator={validator})" @classmethod def get_registered_knob(cls, name): @@ -145,7 +150,6 @@ def print_knobs_table(cls, ctx, param, value): click.echo(cls.get_knob_defaults_as_table()) ctx.exit() - @classmethod def get_knob_defaults_as_table(cls): """ @@ -158,8 +162,7 @@ def get_knob_defaults_as_table(cls): 'Knob': name, 'Description': cls.get_registered_knob(name).description, 'Default': cls.get_registered_knob(name).default - } - for name in sorted(cls._register.keys()) + } for name in sorted(cls._register.keys()) ] return tabulate.tabulate(knob_list, headers='keys', tablefmt='fancy_grid') @@ -172,15 +175,67 @@ def print_knobs_env(cls, ctx, param, value): @classmethod def get_knob_defaults(cls): - r""" Returns a string with defaults + """ Returns a string with defaults >>> Knob.get_knob_defaults() '# \n# HAVE_RUM=True\n\n# Yar\n# JOLLY_ROGER_PIRATES=124\n\n# Foo Bar\n# WUNDER=BAR\n' """ return '\n'.join( - ['# {description}\n# {knob}={default}\n'.format( - description=cls.get_registered_knob(name).description, - knob=name, - default=cls.get_registered_knob(name).default) - for name in sorted(cls._register.keys())] + [ + '# {description}\n# {knob}={default}\n'.format( + description=cls.get_registered_knob(name).description, + knob=name, + default=cls.get_registered_knob(name).default + ) for name in sorted(cls._register.keys()) + ] ) + + +class ListKnob(Knob): + """ + A specialised Knob that expects its value to be a json list environment variable like: + ENV_LIST_EXAMPLE='["Foo", "bar"]' + """ + + def __init__( + self, + env_name: str, + default, + unit: str = '', + description: str = '', + validator=None, + ): + """ + :param env_name: Name of environment variable + :param default: Default knob setting + :param unit: Unit description + :param description: What does this knob do + :param validator: Callable to validate value + """ + + # the default's type sets the python type of the value + # retrieved from the environment + self._cast = type([]) + + super().__init__(env_name, default, unit, description, validator) + + def get(self): + """ + convert json env variable if set to list + """ + source_value = os.getenv(self.env_name) + # set the environment if it is not set + if source_value is None: + os.environ[self.env_name] = str(self.default) + return self.default + + try: + val = json.loads(source_value) + except ValueError as e: + click.secho(e.message, err=True, color='red') + sys.exit(1) + + if self.validator: + val = self.validator(val) + + return val