diff --git a/over/app.py b/over/app.py index 43a0fef..50c922f 100644 --- a/over/app.py +++ b/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) + + 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 """ # create the config dir - if not os.path.exists(self.dir): - self.print("created config directory %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 %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 %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,9 +367,14 @@ 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("[Options<.>]") for option in self.options.values(): - if option.show_in_help: + if option.in_help: # option name and type full_names = ["--%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 diff --git a/over/docs.py b/over/docs.py index 8caacb5..8d8112a 100644 --- a/over/docs.py +++ b/over/docs.py @@ -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 -Exp<.> primary<.> is the same as --no-enabled<.> --no-execute<.> --pool<.> primary<.> or +Nv<.> drop<.> expands to --normalize<.> --video<.> drop<.>. Notice how the entire group takes on the boolean value of the leading +<.> or -<.>.", 'If an option is overwriting (over<.>.app<.>.Option<.>.overwrite<.> = True<.>) the latest (rightmost) instance of the same option overwrites all previous ones. Otherwise, all instances are used. For example, +v<.> drop<.> +v<.> copy<.> +v<.> x264<.> evaluates to ["x264"]<.> if the option is defined as overwriting, whereas if it was not it would evaluate to ["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 """ diff --git a/over/text.py b/over/text.py index 8a5ee45..3560c7c 100644 --- a/over/text.py +++ b/over/text.py @@ -288,7 +288,8 @@ class Output: debug = " ?<.>" start = ">>><.>" exec = start - warn = " #<.>" + note = " #<.>" + warn = " !<.>" fail = "!!!<.>" done = " *<.>" class long: @@ -296,6 +297,7 @@ class Output: debug = "DEBG<.>" start = "EXEC<.>" exec = start + note = "NOTE<.>" warn = "WARN<.>" fail = "FAIL<.>" done = "DONE<.>" diff --git a/over/version.py b/over/version.py index 04db4c4..12b2768 100644 --- a/over/version.py +++ b/over/version.py @@ -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))