"""File utility functions for Sphinx.""" from __future__ import annotations import os import posixpath from typing import TYPE_CHECKING, Any, Callable from docutils.utils import relative_path from sphinx.util.osutil import copyfile, ensuredir if TYPE_CHECKING: from sphinx.util.template import BaseRenderer from sphinx.util.typing import PathMatcher def copy_asset_file(source: str | os.PathLike[str], destination: str | os.PathLike[str], context: dict[str, Any] | None = None, renderer: BaseRenderer | None = None) -> 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 """ if not os.path.exists(source): return if os.path.isdir(destination): # Use source filename if destination points a directory destination = os.path.join(destination, os.path.basename(source)) else: destination = str(destination) if os.path.basename(source).endswith(('_t', '_T')) and context is not None: if renderer is None: from sphinx.util.template import SphinxRenderer renderer = SphinxRenderer() with open(source, encoding='utf-8') as fsrc: if destination.endswith(('_t', '_T')): destination = destination[:-2] with open(destination, 'w', encoding='utf-8') as fdst: fdst.write(renderer.render_string(fsrc.read(), context)) else: copyfile(source, destination) 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) -> 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. :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. """ if not os.path.exists(source): return if renderer is None: from sphinx.util.template import SphinxRenderer renderer = SphinxRenderer() ensuredir(destination) if os.path.isfile(source): copy_asset_file(source, destination, context, renderer) return for root, dirs, files in os.walk(source, followlinks=True): reldir = relative_path(source, root) 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, renderer) except Exception as exc: if onerror: onerror(posixpath.join(root, filename), exc) else: raise