sunpy-sphinx-theme 2.3.0__tar.gz → 2.5.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.
Files changed (56) hide show
  1. sunpy_sphinx_theme-2.5.0/.codespellrc +2 -0
  2. {sunpy_sphinx_theme-2.3.0 → sunpy_sphinx_theme-2.5.0}/.rtd-environment.yml +1 -1
  3. {sunpy_sphinx_theme-2.3.0 → sunpy_sphinx_theme-2.5.0}/PKG-INFO +9 -3
  4. sunpy_sphinx_theme-2.5.0/conftest.py +143 -0
  5. sunpy_sphinx_theme-2.5.0/flake.nix +35 -0
  6. sunpy_sphinx_theme-2.5.0/package.json +8 -0
  7. {sunpy_sphinx_theme-2.3.0 → sunpy_sphinx_theme-2.5.0}/pyproject.toml +22 -2
  8. {sunpy_sphinx_theme-2.3.0 → sunpy_sphinx_theme-2.5.0}/ruff.toml +7 -8
  9. {sunpy_sphinx_theme-2.3.0 → sunpy_sphinx_theme-2.5.0}/src/sunpy_sphinx_theme/__init__.py +66 -47
  10. sunpy_sphinx_theme-2.5.0/src/sunpy_sphinx_theme/tests/__init__.py +3 -0
  11. sunpy_sphinx_theme-2.5.0/src/sunpy_sphinx_theme/tests/helpers.py +13 -0
  12. sunpy_sphinx_theme-2.5.0/src/sunpy_sphinx_theme/tests/sites/base/conf.py +9 -0
  13. sunpy_sphinx_theme-2.5.0/src/sunpy_sphinx_theme/tests/sites/base/index.rst +4 -0
  14. sunpy_sphinx_theme-2.5.0/src/sunpy_sphinx_theme/tests/sites/derived/conf.py +16 -0
  15. sunpy_sphinx_theme-2.5.0/src/sunpy_sphinx_theme/tests/sites/derived/index.rst +4 -0
  16. sunpy_sphinx_theme-2.5.0/src/sunpy_sphinx_theme/tests/sites/derived/theme/theme.toml +15 -0
  17. sunpy_sphinx_theme-2.5.0/src/sunpy_sphinx_theme/tests/test_a11y.py +187 -0
  18. sunpy_sphinx_theme-2.5.0/src/sunpy_sphinx_theme/tests/test_config_subtheme.py +64 -0
  19. sunpy_sphinx_theme-2.5.0/src/sunpy_sphinx_theme/tests/test_goatcounter.py +164 -0
  20. sunpy_sphinx_theme-2.5.0/src/sunpy_sphinx_theme/tests/test_options.py +17 -0
  21. sunpy_sphinx_theme-2.5.0/src/sunpy_sphinx_theme/tests/test_screenshots.py +40 -0
  22. {sunpy_sphinx_theme-2.3.0 → sunpy_sphinx_theme-2.5.0}/src/sunpy_sphinx_theme/theme/sunpy/theme.toml +3 -3
  23. {sunpy_sphinx_theme-2.3.0 → sunpy_sphinx_theme-2.5.0}/src/sunpy_sphinx_theme.egg-info/PKG-INFO +9 -3
  24. {sunpy_sphinx_theme-2.3.0 → sunpy_sphinx_theme-2.5.0}/src/sunpy_sphinx_theme.egg-info/SOURCES.txt +16 -1
  25. {sunpy_sphinx_theme-2.3.0 → sunpy_sphinx_theme-2.5.0}/src/sunpy_sphinx_theme.egg-info/requires.txt +9 -1
  26. {sunpy_sphinx_theme-2.3.0 → sunpy_sphinx_theme-2.5.0}/src/sunpy_sphinx_theme.egg-info/scm_file_list.json +16 -1
  27. sunpy_sphinx_theme-2.5.0/src/sunpy_sphinx_theme.egg-info/scm_version.json +8 -0
  28. sunpy_sphinx_theme-2.3.0/_typos.toml +0 -25
  29. sunpy_sphinx_theme-2.3.0/src/sunpy_sphinx_theme.egg-info/scm_version.json +0 -8
  30. {sunpy_sphinx_theme-2.3.0 → sunpy_sphinx_theme-2.5.0}/.editorconfig +0 -0
  31. {sunpy_sphinx_theme-2.3.0 → sunpy_sphinx_theme-2.5.0}/.readthedocs.yaml +0 -0
  32. {sunpy_sphinx_theme-2.3.0 → sunpy_sphinx_theme-2.5.0}/.stylelintrc.json +0 -0
  33. {sunpy_sphinx_theme-2.3.0 → sunpy_sphinx_theme-2.5.0}/LICENSE.md +0 -0
  34. {sunpy_sphinx_theme-2.3.0 → sunpy_sphinx_theme-2.5.0}/MANIFEST.in +0 -0
  35. {sunpy_sphinx_theme-2.3.0 → sunpy_sphinx_theme-2.5.0}/README.md +0 -0
  36. {sunpy_sphinx_theme-2.3.0 → sunpy_sphinx_theme-2.5.0}/setup.cfg +0 -0
  37. {sunpy_sphinx_theme-2.3.0 → sunpy_sphinx_theme-2.5.0}/src/sunpy_sphinx_theme/cards.py +0 -0
  38. {sunpy_sphinx_theme-2.3.0 → sunpy_sphinx_theme-2.5.0}/src/sunpy_sphinx_theme/conf.py +0 -0
  39. {sunpy_sphinx_theme-2.3.0 → sunpy_sphinx_theme-2.5.0}/src/sunpy_sphinx_theme/theme/sunpy/components/built-with.html +0 -0
  40. {sunpy_sphinx_theme-2.3.0 → sunpy_sphinx_theme-2.5.0}/src/sunpy_sphinx_theme/theme/sunpy/components/footer-links.html +0 -0
  41. {sunpy_sphinx_theme-2.3.0 → sunpy_sphinx_theme-2.5.0}/src/sunpy_sphinx_theme/theme/sunpy/components/navbar-logo.html +0 -0
  42. {sunpy_sphinx_theme-2.3.0 → sunpy_sphinx_theme-2.5.0}/src/sunpy_sphinx_theme/theme/sunpy/components/navbar_center.html +0 -0
  43. {sunpy_sphinx_theme-2.3.0 → sunpy_sphinx_theme-2.5.0}/src/sunpy_sphinx_theme/theme/sunpy/components/navbar_start.html +0 -0
  44. {sunpy_sphinx_theme-2.3.0 → sunpy_sphinx_theme-2.5.0}/src/sunpy_sphinx_theme/theme/sunpy/components/sst-sidebar-nav.html +0 -0
  45. {sunpy_sphinx_theme-2.3.0 → sunpy_sphinx_theme-2.5.0}/src/sunpy_sphinx_theme/theme/sunpy/sections/header.html +0 -0
  46. {sunpy_sphinx_theme-2.3.0 → sunpy_sphinx_theme-2.5.0}/src/sunpy_sphinx_theme/theme/sunpy/static/css/rtd_enhanced_search.css +0 -0
  47. {sunpy_sphinx_theme-2.3.0 → sunpy_sphinx_theme-2.5.0}/src/sunpy_sphinx_theme/theme/sunpy/static/css/sunpy_cards.css +0 -0
  48. {sunpy_sphinx_theme-2.3.0 → sunpy_sphinx_theme-2.5.0}/src/sunpy_sphinx_theme/theme/sunpy/static/img/favicon-32.ico +0 -0
  49. {sunpy_sphinx_theme-2.3.0 → sunpy_sphinx_theme-2.5.0}/src/sunpy_sphinx_theme/theme/sunpy/static/img/sunpy_icon.svg +0 -0
  50. {sunpy_sphinx_theme-2.3.0 → sunpy_sphinx_theme-2.5.0}/src/sunpy_sphinx_theme/theme/sunpy/static/img/sunpy_icon_128x128.png +0 -0
  51. {sunpy_sphinx_theme-2.3.0 → sunpy_sphinx_theme-2.5.0}/src/sunpy_sphinx_theme/theme/sunpy/static/js/rtd_enhanced_search.js +0 -0
  52. {sunpy_sphinx_theme-2.3.0 → sunpy_sphinx_theme-2.5.0}/src/sunpy_sphinx_theme/theme/sunpy/static/js/submenu-concertina-toggle.js +0 -0
  53. {sunpy_sphinx_theme-2.3.0 → sunpy_sphinx_theme-2.5.0}/src/sunpy_sphinx_theme/theme/sunpy/static/sunpy_style.css +0 -0
  54. {sunpy_sphinx_theme-2.3.0 → sunpy_sphinx_theme-2.5.0}/src/sunpy_sphinx_theme.egg-info/dependency_links.txt +0 -0
  55. {sunpy_sphinx_theme-2.3.0 → sunpy_sphinx_theme-2.5.0}/src/sunpy_sphinx_theme.egg-info/entry_points.txt +0 -0
  56. {sunpy_sphinx_theme-2.3.0 → sunpy_sphinx_theme-2.5.0}/src/sunpy_sphinx_theme.egg-info/top_level.txt +0 -0
@@ -0,0 +1,2 @@
1
+ [codespell]
2
+ ignore-words-list = ANDed,arange,iy1,iy2,EIS,eis,EIT,eit,HAE,his,His,nd,NDData,along,alog10,FOVs,FOV,OT,ot,pn,CPY
@@ -2,6 +2,6 @@ name: rtd_sunpy_sphinx_theme
2
2
  channels:
3
3
  - conda-forge
4
4
  dependencies:
5
- - python=3.11
5
+ - python=3.14
6
6
  - pip
7
7
  - graphviz!=2.42.*,!=2.43.*
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: sunpy-sphinx-theme
3
- Version: 2.3.0
3
+ Version: 2.5.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.9
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.23,>=0.22.0rc0
42
+ Requires-Dist: pydata-sphinx-theme<0.25,>=0.24.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,12 @@ 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
+ Provides-Extra: test-playwright
56
+ Requires-Dist: playwright==1.64.1; extra == "test-playwright"
57
+ Requires-Dist: pytest-playwright==0.10.0; extra == "test-playwright"
58
+ Requires-Dist: playwright-figure-tests; extra == "test-playwright"
53
59
  Dynamic: license-file
54
60
 
55
61
  # SunPy Project Sphinx Theme
@@ -0,0 +1,143 @@
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
+
25
+ repo_path = Path(__file__).parent
26
+ tests_path = repo_path / "src" / "sunpy_sphinx_theme" / "tests"
27
+ docs_build_path = repo_path / "docs" / "_build" / "html"
28
+
29
+
30
+ # -- global fixture to build sphinx tmp docs ---------------------------------
31
+
32
+
33
+ class SphinxBuild:
34
+ """Helper class to build a test documentation."""
35
+
36
+ def __init__(self, app: SphinxTestApp, src: Path):
37
+ self.app = app
38
+ self.src = src
39
+
40
+ def build(self, *, no_warning: bool = True) -> SphinxBuild:
41
+ """Build the application."""
42
+ self.app.build()
43
+ if no_warning is True:
44
+ assert self.warnings == "", self.status
45
+ return self
46
+
47
+ @property
48
+ def status(self) -> str:
49
+ """Returns the status of the current build."""
50
+ return self.app.status.getvalue()
51
+
52
+ @property
53
+ def warnings(self) -> str:
54
+ """Returns the warnings raised by the current build."""
55
+ return self.app.warning.getvalue()
56
+
57
+ @property
58
+ def outdir(self) -> Path:
59
+ """Returns the output directory of the current build."""
60
+ return Path(self.app.outdir)
61
+
62
+ def html_tree(self, *path) -> BeautifulSoup:
63
+ """Returns the html tree of the current build."""
64
+ path_page = self.outdir.joinpath(*path)
65
+ if not path_page.exists():
66
+ msg = f"{path_page} does not exist"
67
+ raise ValueError(msg)
68
+ return BeautifulSoup(path_page.read_text("utf8"), "html.parser")
69
+
70
+
71
+ @pytest.fixture
72
+ def sphinx_build_factory(make_app: Callable, tmp_path: Path, request) -> Callable:
73
+ """Return a factory builder pointing to the tmp directory."""
74
+
75
+ def _func(src_folder: str, files: dict[str, str] | None = None, **kwargs) -> SphinxBuild:
76
+ """Create the SphinxBuild from the source folder."""
77
+ no_temp = environ.get("SST_TEST_HTML_DIR")
78
+ nonlocal tmp_path
79
+ if no_temp is not None:
80
+ tmp_path = Path(no_temp) / request.node.name / str(src_folder)
81
+ srcdir = tmp_path / src_folder
82
+ copytree(tests_path / "sites" / src_folder, tmp_path / src_folder)
83
+ for filename, contents in (files or {}).items():
84
+ filepath = Path(srcdir / filename).resolve()
85
+ filepath.parent.mkdir(exist_ok=True, parents=True)
86
+ filepath.write_text(contents)
87
+ app = make_app(srcdir=srcdir, **kwargs)
88
+ return SphinxBuild(app, tmp_path / src_folder)
89
+
90
+ return _func
91
+
92
+
93
+ # -- global fixture to serve the built docs ----------------------------------
94
+
95
+
96
+ @pytest.fixture(scope="session")
97
+ def url_base():
98
+ """Start local server on built docs and return the localhost URL as the base URL."""
99
+ # The accessibility tests are run against a build of our documentation, so
100
+ # fail early with a helpful message if it has not been built yet.
101
+ if not docs_build_path.is_dir():
102
+ msg = f"No docs build found at {docs_build_path}; build it first with 'tox -e build_docs'"
103
+ pytest.fail(msg, pytrace=False)
104
+
105
+ # Use a port that is not commonly used during development or else you will
106
+ # force the developer to stop running their dev server in order to run the
107
+ # tests.
108
+ port = 8213
109
+ host = "localhost"
110
+ url = f"http://{host}:{port}"
111
+
112
+ # Try starting the server
113
+ process = Popen(
114
+ [sys.executable, "-m", "http.server", str(port), "--directory", docs_build_path],
115
+ stdout=PIPE,
116
+ )
117
+
118
+ # Try connecting to the server
119
+ retries = 5
120
+ while retries > 0:
121
+ conn = HTTPConnection(host, port)
122
+ try:
123
+ conn.request("HEAD", "/")
124
+ response = conn.getresponse()
125
+ if response is not None:
126
+ yield url
127
+ break
128
+ except ConnectionRefusedError:
129
+ time.sleep(1)
130
+ retries -= 1
131
+ finally:
132
+ conn.close()
133
+
134
+ # If the code above never yields a URL, then we were never able to connect
135
+ # to the server and retries == 0.
136
+ if not retries:
137
+ msg_0 = "Failed to start http server in 5 seconds"
138
+ raise RuntimeError(msg_0)
139
+ else:
140
+ # Otherwise the server started and this fixture is done now and we clean
141
+ # up by stopping the server.
142
+ process.terminate()
143
+ process.wait()
@@ -0,0 +1,35 @@
1
+ {
2
+ description = "sunpy-sphinx-theme dev shell (provides Playwright browsers for NixOS)";
3
+
4
+ inputs = {
5
+ flake-parts.url = "github:hercules-ci/flake-parts";
6
+ nixpkgs.url = "https://channels.nixos.org/nixos-unstable/nixexprs.tar.zst";
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
+ # playwright-driver.browsers provides Firefox/Chromium patched to run on
13
+ # NixOS. The pip `playwright` package reads PLAYWRIGHT_BROWSERS_PATH and
14
+ # uses these browsers directly; its version must match whatever nixpkgs
15
+ # pins here. nodejs is for the a11y env's npm install (axe-core).
16
+ # bash is needed so `nix develop --command` can evaluate shellHook
17
+ # (the devShell otherwise has no shell on PATH).
18
+ packages = with pkgs; [
19
+ bash
20
+ nodejs_latest
21
+ playwright-test
22
+ playwright-driver.browsers
23
+ ];
24
+
25
+ shellHook = ''
26
+ export PLAYWRIGHT_BROWSERS_PATH=${pkgs.playwright-driver.browsers}
27
+ export PLAYWRIGHT_SKIP_VALIDATE_HOST_REQUIREMENTS=true
28
+ # Browsers installed by nix so no-op the install command in tox
29
+ export PLAYWRIGHT_INSTALL_CMD="python -c pass"
30
+ '';
31
+ };
32
+ };
33
+ systems = [ "x86_64-linux" "aarch64-linux" "x86_64-darwin" "aarch64-darwin" ];
34
+ };
35
+ }
@@ -0,0 +1,8 @@
1
+ {
2
+ "name": "sunpy-sphinx-theme",
3
+ "private": true,
4
+ "description": "Node dependencies for testing the sunpy-sphinx-theme",
5
+ "devDependencies": {
6
+ "axe-core": "^4.13.0"
7
+ }
8
+ }
@@ -20,9 +20,9 @@ classifiers = [
20
20
  "Framework :: Sphinx :: Theme",
21
21
  "Operating System :: OS Independent",
22
22
  ]
23
- requires-python = ">=3.9"
23
+ requires-python = ">=3.11"
24
24
  dependencies = [
25
- "pydata-sphinx-theme>=0.22.0rc0,<0.23",
25
+ "pydata-sphinx-theme>=0.24.0,<0.25",
26
26
  "sphinxext-opengraph>=0.13",
27
27
  "sphinx>=7.3.0"
28
28
  ]
@@ -37,6 +37,14 @@ docs = [
37
37
  "sphinx-togglebutton",
38
38
  "sunpy",
39
39
  ]
40
+ test = [
41
+ "pytest>=7.0",
42
+ ]
43
+ test-playwright = [
44
+ "playwright==1.64.1",
45
+ "pytest-playwright==0.10.0",
46
+ "playwright-figure-tests",
47
+ ]
40
48
 
41
49
  [project.entry-points]
42
50
  "sphinx.html_themes" = { sunpy = "sunpy_sphinx_theme" }
@@ -46,6 +54,13 @@ include-package-data = true
46
54
 
47
55
  [tool.setuptools_scm]
48
56
 
57
+ [tool.pytest.ini_options]
58
+ markers = "a11y: mark a test as an accessibility test"
59
+ testpaths = ["tests"]
60
+ addopts = ["--durations=10", "-m not mpl_image_compare and not a11y"]
61
+ mpl-results-path = "figure_test_images"
62
+ mpl-use-full-test-name = true
63
+
49
64
  [ tool.gilesbot ]
50
65
  [ tool.gilesbot.circleci_artifacts.giles ]
51
66
  enabled = false
@@ -65,6 +80,11 @@ include-package-data = true
65
80
  message = "sunpy docs with devdeps preview"
66
81
  report_on_fail = true
67
82
 
83
+ [ tool.gilesbot.circleci_artifacts.visual-tests-summary ]
84
+ url = "fig_comparison.html"
85
+ message = "screenshot test summary page"
86
+ report_on_fail = true
87
+
68
88
  [ tool.gilesbot.circleci_artifacts.astropy_website ]
69
89
  url = "astropy.org/_build/html/index.html"
70
90
  message = "Astropy website preview"
@@ -49,17 +49,16 @@ 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
- "N806", # in function should be lowercase
55
+ "INP001", # tests are not part of an implicit namespace package
56
56
  "S101", # Use of `assert` detected
57
- ]
58
- "sunpy_soar/version.py" = [
59
- "D100", # Missing docstring in public module
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)
60
+ "PLR0913", # Too many arguments in function definition
61
+ "PLR0917", # Too many positional arguments
63
62
  ]
64
63
 
65
64
  [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(theme_options["sst_logo"])
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
- search_projects = theme_config.get("rtd_search_projects", None)
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
- def filter_doc_links(links):
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 setup(app: Sphinx):
231
- app.setup_extension("sphinxext.opengraph")
232
-
233
- # Register theme
234
- theme_dir = get_html_theme_path()
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 = sunpy_goat_url if root_domain == "https://sunpy.org" else None
250
- if primary_goat_url := theme_options.get("goatcounter_analytics_url", default_goat_url):
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 is False:
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
- if theme_options.get("rtd_search", True):
279
- # Add project-wide search
280
- app.add_css_file("css/rtd_enhanced_search.css")
281
- app.add_js_file(
282
- "js/rtd_enhanced_search.js",
283
- loading_method="async",
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,9 @@
1
+ """Test conf file."""
2
+
3
+ project = "SunPy Theme Tests"
4
+ copyright = "2026, The SunPy Community" # NOQA: A001
5
+ author = "The SunPy Community"
6
+
7
+ extensions = []
8
+ html_theme = "sunpy"
9
+ html_static_path = []
@@ -0,0 +1,4 @@
1
+ Test Documentation
2
+ ==================
3
+
4
+ A page used to test the theme.
@@ -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,4 @@
1
+ Test Documentation
2
+ ==================
3
+
4
+ A page used to test a theme derived from the sunpy 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
+ ]