"""File utility functions for Sphinx."""
from __future__ import annotations
import os
import posixpath
from pathlib import Path
from typing import TYPE_CHECKING
from sphinx.locale import __
from sphinx.util import logging
from sphinx.util.osutil import _relative_path, copyfile, ensuredir
if TYPE_CHECKING:
from collections.abc import Callable
from typing import Any
from sphinx.util.template import BaseRenderer
from sphinx.util.typing import PathMatcher
logger = logging.getLogger(__name__)
def _template_basename(filename: Path) -> Path | None:
"""Given an input filename:
If the input looks like a template, then return the filename output should
be written to. Otherwise, return no result (None).
"""
basename = filename.name.lower()
if basename.endswith('_t'):
return filename.with_name(filename.name[:-2])
elif basename.endswith('.jinja'):
return filename.with_name(filename.name[:-6])
return None
def copy_asset_file(
source: str | os.PathLike[str],
destination: str | os.PathLike[str],
context: dict[str, Any] | None = None,
renderer: BaseRenderer | None = None,
*,
force: bool = False,
) -> None:
"""Copy an asset file to destination.
On copying, it expands the template variables if context argument is given and
the asset is a template file.
:param source: The path to source file
:param destination: The path to destination file or directory
:param context: The template variables. If not given, template files are simply copied
:param renderer: The template engine. If not given, SphinxRenderer is used by default
:param bool force: Overwrite the destination file even if it exists.
"""
source = Path(source)
if not source.exists():
return
destination = Path(destination)
if destination.is_dir():
# Use source filename if destination points a directory
destination /= source.name
if _template_basename(source) and context is not None:
if renderer is None:
from sphinx.util.template import SphinxRenderer
renderer = SphinxRenderer()
template_content = source.read_text(encoding='utf-8')
rendered_template = renderer.render_string(template_content, context)
if not force and destination.exists() and template_content != rendered_template:
msg = __(
'Aborted attempted copy from rendered template %s to %s '
'(the destination path has existing data).'
)
logger.warning(
msg,
os.fsdecode(source),
os.fsdecode(destination),
type='misc',
subtype='copy_overwrite',
)
return
destination = _template_basename(destination) or destination
msg = __('Writing evaluated template result to %s')
logger.info(
msg,
os.fsdecode(destination),
type='misc',
subtype='template_evaluation',
)
destination.write_text(rendered_template, encoding='utf-8')
else:
copyfile(source, destination, force=force)
def copy_asset(
source: str | os.PathLike[str],
destination: str | os.PathLike[str],
excluded: PathMatcher = lambda path: False,
context: dict[str, Any] | None = None,
renderer: BaseRenderer | None = None,
onerror: Callable[[str, Exception], None] | None = None,
*,
force: bool = False,
) -> None:
"""Copy asset files to destination recursively.
On copying, it expands the template variables if context argument is given and
the asset is a template file.
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
:param context: The template variables. If not given, template files are simply copied
:param renderer: The template engine. If not given, SphinxRenderer is used by default
:param onerror: The error handler.
:param bool force: Overwrite the destination file even if it exists.
"""
source = Path(source)
if not source.exists():
return
if renderer is None:
from sphinx.util.template import SphinxRenderer
renderer = SphinxRenderer()
ensuredir(destination)
if source.is_file():
copy_asset_file(
source, destination, context=context, renderer=renderer, force=force
)
return
for root, dirs, files in os.walk(source, followlinks=True):
root_p = Path(root)
reldir = _relative_path(root_p, source).as_posix()
for dir in dirs.copy():
if excluded(posixpath.join(reldir, dir)):
dirs.remove(dir)
else:
ensuredir(posixpath.join(destination, reldir, dir))
for filename in files:
if not excluded(posixpath.join(reldir, filename)):
try:
copy_asset_file(
posixpath.join(root, filename),
posixpath.join(destination, reldir),
context=context,
renderer=renderer,
force=force,
)
except Exception as exc:
if onerror:
onerror(posixpath.join(root, filename), exc)
else:
raise