Skip to content

Commit

Permalink
Remove Python 2 support. Adds ListKnob
Browse files Browse the repository at this point in the history
  • Loading branch information
sthysel committed Sep 25, 2018
1 parent d546cce commit fc3ba37
Show file tree
Hide file tree
Showing 3 changed files with 109 additions and 48 deletions.
22 changes: 19 additions & 3 deletions setup.cfg
Original file line number Diff line number Diff line change
Expand Up @@ -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
36 changes: 13 additions & 23 deletions src/environment.py
Original file line number Diff line number Diff line change
@@ -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
Expand All @@ -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)
Expand All @@ -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


Expand All @@ -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
Expand All @@ -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))
Expand All @@ -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)
Expand Down Expand Up @@ -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:
Expand Down
99 changes: 77 additions & 22 deletions src/knobs.py
Original file line number Diff line number Diff line change
@@ -1,7 +1,6 @@
from __future__ import absolute_import

import os
import sys
import json

import click
import tabulate
Expand All @@ -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.
Expand All @@ -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
Expand Down Expand Up @@ -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):
"""
Expand Down Expand Up @@ -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):
Expand All @@ -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):
"""
Expand All @@ -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')

Expand All @@ -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

0 comments on commit fc3ba37

Please sign in to comment.