- 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 :)
This commit is contained in:
parent
b30cab6926
commit
d0fd7e90c5
7 changed files with 91 additions and 42 deletions
107
over/app.py
107
over/app.py
|
@ -7,7 +7,6 @@ from collections import OrderedDict
|
||||||
import enum
|
import enum
|
||||||
import sys
|
import sys
|
||||||
import re
|
import re
|
||||||
import traceback
|
|
||||||
|
|
||||||
# --------------------------------------------------
|
# --------------------------------------------------
|
||||||
# Local imports
|
# Local imports
|
||||||
|
@ -21,25 +20,44 @@ from . import types
|
||||||
|
|
||||||
class UnknownAbbreviation(Exception):
|
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
|
pass
|
||||||
|
|
||||||
class UnknownOption(Exception):
|
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):
|
class ReadingUnsetOption(Exception):
|
||||||
"""
|
"""
|
||||||
This option has no default, config file or command line value set.
|
This option has no default, config file or command line value set.
|
||||||
"""
|
"""
|
||||||
|
|
||||||
pass
|
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):
|
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.
|
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:
|
if source != self.source:
|
||||||
|
@ -85,10 +103,7 @@ class Option:
|
||||||
if skip_callback:
|
if skip_callback:
|
||||||
value = raw_value
|
value = raw_value
|
||||||
else:
|
else:
|
||||||
if self.count == 1:
|
value = self.callback(*raw_value)
|
||||||
value = self.callback(raw_value[0])
|
|
||||||
else:
|
|
||||||
value = self.callback(raw_value)
|
|
||||||
|
|
||||||
if self.overwrite:
|
if self.overwrite:
|
||||||
self._value = value
|
self._value = value
|
||||||
|
@ -109,13 +124,20 @@ class ConfigRouter:
|
||||||
self.options = options
|
self.options = options
|
||||||
|
|
||||||
def __getattr__(self, name):
|
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):
|
def expand_group(token, options):
|
||||||
"""
|
"""
|
||||||
@action expanding a commandline group
|
@while expanding a commandline group
|
||||||
"""
|
"""
|
||||||
|
|
||||||
output = []
|
output = []
|
||||||
|
@ -156,13 +178,14 @@ class Main:
|
||||||
self.license = license
|
self.license = license
|
||||||
self.print = text.Output(name)
|
self.print = text.Output(name)
|
||||||
self.options = OrderedDict()
|
self.options = OrderedDict()
|
||||||
|
self.options_by_abbr = OrderedDict()
|
||||||
self.cfg = ConfigRouter(self.options)
|
self.cfg = ConfigRouter(self.options)
|
||||||
self.targets = []
|
self.targets = []
|
||||||
self.auto_add_help = auto_add_help
|
self.auto_add_help = auto_add_help
|
||||||
self.invocation = cmd.format_invocation(sys.argv)
|
self.invocation = cmd.format_invocation(sys.argv)
|
||||||
|
|
||||||
if use_cfg_file:
|
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:
|
if handle_exceptions:
|
||||||
|
@ -180,21 +203,31 @@ class Main:
|
||||||
|
|
||||||
def add_option(self, *args, **kwargs):
|
def add_option(self, *args, **kwargs):
|
||||||
"""
|
"""
|
||||||
@action creating a new configuration option
|
@while registering a new configuration option
|
||||||
"""
|
"""
|
||||||
|
|
||||||
option = Option(*args, **kwargs)
|
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):
|
def add_doc(self, chapter, paragraphs):
|
||||||
|
# TODO
|
||||||
...
|
...
|
||||||
|
|
||||||
def enable_help(self, name="help", abbr="h"):
|
def enable_help(self, name="help", abbr="h"):
|
||||||
"""
|
"""
|
||||||
Map application help to --name and -abbr, and enable library ---help.
|
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)
|
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.
|
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):
|
def parse_config(self):
|
||||||
|
# TODO
|
||||||
...
|
...
|
||||||
|
|
||||||
def parse_cmdline(self, cmdline=None):
|
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
|
+A -A +A evaluates to True. A cumulative option would evaluate to a list of
|
||||||
[True, False, True].
|
[True, False, True].
|
||||||
|
|
||||||
@action parsing commandline arguments
|
@while parsing commandline arguments
|
||||||
"""
|
"""
|
||||||
|
|
||||||
# todo update invocation
|
|
||||||
cmdline = cmdline or sys.argv[1:]
|
cmdline = cmdline or sys.argv[1:]
|
||||||
|
|
||||||
# placing it here ensures --help is the last option in the list of options
|
# placing it here ensures --help is the last option in the list of options
|
||||||
|
@ -252,6 +285,7 @@ class Main:
|
||||||
remaining_payload_tokens = 0
|
remaining_payload_tokens = 0
|
||||||
temporary_payload = []
|
temporary_payload = []
|
||||||
guard_passed = False
|
guard_passed = False
|
||||||
|
invocation = []
|
||||||
|
|
||||||
for top_token in cmdline:
|
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
|
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]
|
expanded = [top_token]
|
||||||
|
|
||||||
for token in expanded:
|
for token in expanded:
|
||||||
|
invocation.append(token)
|
||||||
|
|
||||||
if not (remaining_payload_tokens or guard_passed) and token[:2] == "--":
|
if not (remaining_payload_tokens or guard_passed) and token[:2] == "--":
|
||||||
if token == "--":
|
if token == "--":
|
||||||
guard_passed = True
|
guard_passed = True
|
||||||
|
@ -294,10 +330,15 @@ class Main:
|
||||||
last_read_option.set_value(temporary_payload, Option_sources.cmdline)
|
last_read_option.set_value(temporary_payload, Option_sources.cmdline)
|
||||||
else:
|
else:
|
||||||
self.targets.append(token)
|
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):
|
def parse(self, cmdline=None):
|
||||||
"""
|
"""
|
||||||
@action assembling application configuration
|
@while assembling application configuration
|
||||||
"""
|
"""
|
||||||
|
|
||||||
self.parse_config()
|
self.parse_config()
|
||||||
|
@ -305,7 +346,7 @@ class Main:
|
||||||
|
|
||||||
def exception_handler(self, exception_type, exception, trace):
|
def exception_handler(self, exception_type, exception, trace):
|
||||||
"""
|
"""
|
||||||
@action formatting a traceback
|
@while formatting a traceback
|
||||||
|
|
||||||
Over Exception handler - prints human readable tracebacks.
|
Over Exception handler - prints human readable tracebacks.
|
||||||
"""
|
"""
|
||||||
|
@ -314,10 +355,8 @@ class Main:
|
||||||
|
|
||||||
# todo top level program name
|
# todo top level program name
|
||||||
# todo final level exception name and text
|
# todo final level exception name and text
|
||||||
# fixme remove traceback
|
|
||||||
|
|
||||||
tb_lines = ["", "---------------- Stack trace ----------------", "In program <W>%s<.>" %(self.invocation)]
|
tb_lines = ["", "---------------- Stack trace ----------------", "In program <W>%s<.>" %(self.invocation)]
|
||||||
trace = trace.tb_next
|
|
||||||
|
|
||||||
while trace:
|
while trace:
|
||||||
frame = trace.tb_frame
|
frame = trace.tb_frame
|
||||||
|
@ -340,18 +379,28 @@ class Main:
|
||||||
else:
|
else:
|
||||||
method = None
|
method = None
|
||||||
method_display_name = "<y>%s<.>" %(method_name)
|
method_display_name = "<y>%s<.>" %(method_name)
|
||||||
method_type = "<r>unknown callable<.>"
|
if method_name == "<module>":
|
||||||
|
method_type = "module"
|
||||||
|
else:
|
||||||
|
method_type = "<r>unknown callable<.>"
|
||||||
|
|
||||||
# use a docstring-provided action description if available
|
# use a docstring-provided action description if available
|
||||||
if method and method.__doc__ and "@action" in method.__doc__:
|
if method and method.__doc__ and "@while" in method.__doc__:
|
||||||
action = "while <m>%s<.> " %(re.findall("@action (.+)", method.__doc__)[0].strip())
|
action = "while <m>%s<.> " %(re.findall("@while (.+)", method.__doc__)[0].strip())
|
||||||
else:
|
else:
|
||||||
action = ""
|
action = ""
|
||||||
|
|
||||||
tb_lines.append("%sin %s <y>%s<.> at %s:%d," %(action, method_type, method_display_name, frame.f_code.co_filename, frame.f_lineno))
|
tb_lines.append("%sin %s <y>%s<.> at %s:%d," %(action, method_type, method_display_name, frame.f_code.co_filename, frame.f_lineno))
|
||||||
|
|
||||||
reason = ": <R>%s<.>" %(" ".join(exception.args)) if exception.args else ""
|
if hasattr(exception, "description"):
|
||||||
tb_lines.append("exception <Y>%s<.> was raised%s (%s)" %(exception_type.__name__, reason, exception.__doc__.strip()))
|
reason = ": <R>%s<.>" %(exception.description)
|
||||||
|
elif exception.args:
|
||||||
|
reason = ": <R>%s<.>" %(" ".join(exception.args))
|
||||||
|
else:
|
||||||
|
reason = ""
|
||||||
|
|
||||||
|
doc = " [%s]" %(exception.__doc__.strip()) if exception.__doc__ else ""
|
||||||
|
tb_lines.append("exception <Y>%s<.> was raised%s%s" %(exception_type.__name__, reason, doc))
|
||||||
|
|
||||||
last_i = len(tb_lines) - 1
|
last_i = len(tb_lines) - 1
|
||||||
|
|
||||||
|
|
|
@ -11,7 +11,7 @@ def touch(path, times=None):
|
||||||
|
|
||||||
`times` is a tuple of (atime, mtime) and defaults to "now".
|
`times` is a tuple of (atime, mtime) and defaults to "now".
|
||||||
|
|
||||||
@action touching a file
|
@while touching a file
|
||||||
"""
|
"""
|
||||||
|
|
||||||
path = os.path.expanduser(path)
|
path = os.path.expanduser(path)
|
||||||
|
@ -25,7 +25,7 @@ def count_lines(path):
|
||||||
"""
|
"""
|
||||||
A reasonably fast and memory-lean line counter.
|
A reasonably fast and memory-lean line counter.
|
||||||
|
|
||||||
@action counting lines in a file
|
@while counting lines in a file
|
||||||
"""
|
"""
|
||||||
|
|
||||||
lines = 0
|
lines = 0
|
||||||
|
|
|
@ -11,7 +11,7 @@ def import_module(path):
|
||||||
|
|
||||||
Based on the work of Yuval Greenfield released into the public domain.
|
Based on the work of Yuval Greenfield released into the public domain.
|
||||||
|
|
||||||
@action importing a module
|
@while importing a module
|
||||||
"""
|
"""
|
||||||
|
|
||||||
import imp
|
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.
|
are replaced with a dot.
|
||||||
The `output` must implement a .write(str x) method. If `output` is None, the string is returned instead.
|
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
|
import io
|
||||||
|
|
12
over/text.py
12
over/text.py
|
@ -145,7 +145,7 @@ def render(text, colors=True):
|
||||||
Color tags are <x> where x is the color code from ansi_colors.
|
Color tags are <x> where x is the color code from ansi_colors.
|
||||||
<.> resets the color. Use <<x> for a literal <x>.
|
<.> resets the color. Use <<x> for a literal <x>.
|
||||||
|
|
||||||
@action coloring text
|
@while coloring text
|
||||||
"""
|
"""
|
||||||
|
|
||||||
text = str(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]
|
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 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):
|
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.
|
Formats text into an indented paragraph that fits the terminal and returns it.
|
||||||
Correctly handles colors.
|
Correctly handles colors.
|
||||||
|
|
||||||
@action formatting a paragraph
|
@while formatting a paragraph
|
||||||
"""
|
"""
|
||||||
|
|
||||||
fit_into_width = (width or get_terminal_size()[1]) - indent
|
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):
|
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
|
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.
|
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.
|
"precision" is the displayed floating point precision. Use 0 to force an integer.
|
||||||
|
|
||||||
@action creating a ProgressBar
|
@while creating a ProgressBar
|
||||||
"""
|
"""
|
||||||
|
|
||||||
self.format = format
|
self.format = format
|
||||||
|
@ -523,7 +523,7 @@ class ProgressBar:
|
||||||
"""
|
"""
|
||||||
Renders the progressbar into a stream. If clear is True, clears the previous content first.
|
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]
|
width = self.width or get_terminal_size()[1]
|
||||||
|
|
|
@ -22,7 +22,7 @@ class ndict(OrderedDict):
|
||||||
|
|
||||||
def __getattr__(self, name):
|
def __getattr__(self, name):
|
||||||
"""
|
"""
|
||||||
@action looking up an attribute
|
@while looking up an attribute
|
||||||
"""
|
"""
|
||||||
|
|
||||||
if name in self:
|
if name in self:
|
||||||
|
|
|
@ -22,7 +22,7 @@ cdef class ndict(OrderedDict):
|
||||||
|
|
||||||
def __getattr__(self, str name):
|
def __getattr__(self, str name):
|
||||||
"""
|
"""
|
||||||
@action looking up an attribute
|
@while looking up an attribute
|
||||||
"""
|
"""
|
||||||
|
|
||||||
if name in self:
|
if name in self:
|
||||||
|
|
|
@ -4,5 +4,5 @@
|
||||||
major = 0 # VERSION_MAJOR_IDENTIFIER
|
major = 0 # VERSION_MAJOR_IDENTIFIER
|
||||||
minor = 0 # VERSION_MINOR_IDENTIFIER
|
minor = 0 # VERSION_MINOR_IDENTIFIER
|
||||||
# VERSION_LAST_MM 0.0
|
# 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))
|
str = ".".join(str(v) for v in (major, minor, patch))
|
||||||
|
|
Loading…
Add table
Add a link
Reference in a new issue