Source code for cardinal_pythonlib.sphinxtools

#!/usr/bin/env python
# cardinal_pythonlib/sphinxtools.py

"""
===============================================================================

    Original code copyright (C) 2009-2022 Rudolf Cardinal (rudolf@pobox.com).

    This file is part of cardinal_pythonlib.

    Licensed under the Apache License, Version 2.0 (the "License");
    you may not use this file except in compliance with the License.
    You may obtain a copy of the License at

        https://www.apache.org/licenses/LICENSE-2.0

    Unless required by applicable law or agreed to in writing, software
    distributed under the License is distributed on an "AS IS" BASIS,
    WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
    See the License for the specific language governing permissions and
    limitations under the License.

===============================================================================

**Functions to help with Sphinx, in particular the generation of autodoc
files.**

Rationale: if you want Sphinx ``autodoc`` code to appear as "one module per
Sphinx page" (which I normally do), you need one ``.rst`` file per module.

"""

from enum import Enum
from fnmatch import fnmatch
import glob
import logging
from os.path import (
    abspath,
    basename,
    dirname,
    exists,
    expanduser,
    isdir,
    isfile,
    join,
    relpath,
    sep,
    splitext,
)
from typing import Dict, Iterable, List, NoReturn, TYPE_CHECKING, Union

from cardinal_pythonlib.fileops import mkdir_p, relative_filename_within_dir
from cardinal_pythonlib.logs import BraceStyleAdapter
from cardinal_pythonlib.reprfunc import auto_repr
from pygments.lexers import get_lexer_for_filename
from pygments.util import ClassNotFound

if TYPE_CHECKING:
    from pygments.lexer import Lexer


log = BraceStyleAdapter(logging.getLogger(__name__))


# =============================================================================
# Constants
# =============================================================================

AUTOGENERATED_COMMENT = ".. THIS FILE IS AUTOMATICALLY GENERATED. DO NOT EDIT."
DEFAULT_INDEX_TITLE = "Automatic documentation of source code"
DEFAULT_SKIP_GLOBS = ["__init__.py"]
EXT_PYTHON = ".py"
EXT_RST = ".rst"
CODE_TYPE_NONE = "none"


[docs]class AutodocMethod(Enum): """ Enum to specify the method of autodocumenting a file. """ BEST = 0 CONTENTS = 1 AUTOMODULE = 2
# ============================================================================= # Helper functions # =============================================================================
[docs]def rst_underline(heading: str, underline_char: str) -> str: """ Underlines a heading for RST files. Args: heading: text to underline underline_char: character to use Returns: underlined heading, over two lines (without a final terminating newline) """ assert "\n" not in heading assert len(underline_char) == 1 return heading + "\n" + (underline_char * len(heading))
def fail(msg: str) -> NoReturn: log.critical(msg) raise RuntimeError(msg)
[docs]def write_if_allowed( filename: str, content: str, overwrite: bool = False, mock: bool = False ) -> None: """ Writes the contents to a file, if permitted. Args: filename: filename to write content: contents to write overwrite: permit overwrites? mock: pretend to write, but don't Raises: RuntimeError: if file exists but overwriting not permitted """ # Check we're allowed if not overwrite and exists(filename): fail(f"File exists, not overwriting: {filename!r}") # Make the directory, if necessary directory = dirname(filename) if not mock: mkdir_p(directory) # Write the file log.info("Writing to {!r}", filename) if mock: log.warning("Skipping writes as in mock mode") else: with open(filename, "wt") as outfile: outfile.write(content)
[docs]def filename_matches_glob(filename: str, globtext: str) -> bool: """ The ``glob.glob`` function doesn't do exclusion very well. We don't want to have to specify root directories for exclusion patterns. We don't want to have to trawl a massive set of files to find exclusion files. So let's implement a glob match. Args: filename: filename globtext: glob Returns: does the filename match the glob? See also: - https://stackoverflow.com/questions/20638040/glob-exclude-pattern """ # Quick check on basename-only matching if fnmatch(filename, globtext): log.debug("{!r} matches {!r}", filename, globtext) return True bname = basename(filename) if fnmatch(bname, globtext): log.debug("{!r} matches {!r}", bname, globtext) return True # Directory matching: is actually accomplished by the code above! # Otherwise: return False
# ============================================================================= # FileToAutodocument # =============================================================================
[docs]class FileToAutodocument(object): """ Class representing a file to document automatically via Sphinx autodoc. Example: .. code-block:: python import logging from cardinal_pythonlib.logs import * from cardinal_pythonlib.sphinxtools import * main_only_quicksetup_rootlogger(level=logging.DEBUG) f = FileToAutodocument( source_filename="~/Documents/code/cardinal_pythonlib/cardinal_pythonlib/sphinxtools.py", project_root_dir="~/Documents/code/cardinal_pythonlib", target_rst_filename="~/Documents/code/cardinal_pythonlib/docs/source/autodoc/sphinxtools.rst", ) print(f) f.source_extension f.is_python f.source_filename_rel_project_root f.rst_dir f.source_filename_rel_rst_file f.rst_filename_rel_project_root f.rst_filename_rel_autodoc_index( "~/Documents/code/cardinal_pythonlib/docs/source/autodoc/_index.rst") f.python_module_name f.pygments_code_type print(f.rst_content(prefix=".. Hello!")) print(f.rst_content(prefix=".. Hello!", method=AutodocMethod.CONTENTS)) f.write_rst(prefix=".. Hello!") """ def __init__( self, source_filename: str, project_root_dir: str, target_rst_filename: str, method: AutodocMethod = AutodocMethod.BEST, python_package_root_dir: str = None, source_rst_title_style_python: bool = True, pygments_language_override: Dict[str, str] = None, ) -> None: """ Args: source_filename: source file (e.g. Python, C++, XML file) to document project_root_dir: root directory of the whole project target_rst_filename: filenamd of an RST file to write that will document the source file method: instance of :class:`AutodocMethod`; for example, should we ask Sphinx's ``autodoc`` to read docstrings and build us a pretty page, or just include the contents with syntax highlighting? python_package_root_dir: if your Python modules live in a directory other than ``project_root_dir``, specify it here source_rst_title_style_python: if ``True`` and the file is a Python file and ``method == AutodocMethod.AUTOMODULE``, the heading used will be in the style of a Python module, ``x.y.z``. Otherwise, it will be a path (``x/y/z``). pygments_language_override: if specified, a dictionary mapping file specifications to Pygments languages (for example: a ``.pro`` file will be autodetected as Prolog, but you might want to map that to ``none`` for Qt project files). """ self.source_filename = abspath(expanduser(source_filename)) self.project_root_dir = abspath(expanduser(project_root_dir)) self.target_rst_filename = abspath(expanduser(target_rst_filename)) self.method = method self.source_rst_title_style_python = source_rst_title_style_python self.python_package_root_dir = ( abspath(expanduser(python_package_root_dir)) if python_package_root_dir else self.project_root_dir ) self.pygments_language_override = ( pygments_language_override or {} ) # type: Dict[str, str] assert isfile( self.source_filename ), f"Not a file: source_filename={self.source_filename!r}" assert isdir( self.project_root_dir ), f"Not a directory: project_root_dir={self.project_root_dir!r}" assert relative_filename_within_dir( filename=self.source_filename, directory=self.project_root_dir ), ( f"Source file {self.source_filename!r} is not within " f"project directory {self.project_root_dir!r}" ) assert relative_filename_within_dir( filename=self.python_package_root_dir, directory=self.project_root_dir, ), ( f"Python root {self.python_package_root_dir!r} is not within " f"project directory {self.project_root_dir!r}" ) assert isinstance(method, AutodocMethod) def __repr__(self) -> str: return auto_repr(self) @property def source_extension(self) -> str: """ Returns the extension of the source filename. """ return splitext(self.source_filename)[1] @property def is_python(self) -> bool: """ Is the source file a Python file? """ return self.source_extension == EXT_PYTHON @property def source_filename_rel_project_root(self) -> str: """ Returns the name of the source filename, relative to the project root. Used to calculate file titles. """ return relpath(self.source_filename, start=self.project_root_dir) @property def source_filename_rel_python_root(self) -> str: """ Returns the name of the source filename, relative to the Python package root. Used to calculate the name of Python modules. """ return relpath( self.source_filename, start=self.python_package_root_dir ) @property def rst_dir(self) -> str: """ Returns the directory of the target RST file. """ return dirname(self.target_rst_filename) @property def source_filename_rel_rst_file(self) -> str: """ Returns the source filename as seen from the RST filename that we will generate. Used for ``.. include::`` commands. """ return relpath(self.source_filename, start=self.rst_dir) @property def rst_filename_rel_project_root(self) -> str: """ Returns the filename of the target RST file, relative to the project root directory. Used for labelling the RST file itself. """ return relpath(self.target_rst_filename, start=self.project_root_dir)
[docs] def rst_filename_rel_autodoc_index(self, index_filename: str) -> str: """ Returns the filename of the target RST file, relative to a specified index file. Used to make the index refer to the RST. """ index_dir = dirname(abspath(expanduser(index_filename))) return relpath(self.target_rst_filename, start=index_dir)
@property def python_module_name(self) -> str: """ Returns the name of the Python module that this instance refers to, in dotted Python module notation, or a blank string if it doesn't. """ if not self.is_python: return "" filepath = self.source_filename_rel_python_root dirs_and_base = splitext(filepath)[0] dir_and_file_parts = dirs_and_base.split(sep) return ".".join(dir_and_file_parts) @property def pygments_language(self) -> str: """ Returns the code type annotation for Pygments; e.g. ``python`` for Python, ``cpp`` for C++, etc. """ for globtext, language in self.pygments_language_override.items(): if filename_matches_glob(self.source_filename, globtext): return language try: lexer = get_lexer_for_filename(self.source_filename) # type: Lexer return lexer.name except ClassNotFound: log.warning( "Don't know Pygments code type for extension {!r}", self.source_extension, ) return CODE_TYPE_NONE
[docs] def rst_content( self, prefix: str = "", suffix: str = "", heading_underline_char: str = "=", method: AutodocMethod = None, ) -> str: """ Returns the text contents of an RST file that will automatically document our source file. Args: prefix: prefix, e.g. RST copyright comment suffix: suffix, after the part we're creating heading_underline_char: RST character to use to underline the heading method: optional method to override ``self.method``; see constructor Returns: the RST contents """ spacer = " " # Choose our final method if method is None: method = self.method is_python = self.is_python if method == AutodocMethod.BEST: if is_python: method = AutodocMethod.AUTOMODULE else: method = AutodocMethod.CONTENTS elif method == AutodocMethod.AUTOMODULE: if not is_python: method = AutodocMethod.CONTENTS # Write the instruction if method == AutodocMethod.AUTOMODULE: if self.source_rst_title_style_python: title = self.python_module_name else: title = self.source_filename_rel_project_root instruction = ( f".. automodule:: {self.python_module_name}\n" f" :members:" ) elif method == AutodocMethod.CONTENTS: title = self.source_filename_rel_project_root # Using ".. include::" with options like ":code: python" doesn't # work properly; everything comes out as Python. # Instead, see http://www.sphinx-doc.org/en/1.4.9/markup/code.html; # we need ".. literalinclude::" with ":language: LANGUAGE". instruction = ( ".. literalinclude:: {filename}\n" "{spacer}:language: {language}".format( filename=self.source_filename_rel_rst_file, spacer=spacer, language=self.pygments_language, ) ) else: raise ValueError("Bad method!") # Create the whole file content = ( """ .. {filename} {AUTOGENERATED_COMMENT} {prefix} {underlined_title} {instruction} {suffix} """.format( filename=self.rst_filename_rel_project_root, AUTOGENERATED_COMMENT=AUTOGENERATED_COMMENT, prefix=prefix, underlined_title=rst_underline( title, underline_char=heading_underline_char ), instruction=instruction, suffix=suffix, ).strip() + "\n" ) return content
[docs] def write_rst( self, prefix: str = "", suffix: str = "", heading_underline_char: str = "=", method: AutodocMethod = None, overwrite: bool = False, mock: bool = False, ) -> None: """ Writes the RST file to our destination RST filename, making any necessary directories. Args: prefix: as for :func:`rst_content` suffix: as for :func:`rst_content` heading_underline_char: as for :func:`rst_content` method: as for :func:`rst_content` overwrite: overwrite the file if it exists already? mock: pretend to write, but don't """ content = self.rst_content( prefix=prefix, suffix=suffix, heading_underline_char=heading_underline_char, method=method, ) write_if_allowed( self.target_rst_filename, content, overwrite=overwrite, mock=mock )
# ============================================================================= # AutodocIndex # =============================================================================
[docs]class AutodocIndex(object): """ Class to make an RST file that indexes others. Example: .. code-block:: python import logging from cardinal_pythonlib.logs import * from cardinal_pythonlib.sphinxtools import * main_only_quicksetup_rootlogger(level=logging.INFO) # Example where one index contains another: subidx = AutodocIndex( index_filename="~/Documents/code/cardinal_pythonlib/docs/source/autodoc/_index2.rst", highest_code_dir="~/Documents/code/cardinal_pythonlib", project_root_dir="~/Documents/code/cardinal_pythonlib", autodoc_rst_root_dir="~/Documents/code/cardinal_pythonlib/docs/source/autodoc", source_filenames_or_globs="~/Documents/code/cardinal_pythonlib/docs/*.py", ) idx = AutodocIndex( index_filename="~/Documents/code/cardinal_pythonlib/docs/source/autodoc/_index.rst", highest_code_dir="~/Documents/code/cardinal_pythonlib", project_root_dir="~/Documents/code/cardinal_pythonlib", autodoc_rst_root_dir="~/Documents/code/cardinal_pythonlib/docs/source/autodoc", source_filenames_or_globs="~/Documents/code/cardinal_pythonlib/cardinal_pythonlib/*.py", ) idx.add_index(subidx) print(idx.index_content()) idx.write_index_and_rst_files(overwrite=True, mock=True) # Example with a flat index: flatidx = AutodocIndex( index_filename="~/Documents/code/cardinal_pythonlib/docs/source/autodoc/_index.rst", highest_code_dir="~/Documents/code/cardinal_pythonlib/cardinal_pythonlib", project_root_dir="~/Documents/code/cardinal_pythonlib", autodoc_rst_root_dir="~/Documents/code/cardinal_pythonlib/docs/source/autodoc", source_filenames_or_globs="~/Documents/code/cardinal_pythonlib/cardinal_pythonlib/*.py", ) print(flatidx.index_content()) flatidx.write_index_and_rst_files(overwrite=True, mock=True) """ def __init__( self, index_filename: str, project_root_dir: str, autodoc_rst_root_dir: str, highest_code_dir: str, python_package_root_dir: str = None, source_filenames_or_globs: Union[str, Iterable[str]] = None, index_heading_underline_char: str = "-", source_rst_heading_underline_char: str = "~", title: str = DEFAULT_INDEX_TITLE, introductory_rst: str = "", recursive: bool = True, skip_globs: List[str] = None, toctree_maxdepth: int = 1, method: AutodocMethod = AutodocMethod.BEST, rst_prefix: str = "", rst_suffix: str = "", source_rst_title_style_python: bool = True, pygments_language_override: Dict[str, str] = None, ) -> None: """ Args: index_filename: filename of the index ``.RST`` (ReStructured Text) file to create project_root_dir: top-level directory for the whole project autodoc_rst_root_dir: directory within which all automatically generated ``.RST`` files (each to document a specific source file) will be placed. A directory hierarchy within this directory will be created, reflecting the structure of the code relative to ``highest_code_dir`` (q.v.). highest_code_dir: the "lowest" directory such that all code is found within it; the directory structure within ``autodoc_rst_root_dir`` is to ``.RST`` files what the directory structure is of the source files, relative to ``highest_code_dir``. python_package_root_dir: if your Python modules live in a directory other than ``project_root_dir``, specify it here source_filenames_or_globs: optional string, or list of strings, each describing a file or glob-style file specification; these are the source filenames to create automatic RST` for. If you don't specify them here, you can use :func:`add_source_files`. To add sub-indexes, use :func:`add_index` and :func:`add_indexes`. index_heading_underline_char: the character used to underline the title in the index file source_rst_heading_underline_char: the character used to underline the heading in each of the source files title: title for the index introductory_rst: extra RST for the index, which goes between the title and the table of contents recursive: use :func:`glob.glob` in recursive mode? skip_globs: list of file names or file specifications to skip; e.g. ``['__init__.py']`` toctree_maxdepth: ``maxdepth`` parameter for the ``toctree`` command generated in the index file method: see :class:`FileToAutodocument` rst_prefix: optional RST content (e.g. copyright comment) to put early on in each of the RST files rst_suffix: optional RST content to put late on in each of the RST files source_rst_title_style_python: make the individual RST files use titles in the style of Python modules, ``x.y.z``, rather than path style (``x/y/z``); path style will be used for non-Python files in any case. pygments_language_override: if specified, a dictionary mapping file specifications to Pygments languages (for example: a ``.pro`` file will be autodetected as Prolog, but you might want to map that to ``none`` for Qt project files). """ assert index_filename assert project_root_dir assert autodoc_rst_root_dir assert isinstance(toctree_maxdepth, int) assert isinstance(method, AutodocMethod) self.index_filename = abspath(expanduser(index_filename)) self.title = title self.introductory_rst = introductory_rst self.project_root_dir = abspath(expanduser(project_root_dir)) self.autodoc_rst_root_dir = abspath(expanduser(autodoc_rst_root_dir)) self.highest_code_dir = abspath(expanduser(highest_code_dir)) self.python_package_root_dir = ( abspath(expanduser(python_package_root_dir)) if python_package_root_dir else self.project_root_dir ) self.index_heading_underline_char = index_heading_underline_char self.source_rst_heading_underline_char = ( source_rst_heading_underline_char ) self.recursive = recursive self.skip_globs = ( skip_globs if skip_globs is not None else DEFAULT_SKIP_GLOBS ) self.toctree_maxdepth = toctree_maxdepth self.method = method self.rst_prefix = rst_prefix self.rst_suffix = rst_suffix self.source_rst_title_style_python = source_rst_title_style_python self.pygments_language_override = ( pygments_language_override or {} ) # type: Dict[str, str] assert isdir( self.project_root_dir ), f"Not a directory: project_root_dir={self.project_root_dir!r}" assert relative_filename_within_dir( filename=self.index_filename, directory=self.project_root_dir ), ( f"Index file {self.index_filename!r} is not within " f"project directory {self.project_root_dir!r}" ) assert relative_filename_within_dir( filename=self.highest_code_dir, directory=self.project_root_dir ), ( f"Highest code directory {self.highest_code_dir!r} is not within " f"project directory {self.project_root_dir!r}" ) assert relative_filename_within_dir( filename=self.autodoc_rst_root_dir, directory=self.project_root_dir ), ( f"Autodoc RST root directory {self.autodoc_rst_root_dir!r} is not " f"within project directory {self.project_root_dir!r}" ) assert isinstance(method, AutodocMethod) assert isinstance(recursive, bool) self.files_to_index = ( [] ) # type: List[Union[FileToAutodocument, AutodocIndex]] if source_filenames_or_globs: self.add_source_files(source_filenames_or_globs) def __repr__(self) -> str: return auto_repr(self)
[docs] def add_source_files( self, source_filenames_or_globs: Union[str, List[str]], method: AutodocMethod = None, recursive: bool = None, source_rst_title_style_python: bool = None, pygments_language_override: Dict[str, str] = None, ) -> None: """ Adds source files to the index. Args: source_filenames_or_globs: string containing a filename or a glob, describing the file(s) to be added, or a list of such strings method: optional method to override ``self.method`` recursive: use :func:`glob.glob` in recursive mode? (If ``None``, the default, uses the version from the constructor.) source_rst_title_style_python: optional to override ``self.source_rst_title_style_python`` pygments_language_override: optional to override ``self.pygments_language_override`` """ if not source_filenames_or_globs: return if method is None: # Use the default method = self.method if recursive is None: recursive = self.recursive if source_rst_title_style_python is None: source_rst_title_style_python = self.source_rst_title_style_python if pygments_language_override is None: pygments_language_override = self.pygments_language_override # Get a sorted list of filenames final_filenames = self.get_sorted_source_files( source_filenames_or_globs, recursive=recursive ) # Process that sorted list for source_filename in final_filenames: self.files_to_index.append( FileToAutodocument( source_filename=source_filename, project_root_dir=self.project_root_dir, python_package_root_dir=self.python_package_root_dir, target_rst_filename=self.specific_file_rst_filename( source_filename ), method=method, source_rst_title_style_python=source_rst_title_style_python, # noqa: E501 pygments_language_override=pygments_language_override, ) )
[docs] def get_sorted_source_files( self, source_filenames_or_globs: Union[str, List[str]], recursive: bool = True, ) -> List[str]: """ Returns a sorted list of filenames to process, from a filename, a glob string, or a list of filenames/globs. Args: source_filenames_or_globs: filename/glob, or list of them recursive: use :func:`glob.glob` in recursive mode? Returns: sorted list of files to process """ if isinstance(source_filenames_or_globs, str): source_filenames_or_globs = [source_filenames_or_globs] final_filenames = [] # type: List[str] for sfg in source_filenames_or_globs: sfg_expanded = expanduser(sfg) log.debug("Looking for: {!r}", sfg_expanded) for filename in glob.glob(sfg_expanded, recursive=recursive): log.debug("Trying: {!r}", filename) if self.should_exclude(filename): log.info("Skipping file {!r}", filename) continue final_filenames.append(filename) final_filenames.sort() return final_filenames
[docs] def should_exclude(self, filename) -> bool: """ Should we exclude this file from consideration? """ for skip_glob in self.skip_globs: if filename_matches_glob(filename, skip_glob): return True return False
[docs] def add_index(self, index: "AutodocIndex") -> None: """ Add a sub-index file to this index. Args: index: index file to add, as an instance of :class:`AutodocIndex` """ self.files_to_index.append(index)
[docs] def add_indexes(self, indexes: List["AutodocIndex"]) -> None: """ Adds multiple sub-indexes to this index. Args: indexes: list of sub-indexes """ for index in indexes: self.add_index(index)
[docs] def specific_file_rst_filename(self, source_filename: str) -> str: """ Gets the RST filename corresponding to a source filename. See the help for the constructor for more details. Args: source_filename: source filename within current project Returns: RST filename Note in particular: the way we structure the directories means that we won't get clashes between files with idential names in two different directories. However, we must also incorporate the original source filename, in particular for C++ where ``thing.h`` and ``thing.cpp`` must not generate the same RST filename. So we just add ``.rst``. """ highest_code_to_target = relative_filename_within_dir( source_filename, self.highest_code_dir ) bname = basename(source_filename) result = join( self.autodoc_rst_root_dir, dirname(highest_code_to_target), bname + EXT_RST, ) log.debug("Source {!r} -> RST {!r}", source_filename, result) return result
[docs] def write_index_and_rst_files( self, overwrite: bool = False, mock: bool = False ) -> None: """ Writes both the individual RST files and the index. Args: overwrite: allow existing files to be overwritten? mock: pretend to write, but don't """ for f in self.files_to_index: if isinstance(f, FileToAutodocument): f.write_rst( prefix=self.rst_prefix, suffix=self.rst_suffix, heading_underline_char=self.source_rst_heading_underline_char, # noqa: E501 overwrite=overwrite, mock=mock, ) elif isinstance(f, AutodocIndex): f.write_index_and_rst_files(overwrite=overwrite, mock=mock) else: fail(f"Unknown thing in files_to_index: {f!r}") self.write_index(overwrite=overwrite, mock=mock)
@property def index_filename_rel_project_root(self) -> str: """ Returns the name of the index filename, relative to the project root. Used for labelling the index file. """ return relpath(self.index_filename, start=self.project_root_dir)
[docs] def index_filename_rel_other_index(self, other: str) -> str: """ Returns the filename of this index, relative to the director of another index. (For inserting a reference to this index into ``other``.) Args: other: the other index Returns: relative filename of our index """ return relpath(self.index_filename, start=dirname(other))
[docs] def index_content(self) -> str: """ Returns the contents of the index RST file. """ # Build the toctree command index_filename = self.index_filename spacer = " " toctree_lines = [ ".. toctree::", spacer + f":maxdepth: {self.toctree_maxdepth}", "", ] for f in self.files_to_index: if isinstance(f, FileToAutodocument): rst_filename = spacer + f.rst_filename_rel_autodoc_index( index_filename ) elif isinstance(f, AutodocIndex): rst_filename = spacer + f.index_filename_rel_other_index( index_filename ) else: fail(f"Unknown thing in files_to_index: {f!r}") rst_filename = "" # won't get here; for the type checker toctree_lines.append(rst_filename) toctree = "\n".join(toctree_lines) # Create the whole file content = ( """ .. {filename} {AUTOGENERATED_COMMENT} {prefix} {underlined_title} {introductory_rst} {toctree} {suffix} """.format( filename=self.index_filename_rel_project_root, AUTOGENERATED_COMMENT=AUTOGENERATED_COMMENT, prefix=self.rst_prefix, underlined_title=rst_underline( self.title, underline_char=self.index_heading_underline_char, ), introductory_rst=self.introductory_rst, toctree=toctree, suffix=self.rst_suffix, ).strip() + "\n" ) return content
[docs] def write_index(self, overwrite: bool = False, mock: bool = False) -> None: """ Writes the index file, if permitted. Args: overwrite: allow existing files to be overwritten? mock: pretend to write, but don't """ write_if_allowed( self.index_filename, self.index_content(), overwrite=overwrite, mock=mock, )