052ed8e119
This is a little more realistic. Sphinx 6.0.0 was released in December 2022 [1]. In the absence of a lower-constraints job, I manually tested that our own docs still build with this version. [1] https://pypi.org/project/Sphinx/6.0.0/ Change-Id: I62dcc47b7053deb46139ca1f46c0d8e6fc2720d6 Signed-off-by: Stephen Finucane <stephenfin@redhat.com>
555 lines
19 KiB
Python
555 lines
19 KiB
Python
# Copyright 2015 Rackspace US, Inc.
|
|
#
|
|
# Licensed under the Apache License, Version 2.0 (the "License"); you may
|
|
# not use this file except in compliance with the License. You may obtain
|
|
# a copy of the License at
|
|
#
|
|
# http://www.apache.org/licenses/LICENSE-2.0
|
|
#
|
|
# Unless required by applicable law or agreed to in writing, software
|
|
# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
|
|
# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
|
|
# License for the specific language governing permissions and limitations
|
|
# under the License.
|
|
|
|
import configparser
|
|
import os
|
|
import subprocess
|
|
import textwrap
|
|
|
|
import dulwich.repo
|
|
from pbr import packaging
|
|
from sphinx.ext import extlinks
|
|
from sphinx.util import logging
|
|
|
|
from . import version
|
|
from openstackdocstheme import paths
|
|
|
|
_series = None
|
|
_project = None
|
|
_giturl = 'https://opendev.org/{}/src/{}'
|
|
_html_context_data = None
|
|
|
|
logger = logging.getLogger(__name__)
|
|
|
|
|
|
def _has_stable_branches():
|
|
try:
|
|
repo = dulwich.repo.Repo.discover()
|
|
except dulwich.repo.NotGitRepository:
|
|
return False
|
|
|
|
refs = repo.get_refs()
|
|
for ref in refs.keys():
|
|
ref = ref.decode('utf-8')
|
|
if ref.startswith('refs/remotes/origin/stable'):
|
|
return True
|
|
|
|
return False
|
|
|
|
|
|
def _ref_sort_key(ref):
|
|
# note(frickler): make sure to sort series like 2023.1 after zed
|
|
if ref.startswith("2"):
|
|
return "zzz" + ref
|
|
return ref
|
|
|
|
|
|
def _get_other_versions(app):
|
|
if not app.config.html_theme_options.get('show_other_versions', False):
|
|
return []
|
|
|
|
all_series = []
|
|
try:
|
|
repo = dulwich.repo.Repo.discover()
|
|
except dulwich.repo.NotGitRepository:
|
|
return []
|
|
|
|
refs = repo.get_refs()
|
|
for ref in refs.keys():
|
|
ref = ref.decode('utf-8')
|
|
if ref.startswith('refs/remotes/origin/stable') or ref.startswith(
|
|
'refs/remotes/origin/unmaintained'
|
|
):
|
|
series = ref.rpartition('/')[-1]
|
|
all_series.append(series)
|
|
elif ref.startswith('refs/tags/') and ref.endswith('-eol'):
|
|
series = ref.rpartition('/')[-1][:-4]
|
|
all_series.append(series)
|
|
all_series = list(set(all_series))
|
|
all_series.sort(key=_ref_sort_key)
|
|
|
|
# NOTE(frickler): Since older versions are now unmaintained,
|
|
# assume that the earliest version we want to link to is for
|
|
# zed. Projects that have older docs online can set the option
|
|
# to indicate another start point. Projects that come later should
|
|
# automatically include everything they actually have available
|
|
# because the start point is not present in the list.
|
|
earliest_desired = app.config.html_theme_options.get(
|
|
'earliest_published_series', 'zed'
|
|
)
|
|
if earliest_desired and earliest_desired in all_series:
|
|
interesting_series = all_series[all_series.index(earliest_desired) :]
|
|
else:
|
|
interesting_series = all_series
|
|
|
|
# Reverse the list because we want the most recent to appear at
|
|
# the top of the dropdown. Add the "latest" release to the
|
|
# front of the list.
|
|
interesting_series.append("latest")
|
|
interesting_series.reverse()
|
|
return interesting_series
|
|
|
|
|
|
def _get_doc_path(app):
|
|
# Handle 'doc/{docType}/source' paths
|
|
doc_parts = os.path.abspath(app.srcdir).split(os.sep)[-3:]
|
|
if doc_parts[0] == 'doc' and doc_parts[2] == 'source':
|
|
return '/'.join(doc_parts)
|
|
|
|
# Handle '{docType}/source' paths
|
|
doc_parts = os.path.abspath(app.srcdir).split(os.sep)[-2:]
|
|
if doc_parts[1] == 'source':
|
|
return '/'.join(doc_parts)
|
|
|
|
logger.info(
|
|
"[openstackdocstheme] cannot identify project's root directory."
|
|
)
|
|
return
|
|
|
|
|
|
def _html_page_context(app, pagename, templatename, context, doctree):
|
|
global _html_context_data
|
|
if _html_context_data is None:
|
|
logger.debug('[openstackdocstheme] building html context')
|
|
|
|
if app.config.repository_name is not None:
|
|
logger.info(
|
|
"[openstackdocstheme] "
|
|
"the 'repository_name' config option has been deprecated and "
|
|
"replaced by the 'openstackdocs_repo_name' option; support "
|
|
"for the former will be dropped in a future release"
|
|
)
|
|
app.config.openstackdocs_repo_name = app.config.repository_name
|
|
|
|
if app.config.use_storyboard is not None:
|
|
logger.info(
|
|
"[openstackdocstheme] "
|
|
"the 'use_storyboard' config option has been deprecated and "
|
|
"replaced by the 'openstackdocs_use_storyboard' option; "
|
|
"support for the former will be dropped in a future release"
|
|
)
|
|
app.config.openstackdocs_use_storyboard = app.config.use_storyboard
|
|
|
|
if app.config.bug_project is not None:
|
|
logger.info(
|
|
"[openstackdocstheme] "
|
|
"the 'bug_project' config option has been deprecated and "
|
|
"replaced by the 'openstackdocs_bug_project' option; support "
|
|
"for the former will be dropped in a future release"
|
|
)
|
|
app.config.openstackdocs_bug_project = app.config.bug_project
|
|
|
|
if app.config.bug_tag is not None:
|
|
logger.info(
|
|
"[openstackdocstheme] "
|
|
"the 'bug_tag' config option has been deprecated and "
|
|
"replaced by the 'openstackdocs_bug_tag' option; support "
|
|
"for the former will be dropped in a future release"
|
|
)
|
|
app.config.openstackdocs_bug_project = app.config.bug_project
|
|
|
|
_html_context_data = {}
|
|
try:
|
|
_html_context_data['gitsha'] = (
|
|
subprocess.check_output(
|
|
['git', 'rev-parse', 'HEAD'],
|
|
)
|
|
.decode('utf-8')
|
|
.strip()
|
|
)
|
|
except Exception:
|
|
logger.warning(
|
|
'[openstackdocstheme] cannot get gitsha from git repository'
|
|
)
|
|
_html_context_data['gitsha'] = 'unknown'
|
|
|
|
doc_path = _get_doc_path(app)
|
|
repo_name = app.config.openstackdocs_repo_name
|
|
_html_context_data['repository_name'] = repo_name
|
|
logger.debug('[openstackdocstheme] repository_name %r', repo_name)
|
|
if repo_name and doc_path:
|
|
_html_context_data['giturl'] = _giturl.format(repo_name, doc_path)
|
|
logger.debug(
|
|
'[openstackdocstheme] giturl %r',
|
|
_html_context_data['giturl'],
|
|
)
|
|
|
|
use_storyboard = app.config.openstackdocs_use_storyboard
|
|
_html_context_data['use_storyboard'] = use_storyboard
|
|
|
|
bug_project = app.config.openstackdocs_bug_project
|
|
if bug_project:
|
|
logger.debug(
|
|
'[openstackdocstheme] bug_project (from user) %r',
|
|
bug_project,
|
|
)
|
|
elif use_storyboard:
|
|
bug_project = repo_name
|
|
logger.debug(
|
|
'[openstackdocstheme] bug_project (use_storyboard set) %r',
|
|
bug_project,
|
|
)
|
|
|
|
if bug_project:
|
|
_html_context_data['bug_project'] = bug_project
|
|
|
|
# Previously storyboard showed numbers that were used, keep
|
|
# for old conf.py files:
|
|
if bug_project and bug_project.isdigit():
|
|
logger.debug(
|
|
'[openstackdocstheme] bug_project looks like a number, '
|
|
'setting use_storyboard'
|
|
)
|
|
_html_context_data['use_storyboard'] = True
|
|
|
|
bug_tag = app.config.openstackdocs_bug_tag
|
|
if bug_tag:
|
|
_html_context_data['bug_tag'] = bug_tag
|
|
logger.debug('[openstackdocstheme] bug_tag %r', bug_tag)
|
|
|
|
_html_context_data['pdf_link'] = app.config.openstackdocs_pdf_link
|
|
logger.debug(
|
|
'[openstackdocstheme] pdf_link %r',
|
|
_html_context_data['pdf_link'],
|
|
)
|
|
|
|
if app.config.openstackdocs_pdf_filename:
|
|
_html_context_data['pdf_filename'] = (
|
|
app.config.openstackdocs_pdf_filename
|
|
)
|
|
else:
|
|
short_repo_name = repo_name.split('/')[-1]
|
|
_html_context_data['pdf_filename'] = f'doc-{short_repo_name}.pdf'
|
|
|
|
if _html_context_data['pdf_link']:
|
|
logger.debug(
|
|
'[openstackdocstheme] pdf_filename %r',
|
|
_html_context_data['pdf_filename'],
|
|
)
|
|
|
|
_html_context_data['series'] = _get_series_name()
|
|
logger.debug(
|
|
'[openstackdocstheme] series %r',
|
|
_html_context_data['series'],
|
|
)
|
|
|
|
# Do not show the badge in these cases:
|
|
# - display_badge is false
|
|
# - repo has no stable/ branches
|
|
# - directory is named api-guide, api-ref, or releasenotes
|
|
if not app.config.html_theme_options.get('display_badge', True):
|
|
_html_context_data['display_badge'] = False
|
|
logger.debug(
|
|
'[openstackdocstheme] display_badge False (configured by user)'
|
|
)
|
|
elif _has_stable_branches():
|
|
doc_parts = os.path.abspath(app.srcdir).split(os.sep)[-2:]
|
|
if doc_parts[0] in ('api-guide', 'api-ref', 'releasenotes'):
|
|
_html_context_data['display_badge'] = False
|
|
logger.debug(
|
|
'[openstackdocstheme] display_badge False (doc name '
|
|
'contains %r)',
|
|
doc_parts[0],
|
|
)
|
|
else:
|
|
_html_context_data['display_badge'] = True
|
|
logger.debug(
|
|
'[openstackdocstheme] display_badge True (stable branches)'
|
|
)
|
|
else:
|
|
_html_context_data['display_badge'] = False
|
|
logger.debug(
|
|
'[openstackdocstheme] display_badge False (no stable branches)'
|
|
)
|
|
|
|
context.update(_html_context_data)
|
|
context['other_versions'] = _get_other_versions(app)
|
|
logger.debug(
|
|
'[openstackdocstheme] other_versions %s',
|
|
context['other_versions'],
|
|
)
|
|
|
|
|
|
def _get_series_name():
|
|
"Return string name of release series, or 'latest'"
|
|
global _series
|
|
if _series is None:
|
|
try:
|
|
git_root_dir = (
|
|
subprocess.check_output(
|
|
['git', 'rev-parse', '--show-toplevel'],
|
|
)
|
|
.decode('utf-8')
|
|
.strip()
|
|
)
|
|
except Exception:
|
|
logger.info(
|
|
'[openstackdocstheme] cannot find git top directory, '
|
|
'assuming "."'
|
|
)
|
|
git_root_dir = '.'
|
|
|
|
parser = configparser.ConfigParser()
|
|
in_file = os.path.join(git_root_dir, '.gitreview')
|
|
parsed = parser.read(in_file)
|
|
if not parsed:
|
|
logger.info('[openstackdocstheme] no %s found', in_file)
|
|
|
|
try:
|
|
branch = parser.get('gerrit', 'defaultbranch')
|
|
except configparser.Error:
|
|
_series = 'latest'
|
|
else:
|
|
_series = branch.rpartition('/')[-1]
|
|
|
|
return _series
|
|
|
|
|
|
def _setup_link_roles(app):
|
|
series = _get_series_name()
|
|
for project_name in app.config.openstackdocs_projects:
|
|
url = 'https://docs.openstack.org/{}/{}/%s'.format(
|
|
project_name, series
|
|
)
|
|
role_name = '{}-doc'.format(project_name)
|
|
logger.debug(
|
|
'[openstackdocstheme] adding role %s to link to %s',
|
|
role_name,
|
|
url,
|
|
)
|
|
|
|
role = extlinks.make_link_role(project_name, url, project_name)
|
|
|
|
app.add_role(role_name, role)
|
|
|
|
|
|
def _find_setup_cfg(srcdir):
|
|
"""Find the 'setup.cfg' file, if it exists.
|
|
|
|
This assumes we're using 'doc/source' for documentation, but also allows
|
|
for single level 'doc' paths.
|
|
"""
|
|
# TODO(stephenfin): Are we sure that this will always exist, e.g. for
|
|
# an sdist or wheel? Perhaps we should check for 'PKG-INFO' or
|
|
# 'METADATA' files, a la 'pbr.packaging._get_version_from_pkg_metadata'
|
|
for path in [
|
|
os.path.join(srcdir, os.pardir, 'setup.cfg'),
|
|
os.path.join(srcdir, os.pardir, os.pardir, 'setup.cfg'),
|
|
]:
|
|
if os.path.exists(path):
|
|
return path
|
|
|
|
return None
|
|
|
|
|
|
def _get_project_name(srcdir):
|
|
"""Return string name of project name, or None.
|
|
|
|
This assumes every project is using 'pbr' and, therefore, the metadata can
|
|
be extracted from 'setup.cfg'.
|
|
|
|
We don't rely on distutils/setuptools as we don't want to actually install
|
|
the package simply to build docs.
|
|
"""
|
|
global _project
|
|
if _project is None:
|
|
parser = configparser.ConfigParser()
|
|
|
|
path = _find_setup_cfg(srcdir)
|
|
if not path or not parser.read(path):
|
|
logger.info(
|
|
'[openstackdocstheme] could not find a setup.cfg to extract '
|
|
'project name from'
|
|
)
|
|
return None
|
|
|
|
try:
|
|
# for project name we use the name in setup.cfg, but if the
|
|
# length is longer then 32 we use summary. Otherwise the
|
|
# menu rendering looks broken
|
|
project = parser.get('metadata', 'name')
|
|
if len(project.split()) == 1 and len(project) > 32:
|
|
project = parser.get('metadata', 'summary')
|
|
except configparser.Error:
|
|
logger.info(
|
|
'[openstackdocstheme] could not extract project metadata from '
|
|
'setup.cfg'
|
|
)
|
|
return None
|
|
_project = project
|
|
return _project
|
|
|
|
|
|
def _config_inited(app, config):
|
|
if config.openstack_projects is not None:
|
|
logger.info(
|
|
"[openstackdocstheme] "
|
|
"the 'openstack_projects' config option has been deprecated and "
|
|
"replaced by the 'openstackdocs_projects' option; support "
|
|
"for the former will be dropped in a future release"
|
|
)
|
|
config.openstackdocs_projects = config.openstack_projects
|
|
|
|
# we only override configuration if the theme has been configured, meaning
|
|
# users are using these features
|
|
if config.html_theme not in ['openstackdocs', 'starlingxdocs']:
|
|
return
|
|
|
|
if config.openstackdocs_auto_name:
|
|
project_name = _get_project_name(app.srcdir)
|
|
|
|
if config.project and project_name:
|
|
logger.info(
|
|
"[openstackdocstheme] "
|
|
"overriding configured project name (%s) with name extracted "
|
|
"from the package (%s); you can disable this behavior with "
|
|
"the 'openstackdocs_auto_name' option",
|
|
config.project,
|
|
project_name,
|
|
)
|
|
|
|
if project_name:
|
|
config.project = project_name
|
|
|
|
config.html_last_updated_fmt = '%Y-%m-%d %H:%M'
|
|
|
|
if config.openstackdocs_auto_version is False:
|
|
logger.debug(
|
|
'[openstackdocstheme] auto-versioning disabled (configured by '
|
|
'user)'
|
|
)
|
|
auto_version = False
|
|
elif config.openstackdocs_auto_version is True:
|
|
logger.debug(
|
|
'[openstackdocstheme] auto-versioning enabled (configured by user)'
|
|
)
|
|
auto_version = True
|
|
else: # None
|
|
doc_parts = os.path.abspath(app.srcdir).split(os.sep)[-2:]
|
|
if doc_parts[0] in ('api-guide', 'api-ref', 'releasenotes'):
|
|
logger.debug(
|
|
f'[openstackdocstheme] auto-versioning disabled (doc name '
|
|
f'contains {doc_parts[0]}'
|
|
)
|
|
auto_version = False
|
|
else:
|
|
logger.debug(
|
|
'[openstackdocstheme] auto-versioning enabled (default)'
|
|
)
|
|
auto_version = True
|
|
|
|
if auto_version:
|
|
real_project_name = _get_project_name(app.srcdir)
|
|
try:
|
|
project_version = packaging.get_version(real_project_name)
|
|
except Exception:
|
|
project_version = ''
|
|
|
|
if not project_version:
|
|
logger.warning(
|
|
'[openstackdocstheme] could not extract version from '
|
|
'project; defaulting to unversioned'
|
|
)
|
|
|
|
config.version = project_version
|
|
config.release = project_version
|
|
|
|
|
|
def _builder_inited(app):
|
|
theme_dir = paths.get_html_theme_path()
|
|
logger.info('[openstackdocstheme] using theme from %s', theme_dir)
|
|
|
|
_setup_link_roles(app)
|
|
|
|
# we only override configuration if the theme has been configured, meaning
|
|
# users are using these features
|
|
if app.config.html_theme not in ['openstackdocs', 'starlingxdocs']:
|
|
return
|
|
|
|
# Override default setting
|
|
app.config.latex_engine = 'xelatex'
|
|
|
|
theme_logo = paths.get_theme_logo_path(app.config.html_theme)
|
|
pdf_theme_path = paths.get_pdf_theme_path(app.config.html_theme)
|
|
latex_elements = {
|
|
'papersize': 'a4paper',
|
|
'pointsize': '11pt',
|
|
'figure_align': 'H',
|
|
'classoptions': ',openany',
|
|
}
|
|
|
|
if app.config.latex_elements:
|
|
latex_elements.update(app.config.latex_elements)
|
|
|
|
preamble = textwrap.dedent(
|
|
r"""
|
|
\usepackage{%s}
|
|
\\newcommand{\openstacklogo}{%s}
|
|
"""
|
|
) % (pdf_theme_path, theme_logo)
|
|
|
|
if 'preamble' in latex_elements:
|
|
preamble += latex_elements['preamble']
|
|
|
|
latex_elements['preamble'] = preamble
|
|
|
|
app.config.latex_elements = latex_elements
|
|
|
|
|
|
def setup(app):
|
|
logger.info(
|
|
'[openstackdocstheme] version: %s',
|
|
version.version_info.version_string(),
|
|
)
|
|
logger.debug('[openstackdocstheme] connecting events')
|
|
|
|
# extensions
|
|
app.connect('config-inited', _config_inited)
|
|
app.connect('builder-inited', _builder_inited)
|
|
app.connect('html-page-context', _html_page_context)
|
|
|
|
# config options
|
|
app.add_config_value('openstackdocs_repo_name', '', 'env')
|
|
app.add_config_value('openstackdocs_bug_project', '', 'env')
|
|
app.add_config_value('openstackdocs_bug_tag', '', 'env')
|
|
app.add_config_value('openstackdocs_projects', [], 'env')
|
|
app.add_config_value('openstackdocs_use_storyboard', False, 'env')
|
|
app.add_config_value('openstackdocs_auto_version', None, 'env')
|
|
app.add_config_value('openstackdocs_auto_name', True, 'env')
|
|
app.add_config_value('openstackdocs_pdf_link', False, 'env')
|
|
app.add_config_value('openstackdocs_pdf_filename', None, 'env')
|
|
|
|
# legacy config options
|
|
app.add_config_value('repository_name', None, 'env')
|
|
app.add_config_value('bug_project', None, 'env')
|
|
app.add_config_value('bug_tag', None, 'env')
|
|
app.add_config_value('openstack_projects', None, 'env')
|
|
app.add_config_value('use_storyboard', None, 'env')
|
|
|
|
# themes
|
|
app.add_html_theme(
|
|
'openstackdocs',
|
|
os.path.abspath(os.path.dirname(__file__)) + '/theme/openstackdocs',
|
|
)
|
|
app.add_html_theme(
|
|
'starlingxdocs',
|
|
os.path.abspath(os.path.dirname(__file__)) + '/theme/starlingxdocs',
|
|
)
|
|
|
|
return {
|
|
'parallel_read_safe': True,
|
|
'parallel_write_safe': True,
|
|
'version': version.version_info.version_string(),
|
|
}
|