Skip to content

Commit ab0e151

Browse files
committed
🤖 Set build config via Sphinx ext
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.
1 parent 277aaca commit ab0e151

File tree

3 files changed

+149
-1
lines changed

3 files changed

+149
-1
lines changed
+110
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,110 @@
1+
"""Sphinx extension for setting up the build settings."""
2+
3+
import subprocess
4+
from dataclasses import dataclass
5+
from functools import cache
6+
from pathlib import Path
7+
from tomllib import loads as parse_toml_string
8+
from typing import Literal
9+
10+
from sphinx.application import Sphinx
11+
from sphinx.util import logging
12+
13+
14+
logger = logging.getLogger(__name__)
15+
16+
17+
DOCSITE_ROOT_DIR = Path(__file__).parents[1].resolve()
18+
DOCSITE_EOL_CONFIG_PATH = DOCSITE_ROOT_DIR / 'end_of_life.toml'
19+
20+
21+
@dataclass(frozen=True)
22+
class Distribution:
23+
end_of_life: list[str]
24+
supported: list[str]
25+
26+
@classmethod
27+
def from_dict(cls, raw_dist: dict[str, list[str]]) -> 'Distribution':
28+
return cls(
29+
**{
30+
kind.replace('-', '_'): versions
31+
for kind, versions in raw_dist.items()
32+
},
33+
)
34+
35+
36+
EOLConfigType = dict[str, Distribution]
37+
38+
39+
@cache
40+
def _read_eol_data() -> EOLConfigType:
41+
raw_config_dict = parse_toml_string(DOCSITE_EOL_CONFIG_PATH.read_text())
42+
43+
return {
44+
dist_name: Distribution.from_dict(dist_data)
45+
for dist_name, dist_data in raw_config_dict['distribution'].items()
46+
}
47+
48+
49+
@cache
50+
def _is_eol_build(git_branch: str, kind: str) -> bool:
51+
return git_branch in _read_eol_data()[kind].end_of_life
52+
53+
54+
@cache
55+
def _get_current_git_branch():
56+
git_branch_cmd = 'git', 'rev-parse', '--abbrev-ref', 'HEAD'
57+
58+
try:
59+
return subprocess.check_output(git_branch_cmd, text=True).strip()
60+
except subprocess.CalledProcessError as proc_err:
61+
raise LookupError(
62+
f'Failed to locate current Git branch: {proc_err !s}',
63+
) from proc_err
64+
65+
66+
def _set_global_j2_context(app, config):
67+
if 'is_eol' in config.html_context:
68+
raise ValueError(
69+
'`is_eol` found in `html_context` unexpectedly. '
70+
'It should not be set in `conf.py`.',
71+
) from None
72+
73+
dist_name = (
74+
'ansible-core' if app.tags.has('core')
75+
else 'ansible' if app.tags.has('ansible')
76+
else None
77+
)
78+
79+
if dist_name is None:
80+
return
81+
82+
try:
83+
git_branch = _get_current_git_branch()
84+
except LookupError as lookup_err:
85+
logger.info(str(lookup_err))
86+
return
87+
88+
config.html_context['is_eol'] = _is_eol_build(
89+
git_branch=git_branch, kind=dist_name,
90+
)
91+
92+
93+
def setup(app: Sphinx) -> dict[str, bool | str]:
94+
"""Initialize the extension.
95+
96+
:param app: A Sphinx application object.
97+
:returns: Extension metadata as a dict.
98+
"""
99+
100+
# NOTE: `config-inited` is used because it runs once as opposed to
101+
# NOTE: `html-page-context` that runs per each page. The data we
102+
# NOTE: compute is immutable throughout the build so there's no need
103+
# NOTE: to have a callback that would be executed hundreds of times.
104+
app.connect('config-inited', _set_global_j2_context)
105+
106+
return {
107+
'parallel_read_safe': True,
108+
'parallel_write_safe': True,
109+
'version': app.config.release,
110+
}

‎docs/docsite/end_of_life.toml

+25
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,25 @@
1+
[distribution.ansible]
2+
end-of-life = [
3+
'stable-2.15',
4+
'stable-2.14',
5+
'stable-2.13',
6+
]
7+
supported = [
8+
'devel',
9+
'stable-2.18',
10+
'stable-2.17',
11+
'stable-2.16',
12+
]
13+
14+
[distribution.ansible-core]
15+
end-of-life = [
16+
'stable-2.15',
17+
'stable-2.14',
18+
'stable-2.13',
19+
]
20+
supported = [
21+
'devel',
22+
'stable-2.18',
23+
'stable-2.17',
24+
'stable-2.16',
25+
]

‎docs/docsite/rst/conf.py

+14-1
Original file line numberDiff line numberDiff line change
@@ -18,6 +18,17 @@
1818

1919
import sys
2020
import os
21+
from pathlib import Path
22+
23+
24+
DOCS_ROOT_DIR = Path(__file__).parent.resolve()
25+
26+
27+
# Make in-tree extension importable in non-tox setups/envs, like RTD.
28+
# Refs:
29+
# https://github.com/readthedocs/readthedocs.org/issues/6311
30+
# https://github.com/readthedocs/readthedocs.org/issues/7182
31+
sys.path.insert(0, str(DOCS_ROOT_DIR.parent / '_ext'))
2132

2233
# If your extensions are in another directory, add it here. If the directory
2334
# is relative to the documentation root, use os.path.abspath to make it
@@ -65,6 +76,9 @@
6576
'notfound.extension',
6677
'sphinx_antsibull_ext', # provides CSS for the plugin/module docs generated by antsibull
6778
'sphinx_copybutton',
79+
80+
# In-tree extensions:
81+
'build_context', # computes build settings for env context
6882
]
6983

7084
# Later on, add 'sphinx.ext.viewcode' to the list if you want to have
@@ -227,7 +241,6 @@
227241
html_context = {
228242
'display_github': 'True',
229243
'show_sphinx': False,
230-
'is_eol': False,
231244
'github_user': 'ansible',
232245
'github_repo': 'ansible-documentation',
233246
'github_version': 'devel',

0 commit comments

Comments
 (0)