From 10268754122d3d069a7d584d15ebe3c2060be8d5 Mon Sep 17 00:00:00 2001 From: Sam Rabin Date: Wed, 21 May 2025 09:52:26 -0600 Subject: [PATCH 1/5] Add script build_docs_to_publish. One command to build all the versions of the docs and move them into a directory structure for publication. Requires the parent repo to have a version_list.py file next to doc-builder. --- .git-blame-ignore-revs | 2 + __init__.py | 0 build_docs_to_publish | 147 ++++++++++++++++++++++++++ doc_builder/build_commands.py | 1 + doc_builder/build_docs.py | 127 ++++++++++++++++------ doc_builder/build_docs_shared_args.py | 67 ++++++++++++ doc_builder/docs_version.py | 39 +++++++ doc_builder/sys_utils.py | 55 +++++++++- test/Makefile | 2 +- test/test_sys_build_docs.py | 2 + test/test_sys_git_current_branch.py | 2 + test/test_unit_cmdline_args.py | 84 +++++++++++++++ test/test_unit_get_build_command.py | 3 +- test/test_unit_get_build_dir.py | 7 +- 14 files changed, 501 insertions(+), 37 deletions(-) create mode 100644 __init__.py create mode 100755 build_docs_to_publish create mode 100644 doc_builder/build_docs_shared_args.py create mode 100644 doc_builder/docs_version.py create mode 100644 test/test_unit_cmdline_args.py diff --git a/.git-blame-ignore-revs b/.git-blame-ignore-revs index f3d6d27..f66f94d 100644 --- a/.git-blame-ignore-revs +++ b/.git-blame-ignore-revs @@ -1 +1,3 @@ ce25647921a8f82c3b5009bdd07a620545b91a0c +8762f2d8834a9ba391b73ca4ac36f3bb19b169ed +04f3a94bdb9fc8c402286ebdc3ff9cb688c1e4b6 diff --git a/__init__.py b/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/build_docs_to_publish b/build_docs_to_publish new file mode 100755 index 0000000..0942bca --- /dev/null +++ b/build_docs_to_publish @@ -0,0 +1,147 @@ +#!/usr/bin/env python3 + +""" +Loop through all versions of the documentation, building each and moving it to a directory for +publication. + +# Adapted from https://www.codingwiththomas.com/blog/my-sphinx-best-practice-for-a-multiversion-documentation-in-different-languages +# (last visited 2025-05-20) +""" + +import sys +import os +import subprocess +import argparse + +# pylint: disable=import-error,no-name-in-module +from doc_builder.build_docs import ( + main as build_docs, +) +from doc_builder.build_docs_shared_args import main as build_docs_shared_args +from doc_builder.sys_utils import get_git_head_or_branch, check_permanent_file + +# Change to the parent director of doc-builder and add to Python path +os.chdir(os.path.join(os.path.dirname(__file__), os.pardir)) +sys.path.insert(0, os.getcwd()) + +# Import our definitions of each documentation version. +# pylint: disable=wrong-import-position +from source.version_list import ( + LATEST_REF, + VERSION_LIST, +) + + +# Path to certain important files +# TODO: Make these cmd-line options +SOURCE = "source" +CONF_PY = os.path.join(SOURCE, "conf.py") +VERSIONS_PY = os.path.join(SOURCE, "version_list.py") +STATIC_DIR = os.path.join(SOURCE, "_static") +TEMPLATES_DIR = os.path.join(SOURCE, "_templates") + + +def checkout_and_build(version, args): + """ + Check out docs for a version and build + """ + + # Get the current branch, or SHA if detached HEAD + orig_ref = get_git_head_or_branch() + + # Some files/directories/submodules must stay the same for all builds. We list these in + # the permanent_files list. + permanent_files = [CONF_PY, VERSIONS_PY, STATIC_DIR, TEMPLATES_DIR, "doc-builder"] + + # Check some things about "permanent" files before checkout + for filename in permanent_files: + check_permanent_file(filename) + + # Check out the git reference of this version (branch name, tag, or commit SHA) + subprocess.check_output("git checkout " + version.ref, shell=True) + + # Check out LATEST_REF version of permanent files + for filename in permanent_files: + subprocess.check_output(f"git checkout {LATEST_REF} -- {filename}", shell=True) + + # Build the docs for this version + build_args = [ + "-r", + args.repo_root, + "-v", + version.short_name, + "--version-display-name", + version.display_name, + "--versions", + "--site-root", + args.site_root, + "--clean", + ] + if args.build_with_docker: + build_args += ["-d"] + print(" ".join(build_args)) + build_docs(build_args) + + # Go back to original git status. + # 1. Get the current ref's version of doc-builder to avoid "would be overwritten by checkout" + # errors. + subprocess.check_output("git submodule update --checkout doc-builder", shell=True) + # 2. Check out the original git ref (branch or commit SHA) + subprocess.check_output("git checkout " + orig_ref, shell=True) + # 3. Restore the current version's doc-builder + subprocess.check_output("git submodule update --checkout doc-builder", shell=True) + + +def check_version_list(): + """ + Check version list for problems + """ + has_default = False + for version in VERSION_LIST: + # Expect at most one version with landing_version True + if version.landing_version: + if has_default: + raise RuntimeError( + "Expected at most one version with landing_version True" + ) + has_default = True + + +def main(): + + # Set up parser + parser = argparse.ArgumentParser() + + # Arguments shared with build_docs + parser = build_docs_shared_args(parser) + + # Custom arguments for build_docs_to_publish + parser.add_argument( + "--publish-dir", + default="_publish", + help="Where the docs should be moved after being built", + ) + + # Parse arguments + args = parser.parse_args() + + # Check version list for problems + check_version_list() + + # Loop over all documentation versions + for version in VERSION_LIST: + # Build this version + checkout_and_build(version, args) + + # Copy this version to the publication directory + src = os.path.join(args.repo_root, "versions", version.short_name, "html") + if version.landing_version: + dst = args.publish_dir + else: + dst = os.path.join(args.publish_dir, version.short_name) + os.makedirs(dst) + subprocess.check_output(f"mv '{src}'/* '{dst}'/", shell=True) + + +if __name__ == "__main__": + main() diff --git a/doc_builder/build_commands.py b/doc_builder/build_commands.py index fdbdd99..4a794b1 100644 --- a/doc_builder/build_commands.py +++ b/doc_builder/build_commands.py @@ -65,6 +65,7 @@ def get_build_dir(build_dir=None, repo_root=None, version=None): def get_build_command( + *, build_dir, run_from_dir, build_target, diff --git a/doc_builder/build_docs.py b/doc_builder/build_docs.py index ef4b450..6481060 100644 --- a/doc_builder/build_docs.py +++ b/doc_builder/build_docs.py @@ -8,12 +8,30 @@ import random import string import sys +from urllib.parse import urlparse import signal + +# pylint: disable=import-error,no-name-in-module from doc_builder.build_commands import ( get_build_dir, get_build_command, DEFAULT_DOCKER_IMAGE, ) +from doc_builder.build_docs_shared_args import bd_dir_group, bd_parser + + +def is_web_url(url_string): + """ + Checks if a string is a valid web URL. + + Args: + url_string: The string to check. + + Returns: + True if the string is a valid web URL, False otherwise. + """ + result = urlparse(url_string) + return all([result.scheme, result.netloc]) def commandline_options(cmdline_args=None): @@ -73,14 +91,11 @@ def commandline_options(cmdline_args=None): help="Full path to the directory in which the doc build should go.", ) - dir_group.add_argument( - "-r", - "--repo-root", - default=None, - help="Root directory of the repository holding documentation builds.\n" - "(If there are other path elements between the true repo root and\n" - "the 'versions' directory, those should be included in this path.)", - ) + # Add argument(s) to dir_group that are also in build_docs_to_publish's parser + dir_group = bd_dir_group(dir_group) + + # Add argument(s) to parser that are also in build_docs_to_publish's parser + parser = bd_parser(parser) parser.add_argument( "-v", @@ -95,24 +110,13 @@ def commandline_options(cmdline_args=None): ) parser.add_argument( - "-c", "--clean", action="store_true", help="Before building, run 'make clean'." + "--version-display-name", + default=None, + help="Version name for display in dropdown menu. If absent, uses -v/--version.", ) parser.add_argument( - "-d", - "--build-with-docker", - action="store_true", - help="Use a Docker container to build the documentation,\n" - "rather than relying on locally-installed versions of Sphinx, etc.\n" - "This assumes that Docker is installed and running on your system.\n" - "\n" - "NOTE: This mounts your home directory in the Docker image.\n" - "Therefore, both the current directory (containing the Makefile for\n" - "building the documentation) and the documentation build directory\n" - "must reside somewhere within your home directory." - "\n" - f"Default image: {DEFAULT_DOCKER_IMAGE}\n" - "This can be changed with -i/--docker-image.", + "-c", "--clean", action="store_true", help="Before building, run 'make clean'." ) parser.add_argument( @@ -136,6 +140,12 @@ def commandline_options(cmdline_args=None): help="Number of parallel jobs to use for the make process.\n" "Default is 4.", ) + parser.add_argument( + "--versions", + action="store_true", + help="Build multiple versions of the docs, with drop-down switcher menu.", + ) + parser.add_argument( "-w", "--warnings-as-warnings", @@ -144,6 +154,18 @@ def commandline_options(cmdline_args=None): ) options = parser.parse_args(cmdline_args) + + print(f"options: {options}") + + if options.versions: + if not options.site_root: + raise RuntimeError( + "--site-root must be provided when --versions is enabled" + ) + if not is_web_url(options.site_root) and not os.path.isabs(options.site_root): + raise RuntimeError( + f"--site-root is neither a web URL nor an absolute path: '{options.site_root}'" + ) if options.docker_image: options.docker_image = options.docker_image.lower() @@ -154,12 +176,53 @@ def commandline_options(cmdline_args=None): return options -def run_build_command(build_command, version): +def setup_env_var(build_command, env, env_var, value, docker): + """ + Set up an environment variable, depending on whether using Docker or not + """ + if docker: + # Need to pass to Docker via the build command + build_command.insert(-3, "-e") + build_command.insert(-3, f"{env_var}={value}") + else: + env[env_var] = value + return build_command, env + + +def run_build_command(build_command, version, options): """Echo and then run the given build command""" - build_command_str = " ".join(build_command) - print(build_command_str) env = os.environ.copy() - env["current_version"] = version + + # Set version display name (in drop-down menu) + if options.version_display_name: + value = options.version_display_name + else: + value = version + build_command, env = setup_env_var( + build_command, env, "version_display_name", value, options.build_with_docker + ) + + # Things to do/set based on whether including version dropdown + if options.versions: + version_dropdown = "True" + build_command, env = setup_env_var( + build_command, + env, + "pages_root", + options.site_root, + options.build_with_docker, + ) + else: + version_dropdown = "" + build_command, env = setup_env_var( + build_command, + env, + "version_dropdown", + version_dropdown, + options.build_with_docker, + ) + + print(" ".join(build_command)) subprocess.check_call(build_command, env=env) @@ -169,7 +232,9 @@ def setup_for_docker(): Returns a name that should be used in the docker run command """ - docker_name = "build_docs_" + "".join(random.choice(string.ascii_lowercase) for _ in range(8)) + docker_name = "build_docs_" + "".join( + random.choice(string.ascii_lowercase) for _ in range(8) + ) # It seems that, if we kill the build_docs process with Ctrl-C, the docker process # continues. Handle that by implementing a signal handler. There may be a better / @@ -227,7 +292,9 @@ def main(cmdline_args=None): docker_name=docker_name, docker_image=opts.docker_image, ) - run_build_command(build_command=clean_command, version=version) + run_build_command( + build_command=clean_command, version=version, options=opts + ) build_command = get_build_command( build_dir=build_dir, @@ -239,4 +306,4 @@ def main(cmdline_args=None): docker_image=opts.docker_image, warnings_as_warnings=opts.warnings_as_warnings, ) - run_build_command(build_command=build_command, version=version) + run_build_command(build_command=build_command, version=version, options=opts) diff --git a/doc_builder/build_docs_shared_args.py b/doc_builder/build_docs_shared_args.py new file mode 100644 index 0000000..a380b26 --- /dev/null +++ b/doc_builder/build_docs_shared_args.py @@ -0,0 +1,67 @@ +""" +build_docs and build_docs_to_publish share some args. This module adds them to a parser or parser +group. +""" + +# pylint: disable=import-error,no-name-in-module +from .build_commands import DEFAULT_DOCKER_IMAGE + + +def bd_parser(parser, site_root_required=False): + """ + Add arguments that build_docs has in its overall parser. + + # site_root_required: Should be True from build_docs_to_publish, False from build_docs + """ + parser.add_argument( + "--site-root", + required=site_root_required, + help="URL or absolute file path that should contain the top-level index.html", + ) + parser.add_argument( + "-d", + "--build-with-docker", + action="store_true", + help="Use a Docker container to build the documentation,\n" + "rather than relying on locally-installed versions of Sphinx, etc.\n" + "This assumes that Docker is installed and running on your system.\n" + "\n" + "NOTE: This mounts your home directory in the Docker image.\n" + "Therefore, both the current directory (containing the Makefile for\n" + "building the documentation) and the documentation build directory\n" + "must reside somewhere within your home directory." + "\n" + f"Default image: {DEFAULT_DOCKER_IMAGE}\n" + "This can be changed with -i/--docker-image.", + ) + return parser + + +def bd_dir_group(parser_or_group, repo_root_default=None): + """ + Add arguments that build_docs has in its dir_group + """ + parser_or_group.add_argument( + "-r", + "--repo-root", + default=repo_root_default, + help="Root directory of the repository holding documentation builds.\n" + "(If there are other path elements between the true repo root and\n" + "the 'versions' directory, those should be included in this path.)", + ) + return parser_or_group + + +def main(parser): + """ + Add all arguments to parser, even if build_docs has them in dir_group + """ + + # Settings for build_docs_to_publish, because main() should only ever be called from there + site_root_required = True + repo_root_default = "_build" + + parser = bd_parser(parser, site_root_required) + parser = bd_dir_group(parser, repo_root_default) + + return parser diff --git a/doc_builder/docs_version.py b/doc_builder/docs_version.py new file mode 100644 index 0000000..77ae176 --- /dev/null +++ b/doc_builder/docs_version.py @@ -0,0 +1,39 @@ +""" +A class defining characteristics of a documentation version +""" + + +class DocsVersion: + """ + A class defining characteristics of a documentation version + """ + + # pylint: disable=too-few-public-methods,too-many-arguments + def __init__( + self, + *, + short_name, + display_name, + ref, + landing_version=False, + ): + + # The name of this version in file/URL paths + self.short_name = short_name + + # What gets shown in the dropdown menu + self.display_name = display_name + + # Whether this version should be the one on the landing page (i.e., default version) + self.landing_version = landing_version + + # Branch, tag, or commit SHA + self.ref = ref + + def subdir(self): + """ + Get the subdirectory under --publish-dir where this version's HTML will be moved + """ + if self.landing_version: + return "" + return self.short_name diff --git a/doc_builder/sys_utils.py b/doc_builder/sys_utils.py index c562bce..738d04a 100644 --- a/doc_builder/sys_utils.py +++ b/doc_builder/sys_utils.py @@ -6,6 +6,57 @@ import os +def check_permanent_file(filename): + """ + Check a "permanent" file (one that we don't want to change between doc version builds) + """ + + # Ensure file exists + if not os.path.exists(filename): + raise FileNotFoundError(filename) + + # Error if file contains uncommitted changes + cmd = f"git add . && git diff --quiet {filename} && git diff --cached --quiet {filename}" + try: + subprocess.check_output(cmd, shell=True) + except subprocess.CalledProcessError as e: + subprocess.check_output( + "git reset", shell=True + ) # Unstage files staged by `git add` + msg = f"Important file/submodule may contain uncommitted changes: '{filename}'" + raise RuntimeError(msg) from e + + +def get_git_head_or_branch(): + """ + Get the name of the current branch. If detached HEAD, get current commit SHA. + """ + try: + result = subprocess.run( + ["git", "symbolic-ref", "--short", "-q", "HEAD"], + stdout=subprocess.PIPE, + stderr=subprocess.DEVNULL, + text=True, + check=True, + ) + output = result.stdout.strip() + except subprocess.CalledProcessError: + output = "" + + if not output: + # Fallback to commit SHA + result = subprocess.run( + ["git", "rev-parse", "HEAD"], + stdout=subprocess.PIPE, + stderr=subprocess.DEVNULL, + text=True, + check=True, + ) + output = result.stdout.strip() + + return output + + def git_current_branch(): """Determines the name of the current git branch @@ -19,7 +70,9 @@ def git_current_branch(): try: # Suppress stderr because we don't want to clutter output with # git's message, e.g., if we're not in a git repository. - branch_name = subprocess.check_output(cmd, stderr=devnull, universal_newlines=True) + branch_name = subprocess.check_output( + cmd, stderr=devnull, universal_newlines=True + ) except subprocess.CalledProcessError: branch_found = False branch_name = "" diff --git a/test/Makefile b/test/Makefile index c329f75..aca6a89 100644 --- a/test/Makefile +++ b/test/Makefile @@ -35,7 +35,7 @@ PYLINT_ARGS=-j 2 --rcfile=.pylint.rc # source files SRC = \ - ../build_docs \ + ../build_docs* \ ../doc_builder/*.py TEST_DIR = . diff --git a/test/test_sys_build_docs.py b/test/test_sys_build_docs.py index f1c5d97..aee5dd2 100644 --- a/test/test_sys_build_docs.py +++ b/test/test_sys_build_docs.py @@ -7,6 +7,8 @@ import tempfile import shutil import os + +# pylint: disable=import-error,no-name-in-module from test.test_utils.git_helpers import ( make_git_repo, add_git_commit, diff --git a/test/test_sys_git_current_branch.py b/test/test_sys_git_current_branch.py index 6305725..8a7c414 100644 --- a/test/test_sys_git_current_branch.py +++ b/test/test_sys_git_current_branch.py @@ -9,6 +9,8 @@ import tempfile import shutil import os + +# pylint: disable=import-error,no-name-in-module from test.test_utils.git_helpers import ( make_git_repo, add_git_commit, diff --git a/test/test_unit_cmdline_args.py b/test/test_unit_cmdline_args.py new file mode 100644 index 0000000..605f395 --- /dev/null +++ b/test/test_unit_cmdline_args.py @@ -0,0 +1,84 @@ +#!/usr/bin/env python3 + +"""Unit test driver for command-line arg parsing""" + +import unittest + +import os +from doc_builder.build_docs import is_web_url, commandline_options + + +class TestCmdlineArgs(unittest.TestCase): + """Test the command-line arguments and parsing""" + + # Allow long method names + # pylint: disable=invalid-name + + def setUp(self): + """Run this before each test""" + self.fake_builddir = ["-b", "abc"] + self.fake_abspath = os.path.sep + os.path.join("some", "abs", "path") + self.fake_relpath = os.path.join(os.pardir, "some", "rel", "path") + self.fake_url = "https://www.google.com" + + def test_is_web_url(self): + """Ensure that all these are valid web URLs, even if they don't exist""" + urls = [ + "https://www.google.com", + "http://example.com", + "ftp://fileserver.com", + "https://www.google.com/path/to/resource?query=string#fragment", + "https://user:password@www.example.com:8080/path/to/resource?query=string#fragment", + ] + for url in urls: + print(url) + self.assertTrue(is_web_url(url)) + + def test_isnt_web_url(self): + """Ensure that all these are NOT valid web URLs""" + urls = [ + "www.example.com", + "invalid url", + self.fake_abspath, + self.fake_relpath, + ] + + for url in urls: + print(url) + self.assertFalse(is_web_url(url)) + + def test_no_versions_no_siteroot(self): + """Ensure no error when you don't provide --versions or --siteroot""" + commandline_options(self.fake_builddir) + + def test_versions_and_siteroot_abs(self): + """Ensure no error when you provide --versions and an absolute path for --site-root""" + commandline_options(self.fake_builddir + ["--versions", "--site-root", self.fake_abspath]) + + def test_versions_and_siteroot_url(self): + """Ensure no error when you provide --versions and a URL for --site-root""" + commandline_options(self.fake_builddir + ["--versions", "--site-root", self.fake_url]) + + def test_versions_and_siteroot_rel_error(self): + """Ensure error when you provide --versions and a valid relative path for --site-root""" + msg = "--site-root is neither a web URL nor an absolute path" + with self.assertRaisesRegex(RuntimeError, msg): + commandline_options( + self.fake_builddir + ["--versions", "--site-root", self.fake_relpath] + ) + + def test_versions_and_siteroot_neither_error(self): + """Ensure error when you provide --versions and just some string for --site-root""" + msg = "--site-root is neither a web URL nor an absolute path" + with self.assertRaisesRegex(RuntimeError, msg): + commandline_options(self.fake_builddir + ["--versions", "--site-root", "abc123"]) + + def test_versions_but_no_siteroot_error(self): + """Ensure error when you provide --versions but not --site-root""" + msg = "--site-root must be provided when --versions is enabled" + with self.assertRaisesRegex(RuntimeError, msg): + commandline_options(self.fake_builddir + ["--versions"]) + + +if __name__ == "__main__": + unittest.main() diff --git a/test/test_unit_get_build_command.py b/test/test_unit_get_build_command.py index f0cea6e..cbfb12b 100644 --- a/test/test_unit_get_build_command.py +++ b/test/test_unit_get_build_command.py @@ -1,7 +1,6 @@ #!/usr/bin/env python3 -"""Unit test driver for get_build_command function -""" +"""Unit test driver for get_build_command function""" import os import unittest diff --git a/test/test_unit_get_build_dir.py b/test/test_unit_get_build_dir.py index 247fa16..de3dfc9 100644 --- a/test/test_unit_get_build_dir.py +++ b/test/test_unit_get_build_dir.py @@ -1,7 +1,6 @@ #!/usr/bin/env python3 -"""Unit test driver for get_build_dir function -""" +"""Unit test driver for get_build_dir function""" import shutil import unittest @@ -13,7 +12,9 @@ # For python3 from unittest import mock import os -from test.test_utils.sys_utils_fake import make_fake_isdir +from test.test_utils.sys_utils_fake import ( + make_fake_isdir, +) # pylint: disable=import-error,no-name-in-module from doc_builder.build_commands import get_build_dir From de3738b390268cb54bde295e5c5ce5c65eb955d9 Mon Sep 17 00:00:00 2001 From: Sam Rabin Date: Thu, 22 May 2025 22:20:06 -0600 Subject: [PATCH 2/5] Move conf.py, _static/, and _templates/ here. New options to build_docs* scripts allow users to specify custom paths if they don't want to use the ones built in to doc-builder. Requires the parent repo to have a substitutions.py file next to doc-builder. --- .git-blame-ignore-revs | 1 + _static/css/custom.css | 17 +++ _templates/footer.html | 5 + _templates/landing.index.html | 6 + _templates/versions.html | 26 ++++ build_docs_to_publish | 16 +- conf.py | 208 ++++++++++++++++++++++++++ doc_builder/build_commands.py | 26 +++- doc_builder/build_docs.py | 9 ++ doc_builder/build_docs_shared_args.py | 16 ++ test/conf.py | 201 +++++++++++++++++++++++++ test/test_unit_get_build_command.py | 48 +++++- 12 files changed, 568 insertions(+), 11 deletions(-) create mode 100644 _static/css/custom.css create mode 100644 _templates/footer.html create mode 100644 _templates/landing.index.html create mode 100644 _templates/versions.html create mode 100644 conf.py create mode 100644 test/conf.py diff --git a/.git-blame-ignore-revs b/.git-blame-ignore-revs index f66f94d..4c73ce1 100644 --- a/.git-blame-ignore-revs +++ b/.git-blame-ignore-revs @@ -1,3 +1,4 @@ ce25647921a8f82c3b5009bdd07a620545b91a0c 8762f2d8834a9ba391b73ca4ac36f3bb19b169ed 04f3a94bdb9fc8c402286ebdc3ff9cb688c1e4b6 +81b7b35c4e07551459a98b7be17b6b6400d12e3a diff --git a/_static/css/custom.css b/_static/css/custom.css new file mode 100644 index 0000000..10abb45 --- /dev/null +++ b/_static/css/custom.css @@ -0,0 +1,17 @@ +/* Make equation numbers float to the right */ +.eqno { + margin-left: 5px; + float: right; +} +/* Hide the link... */ +.math .headerlink { + display: none; + visibility: hidden; +} +/* ...unless the equation is hovered */ +.math:hover .headerlink { + display: inline-block; + visibility: visible; + /* Place link in margin and keep equation number aligned with boundary */ + margin-right: -0.7em; +} diff --git a/_templates/footer.html b/_templates/footer.html new file mode 100644 index 0000000..a7c22a3 --- /dev/null +++ b/_templates/footer.html @@ -0,0 +1,5 @@ +{% extends "!footer.html" %} +{% block extrafooter %} + {{ super() }} + +{% endblock %} diff --git a/_templates/landing.index.html b/_templates/landing.index.html new file mode 100644 index 0000000..1d2002e --- /dev/null +++ b/_templates/landing.index.html @@ -0,0 +1,6 @@ + + + + + + \ No newline at end of file diff --git a/_templates/versions.html b/_templates/versions.html new file mode 100644 index 0000000..d219003 --- /dev/null +++ b/_templates/versions.html @@ -0,0 +1,26 @@ +
+ + Version: {{ current_version }} + + +
+ {% if languages|length >= 1 %} +
+
{{ _('Languages') }}
+ {% for the_language, url in languages %} +
{{ the_language }}
+ {% endfor %} +
+ {% endif %} + {% if versions|length >= 1 %} +
+
{{ _('Versions') }}
+ {% for the_version, url in versions %} +
{{ the_version }}
+ {% endfor %} +
+ {% endif %} +
+ +
+
diff --git a/build_docs_to_publish b/build_docs_to_publish index 0942bca..f8e87c0 100755 --- a/build_docs_to_publish +++ b/build_docs_to_publish @@ -26,7 +26,7 @@ sys.path.insert(0, os.getcwd()) # Import our definitions of each documentation version. # pylint: disable=wrong-import-position -from source.version_list import ( +from version_list import ( LATEST_REF, VERSION_LIST, ) @@ -35,10 +35,8 @@ from source.version_list import ( # Path to certain important files # TODO: Make these cmd-line options SOURCE = "source" -CONF_PY = os.path.join(SOURCE, "conf.py") -VERSIONS_PY = os.path.join(SOURCE, "version_list.py") -STATIC_DIR = os.path.join(SOURCE, "_static") -TEMPLATES_DIR = os.path.join(SOURCE, "_templates") +VERSIONS_PY = os.path.join("version_list.py") +MAKEFILE = "Makefile" def checkout_and_build(version, args): @@ -51,7 +49,7 @@ def checkout_and_build(version, args): # Some files/directories/submodules must stay the same for all builds. We list these in # the permanent_files list. - permanent_files = [CONF_PY, VERSIONS_PY, STATIC_DIR, TEMPLATES_DIR, "doc-builder"] + permanent_files = [VERSIONS_PY, "doc-builder", MAKEFILE] # Check some things about "permanent" files before checkout for filename in permanent_files: @@ -79,6 +77,12 @@ def checkout_and_build(version, args): ] if args.build_with_docker: build_args += ["-d"] + if args.conf_py_path: + build_args += ["--conf-py-path", args.conf_py_path] + if args.static_path: + build_args += ["--static-path", args.static_path] + if args.templates_path: + build_args += ["--templates-path", args.templates_path] print(" ".join(build_args)) build_docs(build_args) diff --git a/conf.py b/conf.py new file mode 100644 index 0000000..cce38a3 --- /dev/null +++ b/conf.py @@ -0,0 +1,208 @@ +# -*- coding: utf-8 -*- +# +# This file is execfile()d with the current directory set to its +# containing dir. +# +# Note that not all possible configuration values are present in this +# autogenerated file. +# +# All configuration values have a default; values that are commented out +# serve to show the default. + +# If extensions (or modules to document with autodoc) are in another directory, +# add these directories to sys.path here. If the directory is relative to the +# documentation root, use os.path.abspath to make it absolute, like shown here. +# +import os +import sys +import sphinx_rtd_theme + +# Assumes substitutions.py and version_list.py are in the parent dir of doc-builder +# pylint: disable=wrong-import-position +dir2add = os.path.join(os.path.dirname(__file__), os.pardir) +sys.path.insert(0, dir2add) +import substitutions as subs # pylint: disable=import-error +from version_list import VERSION_LIST # pylint: disable=import-error + + +# -- General configuration ------------------------------------------------ + +# If your documentation needs a minimal Sphinx version, state it here. +# +# needs_sphinx = '1.0' + +# Add any Sphinx extension module names here, as strings. They can be +# extensions coming with Sphinx (named 'sphinx.ext.*') or your custom +# ones. +extensions = ['sphinx.ext.intersphinx', + 'sphinx.ext.autodoc', + 'sphinx.ext.todo', + 'sphinx.ext.coverage', + 'sphinx.ext.githubpages', + 'sphinx_mdinclude', + ] + +# Add any paths that contain templates here, relative to this directory. +if os.environ["templates_path"]: + templates_path = [os.environ["templates_path"]] + if not all(os.path.isdir(x) for x in templates_path): + raise RuntimeError(f"Some member of templates_path does not exist: {templates_path}") + +# The suffix(es) of source filenames. +# You can specify multiple suffix as a list of string: +# +source_suffix = ['.rst', '.md'] +# source_suffix = '.rst' + +# The master toctree document. +source_start_file = 'index' + +# Save standard Sphinx substitution vars separately +project = subs.project +copyright = subs.copyright # pylint: disable=redefined-builtin +author = subs.author +version = subs.version +release = subs.release + +# version_label is not a standard sphinx variable, so we need some custom rst to allow +# pages to use it. We need a separate replacement for the bolded version because it +# doesn't work to have variable replacements within formatting. +rst_epilog = """ +.. |version_label| replace:: {version_label} +.. |version_label_bold| replace:: **{version_label}** +""".format(version_label=subs.version_label) + +# The language for content autogenerated by Sphinx. Refer to documentation +# for a list of supported languages. +# +# This is also used if you do content translation via gettext catalogs. +# Usually you set "language" from the command line for these cases. +language = "en" + +# List of patterns, relative to source directory, that match files and +# directories to ignore when looking for source files. +# This patterns also effect to html_static_path and html_extra_path +exclude_patterns = [] + +# The name of the Pygments (syntax highlighting) style to use. +pygments_style = 'sphinx' + +# If true, `todo` and `todoList` produce output, else they produce nothing. +todo_include_todos = True + +# -- Options for HTML output ---------------------------------------------- + +# The theme to use for HTML and HTML Help pages. See the documentation for +# a list of builtin themes. +# +html_theme = 'sphinx_rtd_theme' + +# Theme options are theme-specific and customize the look and feel of a theme +# further. For a list of options available for each theme, see the +# documentation. + +# Add any paths that contain custom static files (such as style sheets) here, +# relative to this directory. They are copied after the builtin static files, +# so a file named "default.css" will overwrite the builtin "default.css". +html_static_path = [os.environ["html_static_path"]] + + +# -- Options for HTMLHelp output ------------------------------------------ + +if getattr(subs, "htmlhelp", False): + htmlhelp_basename = subs.htmlhelp["basename"] + + +# -- Options for LaTeX output --------------------------------------------- +if getattr(subs, "latex", False): + + latex_elements = { + # The paper size ('letterpaper' or 'a4paper'). + # + # 'papersize': 'letterpaper', + + # The font size ('10pt', '11pt' or '12pt'). + # + # 'pointsize': '10pt', + + # Additional stuff for the LaTeX preamble. + # + 'preamble': '\\usepackage{hyperref}', + + 'fncychap': '\\usepackage[Conny]{fncychap}', + + # Latex figure (float) alignment + # + # 'figure_align': 'htbp', + } + + # Grouping the document tree into LaTeX files. List of tuples + # (source start file, target name, title, + # author, documentclass [howto, manual, or own class]). + latex_documents = [( + source_start_file, + subs.latex["target_name"], + subs.latex["title"], + author, + subs.latex["category"], + )] + + +# Options for manual page and Texinfo output +if getattr(subs, "mantex", False): + + # One entry per manual page. List of tuples + # (source start file, name, title, authors, manual section). + man_pages = [ + (source_start_file, subs.mantex["name"], subs.mantex["title"], [author], 1), + ] + + if getattr(subs, "tex", False): + # Grouping the document tree into Texinfo files. List of tuples + # (source start file, target name, title, author, + # dir menu entry, description, category) + texinfo_documents = [( + source_start_file, + subs.mantex["name"], + subs.mantex["title"], + author, + subs.tex["dirmenu_entry"], + subs.tex["description"], + subs.tex["category"]), + ] + +# Example configuration for intersphinx: refer to the Python standard library. +intersphinx_mapping = {'python': ('https://docs.python.org/', None)} + +numfig = True +numfig_format = {'figure': 'Figure %s', + 'table': 'Table %s', + 'code-block': 'Code %s', + 'section': '%s', + } +numfig_secnum_depth = 2 + +def setup(app): + app.add_css_file('css/custom.css') + +try: + html_context +except NameError: + html_context = dict() + +html_context["display_lower_left"] = True + +# Whether to show the version dropdown. If not set as environment variable, or environment variable +# is Python-falsey, do not show it. +version_dropdown = os.environ.get("version_dropdown") + +if version_dropdown: + html_context["current_version"] = os.environ["version_display_name"] + + html_context["versions"] = [] + pages_root = os.environ["pages_root"] + for this_version in VERSION_LIST: + html_context["versions"].append([ + this_version.display_name, + os.path.join(pages_root, this_version.subdir()), + ]) diff --git a/doc_builder/build_commands.py b/doc_builder/build_commands.py index 4a794b1..0fbd325 100644 --- a/doc_builder/build_commands.py +++ b/doc_builder/build_commands.py @@ -74,6 +74,9 @@ def get_build_command( docker_name=None, warnings_as_warnings=False, docker_image=DEFAULT_DOCKER_IMAGE, + conf_py_path=None, + static_path=None, + templates_path=None, ): # pylint: disable=too-many-arguments,too-many-locals """Return a string giving the build command. @@ -94,6 +97,7 @@ def get_build_command( build_target=build_target, num_make_jobs=num_make_jobs, warnings_as_warnings=warnings_as_warnings, + conf_py_path=conf_py_path, ) # But if we're using Docker, we have more work to do to create the command.... @@ -103,7 +107,9 @@ def get_build_command( # check this assumption below). docker_mountpoint = os.path.expanduser("~") - errmsg_if_not_under_mountpoint = "build_docs must be run from somewhere in your home directory" + errmsg_if_not_under_mountpoint = ( + "build_docs must be run from somewhere in your home directory" + ) docker_workdir = _docker_path_from_local_path( local_path=run_from_dir, docker_mountpoint=docker_mountpoint, @@ -129,6 +135,7 @@ def get_build_command( build_target=build_target, num_make_jobs=num_make_jobs, warnings_as_warnings=warnings_as_warnings, + conf_py_path=conf_py_path, ) docker_command = [ @@ -151,7 +158,9 @@ def get_build_command( return docker_command -def _get_make_command(build_dir, build_target, num_make_jobs, warnings_as_warnings): +def _get_make_command( + build_dir, build_target, num_make_jobs, warnings_as_warnings, conf_py_path +): """Return the make command to run (as a list) Args: @@ -162,11 +171,20 @@ def _get_make_command(build_dir, build_target, num_make_jobs, warnings_as_warnin builddir_arg = f"BUILDDIR={build_dir}" sphinxopts = "SPHINXOPTS=" if not warnings_as_warnings: - sphinxopts += "-W --keep-going" + sphinxopts += "-W --keep-going " + if conf_py_path: + if not os.path.exists(conf_py_path): + raise FileNotFoundError(f"--conf-py-path not found: '{conf_py_path}'") + if not os.path.isdir(conf_py_path): + conf_py_path = os.path.dirname(conf_py_path) + sphinxopts += f"-c '{conf_py_path}' " + sphinxopts = sphinxopts.rstrip() return ["make", sphinxopts, builddir_arg, "-j", str(num_make_jobs), build_target] -def _docker_path_from_local_path(local_path, docker_mountpoint, errmsg_if_not_under_mountpoint): +def _docker_path_from_local_path( + local_path, docker_mountpoint, errmsg_if_not_under_mountpoint +): """Given a path on the local file system, return the equivalent path in Docker space Args: diff --git a/doc_builder/build_docs.py b/doc_builder/build_docs.py index 6481060..608b5f7 100644 --- a/doc_builder/build_docs.py +++ b/doc_builder/build_docs.py @@ -202,6 +202,14 @@ def run_build_command(build_command, version, options): build_command, env, "version_display_name", value, options.build_with_docker ) + # Set paths to certain directories + build_command, env = setup_env_var( + build_command, env, "html_static_path", options.static_path, options.build_with_docker + ) + build_command, env = setup_env_var( + build_command, env, "templates_path", options.templates_path, options.build_with_docker + ) + # Things to do/set based on whether including version dropdown if options.versions: version_dropdown = "True" @@ -305,5 +313,6 @@ def main(cmdline_args=None): docker_name=docker_name, docker_image=opts.docker_image, warnings_as_warnings=opts.warnings_as_warnings, + conf_py_path=opts.conf_py_path, ) run_build_command(build_command=build_command, version=version, options=opts) diff --git a/doc_builder/build_docs_shared_args.py b/doc_builder/build_docs_shared_args.py index a380b26..981022a 100644 --- a/doc_builder/build_docs_shared_args.py +++ b/doc_builder/build_docs_shared_args.py @@ -3,6 +3,7 @@ group. """ +import os # pylint: disable=import-error,no-name-in-module from .build_commands import DEFAULT_DOCKER_IMAGE @@ -34,6 +35,21 @@ def bd_parser(parser, site_root_required=False): f"Default image: {DEFAULT_DOCKER_IMAGE}\n" "This can be changed with -i/--docker-image.", ) + parser.add_argument( + "--conf-py-path", + help="Path to conf.py", + default=None, + ) + parser.add_argument( + "--static-path", + help="Path to _static/. If relative, must be relative to conf.py.", + default="_static", + ) + parser.add_argument( + "--templates-path", + help="Path to _templates/. If relative, must be relative to conf.py.", + default="_templates", + ) return parser diff --git a/test/conf.py b/test/conf.py new file mode 100644 index 0000000..baa9d2d --- /dev/null +++ b/test/conf.py @@ -0,0 +1,201 @@ +# -*- coding: utf-8 -*- +# +# A copy of ../conf.py but with some lines changed to make new relpaths work: +# dir2add = ... +# templates_path = ... +# html_static_path = ... + +# +import os +import sys +import sphinx_rtd_theme + +# Assumes substitutions.py and version_list.py are in the parent dir of doc-builder +# pylint: disable=wrong-import-position +dir2add = os.path.join(os.path.dirname(__file__), os.pardir, os.pardir) +sys.path.insert(0, dir2add) +import substitutions as subs # pylint: disable=import-error +from version_list import VERSION_LIST # pylint: disable=import-error + + +# -- General configuration ------------------------------------------------ + +# If your documentation needs a minimal Sphinx version, state it here. +# +# needs_sphinx = '1.0' + +# Add any Sphinx extension module names here, as strings. They can be +# extensions coming with Sphinx (named 'sphinx.ext.*') or your custom +# ones. +extensions = ['sphinx.ext.intersphinx', + 'sphinx.ext.autodoc', + 'sphinx.ext.todo', + 'sphinx.ext.coverage', + 'sphinx.ext.githubpages', + 'sphinx_mdinclude', + ] + +# Add any paths that contain templates here, relative to this directory. +if os.environ["templates_path"]: + templates_path = [os.environ["templates_path"]] + if not all(os.path.isdir(x) for x in templates_path): + raise RuntimeError(f"Some member of templates_path does not exist: {templates_path}") + +# The suffix(es) of source filenames. +# You can specify multiple suffix as a list of string: +# +source_suffix = ['.rst', '.md'] +# source_suffix = '.rst' + +# The master toctree document. +source_start_file = 'index' + +# Save standard Sphinx substitution vars separately +project = subs.project +copyright = subs.copyright # pylint: disable=redefined-builtin +author = subs.author +version = subs.version +release = subs.release + +# version_label is not a standard sphinx variable, so we need some custom rst to allow +# pages to use it. We need a separate replacement for the bolded version because it +# doesn't work to have variable replacements within formatting. +rst_epilog = """ +.. |version_label| replace:: {version_label} +.. |version_label_bold| replace:: **{version_label}** +""".format(version_label=subs.version_label) + +# The language for content autogenerated by Sphinx. Refer to documentation +# for a list of supported languages. +# +# This is also used if you do content translation via gettext catalogs. +# Usually you set "language" from the command line for these cases. +language = "en" + +# List of patterns, relative to source directory, that match files and +# directories to ignore when looking for source files. +# This patterns also effect to html_static_path and html_extra_path +exclude_patterns = [] + +# The name of the Pygments (syntax highlighting) style to use. +pygments_style = 'sphinx' + +# If true, `todo` and `todoList` produce output, else they produce nothing. +todo_include_todos = True + +# -- Options for HTML output ---------------------------------------------- + +# The theme to use for HTML and HTML Help pages. See the documentation for +# a list of builtin themes. +# +html_theme = 'sphinx_rtd_theme' + +# Theme options are theme-specific and customize the look and feel of a theme +# further. For a list of options available for each theme, see the +# documentation. + +# Add any paths that contain custom static files (such as style sheets) here, +# relative to this directory. They are copied after the builtin static files, +# so a file named "default.css" will overwrite the builtin "default.css". +html_static_path = [os.environ["html_static_path"]] + + +# -- Options for HTMLHelp output ------------------------------------------ + +if getattr(subs, "htmlhelp", False): + htmlhelp_basename = subs.htmlhelp["basename"] + + +# -- Options for LaTeX output --------------------------------------------- +if getattr(subs, "latex", False): + + latex_elements = { + # The paper size ('letterpaper' or 'a4paper'). + # + # 'papersize': 'letterpaper', + + # The font size ('10pt', '11pt' or '12pt'). + # + # 'pointsize': '10pt', + + # Additional stuff for the LaTeX preamble. + # + 'preamble': '\\usepackage{hyperref}', + + 'fncychap': '\\usepackage[Conny]{fncychap}', + + # Latex figure (float) alignment + # + # 'figure_align': 'htbp', + } + + # Grouping the document tree into LaTeX files. List of tuples + # (source start file, target name, title, + # author, documentclass [howto, manual, or own class]). + latex_documents = [( + source_start_file, + subs.latex["target_name"], + subs.latex["title"], + author, + subs.latex["category"], + )] + + +# Options for manual page and Texinfo output +if getattr(subs, "mantex", False): + + # One entry per manual page. List of tuples + # (source start file, name, title, authors, manual section). + man_pages = [ + (source_start_file, subs.mantex["name"], subs.mantex["title"], [author], 1), + ] + + if getattr(subs, "tex", False): + # Grouping the document tree into Texinfo files. List of tuples + # (source start file, target name, title, author, + # dir menu entry, description, category) + texinfo_documents = [( + source_start_file, + subs.mantex["name"], + subs.mantex["title"], + author, + subs.tex["dirmenu_entry"], + subs.tex["description"], + subs.tex["category"]), + ] + +# Example configuration for intersphinx: refer to the Python standard library. +intersphinx_mapping = {'python': ('https://docs.python.org/', None)} + +numfig = True +numfig_format = {'figure': 'Figure %s', + 'table': 'Table %s', + 'code-block': 'Code %s', + 'section': '%s', + } +numfig_secnum_depth = 2 + +def setup(app): + app.add_css_file('css/custom.css') + +try: + html_context +except NameError: + html_context = dict() + +html_context["display_lower_left"] = True + +# Whether to show the version dropdown. If not set as environment variable, or environment variable +# is Python-falsey, do not show it. +version_dropdown = os.environ.get("version_dropdown") + +if version_dropdown: + html_context["current_version"] = os.environ["version_display_name"] + + html_context["versions"] = [] + pages_root = os.environ["pages_root"] + for this_version in VERSION_LIST: + html_context["versions"].append([ + this_version.display_name, + os.path.join(pages_root, this_version.subdir()), + ]) diff --git a/test/test_unit_get_build_command.py b/test/test_unit_get_build_command.py index cbfb12b..a24e742 100644 --- a/test/test_unit_get_build_command.py +++ b/test/test_unit_get_build_command.py @@ -45,10 +45,55 @@ def test_basic(self): ] self.assertEqual(expected, build_command) + def test_custom_conf_py_path(self): + """Tests usage with --conf-py-path as file""" + conf_py_path = os.path.join(os.path.dirname(__file__), "conf.py") + build_command = get_build_command( + build_dir="/path/to/foo", + run_from_dir="/irrelevant/path", + build_target="html", + num_make_jobs=4, + docker_name=None, + version="None", + conf_py_path=conf_py_path, + ) + expected = [ + "make", + f"SPHINXOPTS=-W --keep-going -c '{os.path.dirname(conf_py_path)}'", + "BUILDDIR=/path/to/foo", + "-j", + "4", + "html", + ] + self.assertEqual(expected, build_command) + + def test_custom_conf_py_path_dir(self): + """Tests usage with --conf-py-path as directory""" + conf_py_path = os.path.dirname(__file__) + build_command = get_build_command( + build_dir="/path/to/foo", + run_from_dir="/irrelevant/path", + build_target="html", + num_make_jobs=4, + docker_name=None, + version="None", + conf_py_path=conf_py_path, + ) + expected = [ + "make", + f"SPHINXOPTS=-W --keep-going -c '{conf_py_path}'", + "BUILDDIR=/path/to/foo", + "-j", + "4", + "html", + ] + self.assertEqual(expected, build_command) + @patch("os.path.expanduser") def test_docker(self, mock_expanduser): """Tests usage with use_docker=True""" mock_expanduser.return_value = "/path/to/username" + conf_py_path = os.path.join(os.path.dirname(__file__), "conf.py") build_command = get_build_command( build_dir="/path/to/username/foorepos/foodocs/versions/main", run_from_dir="/path/to/username/foorepos/foocode/doc", @@ -56,6 +101,7 @@ def test_docker(self, mock_expanduser): num_make_jobs=4, docker_name="foo", version="None", + conf_py_path=conf_py_path, ) expected = [ "docker", @@ -74,7 +120,7 @@ def test_docker(self, mock_expanduser): "current_version=None", "ghcr.io/escomp/ctsm/ctsm-docs:v1.0.1", "make", - "SPHINXOPTS=-W --keep-going", + f"SPHINXOPTS=-W --keep-going -c '{os.path.dirname(conf_py_path)}'", "BUILDDIR=/home/user/mounted_home/foorepos/foodocs/versions/main", "-j", "4", From ac69349085bb8450d760cf33a5ee46701eadd4b9 Mon Sep 17 00:00:00 2001 From: Sam Rabin Date: Fri, 23 May 2025 00:32:17 -0600 Subject: [PATCH 3/5] Reformat with black. --- build_docs_to_publish | 5 +---- doc_builder/build_commands.py | 12 +++--------- doc_builder/build_docs.py | 14 ++++---------- doc_builder/build_docs_shared_args.py | 1 + doc_builder/docs_version.py | 1 - doc_builder/sys_utils.py | 8 ++------ 6 files changed, 11 insertions(+), 30 deletions(-) diff --git a/build_docs_to_publish b/build_docs_to_publish index f8e87c0..06dda80 100755 --- a/build_docs_to_publish +++ b/build_docs_to_publish @@ -105,14 +105,11 @@ def check_version_list(): # Expect at most one version with landing_version True if version.landing_version: if has_default: - raise RuntimeError( - "Expected at most one version with landing_version True" - ) + raise RuntimeError("Expected at most one version with landing_version True") has_default = True def main(): - # Set up parser parser = argparse.ArgumentParser() diff --git a/doc_builder/build_commands.py b/doc_builder/build_commands.py index 0fbd325..57f1217 100644 --- a/doc_builder/build_commands.py +++ b/doc_builder/build_commands.py @@ -107,9 +107,7 @@ def get_build_command( # check this assumption below). docker_mountpoint = os.path.expanduser("~") - errmsg_if_not_under_mountpoint = ( - "build_docs must be run from somewhere in your home directory" - ) + errmsg_if_not_under_mountpoint = "build_docs must be run from somewhere in your home directory" docker_workdir = _docker_path_from_local_path( local_path=run_from_dir, docker_mountpoint=docker_mountpoint, @@ -158,9 +156,7 @@ def get_build_command( return docker_command -def _get_make_command( - build_dir, build_target, num_make_jobs, warnings_as_warnings, conf_py_path -): +def _get_make_command(build_dir, build_target, num_make_jobs, warnings_as_warnings, conf_py_path): """Return the make command to run (as a list) Args: @@ -182,9 +178,7 @@ def _get_make_command( return ["make", sphinxopts, builddir_arg, "-j", str(num_make_jobs), build_target] -def _docker_path_from_local_path( - local_path, docker_mountpoint, errmsg_if_not_under_mountpoint -): +def _docker_path_from_local_path(local_path, docker_mountpoint, errmsg_if_not_under_mountpoint): """Given a path on the local file system, return the equivalent path in Docker space Args: diff --git a/doc_builder/build_docs.py b/doc_builder/build_docs.py index 608b5f7..bb9b9b0 100644 --- a/doc_builder/build_docs.py +++ b/doc_builder/build_docs.py @@ -154,14 +154,12 @@ def commandline_options(cmdline_args=None): ) options = parser.parse_args(cmdline_args) - + print(f"options: {options}") if options.versions: if not options.site_root: - raise RuntimeError( - "--site-root must be provided when --versions is enabled" - ) + raise RuntimeError("--site-root must be provided when --versions is enabled") if not is_web_url(options.site_root) and not os.path.isabs(options.site_root): raise RuntimeError( f"--site-root is neither a web URL nor an absolute path: '{options.site_root}'" @@ -240,9 +238,7 @@ def setup_for_docker(): Returns a name that should be used in the docker run command """ - docker_name = "build_docs_" + "".join( - random.choice(string.ascii_lowercase) for _ in range(8) - ) + docker_name = "build_docs_" + "".join(random.choice(string.ascii_lowercase) for _ in range(8)) # It seems that, if we kill the build_docs process with Ctrl-C, the docker process # continues. Handle that by implementing a signal handler. There may be a better / @@ -300,9 +296,7 @@ def main(cmdline_args=None): docker_name=docker_name, docker_image=opts.docker_image, ) - run_build_command( - build_command=clean_command, version=version, options=opts - ) + run_build_command(build_command=clean_command, version=version, options=opts) build_command = get_build_command( build_dir=build_dir, diff --git a/doc_builder/build_docs_shared_args.py b/doc_builder/build_docs_shared_args.py index 981022a..035db41 100644 --- a/doc_builder/build_docs_shared_args.py +++ b/doc_builder/build_docs_shared_args.py @@ -4,6 +4,7 @@ """ import os + # pylint: disable=import-error,no-name-in-module from .build_commands import DEFAULT_DOCKER_IMAGE diff --git a/doc_builder/docs_version.py b/doc_builder/docs_version.py index 77ae176..fd03895 100644 --- a/doc_builder/docs_version.py +++ b/doc_builder/docs_version.py @@ -17,7 +17,6 @@ def __init__( ref, landing_version=False, ): - # The name of this version in file/URL paths self.short_name = short_name diff --git a/doc_builder/sys_utils.py b/doc_builder/sys_utils.py index 738d04a..f8e8da3 100644 --- a/doc_builder/sys_utils.py +++ b/doc_builder/sys_utils.py @@ -20,9 +20,7 @@ def check_permanent_file(filename): try: subprocess.check_output(cmd, shell=True) except subprocess.CalledProcessError as e: - subprocess.check_output( - "git reset", shell=True - ) # Unstage files staged by `git add` + subprocess.check_output("git reset", shell=True) # Unstage files staged by `git add` msg = f"Important file/submodule may contain uncommitted changes: '{filename}'" raise RuntimeError(msg) from e @@ -70,9 +68,7 @@ def git_current_branch(): try: # Suppress stderr because we don't want to clutter output with # git's message, e.g., if we're not in a git repository. - branch_name = subprocess.check_output( - cmd, stderr=devnull, universal_newlines=True - ) + branch_name = subprocess.check_output(cmd, stderr=devnull, universal_newlines=True) except subprocess.CalledProcessError: branch_found = False branch_name = "" From 3e5dd1678a79a94a760b5b7a8286a9b744ce137e Mon Sep 17 00:00:00 2001 From: Sam Rabin Date: Fri, 23 May 2025 00:32:39 -0600 Subject: [PATCH 4/5] Add previous commit to .git-blame-ignore-revs. --- .git-blame-ignore-revs | 1 + 1 file changed, 1 insertion(+) diff --git a/.git-blame-ignore-revs b/.git-blame-ignore-revs index 4c73ce1..7a6df84 100644 --- a/.git-blame-ignore-revs +++ b/.git-blame-ignore-revs @@ -2,3 +2,4 @@ ce25647921a8f82c3b5009bdd07a620545b91a0c 8762f2d8834a9ba391b73ca4ac36f3bb19b169ed 04f3a94bdb9fc8c402286ebdc3ff9cb688c1e4b6 81b7b35c4e07551459a98b7be17b6b6400d12e3a +ac69349085bb8450d760cf33a5ee46701eadd4b9 From 8281cae934488cf115bbb1ea7afc50711119d58d Mon Sep 17 00:00:00 2001 From: Sam Rabin Date: Fri, 23 May 2025 00:39:50 -0600 Subject: [PATCH 5/5] Satisfy pylint. --- build_docs | 2 +- build_docs_to_publish | 10 +++++++--- doc_builder/build_commands.py | 4 +--- doc_builder/build_docs_shared_args.py | 2 -- doc_builder/sys_utils.py | 4 ++-- test/test_unit_cmdline_args.py | 2 +- test/test_unit_get_build_command.py | 2 +- test/test_unit_get_build_dir.py | 4 +++- 8 files changed, 16 insertions(+), 14 deletions(-) diff --git a/build_docs b/build_docs index 3e0db1f..f05599c 100755 --- a/build_docs +++ b/build_docs @@ -8,7 +8,7 @@ This should be run from the directory that contains the Makefile for building the documentation. """ -from doc_builder import build_docs +from doc_builder import build_docs # pylint: disable=import-error if __name__ == "__main__": build_docs.main() diff --git a/build_docs_to_publish b/build_docs_to_publish index 06dda80..37693c6 100755 --- a/build_docs_to_publish +++ b/build_docs_to_publish @@ -4,8 +4,9 @@ Loop through all versions of the documentation, building each and moving it to a directory for publication. -# Adapted from https://www.codingwiththomas.com/blog/my-sphinx-best-practice-for-a-multiversion-documentation-in-different-languages -# (last visited 2025-05-20) +Adapted from https://www.codingwiththomas.com/blog/my-sphinx-best-practice-for-a-multiversion- +documentation-in-different-languages +(last visited 2025-05-20) """ import sys @@ -33,7 +34,6 @@ from version_list import ( # Path to certain important files -# TODO: Make these cmd-line options SOURCE = "source" VERSIONS_PY = os.path.join("version_list.py") MAKEFILE = "Makefile" @@ -110,6 +110,10 @@ def check_version_list(): def main(): + """ + Loop through all versions of the documentation, building each and moving it to a directory for + publication. + """ # Set up parser parser = argparse.ArgumentParser() diff --git a/doc_builder/build_commands.py b/doc_builder/build_commands.py index 57f1217..750e89d 100644 --- a/doc_builder/build_commands.py +++ b/doc_builder/build_commands.py @@ -4,7 +4,7 @@ import os import pathlib -from doc_builder import sys_utils +from doc_builder import sys_utils # pylint: disable=import-error DEFAULT_DOCKER_IMAGE = "ghcr.io/escomp/ctsm/ctsm-docs:v1.0.1" @@ -75,8 +75,6 @@ def get_build_command( warnings_as_warnings=False, docker_image=DEFAULT_DOCKER_IMAGE, conf_py_path=None, - static_path=None, - templates_path=None, ): # pylint: disable=too-many-arguments,too-many-locals """Return a string giving the build command. diff --git a/doc_builder/build_docs_shared_args.py b/doc_builder/build_docs_shared_args.py index 035db41..3d86f17 100644 --- a/doc_builder/build_docs_shared_args.py +++ b/doc_builder/build_docs_shared_args.py @@ -3,8 +3,6 @@ group. """ -import os - # pylint: disable=import-error,no-name-in-module from .build_commands import DEFAULT_DOCKER_IMAGE diff --git a/doc_builder/sys_utils.py b/doc_builder/sys_utils.py index f8e8da3..6668467 100644 --- a/doc_builder/sys_utils.py +++ b/doc_builder/sys_utils.py @@ -19,10 +19,10 @@ def check_permanent_file(filename): cmd = f"git add . && git diff --quiet {filename} && git diff --cached --quiet {filename}" try: subprocess.check_output(cmd, shell=True) - except subprocess.CalledProcessError as e: + except subprocess.CalledProcessError as exception: subprocess.check_output("git reset", shell=True) # Unstage files staged by `git add` msg = f"Important file/submodule may contain uncommitted changes: '{filename}'" - raise RuntimeError(msg) from e + raise RuntimeError(msg) from exception def get_git_head_or_branch(): diff --git a/test/test_unit_cmdline_args.py b/test/test_unit_cmdline_args.py index 605f395..b5db50e 100644 --- a/test/test_unit_cmdline_args.py +++ b/test/test_unit_cmdline_args.py @@ -5,7 +5,7 @@ import unittest import os -from doc_builder.build_docs import is_web_url, commandline_options +from doc_builder.build_docs import is_web_url, commandline_options # pylint: disable=import-error class TestCmdlineArgs(unittest.TestCase): diff --git a/test/test_unit_get_build_command.py b/test/test_unit_get_build_command.py index a24e742..6cb0482 100644 --- a/test/test_unit_get_build_command.py +++ b/test/test_unit_get_build_command.py @@ -5,7 +5,7 @@ import os import unittest from unittest.mock import patch -from doc_builder.build_commands import get_build_command +from doc_builder.build_commands import get_build_command # pylint: disable=import-error # Allow names that pylint doesn't like, because otherwise I find it hard # to make readable unit test names diff --git a/test/test_unit_get_build_dir.py b/test/test_unit_get_build_dir.py index de3dfc9..b0e181f 100644 --- a/test/test_unit_get_build_dir.py +++ b/test/test_unit_get_build_dir.py @@ -12,9 +12,11 @@ # For python3 from unittest import mock import os + +# pylint: disable=import-error,no-name-in-module from test.test_utils.sys_utils_fake import ( make_fake_isdir, -) # pylint: disable=import-error,no-name-in-module +) from doc_builder.build_commands import get_build_dir