sunpy-sphinx-theme 2.3.0__tar.gz → 2.4.0__tar.gz
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- {sunpy_sphinx_theme-2.3.0 → sunpy_sphinx_theme-2.4.0}/.rtd-environment.yml +1 -1
- {sunpy_sphinx_theme-2.3.0 → sunpy_sphinx_theme-2.4.0}/PKG-INFO +6 -3
- sunpy_sphinx_theme-2.4.0/conftest.py +142 -0
- sunpy_sphinx_theme-2.4.0/flake.nix +34 -0
- sunpy_sphinx_theme-2.4.0/package.json +8 -0
- {sunpy_sphinx_theme-2.3.0 → sunpy_sphinx_theme-2.4.0}/pyproject.toml +11 -2
- {sunpy_sphinx_theme-2.3.0 → sunpy_sphinx_theme-2.4.0}/ruff.toml +5 -8
- {sunpy_sphinx_theme-2.3.0 → sunpy_sphinx_theme-2.4.0}/src/sunpy_sphinx_theme/__init__.py +66 -47
- sunpy_sphinx_theme-2.4.0/src/sunpy_sphinx_theme/tests/__init__.py +3 -0
- sunpy_sphinx_theme-2.4.0/src/sunpy_sphinx_theme/tests/helpers.py +13 -0
- sunpy_sphinx_theme-2.4.0/src/sunpy_sphinx_theme/tests/sites/base/conf.py +9 -0
- sunpy_sphinx_theme-2.4.0/src/sunpy_sphinx_theme/tests/sites/base/index.rst +4 -0
- sunpy_sphinx_theme-2.4.0/src/sunpy_sphinx_theme/tests/sites/derived/conf.py +16 -0
- sunpy_sphinx_theme-2.4.0/src/sunpy_sphinx_theme/tests/sites/derived/index.rst +4 -0
- sunpy_sphinx_theme-2.4.0/src/sunpy_sphinx_theme/tests/sites/derived/theme/theme.toml +15 -0
- sunpy_sphinx_theme-2.4.0/src/sunpy_sphinx_theme/tests/test_a11y.py +177 -0
- sunpy_sphinx_theme-2.4.0/src/sunpy_sphinx_theme/tests/test_config_subtheme.py +64 -0
- sunpy_sphinx_theme-2.4.0/src/sunpy_sphinx_theme/tests/test_goatcounter.py +164 -0
- sunpy_sphinx_theme-2.4.0/src/sunpy_sphinx_theme/tests/test_options.py +17 -0
- {sunpy_sphinx_theme-2.3.0 → sunpy_sphinx_theme-2.4.0}/src/sunpy_sphinx_theme/theme/sunpy/theme.toml +3 -3
- {sunpy_sphinx_theme-2.3.0 → sunpy_sphinx_theme-2.4.0}/src/sunpy_sphinx_theme.egg-info/PKG-INFO +6 -3
- {sunpy_sphinx_theme-2.3.0 → sunpy_sphinx_theme-2.4.0}/src/sunpy_sphinx_theme.egg-info/SOURCES.txt +14 -0
- {sunpy_sphinx_theme-2.3.0 → sunpy_sphinx_theme-2.4.0}/src/sunpy_sphinx_theme.egg-info/requires.txt +5 -1
- {sunpy_sphinx_theme-2.3.0 → sunpy_sphinx_theme-2.4.0}/src/sunpy_sphinx_theme.egg-info/scm_file_list.json +14 -0
- sunpy_sphinx_theme-2.4.0/src/sunpy_sphinx_theme.egg-info/scm_version.json +8 -0
- sunpy_sphinx_theme-2.3.0/src/sunpy_sphinx_theme.egg-info/scm_version.json +0 -8
- {sunpy_sphinx_theme-2.3.0 → sunpy_sphinx_theme-2.4.0}/.editorconfig +0 -0
- {sunpy_sphinx_theme-2.3.0 → sunpy_sphinx_theme-2.4.0}/.readthedocs.yaml +0 -0
- {sunpy_sphinx_theme-2.3.0 → sunpy_sphinx_theme-2.4.0}/.stylelintrc.json +0 -0
- {sunpy_sphinx_theme-2.3.0 → sunpy_sphinx_theme-2.4.0}/LICENSE.md +0 -0
- {sunpy_sphinx_theme-2.3.0 → sunpy_sphinx_theme-2.4.0}/MANIFEST.in +0 -0
- {sunpy_sphinx_theme-2.3.0 → sunpy_sphinx_theme-2.4.0}/README.md +0 -0
- {sunpy_sphinx_theme-2.3.0 → sunpy_sphinx_theme-2.4.0}/_typos.toml +0 -0
- {sunpy_sphinx_theme-2.3.0 → sunpy_sphinx_theme-2.4.0}/setup.cfg +0 -0
- {sunpy_sphinx_theme-2.3.0 → sunpy_sphinx_theme-2.4.0}/src/sunpy_sphinx_theme/cards.py +0 -0
- {sunpy_sphinx_theme-2.3.0 → sunpy_sphinx_theme-2.4.0}/src/sunpy_sphinx_theme/conf.py +0 -0
- {sunpy_sphinx_theme-2.3.0 → sunpy_sphinx_theme-2.4.0}/src/sunpy_sphinx_theme/theme/sunpy/components/built-with.html +0 -0
- {sunpy_sphinx_theme-2.3.0 → sunpy_sphinx_theme-2.4.0}/src/sunpy_sphinx_theme/theme/sunpy/components/footer-links.html +0 -0
- {sunpy_sphinx_theme-2.3.0 → sunpy_sphinx_theme-2.4.0}/src/sunpy_sphinx_theme/theme/sunpy/components/navbar-logo.html +0 -0
- {sunpy_sphinx_theme-2.3.0 → sunpy_sphinx_theme-2.4.0}/src/sunpy_sphinx_theme/theme/sunpy/components/navbar_center.html +0 -0
- {sunpy_sphinx_theme-2.3.0 → sunpy_sphinx_theme-2.4.0}/src/sunpy_sphinx_theme/theme/sunpy/components/navbar_start.html +0 -0
- {sunpy_sphinx_theme-2.3.0 → sunpy_sphinx_theme-2.4.0}/src/sunpy_sphinx_theme/theme/sunpy/components/sst-sidebar-nav.html +0 -0
- {sunpy_sphinx_theme-2.3.0 → sunpy_sphinx_theme-2.4.0}/src/sunpy_sphinx_theme/theme/sunpy/sections/header.html +0 -0
- {sunpy_sphinx_theme-2.3.0 → sunpy_sphinx_theme-2.4.0}/src/sunpy_sphinx_theme/theme/sunpy/static/css/rtd_enhanced_search.css +0 -0
- {sunpy_sphinx_theme-2.3.0 → sunpy_sphinx_theme-2.4.0}/src/sunpy_sphinx_theme/theme/sunpy/static/css/sunpy_cards.css +0 -0
- {sunpy_sphinx_theme-2.3.0 → sunpy_sphinx_theme-2.4.0}/src/sunpy_sphinx_theme/theme/sunpy/static/img/favicon-32.ico +0 -0
- {sunpy_sphinx_theme-2.3.0 → sunpy_sphinx_theme-2.4.0}/src/sunpy_sphinx_theme/theme/sunpy/static/img/sunpy_icon.svg +0 -0
- {sunpy_sphinx_theme-2.3.0 → sunpy_sphinx_theme-2.4.0}/src/sunpy_sphinx_theme/theme/sunpy/static/img/sunpy_icon_128x128.png +0 -0
- {sunpy_sphinx_theme-2.3.0 → sunpy_sphinx_theme-2.4.0}/src/sunpy_sphinx_theme/theme/sunpy/static/js/rtd_enhanced_search.js +0 -0
- {sunpy_sphinx_theme-2.3.0 → sunpy_sphinx_theme-2.4.0}/src/sunpy_sphinx_theme/theme/sunpy/static/js/submenu-concertina-toggle.js +0 -0
- {sunpy_sphinx_theme-2.3.0 → sunpy_sphinx_theme-2.4.0}/src/sunpy_sphinx_theme/theme/sunpy/static/sunpy_style.css +0 -0
- {sunpy_sphinx_theme-2.3.0 → sunpy_sphinx_theme-2.4.0}/src/sunpy_sphinx_theme.egg-info/dependency_links.txt +0 -0
- {sunpy_sphinx_theme-2.3.0 → sunpy_sphinx_theme-2.4.0}/src/sunpy_sphinx_theme.egg-info/entry_points.txt +0 -0
- {sunpy_sphinx_theme-2.3.0 → sunpy_sphinx_theme-2.4.0}/src/sunpy_sphinx_theme.egg-info/top_level.txt +0 -0
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: sunpy-sphinx-theme
|
|
3
|
-
Version: 2.
|
|
3
|
+
Version: 2.4.0
|
|
4
4
|
Summary: The sphinx theme for the SunPy website and documentation.
|
|
5
5
|
Author: The SunPy Developers
|
|
6
6
|
License: BSD 2-Clause License
|
|
@@ -36,10 +36,10 @@ Classifier: Programming Language :: Python :: 3
|
|
|
36
36
|
Classifier: Framework :: Sphinx
|
|
37
37
|
Classifier: Framework :: Sphinx :: Theme
|
|
38
38
|
Classifier: Operating System :: OS Independent
|
|
39
|
-
Requires-Python: >=3.
|
|
39
|
+
Requires-Python: >=3.11
|
|
40
40
|
Description-Content-Type: text/markdown
|
|
41
41
|
License-File: LICENSE.md
|
|
42
|
-
Requires-Dist: pydata-sphinx-theme<0.
|
|
42
|
+
Requires-Dist: pydata-sphinx-theme<0.24,>=0.22.0
|
|
43
43
|
Requires-Dist: sphinxext-opengraph>=0.13
|
|
44
44
|
Requires-Dist: sphinx>=7.3.0
|
|
45
45
|
Provides-Extra: docs
|
|
@@ -50,6 +50,9 @@ Requires-Dist: sphinx-design; extra == "docs"
|
|
|
50
50
|
Requires-Dist: sphinx-gallery; extra == "docs"
|
|
51
51
|
Requires-Dist: sphinx-togglebutton; extra == "docs"
|
|
52
52
|
Requires-Dist: sunpy; extra == "docs"
|
|
53
|
+
Provides-Extra: test
|
|
54
|
+
Requires-Dist: pytest>=7.0; extra == "test"
|
|
55
|
+
Requires-Dist: pytest-playwright; extra == "test"
|
|
53
56
|
Dynamic: license-file
|
|
54
57
|
|
|
55
58
|
# SunPy Project Sphinx Theme
|
|
@@ -0,0 +1,142 @@
|
|
|
1
|
+
"""Configuration of the pytest session."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
import sys
|
|
6
|
+
import time
|
|
7
|
+
from http.client import HTTPConnection
|
|
8
|
+
from os import environ
|
|
9
|
+
from pathlib import Path
|
|
10
|
+
from shutil import copytree
|
|
11
|
+
from subprocess import PIPE, Popen
|
|
12
|
+
from typing import TYPE_CHECKING
|
|
13
|
+
|
|
14
|
+
import pytest
|
|
15
|
+
from bs4 import BeautifulSoup
|
|
16
|
+
|
|
17
|
+
if TYPE_CHECKING:
|
|
18
|
+
from collections.abc import Callable
|
|
19
|
+
|
|
20
|
+
from sphinx.testing.util import SphinxTestApp
|
|
21
|
+
|
|
22
|
+
pytest_plugins = "sphinx.testing.fixtures"
|
|
23
|
+
|
|
24
|
+
repo_path = Path(__file__).parent
|
|
25
|
+
tests_path = repo_path / "src" / "sunpy_sphinx_theme" / "tests"
|
|
26
|
+
docs_build_path = repo_path / "docs" / "_build" / "html"
|
|
27
|
+
|
|
28
|
+
|
|
29
|
+
# -- global fixture to build sphinx tmp docs ---------------------------------
|
|
30
|
+
|
|
31
|
+
|
|
32
|
+
class SphinxBuild:
|
|
33
|
+
"""Helper class to build a test documentation."""
|
|
34
|
+
|
|
35
|
+
def __init__(self, app: SphinxTestApp, src: Path):
|
|
36
|
+
self.app = app
|
|
37
|
+
self.src = src
|
|
38
|
+
|
|
39
|
+
def build(self, *, no_warning: bool = True) -> SphinxBuild:
|
|
40
|
+
"""Build the application."""
|
|
41
|
+
self.app.build()
|
|
42
|
+
if no_warning is True:
|
|
43
|
+
assert self.warnings == "", self.status
|
|
44
|
+
return self
|
|
45
|
+
|
|
46
|
+
@property
|
|
47
|
+
def status(self) -> str:
|
|
48
|
+
"""Returns the status of the current build."""
|
|
49
|
+
return self.app.status.getvalue()
|
|
50
|
+
|
|
51
|
+
@property
|
|
52
|
+
def warnings(self) -> str:
|
|
53
|
+
"""Returns the warnings raised by the current build."""
|
|
54
|
+
return self.app.warning.getvalue()
|
|
55
|
+
|
|
56
|
+
@property
|
|
57
|
+
def outdir(self) -> Path:
|
|
58
|
+
"""Returns the output directory of the current build."""
|
|
59
|
+
return Path(self.app.outdir)
|
|
60
|
+
|
|
61
|
+
def html_tree(self, *path) -> BeautifulSoup:
|
|
62
|
+
"""Returns the html tree of the current build."""
|
|
63
|
+
path_page = self.outdir.joinpath(*path)
|
|
64
|
+
if not path_page.exists():
|
|
65
|
+
msg = f"{path_page} does not exist"
|
|
66
|
+
raise ValueError(msg)
|
|
67
|
+
return BeautifulSoup(path_page.read_text("utf8"), "html.parser")
|
|
68
|
+
|
|
69
|
+
|
|
70
|
+
@pytest.fixture
|
|
71
|
+
def sphinx_build_factory(make_app: Callable, tmp_path: Path, request) -> Callable:
|
|
72
|
+
"""Return a factory builder pointing to the tmp directory."""
|
|
73
|
+
|
|
74
|
+
def _func(src_folder: str, files: dict[str, str] | None = None, **kwargs) -> SphinxBuild:
|
|
75
|
+
"""Create the SphinxBuild from the source folder."""
|
|
76
|
+
no_temp = environ.get("SST_TEST_HTML_DIR")
|
|
77
|
+
nonlocal tmp_path
|
|
78
|
+
if no_temp is not None:
|
|
79
|
+
tmp_path = Path(no_temp) / request.node.name / str(src_folder)
|
|
80
|
+
srcdir = tmp_path / src_folder
|
|
81
|
+
copytree(tests_path / "sites" / src_folder, tmp_path / src_folder)
|
|
82
|
+
for filename, contents in (files or {}).items():
|
|
83
|
+
filepath = Path(srcdir / filename).resolve()
|
|
84
|
+
filepath.parent.mkdir(exist_ok=True, parents=True)
|
|
85
|
+
filepath.write_text(contents)
|
|
86
|
+
app = make_app(srcdir=srcdir, **kwargs)
|
|
87
|
+
return SphinxBuild(app, tmp_path / src_folder)
|
|
88
|
+
|
|
89
|
+
return _func
|
|
90
|
+
|
|
91
|
+
|
|
92
|
+
# -- global fixture to serve the built docs ----------------------------------
|
|
93
|
+
|
|
94
|
+
|
|
95
|
+
@pytest.fixture(scope="module")
|
|
96
|
+
def url_base():
|
|
97
|
+
"""Start local server on built docs and return the localhost URL as the base URL."""
|
|
98
|
+
# The accessibility tests are run against a build of our documentation, so
|
|
99
|
+
# fail early with a helpful message if it has not been built yet.
|
|
100
|
+
if not docs_build_path.is_dir():
|
|
101
|
+
msg = f"No docs build found at {docs_build_path}; build it first with 'tox -e build_docs'"
|
|
102
|
+
pytest.fail(msg, pytrace=False)
|
|
103
|
+
|
|
104
|
+
# Use a port that is not commonly used during development or else you will
|
|
105
|
+
# force the developer to stop running their dev server in order to run the
|
|
106
|
+
# tests.
|
|
107
|
+
port = 8213
|
|
108
|
+
host = "localhost"
|
|
109
|
+
url = f"http://{host}:{port}"
|
|
110
|
+
|
|
111
|
+
# Try starting the server
|
|
112
|
+
process = Popen(
|
|
113
|
+
[sys.executable, "-m", "http.server", str(port), "--directory", docs_build_path],
|
|
114
|
+
stdout=PIPE,
|
|
115
|
+
)
|
|
116
|
+
|
|
117
|
+
# Try connecting to the server
|
|
118
|
+
retries = 5
|
|
119
|
+
while retries > 0:
|
|
120
|
+
conn = HTTPConnection(host, port)
|
|
121
|
+
try:
|
|
122
|
+
conn.request("HEAD", "/")
|
|
123
|
+
response = conn.getresponse()
|
|
124
|
+
if response is not None:
|
|
125
|
+
yield url
|
|
126
|
+
break
|
|
127
|
+
except ConnectionRefusedError:
|
|
128
|
+
time.sleep(1)
|
|
129
|
+
retries -= 1
|
|
130
|
+
finally:
|
|
131
|
+
conn.close()
|
|
132
|
+
|
|
133
|
+
# If the code above never yields a URL, then we were never able to connect
|
|
134
|
+
# to the server and retries == 0.
|
|
135
|
+
if not retries:
|
|
136
|
+
msg_0 = "Failed to start http server in 5 seconds"
|
|
137
|
+
raise RuntimeError(msg_0)
|
|
138
|
+
else:
|
|
139
|
+
# Otherwise the server started and this fixture is done now and we clean
|
|
140
|
+
# up by stopping the server.
|
|
141
|
+
process.terminate()
|
|
142
|
+
process.wait()
|
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
{
|
|
2
|
+
description = "A very basic flake";
|
|
3
|
+
|
|
4
|
+
inputs = {
|
|
5
|
+
flake-parts.url = "github:hercules-ci/flake-parts";
|
|
6
|
+
nixpkgs.url = "github:nixos/nixpkgs";
|
|
7
|
+
};
|
|
8
|
+
|
|
9
|
+
outputs = { flake-parts, ... } @ inputs: flake-parts.lib.mkFlake { inherit inputs; } {
|
|
10
|
+
perSystem = { config, self', inputs', pkgs, system, ... }: {
|
|
11
|
+
devShells.default = pkgs.mkShell {
|
|
12
|
+
# venvShellHook needs venvDir set on the shell
|
|
13
|
+
venvDir = ".venv";
|
|
14
|
+
|
|
15
|
+
packages = with pkgs; [
|
|
16
|
+
nodejs_latest
|
|
17
|
+
playwright-driver.browsers
|
|
18
|
+
uv
|
|
19
|
+
];
|
|
20
|
+
|
|
21
|
+
# runs every time you enter the shell, after activation
|
|
22
|
+
shellHook = ''
|
|
23
|
+
export PLAYWRIGHT_BROWSERS_PATH=${pkgs.playwright-driver.browsers}
|
|
24
|
+
export PLAYWRIGHT_SKIP_VALIDATE_HOST_REQUIREMENTS=true;
|
|
25
|
+
uv sync --all-extras
|
|
26
|
+
source .venv/bin/activate
|
|
27
|
+
npm install
|
|
28
|
+
'';
|
|
29
|
+
};
|
|
30
|
+
};
|
|
31
|
+
# Declared systems that your flake supports. These will be enumerated in perSystem
|
|
32
|
+
systems = [ "x86_64-linux" "aarch64-linux" "x86_64-darwin" "aarch64-darwin" ];
|
|
33
|
+
};
|
|
34
|
+
}
|
|
@@ -20,9 +20,9 @@ classifiers = [
|
|
|
20
20
|
"Framework :: Sphinx :: Theme",
|
|
21
21
|
"Operating System :: OS Independent",
|
|
22
22
|
]
|
|
23
|
-
requires-python = ">=3.
|
|
23
|
+
requires-python = ">=3.11"
|
|
24
24
|
dependencies = [
|
|
25
|
-
"pydata-sphinx-theme>=0.22.
|
|
25
|
+
"pydata-sphinx-theme>=0.22.0,<0.24",
|
|
26
26
|
"sphinxext-opengraph>=0.13",
|
|
27
27
|
"sphinx>=7.3.0"
|
|
28
28
|
]
|
|
@@ -37,6 +37,10 @@ docs = [
|
|
|
37
37
|
"sphinx-togglebutton",
|
|
38
38
|
"sunpy",
|
|
39
39
|
]
|
|
40
|
+
test = [
|
|
41
|
+
"pytest>=7.0",
|
|
42
|
+
"pytest-playwright",
|
|
43
|
+
]
|
|
40
44
|
|
|
41
45
|
[project.entry-points]
|
|
42
46
|
"sphinx.html_themes" = { sunpy = "sunpy_sphinx_theme" }
|
|
@@ -46,6 +50,11 @@ include-package-data = true
|
|
|
46
50
|
|
|
47
51
|
[tool.setuptools_scm]
|
|
48
52
|
|
|
53
|
+
[tool.pytest.ini_options]
|
|
54
|
+
markers = "a11y: mark a test as an accessibility test"
|
|
55
|
+
testpaths = ["tests"]
|
|
56
|
+
addopts = "--durations=10"
|
|
57
|
+
|
|
49
58
|
[ tool.gilesbot ]
|
|
50
59
|
[ tool.gilesbot.circleci_artifacts.giles ]
|
|
51
60
|
enabled = false
|
|
@@ -49,17 +49,14 @@ lint.extend-ignore = [
|
|
|
49
49
|
"setup.py" = [
|
|
50
50
|
"D100", # Missing docstring in public module
|
|
51
51
|
]
|
|
52
|
-
"test_*.py" = [
|
|
52
|
+
"{src/sunpy_sphinx_theme/tests/*.py,conftest.py,test_*.py}" = [
|
|
53
53
|
"D100", # Missing docstring in public module
|
|
54
54
|
"D103", # Missing docstring in public function
|
|
55
|
-
"
|
|
55
|
+
"INP001", # tests are not part of an implicit namespace package
|
|
56
56
|
"S101", # Use of `assert` detected
|
|
57
|
-
|
|
58
|
-
"
|
|
59
|
-
"
|
|
60
|
-
]
|
|
61
|
-
"sunpy_soar/conftest.py" = [
|
|
62
|
-
"D100", # Missing docstring in public module
|
|
57
|
+
"S404", # subprocess module is possibly insecure
|
|
58
|
+
"S603", # subprocess call: check for execution of untrusted input
|
|
59
|
+
"ARG001", # Unused function argument (kept for interface compatibility)
|
|
63
60
|
]
|
|
64
61
|
|
|
65
62
|
[lint.pydocstyle]
|
|
@@ -12,6 +12,9 @@ from urllib.parse import urljoin, urlparse
|
|
|
12
12
|
|
|
13
13
|
from pydata_sphinx_theme import utils
|
|
14
14
|
from sphinx.application import Sphinx
|
|
15
|
+
from sphinx.util import logging
|
|
16
|
+
|
|
17
|
+
logger = logging.getLogger(__name__)
|
|
15
18
|
|
|
16
19
|
__all__ = ["ON_RTD", "PNG_ICON", "SVG_ICON", "get_html_theme_path"]
|
|
17
20
|
|
|
@@ -45,7 +48,7 @@ def update_config(app) -> None:
|
|
|
45
48
|
theme_options = utils.get_theme_options_dict(app)
|
|
46
49
|
|
|
47
50
|
if theme_options_with_defaults.get("sst_logo") and not isinstance(theme_options_with_defaults["sst_logo"], dict):
|
|
48
|
-
sst_logo = str(
|
|
51
|
+
sst_logo = str(theme_options_with_defaults["sst_logo"])
|
|
49
52
|
theme_options["sst_logo"] = {"light": sst_logo, "dark": sst_logo}
|
|
50
53
|
|
|
51
54
|
theme_options["sst_is_root"] = bool(theme_options_with_defaults.get("sst_is_root", False))
|
|
@@ -188,28 +191,43 @@ def generate_search_config(app):
|
|
|
188
191
|
This function parses the config for the "Documentation" section of the theme config.
|
|
189
192
|
"""
|
|
190
193
|
theme_config = get_theme_options(app)
|
|
191
|
-
|
|
194
|
+
|
|
195
|
+
if not theme_config.get("rtd_search", True):
|
|
196
|
+
return
|
|
197
|
+
|
|
198
|
+
# Add project-wide search
|
|
199
|
+
app.add_css_file("css/rtd_enhanced_search.css")
|
|
200
|
+
app.add_js_file(
|
|
201
|
+
"js/rtd_enhanced_search.js",
|
|
202
|
+
loading_method="async",
|
|
203
|
+
)
|
|
204
|
+
|
|
205
|
+
def filter_doc_links(links):
|
|
206
|
+
out_links = []
|
|
207
|
+
for link in links:
|
|
208
|
+
if isinstance(link[1], list):
|
|
209
|
+
out_links += filter_doc_links(link[1])
|
|
210
|
+
elif isinstance(link[1], str) and link[1].startswith("http"):
|
|
211
|
+
out_links.append({"name": link[0], "link": link[1]})
|
|
212
|
+
else:
|
|
213
|
+
err = f"Unable to parse {link} in the nav tree. Try setting search_projects explicitly or fixing navbar_links."
|
|
214
|
+
raise ValueError(err)
|
|
215
|
+
return out_links
|
|
216
|
+
|
|
217
|
+
search_projects = theme_config.get("rtd_search_projects", []) or []
|
|
192
218
|
if not search_projects:
|
|
193
219
|
navbar_links = theme_config["navbar_links"]
|
|
194
|
-
doc_links = next(section[1] for section in navbar_links if section[0] == "Documentation")
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
out_links = []
|
|
198
|
-
for link in links:
|
|
199
|
-
if isinstance(link[1], list):
|
|
200
|
-
out_links += filter_doc_links(link[1])
|
|
201
|
-
elif isinstance(link[1], str) and link[1].startswith("http"):
|
|
202
|
-
out_links.append({"name": link[0], "link": link[1]})
|
|
203
|
-
else:
|
|
204
|
-
err = f"Unable to parse {link} in the nav tree. Try setting search_projects explicitly or fixing navbar_links."
|
|
205
|
-
raise ValueError(err)
|
|
206
|
-
return out_links
|
|
207
|
-
|
|
208
|
-
search_projects = filter_doc_links(doc_links)
|
|
220
|
+
doc_links = next((section[1] for section in navbar_links if section[0] == "Documentation"), None)
|
|
221
|
+
if doc_links:
|
|
222
|
+
search_projects = filter_doc_links(doc_links)
|
|
209
223
|
|
|
210
224
|
if extra_search_projects := theme_config.get("rtd_extra_search_projects", None):
|
|
211
225
|
search_projects += filter_doc_links(extra_search_projects)
|
|
212
226
|
|
|
227
|
+
if not search_projects:
|
|
228
|
+
logger.info("No RTD search projects were found, disabling RTD search.")
|
|
229
|
+
return
|
|
230
|
+
|
|
213
231
|
load_more_label = theme_config.get("rtd_search_load_more_label", "Load more results")
|
|
214
232
|
no_results_label = theme_config.get("rtd_search_no_results_label", "There are no results for this search")
|
|
215
233
|
script = dedent(f"""
|
|
@@ -227,30 +245,24 @@ def generate_search_config(app):
|
|
|
227
245
|
app.add_js_file(None, body=script)
|
|
228
246
|
|
|
229
247
|
|
|
230
|
-
def
|
|
231
|
-
|
|
232
|
-
|
|
233
|
-
|
|
234
|
-
|
|
235
|
-
app.add_html_theme("sunpy", theme_dir)
|
|
236
|
-
app.add_css_file("sunpy_style.css", priority=600)
|
|
237
|
-
app.connect("builder-inited", update_config, priority=100)
|
|
238
|
-
app.connect("builder-inited", generate_search_config, priority=500)
|
|
239
|
-
app.connect("html-page-context", update_opengraph_context, priority=400)
|
|
240
|
-
app.connect("html-page-context", update_html_context)
|
|
241
|
-
# Conditionally include goat counter js
|
|
242
|
-
# We can't do this in update_config as that causes the scripts to be duplicated.
|
|
243
|
-
# Also in here none of the theme defaults have be applied by `update_config`
|
|
244
|
-
# TODO: Improve this mess
|
|
245
|
-
theme_options = utils.get_theme_options_dict(app)
|
|
248
|
+
def configure_goatcounter(app):
|
|
249
|
+
"""
|
|
250
|
+
Conditionally include goat counter js and configure endpoint.
|
|
251
|
+
"""
|
|
252
|
+
theme_options = get_theme_options(app)
|
|
246
253
|
# We want to default to the sunpy goat counter only if the sst_site_root is sunpy.org
|
|
247
254
|
root_domain = theme_options.get("sst_site_root", "https://sunpy.org")
|
|
248
255
|
sunpy_goat_url = "https://sunpy.goatcounter.com/count"
|
|
249
|
-
default_goat_url =
|
|
250
|
-
|
|
256
|
+
default_goat_url = (
|
|
257
|
+
sunpy_goat_url if root_domain == "https://sunpy.org" else False
|
|
258
|
+
) # Only default on sunpy.org pages
|
|
259
|
+
# The theme default is 'DEFAULT', to allow disabling it
|
|
260
|
+
primary_goat_url = theme_options.get("goatcounter_analytics_url", "DEFAULT")
|
|
261
|
+
primary_goat_url = primary_goat_url if primary_goat_url != "DEFAULT" else default_goat_url
|
|
262
|
+
if primary_goat_url:
|
|
251
263
|
root_domain = root_domain.removeprefix("https://").removeprefix("http://")
|
|
252
264
|
default_endpoint = theme_options.get("goatcounter_non_domain_endpoint", False)
|
|
253
|
-
if default_endpoint
|
|
265
|
+
if default_endpoint in (False, "False"): # sphinx config can sometimes be str
|
|
254
266
|
default_endpoint = ""
|
|
255
267
|
app.add_js_file(
|
|
256
268
|
None,
|
|
@@ -270,18 +282,25 @@ def setup(app: Sphinx):
|
|
|
270
282
|
"https://gc.zgo.at/count.js",
|
|
271
283
|
loading_method="async",
|
|
272
284
|
)
|
|
273
|
-
app.add_js_file(
|
|
274
|
-
"js/submenu-concertina-toggle.js",
|
|
275
|
-
loading_method="async",
|
|
276
|
-
)
|
|
277
285
|
|
|
278
|
-
|
|
279
|
-
|
|
280
|
-
|
|
281
|
-
|
|
282
|
-
|
|
283
|
-
|
|
284
|
-
|
|
286
|
+
|
|
287
|
+
def setup(app: Sphinx):
|
|
288
|
+
app.setup_extension("sphinxext.opengraph")
|
|
289
|
+
|
|
290
|
+
# Register theme
|
|
291
|
+
theme_dir = get_html_theme_path()
|
|
292
|
+
app.add_html_theme("sunpy", theme_dir)
|
|
293
|
+
app.add_css_file("sunpy_style.css", priority=600)
|
|
294
|
+
app.connect("builder-inited", update_config, priority=100)
|
|
295
|
+
app.connect("builder-inited", generate_search_config, priority=500)
|
|
296
|
+
app.connect("builder-inited", configure_goatcounter, priority=510)
|
|
297
|
+
app.connect("html-page-context", update_opengraph_context, priority=400)
|
|
298
|
+
app.connect("html-page-context", update_html_context)
|
|
299
|
+
|
|
300
|
+
app.add_js_file(
|
|
301
|
+
"js/submenu-concertina-toggle.js",
|
|
302
|
+
loading_method="async",
|
|
303
|
+
)
|
|
285
304
|
|
|
286
305
|
return {
|
|
287
306
|
"parallel_read_safe": True,
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
def inline_scripts(html_tree):
|
|
2
|
+
"""Return the content of all inline scripts in a page."""
|
|
3
|
+
return [script.string for script in html_tree.select("script") if script.string]
|
|
4
|
+
|
|
5
|
+
|
|
6
|
+
def script_srcs(html_tree):
|
|
7
|
+
"""Return the src URLs of all external scripts in a page."""
|
|
8
|
+
return [script["src"] for script in html_tree.select("script[src]")]
|
|
9
|
+
|
|
10
|
+
|
|
11
|
+
def stylesheet_hrefs(html_tree):
|
|
12
|
+
"""Return the hrefs of all stylesheets in a page."""
|
|
13
|
+
return [link["href"] for link in html_tree.select("link[rel='stylesheet']")]
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
"""Test conf file for a theme derived from the sunpy theme."""
|
|
2
|
+
|
|
3
|
+
from pathlib import Path
|
|
4
|
+
|
|
5
|
+
project = "SunPy Derived Theme Tests"
|
|
6
|
+
copyright = "2026, The SunPy Community" # NOQA: A001
|
|
7
|
+
author = "The SunPy Community"
|
|
8
|
+
|
|
9
|
+
extensions = ["sunpy_sphinx_theme"]
|
|
10
|
+
html_theme = "derived"
|
|
11
|
+
html_static_path = []
|
|
12
|
+
|
|
13
|
+
|
|
14
|
+
def setup(app):
|
|
15
|
+
"""Register the derived theme with Sphinx."""
|
|
16
|
+
app.add_html_theme("derived", str(Path(__file__).parent / "theme"))
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
[theme]
|
|
2
|
+
inherit = "sunpy"
|
|
3
|
+
|
|
4
|
+
[options]
|
|
5
|
+
sst_site_root = "https://example.org/"
|
|
6
|
+
navbar_links = [
|
|
7
|
+
["Home", "index.html", 2],
|
|
8
|
+
["Docs", "https://docs.example.org/", 3],
|
|
9
|
+
]
|
|
10
|
+
footer_links = [
|
|
11
|
+
["Footer Example", "https://footer.example.org/", 3],
|
|
12
|
+
]
|
|
13
|
+
rtd_search_projects = [
|
|
14
|
+
{ name = "example", link = "https://example.org/docs/" },
|
|
15
|
+
]
|
|
@@ -0,0 +1,177 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Using Axe-core, scan the documentation pages for accessibility violations.
|
|
3
|
+
Note that in contrast with the rest of our tests, the accessibility tests in this file
|
|
4
|
+
are run against a build of our test documentation, not purposely-built test sites.
|
|
5
|
+
"""
|
|
6
|
+
|
|
7
|
+
from urllib.parse import urljoin
|
|
8
|
+
|
|
9
|
+
import pytest
|
|
10
|
+
|
|
11
|
+
# Using importorskip to ensure these tests are only loaded if Playwright is installed.
|
|
12
|
+
playwright = pytest.importorskip("playwright")
|
|
13
|
+
from playwright.sync_api import Page, expect # noqa: E402
|
|
14
|
+
|
|
15
|
+
# Important note: automated accessibility scans can only find a fraction of
|
|
16
|
+
# potential accessibility issues.
|
|
17
|
+
#
|
|
18
|
+
# This test file scans pages from our documentation with a JavaScript library
|
|
19
|
+
# called Axe-core, which checks the page for accessibility violations, such as
|
|
20
|
+
# places on the page with poor color contrast that would be hard for people
|
|
21
|
+
# with low vision to see.
|
|
22
|
+
#
|
|
23
|
+
# Just because a page passes the scan with no accessibility violations does
|
|
24
|
+
# *not* mean that it will be generally usable by a broad range of disabled
|
|
25
|
+
# people. It just means that page is free of common testable accessibility
|
|
26
|
+
# pitfalls.
|
|
27
|
+
|
|
28
|
+
|
|
29
|
+
def filter_ignored_violations(violations, url_pathname, theme):
|
|
30
|
+
"""Filter out ignored axe-core violations.
|
|
31
|
+
|
|
32
|
+
In some tests, we wish to ignore certain accessibility violations that we
|
|
33
|
+
won't ever fix or that we don't plan to fix soon.
|
|
34
|
+
"""
|
|
35
|
+
new_violations = []
|
|
36
|
+
|
|
37
|
+
for violation in violations:
|
|
38
|
+
ignore = False
|
|
39
|
+
|
|
40
|
+
# TODO: Our light theme link color does not pass, either fix it or just accept it
|
|
41
|
+
if violation["id"] == "color-contrast" and theme == "light":
|
|
42
|
+
ignore = True
|
|
43
|
+
# This one is probably fine, it's the headings inside the popup
|
|
44
|
+
if url_pathname == "/cards.html" and violation["id"] == "heading-order":
|
|
45
|
+
ignore = True
|
|
46
|
+
|
|
47
|
+
if url_pathname == "/subsections.html" and violation["id"] in ("region", "summary-name"):
|
|
48
|
+
ignore = True
|
|
49
|
+
|
|
50
|
+
if url_pathname == "/index.html" and violation["id"] in ("region", "summary-name"):
|
|
51
|
+
ignore = True
|
|
52
|
+
|
|
53
|
+
if not ignore:
|
|
54
|
+
new_violations.append(violation)
|
|
55
|
+
|
|
56
|
+
# No violations are currently ignored for our documentation pages. When a
|
|
57
|
+
# violation is found that we do not plan to fix, filter it out here by
|
|
58
|
+
# url_pathname and rule id, in the same manner as the pydata-sphinx-theme
|
|
59
|
+
# does in its tests/test_a11y.py.
|
|
60
|
+
return new_violations
|
|
61
|
+
|
|
62
|
+
|
|
63
|
+
def format_violations(violations):
|
|
64
|
+
"""Return a pretty string representation of Axe-core violations."""
|
|
65
|
+
result = f"""
|
|
66
|
+
|
|
67
|
+
Found {len(violations)} accessibility violation(s):
|
|
68
|
+
"""
|
|
69
|
+
|
|
70
|
+
for violation in violations:
|
|
71
|
+
result += f"""
|
|
72
|
+
|
|
73
|
+
- Rule violated:
|
|
74
|
+
{violation["id"]} - {violation["help"]}
|
|
75
|
+
- URL: {violation["helpUrl"]}
|
|
76
|
+
- Impact: {violation["impact"]}
|
|
77
|
+
- Tags: {" ".join(violation["tags"])}
|
|
78
|
+
- Targets:"""
|
|
79
|
+
|
|
80
|
+
for node in violation["nodes"]:
|
|
81
|
+
for target in node["target"]:
|
|
82
|
+
result += f"""
|
|
83
|
+
- {target}"""
|
|
84
|
+
|
|
85
|
+
result += "\n\n"
|
|
86
|
+
|
|
87
|
+
return result
|
|
88
|
+
|
|
89
|
+
|
|
90
|
+
@pytest.mark.a11y
|
|
91
|
+
@pytest.mark.parametrize("theme", ["light", "dark"])
|
|
92
|
+
@pytest.mark.parametrize(
|
|
93
|
+
("url_pathname", "selector"),
|
|
94
|
+
[
|
|
95
|
+
# The Kitchen Sink page of our test documentation
|
|
96
|
+
("/colors.html", "#colors"),
|
|
97
|
+
("/colors.html", "#admonitions"),
|
|
98
|
+
("/colors.html", "#snippets"),
|
|
99
|
+
# The theme configuration page, including the header and footer
|
|
100
|
+
# elements it renders (navbar links, footer links, footer center)
|
|
101
|
+
("/customizing.html", "#customizing-the-theme"),
|
|
102
|
+
("/customizing.html", "#changing-the-theme-settings"),
|
|
103
|
+
("/customizing.html", "#adjusting-the-styling"),
|
|
104
|
+
# The sphinx-design components used by our documentation
|
|
105
|
+
("/web-components.html", "#sphinx-design-components"),
|
|
106
|
+
("/web-components.html", "#badges-and-button-links"),
|
|
107
|
+
("/web-components.html", "#cards"),
|
|
108
|
+
("/web-components.html", "#tabs"),
|
|
109
|
+
("/web-components.html", "#dropdowns"),
|
|
110
|
+
("/web-components.html", "#copybuttons"),
|
|
111
|
+
("/web-components.html", "#toggle-buttons"),
|
|
112
|
+
# The cards and tables provided by the theme cards extension
|
|
113
|
+
("/cards.html", "#cards-and-tables"),
|
|
114
|
+
# Pages with deeply nested toctrees to stress the sidebar navigation
|
|
115
|
+
("/subsections.html", ""),
|
|
116
|
+
# Using one of the simplest pages on the site, select the whole page
|
|
117
|
+
# for testing in order to effectively test repeated website elements
|
|
118
|
+
# like nav, sidebars, breadcrumbs, footer
|
|
119
|
+
("/index.html", ""), # select whole page
|
|
120
|
+
],
|
|
121
|
+
)
|
|
122
|
+
def test_axe_core(
|
|
123
|
+
page: Page,
|
|
124
|
+
url_base: str,
|
|
125
|
+
theme: str,
|
|
126
|
+
url_pathname: str,
|
|
127
|
+
selector: str,
|
|
128
|
+
):
|
|
129
|
+
"""Should have no Axe-core violations at the provided theme and page section."""
|
|
130
|
+
# Load the page at the provided path
|
|
131
|
+
url_full = urljoin(url_base, url_pathname)
|
|
132
|
+
page.goto(url_full)
|
|
133
|
+
|
|
134
|
+
# Run a line of JavaScript that sets the light/dark theme on the page
|
|
135
|
+
page.evaluate(f"document.documentElement.dataset.theme = '{theme}'")
|
|
136
|
+
|
|
137
|
+
# Wait for CSS transitions (Bootstrap's transitions are 300 ms)
|
|
138
|
+
page.wait_for_timeout(301)
|
|
139
|
+
|
|
140
|
+
# Inject the Axe-core JavaScript library into the page
|
|
141
|
+
page.add_script_tag(path="node_modules/axe-core/axe.min.js")
|
|
142
|
+
|
|
143
|
+
# Run the Axe-core library against a section of the page (unless the
|
|
144
|
+
# selector is empty, then run against the whole page)
|
|
145
|
+
results = page.evaluate("axe.run()" if selector == "" else f"axe.run('{selector}')")
|
|
146
|
+
|
|
147
|
+
# Check found violations against known violations that we do not plan to fix
|
|
148
|
+
filtered_violations = filter_ignored_violations(results["violations"], url_pathname, theme)
|
|
149
|
+
|
|
150
|
+
assert len(filtered_violations) == 0, format_violations(filtered_violations)
|
|
151
|
+
|
|
152
|
+
|
|
153
|
+
@pytest.mark.a11y
|
|
154
|
+
def test_code_block_tab_stop(page: Page, url_base: str) -> None:
|
|
155
|
+
"""Code blocks that have scrollable content should be tab stops."""
|
|
156
|
+
page.set_viewport_size({"width": 1440, "height": 720})
|
|
157
|
+
page.goto(urljoin(url_base, "/customizing.html"))
|
|
158
|
+
|
|
159
|
+
# This code-block contains a line long enough to overflow at narrow
|
|
160
|
+
# viewport widths (the "rtd_search_projects" example with a URL in it)
|
|
161
|
+
code_block = page.locator("css=#rtd-search-projects pre", has_text="rtd_search_projects")
|
|
162
|
+
|
|
163
|
+
# Viewport is wide, so code block content fits, no overflow, no tab stop
|
|
164
|
+
assert code_block.evaluate("el => el.scrollWidth > el.clientWidth") is False
|
|
165
|
+
assert code_block.evaluate("el => el.tabIndex") != 0
|
|
166
|
+
|
|
167
|
+
page.set_viewport_size({"width": 400, "height": 720})
|
|
168
|
+
|
|
169
|
+
# Narrow viewport, content overflows ...
|
|
170
|
+
assert code_block.evaluate("el => el.scrollWidth > el.clientWidth") is True
|
|
171
|
+
|
|
172
|
+
# ... and code block should be a tab stop.
|
|
173
|
+
#
|
|
174
|
+
# Note: expect() will wait until the expect condition is true (up to the
|
|
175
|
+
# test timeout limit). This is important because the resize handler is
|
|
176
|
+
# debounced.
|
|
177
|
+
expect(code_block).to_have_attribute("tabindex", "0")
|
|
@@ -0,0 +1,64 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Tests relating to subthemes (i.e. astropy) setting config defaults.
|
|
3
|
+
"""
|
|
4
|
+
|
|
5
|
+
from sunpy_sphinx_theme.tests.helpers import inline_scripts
|
|
6
|
+
|
|
7
|
+
|
|
8
|
+
def test_theme_defaults_of_derived_theme(sphinx_build_factory):
|
|
9
|
+
"""
|
|
10
|
+
Test the theme.toml defaults of a theme derived from the sunpy theme.
|
|
11
|
+
|
|
12
|
+
Given a minimal theme that derives from the sunpy theme and sets defaults in its theme.toml
|
|
13
|
+
When the docs are built without any user theme options
|
|
14
|
+
Then the derived defaults apply to the navbar, the footer, the site root links and the search config
|
|
15
|
+
"""
|
|
16
|
+
sphinx_build = sphinx_build_factory("derived").build()
|
|
17
|
+
html = sphinx_build.html_tree("index.html")
|
|
18
|
+
|
|
19
|
+
# The derived navbar_links default renders in the navbar
|
|
20
|
+
navbar = html.select_one("ul.bd-navbar-elements")
|
|
21
|
+
home = navbar.select_one('a[href="https://example.org/index.html"]')
|
|
22
|
+
assert home is not None
|
|
23
|
+
assert home.text.strip() == "Home"
|
|
24
|
+
docs = navbar.select_one('a[href="https://docs.example.org/"]')
|
|
25
|
+
assert docs is not None
|
|
26
|
+
assert docs.text.strip() == "Docs"
|
|
27
|
+
|
|
28
|
+
# The derived footer_links default renders in the footer
|
|
29
|
+
footer = html.select_one('.footer-links a[href="https://footer.example.org/"]')
|
|
30
|
+
assert footer is not None
|
|
31
|
+
assert footer.text.strip() == "Footer Example"
|
|
32
|
+
|
|
33
|
+
# The derived rtd_search_projects default is used by the search config script
|
|
34
|
+
search_config = [script for script in inline_scripts(html) if "set_search_config" in script]
|
|
35
|
+
assert len(search_config) == 1
|
|
36
|
+
assert '"name": "example"' in search_config[0]
|
|
37
|
+
assert '"link": "https://example.org/docs/"' in search_config[0]
|
|
38
|
+
|
|
39
|
+
# The sunpy theme.toml defaults still apply where the derived theme does not override
|
|
40
|
+
assert '"label": "Load more results"' in search_config[0]
|
|
41
|
+
|
|
42
|
+
|
|
43
|
+
def test_no_search_projects(sphinx_build_factory, caplog):
|
|
44
|
+
"""
|
|
45
|
+
Given a theme config where no sites are under "Documentation"
|
|
46
|
+
When built
|
|
47
|
+
Then RTD search config is disabled
|
|
48
|
+
"""
|
|
49
|
+
config_toml = """\
|
|
50
|
+
[theme]
|
|
51
|
+
inherit = "sunpy"
|
|
52
|
+
|
|
53
|
+
[options]
|
|
54
|
+
sst_site_root = "https://astropy.org"
|
|
55
|
+
|
|
56
|
+
navbar_links = [
|
|
57
|
+
["Home", "index.html", 2],
|
|
58
|
+
["astropy", "https://docs.astropy.org/", 3],
|
|
59
|
+
]
|
|
60
|
+
"""
|
|
61
|
+
sphinx_build = sphinx_build_factory("derived", files={"theme/theme.toml": config_toml}).build()
|
|
62
|
+
html = sphinx_build.html_tree("index.html")
|
|
63
|
+
search_config = [script for script in inline_scripts(html) if "set_search_config" in script]
|
|
64
|
+
assert len(search_config) == 0
|
|
@@ -0,0 +1,164 @@
|
|
|
1
|
+
"""Tests for the goat counter analytics assets registered by the theme."""
|
|
2
|
+
|
|
3
|
+
import pytest
|
|
4
|
+
|
|
5
|
+
from sunpy_sphinx_theme.tests.helpers import inline_scripts, script_srcs
|
|
6
|
+
|
|
7
|
+
SUNPY_GOAT_URL = "https://sunpy.goatcounter.com/count"
|
|
8
|
+
GOATCOUNTER_COUNT_JS = "https://gc.zgo.at/count.js"
|
|
9
|
+
|
|
10
|
+
|
|
11
|
+
def goatcounter_inline_script(html_tree):
|
|
12
|
+
"""Return the inline script that configures window.goatcounter."""
|
|
13
|
+
scripts = [script for script in inline_scripts(html_tree) if "window.goatcounter" in script]
|
|
14
|
+
assert len(scripts) == 1
|
|
15
|
+
return scripts[0]
|
|
16
|
+
|
|
17
|
+
|
|
18
|
+
def test_goatcounter_default_sunpy_site(sphinx_build_factory):
|
|
19
|
+
"""
|
|
20
|
+
Test the goat counter default for the sunpy site root.
|
|
21
|
+
|
|
22
|
+
Given the docs are built with the default theme options
|
|
23
|
+
When a page is rendered
|
|
24
|
+
Then the inline script configures the sunpy goat counter and the goat counter script is loaded async
|
|
25
|
+
"""
|
|
26
|
+
sphinx_build = sphinx_build_factory("base").build()
|
|
27
|
+
html = sphinx_build.html_tree("index.html")
|
|
28
|
+
|
|
29
|
+
# The inline script configures window.goatcounter for the sunpy domain
|
|
30
|
+
script = goatcounter_inline_script(html)
|
|
31
|
+
assert "var endpoint = '';" in script
|
|
32
|
+
assert "location.hostname.endsWith('sunpy.org')" in script
|
|
33
|
+
assert f"endpoint = '{SUNPY_GOAT_URL}'" in script
|
|
34
|
+
|
|
35
|
+
# The goat counter script itself is loaded async
|
|
36
|
+
count_js = html.select(f'script[src="{GOATCOUNTER_COUNT_JS}"]')
|
|
37
|
+
assert len(count_js) == 1
|
|
38
|
+
assert count_js[0].attrs["async"] == "async"
|
|
39
|
+
|
|
40
|
+
|
|
41
|
+
def test_goatcounter_disabled_for_non_sunpy_site(sphinx_build_factory):
|
|
42
|
+
"""
|
|
43
|
+
Test the goat counter for a non-sunpy site root.
|
|
44
|
+
|
|
45
|
+
Given the site root is a non-sunpy domain with no explicit goat counter URL
|
|
46
|
+
When a page is rendered
|
|
47
|
+
Then no goat counter assets are registered
|
|
48
|
+
"""
|
|
49
|
+
confoverrides = {"html_theme_options": {"sst_site_root": "https://docs.example.org"}}
|
|
50
|
+
sphinx_build = sphinx_build_factory("base", confoverrides=confoverrides).build()
|
|
51
|
+
html = sphinx_build.html_tree("index.html")
|
|
52
|
+
|
|
53
|
+
assert not [script for script in inline_scripts(html) if "window.goatcounter" in script]
|
|
54
|
+
assert GOATCOUNTER_COUNT_JS not in script_srcs(html)
|
|
55
|
+
|
|
56
|
+
|
|
57
|
+
@pytest.mark.parametrize("scheme", ["https", "http"])
|
|
58
|
+
def test_goatcounter_custom_url_for_non_sunpy_site(sphinx_build_factory, scheme):
|
|
59
|
+
"""
|
|
60
|
+
Test an explicit goat counter URL for a non-sunpy site root.
|
|
61
|
+
|
|
62
|
+
Given the site root is a non-sunpy domain with an explicit goat counter URL
|
|
63
|
+
When a page is rendered
|
|
64
|
+
Then the inline script checks the site root without its scheme and uses the explicit URL
|
|
65
|
+
"""
|
|
66
|
+
confoverrides = {
|
|
67
|
+
"html_theme_options": {
|
|
68
|
+
"sst_site_root": f"{scheme}://docs.example.org",
|
|
69
|
+
"goatcounter_analytics_url": "https://example.goatcounter.com/count",
|
|
70
|
+
},
|
|
71
|
+
}
|
|
72
|
+
sphinx_build = sphinx_build_factory("base", confoverrides=confoverrides).build()
|
|
73
|
+
html = sphinx_build.html_tree("index.html")
|
|
74
|
+
|
|
75
|
+
script = goatcounter_inline_script(html)
|
|
76
|
+
assert "location.hostname.endsWith('docs.example.org')" in script
|
|
77
|
+
assert "endpoint = 'https://example.goatcounter.com/count'" in script
|
|
78
|
+
assert GOATCOUNTER_COUNT_JS in script_srcs(html)
|
|
79
|
+
|
|
80
|
+
|
|
81
|
+
@pytest.mark.parametrize("disabled_value", [None, ""])
|
|
82
|
+
def test_goatcounter_explicitly_disabled(sphinx_build_factory, disabled_value):
|
|
83
|
+
"""Test explicitly disabling the goat counter.
|
|
84
|
+
|
|
85
|
+
Given the goat counter URL is set to a falsy value
|
|
86
|
+
When a page is rendered
|
|
87
|
+
Then no goat counter assets are registered
|
|
88
|
+
"""
|
|
89
|
+
confoverrides = {"html_theme_options": {"goatcounter_analytics_url": disabled_value}}
|
|
90
|
+
sphinx_build = sphinx_build_factory("base", confoverrides=confoverrides).build()
|
|
91
|
+
html = sphinx_build.html_tree("index.html")
|
|
92
|
+
|
|
93
|
+
assert not [script for script in inline_scripts(html) if "window.goatcounter" in script]
|
|
94
|
+
assert GOATCOUNTER_COUNT_JS not in script_srcs(html)
|
|
95
|
+
|
|
96
|
+
|
|
97
|
+
@pytest.mark.parametrize(
|
|
98
|
+
("endpoint", "default_line"),
|
|
99
|
+
[
|
|
100
|
+
("https://test.example.com/count", "var endpoint = 'https://test.example.com/count';"),
|
|
101
|
+
(False, "var endpoint = '';"),
|
|
102
|
+
],
|
|
103
|
+
)
|
|
104
|
+
def test_goatcounter_non_domain_endpoint(sphinx_build_factory, endpoint, default_line):
|
|
105
|
+
"""Test the non-domain endpoint of the goat counter.
|
|
106
|
+
|
|
107
|
+
Given goatcounter_non_domain_endpoint is set to a URL or False
|
|
108
|
+
When a page is rendered
|
|
109
|
+
Then the default endpoint in the inline script matches the configured value
|
|
110
|
+
"""
|
|
111
|
+
confoverrides = {"html_theme_options": {"goatcounter_non_domain_endpoint": endpoint}}
|
|
112
|
+
sphinx_build = sphinx_build_factory("base", confoverrides=confoverrides).build()
|
|
113
|
+
html = sphinx_build.html_tree("index.html")
|
|
114
|
+
|
|
115
|
+
script = goatcounter_inline_script(html)
|
|
116
|
+
assert default_line in script
|
|
117
|
+
|
|
118
|
+
|
|
119
|
+
def test_goatcounter_astropy(sphinx_build_factory):
|
|
120
|
+
"""
|
|
121
|
+
Test astropy configuration setup.
|
|
122
|
+
|
|
123
|
+
Given the site root is astropy.org and the goat counter url is set.
|
|
124
|
+
When a page is rendered
|
|
125
|
+
Then the inline script adds the astropy goatcounter only on astropy.org sites.
|
|
126
|
+
"""
|
|
127
|
+
confoverrides = {
|
|
128
|
+
"html_theme_options": {
|
|
129
|
+
"sst_site_root": "https://astropy.org",
|
|
130
|
+
"goatcounter_analytics_url": "https://astropy.goatcounter.com/count",
|
|
131
|
+
},
|
|
132
|
+
}
|
|
133
|
+
sphinx_build = sphinx_build_factory("base", confoverrides=confoverrides).build()
|
|
134
|
+
html = sphinx_build.html_tree("index.html")
|
|
135
|
+
|
|
136
|
+
script = goatcounter_inline_script(html)
|
|
137
|
+
assert "var endpoint = '';" in script
|
|
138
|
+
assert "location.hostname.endsWith('astropy.org')" in script
|
|
139
|
+
assert "endpoint = 'https://astropy.goatcounter.com/count'" in script
|
|
140
|
+
assert GOATCOUNTER_COUNT_JS in script_srcs(html)
|
|
141
|
+
|
|
142
|
+
|
|
143
|
+
def test_goat_counter_theme_defaults(sphinx_build_factory):
|
|
144
|
+
config_toml = """\
|
|
145
|
+
[theme]
|
|
146
|
+
inherit = "sunpy"
|
|
147
|
+
|
|
148
|
+
[options]
|
|
149
|
+
sst_site_root = "https://astropy.org"
|
|
150
|
+
goatcounter_analytics_url = "https://astropy.goatcounter.com/count"
|
|
151
|
+
|
|
152
|
+
navbar_links = [
|
|
153
|
+
["Home", "index.html", 2],
|
|
154
|
+
["astropy", "https://docs.astropy.org/", 3],
|
|
155
|
+
]
|
|
156
|
+
"""
|
|
157
|
+
sphinx_build = sphinx_build_factory("derived", files={"theme/theme.toml": config_toml}).build()
|
|
158
|
+
html = sphinx_build.html_tree("index.html")
|
|
159
|
+
|
|
160
|
+
script = goatcounter_inline_script(html)
|
|
161
|
+
assert "var endpoint = '';" in script
|
|
162
|
+
assert "location.hostname.endsWith('astropy.org')" in script
|
|
163
|
+
assert "endpoint = 'https://astropy.goatcounter.com/count'" in script
|
|
164
|
+
assert GOATCOUNTER_COUNT_JS in script_srcs(html)
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
from sunpy_sphinx_theme.tests.helpers import script_srcs, stylesheet_hrefs
|
|
2
|
+
|
|
3
|
+
|
|
4
|
+
def test_rtd_search_disabled(sphinx_build_factory):
|
|
5
|
+
"""Test disabling the search assets.
|
|
6
|
+
|
|
7
|
+
Given rtd_search is disabled
|
|
8
|
+
When a page is rendered
|
|
9
|
+
Then the search assets are not registered and the goat counter and submenu assets are unaffected
|
|
10
|
+
"""
|
|
11
|
+
confoverrides = {"html_theme_options": {"rtd_search": False}}
|
|
12
|
+
sphinx_build = sphinx_build_factory("base", confoverrides=confoverrides).build()
|
|
13
|
+
html = sphinx_build.html_tree("index.html")
|
|
14
|
+
|
|
15
|
+
srcs = script_srcs(html)
|
|
16
|
+
assert not any("js/rtd_enhanced_search.js" in src for src in srcs)
|
|
17
|
+
assert not any("css/rtd_enhanced_search.css" in href for href in stylesheet_hrefs(html))
|
{sunpy_sphinx_theme-2.3.0 → sunpy_sphinx_theme-2.4.0}/src/sunpy_sphinx_theme/theme/sunpy/theme.toml
RENAMED
|
@@ -71,10 +71,10 @@ footer_links = [
|
|
|
71
71
|
["Forum", "https://community.openastronomy.org/c/sunpy", 3],
|
|
72
72
|
["Chat", "https://openastronomy.element.io/#/room/#sunpy:openastronomy.org", 3],
|
|
73
73
|
]
|
|
74
|
-
goatcounter_analytics_url = ""
|
|
75
|
-
goatcounter_non_domain_endpoint =
|
|
74
|
+
goatcounter_analytics_url = "DEFAULT" # Use a sentinel here to allow disabling by setting to false
|
|
75
|
+
goatcounter_non_domain_endpoint = false
|
|
76
76
|
pygment_light_style = "github-light"
|
|
77
|
-
rtd_search =
|
|
77
|
+
rtd_search = true
|
|
78
78
|
rtd_search_projects = ""
|
|
79
79
|
rtd_extra_search_projects = ""
|
|
80
80
|
rtd_search_load_more_label = "Load more results"
|
{sunpy_sphinx_theme-2.3.0 → sunpy_sphinx_theme-2.4.0}/src/sunpy_sphinx_theme.egg-info/PKG-INFO
RENAMED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: sunpy-sphinx-theme
|
|
3
|
-
Version: 2.
|
|
3
|
+
Version: 2.4.0
|
|
4
4
|
Summary: The sphinx theme for the SunPy website and documentation.
|
|
5
5
|
Author: The SunPy Developers
|
|
6
6
|
License: BSD 2-Clause License
|
|
@@ -36,10 +36,10 @@ Classifier: Programming Language :: Python :: 3
|
|
|
36
36
|
Classifier: Framework :: Sphinx
|
|
37
37
|
Classifier: Framework :: Sphinx :: Theme
|
|
38
38
|
Classifier: Operating System :: OS Independent
|
|
39
|
-
Requires-Python: >=3.
|
|
39
|
+
Requires-Python: >=3.11
|
|
40
40
|
Description-Content-Type: text/markdown
|
|
41
41
|
License-File: LICENSE.md
|
|
42
|
-
Requires-Dist: pydata-sphinx-theme<0.
|
|
42
|
+
Requires-Dist: pydata-sphinx-theme<0.24,>=0.22.0
|
|
43
43
|
Requires-Dist: sphinxext-opengraph>=0.13
|
|
44
44
|
Requires-Dist: sphinx>=7.3.0
|
|
45
45
|
Provides-Extra: docs
|
|
@@ -50,6 +50,9 @@ Requires-Dist: sphinx-design; extra == "docs"
|
|
|
50
50
|
Requires-Dist: sphinx-gallery; extra == "docs"
|
|
51
51
|
Requires-Dist: sphinx-togglebutton; extra == "docs"
|
|
52
52
|
Requires-Dist: sunpy; extra == "docs"
|
|
53
|
+
Provides-Extra: test
|
|
54
|
+
Requires-Dist: pytest>=7.0; extra == "test"
|
|
55
|
+
Requires-Dist: pytest-playwright; extra == "test"
|
|
53
56
|
Dynamic: license-file
|
|
54
57
|
|
|
55
58
|
# SunPy Project Sphinx Theme
|
{sunpy_sphinx_theme-2.3.0 → sunpy_sphinx_theme-2.4.0}/src/sunpy_sphinx_theme.egg-info/SOURCES.txt
RENAMED
|
@@ -6,6 +6,9 @@ LICENSE.md
|
|
|
6
6
|
MANIFEST.in
|
|
7
7
|
README.md
|
|
8
8
|
_typos.toml
|
|
9
|
+
conftest.py
|
|
10
|
+
flake.nix
|
|
11
|
+
package.json
|
|
9
12
|
pyproject.toml
|
|
10
13
|
ruff.toml
|
|
11
14
|
src/sunpy_sphinx_theme/__init__.py
|
|
@@ -19,6 +22,17 @@ src/sunpy_sphinx_theme.egg-info/requires.txt
|
|
|
19
22
|
src/sunpy_sphinx_theme.egg-info/scm_file_list.json
|
|
20
23
|
src/sunpy_sphinx_theme.egg-info/scm_version.json
|
|
21
24
|
src/sunpy_sphinx_theme.egg-info/top_level.txt
|
|
25
|
+
src/sunpy_sphinx_theme/tests/__init__.py
|
|
26
|
+
src/sunpy_sphinx_theme/tests/helpers.py
|
|
27
|
+
src/sunpy_sphinx_theme/tests/test_a11y.py
|
|
28
|
+
src/sunpy_sphinx_theme/tests/test_config_subtheme.py
|
|
29
|
+
src/sunpy_sphinx_theme/tests/test_goatcounter.py
|
|
30
|
+
src/sunpy_sphinx_theme/tests/test_options.py
|
|
31
|
+
src/sunpy_sphinx_theme/tests/sites/base/conf.py
|
|
32
|
+
src/sunpy_sphinx_theme/tests/sites/base/index.rst
|
|
33
|
+
src/sunpy_sphinx_theme/tests/sites/derived/conf.py
|
|
34
|
+
src/sunpy_sphinx_theme/tests/sites/derived/index.rst
|
|
35
|
+
src/sunpy_sphinx_theme/tests/sites/derived/theme/theme.toml
|
|
22
36
|
src/sunpy_sphinx_theme/theme/sunpy/theme.toml
|
|
23
37
|
src/sunpy_sphinx_theme/theme/sunpy/components/built-with.html
|
|
24
38
|
src/sunpy_sphinx_theme/theme/sunpy/components/footer-links.html
|
|
@@ -13,6 +13,7 @@
|
|
|
13
13
|
"MANIFEST.in",
|
|
14
14
|
"README.md",
|
|
15
15
|
"_typos.toml",
|
|
16
|
+
"conftest.py",
|
|
16
17
|
"docs/_static/img/Stuart-Mumford.png",
|
|
17
18
|
"docs/cards.rst",
|
|
18
19
|
"docs/code_ref/animals.rst",
|
|
@@ -32,11 +33,24 @@
|
|
|
32
33
|
"examples/example_template.py",
|
|
33
34
|
"examples/section/README.txt",
|
|
34
35
|
"examples/section/example_template_subsection.py",
|
|
36
|
+
"flake.nix",
|
|
37
|
+
"package.json",
|
|
35
38
|
"pyproject.toml",
|
|
36
39
|
"ruff.toml",
|
|
37
40
|
"src/sunpy_sphinx_theme/__init__.py",
|
|
38
41
|
"src/sunpy_sphinx_theme/cards.py",
|
|
39
42
|
"src/sunpy_sphinx_theme/conf.py",
|
|
43
|
+
"src/sunpy_sphinx_theme/tests/__init__.py",
|
|
44
|
+
"src/sunpy_sphinx_theme/tests/helpers.py",
|
|
45
|
+
"src/sunpy_sphinx_theme/tests/sites/base/conf.py",
|
|
46
|
+
"src/sunpy_sphinx_theme/tests/sites/base/index.rst",
|
|
47
|
+
"src/sunpy_sphinx_theme/tests/sites/derived/conf.py",
|
|
48
|
+
"src/sunpy_sphinx_theme/tests/sites/derived/index.rst",
|
|
49
|
+
"src/sunpy_sphinx_theme/tests/sites/derived/theme/theme.toml",
|
|
50
|
+
"src/sunpy_sphinx_theme/tests/test_a11y.py",
|
|
51
|
+
"src/sunpy_sphinx_theme/tests/test_config_subtheme.py",
|
|
52
|
+
"src/sunpy_sphinx_theme/tests/test_goatcounter.py",
|
|
53
|
+
"src/sunpy_sphinx_theme/tests/test_options.py",
|
|
40
54
|
"src/sunpy_sphinx_theme/theme/sunpy/components/built-with.html",
|
|
41
55
|
"src/sunpy_sphinx_theme/theme/sunpy/components/footer-links.html",
|
|
42
56
|
"src/sunpy_sphinx_theme/theme/sunpy/components/navbar-logo.html",
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
{sunpy_sphinx_theme-2.3.0 → sunpy_sphinx_theme-2.4.0}/src/sunpy_sphinx_theme.egg-info/top_level.txt
RENAMED
|
File without changes
|