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:
Martinez 2016-05-18 13:09:25 +02:00
parent ed947bf8e6
commit d690632ca2
4 changed files with 62 additions and 27 deletions

View file

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

View file

@ -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
"""

View file

@ -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<.>"

View file

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