New features (closes #6):
- config file selection (---config PATH) - dump state as executable command (---dump) - over library desciption (---help) - finished automatic --help view Fixes - callbacks are called after the application's state is determined (closes #9) - many small fixes
This commit is contained in:
parent
5b983a3131
commit
ed947bf8e6
5 changed files with 187 additions and 49 deletions
199
over/app.py
199
over/app.py
|
@ -40,9 +40,7 @@ class UnknownOption(Exception):
|
|||
This option was mentioned in code, the config file or on the command line but is not known.
|
||||
"""
|
||||
|
||||
@property
|
||||
def description(self):
|
||||
return "--%s" %(self.args[0])
|
||||
pass
|
||||
|
||||
class ReadingUnsetOption(Exception):
|
||||
"""
|
||||
|
@ -120,14 +118,16 @@ class Option:
|
|||
else:
|
||||
self.is_boolean = is_boolean
|
||||
|
||||
self.reset() # sets self.source and self._value_list
|
||||
|
||||
if self.default != Option_sources.none:
|
||||
self.set_value(self.default, Option_sources.default)
|
||||
self.reset(True) # sets self.source and self._value_lists
|
||||
|
||||
def reset(self):
|
||||
def reset(self, default=False):
|
||||
self.source = Option_sources.none
|
||||
self._value_list = []
|
||||
self._raw_value_list = []
|
||||
|
||||
if default:
|
||||
if self.default != Option_sources.none:
|
||||
self.set_value(self.default, Option_sources.default)
|
||||
|
||||
def set_value(self, raw_value, source):
|
||||
"""
|
||||
|
@ -157,8 +157,10 @@ class Option:
|
|||
|
||||
if self.overwrite:
|
||||
self._value_list = [value]
|
||||
self._raw_value_list = [raw_value]
|
||||
else:
|
||||
self._value_list.append(value)
|
||||
self._raw_value_list.append(raw_value)
|
||||
|
||||
@property
|
||||
def value(self):
|
||||
|
@ -255,6 +257,18 @@ def serialize_default(option):
|
|||
|
||||
# --------------------------------------------------
|
||||
|
||||
def serialize_source(source):
|
||||
if source == Option_sources.none:
|
||||
return "<R>None<.>"
|
||||
elif source == Option_sources.default:
|
||||
return "default"
|
||||
elif source == Option_sources.config_file:
|
||||
return "<y>config file<.>"
|
||||
elif source == Option_sources.command_line:
|
||||
return "<Y>command line<.>"
|
||||
|
||||
# --------------------------------------------------
|
||||
|
||||
class ConfigFile:
|
||||
"""
|
||||
Config file object. Takes a {name: Option} dictionary and a file path, and:
|
||||
|
@ -263,10 +277,10 @@ class ConfigFile:
|
|||
- missing options are appended to the file
|
||||
"""
|
||||
|
||||
def __init__(self, options, app_name, app_version):
|
||||
def __init__(self, options, app_name, app_version, force_path=None):
|
||||
self.options = options
|
||||
self.dir = get_xdg_paths(app_name)["config"]
|
||||
self.path = os.path.join(self.dir, "main.cfg")
|
||||
self.path = force_path or os.path.join(self.dir, "main.cfg")
|
||||
self.print = text.Output(app_name + ".ConfigFile")
|
||||
self.seen_hashes = set()
|
||||
|
||||
|
@ -319,7 +333,7 @@ class ConfigFile:
|
|||
# add new or otherwise missing options
|
||||
with open(self.path, "a") as f:
|
||||
for option in self.options.values():
|
||||
if option.hash not in self.seen_hashes:
|
||||
if option.hash not in self.seen_hashes and option.in_cfg_file:
|
||||
self.print("adding <W>--<G>%s<.> to config file" %(option.name))
|
||||
|
||||
f.write(docs.config_file_item %(
|
||||
|
@ -363,10 +377,13 @@ class Main:
|
|||
self.print = text.Output(name)
|
||||
self.options = OrderedDict()
|
||||
self.options_by_abbr = OrderedDict()
|
||||
self.docs = OrderedDict()
|
||||
self.cfg = OptionRouter(self.options)
|
||||
self.targets = []
|
||||
self.invocation = cmd.format_invocation(sys.argv)
|
||||
self.features = types.ndict()
|
||||
self.last_command_line = None
|
||||
self.using_alternate_config = False
|
||||
|
||||
for feature_name in self.default_features:
|
||||
if feature_name in features:
|
||||
|
@ -391,6 +408,23 @@ class Main:
|
|||
|
||||
sys.exit(rv)
|
||||
|
||||
def load_alternate_config(self, path):
|
||||
"""
|
||||
Resets the option's internal state and loads a different config file.
|
||||
|
||||
@while loading alternate config file
|
||||
"""
|
||||
|
||||
if self.using_alternate_config:
|
||||
return
|
||||
else:
|
||||
self.using_alternate_config = True
|
||||
|
||||
for option in self.options.values():
|
||||
option.reset(True)
|
||||
|
||||
self.parse(self.last_command_line, path)
|
||||
|
||||
def add_option(self, *args, **kwargs):
|
||||
"""
|
||||
@while registering a new configuration option
|
||||
|
@ -410,8 +444,31 @@ class Main:
|
|||
self.options_by_abbr[option.abbr] = option
|
||||
|
||||
def add_doc(self, chapter, paragraphs):
|
||||
# TODO
|
||||
...
|
||||
self.docs[chapter] = paragraphs
|
||||
|
||||
def dump(self):
|
||||
"""
|
||||
Prints the application's own configuration state as one long command line.
|
||||
|
||||
Skips action verbs.
|
||||
"""
|
||||
|
||||
tokens = [sys.argv[0]] # FIXME breaks encapsulation
|
||||
|
||||
for option in self.options.values():
|
||||
if option.is_boolean and option.count == 0:
|
||||
if option.value:
|
||||
tokens.append("<W>--<g>%s<.>" %(option.name))
|
||||
else:
|
||||
tokens.append("<R>--no-<g>%s<.>" %(option.name))
|
||||
elif option.count > 0:
|
||||
for raw_value in option._raw_value_list:
|
||||
tokens.append("<W>--<g>%s<.> <M>%s<.>" %(option.name, cmd.format_invocation(str(x) for x in raw_value)))
|
||||
|
||||
self.print(" ".join(tokens), format="<t>")
|
||||
|
||||
def activate_debug(self):
|
||||
raise NotImplementedError("debug is not yet here")
|
||||
|
||||
def enable_help(self, name="help", abbr="h"):
|
||||
"""
|
||||
|
@ -420,31 +477,98 @@ class Main:
|
|||
@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 library help and about views.", lambda: self.help(docs=docs.over_desc), in_cfg_file=False)
|
||||
self.add_option(name, "Display the application configuration view.", self.help, abbr=abbr, in_cfg_file=False)
|
||||
self.add_option("-help", "Display the library help and about views.", lambda: self.help(alternate_docs=docs.over_docs), in_cfg_file=False)
|
||||
self.add_option("-config", "Path to an alternate config file.", self.load_alternate_config, count=1, in_cfg_file=False)
|
||||
self.add_option("-dump", "Show a command line that exactly represents the application's state.", self.dump, in_cfg_file=False)
|
||||
self.add_option("-debug", "Enable extra debug messages (sets <c>over<.>.<c>app<.>.<c>Main<.>.<y>debug<.> = <M>True<.>) Currently does nothing.", self.activate_debug, in_cfg_file=False)
|
||||
|
||||
def help(self, option=None, docs=None):
|
||||
def help(self, alternate_docs=None):
|
||||
"""
|
||||
Displays a help text and exits the program.
|
||||
|
||||
If `option` == `docs` == None, full application help and configuration is displayed:
|
||||
Displays a help text and exits the program:
|
||||
- application name, version and license name
|
||||
- application description and other documentation supplied with `Main.add_doc`
|
||||
- a list of application options with their descriptions and current values
|
||||
- a list of currently specified targets.
|
||||
|
||||
If `option` is set to an option's name, only that option will be described.
|
||||
|
||||
If `docs` is set to something else, only those docs will be displayed.
|
||||
|
||||
@while displaying help
|
||||
"""
|
||||
|
||||
self.print("TODO called for help!")
|
||||
print = text.Output("help", format="<t>", end="\n")
|
||||
|
||||
# App name and version
|
||||
print("[<W>Application<.>]")
|
||||
print(" <W>%s<.>-<c>%s<.> licensed under the <W>%s<.>" %(self.name, self.version, self.license))
|
||||
print(" using over-%s" %(version.str))
|
||||
|
||||
# App docs
|
||||
print("")
|
||||
for chapter, paragraphs in (alternate_docs or self.docs).items():
|
||||
print("[<W>%s<.>]" %(chapter))
|
||||
|
||||
for paragraph in paragraphs:
|
||||
print(paragraph, format=" <i><t>")
|
||||
print("")
|
||||
|
||||
# App options
|
||||
if not alternate_docs:
|
||||
print("[<W>Options<.>]")
|
||||
|
||||
for option in self.options.values():
|
||||
if option.show_in_help:
|
||||
# option name and type
|
||||
full_names = ["<W>--<g>%s<.>" %(option.name)]
|
||||
abbr_names = []
|
||||
|
||||
if option.abbr:
|
||||
abbr_names.append("<W>+<G>%s<.>" %(option.abbr))
|
||||
|
||||
if option.is_boolean and option.count == 0:
|
||||
full_names.append("<R>--no-<g>%s<.>" %(option.name))
|
||||
|
||||
if option.abbr:
|
||||
abbr_names.append("<R>-<G>%s<.>" %(option.abbr))
|
||||
|
||||
line_parts = [" " + " or ".join(full_names)]
|
||||
|
||||
if abbr_names:
|
||||
line_parts.append(" (%s)" %(" or ".join(abbr_names)))
|
||||
|
||||
if not (option.count == 0 and not option.is_boolean): # don't display type on actions
|
||||
line_parts.append("; type <B>%s<.>" %(serialize_callback(option.callback)))
|
||||
|
||||
if not option.overwrite:
|
||||
line_parts.append(", <C>can be used more than once<.>")
|
||||
|
||||
print("".join(line_parts))
|
||||
|
||||
# Current value
|
||||
if option.source != Option_sources.none:
|
||||
for raw_value in option._raw_value_list:
|
||||
print(" Current value (%s): <M>%s<.>" %(
|
||||
serialize_source(option.source),
|
||||
cmd.format_invocation(str(x) for x in raw_value)
|
||||
))
|
||||
|
||||
# some misc flags
|
||||
if not option.in_cfg_file:
|
||||
print(" (<c>not in config file<.>)")
|
||||
|
||||
# the desc text
|
||||
print(option.description, format=" <i><t>")
|
||||
|
||||
print("")
|
||||
|
||||
# Current targets, if any
|
||||
if self.targets:
|
||||
print("[<W>Targets<.>]")
|
||||
print(" <M>%s<.>" %(cmd.format_invocation(self.targets)))
|
||||
|
||||
self.exit()
|
||||
|
||||
def parse_config(self):
|
||||
def parse_config(self, force_path=None):
|
||||
if self.features.config_file:
|
||||
self.config_file = ConfigFile(self.options, self.name, self.version)
|
||||
self.config_file = ConfigFile(self.options, self.name, self.version, force_path)
|
||||
else:
|
||||
self.config_file = None
|
||||
|
||||
|
@ -468,9 +592,10 @@ class Main:
|
|||
"""
|
||||
|
||||
command_line = command_line or sys.argv[1:]
|
||||
self.last_command_line = command_line
|
||||
|
||||
# placing it here ensures --help is the last option in the list of options
|
||||
if self.features.auto_add_help:
|
||||
if self.features.auto_add_help and "help" not in self.options:
|
||||
self.enable_help()
|
||||
|
||||
last_read_option = None
|
||||
|
@ -478,6 +603,7 @@ class Main:
|
|||
temporary_payload = []
|
||||
guard_passed = False
|
||||
invocation = [sys.argv[0]]
|
||||
scheduled_actions = []
|
||||
|
||||
for top_token in command_line:
|
||||
# if it's a group
|
||||
|
@ -511,9 +637,13 @@ class Main:
|
|||
|
||||
last_read_option = option
|
||||
|
||||
# single booleans are handled specially because of their --option/--no-option forms
|
||||
if option.is_boolean and option.count == 0:
|
||||
option.set_value([option_is_positive], Option_sources.command_line)
|
||||
if option.count == 0:
|
||||
if option.is_boolean:
|
||||
# single booleans are handled specially because of their --option/--no-option forms
|
||||
option.set_value([option_is_positive], Option_sources.command_line)
|
||||
else:
|
||||
# --action verbs just want their callbacks called
|
||||
scheduled_actions.append(option.callback)
|
||||
else:
|
||||
remaining_payload_tokens = option.count
|
||||
temporary_payload = []
|
||||
|
@ -531,13 +661,16 @@ class Main:
|
|||
raise IncompleteArguments(last_read_option.name, last_read_option.count, remaining_payload_tokens)
|
||||
|
||||
self.invocation = cmd.format_invocation(invocation)
|
||||
|
||||
for action in scheduled_actions:
|
||||
action()
|
||||
|
||||
def parse(self, command_line=None):
|
||||
def setup(self, command_line=None, force_config=None):
|
||||
"""
|
||||
@while assembling application configuration
|
||||
@while determining application configuration
|
||||
"""
|
||||
|
||||
self.parse_config()
|
||||
self.parse_config(force_config)
|
||||
self.parse_command_line(command_line)
|
||||
|
||||
def stack_tracer(self, exception_type, exception, trace):
|
||||
|
|
Loading…
Add table
Add a link
Reference in a new issue