From 8f757d7735fb798e4d119527fd6df50ea3eaaf80 Mon Sep 17 00:00:00 2001 From: Chris Lane Date: Thu, 31 Jan 2019 16:45:28 -0500 Subject: [PATCH 01/15] 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 --- bin/cheat | 5 +- cheat/configuration.py | 181 +++++++++++++++++------------------------ cheat/sheets.py | 21 ++--- cheat/utils.py | 22 ++--- config/cheat | 6 +- 5 files changed, 99 insertions(+), 136 deletions(-) diff --git a/bin/cheat b/bin/cheat index 252e52a..5e90ec4 100755 --- a/bin/cheat +++ b/bin/cheat @@ -43,13 +43,16 @@ from cheat.configuration import Configuration from docopt import docopt import os - if __name__ == '__main__': # parse the command-line options options = docopt(__doc__, version='cheat 2.4.2') + # initialize and validate configs config = Configuration() + config.validate() + + # bootsrap sheets = Sheets(config) utils = Utils(config) sheet = Sheet(sheets, utils) diff --git a/cheat/configuration.py b/cheat/configuration.py index 1700624..da3ed7d 100644 --- a/cheat/configuration.py +++ b/cheat/configuration.py @@ -2,133 +2,98 @@ import os from cheat.utils import Utils import json - class Configuration: def __init__(self): - self._get_global_conf_file_path() - self._get_local_conf_file_path() - self._saved_configuration = self._get_configuration() - - def _get_configuration(self): - # get options from config files and environment vairables - merged_config = {} + # compute the location of the config files + config_file_path_global = os.environ.get('CHEAT_GLOBAL_CONF_PATH') \ + or '/etc/cheat' + config_file_path_local = (os.environ.get('CHEAT_LOCAL_CONF_PATH') \ + or os.path.expanduser('~/.config/cheat/cheat')) + # attempt to read the global config file + config = {} try: - merged_config.update( - self._read_configuration_file(self.glob_config_path) - ) + config.update(self._read_config_file(config_file_path_global)) except Exception as e: - Utils.warn('error while parsing global configuration Reason: ' - + e.message - ) + Utils.warn('Error while parsing global configuration: ' + e.message) + # attempt to read the local config file try: - merged_config.update( - self._read_configuration_file(self.local_config_path) - ) + config.update(self._read_config_file(config_file_path_local)) except Exception as e: - Utils.warn('error while parsing user configuration Reason: ' - + e.message - ) + Utils.warn('Error while parsing local configuration: ' + 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 - read_config = {} + config = {} if (os.path.isfile(path)): with open(path) as config_file: - read_config.update(json.load(config_file)) - return read_config + config.update(json.load(config_file)) + return config - def _read_env_vars_config(self): - read_config = {} + def _select(self, values): + for v in values: + if v is not None: + return v - # NOTE: These variables are left here because of backwards - # compatibility and are supported only as env vars but not in - # configuration file + def validate(self): + """ Validate configuration parameters """ - if (os.environ.get('VISUAL')): - read_config['EDITOR'] = os.environ.get('VISUAL') - - # 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 = [ + # assert that cheat_highlight contains a valid value + highlights = [ 'grey', 'red', 'green', 'yellow', - 'blue', 'magenta', 'cyan', 'white' + 'blue', 'magenta', 'cyan', 'white', + False ] - if ( - config.get('CHEAT_HIGHLIGHT') and - config.get('CHEAT_HIGHLIGHT') not in colors - ): - Utils.die("%s %s" % ('CHEAT_HIGHLIGHT must be one of:', colors)) + if (self.cheat_highlight not in highlights): + Utils.die("%s %s" % ('CHEAT_HIGHLIGHT must be one of:', highlights)) - def _read_env_var(self, current_config, key, alias=None): - 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') + return True diff --git a/cheat/sheets.py b/cheat/sheets.py index 201c87d..f45c225 100644 --- a/cheat/sheets.py +++ b/cheat/sheets.py @@ -7,20 +7,21 @@ from cheat.utils import Utils class Sheets: def __init__(self, config): - self._default_cheat_dir = config.get_default_cheat_dir() - self._cheatpath = config.get_cheatpath() + self._config = config self._utils = Utils(config) def default_path(self): """ Returns the default cheatsheet path """ # 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')) default_sheets_dir = os.path.expanduser( 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): try: # @kludge: unclear on why this is necessary @@ -28,15 +29,15 @@ class Sheets: os.mkdir(default_sheets_dir) 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): - Utils.die('The DEFAULT_CHEAT_DIR (' + Utils.die('The CHEAT_DEFAULT_DIR (' + default_sheets_dir + ') is not readable.') 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 + ') is not writable.') @@ -67,8 +68,8 @@ class Sheets: ] # merge the CHEATPATH paths into the sheet_paths - if self._cheatpath: - for path in self._cheatpath.split(os.pathsep): + if self._config.cheat_path: + for path in self._config.cheat_path.split(os.pathsep): if os.path.isdir(path): sheet_paths.append(path) diff --git a/cheat/utils.py b/cheat/utils.py index e9d79e9..f1a46a3 100644 --- a/cheat/utils.py +++ b/cheat/utils.py @@ -7,15 +7,13 @@ import sys class Utils: def __init__(self, config): - self._displaycolors = config.get_cheatcolors() - self._editor_executable = config.get_editor() - self._highlight_color = config.get_highlight() + self._config = config def highlight(self, needle, haystack): """ Highlights a search term matched within a line """ # if a highlight color is not configured, exit early - if not self._highlight_color: + if not self._config.cheat_highlight: return haystack # otherwise, attempt to import the termcolor library @@ -27,18 +25,14 @@ class Utils: return 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): """ Colorizes cheatsheet content if so configured """ - # cover all possible positive values to be safe - positive_values = ["True", "true", "1", 1, True] - - # 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 + # only colorize if cheat_colors is true, and stdout is a tty + if (self._config.cheat_colors is False or not sys.stdout.isatty()): + return sheet_content # don't attempt to colorize an empty cheatsheet if not sheet_content.strip(): @@ -78,13 +72,13 @@ class Utils: """ Determines the user's preferred editor """ # assert that the editor is set - if (not self._editor_executable): + if (not self._config.cheat_editor): Utils.die( 'You must set a CHEAT_EDITOR, VISUAL, or EDITOR environment ' '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): """ Open `filepath` using the EDITOR specified by the env variables """ diff --git a/config/cheat b/config/cheat index fb1db33..bb18291 100644 --- a/config/cheat +++ b/config/cheat @@ -1,5 +1,5 @@ { - "CHEAT_COLORS": true, - "CHEAT_EDITOR":"vi", - "CHEAT_PATH":"/usr/share/cheat" + "CHEAT_COLORS" : true, + "CHEAT_EDITOR" : "vi", + "CHEAT_PATH" : "/usr/share/cheat" } From ab87bb11c46051aa65f75dacf4d9dc1b97cab029 Mon Sep 17 00:00:00 2001 From: Chris Lane Date: Thu, 31 Jan 2019 17:01:46 -0500 Subject: [PATCH 02/15] Refactored (2) Created an `Editor` class out methods in the `Util` class to enhance code clarity. --- bin/cheat | 10 ++++++---- cheat/editor.py | 28 ++++++++++++++++++++++++++++ cheat/sheet.py | 8 +++++--- cheat/utils.py | 20 -------------------- 4 files changed, 39 insertions(+), 27 deletions(-) create mode 100644 cheat/editor.py diff --git a/bin/cheat b/bin/cheat index 5e90ec4..2b518e7 100755 --- a/bin/cheat +++ b/bin/cheat @@ -36,10 +36,11 @@ Examples: # require the dependencies from __future__ import print_function -from cheat.sheets import Sheets -from cheat.sheet import Sheet -from cheat.utils import Utils from cheat.configuration import Configuration +from cheat.editor import Editor +from cheat.sheet import Sheet +from cheat.sheets import Sheets +from cheat.utils import Utils from docopt import docopt import os @@ -53,9 +54,10 @@ if __name__ == '__main__': config.validate() # bootsrap + editor = Editor(config) sheets = Sheets(config) utils = Utils(config) - sheet = Sheet(sheets, utils) + sheet = Sheet(sheets, utils, editor) # list directories if options['--directories']: diff --git a/cheat/editor.py b/cheat/editor.py new file mode 100644 index 0000000..cb35bd9 --- /dev/null +++ b/cheat/editor.py @@ -0,0 +1,28 @@ +from __future__ import print_function +import subprocess + + +class Editor: + + def __init__(self, config): + self._config = config + + def editor(self): + """ Determines the user's preferred editor """ + + # assert that the editor is set + if (not self._config.cheat_editor): + Utils.die( + 'You must set a CHEAT_EDITOR, VISUAL, or EDITOR environment ' + 'variable or setting in order to create/edit a cheatsheet.' + ) + + return self._config.cheat_editor + + def open(self, filepath): + """ Open `filepath` using the EDITOR specified by the env variables """ + editor_cmd = self.editor().split() + try: + subprocess.call(editor_cmd + [filepath]) + except OSError: + Utils.die('Could not launch ' + self.editor()) diff --git a/cheat/sheet.py b/cheat/sheet.py index 81553a1..0fffd82 100644 --- a/cheat/sheet.py +++ b/cheat/sheet.py @@ -2,14 +2,16 @@ import io import os import shutil +from cheat.editor import Editor from cheat.utils import Utils class Sheet: - def __init__(self, sheets, utils): + def __init__(self, sheets, utils, editor): self._sheets = sheets self._utils = utils + self._editor = editor def copy(self, current_sheet_path, new_sheet_path): """ Copies a sheet to a new path """ @@ -44,11 +46,11 @@ class Sheet: def create(self, sheet): """ Creates a cheatsheet """ new_sheet_path = os.path.join(self._sheets.default_path(), sheet) - self._utils.open_with_editor(new_sheet_path) + self._editor.open(new_sheet_path) def edit(self, sheet): """ Opens a cheatsheet for editing """ - self._utils.open_with_editor(self.path(sheet)) + self._editor.open(self.path(sheet)) def exists(self, sheet): """ Predicate that returns true if the sheet exists """ diff --git a/cheat/utils.py b/cheat/utils.py index f1a46a3..fad1453 100644 --- a/cheat/utils.py +++ b/cheat/utils.py @@ -68,26 +68,6 @@ class Utils: Utils.warn(message) exit(1) - def editor(self): - """ Determines the user's preferred editor """ - - # assert that the editor is set - if (not self._config.cheat_editor): - Utils.die( - 'You must set a CHEAT_EDITOR, VISUAL, or EDITOR environment ' - 'variable or setting in order to create/edit a cheatsheet.' - ) - - return self._config.cheat_editor - - def open_with_editor(self, filepath): - """ Open `filepath` using the EDITOR specified by the env variables """ - editor_cmd = self.editor().split() - try: - subprocess.call(editor_cmd + [filepath]) - except OSError: - Utils.die('Could not launch ' + self.editor()) - @staticmethod def warn(message): """ Prints a message to stderr """ From 928637c9db06b9f08f1bc6e124e3feffb5de4457 Mon Sep 17 00:00:00 2001 From: Chris Lane Date: Thu, 31 Jan 2019 17:14:21 -0500 Subject: [PATCH 03/15] Refactored (3) Removed unnecessary `import` calls. --- bin/cheat | 3 +-- cheat/sheet.py | 6 +----- cheat/utils.py | 2 -- 3 files changed, 2 insertions(+), 9 deletions(-) diff --git a/bin/cheat b/bin/cheat index 2b518e7..7358e93 100755 --- a/bin/cheat +++ b/bin/cheat @@ -42,7 +42,6 @@ from cheat.sheet import Sheet from cheat.sheets import Sheets from cheat.utils import Utils from docopt import docopt -import os if __name__ == '__main__': @@ -57,7 +56,7 @@ if __name__ == '__main__': editor = Editor(config) sheets = Sheets(config) utils = Utils(config) - sheet = Sheet(sheets, utils, editor) + sheet = Sheet(sheets, editor) # list directories if options['--directories']: diff --git a/cheat/sheet.py b/cheat/sheet.py index 0fffd82..cf7fbfe 100644 --- a/cheat/sheet.py +++ b/cheat/sheet.py @@ -2,15 +2,11 @@ import io import os import shutil -from cheat.editor import Editor -from cheat.utils import Utils - class Sheet: - def __init__(self, sheets, utils, editor): + def __init__(self, sheets, editor): self._sheets = sheets - self._utils = utils self._editor = editor def copy(self, current_sheet_path, new_sheet_path): diff --git a/cheat/utils.py b/cheat/utils.py index fad1453..b865756 100644 --- a/cheat/utils.py +++ b/cheat/utils.py @@ -1,6 +1,4 @@ from __future__ import print_function -import os -import subprocess import sys From 878d7e7e1b43dcbd2fc85f16312b4bbd0a842120 Mon Sep 17 00:00:00 2001 From: Chris Lane Date: Thu, 31 Jan 2019 17:40:53 -0500 Subject: [PATCH 04/15] Refactored (4) Improved handling of edge-cases in `configuration.py`. --- cheat/configuration.py | 23 ++++++++++++++--------- 1 file changed, 14 insertions(+), 9 deletions(-) diff --git a/cheat/configuration.py b/cheat/configuration.py index da3ed7d..1ad7ad4 100644 --- a/cheat/configuration.py +++ b/cheat/configuration.py @@ -1,6 +1,6 @@ -import os from cheat.utils import Utils import json +import os class Configuration: @@ -29,16 +29,11 @@ class Configuration: # self.cheat_colors self.cheat_colors = self._select([ - os.environ.get('CHEAT_COLORS'), - os.environ.get('CHEATCOLORS'), - config.get('CHEAT_COLORS'), + self._boolify(os.environ.get('CHEAT_COLORS')), + self._boolify(os.environ.get('CHEATCOLORS')), + self._boolify(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 # self.cheat_default_dir self.cheat_default_dir = self._select([ @@ -62,6 +57,8 @@ class Configuration: config.get('CHEAT_HIGHLIGHT'), False, ]) + if (self.cheat_highlight.strip().lower() == "false"): + self.cheat_highlight = False # self.cheat_path self.cheat_path = self._select([ @@ -71,6 +68,14 @@ class Configuration: '/usr/share/cheat', ]) + def _boolify(self, value): + # if `value` is not a string, return it as-is + if not isinstance(value, str): + return value + + # otherwise, convert "true" and "false" to Boolean counterparts + return value.strip().lower() == "true" + def _read_config_file(self, path): # Reads configuration file and returns list of set variables config = {} From 7c4fc546816263833bc3867e6ccd2352675666f3 Mon Sep 17 00:00:00 2001 From: Chris Lane Date: Thu, 31 Jan 2019 17:43:21 -0500 Subject: [PATCH 05/15] Refactored (5) - Extracted `Colorize` class out of `Util` class. (The latter now only contains static methods.) - Renamed methods in `Colorize` class for improved clarity. - Refactored as necessary to accommodate the changes above. --- bin/cheat | 11 +++++---- cheat/colorize.py | 61 +++++++++++++++++++++++++++++++++++++++++++++++ cheat/editor.py | 1 + cheat/sheet.py | 1 + cheat/sheets.py | 6 ++--- cheat/utils.py | 56 ------------------------------------------- 6 files changed, 72 insertions(+), 64 deletions(-) create mode 100644 cheat/colorize.py diff --git a/bin/cheat b/bin/cheat index 7358e93..a1a5aa6 100755 --- a/bin/cheat +++ b/bin/cheat @@ -36,11 +36,11 @@ Examples: # require the dependencies from __future__ import print_function +from cheat.colorize import Colorize from cheat.configuration import Configuration from cheat.editor import Editor from cheat.sheet import Sheet from cheat.sheets import Sheets -from cheat.utils import Utils from docopt import docopt if __name__ == '__main__': @@ -54,8 +54,9 @@ if __name__ == '__main__': # bootsrap editor = Editor(config) - sheets = Sheets(config) - utils = Utils(config) + colorize = Colorize(config) + + sheets = Sheets(config, colorize) sheet = Sheet(sheets, editor) # list directories @@ -72,8 +73,8 @@ if __name__ == '__main__': # search among the cheatsheets elif options['--search']: - print(utils.colorize(sheets.search(options[''])), end="") + print(colorize.syntax(sheets.search(options[''])), end="") # print the cheatsheet else: - print(utils.colorize(sheet.read(options[''])), end="") + print(colorize.syntax(sheet.read(options[''])), end="") diff --git a/cheat/colorize.py b/cheat/colorize.py new file mode 100644 index 0000000..f9ca6ef --- /dev/null +++ b/cheat/colorize.py @@ -0,0 +1,61 @@ +from __future__ import print_function +import sys + + +class Colorize: + + def __init__(self, config): + self._config = config + + def search(self, needle, haystack): + """ Colorizes search results matched within a line """ + + # if a highlight color is not configured, exit early + if not self._config.cheat_highlight: + return haystack + + # otherwise, attempt to import the termcolor library + try: + from termcolor import colored + + # if the import fails, return uncolored text + except ImportError: + return haystack + + # if the import succeeds, colorize the needle in haystack + return haystack.replace(needle, colored(needle, self._config.cheat_highlight)) + + def syntax(self, sheet_content): + """ Applies syntax highlighting """ + + # only colorize if cheat_colors is true, and stdout is a tty + if (self._config.cheat_colors is False or not sys.stdout.isatty()): + return sheet_content + + # don't attempt to colorize an empty cheatsheet + if not sheet_content.strip(): + return "" + + # otherwise, attempt to import the pygments library + try: + from pygments import highlight + from pygments.lexers import get_lexer_by_name + from pygments.formatters import TerminalFormatter + + # if the import fails, return uncolored text + except ImportError: + return sheet_content + + # otherwise, attempt to colorize + first_line = sheet_content.splitlines()[0] + lexer = get_lexer_by_name('bash') + + # apply syntax-highlighting if the first line is a code-fence + if first_line.startswith('```'): + sheet_content = '\n'.join(sheet_content.split('\n')[1:-2]) + try: + lexer = get_lexer_by_name(first_line[3:]) + except Exception: + pass + + return highlight(sheet_content, lexer, TerminalFormatter()) diff --git a/cheat/editor.py b/cheat/editor.py index cb35bd9..0da2fc4 100644 --- a/cheat/editor.py +++ b/cheat/editor.py @@ -1,4 +1,5 @@ from __future__ import print_function +from cheat.utils import Utils import subprocess diff --git a/cheat/sheet.py b/cheat/sheet.py index cf7fbfe..2221084 100644 --- a/cheat/sheet.py +++ b/cheat/sheet.py @@ -1,3 +1,4 @@ +from cheat.utils import Utils import io import os import shutil diff --git a/cheat/sheets.py b/cheat/sheets.py index f45c225..4bbcdde 100644 --- a/cheat/sheets.py +++ b/cheat/sheets.py @@ -6,9 +6,9 @@ from cheat.utils import Utils class Sheets: - def __init__(self, config): + def __init__(self, config, colorize): self._config = config - self._utils = Utils(config) + self._colorize = colorize; def default_path(self): """ Returns the default cheatsheet path """ @@ -95,7 +95,7 @@ class Sheets: match = '' for line in io.open(cheatsheet[1], encoding='utf-8'): if term in line: - match += ' ' + self._utils.highlight(term, line) + match += ' ' + self._colorize.search(term, line) if match != '': result += cheatsheet[0] + ":\n" + match + "\n" diff --git a/cheat/utils.py b/cheat/utils.py index b865756..01bd8a3 100644 --- a/cheat/utils.py +++ b/cheat/utils.py @@ -4,62 +4,6 @@ import sys class Utils: - def __init__(self, config): - self._config = config - - def highlight(self, needle, haystack): - """ Highlights a search term matched within a line """ - - # if a highlight color is not configured, exit early - if not self._config.cheat_highlight: - return haystack - - # otherwise, attempt to import the termcolor library - try: - from termcolor import colored - - # if the import fails, return uncolored text - except ImportError: - return haystack - - # if the import succeeds, colorize the needle in haystack - return haystack.replace(needle, colored(needle, self._config.cheat_highlight)) - - def colorize(self, sheet_content): - """ Colorizes cheatsheet content if so configured """ - - # only colorize if cheat_colors is true, and stdout is a tty - if (self._config.cheat_colors is False or not sys.stdout.isatty()): - return sheet_content - - # don't attempt to colorize an empty cheatsheet - if not sheet_content.strip(): - return "" - - # otherwise, attempt to import the pygments library - try: - from pygments import highlight - from pygments.lexers import get_lexer_by_name - from pygments.formatters import TerminalFormatter - - # if the import fails, return uncolored text - except ImportError: - return sheet_content - - # otherwise, attempt to colorize - first_line = sheet_content.splitlines()[0] - lexer = get_lexer_by_name('bash') - - # apply syntax-highlighting if the first line is a code-fence - if first_line.startswith('```'): - sheet_content = '\n'.join(sheet_content.split('\n')[1:-2]) - try: - lexer = get_lexer_by_name(first_line[3:]) - except Exception: - pass - - return highlight(sheet_content, lexer, TerminalFormatter()) - @staticmethod def die(message): """ Prints a message to stderr and then terminates """ From 145a81dcd6054830ae55292f1db3be1cec98a9b4 Mon Sep 17 00:00:00 2001 From: Chris Lane Date: Thu, 31 Jan 2019 17:53:34 -0500 Subject: [PATCH 06/15] Var renames Replaced more references to deprecated envvar names to their newer counterparts. --- bin/cheat | 2 +- cheat/sheet.py | 4 ++-- cheat/sheets.py | 6 +++--- 3 files changed, 6 insertions(+), 6 deletions(-) diff --git a/bin/cheat b/bin/cheat index a1a5aa6..a41647b 100755 --- a/bin/cheat +++ b/bin/cheat @@ -13,7 +13,7 @@ Usage: cheat -v Options: - -d --directories List directories on CHEATPATH + -d --directories List directories on $CHEAT_PATH -e --edit Edit cheatsheet -l --list List cheatsheets -s --search Search cheatsheets for diff --git a/cheat/sheet.py b/cheat/sheet.py index 2221084..94c7630 100644 --- a/cheat/sheet.py +++ b/cheat/sheet.py @@ -13,12 +13,12 @@ class Sheet: def copy(self, current_sheet_path, new_sheet_path): """ Copies a sheet to a new path """ - # attempt to copy the sheet to DEFAULT_CHEAT_DIR + # attempt to copy the sheet to CHEAT_DEFAULT_DIR try: shutil.copy(current_sheet_path, new_sheet_path) # fail gracefully if the cheatsheet cannot be copied. This can happen - # if DEFAULT_CHEAT_DIR does not exist + # if CHEAT_DEFAULT_DIR does not exist except IOError: Utils.die('Could not copy cheatsheet for editing.') diff --git a/cheat/sheets.py b/cheat/sheets.py index 4bbcdde..2141c1a 100644 --- a/cheat/sheets.py +++ b/cheat/sheets.py @@ -67,15 +67,15 @@ class Sheets: self.default_path(), ] - # merge the CHEATPATH paths into the sheet_paths + # merge the CHEAT_PATH paths into the sheet_paths if self._config.cheat_path: for path in self._config.cheat_path.split(os.pathsep): if os.path.isdir(path): sheet_paths.append(path) if not sheet_paths: - Utils.die('The DEFAULT_CHEAT_DIR dir does not exist ' - + 'or the CHEATPATH is not set.') + Utils.die('The CHEAT_DEFAULT_DIR dir does not exist ' + + 'or the CHEAT_PATH is not set.') return sheet_paths From d61e4e7c34fabe6179c8447995d3a9c9dd5a5e27 Mon Sep 17 00:00:00 2001 From: Chris Lane Date: Thu, 31 Jan 2019 18:08:19 -0500 Subject: [PATCH 07/15] Refactored (6) Standardized (mostly) how the various classes are initialized. --- bin/cheat | 7 ++----- cheat/sheet.py | 5 +++-- cheat/sheets.py | 8 ++++---- 3 files changed, 9 insertions(+), 11 deletions(-) diff --git a/bin/cheat b/bin/cheat index a41647b..80cd5f3 100755 --- a/bin/cheat +++ b/bin/cheat @@ -38,7 +38,6 @@ Examples: from __future__ import print_function from cheat.colorize import Colorize from cheat.configuration import Configuration -from cheat.editor import Editor from cheat.sheet import Sheet from cheat.sheets import Sheets from docopt import docopt @@ -53,12 +52,10 @@ if __name__ == '__main__': config.validate() # bootsrap - editor = Editor(config) + sheets = Sheets(config) + sheet = Sheet(config, sheets) colorize = Colorize(config) - sheets = Sheets(config, colorize) - sheet = Sheet(sheets, editor) - # list directories if options['--directories']: print("\n".join(sheets.paths())) diff --git a/cheat/sheet.py b/cheat/sheet.py index 94c7630..e06ace0 100644 --- a/cheat/sheet.py +++ b/cheat/sheet.py @@ -1,3 +1,4 @@ +from cheat.editor import Editor from cheat.utils import Utils import io import os @@ -6,9 +7,9 @@ import shutil class Sheet: - def __init__(self, sheets, editor): + def __init__(self, config, sheets): self._sheets = sheets - self._editor = editor + self._editor = Editor(config) def copy(self, current_sheet_path, new_sheet_path): """ Copies a sheet to a new path """ diff --git a/cheat/sheets.py b/cheat/sheets.py index 2141c1a..2816d4b 100644 --- a/cheat/sheets.py +++ b/cheat/sheets.py @@ -1,14 +1,14 @@ +from cheat.colorize import Colorize +from cheat.utils import Utils import io import os -from cheat.utils import Utils - class Sheets: - def __init__(self, config, colorize): + def __init__(self, config): self._config = config - self._colorize = colorize; + self._colorize = Colorize(config); def default_path(self): """ Returns the default cheatsheet path """ From e2b57282833831c3acb10d7a88ac5ae64eab07ed Mon Sep 17 00:00:00 2001 From: Chris Lane Date: Thu, 31 Jan 2019 20:03:10 -0500 Subject: [PATCH 08/15] Refactored (7) Refactored for general code-clarity, with particular focus on removing needless abstraction within `Sheet` and `Sheets` classes. --- bin/cheat | 29 ++++++++++++++++- cheat/configuration.py | 17 +++++++--- cheat/sheet.py | 9 ++--- cheat/sheets.py | 74 ++++++++++++++---------------------------- 4 files changed, 69 insertions(+), 60 deletions(-) diff --git a/bin/cheat b/bin/cheat index 80cd5f3..45524ac 100755 --- a/bin/cheat +++ b/bin/cheat @@ -41,6 +41,7 @@ from cheat.configuration import Configuration from cheat.sheet import Sheet from cheat.sheets import Sheets from docopt import docopt +import os if __name__ == '__main__': @@ -51,6 +52,32 @@ if __name__ == '__main__': config = Configuration() config.validate() + # create the CHEAT_DEFAULT_DIR if it does not exist + if not os.path.isdir(config.cheat_default_dir): + try: + os.mkdir(config.cheat_default_dir) + + except OSError: + Utils.die("%s %s %s" % ( + 'Could not create CHEAT_DEFAULT_DIR (', + config.cheat_default_dir, + ')') + ) + + # assert that the CHEAT_DEFAULT_DIR is readable and writable + if not os.access(config.cheat_default_dir, os.R_OK): + Utils.die("%s %s %s" % ( + 'The CHEAT_DEFAULT_DIR (', + config.cheat_default_dir, + ') is not readable') + ) + if not os.access(config.cheat_default_dir, os.W_OK): + Utils.die("%s %s %s" % ( + 'The CHEAT_DEFAULT_DIR (', + config.cheat_default_dir, + ') is not writeable') + ) + # bootsrap sheets = Sheets(config) sheet = Sheet(config, sheets) @@ -58,7 +85,7 @@ if __name__ == '__main__': # list directories if options['--directories']: - print("\n".join(sheets.paths())) + print("\n".join(sheets.directories())) # list cheatsheets elif options['--list']: diff --git a/cheat/configuration.py b/cheat/configuration.py index 1ad7ad4..3d8a8e5 100644 --- a/cheat/configuration.py +++ b/cheat/configuration.py @@ -6,10 +6,14 @@ class Configuration: def __init__(self): # compute the location of the config files - config_file_path_global = os.environ.get('CHEAT_GLOBAL_CONF_PATH') \ - or '/etc/cheat' - config_file_path_local = (os.environ.get('CHEAT_LOCAL_CONF_PATH') \ - or os.path.expanduser('~/.config/cheat/cheat')) + config_file_path_global = self._select([ + os.environ.get('CHEAT_GLOBAL_CONF_PATH'), + '/etc/cheat', + ]) + config_file_path_local = self._select([ + os.environ.get('CHEAT_LOCAL_CONF_PATH'), + os.path.expanduser('~/.config/cheat/cheat'), + ]) # attempt to read the global config file config = {} @@ -39,7 +43,10 @@ class Configuration: self.cheat_default_dir = self._select([ os.environ.get('CHEAT_DEFAULT_DIR'), os.environ.get('DEFAULT_CHEAT_DIR'), - '~/.cheat', + # TODO: XDG home? + os.path.expanduser( + os.path.expandvars(os.path.join('~', '.cheat')) + ), ]) # self.cheat_editor diff --git a/cheat/sheet.py b/cheat/sheet.py index e06ace0..e33df5a 100644 --- a/cheat/sheet.py +++ b/cheat/sheet.py @@ -8,8 +8,9 @@ import shutil class Sheet: def __init__(self, config, sheets): - self._sheets = sheets + self._config = config self._editor = Editor(config) + self._sheets = sheets def copy(self, current_sheet_path, new_sheet_path): """ Copies a sheet to a new path """ @@ -34,7 +35,7 @@ class Sheet: # default path before editing elif self.exists(sheet) and not self.exists_in_default_path(sheet): self.copy(self.path(sheet), - os.path.join(self._sheets.default_path(), sheet)) + os.path.join(self._config.cheat_default_dir, sheet)) self.edit(sheet) # if it exists and is in the default path, then just open it @@ -43,7 +44,7 @@ class Sheet: def create(self, sheet): """ Creates a cheatsheet """ - new_sheet_path = os.path.join(self._sheets.default_path(), sheet) + new_sheet_path = os.path.join(self._config.cheat_default_dir, sheet) self._editor.open(new_sheet_path) def edit(self, sheet): @@ -57,7 +58,7 @@ class Sheet: def exists_in_default_path(self, sheet): """ Predicate that returns true if the sheet exists in default_path""" - default_path_sheet = os.path.join(self._sheets.default_path(), sheet) + default_path_sheet = os.path.join(self._config.cheat_default_dir, sheet) return (sheet in self._sheets.get() and os.access(default_path_sheet, os.R_OK)) diff --git a/cheat/sheets.py b/cheat/sheets.py index 2816d4b..d83d832 100644 --- a/cheat/sheets.py +++ b/cheat/sheets.py @@ -10,47 +10,25 @@ class Sheets: self._config = config self._colorize = Colorize(config); - def default_path(self): - """ Returns the default cheatsheet path """ + # Assembles a dictionary of cheatsheets as name => file-path + self._sheets = {} + sheet_paths = [ + config.cheat_default_dir + ] - # determine the default cheatsheet dir - # 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')) - default_sheets_dir = os.path.expanduser( - os.path.expandvars(default_sheets_dir)) + # merge the CHEAT_PATH paths into the sheet_paths + if config.cheat_path: + for path in config.cheat_path.split(os.pathsep): + if os.path.isdir(path): + sheet_paths.append(path) - # create the CHEAT_DEFAULT_DIR if it does not exist - if not os.path.isdir(default_sheets_dir): - try: - # @kludge: unclear on why this is necessary - os.umask(0000) - os.mkdir(default_sheets_dir) - - except OSError: - Utils.die('Could not create CHEAT_DEFAULT_DIR') - - # assert that the CHEAT_DEFAULT_DIR is readable and writable - if not os.access(default_sheets_dir, os.R_OK): - Utils.die('The CHEAT_DEFAULT_DIR (' - + default_sheets_dir - + ') is not readable.') - if not os.access(default_sheets_dir, os.W_OK): - Utils.die('The CHEAT_DEFAULT_DIR (' - + default_sheets_dir - + ') is not writable.') - - # return the default dir - return default_sheets_dir - - def get(self): - """ Assembles a dictionary of cheatsheets as name => file-path """ - cheats = {} + if not sheet_paths: + Utils.die('The CHEAT_DEFAULT_DIR dir does not exist ' + + 'or the CHEAT_PATH is not set.') # otherwise, scan the filesystem - for cheat_dir in reversed(self.paths()): - cheats.update( + for cheat_dir in reversed(sheet_paths): + self._sheets.update( dict([ (cheat, os.path.join(cheat_dir, cheat)) for cheat in os.listdir(cheat_dir) @@ -59,26 +37,22 @@ class Sheets: ]) ) - return cheats - - def paths(self): + def directories(self): """ Assembles a list of directories containing cheatsheets """ sheet_paths = [ - self.default_path(), + self._config.cheat_default_dir, ] - # merge the CHEAT_PATH paths into the sheet_paths - if self._config.cheat_path: - for path in self._config.cheat_path.split(os.pathsep): - if os.path.isdir(path): - sheet_paths.append(path) - - if not sheet_paths: - Utils.die('The CHEAT_DEFAULT_DIR dir does not exist ' - + 'or the CHEAT_PATH is not set.') + # merge the CHEATPATH paths into the sheet_paths + for path in self._config.cheat_path.split(os.pathsep): + sheet_paths.append(path) return sheet_paths + def get(self): + """ Returns a dictionary of cheatsheets as name => file-path """ + return self._sheets + def list(self): """ Lists the available cheatsheets """ sheet_list = '' From 5793c1845ae15bc0db4ab0177920cac52cb235b1 Mon Sep 17 00:00:00 2001 From: Chris Lane Date: Fri, 1 Feb 2019 11:23:38 -0500 Subject: [PATCH 09/15] Refactored (8) Refactored `Sheet` class: - Removed unnecessary indirection and extraneous methods - Renamed some methods to conform to Pythonic conventions - Renamed the `create_or_edit` method to `edit` to be consistent with subcommand name (`--edit`) --- bin/cheat | 2 +- cheat/sheet.py | 82 ++++++++++++++++++++------------------------------ 2 files changed, 33 insertions(+), 51 deletions(-) diff --git a/bin/cheat b/bin/cheat index 45524ac..cceb50b 100755 --- a/bin/cheat +++ b/bin/cheat @@ -93,7 +93,7 @@ if __name__ == '__main__': # create/edit cheatsheet elif options['--edit']: - sheet.create_or_edit(options['']) + sheet.edit(options['']) # search among the cheatsheets elif options['--search']: diff --git a/cheat/sheet.py b/cheat/sheet.py index e33df5a..75b5d9a 100644 --- a/cheat/sheet.py +++ b/cheat/sheet.py @@ -12,69 +12,51 @@ class Sheet: self._editor = Editor(config) self._sheets = sheets - def copy(self, current_sheet_path, new_sheet_path): - """ Copies a sheet to a new path """ - - # attempt to copy the sheet to CHEAT_DEFAULT_DIR - try: - shutil.copy(current_sheet_path, new_sheet_path) - - # fail gracefully if the cheatsheet cannot be copied. This can happen - # if CHEAT_DEFAULT_DIR does not exist - except IOError: - Utils.die('Could not copy cheatsheet for editing.') - - def create_or_edit(self, sheet): - """ Creates or edits a cheatsheet """ - - # if the cheatsheet does not exist - if not self.exists(sheet): - self.create(sheet) - - # if the cheatsheet exists but not in the default_path, copy it to the - # default path before editing - elif self.exists(sheet) and not self.exists_in_default_path(sheet): - self.copy(self.path(sheet), - os.path.join(self._config.cheat_default_dir, sheet)) - self.edit(sheet) - - # if it exists and is in the default path, then just open it - else: - self.edit(sheet) - - def create(self, sheet): - """ Creates a cheatsheet """ - new_sheet_path = os.path.join(self._config.cheat_default_dir, sheet) - self._editor.open(new_sheet_path) - - def edit(self, sheet): - """ Opens a cheatsheet for editing """ - self._editor.open(self.path(sheet)) - - def exists(self, sheet): + def _exists(self, sheet): """ Predicate that returns true if the sheet exists """ return (sheet in self._sheets.get() and - os.access(self.path(sheet), os.R_OK)) + os.access(self._path(sheet), os.R_OK)) - def exists_in_default_path(self, sheet): + def _exists_in_default_path(self, sheet): """ Predicate that returns true if the sheet exists in default_path""" default_path_sheet = os.path.join(self._config.cheat_default_dir, sheet) return (sheet in self._sheets.get() and os.access(default_path_sheet, os.R_OK)) - def is_writable(self, sheet): - """ Predicate that returns true if the sheet is writeable """ - return (sheet in self._sheets.get() and - os.access(self.path(sheet), os.W_OK)) - - def path(self, sheet): + def _path(self, sheet): """ Returns a sheet's filesystem path """ return self._sheets.get()[sheet] + def edit(self, sheet): + """ Creates or edits a cheatsheet """ + + # if the cheatsheet does not exist + if not self._exists(sheet): + new_sheet_path = os.path.join(self._config.cheat_default_dir, sheet) + self._editor.open(new_sheet_path) + + # if the cheatsheet exists but not in the default_path, copy it to the + # default path before editing + elif self._exists(sheet) and not self._exists_in_default_path(sheet): + try: + shutil.copy(self._path(sheet), + os.path.join(self._config.cheat_default_dir, sheet)) + + # fail gracefully if the cheatsheet cannot be copied. This can happen + # if CHEAT_DEFAULT_DIR does not exist + except IOError: + Utils.die('Could not copy cheatsheet for editing.') + + self._editor.open(self._path(sheet)) + + # if it exists and is in the default path, then just open it + else: + self._editor.open(self._path(sheet)) + def read(self, sheet): """ Returns the contents of the cheatsheet as a String """ - if not self.exists(sheet): + if not self._exists(sheet): Utils.die('No cheatsheet found for ' + sheet) - with io.open(self.path(sheet), encoding='utf-8') as cheatfile: + with io.open(self._path(sheet), encoding='utf-8') as cheatfile: return cheatfile.read() From a657699a24c209e3a1581f1ee544aa2f35ac1f3c Mon Sep 17 00:00:00 2001 From: Chris Lane Date: Fri, 1 Feb 2019 11:43:11 -0500 Subject: [PATCH 10/15] Refactored (9) Moved some functionality into the `Util` class. --- cheat/configuration.py | 22 +++++++--------------- cheat/utils.py | 10 ++++++++++ 2 files changed, 17 insertions(+), 15 deletions(-) diff --git a/cheat/configuration.py b/cheat/configuration.py index 3d8a8e5..75251af 100644 --- a/cheat/configuration.py +++ b/cheat/configuration.py @@ -33,9 +33,9 @@ class Configuration: # self.cheat_colors self.cheat_colors = self._select([ - self._boolify(os.environ.get('CHEAT_COLORS')), - self._boolify(os.environ.get('CHEATCOLORS')), - self._boolify(config.get('CHEAT_COLORS')), + Utils.boolify(os.environ.get('CHEAT_COLORS')), + Utils.boolify(os.environ.get('CHEATCOLORS')), + Utils.boolify(config.get('CHEAT_COLORS')), True, ]) @@ -64,8 +64,8 @@ class Configuration: config.get('CHEAT_HIGHLIGHT'), False, ]) - if (self.cheat_highlight.strip().lower() == "false"): - self.cheat_highlight = False + if isinstance(self.cheat_highlight, str): + Utils.boolify(self.cheat_highlight) # self.cheat_path self.cheat_path = self._select([ @@ -75,18 +75,10 @@ class Configuration: '/usr/share/cheat', ]) - def _boolify(self, value): - # if `value` is not a string, return it as-is - if not isinstance(value, str): - return value - - # otherwise, convert "true" and "false" to Boolean counterparts - return value.strip().lower() == "true" - def _read_config_file(self, path): # Reads configuration file and returns list of set variables config = {} - if (os.path.isfile(path)): + if os.path.isfile(path): with open(path) as config_file: config.update(json.load(config_file)) return config @@ -105,7 +97,7 @@ class Configuration: 'blue', 'magenta', 'cyan', 'white', False ] - if (self.cheat_highlight not in highlights): + if self.cheat_highlight not in highlights: Utils.die("%s %s" % ('CHEAT_HIGHLIGHT must be one of:', highlights)) return True diff --git a/cheat/utils.py b/cheat/utils.py index 01bd8a3..75f3802 100644 --- a/cheat/utils.py +++ b/cheat/utils.py @@ -14,3 +14,13 @@ class Utils: def warn(message): """ Prints a message to stderr """ print((message), file=sys.stderr) + + @staticmethod + def boolify(value): + """ Type-converts 'true' and 'false' (strings) to Boolean equivalents """ + # if `value` is not a string, return it as-is + if not isinstance(value, str): + return value + + # otherwise, convert "true" and "false" to Boolean counterparts + return value.strip().lower() == "true" From df21731c02bb538df1d89e06c01007c5d6bcdbbd Mon Sep 17 00:00:00 2001 From: Chris Lane Date: Fri, 1 Feb 2019 11:43:38 -0500 Subject: [PATCH 11/15] Trivial Python style corrections --- cheat/colorize.py | 2 +- cheat/editor.py | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/cheat/colorize.py b/cheat/colorize.py index f9ca6ef..89d79f8 100644 --- a/cheat/colorize.py +++ b/cheat/colorize.py @@ -29,7 +29,7 @@ class Colorize: """ Applies syntax highlighting """ # only colorize if cheat_colors is true, and stdout is a tty - if (self._config.cheat_colors is False or not sys.stdout.isatty()): + if self._config.cheat_colors is False or not sys.stdout.isatty(): return sheet_content # don't attempt to colorize an empty cheatsheet diff --git a/cheat/editor.py b/cheat/editor.py index 0da2fc4..c48351f 100644 --- a/cheat/editor.py +++ b/cheat/editor.py @@ -12,7 +12,7 @@ class Editor: """ Determines the user's preferred editor """ # assert that the editor is set - if (not self._config.cheat_editor): + if not self._config.cheat_editor: Utils.die( 'You must set a CHEAT_EDITOR, VISUAL, or EDITOR environment ' 'variable or setting in order to create/edit a cheatsheet.' From ba47dc2cbcefad6c02c2983597d6635b57f0ff05 Mon Sep 17 00:00:00 2001 From: Chris Lane Date: Fri, 1 Feb 2019 14:42:10 -0500 Subject: [PATCH 12/15] Refactored (10) - Added `ci/lint.sh`, which uses `flake8` to lint the relevant files - Made changes to appease the linter. - Bugfix in `cheat/configuration` (missing dependency) --- bin/cheat | 1 + cheat/__init__.py | 4 ---- cheat/colorize.py | 5 +++-- cheat/configuration.py | 7 +++++-- cheat/sheet.py | 18 ++++++++++-------- cheat/sheets.py | 2 +- cheat/utils.py | 2 +- ci/lint.sh | 9 +++++++++ 8 files changed, 30 insertions(+), 18 deletions(-) create mode 100755 ci/lint.sh diff --git a/bin/cheat b/bin/cheat index cceb50b..3791427 100755 --- a/bin/cheat +++ b/bin/cheat @@ -40,6 +40,7 @@ from cheat.colorize import Colorize from cheat.configuration import Configuration from cheat.sheet import Sheet from cheat.sheets import Sheets +from cheat.utils import Utils from docopt import docopt import os diff --git a/cheat/__init__.py b/cheat/__init__.py index 2560cf4..e69de29 100644 --- a/cheat/__init__.py +++ b/cheat/__init__.py @@ -1,4 +0,0 @@ -from . import sheet -from . import sheets -from . import utils -from . import configuration \ No newline at end of file diff --git a/cheat/colorize.py b/cheat/colorize.py index 89d79f8..7b18beb 100644 --- a/cheat/colorize.py +++ b/cheat/colorize.py @@ -23,14 +23,15 @@ class Colorize: return haystack # if the import succeeds, colorize the needle in haystack - return haystack.replace(needle, colored(needle, self._config.cheat_highlight)) + return haystack.replace(needle, + colored(needle, self._config.cheat_highlight)) def syntax(self, sheet_content): """ Applies syntax highlighting """ # only colorize if cheat_colors is true, and stdout is a tty if self._config.cheat_colors is False or not sys.stdout.isatty(): - return sheet_content + return sheet_content # don't attempt to colorize an empty cheatsheet if not sheet_content.strip(): diff --git a/cheat/configuration.py b/cheat/configuration.py index 75251af..73a5021 100644 --- a/cheat/configuration.py +++ b/cheat/configuration.py @@ -2,6 +2,7 @@ from cheat.utils import Utils import json import os + class Configuration: def __init__(self): @@ -20,7 +21,8 @@ class Configuration: try: config.update(self._read_config_file(config_file_path_global)) except Exception as e: - Utils.warn('Error while parsing global configuration: ' + e.message) + Utils.warn('Error while parsing global configuration: ' + + e.message) # attempt to read the local config file try: @@ -98,6 +100,7 @@ class Configuration: False ] if self.cheat_highlight not in highlights: - Utils.die("%s %s" % ('CHEAT_HIGHLIGHT must be one of:', highlights)) + Utils.die("%s %s" % + ('CHEAT_HIGHLIGHT must be one of:', highlights)) return True diff --git a/cheat/sheet.py b/cheat/sheet.py index 75b5d9a..dfd42ff 100644 --- a/cheat/sheet.py +++ b/cheat/sheet.py @@ -19,9 +19,9 @@ class Sheet: def _exists_in_default_path(self, sheet): """ Predicate that returns true if the sheet exists in default_path""" - default_path_sheet = os.path.join(self._config.cheat_default_dir, sheet) + default_path = os.path.join(self._config.cheat_default_dir, sheet) return (sheet in self._sheets.get() and - os.access(default_path_sheet, os.R_OK)) + os.access(default_path, os.R_OK)) def _path(self, sheet): """ Returns a sheet's filesystem path """ @@ -32,18 +32,20 @@ class Sheet: # if the cheatsheet does not exist if not self._exists(sheet): - new_sheet_path = os.path.join(self._config.cheat_default_dir, sheet) - self._editor.open(new_sheet_path) + new_path = os.path.join(self._config.cheat_default_dir, sheet) + self._editor.open(new_path) # if the cheatsheet exists but not in the default_path, copy it to the # default path before editing elif self._exists(sheet) and not self._exists_in_default_path(sheet): try: - shutil.copy(self._path(sheet), - os.path.join(self._config.cheat_default_dir, sheet)) + shutil.copy( + self._path(sheet), + os.path.join(self._config.cheat_default_dir, sheet) + ) - # fail gracefully if the cheatsheet cannot be copied. This can happen - # if CHEAT_DEFAULT_DIR does not exist + # fail gracefully if the cheatsheet cannot be copied. This can + # happen if CHEAT_DEFAULT_DIR does not exist except IOError: Utils.die('Could not copy cheatsheet for editing.') diff --git a/cheat/sheets.py b/cheat/sheets.py index d83d832..341fcfd 100644 --- a/cheat/sheets.py +++ b/cheat/sheets.py @@ -8,7 +8,7 @@ class Sheets: def __init__(self, config): self._config = config - self._colorize = Colorize(config); + self._colorize = Colorize(config) # Assembles a dictionary of cheatsheets as name => file-path self._sheets = {} diff --git a/cheat/utils.py b/cheat/utils.py index 75f3802..c481a7c 100644 --- a/cheat/utils.py +++ b/cheat/utils.py @@ -17,7 +17,7 @@ class Utils: @staticmethod def boolify(value): - """ Type-converts 'true' and 'false' (strings) to Boolean equivalents """ + """ Type-converts 'true' and 'false' to Booleans """ # if `value` is not a string, return it as-is if not isinstance(value, str): return value diff --git a/ci/lint.sh b/ci/lint.sh new file mode 100755 index 0000000..e6a3d23 --- /dev/null +++ b/ci/lint.sh @@ -0,0 +1,9 @@ +#!/bin/sh + +# Resolve the app root +SCRIPT=`realpath $0` +SCRIPTPATH=`dirname $SCRIPT` +APPROOT=`realpath "$SCRIPTPATH/.."` + +flake8 $APPROOT/bin/cheat +flake8 $APPROOT/cheat/*.py From 3ad923eff050bb44d6682f0820ff21bf89c1557e Mon Sep 17 00:00:00 2001 From: Chris Lane Date: Fri, 1 Feb 2019 15:10:03 -0500 Subject: [PATCH 13/15] Refactored (11) Renamed `CHEAT_DEFAULT_DIR` to `CHEAT_USER_DIR` because the latter more accurately describes the purpose of the variable. --- README.md | 6 +++--- bin/cheat | 24 ++++++++++++------------ cheat/configuration.py | 5 +++-- cheat/sheet.py | 8 ++++---- cheat/sheets.py | 6 +++--- 5 files changed, 25 insertions(+), 24 deletions(-) diff --git a/README.md b/README.md index a9c86e7..ec2a354 100644 --- a/README.md +++ b/README.md @@ -83,13 +83,13 @@ with your [dotfiles][]. Configuring ----------- -### Setting a CHEAT_DEFAULT_DIR ### +### Setting a CHEAT_USER_DIR ### Personal cheatsheets are saved in the `~/.cheat` directory by default, but you -can specify a different default by exporting a `CHEAT_DEFAULT_DIR` environment +can specify a different default by exporting a `CHEAT_USER_DIR` environment variable: ```sh -export CHEAT_DEFAULT_DIR='/path/to/my/cheats' +export CHEAT_USER_DIR='/path/to/my/cheats' ``` ### Setting a CHEAT_PATH ### diff --git a/bin/cheat b/bin/cheat index 3791427..7ced880 100755 --- a/bin/cheat +++ b/bin/cheat @@ -53,29 +53,29 @@ if __name__ == '__main__': config = Configuration() config.validate() - # create the CHEAT_DEFAULT_DIR if it does not exist - if not os.path.isdir(config.cheat_default_dir): + # create the CHEAT_USER_DIR if it does not exist + if not os.path.isdir(config.cheat_user_dir): try: - os.mkdir(config.cheat_default_dir) + os.mkdir(config.cheat_user_dir) except OSError: Utils.die("%s %s %s" % ( - 'Could not create CHEAT_DEFAULT_DIR (', - config.cheat_default_dir, + 'Could not create CHEAT_USER_DIR (', + config.cheat_user_dir, ')') ) - # assert that the CHEAT_DEFAULT_DIR is readable and writable - if not os.access(config.cheat_default_dir, os.R_OK): + # assert that the CHEAT_USER_DIR is readable and writable + if not os.access(config.cheat_user_dir, os.R_OK): Utils.die("%s %s %s" % ( - 'The CHEAT_DEFAULT_DIR (', - config.cheat_default_dir, + 'The CHEAT_USER_DIR (', + config.cheat_user_dir, ') is not readable') ) - if not os.access(config.cheat_default_dir, os.W_OK): + if not os.access(config.cheat_user_dir, os.W_OK): Utils.die("%s %s %s" % ( - 'The CHEAT_DEFAULT_DIR (', - config.cheat_default_dir, + 'The CHEAT_USER_DIR (', + config.cheat_user_dir, ') is not writeable') ) diff --git a/cheat/configuration.py b/cheat/configuration.py index 73a5021..7b2d3bf 100644 --- a/cheat/configuration.py +++ b/cheat/configuration.py @@ -41,8 +41,9 @@ class Configuration: True, ]) - # self.cheat_default_dir - self.cheat_default_dir = self._select([ + # self.cheat_user_dir + self.cheat_user_dir = self._select([ + os.environ.get('CHEAT_USER_DIR'), os.environ.get('CHEAT_DEFAULT_DIR'), os.environ.get('DEFAULT_CHEAT_DIR'), # TODO: XDG home? diff --git a/cheat/sheet.py b/cheat/sheet.py index dfd42ff..4cd3f96 100644 --- a/cheat/sheet.py +++ b/cheat/sheet.py @@ -19,7 +19,7 @@ class Sheet: def _exists_in_default_path(self, sheet): """ Predicate that returns true if the sheet exists in default_path""" - default_path = os.path.join(self._config.cheat_default_dir, sheet) + default_path = os.path.join(self._config.cheat_user_dir, sheet) return (sheet in self._sheets.get() and os.access(default_path, os.R_OK)) @@ -32,7 +32,7 @@ class Sheet: # if the cheatsheet does not exist if not self._exists(sheet): - new_path = os.path.join(self._config.cheat_default_dir, sheet) + new_path = os.path.join(self._config.cheat_user_dir, sheet) self._editor.open(new_path) # if the cheatsheet exists but not in the default_path, copy it to the @@ -41,11 +41,11 @@ class Sheet: try: shutil.copy( self._path(sheet), - os.path.join(self._config.cheat_default_dir, sheet) + os.path.join(self._config.cheat_user_dir, sheet) ) # fail gracefully if the cheatsheet cannot be copied. This can - # happen if CHEAT_DEFAULT_DIR does not exist + # happen if CHEAT_USER_DIR does not exist except IOError: Utils.die('Could not copy cheatsheet for editing.') diff --git a/cheat/sheets.py b/cheat/sheets.py index 341fcfd..8458ea6 100644 --- a/cheat/sheets.py +++ b/cheat/sheets.py @@ -13,7 +13,7 @@ class Sheets: # Assembles a dictionary of cheatsheets as name => file-path self._sheets = {} sheet_paths = [ - config.cheat_default_dir + config.cheat_user_dir ] # merge the CHEAT_PATH paths into the sheet_paths @@ -23,7 +23,7 @@ class Sheets: sheet_paths.append(path) if not sheet_paths: - Utils.die('The CHEAT_DEFAULT_DIR dir does not exist ' + Utils.die('The CHEAT_USER_DIR dir does not exist ' + 'or the CHEAT_PATH is not set.') # otherwise, scan the filesystem @@ -40,7 +40,7 @@ class Sheets: def directories(self): """ Assembles a list of directories containing cheatsheets """ sheet_paths = [ - self._config.cheat_default_dir, + self._config.cheat_user_dir, ] # merge the CHEATPATH paths into the sheet_paths From a37577ee85dd7a05a0ae549850140aaadef0d51e Mon Sep 17 00:00:00 2001 From: Chris Lane Date: Fri, 1 Feb 2019 15:18:23 -0500 Subject: [PATCH 14/15] Trivial: docstrings Updated some docstring comments. --- cheat/configuration.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/cheat/configuration.py b/cheat/configuration.py index 7b2d3bf..4358a2f 100644 --- a/cheat/configuration.py +++ b/cheat/configuration.py @@ -79,7 +79,7 @@ class Configuration: ]) def _read_config_file(self, path): - # Reads configuration file and returns list of set variables + """ Reads configuration file and returns list of set variables """ config = {} if os.path.isfile(path): with open(path) as config_file: @@ -92,7 +92,7 @@ class Configuration: return v def validate(self): - """ Validate configuration parameters """ + """ Validates configuration parameters """ # assert that cheat_highlight contains a valid value highlights = [ From 9931b78c5f10c0dce9b3f79a8f82f4161a5ea3c4 Mon Sep 17 00:00:00 2001 From: Chris Lane Date: Fri, 1 Feb 2019 15:24:04 -0500 Subject: [PATCH 15/15] Lint - Added instruction to lint `setup.py` to `ci/lint.sh` - Updated `setup.py` per linter suggestions --- ci/lint.sh | 1 + setup.py | 24 ++++++++++++------------ 2 files changed, 13 insertions(+), 12 deletions(-) diff --git a/ci/lint.sh b/ci/lint.sh index e6a3d23..bfbb640 100755 --- a/ci/lint.sh +++ b/ci/lint.sh @@ -5,5 +5,6 @@ SCRIPT=`realpath $0` SCRIPTPATH=`dirname $SCRIPT` APPROOT=`realpath "$SCRIPTPATH/.."` +flake8 $APPROOT/setup.py flake8 $APPROOT/bin/cheat flake8 $APPROOT/cheat/*.py diff --git a/setup.py b/setup.py index 43cc96a..d6410f6 100644 --- a/setup.py +++ b/setup.py @@ -9,31 +9,31 @@ cheat_path = os.environ.get('CHEAT_PATH') or '/usr/share/cheat' # aggregate the systme-wide cheatsheets cheat_files = [] for f in os.listdir('cheat/cheatsheets/'): - cheat_files.append(os.path.join('cheat/cheatsheets/',f)) + cheat_files.append(os.path.join('cheat/cheatsheets/', f)) # specify build params setup( - name = 'cheat', - version = '2.4.2', - author = 'Chris Lane', - author_email = 'chris@chris-allen-lane.com', - license = 'GPL3', - description = 'cheat allows you to create and view interactive cheatsheets ' + name='cheat', + version='2.4.2', + author='Chris Lane', + author_email='chris@chris-allen-lane.com', + license='GPL3', + description='cheat allows you to create and view interactive cheatsheets ' 'on the command-line. It was designed to help remind *nix system ' 'administrators of options for commands that they use frequently, but not ' 'frequently enough to remember.', - url = 'https://github.com/chrisallenlane/cheat', - packages = [ + url='https://github.com/chrisallenlane/cheat', + packages=[ 'cheat', 'cheat.test', ], - scripts = ['bin/cheat'], - install_requires = [ + scripts=['bin/cheat'], + install_requires=[ 'docopt >= 0.6.1', 'pygments >= 1.6.0', 'termcolor >= 1.1.0', ], - data_files = [ + data_files=[ (cheat_path, cheat_files), ('/etc', ['config/cheat']), ],