From ab0e1512a5f61950a4a1a20840dbda1569ace2f0 Mon Sep 17 00:00:00 2001 From: Sviatoslav Sydorenko Date: Fri, 17 Jan 2025 04:38:19 +0100 Subject: [PATCH] =?UTF-8?q?=F0=9F=A4=96=20Set=20build=20config=20via=20Sph?= =?UTF-8?q?inx=20ext?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit This is an initial change allowing for restructuring how the config settings are computed and putting that logic into a dedicated Sphinx extension. The idea is that this extension may have multiple callbacks that set configuration for Sphinx based on tags and the environment state. As an example, this in-tree extension implements setting the `is_eol` variable in Jinja2 context very early in Sphinx life cycle. It does this based on inspecting the state of current Git checkout as well as reading a config file listing EOL and supported versions of `ansible-core`. The configuration format is TOML as it's gained a lot of the ecosystem adoption over the past years and its parser made its way into the standard library of Python, while PyYAML remains a third-party dependency. Supersedes #2251. --- docs/docsite/_ext/build_context.py | 110 +++++++++++++++++++++++++++++ docs/docsite/end_of_life.toml | 25 +++++++ docs/docsite/rst/conf.py | 15 +++- 3 files changed, 149 insertions(+), 1 deletion(-) create mode 100644 docs/docsite/_ext/build_context.py create mode 100644 docs/docsite/end_of_life.toml diff --git a/docs/docsite/_ext/build_context.py b/docs/docsite/_ext/build_context.py new file mode 100644 index 0000000000..2020976f93 --- /dev/null +++ b/docs/docsite/_ext/build_context.py @@ -0,0 +1,110 @@ +"""Sphinx extension for setting up the build settings.""" + +import subprocess +from dataclasses import dataclass +from functools import cache +from pathlib import Path +from tomllib import loads as parse_toml_string +from typing import Literal + +from sphinx.application import Sphinx +from sphinx.util import logging + + +logger = logging.getLogger(__name__) + + +DOCSITE_ROOT_DIR = Path(__file__).parents[1].resolve() +DOCSITE_EOL_CONFIG_PATH = DOCSITE_ROOT_DIR / 'end_of_life.toml' + + +@dataclass(frozen=True) +class Distribution: + end_of_life: list[str] + supported: list[str] + + @classmethod + def from_dict(cls, raw_dist: dict[str, list[str]]) -> 'Distribution': + return cls( + **{ + kind.replace('-', '_'): versions + for kind, versions in raw_dist.items() + }, + ) + + +EOLConfigType = dict[str, Distribution] + + +@cache +def _read_eol_data() -> EOLConfigType: + raw_config_dict = parse_toml_string(DOCSITE_EOL_CONFIG_PATH.read_text()) + + return { + dist_name: Distribution.from_dict(dist_data) + for dist_name, dist_data in raw_config_dict['distribution'].items() + } + + +@cache +def _is_eol_build(git_branch: str, kind: str) -> bool: + return git_branch in _read_eol_data()[kind].end_of_life + + +@cache +def _get_current_git_branch(): + git_branch_cmd = 'git', 'rev-parse', '--abbrev-ref', 'HEAD' + + try: + return subprocess.check_output(git_branch_cmd, text=True).strip() + except subprocess.CalledProcessError as proc_err: + raise LookupError( + f'Failed to locate current Git branch: {proc_err !s}', + ) from proc_err + + +def _set_global_j2_context(app, config): + if 'is_eol' in config.html_context: + raise ValueError( + '`is_eol` found in `html_context` unexpectedly. ' + 'It should not be set in `conf.py`.', + ) from None + + dist_name = ( + 'ansible-core' if app.tags.has('core') + else 'ansible' if app.tags.has('ansible') + else None + ) + + if dist_name is None: + return + + try: + git_branch = _get_current_git_branch() + except LookupError as lookup_err: + logger.info(str(lookup_err)) + return + + config.html_context['is_eol'] = _is_eol_build( + git_branch=git_branch, kind=dist_name, + ) + + +def setup(app: Sphinx) -> dict[str, bool | str]: + """Initialize the extension. + + :param app: A Sphinx application object. + :returns: Extension metadata as a dict. + """ + + # NOTE: `config-inited` is used because it runs once as opposed to + # NOTE: `html-page-context` that runs per each page. The data we + # NOTE: compute is immutable throughout the build so there's no need + # NOTE: to have a callback that would be executed hundreds of times. + app.connect('config-inited', _set_global_j2_context) + + return { + 'parallel_read_safe': True, + 'parallel_write_safe': True, + 'version': app.config.release, + } diff --git a/docs/docsite/end_of_life.toml b/docs/docsite/end_of_life.toml new file mode 100644 index 0000000000..5c4feee29c --- /dev/null +++ b/docs/docsite/end_of_life.toml @@ -0,0 +1,25 @@ +[distribution.ansible] +end-of-life = [ + 'stable-2.15', + 'stable-2.14', + 'stable-2.13', +] +supported = [ + 'devel', + 'stable-2.18', + 'stable-2.17', + 'stable-2.16', +] + +[distribution.ansible-core] +end-of-life = [ + 'stable-2.15', + 'stable-2.14', + 'stable-2.13', +] +supported = [ + 'devel', + 'stable-2.18', + 'stable-2.17', + 'stable-2.16', +] diff --git a/docs/docsite/rst/conf.py b/docs/docsite/rst/conf.py index 372fc1f5d6..3b026a34bf 100644 --- a/docs/docsite/rst/conf.py +++ b/docs/docsite/rst/conf.py @@ -18,6 +18,17 @@ import sys import os +from pathlib import Path + + +DOCS_ROOT_DIR = Path(__file__).parent.resolve() + + +# Make in-tree extension importable in non-tox setups/envs, like RTD. +# Refs: +# https://github.com/readthedocs/readthedocs.org/issues/6311 +# https://github.com/readthedocs/readthedocs.org/issues/7182 +sys.path.insert(0, str(DOCS_ROOT_DIR.parent / '_ext')) # If your extensions are in another directory, add it here. If the directory # is relative to the documentation root, use os.path.abspath to make it @@ -65,6 +76,9 @@ 'notfound.extension', 'sphinx_antsibull_ext', # provides CSS for the plugin/module docs generated by antsibull 'sphinx_copybutton', + + # In-tree extensions: + 'build_context', # computes build settings for env context ] # Later on, add 'sphinx.ext.viewcode' to the list if you want to have @@ -227,7 +241,6 @@ html_context = { 'display_github': 'True', 'show_sphinx': False, - 'is_eol': False, 'github_user': 'ansible', 'github_repo': 'ansible-documentation', 'github_version': 'devel',