diff --git a/src/forthlift/forthlift.py b/src/forthlift/forthlift.py index 892fb68..6401913 100755 --- a/src/forthlift/forthlift.py +++ b/src/forthlift/forthlift.py @@ -4,6 +4,7 @@ import re import sys import locale import logging +import inspect import argparse import datetime @@ -24,6 +25,7 @@ class stream: raise NotImplementedError class stdin(Stream): + """Stream from the standard input.""" def __call__(self): return sys.stdin @@ -33,10 +35,12 @@ class consume: raise NotImplementedError class lines(Consume): + """Consume line by line.""" def __call__(self, stream): return stream.readlines() class paragraphs(Consume): + """Consume paragraph by paragraph (separated by an empty line).""" def __call__(self, stream): pars = [] current = "" @@ -50,9 +54,15 @@ class consume: return pars class sections(Consume): - def __init__(self, mark = r"^#", skip = "False"): + """Consume section by section. A new section starts when a line matches the `mark` regexp. If `skip` is set to 'skip', the marked line is not consumed.""" + def __init__(self, mark = r"^#", skip = "noskip"): self.mark = mark - self.skip = bool(skip) + if skip == 'skip': + self.skip = True + elif skip == 'noskip': + self.skip = False + else: + self.skip = bool(skip) def __call__(self, stream): sec = [] @@ -70,6 +80,7 @@ class consume: return sec class nlines(Consume): + """Consume by groups of `nb` lines.""" def __init__(self, nb = "10"): self.nb = int(nb) @@ -89,17 +100,18 @@ class consume: return sec - class format: class Format: def __call__(self, items): raise NotImplementedError - class as_is(Format): + class asis(Format): + """Do not format anything.""" def __call__(self, items): return items class trim(Format): + """Split items if their length is longer than `max`, and create new items with the remaining parts.""" # Every argument is a string. def __init__(self, max = "140"): self.max = int(max) @@ -120,6 +132,7 @@ class format: return trimmed class eol(Format): + """Add an end of line after the item.""" def __call__(self, items): eoled = [] for item in items: @@ -127,6 +140,7 @@ class format: return eoled class strip(Format): + """Remove any space character around the item.""" def __call__(self, items): stripped = [] for item in items: @@ -134,6 +148,7 @@ class format: return stripped class skip(Format): + """Skip items containing only spaces or being empty.""" def __call__(self, items): res = [] for item in items: @@ -141,7 +156,8 @@ class format: res.append( item ) return res - class paragraph(Format): + class glue(Format): + """Glue consecutive items together if they are not separated by an empty one.""" def __call__(self, items): glued = [] current = "" @@ -154,13 +170,15 @@ class format: return glued class panel(Format): + """Surround each item by an ascii-art box. NOTE: only works when lifted on stdout.""" def __call__(self, items): panel = [] for item in items: panel.append( Panel.fit(item.strip()) ) return panel - class count(Format): + class counter(Format): + """Add a counter at the end of each items, with the current index and the total. If `end` is given, it is added at the very last item. If `sep` is given, it is appended to the item before the counter itself.""" def __init__(self, end = ' ␄', sep = '\n'): self.sep = sep self.end = end @@ -175,6 +193,7 @@ class format: return res class suffix(Format): + """Add the `content` string after each item. If `sep` is given, it is appended to the item before the content and after the item.""" def __init__(self, content = "", sep = '\n'): self.content = content self.sep = sep @@ -183,6 +202,7 @@ class format: return [i+self.sep+self.content for i in items] class prefix(Format): + """Add the `content` string before each item. If `sep` is given, it is prepended to the item after the content and before the item.""" def __init__(self, content = "", sep = '\n'): self.content = content self.sep = sep @@ -197,6 +217,7 @@ class lift: raise NotImplementedError class stdout(Lift): + """Print the items on the standard output.""" def __call__(self, items): for item in items: if item: @@ -212,7 +233,7 @@ class Forthlifter: def __init__(self, consumer = consume.lines(), streamers = [stream.stdin()], - formatters = [format.as_is()], + formatters = [format.asis()], lifters = [lift.stdout()], ): if not isinstance(streamers, list): @@ -277,6 +298,7 @@ def classes_of(namespace): subs = {cls.__name__:cls for cls in itf.__subclasses__()} return subs + def operator(asked_op): logger.debug(f"├ Parsed operators:") for op in asked_op: @@ -295,44 +317,86 @@ def operator(asked_op): yield name,args logger.debug("│ └OK") + +def help_op(ops): + h = "" + itf = list(ops.values())[0].__mro__[1].__name__.lower() + h += f"\nAVAILABLE OPERATORS FOR --{itf}:\n" + for name,cls in ops.items(): + # Signature + hsig = f" -{itf[0]} {name.lower()}" + args = inspect.getfullargspec(cls)[0] + if args: + sign = inspect.signature(cls) + args = [sign.parameters[a].name for a in sign.parameters] + hsig += f"[:{','.join(args)}]" + h += hsig + + # Example (using defaults) + hex = f"-{itf[0]} " + args = inspect.getfullargspec(cls)[0] + if args: + sep = ":" + sign = inspect.signature(cls) + defs = [sign.parameters[a].default for a in sign.parameters] + if not all(defs) or any(re.match(r'\s', d) for d in defs): + hdefs = [d.replace("\n", r"\n") for d in defs] + hex += f"'{name.lower()}{sep}{','.join(hdefs)}'" + else: + hex += f"{name.lower()}{sep}{','.join(defs)}" + h += f"\n\tDefault: {hex}" + else: + hex += name.lower() + + if cls.__doc__: + h+= f"\n\t{cls.__doc__}\n" + else: + h += "\n" + return h + def main(): errors = {"NO_ERROR":0, "UNKNOWN_ERROR":1, "NO_SETUP_NEEDED":2, "NO_APP_KEY":10} usage = "Post text read on the standard input to a website or an API." - parser = argparse.ArgumentParser( description=usage, - formatter_class=argparse.ArgumentDefaultsHelpFormatter) - streamers = classes_of(stream) - parser.add_argument("-s", "--stream", - # choices = streamers.keys(), - metavar = '{' + ','.join(streamers) + '}', - default = [], - action="append", - help="Where to get items.") - consumers = classes_of(consume) - parser.add_argument("-c", "--consume", - # choices = consumers.keys(), - metavar = '{' + ','.join(consumers) + '}', - default = "lines", - help="How to get the content form items.") - formaters = classes_of(format) - parser.add_argument("-f", "--format", - # choices = formaters.keys(), - metavar = '{' + ','.join(formaters) + '}', - default = [], - action="append", - help="How to format items.") - lifters = classes_of(lift) - parser.add_argument("-l", "--lift", - # choices = lifters.keys(), - metavar = '{' + ','.join(lifters) + '}', + + epilog = "" + epilog += help_op(streamers) + epilog += help_op(consumers) + epilog += help_op(formaters) + epilog += help_op(lifters) + + parser = argparse.ArgumentParser( + description=usage, + formatter_class = argparse.RawTextHelpFormatter, + epilog = epilog) + + parser.add_argument("-s", "--stream", + metavar = "STREAM(S)", default = [], action="append", - help="How to lift items.") + help="Where to get items (several occurences possibles, order matters).") + + parser.add_argument("-c", "--consume", + metavar = "CONSUME", + default = "lines", + help="How to extract the content from the stream (only one occurence).") + + parser.add_argument("-f", "--format", + metavar = "FORMAT(S)", + default = [], + action="append", + help="How to format items (several occurences possibles, order matters).") + + parser.add_argument("-l", "--lift", + metavar = "LIFT(S)", + default = [], + action="append", + help="How to send items somewhere (several occurences possibles, order matters).") asked = parser.parse_args() @@ -352,7 +416,7 @@ def main(): asked.stream = ["stdin"] if not asked.format: - asked.format = ["as_is"] + asked.format = ["asis"] if not asked.lift: asked.lift = ["stdout"]