From 37f543fa121317ffea474978b1840896012159ff Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Melissa=20Nu=C3=B1o?= Date: Mon, 21 Oct 2019 02:50:38 -0600 Subject: [PATCH] Enable logging on import of module instead of only in main(). --- pyproject.toml | 16 ++++ rcli/__init__.py | 11 +++ rcli/autodetect.py | 99 +++++++++++++++---------- rcli/backports/get_terminal_size.py | 6 +- rcli/call.py | 48 ++++++------ rcli/config.py | 35 +++++---- rcli/dispatcher.py | 40 +++++----- rcli/display.py | 68 +++++++++-------- rcli/exceptions.py | 10 ++- rcli/log.py | 77 +++++++++++--------- rcli/usage.py | 109 +++++++++++++++------------- tox.ini | 2 +- 12 files changed, 305 insertions(+), 216 deletions(-) create mode 100644 pyproject.toml diff --git a/pyproject.toml b/pyproject.toml new file mode 100644 index 0000000..bc77432 --- /dev/null +++ b/pyproject.toml @@ -0,0 +1,16 @@ +[tool.black] +line-length = 79 +exclude = ''' +/( + \.eggs + | \.git + | \.mypy_cache + | \.pytest_cache + | \.tox + | \.venv + | _build + | buck-out + | build + | dist +)/ +''' diff --git a/rcli/__init__.py b/rcli/__init__.py index f830e94..479dcbb 100644 --- a/rcli/__init__.py +++ b/rcli/__init__.py @@ -1,2 +1,13 @@ # -*- coding: utf-8 -*- """The primary module for the program.""" + +import sys + + +if sys.excepthook is sys.__excepthook__: + import logging + + from . import log + + sys.excepthook = log.excepthook + log.enable_logging(None) diff --git a/rcli/autodetect.py b/rcli/autodetect.py index 04886df..f2e96c6 100644 --- a/rcli/autodetect.py +++ b/rcli/autodetect.py @@ -24,7 +24,8 @@ _EntryPoint = collections.namedtuple( # All data representing an entry point. - '_EntryPoint', ('command', 'subcommand', 'callable')) + "_EntryPoint", ("command", "subcommand", "callable") +) def setup_keyword(dist, _, value): @@ -43,12 +44,13 @@ def setup_keyword(dist, _, value): dist.entry_points = _ensure_entry_points_is_dict(dist.entry_points) for command, subcommands in six.iteritems(_get_commands(dist)): - entry_point = '{command} = rcli.dispatcher:main'.format( - command=command) - entry_points = dist.entry_points.setdefault('console_scripts', []) + entry_point = "{command} = rcli.dispatcher:main".format( + command=command + ) + entry_points = dist.entry_points.setdefault("console_scripts", []) if entry_point not in entry_points: entry_points.append(entry_point) - dist.entry_points.setdefault('rcli', []).extend(subcommands) + dist.entry_points.setdefault("rcli", []).extend(subcommands) def _ensure_entry_points_is_dict(entry_points): @@ -57,9 +59,11 @@ def _ensure_entry_points_is_dict(entry_points): elif isinstance(entry_points, str): config = configparser.ConfigParser() config.read_string(entry_points) - return {k: ['='.join(t) for t in section.items()] - for k, section in config.items() - if k != config.default_section} + return { + k: ["=".join(t) for t in section.items()] + for k, section in config.items() + if k != config.default_section + } return entry_points @@ -72,19 +76,25 @@ def egg_info_writer(cmd, basename, filename): basename: The basename of the file to write. filename: The full path of the file to write into the egg info. """ - setupcfg = next((f for f in setuptools.findall() - if os.path.basename(f) == 'setup.cfg'), None) + setupcfg = next( + ( + f + for f in setuptools.findall() + if os.path.basename(f) == "setup.cfg" + ), + None, + ) if not setupcfg: return parser = six.moves.configparser.ConfigParser() # type: ignore parser.read(setupcfg) - if not parser.has_section('rcli') or not parser.items('rcli'): + if not parser.has_section("rcli") or not parser.items("rcli"): return - config = dict(parser.items('rcli')) # type: typing.Dict[str, typing.Any] + config = dict(parser.items("rcli")) # type: typing.Dict[str, typing.Any] for k, v in six.iteritems(config): - if v.lower() in ('y', 'yes', 'true'): + if v.lower() in ("y", "yes", "true"): config[k] = True - elif v.lower() in ('n', 'no', 'false'): + elif v.lower() in ("n", "no", "false"): config[k] = False else: try: @@ -94,8 +104,9 @@ def egg_info_writer(cmd, basename, filename): cmd.write_file(basename, filename, json.dumps(config)) -def _get_commands(dist # type: setuptools.dist.Distribution - ): +def _get_commands( + dist # type: setuptools.dist.Distribution +): # type: (...) -> typing.Dict[str, typing.Set[str]] """Find all commands belonging to the given distribution. @@ -107,8 +118,11 @@ def _get_commands(dist # type: setuptools.dist.Distribution A dictionary containing a mapping of primary commands to sets of subcommands. """ - py_files = (f for f in setuptools.findall() - if os.path.splitext(f)[1].lower() == '.py') + py_files = ( + f + for f in setuptools.findall() + if os.path.splitext(f)[1].lower() == ".py" + ) pkg_files = (f for f in py_files if _get_package_name(f) in dist.packages) commands = {} # type: typing.Dict[str, typing.Set[str]] for file_name in pkg_files: @@ -121,10 +135,11 @@ def _get_commands(dist # type: setuptools.dist.Distribution return commands -def _append_commands(dct, # type: typing.Dict[str, typing.Set[str]] - module_name, # type: str - commands # type:typing.Iterable[_EntryPoint] - ): +def _append_commands( + dct, # type: typing.Dict[str, typing.Set[str]] + module_name, # type: str + commands, # type:typing.Iterable[_EntryPoint] +): # type: (...) -> None """Append entry point strings representing the given Command objects. @@ -137,13 +152,15 @@ def _append_commands(dct, # type: typing.Dict[str, typing.Set[str]] commands: A list of Command objects to convert to entry point strings. """ for command in commands: - entry_point = '{command}{subcommand} = {module}{callable}'.format( + entry_point = "{command}{subcommand} = {module}{callable}".format( command=command.command, - subcommand=(':{}'.format(command.subcommand) - if command.subcommand else ''), + subcommand=( + ":{}".format(command.subcommand) if command.subcommand else "" + ), module=module_name, - callable=(':{}'.format(command.callable) - if command.callable else ''), + callable=( + ":{}".format(command.callable) if command.callable else "" + ), ) dct.setdefault(command.command, set()).add(entry_point) @@ -159,7 +176,7 @@ def _get_package_name(file_name): Converts the file name to a python-style module name and retrieves the package component. """ - return _get_module_name(file_name).rsplit('.', 1)[0] + return _get_module_name(file_name).rsplit(".", 1)[0] def _get_module_name(file_name): @@ -172,7 +189,7 @@ def _get_module_name(file_name): Returns: Converts the file name to a python-style module and returns the name. """ - return file_name[:-3].replace('/', '.') + return file_name[:-3].replace("/", ".") def _get_module_commands(module): @@ -188,12 +205,18 @@ def _get_module_commands(module): Yields: Command objects that represent entry points to append to setup.py. """ - cls = next((n for n in module.body - if isinstance(n, ast.ClassDef) and n.name == 'Command'), None) + cls = next( + ( + n + for n in module.body + if isinstance(n, ast.ClassDef) and n.name == "Command" + ), + None, + ) if not cls: return methods = (n.name for n in cls.body if isinstance(n, ast.FunctionDef)) - if '__call__' not in methods: + if "__call__" not in methods: return docstring = ast.get_docstring(module) for commands, _ in usage.parse_commands(docstring): @@ -216,11 +239,12 @@ def _get_class_commands(module): nodes = (n for n in module.body if isinstance(n, ast.ClassDef)) for cls in nodes: methods = (n.name for n in cls.body if isinstance(n, ast.FunctionDef)) - if '__call__' in methods: + if "__call__" in methods: docstring = ast.get_docstring(cls) for commands, _ in usage.parse_commands(docstring): - yield _EntryPoint(commands[0], next(iter(commands[1:]), None), - cls.name) + yield _EntryPoint( + commands[0], next(iter(commands[1:]), None), cls.name + ) def _get_function_commands(module): @@ -240,5 +264,6 @@ def _get_function_commands(module): for func in nodes: docstring = ast.get_docstring(func) for commands, _ in usage.parse_commands(docstring): - yield _EntryPoint(commands[0], next(iter(commands[1:]), None), - func.name) + yield _EntryPoint( + commands[0], next(iter(commands[1:]), None), func.name + ) diff --git a/rcli/backports/get_terminal_size.py b/rcli/backports/get_terminal_size.py index 3193136..32434fd 100644 --- a/rcli/backports/get_terminal_size.py +++ b/rcli/backports/get_terminal_size.py @@ -12,6 +12,6 @@ try: from shutil import get_terminal_size except ImportError: - from backports.shutil_get_terminal_size import ( # noqa: F401 - get_terminal_size - ) + from backports.shutil_get_terminal_size import ( + get_terminal_size, + ) # noqa: F401 diff --git a/rcli/call.py b/rcli/call.py index b7454e1..d476da0 100644 --- a/rcli/call.py +++ b/rcli/call.py @@ -13,23 +13,22 @@ from types import ( # noqa: F401 pylint: disable=unused-import FunctionType, MethodType, - ModuleType + ModuleType, ) -import collections -import inspect -import keyword -import logging -import re - -from typingplus import ( # noqa: F401 pylint: disable=unused-import +from typing import ( # noqa: F401 pylint: disable=unused-import Any, Dict, Generator, Tuple, Union, - cast, - get_type_hints ) + +from typet.typing import cast, get_type_hints +import collections +import inspect +import keyword +import logging +import re import six from . import config # noqa: F401 pylint: disable=unused-import @@ -38,8 +37,8 @@ _LOGGER = logging.getLogger(__name__) -_getspec = getattr(inspect, 'get{}argspec'.format('full' if six.PY3 else '')) -_ArgSpec = collections.namedtuple('_ArgSpec', ('args', 'varargs', 'varkw')) +_getspec = getattr(inspect, "get{}argspec".format("full" if six.PY3 else "")) +_ArgSpec = collections.namedtuple("_ArgSpec", ("args", "varargs", "varkw")) def call(func, args): @@ -52,10 +51,12 @@ def call(func, args): Returns: The return value of func. """ - assert hasattr(func, '__call__'), 'Cannot call func: {}'.format( - func.__name__) + assert hasattr(func, "__call__"), "Cannot call func: {}".format( + func.__name__ + ) raw_func = ( - func if isinstance(func, FunctionType) else func.__class__.__call__) + func if isinstance(func, FunctionType) else func.__class__.__call__ + ) hints = collections.defaultdict(lambda: Any, get_type_hints(raw_func)) argspec = _getargspec(raw_func) named_args = {} @@ -73,7 +74,8 @@ def call(func, args): if nk == argspec.varargs: varargs = value elif (nk in argspec.args or argspec.varkw) and ( - nk not in named_args or named_args[nk] is None): + nk not in named_args or named_args[nk] is None + ): named_args[nk] = value return func(*varargs, **named_args) @@ -96,11 +98,13 @@ def get_callable(subcommand): callable class named Command. """ _LOGGER.debug( - 'Creating callable from subcommand "%s".', subcommand.__name__) + 'Creating callable from subcommand "%s".', subcommand.__name__ + ) if isinstance(subcommand, ModuleType): - _LOGGER.debug('Subcommand is a module.') - assert hasattr(subcommand, 'Command'), ( - 'Module subcommand must have callable "Command" class definition.') + _LOGGER.debug("Subcommand is a module.") + assert hasattr( + subcommand, "Command" + ), 'Module subcommand must have callable "Command" class definition.' callable_ = subcommand.Command # type: ignore else: callable_ = subcommand @@ -148,9 +152,9 @@ def _normalize(args): the parameter. """ for k, v in six.iteritems(args): - nk = re.sub(r'\W|^(?=\d)', '_', k).strip('_').lower() + nk = re.sub(r"\W|^(?=\d)", "_", k).strip("_").lower() do_not_shadow = dir(six.moves.builtins) # type: ignore if keyword.iskeyword(nk) or nk in do_not_shadow: - nk += '_' + nk += "_" _LOGGER.debug('Normalized "%s" to "%s".', k, nk) yield k, nk, v diff --git a/rcli/config.py b/rcli/config.py index 36f7f3c..525265e 100644 --- a/rcli/config.py +++ b/rcli/config.py @@ -32,11 +32,10 @@ RcliEntryPoint = typing.Union[types.FunctionType, type, types.ModuleType] -@six.add_metaclass(Singleton) -class _RcliConfig(object): +class _RcliConfig: """A global settings object for the command and the configuration.""" - _EP_MOD_NAME = 'rcli.dispatcher' # The console script entry point module. + _EP_MOD_NAME = "rcli.dispatcher" # The console script entry point module. def __init__(self): # type: () -> None @@ -46,9 +45,10 @@ def __init__(self): self._version = None # type: str self._entry_point = None # type: pkg_resources.EntryPoint self._config = {} # type: typing.Dict[str, typing.Any] - if (self.distribution and - self.distribution.has_metadata('rcli-config.json')): - data = self.distribution.get_metadata('rcli-config.json') + if self.distribution and self.distribution.has_metadata( + "rcli-config.json" + ): + data = self.distribution.get_metadata("rcli-config.json") self._config = json.loads(data) @property @@ -57,7 +57,8 @@ def command(self): """The name of the active command.""" if not self._command: self._command = os.path.basename( - os.path.realpath(os.path.abspath(sys.argv[0]))) + os.path.realpath(os.path.abspath(sys.argv[0])) + ) return self._command @property @@ -65,17 +66,17 @@ def subcommands(self): # type: () -> typing.Dict[str, RcliEntryPoint] """A mapping of subcommand names to loaded entry point targets.""" if not self._subcommands: - regex = re.compile(r'{}:(?P[^:]+)$'.format(self.command)) - for ep in pkg_resources.iter_entry_points(group='rcli'): + regex = re.compile(r"{}:(?P[^:]+)$".format(self.command)) + for ep in pkg_resources.iter_entry_points(group="rcli"): try: if ep.name == self.command: self._subcommands[None] = ep.load() else: match = re.match(regex, ep.name) if match: - self._subcommands[match.group('name')] = ep.load() - except ImportError: - _LOGGER.exception('Unable to load command. Skipping.') + self._subcommands[match.group("name")] = ep.load() + except Exception: + _LOGGER.exception("Unable to load command. Skipping.") return self._subcommands @property @@ -83,7 +84,7 @@ def version(self): # type: () -> str """The version defined in the distribution.""" if not self._version: - if hasattr(self.distribution, 'version'): + if hasattr(self.distribution, "version"): self._version = str(self.distribution) return self._version @@ -92,9 +93,11 @@ def entry_point(self): # type: () -> pkg_resources.EntryPoint """The currently active entry point.""" if not self._entry_point: - for ep in pkg_resources.iter_entry_points(group='console_scripts'): - if (ep.name == self.command and - ep.module_name == self._EP_MOD_NAME): + for ep in pkg_resources.iter_entry_points(group="console_scripts"): + if ( + ep.name == self.command + and ep.module_name == self._EP_MOD_NAME + ): self._entry_point = ep return self._entry_point diff --git a/rcli/dispatcher.py b/rcli/dispatcher.py index 2b45979..8a8abfd 100644 --- a/rcli/dispatcher.py +++ b/rcli/dispatcher.py @@ -11,23 +11,21 @@ import logging import sys import types # noqa: F401 pylint: disable=unused-import +import typing # noqa: F401 pylint: disable=unused-import from docopt import docopt import colorama import six -import typingplus as typing from . import ( # noqa: F401 pylint: disable=unused-import exceptions as exc, log, call, config, - usage + usage, ) from .config import settings -typing.upgrade_typing() - _LOGGER = logging.getLogger(__name__) @@ -40,19 +38,22 @@ def main(): """ colorama.init(wrap=six.PY3) doc = usage.get_primary_command_usage() - allow_subcommands = '' in doc - args = docopt(doc, version=settings.version, - options_first=allow_subcommands) + allow_subcommands = "" in doc + args = docopt( + doc, version=settings.version, options_first=allow_subcommands + ) if sys.excepthook is sys.__excepthook__: sys.excepthook = log.excepthook try: log.enable_logging(log.get_log_level(args)) - default_args = sys.argv[2 if args.get('') else 1:] - if (args.get('') == 'help' and - None not in settings.subcommands): - subcommand = next(iter(args.get('', default_args)), None) + default_args = sys.argv[2 if args.get("") else 1 :] + if ( + args.get("") == "help" + and None not in settings.subcommands + ): + subcommand = next(iter(args.get("", default_args)), None) return usage.get_help_usage(subcommand) - argv = [args.get('')] + args.get('', default_args) + argv = [args.get("")] + args.get("", default_args) return _run_command(argv) except exc.InvalidCliValueError as e: return str(e) @@ -71,9 +72,10 @@ def _get_subcommand(name): _LOGGER.debug('Accessing subcommand "%s".', name) if name not in settings.subcommands: raise ValueError( - '"{subcommand}" is not a {command} command. \'{command} help -a\' ' - 'lists all available subcommands.'.format( - command=settings.command, subcommand=name) + "\"{subcommand}\" is not a {command} command. '{command} help -a' " + "lists all available subcommands.".format( + command=settings.command, subcommand=name + ) ) return settings.subcommands[name] @@ -95,8 +97,12 @@ def _run_command(argv): ValueError: Raised if the user attempted to run an invalid command. """ command_name, argv = _get_command_and_argv(argv) - _LOGGER.info('Running command "%s %s" with args: %s', settings.command, - command_name, argv) + _LOGGER.info( + 'Running command "%s %s" with args: %s', + settings.command, + command_name, + argv, + ) subcommand = _get_subcommand(command_name) func = call.get_callable(subcommand) doc = usage.format_usage(subcommand.__doc__) diff --git a/rcli/display.py b/rcli/display.py index 7acb790..068d326 100644 --- a/rcli/display.py +++ b/rcli/display.py @@ -27,11 +27,7 @@ import sys import time -from colorama import ( - Cursor, - Fore, - Style -) +from colorama import Cursor, Fore, Style from tqdm import tqdm from .backports.get_terminal_size import get_terminal_size @@ -64,21 +60,22 @@ def __init__(self, message, color, exc=None): def hidden_cursor(): """Temporarily hide the terminal cursor.""" if sys.stdout.isatty(): - _LOGGER.debug('Hiding cursor.') - print('\x1B[?25l', end='') + _LOGGER.debug("Hiding cursor.") + print("\x1B[?25l", end="") sys.stdout.flush() try: yield finally: if sys.stdout.isatty(): - _LOGGER.debug('Showing cursor.') - print('\n\x1B[?25h', end='') + _LOGGER.debug("Showing cursor.") + print("\n\x1B[?25h", end="") sys.stdout.flush() @contextlib.contextmanager def display_status(): """Display an OK or FAILED message for the context block.""" + def print_status(msg, color): """Print the status message. @@ -86,15 +83,17 @@ def print_status(msg, color): msg: The message to display (e.g. OK or FAILED). color: The ANSI color code to use in displaying the message. """ - print('\r' if sys.stdout.isatty() else '\t', end='') - print('{}{}[{color}{msg}{}]{}'.format( - Cursor.FORWARD(_ncols() - 8), - Style.BRIGHT, - Fore.RESET, - Style.RESET_ALL, - color=color, - msg=msg[:6].upper().center(6) - )) + print("\r" if sys.stdout.isatty() else "\t", end="") + print( + "{}{}[{color}{msg}{}]{}".format( + Cursor.FORWARD(_ncols() - 8), + Style.BRIGHT, + Fore.RESET, + Style.RESET_ALL, + color=color, + msg=msg[:6].upper().center(6), + ) + ) sys.stdout.flush() try: @@ -107,10 +106,10 @@ def print_status(msg, color): except (KeyboardInterrupt, EOFError): raise except Exception: - print_status('FAILED', Fore.RED) + print_status("FAILED", Fore.RED) raise else: - print_status('OK', Fore.GREEN) + print_status("OK", Fore.GREEN) @contextlib.contextmanager @@ -120,6 +119,7 @@ def timed_display(msg): Args: msg: The header message to print at the beginning of the timed block. """ + def print_header(msg, newline=True): """Print a header line. @@ -130,10 +130,11 @@ def print_header(msg, newline=True): overwrite another. """ if sys.stdout.isatty(): - print('\r', end=Style.BRIGHT + Fore.BLUE) - print(' {} '.format(msg).center(_ncols(), '='), - end='\n{}'.format(Style.RESET_ALL) - if newline else Style.RESET_ALL) + print("\r", end=Style.BRIGHT + Fore.BLUE) + print( + " {} ".format(msg).center(_ncols(), "="), + end="\n{}".format(Style.RESET_ALL) if newline else Style.RESET_ALL, + ) sys.stdout.flush() def print_message(msg): @@ -143,9 +144,9 @@ def print_message(msg): msg: The message to display before running the task. """ if sys.stdout.isatty(): - print('\r', end='') + print("\r", end="") msg = msg.ljust(_ncols()) - print(msg, end='') + print(msg, end="") sys.stdout.flush() start = time.time() @@ -155,7 +156,7 @@ def print_message(msg): yield print_message finally: delta = time.time() - start - print_header('completed in {:.2f}s'.format(delta), False) + print_header("completed in {:.2f}s".format(delta), False) def run_tasks(header, tasks): @@ -169,10 +170,15 @@ def run_tasks(header, tasks): """ tasks = list(tasks) with timed_display(header) as print_message: - with tqdm(tasks, position=1, desc='Progress', disable=None, - bar_format='{desc}{percentage:3.0f}% |{bar}|', - total=sum(t[2] if len(t) > 2 else 1 for t in tasks), - dynamic_ncols=True) as pbar: + with tqdm( + tasks, + position=1, + desc="Progress", + disable=None, + bar_format="{desc}{percentage:3.0f}% |{bar}|", + total=sum(t[2] if len(t) > 2 else 1 for t in tasks), + dynamic_ncols=True, + ) as pbar: for task in tasks: print_message(task[0]) with display_status(): diff --git a/rcli/exceptions.py b/rcli/exceptions.py index 398b9b1..28d6672 100644 --- a/rcli/exceptions.py +++ b/rcli/exceptions.py @@ -32,9 +32,10 @@ def __init__(self, parameter, value, valid_values=None): parameter. """ msg = 'Invalid value "{value}" supplied to {parameter}.'.format( - parameter=parameter, value=value) + parameter=parameter, value=value + ) if valid_values: - msg += ' Valid options are: {}'.format(', '.join(valid_values)) + msg += " Valid options are: {}".format(", ".join(valid_values)) super(InvalidCliValueError, self).__init__(msg) @@ -49,7 +50,7 @@ def __init__(self, log_level): log_level: The invalid value passed as the log level. """ super(InvalidLogLevelError, self).__init__( - '--log-level', log_level, ('DEBUG', 'INFO', 'WARN', 'ERROR') + "--log-level", log_level, ("DEBUG", "INFO", "WARN", "ERROR") ) @@ -68,4 +69,5 @@ def __init__(self, type_, value): self.type_ = type_ self.value = value super(CastError, self).__init__( - 'Unable to cast "{}" to {}.'.format(value, type_.__name__)) + 'Unable to cast "{}" to {}.'.format(value, type_.__name__) + ) diff --git a/rcli/log.py b/rcli/log.py index 1090803..c11a04c 100644 --- a/rcli/log.py +++ b/rcli/log.py @@ -36,14 +36,17 @@ def write_logfile(): # type: () -> None """Write a DEBUG log file COMMAND-YYYYMMDD-HHMMSS.ffffff.log.""" command = os.path.basename(os.path.realpath(os.path.abspath(sys.argv[0]))) - now = datetime.datetime.now().strftime('%Y%m%d-%H%M%S.%f') - filename = '{}-{}.log'.format(command, now) - with open(filename, 'w') as logfile: + now = datetime.datetime.now().strftime("%Y%m%d-%H%M%S.%f") + filename = "{}-{}.log".format(command, now) + with open(filename, "w") as logfile: if six.PY3: logfile.write(_LOGFILE_STREAM.getvalue()) else: - logfile.write(_LOGFILE_STREAM.getvalue().decode( # type: ignore - errors='replace')) + logfile.write( + _LOGFILE_STREAM.getvalue().decode( # type: ignore + errors="replace" + ) + ) def get(): @@ -64,6 +67,8 @@ def excepthook(type, value, traceback): # pylint: disable=unused-argument else: message = handle_unexpected_exception(value) print(message, file=sys.stderr) + + # pragma pylint: enable=redefined-builtin @@ -79,12 +84,12 @@ def handle_unexpected_exception(exc): """ try: write_logfile() - addendum = 'Please see the log file for more information.' + addendum = "Please see the log file for more information." except IOError: - addendum = 'Unable to write log file.' + addendum = "Unable to write log file." try: message = str(exc) - return '{}{}{}'.format(message, '\n' if message else '', addendum) + return "{}{}{}".format(message, "\n" if message else "", addendum) except Exception: # pylint: disable=broad-except return str(exc) @@ -100,8 +105,9 @@ def enable_logging(log_level): root_logger.setLevel(logging.DEBUG) logfile_handler = logging.StreamHandler(_LOGFILE_STREAM) logfile_handler.setLevel(logging.DEBUG) - logfile_handler.setFormatter(logging.Formatter( - '%(levelname)s [%(asctime)s][%(name)s] %(message)s')) + logfile_handler.setFormatter( + logging.Formatter("%(levelname)s [%(asctime)s][%(name)s] %(message)s") + ) root_logger.addHandler(logfile_handler) if signal.getsignal(signal.SIGTERM) == signal.SIG_DFL: signal.signal(signal.SIGTERM, _logfile_sigterm_handler) @@ -131,25 +137,25 @@ def get_log_level(args): """ index = -1 log_level = None - if '' in args and args['']: - index = sys.argv.index(args['']) - if args.get('--debug'): - log_level = 'DEBUG' - if '--debug' in sys.argv and sys.argv.index('--debug') < index: - sys.argv.remove('--debug') - elif '-d' in sys.argv and sys.argv.index('-d') < index: - sys.argv.remove('-d') - elif args.get('--verbose'): - log_level = 'INFO' - if '--verbose' in sys.argv and sys.argv.index('--verbose') < index: - sys.argv.remove('--verbose') - elif '-v' in sys.argv and sys.argv.index('-v') < index: - sys.argv.remove('-v') - elif args.get('--log-level'): - log_level = args['--log-level'] - sys.argv.remove('--log-level') + if "" in args and args[""]: + index = sys.argv.index(args[""]) + if args.get("--debug"): + log_level = "DEBUG" + if "--debug" in sys.argv and sys.argv.index("--debug") < index: + sys.argv.remove("--debug") + elif "-d" in sys.argv and sys.argv.index("-d") < index: + sys.argv.remove("-d") + elif args.get("--verbose"): + log_level = "INFO" + if "--verbose" in sys.argv and sys.argv.index("--verbose") < index: + sys.argv.remove("--verbose") + elif "-v" in sys.argv and sys.argv.index("-v") < index: + sys.argv.remove("-v") + elif args.get("--log-level"): + log_level = args["--log-level"] + sys.argv.remove("--log-level") sys.argv.remove(log_level) - if log_level not in (None, 'DEBUG', 'INFO', 'WARN', 'ERROR'): + if log_level not in (None, "DEBUG", "INFO", "WARN", "ERROR"): raise exceptions.InvalidLogLevelError(log_level) return getattr(logging, log_level) if log_level else None @@ -161,10 +167,12 @@ def _logfile_sigterm_handler(*_): Raises: SystemExit: Contains the signal as the return code. """ - logging.error('Received SIGTERM.') + logging.error("Received SIGTERM.") write_logfile() - print('Received signal. Please see the log file for more information.', - file=sys.stderr) + print( + "Received signal. Please see the log file for more information.", + file=sys.stderr, + ) sys.exit(signal) @@ -190,16 +198,17 @@ def format(self, record): else: color = colorama.Fore.CYAN format_template = ( - '{}{}%(levelname)s{} [%(asctime)s][%(name)s]{} %(message)s') + "{}{}%(levelname)s{} [%(asctime)s][%(name)s]{} %(message)s" + ) if sys.stdout.isatty(): self._fmt = format_template.format( colorama.Style.BRIGHT, color, colorama.Fore.RESET, - colorama.Style.RESET_ALL + colorama.Style.RESET_ALL, ) else: - self._fmt = format_template.format(*[''] * 4) + self._fmt = format_template.format(*[""] * 4) if six.PY3: self._style._fmt = self._fmt # pylint: disable=protected-access return super(_LogColorFormatter, self).format(record) diff --git a/rcli/usage.py b/rcli/usage.py index 3644f10..ca32d38 100644 --- a/rcli/usage.py +++ b/rcli/usage.py @@ -18,7 +18,7 @@ Generator, List, Optional, - Tuple + Tuple, ) import collections import inspect @@ -48,16 +48,18 @@ {{message}} '{command} help -a' lists all available subcommands. See '{command} help ' for more information on a specific command. -""".format(command=settings.command) +""".format( + command=settings.command +) -def get_primary_command_usage(message=''): +def get_primary_command_usage(message=""): # type: (str) -> str """Return the usage string for the primary command.""" if not settings.merge_primary_command and None in settings.subcommands: return format_usage(settings.subcommands[None].__doc__) if not message: - message = '\n{}\n'.format(settings.message) if settings.message else '' + message = "\n{}\n".format(settings.message) if settings.message else "" doc = _DEFAULT_DOC.format(message=message) if None in settings.subcommands: return _merge_doc(doc, settings.subcommands[None].__doc__) @@ -80,18 +82,19 @@ def get_help_usage(command): """ if not command: doc = get_primary_command_usage() - elif command in ('-a', '--all'): + elif command in ("-a", "--all"): subcommands = [k for k in settings.subcommands if k is not None] - available_commands = subcommands + ['help'] - command_doc = '\nAvailable commands:\n{}\n'.format( - '\n'.join(' {}'.format(c) for c in sorted(available_commands))) + available_commands = subcommands + ["help"] + command_doc = "\nAvailable commands:\n{}\n".format( + "\n".join(" {}".format(c) for c in sorted(available_commands)) + ) doc = get_primary_command_usage(command_doc) - elif command.startswith('-'): + elif command.startswith("-"): raise ValueError("Unrecognized option '{}'.".format(command)) elif command in settings.subcommands: subcommand = settings.subcommands[command] doc = format_usage(subcommand.__doc__) - docopt.docopt(doc, argv=('--help',)) + docopt.docopt(doc, argv=("--help",)) def format_usage(doc, width=None): @@ -105,9 +108,9 @@ def format_usage(doc, width=None): The docstring formatted to parse and display to the user. This includes dedenting, rewrapping, and translating the docstring if necessary. """ - sections = doc.replace('\r', '').split('\n\n') + sections = doc.replace("\r", "").split("\n\n") width = width or get_terminal_size().columns or 80 - return '\n\n'.join(_wrap_section(s.strip(), width) for s in sections) + return "\n\n".join(_wrap_section(s.strip(), width) for s in sections) def parse_commands(docstring): @@ -127,7 +130,7 @@ def parse_commands(docstring): return except docopt.DocoptExit: pass - for command in _parse_section('usage', docstring): + for command in _parse_section("usage", docstring): args = command.split() commands = [] i = 0 @@ -151,16 +154,17 @@ def _merge_doc(original, to_merge): A new usage string that contains information from both usage strings. """ if not original: - return to_merge or '' + return to_merge or "" if not to_merge: - return original or '' + return original or "" sections = [] - for name in ('usage', 'arguments', 'options'): - sections.append(_merge_section( - _get_section(name, original), - _get_section(name, to_merge) - )) - return format_usage('\n\n'.join(s for s in sections).rstrip()) + for name in ("usage", "arguments", "options"): + sections.append( + _merge_section( + _get_section(name, original), _get_section(name, to_merge) + ) + ) + return format_usage("\n\n".join(s for s in sections).rstrip()) def _merge_section(original, to_merge): @@ -176,18 +180,18 @@ def _merge_section(original, to_merge): the section lines from both. """ if not original: - return to_merge or '' + return to_merge or "" if not to_merge: - return original or '' + return original or "" try: - index = original.index(':') + 1 + index = original.index(":") + 1 except ValueError: - index = original.index('\n') + index = original.index("\n") name = original[:index].strip() - section = '\n '.join( - (original[index + 1:].lstrip(), to_merge[index + 1:].lstrip()) + section = "\n ".join( + (original[index + 1 :].lstrip(), to_merge[index + 1 :].lstrip()) ).rstrip() - return '{name}\n {section}'.format(name=name, section=section) + return "{name}\n {section}".format(name=name, section=section) def _get_section(name, source): @@ -203,8 +207,9 @@ def _get_section(name, source): multiple times, each instance will be merged into a single section. """ pattern = re.compile( - '^([^\n]*{name}[^\n]*\n?(?:[ \t].*?(?:\n|$))*)'.format(name=name), - re.IGNORECASE | re.MULTILINE) + "^([^\n]*{name}[^\n]*\n?(?:[ \t].*?(?:\n|$))*)".format(name=name), + re.IGNORECASE | re.MULTILINE, + ) usage = None for section in pattern.findall(source): usage = _merge_section(usage, section.strip()) @@ -225,14 +230,15 @@ def _wrap_section(source, width): Returns: The wrapped section string. """ - if _get_section('usage', source): + if _get_section("usage", source): return _wrap_usage_section(source, width) if _is_definition_section(source): return _wrap_definition_section(source, width) lines = inspect.cleandoc(source).splitlines() - paragraphs = (textwrap.wrap(line, width, replace_whitespace=False) - for line in lines) - return '\n'.join(line for paragraph in paragraphs for line in paragraph) + paragraphs = ( + textwrap.wrap(line, width, replace_whitespace=False) for line in lines + ) + return "\n".join(line for paragraph in paragraphs for line in paragraph) def _is_definition_section(source): @@ -245,9 +251,10 @@ def _is_definition_section(source): True if the source describes a definition section; otherwise, False. """ try: - definitions = textwrap.dedent(source).split('\n', 1)[1].splitlines() + definitions = textwrap.dedent(source).split("\n", 1)[1].splitlines() return all( - re.match(r'\s\s+((?!\s\s).+)\s\s+.+', s) for s in definitions) + re.match(r"\s\s+((?!\s\s).+)\s\s+.+", s) for s in definitions + ) except IndexError: return False @@ -268,16 +275,16 @@ def _wrap_usage_section(source, width): """ if not any(len(line) > width for line in source.splitlines()): return source - section_header = source[:source.index(':') + 1].strip() + section_header = source[: source.index(":") + 1].strip() lines = [section_header] for commands, args in parse_commands(source): - command = ' {} '.format(' '.join(commands)) + command = " {} ".format(" ".join(commands)) max_len = width - len(command) - sep = '\n' + ' ' * len(command) - wrapped_args = sep.join(textwrap.wrap(' '.join(args), max_len)) + sep = "\n" + " " * len(command) + wrapped_args = sep.join(textwrap.wrap(" ".join(args), max_len)) full_command = command + wrapped_args lines += full_command.splitlines() - return '\n'.join(lines) + return "\n".join(lines) def _wrap_definition_section(source, width): @@ -293,18 +300,18 @@ def _wrap_definition_section(source, width): Returns: The wrapped section string. """ - index = source.index('\n') + 1 + index = source.index("\n") + 1 definitions, max_len = _get_definitions(source[index:]) - sep = '\n' + ' ' * (max_len + 4) + sep = "\n" + " " * (max_len + 4) lines = [source[:index].strip()] for arg, desc in six.iteritems(definitions): wrapped_desc = sep.join(textwrap.wrap(desc, width - max_len - 4)) - lines.append(' {arg:{size}} {desc}'.format( - arg=arg, - size=str(max_len), - desc=wrapped_desc - )) - return '\n'.join(lines) + lines.append( + " {arg:{size}} {desc}".format( + arg=arg, size=str(max_len), desc=wrapped_desc + ) + ) + return "\n".join(lines) def _get_definitions(source): @@ -325,7 +332,7 @@ def _get_definitions(source): non_empty_lines = (s for s in lines if s) for line in non_empty_lines: if line: - arg, desc = re.split(r'\s\s+', line.strip()) + arg, desc = re.split(r"\s\s+", line.strip()) arg_len = len(arg) if arg_len > max_len: max_len = arg_len @@ -357,5 +364,5 @@ def _parse_section(name, source): if not commands or line[:1].isalpha() and line[:1].islower(): commands.append(line) else: - commands[-1] = '{} {}'.format(commands[-1].strip(), line.strip()) + commands[-1] = "{} {}".format(commands[-1].strip(), line.strip()) return commands diff --git a/tox.ini b/tox.ini index ce35ebd..ba27ff8 100644 --- a/tox.ini +++ b/tox.ini @@ -1,6 +1,6 @@ [tox] skipsdist = True -envlist = py{27,33,34,35,36} +envlist = py{35,36,37,38} [testenv] setenv =