From c92c0fb6e90c470f1084e7f06e53e79c0105d4f0 Mon Sep 17 00:00:00 2001 From: Martinez Date: Tue, 10 Oct 2017 00:44:03 +0200 Subject: [PATCH 01/26] update .gitignore --- .gitignore | 4 ++-- over/version.py | 4 ++-- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/.gitignore b/.gitignore index 07d2832..2133047 100644 --- a/.gitignore +++ b/.gitignore @@ -1,3 +1,3 @@ -*.so -*.pyc __pycache__ +/MANIFEST +/dist diff --git a/over/version.py b/over/version.py index 2e15d15..e1d009c 100644 --- a/over/version.py +++ b/over/version.py @@ -4,5 +4,5 @@ major = 2 # VERSION_MAJOR_IDENTIFIER minor = 0 # VERSION_MINOR_IDENTIFIER # VERSION_LAST_MM 2.0 -patch = 0 # VERSION_PATCH_IDENTIFIER -str = "2.0.0" # VERSION_STRING_IDENTIFIER +patch = 1 # VERSION_PATCH_IDENTIFIER +str = "2.0.1" # VERSION_STRING_IDENTIFIER From f8b9f983ad02d0af1fa7979d700cf7dbd3dbe3b1 Mon Sep 17 00:00:00 2001 From: Martin Date: Fri, 13 Oct 2017 14:28:15 +0200 Subject: [PATCH 02/26] improved over.types.ndict.__repr__ --- over/misc.py | 12 ++++++------ over/types.py | 4 +++- 2 files changed, 9 insertions(+), 7 deletions(-) diff --git a/over/misc.py b/over/misc.py index a953cc2..803d34a 100644 --- a/over/misc.py +++ b/over/misc.py @@ -114,7 +114,7 @@ def hexdump(data, indent=0, offset=16, show_header=True, show_offsets=True, show """ Writes a hex dump of `data` to `output`. - The output text is indented with spaces and contains `offset` bytes per line. + The output text is indented with spaces and contains `width` bytes per line. If `show_header` is True, a single line with byte numbers preceeds all output. If `show_offsets` is True, each line is prefixed with the address of the first byte of that line. If `show_ascii` is True, each line is suffixed with its ASCII representation. Unprintable characters @@ -145,11 +145,11 @@ def hexdump(data, indent=0, offset=16, show_header=True, show_offsets=True, show if show_offsets: line.append("offset"[:offset_figures].ljust(offset_figures + 2)) - for i in range(offset): + for i in range(width): line.append("%2x" %(i)) if show_ascii: - line.append(" *{0}*".format("ASCII".center(offset, "-"))) + line.append(" *{0}*".format("ASCII".center(width, "-"))) output_io.write(" ".join(line) + "\n") @@ -163,7 +163,7 @@ def hexdump(data, indent=0, offset=16, show_header=True, show_offsets=True, show hex_bytes = [] ascii_bytes = [] - for local_i, i in enumerate(range(ptr, ptr+offset)): + for local_i, i in enumerate(range(ptr, ptr+width)): if i < len(data): c = data[i] hex_bytes.append("%02x" %(c)) @@ -173,7 +173,7 @@ def hexdump(data, indent=0, offset=16, show_header=True, show_offsets=True, show else: ascii_bytes.append(".") elif i == len(data): - hex_bytes.extend([" "] * (offset - local_i)) + hex_bytes.extend([" "] * (width - local_i)) if use_colors: output_io.write(text.render("")) output_io.write(" ".join(hex_bytes)) @@ -186,7 +186,7 @@ def hexdump(data, indent=0, offset=16, show_header=True, show_offsets=True, show output_io.write("\n") - ptr += offset + ptr += width if not output: output_io.seek(0) diff --git a/over/types.py b/over/types.py index db19466..19c18d9 100644 --- a/over/types.py +++ b/over/types.py @@ -26,7 +26,9 @@ class ndict: object.__setattr__(self, "d", OrderedDict(*args, **kwargs)) def __repr__(self): - return "|" + repr(self.d) + atoms = ["(%s, %s)" %(repr(k), repr(v)) for k, v in self.items()] + + return "ndict([" + ", ".join(atoms) + "])" def __iter__(self): return self.d.__iter__() From 1ed1087a1acf3985f4cc5e88d4c891826ec754c9 Mon Sep 17 00:00:00 2001 From: Martin Date: Fri, 13 Oct 2017 18:23:08 +0200 Subject: [PATCH 03/26] fix missing over.types.ndict.keys --- over/types.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/over/types.py b/over/types.py index 19c18d9..a364726 100644 --- a/over/types.py +++ b/over/types.py @@ -20,7 +20,7 @@ class ndict: {"alpha": 1, "beta": 42} """ - __methods__ = ["values", "items"] + __methods__ = ["values", "items", "keys"] def __init__(self, *args, **kwargs): object.__setattr__(self, "d", OrderedDict(*args, **kwargs)) From 37f36ef3012e86b075315f4e3f3a938da7062464 Mon Sep 17 00:00:00 2001 From: Martinez Date: Sun, 15 Oct 2017 10:00:39 +0200 Subject: [PATCH 04/26] minor string fix --- over/app.py | 2 +- over/version.py | 4 ++-- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/over/app.py b/over/app.py index 65fab46..6c360f0 100644 --- a/over/app.py +++ b/over/app.py @@ -542,7 +542,7 @@ class Main: # App name and version print("[Application<.>]") - print(" %s<.>-%s<.> licensed under the %s<.>" %(self.name, self.version, self.license)) + print(" %s<.>-%s<.> licensed under %s<.>" %(self.name, self.version, self.license)) print(" using over-%s" %(version.str)) # Main features diff --git a/over/version.py b/over/version.py index e1d009c..ea54197 100644 --- a/over/version.py +++ b/over/version.py @@ -4,5 +4,5 @@ major = 2 # VERSION_MAJOR_IDENTIFIER minor = 0 # VERSION_MINOR_IDENTIFIER # VERSION_LAST_MM 2.0 -patch = 1 # VERSION_PATCH_IDENTIFIER -str = "2.0.1" # VERSION_STRING_IDENTIFIER +patch = 9 # VERSION_PATCH_IDENTIFIER +str = "2.0.9" # VERSION_STRING_IDENTIFIER From cfc21979435c37885afdbe14f270bd3b01431d37 Mon Sep 17 00:00:00 2001 From: Martinez Date: Sun, 15 Oct 2017 10:17:01 +0200 Subject: [PATCH 05/26] more minor doc fixes --- over/docs.py | 6 +++--- over/version.py | 4 ++-- 2 files changed, 5 insertions(+), 5 deletions(-) diff --git a/over/docs.py b/over/docs.py index 982cff4..a03c629 100644 --- a/over/docs.py +++ b/over/docs.py @@ -13,7 +13,7 @@ over_docs["Options"] = [ "Boolean options can accept 0 words - then they become flags (switches) and are toggled directly by their names: --enabled<.> evaluates to True<.>, --no-enabled<.> to False<.>.", "Other flags (options that accept 0 words) directly trigger callbacks. Those are usually referred to as actions. A good example is --help<.>.", "Options can be referred to by their abbreviated, single character names, if defined by the application. Abbreviated names are interchangeable with full names. Boolean flags get shortened from --enabled<.> to +E<.>, and --no-enabled<.> to -E<.>.", - "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 -<.>.", + "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."] @@ -22,14 +22,14 @@ over_docs["Config File"] = ["If enabled by the application, a config file will b colors = [] for code, (_, name) in ansi_colors.items(): - c = "<<%s><.>: <%s>%s<.>" %(code, code, name) + c = "<<%s> = <%s>%s<.>" %(code, code, name) if code == "k": c += ' (this one says "black")' colors.append(c) -over_docs["Available Colors"] = colors +over_docs["Available Colors"] = [", ".join(colors)] config_file_header = """# Configuration file for %s-%s # Generated by over-%s diff --git a/over/version.py b/over/version.py index ea54197..4961fa8 100644 --- a/over/version.py +++ b/over/version.py @@ -4,5 +4,5 @@ major = 2 # VERSION_MAJOR_IDENTIFIER minor = 0 # VERSION_MINOR_IDENTIFIER # VERSION_LAST_MM 2.0 -patch = 9 # VERSION_PATCH_IDENTIFIER -str = "2.0.9" # VERSION_STRING_IDENTIFIER +patch = 10 # VERSION_PATCH_IDENTIFIER +str = "2.0.10" # VERSION_STRING_IDENTIFIER From cc7d97c81191c4b610bbaf18f5817a52afb07d25 Mon Sep 17 00:00:00 2001 From: Martin Date: Mon, 16 Oct 2017 10:49:11 +0200 Subject: [PATCH 06/26] chasing own tail --- over/misc.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/over/misc.py b/over/misc.py index 803d34a..af8e526 100644 --- a/over/misc.py +++ b/over/misc.py @@ -110,7 +110,7 @@ def debugger(): # -------------------------------------------------- -def hexdump(data, indent=0, offset=16, show_header=True, show_offsets=True, show_ascii=True, use_colors=True, initial_offset=0, output=sys.stdout): +def hexdump(data, indent=0, width=16, show_header=True, show_offsets=True, show_ascii=True, use_colors=True, initial_offset=0, output=sys.stdout): """ Writes a hex dump of `data` to `output`. From 5ed3e16d65248ce15502b1fa402f17f1cb9bf8e9 Mon Sep 17 00:00:00 2001 From: Martin Date: Thu, 23 Nov 2017 16:01:21 +0100 Subject: [PATCH 07/26] add over.callback.integers --- over/callback.py | 12 ++++++++++++ 1 file changed, 12 insertions(+) diff --git a/over/callback.py b/over/callback.py index 591af05..2abbb88 100644 --- a/over/callback.py +++ b/over/callback.py @@ -99,3 +99,15 @@ def integer(arg): return int(arg, 8) return int(arg) + +def integers(*args): + """ + @while converting arguments to ints + """ + + out = [] + + for arg in args: + out.append(integer(arg)) + + return out From f31e2137ac86deada840c98fbd6c63a7aa4d8394 Mon Sep 17 00:00:00 2001 From: Martin Sekera Date: Wed, 12 Dec 2018 22:40:28 +0100 Subject: [PATCH 08/26] add over.app.Main.setup arg reset --- over/app.py | 6 +++++- over/version.py | 8 ++++---- 2 files changed, 9 insertions(+), 5 deletions(-) diff --git a/over/app.py b/over/app.py index 6c360f0..64d7e1c 100644 --- a/over/app.py +++ b/over/app.py @@ -726,11 +726,15 @@ class Main: for action in scheduled_actions: action() - def setup(self, command_line=None, force_config=None): + def setup(self, command_line=None, force_config=None, reset=False): """ @while determining application configuration """ + if reset: + for option in self.options.values(): + option.reset(True) + self.parse_config(force_config) self.parse_command_line(command_line) diff --git a/over/version.py b/over/version.py index 4961fa8..e60543e 100644 --- a/over/version.py +++ b/over/version.py @@ -2,7 +2,7 @@ # encoding: utf-8 major = 2 # VERSION_MAJOR_IDENTIFIER -minor = 0 # VERSION_MINOR_IDENTIFIER -# VERSION_LAST_MM 2.0 -patch = 10 # VERSION_PATCH_IDENTIFIER -str = "2.0.10" # VERSION_STRING_IDENTIFIER +minor = 1 # VERSION_MINOR_IDENTIFIER +# VERSION_LAST_MM 2.1 +patch = 0 # VERSION_PATCH_IDENTIFIER +str = "2.1.0" # VERSION_STRING_IDENTIFIER From 430ed6e34d84604142fa896b194a2422df104777 Mon Sep 17 00:00:00 2001 From: Martin Sekera Date: Sun, 31 Mar 2019 00:13:55 +0100 Subject: [PATCH 09/26] hexdump: skip lines if the content is the same --- over/misc.py | 40 +++++++++++++++++++++++++++++++--------- over/version.py | 4 ++-- 2 files changed, 33 insertions(+), 11 deletions(-) diff --git a/over/misc.py b/over/misc.py index af8e526..1273090 100644 --- a/over/misc.py +++ b/over/misc.py @@ -110,7 +110,7 @@ def debugger(): # -------------------------------------------------- -def hexdump(data, indent=0, width=16, show_header=True, show_offsets=True, show_ascii=True, use_colors=True, initial_offset=0, output=sys.stdout): +def hexdump(data, indent=0, width=16, show_header=True, show_offsets=True, show_ascii=True, use_colors=True, initial_offset=0, output=sys.stdout, skip_same=True): """ Writes a hex dump of `data` to `output`. @@ -120,6 +120,7 @@ def hexdump(data, indent=0, width=16, show_header=True, show_offsets=True, show_ If `show_ascii` is True, each line is suffixed with its ASCII representation. Unprintable characters are replaced with a dot. The `output` must implement a .write(str x) method. If `output` is None, the string is returned instead. + If `skip_same` is True, lines with identical content are abbreviated (omitted). @while creating a hex dump """ @@ -153,13 +154,10 @@ def hexdump(data, indent=0, width=16, show_header=True, show_offsets=True, show_ output_io.write(" ".join(line) + "\n") + skip_cnt = 0 + last_hex_bytes = None + while data[ptr:]: - if indent: - output_io.write(" " * indent) - - if show_offsets: - output_io.write(format_str %(initial_offset + ptr)) - hex_bytes = [] ascii_bytes = [] @@ -175,6 +173,29 @@ def hexdump(data, indent=0, width=16, show_header=True, show_offsets=True, show_ elif i == len(data): hex_bytes.extend([" "] * (width - local_i)) + ptr += width + + # the rest is just rendering + if skip_same: + if hex_bytes == last_hex_bytes: + skip_cnt += 1 + else: + if skip_cnt: + output_io.write("(skipped %d lines, %d B)\n" %(skip_cnt, skip_cnt * width)) + + skip_cnt = 0 + + last_hex_bytes = hex_bytes + + if skip_cnt: + continue + + if indent: + output_io.write(" " * indent) + + if show_offsets: + output_io.write(format_str %(initial_offset + ptr)) + if use_colors: output_io.write(text.render("")) output_io.write(" ".join(hex_bytes)) if use_colors: output_io.write(text.render("<.>")) @@ -185,8 +206,9 @@ def hexdump(data, indent=0, width=16, show_header=True, show_offsets=True, show_ output_io.write(text.render("<.>|") if use_colors else "|") output_io.write("\n") - - ptr += width + + if skip_cnt: + output_io.write("(skipped %d lines, %d B)\n" %(skip_cnt, skip_cnt * width)) if not output: output_io.seek(0) diff --git a/over/version.py b/over/version.py index e60543e..c9378ec 100644 --- a/over/version.py +++ b/over/version.py @@ -4,5 +4,5 @@ major = 2 # VERSION_MAJOR_IDENTIFIER minor = 1 # VERSION_MINOR_IDENTIFIER # VERSION_LAST_MM 2.1 -patch = 0 # VERSION_PATCH_IDENTIFIER -str = "2.1.0" # VERSION_STRING_IDENTIFIER +patch = 1 # VERSION_PATCH_IDENTIFIER +str = "2.1.1" # VERSION_STRING_IDENTIFIER From 1d5090b33e195dac9b62af7a6bf3d37903cdf6d8 Mon Sep 17 00:00:00 2001 From: Martin Sekera Date: Thu, 18 Apr 2019 01:57:58 +0200 Subject: [PATCH 10/26] generalized over.callbacks.directory -> over.callbacks.path --- over/callback.py | 37 +++++++++++++++++++++++++------------ over/version.py | 4 ++-- 2 files changed, 27 insertions(+), 14 deletions(-) diff --git a/over/callback.py b/over/callback.py index 591af05..43c0146 100644 --- a/over/callback.py +++ b/over/callback.py @@ -53,28 +53,41 @@ def strings(*args): return out -def directory(exists=False, writable=False, gio=False): +def path(exists=False, writable=False, validators=[]): """ - Returns a directory callback that raises hell if: - - the supplied directory doesn't exist and `exists` is True - - isn't writable and `writable` is True - - isn't a valid Gio path and `gio` is True + Returns a path callback that takes a path (str) and verifies if it: + - exists iff `exists` is True + - is writable iff `writable` is True + - goes through each (function, message) pair in `validators`, in order - @while generating a callback + A validator is a function that takes a str argument and returns True or False. + If False is returned, the matching message is raised along with a RuntimeError. + The message may contain %s to be replaced by the supplied path. Example: + + validators=[ + (os.path.isdir, "%s is not a directory"), + (os.path.isabs, "%s is a directory, but is not an absolute path.") + ] + + @while generating a path callback """ - if gio: - raise NotImplementedError("Gio support is not yet here") - def cb(arg): path = os.path.abspath(os.path.expanduser(arg)) - if not os.path.isdir(path) and exists: - raise FileNotFoundError("%s (%s) does not exist" %(arg, path)) + if exists and not os.path.exists(path): + raise FileNotFoundError("%s does not exist" %(arg)) - if writable and not os.access(path, os.W_OK | os.X_OK): + if writable and not os.access(path, os.W_OK): raise PermissionError("%s exists but is not writable" %(arg)) + if validators: + for fn, msg_tpl in validators: + if not fn(path): + msg = msg_tpl %(arg) if "%s" in msg_tpl else msg_tpl + + raise RuntimeError(msg) + return path return cb diff --git a/over/version.py b/over/version.py index c9378ec..26e8c61 100644 --- a/over/version.py +++ b/over/version.py @@ -4,5 +4,5 @@ major = 2 # VERSION_MAJOR_IDENTIFIER minor = 1 # VERSION_MINOR_IDENTIFIER # VERSION_LAST_MM 2.1 -patch = 1 # VERSION_PATCH_IDENTIFIER -str = "2.1.1" # VERSION_STRING_IDENTIFIER +patch = 2 # VERSION_PATCH_IDENTIFIER +str = "2.1.2" # VERSION_STRING_IDENTIFIER From cf9fda165fa5e2734db201edf88333bf9a765e62 Mon Sep 17 00:00:00 2001 From: Martin Sekera Date: Thu, 18 Apr 2019 02:08:20 +0200 Subject: [PATCH 11/26] changed the over.callback.path API a bit --- over/callback.py | 13 +++++++++---- over/version.py | 4 ++-- 2 files changed, 11 insertions(+), 6 deletions(-) diff --git a/over/callback.py b/over/callback.py index 43c0146..252dd8b 100644 --- a/over/callback.py +++ b/over/callback.py @@ -53,11 +53,11 @@ def strings(*args): return out -def path(exists=False, writable=False, validators=[]): +def path(exists=False, permissions=None, validators=None): """ Returns a path callback that takes a path (str) and verifies if it: - exists iff `exists` is True - - is writable iff `writable` is True + - its permissions match those in `permissions` ("rwx" or any subset, e.g. "r--", "-w-", dashes are optional) - goes through each (function, message) pair in `validators`, in order A validator is a function that takes a str argument and returns True or False. @@ -78,8 +78,13 @@ def path(exists=False, writable=False, validators=[]): if exists and not os.path.exists(path): raise FileNotFoundError("%s does not exist" %(arg)) - if writable and not os.access(path, os.W_OK): - raise PermissionError("%s exists but is not writable" %(arg)) + if permissions: + if "r" in permissions and not os.access(path, os.R_OK): + raise PermissionError("%s is not readable" %(arg)) + if "w" in permissions and not os.access(path, os.W_OK): + raise PermissionError("%s is not writable" %(arg)) + if "x" in permissions and not os.access(path, os.X_OK): + raise PermissionError("%s is not executable" %(arg)) if validators: for fn, msg_tpl in validators: diff --git a/over/version.py b/over/version.py index 26e8c61..14cbadc 100644 --- a/over/version.py +++ b/over/version.py @@ -4,5 +4,5 @@ major = 2 # VERSION_MAJOR_IDENTIFIER minor = 1 # VERSION_MINOR_IDENTIFIER # VERSION_LAST_MM 2.1 -patch = 2 # VERSION_PATCH_IDENTIFIER -str = "2.1.2" # VERSION_STRING_IDENTIFIER +patch = 3 # VERSION_PATCH_IDENTIFIER +str = "2.1.3" # VERSION_STRING_IDENTIFIER From 908857a31700feda751d0ad41146466b2a721650 Mon Sep 17 00:00:00 2001 From: Martin Sekera Date: Thu, 18 Apr 2019 02:18:15 +0200 Subject: [PATCH 12/26] changed the over.callback.path API some more --- over/callback.py | 13 ++++++++++++- over/version.py | 4 ++-- 2 files changed, 14 insertions(+), 3 deletions(-) diff --git a/over/callback.py b/over/callback.py index 252dd8b..40f077f 100644 --- a/over/callback.py +++ b/over/callback.py @@ -87,7 +87,18 @@ def path(exists=False, permissions=None, validators=None): raise PermissionError("%s is not executable" %(arg)) if validators: - for fn, msg_tpl in validators: + if hasattr(validators, "__iter__"): + src = validators + else: + src = [validators] + + for v in src: + if hasattr(v, "__iter__"): + fn, msg_tpl = v + else: + fn = v + msg_tpl = "%%s failed validation by '%s'" %(fn.__name__) + if not fn(path): msg = msg_tpl %(arg) if "%s" in msg_tpl else msg_tpl diff --git a/over/version.py b/over/version.py index 14cbadc..87de0f3 100644 --- a/over/version.py +++ b/over/version.py @@ -4,5 +4,5 @@ major = 2 # VERSION_MAJOR_IDENTIFIER minor = 1 # VERSION_MINOR_IDENTIFIER # VERSION_LAST_MM 2.1 -patch = 3 # VERSION_PATCH_IDENTIFIER -str = "2.1.3" # VERSION_STRING_IDENTIFIER +patch = 4 # VERSION_PATCH_IDENTIFIER +str = "2.1.4" # VERSION_STRING_IDENTIFIER From 7634b3e35d3b580ce701fc4a673650c83735c903 Mon Sep 17 00:00:00 2001 From: Martin Sekera Date: Tue, 23 Apr 2019 01:51:51 +0200 Subject: [PATCH 13/26] add over.cmd.Command.__str__ --- over/cmd.py | 3 +++ over/version.py | 4 ++-- 2 files changed, 5 insertions(+), 2 deletions(-) diff --git a/over/cmd.py b/over/cmd.py index 0b12703..311b25d 100644 --- a/over/cmd.py +++ b/over/cmd.py @@ -61,6 +61,9 @@ class Command: self.__dict__["sequence_original"] = list(sequence) self.reset() + def __str__(self): + return " ".join(self.dump(pretty=True)) + def __setattr__(self, name, value): found = False diff --git a/over/version.py b/over/version.py index 87de0f3..e0f48db 100644 --- a/over/version.py +++ b/over/version.py @@ -4,5 +4,5 @@ major = 2 # VERSION_MAJOR_IDENTIFIER minor = 1 # VERSION_MINOR_IDENTIFIER # VERSION_LAST_MM 2.1 -patch = 4 # VERSION_PATCH_IDENTIFIER -str = "2.1.4" # VERSION_STRING_IDENTIFIER +patch = 5 # VERSION_PATCH_IDENTIFIER +str = "2.1.5" # VERSION_STRING_IDENTIFIER From 43ba8ee6a37e4ea8bf66d33351c2145359f1417f Mon Sep 17 00:00:00 2001 From: Martin Date: Tue, 6 Aug 2019 17:38:28 +0200 Subject: [PATCH 14/26] add over.cfg.Config - a simple read-only API for JSON configs --- over/__init__.py | 1 + over/cfg.py | 69 ++++++++++++++++++++++++++++++++++++++++++++++++ 2 files changed, 70 insertions(+) create mode 100644 over/cfg.py diff --git a/over/__init__.py b/over/__init__.py index 64db32b..90f3ac7 100644 --- a/over/__init__.py +++ b/over/__init__.py @@ -6,6 +6,7 @@ import sys from . import app from . import aux from . import callback +from . import cfg from . import cmd from . import docs from . import file diff --git a/over/cfg.py b/over/cfg.py new file mode 100644 index 0000000..6dfdfc7 --- /dev/null +++ b/over/cfg.py @@ -0,0 +1,69 @@ +#! /usr/bin/env python3 +# encoding: utf-8 + +# -------------------------------------------------- +# Library imports +import json +import jsmin + +# -------------------------------------------------- +# Local imports + +# -------------------------------------------------- + +class Config: + def __init__(self, source=None, readonly=False): + """ + Can be loaded from a JSON file (str path) or from a python dict. + """ + + if type(source) == str: + with open(source) as f: + initial = json.loads(jsmin.jsmin(f.read())) + elif source is None: + initial = {} + else: + initial = source + + assert type(initial) == dict + object.__setattr__(self, "raw", initial) + object.__setattr__(self, "readonly", readonly) + + def keys(self): + return self.raw.keys() + + def __getitem__(self, name): + return self.raw[name] + + def __setitem__(self, name, value): + if self.readonly: + raise AttributeError("object is not writable") + + self.raw[name] = value + + def __setattr__(self, name, value): + if self.readonly: + raise AttributeError("object is not writable") + + self.raw[name] = value + + def __getattr__(self, name): + matches = [key for key in self.raw if key.replace("-", "_") == name] + + if matches: + assert len(matches) == 1 + + value = self.raw[matches[0]] + + if type(value) == dict: + return Config(value) + else: + return value + else: + raise KeyError(name) + + def __contains__(self, name): + return name in self.raw + + def __repr__(self): + return "Config(%s)" %(" ".join("%s=%s" %(k, "…" if type(v) == dict else v) for k, v in self.raw.items())) From 193302708c4858ef59043bfab044f582f23f6d92 Mon Sep 17 00:00:00 2001 From: Martin Date: Wed, 7 Aug 2019 11:53:39 +0200 Subject: [PATCH 15/26] over.cfg.Config - add .items() and .values() --- over/cfg.py | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/over/cfg.py b/over/cfg.py index 6dfdfc7..926ea25 100644 --- a/over/cfg.py +++ b/over/cfg.py @@ -32,6 +32,12 @@ class Config: def keys(self): return self.raw.keys() + def items(self): + return self.raw.items() + + def values(self): + return self.raw.values() + def __getitem__(self, name): return self.raw[name] From 0f1d72149e679ad99d069c9a3c836f56bb7dea31 Mon Sep 17 00:00:00 2001 From: Martin Sekera Date: Sat, 7 Mar 2020 18:05:22 +0100 Subject: [PATCH 16/26] fix hexdump offset being off by one line --- over/misc.py | 2 +- over/version.py | 4 ++-- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/over/misc.py b/over/misc.py index 1273090..0f3e4cc 100644 --- a/over/misc.py +++ b/over/misc.py @@ -194,7 +194,7 @@ def hexdump(data, indent=0, width=16, show_header=True, show_offsets=True, show_ output_io.write(" " * indent) if show_offsets: - output_io.write(format_str %(initial_offset + ptr)) + output_io.write(format_str %(initial_offset + ptr - width)) if use_colors: output_io.write(text.render("")) output_io.write(" ".join(hex_bytes)) diff --git a/over/version.py b/over/version.py index e0f48db..d68c168 100644 --- a/over/version.py +++ b/over/version.py @@ -4,5 +4,5 @@ major = 2 # VERSION_MAJOR_IDENTIFIER minor = 1 # VERSION_MINOR_IDENTIFIER # VERSION_LAST_MM 2.1 -patch = 5 # VERSION_PATCH_IDENTIFIER -str = "2.1.5" # VERSION_STRING_IDENTIFIER +patch = 6 # VERSION_PATCH_IDENTIFIER +str = "2.1.6" # VERSION_STRING_IDENTIFIER From ca06e2438af8f3478088496b47906d3a1a2a0540 Mon Sep 17 00:00:00 2001 From: Martin Sekera Date: Mon, 9 Mar 2020 07:42:49 +0100 Subject: [PATCH 17/26] minor hexdump UI fix --- over/misc.py | 4 ++-- over/version.py | 4 ++-- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/over/misc.py b/over/misc.py index 0f3e4cc..639b977 100644 --- a/over/misc.py +++ b/over/misc.py @@ -181,7 +181,7 @@ def hexdump(data, indent=0, width=16, show_header=True, show_offsets=True, show_ skip_cnt += 1 else: if skip_cnt: - output_io.write("(skipped %d lines, %d B)\n" %(skip_cnt, skip_cnt * width)) + output_io.write("%s > skipping %d identical lines, %d B\n" %(" "*offset_figures, skip_cnt, skip_cnt * width)) skip_cnt = 0 @@ -208,7 +208,7 @@ def hexdump(data, indent=0, width=16, show_header=True, show_offsets=True, show_ output_io.write("\n") if skip_cnt: - output_io.write("(skipped %d lines, %d B)\n" %(skip_cnt, skip_cnt * width)) + output_io.write("%s > skipping %d identical lines, %d B\n" %(" "*offset_figures, skip_cnt, skip_cnt * width)) if not output: output_io.seek(0) diff --git a/over/version.py b/over/version.py index d68c168..44ca5cf 100644 --- a/over/version.py +++ b/over/version.py @@ -4,5 +4,5 @@ major = 2 # VERSION_MAJOR_IDENTIFIER minor = 1 # VERSION_MINOR_IDENTIFIER # VERSION_LAST_MM 2.1 -patch = 6 # VERSION_PATCH_IDENTIFIER -str = "2.1.6" # VERSION_STRING_IDENTIFIER +patch = 7 # VERSION_PATCH_IDENTIFIER +str = "2.1.7" # VERSION_STRING_IDENTIFIER From b7c3ca3e026f176535e48dc6ae9e30a7dfb6e0d0 Mon Sep 17 00:00:00 2001 From: Martin Sekera Date: Tue, 15 Sep 2020 01:50:32 +0200 Subject: [PATCH 18/26] fix over.cmd.Command.dump not quoting empty strings --- over/cmd.py | 2 +- over/version.py | 4 ++-- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/over/cmd.py b/over/cmd.py index 311b25d..90fec41 100644 --- a/over/cmd.py +++ b/over/cmd.py @@ -97,7 +97,7 @@ class Command: out.append(str(item)) if pretty: - return [('"%s"' %(a) if char_in_str(" $()[];\\", a) else a) for a in out] + return [('"%s"' %(a) if (not a or char_in_str(" $()[];\\", a)) else a) for a in out] else: return out diff --git a/over/version.py b/over/version.py index 44ca5cf..9597e13 100644 --- a/over/version.py +++ b/over/version.py @@ -4,5 +4,5 @@ major = 2 # VERSION_MAJOR_IDENTIFIER minor = 1 # VERSION_MINOR_IDENTIFIER # VERSION_LAST_MM 2.1 -patch = 7 # VERSION_PATCH_IDENTIFIER -str = "2.1.7" # VERSION_STRING_IDENTIFIER +patch = 8 # VERSION_PATCH_IDENTIFIER +str = "2.1.8" # VERSION_STRING_IDENTIFIER From 396ae4e49d3863a886d4b88a15dab757ed4b7f86 Mon Sep 17 00:00:00 2001 From: Martin Sekera Date: Mon, 15 Feb 2021 00:29:30 +0100 Subject: [PATCH 19/26] over.app: add .dirs with the app's xdg dirs --- over/app.py | 1 + over/version.py | 4 ++-- 2 files changed, 3 insertions(+), 2 deletions(-) diff --git a/over/app.py b/over/app.py index 64d7e1c..8e4b5c2 100644 --- a/over/app.py +++ b/over/app.py @@ -425,6 +425,7 @@ class Main: self.last_command_line = None self.using_alternate_config = False self.uncontained_exception_callbacks = [] # (function, args) + self.dirs = get_xdg_paths(name) for feature_name in self.default_features: if feature_name in features: diff --git a/over/version.py b/over/version.py index 9597e13..dcc0238 100644 --- a/over/version.py +++ b/over/version.py @@ -4,5 +4,5 @@ major = 2 # VERSION_MAJOR_IDENTIFIER minor = 1 # VERSION_MINOR_IDENTIFIER # VERSION_LAST_MM 2.1 -patch = 8 # VERSION_PATCH_IDENTIFIER -str = "2.1.8" # VERSION_STRING_IDENTIFIER +patch = 9 # VERSION_PATCH_IDENTIFIER +str = "2.1.9" # VERSION_STRING_IDENTIFIER From 3f6b7cd24eacd16f06b03a9db311a13dbd786ad6 Mon Sep 17 00:00:00 2001 From: Martin Sekera Date: Tue, 16 Feb 2021 17:56:59 +0100 Subject: [PATCH 20/26] add Main.info(), Main.done(), Main.warn(), Main.fail() log aliases --- over/app.py | 14 +++++++++++++- 1 file changed, 13 insertions(+), 1 deletion(-) diff --git a/over/app.py b/over/app.py index 8e4b5c2..476851f 100644 --- a/over/app.py +++ b/over/app.py @@ -219,7 +219,7 @@ def expand_group(token, options): options_by_abbr = {option.abbr: option for option in options.values() if option.abbr} - group_positive = token[0] is "+" + group_positive = token[0] == "+" for abbr in token[1:]: try: @@ -443,6 +443,18 @@ class Main: def __repr__(self): return 'over.app.Main(name="%s")' %(self.name) + def info(self, *args, **kwargs): + self.print(*args, **kwargs) + + def done(self, *args, **kwargs): + self.print(*args, tag=self.print.tl.done, **kwargs) + + def warn(self, *args, **kwargs): + self.print(*args, tag=self.print.tl.warn, **kwargs) + + def fail(self, *args, **kwargs): + self.print(*args, tag=self.print.tl.fail, **kwargs) + def exit(self, rv=0): """ Terminates the program and returns `rv`. From 2645383a6f4975e5b06c0c19f47d092e00f9170c Mon Sep 17 00:00:00 2001 From: Martin Sekera Date: Tue, 16 Feb 2021 17:58:09 +0100 Subject: [PATCH 21/26] fix callback.path ignoring the 'exists' attr on writable paths --- over/callback.py | 10 ++++++++-- over/version.py | 4 ++-- 2 files changed, 10 insertions(+), 4 deletions(-) diff --git a/over/callback.py b/over/callback.py index 807ed35..179451b 100644 --- a/over/callback.py +++ b/over/callback.py @@ -81,8 +81,14 @@ def path(exists=False, permissions=None, validators=None): if permissions: if "r" in permissions and not os.access(path, os.R_OK): raise PermissionError("%s is not readable" %(arg)) - if "w" in permissions and not os.access(path, os.W_OK): - raise PermissionError("%s is not writable" %(arg)) + + if exists or os.path.exists(path): + if "w" in permissions and not os.access(path, os.W_OK): + raise PermissionError("%s is not writable" %(arg)) + else: + if "w" in permissions and not os.access(os.path.dirname(path), os.W_OK): + raise PermissionError("%s cannot be created" %(arg)) + if "x" in permissions and not os.access(path, os.X_OK): raise PermissionError("%s is not executable" %(arg)) diff --git a/over/version.py b/over/version.py index dcc0238..c9a41f7 100644 --- a/over/version.py +++ b/over/version.py @@ -4,5 +4,5 @@ major = 2 # VERSION_MAJOR_IDENTIFIER minor = 1 # VERSION_MINOR_IDENTIFIER # VERSION_LAST_MM 2.1 -patch = 9 # VERSION_PATCH_IDENTIFIER -str = "2.1.9" # VERSION_STRING_IDENTIFIER +patch = 10 # VERSION_PATCH_IDENTIFIER +str = "2.1.10" # VERSION_STRING_IDENTIFIER From 10311ffd46f2ded67396ece94388f2562fa6f8a0 Mon Sep 17 00:00:00 2001 From: Martin Sekera Date: Fri, 19 Feb 2021 01:53:47 +0100 Subject: [PATCH 22/26] silence warnings in cmd.Command --- over/cmd.py | 2 +- over/version.py | 4 ++-- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/over/cmd.py b/over/cmd.py index 90fec41..c5051bf 100644 --- a/over/cmd.py +++ b/over/cmd.py @@ -108,7 +108,7 @@ class Command: stderr capture stderr instead of stdout (can't do both yet) """ - self.__dict__["process"] = subprocess.Popen(self.dump(), stdout=subprocess.PIPE, stderr=subprocess.PIPE, bufsize=1) + self.__dict__["process"] = subprocess.Popen(self.dump(), stdout=subprocess.PIPE, stderr=subprocess.PIPE, bufsize=16) self.__dict__["fifo"] = queue.Queue() self.__dict__["thread"] = threading.Thread( target=capture_output, diff --git a/over/version.py b/over/version.py index c9a41f7..23d8ef3 100644 --- a/over/version.py +++ b/over/version.py @@ -4,5 +4,5 @@ major = 2 # VERSION_MAJOR_IDENTIFIER minor = 1 # VERSION_MINOR_IDENTIFIER # VERSION_LAST_MM 2.1 -patch = 10 # VERSION_PATCH_IDENTIFIER -str = "2.1.10" # VERSION_STRING_IDENTIFIER +patch = 11 # VERSION_PATCH_IDENTIFIER +str = "2.1.11" # VERSION_STRING_IDENTIFIER From f3fdf22e530cf51dd86775ea57e8308b156ea0e1 Mon Sep 17 00:00:00 2001 From: Martin Sekera Date: Wed, 24 Mar 2021 16:26:25 +0100 Subject: [PATCH 23/26] add over.file.filelist function --- over/file.py | 15 +++++++++++++++ over/version.py | 8 ++++---- 2 files changed, 19 insertions(+), 4 deletions(-) diff --git a/over/file.py b/over/file.py index e3e4450..98b7a5d 100644 --- a/over/file.py +++ b/over/file.py @@ -54,3 +54,18 @@ def context_dir(path): yield finally: os.chdir(previous) + +# -------------------------------------------------- + +def filelist(d): + """ + Returns a filelist of d. + """ + + files = [] + + for root, directories, filenames in os.walk(d): + for filename in filenames: + files.append(os.path.join(root, filename)) + + return files diff --git a/over/version.py b/over/version.py index 23d8ef3..61bd81d 100644 --- a/over/version.py +++ b/over/version.py @@ -2,7 +2,7 @@ # encoding: utf-8 major = 2 # VERSION_MAJOR_IDENTIFIER -minor = 1 # VERSION_MINOR_IDENTIFIER -# VERSION_LAST_MM 2.1 -patch = 11 # VERSION_PATCH_IDENTIFIER -str = "2.1.11" # VERSION_STRING_IDENTIFIER +minor = 2 # VERSION_MINOR_IDENTIFIER +# VERSION_LAST_MM 2.2 +patch = 0 # VERSION_PATCH_IDENTIFIER +str = "2.2.0" # VERSION_STRING_IDENTIFIER From 03ab7834377732e4f14a38f52dbfaa1c76eb1d6d Mon Sep 17 00:00:00 2001 From: Martin Sekera Date: Wed, 24 Mar 2021 16:37:37 +0100 Subject: [PATCH 24/26] fix indent in over.file.filelist --- over/file.py | 4 ++-- over/version.py | 4 ++-- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/over/file.py b/over/file.py index 98b7a5d..634e673 100644 --- a/over/file.py +++ b/over/file.py @@ -67,5 +67,5 @@ def filelist(d): for root, directories, filenames in os.walk(d): for filename in filenames: files.append(os.path.join(root, filename)) - - return files + + return files diff --git a/over/version.py b/over/version.py index 61bd81d..48f57bf 100644 --- a/over/version.py +++ b/over/version.py @@ -4,5 +4,5 @@ major = 2 # VERSION_MAJOR_IDENTIFIER minor = 2 # VERSION_MINOR_IDENTIFIER # VERSION_LAST_MM 2.2 -patch = 0 # VERSION_PATCH_IDENTIFIER -str = "2.2.0" # VERSION_STRING_IDENTIFIER +patch = 1 # VERSION_PATCH_IDENTIFIER +str = "2.2.1" # VERSION_STRING_IDENTIFIER From 852cce04b628c6408002642f1ba936645fca80e1 Mon Sep 17 00:00:00 2001 From: Martin Sekera Date: Wed, 7 Apr 2021 23:05:08 +0200 Subject: [PATCH 25/26] over.app.Option: add new source type (preset) --- over/app.py | 4 ++++ over/text.py | 2 +- over/version.py | 8 ++++---- 3 files changed, 9 insertions(+), 5 deletions(-) diff --git a/over/app.py b/over/app.py index 476851f..8120804 100644 --- a/over/app.py +++ b/over/app.py @@ -107,6 +107,7 @@ class Option_sources(enum.Enum): default = 1 config_file = 2 command_line = 3 + preset = 4 class Option: 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): @@ -446,6 +447,9 @@ class Main: def info(self, *args, **kwargs): self.print(*args, **kwargs) + def start(self, *args, **kwargs): + self.print(*args, tag=self.print.tl.start, **kwargs) + def done(self, *args, **kwargs): self.print(*args, tag=self.print.tl.done, **kwargs) diff --git a/over/text.py b/over/text.py index ae90285..fc2c1cb 100644 --- a/over/text.py +++ b/over/text.py @@ -319,7 +319,7 @@ class Output: Formatting ========== You can use all formatting characters supported by strftime, as well as: - - tag + <@> - tag - name - supplied text - indentation start marker diff --git a/over/version.py b/over/version.py index 48f57bf..edcb6aa 100644 --- a/over/version.py +++ b/over/version.py @@ -2,7 +2,7 @@ # encoding: utf-8 major = 2 # VERSION_MAJOR_IDENTIFIER -minor = 2 # VERSION_MINOR_IDENTIFIER -# VERSION_LAST_MM 2.2 -patch = 1 # VERSION_PATCH_IDENTIFIER -str = "2.2.1" # VERSION_STRING_IDENTIFIER +minor = 3 # VERSION_MINOR_IDENTIFIER +# VERSION_LAST_MM 2.3 +patch = 0 # VERSION_PATCH_IDENTIFIER +str = "2.3.0" # VERSION_STRING_IDENTIFIER From 8f505849af34186382e2df156ae318f8b56294ac Mon Sep 17 00:00:00 2001 From: Martin Sekera Date: Tue, 24 Aug 2021 09:23:29 +0200 Subject: [PATCH 26/26] over-3.0 Text output API was brought in line with ACE and similar libraries: there are different functions for each log level, all with prinf semantics. Unit is now fully usable with base-2 (MiB) and base-10 (MB) systems, as well as arbitrary-base logarithms (dBm). ProgressBar has been updated to expose more Unit configuration. --- over/app.py | 57 ++++------ over/aux.py | 4 +- over/cmd.py | 10 +- over/text.py | 269 ++++++++++++++++++++++++++++-------------------- over/version.py | 8 +- 5 files changed, 193 insertions(+), 155 deletions(-) diff --git a/over/app.py b/over/app.py index 8120804..0f8e654 100644 --- a/over/app.py +++ b/over/app.py @@ -305,7 +305,7 @@ class ConfigFile: self.options = options self.path = path self.ignore_unknown = ignore_unknown - self.print = text.Output("over.app.ConfigFile") + self.log = text.Log("over.app.ConfigFile") self.seen_hashes = set() def read_config(self): @@ -356,13 +356,13 @@ class ConfigFile: 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)) + self.log.info("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(header_template %header_args) - self.print("created empty config file %s<.>" %(self.path)) + self.log.info("created empty config file %s<.>", self.path) # add new or otherwise missing options updated = set() @@ -370,7 +370,7 @@ class ConfigFile: 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: - self.print("adding --%s<.> to config file" %(option.name)) + self.log.info("adding --%s<.> to config file", option.name) f.write(docs.config_file_item %( option.hash, @@ -415,7 +415,7 @@ class Main: self.name = name self.version = version self.license = license - self.print = text.Output(name) + self.log = text.Log(name) self.options = OrderedDict() self.options_by_abbr = OrderedDict() self.docs = OrderedDict() @@ -444,21 +444,6 @@ class Main: def __repr__(self): return 'over.app.Main(name="%s")' %(self.name) - def info(self, *args, **kwargs): - self.print(*args, **kwargs) - - def start(self, *args, **kwargs): - self.print(*args, tag=self.print.tl.start, **kwargs) - - def done(self, *args, **kwargs): - self.print(*args, tag=self.print.tl.done, **kwargs) - - def warn(self, *args, **kwargs): - self.print(*args, tag=self.print.tl.warn, **kwargs) - - def fail(self, *args, **kwargs): - self.print(*args, tag=self.print.tl.fail, **kwargs) - def exit(self, rv=0): """ Terminates the program and returns `rv`. @@ -526,7 +511,7 @@ class Main: for target in self.targets: tokens.append(target) - self.print(" ".join(tokens), format="") + self.log.write(" ".join(tokens), format="") def activate_debug(self): raise NotImplementedError("debug is not yet here") @@ -555,23 +540,23 @@ class Main: @while displaying help """ - print = text.Output("help", format="", end="\n") + print = text.Log("help", format="", end="\n").write # App name and version print("[Application<.>]") - print(" %s<.>-%s<.> licensed under %s<.>" %(self.name, self.version, self.license)) + print(" %s<.>-%s<.> licensed under %s<.>", self.name, self.version, self.license) print(" using over-%s" %(version.str)) # Main features print("") print("[over.app.Main features<.>]") for feature_name in self.features: - print(" %s: %s<.> (%s)" %(" ON<.>" if self.features[feature_name] else "OFF<.>", feature_name, self.default_features[feature_name][1])) + print(" %s: %s<.> (%s)", " ON<.>" if self.features[feature_name] else "OFF<.>", feature_name, self.default_features[feature_name][1]) # App docs print("") for chapter, paragraphs in (alternate_docs or self.docs).items(): - print("[%s<.>]" %(chapter)) + print("[%s<.>]", chapter) for paragraph in paragraphs: print(paragraph, format=" ") @@ -618,10 +603,10 @@ class Main: else: colored_current_value = '' + colored_current_value + '<.>' - print(" Current value (%s): %s" %( + print(" Current value (%s): %s", serialize_source(option.source), colored_current_value - )) + ) # some misc flags if not option.in_cfg_file: @@ -635,7 +620,7 @@ class Main: # Current targets, if any if self.targets: print("[Targets<.>]") - print(" %s<.>" %(cmd.format_invocation(self.targets))) + print(" %s<.>", cmd.format_invocation(self.targets)) self.exit() @@ -764,7 +749,7 @@ class Main: If things go south even here, prints the topmost traceback and gives up in a safe manner. """ - self.print("uncontained exception<.> %s<.> raised" %(exception_type.__name__), self.print.tl.fail) + self.log.fail("uncontained exception<.> %s<.> raised", exception_type.__name__) try: tb_lines = ["", "---------------- Stack trace ----------------", "In program %s<.>" %(self.invocation)] @@ -824,26 +809,26 @@ class Main: else: format = "%s - " %((i - 3) * " ") - self.print(line, format=format, end="\n") + self.log.write(line, format=format, end="\n") - self.print("---------------------------------------------", format="", end="\n") + self.log.write("---------------------------------------------", format="", end="\n") except: - self.print("failed to contain exception<.>", self.print.tl.epic) + self.log.epic("failed to contain exception<.>") traceback.print_exc() if self.uncontained_exception_callbacks: - self.print("executing containment callbacks", self.print.tl.exec) + self.log.start("executing containment callbacks") l = len(self.uncontained_exception_callbacks) for i, (cb, ctx) in enumerate(self.uncontained_exception_callbacks): - self.print("(%d/%d) %s" %(i+1, l, cb.__name__)) + self.log.write("(%d/%d) %s", i+1, l, cb.__name__) try: cb(*ctx) except Exception as e: - self.print("(%d/%d) %s failed<.> (%s<.>)" %(i+1, l, cb.__name__, e.__class__.__name__), self.print.tl.epic, end=":\n") + self.log.epic("(%d/%d) %s failed<.> (%s<.>)", i+1, l, cb.__name__, e.__class__.__name__, end=":\n") exc_info = sys.exc_info() traceback.print_exception(*exc_info) del exc_info - self.print("containment callbacks executed", self.print.tl.done) + self.log.done("containment callbacks executed") diff --git a/over/aux.py b/over/aux.py index 018fb87..bd77731 100644 --- a/over/aux.py +++ b/over/aux.py @@ -7,7 +7,7 @@ import traceback from . import text class DeprecationForwarder: - print = text.Output("over.aux.DeprecationForwarder", stream=sys.stderr) + log = text.Log("over.aux.DeprecationForwarder") def __init__(self, target, old_name, new_name): self._target = target @@ -16,6 +16,6 @@ class DeprecationForwarder: def __getattr__(self, name): caller = traceback.extract_stack()[-2] - self.print("%s is deprecated, please use %s instead (%s:%d)" %(self._old_name, self._new_name, caller[0], caller[1]), self.print.tl.warn) + self.log.warn("%s is deprecated, please use %s instead (%s:%d)", self._old_name, self._new_name, caller[0], caller[1]) return getattr(self._target, name) diff --git a/over/cmd.py b/over/cmd.py index c5051bf..b655d6e 100644 --- a/over/cmd.py +++ b/over/cmd.py @@ -30,10 +30,13 @@ def char_in_str(chars, string): # -------------------------------------------------- -def format_invocation(args): +def escape_quote_invocation(args): escaped = (arg.replace('"', '\\"') for arg in args) - return " ".join(('"%s"' %(a) if char_in_str(' $()[];\\"', a) else a) for a in escaped) + return [('"%s"' %(a) if char_in_str(' $()[];\\"|', a) else a) for a in escaped] + +def format_invocation(args): + return " ".join(escape_quote_invocation(args)) # -------------------------------------------------- @@ -97,7 +100,8 @@ class Command: out.append(str(item)) if pretty: - return [('"%s"' %(a) if (not a or char_in_str(" $()[];\\", a)) else a) for a in out] + return escape_quote_invocation(out) + # return [('"%s"' %(a) if (not a or char_in_str(" $()[];\\", a)) else a) for a in out] else: return out diff --git a/over/text.py b/over/text.py index fc2c1cb..438468a 100644 --- a/over/text.py +++ b/over/text.py @@ -15,16 +15,16 @@ import tzlocal # -------------------------------------------------- -def lexical_join(words, oxford=False): +def lexical_join(words, oxford=True): """ Joins an iterable of words or sentence fragments into a lexical list: >>> lexical_join(["this", "that", "one of them too"]) - "this, that and one of them too" - - >>> lexical_join(["this", "that", "one of them too"], oxford=True) "this, that, and one of them too" + >>> lexical_join(["this", "that", "one of them too"], oxford=False) + "this, that and one of them too" + >>> lexical_join(["this", "that"]) "this and that" @@ -49,66 +49,80 @@ def lexical_join(words, oxford=False): class Unit: """ A object that represents numbers and units in human-readable form. - - TODO use significant digits instead of rigid order boundaries - TODO float superclass? - TODO base_2 prefixes (Ki, Mi, ...) """ - _prefixes = ( + _prefixes_base2 = ( + ("Yi", 80), ("Zi", 70), ("Ei", 60), ("Pi", 50), ("Ti", 40), ("Gi", 30), ("Mi", 20), ("Ki", 10), + ("", 0) + ) + + _prefixes_base10 = ( ("Y", 24), ("Z", 21), ("E", 18), ("P", 15), ("T", 12), ("G", 9), ("M", 6), ("k", 3), - ("h", 2), ("D", 1), ("", 0), ("d", -1), ("c", -2), ("m", -3), ("μ", -6), ("n", -9), + ("", 0), ("d", -1), ("c", -2), ("m", -3), ("µ", -6), ("n", -9), ("p", -12), ("f", -15), ("a", -18), ("z", -21), ("y", -24) ) - def __init__(self, - value, unit=None, dimension=1, - use_prefixes="YZEPTGMkmμnpfazy", format="%.2f pU", - logarithmic=False, log_base=10 - ): + def __init__(self, value, unit=None, base=10, dimension=1, format="%.2f pU", overshoot=1.5, only_prefixes=[]): """ - value the numerical value of the variable (int or float) - unit the symbol to use, if any (str or None) - dimension the dimensionality of the value (1, 2, 3, ...) - use_prefixes which multiplier prefixes to use - - the default "YZEPTGMkmμnpfazy" omits "c" for centi- and "D" for deca- - format use printf notation for value (e.g. %010.5f), p for prefix and U for unit - logarithmic when True, log_10 prefixes are used - log_base logarithm base value + param value value to display [int, float] + param unit base unit (e.g. "m") [str] + param base see Base [int] + param dimension dimensionality of the value (1, 2, 3, ...) [int] + param format format string [str] + param overshoot how much should a lower prefix persist [float] + param restrict_prefixes list of whitelisted prefixes; uses everything if empty [list of str] - note that deca- is correctly rendered as "da", the "D" is used in use_prefixes only + # Base + With linear units, this is either exactly 2 for Base 2 prefixes (Ki, Mi, Gi, ...) + or exactly 10 for Base 10 ones (K, M, G, ... as well as m, µ, n, ...). + + Negative bases force logarithmic behavior with the base equal to the absolute + value, so e.g. base=-10 forces log10 decibels. + + # Overshoot behavior + Overshoot adjusts the boundary between prefixes, e.g. with overshoot=1.0 + a Unit switches from k to M when the value exceeds 1000. With overshoot=1.5, + this happens at 1500. """ + assert (base in [2, 10]) or base < 0 + self.value = float(value) - self.unit = unit if unit else "" + self.unit = unit or "" + self.base = base self.dimension = dimension - self.use_prefixes = use_prefixes self.format = format - self.logarithmic = logarithmic - self.log_base = log_base + self.overshoot = overshoot - if self.logarithmic and (self.value < 0 or self.log_base < 0): + all_prefixes = self._prefixes_base2 if base == 2 else self._prefixes_base10 + + if only_prefixes: + self.prefixes = [(prefix, exp) for prefix, exp in all_prefixes if prefix in only_prefixes] + else: + self.prefixes = all_prefixes + + if self.base < 0 and self.value < 0: raise ValueError("math domain error (negative values can't be represented in dB)") def __str__(self): - if self.value == 0.0: - e = 0 - else: - e = round(math.log(abs(self.value), 10), 6) - - if self.logarithmic: - prefix = "dB" - value = math.log(self.value, self.log_base) * 10 - - else: - for prefix, mul in self._prefixes: - if prefix in self.use_prefixes and mul*self.dimension <= e: + # each branch determines a prefix and an adjusted value + if self.base > 0: + for prefix, exp in self.prefixes: + if exp == 0: # always prefer 1.0 + overshoot = 1 + else: + overshoot = self.overshoot + + if self.base ** (exp * self.dimension) * overshoot <= self.value: break - value = self.value / 10**(mul*self.dimension) + value = self.value / self.base ** (exp * self.dimension) + else: + prefix = "dB" + value = math.log(self.value, -self.base) * 10 output = self.format %(value) - output = output.replace("p", prefix if prefix != "D" else "da") # deca- handler + output = output.replace("p", prefix) output = output.replace("U", self.unit) return output @@ -283,35 +297,21 @@ def count_leading(text, char): # -------------------------------------------------- -class Output: +class Log: class tag: - class short: - info = " " - debug = " ?<.>" - start = ">>><.>" - exec = start - note = " #<.>" - warn = " !<.>" - fail = "!!!<.>" - done = " *<.>" - class long: - info = "INFO" - debug = "DEBG<.>" - start = "EXEC<.>" - exec = start - note = "NOTE<.>" - warn = "WARN<.>" - fail = "FAIL<.>" - epic = "EPIC<.>" - done = "DONE<.>" - - ts = tag.short - tl = tag.long + info = "INFO" + debug = "DEBG<.>" + begin = "BEGN<.>" + note = "NOTE<.>" + warn = "WARN<.>" + fail = "FAIL<.>" + epic = "EPIC<.>" + done = "DONE<.>" """ Text UI output renderer. - Prints messages to the stdout with optional eye candy + Prints messages to stderr with optional eye candy like colors and timestamps. If the output stream is not a tty, colors are disabled. @@ -325,7 +325,7 @@ class Output: - indentation start marker """ - def __init__(self, name, format="[%Y-%m-%d %H:%M:%S %Z] <@> -- , ", colors=True, end=".\n", indent=True, stream=sys.stderr): + def __init__(self, name, format="[%Y-%m-%d %H:%M:%S %Z] <@> -- , ", colors=True, end="\n", indent=True, stream=sys.stderr): self.name = name self.format = format self.end = end @@ -337,12 +337,12 @@ class Output: def add_sink(self, stream, colors): self.sinks.append((stream, colors)) - def write(self, text, tag=None, format=None, end=None): + def write(self, fmt, *args, tag=None, format=None, end=None): """ @while displaying text """ - tag = tag or self.__class__.tag.long.info + tag = tag or self.__class__.tag.info format = format or self.format end = end or self.end @@ -350,6 +350,8 @@ class Output: output = output.replace("<@>", tag) output = output.replace("", self.name) + text = fmt % args + if self.indent and "" in format: indent_width = render(output, colors=False).index("") text = paragraph(text, indent=indent_width)[indent_width:] @@ -362,15 +364,29 @@ class Output: sink.write(render(output, colors)) sink.flush() - def flush(self): - """ - Dummy method for compatibility. - """ - - pass - - def __call__(self, *args, **kwargs): + def info(self, *args, **kwargs): self.write(*args, **kwargs) + + def debug(self, *args, **kwargs): + self.write(*args, tag=self.tag.debug, **kwargs) + + def begin(self, *args, **kwargs): + self.write(*args, tag=self.tag.begin, **kwargs) + + def note(self, *args, **kwargs): + self.write(*args, tag=self.tag.note, **kwargs) + + def warn(self, *args, **kwargs): + self.write(*args, tag=self.tag.warn, **kwargs) + + def fail(self, *args, **kwargs): + self.write(*args, tag=self.tag.fail, **kwargs) + + def epic(self, *args, **kwargs): + self.write(*args, tag=self.tag.epic, **kwargs) + + def done(self, *args, **kwargs): + self.write(*args, tag=self.tag.done, **kwargs) # -------------------------------------------------- @@ -391,15 +407,14 @@ def get_terminal_size(): # -------------------------------------------------- class _ProgressBarChannel: - print = Output("over.text._ProgressBarChannel") - - def __init__(self, unit, top, precision, use_prefixes=True, prefix_base2=False, min_width_raw=0, min_width_rate=0, min_width_time=0, min_width_percent=7): + def __init__(self, unit, top, precision, use_prefixes=True, base=10, only_prefixes=[], min_width_raw=0, min_width_rate=0, min_width_time=0, min_width_percent=7): self.unit = unit self.top = top self.dynamic_top = top is None - self.prefix_base2 = prefix_base2 + self.base = base self.precision = precision self.use_prefixes = use_prefixes + self.only_prefixes = only_prefixes self.min_width = { "raw": min_width_raw, "rate": min_width_rate, @@ -409,9 +424,6 @@ class _ProgressBarChannel: self._value = 0 self.set() - - if prefix_base2: - self.print("Unit does not yet support base2 prefixes (e.g. Gi, Mi), using decadic (G, M) instead", self.print.tl.warn) def set(self, value=None): if value is not None: @@ -423,10 +435,13 @@ class _ProgressBarChannel: if unit is None: unit = self.unit - u = Unit(value, unit, format="%.{:d}f pU".format(self.precision)) - - if not self.use_prefixes or just == "percent": - u._prefixes = (("", 0),) # Unit needs fixin" + u = Unit( + value, + unit, + format="%.{:d}f pU".format(self.precision), + base=self.base, + only_prefixes=[""] if (not self.use_prefixes or just == "percent") else self.only_prefixes + ) s = str(u) @@ -490,20 +505,18 @@ class ProgressBar: width is the desired size of the progressbar drawing area in characters (columns) and defaults to terminal width. - space_before_unit enables a space character between a value and its unit. - channels is a dict that for each channel lists its properties: { "a": { "unit": "f", - "prefix_base2": False, "top": 39600, "precision": 1 }, "b": { - "unit": "o", - "prefix_base2": True, + "unit": "B", + "base": 2, + "only_prefixes": ["Ki", "Mi"], "top": measure_file_size_closure("/path/to/file"), "precision": 0 } @@ -511,7 +524,7 @@ class ProgressBar: Channel IDs (the "a", "b") are arbitrary lowercase letters ([a-z]). - Properties "unit" and "prefix_base2" are passed to over.core.text.Unit. + Properties "unit", "base", and "only_prefixes" are passed to over.text.Unit. "top" is the value of the channel that corresponds to 100%. Channels are allowed to exceed this value. It can either be a number or a callable. If it"s a callable, it will be called without arguments and shall return a number. @@ -658,28 +671,64 @@ class ProgressBar: # -------------------------------------------------- -if __name__ == "__main__": - o = Output("over.text") - o("Sample info message") - o("Sample debug message", o.tl.debug) - o("Sample action start message", o.tl.start) - o("Sample action success message", o.tl.done) - o("Sample warning message", o.tl.warn) - o("Sample error message", o.tl.fail) +def timestamp_to_hms(t): + """ + Converts a duration in seconds to a [[?h ]?m ]?s representation. - o("Available colors", end=":\n") + >>> timestamp_to_hms(12345) + '3h 25m 45s' + + >>> timestamp_to_hms(3600) + '1h 0m 0s' + + >>> timestamp_to_hms(1234) + '20m 34s' + + >>> timestamp_to_hms(12) + '12s' + """ + + hh = t // 3600 + mm = (t % 3600) // 60 + ss = t % 60 + + out = [] + + if hh: + out.append("%dh" %(hh)) + + if mm or hh: + out.append("%dm" %(mm)) + + out.append("%ds" %(ss)) + + return " ".join(out) + +# -------------------------------------------------- + +if __name__ == "__main__": + log = Log("over.text") + log.info("Sample info message") + log.debug("Sample debug message") + log.begin("Sample action start message") + log.done("Sample action success message") + log.warn("Sample warning message") + log.fail("Sample error message") + log.note("Sample note message") + + log.write("Available colors", end=":\n") for abbr, (code, name) in sorted(ansi_colors.items()): - o("%s = <%s>%s<.>" %(abbr, abbr, name)) + log.write("%s = <%s>%s<.>", abbr, abbr, name) - o("ProgressBar test") + log.begin("ProgressBar test") pb = ProgressBar( "§%a (§ra/§za) [§=a>§ A] §sa [§rb/§zb] (ETA §TA)", { "a": { "unit": "f", - "prefix_base2": False, + "base": 10, "top": 39600, "precision": 1, "min_width_raw": 10, @@ -688,8 +737,8 @@ if __name__ == "__main__": "min_width_time": 0 }, "b": { - "unit": "o", - "prefix_base2": True, + "unit": "B", + "base": 2, "top": 3200, "precision": 0, "min_width_raw": 10, @@ -707,4 +756,4 @@ if __name__ == "__main__": pb.render() time.sleep(0.25) - pb.end(not True) + pb.end(True) diff --git a/over/version.py b/over/version.py index edcb6aa..90452d4 100644 --- a/over/version.py +++ b/over/version.py @@ -1,8 +1,8 @@ #! /usr/bin/env python3 # encoding: utf-8 -major = 2 # VERSION_MAJOR_IDENTIFIER -minor = 3 # VERSION_MINOR_IDENTIFIER -# VERSION_LAST_MM 2.3 +major = 3 # VERSION_MAJOR_IDENTIFIER +minor = 0 # VERSION_MINOR_IDENTIFIER +# VERSION_LAST_MM 3.0 patch = 0 # VERSION_PATCH_IDENTIFIER -str = "2.3.0" # VERSION_STRING_IDENTIFIER +str = "3.0.0" # VERSION_STRING_IDENTIFIER