diff --git a/build/pkgs/mathjax/SPKG.rst b/build/pkgs/mathjax/SPKG.rst index 2cb2508b733..de7b4baa3ff 100644 --- a/build/pkgs/mathjax/SPKG.rst +++ b/build/pkgs/mathjax/SPKG.rst @@ -6,7 +6,7 @@ Description MathJax is a JavaScript library for displaying mathematical formulas. -MathJax is used by the Sage documentation built by Sphinx. +MathJax is used in the Sage documentation built by Sphinx. License ------- @@ -30,3 +30,8 @@ Special Update/Build Instructions None + +Patches +------- + +None. diff --git a/build/pkgs/mathjax/checksums.ini b/build/pkgs/mathjax/checksums.ini index 6a6b233b2ae..76e362f6b28 100644 --- a/build/pkgs/mathjax/checksums.ini +++ b/build/pkgs/mathjax/checksums.ini @@ -2,3 +2,4 @@ tarball=mathjax-VERSION.tar.gz sha1=3f7abecf8cacd7f5d7f9ae6c3baca7739101c17d md5=ba1a65ab58aaad6c84f39735c619bc34 cksum=1142131398 +upstream_url=https://trac.sagemath.org/raw-attachment/ticket/25833/mathjax-3.2.0.tar.gz diff --git a/build/pkgs/mathjax/spkg-src b/build/pkgs/mathjax/spkg-src index 49f81e7a993..bd7fa941cc9 100755 --- a/build/pkgs/mathjax/spkg-src +++ b/build/pkgs/mathjax/spkg-src @@ -4,11 +4,11 @@ set -e [ -n "${SAGE_ROOT}" ] || SAGE_ROOT="$(pwd)/../../../" -# Determine the latest version +# determine the latest version GIT_VERSION="$(curl https://github.com/mathjax/MathJax/releases | grep 'MathJax v' | head -1 | sed 's|^.*MathJax v||g' | sed 's/\s*$//g')" echo "GIT_VERSION=$GIT_VERSION" -# Fetch and rename the latest version +# fetch and rename the latest version URL="https://github.com/mathjax/MathJax/archive/refs/tags/${GIT_VERSION}.zip" echo "Downloading $URL" rm -rf src @@ -18,51 +18,17 @@ else tar xzf "$UPSTREAM_SOURCE_TARBALL" fi -# Put files under mathjax directory +# put files under mathjax directory mkdir src mv MathJax-${GIT_VERSION}/es5 src/mathjax rm -r MathJax-${GIT_VERSION} - -# The following block of commented-out lines were used to reduce the package -# size of MathJax2. We keep these lines for the future when we will want to -# reuse and rewrite them to remove unnecessary font files from MathJax3. - -## Trimming I -- removing files unnecessary for deployment -#FILEDIRS_TO_REMOVE='docs/ test/ unpacked/ .gitignore README-branch.txt README.md bower.json' -#for filedir in ${FILEDIRS_TO_REMOVE} ; do -# rm -rf "src/${filedir}" -#done -# -## Trimming II -- not strictly necessary (requires the patch nopng_config.patch) -#rm -rf 'src/fonts/HTML-CSS/TeX/png/' -# -## Trimming III -- fonts -#FONTS_TO_REMOVE='Asana-Math Gyre-Pagella Gyre-Termes Latin-Modern Neo-Euler' -#for font in ${FONTS_TO_REMOVE} ; do -# find . -type d -name "${font}" -prune -exec rm -rf {} \; -#done -# -#FONT_FORMATS_TO_REMOVE='eot otf svg' -#for fontformat in ${FONT_FORMATS_TO_REMOVE} ; do -# find . -type d -name "${fontformat}" -prune -exec rm -rf {} \; -#done -# -## Trimming IV -- reducing input and output options -#OUTPUT_OPTIONS_TO_REMOVE='NativeMML SVG' -#for output in ${OUTPUT_OPTIONS_TO_REMOVE} ; do -# rm -rf "src/jax/output/${output}" -#done - - PACKAGE_VERSION=${GIT_VERSION} -# Repackage. +# repackage tar czf "$SAGE_ROOT/upstream/mathjax-${PACKAGE_VERSION}.tar.gz" src rm -rf src -# Update package info +# update package info echo "${PACKAGE_VERSION}" > 'package-version.txt' "$SAGE_ROOT"/sage --package fix-checksum mathjax - - diff --git a/pkgs/sagemath-objects/MANIFEST.in b/pkgs/sagemath-objects/MANIFEST.in index fb7c047d6c6..ee82e1ae64c 100644 --- a/pkgs/sagemath-objects/MANIFEST.in +++ b/pkgs/sagemath-objects/MANIFEST.in @@ -62,7 +62,7 @@ include sage/misc/flatten.* # dep of sage/categories/coxeter_groups. include sage/misc/lazy_import*.* include sage/misc/sageinspect.* # dep of sage/misc/lazy_import -graft sage/docs # dep of sage/misc/lazy_import +include sage/misc/instancedoc.* # dep of sage/misc/lazy_import include sage/misc/persist.* include sage/misc/sage_unittest.* # dep of sage/misc/persist diff --git a/src/doc/ca/intro/conf.py b/src/doc/ca/intro/conf.py index 9cafb361385..2f4eb7f1873 100644 --- a/src/doc/ca/intro/conf.py +++ b/src/doc/ca/intro/conf.py @@ -10,13 +10,13 @@ # All configuration values have a default; values that are commented out # serve to show the default. -from sage.docs.conf import release -from sage.docs.conf import * # NOQA +from sage_docbuild.conf import release +from sage_docbuild.conf import * # NOQA # Add any paths that contain custom static files (such as style sheets), # relative to this directory to html_static_path. They are copied after the # builtin static files, so a file named "default.css" will overwrite the -# builtin "default.css". html_common_static_path imported from sage.docs.conf +# builtin "default.css". html_common_static_path imported from sage_docbuild.conf # contains common paths. html_static_path = [] + html_common_static_path diff --git a/src/doc/de/a_tour_of_sage/conf.py b/src/doc/de/a_tour_of_sage/conf.py index a3724e5d1f8..47355ae5a22 100644 --- a/src/doc/de/a_tour_of_sage/conf.py +++ b/src/doc/de/a_tour_of_sage/conf.py @@ -12,13 +12,13 @@ # All configuration values have a default; values that are commented out # serve to show the default. -from sage.docs.conf import release -from sage.docs.conf import * # NOQA +from sage_docbuild.conf import release +from sage_docbuild.conf import * # NOQA # Add any paths that contain custom static files (such as style sheets), # relative to this directory to html_static_path. They are copied after the # builtin static files, so a file named "default.css" will overwrite the -# builtin "default.css". html_common_static_path imported from sage.docs.conf +# builtin "default.css". html_common_static_path imported from sage_docbuild.conf # contains common paths. html_static_path = [] + html_common_static_path diff --git a/src/doc/de/thematische_anleitungen/conf.py b/src/doc/de/thematische_anleitungen/conf.py index 82a9ec207b2..114346944d5 100644 --- a/src/doc/de/thematische_anleitungen/conf.py +++ b/src/doc/de/thematische_anleitungen/conf.py @@ -10,13 +10,13 @@ # All configuration values have a default; values that are commented out # serve to show the default. -from sage.docs.conf import release -from sage.docs.conf import * # NOQA +from sage_docbuild.conf import release +from sage_docbuild.conf import * # NOQA # Add any paths that contain custom static files (such as style sheets), # relative to this directory to html_static_path. They are copied after the # builtin static files, so a file named "default.css" will overwrite the -# builtin "default.css". html_common_static_path imported from sage.docs.conf +# builtin "default.css". html_common_static_path imported from sage_docbuild.conf # contains common paths. html_static_path = [] + html_common_static_path diff --git a/src/doc/de/tutorial/conf.py b/src/doc/de/tutorial/conf.py index 4b7ade4d394..16804d981c9 100644 --- a/src/doc/de/tutorial/conf.py +++ b/src/doc/de/tutorial/conf.py @@ -10,13 +10,13 @@ # All configuration values have a default; values that are commented out # serve to show the default. -from sage.docs.conf import release -from sage.docs.conf import * # NOQA +from sage_docbuild.conf import release +from sage_docbuild.conf import * # NOQA # Add any paths that contain custom static files (such as style sheets), # relative to this directory to html_static_path. They are copied after the # builtin static files, so a file named "default.css" will overwrite the -# builtin "default.css". html_common_static_path imported from sage.docs.conf +# builtin "default.css". html_common_static_path imported from sage_docbuild.conf # contains common paths. html_static_path = [] + html_common_static_path diff --git a/src/doc/en/a_tour_of_sage/conf.py b/src/doc/en/a_tour_of_sage/conf.py index 60f8b638750..89225513782 100644 --- a/src/doc/en/a_tour_of_sage/conf.py +++ b/src/doc/en/a_tour_of_sage/conf.py @@ -10,13 +10,13 @@ # All configuration values have a default; values that are commented out # serve to show the default. -from sage.docs.conf import release -from sage.docs.conf import * # NOQA +from sage_docbuild.conf import release +from sage_docbuild.conf import * # NOQA # Add any paths that contain custom static files (such as style sheets), # relative to this directory to html_static_path. They are copied after the # builtin static files, so a file named "default.css" will overwrite the -# builtin "default.css". html_common_static_path imported from sage.docs.conf +# builtin "default.css". html_common_static_path imported from sage_docbuild.conf # contains common paths. html_static_path = [] + html_common_static_path diff --git a/src/doc/en/constructions/conf.py b/src/doc/en/constructions/conf.py index d50acf00386..eee2feb033a 100644 --- a/src/doc/en/constructions/conf.py +++ b/src/doc/en/constructions/conf.py @@ -10,13 +10,13 @@ # All configuration values have a default; values that are commented out # serve to show the default. -from sage.docs.conf import release -from sage.docs.conf import * # NOQA +from sage_docbuild.conf import release +from sage_docbuild.conf import * # NOQA # Add any paths that contain custom static files (such as style sheets), # relative to this directory to html_static_path. They are copied after the # builtin static files, so a file named "default.css" will overwrite the -# builtin "default.css". html_common_static_path imported from sage.docs.conf +# builtin "default.css". html_common_static_path imported from sage_docbuild.conf # contains common paths. html_static_path = [] + html_common_static_path diff --git a/src/doc/en/developer/conf.py b/src/doc/en/developer/conf.py index 3be07cc7d3e..1ee9e105947 100644 --- a/src/doc/en/developer/conf.py +++ b/src/doc/en/developer/conf.py @@ -10,13 +10,13 @@ # All configuration values have a default; values that are commented out # serve to show the default. -from sage.docs.conf import release -from sage.docs.conf import * # NOQA +from sage_docbuild.conf import release +from sage_docbuild.conf import * # NOQA # Add any paths that contain custom static files (such as style sheets), # relative to this directory to html_static_path. They are copied after the # builtin static files, so a file named "default.css" will overwrite the -# builtin "default.css". html_common_static_path imported from sage.docs.conf +# builtin "default.css". html_common_static_path imported from sage_docbuild.conf # contains common paths. html_static_path = [] + html_common_static_path diff --git a/src/doc/en/faq/conf.py b/src/doc/en/faq/conf.py index 8b70e81e4b6..42c3378b129 100644 --- a/src/doc/en/faq/conf.py +++ b/src/doc/en/faq/conf.py @@ -12,13 +12,13 @@ # All configuration values have a default; values that are commented out # serve to show the default. -from sage.docs.conf import release -from sage.docs.conf import * +from sage_docbuild.conf import release +from sage_docbuild.conf import * # Add any paths that contain custom static files (such as style sheets), # relative to this directory to html_static_path. They are copied after the # builtin static files, so a file named "default.css" will overwrite the -# builtin "default.css". html_common_static_path imported from sage.docs.conf +# builtin "default.css". html_common_static_path imported from sage_docbuild.conf # contains common paths. html_static_path = [] + html_common_static_path diff --git a/src/doc/en/installation/conf.py b/src/doc/en/installation/conf.py index 268f9fa4648..33ed20fa8e9 100644 --- a/src/doc/en/installation/conf.py +++ b/src/doc/en/installation/conf.py @@ -10,13 +10,13 @@ # All configuration values have a default; values that are commented out # serve to show the default. -from sage.docs.conf import release -from sage.docs.conf import * # NOQA +from sage_docbuild.conf import release +from sage_docbuild.conf import * # NOQA # Add any paths that contain custom static files (such as style sheets), # relative to this directory to html_static_path. They are copied after the # builtin static files, so a file named "default.css" will overwrite the -# builtin "default.css". html_common_static_path imported from sage.docs.conf +# builtin "default.css". html_common_static_path imported from sage_docbuild.conf # contains common paths. html_static_path = [] + html_common_static_path diff --git a/src/doc/en/prep/conf.py b/src/doc/en/prep/conf.py index 4891ddc7868..bbc6663b3df 100644 --- a/src/doc/en/prep/conf.py +++ b/src/doc/en/prep/conf.py @@ -10,13 +10,13 @@ # All configuration values have a default; values that are commented out # serve to show the default. -from sage.docs.conf import release -from sage.docs.conf import * # NOQA +from sage_docbuild.conf import release +from sage_docbuild.conf import * # NOQA # Add any paths that contain custom static files (such as style sheets), # relative to this directory to html_static_path. They are copied after the # builtin static files, so a file named "default.css" will overwrite the -# builtin "default.css". html_common_static_path imported from sage.docs.conf +# builtin "default.css". html_common_static_path imported from sage_docbuild.conf # contains common paths. html_static_path = [] + html_common_static_path diff --git a/src/doc/en/reference/conf.py b/src/doc/en/reference/conf.py index 6ce70316d9f..86aa0b05a81 100644 --- a/src/doc/en/reference/conf.py +++ b/src/doc/en/reference/conf.py @@ -12,13 +12,13 @@ import os from sage.env import SAGE_DOC_SRC, SAGE_DOC -from sage.docs.conf import release, latex_elements, exclude_patterns -from sage.docs.conf import * +from sage_docbuild.conf import release, latex_elements, exclude_patterns +from sage_docbuild.conf import * # Add any paths that contain custom static files (such as style sheets), # relative to this directory to html_static_path. They are copied after the # builtin static files, so a file named "default.css" will overwrite the -# builtin "default.css". html_common_static_path imported from sage.docs.conf +# builtin "default.css". html_common_static_path imported from sage_docbuild.conf # contains common paths. html_static_path = [] + html_common_static_path diff --git a/src/doc/en/reference/conf_sub.py b/src/doc/en/reference/conf_sub.py index 9d22a36e497..b6f20311d68 100644 --- a/src/doc/en/reference/conf_sub.py +++ b/src/doc/en/reference/conf_sub.py @@ -12,13 +12,13 @@ import os from sage.env import SAGE_DOC_SRC, SAGE_DOC -from sage.docs.conf import release, exclude_patterns -from sage.docs.conf import * +from sage_docbuild.conf import release, exclude_patterns +from sage_docbuild.conf import * # Add any paths that contain custom static files (such as style sheets), # relative to this directory to html_static_path. They are copied after the # builtin static files, so a file named "default.css" will overwrite the -# builtin "default.css". html_common_static_path imported from sage.docs.conf +# builtin "default.css". html_common_static_path imported from sage_docbuild.conf # contains common paths. html_static_path = [] + html_common_static_path diff --git a/src/doc/en/reference/documentation/conf.py b/src/doc/en/reference/documentation/conf.py new file mode 120000 index 00000000000..2bdf7e68470 --- /dev/null +++ b/src/doc/en/reference/documentation/conf.py @@ -0,0 +1 @@ +../conf_sub.py \ No newline at end of file diff --git a/src/doc/en/reference/documentation/index.rst b/src/doc/en/reference/documentation/index.rst new file mode 100644 index 00000000000..13d17594db3 --- /dev/null +++ b/src/doc/en/reference/documentation/index.rst @@ -0,0 +1,12 @@ +Documentation System +==================== + +.. toctree:: + :maxdepth: 1 + + sage_docbuild/__main__ + sage_docbuild/builders + sage_docbuild/build_options + sage_docbuild/sphinxbuild + sage_docbuild/conf + sage_docbuild/utils diff --git a/src/doc/en/reference/footer.txt b/src/doc/en/reference/footer.txt index 94b024d6dab..22b60ffa26f 100644 --- a/src/doc/en/reference/footer.txt +++ b/src/doc/en/reference/footer.txt @@ -1,13 +1,13 @@ Indices and Tables ================== +* `Index <../genindex.html>`_ +* `Module Index <../py-modindex.html>`_ +* `Search Page <../search.html>`_ + .. comment: the following math environment forces Sphinx to load MathJax in the index.rst pages. Do not delete it! .. math:: :nowrap: - -* `Index <../genindex.html>`_ -* `Module Index <../py-modindex.html>`_ -* `Search Page <../search.html>`_ diff --git a/src/doc/en/reference/index.rst b/src/doc/en/reference/index.rst index 9cb0d0b47e1..065bccac955 100644 --- a/src/doc/en/reference/index.rst +++ b/src/doc/en/reference/index.rst @@ -7,7 +7,7 @@ Welcome to the Sage Reference Manual! Here you find documentation for all of `Sage `_'s features, illustrated with lots of examples. A thematic index follows. -This documentation is licensed under the `Creative Commons Attribution-Share Alike 3.0 License`__:math:`.` +This documentation is licensed under the `Creative Commons Attribution-Share Alike 3.0 License`__. __ http://creativecommons.org/licenses/by-sa/3.0/ @@ -97,7 +97,7 @@ Geometry, Topology, and Homological Algebra * :doc:`Euclidean Spaces and Vector Calculus ` * :doc:`Combinatorial and Discrete Geometry ` -* :doc:`Cell Complexes, Simplicial Complexes, and +* :doc:`Cell Complexes, Simplicial Complexes, and Simplicial Sets ` * :doc:`Manifolds and Differential Geometry ` * :doc:`Hyperbolic Geometry ` @@ -157,6 +157,11 @@ Interfaces * :doc:`C/C++ Library Interfaces ` * :doc:`Python Technicalities ` +Documentation System +-------------------- + +* :doc:`Documentation System ` + General Information =================== @@ -170,3 +175,10 @@ Indices and Tables * :ref:`genindex` * :ref:`modindex` * :ref:`search` + +.. + comment: the following math environment forces Sphinx to load MathJax + in the index.rst pages. Do not delete it! + +.. math:: + :nowrap: diff --git a/src/doc/en/thematic_tutorials/conf.py b/src/doc/en/thematic_tutorials/conf.py index f5c5af6cb62..3b638d40173 100644 --- a/src/doc/en/thematic_tutorials/conf.py +++ b/src/doc/en/thematic_tutorials/conf.py @@ -12,13 +12,13 @@ # All configuration values have a default; values that are commented out # serve to show the default. -from sage.docs.conf import release -from sage.docs.conf import * +from sage_docbuild.conf import release +from sage_docbuild.conf import * # Add any paths that contain custom static files (such as style sheets), # relative to this directory to html_static_path. They are copied after the # builtin static files, so a file named "default.css" will overwrite the -# builtin "default.css". html_common_static_path imported from sage.docs.conf +# builtin "default.css". html_common_static_path imported from sage_docbuild.conf # contains common paths. html_static_path = [] + html_common_static_path diff --git a/src/doc/en/thematic_tutorials/explicit_methods_in_number_theory/conf.py b/src/doc/en/thematic_tutorials/explicit_methods_in_number_theory/conf.py index 0fba6acc072..cdcbdb584c9 100644 --- a/src/doc/en/thematic_tutorials/explicit_methods_in_number_theory/conf.py +++ b/src/doc/en/thematic_tutorials/explicit_methods_in_number_theory/conf.py @@ -10,13 +10,13 @@ # All configuration values have a default; values that are commented out # serve to show the default. -from sage.docs.conf import release -from sage.docs.conf import * # NOQA +from sage_docbuild.conf import release +from sage_docbuild.conf import * # NOQA # Add any paths that contain custom static files (such as style sheets), # relative to this directory to html_static_path. They are copied after the # builtin static files, so a file named "default.css" will overwrite the -# builtin "default.css". html_common_static_path imported from sage.docs.conf +# builtin "default.css". html_common_static_path imported from sage_docbuild.conf # contains common paths. html_static_path = [] + html_common_static_path diff --git a/src/doc/en/thematic_tutorials/numerical_sage/conf.py b/src/doc/en/thematic_tutorials/numerical_sage/conf.py index da30268c1b4..08e174fde3b 100644 --- a/src/doc/en/thematic_tutorials/numerical_sage/conf.py +++ b/src/doc/en/thematic_tutorials/numerical_sage/conf.py @@ -10,13 +10,13 @@ # All configuration values have a default; values that are commented out # serve to show the default. -from sage.docs.conf import release -from sage.docs.conf import * # NOQA +from sage_docbuild.conf import release +from sage_docbuild.conf import * # NOQA # Add any paths that contain custom static files (such as style sheets), # relative to this directory to html_static_path. They are copied after the # builtin static files, so a file named "default.css" will overwrite the -# builtin "default.css". html_common_static_path imported from sage.docs.conf +# builtin "default.css". html_common_static_path imported from sage_docbuild.conf # contains common paths. html_static_path = [] + html_common_static_path diff --git a/src/doc/en/tutorial/conf.py b/src/doc/en/tutorial/conf.py index d3f2ddf1204..b2b525d2c2a 100644 --- a/src/doc/en/tutorial/conf.py +++ b/src/doc/en/tutorial/conf.py @@ -10,13 +10,13 @@ # All configuration values have a default; values that are commented out # serve to show the default. -from sage.docs.conf import release -from sage.docs.conf import * # NOQA +from sage_docbuild.conf import release +from sage_docbuild.conf import * # NOQA # Add any paths that contain custom static files (such as style sheets), # relative to this directory to html_static_path. They are copied after the # builtin static files, so a file named "default.css" will overwrite the -# builtin "default.css". html_common_static_path imported from sage.docs.conf +# builtin "default.css". html_common_static_path imported from sage_docbuild.conf # contains common paths. html_static_path = [] + html_common_static_path diff --git a/src/doc/en/website/conf.py b/src/doc/en/website/conf.py index caf790a3977..7794fb9324a 100644 --- a/src/doc/en/website/conf.py +++ b/src/doc/en/website/conf.py @@ -10,13 +10,13 @@ # All configuration values have a default; values that are commented out # serve to show the default. -from sage.docs.conf import release -from sage.docs.conf import * # NOQA +from sage_docbuild.conf import release +from sage_docbuild.conf import * # NOQA # Add any paths that contain custom static files (such as style sheets), # relative to this directory to html_static_path. They are copied after the # builtin static files, so a file named "default.css" will overwrite the -# builtin "default.css". html_common_static_path imported from sage.docs.conf +# builtin "default.css". html_common_static_path imported from sage_docbuild.conf # contains common paths. html_static_path = [] + html_common_static_path diff --git a/src/doc/en/website/templates/index.html b/src/doc/en/website/templates/index.html index 5f34a99dc62..d8895243052 100644 --- a/src/doc/en/website/templates/index.html +++ b/src/doc/en/website/templates/index.html @@ -15,6 +15,13 @@ display: none; {%- endif %} } + table.contentstable { + align: center; + border-spacing: 20px; + } + table.contentstable span { {# trac #33600 comment:22 #} + border-spacing: initial; + } {% endblock %} @@ -33,7 +40,7 @@

Tutorials and FAQ

- +
  • ', r'
  • ; change rst headers to html headers - rst_toc = re.sub(r'\*(.*)\n', - r'
  • \1
  • \n', rst_toc) - rst_toc = re.sub(r'\n([A-Z][a-zA-Z, ]*)\n[=]*\n', - r'\n\n\n

    \1

    \n\n
      \n', rst_toc) - rst_toc = re.sub(r'\n([A-Z][a-zA-Z, ]*)\n[-]*\n', - r'
    \n\n\n

    \1

    \n\n
      \n', rst_toc) - # now write the file. - with open(os.path.join(output_dir, 'index.html'), 'w') as new_index: - new_index.write(html[:html_end_preamble]) - new_index.write('

      Sage Reference Manual (PDF version)

      ') - new_index.write(rst_body) - new_index.write('
        ') - new_index.write(rst_toc) - new_index.write('
      \n\n') - new_index.write(html[html_bottom:]) - logger.warning(''' -PDF documents have been created in subdirectories of - - %s - -Alternatively, you can open - - %s - -for a webpage listing all of the documents.''' % (output_dir, - os.path.join(output_dir, - 'index.html'))) - - -class ReferenceSubBuilder(DocBuilder): - """ - This class builds sub-components of the reference manual. It is - responsible for making sure that the auto generated reST files for the - Sage library are up to date. - - When building any output, we must first go through and check - to see if we need to update any of the autogenerated reST - files. There are two cases where this would happen: - - 1. A new module gets added to one of the toctrees. - - 2. The actual module gets updated and possibly contains a new - title. - """ - def __init__(self, *args, **kwds): - DocBuilder.__init__(self, *args, **kwds) - self._wrap_builder_helpers() - - def _wrap_builder_helpers(self): - from functools import partial, update_wrapper - for attr in dir(self): - if hasattr(getattr(self, attr), 'is_output_format'): - f = partial(self._wrapper, attr) - f.is_output_format = True - update_wrapper(f, getattr(self, attr)) - setattr(self, attr, f) - - def _wrapper(self, build_type, *args, **kwds): - """ - This is the wrapper around the builder_helper methods that - goes through and makes sure things are up to date. - """ - # Force regeneration of all modules if the inherited - # and/or underscored members options have changed. - cache = self.get_cache() - force = False - try: - if (cache['option_inherited'] != self._options.inherited or - cache['option_underscore'] != self._options.underscore): - logger.info("Detected change(s) in inherited and/or underscored members option(s).") - force = True - except KeyError: - force = True - cache['option_inherited'] = self._options.inherited - cache['option_underscore'] = self._options.underscore - self.save_cache() - - # After "sage -clone", refresh the reST file mtimes in - # environment.pickle. - if self._options.update_mtimes: - logger.info("Checking for reST file mtimes to update...") - self.update_mtimes() - - if force: - # Write reST files for all modules from scratch. - self.clean_auto() - for module_name in self.get_all_included_modules(): - self.write_auto_rest_file(module_name) - else: - # Write reST files for new and updated modules. - for module_name in self.get_new_and_updated_modules(): - self.write_auto_rest_file(module_name) - - # Copy over the custom reST files from _sage - _sage = os.path.join(self.dir, '_sage') - if os.path.exists(_sage): - logger.info("Copying over custom reST files from %s ...", _sage) - shutil.copytree(_sage, os.path.join(self.dir, 'sage')) - - getattr(DocBuilder, build_type)(self, *args, **kwds) - - def cache_filename(self): - """ - Return the filename where the pickle of the reference cache - is stored. - """ - return os.path.join(self._doctrees_dir(), 'reference.pickle') - - @cached_method - def get_cache(self): - """ - Retrieve the reference cache which contains the options previously used - by the reference builder. - - If it doesn't exist, then we just return an empty dictionary. If it - is corrupted, return an empty dictionary. - """ - filename = self.cache_filename() - if not os.path.exists(filename): - return {} - with open(self.cache_filename(), 'rb') as file: - try: - cache = pickle.load(file) - except Exception: - logger.debug("Cache file '%s' is corrupted; ignoring it..." % filename) - cache = {} - else: - logger.debug("Loaded the reference cache: %s", filename) - return cache - - def save_cache(self): - """ - Pickle the current reference cache for later retrieval. - """ - cache = self.get_cache() - try: - with open(self.cache_filename(), 'wb') as file: - pickle.dump(cache, file) - logger.debug("Saved the reference cache: %s", self.cache_filename()) - except PermissionError: - logger.debug("Permission denied for the reference cache: %s", self.cache_filename()) - - def get_sphinx_environment(self): - """ - Return the Sphinx environment for this project. - """ - class FakeConfig(object): - values = tuple() - - class FakeApp(object): - def __init__(self, dir): - self.srcdir = dir - self.config = FakeConfig() - - env_pickle = os.path.join(self._doctrees_dir(), 'environment.pickle') - try: - with open(env_pickle, 'rb') as f: - env = pickle.load(f) - env.app = FakeApp(self.dir) - env.config.values = env.app.config.values - logger.debug("Opened Sphinx environment: %s", env_pickle) - return env - except (IOError, EOFError) as err: - logger.debug( - f"Failed to open Sphinx environment '{env_pickle}'", exc_info=True) - - def update_mtimes(self): - """ - Update the modification times for reST files in the Sphinx - environment for this project. - """ - env = self.get_sphinx_environment() - if env is not None: - for doc in env.all_docs: - env.all_docs[doc] = time.time() - logger.info("Updated %d reST file mtimes", len(env.all_docs)) - # This is the only place we need to save (as opposed to - # load) Sphinx's pickle, so we do it right here. - env_pickle = os.path.join(self._doctrees_dir(), - 'environment.pickle') - - # When cloning a new branch (see - # SAGE_LOCAL/bin/sage-clone), we hard link the doc output. - # To avoid making unlinked, potentially inconsistent - # copies of the environment, we *don't* use - # env.topickle(env_pickle), which first writes a temporary - # file. We adapt sphinx.environment's - # BuildEnvironment.topickle: - - # remove unpicklable attributes - env.set_warnfunc(None) - del env.config.values - with open(env_pickle, 'wb') as picklefile: - # remove potentially pickling-problematic values from config - for key, val in vars(env.config).items(): - if key.startswith('_') or isinstance(val, (types.ModuleType, - types.FunctionType, - type)): - del env.config[key] - pickle.dump(env, picklefile, pickle.HIGHEST_PROTOCOL) - - logger.debug("Saved Sphinx environment: %s", env_pickle) - - def get_modified_modules(self): - """ - Return an iterator for all the modules that have been modified - since the documentation was last built. - """ - env = self.get_sphinx_environment() - if env is None: - logger.debug("Stopped check for modified modules.") - return - try: - added, changed, removed = env.get_outdated_files(False) - logger.info("Sphinx found %d modified modules", len(changed)) - except OSError as err: - logger.debug("Sphinx failed to determine modified modules: %s", err) - return - for name in changed: - # Only pay attention to files in a directory sage/... In - # particular, don't treat a file like 'sagetex.rst' in - # doc/en/reference/misc as an autogenerated file: see - # #14199. - if name.startswith('sage' + os.sep): - yield name - - def print_modified_modules(self): - """ - Print a list of all the modules that have been modified since - the documentation was last built. - """ - for module_name in self.get_modified_modules(): - print(module_name) - - def get_all_rst_files(self, exclude_sage=True): - """ - Return an iterator for all rst files which are not - autogenerated. - """ - for directory, subdirs, files in os.walk(self.dir): - if exclude_sage and directory.startswith(os.path.join(self.dir, 'sage')): - continue - for filename in files: - if not filename.endswith('.rst'): - continue - yield os.path.join(directory, filename) - - def get_all_included_modules(self): - """ - Return an iterator for all modules which are included in the - reference manual. - """ - for filename in self.get_all_rst_files(): - for module in self.get_modules(filename): - yield module - - def get_new_and_updated_modules(self): - """ - Return an iterator for all new and updated modules that appear in - the toctrees, and remove obsolete old modules. - """ - env = self.get_sphinx_environment() - if env is None: - all_docs = {} - else: - all_docs = env.all_docs - - new_modules = [] - updated_modules = [] - old_modules = [] - for module_name in self.get_all_included_modules(): - docname = module_name.replace('.', os.path.sep) - - if docname not in all_docs: - new_modules.append(module_name) - yield module_name - continue - - # get the modification timestamp of the reST doc for the module - mtime = all_docs[docname] - try: - with warnings.catch_warnings(): - # primarily intended to ignore deprecation warnings - warnings.simplefilter("ignore") - __import__(module_name) - except ImportError as err: - logger.error("Warning: Could not import %s %s", module_name, err) - raise - - module_filename = sys.modules[module_name].__file__ - if (module_filename.endswith('.pyc') or module_filename.endswith('.pyo')): - source_filename = module_filename[:-1] - if (os.path.exists(source_filename)): - module_filename = source_filename - newtime = os.path.getmtime(module_filename) - - if newtime > mtime: - updated_modules.append(module_name) - yield module_name - else: # keep good old module - old_modules.append(module_name) - - removed_modules = [] - for docname in all_docs.keys(): - if docname.startswith('sage' + os.path.sep): - module_name = docname.replace(os.path.sep, '.') - if not (module_name in old_modules or module_name in updated_modules): - try: - os.remove(os.path.join(self.dir, docname) + '.rst') - except OSError: # already removed - pass - logger.debug("Deleted auto-generated reST file {}".format(docname)) - removed_modules.append(module_name) - - logger.info("Found %d new modules", len(new_modules)) - logger.info("Found %d updated modules", len(updated_modules)) - logger.info("Removed %d obsolete modules", len(removed_modules)) - - def print_new_and_updated_modules(self): - """ - Print all the modules that appear in the toctrees that - are newly included or updated. - """ - for module_name in self.get_new_and_updated_modules(): - print(module_name) - - def get_modules(self, filename): - """ - Given a filename for a reST file, return an iterator for - all of the autogenerated reST files that it includes. - """ - # Create the regular expression used to detect an autogenerated file - auto_re = re.compile(r'^\s*(..\/)*(sage(nb)?\/[\w\/]*)\s*$') - - # Read the lines - with open(filename) as f: - lines = f.readlines() - for line in lines: - match = auto_re.match(line) - if match: - yield match.group(2).replace(os.path.sep, '.') - - def get_module_docstring_title(self, module_name): - """ - Return the title of the module from its docstring. - """ - # Try to import the module - try: - __import__(module_name) - except ImportError as err: - logger.error("Warning: Could not import %s %s", module_name, err) - return "UNABLE TO IMPORT MODULE" - module = sys.modules[module_name] - - # Get the docstring - doc = module.__doc__ - if doc is None: - doc = module.doc if hasattr(module, 'doc') else "" - - # Extract the title - i = doc.find('\n') - if i != -1: - return doc[i + 1:].lstrip().splitlines()[0] - else: - return doc - - def auto_rest_filename(self, module_name): - """ - Return the name of the file associated to a given module - - EXAMPLES:: - - sage: from sage_docbuild import ReferenceSubBuilder - sage: ReferenceSubBuilder("reference").auto_rest_filename("sage.combinat.partition") - '.../en/reference/sage/combinat/partition.rst' - """ - return self.dir + os.path.sep + module_name.replace('.', os.path.sep) + '.rst' - - def write_auto_rest_file(self, module_name): - """ - Write the autogenerated reST file for module_name. - """ - if not module_name.startswith('sage'): - return - filename = self.auto_rest_filename(module_name) - os.makedirs(os.path.dirname(filename), exist_ok=True) - - title = self.get_module_docstring_title(module_name) - - if title == '': - logger.error("Warning: Missing title for %s", module_name) - title = "MISSING TITLE" - - with open(filename, 'w') as outfile: - # Don't doctest the autogenerated file. - outfile.write(".. nodoctest\n\n") - # Now write the actual content. - outfile.write(".. _%s:\n\n" % (module_name.replace(".__init__", ""))) - outfile.write(title + '\n') - outfile.write('=' * len(title) + "\n\n") - outfile.write('.. This file has been autogenerated.\n\n') - - inherited = ':inherited-members:' if self._options.inherited else '' - - automodule = ''' -.. automodule:: %s - :members: - :undoc-members: - :show-inheritance: - %s - -''' - outfile.write(automodule % (module_name, inherited)) - - def clean_auto(self): - """ - Remove all autogenerated reST files. - """ - try: - shutil.rmtree(os.path.join(self.dir, 'sage')) - logger.debug("Deleted auto-generated reST files in: %s", - os.path.join(self.dir, 'sage')) - except OSError: - pass - - def get_unincluded_modules(self): - """ - Return an iterator for all the modules in the Sage library - which are not included in the reference manual. - """ - # Make a dictionary of the included modules - included_modules = {} - for module_name in self.get_all_included_modules(): - included_modules[module_name] = True - - base_path = os.path.join(SAGE_SRC, 'sage') - for directory, subdirs, files in os.walk(base_path): - for filename in files: - if not (filename.endswith('.py') or - filename.endswith('.pyx')): - continue - - path = os.path.join(directory, filename) - - # Create the module name - module_name = path[len(base_path):].replace(os.path.sep, '.') - module_name = 'sage' + module_name - module_name = module_name[:-4] if module_name.endswith('pyx') else module_name[:-3] - - # Exclude some ones -- we don't want init the manual - if module_name.endswith('__init__') or module_name.endswith('all'): - continue - - if module_name not in included_modules: - yield module_name - - def print_unincluded_modules(self): - """ - Print all of the modules which are not included in the Sage - reference manual. - """ - for module_name in self.get_unincluded_modules(): - print(module_name) - - def print_included_modules(self): - """ - Print all of the modules that are included in the Sage reference - manual. - """ - for module_name in self.get_all_included_modules(): - print(module_name) - - -class SingleFileBuilder(DocBuilder): - """ - This is the class used to build the documentation for a single - user-specified file. If the file is called 'foo.py', then the - documentation is built in ``DIR/foo/`` if the user passes the - command line option "-o DIR", or in ``DOT_SAGE/docbuild/foo/`` - otherwise. - """ - def __init__(self, path): - """ - INPUT: - - - ``path`` - the path to the file for which documentation - should be built - """ - self.lang = 'en' - self.name = 'single_file' - path = os.path.abspath(path) - - # Create docbuild and relevant subdirectories, e.g., - # the static and templates directories in the output directory. - # By default, this is DOT_SAGE/docbuild/MODULE_NAME, but can - # also be specified at the command line. - module_name = os.path.splitext(os.path.basename(path))[0] - latex_name = module_name.replace('_', r'\\_') - - if self._options.output_dir: - base_dir = os.path.join(self._options.output_dir, module_name) - if os.path.exists(base_dir): - logger.warning('Warning: Directory %s exists. It is safer to build in a new directory.' % base_dir) - else: - base_dir = os.path.join(DOT_SAGE, 'docbuild', module_name) - try: - shutil.rmtree(base_dir) - except OSError: - pass - self.dir = os.path.join(base_dir, 'source') - - os.makedirs(os.path.join(self.dir, "static"), exist_ok=True) - os.makedirs(os.path.join(self.dir, "templates"), exist_ok=True) - # Write self.dir/conf.py - conf = r"""# This file is automatically generated by {}, do not edit! - -import sys, os, contextlib -sys.path.append({!r}) - -from sage.docs.conf import * -html_static_path = [] + html_common_static_path - -project = 'Documentation for {}' -release = 'unknown' -name = {!r} -html_title = project -html_short_title = project -htmlhelp_basename = name - -with contextlib.suppress(ValueError): - extensions.remove('multidocs') # see #29651 - extensions.remove('inventory_builder') - -latex_domain_indices = False -latex_documents = [ - ('index', name + '.tex', 'Documentation for {}', - 'unknown', 'manual'), -] -""".format(__file__, self.dir, module_name, module_name, latex_name) - - if 'SAGE_DOC_UNDERSCORE' in os.environ: - conf += r""" -def setup(app): - app.connect('autodoc-skip-member', skip_member) -""" - - with open(os.path.join(self.dir, 'conf.py'), 'w') as conffile: - conffile.write(conf) - - # Write self.dir/index.rst - title = 'Docs for file %s' % path - heading = title + "\n" + ("=" * len(title)) - index = r"""{} - -.. This file is automatically generated by {}, do not edit! - -.. automodule:: {} - :members: - :undoc-members: - :show-inheritance: - -""".format(heading, __file__, module_name) - with open(os.path.join(self.dir, 'index.rst'), 'w') as indexfile: - indexfile.write(index) - - # Create link from original file to self.dir. Note that we - # append self.dir to sys.path in conf.py. This is reasonably - # safe (but not perfect), since we just created self.dir. - try: - os.symlink(path, os.path.join(self.dir, os.path.basename(path))) - except OSError: - pass - - def _output_dir(self, type): - """ - Return the directory where the output of type ``type`` is stored. - - If the directory does not exist, then it will automatically be - created. - """ - base_dir = os.path.split(self.dir)[0] - d = os.path.join(base_dir, "output", type) - os.makedirs(d, exist_ok=True) - return d - - def _doctrees_dir(self): - """ - Return the directory where the doctrees are stored. - - If the directory does not exist, then it will automatically be - created. - """ - return self._output_dir('doctrees') - - -def get_builder(name): - """ - Return an appropriate *Builder object for the document ``name``. - - DocBuilder and its subclasses do all the real work in building the - documentation. - """ - if name == 'all': - from sage.misc.superseded import deprecation - deprecation(31948, 'avoid using "sage --docbuild all html" and "sage --docbuild all pdf"; ' - 'use "make doc" and "make doc-pdf" instead, if available.') - return AllBuilder() - elif name == 'reference_top': - return ReferenceTopBuilder('reference') - elif name.endswith('reference'): - return ReferenceBuilder(name) - elif 'reference' in name and os.path.exists(os.path.join(SAGE_DOC_SRC, 'en', name)): - return ReferenceSubBuilder(name) - elif name.endswith('website'): - return WebsiteBuilder(name) - elif name.startswith('file='): - path = name[5:] - if path.endswith('.sage') or path.endswith('.pyx'): - raise NotImplementedError('Building documentation for a single file only works for Python files.') - return SingleFileBuilder(path) - elif name in get_documents() or name in AllBuilder().get_all_documents(): - return DocBuilder(name) - else: - print("'%s' is not a recognized document. Type 'sage --docbuild -D' for a list" % name) - print("of documents, or 'sage --docbuild --help' for more help.") - sys.exit(1) - - -def format_columns(lst, align='<', cols=None, indent=4, pad=3, width=80): - """ - Utility function that formats a list as a simple table and returns - a Unicode string representation. - - The number of columns is - computed from the other options, unless it's passed as a keyword - argument. For help on Python's string formatter, see - - https://docs.python.org/library/string.html#format-string-syntax - """ - # Can we generalize this (efficiently) to other / multiple inputs - # and generators? - size = max(map(len, lst)) + pad - if cols is None: - import math - cols = math.trunc((width - indent) / size) - s = " " * indent - for i in range(len(lst)): - if i != 0 and i % cols == 0: - s += "\n" + " " * indent - s += "{0:{1}{2}}".format(lst[i], align, size) - s += "\n" - return s - - -def help_usage(s="", compact=False): - """ - Append and return a brief usage message for the Sage documentation builder. - - If 'compact' is False, the function adds a final newline character. - """ - s += "sage --docbuild [OPTIONS] DOCUMENT (FORMAT | COMMAND)" - if not compact: - s += "\n" - return s - - -def help_description(s="", compact=False): - """ - Append and return a brief description of the Sage documentation builder. - - If 'compact' is ``False``, the function adds a final newline character. - """ - s += "Build or return information about Sage documentation. " - s += "A DOCUMENT and either a FORMAT or a COMMAND are required." - if not compact: - s += "\n" - return s - - -def help_examples(s=""): - """ - Append and return some usage examples for the Sage documentation builder. - """ - s += "Examples:\n" - s += " sage --docbuild -C all\n" - s += " sage --docbuild constructions pdf\n" - s += " sage --docbuild reference html -jv3\n" - s += " sage --docbuild reference print_unincluded_modules\n" - s += " sage --docbuild developer html --sphinx-opts='-q,-aE' --verbose 2" - return s - - -def get_documents(): - """ - Return a list of document names the Sage documentation builder - will accept as command-line arguments. - """ - all_b = AllBuilder() - docs = all_b.get_all_documents() - docs = [(d[3:] if d[0:3] == 'en/' else d) for d in docs] - return docs - - -def help_documents(): - """ - Append and return a tabular list of documents, including a - shortcut 'all' for all documents, available to the Sage - documentation builder. - """ - docs = get_documents() - s = "DOCUMENTs:\n" - s += format_columns(docs) - s += "\n" - if 'reference' in docs: - s += "Other valid document names take the form 'reference/DIR', where\n" - s += "DIR is a subdirectory of SAGE_DOC_SRC/en/reference/.\n" - s += "This builds just the specified part of the reference manual.\n" - s += "DOCUMENT may also have the form 'file=/path/to/FILE', which builds\n" - s += "the documentation for the specified file.\n" - return s - - -def get_formats(): - """ - Return a list of output formats the Sage documentation builder - will accept on the command-line. - """ - tut_b = DocBuilder('en/tutorial') - formats = tut_b._output_formats() - formats.remove('html') - return ['html', 'pdf'] + formats - - -def help_formats(): - """ - Append and return a tabular list of output formats available to - the Sage documentation builder. - """ - return "FORMATs:\n" + format_columns(get_formats()) - - -def help_commands(name='all'): - """ - Append and return a tabular list of commands, if any, the Sage - documentation builder can run on the indicated document. The - default is to list all commands for all documents. - """ - # To do: Generate the lists dynamically, using class attributes, - # as with the Builders above. - s = "" - command_dict = {'reference': [ - 'print_included_modules', 'print_modified_modules (*)', - 'print_unincluded_modules', 'print_new_and_updated_modules (*)']} - for doc in command_dict: - if name == 'all' or doc == name: - s += "COMMANDs for the DOCUMENT '" + doc + "':\n" - s += format_columns(command_dict[doc]) - s += "(*) Since the last build.\n" - return s - - -class help_message_long(argparse.Action): - """ - Print an extended help message for the Sage documentation builder - and exits. - """ - def __call__(self, parser, namespace, values, option_string=None): - help_funcs = [help_usage, help_description, help_documents, - help_formats, help_commands] - for f in help_funcs: - print(f()) - parser.print_help() - print(help_examples()) - sys.exit(0) - - -class help_message_short(argparse.Action): - """ - Print a help message for the Sage documentation builder. - - The message includes command-line usage and a list of options. - The message is printed only on the first call. If error is True - during this call, the message is printed only if the user hasn't - requested a list (e.g., documents, formats, commands). - """ - def __call__(self, parser, namespace, values, option_string=None): - if not hasattr(namespace, 'printed_help'): - parser.print_help() - setattr(namespace, 'printed_help', 1) - sys.exit(0) - - -class help_wrapper(argparse.Action): - """ - A helper wrapper for command-line options to the Sage - documentation builder that print lists, such as document names, - formats, and document-specific commands. - """ - def __call__(self, parser, namespace, values, option_string=None): - if option_string in ['-D', '--documents']: - print(help_documents(), end="") - if option_string in ['-F', '--formats']: - print(help_formats(), end="") - if self.dest == 'commands': - print(help_commands(values), end="") - if self.dest == 'all_documents': - if values == 'reference': - b = ReferenceBuilder('reference') - refdir = os.path.join(os.environ['SAGE_DOC_SRC'], 'en', b.name) - s = b.get_all_documents(refdir) - # Put the bibliography first, because it needs to be built first: - s.remove('reference/references') - s.insert(0, 'reference/references') - elif values == 'all': - s = get_documents() - # Put the reference manual first, because it needs to be built first: - s.remove('reference') - s.insert(0, 'reference') - for d in s: - print(d) - setattr(namespace, 'printed_list', 1) - sys.exit(0) - - -def setup_parser(): - """ - Set up and return a command-line ArgumentParser instance for the - Sage documentation builder. - """ - # Documentation: https://docs.python.org/library/argparse.html - parser = argparse.ArgumentParser(usage=help_usage(compact=True), - description=help_description(compact=True), - add_help=False) - # Standard options. Note: We use explicit option.dest names - # to avoid ambiguity. - standard = parser.add_argument_group("Standard") - standard.add_argument("-h", "--help", nargs=0, action=help_message_short, - help="show a help message and exit") - standard.add_argument("-H", "--help-all", nargs=0, action=help_message_long, - help="show an extended help message and exit") - standard.add_argument("-D", "--documents", nargs=0, action=help_wrapper, - help="list all available DOCUMENTs") - standard.add_argument("-F", "--formats", nargs=0, action=help_wrapper, - help="list all output FORMATs") - standard.add_argument("-C", "--commands", dest="commands", - type=str, metavar="DOC", action=help_wrapper, - help="list all COMMANDs for DOCUMENT DOC; use 'all' to list all") - standard.add_argument("-i", "--inherited", dest="inherited", - action="store_true", - help="include inherited members in reference manual; may be slow, may fail for PDF output") - standard.add_argument("-u", "--underscore", dest="underscore", - action="store_true", - help="include variables prefixed with '_' in reference manual; may be slow, may fail for PDF output") - standard.add_argument("-j", "--mathjax", "--jsmath", dest="mathjax", - action="store_true", - help="ignored for backwards compatibility") - standard.add_argument("--no-plot", dest="no_plot", - action="store_true", - help="do not include graphics auto-generated using the '.. plot' markup") - standard.add_argument("--include-tests-blocks", dest="skip_tests", default=True, - action="store_false", - help="include TESTS blocks in the reference manual") - standard.add_argument("--no-pdf-links", dest="no_pdf_links", - action="store_true", - help="do not include PDF links in DOCUMENT 'website'; FORMATs: html, json, pickle, web") - standard.add_argument("--warn-links", dest="warn_links", - action="store_true", - help="issue a warning whenever a link is not properly resolved; equivalent to '--sphinx-opts -n' (sphinx option: nitpicky)") - standard.add_argument("--check-nested", dest="check_nested", - action="store_true", - help="check picklability of nested classes in DOCUMENT 'reference'") - standard.add_argument("--no-prune-empty-dirs", dest="no_prune_empty_dirs", - action="store_true", - help="do not prune empty directories in the documentation sources") - standard.add_argument("--use-cdns", dest="use_cdns", default=False, - action="store_true", - help="assume internet connection and use CDNs; in particular, use MathJax CDN") - standard.add_argument("-N", "--no-colors", dest="color", - action="store_false", - help="do not color output; does not affect children") - standard.add_argument("-q", "--quiet", dest="verbose", - action="store_const", const=0, - help="work quietly; same as --verbose=0") - standard.add_argument("-v", "--verbose", dest="verbose", - type=int, default=1, metavar="LEVEL", - action="store", - help="report progress at LEVEL=0 (quiet), 1 (normal), 2 (info), or 3 (debug); does not affect children") - standard.add_argument("-o", "--output", dest="output_dir", default=None, - metavar="DIR", action="store", - help="if DOCUMENT is a single file ('file=...'), write output to this directory") - - # Advanced options. - advanced = parser.add_argument_group("Advanced", - "Use these options with care.") - advanced.add_argument("-S", "--sphinx-opts", dest="sphinx_opts", - type=str, metavar="OPTS", - action="store", - help="pass comma-separated OPTS to sphinx-build; must precede OPTS with '=', as in '-S=-q,-aE' or '-S=\"-q,-aE\"'") - advanced.add_argument("-U", "--update-mtimes", dest="update_mtimes", - action="store_true", - help="before building reference manual, update modification times for auto-generated reST files") - advanced.add_argument("-k", "--keep-going", dest="keep_going", - action="store_true", - help="Do not abort on errors but continue as much as possible after an error") - advanced.add_argument("--all-documents", dest="all_documents", - type=str, metavar="ARG", - choices=['all', 'reference'], - action=help_wrapper, - help="if ARG is 'reference', list all subdocuments" - " of en/reference. If ARG is 'all', list all main" - " documents") - parser.add_argument("document", nargs='?', type=str, metavar="DOCUMENT", - help="name of the document to build. It can be either one of the documents listed by -D or 'file=/path/to/FILE' to build documentation for this specific file.") - parser.add_argument("format", nargs='?', type=str, - metavar="FORMAT or COMMAND", help='document output format (or command)') - return parser - - -def setup_logger(verbose=1, color=True): - r""" - Set up a Python Logger instance for the Sage documentation builder. - - The optional argument sets logger's level and message format. - - EXAMPLES:: - - sage: from sage_docbuild import setup_logger, logger - sage: setup_logger() - sage: type(logger) - - """ - # Set up colors. Adapted from sphinx.cmdline. - import sphinx.util.console as c - if not color or not sys.stdout.isatty() or not c.color_terminal(): - c.nocolor() - - # Available colors: black, darkgray, (dark)red, dark(green), - # brown, yellow, (dark)blue, purple, fuchsia, turquoise, teal, - # lightgray, white. Available styles: reset, bold, faint, - # standout, underline, blink. - - # Set up log record formats. - format_std = "%(message)s" - formatter = logging.Formatter(format_std) - - # format_debug = "%(module)s #%(lineno)s %(funcName)s() %(message)s" - fields = ['%(module)s', '#%(lineno)s', '%(funcName)s()', '%(message)s'] - colors = ['darkblue', 'darkred', 'brown', 'reset'] - styles = ['reset', 'reset', 'reset', 'reset'] - format_debug = "" - for i in range(len(fields)): - format_debug += c.colorize(styles[i], c.colorize(colors[i], fields[i])) - if i != len(fields): - format_debug += " " - - # Note: There's also Handler.setLevel(). The argument is the - # lowest severity message that the respective logger or handler - # will pass on. The default levels are DEBUG, INFO, WARNING, - # ERROR, and CRITICAL. We use "WARNING" for normal verbosity and - # "ERROR" for quiet operation. It's possible to define custom - # levels. See the documentation for details. - if verbose == 0: - logger.setLevel(logging.ERROR) - if verbose == 1: - logger.setLevel(logging.WARNING) - if verbose == 2: - logger.setLevel(logging.INFO) - if verbose == 3: - logger.setLevel(logging.DEBUG) - formatter = logging.Formatter(format_debug) - - handler = logging.StreamHandler() - handler.setFormatter(formatter) - logger.addHandler(handler) - - -class IntersphinxCache: - """ - Replace sphinx.ext.intersphinx.fetch_inventory by an in-memory - cached version. - """ - def __init__(self): - self.inventories = {} - self.real_fetch_inventory = sphinx.ext.intersphinx.fetch_inventory - sphinx.ext.intersphinx.fetch_inventory = self.fetch_inventory - - def fetch_inventory(self, app, uri, inv): - """ - Return the result of ``sphinx.ext.intersphinx.fetch_inventory()`` - from a cache if possible. Otherwise, call - ``sphinx.ext.intersphinx.fetch_inventory()`` and cache the result. - """ - t = (uri, inv) - try: - return self.inventories[t] - except KeyError: - i = self.real_fetch_inventory(app, uri, inv) - self.inventories[t] = i - return i - - -def main(): - # Parse the command-line. - parser = setup_parser() - args = parser.parse_args() - DocBuilder._options = args - - # Get the name and type (target format) of the document we are - # trying to build. - name, typ = args.document, args.format - if not name or not typ: - parser.print_help() - sys.exit(1) - - # Set up module-wide logging. - setup_logger(args.verbose, args.color) - - def excepthook(*exc_info): - logger.error('Error building the documentation.', exc_info=exc_info) - if INCREMENTAL_BUILD: - logger.error(''' - Note: incremental documentation builds sometimes cause spurious - error messages. To be certain that these are real errors, run - "make doc-clean doc-uninstall" first and try again.''') - - sys.excepthook = excepthook - - # Process selected options. - if args.check_nested: - os.environ['SAGE_CHECK_NESTED'] = 'True' - - if args.underscore: - os.environ['SAGE_DOC_UNDERSCORE'] = "True" - - global ALLSPHINXOPTS, WEBSITESPHINXOPTS, ABORT_ON_ERROR - if args.sphinx_opts: - ALLSPHINXOPTS += args.sphinx_opts.replace(',', ' ') + " " - if args.no_pdf_links: - WEBSITESPHINXOPTS = " -A hide_pdf_links=1 " - if args.warn_links: - ALLSPHINXOPTS += "-n " - if args.no_plot: - os.environ['SAGE_SKIP_PLOT_DIRECTIVE'] = 'yes' - if args.skip_tests: - os.environ['SAGE_SKIP_TESTS_BLOCKS'] = 'True' - if args.use_cdns: - os.environ['SAGE_USE_CDNS'] = 'yes' - - ABORT_ON_ERROR = not args.keep_going - - if not args.no_prune_empty_dirs: - # Delete empty directories. This is needed in particular for empty - # directories due to "git checkout" which never deletes empty - # directories it leaves behind. See Trac #20010. - # Trac #31948: This is not parallelization-safe; use the option - # --no-prune-empty-dirs to turn it off - for dirpath, dirnames, filenames in os.walk(SAGE_DOC_SRC, topdown=False): - if not dirnames + filenames: - logger.warning('Deleting empty directory {0}'.format(dirpath)) - os.rmdir(dirpath) - - # Set up Intersphinx cache - _ = IntersphinxCache() - - builder = getattr(get_builder(name), typ) - builder() diff --git a/src/sage_docbuild/__main__.py b/src/sage_docbuild/__main__.py index 031df43e1f1..8c6e9031b82 100644 --- a/src/sage_docbuild/__main__.py +++ b/src/sage_docbuild/__main__.py @@ -1,2 +1,500 @@ -from . import main -main() +r""" +Sage docbuild main + +This module defines the Sage documentation build command:: + + sage --docbuild [OPTIONS] DOCUMENT (FORMAT | COMMAND) + +If ``FORMAT`` is given, it builds ``DOCUMENT`` in ``FORMAT``. If ``COMMAND`` is +given, it returns information about ``DOCUMENT``. + +Run ``sage --docbuild`` to get detailed explanations about +arguments and options. + +Positional arguments:: + + DOCUMENT name of the document to build. It can be either one + of the documents listed by -D or 'file=/path/to/FILE' to build documentation + for this specific file. + FORMAT or COMMAND document output format (or command) + +Standard options:: + + -h, --help show a help message and exit + -H, --help-all show an extended help message and exit + -D, --documents list all available DOCUMENTs + -F, --formats list all output FORMATs + -C DOC, --commands DOC list all COMMANDs for DOCUMENT DOC; use 'all' to list all + -i, --inherited include inherited members in reference manual; may be slow, may fail for PDF output + -u, --underscore include variables prefixed with '_' in reference + manual; may be slow, may fail for PDF output + -j, --mathjax, --jsmath ignored for backwards compatibility + --no-plot do not include graphics auto-generated using the '.. plot' markup + --include-tests-blocks include TESTS blocks in the reference manual + --no-pdf-links do not include PDF links in DOCUMENT 'website'; + FORMATs: html, json, pickle, web + --warn-links issue a warning whenever a link is not properly + resolved; equivalent to '--sphinx-opts -n' (sphinx option: nitpicky) + --check-nested check picklability of nested classes in DOCUMENT 'reference' + --no-prune-empty-dirs do not prune empty directories in the documentation sources + -N, --no-colors do not color output; does not affect children + -q, --quiet work quietly; same as --verbose=0 + -v LEVEL, --verbose LEVEL report progress at LEVEL=0 (quiet), 1 (normal), 2 + (info), or 3 (debug); does not affect children + -o DIR, --output DIR if DOCUMENT is a single file ('file=...'), write output to this directory + +Advanced options:: + + -S OPTS, --sphinx-opts OPTS pass comma-separated OPTS to sphinx-build; must + precede OPTS with '=', as in '-S=-q,-aE' or '-S="-q,-aE"' + -U, --update-mtimes before building reference manual, update + modification times for auto-generated reST files + -k, --keep-going do not abort on errors but continue as much as + possible after an error + --all-documents ARG if ARG is 'reference', list all subdocuments of + en/reference. If ARG is 'all', list all main documents + +""" + +import logging +import argparse +import os +import sys +import sphinx.ext.intersphinx +from sage.env import SAGE_DOC_SRC +from .builders import DocBuilder, ReferenceBuilder, get_builder, get_documents +from .build_options import (INCREMENTAL_BUILD, + ALLSPHINXOPTS, WEBSITESPHINXOPTS, ABORT_ON_ERROR) + +logger = logging.getLogger(__name__) + +def format_columns(lst, align='<', cols=None, indent=4, pad=3, width=80): + """ + Utility function that formats a list as a simple table and returns + a Unicode string representation. + + The number of columns is + computed from the other options, unless it's passed as a keyword + argument. For help on Python's string formatter, see + + https://docs.python.org/library/string.html#format-string-syntax + """ + # Can we generalize this (efficiently) to other / multiple inputs + # and generators? + size = max(map(len, lst)) + pad + if cols is None: + import math + cols = math.trunc((width - indent) / size) + s = " " * indent + for i in range(len(lst)): + if i != 0 and i % cols == 0: + s += "\n" + " " * indent + s += "{0:{1}{2}}".format(lst[i], align, size) + s += "\n" + return s + + +def help_usage(s="", compact=False): + """ + Append and return a brief usage message for the Sage documentation builder. + + If 'compact' is False, the function adds a final newline character. + """ + s += "sage --docbuild [OPTIONS] DOCUMENT (FORMAT | COMMAND)" + if not compact: + s += "\n" + return s + + +def help_description(s="", compact=False): + """ + Append and return a brief description of the Sage documentation builder. + + If 'compact' is ``False``, the function adds a final newline character. + """ + s += "Build or return information about Sage documentation. " + s += "A DOCUMENT and either a FORMAT or a COMMAND are required." + if not compact: + s += "\n" + return s + + +def help_examples(s=""): + """ + Append and return some usage examples for the Sage documentation builder. + """ + s += "Examples:\n" + s += " sage --docbuild -C all\n" + s += " sage --docbuild constructions pdf\n" + s += " sage --docbuild reference html -jv3\n" + s += " sage --docbuild reference print_unincluded_modules\n" + s += " sage --docbuild developer html --sphinx-opts='-q,-aE' --verbose 2" + return s + + +def help_documents(): + """ + Append and return a tabular list of documents, including a + shortcut 'all' for all documents, available to the Sage + documentation builder. + """ + docs = get_documents() + s = "DOCUMENTs:\n" + s += format_columns(docs) + s += "\n" + if 'reference' in docs: + s += "Other valid document names take the form 'reference/DIR', where\n" + s += "DIR is a subdirectory of SAGE_DOC_SRC/en/reference/.\n" + s += "This builds just the specified part of the reference manual.\n" + s += "DOCUMENT may also have the form 'file=/path/to/FILE', which builds\n" + s += "the documentation for the specified file.\n" + return s + + +def get_formats(): + """ + Return a list of output formats the Sage documentation builder + will accept on the command-line. + """ + tut_b = DocBuilder('en/tutorial') + formats = tut_b._output_formats() + formats.remove('html') + return ['html', 'pdf'] + formats + + +def help_formats(): + """ + Append and return a tabular list of output formats available to + the Sage documentation builder. + """ + return "FORMATs:\n" + format_columns(get_formats()) + + +def help_commands(name='all'): + """ + Append and return a tabular list of commands, if any, the Sage + documentation builder can run on the indicated document. The + default is to list all commands for all documents. + """ + # To do: Generate the lists dynamically, using class attributes, + # as with the Builders above. + s = "" + command_dict = {'reference': [ + 'print_included_modules', 'print_modified_modules (*)', + 'print_unincluded_modules', 'print_new_and_updated_modules (*)']} + for doc in command_dict: + if name == 'all' or doc == name: + s += "COMMANDs for the DOCUMENT '" + doc + "':\n" + s += format_columns(command_dict[doc]) + s += "(*) Since the last build.\n" + return s + + +class help_message_long(argparse.Action): + """ + Print an extended help message for the Sage documentation builder + and exits. + """ + def __call__(self, parser, namespace, values, option_string=None): + help_funcs = [help_usage, help_description, help_documents, + help_formats, help_commands] + for f in help_funcs: + print(f()) + parser.print_help() + print(help_examples()) + sys.exit(0) + + +class help_message_short(argparse.Action): + """ + Print a help message for the Sage documentation builder. + + The message includes command-line usage and a list of options. + The message is printed only on the first call. If error is True + during this call, the message is printed only if the user hasn't + requested a list (e.g., documents, formats, commands). + """ + def __call__(self, parser, namespace, values, option_string=None): + if not hasattr(namespace, 'printed_help'): + parser.print_help() + setattr(namespace, 'printed_help', 1) + sys.exit(0) + + +class help_wrapper(argparse.Action): + """ + A helper wrapper for command-line options to the Sage + documentation builder that print lists, such as document names, + formats, and document-specific commands. + """ + def __call__(self, parser, namespace, values, option_string=None): + if option_string in ['-D', '--documents']: + print(help_documents(), end="") + if option_string in ['-F', '--formats']: + print(help_formats(), end="") + if self.dest == 'commands': + print(help_commands(values), end="") + if self.dest == 'all_documents': + if values == 'reference': + b = ReferenceBuilder('reference') + refdir = os.path.join(os.environ['SAGE_DOC_SRC'], 'en', b.name) + s = b.get_all_documents(refdir) + # Put the bibliography first, because it needs to be built first: + s.remove('reference/references') + s.insert(0, 'reference/references') + elif values == 'all': + s = get_documents() + # Put the reference manual first, because it needs to be built first: + s.remove('reference') + s.insert(0, 'reference') + for d in s: + print(d) + setattr(namespace, 'printed_list', 1) + sys.exit(0) + + +def setup_parser(): + """ + Set up and return a command-line ArgumentParser instance for the + Sage documentation builder. + """ + # Documentation: https://docs.python.org/library/argparse.html + parser = argparse.ArgumentParser(usage=help_usage(compact=True), + description=help_description(compact=True), + add_help=False) + # Standard options. Note: We use explicit option.dest names + # to avoid ambiguity. + standard = parser.add_argument_group("Standard") + standard.add_argument("-h", "--help", nargs=0, action=help_message_short, + help="show a help message and exit") + standard.add_argument("-H", "--help-all", nargs=0, action=help_message_long, + help="show an extended help message and exit") + standard.add_argument("-D", "--documents", nargs=0, action=help_wrapper, + help="list all available DOCUMENTs") + standard.add_argument("-F", "--formats", nargs=0, action=help_wrapper, + help="list all output FORMATs") + standard.add_argument("-C", "--commands", dest="commands", + type=str, metavar="DOC", action=help_wrapper, + help="list all COMMANDs for DOCUMENT DOC; use 'all' to list all") + standard.add_argument("-i", "--inherited", dest="inherited", + action="store_true", + help="include inherited members in reference manual; may be slow, may fail for PDF output") + standard.add_argument("-u", "--underscore", dest="underscore", + action="store_true", + help="include variables prefixed with '_' in reference manual; may be slow, may fail for PDF output") + standard.add_argument("-j", "--mathjax", "--jsmath", dest="mathjax", + action="store_true", + help="ignored for backwards compatibility") + standard.add_argument("--no-plot", dest="no_plot", + action="store_true", + help="do not include graphics auto-generated using the '.. plot' markup") + standard.add_argument("--include-tests-blocks", dest="skip_tests", default=True, + action="store_false", + help="include TESTS blocks in the reference manual") + standard.add_argument("--no-pdf-links", dest="no_pdf_links", + action="store_true", + help="do not include PDF links in DOCUMENT 'website'; FORMATs: html, json, pickle, web") + standard.add_argument("--warn-links", dest="warn_links", + action="store_true", + help="issue a warning whenever a link is not properly resolved; equivalent to '--sphinx-opts -n' (sphinx option: nitpicky)") + standard.add_argument("--check-nested", dest="check_nested", + action="store_true", + help="check picklability of nested classes in DOCUMENT 'reference'") + standard.add_argument("--no-prune-empty-dirs", dest="no_prune_empty_dirs", + action="store_true", + help="do not prune empty directories in the documentation sources") + standard.add_argument("--use-cdns", dest="use_cdns", default=False, + action="store_true", + help="assume internet connection and use CDNs; in particular, use MathJax CDN") + standard.add_argument("-N", "--no-colors", dest="color", + action="store_false", + help="do not color output; does not affect children") + standard.add_argument("-q", "--quiet", dest="verbose", + action="store_const", const=0, + help="work quietly; same as --verbose=0") + standard.add_argument("-v", "--verbose", dest="verbose", + type=int, default=1, metavar="LEVEL", + action="store", + help="report progress at LEVEL=0 (quiet), 1 (normal), 2 (info), or 3 (debug); does not affect children") + standard.add_argument("-o", "--output", dest="output_dir", default=None, + metavar="DIR", action="store", + help="if DOCUMENT is a single file ('file=...'), write output to this directory") + + # Advanced options. + advanced = parser.add_argument_group("Advanced", + "Use these options with care.") + advanced.add_argument("-S", "--sphinx-opts", dest="sphinx_opts", + type=str, metavar="OPTS", + action="store", + help="pass comma-separated OPTS to sphinx-build; must precede OPTS with '=', as in '-S=-q,-aE' or '-S=\"-q,-aE\"'") + advanced.add_argument("-U", "--update-mtimes", dest="update_mtimes", + action="store_true", + help="before building reference manual, update modification times for auto-generated reST files") + advanced.add_argument("-k", "--keep-going", dest="keep_going", + action="store_true", + help="Do not abort on errors but continue as much as possible after an error") + advanced.add_argument("--all-documents", dest="all_documents", + type=str, metavar="ARG", + choices=['all', 'reference'], + action=help_wrapper, + help="if ARG is 'reference', list all subdocuments" + " of en/reference. If ARG is 'all', list all main" + " documents") + parser.add_argument("document", nargs='?', type=str, metavar="DOCUMENT", + help="name of the document to build. It can be either one of the documents listed by -D or 'file=/path/to/FILE' to build documentation for this specific file.") + parser.add_argument("format", nargs='?', type=str, + metavar="FORMAT or COMMAND", help='document output format (or command)') + return parser + + +def setup_logger(verbose=1, color=True): + r""" + Set up a Python Logger instance for the Sage documentation builder. + + The optional argument sets logger's level and message format. + + EXAMPLES:: + + sage: from sage_docbuild.__main__ import setup_logger, logger + sage: setup_logger() + sage: type(logger) + + """ + # Set up colors. Adapted from sphinx.cmdline. + import sphinx.util.console as c + if not color or not sys.stdout.isatty() or not c.color_terminal(): + c.nocolor() + + # Available colors: black, darkgray, (dark)red, dark(green), + # brown, yellow, (dark)blue, purple, fuchsia, turquoise, teal, + # lightgray, white. Available styles: reset, bold, faint, + # standout, underline, blink. + + # Set up log record formats. + format_std = "%(message)s" + formatter = logging.Formatter(format_std) + + # format_debug = "%(module)s #%(lineno)s %(funcName)s() %(message)s" + fields = ['%(module)s', '#%(lineno)s', '%(funcName)s()', '%(message)s'] + colors = ['darkblue', 'darkred', 'brown', 'reset'] + styles = ['reset', 'reset', 'reset', 'reset'] + format_debug = "" + for i in range(len(fields)): + format_debug += c.colorize(styles[i], c.colorize(colors[i], fields[i])) + if i != len(fields): + format_debug += " " + + # Note: There's also Handler.setLevel(). The argument is the + # lowest severity message that the respective logger or handler + # will pass on. The default levels are DEBUG, INFO, WARNING, + # ERROR, and CRITICAL. We use "WARNING" for normal verbosity and + # "ERROR" for quiet operation. It's possible to define custom + # levels. See the documentation for details. + if verbose == 0: + logger.setLevel(logging.ERROR) + if verbose == 1: + logger.setLevel(logging.WARNING) + if verbose == 2: + logger.setLevel(logging.INFO) + if verbose == 3: + logger.setLevel(logging.DEBUG) + formatter = logging.Formatter(format_debug) + + handler = logging.StreamHandler() + handler.setFormatter(formatter) + logger.addHandler(handler) + + +class IntersphinxCache: + """ + Replace sphinx.ext.intersphinx.fetch_inventory by an in-memory + cached version. + """ + def __init__(self): + self.inventories = {} + self.real_fetch_inventory = sphinx.ext.intersphinx.fetch_inventory + sphinx.ext.intersphinx.fetch_inventory = self.fetch_inventory + + def fetch_inventory(self, app, uri, inv): + """ + Return the result of ``sphinx.ext.intersphinx.fetch_inventory()`` + from a cache if possible. Otherwise, call + ``sphinx.ext.intersphinx.fetch_inventory()`` and cache the result. + """ + t = (uri, inv) + try: + return self.inventories[t] + except KeyError: + i = self.real_fetch_inventory(app, uri, inv) + self.inventories[t] = i + return i + + +def main(): + # Parse the command-line. + parser = setup_parser() + args = parser.parse_args() + DocBuilder._options = args + + # Get the name and type (target format) of the document we are + # trying to build. + name, typ = args.document, args.format + if not name or not typ: + parser.print_help() + sys.exit(1) + + # Set up module-wide logging. + setup_logger(args.verbose, args.color) + + def excepthook(*exc_info): + logger.error('Error building the documentation.', exc_info=exc_info) + if INCREMENTAL_BUILD: + logger.error(''' + Note: incremental documentation builds sometimes cause spurious + error messages. To be certain that these are real errors, run + "make doc-clean doc-uninstall" first and try again.''') + + sys.excepthook = excepthook + + # Process selected options. + if args.check_nested: + os.environ['SAGE_CHECK_NESTED'] = 'True' + + if args.underscore: + os.environ['SAGE_DOC_UNDERSCORE'] = "True" + + global ALLSPHINXOPTS, WEBSITESPHINXOPTS, ABORT_ON_ERROR + if args.sphinx_opts: + ALLSPHINXOPTS += args.sphinx_opts.replace(',', ' ') + " " + if args.no_pdf_links: + WEBSITESPHINXOPTS = " -A hide_pdf_links=1 " + if args.warn_links: + ALLSPHINXOPTS += "-n " + if args.no_plot: + os.environ['SAGE_SKIP_PLOT_DIRECTIVE'] = 'yes' + if args.skip_tests: + os.environ['SAGE_SKIP_TESTS_BLOCKS'] = 'True' + if args.use_cdns: + os.environ['SAGE_USE_CDNS'] = 'yes' + + ABORT_ON_ERROR = not args.keep_going + + if not args.no_prune_empty_dirs: + # Delete empty directories. This is needed in particular for empty + # directories due to "git checkout" which never deletes empty + # directories it leaves behind. See Trac #20010. + # Trac #31948: This is not parallelization-safe; use the option + # --no-prune-empty-dirs to turn it off + for dirpath, dirnames, filenames in os.walk(SAGE_DOC_SRC, topdown=False): + if not dirnames + filenames: + logger.warning('Deleting empty directory {0}'.format(dirpath)) + os.rmdir(dirpath) + + # Set up Intersphinx cache + _ = IntersphinxCache() + + builder = getattr(get_builder(name), typ) + builder() + +if __name__ == '__main__': + sys.exit(main()) diff --git a/src/sage_docbuild/build_options.py b/src/sage_docbuild/build_options.py index 50bac1737b7..4857c1ed125 100644 --- a/src/sage_docbuild/build_options.py +++ b/src/sage_docbuild/build_options.py @@ -1,6 +1,9 @@ -############################################### -# Options for building the Sage documentation # -############################################### +r""" +Build options + +This module defines options for building Sage documentation. +""" + import os import re diff --git a/src/sage_docbuild/builders.py b/src/sage_docbuild/builders.py new file mode 100644 index 00000000000..b6b723976cd --- /dev/null +++ b/src/sage_docbuild/builders.py @@ -0,0 +1,1360 @@ +""" +Documentation builders + +This module is the starting point for building documentation, and is +responsible to figure out what to build and with which options. The actual +documentation build for each individual document is then done in a subprocess +call to Sphinx, see :func:`builder_helper`. Note that + +* The builders are configured with ``build_options.py``; +* The Sphinx subprocesses are configured in ``conf.py``. + +:class:`DocBuilder` is the base class of all Builders. It has builder helpers +:meth:`html()`, :meth:`latex`, :meth:`pdf`, :meth:`inventory`, etc, which are +invoked depending on the output type. Each type corresponds with the Sphinx +builder format, except that ``pdf`` is Sphinx latex builder plus compiling +latex to pdf. Note that Sphinx inventory builder is not native to Sphinx +but provided by Sage. See ``sage_docbuild/ext/inventory_builder.py``. The +Sphinx inventory builder is a dummy builder with no actual output but produces +doctree files in ``local/share/doctree`` and ``inventory.inv`` inventory files +in ``local/share/inventory``. + +The reference manual is built in two passes, first by :class:`ReferenceBuilder` +with ``inventory`` output type and secondly with``html`` output type. The +:class:`ReferenceBuilder` itself uses :class:`ReferenceTopBuilder` and +:class:`ReferenceSubBuilder` to build subcomponents of the reference manual. +The :class:`ReferenceSubBuilder` examines the modules included in the +subcomponent by comparing the modification times of the module files with the +times saved in ``local/share/doctree/reference.pickle`` from the previous +build. Then new rst files are generated for new and updated modules. See +:meth:`get_new_and_updated_modules()`. + +After :trac:`31948`, when Sage is built, :class:`ReferenceBuilder` is not used +and its responsibility is now taken by the ``Makefile`` in ``$SAGE_ROOT/src/doc``. +""" + +# **************************************************************************** +# Copyright (C) 2008-2009 Mike Hansen +# 2009-2010 Mitesh Patel +# 2009-2015 J. H. Palmieri +# 2009 Carl Witty +# 2010-2017 Jeroen Demeyer +# 2012 William Stein +# 2012-2014 Nicolas M. Thiery +# 2012-2015 André Apitzsch +# 2012 Florent Hivert +# 2013-2014 Volker Braun +# 2013 R. Andrew Ohana +# 2015 Thierry Monteil +# 2015 Marc Mezzarobba +# 2015 Travis Scrimshaw +# 2016-2017 Frédéric Chapoton +# 2016 Erik M. Bray +# 2017 Kwankyu Lee +# 2017 François Bissey +# 2018 Julian Rüth +# +# This program is free software: you can redistribute it and/or modify +# it under the terms of the GNU General Public License as published by +# the Free Software Foundation, either version 2 of the License, or +# (at your option) any later version. +# https://www.gnu.org/licenses/ +# **************************************************************************** + +import logging +import os +import pickle +import re +import shutil +import subprocess +import sys +import time +import types +import warnings + +import sage.all +from sage.misc.cachefunc import cached_method +# Do not import SAGE_DOC globally as it interferes with doctesting with a random replacement +from sage.env import SAGE_DOC_SRC, SAGE_SRC, DOT_SAGE + +from .build_options import (LANGUAGES, OMIT, + ALLSPHINXOPTS, NUM_THREADS, WEBSITESPHINXOPTS, + ABORT_ON_ERROR) + +from .utils import build_many as _build_many + +logger = logging.getLogger(__name__) + + +########################################## +# Parallel Building Ref Manual # +########################################## + +def build_ref_doc(args): + doc = args[0] + lang = args[1] + format = args[2] + kwds = args[3] + args = args[4:] + if format == 'inventory': # you must not use the inventory to build the inventory + kwds['use_multidoc_inventory'] = False + getattr(ReferenceSubBuilder(doc, lang), format)(*args, **kwds) + + +########################################## +# Builders # +########################################## + +def builder_helper(type): + """ + Return a function which builds the documentation for + output type ``type``. + + TESTS: + + Check that :trac:`25161` has been resolved:: + + sage: from sage_docbuild.builders import DocBuilder + sage: from sage_docbuild.__main__ import setup_parser + sage: DocBuilder._options = setup_parser().parse_args([]) # builder_helper needs _options to be set + + sage: import sage_docbuild.sphinxbuild + sage: def raiseBaseException(): + ....: raise BaseException("abort pool operation") + sage: original_runsphinx, sage_docbuild.sphinxbuild.runsphinx = sage_docbuild.sphinxbuild.runsphinx, raiseBaseException + + sage: from sage.misc.temporary_file import tmp_dir + sage: os.environ['SAGE_DOC'] = tmp_dir() + sage: sage.env.var('SAGE_DOC') # random + sage: from sage_docbuild.builders import builder_helper, build_ref_doc + sage: from sage_docbuild.builders import _build_many as build_many + sage: helper = builder_helper("html") + sage: try: # optional - sagemath_doc_html + ....: build_many(build_ref_doc, [("docname", "en", "html", {})]) + ....: except Exception as E: + ....: "Non-exception during docbuild: abort pool operation" in str(E) + True + """ + def f(self, *args, **kwds): + output_dir = self._output_dir(type) + + options = ALLSPHINXOPTS + + if self.name == 'website': + # WEBSITESPHINXOPTS is either empty or " -A hide_pdf_links=1 " + options += WEBSITESPHINXOPTS + + if kwds.get('use_multidoc_inventory', True) and type != 'inventory': + options += ' -D multidoc_first_pass=0' + else: + options += ' -D multidoc_first_pass=1' + + + build_command = '-b %s -d %s %s %s %s' % (type, self._doctrees_dir(), + options, self.dir, + output_dir) + + # Provide "pdf" tag to be used with "only" directive as an alias of "latex" + if type == 'latex': + build_command = '-t pdf ' + build_command + + logger.debug(build_command) + + # Run Sphinx with Sage's special logger + sys.argv = ["sphinx-build"] + build_command.split() + from .sphinxbuild import runsphinx + try: + runsphinx() + except Exception: + if ABORT_ON_ERROR: + raise + except BaseException as e: + # We need to wrap a BaseException that is not an Exception in a + # regular Exception. Otherwise multiprocessing.Pool.get hangs, see + # #25161 + if ABORT_ON_ERROR: + raise Exception("Non-exception during docbuild: %s" % (e,), e) + + if "/latex" in output_dir: + logger.warning("LaTeX file written to {}".format(output_dir)) + else: + logger.warning( + "Build finished. The built documents can be found in {}". + format(output_dir)) + + f.is_output_format = True + return f + + +class DocBuilder(object): + def __init__(self, name, lang='en'): + """ + INPUT: + + - ``name`` - the name of a subdirectory in SAGE_DOC_SRC, such as + 'tutorial' or 'bordeaux_2008' + + - ``lang`` - (default "en") the language of the document. + """ + doc = name.split(os.path.sep) + + if doc[0] in LANGUAGES: + lang = doc[0] + doc.pop(0) + + self.name = os.path.join(*doc) + self.lang = lang + self.dir = os.path.join(SAGE_DOC_SRC, self.lang, self.name) + + def _output_dir(self, type): + """ + Return the directory where the output of type ``type`` is stored. + + If the directory does not exist, then it will automatically be + created. + + EXAMPLES:: + + sage: from sage_docbuild.builders import DocBuilder + sage: b = DocBuilder('tutorial') + sage: b._output_dir('html') # optional - sagemath_doc_html + '.../html/en/tutorial' + """ + from sage.env import SAGE_DOC + d = os.path.join(SAGE_DOC, type, self.lang, self.name) + os.makedirs(d, exist_ok=True) + return d + + def _doctrees_dir(self): + """ + Return the directory where the doctrees are stored. + + If the directory does not exist, then it will automatically be + created. + + EXAMPLES:: + + sage: from sage_docbuild.builders import DocBuilder + sage: b = DocBuilder('tutorial') + sage: b._doctrees_dir() # optional - sagemath_doc_html + '.../doctrees/en/tutorial' + """ + from sage.env import SAGE_DOC + d = os.path.join(SAGE_DOC, 'doctrees', self.lang, self.name) + os.makedirs(d, exist_ok=True) + return d + + def _output_formats(self): + """ + Return a list of the possible output formats. + + EXAMPLES:: + + sage: from sage_docbuild.builders import DocBuilder + sage: b = DocBuilder('tutorial') + sage: b._output_formats() + ['changes', 'html', 'htmlhelp', 'inventory', 'json', 'latex', 'linkcheck', 'pickle', 'web'] + """ + # Go through all the attributes of self and check to + # see which ones have an 'is_output_format' attribute. These + # are the ones created with builder_helper. + output_formats = [] + for attr in dir(self): + if hasattr(getattr(self, attr), 'is_output_format'): + output_formats.append(attr) + output_formats.sort() + return output_formats + + def pdf(self): + """ + Build the PDF files for this document. + + This is done by first (re)-building the LaTeX output, going + into that LaTeX directory, and running 'make all-pdf' there. + + EXAMPLES:: + + sage: from sage_docbuild.builders import DocBuilder + sage: b = DocBuilder('tutorial') + sage: b.pdf() #not tested + """ + self.latex() + tex_dir = self._output_dir('latex') + pdf_dir = self._output_dir('pdf') + + if self.name == 'reference': + # recover maths in tex, undoing what Sphinx did (trac #29993) + tex_file = os.path.join(tex_dir, 'reference.tex') + with open(tex_file) as f: + ref = f.read() + ref = re.sub(r'\\textbackslash{}', r'\\', ref) + ref = re.sub(r'\\textbackslash{}', r'\\', ref) + ref = re.sub(r'\\{', r'{', ref) + ref = re.sub(r'\\}', r'}', ref) + ref = re.sub(r'\\_', r'_', ref) + ref = re.sub(r'\\textasciicircum{}', r'^', ref) + with open(tex_file, 'w') as f: + f.write(ref) + + make_target = "cd '%s' && $MAKE %s && mv -f *.pdf '%s'" + error_message = "failed to run $MAKE %s in %s" + command = 'all-pdf' + + if subprocess.call(make_target % (tex_dir, command, pdf_dir), close_fds=False, shell=True): + raise RuntimeError(error_message % (command, tex_dir)) + logger.warning("Build finished. The built documents can be found in %s", pdf_dir) + + def clean(self, *args): + shutil.rmtree(self._doctrees_dir()) + output_formats = list(args) if args else self._output_formats() + for format in output_formats: + shutil.rmtree(self._output_dir(format), ignore_errors=True) + + html = builder_helper('html') + pickle = builder_helper('pickle') + web = pickle + json = builder_helper('json') + htmlhelp = builder_helper('htmlhelp') + latex = builder_helper('latex') + changes = builder_helper('changes') + linkcheck = builder_helper('linkcheck') + # import the customized builder for object.inv files + inventory = builder_helper('inventory') + + +def build_many(target, args, processes=None): + """ + Thin wrapper around `sage_docbuild.utils.build_many` which uses the + docbuild settings ``NUM_THREADS`` and ``ABORT_ON_ERROR``. + """ + if processes is None: + processes = NUM_THREADS + try: + _build_many(target, args, processes=processes) + except BaseException: + if ABORT_ON_ERROR: + raise + + +########################################## +# Parallel Building Ref Manual # +########################################## + +def build_other_doc(args): + document = args[0] + name = args[1] + kwds = args[2] + args = args[3:] + logger.warning("\nBuilding %s.\n" % document) + getattr(get_builder(document), name)(*args, **kwds) + + +class AllBuilder(object): + """ + A class used to build all of the documentation. + """ + def __getattr__(self, attr): + """ + For any attributes not explicitly defined, we just go through + all of the documents and call their attr. For example, + 'AllBuilder().json()' will go through all of the documents + and call the json() method on their builders. + """ + from functools import partial + return partial(self._wrapper, attr) + + def _wrapper(self, name, *args, **kwds): + """ + This is the function which goes through all of the documents + and does the actual building. + """ + start = time.time() + docs = self.get_all_documents() + refs = [x for x in docs if x.endswith('reference')] + others = [x for x in docs if not x.endswith('reference')] + + # Build the reference manual twice to resolve references. That is, + # build once with the inventory builder to construct the intersphinx + # inventory files, and then build the second time for real. So the + # first build should be as fast as possible; + logger.warning("\nBuilding reference manual, first pass.\n") + for document in refs: + getattr(get_builder(document), 'inventory')(*args, **kwds) + + from sage.env import SAGE_DOC + logger.warning("Building reference manual, second pass.\n") + os.makedirs(os.path.join(SAGE_DOC, "html", "en", "reference", "_static"), exist_ok=True) + for document in refs: + getattr(get_builder(document), name)(*args, **kwds) + + # build the other documents in parallel + L = [(doc, name, kwds) + args for doc in others] + + # Trac #31344: Work around crashes from multiprocessing + if sys.platform == 'darwin': + for target in L: + build_other_doc(target) + else: + build_many(build_other_doc, L) + logger.warning("Elapsed time: %.1f seconds." % (time.time() - start)) + logger.warning("Done building the documentation!") + + def get_all_documents(self): + """ + Return a list of all of the documents. + + A document is a directory within one of the language + subdirectories of SAGE_DOC_SRC specified by the global + LANGUAGES variable. + + EXAMPLES:: + + sage: from sage_docbuild.builders import AllBuilder + sage: documents = AllBuilder().get_all_documents() + sage: 'en/tutorial' in documents # optional - sage_spkg + True + sage: documents[0] == 'en/reference' + True + """ + documents = [] + for lang in LANGUAGES: + for document in os.listdir(os.path.join(SAGE_DOC_SRC, lang)): + if (document not in OMIT + and os.path.isdir(os.path.join(SAGE_DOC_SRC, lang, document))): + documents.append(os.path.join(lang, document)) + + # Ensure that the reference guide is compiled first so that links from + # the other documents to it are correctly resolved. + if 'en/reference' in documents: + documents.remove('en/reference') + documents.insert(0, 'en/reference') + + return documents + + +class WebsiteBuilder(DocBuilder): + def html(self): + """ + After we have finished building the website index page, we copy + everything one directory up. + + In addition, an index file is installed into the root doc directory. + """ + DocBuilder.html(self) + html_output_dir = self._output_dir('html') + for f in os.listdir(html_output_dir): + src = os.path.join(html_output_dir, f) + dst = os.path.join(html_output_dir, '..', f) + if os.path.isdir(src): + shutil.rmtree(dst, ignore_errors=True) + shutil.copytree(src, dst) + else: + shutil.copy2(src, dst) + + root_index_file = os.path.join(html_output_dir, '../../../index.html') + shutil.copy2(os.path.join(SAGE_DOC_SRC, self.lang, 'website', 'root_index.html'), + root_index_file) + + def clean(self): + """ + When we clean the output for the website index, we need to + remove all of the HTML that were placed in the parent + directory. + + In addition, remove the index file installed into the root doc directory. + """ + html_output_dir = self._output_dir('html') + parent_dir = os.path.realpath(os.path.join(html_output_dir, '..')) + for filename in os.listdir(html_output_dir): + parent_filename = os.path.join(parent_dir, filename) + if not os.path.exists(parent_filename): + continue + if os.path.isdir(parent_filename): + shutil.rmtree(parent_filename, ignore_errors=True) + else: + os.unlink(parent_filename) + + root_index_file = os.path.join(html_output_dir, '../../../index.html') + if os.path.exists(root_index_file): + os.remove(root_index_file) + + DocBuilder.clean(self) + + +class ReferenceBuilder(AllBuilder): + """ + This class builds the reference manual. It uses DocBuilder to + build the top-level page and ReferenceSubBuilder for each + sub-component. + """ + def __init__(self, name, lang='en'): + """ + Record the reference manual's name, in case it's not + identical to 'reference'. + """ + AllBuilder.__init__(self) + doc = name.split(os.path.sep) + + if doc[0] in LANGUAGES: + lang = doc[0] + doc.pop(0) + + self.name = doc[0] + self.lang = lang + + def _output_dir(self, type, lang=None): + """ + Return the directory where the output of type ``type`` is stored. + + If the directory does not exist, then it will automatically be + created. + + EXAMPLES:: + + sage: from sage_docbuild.builders import ReferenceBuilder + sage: b = ReferenceBuilder('reference') + sage: b._output_dir('html') # optional - sagemath_doc_html + '.../html/en/reference' + """ + from sage.env import SAGE_DOC + if lang is None: + lang = self.lang + d = os.path.join(SAGE_DOC, type, lang, self.name) + os.makedirs(d, exist_ok=True) + return d + + def _refdir(self): + return os.path.join(SAGE_DOC_SRC, self.lang, self.name) + + def _build_bibliography(self, format, *args, **kwds): + """ + Build the bibliography only + + The bibliography references.aux is referenced by the other + manuals and needs to be built first. + """ + refdir = self._refdir() + references = [ + (doc, self.lang, format, kwds) + args for doc in self.get_all_documents(refdir) + if doc == 'reference/references' + ] + build_many(build_ref_doc, references) + + def _build_everything_except_bibliography(self, format, *args, **kwds): + """ + Build the entire reference manual except the bibliography + """ + refdir = self._refdir() + non_references = [ + (doc, self.lang, format, kwds) + args for doc in self.get_all_documents(refdir) + if doc != 'reference/references' + ] + build_many(build_ref_doc, non_references) + + def _build_top_level(self, format, *args, **kwds): + """ + Build top-level document. + """ + getattr(ReferenceTopBuilder('reference'), format)(*args, **kwds) + + def _wrapper(self, format, *args, **kwds): + """ + Build reference manuals: build the + top-level document and its components. + """ + logger.info('Building bibliography') + self._build_bibliography(format, *args, **kwds) + logger.info('Bibliography finished, building dependent manuals') + self._build_everything_except_bibliography(format, *args, **kwds) + # The html refman must be build at the end to ensure correct + # merging of indexes and inventories. + # Sphinx is run here in the current process (not in a + # subprocess) and the IntersphinxCache gets populated to be + # used for the second pass of the reference manual and for + # the other documents. + self._build_top_level(format, *args, **kwds) + + def get_all_documents(self, refdir): + """ + Return a list of all reference manual components to build. + + We add a component name if it's a subdirectory of the manual's + directory and contains a file named 'index.rst'. + + We return the largest component (most subdirectory entries) + first since they will take the longest to build. + + EXAMPLES:: + + sage: from sage_docbuild.builders import ReferenceBuilder + sage: b = ReferenceBuilder('reference') + sage: refdir = os.path.join(os.environ['SAGE_DOC_SRC'], 'en', b.name) # optional - sage_spkg + sage: sorted(b.get_all_documents(refdir)) # optional - sage_spkg + ['reference/algebras', + 'reference/arithgroup', + ..., + 'reference/valuations'] + """ + documents = [] + + for doc in os.listdir(refdir): + directory = os.path.join(refdir, doc) + if os.path.exists(os.path.join(directory, 'index.rst')): + n = len(os.listdir(directory)) + documents.append((-n, os.path.join(self.name, doc))) + + return [doc[1] for doc in sorted(documents)] + + +class ReferenceTopBuilder(DocBuilder): + """ + This class builds the top-level page of the reference manual. + """ + def __init__(self, *args, **kwds): + DocBuilder.__init__(self, *args, **kwds) + self.name = 'reference' + self.lang = 'en' + + def _output_dir(self, type, lang=None): + """ + Return the directory where the output of type ``type`` is stored. + + If the directory does not exist, then it will automatically be + created. + + EXAMPLES:: + + sage: from sage_docbuild.builders import ReferenceTopBuilder + sage: b = ReferenceTopBuilder('reference') + sage: b._output_dir('html') # optional - sagemath_doc_html + '.../html/en/reference' + """ + from sage.env import SAGE_DOC + if lang is None: + lang = self.lang + d = os.path.join(SAGE_DOC, type, lang, self.name) + os.makedirs(d, exist_ok=True) + return d + + def pdf(self): + """ + Build top-level document. + """ + super().pdf() + + # we need to build master index file which lists all + # of the PDF file. So we create an html file, based on + # the file index.html from the "reference_top" target. + + # First build the top reference page. This only takes a few seconds. + getattr(get_builder('reference_top'), 'html')() + + from sage.env import SAGE_DOC + reference_dir = os.path.join(SAGE_DOC, 'html', 'en', 'reference') + output_dir = self._output_dir('pdf') + + # Install in output_dir a symlink to the directory containing static files. + # Prefer relative path for symlinks. + relpath = os.path.relpath(reference_dir, output_dir) + try: + os.symlink(os.path.join(relpath, '_static'), os.path.join(output_dir, '_static')) + except FileExistsError: + pass + + # Now modify top reference index.html page and write it to output_dir. + with open(os.path.join(reference_dir, 'index.html')) as f: + html = f.read() + html_output_dir = os.path.dirname(reference_dir) + + # Fix links in navigation bar + html = re.sub(r'Sage(.*)Documentation', + r'Sage\2Documentation', + html) + html = re.sub(r'Reference Manual', + r'Reference Manual (PDF version)', + html) + html = re.sub(r'
    • ', r'
    • ; change rst headers to html headers + rst_toc = re.sub(r'\*(.*)\n', + r'
    • \1
    • \n', rst_toc) + rst_toc = re.sub(r'\n([A-Z][a-zA-Z, ]*)\n[=]*\n', + r'
    \n\n\n

    \1

    \n\n
      \n', rst_toc) + rst_toc = re.sub(r'\n([A-Z][a-zA-Z, ]*)\n[-]*\n', + r'
    \n\n\n

    \1

    \n\n
      \n', rst_toc) + # now write the file. + with open(os.path.join(output_dir, 'index.html'), 'w') as new_index: + new_index.write(html[:html_end_preamble]) + new_index.write('

      Sage Reference Manual (PDF version)

      ') + new_index.write(rst_body) + new_index.write('
        ') + new_index.write(rst_toc) + new_index.write('
      \n\n') + new_index.write(html[html_bottom:]) + logger.warning(''' +PDF documents have been created in subdirectories of + + %s + +Alternatively, you can open + + %s + +for a webpage listing all of the documents.''' % (output_dir, + os.path.join(output_dir, + 'index.html'))) + + +class ReferenceSubBuilder(DocBuilder): + """ + This class builds sub-components of the reference manual. It is + responsible for making sure that the auto generated reST files for the + Sage library are up to date. + + When building any output, we must first go through and check + to see if we need to update any of the autogenerated reST + files. There are two cases where this would happen: + + 1. A new module gets added to one of the toctrees. + + 2. The actual module gets updated and possibly contains a new + title. + """ + def __init__(self, *args, **kwds): + DocBuilder.__init__(self, *args, **kwds) + self._wrap_builder_helpers() + + def _wrap_builder_helpers(self): + from functools import partial, update_wrapper + for attr in dir(self): + if hasattr(getattr(self, attr), 'is_output_format'): + f = partial(self._wrapper, attr) + f.is_output_format = True + update_wrapper(f, getattr(self, attr)) + setattr(self, attr, f) + + def _wrapper(self, build_type, *args, **kwds): + """ + This is the wrapper around the builder_helper methods that + goes through and makes sure things are up to date. + """ + # Force regeneration of all modules if the inherited + # and/or underscored members options have changed. + cache = self.get_cache() + force = False + try: + if (cache['option_inherited'] != self._options.inherited or + cache['option_underscore'] != self._options.underscore): + logger.info("Detected change(s) in inherited and/or underscored members option(s).") + force = True + except KeyError: + force = True + cache['option_inherited'] = self._options.inherited + cache['option_underscore'] = self._options.underscore + self.save_cache() + + # After "sage -clone", refresh the reST file mtimes in + # environment.pickle. + if self._options.update_mtimes: + logger.info("Checking for reST file mtimes to update...") + self.update_mtimes() + + if force: + # Write reST files for all modules from scratch. + self.clean_auto() + for module_name in self.get_all_included_modules(): + self.write_auto_rest_file(module_name) + else: + # Write reST files for new and updated modules. + for module_name in self.get_new_and_updated_modules(): + self.write_auto_rest_file(module_name) + + # Copy over the custom reST files from _sage + _sage = os.path.join(self.dir, '_sage') + if os.path.exists(_sage): + logger.info("Copying over custom reST files from %s ...", _sage) + shutil.copytree(_sage, os.path.join(self.dir, 'sage')) + + getattr(DocBuilder, build_type)(self, *args, **kwds) + + def cache_filename(self): + """ + Return the filename where the pickle of the reference cache + is stored. + """ + return os.path.join(self._doctrees_dir(), 'reference.pickle') + + @cached_method + def get_cache(self): + """ + Retrieve the reference cache which contains the options previously used + by the reference builder. + + If it doesn't exist, then we just return an empty dictionary. If it + is corrupted, return an empty dictionary. + """ + filename = self.cache_filename() + if not os.path.exists(filename): + return {} + with open(self.cache_filename(), 'rb') as file: + try: + cache = pickle.load(file) + except Exception: + logger.debug("Cache file '%s' is corrupted; ignoring it..." % filename) + cache = {} + else: + logger.debug("Loaded the reference cache: %s", filename) + return cache + + def save_cache(self): + """ + Pickle the current reference cache for later retrieval. + """ + cache = self.get_cache() + try: + with open(self.cache_filename(), 'wb') as file: + pickle.dump(cache, file) + logger.debug("Saved the reference cache: %s", self.cache_filename()) + except PermissionError: + logger.debug("Permission denied for the reference cache: %s", self.cache_filename()) + + def get_sphinx_environment(self): + """ + Return the Sphinx environment for this project. + """ + class FakeConfig(object): + values = tuple() + + class FakeApp(object): + def __init__(self, dir): + self.srcdir = dir + self.config = FakeConfig() + + env_pickle = os.path.join(self._doctrees_dir(), 'environment.pickle') + try: + with open(env_pickle, 'rb') as f: + env = pickle.load(f) + env.app = FakeApp(self.dir) + env.config.values = env.app.config.values + logger.debug("Opened Sphinx environment: %s", env_pickle) + return env + except (IOError, EOFError) as err: + logger.debug( + f"Failed to open Sphinx environment '{env_pickle}'", exc_info=True) + + def update_mtimes(self): + """ + Update the modification times for reST files in the Sphinx + environment for this project. + """ + env = self.get_sphinx_environment() + if env is not None: + for doc in env.all_docs: + env.all_docs[doc] = time.time() + logger.info("Updated %d reST file mtimes", len(env.all_docs)) + # This is the only place we need to save (as opposed to + # load) Sphinx's pickle, so we do it right here. + env_pickle = os.path.join(self._doctrees_dir(), + 'environment.pickle') + + # When cloning a new branch (see + # SAGE_LOCAL/bin/sage-clone), we hard link the doc output. + # To avoid making unlinked, potentially inconsistent + # copies of the environment, we *don't* use + # env.topickle(env_pickle), which first writes a temporary + # file. We adapt sphinx.environment's + # BuildEnvironment.topickle: + + # remove unpicklable attributes + env.set_warnfunc(None) + del env.config.values + with open(env_pickle, 'wb') as picklefile: + # remove potentially pickling-problematic values from config + for key, val in vars(env.config).items(): + if key.startswith('_') or isinstance(val, (types.ModuleType, + types.FunctionType, + type)): + del env.config[key] + pickle.dump(env, picklefile, pickle.HIGHEST_PROTOCOL) + + logger.debug("Saved Sphinx environment: %s", env_pickle) + + def get_modified_modules(self): + """ + Return an iterator for all the modules that have been modified + since the documentation was last built. + """ + env = self.get_sphinx_environment() + if env is None: + logger.debug("Stopped check for modified modules.") + return + try: + added, changed, removed = env.get_outdated_files(False) + logger.info("Sphinx found %d modified modules", len(changed)) + except OSError as err: + logger.debug("Sphinx failed to determine modified modules: %s", err) + return + for name in changed: + # Only pay attention to files in a directory sage/... In + # particular, don't treat a file like 'sagetex.rst' in + # doc/en/reference/misc as an autogenerated file: see + # #14199. + if name.startswith('sage' + os.sep): + yield name + + def print_modified_modules(self): + """ + Print a list of all the modules that have been modified since + the documentation was last built. + """ + for module_name in self.get_modified_modules(): + print(module_name) + + def get_all_rst_files(self, exclude_sage=True): + """ + Return an iterator for all rst files which are not + autogenerated. + """ + for directory, subdirs, files in os.walk(self.dir): + if exclude_sage and directory.startswith(os.path.join(self.dir, 'sage')): + continue + for filename in files: + if not filename.endswith('.rst'): + continue + yield os.path.join(directory, filename) + + def get_all_included_modules(self): + """ + Return an iterator for all modules which are included in the + reference manual. + """ + for filename in self.get_all_rst_files(): + for module in self.get_modules(filename): + yield module + + def get_new_and_updated_modules(self): + """ + Return an iterator for all new and updated modules that appear in + the toctrees, and remove obsolete old modules. + """ + env = self.get_sphinx_environment() + if env is None: + all_docs = {} + else: + all_docs = env.all_docs + + new_modules = [] + updated_modules = [] + old_modules = [] + for module_name in self.get_all_included_modules(): + docname = module_name.replace('.', os.path.sep) + + if docname not in all_docs: + new_modules.append(module_name) + yield module_name + continue + + # get the modification timestamp of the reST doc for the module + mtime = all_docs[docname] + try: + with warnings.catch_warnings(): + # primarily intended to ignore deprecation warnings + warnings.simplefilter("ignore") + __import__(module_name) + except ImportError as err: + logger.error("Warning: Could not import %s %s", module_name, err) + raise + + module_filename = sys.modules[module_name].__file__ + if (module_filename.endswith('.pyc') or module_filename.endswith('.pyo')): + source_filename = module_filename[:-1] + if (os.path.exists(source_filename)): + module_filename = source_filename + newtime = os.path.getmtime(module_filename) + + if newtime > mtime: + updated_modules.append(module_name) + yield module_name + else: # keep good old module + old_modules.append(module_name) + + removed_modules = [] + for docname in all_docs.keys(): + if docname.startswith('sage' + os.path.sep): + module_name = docname.replace(os.path.sep, '.') + if not (module_name in old_modules or module_name in updated_modules): + try: + os.remove(os.path.join(self.dir, docname) + '.rst') + except OSError: # already removed + pass + logger.debug("Deleted auto-generated reST file {}".format(docname)) + removed_modules.append(module_name) + + logger.info("Found %d new modules", len(new_modules)) + logger.info("Found %d updated modules", len(updated_modules)) + logger.info("Removed %d obsolete modules", len(removed_modules)) + + def print_new_and_updated_modules(self): + """ + Print all the modules that appear in the toctrees that + are newly included or updated. + """ + for module_name in self.get_new_and_updated_modules(): + print(module_name) + + def get_modules(self, filename): + """ + Given a filename for a reST file, return an iterator for + all of the autogenerated reST files that it includes. + """ + # Create the regular expression used to detect an autogenerated file + auto_re = re.compile(r'^\s*(..\/)*(sage(_docbuild)?\/[\w\/]*)\s*$') + + # Read the lines + with open(filename) as f: + lines = f.readlines() + for line in lines: + match = auto_re.match(line) + if match: + yield match.group(2).replace(os.path.sep, '.') + + def get_module_docstring_title(self, module_name): + """ + Return the title of the module from its docstring. + """ + # Try to import the module + try: + __import__(module_name) + except ImportError as err: + logger.error("Warning: Could not import %s %s", module_name, err) + return "UNABLE TO IMPORT MODULE" + module = sys.modules[module_name] + + # Get the docstring + doc = module.__doc__ + if doc is None: + doc = module.doc if hasattr(module, 'doc') else "" + + # Extract the title + i = doc.find('\n') + if i != -1: + return doc[i + 1:].lstrip().splitlines()[0] + else: + return doc + + def auto_rest_filename(self, module_name): + """ + Return the name of the file associated to a given module + + EXAMPLES:: + + sage: from sage_docbuild.builders import ReferenceSubBuilder + sage: ReferenceSubBuilder("reference").auto_rest_filename("sage.combinat.partition") + '.../en/reference/sage/combinat/partition.rst' + """ + return self.dir + os.path.sep + module_name.replace('.', os.path.sep) + '.rst' + + def write_auto_rest_file(self, module_name): + """ + Write the autogenerated reST file for module_name. + """ + if not module_name.startswith('sage'): + return + filename = self.auto_rest_filename(module_name) + os.makedirs(os.path.dirname(filename), exist_ok=True) + + title = self.get_module_docstring_title(module_name) + + if title == '': + logger.error("Warning: Missing title for %s", module_name) + title = "MISSING TITLE" + + with open(filename, 'w') as outfile: + # Don't doctest the autogenerated file. + outfile.write(".. nodoctest\n\n") + # Now write the actual content. + outfile.write(".. _%s:\n\n" % (module_name.replace(".__init__", ""))) + outfile.write(title + '\n') + outfile.write('=' * len(title) + "\n\n") + outfile.write('.. This file has been autogenerated.\n\n') + + inherited = ':inherited-members:' if self._options.inherited else '' + + automodule = ''' +.. automodule:: %s + :members: + :undoc-members: + :show-inheritance: + %s + +''' + outfile.write(automodule % (module_name, inherited)) + + def clean_auto(self): + """ + Remove all autogenerated reST files. + """ + try: + shutil.rmtree(os.path.join(self.dir, 'sage')) + logger.debug("Deleted auto-generated reST files in: %s", + os.path.join(self.dir, 'sage')) + except OSError: + pass + + def get_unincluded_modules(self): + """ + Return an iterator for all the modules in the Sage library + which are not included in the reference manual. + """ + # Make a dictionary of the included modules + included_modules = {} + for module_name in self.get_all_included_modules(): + included_modules[module_name] = True + + base_path = os.path.join(SAGE_SRC, 'sage') + for directory, subdirs, files in os.walk(base_path): + for filename in files: + if not (filename.endswith('.py') or + filename.endswith('.pyx')): + continue + + path = os.path.join(directory, filename) + + # Create the module name + module_name = path[len(base_path):].replace(os.path.sep, '.') + module_name = 'sage' + module_name + module_name = module_name[:-4] if module_name.endswith('pyx') else module_name[:-3] + + # Exclude some ones -- we don't want init the manual + if module_name.endswith('__init__') or module_name.endswith('all'): + continue + + if module_name not in included_modules: + yield module_name + + def print_unincluded_modules(self): + """ + Print all of the modules which are not included in the Sage + reference manual. + """ + for module_name in self.get_unincluded_modules(): + print(module_name) + + def print_included_modules(self): + """ + Print all of the modules that are included in the Sage reference + manual. + """ + for module_name in self.get_all_included_modules(): + print(module_name) + + +class SingleFileBuilder(DocBuilder): + """ + This is the class used to build the documentation for a single + user-specified file. If the file is called 'foo.py', then the + documentation is built in ``DIR/foo/`` if the user passes the + command line option "-o DIR", or in ``DOT_SAGE/docbuild/foo/`` + otherwise. + """ + def __init__(self, path): + """ + INPUT: + + - ``path`` - the path to the file for which documentation + should be built + """ + self.lang = 'en' + self.name = 'single_file' + path = os.path.abspath(path) + + # Create docbuild and relevant subdirectories, e.g., + # the static and templates directories in the output directory. + # By default, this is DOT_SAGE/docbuild/MODULE_NAME, but can + # also be specified at the command line. + module_name = os.path.splitext(os.path.basename(path))[0] + latex_name = module_name.replace('_', r'\\_') + + if self._options.output_dir: + base_dir = os.path.join(self._options.output_dir, module_name) + if os.path.exists(base_dir): + logger.warning('Warning: Directory %s exists. It is safer to build in a new directory.' % base_dir) + else: + base_dir = os.path.join(DOT_SAGE, 'docbuild', module_name) + try: + shutil.rmtree(base_dir) + except OSError: + pass + self.dir = os.path.join(base_dir, 'source') + + os.makedirs(os.path.join(self.dir, "static"), exist_ok=True) + os.makedirs(os.path.join(self.dir, "templates"), exist_ok=True) + # Write self.dir/conf.py + conf = r"""# This file is automatically generated by {}, do not edit! + +import sys, os, contextlib +sys.path.append({!r}) + +from sage.docs.conf import * +html_static_path = [] + html_common_static_path + +project = 'Documentation for {}' +release = 'unknown' +name = {!r} +html_title = project +html_short_title = project +htmlhelp_basename = name + +with contextlib.suppress(ValueError): + extensions.remove('multidocs') # see #29651 + extensions.remove('inventory_builder') + +latex_domain_indices = False +latex_documents = [ + ('index', name + '.tex', 'Documentation for {}', + 'unknown', 'manual'), +] +""".format(__file__, self.dir, module_name, module_name, latex_name) + + if 'SAGE_DOC_UNDERSCORE' in os.environ: + conf += r""" +def setup(app): + app.connect('autodoc-skip-member', skip_member) +""" + + with open(os.path.join(self.dir, 'conf.py'), 'w') as conffile: + conffile.write(conf) + + # Write self.dir/index.rst + title = 'Docs for file %s' % path + heading = title + "\n" + ("=" * len(title)) + index = r"""{} + +.. This file is automatically generated by {}, do not edit! + +.. automodule:: {} + :members: + :undoc-members: + :show-inheritance: + +""".format(heading, __file__, module_name) + with open(os.path.join(self.dir, 'index.rst'), 'w') as indexfile: + indexfile.write(index) + + # Create link from original file to self.dir. Note that we + # append self.dir to sys.path in conf.py. This is reasonably + # safe (but not perfect), since we just created self.dir. + try: + os.symlink(path, os.path.join(self.dir, os.path.basename(path))) + except OSError: + pass + + def _output_dir(self, type): + """ + Return the directory where the output of type ``type`` is stored. + + If the directory does not exist, then it will automatically be + created. + """ + base_dir = os.path.split(self.dir)[0] + d = os.path.join(base_dir, "output", type) + os.makedirs(d, exist_ok=True) + return d + + def _doctrees_dir(self): + """ + Return the directory where the doctrees are stored. + + If the directory does not exist, then it will automatically be + created. + """ + return self._output_dir('doctrees') + + +def get_builder(name): + """ + Return an appropriate *Builder* object for the document ``name``. + + DocBuilder and its subclasses do all the real work in building the + documentation. + """ + if name == 'all': + from sage.misc.superseded import deprecation + deprecation(31948, 'avoid using "sage --docbuild all html" and "sage --docbuild all pdf"; ' + 'use "make doc" and "make doc-pdf" instead, if available.') + return AllBuilder() + elif name == 'reference_top': + return ReferenceTopBuilder('reference') + elif name.endswith('reference'): + return ReferenceBuilder(name) + elif 'reference' in name and os.path.exists(os.path.join(SAGE_DOC_SRC, 'en', name)): + return ReferenceSubBuilder(name) + elif name.endswith('website'): + return WebsiteBuilder(name) + elif name.startswith('file='): + path = name[5:] + if path.endswith('.sage') or path.endswith('.pyx'): + raise NotImplementedError('Building documentation for a single file only works for Python files.') + return SingleFileBuilder(path) + elif name in get_documents() or name in AllBuilder().get_all_documents(): + return DocBuilder(name) + else: + print("'%s' is not a recognized document. Type 'sage --docbuild -D' for a list" % name) + print("of documents, or 'sage --docbuild --help' for more help.") + sys.exit(1) + + +def get_documents(): + """ + Return a list of document names the Sage documentation builder + will accept as command-line arguments. + """ + all_b = AllBuilder() + docs = all_b.get_all_documents() + docs = [(d[3:] if d[0:3] == 'en/' else d) for d in docs] + return docs diff --git a/src/sage_docbuild/conf.py b/src/sage_docbuild/conf.py new file mode 100644 index 00000000000..13c969a0ec8 --- /dev/null +++ b/src/sage_docbuild/conf.py @@ -0,0 +1,984 @@ +import sys +import os +import sphinx +from sage.env import SAGE_DOC_SRC, SAGE_DOC, THEBE_DIR, PPLPY_DOCS, MATHJAX_DIR +from sage.misc.latex_macros import sage_mathjax_macros +import sage.version +from sage.misc.sagedoc import extlinks +import dateutil.parser +from docutils import nodes +from docutils.transforms import Transform +from sphinx.ext.doctest import blankline_re +from sphinx import highlighting +import sphinx.ext.intersphinx as intersphinx +from IPython.lib.lexers import IPythonConsoleLexer, IPyLexer + + +# General configuration +# --------------------- + +# Add any Sphinx extension module names here, as strings. They can be extensions +# coming with Sphinx (named 'sphinx.ext.*') or your custom ones. +extensions = [ + 'sage_docbuild.ext.inventory_builder', + 'sage_docbuild.ext.multidocs', + 'sage_docbuild.ext.sage_autodoc', + 'sphinx.ext.todo', + 'sphinx.ext.extlinks', + 'sphinx.ext.mathjax', + 'IPython.sphinxext.ipython_directive', + 'matplotlib.sphinxext.plot_directive', + 'jupyter_sphinx', +] + +jupyter_execute_default_kernel = 'sagemath' + +jupyter_sphinx_thebelab_config = { + 'requestKernel': True, + 'binderOptions': { + 'repo': "sagemath/sage-binder-env", + }, + 'kernelOptions': { + 'name': "sagemath", + 'kernelName': "sagemath", + 'path': ".", + }, +} + +# This code is executed before each ".. PLOT::" directive in the Sphinx +# documentation. It defines a 'sphinx_plot' function that displays a Sage object +# through matplotlib, so that it will be displayed in the HTML doc +plot_html_show_source_link = False +plot_pre_code = r""" +# Set locale to prevent having commas in decimal numbers +# in tachyon input (see https://trac.sagemath.org/ticket/28971) +import locale +locale.setlocale(locale.LC_NUMERIC, 'C') +def sphinx_plot(graphics, **kwds): + import matplotlib.image as mpimg + import matplotlib.pyplot as plt + from sage.misc.temporary_file import tmp_filename + from sage.plot.graphics import _parse_figsize + if os.environ.get('SAGE_SKIP_PLOT_DIRECTIVE', 'no') != 'yes': + ## Option handling is taken from Graphics.save + options = dict() + if isinstance(graphics, sage.plot.graphics.Graphics): + options.update(sage.plot.graphics.Graphics.SHOW_OPTIONS) + options.update(graphics._extra_kwds) + options.update(kwds) + elif isinstance(graphics, sage.plot.multigraphics.MultiGraphics): + options.update(kwds) + else: + graphics = graphics.plot(**kwds) + dpi = options.pop('dpi', None) + transparent = options.pop('transparent', None) + fig_tight = options.pop('fig_tight', None) + figsize = options.pop('figsize', None) + if figsize is not None: + figsize = _parse_figsize(figsize) + plt.figure(figsize=figsize) + figure = plt.gcf() + if isinstance(graphics, (sage.plot.graphics.Graphics, + sage.plot.multigraphics.MultiGraphics)): + graphics.matplotlib(figure=figure, figsize=figsize, **options) + if isinstance(graphics, (sage.plot.graphics.Graphics, + sage.plot.multigraphics.GraphicsArray)): + # for Graphics and GraphicsArray, tight_layout adjusts the + # *subplot* parameters so ticks aren't cut off, etc. + figure.tight_layout() + else: + # 3d graphics via png + import matplotlib as mpl + mpl.rcParams['image.interpolation'] = 'bilinear' + mpl.rcParams['image.resample'] = False + mpl.rcParams['figure.figsize'] = [8.0, 6.0] + mpl.rcParams['figure.dpi'] = 80 + mpl.rcParams['savefig.dpi'] = 100 + fn = tmp_filename(ext=".png") + graphics.save(fn) + img = mpimg.imread(fn) + plt.imshow(img) + plt.axis("off") + plt.margins(0) + if not isinstance(graphics, sage.plot.multigraphics.MultiGraphics): + plt.tight_layout(pad=0) + +from sage.all_cmdline import * +""" + +plot_html_show_formats = False +plot_formats = ['svg', 'pdf', 'png'] + +# We do *not* fully initialize intersphinx since we call it by hand +# in find_sage_dangling_links. +#, 'sphinx.ext.intersphinx'] + +# Add any paths that contain templates here, relative to this directory. +templates_path = [os.path.join(SAGE_DOC_SRC, 'common', 'templates'), 'templates'] + +# The suffix of source filenames. +source_suffix = '.rst' + +# The master toctree document. +master_doc = 'index' + +# General information about the project. +project = "" +copyright = "2005--{}, The Sage Development Team".format(dateutil.parser.parse(sage.version.date).year) + +# The version info for the project you're documenting, acts as replacement for +# |version| and |release|, also used in various other places throughout the +# built documents. +version = sage.version.version +release = sage.version.version + +# The language for content autogenerated by Sphinx. Refer to documentation +# for a list of supported languages. +#language = None + +# There are two options for replacing |today|: either, you set today to some +# non-false value, then it is used: +#today = '' +# Else, today_fmt is used as the format for a strftime call. +#today_fmt = '%B %d, %Y' + +# List of glob-style patterns that should be excluded when looking for +# source files. [1] They are matched against the source file names +# relative to the source directory, using slashes as directory +# separators on all platforms. +exclude_patterns = ['.build'] + +# The reST default role (used for this markup: `text`) to use for all documents. +default_role = 'math' + +# If true, '()' will be appended to :func: etc. cross-reference text. +#add_function_parentheses = True + +# If true, the current module name will be prepended to all description +# unit titles (such as .. function::). +#add_module_names = True + +# If true, sectionauthor and moduleauthor directives will be shown in the +# output. They are ignored by default. +#show_authors = False + +# The name of the Pygments (syntax highlighting) style to use. NOTE: +# This overrides a HTML theme's corresponding setting (see below). +pygments_style = 'sphinx' + +# Default lexer to use when highlighting code blocks, using the IPython +# console lexers. 'ipycon' is the IPython console, which is what we want +# for most code blocks: anything with "sage:" prompts. For other IPython, +# like blocks which might appear in a notebook cell, use 'ipython'. +highlighting.lexers['ipycon'] = IPythonConsoleLexer(in1_regex=r'sage: ', in2_regex=r'[.][.][.][.]: ') +highlighting.lexers['ipython'] = IPyLexer() +highlight_language = 'ipycon' + +# Extension configuration +# ----------------------- + +# include the todos +todo_include_todos = True + +# Cross-links to other project's online documentation. +python_version = sys.version_info.major + +def set_intersphinx_mappings(app, config): + """ + Add precompiled inventory (the objects.inv) + """ + refpath = os.path.join(SAGE_DOC, "html", "en", "reference") + invpath = os.path.join(SAGE_DOC, "inventory", "en", "reference") + if app.config.multidoc_first_pass == 1 or \ + not (os.path.exists(refpath) and os.path.exists(invpath)): + app.config.intersphinx_mapping = {} + return + + app.config.intersphinx_mapping = { + 'python': ('https://docs.python.org/', + os.path.join(SAGE_DOC_SRC, "common", + "python{}.inv".format(python_version))), + 'pplpy': (PPLPY_DOCS, None)} + + # Add master intersphinx mapping + dst = os.path.join(invpath, 'objects.inv') + app.config.intersphinx_mapping['sagemath'] = (refpath, dst) + + # Add intersphinx mapping for subdirectories + # We intentionally do not name these such that these get higher + # priority in case of conflicts + for directory in os.listdir(os.path.join(invpath)): + if directory == 'jupyter_execute': + # This directory is created by jupyter-sphinx extension for + # internal use and should be ignored here. See trac #33507. + continue + if os.path.isdir(os.path.join(invpath, directory)): + src = os.path.join(refpath, directory) + dst = os.path.join(invpath, directory, 'objects.inv') + app.config.intersphinx_mapping[src] = dst + + intersphinx.normalize_intersphinx_mapping(app, config) + +# By default document are not master. +multidocs_is_master = True + +# Options for HTML output +# ----------------------- + +# Sage default HTML theme. We use a custom theme to set a Pygments style, +# stylesheet, and insert MathJax macros. See the directory +# doc/common/themes/sage-classic/ for files comprising the custom theme. +html_theme = 'sage-classic' + +# Theme options are theme-specific and customize the look and feel of +# a theme further. For a list of options available for each theme, +# see the documentation. +html_theme_options = {} + +# Add any paths that contain custom themes here, relative to this directory. +html_theme_path = [os.path.join(SAGE_DOC_SRC, 'common', 'themes')] + +# HTML style sheet NOTE: This overrides a HTML theme's corresponding +# setting. +#html_style = 'default.css' + +# The name for this set of Sphinx documents. If None, it defaults to +# " v documentation". +#html_title = None + +# A shorter title for the navigation bar. Default is the same as html_title. +#html_short_title = None + +# The name of an image file (within the static path) to place at the top of +# the sidebar. +#html_logo = 'sagelogo-word.ico' + +# The name of an image file (within the static path) to use as favicon of the +# docs. This file should be a Windows icon file (.ico) being 16x16 or 32x32 +# pixels large. +html_favicon = 'favicon.ico' + +# html_static_path defined here and imported in the actual configuration file +# conf.py read by Sphinx was the cause of subtle bugs in builders (see #30418 for +# instance). Hence now html_common_static_path contains the common paths to static +# files, and is combined to html_static_path in each conf.py file read by Sphinx. +html_common_static_path = [os.path.join(SAGE_DOC_SRC, 'common', 'static'), + THEBE_DIR, 'static'] + +# Configure MathJax +# https://docs.mathjax.org/en/latest/options/input/tex.html +mathjax3_config = { + "tex": { + # Add custom sage macros + # http://docs.mathjax.org/en/latest/input/tex/macros.html + "macros": sage_mathjax_macros(), + # Add $...$ as possible inline math + # https://docs.mathjax.org/en/latest/input/tex/delimiters.html#tex-and-latex-math-delimiters + "inlineMath": [["$", "$"], ["\\(", "\\)"]], + # Increase the limit the size of the string to be processed + # https://docs.mathjax.org/en/latest/options/input/tex.html#option-descriptions + "maxBuffer": 50 * 1024, + # Use colorv2 extension instead of built-in color extension + # https://docs.mathjax.org/en/latest/input/tex/extensions/autoload.html#tex-autoload-options + # https://docs.mathjax.org/en/latest/input/tex/extensions/colorv2.html#tex-colorv2 + "autoload": {"color": [], "colorv2": ["color"]}, + }, +} + +if os.environ.get('SAGE_USE_CDNS', 'no') == 'yes': + mathjax_path = "https://cdn.jsdelivr.net/npm/mathjax@3/es5/tex-chtml.js" +else: + mathjax_path = 'mathjax/tex-chtml.js' + html_common_static_path += [MATHJAX_DIR] + +# A list of glob-style patterns that should be excluded when looking for source +# files. They are matched against the source file names relative to the +# source directory, using slashes as directory separators on all platforms. +exclude_patterns = [] + +# If not '', a 'Last updated on:' timestamp is inserted at every page bottom, +# using the given strftime format. +#html_last_updated_fmt = '%b %d, %Y' + +# If true, SmartyPants will be used to convert quotes and dashes to +# typographically correct entities. +#html_use_smartypants = True + +# Custom sidebar templates, maps document names to template names. +#html_sidebars = {} + +# Additional templates that should be rendered to pages, maps page names to +# template names. +#html_additional_pages = {} + +# If false, no module index is generated. +#html_use_modindex = True + +# A list of prefixes that are ignored for sorting the Python module index ( if +# this is set to ['foo.'], then foo.bar is shown under B, not F). Works only +# for the HTML builder currently. +modindex_common_prefix = ['sage.'] + +# If false, no index is generated. +#html_use_index = True + +# If true, the index is split into individual pages for each letter. +html_split_index = True + +# If true, the reST sources are included in the HTML build as _sources/. +#html_copy_source = True + +# If true, an OpenSearch description file will be output, and all pages will +# contain a tag referring to it. The value of this option must be the +# base URL from which the finished HTML is served. +#html_use_opensearch = '' + +# If nonempty, this is the file name suffix for HTML files (e.g. ".xhtml"). +#html_file_suffix = '' + +# Output file base name for HTML help builder. +#htmlhelp_basename = '' + +# Options for LaTeX output +# ------------------------ +# See http://sphinx-doc.org/config.html#confval-latex_elements +latex_elements = {} + +# The paper size ('letterpaper' or 'a4paper'). +#latex_elements['papersize'] = 'letterpaper' + +# The font size ('10pt', '11pt' or '12pt'). +#latex_elements['pointsize'] = '10pt' + +# Grouping the document tree into LaTeX files. List of tuples +# (source start file, target name, title, author, document class [howto/manual]). +latex_documents = [] + +# The name of an image file (relative to this directory) to place at the top of +# the title page. +#latex_logo = 'sagelogo-word.png' + +# For "manual" documents, if this is true, then toplevel headings are parts, +# not chapters. +#latex_use_parts = False + +# Additional stuff for the LaTeX preamble. +latex_elements['preamble'] = r""" +\usepackage{amsmath} +\usepackage{amssymb} +\usepackage{textcomp} +\usepackage{mathrsfs} +\usepackage{iftex} + +% Only declare unicode characters when compiling with pdftex; E.g. japanese +% tutorial does not use pdftex +\ifPDFTeX + \DeclareUnicodeCharacter{01CE}{\capitalcaron a} + \DeclareUnicodeCharacter{0428}{cyrillic Sha} + \DeclareUnicodeCharacter{250C}{+} + \DeclareUnicodeCharacter{2510}{+} + \DeclareUnicodeCharacter{2514}{+} + \DeclareUnicodeCharacter{2518}{+} + \DeclareUnicodeCharacter{253C}{+} + + \DeclareUnicodeCharacter{03B1}{\ensuremath{\alpha}} + \DeclareUnicodeCharacter{03B2}{\ensuremath{\beta}} + \DeclareUnicodeCharacter{03B3}{\ensuremath{\gamma}} + \DeclareUnicodeCharacter{0393}{\ensuremath{\Gamma}} + \DeclareUnicodeCharacter{03B4}{\ensuremath{\delta}} + \DeclareUnicodeCharacter{0394}{\ensuremath{\Delta}} + \DeclareUnicodeCharacter{03B5}{\ensuremath{\varepsilon}} + \DeclareUnicodeCharacter{03B6}{\ensuremath{\zeta}} + \DeclareUnicodeCharacter{03B7}{\ensuremath{\eta}} + \DeclareUnicodeCharacter{03B8}{\ensuremath{\vartheta}} + \DeclareUnicodeCharacter{0398}{\ensuremath{\Theta}} + \DeclareUnicodeCharacter{03BA}{\ensuremath{\kappa}} + \DeclareUnicodeCharacter{03BB}{\ensuremath{\lambda}} + \DeclareUnicodeCharacter{039B}{\ensuremath{\Lambda}} + \DeclareUnicodeCharacter{00B5}{\ensuremath{\mu}} % micron sign + \DeclareUnicodeCharacter{03BC}{\ensuremath{\mu}} + \DeclareUnicodeCharacter{03BD}{\ensuremath{\nu}} + \DeclareUnicodeCharacter{03BE}{\ensuremath{\xi}} + \DeclareUnicodeCharacter{039E}{\ensuremath{\Xi}} + \DeclareUnicodeCharacter{03B9}{\ensuremath{\iota}} + \DeclareUnicodeCharacter{03C0}{\ensuremath{\pi}} + \DeclareUnicodeCharacter{03A0}{\ensuremath{\Pi}} + \DeclareUnicodeCharacter{03C1}{\ensuremath{\rho}} + \DeclareUnicodeCharacter{03C3}{\ensuremath{\sigma}} + \DeclareUnicodeCharacter{03A3}{\ensuremath{\Sigma}} + \DeclareUnicodeCharacter{03C4}{\ensuremath{\tau}} + \DeclareUnicodeCharacter{03C6}{\ensuremath{\varphi}} + \DeclareUnicodeCharacter{03A6}{\ensuremath{\Phi}} + \DeclareUnicodeCharacter{03C7}{\ensuremath{\chi}} + \DeclareUnicodeCharacter{03C8}{\ensuremath{\psi}} + \DeclareUnicodeCharacter{03A8}{\ensuremath{\Psi}} + \DeclareUnicodeCharacter{03C9}{\ensuremath{\omega}} + \DeclareUnicodeCharacter{03A9}{\ensuremath{\Omega}} + \DeclareUnicodeCharacter{03C5}{\ensuremath{\upsilon}} + \DeclareUnicodeCharacter{03A5}{\ensuremath{\Upsilon}} + \DeclareUnicodeCharacter{2113}{\ell} + + \DeclareUnicodeCharacter{2148}{\ensuremath{\id}} + \DeclareUnicodeCharacter{2202}{\ensuremath{\partial}} + \DeclareUnicodeCharacter{2205}{\ensuremath{\emptyset}} + \DeclareUnicodeCharacter{2208}{\ensuremath{\in}} + \DeclareUnicodeCharacter{2209}{\ensuremath{\notin}} + \DeclareUnicodeCharacter{2211}{\ensuremath{\sum}} + \DeclareUnicodeCharacter{221A}{\ensuremath{\sqrt{}}} + \DeclareUnicodeCharacter{221E}{\ensuremath{\infty}} + \DeclareUnicodeCharacter{2227}{\ensuremath{\wedge}} + \DeclareUnicodeCharacter{2228}{\ensuremath{\vee}} + \DeclareUnicodeCharacter{2229}{\ensuremath{\cap}} + \DeclareUnicodeCharacter{222A}{\ensuremath{\cup}} + \DeclareUnicodeCharacter{222B}{\ensuremath{\int}} + \DeclareUnicodeCharacter{2248}{\ensuremath{\approx}} + \DeclareUnicodeCharacter{2260}{\ensuremath{\neq}} + \DeclareUnicodeCharacter{2264}{\ensuremath{\leq}} + \DeclareUnicodeCharacter{2265}{\ensuremath{\geq}} + \DeclareUnicodeCharacter{2293}{\ensuremath{\sqcap}} + \DeclareUnicodeCharacter{2294}{\ensuremath{\sqcup}} + \DeclareUnicodeCharacter{22C0}{\ensuremath{\bigwedge}} + \DeclareUnicodeCharacter{22C1}{\ensuremath{\bigvee}} + \DeclareUnicodeCharacter{22C2}{\ensuremath{\bigcap}} + \DeclareUnicodeCharacter{22C3}{\ensuremath{\bigcup}} + \DeclareUnicodeCharacter{2323}{\ensuremath{\smile}} % cup product + \DeclareUnicodeCharacter{00B1}{\ensuremath{\pm}} + \DeclareUnicodeCharacter{2A02}{\ensuremath{\bigotimes}} + \DeclareUnicodeCharacter{2297}{\ensuremath{\otimes}} + \DeclareUnicodeCharacter{2A01}{\ensuremath{\oplus}} + \DeclareUnicodeCharacter{00BD}{\ensuremath{\nicefrac{1}{2}}} + \DeclareUnicodeCharacter{00D7}{\ensuremath{\times}} + \DeclareUnicodeCharacter{00B7}{\ensuremath{\cdot}} + \DeclareUnicodeCharacter{230A}{\ensuremath{\lfloor}} + \DeclareUnicodeCharacter{230B}{\ensuremath{\rfloor}} + \DeclareUnicodeCharacter{2308}{\ensuremath{\lceil}} + \DeclareUnicodeCharacter{2309}{\ensuremath{\rceil}} + \DeclareUnicodeCharacter{22C5}{\ensuremath{\cdot}} + \DeclareUnicodeCharacter{2227}{\ensuremath{\wedge}} + \DeclareUnicodeCharacter{22C0}{\ensuremath{\bigwedge}} + \DeclareUnicodeCharacter{2192}{\ensuremath{\to}} + \DeclareUnicodeCharacter{21A6}{\ensuremath{\mapsto}} + \DeclareUnicodeCharacter{2102}{\ensuremath{\mathbb{C}}} + \DeclareUnicodeCharacter{211A}{\ensuremath{\mathbb{Q}}} + \DeclareUnicodeCharacter{211D}{\ensuremath{\mathbb{R}}} + \DeclareUnicodeCharacter{2124}{\ensuremath{\mathbb{Z}}} + \DeclareUnicodeCharacter{2202}{\ensuremath{\partial}} + + \DeclareUnicodeCharacter{2070}{\ensuremath{{}^0}} + \DeclareUnicodeCharacter{00B9}{\ensuremath{{}^1}} + \DeclareUnicodeCharacter{00B2}{\ensuremath{{}^2}} + \DeclareUnicodeCharacter{00B3}{\ensuremath{{}^3}} + \DeclareUnicodeCharacter{2074}{\ensuremath{{}^4}} + \DeclareUnicodeCharacter{2075}{\ensuremath{{}^5}} + \DeclareUnicodeCharacter{2076}{\ensuremath{{}^6}} + \DeclareUnicodeCharacter{2077}{\ensuremath{{}^7}} + \DeclareUnicodeCharacter{2078}{\ensuremath{{}^8}} + \DeclareUnicodeCharacter{2079}{\ensuremath{{}^9}} + \DeclareUnicodeCharacter{207A}{\ensuremath{{}^+}} + \DeclareUnicodeCharacter{207B}{\ensuremath{{}^-}} + \DeclareUnicodeCharacter{141F}{\ensuremath{{}^/}} + \DeclareUnicodeCharacter{2080}{\ensuremath{{}_0}} + \DeclareUnicodeCharacter{2081}{\ensuremath{{}_1}} + \DeclareUnicodeCharacter{2082}{\ensuremath{{}_2}} + \DeclareUnicodeCharacter{2083}{\ensuremath{{}_3}} + \DeclareUnicodeCharacter{2084}{\ensuremath{{}_4}} + \DeclareUnicodeCharacter{2085}{\ensuremath{{}_5}} + \DeclareUnicodeCharacter{2086}{\ensuremath{{}_6}} + \DeclareUnicodeCharacter{2087}{\ensuremath{{}_7}} + \DeclareUnicodeCharacter{2088}{\ensuremath{{}_8}} + \DeclareUnicodeCharacter{2089}{\ensuremath{{}_9}} + \DeclareUnicodeCharacter{208A}{\ensuremath{{}_+}} + \DeclareUnicodeCharacter{208B}{\ensuremath{{}_-}} + \DeclareUnicodeCharacter{1D62}{\ensuremath{{}_i}} + \DeclareUnicodeCharacter{2C7C}{\ensuremath{{}_j}} + + \newcommand{\sageMexSymbol}[1] + {{\fontencoding{OMX}\fontfamily{cmex}\selectfont\raisebox{0.75em}{\symbol{#1}}}} + \DeclareUnicodeCharacter{239B}{\sageMexSymbol{"30}} % parenlefttp + \DeclareUnicodeCharacter{239C}{\sageMexSymbol{"42}} % parenleftex + \DeclareUnicodeCharacter{239D}{\sageMexSymbol{"40}} % parenleftbt + \DeclareUnicodeCharacter{239E}{\sageMexSymbol{"31}} % parenrighttp + \DeclareUnicodeCharacter{239F}{\sageMexSymbol{"43}} % parenrightex + \DeclareUnicodeCharacter{23A0}{\sageMexSymbol{"41}} % parenrightbt + \DeclareUnicodeCharacter{23A1}{\sageMexSymbol{"32}} % bracketlefttp + \DeclareUnicodeCharacter{23A2}{\sageMexSymbol{"36}} % bracketleftex + \DeclareUnicodeCharacter{23A3}{\sageMexSymbol{"34}} % bracketleftbt + \DeclareUnicodeCharacter{23A4}{\sageMexSymbol{"33}} % bracketrighttp + \DeclareUnicodeCharacter{23A5}{\sageMexSymbol{"37}} % bracketrightex + \DeclareUnicodeCharacter{23A6}{\sageMexSymbol{"35}} % bracketrightbt + + \DeclareUnicodeCharacter{23A7}{\sageMexSymbol{"38}} % curly brace left top + \DeclareUnicodeCharacter{23A8}{\sageMexSymbol{"3C}} % curly brace left middle + \DeclareUnicodeCharacter{23A9}{\sageMexSymbol{"3A}} % curly brace left bottom + \DeclareUnicodeCharacter{23AA}{\sageMexSymbol{"3E}} % curly brace extension + \DeclareUnicodeCharacter{23AB}{\sageMexSymbol{"39}} % curly brace right top + \DeclareUnicodeCharacter{23AC}{\sageMexSymbol{"3D}} % curly brace right middle + \DeclareUnicodeCharacter{23AD}{\sageMexSymbol{"3B}} % curly brace right bottom + \DeclareUnicodeCharacter{23B0}{\{} % 2-line curly brace left top half (not in cmex) + \DeclareUnicodeCharacter{23B1}{\}} % 2-line curly brace right top half (not in cmex) + + \DeclareUnicodeCharacter{2320}{\ensuremath{\int}} % top half integral + \DeclareUnicodeCharacter{2321}{\ensuremath{\int}} % bottom half integral + \DeclareUnicodeCharacter{23AE}{\ensuremath{\|}} % integral extenison + + % Box drawings light + \DeclareUnicodeCharacter{2500}{-} % h + \DeclareUnicodeCharacter{2502}{|} % v + \DeclareUnicodeCharacter{250C}{+} % dr + \DeclareUnicodeCharacter{2510}{+} % dl + \DeclareUnicodeCharacter{2514}{+} % ur + \DeclareUnicodeCharacter{2518}{+} % ul + \DeclareUnicodeCharacter{251C}{+} % vr + \DeclareUnicodeCharacter{2524}{+} % vl + \DeclareUnicodeCharacter{252C}{+} % dh + \DeclareUnicodeCharacter{2534}{+} % uh + \DeclareUnicodeCharacter{253C}{+} % vh + \DeclareUnicodeCharacter{2571}{/} % upper right to lower left + \DeclareUnicodeCharacter{2571}{\setminus} % upper left to lower right + + \DeclareUnicodeCharacter{25CF}{\ensuremath{\bullet}} % medium black circle + \DeclareUnicodeCharacter{26AC}{\ensuremath{\circ}} % medium small white circle + \DeclareUnicodeCharacter{256D}{+} + \DeclareUnicodeCharacter{256E}{+} + \DeclareUnicodeCharacter{256F}{+} + \DeclareUnicodeCharacter{2570}{+} +\fi + +\let\textLaTeX\LaTeX +\AtBeginDocument{\renewcommand*{\LaTeX}{\hbox{\textLaTeX}}} + +% Workaround for a LaTeX bug -- see trac #31397 and +% https://tex.stackexchange.com/questions/583391/mactex-2020-error-with-report-hyperref-mathbf-in-chapter. +\makeatletter +\pdfstringdefDisableCommands{% + \let\mathbf\@firstofone +} +\makeatother +""" + +# Documents to append as an appendix to all manuals. +#latex_appendices = [] + +# If false, no module index is generated. +#latex_use_modindex = True + +##################################################### +# add LaTeX macros for Sage + +from sage.misc.latex_macros import sage_latex_macros + +try: + pngmath_latex_preamble # check whether this is already defined +except NameError: + pngmath_latex_preamble = "" + +for macro in sage_latex_macros(): + # used when building latex and pdf versions + latex_elements['preamble'] += macro + '\n' + # used when building html version + pngmath_latex_preamble += macro + '\n' + +##################################################### +# add custom context variables for templates + +def add_page_context(app, pagename, templatename, context, doctree): + # # The template function + # def template_function(arg): + # return "Your string is " + arg + # # Add it to the page's context + # context['template_function'] = template_function + path1 = os.path.dirname(app.builder.get_outfilename(pagename)) + path2 = os.path.join(SAGE_DOC, 'html', 'en') + relpath = os.path.relpath(path2, path1) + context['release'] = release + context['documentation_title'] = 'Sage {}'.format(release) + ' Documentation' + context['documentation_root'] = os.path.join(relpath, 'index.html') + if 'website' in path1: + context['title'] = 'Documentation' + context['website'] = True + + if 'reference' in path1 and not path1.endswith('reference'): + path2 = os.path.join(SAGE_DOC, 'html', 'en', 'reference') + relpath = os.path.relpath(path2, path1) + context['reference_title'] = 'Reference Manual' + context['reference_root'] = os.path.join(relpath, 'index.html') + context['refsub'] = True + +##################################################### + +def process_docstring_aliases(app, what, name, obj, options, docstringlines): + """ + Change the docstrings for aliases to point to the original object. + """ + basename = name.rpartition('.')[2] + if hasattr(obj, '__name__') and obj.__name__ != basename: + docstringlines[:] = ['See :obj:`%s`.' % name] + +def process_directives(app, what, name, obj, options, docstringlines): + """ + Remove 'nodetex' and other directives from the first line of any + docstring where they appear. + """ + if len(docstringlines) == 0: + return + first_line = docstringlines[0] + directives = [ d.lower() for d in first_line.split(',') ] + if 'nodetex' in directives: + docstringlines.pop(0) + +def process_docstring_cython(app, what, name, obj, options, docstringlines): + """ + Remove Cython's filename and location embedding. + """ + if len(docstringlines) <= 1: + return + + first_line = docstringlines[0] + if first_line.startswith('File:') and '(starting at' in first_line: + #Remove the first two lines + docstringlines.pop(0) + docstringlines.pop(0) + +def process_docstring_module_title(app, what, name, obj, options, docstringlines): + """ + Removes the first line from the beginning of the module's docstring. This + corresponds to the title of the module's documentation page. + """ + if what != "module": + return + + #Remove any additional blank lines at the beginning + title_removed = False + while len(docstringlines) > 1 and not title_removed: + if docstringlines[0].strip() != "": + title_removed = True + docstringlines.pop(0) + + #Remove any additional blank lines at the beginning + while len(docstringlines) > 1: + if docstringlines[0].strip() == "": + docstringlines.pop(0) + else: + break + +skip_picklability_check_modules = [ + #'sage.misc.test_nested_class', # for test only + 'sage.misc.latex', + 'sage.misc.explain_pickle', + '__builtin__', +] + +def check_nested_class_picklability(app, what, name, obj, skip, options): + """ + Print a warning if pickling is broken for nested classes. + """ + if hasattr(obj, '__dict__') and hasattr(obj, '__module__'): + # Check picklability of nested classes. Adapted from + # sage.misc.nested_class.modify_for_nested_pickle. + module = sys.modules[obj.__module__] + for (nm, v) in obj.__dict__.items(): + if (isinstance(v, type) and + v.__name__ == nm and + v.__module__ == module.__name__ and + getattr(module, nm, None) is not v and + v.__module__ not in skip_picklability_check_modules): + # OK, probably this is an *unpicklable* nested class. + app.warn('Pickling of nested class %r is probably broken. ' + 'Please set the metaclass of the parent class to ' + 'sage.misc.nested_class.NestedClassMetaclass.' % ( + v.__module__ + '.' + name + '.' + nm)) + + +def skip_member(app, what, name, obj, skip, options): + """ + To suppress Sphinx warnings / errors, we + + - Don't include [aliases of] builtins. + + - Don't include the docstring for any nested class which has been + inserted into its module by + :class:`sage.misc.NestedClassMetaclass` only for pickling. The + class will be properly documented inside its surrounding class. + + - Optionally, check whether pickling is broken for nested classes. + + - Optionally, include objects whose name begins with an underscore + ('_'), i.e., "private" or "hidden" attributes, methods, etc. + + Otherwise, we abide by Sphinx's decision. Note: The object + ``obj`` is excluded (included) if this handler returns True + (False). + """ + if 'SAGE_CHECK_NESTED' in os.environ: + check_nested_class_picklability(app, what, name, obj, skip, options) + + if getattr(obj, '__module__', None) == '__builtin__': + return True + + objname = getattr(obj, "__name__", None) + if objname is not None: + # check if name was inserted to the module by NestedClassMetaclass + if name.find('.') != -1 and objname.find('.') != -1: + if objname.split('.')[-1] == name.split('.')[-1]: + return True + + if 'SAGE_DOC_UNDERSCORE' in os.environ: + if name.split('.')[-1].startswith('_'): + return False + + return skip + + +def process_dollars(app, what, name, obj, options, docstringlines): + r""" + Replace dollar signs with backticks. + + See sage.misc.sagedoc.process_dollars for more information. + """ + if len(docstringlines) and name.find("process_dollars") == -1: + from sage.misc.sagedoc import process_dollars as sagedoc_dollars + s = sagedoc_dollars("\n".join(docstringlines)) + lines = s.split("\n") + for i in range(len(lines)): + docstringlines[i] = lines[i] + +def process_inherited(app, what, name, obj, options, docstringlines): + """ + If we're including inherited members, omit their docstrings. + """ + if not options.get('inherited-members'): + return + + if what in ['class', 'data', 'exception', 'function', 'module']: + return + + name = name.split('.')[-1] + + if what == 'method' and hasattr(obj, 'im_class'): + if name in obj.im_class.__dict__.keys(): + return + + if what == 'attribute' and hasattr(obj, '__objclass__'): + if name in obj.__objclass__.__dict__.keys(): + return + + for i in range(len(docstringlines)): + docstringlines.pop() + +dangling_debug = False + +def debug_inf(app, message): + if dangling_debug: + app.info(message) + +def call_intersphinx(app, env, node, contnode): + r""" + Call intersphinx and make links between Sage manuals relative. + + TESTS: + + Check that the link from the thematic tutorials to the reference + manual is relative, see :trac:`20118`:: + + sage: from sage.env import SAGE_DOC + sage: thematic_index = os.path.join(SAGE_DOC, "html", "en", "thematic_tutorials", "index.html") + sage: for line in open(thematic_index).readlines(): # optional - sagemath_doc_html + ....: if "padics" in line: + ....: _ = sys.stdout.write(line) +
    • Introduction to the p-adics

    • + """ + debug_inf(app, "???? Trying intersphinx for %s" % node['reftarget']) + builder = app.builder + res = intersphinx.missing_reference( + app, env, node, contnode) + if res: + # Replace absolute links to $SAGE_DOC by relative links: this + # allows to copy the whole documentation tree somewhere else + # without breaking links, see Trac #20118. + if res['refuri'].startswith(SAGE_DOC): + here = os.path.dirname(os.path.join(builder.outdir, + node['refdoc'])) + res['refuri'] = os.path.relpath(res['refuri'], here) + debug_inf(app, "++++ Found at %s" % res['refuri']) + else: + debug_inf(app, "---- Intersphinx: %s not Found" % node['reftarget']) + return res + +def find_sage_dangling_links(app, env, node, contnode): + r""" + Try to find dangling link in local module imports or all.py. + """ + debug_inf(app, "==================== find_sage_dangling_links ") + + reftype = node['reftype'] + reftarget = node['reftarget'] + try: + doc = node['refdoc'] + except KeyError: + debug_inf(app, "-- no refdoc in node %s" % node) + return None + + debug_inf(app, "Searching %s from %s"%(reftarget, doc)) + + # Workaround: in Python's doc 'object', 'list', ... are documented as a + # function rather than a class + if reftarget in base_class_as_func and reftype == 'class': + node['reftype'] = 'func' + + res = call_intersphinx(app, env, node, contnode) + if res: + debug_inf(app, "++ DONE %s"%(res['refuri'])) + return res + + if node.get('refdomain') != 'py': # not a python file + return None + + try: + module = node['py:module'] + cls = node['py:class'] + except KeyError: + debug_inf(app, "-- no module or class for :%s:%s"%(reftype, reftarget)) + return None + + basename = reftarget.split(".")[0] + try: + target_module = getattr(sys.modules['sage.all'], basename).__module__ + debug_inf(app, "++ found %s using sage.all in %s" % (basename, target_module)) + except AttributeError: + try: + target_module = getattr(sys.modules[node['py:module']], basename).__module__ + debug_inf(app, "++ found %s in this module" % (basename,)) + except AttributeError: + debug_inf(app, "-- %s not found in sage.all or this module" % (basename)) + return None + except KeyError: + target_module = None + if target_module is None: + target_module = "" + debug_inf(app, "?? found in None !!!") + + newtarget = target_module+'.'+reftarget + node['reftarget'] = newtarget + + # adapted from sphinx/domains/python.py + builder = app.builder + searchmode = node.hasattr('refspecific') and 1 or 0 + matches = builder.env.domains['py'].find_obj( + builder.env, module, cls, newtarget, reftype, searchmode) + if not matches: + debug_inf(app, "?? no matching doc for %s"%newtarget) + return call_intersphinx(app, env, node, contnode) + elif len(matches) > 1: + env.warn(target_module, + 'more than one target found for cross-reference ' + '%r: %s' % (newtarget, + ', '.join(match[0] for match in matches)), + node.line) + name, obj = matches[0] + debug_inf(app, "++ match = %s %s"%(name, obj)) + + from docutils import nodes + newnode = nodes.reference('', '', internal=True) + if name == target_module: + newnode['refid'] = name + else: + newnode['refuri'] = builder.get_relative_uri(node['refdoc'], obj[0]) + newnode['refuri'] += '#' + name + debug_inf(app, "++ DONE at URI %s"%(newnode['refuri'])) + newnode['reftitle'] = name + newnode.append(contnode) + return newnode + +# lists of basic Python class which are documented as functions +base_class_as_func = [ + 'bool', 'complex', 'dict', 'file', 'float', + 'frozenset', 'int', 'list', 'long', 'object', + 'set', 'slice', 'str', 'tuple', 'type', 'unicode', 'xrange'] + +# Nit picky option configuration: Put here broken links we want to ignore. For +# link to the Python documentation several links where broken because there +# where class listed as functions. Expand the list 'base_class_as_func' above +# instead of marking the link as broken. +nitpick_ignore = [ + ('py:class', 'twisted.web2.resource.Resource'), + ('py:class', 'twisted.web2.resource.PostableResource')] + +def nitpick_patch_config(app): + """ + Patch the default config for nitpicky + + Calling path_config ensure that nitpicky is not considered as a Sphinx + environment variable but rather as a Sage environment variable. As a + consequence, changing it doesn't force the recompilation of the entire + documentation. + """ + app.config.values['nitpicky'] = (False, 'sage') + app.config.values['nitpick_ignore'] = ([], 'sage') + +def skip_TESTS_block(app, what, name, obj, options, docstringlines): + """ + Skip blocks labeled "TESTS:". + + See sage.misc.sagedoc.skip_TESTS_block for more information. + """ + from sage.misc.sagedoc import skip_TESTS_block as sagedoc_skip_TESTS + if not docstringlines: + # No docstring, so don't do anything. See Trac #19932. + return + s = sagedoc_skip_TESTS("\n".join(docstringlines)) + lines = s.split("\n") + for i in range(len(lines)): + docstringlines[i] = lines[i] + while len(docstringlines) > len(lines): + del docstringlines[len(lines)] + +class SagemathTransform(Transform): + """ + Transform for code-blocks. + + This allows Sphinx to treat code-blocks with prompt "sage:" as + associated with the pycon lexer, and in particular, to change + "" to a blank line. + """ + default_priority = 500 + + def apply(self): + for node in self.document.traverse(nodes.literal_block): + if node.get('language') is None and node.astext().startswith('sage:'): + node['language'] = 'ipycon' + source = node.rawsource + source = blankline_re.sub('', source) + node.rawsource = source + node[:] = [nodes.Text(source)] + +from sage.misc.sageinspect import sage_getargspec +autodoc_builtin_argspec = sage_getargspec + +def setup(app): + app.connect('autodoc-process-docstring', process_docstring_cython) + app.connect('autodoc-process-docstring', process_directives) + app.connect('autodoc-process-docstring', process_docstring_module_title) + app.connect('autodoc-process-docstring', process_dollars) + app.connect('autodoc-process-docstring', process_inherited) + if os.environ.get('SAGE_SKIP_TESTS_BLOCKS', False): + app.connect('autodoc-process-docstring', skip_TESTS_block) + app.connect('autodoc-skip-member', skip_member) + app.add_transform(SagemathTransform) + + # When building the standard docs, app.srcdir is set to SAGE_DOC_SRC + + # 'LANGUAGE/DOCNAME', but when doing introspection, app.srcdir is + # set to a temporary directory. We don't want to use intersphinx, + # etc., when doing introspection. + if app.srcdir.startswith(SAGE_DOC_SRC): + app.add_config_value('intersphinx_mapping', {}, False) + app.add_config_value('intersphinx_cache_limit', 5, False) + app.add_config_value('intersphinx_disabled_reftypes', [], False) + app.connect('config-inited', set_intersphinx_mappings) + app.connect('builder-inited', intersphinx.load_mappings) + # We do *not* fully initialize intersphinx since we call it by hand + # in find_sage_dangling_links. + # app.connect('missing-reference', missing_reference) + app.connect('missing-reference', find_sage_dangling_links) + app.connect('builder-inited', nitpick_patch_config) + app.connect('html-page-context', add_page_context) + diff --git a/src/sage_docbuild/sphinxbuild.py b/src/sage_docbuild/sphinxbuild.py index a39c99ffe9f..5c2b20c102a 100644 --- a/src/sage_docbuild/sphinxbuild.py +++ b/src/sage_docbuild/sphinxbuild.py @@ -1,8 +1,9 @@ # -*- coding: utf-8 -*- r""" -This is Sage's version of the sphinx-build script +Sphinx build script -We redirect stdout and stderr to our own logger, and remove some unwanted chatter. +This is Sage's version of the ``sphinx-build`` script. We redirect ``stdout`` and +``stderr`` to our own logger, and remove some unwanted chatter. """ # **************************************************************************** # Copyright (C) 2013-2014 Volker Braun diff --git a/src/sage_docbuild/utils.py b/src/sage_docbuild/utils.py index 4d815f9dd4e..196f761650e 100644 --- a/src/sage_docbuild/utils.py +++ b/src/sage_docbuild/utils.py @@ -1,4 +1,6 @@ -"""Miscellaneous utilities for running the docbuilder.""" +r""" +Utilities +""" import errno import os @@ -96,7 +98,7 @@ def build_many(target, args, processes=None): This is a simplified version of ``multiprocessing.Pool.map`` from the Python standard library which avoids a couple of its pitfalls. In - particular, it can abort (with a `RuntimeError`) without hanging if one of + particular, it can abort (with a ``RuntimeError``) without hanging if one of the worker processes unexpectedly dies. It also has semantics equivalent to ``maxtasksperchild=1``; that is, one process is started per argument. As such, this is inefficient for processing large numbers of fast tasks, @@ -106,12 +108,12 @@ def build_many(target, args, processes=None): It also avoids starting new processes from a pthread, which results in at least two known issues: - * On versions of Cygwin prior to 3.0.0 there were bugs in mmap handling - on threads (see https://trac.sagemath.org/ticket/27214#comment:25). + * On versions of Cygwin prior to 3.0.0 there were bugs in mmap handling + on threads (see :trac:`27214#comment:25`). - * When PARI is built with multi-threading support, forking a Sage - process from a thread leaves the main Pari interface instance broken - (see https://trac.sagemath.org/ticket/26608#comment:38). + * When PARI is built with multi-threading support, forking a Sage + process from a thread leaves the main Pari interface instance broken + (see :trac:`26608#comment:38`). In the future this may be replaced by a generalized version of the more robust parallel processing implementation from ``sage.doctest.forker``. @@ -133,9 +135,9 @@ def build_many(target, args, processes=None): Processed task ... Processed task ... - Unlike the first version of `build_many` which was only intended to get + Unlike the first version of ``build_many`` which was only intended to get around the Cygwin bug, this version can also return a result, and thus can - be used as a replacement for `multiprocessing.Pool.map` (i.e. it still + be used as a replacement for ``multiprocessing.Pool.map`` (i.e. it still blocks until the result is ready):: sage: def square(N): @@ -144,7 +146,7 @@ def build_many(target, args, processes=None): [0, 1, 4, 9, ..., 9604, 9801] If the target function raises an exception in any of the workers, - `build_many` raises that exception and all other results are discarded. + ``build_many`` raises that exception and all other results are discarded. Any in-progress tasks may still be allowed to complete gracefully before the exception is raised:: @@ -173,7 +175,7 @@ def build_many(target, args, processes=None): Similarly, if one of the worker processes dies unexpectedly otherwise exits non-zero (e.g. killed by a signal) any in-progress tasks will be completed - gracefully, but then a `RuntimeError` is raised and pending tasks are not + gracefully, but then a ``RuntimeError`` is raised and pending tasks are not started:: sage: def target(N):