From d0fd7e90c57a542a82e2971e1c27e0ab13817a5c Mon Sep 17 00:00:00 2001 From: Martinez Date: Mon, 16 May 2016 00:27:08 +0200 Subject: [PATCH] - minor changes - add Exception.description property support - add a few sanity checks to over.app.Main - added top-level stack frame display to over.app.Main.exception_handler - the command line parser is now usable :) --- over/app.py | 107 +++++++++++++++++++++++++++++++++++------------- over/file.py | 4 +- over/misc.py | 4 +- over/text.py | 12 +++--- over/types.py | 2 +- over/types.pyx | 2 +- over/version.py | 2 +- 7 files changed, 91 insertions(+), 42 deletions(-) diff --git a/over/app.py b/over/app.py index 01e5a90..688afed 100644 --- a/over/app.py +++ b/over/app.py @@ -7,7 +7,6 @@ from collections import OrderedDict import enum import sys import re -import traceback # -------------------------------------------------- # Local imports @@ -21,25 +20,44 @@ from . import types class UnknownAbbreviation(Exception): """ - This abbreviation doesn't map to any known option. + This abbreviation doesn't map to any known option. If this was a target, place it at the end of the line after a guard (--). """ pass class UnknownOption(Exception): """ - This option was passed on the command line but is not known. + This option was mentioned in code, the config file or on the command line but is not known. """ - pass + @property + def description(self): + return "--%s" %(self.args[0]) class ReadingUnsetOption(Exception): """ This option has no default, config file or command line value set. """ - + pass +class OptionNameUsed(Exception): + """ + This option's name or abbreviation is already used by a previously defined option. + """ + + pass + +class IncompleteArguments(Exception): + @property + def description(self): + option_name, option_count, option_remaining = self.args + + if option_remaining == option_count: + return "option --%s takes a %d-word argument but has received none" %(option_name, option_count) + else: + return "option --%s takes a %d-word argument but has only received %d" %(option_name, option_count, option_count - option_remaining) + # -------------------------------------------------- class Option_sources(enum.Enum): @@ -74,7 +92,7 @@ class Option: """ Adds a value to this option's state. If the `source` is different from `self.source`, resets the state. - @action setting option value + @while setting option value """ if source != self.source: @@ -85,10 +103,7 @@ class Option: if skip_callback: value = raw_value else: - if self.count == 1: - value = self.callback(raw_value[0]) - else: - value = self.callback(raw_value) + value = self.callback(*raw_value) if self.overwrite: self._value = value @@ -109,13 +124,20 @@ class ConfigRouter: self.options = options def __getattr__(self, name): - return self.options[name].value + """ + @while retrieving an option's value + """ + + try: + return self.options[name].value + except KeyError: + raise UnknownOption(name) # -------------------------------------------------- def expand_group(token, options): """ - @action expanding a commandline group + @while expanding a commandline group """ output = [] @@ -156,13 +178,14 @@ class Main: self.license = license self.print = text.Output(name) self.options = OrderedDict() + self.options_by_abbr = OrderedDict() self.cfg = ConfigRouter(self.options) self.targets = [] self.auto_add_help = auto_add_help self.invocation = cmd.format_invocation(sys.argv) if use_cfg_file: - # ensure it exists, update it with new values, etc. + # TODO ensure it exists, update it with new values, etc. ... if handle_exceptions: @@ -180,21 +203,31 @@ class Main: def add_option(self, *args, **kwargs): """ - @action creating a new configuration option + @while registering a new configuration option """ option = Option(*args, **kwargs) - self.options[option.name] = option + if option.name in self.options: + raise OptionNameUsed("name %s is already in use" %(option.name)) + else: + self.options[option.name] = option + + if option.abbr: + if option.abbr in self.options_by_abbr: + raise OptionNameUsed("abbreviation %s is already used by --%s" %(option.abbr, self.options_by_abbr[option.abbr].name)) + else: + self.options_by_abbr[option.abbr] = option def add_doc(self, chapter, paragraphs): + # TODO ... def enable_help(self, name="help", abbr="h"): """ Map application help to --name and -abbr, and enable library ---help. - @action adding help commandline options + @while adding help commandline options """ self.add_option("help", "Display the application configuration view.", self.help, abbr=abbr, in_cfg_file=False) @@ -214,12 +247,13 @@ class Main: If `docs` is set to something else, only those docs will be displayed. - @action displaying help + @while displaying help """ - self.print("called for help!") + self.print("TODO called for help!") def parse_config(self): + # TODO ... def parse_cmdline(self, cmdline=None): @@ -238,10 +272,9 @@ class Main: +A -A +A evaluates to True. A cumulative option would evaluate to a list of [True, False, True]. - @action parsing commandline arguments + @while parsing commandline arguments """ - # todo update invocation cmdline = cmdline or sys.argv[1:] # placing it here ensures --help is the last option in the list of options @@ -252,6 +285,7 @@ class Main: remaining_payload_tokens = 0 temporary_payload = [] guard_passed = False + invocation = [] for top_token in cmdline: if not (remaining_payload_tokens or guard_passed) and len(top_token) >= 2 and top_token[0] in "+-" and top_token[1] != "-": # it's a group @@ -260,6 +294,8 @@ class Main: expanded = [top_token] for token in expanded: + invocation.append(token) + if not (remaining_payload_tokens or guard_passed) and token[:2] == "--": if token == "--": guard_passed = True @@ -294,10 +330,15 @@ class Main: last_read_option.set_value(temporary_payload, Option_sources.cmdline) else: self.targets.append(token) + + if remaining_payload_tokens: + raise IncompleteArguments(last_read_option.name, last_read_option.count, remaining_payload_tokens) + + self.invocation = cmd.format_invocation(invocation) def parse(self, cmdline=None): """ - @action assembling application configuration + @while assembling application configuration """ self.parse_config() @@ -305,7 +346,7 @@ class Main: def exception_handler(self, exception_type, exception, trace): """ - @action formatting a traceback + @while formatting a traceback Over Exception handler - prints human readable tracebacks. """ @@ -314,10 +355,8 @@ class Main: # todo top level program name # todo final level exception name and text - # fixme remove traceback tb_lines = ["", "---------------- Stack trace ----------------", "In program %s<.>" %(self.invocation)] - trace = trace.tb_next while trace: frame = trace.tb_frame @@ -340,18 +379,28 @@ class Main: else: method = None method_display_name = "%s<.>" %(method_name) - method_type = "unknown callable<.>" + if method_name == "": + method_type = "module" + else: + method_type = "unknown callable<.>" # use a docstring-provided action description if available - if method and method.__doc__ and "@action" in method.__doc__: - action = "while %s<.> " %(re.findall("@action (.+)", method.__doc__)[0].strip()) + if method and method.__doc__ and "@while" in method.__doc__: + action = "while %s<.> " %(re.findall("@while (.+)", method.__doc__)[0].strip()) else: action = "" tb_lines.append("%sin %s %s<.> at %s:%d," %(action, method_type, method_display_name, frame.f_code.co_filename, frame.f_lineno)) - reason = ": %s<.>" %(" ".join(exception.args)) if exception.args else "" - tb_lines.append("exception %s<.> was raised%s (%s)" %(exception_type.__name__, reason, exception.__doc__.strip())) + if hasattr(exception, "description"): + reason = ": %s<.>" %(exception.description) + elif exception.args: + reason = ": %s<.>" %(" ".join(exception.args)) + else: + reason = "" + + doc = " [%s]" %(exception.__doc__.strip()) if exception.__doc__ else "" + tb_lines.append("exception %s<.> was raised%s%s" %(exception_type.__name__, reason, doc)) last_i = len(tb_lines) - 1 diff --git a/over/file.py b/over/file.py index 27cc759..fc2b2cc 100644 --- a/over/file.py +++ b/over/file.py @@ -11,7 +11,7 @@ def touch(path, times=None): `times` is a tuple of (atime, mtime) and defaults to "now". - @action touching a file + @while touching a file """ path = os.path.expanduser(path) @@ -25,7 +25,7 @@ def count_lines(path): """ A reasonably fast and memory-lean line counter. - @action counting lines in a file + @while counting lines in a file """ lines = 0 diff --git a/over/misc.py b/over/misc.py index 735afa3..6ba4a39 100644 --- a/over/misc.py +++ b/over/misc.py @@ -11,7 +11,7 @@ def import_module(path): Based on the work of Yuval Greenfield released into the public domain. - @action importing a module + @while importing a module """ import imp @@ -101,7 +101,7 @@ def hexdump(data, indent=0, offset=16, show_header=True, show_offsets=True, show are replaced with a dot. The `output` must implement a .write(str x) method. If `output` is None, the string is returned instead. - @action creating a hex dump + @while creating a hex dump """ import io diff --git a/over/text.py b/over/text.py index 091be53..2505bf5 100644 --- a/over/text.py +++ b/over/text.py @@ -145,7 +145,7 @@ def render(text, colors=True): Color tags are where x is the color code from ansi_colors. <.> resets the color. Use < for a literal . - @action coloring text + @while coloring text """ text = str(text) @@ -191,7 +191,7 @@ def rfind(text, C, start, length): indices = [i for i, c in enumerate(text[start:]) if c == C] # return the original index at the same position as the last in colorless - return start + indices[len(indices_colorless) - 1] + return start + (indices[len(indices_colorless) - 1] if indices_colorless else 0) def paragraph(text, width=0, indent=0, stamp=None): """ @@ -203,7 +203,7 @@ def paragraph(text, width=0, indent=0, stamp=None): Formats text into an indented paragraph that fits the terminal and returns it. Correctly handles colors. - @action formatting a paragraph + @while formatting a paragraph """ fit_into_width = (width or get_terminal_size()[1]) - indent @@ -329,7 +329,7 @@ class Output: def print(self, text, tag=None, format=None, colors=None, end=None): """ - @action displaying text + @while displaying text """ tag = tag or self.__class__.tag.long.info @@ -498,7 +498,7 @@ class ProgressBar: it"s a callable, it will be called without arguments and shall return a number. "precision" is the displayed floating point precision. Use 0 to force an integer. - @action creating a ProgressBar + @while creating a ProgressBar """ self.format = format @@ -523,7 +523,7 @@ class ProgressBar: """ Renders the progressbar into a stream. If clear is True, clears the previous content first. - @action drawing a progressbar update + @while drawing a progressbar update """ width = self.width or get_terminal_size()[1] diff --git a/over/types.py b/over/types.py index 878a459..ef7a671 100644 --- a/over/types.py +++ b/over/types.py @@ -22,7 +22,7 @@ class ndict(OrderedDict): def __getattr__(self, name): """ - @action looking up an attribute + @while looking up an attribute """ if name in self: diff --git a/over/types.pyx b/over/types.pyx index 049c333..5d7d102 100644 --- a/over/types.pyx +++ b/over/types.pyx @@ -22,7 +22,7 @@ cdef class ndict(OrderedDict): def __getattr__(self, str name): """ - @action looking up an attribute + @while looking up an attribute """ if name in self: diff --git a/over/version.py b/over/version.py index afbf050..36a2493 100644 --- a/over/version.py +++ b/over/version.py @@ -4,5 +4,5 @@ major = 0 # VERSION_MAJOR_IDENTIFIER minor = 0 # VERSION_MINOR_IDENTIFIER # VERSION_LAST_MM 0.0 -patch = 3 # VERSION_PATCH_IDENTIFIER +patch = 4 # VERSION_PATCH_IDENTIFIER str = ".".join(str(v) for v in (major, minor, patch))