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.
Files changed (54) hide show
  1. {sunpy_sphinx_theme-2.3.0 → sunpy_sphinx_theme-2.4.0}/.rtd-environment.yml +1 -1
  2. {sunpy_sphinx_theme-2.3.0 → sunpy_sphinx_theme-2.4.0}/PKG-INFO +6 -3
  3. sunpy_sphinx_theme-2.4.0/conftest.py +142 -0
  4. sunpy_sphinx_theme-2.4.0/flake.nix +34 -0
  5. sunpy_sphinx_theme-2.4.0/package.json +8 -0
  6. {sunpy_sphinx_theme-2.3.0 → sunpy_sphinx_theme-2.4.0}/pyproject.toml +11 -2
  7. {sunpy_sphinx_theme-2.3.0 → sunpy_sphinx_theme-2.4.0}/ruff.toml +5 -8
  8. {sunpy_sphinx_theme-2.3.0 → sunpy_sphinx_theme-2.4.0}/src/sunpy_sphinx_theme/__init__.py +66 -47
  9. sunpy_sphinx_theme-2.4.0/src/sunpy_sphinx_theme/tests/__init__.py +3 -0
  10. sunpy_sphinx_theme-2.4.0/src/sunpy_sphinx_theme/tests/helpers.py +13 -0
  11. sunpy_sphinx_theme-2.4.0/src/sunpy_sphinx_theme/tests/sites/base/conf.py +9 -0
  12. sunpy_sphinx_theme-2.4.0/src/sunpy_sphinx_theme/tests/sites/base/index.rst +4 -0
  13. sunpy_sphinx_theme-2.4.0/src/sunpy_sphinx_theme/tests/sites/derived/conf.py +16 -0
  14. sunpy_sphinx_theme-2.4.0/src/sunpy_sphinx_theme/tests/sites/derived/index.rst +4 -0
  15. sunpy_sphinx_theme-2.4.0/src/sunpy_sphinx_theme/tests/sites/derived/theme/theme.toml +15 -0
  16. sunpy_sphinx_theme-2.4.0/src/sunpy_sphinx_theme/tests/test_a11y.py +177 -0
  17. sunpy_sphinx_theme-2.4.0/src/sunpy_sphinx_theme/tests/test_config_subtheme.py +64 -0
  18. sunpy_sphinx_theme-2.4.0/src/sunpy_sphinx_theme/tests/test_goatcounter.py +164 -0
  19. sunpy_sphinx_theme-2.4.0/src/sunpy_sphinx_theme/tests/test_options.py +17 -0
  20. {sunpy_sphinx_theme-2.3.0 → sunpy_sphinx_theme-2.4.0}/src/sunpy_sphinx_theme/theme/sunpy/theme.toml +3 -3
  21. {sunpy_sphinx_theme-2.3.0 → sunpy_sphinx_theme-2.4.0}/src/sunpy_sphinx_theme.egg-info/PKG-INFO +6 -3
  22. {sunpy_sphinx_theme-2.3.0 → sunpy_sphinx_theme-2.4.0}/src/sunpy_sphinx_theme.egg-info/SOURCES.txt +14 -0
  23. {sunpy_sphinx_theme-2.3.0 → sunpy_sphinx_theme-2.4.0}/src/sunpy_sphinx_theme.egg-info/requires.txt +5 -1
  24. {sunpy_sphinx_theme-2.3.0 → sunpy_sphinx_theme-2.4.0}/src/sunpy_sphinx_theme.egg-info/scm_file_list.json +14 -0
  25. sunpy_sphinx_theme-2.4.0/src/sunpy_sphinx_theme.egg-info/scm_version.json +8 -0
  26. sunpy_sphinx_theme-2.3.0/src/sunpy_sphinx_theme.egg-info/scm_version.json +0 -8
  27. {sunpy_sphinx_theme-2.3.0 → sunpy_sphinx_theme-2.4.0}/.editorconfig +0 -0
  28. {sunpy_sphinx_theme-2.3.0 → sunpy_sphinx_theme-2.4.0}/.readthedocs.yaml +0 -0
  29. {sunpy_sphinx_theme-2.3.0 → sunpy_sphinx_theme-2.4.0}/.stylelintrc.json +0 -0
  30. {sunpy_sphinx_theme-2.3.0 → sunpy_sphinx_theme-2.4.0}/LICENSE.md +0 -0
  31. {sunpy_sphinx_theme-2.3.0 → sunpy_sphinx_theme-2.4.0}/MANIFEST.in +0 -0
  32. {sunpy_sphinx_theme-2.3.0 → sunpy_sphinx_theme-2.4.0}/README.md +0 -0
  33. {sunpy_sphinx_theme-2.3.0 → sunpy_sphinx_theme-2.4.0}/_typos.toml +0 -0
  34. {sunpy_sphinx_theme-2.3.0 → sunpy_sphinx_theme-2.4.0}/setup.cfg +0 -0
  35. {sunpy_sphinx_theme-2.3.0 → sunpy_sphinx_theme-2.4.0}/src/sunpy_sphinx_theme/cards.py +0 -0
  36. {sunpy_sphinx_theme-2.3.0 → sunpy_sphinx_theme-2.4.0}/src/sunpy_sphinx_theme/conf.py +0 -0
  37. {sunpy_sphinx_theme-2.3.0 → sunpy_sphinx_theme-2.4.0}/src/sunpy_sphinx_theme/theme/sunpy/components/built-with.html +0 -0
  38. {sunpy_sphinx_theme-2.3.0 → sunpy_sphinx_theme-2.4.0}/src/sunpy_sphinx_theme/theme/sunpy/components/footer-links.html +0 -0
  39. {sunpy_sphinx_theme-2.3.0 → sunpy_sphinx_theme-2.4.0}/src/sunpy_sphinx_theme/theme/sunpy/components/navbar-logo.html +0 -0
  40. {sunpy_sphinx_theme-2.3.0 → sunpy_sphinx_theme-2.4.0}/src/sunpy_sphinx_theme/theme/sunpy/components/navbar_center.html +0 -0
  41. {sunpy_sphinx_theme-2.3.0 → sunpy_sphinx_theme-2.4.0}/src/sunpy_sphinx_theme/theme/sunpy/components/navbar_start.html +0 -0
  42. {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
  43. {sunpy_sphinx_theme-2.3.0 → sunpy_sphinx_theme-2.4.0}/src/sunpy_sphinx_theme/theme/sunpy/sections/header.html +0 -0
  44. {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
  45. {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
  46. {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
  47. {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
  48. {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
  49. {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
  50. {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
  51. {sunpy_sphinx_theme-2.3.0 → sunpy_sphinx_theme-2.4.0}/src/sunpy_sphinx_theme/theme/sunpy/static/sunpy_style.css +0 -0
  52. {sunpy_sphinx_theme-2.3.0 → sunpy_sphinx_theme-2.4.0}/src/sunpy_sphinx_theme.egg-info/dependency_links.txt +0 -0
  53. {sunpy_sphinx_theme-2.3.0 → sunpy_sphinx_theme-2.4.0}/src/sunpy_sphinx_theme.egg-info/entry_points.txt +0 -0
  54. {sunpy_sphinx_theme-2.3.0 → sunpy_sphinx_theme-2.4.0}/src/sunpy_sphinx_theme.egg-info/top_level.txt +0 -0
@@ -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.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.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.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
+ }
@@ -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.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
- "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)
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(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
+ ]
@@ -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))
@@ -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 = "False"
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 = "True"
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"
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: sunpy-sphinx-theme
3
- Version: 2.3.0
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.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.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
@@ -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
@@ -1,4 +1,4 @@
1
- pydata-sphinx-theme<0.23,>=0.22.0rc0
1
+ pydata-sphinx-theme<0.24,>=0.22.0
2
2
  sphinxext-opengraph>=0.13
3
3
  sphinx>=7.3.0
4
4
 
@@ -10,3 +10,7 @@ sphinx-design
10
10
  sphinx-gallery
11
11
  sphinx-togglebutton
12
12
  sunpy
13
+
14
+ [test]
15
+ pytest>=7.0
16
+ pytest-playwright
@@ -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",
@@ -0,0 +1,8 @@
1
+ {
2
+ "tag": "2.4.0",
3
+ "distance": 0,
4
+ "node": "gfd0161cbb672c0deac5247be6eb7d34cd8082cc6",
5
+ "dirty": false,
6
+ "branch": "HEAD",
7
+ "node_date": "2026-09-30"
8
+ }
@@ -1,8 +0,0 @@
1
- {
2
- "tag": "2.3.0",
3
- "distance": 0,
4
- "node": "g802a2218a32a8e8e4065daa33dcf2714523d779b",
5
- "dirty": false,
6
- "branch": "HEAD",
7
- "node_date": "2026-09-21"
8
- }