Skip to content

Commit

Permalink
RTD: Vendorize sphinx-build-compatibility
Browse files Browse the repository at this point in the history
... to permit publishing `crate-docs-theme` to PyPI.
  • Loading branch information
amotl committed Oct 8, 2024
1 parent 5f16531 commit 9d1a6a8
Show file tree
Hide file tree
Showing 7 changed files with 301 additions and 2 deletions.
2 changes: 2 additions & 0 deletions CHANGES.rst
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,8 @@ Unreleased
- RTD: Add compatibility fixes for upstream changes at Read The Docs
https://about.readthedocs.com/blog/2024/07/addons-by-default/
- RTD: Fix CSS to hide the RTD project flyout menu
- RTD: Vendorize ``sphinx-build-compatibility`` to permit publishing
``crate-docs-theme`` to PyPI.

2024/09/24 0.34.1
-----------------
Expand Down
1 change: 0 additions & 1 deletion setup.py
Original file line number Diff line number Diff line change
Expand Up @@ -65,7 +65,6 @@
"myst-parser[linkify]<5",
"sphinx>=4.6,<7",
"sphinx-basic-ng==1.0.0b2",
"sphinx-build-compatibility @ git+https://github.com/readthedocs/sphinx-build-compatibility",
"sphinx-copybutton>=0.3.1,<1",
"sphinx-design-elements==0.4.0",
"sphinx-inline-tabs",
Expand Down
3 changes: 2 additions & 1 deletion src/crate/theme/rtd/conf/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -220,7 +220,8 @@ def setup_sphinx_compatibility():
# Exception: 'NoneType' object is not subscriptable
os.environ.setdefault("READTHEDOCS_GIT_COMMIT_HASH", "")

app.setup_extension("sphinx_build_compatibility.extension")
# Register vendorized Sphinx plugin.
app.setup_extension("crate.theme.vendor.rtd_compat.extension")

# Configure Sphinx/RTD to host projects on a custom domain, but also on a non-root resource.
def configure_self_hosted_on_path(app_inited):
Expand Down
Empty file.
Empty file.
242 changes: 242 additions & 0 deletions src/crate/theme/vendor/rtd_compat/extension.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,242 @@
import os

from sphinx import version_info
from sphinx.util import logging

import requests

from .utils import get_github_username_repo, get_bitbucket_username_repo, get_gitlab_username_repo


logger = logging.getLogger(__name__)


# https://www.sphinx-doc.org/en/stable/extdev/appapi.html#event-html-page-context
def manipulate_config(app, config):
logger.info(
'Running "manipulate_config" from Read the Docs "sphinx_build_compatibility" extension. '
'Consider removing it from your requirements and migrating your documentation accordingly. '
'This extension is useful only as a transition but it will not be maintained.'
)

# Add Read the Docs' static path.
# Add to the end because it overwrites previous files.
if not hasattr(config, "html_static_path"):
config.html_static_path = []
if os.path.exists('_static'):
config.html_static_path.append('_static')

# Define this variable in case it's not defined by the user.
# It defaults to `alabaster` which is the default from Sphinx.
# https://www.sphinx-doc.org/en/master/usage/configuration.html#confval-html_theme
if not hasattr(config, "html_theme"):
config.html_theme = 'alabaster'

# Example: ``/docs/``
conf_py_path = "/"
conf_py_path += os.path.relpath(
str(app.srcdir),
os.environ.get("READTHEDOCS_REPOSITORY_PATH"),
).strip("/")
conf_py_path += "/"

git_clone_url = os.environ.get("READTHEDOCS_GIT_CLONE_URL")
github_user, github_repo = get_github_username_repo(git_clone_url)
bitbucket_user, bitbucket_repo = get_bitbucket_username_repo(git_clone_url)
gitlab_user, gitlab_repo = get_gitlab_username_repo(git_clone_url)

project_slug = os.environ.get("READTHEDOCS_PROJECT")
version_slug = os.environ.get("READTHEDOCS_VERSION")
production_domain = os.environ.get("READTHEDOCS_PRODUCTION_DOMAIN", "readthedocs.org")

scheme = "https"
if production_domain.startswith("devthedocs"):
scheme = "http"

# We are using APIv2 to pull active versions, downloads and subprojects
# because APIv3 requires a token.
try:
response_versions = requests.get(
f"{scheme}://{production_domain}/api/v2/version/?project__slug={project_slug}&active=true",
timeout=2,
).json()
versions = [
(version["slug"], f"/{version['project']['language']}/{version['slug']}/")
for version in response_versions["results"]
]
except Exception:
logger.warning(
"An error ocurred when hitting API to fetch active versions. Defaulting to an empty list.",
exc_info=True,
)
versions = []

try:
downloads = []
for version in response_versions["results"]:
if version["slug"] != version_slug:
continue

for key, value in version["downloads"]:
downloads.append(
(
key,
value,
),
)
except Exception:
logger.warning(
"An error ocurred when generating the list of downloads. Defaulting to an empty list.",
exc_info=True,
)
downloads = []

try:
subprojects = []
response_project = requests.get(
f"{scheme}://{production_domain}/api/v2/project/?slug={project_slug}",
timeout=2,
).json()
project_id = response_project["results"][0]["id"]

response_subprojects = requests.get(
f"{scheme}://readthedocs.org/api/v2/project/{project_id}/subprojects/",
timeout=2,
).json()
for subproject in response_subprojects["subprojects"]:
subprojects.append(
(
subproject["slug"],
subproject["canonical_url"],
),
)
except Exception:
logger.warning(
"An error ocurred when hitting API to fetch project/subprojects. Defaulting to an empty list.",
exc_info=True,
)
subprojects = []

# Add project information to the template context.
context = {
'html_theme': config.html_theme,
'current_version': os.environ.get("READTHEDOCS_VERSION_NAME"),
'version_slug': version_slug,

# NOTE: these are used to dump them in some JS files and to build the URLs in flyout.
# However, we are replacing them with the new Addons.
# I wouldn't include them in the first version of the extension.
# We could hardcode them if we want, tho.
#
# 'MEDIA_URL': "{{ settings.MEDIA_URL }}",
# 'STATIC_URL': "{{ settings.STATIC_URL }}",
# 'proxied_static_path': "{{ proxied_static_path }}",

'PRODUCTION_DOMAIN': production_domain,
'versions': versions,
"downloads": downloads,
"subprojects": subprojects,

'slug': project_slug,
'rtd_language': os.environ.get("READTHEDOCS_LANGUAGE"),
'canonical_url': os.environ.get("READTHEDOCS_CANONICAL_URL"),

# NOTE: these seem to not be used.
# 'name': u'{{ project.name }}',
# 'analytics_code': '{{ project.analytics_code }}',
# 'single_version': {{ project.is_single_version }},
# 'programming_language': u'{{ project.programming_language }}',

'conf_py_path': conf_py_path,
# Used only for "readthedocs-sphinx-ext" which we are not installing anymore.
# 'api_host': '{{ api_host }}',
# 'proxied_api_host': '{{ project.proxied_api_host }}',

'github_user': github_user,
'github_repo': github_repo,
'github_version': os.environ.get("READTHEDOCS_GIT_IDENTIFIER"),
'display_github': github_user is not None,
'bitbucket_user': bitbucket_user,
'bitbucket_repo': bitbucket_repo,
'bitbucket_version': os.environ.get("READTHEDOCS_GIT_IDENTIFIER"),
'display_bitbucket': bitbucket_user is not None,
'gitlab_user': gitlab_user,
'gitlab_repo': gitlab_repo,
'gitlab_version': os.environ.get("READTHEDOCS_GIT_IDENTIFIER"),
'display_gitlab': gitlab_user is not None,
'READTHEDOCS': True,
'using_theme': (config.html_theme == "default"),
'new_theme': (config.html_theme == "sphinx_rtd_theme"),
'source_suffix': ".rst",
'ad_free': False,
'docsearch_disabled': False,

# We don't support Google analytics anymore.
# See https://github.com/readthedocs/readthedocs.org/issues/9530
'user_analytics_code': "",
'global_analytics_code': None,

'commit': os.environ.get("READTHEDOCS_GIT_COMMIT_HASH")[:8],
}

# For sphinx >=1.8 we can use html_baseurl to set the canonical URL.
# https://www.sphinx-doc.org/en/master/usage/configuration.html#confval-html_baseurl
if version_info >= (1, 8):
if not hasattr(config, 'html_baseurl'):
config.html_baseurl = context['canonical_url']
context['canonical_url'] = None


if hasattr(config, 'html_context'):
for key in context:
if key not in config.html_context:
config.html_context[key] = context[key]
else:
config.html_context = context

project_language = os.environ.get("READTHEDOCS_LANGUAGE")

# User's Sphinx configurations
language_user = config.language
latex_engine_user = config.latex_engine
latex_elements_user = config.latex_elements

# Remove this once xindy gets installed in Docker image and XINDYOPS
# env variable is supported
# https://github.com/rtfd/readthedocs-docker-images/pull/98
latex_use_xindy = False

chinese = any([
language_user in ('zh_CN', 'zh_TW'),
project_language in ('zh_CN', 'zh_TW'),
])

japanese = any([
language_user == 'ja',
project_language == 'ja',
])

if chinese:
config.latex_engine = latex_engine_user or 'xelatex'

latex_elements_rtd = {
'preamble': '\\usepackage[UTF8]{ctex}\n',
}
config.latex_elements = latex_elements_user or latex_elements_rtd
elif japanese:
config.latex_engine = latex_engine_user or 'platex'

# Make sure our build directory is always excluded
if not hasattr(config, "exclude_patterns"):
config.exclude_patterns = []
config.exclude_patterns.extend(['_build'])


def setup(app):
app.connect('config-inited', manipulate_config)

return {
'version': "0.0.0",
'parallel_read_safe': True,
'parallel_write_safe': True,
}
55 changes: 55 additions & 0 deletions src/crate/theme/vendor/rtd_compat/utils.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,55 @@
import re

# Borrowed from
# https://github.com/readthedocs/readthedocs.org/blob/25e697f1ee63828bba65f41d0c8724769d70402f/readthedocs/projects/constants.py#L345-L366
GITHUB_REGEXS = [
re.compile(r"github.com/(.+)/(.+)(?:\.git){1}$"),
# This must come before the one without a / to make sure we don't capture the /
re.compile(r"github.com/(.+)/(.+)/"),
re.compile(r"github.com/(.+)/(.+)"),
re.compile(r"github.com:(.+)/(.+)\.git$"),
]
BITBUCKET_REGEXS = [
re.compile(r"bitbucket.org/(.+)/(.+)\.git$"),
re.compile(r"@bitbucket.org/(.+)/(.+)\.git$"),
# This must come before the one without a / to make sure we don't capture the /
re.compile(r"bitbucket.org/(.+)/(.+)/"),
re.compile(r"bitbucket.org/(.+)/(.+)"),
re.compile(r"bitbucket.org:(.+)/(.+)\.git$"),
]
GITLAB_REGEXS = [
re.compile(r"gitlab.com/(.+)/(.+)(?:\.git){1}$"),
# This must come before the one without a / to make sure we don't capture the /
re.compile(r"gitlab.com/(.+)/(.+)/"),
re.compile(r"gitlab.com/(.+)/(.+)"),
re.compile(r"gitlab.com:(.+)/(.+)\.git$"),
]


# Borrowed from
# https://github.com/readthedocs/readthedocs.org/blob/25e697f1ee63828bba65f41d0c8724769d70402f/readthedocs/builds/utils.py#L24-L48
def get_github_username_repo(url):
if "github" in url:
for regex in GITHUB_REGEXS:
match = regex.search(url)
if match:
return match.groups()
return (None, None)


def get_bitbucket_username_repo(url=None):
if "bitbucket" in url:
for regex in BITBUCKET_REGEXS:
match = regex.search(url)
if match:
return match.groups()
return (None, None)


def get_gitlab_username_repo(url=None):
if "gitlab" in url:
for regex in GITLAB_REGEXS:
match = regex.search(url)
if match:
return match.groups()
return (None, None)

0 comments on commit 9d1a6a8

Please sign in to comment.