feat(doc): autoextract operators' help

This commit is contained in:
Johann Dreo 2026-04-06 14:12:01 +02:00
commit e0adccda32

View file

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