Add `app.add_static_dir()` for copying extension static files (#14219)

Co-authored-by: Adam Turner <9087854+AA-Turner@users.noreply.github.com>
This commit is contained in:
Jared DillardandAdam Turner authored and GitHub committed 2025-12-31 14:58:12 +00:00
1 parent 20f1c46790
commit b127b9478a
13 files changed
+130 -1

No files matched your search

+4
View File
@@ -17,6 +17,10 @@ Deprecated
Features added
--------------
* Add :meth:`~sphinx.application.Sphinx.add_static_dir` for copying static
assets from extensions to the build output.
Patch by Jared Dillard
Bugs fixed
----------
+2
View File
@@ -71,6 +71,8 @@ package.
.. automethod:: Sphinx.add_css_file
.. automethod:: Sphinx.add_static_dir
.. automethod:: Sphinx.add_latex_package
.. automethod:: Sphinx.add_lexer
+5
View File
@@ -1739,6 +1739,11 @@ and also make use of these options.
An alternative approach is to use :confval:`html_extra_path`,
which allows copying dotfiles under the directories.
.. seealso::
:meth:`~sphinx.application.Sphinx.add_static_dir`, for use in extensions
to copy static files to the output directory.
.. versionchanged:: 0.4
The paths in :confval:`html_static_path` can now contain subdirectories.
+43 -1
View File
@@ -10,6 +10,7 @@ import pickle
import sys
from collections import deque
from io import StringIO
from pathlib import Path
from typing import TYPE_CHECKING, overload
from docutils.parsers.rst import roles
@@ -37,7 +38,6 @@ from sphinx.util.tags import Tags
if TYPE_CHECKING:
import os
from collections.abc import Callable, Collection, Iterable, Sequence, Set
from pathlib import Path
from typing import IO, Any, Final, Literal
from docutils import nodes
@@ -1484,6 +1484,9 @@ class Sphinx:
A JavaScript file can be added to the specific HTML page when an extension
calls this method on :event:`html-page-context` event.
.. seealso::
:meth:`add_static_dir` for copying static files to the output directory
.. versionadded:: 0.5
.. versionchanged:: 1.8
@@ -1549,6 +1552,9 @@ class Sphinx:
A CSS file can be added to the specific HTML page when an extension calls
this method on :event:`html-page-context` event.
.. seealso::
:meth:`add_static_dir` for copying static files to the output directory
.. versionadded:: 1.0
.. versionchanged:: 1.6
@@ -1572,6 +1578,42 @@ class Sphinx:
filename, priority=priority, **kwargs
)
def add_static_dir(self, path: str | os.PathLike[str]) -> None:
"""Register a static directory to include in HTML output.
The given directory's contents will be copied to the ``_static``
directory during an HTML build. Files from extension static directories
are copied after theme static files and before any directories from
the user-configured ``html_static_path`` setting.
Sphinx has built-in support for ``static/`` directories in themes;
theme developers should only use this method to register further
directories to be copied.
:param path: The path to a directory containing static files.
This is typically relative to the extension's package
directory.
Example::
from pathlib import Path
def setup(app):
# All files in this directory are copied to _static/,
# preserving the subdirectory structure
app.add_static_dir(Path(__file__).parent / 'static')
# Add JavaScript and CSS files to HTML pages,
# the paths are relative to _static/
app.add_js_file('js/my_extension.js')
app.add_css_file('css/my_extension.css')
.. versionadded:: 9.1
"""
path = Path(path)
logger.debug("[app] adding static_dir: '%s'", path)
self.registry.add_static_dir(path)
def add_latex_package(
self, packagename: str, options: str | None = None, after_hyperref: bool = False
) -> None:
+10
View File
@@ -865,6 +865,15 @@ class StandaloneHTMLBuilder(Builder):
force=True,
)
def copy_static_dirs(self) -> None:
"""Copy static files registered by extensions."""
for static_dir in self._registry.static_dirs:
if static_dir.is_dir():
shutil.copytree(static_dir, self._static_dir, dirs_exist_ok=True)
else:
msg = __("extension static directory '%s' does not exist")
logger.warning(msg, static_dir)
def copy_html_static_files(self, context: dict[str, Any]) -> None:
def onerror(filename: str, error: Exception) -> None:
logger.warning(
@@ -916,6 +925,7 @@ class StandaloneHTMLBuilder(Builder):
self.copy_translation_js()
self.copy_stemmer_js()
self.copy_theme_static_files(context)
self.copy_static_dirs()
self.copy_html_static_files(context)
self.copy_html_logo()
self.copy_html_favicon()
+9
View File
@@ -22,6 +22,7 @@ from sphinx.util.logging import prefixed_warnings
if TYPE_CHECKING:
import os
from collections.abc import Callable, Iterator, Mapping, Sequence
from pathlib import Path
from typing import Any
from docutils import nodes
@@ -125,6 +126,9 @@ class SphinxComponentRegistry:
#: js_files; list of JS paths or URLs
self.js_files: list[tuple[str | None, dict[str, Any]]] = []
#: static directories registered by extensions
self.static_dirs: list[Path] = []
#: LaTeX packages; list of package names and its options
self.latex_packages: list[tuple[str, str | None]] = []
@@ -466,6 +470,11 @@ class SphinxComponentRegistry:
logger.debug('[app] adding js_file: %r, %r', filename, attributes)
self.js_files.append((filename, attributes))
def add_static_dir(self, path: Path) -> None:
"""Register a static directory for extensions."""
logger.debug("[app] adding static_dir: '%s'", path)
self.static_dirs.append(path)
def has_latex_package(self, name: str) -> bool:
packages = self.latex_packages + self.latex_packages_after_hyperref
return bool([x for x in packages if x[0] == name])
+3
View File
@@ -115,6 +115,9 @@ def copy_asset(
Use ``copy_asset_file`` instead to copy a single file.
Use ``app.add_static_dir()`` instead to copy static files to
the output directory.
:param source: The path to source file or directory
:param destination: The path to destination directory
:param excluded: The matcher to determine the given path should be copied or not
+7
View File
@@ -0,0 +1,7 @@
import sys
from pathlib import Path
sys.path.insert(0, str(Path.cwd().resolve()))
project = 'Test Extension Static Dir'
extensions = ['staticdirext']
@@ -0,0 +1,4 @@
Test
====
Content for testing extension static directories.
@@ -0,0 +1,16 @@
"""Test extension that registers a static directory."""
from pathlib import Path
HERE = Path(__file__).resolve().parent
def setup(app):
app.add_static_dir(HERE / 'static')
app.add_js_file('js/myext.js')
app.add_css_file('css/myext.css')
return {
'version': '1.0',
'parallel_read_safe': True,
'parallel_write_safe': True,
}
@@ -0,0 +1,2 @@
/* Extension CSS file */
.myext { color: blue; }
@@ -0,0 +1,2 @@
// Extension JavaScript file
console.log('myext loaded');
@@ -180,3 +180,26 @@ def test_javscript_loading_method(app: SphinxTestApp) -> None:
assert '<script src="_static/normal.js"></script>' in content
assert '<script async="async" src="_static/early.js"></script>' in content
assert '<script defer="defer" src="_static/late.js"></script>' in content
@pytest.mark.sphinx('html', testroot='ext_static_dir')
def test_extension_static_dir(app: SphinxTestApp) -> None:
"""Test that extension static directories are copied."""
app.build(force_all=True)
# Verify extension static files were copied to _static
assert (app.outdir / '_static' / 'js' / 'myext.js').is_file()
assert (app.outdir / '_static' / 'css' / 'myext.css').is_file()
# Verify JS and CSS are included in the HTML output
content = (app.outdir / 'index.html').read_text(encoding='utf8')
assert '_static/js/myext.js' in content
assert '_static/css/myext.css' in content
@pytest.mark.sphinx('html', testroot='ext_static_dir')
def test_extension_static_dir_registry(app: SphinxTestApp) -> None:
"""Test that extension static directories are registered in the registry."""
# Verify the static dir is registered
assert len(app.registry.static_dirs) == 1
assert app.registry.static_dirs[0].name == 'static'