Initial commit
93
.gitignore
vendored
Normal file
|
@ -0,0 +1,93 @@
|
|||
# Byte-compiled / optimized / DLL files
|
||||
__pycache__/
|
||||
*.py[cod]
|
||||
*$py.class
|
||||
|
||||
# C extensions
|
||||
*.so
|
||||
|
||||
# Distribution / packaging
|
||||
.Python
|
||||
env/
|
||||
build/
|
||||
develop-eggs/
|
||||
dist/
|
||||
downloads/
|
||||
eggs/
|
||||
.eggs/
|
||||
lib/
|
||||
lib64/
|
||||
parts/
|
||||
sdist/
|
||||
var/
|
||||
*.egg-info/
|
||||
.installed.cfg
|
||||
*.egg
|
||||
|
||||
# PyInstaller
|
||||
# Usually these files are written by a python script from a template
|
||||
# before PyInstaller builds the exe, so as to inject date/other infos into it.
|
||||
*.manifest
|
||||
*.spec
|
||||
|
||||
# Installer logs
|
||||
pip-log.txt
|
||||
pip-delete-this-directory.txt
|
||||
|
||||
# Unit test / coverage reports
|
||||
htmlcov/
|
||||
.tox/
|
||||
.coverage
|
||||
.coverage.*
|
||||
.cache
|
||||
nosetests.xml
|
||||
coverage.xml
|
||||
*,cover
|
||||
.hypothesis/
|
||||
|
||||
# Translations
|
||||
*.mo
|
||||
*.pot
|
||||
|
||||
# Django stuff:
|
||||
*.log
|
||||
local_settings.py
|
||||
|
||||
# Flask stuff:
|
||||
instance/
|
||||
.webassets-cache
|
||||
|
||||
# Scrapy stuff:
|
||||
.scrapy
|
||||
|
||||
# Sphinx documentation
|
||||
docs/_build/
|
||||
|
||||
# PyBuilder
|
||||
target/
|
||||
|
||||
# IPython Notebook
|
||||
.ipynb_checkpoints
|
||||
|
||||
# pyenv
|
||||
.python-version
|
||||
|
||||
# celery beat schedule file
|
||||
celerybeat-schedule
|
||||
|
||||
# dotenv
|
||||
.env
|
||||
|
||||
# virtualenv
|
||||
venv/
|
||||
ENV/
|
||||
|
||||
# pipenv
|
||||
.venv/
|
||||
|
||||
# Spyder project settings
|
||||
.spyderproject
|
||||
|
||||
# Rope project settings
|
||||
.ropeproject
|
||||
|
21
LICENSE
Normal file
|
@ -0,0 +1,21 @@
|
|||
The MIT License (MIT)
|
||||
|
||||
Copyright (c) 2016 Robpol86
|
||||
|
||||
Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||
of this software and associated documentation files (the "Software"), to deal
|
||||
in the Software without restriction, including without limitation the rights
|
||||
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
||||
copies of the Software, and to permit persons to whom the Software is
|
||||
furnished to do so, subject to the following conditions:
|
||||
|
||||
The above copyright notice and this permission notice shall be included in all
|
||||
copies or substantial portions of the Software.
|
||||
|
||||
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
||||
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
||||
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
||||
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
||||
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
||||
SOFTWARE.
|
16
Pipfile
Normal file
|
@ -0,0 +1,16 @@
|
|||
[[source]]
|
||||
name = "pypi"
|
||||
url = "https://pypi.org/simple"
|
||||
verify_ssl = true
|
||||
|
||||
[dev-packages]
|
||||
|
||||
[packages]
|
||||
click = "*"
|
||||
colorclass = "*"
|
||||
sphinx = "*"
|
||||
sphinx_rtd_theme = "*"
|
||||
typing = {version = "*", markers="python_version >= '3.0' and python_version < '3.5'"}
|
||||
|
||||
[requires]
|
||||
python_version = "3"
|
215
Pipfile.lock
generated
Normal file
|
@ -0,0 +1,215 @@
|
|||
{
|
||||
"_meta": {
|
||||
"hash": {
|
||||
"sha256": "54db8168514bb292bfac3bdc169c417cea36001321d31e51752213ce5d3d5554"
|
||||
},
|
||||
"pipfile-spec": 6,
|
||||
"requires": {
|
||||
"python_version": "3"
|
||||
},
|
||||
"sources": [
|
||||
{
|
||||
"name": "pypi",
|
||||
"url": "https://pypi.org/simple",
|
||||
"verify_ssl": true
|
||||
}
|
||||
]
|
||||
},
|
||||
"default": {
|
||||
"alabaster": {
|
||||
"hashes": [
|
||||
"sha256:446438bdcca0e05bd45ea2de1668c1d9b032e1a9154c2c259092d77031ddd359",
|
||||
"sha256:a661d72d58e6ea8a57f7a86e37d86716863ee5e92788398526d58b26a4e4dc02"
|
||||
],
|
||||
"version": "==0.7.12"
|
||||
},
|
||||
"babel": {
|
||||
"hashes": [
|
||||
"sha256:6778d85147d5d85345c14a26aada5e478ab04e39b078b0745ee6870c2b5cf669",
|
||||
"sha256:8cba50f48c529ca3fa18cf81fa9403be176d374ac4d60738b839122dfaaa3d23"
|
||||
],
|
||||
"version": "==2.6.0"
|
||||
},
|
||||
"certifi": {
|
||||
"hashes": [
|
||||
"sha256:47f9c83ef4c0c621eaef743f133f09fa8a74a9b75f037e8624f83bd1b6626cb7",
|
||||
"sha256:993f830721089fef441cdfeb4b2c8c9df86f0c63239f06bd025a76a7daddb033"
|
||||
],
|
||||
"version": "==2018.11.29"
|
||||
},
|
||||
"chardet": {
|
||||
"hashes": [
|
||||
"sha256:84ab92ed1c4d4f16916e05906b6b75a6c0fb5db821cc65e70cbd64a3e2a5eaae",
|
||||
"sha256:fc323ffcaeaed0e0a02bf4d117757b98aed530d9ed4531e3e15460124c106691"
|
||||
],
|
||||
"version": "==3.0.4"
|
||||
},
|
||||
"click": {
|
||||
"hashes": [
|
||||
"sha256:2335065e6395b9e67ca716de5f7526736bfa6ceead690adf616d925bdc622b13",
|
||||
"sha256:5b94b49521f6456670fdb30cd82a4eca9412788a93fa6dd6df72c94d5a8ff2d7"
|
||||
],
|
||||
"index": "pypi",
|
||||
"version": "==7.0"
|
||||
},
|
||||
"colorclass": {
|
||||
"hashes": [
|
||||
"sha256:b05c2a348dfc1aff2d502527d78a5b7b7e2f85da94a96c5081210d8e9ee8e18b"
|
||||
],
|
||||
"index": "pypi",
|
||||
"version": "==2.2.0"
|
||||
},
|
||||
"docutils": {
|
||||
"hashes": [
|
||||
"sha256:02aec4bd92ab067f6ff27a38a38a41173bf01bed8f89157768c1573f53e474a6",
|
||||
"sha256:51e64ef2ebfb29cae1faa133b3710143496eca21c530f3f71424d77687764274",
|
||||
"sha256:7a4bd47eaf6596e1295ecb11361139febe29b084a87bf005bf899f9a42edc3c6"
|
||||
],
|
||||
"version": "==0.14"
|
||||
},
|
||||
"idna": {
|
||||
"hashes": [
|
||||
"sha256:156a6814fb5ac1fc6850fb002e0852d56c0c8d2531923a51032d1b70760e186e",
|
||||
"sha256:684a38a6f903c1d71d6d5fac066b58d7768af4de2b832e426ec79c30daa94a16"
|
||||
],
|
||||
"version": "==2.7"
|
||||
},
|
||||
"imagesize": {
|
||||
"hashes": [
|
||||
"sha256:3f349de3eb99145973fefb7dbe38554414e5c30abd0c8e4b970a7c9d09f3a1d8",
|
||||
"sha256:f3832918bc3c66617f92e35f5d70729187676313caa60c187eb0f28b8fe5e3b5"
|
||||
],
|
||||
"version": "==1.1.0"
|
||||
},
|
||||
"jinja2": {
|
||||
"hashes": [
|
||||
"sha256:74c935a1b8bb9a3947c50a54766a969d4846290e1e788ea44c1392163723c3bd",
|
||||
"sha256:f84be1bb0040caca4cea721fcbbbbd61f9be9464ca236387158b0feea01914a4"
|
||||
],
|
||||
"version": "==2.10"
|
||||
},
|
||||
"markupsafe": {
|
||||
"hashes": [
|
||||
"sha256:048ef924c1623740e70204aa7143ec592504045ae4429b59c30054cb31e3c432",
|
||||
"sha256:130f844e7f5bdd8e9f3f42e7102ef1d49b2e6fdf0d7526df3f87281a532d8c8b",
|
||||
"sha256:19f637c2ac5ae9da8bfd98cef74d64b7e1bb8a63038a3505cd182c3fac5eb4d9",
|
||||
"sha256:1b8a7a87ad1b92bd887568ce54b23565f3fd7018c4180136e1cf412b405a47af",
|
||||
"sha256:1c25694ca680b6919de53a4bb3bdd0602beafc63ff001fea2f2fc16ec3a11834",
|
||||
"sha256:1f19ef5d3908110e1e891deefb5586aae1b49a7440db952454b4e281b41620cd",
|
||||
"sha256:1fa6058938190ebe8290e5cae6c351e14e7bb44505c4a7624555ce57fbbeba0d",
|
||||
"sha256:31cbb1359e8c25f9f48e156e59e2eaad51cd5242c05ed18a8de6dbe85184e4b7",
|
||||
"sha256:3e835d8841ae7863f64e40e19477f7eb398674da6a47f09871673742531e6f4b",
|
||||
"sha256:4e97332c9ce444b0c2c38dd22ddc61c743eb208d916e4265a2a3b575bdccb1d3",
|
||||
"sha256:525396ee324ee2da82919f2ee9c9e73b012f23e7640131dd1b53a90206a0f09c",
|
||||
"sha256:52b07fbc32032c21ad4ab060fec137b76eb804c4b9a1c7c7dc562549306afad2",
|
||||
"sha256:52ccb45e77a1085ec5461cde794e1aa037df79f473cbc69b974e73940655c8d7",
|
||||
"sha256:5c3fbebd7de20ce93103cb3183b47671f2885307df4a17a0ad56a1dd51273d36",
|
||||
"sha256:5e5851969aea17660e55f6a3be00037a25b96a9b44d2083651812c99d53b14d1",
|
||||
"sha256:5edfa27b2d3eefa2210fb2f5d539fbed81722b49f083b2c6566455eb7422fd7e",
|
||||
"sha256:7d263e5770efddf465a9e31b78362d84d015cc894ca2c131901a4445eaa61ee1",
|
||||
"sha256:83381342bfc22b3c8c06f2dd93a505413888694302de25add756254beee8449c",
|
||||
"sha256:857eebb2c1dc60e4219ec8e98dfa19553dae33608237e107db9c6078b1167856",
|
||||
"sha256:98e439297f78fca3a6169fd330fbe88d78b3bb72f967ad9961bcac0d7fdd1550",
|
||||
"sha256:bf54103892a83c64db58125b3f2a43df6d2cb2d28889f14c78519394feb41492",
|
||||
"sha256:d9ac82be533394d341b41d78aca7ed0e0f4ba5a2231602e2f05aa87f25c51672",
|
||||
"sha256:e982fe07ede9fada6ff6705af70514a52beb1b2c3d25d4e873e82114cf3c5401",
|
||||
"sha256:edce2ea7f3dfc981c4ddc97add8a61381d9642dc3273737e756517cc03e84dd6",
|
||||
"sha256:efdc45ef1afc238db84cb4963aa689c0408912a0239b0721cb172b4016eb31d6",
|
||||
"sha256:f137c02498f8b935892d5c0172560d7ab54bc45039de8805075e19079c639a9c",
|
||||
"sha256:f82e347a72f955b7017a39708a3667f106e6ad4d10b25f237396a7115d8ed5fd",
|
||||
"sha256:fb7c206e01ad85ce57feeaaa0bf784b97fa3cad0d4a5737bc5295785f5c613a1"
|
||||
],
|
||||
"version": "==1.1.0"
|
||||
},
|
||||
"packaging": {
|
||||
"hashes": [
|
||||
"sha256:0886227f54515e592aaa2e5a553332c73962917f2831f1b0f9b9f4380a4b9807",
|
||||
"sha256:f95a1e147590f204328170981833854229bb2912ac3d5f89e2a8ccd2834800c9"
|
||||
],
|
||||
"version": "==18.0"
|
||||
},
|
||||
"pygments": {
|
||||
"hashes": [
|
||||
"sha256:6301ecb0997a52d2d31385e62d0a4a4cf18d2f2da7054a5ddad5c366cd39cee7",
|
||||
"sha256:82666aac15622bd7bb685a4ee7f6625dd716da3ef7473620c192c0168aae64fc"
|
||||
],
|
||||
"version": "==2.3.0"
|
||||
},
|
||||
"pyparsing": {
|
||||
"hashes": [
|
||||
"sha256:40856e74d4987de5d01761a22d1621ae1c7f8774585acae358aa5c5936c6c90b",
|
||||
"sha256:f353aab21fd474459d97b709e527b5571314ee5f067441dc9f88e33eecd96592"
|
||||
],
|
||||
"version": "==2.3.0"
|
||||
},
|
||||
"pytz": {
|
||||
"hashes": [
|
||||
"sha256:31cb35c89bd7d333cd32c5f278fca91b523b0834369e757f4c5641ea252236ca",
|
||||
"sha256:8e0f8568c118d3077b46be7d654cc8167fa916092e28320cde048e54bfc9f1e6"
|
||||
],
|
||||
"version": "==2018.7"
|
||||
},
|
||||
"requests": {
|
||||
"hashes": [
|
||||
"sha256:65b3a120e4329e33c9889db89c80976c5272f56ea92d3e74da8a463992e3ff54",
|
||||
"sha256:ea881206e59f41dbd0bd445437d792e43906703fff75ca8ff43ccdb11f33f263"
|
||||
],
|
||||
"version": "==2.20.1"
|
||||
},
|
||||
"six": {
|
||||
"hashes": [
|
||||
"sha256:70e8a77beed4562e7f14fe23a786b54f6296e34344c23bc42f07b15018ff98e9",
|
||||
"sha256:832dc0e10feb1aa2c68dcc57dbb658f1c7e65b9b61af69048abc87a2db00a0eb"
|
||||
],
|
||||
"version": "==1.11.0"
|
||||
},
|
||||
"snowballstemmer": {
|
||||
"hashes": [
|
||||
"sha256:919f26a68b2c17a7634da993d91339e288964f93c274f1343e3bbbe2096e1128",
|
||||
"sha256:9f3bcd3c401c3e862ec0ebe6d2c069ebc012ce142cce209c098ccb5b09136e89"
|
||||
],
|
||||
"version": "==1.2.1"
|
||||
},
|
||||
"sphinx": {
|
||||
"hashes": [
|
||||
"sha256:120732cbddb1b2364471c3d9f8bfd4b0c5b550862f99a65736c77f970b142aea",
|
||||
"sha256:b348790776490894e0424101af9c8413f2a86831524bd55c5f379d3e3e12ca64"
|
||||
],
|
||||
"index": "pypi",
|
||||
"version": "==1.8.2"
|
||||
},
|
||||
"sphinx-rtd-theme": {
|
||||
"hashes": [
|
||||
"sha256:02f02a676d6baabb758a20c7a479d58648e0f64f13e07d1b388e9bb2afe86a09",
|
||||
"sha256:d0f6bc70f98961145c5b0e26a992829363a197321ba571b31b24ea91879e0c96"
|
||||
],
|
||||
"index": "pypi",
|
||||
"version": "==0.4.2"
|
||||
},
|
||||
"sphinxcontrib-websupport": {
|
||||
"hashes": [
|
||||
"sha256:68ca7ff70785cbe1e7bccc71a48b5b6d965d79ca50629606c7861a21b206d9dd",
|
||||
"sha256:9de47f375baf1ea07cdb3436ff39d7a9c76042c10a769c52353ec46e4e8fc3b9"
|
||||
],
|
||||
"version": "==1.1.0"
|
||||
},
|
||||
"typing": {
|
||||
"hashes": [
|
||||
"sha256:4027c5f6127a6267a435201981ba156de91ad0d1d98e9ddc2aa173453453492d",
|
||||
"sha256:57dcf675a99b74d64dacf6fba08fb17cf7e3d5fdff53d4a30ea2a5e7e52543d4",
|
||||
"sha256:a4c8473ce11a65999c8f59cb093e70686b6c84c98df58c1dae9b3b196089858a"
|
||||
],
|
||||
"index": "pypi",
|
||||
"markers": "python_version >= '3.0' and python_version < '3.5'",
|
||||
"version": "==3.6.6"
|
||||
},
|
||||
"urllib3": {
|
||||
"hashes": [
|
||||
"sha256:61bf29cada3fc2fbefad4fdf059ea4bd1b4a86d2b6d15e1c7c0b582b9752fe39",
|
||||
"sha256:de9529817c93f27c8ccbfead6985011db27bd0ddfcdb2d86f3f663385c6a9c22"
|
||||
],
|
||||
"version": "==1.24.1"
|
||||
}
|
||||
},
|
||||
"develop": {}
|
||||
}
|
41
README.rst
Normal file
|
@ -0,0 +1,41 @@
|
|||
===============
|
||||
sphinx-versions
|
||||
===============
|
||||
|
||||
Sphinx extension that allows building versioned docs for self-hosting.
|
||||
|
||||
* Python 3.4, and 3.5 supported on Linux and OS X.
|
||||
* Python 3.4, and 3.5 supported on Windows (both 32 and 64 bit versions of Python).
|
||||
|
||||
Full documentation: https://sphinx-versions.readthedocs.io
|
||||
|
||||
This project is, for the most part, a fork of https://github.com/Robpol86/sphinxcontrib-versioning, with some additions and removals.
|
||||
|
||||
How to use
|
||||
==========
|
||||
|
||||
Most basic usage:
|
||||
|
||||
.. code:: bash
|
||||
|
||||
sphinx-versions --help
|
||||
sphinx-versions build --help
|
||||
|
||||
|
||||
.. changelog-section-start
|
||||
|
||||
Changelog
|
||||
=========
|
||||
|
||||
This project adheres to `Semantic Versioning <http://semver.org/>`_.
|
||||
|
||||
1.0.0 - 2018-12-08
|
||||
------------------
|
||||
|
||||
Changes
|
||||
* From *sphinxcontrib-versionning* *v2.2.1*, added compatibility with *Sphinx 1.8.2*.
|
||||
* From *sphinxcontrib-versionning* *v2.2.1*, removed `push` commands, considered not core for our own usage.
|
||||
* Migrates to ``pipenv`` as the recommanded installation process.
|
||||
* Use `-s` option instead of `--no-patch` in `git show` (this is for git 1.8.3.1 compatibility).
|
||||
|
||||
.. changelog-section-end
|
19
docs/Makefile
Normal file
|
@ -0,0 +1,19 @@
|
|||
# Minimal makefile for Sphinx documentation
|
||||
#
|
||||
|
||||
# You can set these variables from the command line.
|
||||
SPHINXOPTS =
|
||||
SPHINXBUILD = sphinx-build
|
||||
SOURCEDIR = .
|
||||
BUILDDIR = _build
|
||||
|
||||
# Put it first so that "make" without argument is like "make help".
|
||||
help:
|
||||
@$(SPHINXBUILD) -M help "$(SOURCEDIR)" "$(BUILDDIR)" $(SPHINXOPTS) $(O)
|
||||
|
||||
.PHONY: help Makefile
|
||||
|
||||
# Catch-all target: route all unknown targets to Sphinx using the new
|
||||
# "make mode" option. $(O) is meant as a shortcut for $(SPHINXOPTS).
|
||||
%: Makefile
|
||||
@$(SPHINXBUILD) -M $@ "$(SOURCEDIR)" "$(BUILDDIR)" $(SPHINXOPTS) $(O)
|
BIN
docs/_static/Favicon_logo_Smile.png
vendored
Normal file
After Width: | Height: | Size: 9.2 KiB |
34
docs/banner.rst
Normal file
|
@ -0,0 +1,34 @@
|
|||
.. _banner:
|
||||
|
||||
==============
|
||||
Banner Message
|
||||
==============
|
||||
|
||||
Banner messages can be displayed at the top of every document informing users that they are currently viewing either old or the development version of the project's documentation, with the exception of the :option:`--banner-main-ref`. This feature is inspired by banner on the `Jinja2 documentation <http://jinja.pocoo.org/docs/dev/>`_.
|
||||
|
||||
The banner feature is disabled by default. It can be enabled with the :option:`--show-banner` setting (see the :ref:`settings <setting-show-banner>` for more configuration options).
|
||||
|
||||
From branch to `main-ref`
|
||||
-------------------------
|
||||
|
||||
The message displayed when users are viewing docs from a branch and the :option:`--banner-main-ref` is a tag. The entire banner is a link that sends users to the latest version of the current page if it exists there.
|
||||
|
||||
.. figure:: screenshots/sphinx_rtd_theme_banner_dev.png
|
||||
:target: _images/sphinx_rtd_theme_banner_dev.png
|
||||
|
||||
From old tag to `main-ref`
|
||||
--------------------------
|
||||
|
||||
The message displayed when users are viewing docs from a tag and the :option:`--banner-main-ref` is a tag. Like the message above this one links users to the latest version of the current page.
|
||||
|
||||
.. figure:: screenshots/sphinx_rtd_theme_banner_old.png
|
||||
:target: _images/sphinx_rtd_theme_banner_old.png
|
||||
|
||||
From a page not existing in the `main-ref`
|
||||
------------------------------------------
|
||||
|
||||
An example of a banner message from a page that does not exist in the :option:`--banner-main-ref` version. Since there is no page to link to this is just text informing the user that they're viewing the development version of the docs.
|
||||
|
||||
.. figure:: screenshots/sphinx_rtd_theme_banner_nourl.png
|
||||
:target: _images/sphinx_rtd_theme_banner_nourl.png
|
||||
|
5
docs/changelog.rst
Normal file
|
@ -0,0 +1,5 @@
|
|||
.. _changelog:
|
||||
|
||||
.. include:: ../README.rst
|
||||
:start-after: changelog-section-start
|
||||
:end-before: changelog-section-end
|
38
docs/conf.py
Normal file
|
@ -0,0 +1,38 @@
|
|||
"""Sphinx configuration file."""
|
||||
|
||||
import os
|
||||
import sys
|
||||
import time
|
||||
|
||||
# -- Project information -----------------------------------------------------
|
||||
|
||||
copyright = '2018, Smile'
|
||||
author = 'Smile'
|
||||
|
||||
# General configuration.
|
||||
sys.path.append(os.path.realpath(os.path.join(os.path.dirname(__file__), '..')))
|
||||
html_last_updated_fmt = '%c'
|
||||
master_doc = 'index'
|
||||
project = __import__('setup').NAME
|
||||
pygments_style = 'friendly'
|
||||
release = version = __import__('setup').VERSION
|
||||
templates_path = ['_templates']
|
||||
extensions = list()
|
||||
|
||||
|
||||
# Options for HTML output.
|
||||
html_context = dict(
|
||||
conf_py_path='/docs/',
|
||||
source_suffix='.rst',
|
||||
)
|
||||
html_copy_source = False
|
||||
html_favicon='_static/Favicon_logo_Smile.png'
|
||||
html_logo=''
|
||||
html_theme = 'sphinx_rtd_theme'
|
||||
html_title = project
|
||||
|
||||
|
||||
# sphinx-versions
|
||||
scv_banner_greatest_tag = True
|
||||
scv_show_banner = True
|
||||
scv_sort = ('semver', 'time')
|
166
docs/context.rst
Normal file
|
@ -0,0 +1,166 @@
|
|||
.. _context:
|
||||
|
||||
================
|
||||
HTML Context API
|
||||
================
|
||||
|
||||
The following Jinja2_ context variables are exposed in `the Sphinx HTML builder context <sphinx_context_>`_ in all
|
||||
versions.
|
||||
|
||||
Versions Iterable
|
||||
=================
|
||||
|
||||
``versions`` is the main variable of interest. It yields names of other (and the current) versions and relative URLs to
|
||||
them. You can iterate on it to get all branches and tags, or use special properties attached to it to yield just
|
||||
branches or just tags.
|
||||
|
||||
.. attribute:: versions
|
||||
|
||||
An iterable that yields 2-item tuples of strings. The first item is the version (branch/tag) name while the second
|
||||
item is the relative path to the documentation for that version. The path is URL safe and takes into account HTML
|
||||
pages in sub directories.
|
||||
|
||||
.. code-block:: jinja
|
||||
|
||||
{%- for name, url in versions %}
|
||||
<li><a href="{{ url }}">{{ name }}</a></li>
|
||||
{%- endfor %}
|
||||
|
||||
.. attribute:: versions.branches
|
||||
|
||||
The ``versions`` iterable has a **branches** property that itself yields versions in branches (filtering out git
|
||||
tags). The order is the same and it yields the same tuples.
|
||||
|
||||
.. code-block:: jinja
|
||||
|
||||
<dl>
|
||||
<dt>Branches</dt>
|
||||
{%- for name, url in versions.branches %}
|
||||
<dd><a href="{{ url }}">{{ name }}</a></dd>
|
||||
{%- endfor %}
|
||||
</dl>
|
||||
|
||||
.. attribute:: versions.tags
|
||||
|
||||
The ``versions`` iterable also has a **tags** property that itself yields versions in tags (filtering out git
|
||||
branches). Just as the **branches** property the order is maintained and the yielded tuples are the same.
|
||||
|
||||
.. code-block:: jinja
|
||||
|
||||
<dl>
|
||||
<dt>Tags</dt>
|
||||
{%- for name, url in versions.tags %}
|
||||
<dd><a href="{{ url }}">{{ name }}</a></dd>
|
||||
{%- endfor %}
|
||||
</dl>
|
||||
|
||||
Functions
|
||||
=========
|
||||
|
||||
.. function:: vhasdoc(other_version)
|
||||
|
||||
Similar to Sphinx's `hasdoc() <sphinx_hasdoc_>`_ function. Returns True if the current document exists in another
|
||||
version.
|
||||
|
||||
.. code-block:: jinja
|
||||
|
||||
{% if vhasdoc('master') %}
|
||||
This doc is available in <a href="../master/index.html">master</a>.
|
||||
{% endif %}
|
||||
|
||||
.. function:: vpathto(other_version)
|
||||
|
||||
Similar to Sphinx's `pathto() <sphinx_pathto_>`_ function. Has two behaviors:
|
||||
|
||||
1. If the current document exists in the specified other version pathto() returns the relative URL to that document.
|
||||
2. If the current document does not exist in the other version the relative URL to that version's
|
||||
`master_doc <sphinx_master_doc_>`_ is returned instead.
|
||||
|
||||
.. code-block:: jinja
|
||||
|
||||
{% if vhasdoc('master') %}
|
||||
This doc is available in <a href="{{ vpathto('master') }}">master</a>.
|
||||
{% else %}
|
||||
Go to <a href="{{ vpathto('master') }}">master</a> for the latest docs.
|
||||
{% endif %}
|
||||
|
||||
Banner Variables
|
||||
================
|
||||
|
||||
These variables are exposed in the Jinja2 context to facilitate displaying the banner message and deciding which message
|
||||
to display.
|
||||
|
||||
.. attribute:: scv_banner_greatest_tag
|
||||
|
||||
A boolean set to True if :option:`--banner-greatest-tag` is used.
|
||||
|
||||
.. attribute:: scv_banner_main_ref_is_branch
|
||||
|
||||
A boolean set to True if the banner main ref is a branch.
|
||||
|
||||
.. attribute:: scv_banner_main_ref_is_tag
|
||||
|
||||
A boolean set to True if the banner main ref is a tag.
|
||||
|
||||
.. attribute:: scv_banner_main_version
|
||||
|
||||
A string, the value of :option:`--banner-main-ref`.
|
||||
|
||||
.. attribute:: scv_banner_recent_tag
|
||||
|
||||
A boolean set to True if :option:`--banner-recent-tag` is used.
|
||||
|
||||
.. attribute:: scv_show_banner
|
||||
|
||||
A boolean set to True if :option:`--show-banner` is used.
|
||||
|
||||
Other Variables
|
||||
===============
|
||||
|
||||
.. attribute:: current_version
|
||||
|
||||
A string of the current version being built. This will be the git ref name (e.g. a branch name or tag name).
|
||||
|
||||
.. code-block:: jinja
|
||||
|
||||
<h3>Current Version: {{ current_version }}</h3>
|
||||
|
||||
.. attribute:: scv_is_branch
|
||||
|
||||
A boolean set to True if the current version being built is from a git branch.
|
||||
|
||||
.. attribute:: scv_is_greatest_tag
|
||||
|
||||
A boolean set to True if the current version being built is:
|
||||
|
||||
* From a git tag.
|
||||
* A valid semver-formatted name (e.g. v1.2.3).
|
||||
* The highest version number.
|
||||
|
||||
.. attribute:: scv_is_recent_branch
|
||||
|
||||
A boolean set to True if the current version being built is a git branch and is the most recent commit out of just
|
||||
git branches.
|
||||
|
||||
.. attribute:: scv_is_recent_ref
|
||||
|
||||
A boolean set to True if the current version being built is the most recent git commit (branch or tag).
|
||||
|
||||
.. attribute:: scv_is_recent_tag
|
||||
|
||||
A boolean set to True if the current version being built is a git tag and is the most recent commit out of just git
|
||||
tags.
|
||||
|
||||
.. attribute:: scv_is_root
|
||||
|
||||
A boolean set to True if the current version being built is in the web root (defined by :option:`--root-ref`).
|
||||
|
||||
.. attribute:: scv_is_tag
|
||||
|
||||
A boolean set to True if the current version being built is from a git tag.
|
||||
|
||||
.. _Jinja2: http://jinja.pocoo.org/
|
||||
.. _sphinx_context: http://www.sphinx-doc.org/en/stable/config.html?highlight=context#confval-html_context
|
||||
.. _sphinx_hasdoc: http://www.sphinx-doc.org/en/stable/templating.html#hasdoc
|
||||
.. _sphinx_master_doc: http://www.sphinx-doc.org/en/stable/config.html#confval-master_doc
|
||||
.. _sphinx_pathto: http://www.sphinx-doc.org/en/stable/templating.html#pathto
|
70
docs/contributing.rst
Normal file
|
@ -0,0 +1,70 @@
|
|||
.. _contributing:
|
||||
|
||||
=================
|
||||
How to contribute
|
||||
=================
|
||||
|
||||
Make sure you meet the :ref:`requirements to use <requirements-to-use>` the project, first.
|
||||
|
||||
Requirements
|
||||
============
|
||||
|
||||
Install the virtualenv dependencies
|
||||
-----------------------------------
|
||||
|
||||
To install required dependencies, use the command:
|
||||
|
||||
.. code-block:: shell
|
||||
|
||||
$ pipenv install --dev
|
||||
|
||||
This will install in your local virtualenv all the required dependencies to contribute to this project.
|
||||
|
||||
The ``--dev`` option allows the installation of the *dev-packages* dependencies.
|
||||
|
||||
|
||||
Install build and distribution tools
|
||||
------------------------------------
|
||||
|
||||
* Install latest version of required tools
|
||||
|
||||
.. code:: bash
|
||||
|
||||
pip install --user -U setuptools wheel twine
|
||||
|
||||
|
||||
Build and upload a new version of sphinx-versions
|
||||
=================================================
|
||||
|
||||
Update the version
|
||||
------------------
|
||||
|
||||
You need to update two different files :
|
||||
|
||||
* ``setup.py``: contains the `VERSION` constant, used to identify the version built and uploaded to the nexus.
|
||||
* ``sphinxcontrib/versioning/__init__.py``: contains the ``__version__`` constant, used to identify the package version.
|
||||
|
||||
|
||||
Update the README.rst
|
||||
---------------------
|
||||
|
||||
You need to add a section in the ``README.rst`` for the newly created version (follow the pattern of other versions).
|
||||
|
||||
|
||||
Generate package to distribute
|
||||
------------------------------
|
||||
|
||||
This builds your python project and creates the `dist` directory (among other things).
|
||||
|
||||
.. code:: bash
|
||||
|
||||
python3 setup.py sdist bdist_wheel
|
||||
|
||||
Upload your package to nexus
|
||||
----------------------------
|
||||
|
||||
.. code:: bash
|
||||
|
||||
twine upload dist/*
|
||||
|
||||
After this command, your package is available on https://pypi.org. Anyone can install it using `pip install sphinx-versions`.
|
51
docs/index.rst
Normal file
|
@ -0,0 +1,51 @@
|
|||
=========================
|
||||
sphinx-versions |version|
|
||||
=========================
|
||||
|
||||
A Sphinx extension that lets you build Sphinx docs for all versions of your project without needing special hosting
|
||||
services.
|
||||
|
||||
+--------------------+--------------------+
|
||||
| **A Few Examples** |
|
||||
+--------------------+--------------------+
|
||||
| *alabaster* | *sphinx_rtd_theme* |
|
||||
| | |
|
||||
| |alabaster| | |sphinx_rtd_theme| |
|
||||
+--------------------+--------------------+
|
||||
| *classic* | *nature* |
|
||||
| | |
|
||||
| |classic| | |nature| |
|
||||
+--------------------+--------------------+
|
||||
|
||||
.. |alabaster| image:: screenshots/alabaster.png
|
||||
:target: _images/alabaster.png
|
||||
.. |sphinx_rtd_theme| image:: screenshots/sphinx_rtd_theme.png
|
||||
:target: _images/sphinx_rtd_theme.png
|
||||
.. |classic| image:: screenshots/classic.png
|
||||
:target: _images/classic.png
|
||||
.. |nature| image:: screenshots/nature.png
|
||||
:target: _images/nature.png
|
||||
|
||||
Project Links
|
||||
=============
|
||||
|
||||
* Documentation: https://sphinx-versions.readthedocs.io
|
||||
* Source code: https://github.com/Smile-SA/sphinx-versions
|
||||
|
||||
.. toctree::
|
||||
:maxdepth: 2
|
||||
:caption: General
|
||||
|
||||
install
|
||||
tutorial
|
||||
banner
|
||||
settings
|
||||
context
|
||||
themes
|
||||
|
||||
.. toctree::
|
||||
:maxdepth: 1
|
||||
:caption: Appendix
|
||||
|
||||
contributing
|
||||
changelog
|
54
docs/install.rst
Normal file
|
@ -0,0 +1,54 @@
|
|||
.. _install:
|
||||
|
||||
============
|
||||
Installation
|
||||
============
|
||||
|
||||
.. _requirements-to-use:
|
||||
|
||||
Requirements
|
||||
============
|
||||
|
||||
Install Python & PIP & pipenv
|
||||
-----------------------------
|
||||
|
||||
First, you need to install Python 3 and PIP (if not already present on your system):
|
||||
|
||||
.. code-block:: shell
|
||||
|
||||
$ sudo apt-get install python3-pip
|
||||
|
||||
You might need to update PIP right away
|
||||
|
||||
.. code-block:: shell
|
||||
|
||||
$ pip3 install -U pip
|
||||
|
||||
Then, install ``pipenv``
|
||||
|
||||
.. code-block:: shell
|
||||
|
||||
$ pip install --user -U pipenv
|
||||
|
||||
Installation
|
||||
============
|
||||
|
||||
`pipenv` install
|
||||
----------------
|
||||
|
||||
The suggested way to get `sphinx-versions` is to use `pipenv <https://pipenv.readthedocs.io>`_. Simply run this command, from your current project:
|
||||
|
||||
.. code-block:: bash
|
||||
|
||||
pipenv install sphinx-versions
|
||||
|
||||
Clone and Install
|
||||
-----------------
|
||||
|
||||
Lastly you can also just clone the repo and install from it. Usually you only need to do this if you plan on :ref:`contributing <contributing>` to the project.
|
||||
|
||||
.. code-block:: bash
|
||||
|
||||
git clone git@github.com:Smile-SA/sphinx-versions.git
|
||||
cd sphinx-versions
|
||||
python setup.py install
|
BIN
docs/screenshots/alabaster.png
Normal file
After Width: | Height: | Size: 58 KiB |
BIN
docs/screenshots/bizstyle.png
Normal file
After Width: | Height: | Size: 57 KiB |
BIN
docs/screenshots/classic.png
Normal file
After Width: | Height: | Size: 70 KiB |
BIN
docs/screenshots/nature.png
Normal file
After Width: | Height: | Size: 66 KiB |
BIN
docs/screenshots/pyramid.png
Normal file
After Width: | Height: | Size: 68 KiB |
BIN
docs/screenshots/sphinx_rtd_theme.png
Normal file
After Width: | Height: | Size: 87 KiB |
BIN
docs/screenshots/sphinx_rtd_theme_banner_dev.png
Normal file
After Width: | Height: | Size: 79 KiB |
BIN
docs/screenshots/sphinx_rtd_theme_banner_nourl.png
Normal file
After Width: | Height: | Size: 77 KiB |
BIN
docs/screenshots/sphinx_rtd_theme_banner_old.png
Normal file
After Width: | Height: | Size: 79 KiB |
BIN
docs/screenshots/sphinxdoc.png
Normal file
After Width: | Height: | Size: 57 KiB |
BIN
docs/screenshots/traditional.png
Normal file
After Width: | Height: | Size: 66 KiB |
272
docs/settings.rst
Normal file
|
@ -0,0 +1,272 @@
|
|||
.. _settings:
|
||||
|
||||
========
|
||||
Settings
|
||||
========
|
||||
|
||||
.. code-block:: bash
|
||||
|
||||
sphinx-versioning [GLOBAL_OPTIONS] build [OPTIONS] REL_SOURCE... DESTINATION
|
||||
|
||||
sphinx-versions reads settings from two sources:
|
||||
|
||||
* Your Sphinx **conf.py** file.
|
||||
* Command line arguments.
|
||||
|
||||
Command line arguments always override anything set in conf.py. You can specify the path to conf.py with the
|
||||
:option:`--local-conf` argument or sphinx-versions will look at the first conf.py it finds in your :option:`REL_SOURCE`
|
||||
directories. To completely disable using a conf.py file specify the :option:`--no-local-conf` command line argument.
|
||||
|
||||
Below are both the command line arguments available as well as the conf.py variable names sphinx-versions looks for. All
|
||||
conf.py variable names are prefixed with ``scv_``.
|
||||
|
||||
An example:
|
||||
|
||||
.. code-block:: python
|
||||
|
||||
# conf.py
|
||||
author = 'Your Name'
|
||||
project = 'My Project'
|
||||
scv_greatest_tag = True
|
||||
|
||||
Global Options
|
||||
==============
|
||||
|
||||
These options apply to to :ref:`build <build-arguments>` sub commands. They must be specified before the build command or else you'll get an error.
|
||||
|
||||
.. option:: -c <directory>, --chdir <directory>
|
||||
|
||||
Change the current working directory of the program to this path.
|
||||
|
||||
.. option:: -g <directory>, --git-root <directory>
|
||||
|
||||
Path to directory in the local repo. Default is the current working directory.
|
||||
|
||||
.. option:: -l <file>, --local-conf <file>
|
||||
|
||||
Path to conf.py for sphinx-versions to read its config from. Does not affect conf.py loaded by sphinx-build.
|
||||
|
||||
If not specified the default behavior is to have sphinx-versions look for a conf.py file in any :option:`REL_SOURCE`
|
||||
directory within the current working directory. Stops at the first conf.py found if any.
|
||||
|
||||
.. option:: -L, --no-local-conf
|
||||
|
||||
Disables searching for or loading a local conf.py for sphinx-versions settings. Does not affect conf.py loaded by
|
||||
sphinx-build.
|
||||
|
||||
.. option:: -N, --no-colors
|
||||
|
||||
By default INFO, WARNING, and ERROR log/print statements use console colors. Use this argument to disable colors and
|
||||
log/print plain text.
|
||||
|
||||
.. option:: -v, --verbose
|
||||
|
||||
Enable verbose/debug logging with timestamps and git command outputs. Implies :option:`--no-colors`. If specified
|
||||
more than once excess options (number used - 1) will be passed to sphinx-build.
|
||||
|
||||
.. _common-positional-arguments:
|
||||
|
||||
Common Positional Arguments
|
||||
===========================
|
||||
|
||||
The :ref:`build <build-arguments>` sub commands use these arguments.
|
||||
|
||||
.. option:: REL_SOURCE
|
||||
|
||||
The path to the docs directory relative to the git root. If the source directory has moved around between git tags
|
||||
you can specify additional directories.
|
||||
|
||||
This cannot be an absolute path, it must be relative to the root of your git repository. Sometimes projects move
|
||||
files around so documentation might not always have been in one place. To mitigate this you can specify additional
|
||||
relative paths and the first one that has a **conf.py** will be selected for each branch/tag. Any branch/tag that
|
||||
doesn't have a conf.py file in one of these REL_SOURCEs will be ignored.
|
||||
|
||||
.. option:: --, scv_overflow
|
||||
|
||||
It is possible to give the underlying ``sphinx-build`` program command line options. sphinx-versions passes everything
|
||||
after ``--`` to it. For example if you changed the theme for your docs between versions and want docs for all
|
||||
versions to have the same theme, you can run:
|
||||
|
||||
.. code-block:: bash
|
||||
|
||||
sphinx-versioning build docs docs/_build/html -- -A html_theme=sphinx_rtd_theme
|
||||
|
||||
This setting may also be specified in your conf.py file. It must be a tuple of strings:
|
||||
|
||||
.. code-block:: python
|
||||
|
||||
scv_overflow = ("-A", "html_theme=sphinx_rtd_theme")
|
||||
|
||||
.. _build-arguments:
|
||||
|
||||
Build Arguments
|
||||
===============
|
||||
|
||||
The ``build`` sub command builds all versions locally. It always gets the latest branches and tags from origin and
|
||||
builds those doc files.
|
||||
|
||||
Positional Arguments
|
||||
--------------------
|
||||
|
||||
In addition to the :ref:`common arguments <common-positional-arguments>`:
|
||||
|
||||
.. option:: DESTINATION
|
||||
|
||||
The path to the directory that will hold all generated docs for all versions.
|
||||
|
||||
This is the local path on the file sytem that will hold HTML files. It can be relative to the current working
|
||||
directory or an absolute directory path.
|
||||
|
||||
.. _build-options:
|
||||
|
||||
Options
|
||||
-------
|
||||
|
||||
These options are available for the build sub command:
|
||||
|
||||
.. option:: -a, --banner-greatest-tag, scv_banner_greatest_tag
|
||||
|
||||
Override banner-main-ref to be the tag with the highest version number. If no tags have docs then this option is
|
||||
ignored and :option:`--banner-main-ref` is used.
|
||||
|
||||
This setting may also be specified in your conf.py file. It must be a boolean:
|
||||
|
||||
.. code-block:: python
|
||||
|
||||
scv_banner_greatest_tag = True
|
||||
|
||||
.. option:: -A, --banner-recent-tag, scv_banner_recent_tag
|
||||
|
||||
Override banner-main-ref to be the most recent committed tag. If no tags have docs then this option is ignored and
|
||||
:option:`--banner-main-ref` is used.
|
||||
|
||||
This setting may also be specified in your conf.py file. It must be a boolean:
|
||||
|
||||
.. code-block:: python
|
||||
|
||||
scv_banner_recent_tag = True
|
||||
|
||||
.. _setting-show-banner:
|
||||
|
||||
.. option:: -b, --show-banner, scv_show_banner
|
||||
|
||||
Show a warning banner. Enables the :ref:`banner` feature.
|
||||
|
||||
This setting may also be specified in your conf.py file. It must be a boolean:
|
||||
|
||||
.. code-block:: python
|
||||
|
||||
scv_show_banner = True
|
||||
|
||||
.. option:: -B <ref>, --banner-main-ref <ref>, scv_banner_main_ref
|
||||
|
||||
The branch/tag considered to be the latest/current version. The banner will not be displayed in this ref, only in
|
||||
all others. Default is **master**.
|
||||
|
||||
If the banner-main-ref does not exist or does not have docs the banner will be disabled completely in all versions.
|
||||
Docs will continue to be built.
|
||||
|
||||
This setting may also be specified in your conf.py file. It must be a string:
|
||||
|
||||
.. code-block:: python
|
||||
|
||||
scv_banner_main_ref = 'feature_branch'
|
||||
|
||||
.. option:: -i, --invert, scv_invert
|
||||
|
||||
Invert the order of branches/tags displayed in the sidebars in generated HTML documents. The default order is
|
||||
whatever git prints when running "**git ls-remote --heads --tags**".
|
||||
|
||||
This setting may also be specified in your conf.py file. It must be a boolean:
|
||||
|
||||
.. code-block:: python
|
||||
|
||||
scv_invert = True
|
||||
|
||||
.. option:: -p <kind>, --priority <kind>, scv_priority
|
||||
|
||||
``kind`` may be either **branches** or **tags**. This argument is for themes that don't split up branches and tags
|
||||
in the generated HTML (e.g. sphinx_rtd_theme). This argument groups branches and tags together and whichever is
|
||||
selected for ``kind`` will be displayed first.
|
||||
|
||||
This setting may also be specified in your conf.py file. It must be a string:
|
||||
|
||||
.. code-block:: python
|
||||
|
||||
scv_priority = 'branches'
|
||||
|
||||
.. option:: -r <ref>, --root-ref <ref>, scv_root_ref
|
||||
|
||||
The branch/tag at the root of :option:`DESTINATION`. Will also be in subdirectories like the others. Default is
|
||||
**master**.
|
||||
|
||||
If the root-ref does not exist or does not have docs, ``sphinx-versioning`` will fail and exit. The root-ref must
|
||||
have docs.
|
||||
|
||||
This setting may also be specified in your conf.py file. It must be a string:
|
||||
|
||||
.. code-block:: python
|
||||
|
||||
scv_root_ref = 'feature_branch'
|
||||
|
||||
.. option:: -s <value>, --sort <value>, scv_sort
|
||||
|
||||
Sort versions by one or more certain kinds of values. Valid values are ``semver``, ``alpha``, and ``time``.
|
||||
|
||||
You can specify just one (e.g. "semver"), or more. The "semver" value sorts versions by
|
||||
`Semantic Versioning <http://semver.org/>`_, with the highest version being first (e.g. 3.0.0, 2.10.0, 1.0.0).
|
||||
Non-semver branches/tags will be sorted after all valid semver formats. This is where the multiple sort values come
|
||||
in. You can specify "alpha" to sort the remainder alphabetically or "time" to sort chronologically (most recent
|
||||
commit first).
|
||||
|
||||
This setting may also be specified in your conf.py file. It must be a tuple of strings:
|
||||
|
||||
.. code-block:: python
|
||||
|
||||
scv_sort = ('semver',)
|
||||
|
||||
.. option:: -t, --greatest-tag, scv_greatest_tag
|
||||
|
||||
Override root-ref to be the tag with the highest version number. If no tags have docs then this option is ignored
|
||||
and :option:`--root-ref` is used.
|
||||
|
||||
This setting may also be specified in your conf.py file. It must be a boolean:
|
||||
|
||||
.. code-block:: python
|
||||
|
||||
scv_greatest_tag = True
|
||||
|
||||
.. option:: -T, --recent-tag, scv_recent_tag
|
||||
|
||||
Override root-ref to be the most recent committed tag. If no tags have docs then this option is ignored and
|
||||
:option:`--root-ref` is used.
|
||||
|
||||
This setting may also be specified in your conf.py file. It must be a boolean:
|
||||
|
||||
.. code-block:: python
|
||||
|
||||
scv_recent_tag = True
|
||||
|
||||
.. option:: -w <pattern>, --whitelist-branches <pattern>, scv_whitelist_branches
|
||||
|
||||
Filter out branches not matching the pattern. Can be a simple string or a regex pattern. Specify multiple times to
|
||||
include more patterns in the whitelist.
|
||||
|
||||
This setting may also be specified in your conf.py file. It must be a tuple of either strings or ``re.compile()``
|
||||
objects:
|
||||
|
||||
.. code-block:: python
|
||||
|
||||
scv_whitelist_branches = ('master', 'latest')
|
||||
|
||||
.. option:: -W <pattern>, --whitelist-tags <pattern>, scv_whitelist_tags
|
||||
|
||||
Same as :option:`--whitelist-branches` but for git tags instead.
|
||||
|
||||
This setting may also be specified in your conf.py file. It must be a tuple of either strings or ``re.compile()``
|
||||
objects:
|
||||
|
||||
.. code-block:: python
|
||||
|
||||
scv_whitelist_tags = (re.compile(r'^v\d+\.\d+\.\d+$'),)
|
||||
|
56
docs/themes.rst
Normal file
|
@ -0,0 +1,56 @@
|
|||
.. _themes:
|
||||
|
||||
================
|
||||
Supported Themes
|
||||
================
|
||||
|
||||
Below are screen shots of the supported built-in Sphinx themes. You can the "Versions" section in each screen shot on
|
||||
sidebars.
|
||||
|
||||
sphinx_rtd_theme
|
||||
----------------
|
||||
|
||||
.. figure:: screenshots/sphinx_rtd_theme.png
|
||||
:target: _images/sphinx_rtd_theme.png
|
||||
|
||||
alabaster
|
||||
---------
|
||||
|
||||
.. figure:: screenshots/alabaster.png
|
||||
:target: _images/alabaster.png
|
||||
|
||||
classic
|
||||
-------
|
||||
|
||||
.. figure:: screenshots/classic.png
|
||||
:target: _images/classic.png
|
||||
|
||||
nature
|
||||
------
|
||||
|
||||
.. figure:: screenshots/nature.png
|
||||
:target: _images/nature.png
|
||||
|
||||
sphinxdoc
|
||||
---------
|
||||
|
||||
.. figure:: screenshots/sphinxdoc.png
|
||||
:target: _images/sphinxdoc.png
|
||||
|
||||
bizstyle
|
||||
--------
|
||||
|
||||
.. figure:: screenshots/bizstyle.png
|
||||
:target: _images/bizstyle.png
|
||||
|
||||
pyramid
|
||||
-------
|
||||
|
||||
.. figure:: screenshots/pyramid.png
|
||||
:target: _images/pyramid.png
|
||||
|
||||
traditional
|
||||
-----------
|
||||
|
||||
.. figure:: screenshots/traditional.png
|
||||
:target: _images/traditional.png
|
62
docs/tutorial.rst
Normal file
|
@ -0,0 +1,62 @@
|
|||
.. _tutorial:
|
||||
|
||||
========
|
||||
Tutorial
|
||||
========
|
||||
|
||||
This guide will go over the basics of the project.
|
||||
|
||||
Make sure that you've already :ref:`installed <install>` it.
|
||||
|
||||
.. note::
|
||||
|
||||
If you have installed `sphinx-versions` with `pipenv` (which you should), you need to prefix your ``sphinx-versioning`` commands with ``pipenv run ...`` or execute them in your virtualenv (see `pipenv documentation <https://pipenv.readthedocs.io/>`_ for more information on this matter).
|
||||
|
||||
Building Docs Locally
|
||||
=====================
|
||||
|
||||
Before we begin make sure you have some Sphinx docs already in your project. If not, read `First Steps with Sphinx <http://www.sphinx-doc.org/en/stable/tutorial.html>`_ first. If you just want something quick
|
||||
and dirty you can do the following:
|
||||
|
||||
.. code-block:: bash
|
||||
|
||||
git checkout -b feature_branch master # Create new branch from master.
|
||||
mkdir docs # All documentation will go here (optional, can be anywhere).
|
||||
echo "master_doc = 'index'" > docs/conf.py # Create Sphinx config file.
|
||||
echo -e "Test\n====\n\nSample Documentation" > docs/index.rst # Create one doc.
|
||||
git add docs
|
||||
git commit
|
||||
git push origin feature_branch # Required.
|
||||
|
||||
.. note::
|
||||
|
||||
It is **required** to push doc files to origin. sphinx-versions only works with remote branches/tags and ignores any
|
||||
local changes (committed, staged, unstaged, etc). If you don't push to origin sphinx-versions won't see them. This
|
||||
eliminates race conditions when multiple CI jobs are building docs at the same time.
|
||||
|
||||
.. _build-all-versions:
|
||||
|
||||
Build All Versions
|
||||
------------------
|
||||
|
||||
Now that you've got docs pushed to origin and they build fine with ``sphinx-build`` let's try building them with
|
||||
sphinx-versions:
|
||||
|
||||
.. code-block:: bash
|
||||
|
||||
sphinx-versioning build -r feature_branch docs docs/_build/html
|
||||
open docs/_build/html/index.html
|
||||
|
||||
More information about all of the options can be found at :ref:`settings` or by running with ``--help`` but just for
|
||||
convenience:
|
||||
|
||||
* ``-r feature_branch`` tells the program to build our newly created/pushed branch at the root of the "html" directory.
|
||||
We do this assuming there are no docs in master yet. Otherwise you can omit this argument.
|
||||
* ``docs/_build/html`` is the destination directory that holds generated HTML files.
|
||||
* The final ``docs`` argument is the directory where we put our RST files in, relative to the git root (e.g. if you
|
||||
clone your repo to another directory, that would be the git root directory). You can add more relative paths if you've
|
||||
moved the location of your RST files between different branches/tags.
|
||||
|
||||
The command should have worked and your docs should be available in `docs/_build/html/index.html` with a "Versions"
|
||||
section in the sidebar.
|
||||
|
105
setup.py
Executable file
|
@ -0,0 +1,105 @@
|
|||
#!/usr/bin/env python
|
||||
"""Setup script for the project."""
|
||||
|
||||
from __future__ import print_function
|
||||
|
||||
import codecs
|
||||
import os
|
||||
import re
|
||||
|
||||
from setuptools import Command, setup
|
||||
|
||||
IMPORT = 'sphinxcontrib.versioning'
|
||||
INSTALL_REQUIRES = ['click', 'colorclass', 'sphinx']
|
||||
LICENSE = 'MIT'
|
||||
NAME = 'sphinx-versions'
|
||||
VERSION = '1.0.0'
|
||||
|
||||
|
||||
def readme(path='README.rst'):
|
||||
"""Try to read README.rst or return empty string if failed.
|
||||
|
||||
:param str path: Path to README file.
|
||||
|
||||
:return: File contents.
|
||||
:rtype: str
|
||||
"""
|
||||
path = os.path.realpath(os.path.join(os.path.dirname(__file__), path))
|
||||
handle = None
|
||||
try:
|
||||
handle = codecs.open(path, encoding='utf-8')
|
||||
return handle.read(131072)
|
||||
except IOError:
|
||||
return ''
|
||||
finally:
|
||||
getattr(handle, 'close', lambda: None)()
|
||||
|
||||
|
||||
class CheckVersion(Command):
|
||||
"""Make sure version strings and other metadata match here, in module/package, and other places."""
|
||||
|
||||
description = 'verify consistent version/etc strings in project'
|
||||
user_options = []
|
||||
|
||||
@classmethod
|
||||
def initialize_options(cls):
|
||||
"""Required by distutils."""
|
||||
pass
|
||||
|
||||
@classmethod
|
||||
def finalize_options(cls):
|
||||
"""Required by distutils."""
|
||||
pass
|
||||
|
||||
@classmethod
|
||||
def run(cls):
|
||||
"""Check variables."""
|
||||
project = __import__(IMPORT, fromlist=[''])
|
||||
for expected, var in [(LICENSE, '__license__'), (VERSION, '__version__')]:
|
||||
if getattr(project, var) != expected:
|
||||
raise SystemExit('Mismatch: {0}'.format(var))
|
||||
# Check changelog.
|
||||
if not re.compile(r'^%s - \d{4}-\d{2}-\d{2}[\r\n]' % VERSION, re.MULTILINE).search(readme()):
|
||||
raise SystemExit('Version not found in readme/changelog file.')
|
||||
|
||||
|
||||
if __name__ == '__main__':
|
||||
setup(
|
||||
author='Smile',
|
||||
author_email='cyrille.gachot@smile.fr',
|
||||
classifiers=[
|
||||
'Development Status :: 5 - Production/Stable',
|
||||
'Environment :: Console',
|
||||
'Framework :: Sphinx :: Extension',
|
||||
'Intended Audience :: Developers',
|
||||
'License :: OSI Approved :: MIT License',
|
||||
'Operating System :: MacOS',
|
||||
'Operating System :: POSIX :: Linux',
|
||||
'Operating System :: POSIX',
|
||||
'Programming Language :: Python :: 2.7',
|
||||
'Programming Language :: Python :: 3.3',
|
||||
'Programming Language :: Python :: 3.4',
|
||||
'Programming Language :: Python :: 3.5',
|
||||
'Programming Language :: Python :: Implementation :: PyPy',
|
||||
'Topic :: Documentation :: Sphinx',
|
||||
'Topic :: Software Development :: Documentation',
|
||||
],
|
||||
cmdclass=dict(check_version=CheckVersion),
|
||||
description='Sphinx extension that allows building versioned docs for self-hosting.',
|
||||
entry_points={'console_scripts': ['sphinx-versioning = sphinxcontrib.versioning.__main__:cli']},
|
||||
install_requires=INSTALL_REQUIRES,
|
||||
keywords='sphinx versioning versions version branches tags',
|
||||
license=LICENSE,
|
||||
long_description=readme(),
|
||||
name=NAME,
|
||||
package_data={'': [
|
||||
os.path.join('_static', 'banner.css'),
|
||||
os.path.join('_templates', 'banner.html'),
|
||||
os.path.join('_templates', 'layout.html'),
|
||||
os.path.join('_templates', 'versions.html'),
|
||||
]},
|
||||
packages=['sphinxcontrib', os.path.join('sphinxcontrib', 'versioning')],
|
||||
url='https://github.com/Smile-SA/' + NAME,
|
||||
version=VERSION,
|
||||
zip_safe=False,
|
||||
)
|
3
sphinxcontrib/__init__.py
Normal file
|
@ -0,0 +1,3 @@
|
|||
"""Declare namespace."""
|
||||
|
||||
__import__('pkg_resources').declare_namespace(__name__)
|
7
sphinxcontrib/versioning/__init__.py
Normal file
|
@ -0,0 +1,7 @@
|
|||
"""
|
||||
Sphinx extension that allows building versioned docs for self-hosting.
|
||||
"""
|
||||
|
||||
__author__ = 'Smile'
|
||||
__license__ = 'MIT'
|
||||
__version__ = '1.0.0'
|
319
sphinxcontrib/versioning/__main__.py
Executable file
|
@ -0,0 +1,319 @@
|
|||
"""Entry point of project via setuptools which calls cli()."""
|
||||
|
||||
import logging
|
||||
import os
|
||||
import shutil
|
||||
import time
|
||||
|
||||
import click
|
||||
|
||||
from sphinxcontrib.versioning import __version__
|
||||
from sphinxcontrib.versioning.git import clone, get_root, GitError
|
||||
from sphinxcontrib.versioning.lib import Config, HandledError, TempDir
|
||||
from sphinxcontrib.versioning.routines import build_all, gather_git_info, pre_build, read_local_conf
|
||||
from sphinxcontrib.versioning.setup_logging import setup_logging
|
||||
from sphinxcontrib.versioning.versions import multi_sort, Versions
|
||||
|
||||
IS_EXISTS_DIR = click.Path(exists=True, file_okay=False, dir_okay=True)
|
||||
IS_EXISTS_FILE = click.Path(exists=True, file_okay=True, dir_okay=False)
|
||||
NO_EXECUTE = False # Used in tests.
|
||||
PUSH_RETRIES = 3
|
||||
PUSH_SLEEP = 3 # Seconds.
|
||||
|
||||
|
||||
class ClickGroup(click.Group):
|
||||
"""Truncate docstrings at form-feed character and implement overflow arguments."""
|
||||
|
||||
def __init__(self, *args, **kwargs):
|
||||
"""Constructor.
|
||||
|
||||
:param list args: Passed to super().
|
||||
:param dict kwargs: Passed to super().
|
||||
"""
|
||||
self.overflow = None
|
||||
if 'help' in kwargs and kwargs['help'] and '\f' in kwargs['help']:
|
||||
kwargs['help'] = kwargs['help'].split('\f', 1)[0]
|
||||
super(ClickGroup, self).__init__(*args, **kwargs)
|
||||
|
||||
@staticmethod
|
||||
def custom_sort(param):
|
||||
"""Custom Click(Command|Group).params sorter.
|
||||
|
||||
Case insensitive sort with capitals after lowercase. --version at the end since I can't sort --help.
|
||||
|
||||
:param click.core.Option param: Parameter to evaluate.
|
||||
|
||||
:return: Sort weight.
|
||||
:rtype: int
|
||||
"""
|
||||
option = param.opts[0].lstrip('-')
|
||||
if param.param_type_name != 'option':
|
||||
return False,
|
||||
return True, option == 'version', option.lower(), option.swapcase()
|
||||
|
||||
def get_params(self, ctx):
|
||||
"""Sort order of options before displaying.
|
||||
|
||||
:param click.core.Context ctx: Click context.
|
||||
|
||||
:return: super() return value.
|
||||
"""
|
||||
self.params.sort(key=self.custom_sort)
|
||||
return super(ClickGroup, self).get_params(ctx)
|
||||
|
||||
def main(self, *args, **kwargs):
|
||||
"""Main function called by setuptools.
|
||||
|
||||
:param list args: Passed to super().
|
||||
:param dict kwargs: Passed to super().
|
||||
|
||||
:return: super() return value.
|
||||
"""
|
||||
argv = kwargs.pop('args', click.get_os_args())
|
||||
if '--' in argv:
|
||||
pos = argv.index('--')
|
||||
argv, self.overflow = argv[:pos], tuple(argv[pos + 1:])
|
||||
else:
|
||||
argv, self.overflow = argv, tuple()
|
||||
return super(ClickGroup, self).main(args=argv, *args, **kwargs)
|
||||
|
||||
def invoke(self, ctx):
|
||||
"""Inject overflow arguments into context state.
|
||||
|
||||
:param click.core.Context ctx: Click context.
|
||||
|
||||
:return: super() return value.
|
||||
"""
|
||||
if self.overflow:
|
||||
ctx.ensure_object(Config).update(dict(overflow=self.overflow))
|
||||
return super(ClickGroup, self).invoke(ctx)
|
||||
|
||||
|
||||
class ClickCommand(click.Command):
|
||||
"""Truncate docstrings at form-feed character for click.command()."""
|
||||
|
||||
def __init__(self, *args, **kwargs):
|
||||
"""Constructor."""
|
||||
if 'help' in kwargs and kwargs['help'] and '\f' in kwargs['help']:
|
||||
kwargs['help'] = kwargs['help'].split('\f', 1)[0]
|
||||
super(ClickCommand, self).__init__(*args, **kwargs)
|
||||
|
||||
def get_params(self, ctx):
|
||||
"""Sort order of options before displaying.
|
||||
|
||||
:param click.core.Context ctx: Click context.
|
||||
|
||||
:return: super() return value.
|
||||
"""
|
||||
self.params.sort(key=ClickGroup.custom_sort)
|
||||
return super(ClickCommand, self).get_params(ctx)
|
||||
|
||||
|
||||
@click.group(cls=ClickGroup)
|
||||
@click.option('-c', '--chdir', help='Make this the current working directory before running.', type=IS_EXISTS_DIR)
|
||||
@click.option('-g', '--git-root', help='Path to directory in the local repo. Default is CWD.', type=IS_EXISTS_DIR)
|
||||
@click.option('-l', '--local-conf', help='Path to conf.py for sphinx-versions to read config from.', type=IS_EXISTS_FILE)
|
||||
@click.option('-L', '--no-local-conf', help="Don't attempt to search for nor load a local conf.py file.", is_flag=True)
|
||||
@click.option('-N', '--no-colors', help='Disable colors in the terminal output.', is_flag=True)
|
||||
@click.option('-v', '--verbose', help='Debug logging. Specify more than once for more logging.', count=True)
|
||||
@click.version_option(version=__version__)
|
||||
@click.make_pass_decorator(Config, ensure=True)
|
||||
def cli(config, **options):
|
||||
"""Build versioned Sphinx docs for every branch and tag pushed to origin.
|
||||
|
||||
Supports only building locally with the "build" sub command
|
||||
For more information, run with its own --help.
|
||||
|
||||
The options below are global and must be specified before the sub command name (e.g. -N build ...).
|
||||
\f
|
||||
|
||||
:param sphinxcontrib.versioning.lib.Config config: Runtime configuration.
|
||||
:param dict options: Additional Click options.
|
||||
"""
|
||||
def pre(rel_source):
|
||||
"""To be executed in a Click sub command.
|
||||
|
||||
Needed because if this code is in cli() it will be executed when the user runs: <command> <sub command> --help
|
||||
|
||||
:param tuple rel_source: Possible relative paths (to git root) of Sphinx directory containing conf.py.
|
||||
"""
|
||||
# Setup logging.
|
||||
if not NO_EXECUTE:
|
||||
setup_logging(verbose=config.verbose, colors=not config.no_colors)
|
||||
log = logging.getLogger(__name__)
|
||||
|
||||
# Change current working directory.
|
||||
if config.chdir:
|
||||
os.chdir(config.chdir)
|
||||
log.debug('Working directory: %s', os.getcwd())
|
||||
else:
|
||||
config.update(dict(chdir=os.getcwd()), overwrite=True)
|
||||
|
||||
# Get and verify git root.
|
||||
try:
|
||||
config.update(dict(git_root=get_root(config.git_root or os.getcwd())), overwrite=True)
|
||||
except GitError as exc:
|
||||
log.error(exc.message)
|
||||
log.error(exc.output)
|
||||
raise HandledError
|
||||
|
||||
# Look for local config.
|
||||
if config.no_local_conf:
|
||||
config.update(dict(local_conf=None), overwrite=True)
|
||||
elif not config.local_conf:
|
||||
candidates = [p for p in (os.path.join(s, 'conf.py') for s in rel_source) if os.path.isfile(p)]
|
||||
if candidates:
|
||||
config.update(dict(local_conf=candidates[0]), overwrite=True)
|
||||
else:
|
||||
log.debug("Didn't find a conf.py in any REL_SOURCE.")
|
||||
elif os.path.basename(config.local_conf) != 'conf.py':
|
||||
log.error('Path "%s" must end with conf.py.', config.local_conf)
|
||||
raise HandledError
|
||||
config['pre'] = pre # To be called by Click sub commands.
|
||||
config.update(options)
|
||||
|
||||
|
||||
def build_options(func):
|
||||
"""Add "build" Click options to function.
|
||||
|
||||
:param function func: The function to wrap.
|
||||
|
||||
:return: The wrapped function.
|
||||
:rtype: function
|
||||
"""
|
||||
func = click.option('-a', '--banner-greatest-tag', is_flag=True,
|
||||
help='Override banner-main-ref to be the tag with the highest version number.')(func)
|
||||
func = click.option('-A', '--banner-recent-tag', is_flag=True,
|
||||
help='Override banner-main-ref to be the most recent committed tag.')(func)
|
||||
func = click.option('-b', '--show-banner', help='Show a warning banner.', is_flag=True)(func)
|
||||
func = click.option('-B', '--banner-main-ref',
|
||||
help="Don't show banner on this ref and point banner URLs to this ref. Default master.")(func)
|
||||
func = click.option('-i', '--invert', help='Invert/reverse order of versions.', is_flag=True)(func)
|
||||
func = click.option('-p', '--priority', type=click.Choice(('branches', 'tags')),
|
||||
help="Group these kinds of versions at the top (for themes that don't separate them).")(func)
|
||||
func = click.option('-r', '--root-ref',
|
||||
help='The branch/tag at the root of DESTINATION. Will also be in subdir. Default master.')(func)
|
||||
func = click.option('-s', '--sort', multiple=True, type=click.Choice(('semver', 'alpha', 'time')),
|
||||
help='Sort versions. Specify multiple times to sort equal values of one kind.')(func)
|
||||
func = click.option('-t', '--greatest-tag', is_flag=True,
|
||||
help='Override root-ref to be the tag with the highest version number.')(func)
|
||||
func = click.option('-T', '--recent-tag', is_flag=True,
|
||||
help='Override root-ref to be the most recent committed tag.')(func)
|
||||
func = click.option('-w', '--whitelist-branches', multiple=True,
|
||||
help='Whitelist branches that match the pattern. Can be specified more than once.')(func)
|
||||
func = click.option('-W', '--whitelist-tags', multiple=True,
|
||||
help='Whitelist tags that match the pattern. Can be specified more than once.')(func)
|
||||
|
||||
return func
|
||||
|
||||
|
||||
def override_root_main_ref(config, remotes, banner):
|
||||
"""Override root_ref or banner_main_ref with tags in config if user requested.
|
||||
|
||||
:param sphinxcontrib.versioning.lib.Config config: Runtime configuration.
|
||||
:param iter remotes: List of dicts from Versions.remotes.
|
||||
:param bool banner: Evaluate banner main ref instead of root ref.
|
||||
|
||||
:return: If root/main ref exists.
|
||||
:rtype: bool
|
||||
"""
|
||||
log = logging.getLogger(__name__)
|
||||
greatest_tag = config.banner_greatest_tag if banner else config.greatest_tag
|
||||
recent_tag = config.banner_recent_tag if banner else config.recent_tag
|
||||
|
||||
if greatest_tag or recent_tag:
|
||||
candidates = [r for r in remotes if r['kind'] == 'tags']
|
||||
if candidates:
|
||||
multi_sort(candidates, ['semver' if greatest_tag else 'time'])
|
||||
config.update({'banner_main_ref' if banner else 'root_ref': candidates[0]['name']}, overwrite=True)
|
||||
else:
|
||||
flag = '--banner-main-ref' if banner else '--root-ref'
|
||||
log.warning('No git tags with docs found in remote. Falling back to %s value.', flag)
|
||||
|
||||
ref = config.banner_main_ref if banner else config.root_ref
|
||||
return ref in [r['name'] for r in remotes]
|
||||
|
||||
|
||||
@cli.command(cls=ClickCommand)
|
||||
@build_options
|
||||
@click.argument('REL_SOURCE', nargs=-1, required=True)
|
||||
@click.argument('DESTINATION', type=click.Path(file_okay=False, dir_okay=True))
|
||||
@click.make_pass_decorator(Config)
|
||||
def build(config, rel_source, destination, **options):
|
||||
"""Fetch branches/tags and build all locally.
|
||||
|
||||
Just fetch all remote branches and tags, export them to a temporary directory, run
|
||||
sphinx-build on each one, and then store all built documentation in DESTINATION.
|
||||
|
||||
REL_SOURCE is the path to the docs directory relative to the git root. If the source directory has moved around
|
||||
between git tags you can specify additional directories.
|
||||
|
||||
DESTINATION is the path to the local directory that will hold all generated docs for all versions.
|
||||
|
||||
To pass options to sphinx-build (run for every branch/tag) use a double hyphen
|
||||
(e.g. build docs docs/_build/html -- -D setting=value).
|
||||
\f
|
||||
|
||||
:param sphinxcontrib.versioning.lib.Config config: Runtime configuration.
|
||||
:param tuple rel_source: Possible relative paths (to git root) of Sphinx directory containing conf.py (e.g. docs).
|
||||
:param str destination: Destination directory to copy/overwrite built docs to. Does not delete old files.
|
||||
:param dict options: Additional Click options.
|
||||
"""
|
||||
if 'pre' in config:
|
||||
config.pop('pre')(rel_source)
|
||||
config.update({k: v for k, v in options.items() if v})
|
||||
if config.local_conf:
|
||||
config.update(read_local_conf(config.local_conf), ignore_set=True)
|
||||
if NO_EXECUTE:
|
||||
raise RuntimeError(config, rel_source, destination)
|
||||
log = logging.getLogger(__name__)
|
||||
|
||||
# Gather git data.
|
||||
log.info('Gathering info about the remote git repository...')
|
||||
conf_rel_paths = [os.path.join(s, 'conf.py') for s in rel_source]
|
||||
remotes = gather_git_info(config.git_root, conf_rel_paths, config.whitelist_branches, config.whitelist_tags)
|
||||
if not remotes:
|
||||
log.error('No docs found in any remote branch/tag. Nothing to do.')
|
||||
raise HandledError
|
||||
versions = Versions(
|
||||
remotes,
|
||||
sort=config.sort,
|
||||
priority=config.priority,
|
||||
invert=config.invert,
|
||||
)
|
||||
|
||||
# Get root ref.
|
||||
if not override_root_main_ref(config, versions.remotes, False):
|
||||
log.error('Root ref %s not found in: %s', config.root_ref, ' '.join(r[1] for r in remotes))
|
||||
raise HandledError
|
||||
log.info('Root ref is: %s', config.root_ref)
|
||||
|
||||
# Get banner main ref.
|
||||
if not config.show_banner:
|
||||
config.update(dict(banner_greatest_tag=False, banner_main_ref=None, banner_recent_tag=False), overwrite=True)
|
||||
elif not override_root_main_ref(config, versions.remotes, True):
|
||||
log.warning('Banner main ref %s not found in: %s', config.banner_main_ref, ' '.join(r[1] for r in remotes))
|
||||
log.warning('Disabling banner.')
|
||||
config.update(dict(banner_greatest_tag=False, banner_main_ref=None, banner_recent_tag=False, show_banner=False),
|
||||
overwrite=True)
|
||||
else:
|
||||
log.info('Banner main ref is: %s', config.banner_main_ref)
|
||||
|
||||
# Pre-build.
|
||||
log.info("Pre-running Sphinx to collect versions' master_doc and other info.")
|
||||
exported_root = pre_build(config.git_root, versions)
|
||||
if config.banner_main_ref and config.banner_main_ref not in [r['name'] for r in versions.remotes]:
|
||||
log.warning('Banner main ref %s failed during pre-run. Disabling banner.', config.banner_main_ref)
|
||||
config.update(dict(banner_greatest_tag=False, banner_main_ref=None, banner_recent_tag=False, show_banner=False),
|
||||
overwrite=True)
|
||||
|
||||
# Build.
|
||||
build_all(exported_root, destination, versions)
|
||||
|
||||
# Cleanup.
|
||||
log.debug('Removing: %s', exported_root)
|
||||
shutil.rmtree(exported_root)
|
||||
|
||||
# Store versions in state for push().
|
||||
config['versions'] = versions
|
||||
|
41
sphinxcontrib/versioning/_static/banner.css
Normal file
|
@ -0,0 +1,41 @@
|
|||
.scv-banner {
|
||||
padding: 3px;
|
||||
border-radius: 2px;
|
||||
font-size: 80%;
|
||||
text-align: center;
|
||||
color: white;
|
||||
background: #d40 linear-gradient(-45deg,
|
||||
rgba(255, 255, 255, 0.2) 0%,
|
||||
rgba(255, 255, 255, 0.2) 25%,
|
||||
transparent 25%,
|
||||
transparent 50%,
|
||||
rgba(255, 255, 255, 0.2) 50%,
|
||||
rgba(255, 255, 255, 0.2) 75%,
|
||||
transparent 75%,
|
||||
transparent
|
||||
);
|
||||
background-size: 28px 28px;
|
||||
}
|
||||
.scv-banner > a {
|
||||
color: white;
|
||||
}
|
||||
|
||||
|
||||
.scv-sphinx_rtd_theme {
|
||||
background-color: #2980B9;
|
||||
}
|
||||
|
||||
|
||||
.scv-bizstyle {
|
||||
background-color: #336699;
|
||||
}
|
||||
|
||||
|
||||
.scv-classic {
|
||||
text-align: center !important;
|
||||
}
|
||||
|
||||
|
||||
.scv-traditional {
|
||||
text-align: center !important;
|
||||
}
|
31
sphinxcontrib/versioning/_templates/banner.html
Normal file
|
@ -0,0 +1,31 @@
|
|||
{# Set banner color via CSS. #}
|
||||
{%- set banner_classes = 'scv-banner' %}
|
||||
{%- if html_theme in ('sphinx_rtd_theme', 'bizstyle', 'classic', 'traditional') %}
|
||||
{%- set banner_classes = banner_classes + ' scv-' + html_theme %}
|
||||
{%- endif %}
|
||||
|
||||
{# Set banner message. #}
|
||||
{%- if scv_banner_main_version != current_version %}
|
||||
{# Determine base message. #}
|
||||
{%- if scv_is_branch %}
|
||||
{%- set banner_message = '<b>Warning:</b> This document is for the development version of %s.'|format(project) %}
|
||||
{%- else %}
|
||||
{%- set banner_message = '<b>Warning:</b> This document is for an old version of %s.'|format(project) %}
|
||||
{%- endif %}
|
||||
{# Determine URL of main version. #}
|
||||
{%- if vhasdoc(scv_banner_main_version) %}
|
||||
{%- set banner_message = '<a href="%s">' + banner_message + ' The %s version is %s.</a>' %}
|
||||
{%- if scv_banner_main_ref_is_tag %}
|
||||
{%- set banner_message = banner_message|format(vpathto(scv_banner_main_version), 'latest', scv_banner_main_version) %}
|
||||
{%- else %}
|
||||
{%- set banner_message = banner_message|format(vpathto(scv_banner_main_version), 'main', scv_banner_main_version) %}
|
||||
{%- endif %}
|
||||
{%- endif %}
|
||||
{%- endif %}
|
||||
|
||||
{# Display banner. #}
|
||||
{% block banner %}
|
||||
{%- if banner_message %}
|
||||
<p class="{{ banner_classes }}">{{ banner_message }}</p>
|
||||
{%- endif %}
|
||||
{% endblock %}
|
64
sphinxcontrib/versioning/_templates/versions.html
Normal file
|
@ -0,0 +1,64 @@
|
|||
{% if html_theme == 'sphinx_rtd_theme' %}
|
||||
<div class="rst-versions" data-toggle="rst-versions" role="note" aria-label="versions">
|
||||
<span class="rst-current-version" data-toggle="rst-current-version">
|
||||
<span class="fa fa-book"> Other Versions</span>
|
||||
v: {{ current_version }}
|
||||
<span class="fa fa-caret-down"></span>
|
||||
</span>
|
||||
<div class="rst-other-versions">
|
||||
{%- if versions.tags %}
|
||||
<dl>
|
||||
<dt>Tags</dt>
|
||||
{%- for name, url in versions.tags %}
|
||||
<dd><a href="{{ url }}">{{ name }}</a></dd>
|
||||
{%- endfor %}
|
||||
</dl>
|
||||
{%- endif %}
|
||||
{%- if versions.branches %}
|
||||
<dl>
|
||||
<dt>Branches</dt>
|
||||
{%- for name, url in versions.branches %}
|
||||
<dd><a href="{{ url }}">{{ name }}</a></dd>
|
||||
{%- endfor %}
|
||||
</dl>
|
||||
{%- endif %}
|
||||
</div>
|
||||
</div>
|
||||
{% elif html_theme == 'rasabaster' %}
|
||||
<div class="versions">
|
||||
<p class="caption">Versions</p>
|
||||
<div class="versions-content">
|
||||
<div>
|
||||
<span class="current-version">
|
||||
viewing: {{ current_version }}
|
||||
</span>
|
||||
</div>
|
||||
<div class="other-versions">
|
||||
{%- if versions.tags %}
|
||||
<p>tags</p>
|
||||
<div class="dropdown-content">
|
||||
{%- for name, url in versions.tags %}
|
||||
<a href="{{ url }}">{{ name }}</a>
|
||||
{%- endfor %}
|
||||
</div>
|
||||
{%- endif %}
|
||||
{%- if versions.branches %}
|
||||
<p>branches</p>
|
||||
<div class="dropdown-content">
|
||||
{%- for name, url in versions.branches %}
|
||||
<a href="{{ url }}">{{ name }}</a>
|
||||
{%- endfor %}
|
||||
</div>
|
||||
{%- endif %}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{% else %}
|
||||
<h3>{{ _('Versions') }}</h3>
|
||||
<ul>
|
||||
{%- for name, url in versions %}
|
||||
<li><a href="{{ url }}">{{ name }}</a></li>
|
||||
{%- endfor %}
|
||||
</ul>
|
||||
{%- endif %}
|
393
sphinxcontrib/versioning/git.py
Normal file
|
@ -0,0 +1,393 @@
|
|||
"""Interface with git locally and remotely."""
|
||||
|
||||
import glob
|
||||
import json
|
||||
import logging
|
||||
import os
|
||||
import re
|
||||
import sys
|
||||
import tarfile
|
||||
import time
|
||||
from datetime import datetime
|
||||
from subprocess import CalledProcessError, PIPE, Popen, STDOUT
|
||||
|
||||
IS_WINDOWS = sys.platform == 'win32'
|
||||
RE_ALL_REMOTES = re.compile(r'([\w./-]+)\t([A-Za-z0-9@:/\\._-]+) \((fetch|push)\)\n')
|
||||
RE_REMOTE = re.compile(r'^(?P<sha>[0-9a-f]{5,40})\trefs/(?P<kind>heads|tags)/(?P<name>[\w./-]+(?:\^\{})?)$',
|
||||
re.MULTILINE)
|
||||
RE_UNIX_TIME = re.compile(r'^\d{10}$', re.MULTILINE)
|
||||
WHITELIST_ENV_VARS = (
|
||||
'APPVEYOR',
|
||||
'APPVEYOR_ACCOUNT_NAME',
|
||||
'APPVEYOR_BUILD_ID',
|
||||
'APPVEYOR_BUILD_NUMBER',
|
||||
'APPVEYOR_BUILD_VERSION',
|
||||
'APPVEYOR_FORCED_BUILD',
|
||||
'APPVEYOR_JOB_ID',
|
||||
'APPVEYOR_JOB_NAME',
|
||||
'APPVEYOR_PROJECT_ID',
|
||||
'APPVEYOR_PROJECT_NAME',
|
||||
'APPVEYOR_PROJECT_SLUG',
|
||||
'APPVEYOR_PULL_REQUEST_NUMBER',
|
||||
'APPVEYOR_PULL_REQUEST_TITLE',
|
||||
'APPVEYOR_RE_BUILD',
|
||||
'APPVEYOR_REPO_BRANCH',
|
||||
'APPVEYOR_REPO_COMMIT',
|
||||
'APPVEYOR_REPO_NAME',
|
||||
'APPVEYOR_REPO_PROVIDER',
|
||||
'APPVEYOR_REPO_TAG',
|
||||
'APPVEYOR_REPO_TAG_NAME',
|
||||
'APPVEYOR_SCHEDULED_BUILD',
|
||||
'CI',
|
||||
'CI_PULL_REQUEST',
|
||||
'CI_PULL_REQUESTS',
|
||||
'CIRCLE_BRANCH',
|
||||
'CIRCLE_BUILD_IMAGE',
|
||||
'CIRCLE_BUILD_NUM',
|
||||
'CIRCLE_BUILD_URL',
|
||||
'CIRCLE_COMPARE_URL',
|
||||
'CIRCLE_PR_NUMBER',
|
||||
'CIRCLE_PR_REPONAME',
|
||||
'CIRCLE_PR_USERNAME',
|
||||
'CIRCLE_PREVIOUS_BUILD_NUM',
|
||||
'CIRCLE_PROJECT_REPONAME',
|
||||
'CIRCLE_PROJECT_USERNAME',
|
||||
'CIRCLE_REPOSITORY_URL',
|
||||
'CIRCLE_SHA1',
|
||||
'CIRCLE_TAG',
|
||||
'CIRCLE_USERNAME',
|
||||
'CIRCLECI',
|
||||
'HOSTNAME',
|
||||
'LANG',
|
||||
'LC_ALL',
|
||||
'PLATFORM',
|
||||
'TRAVIS',
|
||||
'TRAVIS_BRANCH',
|
||||
'TRAVIS_BUILD_ID',
|
||||
'TRAVIS_BUILD_NUMBER',
|
||||
'TRAVIS_COMMIT',
|
||||
'TRAVIS_COMMIT_RANGE',
|
||||
'TRAVIS_EVENT_TYPE',
|
||||
'TRAVIS_JOB_ID',
|
||||
'TRAVIS_JOB_NUMBER',
|
||||
'TRAVIS_OS_NAME',
|
||||
'TRAVIS_PULL_REQUEST',
|
||||
'TRAVIS_PYTHON_VERSION',
|
||||
'TRAVIS_REPO_SLUG',
|
||||
'TRAVIS_SECURE_ENV_VARS',
|
||||
'TRAVIS_TAG',
|
||||
'TRAVIS_TEST_RESULT',
|
||||
'USER',
|
||||
)
|
||||
|
||||
|
||||
class GitError(Exception):
|
||||
"""Raised if git exits non-zero."""
|
||||
|
||||
def __init__(self, message, output):
|
||||
"""Constructor."""
|
||||
self.message = message
|
||||
self.output = output
|
||||
super(GitError, self).__init__(message, output)
|
||||
|
||||
|
||||
def chunk(iterator, max_size):
|
||||
"""Chunk a list/set/etc.
|
||||
|
||||
:param iter iterator: The iterable object to chunk.
|
||||
:param int max_size: Max size of each chunk. Remainder chunk may be smaller.
|
||||
|
||||
:return: Yield list of items.
|
||||
:rtype: iter
|
||||
"""
|
||||
gen = iter(iterator)
|
||||
while True:
|
||||
chunked = list()
|
||||
for i, item in enumerate(gen):
|
||||
chunked.append(item)
|
||||
if i >= max_size - 1:
|
||||
break
|
||||
if not chunked:
|
||||
return
|
||||
yield chunked
|
||||
|
||||
|
||||
def run_command(local_root, command, env_var=True, pipeto=None, retry=0, environ=None):
|
||||
"""Run a command and return the output.
|
||||
|
||||
:raise CalledProcessError: Command exits non-zero.
|
||||
|
||||
:param str local_root: Local path to git root directory.
|
||||
:param iter command: Command to run.
|
||||
:param dict environ: Environment variables to set/override in the command.
|
||||
:param bool env_var: Define GIT_DIR environment variable (on non-Windows).
|
||||
:param function pipeto: Pipe `command`'s stdout to this function (only parameter given).
|
||||
:param int retry: Retry this many times on CalledProcessError after 0.1 seconds.
|
||||
|
||||
:return: Command output.
|
||||
:rtype: str
|
||||
"""
|
||||
log = logging.getLogger(__name__)
|
||||
|
||||
# Setup env.
|
||||
env = os.environ.copy()
|
||||
if environ:
|
||||
env.update(environ)
|
||||
if env_var and not IS_WINDOWS:
|
||||
env['GIT_DIR'] = os.path.join(local_root, '.git')
|
||||
else:
|
||||
env.pop('GIT_DIR', None)
|
||||
|
||||
# Run command.
|
||||
with open(os.devnull) as null:
|
||||
main = Popen(command, cwd=local_root, env=env, stdout=PIPE, stderr=PIPE if pipeto else STDOUT, stdin=null)
|
||||
if pipeto:
|
||||
pipeto(main.stdout)
|
||||
main_output = main.communicate()[1].decode('utf-8') # Might deadlock if stderr is written to a lot.
|
||||
else:
|
||||
main_output = main.communicate()[0].decode('utf-8')
|
||||
log.debug(json.dumps(dict(cwd=local_root, command=command, code=main.poll(), output=main_output)))
|
||||
|
||||
# Verify success.
|
||||
if main.poll() != 0:
|
||||
if retry < 1:
|
||||
raise CalledProcessError(main.poll(), command, output=main_output)
|
||||
time.sleep(0.1)
|
||||
return run_command(local_root, command, env_var, pipeto, retry - 1)
|
||||
|
||||
return main_output
|
||||
|
||||
|
||||
def get_root(directory):
|
||||
"""Get root directory of the local git repo from any subdirectory within it.
|
||||
|
||||
:raise GitError: If git command fails (dir not a git repo?).
|
||||
|
||||
:param str directory: Subdirectory in the local repo.
|
||||
|
||||
:return: Root directory of repository.
|
||||
:rtype: str
|
||||
"""
|
||||
command = ['git', 'rev-parse', '--show-toplevel']
|
||||
try:
|
||||
output = run_command(directory, command, env_var=False)
|
||||
except CalledProcessError as exc:
|
||||
raise GitError('Failed to find local git repository root in {}.'.format(repr(directory)), exc.output)
|
||||
if IS_WINDOWS:
|
||||
output = output.replace('/', '\\')
|
||||
return output.strip()
|
||||
|
||||
|
||||
def list_remote(local_root):
|
||||
"""Get remote branch/tag latest SHAs.
|
||||
|
||||
:raise GitError: When git ls-remote fails.
|
||||
|
||||
:param str local_root: Local path to git root directory.
|
||||
|
||||
:return: List of tuples containing strings. Each tuple is sha, name, kind.
|
||||
:rtype: list
|
||||
"""
|
||||
command = ['git', 'ls-remote', '--heads', '--tags']
|
||||
try:
|
||||
output = run_command(local_root, command)
|
||||
except CalledProcessError as exc:
|
||||
raise GitError('Git failed to list remote refs.', exc.output)
|
||||
|
||||
# Dereference annotated tags if any. No need to fetch annotations.
|
||||
if '^{}' in output:
|
||||
parsed = list()
|
||||
for group in (m.groupdict() for m in RE_REMOTE.finditer(output)):
|
||||
dereferenced, name, kind = group['name'].endswith('^{}'), group['name'][:-3], group['kind']
|
||||
if dereferenced and parsed and kind == parsed[-1]['kind'] == 'tags' and name == parsed[-1]['name']:
|
||||
parsed[-1]['sha'] = group['sha']
|
||||
else:
|
||||
parsed.append(group)
|
||||
else:
|
||||
parsed = [m.groupdict() for m in RE_REMOTE.finditer(output)]
|
||||
|
||||
return [[i['sha'], i['name'], i['kind']] for i in parsed]
|
||||
|
||||
|
||||
def filter_and_date(local_root, conf_rel_paths, commits):
|
||||
"""Get commit Unix timestamps and first matching conf.py path. Exclude commits with no conf.py file.
|
||||
|
||||
:raise CalledProcessError: Unhandled git command failure.
|
||||
:raise GitError: A commit SHA has not been fetched.
|
||||
|
||||
:param str local_root: Local path to git root directory.
|
||||
:param iter conf_rel_paths: List of possible relative paths (to git root) of Sphinx conf.py (e.g. docs/conf.py).
|
||||
:param iter commits: List of commit SHAs.
|
||||
|
||||
:return: Commit time (seconds since Unix epoch) for each commit and conf.py path. SHA keys and [int, str] values.
|
||||
:rtype: dict
|
||||
"""
|
||||
dates_paths = dict()
|
||||
|
||||
# Filter without docs.
|
||||
for commit in commits:
|
||||
if commit in dates_paths:
|
||||
continue
|
||||
command = ['git', 'ls-tree', '--name-only', '-r', commit] + conf_rel_paths
|
||||
try:
|
||||
output = run_command(local_root, command)
|
||||
except CalledProcessError as exc:
|
||||
raise GitError('Git ls-tree failed on {0}'.format(commit), exc.output)
|
||||
if output:
|
||||
dates_paths[commit] = [None, output.splitlines()[0].strip()]
|
||||
|
||||
# Get timestamps by groups of 50.
|
||||
command_prefix = ['git', 'show', '-s', '--pretty=format:%ct']
|
||||
for commits_group in chunk(dates_paths, 50):
|
||||
command = command_prefix + commits_group
|
||||
output = run_command(local_root, command)
|
||||
timestamps = [int(i) for i in RE_UNIX_TIME.findall(output)]
|
||||
for i, commit in enumerate(commits_group):
|
||||
dates_paths[commit][0] = timestamps[i]
|
||||
|
||||
# Done.
|
||||
return dates_paths
|
||||
|
||||
|
||||
def fetch_commits(local_root, remotes):
|
||||
"""Fetch from origin.
|
||||
|
||||
:raise CalledProcessError: Unhandled git command failure.
|
||||
|
||||
:param str local_root: Local path to git root directory.
|
||||
:param iter remotes: Output of list_remote().
|
||||
"""
|
||||
# Fetch all known branches.
|
||||
command = ['git', 'fetch', 'origin']
|
||||
run_command(local_root, command)
|
||||
|
||||
# Fetch new branches/tags.
|
||||
for sha, name, kind in remotes:
|
||||
try:
|
||||
run_command(local_root, ['git', 'reflog', sha])
|
||||
except CalledProcessError:
|
||||
run_command(local_root, command + ['refs/{0}/{1}'.format(kind, name)])
|
||||
run_command(local_root, ['git', 'reflog', sha])
|
||||
|
||||
|
||||
def export(local_root, commit, target):
|
||||
"""Export git commit to directory. "Extracts" all files at the commit to the target directory.
|
||||
|
||||
Set mtime of RST files to last commit date.
|
||||
|
||||
:raise CalledProcessError: Unhandled git command failure.
|
||||
|
||||
:param str local_root: Local path to git root directory.
|
||||
:param str commit: Git commit SHA to export.
|
||||
:param str target: Directory to export to.
|
||||
"""
|
||||
log = logging.getLogger(__name__)
|
||||
target = os.path.realpath(target)
|
||||
mtimes = list()
|
||||
|
||||
# Define extract function.
|
||||
def extract(stdout):
|
||||
"""Extract tar archive from "git archive" stdout.
|
||||
|
||||
:param file stdout: Handle to git's stdout pipe.
|
||||
"""
|
||||
queued_links = list()
|
||||
try:
|
||||
with tarfile.open(fileobj=stdout, mode='r|') as tar:
|
||||
for info in tar:
|
||||
log.debug('name: %s; mode: %d; size: %s; type: %s', info.name, info.mode, info.size, info.type)
|
||||
path = os.path.realpath(os.path.join(target, info.name))
|
||||
if not path.startswith(target): # Handle bad paths.
|
||||
log.warning('Ignoring tar object path %s outside of target directory.', info.name)
|
||||
elif info.isdir(): # Handle directories.
|
||||
if not os.path.exists(path):
|
||||
os.makedirs(path, mode=info.mode)
|
||||
elif info.issym() or info.islnk(): # Queue links.
|
||||
queued_links.append(info)
|
||||
else: # Handle files.
|
||||
tar.extract(member=info, path=target)
|
||||
if os.path.splitext(info.name)[1].lower() == '.rst':
|
||||
mtimes.append(info.name)
|
||||
for info in queued_links:
|
||||
# There used to be a check for broken symlinks here, but it was buggy
|
||||
tar.extract(member=info, path=target)
|
||||
except tarfile.TarError as exc:
|
||||
log.debug('Failed to extract output from "git archive" command: %s', str(exc))
|
||||
|
||||
# Run command.
|
||||
run_command(local_root, ['git', 'archive', '--format=tar', commit], pipeto=extract)
|
||||
|
||||
# Set mtime.
|
||||
for file_path in mtimes:
|
||||
last_committed = int(run_command(local_root, ['git', 'log', '-n1', '--format=%at', commit, '--', file_path]))
|
||||
os.utime(os.path.join(target, file_path), (last_committed, last_committed))
|
||||
|
||||
|
||||
def clone(local_root, new_root, remote, branch, rel_dest, exclude):
|
||||
"""Clone "local_root" origin into a new directory and check out a specific branch. Optionally run "git rm".
|
||||
|
||||
:raise CalledProcessError: Unhandled git command failure.
|
||||
:raise GitError: Handled git failures.
|
||||
|
||||
:param str local_root: Local path to git root directory.
|
||||
:param str new_root: Local path empty directory in which branch will be cloned into.
|
||||
:param str remote: The git remote to clone from to.
|
||||
:param str branch: Checkout this branch.
|
||||
:param str rel_dest: Run "git rm" on this directory if exclude is truthy.
|
||||
:param iter exclude: List of strings representing relative file paths to exclude from "git rm".
|
||||
"""
|
||||
log = logging.getLogger(__name__)
|
||||
output = run_command(local_root, ['git', 'remote', '-v'])
|
||||
remotes = dict()
|
||||
for match in RE_ALL_REMOTES.findall(output):
|
||||
remotes.setdefault(match[0], [None, None])
|
||||
if match[2] == 'fetch':
|
||||
remotes[match[0]][0] = match[1]
|
||||
else:
|
||||
remotes[match[0]][1] = match[1]
|
||||
if not remotes:
|
||||
raise GitError('Git repo has no remotes.', output)
|
||||
if remote not in remotes:
|
||||
raise GitError('Git repo missing remote "{}".'.format(remote), output)
|
||||
|
||||
# Clone.
|
||||
try:
|
||||
run_command(new_root, ['git', 'clone', remotes[remote][0], '--depth=1', '--branch', branch, '.'])
|
||||
except CalledProcessError as exc:
|
||||
raise GitError('Failed to clone from remote repo URL.', exc.output)
|
||||
|
||||
# Make sure user didn't select a tag as their DEST_BRANCH.
|
||||
try:
|
||||
run_command(new_root, ['git', 'symbolic-ref', 'HEAD'])
|
||||
except CalledProcessError as exc:
|
||||
raise GitError('Specified branch is not a real branch.', exc.output)
|
||||
|
||||
# Copy all remotes from original repo.
|
||||
for name, (fetch, push) in remotes.items():
|
||||
try:
|
||||
run_command(new_root, ['git', 'remote', 'set-url' if name == 'origin' else 'add', name, fetch], retry=3)
|
||||
run_command(new_root, ['git', 'remote', 'set-url', '--push', name, push], retry=3)
|
||||
except CalledProcessError as exc:
|
||||
raise GitError('Failed to set git remote URL.', exc.output)
|
||||
|
||||
# Done if no exclude.
|
||||
if not exclude:
|
||||
return
|
||||
|
||||
# Resolve exclude paths.
|
||||
exclude_joined = [
|
||||
os.path.relpath(p, new_root) for e in exclude for p in glob.glob(os.path.join(new_root, rel_dest, e))
|
||||
]
|
||||
log.debug('Expanded %s to %s', repr(exclude), repr(exclude_joined))
|
||||
|
||||
# Do "git rm".
|
||||
try:
|
||||
run_command(new_root, ['git', 'rm', '-rf', rel_dest])
|
||||
except CalledProcessError as exc:
|
||||
raise GitError('"git rm" failed to remove ' + rel_dest, exc.output)
|
||||
|
||||
# Restore files in exclude.
|
||||
run_command(new_root, ['git', 'reset', 'HEAD'] + exclude_joined)
|
||||
run_command(new_root, ['git', 'checkout', '--'] + exclude_joined)
|
||||
|
||||
|
167
sphinxcontrib/versioning/lib.py
Normal file
|
@ -0,0 +1,167 @@
|
|||
"""Common objects used throughout the project."""
|
||||
|
||||
import atexit
|
||||
import functools
|
||||
import logging
|
||||
import os
|
||||
import shutil
|
||||
import tempfile
|
||||
import weakref
|
||||
|
||||
import click
|
||||
|
||||
|
||||
class Config(object):
|
||||
"""The global configuration and state of the running program."""
|
||||
|
||||
def __init__(self):
|
||||
"""Constructor."""
|
||||
self._already_set = set()
|
||||
self._program_state = dict()
|
||||
|
||||
# Booleans.
|
||||
self.banner_greatest_tag = False
|
||||
self.banner_recent_tag = False
|
||||
self.greatest_tag = False
|
||||
self.invert = False
|
||||
self.no_colors = False
|
||||
self.no_local_conf = False
|
||||
self.recent_tag = False
|
||||
self.show_banner = False
|
||||
|
||||
# Strings.
|
||||
self.banner_main_ref = 'master'
|
||||
self.chdir = None
|
||||
self.git_root = None
|
||||
self.local_conf = None
|
||||
self.priority = None
|
||||
self.root_ref = 'master'
|
||||
|
||||
# Tuples.
|
||||
self.overflow = tuple()
|
||||
self.sort = tuple()
|
||||
self.whitelist_branches = tuple()
|
||||
self.whitelist_tags = tuple()
|
||||
|
||||
# Integers.
|
||||
self.verbose = 0
|
||||
|
||||
def __contains__(self, item):
|
||||
"""Implement 'key in Config'.
|
||||
|
||||
:param str item: Key to search for.
|
||||
|
||||
:return: If item in self._program_state.
|
||||
:rtype: bool
|
||||
"""
|
||||
return item in self._program_state
|
||||
|
||||
def __iter__(self):
|
||||
"""Yield names and current values of attributes that can be set from Sphinx config files."""
|
||||
for name in (n for n in dir(self) if not n.startswith('_') and not callable(getattr(self, n))):
|
||||
yield name, getattr(self, name)
|
||||
|
||||
def __repr__(self):
|
||||
"""Class representation."""
|
||||
attributes = ('_program_state', 'verbose', 'root_ref', 'overflow')
|
||||
key_value_attrs = ', '.join('{}={}'.format(a, repr(getattr(self, a))) for a in attributes)
|
||||
return '<{}.{} {}>'.format(self.__class__.__module__, self.__class__.__name__, key_value_attrs)
|
||||
|
||||
def __setitem__(self, key, value):
|
||||
"""Implement Config[key] = value, updates self._program_state.
|
||||
|
||||
:param str key: Key to set in self._program_state.
|
||||
:param value: Value to set in self._program_state.
|
||||
"""
|
||||
self._program_state[key] = value
|
||||
|
||||
@classmethod
|
||||
def from_context(cls):
|
||||
"""Retrieve this class' instance from the current Click context.
|
||||
|
||||
:return: Instance of this class.
|
||||
:rtype: Config
|
||||
"""
|
||||
try:
|
||||
ctx = click.get_current_context()
|
||||
except RuntimeError:
|
||||
return cls()
|
||||
return ctx.find_object(cls)
|
||||
|
||||
def pop(self, *args):
|
||||
"""Pop item from self._program_state.
|
||||
|
||||
:param iter args: Passed to self._program_state.
|
||||
|
||||
:return: Object from self._program_state.pop().
|
||||
"""
|
||||
return self._program_state.pop(*args)
|
||||
|
||||
def update(self, params, ignore_set=False, overwrite=False):
|
||||
"""Set instance values from dictionary.
|
||||
|
||||
:param dict params: Click context params.
|
||||
:param bool ignore_set: Skip already-set values instead of raising AttributeError.
|
||||
:param bool overwrite: Allow overwriting already-set values.
|
||||
"""
|
||||
log = logging.getLogger(__name__)
|
||||
valid = {i[0] for i in self}
|
||||
for key, value in params.items():
|
||||
if not hasattr(self, key):
|
||||
raise AttributeError("'{}' object has no attribute '{}'".format(self.__class__.__name__, key))
|
||||
if key not in valid:
|
||||
message = "'{}' object does not support item assignment on '{}'"
|
||||
raise AttributeError(message.format(self.__class__.__name__, key))
|
||||
if key in self._already_set:
|
||||
if ignore_set:
|
||||
log.debug('%s already set in config, skipping.', key)
|
||||
continue
|
||||
if not overwrite:
|
||||
message = "'{}' object does not support item re-assignment on '{}'"
|
||||
raise AttributeError(message.format(self.__class__.__name__, key))
|
||||
setattr(self, key, value)
|
||||
self._already_set.add(key)
|
||||
|
||||
|
||||
class HandledError(click.ClickException):
|
||||
"""Abort the program."""
|
||||
|
||||
def __init__(self):
|
||||
"""Constructor."""
|
||||
super(HandledError, self).__init__(None)
|
||||
|
||||
def show(self, **_):
|
||||
"""Error messages should be logged before raising this exception."""
|
||||
logging.critical('Failure.')
|
||||
|
||||
|
||||
class TempDir(object):
|
||||
"""Similar to TemporaryDirectory in Python 3.x but with tuned weakref implementation."""
|
||||
|
||||
def __init__(self, defer_atexit=False):
|
||||
"""Constructor.
|
||||
|
||||
:param bool defer_atexit: cleanup() to atexit instead of after garbage collection.
|
||||
"""
|
||||
self.name = tempfile.mkdtemp('sphinxcontrib_versioning')
|
||||
if defer_atexit:
|
||||
atexit.register(shutil.rmtree, self.name, True)
|
||||
return
|
||||
try:
|
||||
weakref.finalize(self, shutil.rmtree, self.name, True)
|
||||
except AttributeError:
|
||||
weakref.proxy(self, functools.partial(shutil.rmtree, self.name, True))
|
||||
|
||||
def __enter__(self):
|
||||
"""Return directory path."""
|
||||
return self.name
|
||||
|
||||
def __exit__(self, *_):
|
||||
"""Cleanup when exiting context."""
|
||||
self.cleanup()
|
||||
|
||||
def cleanup(self):
|
||||
"""Recursively delete directory."""
|
||||
shutil.rmtree(self.name, onerror=lambda *a: os.chmod(a[1], __import__('stat').S_IWRITE) or os.unlink(a[1]))
|
||||
if os.path.exists(self.name):
|
||||
raise IOError(17, "File exists: '{}'".format(self.name))
|
185
sphinxcontrib/versioning/routines.py
Normal file
|
@ -0,0 +1,185 @@
|
|||
"""Functions that perform main tasks. Code is here instead of in __main__.py."""
|
||||
|
||||
import json
|
||||
import logging
|
||||
import os
|
||||
import re
|
||||
import subprocess
|
||||
|
||||
from sphinxcontrib.versioning.git import export, fetch_commits, filter_and_date, GitError, list_remote
|
||||
from sphinxcontrib.versioning.lib import Config, HandledError, TempDir
|
||||
from sphinxcontrib.versioning.sphinx_ import build, read_config
|
||||
|
||||
RE_INVALID_FILENAME = re.compile(r'[^0-9A-Za-z.-]')
|
||||
|
||||
|
||||
def read_local_conf(local_conf):
|
||||
"""Search for conf.py in any rel_source directory in CWD and if found read it and return.
|
||||
|
||||
:param str local_conf: Path to conf.py to read.
|
||||
|
||||
:return: Loaded conf.py.
|
||||
:rtype: dict
|
||||
"""
|
||||
log = logging.getLogger(__name__)
|
||||
|
||||
# Attempt to read.
|
||||
log.info('Reading config from %s...', local_conf)
|
||||
try:
|
||||
config = read_config(os.path.dirname(local_conf), '<local>')
|
||||
except HandledError:
|
||||
log.warning('Unable to read file, continuing with only CLI args.')
|
||||
return dict()
|
||||
|
||||
# Filter and return.
|
||||
return {k[4:]: v for k, v in config.items() if k.startswith('scv_') and not k[4:].startswith('_')}
|
||||
|
||||
|
||||
def gather_git_info(root, conf_rel_paths, whitelist_branches, whitelist_tags):
|
||||
"""Gather info about the remote git repository. Get list of refs.
|
||||
|
||||
:raise HandledError: If function fails with a handled error. Will be logged before raising.
|
||||
|
||||
:param str root: Root directory of repository.
|
||||
:param iter conf_rel_paths: List of possible relative paths (to git root) of Sphinx conf.py (e.g. docs/conf.py).
|
||||
:param iter whitelist_branches: Optional list of patterns to filter branches by.
|
||||
:param iter whitelist_tags: Optional list of patterns to filter tags by.
|
||||
|
||||
:return: Commits with docs. A list of tuples: (sha, name, kind, date, conf_rel_path).
|
||||
:rtype: list
|
||||
"""
|
||||
log = logging.getLogger(__name__)
|
||||
|
||||
# List remote.
|
||||
log.info('Getting list of all remote branches/tags...')
|
||||
try:
|
||||
remotes = list_remote(root)
|
||||
except GitError as exc:
|
||||
log.error(exc.message)
|
||||
log.error(exc.output)
|
||||
raise HandledError
|
||||
log.info('Found: %s', ' '.join(i[1] for i in remotes))
|
||||
|
||||
# Filter and date.
|
||||
try:
|
||||
try:
|
||||
dates_paths = filter_and_date(root, conf_rel_paths, (i[0] for i in remotes))
|
||||
except GitError:
|
||||
log.info('Need to fetch from remote...')
|
||||
fetch_commits(root, remotes)
|
||||
try:
|
||||
dates_paths = filter_and_date(root, conf_rel_paths, (i[0] for i in remotes))
|
||||
except GitError as exc:
|
||||
log.error(exc.message)
|
||||
log.error(exc.output)
|
||||
raise HandledError
|
||||
except subprocess.CalledProcessError as exc:
|
||||
log.error(json.dumps(dict(command=exc.cmd, cwd=root, code=exc.returncode, output=exc.output)))
|
||||
log.error('Failed to get dates for all remote commits.')
|
||||
raise HandledError
|
||||
filtered_remotes = [[i[0], i[1], i[2], ] + dates_paths[i[0]] for i in remotes if i[0] in dates_paths]
|
||||
log.info('With docs: %s', ' '.join(i[1] for i in filtered_remotes))
|
||||
if not whitelist_branches and not whitelist_tags:
|
||||
return filtered_remotes
|
||||
|
||||
# Apply whitelist.
|
||||
whitelisted_remotes = list()
|
||||
for remote in filtered_remotes:
|
||||
if remote[2] == 'heads' and whitelist_branches:
|
||||
if not any(re.search(p, remote[1]) for p in whitelist_branches):
|
||||
continue
|
||||
if remote[2] == 'tags' and whitelist_tags:
|
||||
if not any(re.search(p, remote[1]) for p in whitelist_tags):
|
||||
continue
|
||||
whitelisted_remotes.append(remote)
|
||||
log.info('Passed whitelisting: %s', ' '.join(i[1] for i in whitelisted_remotes))
|
||||
|
||||
return whitelisted_remotes
|
||||
|
||||
|
||||
def pre_build(local_root, versions):
|
||||
"""Build docs for all versions to determine root directory and master_doc names.
|
||||
|
||||
Need to build docs to (a) avoid filename collision with files from root_ref and branch/tag names and (b) determine
|
||||
master_doc config values for all versions (in case master_doc changes from e.g. contents.rst to index.rst between
|
||||
versions).
|
||||
|
||||
Exports all commits into a temporary directory and returns the path to avoid re-exporting during the final build.
|
||||
|
||||
:param str local_root: Local path to git root directory.
|
||||
:param sphinxcontrib.versioning.versions.Versions versions: Versions class instance.
|
||||
|
||||
:return: Tempdir path with exported commits as subdirectories.
|
||||
:rtype: str
|
||||
"""
|
||||
log = logging.getLogger(__name__)
|
||||
exported_root = TempDir(True).name
|
||||
|
||||
# Extract all.
|
||||
for sha in {r['sha'] for r in versions.remotes}:
|
||||
target = os.path.join(exported_root, sha)
|
||||
log.debug('Exporting %s to temporary directory.', sha)
|
||||
export(local_root, sha, target)
|
||||
|
||||
# Build root.
|
||||
remote = versions[Config.from_context().root_ref]
|
||||
with TempDir() as temp_dir:
|
||||
log.debug('Building root (before setting root_dirs) in temporary directory: %s', temp_dir)
|
||||
source = os.path.dirname(os.path.join(exported_root, remote['sha'], remote['conf_rel_path']))
|
||||
build(source, temp_dir, versions, remote['name'], True)
|
||||
existing = os.listdir(temp_dir)
|
||||
|
||||
# Define root_dir for all versions to avoid file name collisions.
|
||||
for remote in versions.remotes:
|
||||
root_dir = RE_INVALID_FILENAME.sub('_', remote['name'])
|
||||
while root_dir in existing:
|
||||
root_dir += '_'
|
||||
remote['root_dir'] = root_dir
|
||||
log.debug('%s root directory is %s', remote['name'], root_dir)
|
||||
existing.append(root_dir)
|
||||
|
||||
# Get found_docs and master_doc values for all versions.
|
||||
for remote in list(versions.remotes):
|
||||
log.debug('Partially running sphinx-build to read configuration for: %s', remote['name'])
|
||||
source = os.path.dirname(os.path.join(exported_root, remote['sha'], remote['conf_rel_path']))
|
||||
try:
|
||||
config = read_config(source, remote['name'])
|
||||
except HandledError:
|
||||
log.warning('Skipping. Will not be building: %s', remote['name'])
|
||||
versions.remotes.pop(versions.remotes.index(remote))
|
||||
continue
|
||||
remote['found_docs'] = config['found_docs']
|
||||
remote['master_doc'] = config['master_doc']
|
||||
|
||||
return exported_root
|
||||
|
||||
|
||||
def build_all(exported_root, destination, versions):
|
||||
"""Build all versions.
|
||||
|
||||
:param str exported_root: Tempdir path with exported commits as subdirectories.
|
||||
:param str destination: Destination directory to copy/overwrite built docs to. Does not delete old files.
|
||||
:param sphinxcontrib.versioning.versions.Versions versions: Versions class instance.
|
||||
"""
|
||||
log = logging.getLogger(__name__)
|
||||
|
||||
while True:
|
||||
# Build root.
|
||||
remote = versions[Config.from_context().root_ref]
|
||||
log.info('Building root: %s', remote['name'])
|
||||
source = os.path.dirname(os.path.join(exported_root, remote['sha'], remote['conf_rel_path']))
|
||||
build(source, destination, versions, remote['name'], True)
|
||||
|
||||
# Build all refs.
|
||||
for remote in list(versions.remotes):
|
||||
log.info('Building ref: %s', remote['name'])
|
||||
source = os.path.dirname(os.path.join(exported_root, remote['sha'], remote['conf_rel_path']))
|
||||
target = os.path.join(destination, remote['root_dir'])
|
||||
try:
|
||||
build(source, target, versions, remote['name'], False)
|
||||
except HandledError:
|
||||
log.warning('Skipping. Will not be building %s. Rebuilding everything.', remote['name'])
|
||||
versions.remotes.pop(versions.remotes.index(remote))
|
||||
break # Break out of for loop.
|
||||
else:
|
||||
break # Break out of while loop if for loop didn't execute break statement above.
|
77
sphinxcontrib/versioning/setup_logging.py
Normal file
|
@ -0,0 +1,77 @@
|
|||
"""Code that handles logging for the project."""
|
||||
|
||||
import logging
|
||||
import logging.handlers
|
||||
import sys
|
||||
|
||||
import colorclass
|
||||
|
||||
|
||||
class ColorFormatter(logging.Formatter):
|
||||
"""Custom logging formatter that introduces console colors if not verbose."""
|
||||
|
||||
SPECIAL_SCOPE = __package__
|
||||
|
||||
def __init__(self, verbose, colors):
|
||||
"""Constructor.
|
||||
|
||||
:param bool verbose: Enable verbose logging.
|
||||
:param bool colors: Enable colored output for statements emitted from this project.
|
||||
"""
|
||||
self.verbose = verbose
|
||||
self.colors = colors
|
||||
if verbose:
|
||||
fmt = '%(asctime)s %(process)-5d %(levelname)-8s %(name)-40s %(message)s'
|
||||
else:
|
||||
fmt = '%(message)s'
|
||||
super(ColorFormatter, self).__init__(fmt)
|
||||
|
||||
def format(self, record):
|
||||
"""Apply little arrow and colors to the record.
|
||||
|
||||
Arrow and colors are only applied to sphinxcontrib.versioning log statements.
|
||||
|
||||
:param logging.LogRecord record: The log record object to log.
|
||||
"""
|
||||
formatted = super(ColorFormatter, self).format(record)
|
||||
if self.verbose or not record.name.startswith(self.SPECIAL_SCOPE):
|
||||
return formatted
|
||||
|
||||
# Arrow.
|
||||
formatted = '=> ' + formatted
|
||||
|
||||
# Colors.
|
||||
if not self.colors:
|
||||
return formatted
|
||||
if record.levelno >= logging.ERROR:
|
||||
formatted = str(colorclass.Color.red(formatted))
|
||||
elif record.levelno >= logging.WARNING:
|
||||
formatted = str(colorclass.Color.yellow(formatted))
|
||||
else:
|
||||
formatted = str(colorclass.Color.cyan(formatted))
|
||||
return formatted
|
||||
|
||||
|
||||
def setup_logging(verbose=0, colors=False, name=None):
|
||||
"""Configure console logging. Info and below go to stdout, others go to stderr.
|
||||
|
||||
:param int verbose: Verbosity level. > 0 print debug statements. > 1 passed to sphinx-build.
|
||||
:param bool colors: Print color text in non-verbose mode.
|
||||
:param str name: Which logger name to set handlers to. Used for testing.
|
||||
"""
|
||||
root_logger = logging.getLogger(name)
|
||||
root_logger.setLevel(logging.DEBUG if verbose > 0 else logging.INFO)
|
||||
formatter = ColorFormatter(verbose > 0, colors)
|
||||
if colors:
|
||||
colorclass.Windows.enable()
|
||||
|
||||
handler_stdout = logging.StreamHandler(sys.stdout)
|
||||
handler_stdout.setFormatter(formatter)
|
||||
handler_stdout.setLevel(logging.DEBUG)
|
||||
handler_stdout.addFilter(type('', (logging.Filter,), {'filter': staticmethod(lambda r: r.levelno <= logging.INFO)}))
|
||||
root_logger.addHandler(handler_stdout)
|
||||
|
||||
handler_stderr = logging.StreamHandler(sys.stderr)
|
||||
handler_stderr.setFormatter(formatter)
|
||||
handler_stderr.setLevel(logging.WARNING)
|
||||
root_logger.addHandler(handler_stderr)
|
278
sphinxcontrib/versioning/sphinx_.py
Normal file
|
@ -0,0 +1,278 @@
|
|||
"""Interface with Sphinx."""
|
||||
|
||||
import datetime
|
||||
import logging
|
||||
import multiprocessing
|
||||
import os
|
||||
import sys
|
||||
|
||||
from sphinx import application, locale
|
||||
from sphinx.cmd.build import build_main
|
||||
from sphinx.builders.html import StandaloneHTMLBuilder
|
||||
from sphinx.config import Config as SphinxConfig
|
||||
from sphinx.errors import SphinxError
|
||||
from sphinx.jinja2glue import SphinxFileSystemLoader
|
||||
from sphinx.util.i18n import format_date
|
||||
|
||||
from sphinxcontrib.versioning import __version__
|
||||
from sphinxcontrib.versioning.lib import Config, HandledError, TempDir
|
||||
from sphinxcontrib.versioning.versions import Versions
|
||||
|
||||
SC_VERSIONING_VERSIONS = list() # Updated after forking.
|
||||
STATIC_DIR = os.path.join(os.path.dirname(__file__), '_static')
|
||||
|
||||
|
||||
class EventHandlers(object):
|
||||
"""Hold Sphinx event handlers as static or class methods.
|
||||
|
||||
:ivar multiprocessing.queues.Queue ABORT_AFTER_READ: Communication channel to parent process.
|
||||
:ivar bool BANNER_GREATEST_TAG: Banner URLs point to greatest/highest (semver) tag.
|
||||
:ivar str BANNER_MAIN_VERSION: Banner URLs point to this remote name (from Versions.__getitem__()).
|
||||
:ivar bool BANNER_RECENT_TAG: Banner URLs point to most recently committed tag.
|
||||
:ivar str CURRENT_VERSION: Current version being built.
|
||||
:ivar bool IS_ROOT: Value for context['scv_is_root'].
|
||||
:ivar bool SHOW_BANNER: Display the banner.
|
||||
:ivar sphinxcontrib.versioning.versions.Versions VERSIONS: Versions class instance.
|
||||
"""
|
||||
|
||||
ABORT_AFTER_READ = None
|
||||
BANNER_GREATEST_TAG = False
|
||||
BANNER_MAIN_VERSION = None
|
||||
BANNER_RECENT_TAG = False
|
||||
CURRENT_VERSION = None
|
||||
IS_ROOT = False
|
||||
SHOW_BANNER = False
|
||||
VERSIONS = None
|
||||
|
||||
@staticmethod
|
||||
def builder_inited(app):
|
||||
"""Update the Sphinx builder.
|
||||
|
||||
:param sphinx.application.Sphinx app: Sphinx application object.
|
||||
"""
|
||||
# Add this extension's _templates directory to Sphinx.
|
||||
templates_dir = os.path.join(os.path.dirname(__file__), '_templates')
|
||||
app.builder.templates.pathchain.insert(0, templates_dir)
|
||||
app.builder.templates.loaders.insert(0, SphinxFileSystemLoader(templates_dir))
|
||||
app.builder.templates.templatepathlen += 1
|
||||
|
||||
# Add versions.html to sidebar.
|
||||
if '**' not in app.config.html_sidebars:
|
||||
# default_sidebars was deprecated in Sphinx 1.6+, so only use it if possible (to maintain
|
||||
# backwards compatibility), else don't use it.
|
||||
try:
|
||||
app.config.html_sidebars['**'] = StandaloneHTMLBuilder.default_sidebars + ['versions.html']
|
||||
except AttributeError:
|
||||
app.config.html_sidebars['**'] = ['versions.html']
|
||||
elif 'versions.html' not in app.config.html_sidebars['**']:
|
||||
app.config.html_sidebars['**'].append('versions.html')
|
||||
|
||||
@classmethod
|
||||
def env_updated(cls, app, env):
|
||||
"""Abort Sphinx after initializing config and discovering all pages to build.
|
||||
|
||||
:param sphinx.application.Sphinx app: Sphinx application object.
|
||||
:param sphinx.environment.BuildEnvironment env: Sphinx build environment.
|
||||
"""
|
||||
if cls.ABORT_AFTER_READ:
|
||||
config = {n: getattr(app.config, n) for n in (a for a in dir(app.config) if a.startswith('scv_'))}
|
||||
config['found_docs'] = tuple(str(d) for d in env.found_docs)
|
||||
config['master_doc'] = str(app.config.master_doc)
|
||||
cls.ABORT_AFTER_READ.put(config)
|
||||
sys.exit(0)
|
||||
|
||||
@classmethod
|
||||
def html_page_context(cls, app, pagename, templatename, context, doctree):
|
||||
"""Update the Jinja2 HTML context, exposes the Versions class instance to it.
|
||||
|
||||
:param sphinx.application.Sphinx app: Sphinx application object.
|
||||
:param str pagename: Name of the page being rendered (without .html or any file extension).
|
||||
:param str templatename: Page name with .html.
|
||||
:param dict context: Jinja2 HTML context.
|
||||
:param docutils.nodes.document doctree: Tree of docutils nodes.
|
||||
"""
|
||||
assert templatename or doctree # Unused, for linting.
|
||||
cls.VERSIONS.context = context
|
||||
versions = cls.VERSIONS
|
||||
this_remote = versions[cls.CURRENT_VERSION]
|
||||
banner_main_remote = versions[cls.BANNER_MAIN_VERSION] if cls.SHOW_BANNER else None
|
||||
|
||||
# Update Jinja2 context.
|
||||
context['bitbucket_version'] = cls.CURRENT_VERSION
|
||||
context['current_version'] = cls.CURRENT_VERSION
|
||||
context['github_version'] = cls.CURRENT_VERSION
|
||||
context['html_theme'] = app.config.html_theme
|
||||
context['scv_banner_greatest_tag'] = cls.BANNER_GREATEST_TAG
|
||||
context['scv_banner_main_ref_is_branch'] = banner_main_remote['kind'] == 'heads' if cls.SHOW_BANNER else None
|
||||
context['scv_banner_main_ref_is_tag'] = banner_main_remote['kind'] == 'tags' if cls.SHOW_BANNER else None
|
||||
context['scv_banner_main_version'] = banner_main_remote['name'] if cls.SHOW_BANNER else None
|
||||
context['scv_banner_recent_tag'] = cls.BANNER_RECENT_TAG
|
||||
context['scv_is_branch'] = this_remote['kind'] == 'heads'
|
||||
context['scv_is_greatest_tag'] = this_remote == versions.greatest_tag_remote
|
||||
context['scv_is_recent_branch'] = this_remote == versions.recent_branch_remote
|
||||
context['scv_is_recent_ref'] = this_remote == versions.recent_remote
|
||||
context['scv_is_recent_tag'] = this_remote == versions.recent_tag_remote
|
||||
context['scv_is_root'] = cls.IS_ROOT
|
||||
context['scv_is_tag'] = this_remote['kind'] == 'tags'
|
||||
context['scv_show_banner'] = cls.SHOW_BANNER
|
||||
context['versions'] = versions
|
||||
context['vhasdoc'] = versions.vhasdoc
|
||||
context['vpathto'] = versions.vpathto
|
||||
|
||||
# Insert banner into body.
|
||||
if cls.SHOW_BANNER and 'body' in context:
|
||||
parsed = app.builder.templates.render('banner.html', context)
|
||||
context['body'] = parsed + context['body']
|
||||
# Handle overridden css_files.
|
||||
css_files = context.setdefault('css_files', list())
|
||||
if '_static/banner.css' not in css_files:
|
||||
css_files.append('_static/banner.css')
|
||||
# Handle overridden html_static_path.
|
||||
if STATIC_DIR not in app.config.html_static_path:
|
||||
app.config.html_static_path.append(STATIC_DIR)
|
||||
|
||||
# Reset last_updated with file's mtime (will be last git commit authored date).
|
||||
if app.config.html_last_updated_fmt is not None:
|
||||
file_path = app.env.doc2path(pagename)
|
||||
if os.path.isfile(file_path):
|
||||
lufmt = app.config.html_last_updated_fmt or getattr(locale, '_')('%b %d, %Y')
|
||||
mtime = datetime.datetime.fromtimestamp(os.path.getmtime(file_path))
|
||||
context['last_updated'] = format_date(lufmt, mtime, language=app.config.language)
|
||||
|
||||
|
||||
def setup(app):
|
||||
"""Called by Sphinx during phase 0 (initialization).
|
||||
|
||||
:param sphinx.application.Sphinx app: Sphinx application object.
|
||||
|
||||
:returns: Extension version.
|
||||
:rtype: dict
|
||||
"""
|
||||
# Used internally. For rebuilding all pages when one or versions fail.
|
||||
app.add_config_value('sphinxcontrib_versioning_versions', SC_VERSIONING_VERSIONS, 'html')
|
||||
|
||||
# Needed for banner.
|
||||
app.config.html_static_path.append(STATIC_DIR)
|
||||
app.add_stylesheet('banner.css')
|
||||
|
||||
# Tell Sphinx which config values can be set by the user.
|
||||
for name, default in Config():
|
||||
app.add_config_value('scv_{}'.format(name), default, 'html')
|
||||
|
||||
# Event handlers.
|
||||
app.connect('builder-inited', EventHandlers.builder_inited)
|
||||
app.connect('env-updated', EventHandlers.env_updated)
|
||||
app.connect('html-page-context', EventHandlers.html_page_context)
|
||||
return dict(version=__version__)
|
||||
|
||||
|
||||
class ConfigInject(SphinxConfig):
|
||||
"""Inject this extension info self.extensions. Append after user's extensions."""
|
||||
|
||||
def __init__(self, *args):
|
||||
"""Constructor."""
|
||||
super(ConfigInject, self).__init__(*args)
|
||||
self.extensions.append('sphinxcontrib.versioning.sphinx_')
|
||||
|
||||
|
||||
def _build(argv, config, versions, current_name, is_root):
|
||||
"""Build Sphinx docs via multiprocessing for isolation.
|
||||
|
||||
:param tuple argv: Arguments to pass to Sphinx.
|
||||
:param sphinxcontrib.versioning.lib.Config config: Runtime configuration.
|
||||
:param sphinxcontrib.versioning.versions.Versions versions: Versions class instance.
|
||||
:param str current_name: The ref name of the current version being built.
|
||||
:param bool is_root: Is this build in the web root?
|
||||
"""
|
||||
# Patch.
|
||||
application.Config = ConfigInject
|
||||
if config.show_banner:
|
||||
EventHandlers.BANNER_GREATEST_TAG = config.banner_greatest_tag
|
||||
EventHandlers.BANNER_MAIN_VERSION = config.banner_main_ref
|
||||
EventHandlers.BANNER_RECENT_TAG = config.banner_recent_tag
|
||||
EventHandlers.SHOW_BANNER = True
|
||||
EventHandlers.CURRENT_VERSION = current_name
|
||||
EventHandlers.IS_ROOT = is_root
|
||||
EventHandlers.VERSIONS = versions
|
||||
SC_VERSIONING_VERSIONS[:] = [p for r in versions.remotes for p in sorted(r.items()) if p[0] not in ('sha', 'date')]
|
||||
|
||||
# Update argv.
|
||||
if config.verbose > 1:
|
||||
argv += ('-v',) * (config.verbose - 1)
|
||||
if config.no_colors:
|
||||
argv += ('-N',)
|
||||
if config.overflow:
|
||||
argv += config.overflow
|
||||
|
||||
# Build.
|
||||
result = build_main(argv)
|
||||
if result != 0:
|
||||
raise SphinxError
|
||||
|
||||
|
||||
def _read_config(argv, config, current_name, queue):
|
||||
"""Read the Sphinx config via multiprocessing for isolation.
|
||||
|
||||
:param tuple argv: Arguments to pass to Sphinx.
|
||||
:param sphinxcontrib.versioning.lib.Config config: Runtime configuration.
|
||||
:param str current_name: The ref name of the current version being built.
|
||||
:param multiprocessing.queues.Queue queue: Communication channel to parent process.
|
||||
"""
|
||||
# Patch.
|
||||
EventHandlers.ABORT_AFTER_READ = queue
|
||||
|
||||
# Run.
|
||||
_build(argv, config, Versions(list()), current_name, False)
|
||||
|
||||
|
||||
def build(source, target, versions, current_name, is_root):
|
||||
"""Build Sphinx docs for one version. Includes Versions class instance with names/urls in the HTML context.
|
||||
|
||||
:raise HandledError: If sphinx-build fails. Will be logged before raising.
|
||||
|
||||
:param str source: Source directory to pass to sphinx-build.
|
||||
:param str target: Destination directory to write documentation to (passed to sphinx-build).
|
||||
:param sphinxcontrib.versioning.versions.Versions versions: Versions class instance.
|
||||
:param str current_name: The ref name of the current version being built.
|
||||
:param bool is_root: Is this build in the web root?
|
||||
"""
|
||||
log = logging.getLogger(__name__)
|
||||
argv = (source, target)
|
||||
config = Config.from_context()
|
||||
|
||||
log.debug('Running sphinx-build for %s with args: %s', current_name, str(argv))
|
||||
child = multiprocessing.Process(target=_build, args=(argv, config, versions, current_name, is_root))
|
||||
child.start()
|
||||
child.join() # Block.
|
||||
if child.exitcode != 0:
|
||||
log.error('sphinx-build failed for branch/tag: %s', current_name)
|
||||
raise HandledError
|
||||
|
||||
|
||||
def read_config(source, current_name):
|
||||
"""Read the Sphinx config for one version.
|
||||
|
||||
:raise HandledError: If sphinx-build fails. Will be logged before raising.
|
||||
|
||||
:param str source: Source directory to pass to sphinx-build.
|
||||
:param str current_name: The ref name of the current version being built.
|
||||
|
||||
:return: Specific Sphinx config values.
|
||||
:rtype: dict
|
||||
"""
|
||||
log = logging.getLogger(__name__)
|
||||
queue = multiprocessing.Queue()
|
||||
config = Config.from_context()
|
||||
|
||||
with TempDir() as temp_dir:
|
||||
argv = (source, temp_dir)
|
||||
log.debug('Running sphinx-build for config values with args: %s', str(argv))
|
||||
child = multiprocessing.Process(target=_read_config, args=(argv, config, current_name, queue))
|
||||
child.start()
|
||||
child.join() # Block.
|
||||
if child.exitcode != 0:
|
||||
log.error('sphinx-build failed for branch/tag while reading config: %s', current_name)
|
||||
raise HandledError
|
||||
|
||||
config = queue.get()
|
||||
return config
|
241
sphinxcontrib/versioning/versions.py
Normal file
|
@ -0,0 +1,241 @@
|
|||
"""Collect and sort version strings."""
|
||||
|
||||
import re
|
||||
|
||||
RE_SEMVER = re.compile(r'^v?V?(\d+)(?:\.(\d+))?(?:\.(\d+))?(?:\.(\d+))?(?:\.(\d+))?(?:\.(\d+))?(?:\.(\d+))?([\w.+-]*)$')
|
||||
|
||||
|
||||
def semvers(names):
|
||||
"""Parse versions into integers and convert non-integer meta indicators into integers with ord().
|
||||
|
||||
Each return list item has an indicator as the first item. 0 for valid versions and 1 for invalid. Can be used to
|
||||
sort non-version names (e.g. master, feature_branch, etc) after valid versions. No sorting is done in this function
|
||||
though.
|
||||
|
||||
Read multi_sort() docstring for reasoning behind inverted integers in version_ints variable.
|
||||
|
||||
:param iter names: List of strings representing versions/tags/branches.
|
||||
|
||||
:return: List of parsed versions. E.g. v1.10.0b3 -> [0, 1, 10, 0, ord('b'), ord('3')]
|
||||
:rtype: list
|
||||
"""
|
||||
matches = [(RE_SEMVER.findall(n) or [[]])[0] for n in names]
|
||||
max_len_ints = 0
|
||||
max_len_str = 0
|
||||
|
||||
# Get max lens for padding.
|
||||
for match in (m for m in matches if m):
|
||||
max_len_ints = len(match) # Never changes.
|
||||
max_len_str = max(max_len_str, len(match[-1]))
|
||||
if not max_len_ints:
|
||||
return matches # Nothing to do, all empty.
|
||||
invalid_template = [1] + [0] * (max_len_ints + max_len_str - 1)
|
||||
|
||||
# Parse.
|
||||
exploded_semver = list()
|
||||
for match in matches:
|
||||
if not match:
|
||||
exploded_semver.append(invalid_template[:])
|
||||
continue
|
||||
version_ints = [-int(i or 0) for i in match[:-1]]
|
||||
ints_of_str = [ord(i) for i in match[-1]] + [0] * (max_len_str - len(match[-1]))
|
||||
exploded_semver.append([0] + version_ints + ints_of_str)
|
||||
|
||||
return exploded_semver
|
||||
|
||||
|
||||
def multi_sort(remotes, sort):
|
||||
"""Sort `remotes` in place. Allows sorting by multiple conditions.
|
||||
|
||||
This is needed because Python 3 no longer supports sorting lists of multiple types. Sort keys must all be of the
|
||||
same type.
|
||||
|
||||
Problem: the user expects versions to be sorted latest first and timelogical to be most recent first (when viewing
|
||||
the HTML documentation), yet expects alphabetical sorting to be A before Z.
|
||||
Solution: invert integers (dates and parsed versions).
|
||||
|
||||
:param iter remotes: List of dicts from Versions().remotes.
|
||||
:param iter sort: What to sort by. May be one or more of: alpha, time, semver
|
||||
"""
|
||||
exploded_alpha = list()
|
||||
exploded_semver = list()
|
||||
|
||||
# Convert name to int if alpha is in sort.
|
||||
if 'alpha' in sort:
|
||||
alpha_max_len = max(len(r['name']) for r in remotes)
|
||||
for name in (r['name'] for r in remotes):
|
||||
exploded_alpha.append([ord(i) for i in name] + [0] * (alpha_max_len - len(name)))
|
||||
|
||||
# Parse versions if semver is in sort.
|
||||
if 'semver' in sort:
|
||||
exploded_semver = semvers(r['name'] for r in remotes)
|
||||
|
||||
# Build sort_mapping dict.
|
||||
sort_mapping = dict()
|
||||
for i, remote in enumerate(remotes):
|
||||
key = list()
|
||||
for sort_by in sort:
|
||||
if sort_by == 'alpha':
|
||||
key.extend(exploded_alpha[i])
|
||||
elif sort_by == 'time':
|
||||
key.append(-remote['date'])
|
||||
elif sort_by == 'semver':
|
||||
key.extend(exploded_semver[i])
|
||||
sort_mapping[id(remote)] = key
|
||||
|
||||
# Sort.
|
||||
remotes.sort(key=lambda k: sort_mapping.get(id(k)))
|
||||
|
||||
|
||||
class Versions(object):
|
||||
"""Iterable class that holds all versions and handles sorting and filtering. To be fed into Sphinx's Jinja2 env.
|
||||
|
||||
:ivar iter remotes: List of dicts for every branch/tag.
|
||||
:ivar dict context: Current Jinja2 context, provided by Sphinx's html-page-context API hook.
|
||||
:ivar dict greatest_tag_remote: Tag with the highest version number if it's a valid semver.
|
||||
:ivar dict recent_branch_remote: Most recently committed branch.
|
||||
:ivar dict recent_remote: Most recently committed branch/tag.
|
||||
:ivar dict recent_tag_remote: Most recently committed tag.
|
||||
"""
|
||||
|
||||
def __init__(self, remotes, sort=None, priority=None, invert=False):
|
||||
"""Constructor.
|
||||
|
||||
:param iter remotes: Output of routines.gather_git_info(). Converted to list of dicts as instance variable.
|
||||
:param iter sort: List of strings (order matters) to sort remotes by. Strings may be: alpha, time, semver
|
||||
:param str priority: May be "branches" or "tags". Groups either before the other. Maintains order otherwise.
|
||||
:param bool invert: Invert sorted/grouped remotes at the end of processing.
|
||||
"""
|
||||
self.remotes = [dict(
|
||||
id='/'.join(r[2:0:-1]), # str; kind/name
|
||||
sha=r[0], # str
|
||||
name=r[1], # str
|
||||
kind=r[2], # str
|
||||
date=r[3], # int
|
||||
conf_rel_path=r[4], # str
|
||||
found_docs=tuple(), # tuple of str
|
||||
master_doc='contents', # str
|
||||
root_dir=r[1], # str
|
||||
) for r in remotes]
|
||||
self.context = dict()
|
||||
self.greatest_tag_remote = None
|
||||
self.recent_branch_remote = None
|
||||
self.recent_remote = None
|
||||
self.recent_tag_remote = None
|
||||
|
||||
# Sort one or more times.
|
||||
if sort:
|
||||
multi_sort(self.remotes, [s.strip().lower() for s in sort])
|
||||
|
||||
# Priority.
|
||||
if priority == 'branches':
|
||||
self.remotes.sort(key=lambda r: 1 if r['kind'] == 'tags' else 0)
|
||||
elif priority == 'tags':
|
||||
self.remotes.sort(key=lambda r: 0 if r['kind'] == 'tags' else 1)
|
||||
|
||||
# Invert.
|
||||
if invert:
|
||||
self.remotes.reverse()
|
||||
|
||||
# Get significant remotes.
|
||||
if self.remotes:
|
||||
remotes = self.remotes[:]
|
||||
multi_sort(remotes, ('time',))
|
||||
self.recent_remote = remotes[0]
|
||||
self.recent_branch_remote = ([r for r in remotes if r['kind'] != 'tags'] or [None])[0]
|
||||
self.recent_tag_remote = ([r for r in remotes if r['kind'] == 'tags'] or [None])[0]
|
||||
if self.recent_tag_remote:
|
||||
multi_sort(remotes, ('semver',))
|
||||
greatest_tag_remote = [r for r in remotes if r['kind'] == 'tags'][0]
|
||||
if RE_SEMVER.search(greatest_tag_remote['name']):
|
||||
self.greatest_tag_remote = greatest_tag_remote
|
||||
|
||||
def __bool__(self):
|
||||
"""True if self.remotes is not empty. Python 3.x."""
|
||||
return bool(self.remotes)
|
||||
|
||||
def __nonzero__(self):
|
||||
"""True if self.remotes is not empty. Python 2.x."""
|
||||
return self.__bool__()
|
||||
|
||||
def __len__(self):
|
||||
"""Length of self.remotes."""
|
||||
return len(self.remotes)
|
||||
|
||||
def __getitem__(self, item):
|
||||
"""Retrieve a version dict from self.remotes by any of its attributes."""
|
||||
# First assume item is an attribute.
|
||||
for key in ('id', 'sha', 'name', 'date'):
|
||||
for remote in self.remotes:
|
||||
if remote[key] == item:
|
||||
return remote
|
||||
# Next assume item is a substring of a sha.
|
||||
try:
|
||||
length = len(item)
|
||||
except TypeError: # Not an int.
|
||||
length = 0
|
||||
if length >= 5:
|
||||
for remote in self.remotes:
|
||||
if item in remote['sha']:
|
||||
return remote
|
||||
# Finally assume it's an index. Raises IndexError if item is int.
|
||||
try:
|
||||
return self.remotes[item]
|
||||
except TypeError:
|
||||
pass
|
||||
# Nothing found, IndexError not raised. item was probably a string, raising KeyError.
|
||||
raise KeyError(item)
|
||||
|
||||
def __iter__(self):
|
||||
"""Yield name and urls of branches and tags."""
|
||||
for remote in self.remotes:
|
||||
name = remote['name']
|
||||
yield name, self.vpathto(name)
|
||||
|
||||
@property
|
||||
def branches(self):
|
||||
"""Return list of (name and urls) only branches."""
|
||||
return [(r['name'], self.vpathto(r['name'])) for r in self.remotes if r['kind'] == 'heads']
|
||||
|
||||
@property
|
||||
def tags(self):
|
||||
"""Return list of (name and urls) only tags."""
|
||||
return [(r['name'], self.vpathto(r['name'])) for r in self.remotes if r['kind'] == 'tags']
|
||||
|
||||
def vhasdoc(self, other_version):
|
||||
"""Return True if the other version has the current document. Like Sphinx's hasdoc().
|
||||
|
||||
:raise KeyError: If other_version doesn't exist.
|
||||
|
||||
:param str other_version: Version to link to.
|
||||
|
||||
:return: If current document is in the other version.
|
||||
:rtype: bool
|
||||
"""
|
||||
if self.context['current_version'] == other_version:
|
||||
return True
|
||||
return self.context['pagename'] in self[other_version]['found_docs']
|
||||
|
||||
def vpathto(self, other_version):
|
||||
"""Return relative path to current document in another version. Like Sphinx's pathto().
|
||||
|
||||
If the current document doesn't exist in the other version its master_doc path is returned instead.
|
||||
|
||||
:raise KeyError: If other_version doesn't exist.
|
||||
|
||||
:param str other_version: Version to link to.
|
||||
|
||||
:return: Relative path.
|
||||
:rtype: str
|
||||
"""
|
||||
is_root = self.context['scv_is_root']
|
||||
pagename = self.context['pagename']
|
||||
if self.context['current_version'] == other_version and not is_root:
|
||||
return '{}.html'.format(pagename.split('/')[-1])
|
||||
|
||||
other_remote = self[other_version]
|
||||
other_root_dir = other_remote['root_dir']
|
||||
components = ['..'] * pagename.count('/')
|
||||
components += [other_root_dir] if is_root else ['..', other_root_dir]
|
||||
components += [pagename if self.vhasdoc(other_version) else other_remote['master_doc']]
|
||||
return '{}.html'.format(__import__('posixpath').join(*components))
|