Source code for sphinx_versioned.build

import os
import fnmatch
import shutil
import pathlib
from sphinx import application
from sphinx.errors import SphinxError
from sphinx.cmd.build import build_main

from loguru import logger as log

from sphinx_versioned.sphinx_ import EventHandlers
from sphinx_versioned.lib import TempDir, ConfigInject
from sphinx_versioned.versions import GitVersions, BuiltVersions, PseudoBranch


[docs] class VersionedDocs: """Handles main build workflow. Chronologically, the :meth:`~sphinx_versioned.__init__` first parses the config (input parameters via a :class:`dict`) and then handles various paths, like current workding directory, source directory and output directory. Further, it selects the branches to pre-build/build then finally generates the top-level ``index.html`` file. Parameters ---------- config : :class:`dict` """ def __init__(self, config: dict, debug: bool = False) -> None: self.config = config self._parse_config(config) self._handle_paths() self._versions_to_pre_build = [] self._versions_to_build = [] self._failed_build = [] # Get all versions and make a lookup table self._all_branches = self.versions.all_versions self._lookup_branch = {x.name: x for x in self._all_branches} self._select_exclude_branches() # if `--force` is supplied with no `--main-branch`, make the `_active_branch` as the `main_branch` if not self.main_branch: if self.force_branches: self.main_branch = self.versions.active_branch.name else: self.main_branch = "main" if debug: return self.prebuild() # Adds our extension to the sphinx-config application.Config = ConfigInject self.build() # Adds a top-level `index.html` in `output_dir` which redirects to `output_dir`/`main-branch`/index.html self._generate_top_level_index() print(f"\n\033[92m Successfully built {', '.join([x.name for x in self._built_version])} \033[0m") return def _parse_config(self, config: dict) -> bool: for varname, value in config.items(): setattr(self, varname, value) self._additional_args = () self._additional_args += ("-Q",) if self.quite else () self._additional_args += ("-vv",) if self.verbose else () return True
[docs] def _handle_paths(self) -> None: """Method to handle cwd and path for local config, as well as, configure :class:`~sphinx_versioned.versions.GitVersions` and the output directory. """ self.chdir = self.chdir if self.chdir else os.getcwd() log.debug(f"Working directory {self.chdir}") self.versions = GitVersions(self.git_root, self.output_dir, self.force_branches) self.output_dir = pathlib.Path(self.output_dir) self.local_conf = pathlib.Path(self.local_conf) if self.local_conf.name != "conf.py": self.local_conf = self.local_conf / "conf.py" if not self.local_conf.exists(): log.error(f"conf.py does not exist at {self.local_conf}") raise FileNotFoundError(f"conf.py not found at {self.local_conf.parent}") log.success(f"located conf.py") return
def _select_branches(self) -> None: if not self.select_branches: self._versions_to_pre_build = self._all_branches return for tag in self.select_branches: filtered_tags = fnmatch.filter(self._lookup_branch.keys(), tag) if filtered_tags: self._versions_to_pre_build.extend([self._lookup_branch.get(x) for x in filtered_tags]) elif self.force_branches: log.warning(f"Forcing build for branch `{tag}`, be careful, it may or may not exist!") self._versions_to_pre_build.append(PseudoBranch(tag)) else: log.critical(f"Branch not found/selected: `{tag}`, use `--force` to force the build") return def _exclude_branches(self) -> None: if not self.exclude_branches: return _names_versions_to_pre_build = [x.name for x in self._versions_to_pre_build] for tag in self.exclude_branches: filtered_tags = fnmatch.filter(_names_versions_to_pre_build, tag) for x in filtered_tags: self._versions_to_pre_build.remove(self._lookup_branch.get(x)) return def _select_exclude_branches(self) -> list: log.debug(f"Instructions to select: `{self.select_branches}`") log.debug(f"Instructions to exclude: `{self.exclude_branches}`") self._versions_to_pre_build = [] self._select_branches() self._exclude_branches() log.info(f"selected branches: `{[x.name for x in self._versions_to_pre_build]}`") return
[docs] def _generate_top_level_index(self) -> None: """Generate a top-level ``index.html`` which redirects to the main-branch version specified via ``main_branch``. """ if self.main_branch not in [x.name for x in self._built_version]: log.critical( f"main branch `{self.main_branch}` not found!! / not building `{self.main_branch}`; " "top-level `index.html` will not be generated!" ) return log.success(f"main branch: `{self.main_branch}`; generating top-level `index.html`") with open(self.output_dir / "index.html", "w") as findex: findex.write( f""" <!DOCTYPE html> <html> <head> <meta http-equiv="refresh" content="0; url = {self.main_branch}/index.html" /> </head> """ ) return
[docs] def _build(self, tag, _prebuild: bool = False) -> bool: """Internal build method which actually carries out the pre-build/build transctions inside a temporary directory then copy the asset files to the output directory if it's not a pre-build. Parameters ---------- tag : :class:`git.Branch` or :class:`git.Tag` Branch/tag to build. _prebuild : :class:`bool` Variable to perform/skip pre-builds for selected/all versions. Returns ------- :class:`bool` """ # Checkout tag/branch self.versions.checkout(tag) EventHandlers.CURRENT_VERSION = tag with TempDir() as temp_dir: log.debug(f"Checking out the tag in temporary directory: {temp_dir}") source = str(self.local_conf.parent) target = temp_dir argv = (source, target) argv += self._additional_args result = build_main(argv) if result != 0: raise SphinxError if _prebuild: log.success(f"pre-build succeded for {tag} :)") return True output_with_tag = self.output_dir / tag if not output_with_tag.exists(): output_with_tag.mkdir(parents=True, exist_ok=True) shutil.copytree(temp_dir, output_with_tag, False, None, dirs_exist_ok=True) log.success(f"build succeded for {tag} ;)") return True
[docs] def prebuild(self) -> None: """Pre-build workflow. Method to pre-build the selected/all branches in a temporary environment. Essentially, it builds the various selected/all branches with the vanilla sphinx-build method and pass-on the branches which ends up successful in vanilla sphinx-build. The method carries out the transaction via the internal build method :meth:`~sphinx_versioned.build.VersionedDocs._build`. """ if not self.prebuild_branches: log.info("No pre-builing...") self._versions_to_build = self._versions_to_pre_build return log.debug("Pre-building...") # get active branch self._active_branch = self.versions.active_branch for tag in self._versions_to_pre_build: log.info(f"pre-building: {tag}") try: self._build(tag, _prebuild=True) self._versions_to_build.append(tag) except SphinxError: log.critical(f"Pre-build failed for {tag}") finally: # restore to active branch self.versions.checkout(self._active_branch.name) log.success(f"Prebuilding successful for {', '.join([x.name for x in self._versions_to_build])}") return
[docs] def build(self) -> None: """Build workflow. Method to build the branch in a temporary directory with the modified :class:`sphinx_versioned.lib.ConfigInject` and injectes the versions flyout menu to the footer or the sidebars. The method carries out the transaction via the internal build method :meth:`~sphinx_versioned.build.VersionedDocs._build`. """ # get active branch self._active_branch = self.versions.active_branch self._built_version = [] EventHandlers.VERSIONS = BuiltVersions(self._versions_to_build, self.versions.build_directory) for tag in self._versions_to_build: log.info(f"Building: {tag}") try: self._build(tag.name) self._built_version.append(tag) except SphinxError: log.error(f"build failed for {tag}") exit(-1) finally: # restore to active branch self.versions.checkout(self._active_branch) return
pass