commands: Add --header and --update options to spack commands

The Spack documentation currently hard-codes some functionality in
`conf.py`, which makes the doc build less "pluggable" for things like
localized doc builds.

In particular, we unconditionally generate an index of commands and a
package list as part of the docs, but those should really only be done if
things are not up to date.

This commit does the following:

- Add `--header` option to `spack commands` so that it can do the work of
  prepending text to its output.

- Add `--update FILE` option to `spack commands` that makes it generate a
  new command index *only* if FILE is out of date w.r.t. commands in the
  Spack source.

- Simplify code in `conf.py` to use these options and only update the
  command index when needed.
This commit is contained in:
Todd Gamblin 2019-05-20 12:44:36 -07:00
parent 43aaf8c404
commit 6380f1917a
4 changed files with 143 additions and 54 deletions

View File

@ -51,36 +51,18 @@
# Set an environment variable so that colify will print output like it would to
# a terminal.
os.environ['COLIFY_SIZE'] = '25x120'
#
# Generate package list using spack command
#
with open('package_list.html', 'w') as plist_file:
subprocess.Popen(
[spack_root + '/bin/spack', 'list', '--format=html'],
stdout=plist_file)
#
# Find all the `cmd-spack-*` references and add them to a command index
#
import spack
import spack.cmd
command_names = spack.cmd.all_commands()
documented_commands = set()
for filename in glob('*rst'):
with open(filename) as f:
for line in f:
match = re.match('.. _cmd-(spack-.*):', line)
if match:
documented_commands.add(match.group(1).strip())
os.environ['COLUMNS'] = '120'
shutil.copy('command_index.in', 'command_index.rst')
with open('command_index.rst', 'a') as index:
subprocess.Popen(
[spack_root + '/bin/spack', 'commands', '--format=rst'] + list(
documented_commands),
stdout=index)
# Generate full package list if needed
subprocess.Popen(
['spack', 'list', '--format=html', '--update=package_list.html'])
# Generate a command index if an update is needed
subprocess.call([
'spack', 'commands',
'--format=rst',
'--header=command_index.in',
'--update=command_index.rst'] + glob('*rst'))
#
# Run sphinx-apidoc
@ -158,6 +140,7 @@ def setup(sphinx):
# built documents.
#
# The short X.Y version.
import spack
version = '.'.join(str(s) for s in spack.spack_version_info[:2])
# The full version, including alpha/beta/rc tags.
release = spack.spack_version

View File

@ -12,8 +12,9 @@
class ArgparseWriter(object):
"""Analyzes an argparse ArgumentParser for easy generation of help."""
def __init__(self):
def __init__(self, out=sys.stdout):
self.level = 0
self.out = out
def _write(self, parser, root=True, level=0):
self.parser = parser
@ -148,8 +149,7 @@ def __init__(self, out=sys.stdout, rst_levels=_rst_levels,
strip_root_prog (bool): if ``True``, strip the base command name
from subcommands in output
"""
super(ArgparseWriter, self).__init__()
self.out = out
super(ArgparseRstWriter, self).__init__(out)
self.rst_levels = rst_levels
self.strip_root_prog = strip_root_prog

View File

@ -6,11 +6,15 @@
from __future__ import print_function
import sys
import os
import re
import argparse
import llnl.util.tty as tty
from llnl.util.argparsewriter import ArgparseWriter, ArgparseRstWriter
from llnl.util.tty.colify import colify
import spack.cmd
import spack.main
from spack.main import section_descriptions
@ -35,8 +39,14 @@ def setup_parser(subparser):
'--format', default='names', choices=formatters,
help='format to be used to print the output (default: names)')
subparser.add_argument(
'documented_commands', nargs=argparse.REMAINDER,
help='list of documented commands to cross-references')
'--header', metavar='FILE', default=None, action='store',
help='prepend contents of FILE to the output (useful for rst format)')
subparser.add_argument(
'--update', metavar='FILE', default=None, action='store',
help='write output to the specified file, if any command is newer')
subparser.add_argument(
'rst_files', nargs=argparse.REMAINDER,
help='list of rst files to search for `_cmd-spack-<cmd>` cross-refs')
class SpackArgparseRstWriter(ArgparseRstWriter):
@ -56,17 +66,18 @@ def usage(self, *args):
class SubcommandWriter(ArgparseWriter):
def begin_command(self, prog):
print(' ' * self.level + prog)
self.out.write(' ' * self.level + prog)
self.out.write('\n')
@formatter
def subcommands(args):
def subcommands(args, out):
parser = spack.main.make_argument_parser()
spack.main.add_all_commands(parser)
SubcommandWriter().write(parser)
SubcommandWriter(out).write(parser)
def rst_index(out=sys.stdout):
def rst_index(out):
out.write('\n')
index = spack.main.index_commands()
@ -94,30 +105,65 @@ def rst_index(out=sys.stdout):
@formatter
def rst(args):
# print an index to each command
rst_index()
print()
def rst(args, out):
# create a parser with all commands
parser = spack.main.make_argument_parser()
spack.main.add_all_commands(parser)
# get documented commands from the command line
documented_commands = set(args.documented_commands)
# extract cross-refs of the form `_cmd-spack-<cmd>:` from rst files
documented_commands = set()
for filename in args.rst_files:
with open(filename) as f:
for line in f:
match = re.match(r'\.\. _cmd-(spack-.*):', line)
if match:
documented_commands.add(match.group(1).strip())
# print an index to each command
rst_index(out)
out.write('\n')
# print sections for each command and subcommand
SpackArgparseRstWriter(documented_commands).write(parser, root=1)
SpackArgparseRstWriter(documented_commands, out).write(parser, root=1)
@formatter
def names(args):
for cmd in spack.cmd.all_commands():
print(cmd)
def names(args, out):
colify(spack.cmd.all_commands(), output=out)
def prepend_header(args, out):
if not args.header:
return
with open(args.header) as header:
out.write(header.read())
def commands(parser, args):
formatter = formatters[args.format]
# Print to stdout
formatters[args.format](args)
return
# check header first so we don't open out files unnecessarily
if args.header and not os.path.exists(args.header):
tty.die("No such file: '%s'" % args.header)
# if we're updating an existing file, only write output if a command
# is newer than the file.
if args.update:
if os.path.exists(args.update):
files = [
spack.cmd.get_module(command).__file__.rstrip('c') # pyc -> py
for command in spack.cmd.all_commands()]
last_update = os.path.getmtime(args.update)
if not any(os.path.getmtime(f) > last_update for f in files):
tty.msg('File is up to date: %s' % args.update)
return
tty.msg('Updating file: %s' % args.update)
with open(args.update, 'w') as f:
prepend_header(args, f)
formatter(args, f)
else:
prepend_header(args, sys.stdout)
formatter(args, sys.stdout)

View File

@ -4,14 +4,14 @@
# SPDX-License-Identifier: (Apache-2.0 OR MIT)
import re
import pytest
from llnl.util.argparsewriter import ArgparseWriter
import spack.cmd
import spack.main
from spack.main import SpackCommand
commands = SpackCommand('commands')
commands = spack.main.SpackCommand('commands')
parser = spack.main.make_argument_parser()
spack.main.add_all_commands(parser)
@ -49,3 +49,63 @@ def begin_command(self, prog):
assert prog in out
assert re.sub(r' ', '-', prog) in out
Subcommands().write(parser)
def test_rst_with_input_files(tmpdir):
filename = tmpdir.join('file.rst')
with filename.open('w') as f:
f.write('''
.. _cmd-spack-fetch:
cmd-spack-list:
.. _cmd-spack-stage:
_cmd-spack-install:
.. _cmd-spack-patch:
''')
out = commands('--format=rst', str(filename))
for name in ['fetch', 'stage', 'patch']:
assert (':ref:`More documentation <cmd-spack-%s>`' % name) in out
for name in ['list', 'install']:
assert (':ref:`More documentation <cmd-spack-%s>`' % name) not in out
def test_rst_with_header(tmpdir):
fake_header = 'this is a header!\n\n'
filename = tmpdir.join('header.txt')
with filename.open('w') as f:
f.write(fake_header)
out = commands('--format=rst', '--header', str(filename))
assert out.startswith(fake_header)
with pytest.raises(spack.main.SpackCommandError):
commands('--format=rst', '--header', 'asdfjhkf')
def test_rst_update(tmpdir):
update_file = tmpdir.join('output')
# not yet created when commands is run
commands('--update', str(update_file))
assert update_file.exists()
with update_file.open() as f:
assert f.read()
# created but older than commands
with update_file.open('w') as f:
f.write('empty\n')
update_file.setmtime(0)
commands('--update', str(update_file))
assert update_file.exists()
with update_file.open() as f:
assert f.read() != 'empty\n'
# newer than commands
with update_file.open('w') as f:
f.write('empty\n')
commands('--update', str(update_file))
assert update_file.exists()
with update_file.open() as f:
assert f.read() == 'empty\n'