Add classes for better readability

This commit is contained in:
Tomas Korbar 2018-05-30 08:49:52 +02:00 committed by Tomas Korbar
parent a651426075
commit 7814de96d2
5 changed files with 241 additions and 215 deletions

View File

@ -36,9 +36,10 @@ Examples:
# require the dependencies # require the dependencies
from __future__ import print_function from __future__ import print_function
from cheat import sheets, sheet from cheat.sheets import Sheets
from cheat.utils import colorize from cheat.sheet import Sheet
from cheat.utils import die from cheat.utils import Utils
from cheat.configuration import Configuration
from docopt import docopt from docopt import docopt
import os import os
@ -59,6 +60,11 @@ if __name__ == '__main__':
# parse the command-line options # parse the command-line options
options = docopt(__doc__, version='cheat 2.3.1') options = docopt(__doc__, version='cheat 2.3.1')
config = Configuration()
sheets = Sheets(config.get_default_cheat_dir(),config.get_cheatpath())
sheet = Sheet(config.get_default_cheat_dir(),config.get_cheatpath(),config.get_editor())
utils = Utils(config.get_cheatcolors(),config.get_editor())
# list directories # list directories
if options['--directories']: if options['--directories']:
print("\n".join(sheets.paths())) print("\n".join(sheets.paths()))
@ -73,8 +79,8 @@ if __name__ == '__main__':
# search among the cheatsheets # search among the cheatsheets
elif options['--search']: elif options['--search']:
print(colorize(sheets.search(options['<keyword>'])), end="") print(utils.colorize(sheets.search(options['<keyword>'])), end="")
# print the cheatsheet # print the cheatsheet
else: else:
print(colorize(sheet.read(options['<cheatsheet>'])), end="") print(utils.colorize(sheet.read(options['<cheatsheet>'])), end="")

View File

@ -1,5 +1,5 @@
import os import os
from cheat.utils import warn from cheat.utils import Utils
import json import json
class Configuration: class Configuration:
@ -15,12 +15,12 @@ class Configuration:
try: try:
merged_config.update(self._read_configuration_file('/etc/cheat')) merged_config.update(self._read_configuration_file('/etc/cheat'))
except Exception: except Exception:
warn('error while parsing global configuration') Utils.warn('error while parsing global configuration')
try: try:
merged_config.update(self._read_configuration_file(os.path.expanduser(os.path.join('~','.config','cheat','cheat')))) merged_config.update(self._read_configuration_file(os.path.expanduser(os.path.join('~','.config','cheat','cheat'))))
except Exception: except Exception:
warn('error while parsing user configuration') Utils.warn('error while parsing user configuration')
merged_config.update(self._read_env_vars_config()) merged_config.update(self._read_env_vars_config())

View File

@ -1,76 +1,85 @@
import os import os
import shutil import shutil
from cheat import sheets from cheat.sheets import Sheets
from cheat.utils import die, open_with_editor from cheat.utils import Utils
def copy(current_sheet_path, new_sheet_path):
""" Copies a sheet to a new path """
# attempt to copy the sheet to DEFAULT_CHEAT_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
except IOError:
die('Could not copy cheatsheet for editing.')
def create_or_edit(sheet): class Sheet:
""" Creates or edits a cheatsheet """
# if the cheatsheet does not exist
if not exists(sheet):
create(sheet)
# if the cheatsheet exists but not in the default_path, copy it to the
# default path before editing
elif exists(sheet) and not exists_in_default_path(sheet):
copy(path(sheet), os.path.join(sheets.default_path(), sheet))
edit(sheet)
# if it exists and is in the default path, then just open it
else:
edit(sheet)
def create(sheet): def __init__(self,default_cheat_dir,cheatpath,editor_exec):
""" Creates a cheatsheet """ self.sheets_instance = Sheets(default_cheat_dir,cheatpath)
new_sheet_path = os.path.join(sheets.default_path(), sheet) self.utils_instance = Utils(None,editor_exec)
open_with_editor(new_sheet_path)
def edit(sheet): def copy(self,current_sheet_path, new_sheet_path):
""" Opens a cheatsheet for editing """ """ Copies a sheet to a new path """
open_with_editor(path(sheet))
# attempt to copy the sheet to DEFAULT_CHEAT_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
except IOError:
Utils.die('Could not copy cheatsheet for editing.')
def exists(sheet): def create_or_edit(self,sheet):
""" Predicate that returns true if the sheet exists """ """ Creates or edits a cheatsheet """
return sheet in sheets.get() and os.access(path(sheet), os.R_OK)
# 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.sheets_instance.default_path(), sheet))
self.edit(sheet)
# if it exists and is in the default path, then just open it
else:
self.edit(sheet)
def exists_in_default_path(sheet): def create(self,sheet):
""" Predicate that returns true if the sheet exists in default_path""" """ Creates a cheatsheet """
default_path_sheet = os.path.join(sheets.default_path(), sheet) new_sheet_path = os.path.join(self.sheets_instance.default_path(), sheet)
return sheet in sheets.get() and os.access(default_path_sheet, os.R_OK) self.utils_instance.open_with_editor(new_sheet_path)
def is_writable(sheet): def edit(self,sheet):
""" Predicate that returns true if the sheet is writeable """ """ Opens a cheatsheet for editing """
return sheet in sheets.get() and os.access(path(sheet), os.W_OK) self.utils_instance.open_with_editor(self.path(sheet))
def path(sheet): def exists(self,sheet):
""" Returns a sheet's filesystem path """ """ Predicate that returns true if the sheet exists """
return sheets.get()[sheet] return sheet in self.sheets_instance.get() and os.access(self.path(sheet), os.R_OK)
def read(sheet): def exists_in_default_path(self,sheet):
""" Returns the contents of the cheatsheet as a String """ """ Predicate that returns true if the sheet exists in default_path"""
if not exists(sheet): default_path_sheet = os.path.join(self.sheets_instance.default_path(), sheet)
die('No cheatsheet found for ' + sheet) return sheet in self.sheets_instance.get() and os.access(default_path_sheet, os.R_OK)
with open(path(sheet)) as cheatfile:
return cheatfile.read() def is_writable(self,sheet):
""" Predicate that returns true if the sheet is writeable """
return sheet in self.sheets_instance.get() and os.access(self.path(sheet), os.W_OK)
def path(self,sheet):
""" Returns a sheet's filesystem path """
return self.sheets_instance.get()[sheet]
def read(self,sheet):
""" Returns the contents of the cheatsheet as a String """
if not self.exists(sheet):
Utils.die('No cheatsheet found for ' + sheet)
with open(self.path(sheet)) as cheatfile:
return cheatfile.read()

View File

@ -1,94 +1,100 @@
import os import os
from cheat.utils import die
from cheat.utils import highlight
from cheat.configuration import Configuration from cheat.configuration import Configuration
from cheat.utils import Utils
def default_path(): class Sheets:
""" Returns the default cheatsheet path """
# determine the default cheatsheet dir
default_sheets_dir = Configuration().get_default_cheat_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
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:
die('Could not create DEFAULT_CHEAT_DIR')
# assert that the DEFAULT_CHEAT_DIR is readable and writable
if not os.access(default_sheets_dir, os.R_OK):
die('The DEFAULT_CHEAT_DIR (' + default_sheets_dir +') is not readable.')
if not os.access(default_sheets_dir, os.W_OK):
die('The DEFAULT_CHEAT_DIR (' + default_sheets_dir +') is not writable.')
# return the default dir
return default_sheets_dir
def get(): def __init__(self,default_cheat_dir,cheatpath):
""" Assembles a dictionary of cheatsheets as name => file-path """ self.default_cheat_dir = default_cheat_dir
cheats = {} self.cheatpath = cheatpath
# otherwise, scan the filesystem
for cheat_dir in reversed(paths()):
cheats.update(
dict([
(cheat, os.path.join(cheat_dir, cheat))
for cheat in os.listdir(cheat_dir)
if not cheat.startswith('.')
and not cheat.startswith('__')
])
)
return cheats
def paths(): def default_path(self):
""" Assembles a list of directories containing cheatsheets """ """ Returns the default cheatsheet path """
sheet_paths = [
default_path(),
'/usr/share/cheat',
]
# merge the CHEATPATH paths into the sheet_paths # determine the default cheatsheet dir
if Configuration().get_cheatpath(): default_sheets_dir = self.default_cheat_dir or os.path.join('~', '.cheat')
for path in Configuration().get_cheatpath().split(os.pathsep): default_sheets_dir = os.path.expanduser(os.path.expandvars(default_sheets_dir))
if os.path.isdir(path):
sheet_paths.append(path)
if not sheet_paths: # create the DEFAULT_CHEAT_DIR if it does not exist
die('The DEFAULT_CHEAT_DIR dir does not exist or the CHEATPATH is not set.') if not os.path.isdir(default_sheets_dir):
try:
# @kludge: unclear on why this is necessary
os.umask(0000)
os.mkdir(default_sheets_dir)
return sheet_paths except OSError:
Utils.die('Could not create DEFAULT_CHEAT_DIR')
# assert that the DEFAULT_CHEAT_DIR is readable and writable
if not os.access(default_sheets_dir, os.R_OK):
Utils.die('The DEFAULT_CHEAT_DIR (' + default_sheets_dir +') is not readable.')
if not os.access(default_sheets_dir, os.W_OK):
Utils.die('The DEFAULT_CHEAT_DIR (' + default_sheets_dir +') is not writable.')
# return the default dir
return default_sheets_dir
def list(): def get(self):
""" Lists the available cheatsheets """ """ Assembles a dictionary of cheatsheets as name => file-path """
sheet_list = '' cheats = {}
pad_length = max([len(x) for x in get().keys()]) + 4
for sheet in sorted(get().items()): # otherwise, scan the filesystem
sheet_list += sheet[0].ljust(pad_length) + sheet[1] + "\n" for cheat_dir in reversed(self.paths()):
return sheet_list cheats.update(
dict([
(cheat, os.path.join(cheat_dir, cheat))
for cheat in os.listdir(cheat_dir)
if not cheat.startswith('.')
and not cheat.startswith('__')
])
)
return cheats
def search(term): def paths(self):
""" Searches all cheatsheets for the specified term """ """ Assembles a list of directories containing cheatsheets """
result = '' sheet_paths = [
lowered_term = term.lower() self.default_path(),
'/usr/share/cheat',
]
for cheatsheet in sorted(get().items()): # merge the CHEATPATH paths into the sheet_paths
match = '' if self.cheatpath:
for line in open(cheatsheet[1]): for path in self.cheatpath.split(os.pathsep):
if term in line: if os.path.isdir(path):
match += ' ' + highlight(term, line) sheet_paths.append(path)
if match != '': if not sheet_paths:
result += cheatsheet[0] + ":\n" + match + "\n" Utils.die('The DEFAULT_CHEAT_DIR dir does not exist or the CHEATPATH is not set.')
return result return sheet_paths
def list(self):
""" Lists the available cheatsheets """
sheet_list = ''
pad_length = max([len(x) for x in self.get().keys()]) + 4
for sheet in sorted(self.get().items()):
sheet_list += sheet[0].ljust(pad_length) + sheet[1] + "\n"
return sheet_list
def search(self,term):
""" Searches all cheatsheets for the specified term """
result = ''
for cheatsheet in sorted(self.get().items()):
match = ''
for line in open(cheatsheet[1]):
if term in line:
match += ' ' + line
if match != '':
result += cheatsheet[0] + ":\n" + match + "\n"
return result

View File

@ -3,94 +3,99 @@ import os
import subprocess import subprocess
import sys import sys
from cheat.configuration import Configuration class Utils:
def highlight(needle, haystack):
""" Highlights a search term matched within a line """
# if a highlight color is not configured, exit early
if not 'CHEAT_HIGHLIGHT' in os.environ:
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, os.environ.get('CHEAT_HIGHLIGHT')));
def colorize(sheet_content): def __init__(self,cheatcolors,editor_executable):
""" Colorizes cheatsheet content if so configured """ self.displaycolors = cheatcolors
self.editor_executable = editor_executable
# only colorize if configured to do so, and if stdout is a tty
if not Configuration().get_cheatcolors() or not sys.stdout.isatty():
return sheet_content
# don't attempt to colorize an empty cheatsheet def highlight(self, needle, haystack):
if not sheet_content.strip(): """ Highlights a search term matched within a line """
return ""
# otherwise, attempt to import the pygments library # if a highlight color is not configured, exit early
try: if not 'CHEAT_HIGHLIGHT' in os.environ:
from pygments import highlight return haystack
from pygments.lexers import get_lexer_by_name
from pygments.formatters import TerminalFormatter
# if the import fails, return uncolored text # otherwise, attempt to import the termcolor library
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: try:
lexer = get_lexer_by_name(first_line[3:]) from termcolor import colored
except Exception:
pass
return highlight(sheet_content, lexer, TerminalFormatter()) # 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, os.environ.get('CHEAT_HIGHLIGHT')))
def die(message): def colorize(self,sheet_content):
""" Prints a message to stderr and then terminates """ """ Colorizes cheatsheet content if so configured """
warn(message)
exit(1) # only colorize if configured to do so, and if stdout is a tty
if not self.displaycolors 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())
def editor(): @staticmethod
""" Determines the user's preferred editor """ def die(message):
""" Prints a message to stderr and then terminates """
# determine which editor to use Utils.warn(message)
editor = Configuration().get_editor() exit(1)
# assert that the editor is set
if (not editor):
die(
'You must set a CHEAT_EDITOR, VISUAL, or EDITOR environment '
'variable or setting in order to create/edit a cheatsheet.'
)
return editor
def open_with_editor(filepath): def editor(self):
""" Open `filepath` using the EDITOR specified by the environment variables """ """ Determines the user's preferred editor """
editor_cmd = editor().split()
try: # assert that the editor is set
subprocess.call(editor_cmd + [filepath]) if (not self.editor_executable):
except OSError: Utils.die(
die('Could not launch ' + editor()) 'You must set a CHEAT_EDITOR, VISUAL, or EDITOR environment '
'variable or setting in order to create/edit a cheatsheet.'
)
return self.editor_executable
def warn(message): def open_with_editor(self,filepath):
""" Prints a message to stderr """ """ Open `filepath` using the EDITOR specified by the environment variables """
print((message), file=sys.stderr) 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 """
print((message), file=sys.stderr)