Source code for help2man

"""Provide ``__version__`` for
`importlib.metadata.version() <https://docs.python.org/3/library/importlib.metadata.html#distribution-versions>`_.
"""
import io
import logging
import re
from argparse import ArgumentParser, Namespace
from contextlib import nullcontext, redirect_stderr, redirect_stdout
from datetime import datetime
from pathlib import Path
from typing import Callable

from jinja2 import Template

try:
    from ._version import __version__, __version_tuple__  # type: ignore
except ImportError:
    # for `python -m build` use help2man to generate man page of help2man
    __version__ = "rolling"
    __version_tuple__ = (0, 0, 0, __version__, "")

PAT = re.compile(r"\x1b\[[0-9;]+?m")
logger = logging.getLogger(__name__)
ASSETS_PATH = Path(__file__).absolute().parent / "assets"
TEMPLATES = {
    "man": (ASSETS_PATH / "jinja2" / "template.man.j2").read_text(),
    "markdown": (ASSETS_PATH / "jinja2" / "template.md.j2").read_text(),
}
PAT_SECTION = re.compile(r"\n\n(?=\S)")
PAT_SPACE = re.compile("  +")
DEFAULTS = {
    "help_option": "--help",
    "version_option": "--version",
    "name": "Name",
    "section": "1",
    "manual": "User Commands",
    "source": "",
    "info_page": "",
    "no_discard_stderr": False,
}


[docs]def get_output( f: Callable, args: tuple = (), no_discard_stderr: bool = DEFAULTS["no_discard_stderr"], ) -> str: """Get stdout and stderr of a function. :param f: :type f: Callable :param args: :type args: tuple :param no_discard_stderr: :type no_discard_stderr: bool :rtype: str """ string = io.StringIO() if no_discard_stderr: ctx = redirect_stderr(string) else: ctx = nullcontext() with redirect_stdout(string), ctx: f(*args) string.seek(0) output = string.read() return output
[docs]def parser2strings( parser: ArgumentParser, no_discard_stderr: bool = DEFAULTS["no_discard_stderr"], ) -> tuple[str, str]: """Convert a parser to help string and version string. :param parser: :type parser: ArgumentParser :param no_discard_stderr: :type no_discard_stderr: bool :rtype: tuple[str, str] """ actions = parser._option_string_actions helpstr = get_output(parser.print_help, (), no_discard_stderr) try: versionstr = actions["--version"].version # type: ignore except KeyError: versionstr = "" return helpstr, versionstr
[docs]def parser2man( parser: ArgumentParser, name: str = DEFAULTS["name"], section: str = DEFAULTS["section"], manual: str = DEFAULTS["manual"], source: str = DEFAULTS["source"], info_page: str = DEFAULTS["info_page"], template: str = TEMPLATES["man"], ) -> str: """Convert a parser to man. :param parser: :type parser: ArgumentParser :param name: :type name: str :param section: :type section: str :param manual: :type manual: str :param source: :type source: str :param info_page: :type info_page: str :param template: :type template: str :rtype: str """ man = help2man( *parser2strings(parser), name, section, manual, source, info_page, template, ) return man
[docs]def help2man( helpstr: str, versionstr: str, name: str = DEFAULTS["name"], section: str = DEFAULTS["section"], manual: str = DEFAULTS["manual"], source: str = DEFAULTS["source"], info_page: str = DEFAULTS["info_page"], template: str = TEMPLATES["man"], ) -> str: """Convert help string and version string to man. :param helpstr: :type helpstr: str :param versionstr: :type versionstr: str :param name: :type name: str :param section: :type section: str :param manual: :type manual: str :param source: :type source: str :param info_page: :type info_page: str :param template: :type template: str :rtype: str """ helpstr = PAT.sub("", helpstr) paragraphs = PAT_SECTION.split(helpstr) prog = "" synopsis = "" i = -1 for i, paragraph in enumerate(paragraphs): if paragraph.startswith("usage: ") or paragraph.startswith("Usage: "): synopsis = ( paragraph.replace("usage: ", "") .replace("Usage: ", "") .replace("\n" + " " * len("usage: "), "\n") ) prog = synopsis.split(" ")[0] break elif paragraph.startswith("usage:\n") or paragraph.startswith( "Usage:\n" ): synopsis = paragraph.replace("usage:\n", "").replace( "Usage:\n", "" ) prog = synopsis.strip().split(" ")[0] break if i == 0: # argparse's description may be in 1-st paragraph if paragraphs[1].splitlines()[0].endswith(":"): description = "" else: description = paragraphs[1] elif i == 1: description = " ".join(paragraphs[0].split(" ")[1:]) else: description = "" sections = [] version = copyright = author = bug = "" for paragraph in paragraphs[2:]: lines = paragraph.splitlines() if not lines[0].endswith(":"): tokens = lines[0].split("Report bugs to ") if len(tokens) > 1: bug = tokens[1] break sec = Namespace(title=lines[0].strip(":"), contents=[]) length = len(lines[1]) - len(lines[1].lstrip()) paragraph = paragraph.replace("\n " + " " * length, "") lines = paragraph.splitlines() for line in lines[1:]: line = line[length:] tokens = PAT_SPACE.split(line) content = Namespace() if len(tokens) > 1: content.name = tokens[0] content.description = " ".join(tokens[1:]) else: content.description = tokens[-1] sec.contents += [content] sections += [sec] lines = versionstr.splitlines() try: version = lines[0].split(" ")[-1] for line in lines[1:]: tokens = line.split("Copyright (C) ") if len(tokens) > 1: copyright = tokens[-1] continue tokens = line.split("Written by ") if len(tokens) > 1: author = tokens[-1] except IndexError: logger.warning("version format is not correct.") man = Template(template).render( help2man_version=__version__, date=datetime.now().strftime("%F"), prog=prog, synopsis=synopsis, description=description, sections=sections, bug=bug, author=author, version=version, copyright=copyright, name=name, section=section, manual=manual, source=source, info_page=info_page, ) return man