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:
Martinez 2016-05-18 00:40:42 +02:00
parent 5b983a3131
commit ed947bf8e6
5 changed files with 187 additions and 49 deletions

View file

@ -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):