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
|
||||
|
||||
class InternalError(Exception):
|
||||
"""
|
||||
A possible bug in over.
|
||||
"""
|
||||
|
||||
pass
|
||||
|
||||
class IncompleteArguments(Exception):
|
||||
@property
|
||||
def description(self):
|
||||
|
@ -101,7 +108,7 @@ class Option_sources(enum.Enum):
|
|||
command_line = 3
|
||||
|
||||
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.description = description
|
||||
self.callback = callback
|
||||
|
@ -110,7 +117,7 @@ class Option:
|
|||
self.overwrite = overwrite
|
||||
self.abbr = abbr
|
||||
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()
|
||||
|
||||
if is_boolean is None:
|
||||
|
@ -165,7 +172,7 @@ class Option:
|
|||
@property
|
||||
def value(self):
|
||||
if not self._value_list:
|
||||
raise ReadingUnsetOption("option --%s has no value" %(self.name))
|
||||
return None
|
||||
else:
|
||||
if self.overwrite:
|
||||
return self._value_list[0]
|
||||
|
@ -178,15 +185,20 @@ class OptionRouter:
|
|||
def __init__(self, options):
|
||||
self.options = options
|
||||
|
||||
def __getattr__(self, name):
|
||||
def __getattr__(self, requested_name):
|
||||
"""
|
||||
@while retrieving an option's value
|
||||
"""
|
||||
|
||||
try:
|
||||
return self.options[name].value
|
||||
except KeyError:
|
||||
raise UnknownOption(name)
|
||||
matches = [name for name in self.options if name.replace("-", "_") == requested_name]
|
||||
|
||||
if matches:
|
||||
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
|
||||
"""
|
||||
|
||||
def __init__(self, options, app_name, app_version, force_path=None):
|
||||
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()
|
||||
def __init__(self, options, path):
|
||||
"""
|
||||
"""
|
||||
|
||||
self.read_config()
|
||||
self.sync_config(app_name, app_version)
|
||||
self.options = options
|
||||
self.path = path
|
||||
self.print = text.Output("over.app.ConfigFile")
|
||||
self.seen_hashes = set()
|
||||
|
||||
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
|
||||
"""
|
||||
|
||||
updated = set()
|
||||
|
||||
if os.path.exists(self.path):
|
||||
# read all options in the file
|
||||
with open(self.path) as f:
|
||||
|
@ -313,24 +328,32 @@ class ConfigFile:
|
|||
|
||||
args = shlex.split(R)
|
||||
option.set_value(args, Option_sources.config_file)
|
||||
updated.add(option)
|
||||
|
||||
def sync_config(self, app_name, app_version):
|
||||
return updated
|
||||
|
||||
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
|
||||
"""
|
||||
|
||||
# create the config dir
|
||||
if not os.path.exists(self.dir):
|
||||
self.print("created config directory <c>%s<.>" %(self.dir))
|
||||
os.mkdir(self.dir)
|
||||
config_dir = os.path.dirname(self.path)
|
||||
if config_dir.strip() and not os.path.exists(config_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 not os.path.exists(self.path):
|
||||
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))
|
||||
|
||||
# add new or otherwise missing options
|
||||
updated = set()
|
||||
|
||||
with open(self.path, "a") as f:
|
||||
for option in self.options.values():
|
||||
if option.hash not in self.seen_hashes and option.in_cfg_file:
|
||||
|
@ -344,10 +367,15 @@ class ConfigFile:
|
|||
"" if option.count == 1 else "s",
|
||||
"the last instance counts" if option.overwrite else "can be specified multiple times",
|
||||
format_description(option.description),
|
||||
"#" if new_options_commented else "",
|
||||
option.name,
|
||||
serialize_default(option)
|
||||
))
|
||||
|
||||
updated.add(option)
|
||||
|
||||
return updated
|
||||
|
||||
def __repr__(self):
|
||||
return "ConfigFile(%s)" %(self.path)
|
||||
|
||||
|
@ -515,7 +543,7 @@ class Main:
|
|||
print("[<W>Options<.>]")
|
||||
|
||||
for option in self.options.values():
|
||||
if option.show_in_help:
|
||||
if option.in_help:
|
||||
# option name and type
|
||||
full_names = ["<W>--<g>%s<.>" %(option.name)]
|
||||
abbr_names = []
|
||||
|
@ -568,7 +596,12 @@ class Main:
|
|||
|
||||
def parse_config(self, force_path=None):
|
||||
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:
|
||||
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>-<.>.",
|
||||
'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."]
|
||||
|
||||
|
||||
|
@ -51,6 +51,6 @@ config_file_item = """# --------------------------------------------------
|
|||
#
|
||||
%s
|
||||
#
|
||||
#%s = %s
|
||||
%s%s = %s
|
||||
|
||||
"""
|
||||
|
|
|
@ -288,7 +288,8 @@ class Output:
|
|||
debug = " <c>?<.>"
|
||||
start = "<W>>>><.>"
|
||||
exec = start
|
||||
warn = " <Y>#<.>"
|
||||
note = " <C>#<.>"
|
||||
warn = " <Y>!<.>"
|
||||
fail = "<R>!!!<.>"
|
||||
done = " <G>*<.>"
|
||||
class long:
|
||||
|
@ -296,6 +297,7 @@ class Output:
|
|||
debug = "<c>DEBG<.>"
|
||||
start = "<W>EXEC<.>"
|
||||
exec = start
|
||||
note = "<C>NOTE<.>"
|
||||
warn = "<Y>WARN<.>"
|
||||
fail = "<R>FAIL<.>"
|
||||
done = "<G>DONE<.>"
|
||||
|
|
|
@ -4,5 +4,5 @@
|
|||
major = 1 # VERSION_MAJOR_IDENTIFIER
|
||||
minor = 99 # VERSION_MINOR_IDENTIFIER
|
||||
# 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))
|
||||
|
|
Loading…
Add table
Add a link
Reference in a new issue