generalized over.app.ConfigFile to be usable by other apps (e.g. for over-video's context storage, this closes #8 :)
options with no value return None instead of raising an exception added a NOTE output tag to over.text.Output
This commit is contained in:
parent
ed947bf8e6
commit
d690632ca2
4 changed files with 62 additions and 27 deletions
79
over/app.py
79
over/app.py
|
@ -70,6 +70,13 @@ class OptionSyntaxError(Exception):
|
||||||
|
|
||||||
pass
|
pass
|
||||||
|
|
||||||
|
class InternalError(Exception):
|
||||||
|
"""
|
||||||
|
A possible bug in over.
|
||||||
|
"""
|
||||||
|
|
||||||
|
pass
|
||||||
|
|
||||||
class IncompleteArguments(Exception):
|
class IncompleteArguments(Exception):
|
||||||
@property
|
@property
|
||||||
def description(self):
|
def description(self):
|
||||||
|
@ -101,7 +108,7 @@ class Option_sources(enum.Enum):
|
||||||
command_line = 3
|
command_line = 3
|
||||||
|
|
||||||
class Option:
|
class Option:
|
||||||
def __init__(self, name, description, callback, default=Option_sources.none, count=0, overwrite=True, abbr=None, in_cfg_file=True, show_in_help=True, is_boolean=None):
|
def __init__(self, name, description, callback, default=Option_sources.none, count=0, overwrite=True, abbr=None, in_cfg_file=True, in_help=True, is_boolean=None):
|
||||||
self.name = name
|
self.name = name
|
||||||
self.description = description
|
self.description = description
|
||||||
self.callback = callback
|
self.callback = callback
|
||||||
|
@ -110,7 +117,7 @@ class Option:
|
||||||
self.overwrite = overwrite
|
self.overwrite = overwrite
|
||||||
self.abbr = abbr
|
self.abbr = abbr
|
||||||
self.in_cfg_file = in_cfg_file
|
self.in_cfg_file = in_cfg_file
|
||||||
self.show_in_help = show_in_help
|
self.in_help = in_help
|
||||||
self.hash = hashlib.sha1(name.encode("utf-8")).hexdigest()
|
self.hash = hashlib.sha1(name.encode("utf-8")).hexdigest()
|
||||||
|
|
||||||
if is_boolean is None:
|
if is_boolean is None:
|
||||||
|
@ -165,7 +172,7 @@ class Option:
|
||||||
@property
|
@property
|
||||||
def value(self):
|
def value(self):
|
||||||
if not self._value_list:
|
if not self._value_list:
|
||||||
raise ReadingUnsetOption("option --%s has no value" %(self.name))
|
return None
|
||||||
else:
|
else:
|
||||||
if self.overwrite:
|
if self.overwrite:
|
||||||
return self._value_list[0]
|
return self._value_list[0]
|
||||||
|
@ -178,15 +185,20 @@ class OptionRouter:
|
||||||
def __init__(self, options):
|
def __init__(self, options):
|
||||||
self.options = options
|
self.options = options
|
||||||
|
|
||||||
def __getattr__(self, name):
|
def __getattr__(self, requested_name):
|
||||||
"""
|
"""
|
||||||
@while retrieving an option's value
|
@while retrieving an option's value
|
||||||
"""
|
"""
|
||||||
|
|
||||||
try:
|
matches = [name for name in self.options if name.replace("-", "_") == requested_name]
|
||||||
return self.options[name].value
|
|
||||||
except KeyError:
|
if matches:
|
||||||
raise UnknownOption(name)
|
if len(matches) == 1:
|
||||||
|
return self.options[matches[0]].value
|
||||||
|
else:
|
||||||
|
raise InternalError("more than one option name matched")
|
||||||
|
else:
|
||||||
|
raise UnknownOption(requested_name)
|
||||||
|
|
||||||
# --------------------------------------------------
|
# --------------------------------------------------
|
||||||
|
|
||||||
|
@ -277,21 +289,24 @@ class ConfigFile:
|
||||||
- missing options are appended to the file
|
- missing options are appended to the file
|
||||||
"""
|
"""
|
||||||
|
|
||||||
def __init__(self, options, app_name, app_version, force_path=None):
|
def __init__(self, options, path):
|
||||||
self.options = options
|
"""
|
||||||
self.dir = get_xdg_paths(app_name)["config"]
|
"""
|
||||||
self.path = force_path or os.path.join(self.dir, "main.cfg")
|
|
||||||
self.print = text.Output(app_name + ".ConfigFile")
|
|
||||||
self.seen_hashes = set()
|
|
||||||
|
|
||||||
self.read_config()
|
self.options = options
|
||||||
self.sync_config(app_name, app_version)
|
self.path = path
|
||||||
|
self.print = text.Output("over.app.ConfigFile")
|
||||||
|
self.seen_hashes = set()
|
||||||
|
|
||||||
def read_config(self):
|
def read_config(self):
|
||||||
"""
|
"""
|
||||||
|
Reads the config file, updates self.options with values when available, and returns a set of updated options.
|
||||||
|
|
||||||
@while reading the config file
|
@while reading the config file
|
||||||
"""
|
"""
|
||||||
|
|
||||||
|
updated = set()
|
||||||
|
|
||||||
if os.path.exists(self.path):
|
if os.path.exists(self.path):
|
||||||
# read all options in the file
|
# read all options in the file
|
||||||
with open(self.path) as f:
|
with open(self.path) as f:
|
||||||
|
@ -313,24 +328,32 @@ class ConfigFile:
|
||||||
|
|
||||||
args = shlex.split(R)
|
args = shlex.split(R)
|
||||||
option.set_value(args, Option_sources.config_file)
|
option.set_value(args, Option_sources.config_file)
|
||||||
|
updated.add(option)
|
||||||
|
|
||||||
|
return updated
|
||||||
|
|
||||||
def sync_config(self, app_name, app_version):
|
def update_config(self, header_template, header_args, new_options_commented=True):
|
||||||
"""
|
"""
|
||||||
|
Creates the config file if necessary, adds new or missing options into it, and returns a set of newly added options.
|
||||||
|
|
||||||
@while updating the config file with new options
|
@while updating the config file with new options
|
||||||
"""
|
"""
|
||||||
|
|
||||||
# create the config dir
|
# create the config dir
|
||||||
if not os.path.exists(self.dir):
|
config_dir = os.path.dirname(self.path)
|
||||||
self.print("created config directory <c>%s<.>" %(self.dir))
|
if config_dir.strip() and not os.path.exists(config_dir):
|
||||||
os.mkdir(self.dir)
|
os.mkdir(config_dir)
|
||||||
|
self.print("created config directory <c>%s<.>" %(config_dir))
|
||||||
|
|
||||||
# if the file doesn't exist, create it with a boilerplate header
|
# if the file doesn't exist, create it with a boilerplate header
|
||||||
if not os.path.exists(self.path):
|
if not os.path.exists(self.path):
|
||||||
with open(self.path, "w") as f:
|
with open(self.path, "w") as f:
|
||||||
f.write(docs.config_file_header %(app_name, app_version, version.str))
|
f.write(header_template %header_args)
|
||||||
self.print("created empty config file <c>%s<.>" %(self.path))
|
self.print("created empty config file <c>%s<.>" %(self.path))
|
||||||
|
|
||||||
# add new or otherwise missing options
|
# add new or otherwise missing options
|
||||||
|
updated = set()
|
||||||
|
|
||||||
with open(self.path, "a") as f:
|
with open(self.path, "a") as f:
|
||||||
for option in self.options.values():
|
for option in self.options.values():
|
||||||
if option.hash not in self.seen_hashes and option.in_cfg_file:
|
if option.hash not in self.seen_hashes and option.in_cfg_file:
|
||||||
|
@ -344,9 +367,14 @@ class ConfigFile:
|
||||||
"" if option.count == 1 else "s",
|
"" if option.count == 1 else "s",
|
||||||
"the last instance counts" if option.overwrite else "can be specified multiple times",
|
"the last instance counts" if option.overwrite else "can be specified multiple times",
|
||||||
format_description(option.description),
|
format_description(option.description),
|
||||||
|
"#" if new_options_commented else "",
|
||||||
option.name,
|
option.name,
|
||||||
serialize_default(option)
|
serialize_default(option)
|
||||||
))
|
))
|
||||||
|
|
||||||
|
updated.add(option)
|
||||||
|
|
||||||
|
return updated
|
||||||
|
|
||||||
def __repr__(self):
|
def __repr__(self):
|
||||||
return "ConfigFile(%s)" %(self.path)
|
return "ConfigFile(%s)" %(self.path)
|
||||||
|
@ -515,7 +543,7 @@ class Main:
|
||||||
print("[<W>Options<.>]")
|
print("[<W>Options<.>]")
|
||||||
|
|
||||||
for option in self.options.values():
|
for option in self.options.values():
|
||||||
if option.show_in_help:
|
if option.in_help:
|
||||||
# option name and type
|
# option name and type
|
||||||
full_names = ["<W>--<g>%s<.>" %(option.name)]
|
full_names = ["<W>--<g>%s<.>" %(option.name)]
|
||||||
abbr_names = []
|
abbr_names = []
|
||||||
|
@ -568,7 +596,12 @@ class Main:
|
||||||
|
|
||||||
def parse_config(self, force_path=None):
|
def parse_config(self, force_path=None):
|
||||||
if self.features.config_file:
|
if self.features.config_file:
|
||||||
self.config_file = ConfigFile(self.options, self.name, self.version, force_path)
|
config_dir = get_xdg_paths(self.name)["config"]
|
||||||
|
config_path = force_path or os.path.join(config_dir, "main.cfg")
|
||||||
|
|
||||||
|
self.config_file = ConfigFile(self.options, config_path)
|
||||||
|
self.config_file.read_config()
|
||||||
|
self.config_file.update_config(docs.config_file_header, (self.name, self.version, version.str))
|
||||||
else:
|
else:
|
||||||
self.config_file = None
|
self.config_file = None
|
||||||
|
|
||||||
|
|
|
@ -15,7 +15,7 @@ over_docs["Options"] = [
|
||||||
"Multiple abbreviated options can be grouped together, with one condition: all except the last (rightmost) option must be flags or actions. For example <R>-<G>Exp<.> <M>primary<.> is the same as <R>--no-<g>enabled<.> <R>--no-<g>execute<.> <W>--<g>pool<.> <M>primary<.> or <W>+<G>Nv<.> <M>drop<.> expands to <W>--<g>normalize<.> <W>--<g>video<.> <M>drop<.>. Notice how the entire group takes on the boolean value of the leading <R>+<.> or <W>-<.>.",
|
"Multiple abbreviated options can be grouped together, with one condition: all except the last (rightmost) option must be flags or actions. For example <R>-<G>Exp<.> <M>primary<.> is the same as <R>--no-<g>enabled<.> <R>--no-<g>execute<.> <W>--<g>pool<.> <M>primary<.> or <W>+<G>Nv<.> <M>drop<.> expands to <W>--<g>normalize<.> <W>--<g>video<.> <M>drop<.>. Notice how the entire group takes on the boolean value of the leading <R>+<.> or <W>-<.>.",
|
||||||
'If an option is overwriting (<c>over<.>.<c>app<.>.<c>Option<.>.<y>overwrite<.> = <M>True<.>) the latest (rightmost) instance of the same option overwrites all previous ones. Otherwise, all instances are used. For example, <W>+<G>v<.> <M>drop<.> <W>+<G>v<.> <M>copy<.> <W>+<G>v<.> <M>x264<.> evaluates to <M>["x264"]<.> if the option is defined as overwriting, whereas if it was not it would evaluate to <M>["drop", "copy", "x264"]<.>.'
|
'If an option is overwriting (<c>over<.>.<c>app<.>.<c>Option<.>.<y>overwrite<.> = <M>True<.>) the latest (rightmost) instance of the same option overwrites all previous ones. Otherwise, all instances are used. For example, <W>+<G>v<.> <M>drop<.> <W>+<G>v<.> <M>copy<.> <W>+<G>v<.> <M>x264<.> evaluates to <M>["x264"]<.> if the option is defined as overwriting, whereas if it was not it would evaluate to <M>["drop", "copy", "x264"]<.>.'
|
||||||
]
|
]
|
||||||
over_docs["Configuration sources"] = ["Each option can take its state either from (in order) the application default value, the config file, or the command line. Combining multiple sources (e.g. extending values in the config file with the command line) is not permitted - a new source always resets the option's state. It is possible for an option to have no value. Accessing it during runtime will generate an exception."]
|
over_docs["Configuration sources"] = ["Each option can take its state either from (in order) the application default value, the config file, or the command line. Combining multiple sources (e.g. extending values in the config file with the command line) is not permitted - a new source always resets the option's state. It is possible for an option to have no value."]
|
||||||
over_docs["Config File"] = ["If enabled by the application, a config file will be generated when first executed. The config file will be populated with all known options, their descriptions and some instructions on how to proceed. If a newer version of the application that offers more configurable options is executed, the config file will be automatically updated."]
|
over_docs["Config File"] = ["If enabled by the application, a config file will be generated when first executed. The config file will be populated with all known options, their descriptions and some instructions on how to proceed. If a newer version of the application that offers more configurable options is executed, the config file will be automatically updated."]
|
||||||
|
|
||||||
|
|
||||||
|
@ -51,6 +51,6 @@ config_file_item = """# --------------------------------------------------
|
||||||
#
|
#
|
||||||
%s
|
%s
|
||||||
#
|
#
|
||||||
#%s = %s
|
%s%s = %s
|
||||||
|
|
||||||
"""
|
"""
|
||||||
|
|
|
@ -288,7 +288,8 @@ class Output:
|
||||||
debug = " <c>?<.>"
|
debug = " <c>?<.>"
|
||||||
start = "<W>>>><.>"
|
start = "<W>>>><.>"
|
||||||
exec = start
|
exec = start
|
||||||
warn = " <Y>#<.>"
|
note = " <C>#<.>"
|
||||||
|
warn = " <Y>!<.>"
|
||||||
fail = "<R>!!!<.>"
|
fail = "<R>!!!<.>"
|
||||||
done = " <G>*<.>"
|
done = " <G>*<.>"
|
||||||
class long:
|
class long:
|
||||||
|
@ -296,6 +297,7 @@ class Output:
|
||||||
debug = "<c>DEBG<.>"
|
debug = "<c>DEBG<.>"
|
||||||
start = "<W>EXEC<.>"
|
start = "<W>EXEC<.>"
|
||||||
exec = start
|
exec = start
|
||||||
|
note = "<C>NOTE<.>"
|
||||||
warn = "<Y>WARN<.>"
|
warn = "<Y>WARN<.>"
|
||||||
fail = "<R>FAIL<.>"
|
fail = "<R>FAIL<.>"
|
||||||
done = "<G>DONE<.>"
|
done = "<G>DONE<.>"
|
||||||
|
|
|
@ -4,5 +4,5 @@
|
||||||
major = 1 # VERSION_MAJOR_IDENTIFIER
|
major = 1 # VERSION_MAJOR_IDENTIFIER
|
||||||
minor = 99 # VERSION_MINOR_IDENTIFIER
|
minor = 99 # VERSION_MINOR_IDENTIFIER
|
||||||
# VERSION_LAST_MM 1.99
|
# VERSION_LAST_MM 1.99
|
||||||
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