config file support is ready

- over.app.ConfigFile can read, create and amend config files
- fix over.cmd.format_invocation
- add join to over.text.paragraph
This commit is contained in:
Martinez 2016-05-17 20:58:53 +02:00
parent 17eda81ad2
commit 5b983a3131
6 changed files with 81 additions and 19 deletions

View file

@ -9,6 +9,7 @@ import sys
import re import re
import os import os
import shlex import shlex
import hashlib
try: try:
import xdg.BaseDirectory as xdg_bd import xdg.BaseDirectory as xdg_bd
@ -112,6 +113,7 @@ class Option:
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.show_in_help = show_in_help
self.hash = hashlib.sha1(name.encode("utf-8")).hexdigest()
if is_boolean is None: if is_boolean is None:
self.is_boolean = callback in (bool, callback_module.boolean, callback_module.booleans) self.is_boolean = callback in (bool, callback_module.boolean, callback_module.booleans)
@ -231,6 +233,28 @@ def get_xdg_paths(appname):
# -------------------------------------------------- # --------------------------------------------------
def serialize_callback(f):
return "%s.%s" %(f.__module__, f.__name__)
# --------------------------------------------------
def format_description(description):
colorless = text.render(description, colors=False)
lines = text.paragraph(colorless, 55, 2, join=False)
# comment them out and assemble
return "\n".join("#" + line[1:] for line in lines)
# --------------------------------------------------
def serialize_default(option):
if option.default is Option_sources.none:
return ""
else:
return cmd.format_invocation(str(x) for x in option.default)
# --------------------------------------------------
class ConfigFile: class ConfigFile:
""" """
Config file object. Takes a {name: Option} dictionary and a file path, and: Config file object. Takes a {name: Option} dictionary and a file path, and:
@ -244,6 +268,7 @@ class ConfigFile:
self.dir = get_xdg_paths(app_name)["config"] self.dir = get_xdg_paths(app_name)["config"]
self.path = os.path.join(self.dir, "main.cfg") self.path = os.path.join(self.dir, "main.cfg")
self.print = text.Output(app_name + ".ConfigFile") self.print = text.Output(app_name + ".ConfigFile")
self.seen_hashes = set()
self.read_config() self.read_config()
self.sync_config(app_name, app_version) self.sync_config(app_name, app_version)
@ -259,20 +284,21 @@ class ConfigFile:
for line in f: for line in f:
line = line.strip() line = line.strip()
if line and line[0] != "#": # ignore comments and empty lines if line:
L, R = (t.strip() for t in line.split("=")) if line[0] == "#":
m = re.findall("# ---- ([0-9a-f]{40}) ----", line)
try: if m:
option = self.options[L] self.seen_hashes.add(m[0])
except KeyError:
raise UnknownOption(L)
if option.count > 1:
args = shlex.split(R)
else: else:
args = [R] L, R = (t.strip() for t in line.split("="))
option.set_value(args, Option_sources.config_file) try:
option = self.options[L]
except KeyError:
raise UnknownOption(L)
args = shlex.split(R)
option.set_value(args, Option_sources.config_file)
def sync_config(self, app_name, app_version): def sync_config(self, app_name, app_version):
""" """
@ -292,7 +318,21 @@ class ConfigFile:
# add new or otherwise missing options # add new or otherwise missing options
with open(self.path, "a") as f: with open(self.path, "a") as f:
... for option in self.options.values():
if option.hash not in self.seen_hashes:
self.print("adding <W>--<G>%s<.> to config file" %(option.name))
f.write(docs.config_file_item %(
option.hash,
("--{0} or --no-{0}" if (option.is_boolean and option.count == 0) else "--{0}").format(option.name),
serialize_callback(option.callback),
option.count or "no",
"" if option.count == 1 else "s",
"the last instance counts" if option.overwrite else "can be specified multiple times",
format_description(option.description),
option.name,
serialize_default(option)
))
def __repr__(self): def __repr__(self):
return "ConfigFile(%s)" %(self.path) return "ConfigFile(%s)" %(self.path)

View file

@ -19,6 +19,8 @@ def capture_output(stream, fifo):
stream.close() stream.close()
# --------------------------------------------------
def char_in_str(chars, string): def char_in_str(chars, string):
for char in chars: for char in chars:
if char in string: if char in string:
@ -26,8 +28,14 @@ def char_in_str(chars, string):
return False return False
# --------------------------------------------------
def format_invocation(args): def format_invocation(args):
return " ".join(('"%s"' %(a) if char_in_str(" $()[];\\", a) else a) for a in args) escaped = (arg.replace('"', '\\"') for arg in args)
return " ".join(('"%s"' %(a) if char_in_str(' $()[];\\"', a) else a) for a in escaped)
# --------------------------------------------------
class Command: class Command:
""" """

View file

@ -31,3 +31,16 @@ config_file_header = """# Configuration file for %s-%s
# TODO :) # TODO :)
""" """
config_file_item = """# --------------------------------------------------
# ---- %s ----
# Option %s
# callback: %s
# takes %s argument%s
# %s
#
%s
#
#%s = %s
"""

View file

@ -193,12 +193,13 @@ def rfind(text, C, start, length):
# return the original index at the same position as the last in colorless # return the original index at the same position as the last in colorless
return start + (indices[len(indices_colorless) - 1] if indices_colorless else 0) return start + (indices[len(indices_colorless) - 1] if indices_colorless else 0)
def paragraph(text, width=0, indent=0, stamp=None): def paragraph(text, width=0, indent=0, stamp=None, join=True):
""" """
str text text to format str text text to format
int width required line length; if 0, current terminal width will be used int width required line length; if 0, current terminal width will be used
int indent how many spaces to indent the text with int indent how many spaces to indent the text with
str stamp placed into the first line's indent (whether it fits is your problem) str stamp placed into the first line's indent (whether it fits is your problem)
bool join join lines into one string, otherwise return a list of individual lines
Formats text into an indented paragraph that fits the terminal and returns it. Formats text into an indented paragraph that fits the terminal and returns it.
Correctly handles colors. Correctly handles colors.
@ -236,7 +237,7 @@ def paragraph(text, width=0, indent=0, stamp=None):
lines[i] = indent_str + line lines[i] = indent_str + line
# join # join
return "\n".join(lines) return "\n".join(lines) if join else lines
# -------------------------------------------------- # --------------------------------------------------

View file

@ -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 = 1 # VERSION_PATCH_IDENTIFIER patch = 2 # VERSION_PATCH_IDENTIFIER
str = ".".join(str(v) for v in (major, minor, patch)) str = ".".join(str(v) for v in (major, minor, patch))

View file

@ -30,7 +30,7 @@ def int4(*args):
if __name__ == "__main__": if __name__ == "__main__":
main = over.app.Main("new-over-test", version.str, "LICENSE", features={"config_file": True}) main = over.app.Main("new-over-test", version.str, "LICENSE", features={"config_file": True})
# name, description, callback, default=Option_sources.none, count=0, overwrite=True, abbr=None, in_cfg_file=True, show_in_help=True # name, description, callback, default=Option_sources.none, count=0, overwrite=True, abbr=None, in_cfg_file=True, show_in_help=True
main.add_option("boolean-single", "", over.callback.boolean, [False], abbr="1") main.add_option("boolean-single", "ISO 8601 date for a new transfer (valid with +EHMU), defaults to current date. It's also used for relative times with --analysis-timeframe. You can use a day-count relative to today here.", over.callback.boolean, [False], abbr="1")
main.add_option("boolean-triple", "", over.callback.booleans, [False, False, False], abbr="3", count=3) main.add_option("boolean-triple", "", over.callback.booleans, [False, False, False], abbr="3", count=3)
main.add_option("str-single", "", str, ["kek"], abbr="s", count=1) main.add_option("str-single", "", str, ["kek"], abbr="s", count=1)
main.add_option("str-quad", "", noop, ["ze", "kek", "is", "bek"], abbr="4", count=4) main.add_option("str-quad", "", noop, ["ze", "kek", "is", "bek"], abbr="4", count=4)