diff --git a/docs/changes/78.breaking.rst b/docs/changes/78.breaking.rst new file mode 100644 index 0000000..0b5000c --- /dev/null +++ b/docs/changes/78.breaking.rst @@ -0,0 +1 @@ +Selecting a branch will take precedence over excluding one. diff --git a/docs/changes/78.feature.rst b/docs/changes/78.feature.rst new file mode 100644 index 0000000..329fe52 --- /dev/null +++ b/docs/changes/78.feature.rst @@ -0,0 +1,4 @@ +Added support for configuration file arguments. These arguments can be defined in `conf.py`. +Arguments can be defined in `conf.py` as long as the argument name is predeced by ``sv_``. +For example: `select_branch` becomes `sv_select_branch`. +Specific info about the arguments is available in documentation under settings topic. diff --git a/docs/settings.rst b/docs/settings.rst index 0167e9b..64c1f29 100644 --- a/docs/settings.rst +++ b/docs/settings.rst @@ -10,17 +10,9 @@ Settings The ``sphinx-versioned-docs`` reads options from two sources: - * From the sphinx ``conf.py`` file. * From the provided command-line-arguments. + * From the sphinx ``conf.py`` file. -Configuration File Arguments -============================ - -Currently, only the ``sv_project_url`` can be set via the ``conf.py``. More options coming in future releases. - -.. option:: sv_project_url: - - Setting this variable will make sure that the ``Project home`` is listed on the versions selector badge/menu. Command Line Arguments ====================== @@ -64,6 +56,10 @@ These command line options must be specified when executing the ``sphinx-version ``sphinx-versioned --branch="main, -v*"`` + .. note:: + + Selecting a branch will always take precedence over excluding one. + .. option:: -m , --main-branch Specify the main-branch to which the top-level ``index.html`` redirects to. Default is ``main``. @@ -72,6 +68,14 @@ These command line options must be specified when executing the ``sphinx-version Turns the version selector menu into a floating badge. Default is `False`. +.. option:: --ignore-conf + + Ignores ``conf.py`` configuration file arguments for ``sphinx-versioned-docs``. + + .. warning:: + + ``conf.py`` will still be used in sphinx! + .. option:: --quite, --no-quite Silents the output from `sphinx`. Use `--no-quite` to get complete-output from `sphinx`. Default is `True`. @@ -92,3 +96,89 @@ These command line options must be specified when executing the ``sphinx-version .. option:: --help Show the help message in command-line. + + +Configuration File Arguments +============================ + +.. warning:: + + Unfortunately, due to limitations of the current implementation, all path variables + like git-path, output path, local conf.py path cannot be select + via configuration file argument and must be specified in CLI arguments. + +.. option:: sv_project_url: + + Setting this variable will make sure that the ``Project home`` is listed on the versions selector badge/menu. + +.. option:: sv_select_branch + + Select any particular branches/tags to build. + + The branch/tag names can be separated by ``,`` or ``|`` and supports regex. + + Example: ``sv_select_branch=["main", "v2.0"]`` + + The option above will build ``main``, ``v2.0`` and will skip all others. + + .. note:: + + Selecting a branch will always take precedence over excluding one. + +.. option:: sv_exclude_branch + + Exclude any particular branches/tags from building workflow. + The branch/tag names can be specified in an array with names separated by ``,`` or ``|``. + + Example: ``sv_exclude_branch=["v1.0"]`` + + The option above will exclude ``v1.0`` and will build all others. + +.. option:: sv_main_branch + + Specify the main-branch to which the top-level ``index.html`` redirects to. Default is ``main``. + +.. option:: sv_verbose + + Passed directly to sphinx. Specify more than once for more logging in sphinx. Default is `False`. + +.. option:: sv_force_branch + + Force branch selection. Use this option to build detached head/commits. Default is `False`. + +.. option:: sv_floating_badge + + Turns the version selector menu into a floating badge. Default is `False`. + +.. option:: sv_reset_intersphinx + + Resets intersphinx mapping; acts as a patch for issue `#17 `__. Default is `False`. + +.. option:: sv_sphinx_compability + + Adds compatibility for older sphinx versions by monkey patching certain functions. Default is `False`. + +Template for ``conf.py`` +------------------------ + +.. code:: + + # conf.py + # sphinx arguments + # ... + # ... + + # sphinx-versioned-docs arguments + # This template will have a project url for `sphinx-versioned-docs` + # will exclude `v1.0` branch + # will set `main` as the main branch + # other options can be enabled, if and as requried. + sv_project_url = "https://www.github.com/devanshshukla99/sphinx-versioned-docs" + sv_select_branch = [] + sv_exclude_branch = ["-v1.0"] + sv_main_branch = "main" + sv_verbose = "" + sv_force_branch = False + sv_floating_badge = False + sv_reset_intersphinx = False + sv_sphinx_compability = False diff --git a/sphinx_versioned/__main__.py b/sphinx_versioned/__main__.py index 7a77637..e3bb836 100755 --- a/sphinx_versioned/__main__.py +++ b/sphinx_versioned/__main__.py @@ -1,12 +1,10 @@ -import re import sys import typer from loguru import logger as log from sphinx_versioned.build import VersionedDocs -from sphinx_versioned.sphinx_ import EventHandlers -from sphinx_versioned.lib import mp_sphinx_compatibility, parse_branch_selection +from sphinx_versioned.lib import parse_branch_selection app = typer.Typer(add_completion=False) @@ -61,6 +59,11 @@ def main( floating_badge: bool = typer.Option( False, "--floating-badge", "--badge", help="Turns the version selector menu into a floating badge." ), + ignore_conf: bool = typer.Option( + False, + "--ignore-conf", + help="Ignores conf.py configuration file arguments for sphinx-versioned-docs. Warning: conf.py will still be used in sphinx!", + ), quite: bool = typer.Option( True, help="Silent `sphinx`. Use `--no-quite` to get build output from `sphinx`." ), @@ -73,7 +76,7 @@ def main( loglevel: str = typer.Option( "info", "-log", "--log", help="Provide logging level. Example --log debug, default=info" ), - force_branches: bool = typer.Option( + force_branch: bool = typer.Option( False, "--force", help="Force branch selection. Use this option to build detached head/commits. [Default: False]", @@ -104,51 +107,54 @@ def main( Main branch to which the top-level `index.html` redirects to. [Default = 'main'] floating_badge : :class:`bool` Turns the version selector menu into a floating badge. [Default = `False`] + ignore_conf : :class:`bool` + Ignores conf.py configuration file arguments for sphinx-versioned-docs. Warning: conf.py will still be used in sphinx! quite : :class:`bool` Quite output from `sphinx`. Use `--no-quite` to get output from `sphinx`. [Default = `True`] verbose : :class:`bool` Passed directly to sphinx. Specify more than once for more logging in sphinx. [Default = `False`] loglevel : :class:`str` Provide logging level. Example `--log` debug, [Default='info'] - force_branches : :class:`str` + force_branch : :class:`bool` Force branch selection. Use this option to build detached head/commits. [Default = `False`] Returns ------- :class:`sphinx_versioned.build.VersionedDocs` """ - logger_format = "| {level: <8} | - {message}" - + # Logger init log.remove() + logger_format = "| {level: <8} | - {message}" log.add(sys.stderr, format=logger_format, level=loglevel.upper()) - select_branches, exclude_branches = parse_branch_selection(branches) - - EventHandlers.RESET_INTERSPHINX_MAPPING = reset_intersphinx_mapping - EventHandlers.FLYOUT_FLOATING_BADGE = floating_badge - - if reset_intersphinx_mapping: - log.warning("Forcing --no-prebuild") - prebuild = False + # Parse --branch into either select/exclude variables + select_branch, exclude_branch = parse_branch_selection(branches) - if sphinx_compatibility: - mp_sphinx_compatibility() + config = { + "quite": quite, + "verbose": verbose, + "prebuild": prebuild, + "main_branch": main_branch, + "force_branch": force_branch, + "select_branch": select_branch, + "exclude_branch": exclude_branch, + "floating_badge": floating_badge, + "sphinx_compatibility": sphinx_compatibility, + "reset_intersphinx_mapping": reset_intersphinx_mapping, + } + # Filtered config dict, containing only variables which are `True` + filtered_config = {x: y for x, y in config.items() if y} - return VersionedDocs( - { - "chdir": chdir, - "output_dir": output_dir, - "git_root": git_root, - "local_conf": local_conf, - "prebuild_branches": prebuild, - "select_branches": select_branches, - "exclude_branches": exclude_branches, - "main_branch": main_branch, - "quite": quite, - "verbose": verbose, - "force_branches": force_branches, - } + # VersionedDocs instance + DocsBuilder = VersionedDocs( + chdir=chdir, + git_root=git_root, + local_conf=local_conf, + output_dir=output_dir, + config=filtered_config, + ignore_conf=ignore_conf, ) + return DocsBuilder.run() if __name__ == "__main__": diff --git a/sphinx_versioned/build.py b/sphinx_versioned/build.py index 450af75..b02a1d4 100644 --- a/sphinx_versioned/build.py +++ b/sphinx_versioned/build.py @@ -3,13 +3,14 @@ import shutil import pathlib from sphinx import application +from sphinx.config import Config 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.lib import TempDir, ConfigInject, mp_sphinx_compatibility from sphinx_versioned.versions import GitVersions, BuiltVersions, PseudoBranch @@ -24,13 +25,44 @@ class VersionedDocs: Parameters ---------- + chdir : :class:`str` + chdir location + local_conf : :class:`str` + Location for sphinx `conf.py`. + output_dir : :class:`str` + Documentation output directory. + git_root : :class:`str` + If git root differs from chdir/CWD, that location can be supplied via this variable. + ignore_conf : :class:`bool` + Ignores conf.py configuration file arguments for sphinx-versioned-docs. config : :class:`dict` + CLI configuration arguments. """ - def __init__(self, config: dict, debug: bool = False) -> None: - self.config = config - self._parse_config(config) - self._handle_paths() + def __init__( + self, + chdir: str, + output_dir: str, + git_root: str, + local_conf: str, + config: dict, + ignore_conf: bool = False, + debug: bool = False, + ) -> None: + if chdir: + log.debug(f"chdir: {chdir}") + os.chdir(chdir) + + self.local_conf = pathlib.Path(local_conf) + self.output_dir = pathlib.Path(output_dir) + self.git_root = git_root + + self._raw_cli_config = config + self.ignore_conf = ignore_conf + + # Read sphinx-conf.py variables + self.read_conf() + self.configure_conf() self._versions_to_pre_build = [] self._versions_to_build = [] @@ -43,11 +75,11 @@ def __init__(self, config: dict, debug: bool = False) -> None: 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 + if not self.config.get("main_branch"): + if self.config.get("force_branch"): + self.config["main_branch"] = self.versions.active_branch.name else: - self.main_branch = "main" + self.config["main_branch"] = "main" if debug: return @@ -65,46 +97,76 @@ def __init__(self, config: dict, debug: bool = False) -> None: 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 - - 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) - + def read_conf(self) -> bool: + """Read and parse `conf.py`, CLI arugments to make a combined master config.""" if self.local_conf.name != "conf.py": self.local_conf = self.local_conf / "conf.py" + # If default conf.py location fails 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") + + if self.ignore_conf: + self.config = self._raw_cli_config + return + + # Parse sphinx config file i.e. conf.py + self._sphinx_conf = Config.read(self.local_conf.parent.absolute()) + sv_conf_values = { + x.replace("sv_", ""): y for x, y in self._sphinx_conf._raw_config.items() if x.startswith("sv_") + } + log.debug(f"Configuration file arugments: {sv_conf_values}") + + # Make a master config variable + self.config = {**sv_conf_values, **self._raw_cli_config} + log.debug(f"master config: {self.config}") return - def _select_branches(self) -> None: - if not self.select_branches: + def configure_conf(self) -> None: + # Initialize GitVersions instance + self.versions = GitVersions(self.git_root, self.output_dir, self.config.get("force_branch")) + + if self.config.get("floating_badge"): + EventHandlers.FLYOUT_FLOATING_BADGE = True + + if self.config.get("reset_intersphinx_mapping"): + EventHandlers.RESET_INTERSPHINX_MAPPING = True + log.warning("forcing --no-prebuild") + self.config["prebuild"] = False + + if self.config.get("sphinx_compatibility"): + mp_sphinx_compatibility() + + # Set additional config for sphinx + self._additional_args = () + self._additional_args += ("-Q",) if self.config.get("quite") else () + self._additional_args += ("-vv",) if self.config.get("verbose") else () + return + + def _select_exclude_branches(self) -> list: + log.debug(f"Instructions to select: `{self.config.get('select_branch')}`") + log.debug(f"Instructions to exclude: `{self.config.get('exclude_branch')}`") + self._versions_to_pre_build = [] + + self._select_branch() + + log.info(f"selected branches: `{[x.name for x in self._versions_to_pre_build]}`") + return + + def _select_branch(self) -> None: + if not self.config.get("select_branch"): self._versions_to_pre_build = self._all_branches + self._exclude_branch() return - for tag in self.select_branches: + for tag in self.config.get("select_branch"): 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: + elif self.config.get("force_branch"): 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: @@ -112,41 +174,30 @@ def _select_branches(self) -> None: return - def _exclude_branches(self) -> None: - if not self.exclude_branches: + def _exclude_branch(self) -> None: + if not self.config.get("exclude_branch"): return _names_versions_to_pre_build = [x.name for x in self._versions_to_pre_build] - for tag in self.exclude_branches: + for tag in self.config.get("exclude_branch"): 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 - 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]: + if self.config.get("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}`; " + f"main branch `{self.config.get('main_branch')}` not found!! / not building `{self.config.get('main_branch')}`; " "top-level `index.html` will not be generated!" ) return - log.success(f"main branch: `{self.main_branch}`; generating top-level `index.html`") + log.success(f"main branch: `{self.config.get('main_branch')}`; generating top-level `index.html`") with open(self.output_dir / "index.html", "w") as findex: findex.write( f""" @@ -154,7 +205,7 @@ def _generate_top_level_index(self) -> None: + {self.config.get("main_branch")}/index.html" /> """ ) @@ -212,7 +263,7 @@ def prebuild(self) -> None: The method carries out the transaction via the internal build method :meth:`~sphinx_versioned.build.VersionedDocs._build`. """ - if not self.prebuild_branches: + if not self.config.get("prebuild"): log.info("No pre-builing...") self._versions_to_build = self._versions_to_pre_build return @@ -231,12 +282,12 @@ def prebuild(self) -> None: log.critical(f"Pre-build failed for {tag}") finally: # restore to active branch - self.versions.checkout(self._active_branch.name) + self.versions.checkout(self._active_branch) log.success(f"Prebuilding successful for {', '.join([x.name for x in self._versions_to_build])}") return - def build(self) -> None: + def build(self) -> bool: """Build workflow. Method to build the branch in a temporary directory with the modified @@ -258,11 +309,28 @@ def build(self) -> None: self._build(tag.name) self._built_version.append(tag) except SphinxError: - log.error(f"build failed for {tag}") - exit(-1) + log.error(f"Build failed for {tag}") + return False finally: # restore to active branch self.versions.checkout(self._active_branch) + return True + + def run(self) -> bool: + # Prebuild, but returns if `self.config["prebuild"]` is `False` + self.prebuild() + + # Adds our extension to the sphinx-config + application.Config = ConfigInject + + if 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 + + log.critical(f"Build failed.") return pass diff --git a/sphinx_versioned/versions.py b/sphinx_versioned/versions.py index 7ac85b2..ef8bcc0 100644 --- a/sphinx_versioned/versions.py +++ b/sphinx_versioned/versions.py @@ -69,15 +69,15 @@ class GitVersions(_BranchTag): Git repository root directory. build_directory : :class:`str` Path of build directory. - force_branches : :class:`bool` + force_branch : :class:`bool` This option allows `GitVersions` to treat the detached commits as normal branches. Use this option to build docs for detached head/commits. """ - def __init__(self, git_root: str, build_directory: str, force_branches: bool) -> None: + def __init__(self, git_root: str, build_directory: str, force_branch: bool) -> None: self.git_root = git_root self.build_directory = pathlib.Path(build_directory) - self.force_branches = force_branches + self.force_branch = force_branch # for detached head self._active_branch = None @@ -116,7 +116,7 @@ def _parse_branches(self) -> bool: # check if if the current git status is detached, if yes, and if `--force` is supplied -> append: if self.repo.head.is_detached: log.warning(f"git head detached {self.repo.head.is_detached}") - if self.force_branches: + if self.force_branch: log.debug("Forcing detached commit into PseudoBranch") self.all_versions.append(PseudoBranch(self.repo.head.object.hexsha)) diff --git a/tests/test_branch_selection.py b/tests/test_branch_selection.py index f3b970b..0c1f13e 100644 --- a/tests/test_branch_selection.py +++ b/tests/test_branch_selection.py @@ -43,17 +43,17 @@ def test_parse_branch_selection_regex(branches, select, exclude): parsed_select, parsed_exclude = parse_branch_selection(branches) ver = VersionedDocs( - { - "chdir": ".", - "output_dir": OUTPATH, - "git_root": BASEPATH.parent, - "local_conf": "docs/conf.py", - "select_branches": parsed_select, - "exclude_branches": parsed_exclude, - "main_branch": "main", + chdir=".", + output_dir=OUTPATH, + git_root=BASEPATH.parent, + local_conf="docs/conf.py", + config={ "quite": False, "verbose": True, - "force_branches": True, + "main_branch": "main", + "force_branch": True, + "select_branch": parsed_select, + "exclude_branch": parsed_exclude, }, debug=True, )