From ac9e4411984bab4dfd3808dbbd4425baf227bffc Mon Sep 17 00:00:00 2001 From: Matthew Hughes Date: Tue, 6 Aug 2024 18:59:01 +0100 Subject: [PATCH] Add separate `conf.py` for the different docs types The goal here is to allow building the `man` pages without all the additional requirements needed for the `html` docs, so that e.g. developers packaging `pip` can also package the man pages with minimal dependencies. Specifically, this should have no impact on the build output of `nox -s docs`, but does allow: sphinx-build \ -c docs/man \ -d docs/build/doctrees/man \ -b man \ docs/man \ docs/build/man to run with only `sphinx` dependency (in addition to `pip`s dependencies). While doing this I also used long-opts to `sphinx-build` in the `noxfile` since I found this more understandable at a glance An `__init__.py` was added under `docs/man` to satisfy `mypy`. If this wasn't added it would error: docs/man/conf.py: error: Duplicate module named "conf" (also at "docs/html/conf.py") docs/man/conf.py: note: See https://mypy.readthedocs.io/en/stable/running_mypy.html#mapping-file-paths-to-modules for more info docs/man/conf.py: note: Common resolutions include: a) using `--exclude` to avoid checking one of them, b) adding `__init__.py` somewhere, c) using `--explicit-package-bases` or adjusting MYPYPATH Found 1 error in 1 file (errors prevented further checking) Adding an extra `__init__.py` under `docs/html` would result in a separate error since then the `html` module is local (and not the stdlib one) resulting in a error from `sphinx-build`: Extension error: Could not import extension sphinx.builders.linkcheck (exception: No module named 'html.parser') Issue: https://github.com/pypa/pip/issues/12881 --- .readthedocs.yml | 1 + MANIFEST.in | 1 + docs/common_conf.py | 29 ++++++++ docs/html/conf.py | 66 ++----------------- docs/html/requirements.txt | 8 +++ docs/man/__init__.py | 0 docs/man/conf.py | 53 +++++++++++++++ docs/requirements.txt | 8 --- ...cc-cba6-4eb2-ac22-5225ed3bf448.trivial.rst | 0 noxfile.py | 22 +++++-- 10 files changed, 112 insertions(+), 76 deletions(-) create mode 100644 docs/common_conf.py create mode 100644 docs/html/requirements.txt create mode 100644 docs/man/__init__.py create mode 100644 docs/man/conf.py create mode 100644 news/a75401cc-cba6-4eb2-ac22-5225ed3bf448.trivial.rst diff --git a/.readthedocs.yml b/.readthedocs.yml index c0d2bba55e9..969b5f50d85 100644 --- a/.readthedocs.yml +++ b/.readthedocs.yml @@ -12,3 +12,4 @@ sphinx: python: install: - requirements: docs/requirements.txt + - requirements: docs/html/requirements.txt diff --git a/MANIFEST.in b/MANIFEST.in index 814da8265ca..f3dcf96830f 100644 --- a/MANIFEST.in +++ b/MANIFEST.in @@ -13,6 +13,7 @@ recursive-include src/pip/_vendor *COPYING* include docs/docutils.conf include docs/requirements.txt +include docs/html/requirements.txt exclude .git-blame-ignore-revs exclude .coveragerc diff --git a/docs/common_conf.py b/docs/common_conf.py new file mode 100644 index 00000000000..9096035dc4d --- /dev/null +++ b/docs/common_conf.py @@ -0,0 +1,29 @@ +import os +import re +from typing import Tuple + + +def read_version() -> Tuple[str, str]: + # Find the version and release information. + # We have a single source of truth for our version number: pip's __init__.py file. + # This next bit of code reads from it. + file_with_version = os.path.join( + os.path.dirname(__file__), "..", "src", "pip", "__init__.py" + ) + with open(file_with_version) as f: + for line in f: + m = re.match(r'__version__ = "(.*)"', line) + if m: + __version__ = m.group(1) + # The short X.Y version. + version = ".".join(__version__.split(".")[:2]) + # The full version, including alpha/beta/rc tags. + release = __version__ + return version, release + return "dev", "dev" + + +# General information about the project. +project = "pip" +copyright = "The pip developers" +version, release = read_version() diff --git a/docs/html/conf.py b/docs/html/conf.py index 683ea7b87d8..430cac0a31f 100644 --- a/docs/html/conf.py +++ b/docs/html/conf.py @@ -1,16 +1,16 @@ """Sphinx configuration file for pip's documentation.""" -import glob import os import pathlib -import re import sys -from typing import List, Tuple -# Add the docs/ directory to sys.path, because pip_sphinxext.py is there. +# Add the docs/ directory to sys.path to load the common config, +# and pip_sphinxext.py docs_dir = os.path.dirname(os.path.dirname(__file__)) sys.path.insert(0, docs_dir) +from common_conf import copyright, project, release, version # noqa: E402, F401 + # -- General configuration ------------------------------------------------------------ extensions = [ @@ -28,27 +28,6 @@ "sphinxcontrib.towncrier", ] -# General information about the project. -project = "pip" -copyright = "The pip developers" - -# Find the version and release information. -# We have a single source of truth for our version number: pip's __init__.py file. -# This next bit of code reads from it. -file_with_version = os.path.join(docs_dir, "..", "src", "pip", "__init__.py") -with open(file_with_version) as f: - for line in f: - m = re.match(r'__version__ = "(.*)"', line) - if m: - __version__ = m.group(1) - # The short X.Y version. - version = ".".join(__version__.split(".")[:2]) - # The full version, including alpha/beta/rc tags. - release = __version__ - break - else: # AKA no-break - version = release = "dev" - print("pip version:", version) print("pip release:", release) @@ -95,43 +74,6 @@ html_use_modindex = False html_use_index = False -# -- Options for Manual Pages --------------------------------------------------------- - - -# List of manual pages generated -def determine_man_pages() -> List[Tuple[str, str, str, str, int]]: - """Determine which man pages need to be generated.""" - - def to_document_name(path: str, base_dir: str) -> str: - """Convert a provided path to a Sphinx "document name".""" - relative_path = os.path.relpath(path, base_dir) - root, _ = os.path.splitext(relative_path) - return root.replace(os.sep, "/") - - # Crawl the entire man/commands/ directory and list every file with appropriate - # name and details. - man_dir = os.path.join(docs_dir, "man") - raw_subcommands = glob.glob(os.path.join(man_dir, "commands/*.rst")) - if not raw_subcommands: - raise FileNotFoundError( - "The individual subcommand manpages could not be found!" - ) - - retval = [ - ("index", "pip", "package manager for Python packages", "pip developers", 1), - ] - for fname in raw_subcommands: - fname_base = to_document_name(fname, man_dir) - outname = "pip-" + fname_base.split("/")[1] - description = "description of {} command".format(outname.replace("-", " ")) - - retval.append((fname_base, outname, description, "pip developers", 1)) - - return retval - - -man_pages = determine_man_pages() - # -- Options for sphinx_copybutton ---------------------------------------------------- copybutton_prompt_text = r"\$ | C\:\> " diff --git a/docs/html/requirements.txt b/docs/html/requirements.txt new file mode 100644 index 00000000000..4a5c23400a9 --- /dev/null +++ b/docs/html/requirements.txt @@ -0,0 +1,8 @@ +# currently incompatible with sphinxcontrib-towncrier +# https://github.com/sphinx-contrib/sphinxcontrib-towncrier/issues/92 +towncrier < 24 +furo +myst_parser +sphinx-copybutton +sphinx-inline-tabs +sphinxcontrib-towncrier >= 0.2.0a0 diff --git a/docs/man/__init__.py b/docs/man/__init__.py new file mode 100644 index 00000000000..e69de29bb2d diff --git a/docs/man/conf.py b/docs/man/conf.py new file mode 100644 index 00000000000..2d5a5d894fd --- /dev/null +++ b/docs/man/conf.py @@ -0,0 +1,53 @@ +import glob +import os +import sys +from typing import List, Tuple + +# Add the docs/ directory to sys.path to load the common config +docs_dir = os.path.dirname(os.path.dirname(__file__)) +sys.path.insert(0, docs_dir) + +from common_conf import copyright, project, release, version # noqa: E402, F401 + +extensions = [ + # our extensions + "pip_sphinxext", +] + +print("pip version:", version) +print("pip release:", release) + + +# List of manual pages generated +def determine_man_pages() -> List[Tuple[str, str, str, str, int]]: + """Determine which man pages need to be generated.""" + + def to_document_name(path: str, base_dir: str) -> str: + """Convert a provided path to a Sphinx "document name".""" + relative_path = os.path.relpath(path, base_dir) + root, _ = os.path.splitext(relative_path) + return root.replace(os.sep, "/") + + # Crawl the entire man/commands/ directory and list every file with appropriate + # name and details. + man_dir = os.path.join(docs_dir, "man") + raw_subcommands = glob.glob(os.path.join(man_dir, "commands/*.rst")) + if not raw_subcommands: + raise FileNotFoundError( + "The individual subcommand manpages could not be found!" + ) + + retval = [ + ("index", "pip", "package manager for Python packages", "pip developers", 1), + ] + for fname in raw_subcommands: + fname_base = to_document_name(fname, man_dir) + outname = "pip-" + fname_base.split("/")[1] + description = "description of {} command".format(outname.replace("-", " ")) + + retval.append((fname_base, outname, description, "pip developers", 1)) + + return retval + + +man_pages = determine_man_pages() diff --git a/docs/requirements.txt b/docs/requirements.txt index b3ea82a9de1..35768edd252 100644 --- a/docs/requirements.txt +++ b/docs/requirements.txt @@ -1,12 +1,4 @@ sphinx ~= 7.0 -# currently incompatible with sphinxcontrib-towncrier -# https://github.com/sphinx-contrib/sphinxcontrib-towncrier/issues/92 -towncrier < 24 -furo -myst_parser -sphinx-copybutton -sphinx-inline-tabs -sphinxcontrib-towncrier >= 0.2.0a0 # `docs.pipext` uses pip's internals to generate documentation. So, we install # the current directory to make it work. diff --git a/news/a75401cc-cba6-4eb2-ac22-5225ed3bf448.trivial.rst b/news/a75401cc-cba6-4eb2-ac22-5225ed3bf448.trivial.rst new file mode 100644 index 00000000000..e69de29bb2d diff --git a/noxfile.py b/noxfile.py index 2051362769c..ed2cdf86654 100644 --- a/noxfile.py +++ b/noxfile.py @@ -26,6 +26,7 @@ } REQUIREMENTS = { "docs": "docs/requirements.txt", + "docs-html": "docs/html/requirements.txt", "tests": "tests/requirements.txt", "common-wheels": "tests/requirements-common_wheels.txt", } @@ -131,7 +132,10 @@ def docs(session: nox.Session) -> None: session.install("-r", REQUIREMENTS["docs"]) def get_sphinx_build_command(kind: str) -> List[str]: - # Having the conf.py in the docs/html is weird but needed because we + req_file = REQUIREMENTS.get(f"docs-{kind}") + if req_file is not None: + session.install("--requirement", req_file) + # Having the conf.py in the docs/{html,man} is weird but needed because we # can not use a different configuration directory vs source directory # on RTD currently. So, we'll pass "-c docs/html" here. # See https://github.com/rtfd/readthedocs.org/issues/1543. @@ -139,10 +143,10 @@ def get_sphinx_build_command(kind: str) -> List[str]: return [ "sphinx-build", "--keep-going", - "-W", - "-c", "docs/html", # see note above - "-d", "docs/build/doctrees/" + kind, - "-b", kind, + "--fail-on-warning", + "--conf-dir", "docs/" + kind, # see note above + "--doctree-dir", "docs/build/doctrees/" + kind, + "--builder", kind, "docs/" + kind, "docs/build/" + kind, ] @@ -155,7 +159,13 @@ def get_sphinx_build_command(kind: str) -> List[str]: @nox.session(name="docs-live") def docs_live(session: nox.Session) -> None: session.install("-e", ".") - session.install("-r", REQUIREMENTS["docs"], "sphinx-autobuild") + session.install( + "-r", + REQUIREMENTS["docs"], + "-r", + REQUIREMENTS["docs-html"], + "sphinx-autobuild", + ) session.run( "sphinx-autobuild",