- 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 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 <W>%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 = "<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
|
||||
if method and method.__doc__ and "@action" in method.__doc__:
|
||||
action = "while <m>%s<.> " %(re.findall("@action (.+)", method.__doc__)[0].strip())
|
||||
if method and method.__doc__ and "@while" in method.__doc__:
|
||||
action = "while <m>%s<.> " %(re.findall("@while (.+)", method.__doc__)[0].strip())
|
||||
else:
|
||||
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))
|
||||
|
||||
reason = ": <R>%s<.>" %(" ".join(exception.args)) if exception.args else ""
|
||||
tb_lines.append("exception <Y>%s<.> was raised%s (%s)" %(exception_type.__name__, reason, exception.__doc__.strip()))
|
||||
if hasattr(exception, "description"):
|
||||
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
|
||||
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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
|
||||
|
|
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.
|
||||
<.> resets the color. Use <<x> for a literal <x>.
|
||||
|
||||
@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]
|
||||
|
|
|
@ -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:
|
||||
|
|
|
@ -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:
|
||||
|
|
|
@ -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))
|
||||
|
|
Loading…
Add table
Add a link
Reference in a new issue