Refactored (1)

Performed a general refactoring, focusing on the following:

- Removing layers of abstraction in config handling
- Stubbing out proper config validator
- Updating envvar names located throughout the project
This commit is contained in:
Chris Lane 2019-01-31 16:45:28 -05:00
parent c922ef4c8d
commit 8f757d7735
5 changed files with 99 additions and 136 deletions

View File

@ -43,13 +43,16 @@ from cheat.configuration import Configuration
from docopt import docopt from docopt import docopt
import os import os
if __name__ == '__main__': if __name__ == '__main__':
# parse the command-line options # parse the command-line options
options = docopt(__doc__, version='cheat 2.4.2') options = docopt(__doc__, version='cheat 2.4.2')
# initialize and validate configs
config = Configuration() config = Configuration()
config.validate()
# bootsrap
sheets = Sheets(config) sheets = Sheets(config)
utils = Utils(config) utils = Utils(config)
sheet = Sheet(sheets, utils) sheet = Sheet(sheets, utils)

View File

@ -2,133 +2,98 @@ import os
from cheat.utils import Utils from cheat.utils import Utils
import json import json
class Configuration: class Configuration:
def __init__(self): def __init__(self):
self._get_global_conf_file_path() # compute the location of the config files
self._get_local_conf_file_path() config_file_path_global = os.environ.get('CHEAT_GLOBAL_CONF_PATH') \
self._saved_configuration = self._get_configuration() or '/etc/cheat'
config_file_path_local = (os.environ.get('CHEAT_LOCAL_CONF_PATH') \
def _get_configuration(self): or os.path.expanduser('~/.config/cheat/cheat'))
# get options from config files and environment vairables
merged_config = {}
# attempt to read the global config file
config = {}
try: try:
merged_config.update( config.update(self._read_config_file(config_file_path_global))
self._read_configuration_file(self.glob_config_path)
)
except Exception as e: except Exception as e:
Utils.warn('error while parsing global configuration Reason: ' Utils.warn('Error while parsing global configuration: ' + e.message)
+ e.message
)
# attempt to read the local config file
try: try:
merged_config.update( config.update(self._read_config_file(config_file_path_local))
self._read_configuration_file(self.local_config_path)
)
except Exception as e: except Exception as e:
Utils.warn('error while parsing user configuration Reason: ' Utils.warn('Error while parsing local configuration: ' + e.message)
+ e.message
)
merged_config.update(self._read_env_vars_config()) # With config files read, now begin to apply envvar overrides and
# default values
self._check_configuration(merged_config) # self.cheat_colors
self.cheat_colors = self._select([
os.environ.get('CHEAT_COLORS'),
os.environ.get('CHEATCOLORS'),
config.get('CHEAT_COLORS'),
True,
])
# convert strings to bool as necessary
if (isinstance(self.cheat_colors, str)):
self.cheat_colors = True \
if self.cheat_colors.strip().lower() == 'true' \
else False
return merged_config # self.cheat_default_dir
self.cheat_default_dir = self._select([
os.environ.get('CHEAT_DEFAULT_DIR'),
os.environ.get('DEFAULT_CHEAT_DIR'),
'~/.cheat',
])
def _read_configuration_file(self, path): # self.cheat_editor
self.cheat_editor = self._select([
os.environ.get('CHEAT_EDITOR'),
os.environ.get('EDITOR'),
os.environ.get('VISUAL'),
config.get('CHEAT_EDITOR'),
'vi',
])
# self.cheat_highlight
self.cheat_highlight = self._select([
os.environ.get('CHEAT_HIGHLIGHT'),
config.get('CHEAT_HIGHLIGHT'),
False,
])
# self.cheat_path
self.cheat_path = self._select([
os.environ.get('CHEAT_PATH'),
os.environ.get('CHEATPATH'),
config.get('CHEAT_PATH'),
'/usr/share/cheat',
])
def _read_config_file(self, path):
# Reads configuration file and returns list of set variables # Reads configuration file and returns list of set variables
read_config = {} config = {}
if (os.path.isfile(path)): if (os.path.isfile(path)):
with open(path) as config_file: with open(path) as config_file:
read_config.update(json.load(config_file)) config.update(json.load(config_file))
return read_config return config
def _read_env_vars_config(self): def _select(self, values):
read_config = {} for v in values:
if v is not None:
return v
# NOTE: These variables are left here because of backwards def validate(self):
# compatibility and are supported only as env vars but not in """ Validate configuration parameters """
# configuration file
if (os.environ.get('VISUAL')): # assert that cheat_highlight contains a valid value
read_config['EDITOR'] = os.environ.get('VISUAL') highlights = [
# variables supported both in environment and configuration file
# NOTE: Variables without CHEAT_ prefix are legacy
# key is variable name and value is its legacy_alias
# if variable has no legacy alias then set to None
variables = {'CHEAT_DEFAULT_DIR': 'DEFAULT_CHEAT_DIR',
'CHEAT_PATH': 'CHEATPATH',
'CHEAT_COLORS': 'CHEATCOLORS',
'CHEAT_EDITOR': 'EDITOR',
'CHEAT_HIGHLIGHT': None
}
for (k, v) in variables.items():
self._read_env_var(read_config, k, v)
return read_config
def _check_configuration(self, config):
""" Check values in config and warn user or die """
# validate CHEAT_HIGHLIGHT values if set
colors = [
'grey', 'red', 'green', 'yellow', 'grey', 'red', 'green', 'yellow',
'blue', 'magenta', 'cyan', 'white' 'blue', 'magenta', 'cyan', 'white',
False
] ]
if ( if (self.cheat_highlight not in highlights):
config.get('CHEAT_HIGHLIGHT') and Utils.die("%s %s" % ('CHEAT_HIGHLIGHT must be one of:', highlights))
config.get('CHEAT_HIGHLIGHT') not in colors
):
Utils.die("%s %s" % ('CHEAT_HIGHLIGHT must be one of:', colors))
def _read_env_var(self, current_config, key, alias=None): return True
if os.environ.get(key) is not None:
current_config[key] = os.environ.get(key)
return
elif alias is not None and os.environ.get(alias) is not None:
current_config[key] = os.environ.get(alias)
return
def _get_global_conf_file_path(self):
self.glob_config_path = (os.environ.get('CHEAT_GLOBAL_CONF_PATH')
or '/etc/cheat')
def _get_local_conf_file_path(self):
path = (os.environ.get('CHEAT_LOCAL_CONF_PATH')
or os.path.expanduser('~/.config/cheat/cheat'))
self.local_config_path = path
def _choose_value(self, primary_value_name, secondary_value_name):
""" Return primary or secondary value in saved_configuration
If primary value is in configuration then return it. If it is not
then return secondary. In the absence of both values return None
"""
primary_value = self._saved_configuration.get(primary_value_name)
secondary_value = self._saved_configuration.get(secondary_value_name)
if primary_value is not None:
return primary_value
else:
return secondary_value
def get_default_cheat_dir(self):
return self._choose_value('CHEAT_DEFAULT_DIR', 'DEFAULT_CHEAT_DIR')
def get_cheatpath(self):
return self._choose_value('CHEAT_PATH', 'CHEATPATH')
def get_cheatcolors(self):
return self._choose_value('CHEAT_COLORS', 'CHEATCOLORS')
def get_editor(self):
return self._choose_value('CHEAT_EDITOR', 'EDITOR')
def get_highlight(self):
return self._saved_configuration.get('CHEAT_HIGHLIGHT')

View File

@ -7,20 +7,21 @@ from cheat.utils import Utils
class Sheets: class Sheets:
def __init__(self, config): def __init__(self, config):
self._default_cheat_dir = config.get_default_cheat_dir() self._config = config
self._cheatpath = config.get_cheatpath()
self._utils = Utils(config) self._utils = Utils(config)
def default_path(self): def default_path(self):
""" Returns the default cheatsheet path """ """ Returns the default cheatsheet path """
# determine the default cheatsheet dir # determine the default cheatsheet dir
default_sheets_dir = (self._default_cheat_dir or # TODO: should probably rename `CHEAT_DEFAULT_DIR` to
# `CHEAT_USER_DIR` or something for clarity.
default_sheets_dir = (self._config.cheat_default_dir or
os.path.join('~', '.cheat')) os.path.join('~', '.cheat'))
default_sheets_dir = os.path.expanduser( default_sheets_dir = os.path.expanduser(
os.path.expandvars(default_sheets_dir)) os.path.expandvars(default_sheets_dir))
# create the DEFAULT_CHEAT_DIR if it does not exist # create the CHEAT_DEFAULT_DIR if it does not exist
if not os.path.isdir(default_sheets_dir): if not os.path.isdir(default_sheets_dir):
try: try:
# @kludge: unclear on why this is necessary # @kludge: unclear on why this is necessary
@ -28,15 +29,15 @@ class Sheets:
os.mkdir(default_sheets_dir) os.mkdir(default_sheets_dir)
except OSError: except OSError:
Utils.die('Could not create DEFAULT_CHEAT_DIR') Utils.die('Could not create CHEAT_DEFAULT_DIR')
# assert that the DEFAULT_CHEAT_DIR is readable and writable # assert that the CHEAT_DEFAULT_DIR is readable and writable
if not os.access(default_sheets_dir, os.R_OK): if not os.access(default_sheets_dir, os.R_OK):
Utils.die('The DEFAULT_CHEAT_DIR (' Utils.die('The CHEAT_DEFAULT_DIR ('
+ default_sheets_dir + default_sheets_dir
+ ') is not readable.') + ') is not readable.')
if not os.access(default_sheets_dir, os.W_OK): if not os.access(default_sheets_dir, os.W_OK):
Utils.die('The DEFAULT_CHEAT_DIR (' Utils.die('The CHEAT_DEFAULT_DIR ('
+ default_sheets_dir + default_sheets_dir
+ ') is not writable.') + ') is not writable.')
@ -67,8 +68,8 @@ class Sheets:
] ]
# merge the CHEATPATH paths into the sheet_paths # merge the CHEATPATH paths into the sheet_paths
if self._cheatpath: if self._config.cheat_path:
for path in self._cheatpath.split(os.pathsep): for path in self._config.cheat_path.split(os.pathsep):
if os.path.isdir(path): if os.path.isdir(path):
sheet_paths.append(path) sheet_paths.append(path)

View File

@ -7,15 +7,13 @@ import sys
class Utils: class Utils:
def __init__(self, config): def __init__(self, config):
self._displaycolors = config.get_cheatcolors() self._config = config
self._editor_executable = config.get_editor()
self._highlight_color = config.get_highlight()
def highlight(self, needle, haystack): def highlight(self, needle, haystack):
""" Highlights a search term matched within a line """ """ Highlights a search term matched within a line """
# if a highlight color is not configured, exit early # if a highlight color is not configured, exit early
if not self._highlight_color: if not self._config.cheat_highlight:
return haystack return haystack
# otherwise, attempt to import the termcolor library # otherwise, attempt to import the termcolor library
@ -27,18 +25,14 @@ class Utils:
return haystack return haystack
# if the import succeeds, colorize the needle in haystack # if the import succeeds, colorize the needle in haystack
return haystack.replace(needle, colored(needle, self._highlight_color)) return haystack.replace(needle, colored(needle, self._config.cheat_highlight))
def colorize(self, sheet_content): def colorize(self, sheet_content):
""" Colorizes cheatsheet content if so configured """ """ Colorizes cheatsheet content if so configured """
# cover all possible positive values to be safe # only colorize if cheat_colors is true, and stdout is a tty
positive_values = ["True", "true", "1", 1, True] if (self._config.cheat_colors is False or not sys.stdout.isatty()):
return sheet_content
# only colorize if configured to do so, and if stdout is a tty
if (self._displaycolors not in positive_values or
not sys.stdout.isatty()):
return sheet_content
# don't attempt to colorize an empty cheatsheet # don't attempt to colorize an empty cheatsheet
if not sheet_content.strip(): if not sheet_content.strip():
@ -78,13 +72,13 @@ class Utils:
""" Determines the user's preferred editor """ """ Determines the user's preferred editor """
# assert that the editor is set # assert that the editor is set
if (not self._editor_executable): if (not self._config.cheat_editor):
Utils.die( Utils.die(
'You must set a CHEAT_EDITOR, VISUAL, or EDITOR environment ' 'You must set a CHEAT_EDITOR, VISUAL, or EDITOR environment '
'variable or setting in order to create/edit a cheatsheet.' 'variable or setting in order to create/edit a cheatsheet.'
) )
return self._editor_executable return self._config.cheat_editor
def open_with_editor(self, filepath): def open_with_editor(self, filepath):
""" Open `filepath` using the EDITOR specified by the env variables """ """ Open `filepath` using the EDITOR specified by the env variables """

View File

@ -1,5 +1,5 @@
{ {
"CHEAT_COLORS": true, "CHEAT_COLORS" : true,
"CHEAT_EDITOR":"vi", "CHEAT_EDITOR" : "vi",
"CHEAT_PATH":"/usr/share/cheat" "CHEAT_PATH" : "/usr/share/cheat"
} }