Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Docs and command help improvements #299

Merged
merged 28 commits into from
Jul 20, 2023
Merged
Show file tree
Hide file tree
Changes from 25 commits
Commits
Show all changes
28 commits
Select commit Hold shift + click to select a range
58d10be
docs: Respect HOST and PORT env vars when using `make livehtml`
tsibley Jan 25, 2023
722895d
docs: Watch for source code changes when using `make livehtml`
tsibley Jul 7, 2023
f80cbb2
docs: Mark the "livehtml" target as phony too
tsibley Jul 12, 2023
ed9b83a
docs: Add "serve" target to statically serve the built docs without a…
tsibley Jul 12, 2023
7695e25
argparse: Move walk_commands() out of a test file
tsibley Jul 13, 2023
181c6d5
argparse: Make the "command" parameter of walk_commands() optional
tsibley Jul 13, 2023
f577798
argparse: Don't visit aliased commands twice in walk_commands()
tsibley Jul 12, 2023
f5c7e8c
argparse: Yield the parser for each command from walk_commands()
tsibley Jul 13, 2023
fabd2aa
dev: Test --help-all instead of --help when available
tsibley Jul 12, 2023
efca1bc
remote: Remove outdated note about S3 buckets now that all Groups are…
tsibley Jul 12, 2023
1bfdc49
docs: Fix disagreement of grammatical number
tsibley Jan 26, 2023
8b8a41e
docs: Generate the command doc pages statically instead of dynamically
tsibley Jul 12, 2023
fdd0571
help: Distinguish between `command x y` and ``a b c`` when rendering …
tsibley Jul 13, 2023
19380a6
docs: Add a "command-reference" role for referring to a command's help
tsibley Jul 13, 2023
53057f1
rst: Allow a configurable max width for the formatted text
tsibley Jul 13, 2023
b79c5cf
help: Format text with better width handling
tsibley Jul 13, 2023
77447fd
docs: Use more rST syntax in command/option help…
tsibley Jul 13, 2023
b3d13fc
docs: Generate pages for unlisted commands
tsibley Jul 12, 2023
107e555
rst: Handle :doc:`…/index` references
tsibley Jul 12, 2023
4aaf67b
dev: Show a diff of generated doc when our test checking it's up-to-d…
tsibley Jul 14, 2023
3d87f34
docs: Use a consistent heading for command options
tsibley Jul 19, 2023
c041705
check-setup: Document usage ourselves rather than rely on argparse's …
tsibley Jul 19, 2023
2195a57
dev: Don't force Python 3.6 in devel/setup-venv
tsibley Jul 19, 2023
c215d8e
dev: Expand flake8's exclusions to include any .venv* path
tsibley Jul 19, 2023
4ad5e8e
dev: Skip command doc generation test on Windows
tsibley Jul 19, 2023
a9d0980
dev: Use stricter Sphinx builds
tsibley Jul 19, 2023
bc3a573
login: Document environment variables with the "envvar" directive
tsibley Jul 20, 2023
55bc7b3
CHANGES: Document documentation improvements
tsibley Jul 20, 2023
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion .flake8
Original file line number Diff line number Diff line change
Expand Up @@ -15,7 +15,7 @@ extend-ignore =

exclude =
.git,
.venv,
.venv*,
__pycache__,
build,
dist,
216 changes: 216 additions & 0 deletions devel/generate-command-doc
Original file line number Diff line number Diff line change
@@ -0,0 +1,216 @@
#!/usr/bin/env python3
"""
Generate documentation pages for each Nextstrain CLI command.

Uses the Sphinx ``program`` and ``option`` directives so they can be
cross-referenced. An alternative to using the ``autodoc`` or ``autoprogram``
extensions which dynamically generate rST at Sphinx build time instead.

To generate rST files for all commands::

./devel/generate-command-doc

Files are only written if their contents need updating. The paths of the
updated files are output to stderr.

To instead output (to stdout) the rST for a single command::

./devel/generate-command-doc nextstrain build

This can be useful for development loops.
"""
import os
from argparse import ArgumentParser, SUPPRESS, _SubParsersAction
from contextlib import contextmanager, redirect_stdout
from difflib import diff_bytes, unified_diff
from hashlib import md5
from inspect import cleandoc
from pathlib import Path
from sys import exit, stdout, stderr
from tempfile import TemporaryDirectory
from textwrap import dedent, indent
from typing import Iterable, Tuple, Union

doc = (Path(__file__).resolve().parent.parent / "doc/").relative_to(Path.cwd())
tmp = TemporaryDirectory()

# Force some environment vars before loading any Nextstrain CLI code which may
# inspect them, for reproducible/stable output.
os.environ.update({
# The argparse HelpFormatter cares about terminal size, but we don't want
# that to vary based on where we run this program.
"COLUMNS": "1000",

# Avoid the current user's personal configuration from affecting output.
"NEXTSTRAIN_HOME": tmp.name,

# Ensure we detect a browser for stable `nextstrain view` output.
"BROWSER": "/bin/true",
})

from nextstrain.cli import make_parser
from nextstrain.cli.argparse import HelpFormatter, walk_commands, OPTIONS_TITLE
from nextstrain.cli.debug import debug


# We generate docs for these and they can be directly accessed, but they're
# unlinked so we must note that to suppress warnings from Sphinx.
hidden = {
"nextstrain",
"nextstrain debugger",
"nextstrain init-shell",
}


argparser = ArgumentParser(
prog = "./devel/generate-command-doc",
usage = "./devel/generate-command-doc [--check] [<command> […]]",
description = __doc__,
formatter_class = HelpFormatter)

argparser.add_argument("command_given", nargs = "*", metavar = "<command>", help = "The single command, given as multiple arguments, for which to generate rST.")
argparser.add_argument("--check", action = "store_true", help = "Only check if any file contents have changed; do not update any files. Exits 1 if there are changes, 0 if not.")
argparser.add_argument("--diff", action = "store_true", help = "Show a diff for files that change (or would change, if --check is also specified).")


def main(*, command_given = None, check = False, diff = False):
if check and command_given:
print("error: --check is only supported when updating files for all commands", file = stderr)
return 1

if diff and command_given:
print("error: --diff is only supported when updating files for all commands", file = stderr)
return 1

check_failed = False
command_given = tuple(command_given)

for command, parser in walk_commands(make_parser()):
if command_given and command != command_given:
continue

page = command_page(command, parser)
path = doc / f"{page}.rst"

debug(f"--> {' '.join(command)}")
debug(f"Generating rST…")

rst = generate_rst(command, parser)

if command_given:
print(rst)
else:
new_rst = rst.encode("utf-8")
old_rst = path.read_bytes() if path.exists() else None

new_md5 = md5(new_rst).hexdigest()
old_md5 = md5(old_rst).hexdigest() if old_rst is not None else "0" * 32

debug(f"Old MD5: {old_md5}")
debug(f"New MD5: {new_md5}")

if old_md5 != new_md5:
if check:
check_failed = True
else:
path.write_bytes(new_rst)
debug(f"wrote {len(new_rst):,} bytes ({new_md5}) to {path}")
print(path, file = stderr)

if diff:
stdout.writelines(
unified_diff(
old_rst.decode("utf-8").splitlines(keepends = True) if old_rst is not None else [],
new_rst.decode("utf-8").splitlines(keepends = True),
str(path),
str(path),
old_md5,
new_md5))

else:
debug(f"{path} unchanged")

return 1 if check and check_failed else 0


def command_page(command: Tuple[str, ...], parser: ArgumentParser) -> str:
return (
"commands/"
+ "/".join(command[1:])
+ ("/index" if parser._subparsers else ""))


def generate_rst(command: Tuple[str, ...], parser: ArgumentParser) -> str:
return "\n".join(chunk or "" for chunk in _generate_rst(command, parser))


def _generate_rst(command: Tuple[str, ...], parser: ArgumentParser) -> Iterable[Union[str, None]]:
program = " ".join(command)
formatter = parser.formatter_class(program)
usage = parser.format_usage()
description = cleandoc(parser.description or "")
epilog = cleandoc(parser.epilog or "")

if program in hidden:
yield ":orphan:"
yield

yield ".. default-role:: literal"
yield
yield ".. role:: command-reference(ref)"
yield
yield f".. program:: {program}"
yield
yield f".. _{program}:"
yield
yield "=" * len(program)
yield program
yield "=" * len(program)
yield
yield ".. code-block:: none"
yield
yield indent(usage, " ")
yield
yield description
yield

for group in parser._action_groups:
if not group._group_actions:
continue

title = group.title if group.title and group.title != OPTIONS_TITLE else "options"
description = cleandoc(group.description or "")

yield title
yield "=" * len(title)
yield
yield description
yield

for action in group._group_actions:
if action.help is SUPPRESS:
continue

if isinstance(action, _SubParsersAction):
for choice in action._choices_actions:
subcommand = choice.dest
subparser = action.choices[subcommand]
subpage = command_page((*command, subcommand), subparser)
yield f".. option:: {subcommand}"
yield
yield indent(f"{choice.help}. See :doc:`/{subpage}`.", " ")
yield
else:
invocation = formatter._format_action_invocation(action)
description = (action.help or "") % {"default": action.default}

yield f".. option:: {invocation}"
yield
yield indent(description, " ")
yield

yield epilog


if __name__ == "__main__":
exit(main(**vars(argparser.parse_args())))
2 changes: 1 addition & 1 deletion devel/setup-venv
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,6 @@ venv="$base/.venv"

set -x
rm -rf "$venv"
python3.6 -m venv "$venv"
python3 -m venv "$venv"
"$venv"/bin/pip install --upgrade pip setuptools wheel pip-tools
"$venv"/bin/pip install -e '.[dev]'
14 changes: 12 additions & 2 deletions doc/Makefile
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
# Minimal makefile for Sphinx documentation
#
SHELL := bash -euo pipefail

# You can set these variables from the command line, and also
# from the environment for the first two.
Expand All @@ -8,16 +9,25 @@ SPHINXBUILD ?= sphinx-build
SOURCEDIR = .
BUILDDIR = _build

GENERATE = ../devel/generate-command-doc

# Put it first so that "make" without argument is like "make help".
help:
@$(SPHINXBUILD) -M help "$(SOURCEDIR)" "$(BUILDDIR)" $(SPHINXOPTS) $(O)

.PHONY: help Makefile
.PHONY: help Makefile livehtml

# Catch-all target: route all unknown targets to Sphinx using the new
# "make mode" option. $(O) is meant as a shortcut for $(SPHINXOPTS).
%: Makefile
if [[ $@ != clean ]]; then $(GENERATE); fi
@$(SPHINXBUILD) -M $@ "$(SOURCEDIR)" "$(BUILDDIR)" $(SPHINXOPTS) $(O)

HOST ?= 127.0.0.1
PORT ?= 8000

serve: dirhtml
cd "$(BUILDDIR)/dirhtml" && python3 -m http.server --bind "$(HOST)" "$(PORT)"

livehtml:
sphinx-autobuild -b dirhtml "$(SOURCEDIR)" "$(BUILDDIR)/dirhtml" $(SPHINXOPTS) $(O)
sphinx-autobuild -b dirhtml "$(SOURCEDIR)" "$(BUILDDIR)/dirhtml" --host "$(HOST)" --port "$(PORT)" --watch ../nextstrain/cli --pre-build "$(GENERATE)" $(SPHINXOPTS) $(O)
39 changes: 34 additions & 5 deletions doc/commands/authorization.rst
Original file line number Diff line number Diff line change
@@ -1,9 +1,38 @@
.. default-role:: literal

.. role:: command-reference(ref)

.. program:: nextstrain authorization

.. _nextstrain authorization:

========================
nextstrain authorization
========================

.. argparse::
:module: nextstrain.cli
:func: make_parser
:prog: nextstrain
:path: authorization
.. code-block:: none
usage: nextstrain authorization [-h]
Produce an Authorization header appropriate for nextstrain.org's web API.

This is a development tool unnecessary for normal usage. It's useful for
directly making API requests to nextstrain.org with `curl` or similar
commands. For example::

curl -si https://nextstrain.org/whoami \
--header "Accept: application/json" \
--header @<(nextstrain authorization)

Exits with an error if no one is logged in.

options
=======



.. option:: -h, --help

show this help message and exit

Loading