Skip to content

Commit b127b94

Browse files
jdillardAA-Turner
andauthored
Add app.add_static_dir() for copying extension static files (#14219)
Co-authored-by: Adam Turner <9087854+AA-Turner@users.noreply.github.com>
1 parent 20f1c46 commit b127b94

File tree

13 files changed

+130
-1
lines changed

13 files changed

+130
-1
lines changed

CHANGES.rst

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -17,6 +17,10 @@ Deprecated
1717
Features added
1818
--------------
1919

20+
* Add :meth:`~sphinx.application.Sphinx.add_static_dir` for copying static
21+
assets from extensions to the build output.
22+
Patch by Jared Dillard
23+
2024
Bugs fixed
2125
----------
2226

doc/extdev/appapi.rst

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -71,6 +71,8 @@ package.
7171

7272
.. automethod:: Sphinx.add_css_file
7373

74+
.. automethod:: Sphinx.add_static_dir
75+
7476
.. automethod:: Sphinx.add_latex_package
7577

7678
.. automethod:: Sphinx.add_lexer

doc/usage/configuration.rst

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1739,6 +1739,11 @@ and also make use of these options.
17391739
An alternative approach is to use :confval:`html_extra_path`,
17401740
which allows copying dotfiles under the directories.
17411741

1742+
.. seealso::
1743+
1744+
:meth:`~sphinx.application.Sphinx.add_static_dir`, for use in extensions
1745+
to copy static files to the output directory.
1746+
17421747
.. versionchanged:: 0.4
17431748
The paths in :confval:`html_static_path` can now contain subdirectories.
17441749

sphinx/application.py

Lines changed: 43 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,7 @@
1010
import sys
1111
from collections import deque
1212
from io import StringIO
13+
from pathlib import Path
1314
from typing import TYPE_CHECKING, overload
1415

1516
from docutils.parsers.rst import roles
@@ -37,7 +38,6 @@
3738
if TYPE_CHECKING:
3839
import os
3940
from collections.abc import Callable, Collection, Iterable, Sequence, Set
40-
from pathlib import Path
4141
from typing import IO, Any, Final, Literal
4242

4343
from docutils import nodes
@@ -1484,6 +1484,9 @@ def add_js_file(
14841484
A JavaScript file can be added to the specific HTML page when an extension
14851485
calls this method on :event:`html-page-context` event.
14861486
1487+
.. seealso::
1488+
:meth:`add_static_dir` for copying static files to the output directory
1489+
14871490
.. versionadded:: 0.5
14881491
14891492
.. versionchanged:: 1.8
@@ -1549,6 +1552,9 @@ def add_css_file(self, filename: str, priority: int = 500, **kwargs: Any) -> Non
15491552
A CSS file can be added to the specific HTML page when an extension calls
15501553
this method on :event:`html-page-context` event.
15511554
1555+
.. seealso::
1556+
:meth:`add_static_dir` for copying static files to the output directory
1557+
15521558
.. versionadded:: 1.0
15531559
15541560
.. versionchanged:: 1.6
@@ -1572,6 +1578,42 @@ def add_css_file(self, filename: str, priority: int = 500, **kwargs: Any) -> Non
15721578
filename, priority=priority, **kwargs
15731579
)
15741580

1581+
def add_static_dir(self, path: str | os.PathLike[str]) -> None:
1582+
"""Register a static directory to include in HTML output.
1583+
1584+
The given directory's contents will be copied to the ``_static``
1585+
directory during an HTML build. Files from extension static directories
1586+
are copied after theme static files and before any directories from
1587+
the user-configured ``html_static_path`` setting.
1588+
1589+
Sphinx has built-in support for ``static/`` directories in themes;
1590+
theme developers should only use this method to register further
1591+
directories to be copied.
1592+
1593+
:param path: The path to a directory containing static files.
1594+
This is typically relative to the extension's package
1595+
directory.
1596+
1597+
Example::
1598+
1599+
from pathlib import Path
1600+
1601+
def setup(app):
1602+
# All files in this directory are copied to _static/,
1603+
# preserving the subdirectory structure
1604+
app.add_static_dir(Path(__file__).parent / 'static')
1605+
1606+
# Add JavaScript and CSS files to HTML pages,
1607+
# the paths are relative to _static/
1608+
app.add_js_file('js/my_extension.js')
1609+
app.add_css_file('css/my_extension.css')
1610+
1611+
.. versionadded:: 9.1
1612+
"""
1613+
path = Path(path)
1614+
logger.debug("[app] adding static_dir: '%s'", path)
1615+
self.registry.add_static_dir(path)
1616+
15751617
def add_latex_package(
15761618
self, packagename: str, options: str | None = None, after_hyperref: bool = False
15771619
) -> None:

sphinx/builders/html/__init__.py

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -865,6 +865,15 @@ def onerror(filename: str, error: Exception) -> None:
865865
force=True,
866866
)
867867

868+
def copy_static_dirs(self) -> None:
869+
"""Copy static files registered by extensions."""
870+
for static_dir in self._registry.static_dirs:
871+
if static_dir.is_dir():
872+
shutil.copytree(static_dir, self._static_dir, dirs_exist_ok=True)
873+
else:
874+
msg = __("extension static directory '%s' does not exist")
875+
logger.warning(msg, static_dir)
876+
868877
def copy_html_static_files(self, context: dict[str, Any]) -> None:
869878
def onerror(filename: str, error: Exception) -> None:
870879
logger.warning(
@@ -916,6 +925,7 @@ def copy_static_files(self) -> None:
916925
self.copy_translation_js()
917926
self.copy_stemmer_js()
918927
self.copy_theme_static_files(context)
928+
self.copy_static_dirs()
919929
self.copy_html_static_files(context)
920930
self.copy_html_logo()
921931
self.copy_html_favicon()

sphinx/registry.py

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -22,6 +22,7 @@
2222
if TYPE_CHECKING:
2323
import os
2424
from collections.abc import Callable, Iterator, Mapping, Sequence
25+
from pathlib import Path
2526
from typing import Any
2627

2728
from docutils import nodes
@@ -125,6 +126,9 @@ def __init__(self) -> None:
125126
#: js_files; list of JS paths or URLs
126127
self.js_files: list[tuple[str | None, dict[str, Any]]] = []
127128

129+
#: static directories registered by extensions
130+
self.static_dirs: list[Path] = []
131+
128132
#: LaTeX packages; list of package names and its options
129133
self.latex_packages: list[tuple[str, str | None]] = []
130134

@@ -466,6 +470,11 @@ def add_js_file(self, filename: str | None, **attributes: Any) -> None:
466470
logger.debug('[app] adding js_file: %r, %r', filename, attributes)
467471
self.js_files.append((filename, attributes))
468472

473+
def add_static_dir(self, path: Path) -> None:
474+
"""Register a static directory for extensions."""
475+
logger.debug("[app] adding static_dir: '%s'", path)
476+
self.static_dirs.append(path)
477+
469478
def has_latex_package(self, name: str) -> bool:
470479
packages = self.latex_packages + self.latex_packages_after_hyperref
471480
return bool([x for x in packages if x[0] == name])

sphinx/util/fileutil.py

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -115,6 +115,9 @@ def copy_asset(
115115
116116
Use ``copy_asset_file`` instead to copy a single file.
117117
118+
Use ``app.add_static_dir()`` instead to copy static files to
119+
the output directory.
120+
118121
:param source: The path to source file or directory
119122
:param destination: The path to destination directory
120123
:param excluded: The matcher to determine the given path should be copied or not
Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,7 @@
1+
import sys
2+
from pathlib import Path
3+
4+
sys.path.insert(0, str(Path.cwd().resolve()))
5+
6+
project = 'Test Extension Static Dir'
7+
extensions = ['staticdirext']
Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,4 @@
1+
Test
2+
====
3+
4+
Content for testing extension static directories.
Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,16 @@
1+
"""Test extension that registers a static directory."""
2+
3+
from pathlib import Path
4+
5+
HERE = Path(__file__).resolve().parent
6+
7+
8+
def setup(app):
9+
app.add_static_dir(HERE / 'static')
10+
app.add_js_file('js/myext.js')
11+
app.add_css_file('css/myext.css')
12+
return {
13+
'version': '1.0',
14+
'parallel_read_safe': True,
15+
'parallel_write_safe': True,
16+
}

0 commit comments

Comments
 (0)