pax_global_header00006660000000000000000000000064137150063600014513gustar00rootroot0000000000000052 comment=7215c4f7285b1326e09ab9d7294e02a8debe78ac sphinx-multiversion-0.2.4/000077500000000000000000000000001371500636000155655ustar00rootroot00000000000000sphinx-multiversion-0.2.4/.flake8000066400000000000000000000003461371500636000167430ustar00rootroot00000000000000[flake8] # E203 is not PEP8 compliant and is not compatible with black: # https://black.readthedocs.io/en/stable/the_black_code_style.html#slices # The same goes of E231 and W503 conflicts with black, too. ignore = E203,E231,W503 sphinx-multiversion-0.2.4/.pre-commit-config.yaml000066400000000000000000000017661371500636000220600ustar00rootroot00000000000000repos: - repo: https://github.com/pre-commit/pre-commit-hooks rev: v2.3.0 hooks: - id: trailing-whitespace - id: flake8 - id: check-merge-conflict - id: check-yaml - id: check-executables-have-shebangs - id: mixed-line-ending - repo: https://github.com/psf/black rev: stable hooks: - id: black - repo: local hooks: - id: version-check name: version-check description: "Check if version is consistent in all source files" entry: .pre-commit/version_check.py pass_filenames: false stages: - commit - manual language: python files: ^(\.pre-commit/version_check\.py|setup\.py|sphinx_multiversion/__init__\.py|docs/conf\.py|docs/changelog\.rst)$ additional_dependencies: - sphinx - id: unittest name: unittest description: "Run unittests" entry: python -m unittest discover pass_filenames: false language: python types: - python stages: - commit - manual additional_dependencies: - sphinx sphinx-multiversion-0.2.4/.pre-commit/000077500000000000000000000000001371500636000177175ustar00rootroot00000000000000sphinx-multiversion-0.2.4/.pre-commit/version_check.py000077500000000000000000000123451371500636000231230ustar00rootroot00000000000000#!/usr/bin/env python import importlib.util import os import pkgutil import re import runpy import subprocess import sys import docutils.nodes import docutils.parsers.rst import docutils.utils import docutils.frontend CHANGELOG_PATTERN = re.compile(r"^Version (\S+)((?: \(.+\)))?$") def parse_rst(text: str) -> docutils.nodes.document: parser = docutils.parsers.rst.Parser() components = (docutils.parsers.rst.Parser,) settings = docutils.frontend.OptionParser( components=components ).get_default_values() document = docutils.utils.new_document("", settings=settings) parser.parse(text, document) return document class SectionVisitor(docutils.nodes.NodeVisitor): def __init__(self, *args, **kwargs): super().__init__(*args, **kwargs) self.sectiontitles_found = [] def visit_section(self, node: docutils.nodes.section) -> None: """Called for "section" nodes.""" title = node[0] assert isinstance(title, docutils.nodes.title) self.sectiontitles_found.append(title.astext()) def unknown_visit(self, node: docutils.nodes.Node) -> None: """Called for all other node types.""" pass def get_sphinxchangelog_version(rootdir): with open(os.path.join(rootdir, "docs", "changelog.rst"), mode="r") as f: doc = parse_rst(f.read()) visitor = SectionVisitor(doc) doc.walk(visitor) unique_sectiontitles = set(visitor.sectiontitles_found) assert len(visitor.sectiontitles_found) == len(unique_sectiontitles) assert visitor.sectiontitles_found[0] == "Changelog" matchobj = CHANGELOG_PATTERN.match(visitor.sectiontitles_found[1]) assert matchobj version = matchobj.group(1) version_date = matchobj.group(2) matchobj = CHANGELOG_PATTERN.match(visitor.sectiontitles_found[2]) assert matchobj release = matchobj.group(1) release_date = matchobj.group(2) if version_date: assert version_date == release_date return version, release def get_sphinxconfpy_version(rootdir): """Get version from Sphinx' conf.py.""" sphinx_conf = runpy.run_path(os.path.join(rootdir, "docs", "conf.py")) version, sep, bugfix = sphinx_conf["release"].rpartition(".") assert sep == "." assert bugfix assert version == sphinx_conf["version"] return sphinx_conf["version"], sphinx_conf["release"] def get_setuppy_version(rootdir): """Get version from setup.py.""" setupfile = os.path.join(rootdir, "setup.py") cmd = (sys.executable, setupfile, "--version") release = subprocess.check_output(cmd).decode().rstrip(os.linesep) version = release.rpartition(".")[0] return version, release def get_package_version(rootdir): """Get version from package __init__.py.""" sys.path.insert(0, os.path.join(rootdir)) for modinfo in pkgutil.walk_packages(path=[rootdir]): if modinfo.ispkg and modinfo.name == "sphinx_multiversion": break else: raise FileNotFoundError("package not found") spec = modinfo.module_finder.find_spec(modinfo.name) mod = importlib.util.module_from_spec(spec) spec.loader.exec_module(mod) release = mod.__version__ version = release.rpartition(".")[0] return version, release def main(): rootdir = os.path.join(os.path.dirname(__file__), "..") setuppy_version, setuppy_release = get_setuppy_version(rootdir) package_version, package_release = get_package_version(rootdir) confpy_version, confpy_release = get_sphinxconfpy_version(rootdir) changelog_version, changelog_release = get_sphinxchangelog_version(rootdir) version_head = "Version" version_width = max( ( len(repr(x)) for x in ( version_head, setuppy_version, package_version, confpy_version, changelog_version, ) ) ) release_head = "Release" release_width = max( ( len(repr(x)) for x in ( release_head, setuppy_release, package_release, confpy_release, changelog_release, ) ) ) print( f"File {version_head} {release_head}\n" f"------------------------------- {'-' * version_width}" f" {'-' * release_width}\n" f"setup.py " f" {setuppy_version!r:>{version_width}}" f" {setuppy_release!r:>{release_width}}\n" f"sphinx_multiversion/__init__.py" f" {package_version!r:>{version_width}}" f" {package_release!r:>{release_width}}\n" f"docs/conf.py " f" {confpy_version!r:>{version_width}}" f" {confpy_release!r:>{release_width}}\n" f"docs/changelog.rst " f" {changelog_version!r:>{version_width}}" f" {changelog_release!r:>{release_width}}\n" ) assert setuppy_version == confpy_version assert setuppy_version == package_version assert setuppy_version == changelog_version assert setuppy_release == confpy_release assert setuppy_release == package_release assert setuppy_release == changelog_release if __name__ == "__main__": main() sphinx-multiversion-0.2.4/.travis.yml000066400000000000000000000072161371500636000177040ustar00rootroot00000000000000os: linux dist: xenial language: python jobs: include: - name: "Ubuntu / Python 3.6" stage: Testing python: 3.6 - name: "Ubuntu / Python 3.7" stage: Testing python: 3.7 - name: "Windows / Python 3.6" stage: Testing os: windows language: shell before_install: - choco install python --version 3.6.8 env: - PATH=/c/Python36:/c/Python36/Scripts:$PATH - SKIP=check-executables-have-shebangs - name: "Windows / Python 3.7" stage: Testing os: windows language: shell before_install: - choco install python --version 3.7.8 env: - PATH=/c/Python37:/c/Python37/Scripts:$PATH - SKIP=check-executables-have-shebangs - name: "Ubuntu / Python 3.7" stage: Deployment if: type != pull_request AND (branch = master OR tag IS present) python: 3.7 before_deploy: - touch html/.nojekyll - cp assets/gh-pages-redirect.html html/index.html deploy: # Deploy to PyPI - provider: pypi username: "__token__" password: secure: "0bFhY80KT5zp5wpsQwmBdHNvrC1aXnCUnL268UBb5bcVrTQ/cVr08CPVEZLefRU96CCqCZHCQPh1iP9DCtPEszB3XI0pVydoNJ9Fei9ccHI6bJBtfpLnTwlyygiBiTT3uNlal+/+videWqhDXAs2EHZAQn4L41Na6h1pHBoTu+V8KIJinNJccgiUXeeHdDc4lWCVKU0GfeKtzvgmFZq9B1w7jo+VnOQFKYH3rS80u1xkTrWNiVb3vdt+36PHssNXv5XI9kULsE+ofSdKIwYd7zGxs3ouw+LYhEM6QpDp1ERITAPWhdrY6DTTVdatnClTlzvxRMiqbeKmx/cYIx1x2aXqGxQo0UIcjK2N647ZG6lQAmJnRBc71CJvzGOFQQ+PNnlRlUcOmhnqr8K4bHDhe6Cf1PAFW6SCZvZ8ruXRV726FhRqMzmE/fJwtOY97F0BHJWwBAjbTdDM0J9Hph68YjkMcB96/bRTvGqkwVZTVqPtGppa3uRX2+ljt9pFY3j/jsk77mTYoY5VJrDB8lQSK7dzn8U/vg6q7lbRA5QfYXVIc77/7w29BFgVKv418LlfEh9zlq2/mB0+EX9BGEtEFNMEDzmXvJpEMQ2isDhfACKLO6B8opkfNEgUKhEpUBOTKf0VNSTVdQy1VL+jnXk7SzHo6ncb7PCMMK/yWmlCsvc=" distributions: "sdist bdist_wheel" skip_existing: true on: branch: master repo: Holzhaus/sphinx-multiversion tags: true # Deploy to Github Releases - provider: releases token: secure: "tAwp3NTqbvig1wU0hwkN7aWOAQYnpS12zsAlBc/ud05cYrjrBIjjFltX/1C0tgxxxJ11zdAhTjgUfYFrWbRXJ5hHptyD8fTa8RXfJZ2KuTx44aEHSyNHtl8YbwlXXr9hGjhrUHUulNghpFCAwNgg6oONbocQ3teW6maO7ByLUmO/RbNnligK1iQW6GPHovOg1iTbsZLVhriP2e2/JFRxq5rdnxArNLzwLhg8L4QAuc6YX3gnI1Z2xuBtpgQhNfL5S+hMuKOwONpdMQDYgXD7QbPA0pqSYQRf81vd+/FtZqNnHHku0POsoLLMkscalXxa1AbznvH9vg9nwO0J20XNZ1Mg6BFLpDwPzqLhCAwmeFcz+k78m+3x9bIhS27ps0XCgg0VxjM4Rvaabfm7J9h1aIteZf4oGS+1Er8fK6ejOeGI+tZu/5YXDhpS6hyK6gXey7Ep3SGF5oqrcu80gBrCGkWXrADMogarL6RbcZV2vrYkm5kBorcUA4s3nU2fV5UUnCg83fdlY70kmrNt3Im83ndSH3jOyTDXITjHCDN6+GFjd1eP0h8tcwiGPtVs2JGolSmqq/AzjDqY2CfgwccBDg6MF4gbcgJQdzZ11j/5cH9SNoHN6/V4869vO28XbwTKqbgsKpuajmbKVEqIlGTi2zylgzNbxtySTL0tnZvyO4w=" name: "$TRAVIS_TAG" release_notes: "Release $TRAVIS_TAG" draft: false skip_cleanup: true file_glob: true file: dist/* overwrite: true on: branch: master repo: Holzhaus/sphinx-multiversion tags: true # Deploy documentation - provider: pages skip_cleanup: true keep_history: false token: $GITHUB_TOKEN local_dir: html on: branch: master repo: Holzhaus/sphinx-multiversion stages: - Testing - Deployment cache: pip: true install: - python -m pip install pre-commit setuptools wheel - python -m pip install -r requirements.txt - python setup.py install before_script: - pre-commit run --all-files --show-diff-on-failure - python -m unittest --verbose script: - mkdir html - git fetch --all - python -I -m sphinx_multiversion -W docs html - python setup.py build sdist bdist_wheel sphinx-multiversion-0.2.4/LICENSE000066400000000000000000000025141371500636000165740ustar00rootroot00000000000000BSD 2-Clause License Copyright (c) 2020, Jan Holthuis All rights reserved. Redistribution and use in source and binary forms, with or without modification, are permitted provided that the following conditions are met: 1. Redistributions of source code must retain the above copyright notice, this list of conditions and the following disclaimer. 2. Redistributions in binary form must reproduce the above copyright notice, this list of conditions and the following disclaimer in the documentation and/or other materials provided with the distribution. THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. sphinx-multiversion-0.2.4/README.md000066400000000000000000000006531371500636000170500ustar00rootroot00000000000000# sphinx-multiversion [![Build Status](https://travis-ci.org/Holzhaus/sphinx-multiversion.svg?branch=master)](https://travis-ci.org/Holzhaus/sphinx-multiversion) Sphinx extension for building self-hosted versioned docs. This extension aims to provide a clean implementation that tries to avoid messing with Sphinx internals as much as possible. Documentation can be found at: https://holzhaus.github.io/sphinx-multiversion/ sphinx-multiversion-0.2.4/assets/000077500000000000000000000000001371500636000170675ustar00rootroot00000000000000sphinx-multiversion-0.2.4/assets/gh-pages-redirect.html000066400000000000000000000005731371500636000232540ustar00rootroot00000000000000 Redirecting to https://holzhaus.github.io/sphinx-multiversion/master/ sphinx-multiversion-0.2.4/docs/000077500000000000000000000000001371500636000165155ustar00rootroot00000000000000sphinx-multiversion-0.2.4/docs/_templates/000077500000000000000000000000001371500636000206525ustar00rootroot00000000000000sphinx-multiversion-0.2.4/docs/_templates/versioning.html000066400000000000000000000005041371500636000237220ustar00rootroot00000000000000{% if versions %}

{{ _('Branches') }}

{{ _('Tags') }}

{% endif %} sphinx-multiversion-0.2.4/docs/changelog.rst000066400000000000000000000104351371500636000212010ustar00rootroot00000000000000.. _changelog: ========= Changelog ========= Version 0.2 =========== Version 0.2.4 (2020-08-12) -------------------------- * Skip file existence check for the :file:`.` directory. This fixes an issue if the configuration or source directory is in the local path but reported as missing, because ``git cat-file -e`` always reports an error in that case. (`#12 `_) * Fix file existence check not working on Windows. (`#18 `_, `#19 `_) * Fix bug in the sphinx extension which tried to load the `conf.py` from the source directory instead of the conf directory. This could lead to problems when the two directories differ. (`#11 `_, `#13 `_) * Fix wrong import in :file:`__main__.py` that prevented invocation using ``python -m sphinx_multiversion``. (`#23 `_) * Fix failure to find refs if ``sphinx-multiversion`` was not invoked from the root of the git repository. (`#24 `_, `#25 `_, `#26 `_) * Resolve issues with Sphinx extensions and Python modules not being reloaded when parsing the different :file:`conf.py` files. Now, each config file is parsed in it's own process, and the build is performed using the ``subprocess`` module instead of doing it all from the context of the main module. Python's `interpreter flags `_ (e.g. isolated mode) are passed through to the subprocesses. (`#22 `_, `#28 `_, `#30 `_, `#36 `_) * Rewrite the path handling of the Sphinx extension to handle branch names containing a forward slash properly on Windows and add unittests and Windows CI builds to make sure it doesn't break on future updates. (`#31 `_, `#35 `_) Version 0.2.3 (2020-05-04) -------------------------- * Fixed return codes of main() function and exit with non-zero status if no matching refs were found. * Added some logging calls to the git module. * Fixed bug where local branch was used to check the existence of files on remote branches. Version 0.2.2 (2020-05-01) -------------------------- * Added additional checks to determine if a branch or tag contains both the Sphinx source directory and the :file:`conf.py` file. If that's not the case, that branch or tag is skipped automatically and not copied to the temporary directory. (`#9 `_) Version 0.2.1 (2020-04-19) -------------------------- * Fixed handling of absolute output paths in `vpathto` and ensure that all generated paths are relative. Version 0.2.0 (2020-04-19) -------------------------- * Added a way to override config variables using placeholders that expand to each version's actual value (`#4 `_, `#7 `_). Version 0.1 =========== Version 0.1.1 (2020-03-12) -------------------------- * Fixed version number in documentation * Fixed issue that caused the wrong configuration directory being used when the ``-c`` argument was not specified on the command line Version 0.1.0 (2020-03-11) -------------------------- * Initial release .. _issue4: https://github.com/Holzhaus/sphinx-multiversion/issues/4 .. _issue7: https://github.com/Holzhaus/sphinx-multiversion/issues/7 .. _issue9: https://github.com/Holzhaus/sphinx-multiversion/issues/9 .. _issue11: https://github.com/Holzhaus/sphinx-multiversion/issues/11 .. _issue12: https://github.com/Holzhaus/sphinx-multiversion/issues/12 .. _issue13: https://github.com/Holzhaus/sphinx-multiversion/issues/13 .. _issue18: https://github.com/Holzhaus/sphinx-multiversion/issues/18 .. _issue19: https://github.com/Holzhaus/sphinx-multiversion/issues/19 .. _issue22: https://github.com/Holzhaus/sphinx-multiversion/issues/22 .. _issue23: https://github.com/Holzhaus/sphinx-multiversion/issues/23 .. _issue24: https://github.com/Holzhaus/sphinx-multiversion/issues/24 .. _issue25: https://github.com/Holzhaus/sphinx-multiversion/issues/25 .. _issue26: https://github.com/Holzhaus/sphinx-multiversion/issues/26 .. _issue28: https://github.com/Holzhaus/sphinx-multiversion/issues/28 .. _issue30: https://github.com/Holzhaus/sphinx-multiversion/issues/30 .. _issue31: https://github.com/Holzhaus/sphinx-multiversion/issues/31 .. _issue35: https://github.com/Holzhaus/sphinx-multiversion/issues/35 .. _issue36: https://github.com/Holzhaus/sphinx-multiversion/issues/36 .. _pythonflags: https://docs.python.org/3/using/cmdline.html#miscellaneous-options sphinx-multiversion-0.2.4/docs/conf.py000066400000000000000000000015671371500636000200250ustar00rootroot00000000000000# -*- coding: utf-8 -*- """Sphinx configuration file.""" import time author = "Jan Holthuis" project = "sphinx-multiversion" release = "0.2.4" version = "0.2" copyright = "{}, {}".format(time.strftime("%Y"), author) html_theme = "alabaster" html_theme_options = { "github_repo": "sphinx-multiversion", "github_user": "Holzhaus", "github_banner": True, "github_button": True, "travis_button": True, "show_relbar_bottom": True, } html_last_updated_fmt = "%c" master_doc = "index" pygments_style = "friendly" templates_path = ["_templates"] extensions = [ "sphinx_multiversion", ] templates_path = [ "_templates", ] html_sidebars = { "**": [ "about.html", "navigation.html", "relations.html", "searchbox.html", "versioning.html", ], } smv_remote_whitelist = r"^origin$" smv_branch_whitelist = r"^master$" sphinx-multiversion-0.2.4/docs/configuration.rst000066400000000000000000000126741371500636000221300ustar00rootroot00000000000000.. _configuration: ============= Configuration ============= ``sphinx-multiversion`` reads your Sphinx :file:`conf.py` file for configuration. As usual, you can also override certain options by using ``-D var=value`` on the command line. This is what the default configuration looks like: .. code-block:: python # Whitelist pattern for tags (set to None to ignore all tags) smv_tag_whitelist = r'^.*$' # Whitelist pattern for branches (set to None to ignore all branches) smv_branch_whitelist = r'^.*$' # Whitelist pattern for remotes (set to None to use local branches only) smv_remote_whitelist = None # Pattern for released versions smv_released_pattern = r'^tags/.*$' # Format for versioned output directories inside the build directory smv_outputdir_format = '{ref.name}' # Determines whether remote or local git branches/tags are preferred if their output dirs conflict smv_prefer_remote_refs = False You can override all of these values inside your :file:`conf.py`. .. note:: You can check which tags/branches are matched by running ``sphinx-multiversion`` with the ``--dump-metadata`` flag. Branches or tags that don't contain both the sphinx source directory and the :file:`conf.py` file will be skipped automatically. Tag/Branch/Remote whitelists ============================ Tags, Branches and Remotes are included by `Regular Expressions `_. Here are some examples: .. code-block:: python smv_tag_whitelist = r'^.*$' # Include all tags smv_tag_whitelist = r'^v\d+\.\d+$' # Include tags like "v2.1" smv_branch_whitelist = r'^.*$' # Include all branches smv_branch_whitelist = r'^(?!master).*$' # Include all branches except "master" smv_remote_whitelist = None # Only use local branches smv_remote_whitelist = r'^.*$' # Use branches from all remotes smv_remote_whitelist = r'^(origin|upstream)$' # Use branches from origin and upstream .. note:: To list values to match, you can use ``git branch``, ``git tag`` and ``git remote``. Release Pattern =============== A Regular Expression is used to determine if a version of the documentation has been released or if it's a development version. To allow more flexibility, the regex is evaluated over the full refname. Here are some examples: .. code-block:: python smv_released_pattern = r'^tags/.*$' # Tags only smv_released_pattern = r'^heads/\d+\.\d+$' # Branches like "2.1" smv_released_pattern = r'^(tags/.*|heads/\d+\.\d+)$' # Branches like "2.1" and all tags smv_released_pattern = r'^(heads|remotes/[^/]+)/(?!:master).*$' # Everything except master branch .. note:: To list all refnames , you can use: .. code-block:: bash git for-each-ref --format "%(refname)" | sed 's/^refs\///g' Output Directory Format ======================= Each version will be built into a seperate subdirectory of the Sphinx output directory. The ``smv_outputdir_format`` setting determines the directory structure for the subdirectories. It is a new-style Python formatting string with two parameters - ``ref`` and ``config``. Here are some examples: .. code-block:: python smv_outputdir_format = '{ref.name}' # Use the branch/tag name smv_outputdir_format = '{ref.commit}' # Use the commit hash smv_outputdir_format = '{ref.commit:.7s}' # Use the commit hash truncated to 7 characters smv_outputdir_format = '{ref.refname}' # Use the full refname smv_outputdir_format = '{ref.source}/{ref.name}' # Equivalent to the previous example smv_outputdir_format = 'versions/{config.release}' # Use "versions" as parent directory and the "release" variable from conf.py smv_outputdir_format = '{config.version}/{ref.name}' # Use the version from conf.py as parent directory and the branch/tag name as subdirectory .. seealso:: Have a look at `PyFormat `_ for information how to use new-stye Python formatting. Overriding Configuration Variables ================================== You can override configuration variables the same way as you're used to with ``sphinx-build``. Since ``sphinx-multiversion`` copies the branch data into a temporary directory and builds them there while leaving the current working directory unchanged, relative paths in your :file:`conf.py` will refer to the path of the version *you're building from*, not the path of the version you are trying to build documentation for. Sometimes it might be necessary to override the configured path via a command line overide. ``sphinx-multiversion`` allows you to insert placeholders into your override strings that will automatically be replaced with the correct value for the version you're building the documentation for. Here's an example for the `exhale extension `_: .. code-block:: python sphinx-multiversion docs build/html -D 'exhale_args.containmentFolder=${sourcedir}/api' .. note:: Make sure to enclose the override string in single quotes (``'``) to prevent the shell from treating it as an environment variable and replacing it before it's passed to ``sphinx-multiversion``. .. note:: To see a list of available placeholder names and their values for each version you can use the ``--dump-metadata`` flag. .. _python_regex: https://docs.python.org/3/howto/regex.html .. _python_format: https://pyformat.info/ .. _exhale: https://exhale.readthedocs.io/en/latest/ sphinx-multiversion-0.2.4/docs/context.rst000066400000000000000000000113471371500636000207410ustar00rootroot00000000000000.. _context: ============ HTML Context ============ The following variables and functions are exposed to the `Sphinx HTML builder context `_ in all versions. ``Version`` Objects =================== All versions will be exposed to the HTML context as ``Version`` objects with the following attributes: .. attribute:: name The branch or tag name. .. attribute:: url The URL to the current page in this version. .. attribute:: version The value of the ``version`` variable in ``conf.py``. .. attribute:: release The value of the ``release`` variable in ``conf.py``. .. attribute:: is_released ``True`` if this version matches the :ref:`configured ` ``smv_released_pattern`` regular expression, else ``False``. Versions ======== The most important variable is ``versions``, which can be used to iterate over all found (and whitelisted) versions. .. attribute:: versions An iterable that yields all ``Version`` objects. .. code-block:: jinja

Versions

.. attribute:: versions.branches You can use the ``branches`` property of the ``versions`` iterable to get the ``Version`` objects for all branches. .. code-block:: jinja

Branches

.. attribute:: versions.tags You can use the ``tags`` property of the ``versions`` iterable to get the ``Version`` objects for all tags. .. code-block:: jinja

Tags

.. attribute:: versions.releases You can use the ``releases`` property of the ``versions`` iterable to get all ``Version`` objects where the ``ìs_released`` attribute is ``True``. This is determined by the ``smv_released_pattern`` in the :ref:`Configuration `. .. code-block:: jinja

Releases

.. attribute:: versions.in_development You can use the ``in_development`` property of the ``versions`` iterable to get all ``Version`` objects where the ``ìs_released`` attribute is ``False``. This is determined by the ``smv_released_pattern`` in the :ref:`Configuration `. .. code-block:: jinja

In Development

Functions ========= Similar to Sphinx's `hasdoc() `_ function. .. function:: vhasdoc(other_version) This function is Similar to Sphinx's `hasdoc() `_ function. It takes ``other_version`` as string and returns ``True`` if the current document exists in another version. .. code-block:: jinja {% if vhasdoc('master') %} This page is available in master. {% endif %} .. function:: vpathto(other_version) This function is Similar to Sphinx's `pathto() `_ function. It takes ``other_version`` as string and returns the relative URL to the current page in the other version. If the current page does not exist in that version, the relative URL to its `master_doc `_ is returned instead. .. code-block:: jinja {% if vhasdoc('master') %} This page is also available in master. {% else %} Go to master for the latest docs. {% endif %} Other Variables =============== .. attribute:: current_version A ``Version`` object for of the current version being built. .. code-block:: jinja

Current Version: {{ current_version.name }}

.. attribute:: latest_version A ``Version`` object of the latest released version being built. .. code-block:: jinja

Latest Version: {{ current_version.name }}

.. _sphinx_context: http://www.sphinx-doc.org/en/stable/config.html?highlight=context#confval-html_context .. _sphinx_master_doc: http://www.sphinx-doc.org/en/stable/config.html?highlight=context#confval-master_doc .. _sphinx_hasdoc: http://www.sphinx-doc.org/en/stable/templating.html#hasdoc .. _sphinx_pathto: http://www.sphinx-doc.org/en/stable/templating.html#pathto sphinx-multiversion-0.2.4/docs/faq.rst000066400000000000000000000053561371500636000200270ustar00rootroot00000000000000.. _faq: ========================== Frequently Asked Questions ========================== Why another tool for versioning Sphinx docs? ============================================ While there are several sphinx extensions out there (e.g. `sphinxcontrib-versioning `_ or `sphinx-versions `_) none of them seem to work correctly with recent sphinx versions (as of March 2020). Their code heavily relies on monkey-patching Sphinx internals at runtime, which is error-prone and makes the code a mess. In contrast, the extension part of ``sphinx-multiversion`` does not do any fancy patching, it just provides some HTML context variables. How does it work? ================= Instead of running `sphinx build`, just run `sphinx-multiversion` from the root of your Git repository. It reads your Sphinx :file:`conf.py` file from the currently checked out Git branch for configuration, then generates a list of versions from local or remote tags and branches. This data is written to a JSON file - if you want to have a look what data will be generated, you can use the ``--dump-metadata`` flag. Then it copies the data for each version into separate temporary directories, builds the documentation from each of them and writes the output to the output directory. The :file:`conf.py` file from the currently checked out branch will be used to build old versions, so it's not necessary to make changes old branches or tags to add support for ``sphinx-multiversion``. This also means that theme improvements, template changes, etc. will automatically be applied to old versions without needing to add commits. Do I need to make changes to old branches or tags? ================================================== No, you don't. ``sphinx-multiversion`` will always use the :file:`conf.py` file from your currently checked out branch. The downside is that this behaviour restricts the kinds of changes you may do to your configuration, because it needs to retain compatibility with old branches. For example, if your :file:`conf.py` file hardcodes a path (e.g. for opening a file), but that file does not exist in some older branches that you want to build documentation for, this will cause issues. In these cases you will need to add a check if a file actually exists and adapt the path accordingly. What are the license terms of ``sphinx-multiversion``? ====================================================== ``sphinx-multiversion`` is licensed under the terms of the `BSD 2-Clause license `_. .. _sphinxcontrib_versioning: https://github.com/sphinx-contrib/sphinxcontrib-versioning .. _sphinx_versions: https://github.com/Smile-SA/sphinx-versions .. _bsd_2clause_license: https://choosealicense.com/licenses/bsd-2-clause/ sphinx-multiversion-0.2.4/docs/github_pages.rst000066400000000000000000000053201371500636000217100ustar00rootroot00000000000000.. _github_pages: ======================= Hosting on GitHub Pages ======================= You use `GitHub Pages `_ to host documentation generated by ``sphinx-multiversion``. Setting up the ``gh-pages`` Branch ================================== First, you need to create a ``gh-pages`` branch and disable Jekyll. .. code-block:: bash git checkout --orphan gh-pages touch .nojekyll git add .nojekyll git commit -m "Disable Jekyll" Then, switch back to the branch you were on and build the documentation using ``sphinx-multiversion``: .. code-block:: bash mkdir html sphinx-multiversion docs/ html/ If everything worked fine, you now need to switch back to your ``gh-pages`` branch and commit the data there: .. code-block:: bash git checkout gh-pages for dirname in html/*; do mv "html/$dirname" "$dirname" && git add "$dirname"; done git commit -m "Added HTML docs" git push origin gh-pages Now your documentation should already be online. You can navigate to ``https://username.github.io/reponame/master/`` to see the documentation for the master branch. Redirecting from the Document Root ================================== You can easily redirect users that type ``https://username.github.io/reponame/`` into their addressbar to the documentation for any version you like. Just add a :file:`index.html` file to the root directory of your ``gh-pages`` branch: .. code-block:: html Redirecting to master branch Automating documentation builds with Travis CI ============================================== You can also automate versioned builds using Travis CI. To do that, add this to your :file:`.travis.yml` file: .. code-block:: yaml script: # Build documentation - mkdir html - sphinx-multiversion docs/ html/ before_deploy: # Add .nojekyll file and redirect from docroot to the sphinx output dir - touch html/.nojekyll - cp assets/gh-pages-redirect.html html/index.html deploy: # Only deploy the sphinx output dir as gh-pages branch - provider: pages skip_cleanup: true github_token: $GITHUB_TOKEN keep_history: false local_dir: html .. seealso:: For details, please have a look at the `GitHub Pages Deployment documentation for Travis CI `_. .. _github_pages_website: https://pages.github.com/ .. _travis_gh_pages_deployment: https://docs.travis-ci.com/user/deployment/pages/ sphinx-multiversion-0.2.4/docs/index.rst000066400000000000000000000007301371500636000203560ustar00rootroot00000000000000============================= sphinx-multiversion |version| ============================= A Sphinx extension for building self-hosted versioned documentation. Project Links ============= * Source code: https://github.com/Holzhaus/sphinx-multiversion .. toctree:: :maxdepth: 3 :caption: General install quickstart configuration templates context .. toctree:: :maxdepth: 1 :caption: Appendix github_pages faq changelog sphinx-multiversion-0.2.4/docs/install.rst000066400000000000000000000016411371500636000207170ustar00rootroot00000000000000.. _install: ============ Installation ============ You can install ``sphinx-multiversion`` via `pip `_ or directly from :file:`setup.py`. Install from PyPi =================== Using pip you can easily install the latest release version `PyPI `_: .. code-block:: bash pip install sphinx-multiversion Install from GitHub =================== You can install the latest development version using pip directly from the GitHub repository: .. code-block:: bash pip install git+https://github.com/Holzhaus/sphinx-multiversion.git Install via :file:`setup.py` ============================ It's also possible to clone the Git repository and install the extension using its :file:`setup.py`: .. code-block:: bash git clone https://github.com/Holzhaus/sphinx-multiversion.git cd sphinx-multiversion python setup.py install .. _pip: https://pip.pypa.io/en/stable/ .. _pypi: https://pypi.org/ sphinx-multiversion-0.2.4/docs/quickstart.rst000066400000000000000000000045461371500636000214520ustar00rootroot00000000000000.. _quickstart: ========== Quickstart ========== After :ref:`installation `, using ``sphinx-multiversion`` should be fairly straightforward. To be able to build multiple versions of Sphinx documentation, ``sphinx-multiversion`` acts as wrapper for ``sphinx-build``. If you're already using Sphinx documentation for your project, you can now use ``sphinx-multiversion`` to build the HTML documentation. You can check if it works by running: .. code-block:: bash # Without sphinx-multiversion sphinx-build docs build/html # With sphinx-multiversion sphinx-multiversion docs build/html Don't worry - no version picker will show up in the generated HTML yet. You need to :ref:`configure ` the extension first. .. seealso:: If you're not using Sphinx yet, have a look at the `tutorial `_. Next, you need to add the extension to the :file:`conf.py` file. .. code-block:: python extensions = [ "sphinx_multiversion", ] To make the different versions show up in the HTML, you also need to add a custom template. For example, you could create a new template named :file:`versioning.html` with the following content: .. code-block:: html {% if versions %}

{{ _('Versions') }}

{% endif %} .. seealso:: You can also list branches, tags, released versions and development branches separately. See :ref:`Templates ` for details. Assuming that you're using a theme with sidebar widget support, you just need to make sure that the file is inside the ``templates_path`` and add it to the `html_sidebars `_ variable. .. code-block:: python templates_path = [ "_templates", ] html_sidebars = [ "versioning.html", ] Now rebuild the documentation: .. code-block:: bash sphinx-multiversion docs build/html Done! .. seealso:: By default, all local branches and tags will be included. If you only want to include certain branches/tags or also include remote branches, see :ref:`Configuration `. .. _sphinx_tutorial: http://www.sphinx-doc.org/en/stable/tutorial.html .. _sphinx_html_sidebars: https://www.sphinx-doc.org/en/master/usage/configuration.html#confval-html_sidebars sphinx-multiversion-0.2.4/docs/templates.rst000066400000000000000000000076251371500636000212570ustar00rootroot00000000000000.. _templates: ========= Templates ========= ``sphinx-multiversion`` does not change the look of your HTML output by default. Instead, you can customize the template to cater to your needs. Version Listings ================ To add version listings to your template, you need to add a custom template to your theme. You can take one of the snippets below, put it into :file:`_templates/versioning.html` and add it to your theme's sidebar: .. code-block:: html templates_path = [ "_templates", ] html_sidebars = [ "versioning.html", ] List all branches/tags ---------------------- .. code-block:: html {% if versions %}

{{ _('Versions') }}

{% endif %} List branches and tags separately --------------------------------- .. code-block:: html {% if versions %}

{{ _('Branches') }}

{{ _('Tags') }}

{% endif %} List releases and development versions separately ------------------------------------------------- .. code-block:: html {% if versions %}

{{ _('Releases') }}

{{ _('In Development') }}

{% endif %} Version Banners =============== You can also add version banners to your theme, for example: .. code-block:: html {% extends "page.html" %} {% block body %} {% if current_version and latest_version and current_version != latest_version %}

{% if current_version.is_released %} You're reading an old version of this documentation. If you want up-to-date information, please have a look at {{latest_version.name}}. {% else %} You're reading the documentation for a development version. For the latest released version, please have a look at {{latest_version.name}}. {% endif %}

{% endif %} {{ super() }} {% endblock %}% ReadTheDocs Theme ================= As of version 0.4.3, the `Read the Docs theme `_ does not support sidebar widgets. So instead of adding a custom template to ``html_sidebars``, you need to create a template file named :file:`versions.html` with the following content: .. code-block:: html {%- if current_version %}
Other Versions v: {{ current_version.name }}
{%- if versions.tags %}
Tags
{%- for item in versions.tags %}
{{ item.name }}
{%- endfor %}
{%- endif %} {%- if versions.branches %}
Branches
{%- for item in versions.branches %}
{{ item.name }}
{%- endfor %}
{%- endif %}
{%- endif %} .. _sphinx_rtd_theme: https://pypi.org/project/sphinx-rtd-theme/ sphinx-multiversion-0.2.4/pyproject.toml000066400000000000000000000000361371500636000205000ustar00rootroot00000000000000[tool.black] line-length = 79 sphinx-multiversion-0.2.4/requirements.txt000066400000000000000000000000071371500636000210460ustar00rootroot00000000000000sphinx sphinx-multiversion-0.2.4/setup.py000077500000000000000000000016611371500636000173060ustar00rootroot00000000000000#!/usr/bin/env python3 # -*- coding: utf-8 -*- import os.path from setuptools import setup with open(os.path.join(os.path.dirname(__file__), "README.md")) as f: readme = f.read() setup( name="sphinx-multiversion", description="Add support for multiple versions to sphinx", long_description=readme, long_description_content_type="text/markdown", classifiers=[ "License :: OSI Approved :: BSD License", "Programming Language :: Python :: 3", "Programming Language :: Python :: 3.6", "Programming Language :: Python :: 3.7", ], author="Jan Holthuis", author_email="holthuis.jan@googlemail.com", url="https://holzhaus.github.io/sphinx-multiversion/", version="0.2.4", install_requires=["sphinx >= 2.1"], license="BSD", packages=["sphinx_multiversion"], entry_points={ "console_scripts": ["sphinx-multiversion=sphinx_multiversion:main",], }, ) sphinx-multiversion-0.2.4/sphinx_multiversion/000077500000000000000000000000001371500636000217165ustar00rootroot00000000000000sphinx-multiversion-0.2.4/sphinx_multiversion/__init__.py000066400000000000000000000002101371500636000240200ustar00rootroot00000000000000# -*- coding: utf-8 -*- from .sphinx import setup from .main import main __version__ = "0.2.4" __all__ = [ "setup", "main", ] sphinx-multiversion-0.2.4/sphinx_multiversion/__main__.py000077500000000000000000000001431371500636000240110ustar00rootroot00000000000000#!/usr/bin/env python3 # -*- coding: utf-8 -*- import sys from .main import main sys.exit(main()) sphinx-multiversion-0.2.4/sphinx_multiversion/git.py000066400000000000000000000110141371500636000230500ustar00rootroot00000000000000# -*- coding: utf-8 -*- import collections import datetime import logging import os import re import subprocess import tarfile import tempfile GitRef = collections.namedtuple( "VersionRef", ["name", "commit", "source", "is_remote", "refname", "creatordate",], ) logger = logging.getLogger(__name__) def get_toplevel_path(cwd=None): cmd = ( "git", "rev-parse", "--show-toplevel", ) output = subprocess.check_output(cmd, cwd=cwd).decode() return output.rstrip("\n") def get_all_refs(gitroot): cmd = ( "git", "for-each-ref", "--format", "%(objectname)\t%(refname)\t%(creatordate:iso)", "refs", ) output = subprocess.check_output(cmd, cwd=gitroot).decode() for line in output.splitlines(): is_remote = False fields = line.strip().split("\t") if len(fields) != 3: continue commit = fields[0] refname = fields[1] creatordate = datetime.datetime.strptime( fields[2], "%Y-%m-%d %H:%M:%S %z" ) # Parse refname matchobj = re.match( r"^refs/(heads|tags|remotes/[^/]+)/(\S+)$", refname ) if not matchobj: continue source = matchobj.group(1) name = matchobj.group(2) if source.startswith("remotes/"): is_remote = True yield GitRef(name, commit, source, is_remote, refname, creatordate) def get_refs( gitroot, tag_whitelist, branch_whitelist, remote_whitelist, files=() ): for ref in get_all_refs(gitroot): if ref.source == "tags": if tag_whitelist is None or not re.match(tag_whitelist, ref.name): logger.debug( "Skipping '%s' because tag '%s' doesn't match the " "whitelist pattern", ref.refname, ref.name, ) continue elif ref.source == "heads": if branch_whitelist is None or not re.match( branch_whitelist, ref.name ): logger.debug( "Skipping '%s' because branch '%s' doesn't match the " "whitelist pattern", ref.refname, ref.name, ) continue elif ref.is_remote and remote_whitelist is not None: remote_name = ref.source.partition("/")[2] if not re.match(remote_whitelist, remote_name): logger.debug( "Skipping '%s' because remote '%s' doesn't match the " "whitelist pattern", ref.refname, remote_name, ) continue if branch_whitelist is None or not re.match( branch_whitelist, ref.name ): logger.debug( "Skipping '%s' because branch '%s' doesn't match the " "whitelist pattern", ref.refname, ref.name, ) continue else: logger.debug( "Skipping '%s' because its not a branch or tag", ref.refname ) continue missing_files = [ filename for filename in files if filename != "." and not file_exists(gitroot, ref.refname, filename) ] if missing_files: logger.debug( "Skipping '%s' because it lacks required files: %r", ref.refname, missing_files, ) continue yield ref def file_exists(gitroot, refname, filename): if os.sep != "/": # Git requires / path sep, make sure we use that filename = filename.replace(os.sep, "/") cmd = ( "git", "cat-file", "-e", "{}:{}".format(refname, filename), ) proc = subprocess.run( cmd, cwd=gitroot, stdout=subprocess.DEVNULL, stderr=subprocess.DEVNULL ) return proc.returncode == 0 def copy_tree(gitroot, src, dst, reference, sourcepath="."): with tempfile.SpooledTemporaryFile() as fp: cmd = ( "git", "archive", "--format", "tar", reference.commit, "--", sourcepath, ) subprocess.check_call(cmd, cwd=gitroot, stdout=fp) fp.seek(0) with tarfile.TarFile(fileobj=fp) as tarfp: tarfp.extractall(dst) sphinx-multiversion-0.2.4/sphinx_multiversion/main.py000066400000000000000000000236641371500636000232270ustar00rootroot00000000000000# -*- coding: utf-8 -*- import itertools import argparse import multiprocessing import contextlib import json import logging import os import pathlib import re import string import subprocess import sys import tempfile from sphinx import config as sphinx_config from sphinx import project as sphinx_project from . import sphinx from . import git @contextlib.contextmanager def working_dir(path): prev_cwd = os.getcwd() os.chdir(path) try: yield finally: os.chdir(prev_cwd) def load_sphinx_config_worker(q, confpath, confoverrides, add_defaults): try: with working_dir(confpath): current_config = sphinx_config.Config.read( confpath, confoverrides, ) if add_defaults: current_config.add( "smv_tag_whitelist", sphinx.DEFAULT_TAG_WHITELIST, "html", str ) current_config.add( "smv_branch_whitelist", sphinx.DEFAULT_TAG_WHITELIST, "html", str, ) current_config.add( "smv_remote_whitelist", sphinx.DEFAULT_REMOTE_WHITELIST, "html", str, ) current_config.add( "smv_released_pattern", sphinx.DEFAULT_RELEASED_PATTERN, "html", str, ) current_config.add( "smv_outputdir_format", sphinx.DEFAULT_OUTPUTDIR_FORMAT, "html", str, ) current_config.add("smv_prefer_remote_refs", False, "html", bool) current_config.pre_init_values() current_config.init_values() except Exception as err: q.put(err) return q.put(current_config) def load_sphinx_config(confpath, confoverrides, add_defaults=False): q = multiprocessing.Queue() proc = multiprocessing.Process( target=load_sphinx_config_worker, args=(q, confpath, confoverrides, add_defaults), ) proc.start() proc.join() result = q.get_nowait() if isinstance(result, Exception): raise result return result def get_python_flags(): if sys.flags.bytes_warning: yield "-b" if sys.flags.debug: yield "-d" if sys.flags.hash_randomization: yield "-R" if sys.flags.ignore_environment: yield "-E" if sys.flags.inspect: yield "-i" if sys.flags.isolated: yield "-I" if sys.flags.no_site: yield "-S" if sys.flags.no_user_site: yield "-s" if sys.flags.optimize: yield "-O" if sys.flags.quiet: yield "-q" if sys.flags.verbose: yield "-v" for option, value in sys._xoptions.items(): if value is True: yield from ("-X", option) else: yield from ("-X", "{}={}".format(option, value)) def main(argv=None): if not argv: argv = sys.argv[1:] parser = argparse.ArgumentParser() parser.add_argument("sourcedir", help="path to documentation source files") parser.add_argument("outputdir", help="path to output directory") parser.add_argument( "filenames", nargs="*", help="a list of specific files to rebuild. Ignored if -a is specified", ) parser.add_argument( "-c", metavar="PATH", dest="confdir", help=( "path where configuration file (conf.py) is located " "(default: same as SOURCEDIR)" ), ) parser.add_argument( "-C", action="store_true", dest="noconfig", help="use no config file at all, only -D options", ) parser.add_argument( "-D", metavar="setting=value", action="append", dest="define", default=[], help="override a setting in configuration file", ) parser.add_argument( "--dump-metadata", action="store_true", help="dump generated metadata and exit", ) args, argv = parser.parse_known_args(argv) if args.noconfig: return 1 logger = logging.getLogger(__name__) sourcedir_absolute = os.path.abspath(args.sourcedir) confdir_absolute = ( os.path.abspath(args.confdir) if args.confdir is not None else sourcedir_absolute ) # Conf-overrides confoverrides = {} for d in args.define: key, _, value = d.partition("=") confoverrides[key] = value # Parse config config = load_sphinx_config( confdir_absolute, confoverrides, add_defaults=True ) # Get relative paths to root of git repository gitroot = pathlib.Path( git.get_toplevel_path(cwd=sourcedir_absolute) ).resolve() cwd_absolute = os.path.abspath(".") cwd_relative = os.path.relpath(cwd_absolute, str(gitroot)) logger.debug("Git toplevel path: %s", str(gitroot)) sourcedir = os.path.relpath(sourcedir_absolute, str(gitroot)) logger.debug( "Source dir (relative to git toplevel path): %s", str(sourcedir) ) if args.confdir: confdir = os.path.relpath(confdir_absolute, str(gitroot)) else: confdir = sourcedir logger.debug("Conf dir (relative to git toplevel path): %s", str(confdir)) conffile = os.path.join(confdir, "conf.py") # Get git references gitrefs = git.get_refs( str(gitroot), config.smv_tag_whitelist, config.smv_branch_whitelist, config.smv_remote_whitelist, files=(sourcedir, conffile), ) # Order git refs if config.smv_prefer_remote_refs: gitrefs = sorted(gitrefs, key=lambda x: (not x.is_remote, *x)) else: gitrefs = sorted(gitrefs, key=lambda x: (x.is_remote, *x)) logger = logging.getLogger(__name__) with tempfile.TemporaryDirectory() as tmp: # Generate Metadata metadata = {} outputdirs = set() for gitref in gitrefs: # Clone Git repo repopath = os.path.join(tmp, gitref.commit) try: git.copy_tree(str(gitroot), gitroot.as_uri(), repopath, gitref) except (OSError, subprocess.CalledProcessError): logger.error( "Failed to copy git tree for %s to %s", gitref.refname, repopath, ) continue # Find config confpath = os.path.join(repopath, confdir) try: current_config = load_sphinx_config(confpath, confoverrides) except (OSError, sphinx_config.ConfigError): logger.error( "Failed load config for %s from %s", gitref.refname, confpath, ) continue # Ensure that there are not duplicate output dirs outputdir = config.smv_outputdir_format.format( ref=gitref, config=current_config, ) if outputdir in outputdirs: logger.warning( "outputdir '%s' for %s conflicts with other versions", outputdir, gitref.refname, ) continue outputdirs.add(outputdir) # Get List of files source_suffixes = current_config.source_suffix if isinstance(source_suffixes, str): source_suffixes = [current_config.source_suffix] current_sourcedir = os.path.join(repopath, sourcedir) project = sphinx_project.Project( current_sourcedir, source_suffixes ) metadata[gitref.name] = { "name": gitref.name, "version": current_config.version, "release": current_config.release, "is_released": bool( re.match(config.smv_released_pattern, gitref.refname) ), "source": gitref.source, "creatordate": gitref.creatordate.strftime(sphinx.DATE_FMT), "basedir": repopath, "sourcedir": current_sourcedir, "outputdir": os.path.join( os.path.abspath(args.outputdir), outputdir ), "confdir": confpath, "docnames": list(project.discover()), } if args.dump_metadata: print(json.dumps(metadata, indent=2)) return 0 if not metadata: logger.error("No matching refs found!") return 2 # Write Metadata metadata_path = os.path.abspath(os.path.join(tmp, "versions.json")) with open(metadata_path, mode="w") as fp: json.dump(metadata, fp, indent=2) # Run Sphinx argv.extend(["-D", "smv_metadata_path={}".format(metadata_path)]) for version_name, data in metadata.items(): os.makedirs(data["outputdir"], exist_ok=True) defines = itertools.chain( *( ("-D", string.Template(d).safe_substitute(data)) for d in args.define ) ) current_argv = argv.copy() current_argv.extend( [ *defines, "-D", "smv_current_version={}".format(version_name), "-c", confdir_absolute, data["sourcedir"], data["outputdir"], *args.filenames, ] ) logger.debug("Running sphinx-build with args: %r", current_argv) cmd = ( sys.executable, *get_python_flags(), "-m", "sphinx", *current_argv, ) current_cwd = os.path.join(data["basedir"], cwd_relative) subprocess.check_call(cmd, cwd=current_cwd) return 0 sphinx-multiversion-0.2.4/sphinx_multiversion/sphinx.py000066400000000000000000000145731371500636000236130ustar00rootroot00000000000000# -*- coding: utf-8 -*- import datetime import json import collections import logging import os import posixpath from sphinx import config as sphinx_config from sphinx.util import i18n as sphinx_i18n from sphinx.locale import _ logger = logging.getLogger(__name__) DATE_FMT = "%Y-%m-%d %H:%M:%S %z" DEFAULT_TAG_WHITELIST = r"^.*$" DEFAULT_BRANCH_WHITELIST = r"^.*$" DEFAULT_REMOTE_WHITELIST = None DEFAULT_RELEASED_PATTERN = r"^tags/.*$" DEFAULT_OUTPUTDIR_FORMAT = r"{ref.name}" Version = collections.namedtuple( "Version", ["name", "url", "version", "release", "is_released",] ) class VersionInfo: def __init__(self, app, context, metadata, current_version_name): self.app = app self.context = context self.metadata = metadata self.current_version_name = current_version_name def _dict_to_versionobj(self, v): return Version( name=v["name"], url=self.vpathto(v["name"]), version=v["version"], release=v["release"], is_released=v["is_released"], ) @property def tags(self): return [ self._dict_to_versionobj(v) for v in self.metadata.values() if v["source"] == "tags" ] @property def branches(self): return [ self._dict_to_versionobj(v) for v in self.metadata.values() if v["source"] != "tags" ] @property def releases(self): return [ self._dict_to_versionobj(v) for v in self.metadata.values() if v["is_released"] ] @property def in_development(self): return [ self._dict_to_versionobj(v) for v in self.metadata.values() if not v["is_released"] ] def __iter__(self): for item in self.tags: yield item for item in self.branches: yield item def __getitem__(self, name): v = self.metadata.get(name) if v: return self._dict_to_versionobj(v) def vhasdoc(self, other_version_name): if self.current_version_name == other_version_name: return True other_version = self.metadata[other_version_name] return self.context["pagename"] in other_version["docnames"] def vpathto(self, other_version_name): if self.current_version_name == other_version_name: return "{}.html".format( posixpath.split(self.context["pagename"])[-1] ) # Find relative outputdir paths from common output root current_version = self.metadata[self.current_version_name] other_version = self.metadata[other_version_name] current_outputroot = os.path.abspath(current_version["outputdir"]) other_outputroot = os.path.abspath(other_version["outputdir"]) outputroot = os.path.commonpath((current_outputroot, other_outputroot)) current_outputroot = os.path.relpath( current_outputroot, start=outputroot ) other_outputroot = os.path.relpath(other_outputroot, start=outputroot) # Ensure that we use POSIX separators in the path (for the HTML code) if os.sep != posixpath.sep: current_outputroot = posixpath.join( *os.path.split(current_outputroot) ) other_outputroot = posixpath.join(*os.path.split(other_outputroot)) # Find relative path to root of other_version's outputdir current_outputdir = posixpath.dirname( posixpath.join(current_outputroot, self.context["pagename"]) ) other_outputdir = posixpath.relpath( other_outputroot, start=current_outputdir ) if not self.vhasdoc(other_version_name): return posixpath.join(other_outputdir, "index.html") return posixpath.join( other_outputdir, "{}.html".format(self.context["pagename"]) ) def html_page_context(app, pagename, templatename, context, doctree): versioninfo = VersionInfo( app, context, app.config.smv_metadata, app.config.smv_current_version ) context["versions"] = versioninfo context["vhasdoc"] = versioninfo.vhasdoc context["vpathto"] = versioninfo.vpathto context["current_version"] = versioninfo[app.config.smv_current_version] context["latest_version"] = versioninfo[app.config.smv_latest_version] context["html_theme"] = app.config.html_theme def config_inited(app, config): """Update the Sphinx builder. :param sphinx.application.Sphinx app: Sphinx application object. """ if not config.smv_metadata: if not config.smv_metadata_path: return with open(config.smv_metadata_path, mode="r") as f: metadata = json.load(f) config.smv_metadata = metadata if not config.smv_current_version: return try: data = app.config.smv_metadata[config.smv_current_version] except KeyError: return app.connect("html-page-context", html_page_context) # Restore config values old_config = sphinx_config.Config.read(data["confdir"]) old_config.pre_init_values() old_config.init_values() config.version = data["version"] config.release = data["release"] config.today = old_config.today if not config.today: config.today = sphinx_i18n.format_date( format=config.today_fmt or _("%b %d, %Y"), date=datetime.datetime.strptime(data["creatordate"], DATE_FMT), language=config.language, ) def setup(app): app.add_config_value("smv_metadata", {}, "html") app.add_config_value("smv_metadata_path", "", "html") app.add_config_value("smv_current_version", "", "html") app.add_config_value("smv_latest_version", "master", "html") app.add_config_value("smv_tag_whitelist", DEFAULT_TAG_WHITELIST, "html") app.add_config_value( "smv_branch_whitelist", DEFAULT_BRANCH_WHITELIST, "html" ) app.add_config_value( "smv_remote_whitelist", DEFAULT_REMOTE_WHITELIST, "html" ) app.add_config_value( "smv_released_pattern", DEFAULT_RELEASED_PATTERN, "html" ) app.add_config_value( "smv_outputdir_format", DEFAULT_OUTPUTDIR_FORMAT, "html" ) app.connect("config-inited", config_inited) return { "version": "0.2", "parallel_read_safe": True, "parallel_write_safe": True, } sphinx-multiversion-0.2.4/tests/000077500000000000000000000000001371500636000167275ustar00rootroot00000000000000sphinx-multiversion-0.2.4/tests/__init__.py000066400000000000000000000000001371500636000210260ustar00rootroot00000000000000sphinx-multiversion-0.2.4/tests/test_sphinx.py000066400000000000000000000110231371500636000216460ustar00rootroot00000000000000import os.path import posixpath import tempfile import unittest import sphinx_multiversion class VersionInfoTestCase(unittest.TestCase): def setUp(self): root = tempfile.gettempdir() self.versioninfo = sphinx_multiversion.sphinx.VersionInfo( app=None, context={"pagename": "testpage"}, metadata={ "master": { "name": "master", "version": "", "release": "0.2", "is_released": False, "source": "heads", "creatordate": "2020-08-07 07:45:20 -0700", "basedir": os.path.join(root, "master"), "sourcedir": os.path.join(root, "master", "docs"), "outputdir": os.path.join(root, "build", "html", "master"), "confdir": os.path.join(root, "master", "docs"), "docnames": ["testpage", "appendix/faq"], }, "v0.1.0": { "name": "v0.1.0", "version": "", "release": "0.1.0", "is_released": True, "source": "tags", "creatordate": "2020-07-16 08:45:20 -0100", "basedir": os.path.join(root, "v0.1.0"), "sourcedir": os.path.join(root, "v0.1.0", "docs"), "outputdir": os.path.join(root, "build", "html", "v0.1.0"), "confdir": os.path.join(root, "v0.1.0", "docs"), "docnames": ["old_testpage", "appendix/faq"], }, "branch-with/slash": { "name": "branch-with/slash", "version": "", "release": "0.1.1", "is_released": False, "source": "heads", "creatordate": "2020-08-06 11:53:06 -0400", "basedir": os.path.join(root, "branch-with/slash"), "sourcedir": os.path.join( root, "branch-with/slash", "docs" ), "outputdir": os.path.join( root, "build", "html", "branch-with/slash" ), "confdir": os.path.join(root, "branch-with/slash", "docs"), "docnames": ["testpage"], }, }, current_version_name="master", ) def test_tags_property(self): versions = self.versioninfo.tags self.assertEqual([version.name for version in versions], ["v0.1.0"]) def test_branches_property(self): versions = self.versioninfo.branches self.assertEqual( [version.name for version in versions], ["master", "branch-with/slash"], ) def test_releases_property(self): versions = self.versioninfo.releases self.assertEqual([version.name for version in versions], ["v0.1.0"]) def test_in_development_property(self): versions = self.versioninfo.in_development self.assertEqual( [version.name for version in versions], ["master", "branch-with/slash"], ) def test_vhasdoc(self): self.assertTrue(self.versioninfo.vhasdoc("master")) self.assertFalse(self.versioninfo.vhasdoc("v0.1.0")) self.assertTrue(self.versioninfo.vhasdoc("branch-with/slash")) self.versioninfo.context["pagename"] = "appendix/faq" self.assertTrue(self.versioninfo.vhasdoc("master")) self.assertTrue(self.versioninfo.vhasdoc("v0.1.0")) self.assertFalse(self.versioninfo.vhasdoc("branch-with/slash")) def test_vpathto(self): self.assertEqual(self.versioninfo.vpathto("master"), "testpage.html") self.assertEqual( self.versioninfo.vpathto("v0.1.0"), posixpath.join("..", "v0.1.0", "index.html"), ) self.assertEqual( self.versioninfo.vpathto("branch-with/slash"), posixpath.join("..", "branch-with/slash", "testpage.html"), ) self.versioninfo.context["pagename"] = "appendix/faq" self.assertEqual(self.versioninfo.vpathto("master"), "faq.html") self.assertEqual( self.versioninfo.vpathto("v0.1.0"), posixpath.join("..", "..", "v0.1.0", "appendix", "faq.html"), ) self.assertEqual( self.versioninfo.vpathto("branch-with/slash"), posixpath.join("..", "..", "branch-with/slash", "index.html"), )