Source code for sphinx.project

"""Utility function and classes for Sphinx projects."""

from __future__ import annotations

import contextlib
import os
from pathlib import Path
from typing import TYPE_CHECKING

from sphinx.locale import __
from sphinx.util import logging
from sphinx.util._pathlib import _StrPath
from sphinx.util.matching import get_matching_files
from sphinx.util.osutil import path_stabilize

if TYPE_CHECKING:
    from collections.abc import Iterable

logger = logging.getLogger(__name__)
EXCLUDE_PATHS = ['**/_sources', '.#*', '**/.#*', '*.lproj/**']


[docs] class Project: """A project is the source code set of the Sphinx document(s).""" def __init__( self, srcdir: str | os.PathLike[str], source_suffix: Iterable[str] ) -> None: #: Source directory. self.srcdir = _StrPath(srcdir) #: source_suffix. Same as :confval:`source_suffix`. self.source_suffix = tuple(source_suffix) self._first_source_suffix = next(iter(self.source_suffix), '') #: The name of documents belonging to this project. self.docnames: set[str] = set() # Bijective mapping between docnames and (srcdir relative) paths. self._path_to_docname: dict[Path, str] = {} self._docname_to_path: dict[str, Path] = {}
[docs] def restore(self, other: Project) -> None: """Take over a result of last build.""" self.docnames = other.docnames self._path_to_docname = other._path_to_docname self._docname_to_path = other._docname_to_path
[docs] def discover( self, exclude_paths: Iterable[str] = (), include_paths: Iterable[str] = ('**',) ) -> set[str]: """Find all document files in the source directory and put them in :attr:`docnames`. """ self.docnames.clear() self._path_to_docname.clear() self._docname_to_path.clear() for filename in get_matching_files( self.srcdir, include_paths, [*exclude_paths, *EXCLUDE_PATHS], ): if docname := self.path2doc(filename): if docname in self.docnames: files = [ str(f.relative_to(self.srcdir)) for f in self.srcdir.glob(f'{docname}.*') ] logger.warning( __( 'multiple files found for the document "%s": %s\n' 'Use %r for the build.' ), docname, ', '.join(files), self.doc2path(docname, absolute=True), once=True, ) elif os.access(self.srcdir / filename, os.R_OK): self.docnames.add(docname) path = Path(filename) self._path_to_docname[path] = docname self._docname_to_path[docname] = path else: logger.warning( __('Ignored unreadable document %r.'), filename, location=docname, ) return self.docnames
[docs] def path2doc(self, filename: str | os.PathLike[str]) -> str | None: """Return the docname for the filename if the file is a document. *filename* should be absolute or relative to the source directory. """ path = Path(filename) try: return self._path_to_docname[path] except KeyError: if path.is_absolute(): with contextlib.suppress(ValueError): path = path.relative_to(self.srcdir) for suffix in self.source_suffix: if path.name.endswith(suffix): return path_stabilize(path).removesuffix(suffix) # the file does not have a docname return None
[docs] def doc2path(self, docname: str, absolute: bool) -> _StrPath: """Return the filename for the document name. If *absolute* is True, return as an absolute path. Else, return as a relative path to the source directory. """ try: filename = self._docname_to_path[docname] except KeyError: # Backwards compatibility: the document does not exist filename = Path(docname + self._first_source_suffix) if absolute: return _StrPath(self.srcdir / filename) return _StrPath(filename)