Skip to content

Commit

Permalink
🤖 Set build config via Sphinx ext
Browse files Browse the repository at this point in the history
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.
  • Loading branch information
webknjaz committed Jan 17, 2025
1 parent 277aaca commit 8ef3352
Show file tree
Hide file tree
Showing 3 changed files with 149 additions and 1 deletion.
110 changes: 110 additions & 0 deletions docs/docsite/_ext/build_context.py
Original file line number Diff line number Diff line change
@@ -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.warn(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,
}
25 changes: 25 additions & 0 deletions docs/docsite/end_of_life.toml
Original file line number Diff line number Diff line change
@@ -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',
]
15 changes: 14 additions & 1 deletion docs/docsite/rst/conf.py
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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
Expand Down Expand Up @@ -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',
Expand Down

0 comments on commit 8ef3352

Please sign in to comment.