cog is a neat little library for inserting text into files. It’s small, easy to read, and dependency-free. You might want to use it for automatically inserting help text in a project readme.
In a previous post, I discovered how to
recursively generate Argparse help for pdoc.
Today I discovered that this is also useful for cog
,
and that there’s a better way to wrap the text.
The result still recursively formats help for every subcommand
(and sub-sub-command, and so forth),
and is still useful with pdoc
too
(see the older post for how that works).
Better text wrapping
In the last post, I was wrapping text manually with textwrap
.
However, we can use a formatter_class
to do this for us.
This has the benefit of somewhat shorter code,
but more importantly, it handles long lines better.
Argparse can indent more intelligently than the textwrap
approach
by indenting to a tab stop and only breaking on words when possible.
Here’s an example of the result:
options:
-h, --help show this help message and exit
--verbose Increase verbosity of output
--longhelp LONGHELP This is a very long help text. This is a very long help
text. This is a very long help text. This is a very long
help text. This is a very long help text. This is a very
long help text. This is a very long help text. This is a
very long help text. This is a very long help text. This
is a very long help text.
And here’s the code:
def get_argparse_help_string(
name: str, parser: argparse.ArgumentParser, wrap: int = 80
) -> str:
"""Generate a docstring for an argparse parser that shows the help for the parser and all subparsers, recursively.
Based on an idea from <https://github.com/pdoc3/pdoc/issues/89>
Arguments:
* `name`: The name of the program
* `parser`: The parser
* `wrap`: The number of characters to wrap the help text to (0 to disable)
"""
def help_formatter(prog):
return argparse.HelpFormatter(prog, width=wrap)
def get_parser_help_recursive(
parser: argparse.ArgumentParser, cmd: str = "", root: bool = True
):
docstring = ""
if not root:
docstring += "\n" + "_" * 72 + "\n\n"
docstring += f"> {cmd} --help\n"
parser.formatter_class = help_formatter
docstring += parser.format_help()
for action in parser._actions:
if isinstance(action, argparse._SubParsersAction):
for subcmd, subparser in action.choices.items():
docstring += get_parser_help_recursive(
subparser, f"{cmd} {subcmd}", root=False
)
return docstring
docstring = get_parser_help_recursive(parser, name)
return docstring
Use with cog
We’ll adapt our example project from the previous post.
In this context, we need the project to be installed with pip install -e
into the same venv that contains cog,
so that cog can import it and run its make_parser()
function.
The best way to handle this is to make a real Python package,
and include cogapp
as an optional dependency.
That means we need a few more files this time.
Here’s our directory layout:
exampleapp/
: Our package’s directorypyproject.toml
: A Python project file; this might besetup.py
or whatever insteadreadme.md
: The readme, where we want to include autogenerated helpsrc/exampleapp/
: The package’s source code__init__.py
: Our Python package
And here are the file contents:
pyproject.toml
Note that we include cogapp
in our list of optional development dependencies.
This will automatically install it for us when we do pip install -e .[development]
in a moment.
[project]
name = "exampleapp"
license = { text = "WTFPL" }
readme = "readme.md"
version = "0.0.1"
[project.scripts]
exampleapp = "exampleapp:main"
[project.optional-dependencies]
development = [
"cogapp",
]
[build-system]
requires = ["setuptools"]
build-backend = "setuptools.build_meta"
readme.md
Note the Cog placeholder in comments.
# Example project
This is an example readme for an example project.
## Command options
<!--[[[cog
#
# This section is generated with cog
# Run `cog -r readme.md` and it will overwrite the help output below with help generated from argparse.
#
import cog
from exampleapp import get_argparse_help_string, make_parser
cog.outl(get_argparse_help_string("exampleapp", make_parser(progname="exampleapp")))
]]]-->
<!--[[[end]]]-->
src/exampleapp/__init__.py
A very simply Python package that consists of just one file.
import argparse
def get_argparse_help_string(
name: str, parser: argparse.ArgumentParser, wrap: int = 80
) -> str:
"""Generate a docstring for an argparse parser that shows the help for the parser and all subparsers, recursively.
Based on an idea from <https://github.com/pdoc3/pdoc/issues/89>
Arguments:
* `name`: The name of the program
* `parser`: The parser
* `wrap`: The number of characters to wrap the help text to (0 to disable)
"""
def help_formatter(prog):
return argparse.HelpFormatter(prog, width=wrap)
def get_parser_help_recursive(
parser: argparse.ArgumentParser, cmd: str = "", root: bool = True
):
docstring = ""
if not root:
docstring += "\n" + "_" * 72 + "\n\n"
docstring += f"> {cmd} --help\n"
parser.formatter_class = help_formatter
docstring += parser.format_help()
for action in parser._actions:
if isinstance(action, argparse._SubParsersAction):
for subcmd, subparser in action.choices.items():
docstring += get_parser_help_recursive(
subparser, f"{cmd} {subcmd}", root=False
)
return docstring
docstring = get_parser_help_recursive(parser, name)
return docstring
def make_parser(progname=None) -> argparse.ArgumentParser:
"""Return the ArgumentParser for this program.
progname: The name of the program; tries to guess if not provided.
"""
parser = argparse.ArgumentParser(
prog=progname, description="A program that does something."
)
parser.add_argument(
"--verbose", action="store_true", help="Increase verbosity of output"
)
parser.add_argument(
"--longhelp",
help="This is a very long help text. " * 10,
)
subparsers = parser.add_subparsers()
subparser = subparsers.add_parser("subcommand", help="A subcommand")
subparser.add_argument("--subarg", help="An argument for the subcommand")
return parser
def main():
parser = make_parser()
args = parser.parse_args()
print("Hello from the exampleapp")
print(args)
# ... do whatever in your program
With those files in place, set up a virtual environment:
cd exampleapp
python3 -m venv
. venv/bin/activate
pip install -e '.[development]'
exampleapp
# should print "Hello from exampleapp" and an empty list of arguments
Now all we have to do is follow the instructions in the comment in readme.md
:
cog -r readme.md
And the readme file has been transformed to include the help text!
# Example project
This is an example readme for an example project.
## Command options
<!--[[[cog
#
# This section is generated with cog
# Run `cog -r readme.md` and it will overwrite the help output below with help generated from argparse.
#
import cog
from exampleapp import get_argparse_help_string, make_parser
cog.outl(get_argparse_help_string("exampleapp", make_parser(progname="exampleapp")))
]]]-->
> exampleapp --help
usage: exampleapp [-h] [--verbose] [--longhelp LONGHELP] {subcommand} ...
A program that does something.
positional arguments:
{subcommand}
subcommand A subcommand
options:
-h, --help show this help message and exit
--verbose Increase verbosity of output
--longhelp LONGHELP This is a very long help text. This is a very long help
text. This is a very long help text. This is a very long
help text. This is a very long help text. This is a very
long help text. This is a very long help text. This is a
very long help text. This is a very long help text. This
is a very long help text.
________________________________________________________________________
> exampleapp subcommand --help
usage: exampleapp subcommand [-h] [--subarg SUBARG]
options:
-h, --help show this help message and exit
--subarg SUBARG An argument for the subcommand
<!--[[[end]]]-->
A few difference from last time:
make_parser()
has to be public now, because it will be called outside of this file.make_parser()
now takes aprogname
argument. This is normally not necessary for argparse programs, but argparse gets confused when run by cog and thinks that the program name iscog
, so we have to allow an override.- Text wrapping is simpler per above.