mirror of
https://github.com/sphinx-doc/sphinx.git
synced 2025-02-25 18:55:22 -06:00
Merge branch 'master' into support_remote_images
This commit is contained in:
commit
241c0db7bc
2
AUTHORS
2
AUTHORS
@ -12,6 +12,7 @@ Other co-maintainers:
|
||||
* Robert Lehmann <@lehmannro>
|
||||
* Roland Meister <@rolmei>
|
||||
* Takeshi Komiya <@tk0miya>
|
||||
* Yoshiki Shibukawa <@shibu_jp>
|
||||
|
||||
Other contributors, listed alphabetically, are:
|
||||
|
||||
@ -55,7 +56,6 @@ Other contributors, listed alphabetically, are:
|
||||
* Rob Ruana -- napoleon extension
|
||||
* Stefan Seefeld -- toctree improvements
|
||||
* Gregory Szorc -- performance improvements
|
||||
* Shibukawa Yoshiki -- pluggable search API and Japanese search, epub3 builder improvements
|
||||
* Taku Shimizu -- epub3 builder
|
||||
* Antonio Valentino -- qthelp builder
|
||||
* Filip Vavera -- napoleon todo directive
|
||||
|
8
CHANGES
8
CHANGES
@ -44,6 +44,7 @@ Incompatible changes
|
||||
(refs #3550)
|
||||
* ``Builder.env`` is not filled at instantiation
|
||||
* #3594: LaTeX: single raw directive has been considered as block level element
|
||||
* #3639: If ``html_experimental_html5_writer`` is available, epub builder use it by default.
|
||||
|
||||
Features removed
|
||||
----------------
|
||||
@ -96,7 +97,7 @@ Features added
|
||||
* #3476: setuptools: Support multiple builders
|
||||
* latex: merged cells in LaTeX tables allow code-blocks, lists, blockquotes...
|
||||
as do normal cells (refs: #3435)
|
||||
* HTML buildre uses experimental HTML5 writer if ``html_experimental_html5_builder`` is True
|
||||
* HTML buildre uses experimental HTML5 writer if ``html_experimental_html5_writer`` is True
|
||||
and docutils 0.13 and newer is installed.
|
||||
* LaTeX macros to customize space before and after tables in PDF output (refs #3504)
|
||||
* #3348: Show decorators in literalinclude and viewcode directives
|
||||
@ -111,6 +112,11 @@ Features added
|
||||
* #2961: improve :confval:`autodoc_mock_imports`. Now the config value only
|
||||
requires to declare the top-level modules that should be mocked.
|
||||
Thanks to Robin Jarry.
|
||||
* #3449: On py3, autodoc use inspect.signature for more accurate signature
|
||||
calculation. Thanks to Nathaniel J. Smith.
|
||||
* #3641: Epub theme supports HTML structures that are generated by HTML5 writer.
|
||||
* #3644 autodoc uses inspect instead of checking types. Thanks to
|
||||
Jeroen Demeyer.
|
||||
|
||||
Bugs fixed
|
||||
----------
|
||||
|
@ -125,8 +125,8 @@ The builder's "name" must be given to the **-b** command-line option of
|
||||
|
||||
.. autoattribute:: supported_image_types
|
||||
|
||||
.. module:: sphinx.builders.epub
|
||||
.. class:: EpubBuilder
|
||||
.. module:: sphinx.builders.epub2
|
||||
.. class:: Epub2Builder
|
||||
|
||||
This builder produces the same output as the standalone HTML builder, but
|
||||
also generates an *epub* file for ebook readers. See :ref:`epub-faq` for
|
||||
|
@ -313,7 +313,7 @@ package.
|
||||
.. versionchanged:: 1.6
|
||||
Optional ``alternate`` and/or ``title`` attributes can be supplied with
|
||||
the *alternate* (of boolean type) and *title* (a string) arguments. The
|
||||
default is no title and *alternate*=``False`` (see `this explanation
|
||||
default is no title and *alternate* = ``False`` (see `this explanation
|
||||
<https://developer.mozilla.org/en-US/docs/Web/CSS/Alternative_style_sheets>`_).
|
||||
|
||||
.. method:: Sphinx.add_latex_package(packagename, options=None)
|
||||
|
@ -61,11 +61,12 @@ if False:
|
||||
from sphinx.domains import Domain, Index # NOQA
|
||||
from sphinx.environment.collectors import EnvironmentCollector # NOQA
|
||||
from sphinx.extension import Extension # NOQA
|
||||
from sphinx.theming import Theme # NOQA
|
||||
|
||||
builtin_extensions = (
|
||||
'sphinx.builders.applehelp',
|
||||
'sphinx.builders.changes',
|
||||
'sphinx.builders.epub',
|
||||
'sphinx.builders.epub2',
|
||||
'sphinx.builders.epub3',
|
||||
'sphinx.builders.devhelp',
|
||||
'sphinx.builders.dummy',
|
||||
@ -128,6 +129,7 @@ class Sphinx(object):
|
||||
self.env = None # type: BuildEnvironment
|
||||
self.enumerable_nodes = {} # type: Dict[nodes.Node, Tuple[unicode, Callable]] # NOQA
|
||||
self.post_transforms = [] # type: List[Transform]
|
||||
self.html_themes = {} # type: Dict[unicode, unicode]
|
||||
|
||||
self.srcdir = srcdir
|
||||
self.confdir = confdir
|
||||
@ -739,15 +741,15 @@ class Sphinx(object):
|
||||
def add_stylesheet(self, filename, alternate=False, title=None):
|
||||
# type: (unicode, bool, unicode) -> None
|
||||
logger.debug('[app] adding stylesheet: %r', filename)
|
||||
from sphinx.builders.html import StandaloneHTMLBuilder
|
||||
props = {'rel': 'stylesheet',
|
||||
'filename': filename,
|
||||
'title': title} # type: Dict[unicode, unicode]
|
||||
from sphinx.builders.html import StandaloneHTMLBuilder, Stylesheet
|
||||
if '://' not in filename:
|
||||
props['filename'] = posixpath.join('_static', filename)
|
||||
filename = posixpath.join('_static', filename)
|
||||
if alternate:
|
||||
props['rel'] = 'alternate stylesheet'
|
||||
StandaloneHTMLBuilder.css_files.append(props)
|
||||
rel = u'alternate stylesheet'
|
||||
else:
|
||||
rel = u'stylesheet'
|
||||
css = Stylesheet(filename, title, rel) # type: ignore
|
||||
StandaloneHTMLBuilder.css_files.append(css)
|
||||
|
||||
def add_latex_package(self, packagename, options=None):
|
||||
# type: (unicode, unicode) -> None
|
||||
@ -806,7 +808,7 @@ class TemplateBridge(object):
|
||||
"""
|
||||
|
||||
def init(self, builder, theme=None, dirs=None):
|
||||
# type: (Builder, unicode, List[unicode]) -> None
|
||||
# type: (Builder, Theme, List[unicode]) -> None
|
||||
"""Called by the builder to initialize the template system.
|
||||
|
||||
*builder* is the builder object; you'll probably want to look at the
|
||||
|
@ -1,10 +1,9 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
"""
|
||||
sphinx.builders.epub
|
||||
~~~~~~~~~~~~~~~~~~~~
|
||||
sphinx.builders._epub_base
|
||||
~~~~~~~~~~~~~~~~~~~~~~~~~~
|
||||
|
||||
Build epub files.
|
||||
Originally derived from qthelp.py.
|
||||
Base class of epub2/epub3 builders.
|
||||
|
||||
:copyright: Copyright 2007-2017 by the Sphinx team, see AUTHORS.
|
||||
:license: BSD, see LICENSE for details.
|
||||
@ -12,7 +11,6 @@
|
||||
|
||||
import os
|
||||
import re
|
||||
import warnings
|
||||
from os import path
|
||||
from zipfile import ZIP_DEFLATED, ZIP_STORED, ZipFile
|
||||
from datetime import datetime
|
||||
@ -29,12 +27,10 @@ except ImportError:
|
||||
from docutils import nodes
|
||||
|
||||
from sphinx import addnodes
|
||||
from sphinx import package_dir
|
||||
from sphinx.builders.html import StandaloneHTMLBuilder
|
||||
from sphinx.deprecation import RemovedInSphinx17Warning
|
||||
from sphinx.util import logging
|
||||
from sphinx.util import status_iterator
|
||||
from sphinx.util.osutil import ensuredir, copyfile, make_filename
|
||||
from sphinx.util.osutil import ensuredir, copyfile
|
||||
from sphinx.util.fileutil import copy_asset_file
|
||||
from sphinx.util.smartypants import sphinx_smarty_pants as ssp
|
||||
|
||||
@ -57,9 +53,6 @@ COVERPAGE_NAME = u'epub-cover.xhtml'
|
||||
|
||||
TOCTREE_TEMPLATE = u'toctree-l%d'
|
||||
|
||||
DOCTYPE = u'''<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.1//EN"
|
||||
"http://www.w3.org/TR/xhtml11/DTD/xhtml11.dtd">'''
|
||||
|
||||
LINK_TARGET_TEMPLATE = u' [%(uri)s]'
|
||||
|
||||
FOOTNOTE_LABEL_TEMPLATE = u'#%d'
|
||||
@ -111,9 +104,6 @@ class EpubBuilder(StandaloneHTMLBuilder):
|
||||
META-INF/container.xml. Afterwards, all necessary files are zipped to an
|
||||
epub file.
|
||||
"""
|
||||
name = 'epub2'
|
||||
|
||||
template_dir = path.join(package_dir, 'templates', 'epub2')
|
||||
|
||||
# don't copy the reST source
|
||||
copysource = False
|
||||
@ -133,15 +123,18 @@ class EpubBuilder(StandaloneHTMLBuilder):
|
||||
html_scaled_image_link = False
|
||||
# don't generate search index or include search page
|
||||
search = False
|
||||
# use html5 translator by default
|
||||
default_html5_translator = True
|
||||
|
||||
coverpage_name = COVERPAGE_NAME
|
||||
toctree_template = TOCTREE_TEMPLATE
|
||||
doctype = DOCTYPE
|
||||
link_target_template = LINK_TARGET_TEMPLATE
|
||||
css_link_target_class = CSS_LINK_TARGET_CLASS
|
||||
guide_titles = GUIDE_TITLES
|
||||
media_types = MEDIA_TYPES
|
||||
refuri_re = REFURI_RE
|
||||
template_dir = ""
|
||||
doctype = ""
|
||||
|
||||
def init(self):
|
||||
# type: () -> None
|
||||
@ -452,17 +445,6 @@ class EpubBuilder(StandaloneHTMLBuilder):
|
||||
StandaloneHTMLBuilder.handle_page(self, pagename, addctx, templatename,
|
||||
outfilename, event_arg)
|
||||
|
||||
# Finish by building the epub file
|
||||
def handle_finish(self):
|
||||
# type: () -> None
|
||||
"""Create the metainfo files and finally the epub."""
|
||||
self.get_toc()
|
||||
self.build_mimetype(self.outdir, 'mimetype')
|
||||
self.build_container(self.outdir, 'META-INF/container.xml')
|
||||
self.build_content(self.outdir, 'content.opf')
|
||||
self.build_toc(self.outdir, 'toc.ncx')
|
||||
self.build_epub(self.outdir, self.config.epub_basename + '.epub')
|
||||
|
||||
def build_mimetype(self, outdir, outname):
|
||||
# type: (unicode, unicode) -> None
|
||||
"""Write the metainfo file mimetype."""
|
||||
@ -714,48 +696,3 @@ class EpubBuilder(StandaloneHTMLBuilder):
|
||||
epub.write(path.join(outdir, filename), filename, ZIP_DEFLATED) # type: ignore
|
||||
for filename in self.files:
|
||||
epub.write(path.join(outdir, filename), filename, ZIP_DEFLATED) # type: ignore
|
||||
|
||||
|
||||
def emit_deprecation_warning(app):
|
||||
# type: (Sphinx) -> None
|
||||
if app.builder.__class__ is EpubBuilder:
|
||||
warnings.warn('epub2 builder is deprecated. Please use epub3 builder instead.',
|
||||
RemovedInSphinx17Warning)
|
||||
|
||||
|
||||
def setup(app):
|
||||
# type: (Sphinx) -> Dict[unicode, Any]
|
||||
app.setup_extension('sphinx.builders.html')
|
||||
app.add_builder(EpubBuilder)
|
||||
app.connect('builder-inited', emit_deprecation_warning)
|
||||
|
||||
# config values
|
||||
app.add_config_value('epub_basename', lambda self: make_filename(self.project), None)
|
||||
app.add_config_value('epub_theme', 'epub', 'html')
|
||||
app.add_config_value('epub_theme_options', {}, 'html')
|
||||
app.add_config_value('epub_title', lambda self: self.html_title, 'html')
|
||||
app.add_config_value('epub_author', 'unknown', 'html')
|
||||
app.add_config_value('epub_language', lambda self: self.language or 'en', 'html')
|
||||
app.add_config_value('epub_publisher', 'unknown', 'html')
|
||||
app.add_config_value('epub_copyright', lambda self: self.copyright, 'html')
|
||||
app.add_config_value('epub_identifier', 'unknown', 'html')
|
||||
app.add_config_value('epub_scheme', 'unknown', 'html')
|
||||
app.add_config_value('epub_uid', 'unknown', 'env')
|
||||
app.add_config_value('epub_cover', (), 'env')
|
||||
app.add_config_value('epub_guide', (), 'env')
|
||||
app.add_config_value('epub_pre_files', [], 'env')
|
||||
app.add_config_value('epub_post_files', [], 'env')
|
||||
app.add_config_value('epub_exclude_files', [], 'env')
|
||||
app.add_config_value('epub_tocdepth', 3, 'env')
|
||||
app.add_config_value('epub_tocdup', True, 'env')
|
||||
app.add_config_value('epub_tocscope', 'default', 'env')
|
||||
app.add_config_value('epub_fix_images', False, 'env')
|
||||
app.add_config_value('epub_max_image_width', 0, 'env')
|
||||
app.add_config_value('epub_show_urls', 'inline', 'html')
|
||||
app.add_config_value('epub_use_index', lambda self: self.html_use_index, 'html')
|
||||
|
||||
return {
|
||||
'version': 'builtin',
|
||||
'parallel_read_safe': True,
|
||||
'parallel_write_safe': True,
|
||||
}
|
@ -16,7 +16,7 @@ from six import iteritems
|
||||
|
||||
from sphinx import package_dir
|
||||
from sphinx.locale import _
|
||||
from sphinx.theming import Theme
|
||||
from sphinx.theming import HTMLThemeFactory
|
||||
from sphinx.builders import Builder
|
||||
from sphinx.util import logging
|
||||
from sphinx.util.osutil import ensuredir, os_path
|
||||
@ -42,8 +42,8 @@ class ChangesBuilder(Builder):
|
||||
def init(self):
|
||||
# type: () -> None
|
||||
self.create_template_bridge()
|
||||
Theme.init_themes(self.confdir, self.config.html_theme_path)
|
||||
self.theme = Theme('default')
|
||||
theme_factory = HTMLThemeFactory(self.app)
|
||||
self.theme = theme_factory.create('default')
|
||||
self.templates.init(self, self.theme)
|
||||
|
||||
def get_outdated_docs(self):
|
||||
|
100
sphinx/builders/epub2.py
Normal file
100
sphinx/builders/epub2.py
Normal file
@ -0,0 +1,100 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
"""
|
||||
sphinx.builders.epub2
|
||||
~~~~~~~~~~~~~~~~~~~~~
|
||||
|
||||
Build epub2 files.
|
||||
Originally derived from qthelp.py.
|
||||
|
||||
:copyright: Copyright 2007-2016 by the Sphinx team, see AUTHORS.
|
||||
:license: BSD, see LICENSE for details.
|
||||
"""
|
||||
|
||||
import warnings
|
||||
from os import path
|
||||
|
||||
from sphinx import package_dir
|
||||
from sphinx.builders import _epub_base
|
||||
from sphinx.util.osutil import make_filename
|
||||
from sphinx.deprecation import RemovedInSphinx17Warning
|
||||
|
||||
if False:
|
||||
# For type annotation
|
||||
from typing import Any, Dict # NOQA
|
||||
from sphinx.application import Sphinx # NOQA
|
||||
|
||||
|
||||
DOCTYPE = '''<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.1//EN"
|
||||
"http://www.w3.org/TR/xhtml11/DTD/xhtml11.dtd">'''
|
||||
|
||||
|
||||
# The epub publisher
|
||||
|
||||
class Epub2Builder(_epub_base.EpubBuilder):
|
||||
"""
|
||||
Builder that outputs epub files.
|
||||
|
||||
It creates the metainfo files container.opf, toc.ncx, mimetype, and
|
||||
META-INF/container.xml. Afterwards, all necessary files are zipped to an
|
||||
epub file.
|
||||
"""
|
||||
name = 'epub2'
|
||||
|
||||
template_dir = path.join(package_dir, 'templates', 'epub2')
|
||||
doctype = DOCTYPE
|
||||
|
||||
# Finish by building the epub file
|
||||
def handle_finish(self):
|
||||
# type: () -> None
|
||||
"""Create the metainfo files and finally the epub."""
|
||||
self.get_toc()
|
||||
self.build_mimetype(self.outdir, 'mimetype')
|
||||
self.build_container(self.outdir, 'META-INF/container.xml')
|
||||
self.build_content(self.outdir, 'content.opf')
|
||||
self.build_toc(self.outdir, 'toc.ncx')
|
||||
self.build_epub(self.outdir, self.config.epub_basename + '.epub')
|
||||
|
||||
|
||||
def emit_deprecation_warning(app):
|
||||
# type: (Sphinx) -> None
|
||||
if app.builder.__class__ is Epub2Builder:
|
||||
warnings.warn('epub2 builder is deprecated. Please use epub3 builder instead.',
|
||||
RemovedInSphinx17Warning)
|
||||
|
||||
|
||||
def setup(app):
|
||||
# type: (Sphinx) -> Dict[unicode, Any]
|
||||
app.setup_extension('sphinx.builders.html')
|
||||
app.add_builder(Epub2Builder)
|
||||
app.connect('builder-inited', emit_deprecation_warning)
|
||||
|
||||
# config values
|
||||
app.add_config_value('epub_basename', lambda self: make_filename(self.project), None)
|
||||
app.add_config_value('epub_theme', 'epub', 'html')
|
||||
app.add_config_value('epub_theme_options', {}, 'html')
|
||||
app.add_config_value('epub_title', lambda self: self.html_title, 'html')
|
||||
app.add_config_value('epub_author', 'unknown', 'html')
|
||||
app.add_config_value('epub_language', lambda self: self.language or 'en', 'html')
|
||||
app.add_config_value('epub_publisher', 'unknown', 'html')
|
||||
app.add_config_value('epub_copyright', lambda self: self.copyright, 'html')
|
||||
app.add_config_value('epub_identifier', 'unknown', 'html')
|
||||
app.add_config_value('epub_scheme', 'unknown', 'html')
|
||||
app.add_config_value('epub_uid', 'unknown', 'env')
|
||||
app.add_config_value('epub_cover', (), 'env')
|
||||
app.add_config_value('epub_guide', (), 'env')
|
||||
app.add_config_value('epub_pre_files', [], 'env')
|
||||
app.add_config_value('epub_post_files', [], 'env')
|
||||
app.add_config_value('epub_exclude_files', [], 'env')
|
||||
app.add_config_value('epub_tocdepth', 3, 'env')
|
||||
app.add_config_value('epub_tocdup', True, 'env')
|
||||
app.add_config_value('epub_tocscope', 'default', 'env')
|
||||
app.add_config_value('epub_fix_images', False, 'env')
|
||||
app.add_config_value('epub_max_image_width', 0, 'env')
|
||||
app.add_config_value('epub_show_urls', 'inline', 'html')
|
||||
app.add_config_value('epub_use_index', lambda self: self.html_use_index, 'html')
|
||||
|
||||
return {
|
||||
'version': 'builtin',
|
||||
'parallel_read_safe': True,
|
||||
'parallel_write_safe': True,
|
||||
}
|
@ -16,7 +16,7 @@ from collections import namedtuple
|
||||
|
||||
from sphinx import package_dir
|
||||
from sphinx.config import string_classes, ENUM
|
||||
from sphinx.builders.epub import EpubBuilder
|
||||
from sphinx.builders import _epub_base
|
||||
from sphinx.util import logging
|
||||
from sphinx.util.fileutil import copy_asset_file
|
||||
|
||||
@ -45,10 +45,15 @@ THEME_WRITING_MODES = {
|
||||
'horizontal': 'horizontal-tb',
|
||||
}
|
||||
|
||||
DOCTYPE = u'''<!DOCTYPE html>'''
|
||||
DOCTYPE = '''<!DOCTYPE html>'''
|
||||
|
||||
HTML_TAG = (
|
||||
u'<html xmlns="http://www.w3.org/1999/xhtml" '
|
||||
u'xmlns:epub="http://www.idpf.org/2007/ops">'
|
||||
)
|
||||
|
||||
|
||||
class Epub3Builder(EpubBuilder):
|
||||
class Epub3Builder(_epub_base.EpubBuilder):
|
||||
"""
|
||||
Builder that outputs epub3 files.
|
||||
|
||||
@ -61,6 +66,8 @@ class Epub3Builder(EpubBuilder):
|
||||
supported_remote_images = False
|
||||
template_dir = path.join(package_dir, 'templates', 'epub3')
|
||||
doctype = DOCTYPE
|
||||
html_tag = HTML_TAG
|
||||
use_meta_charset = True
|
||||
|
||||
# Finish by building the epub file
|
||||
def handle_finish(self):
|
||||
@ -135,6 +142,8 @@ class Epub3Builder(EpubBuilder):
|
||||
|
||||
writing_mode = self.config.epub_writing_mode
|
||||
self.globalcontext['theme_writing_mode'] = THEME_WRITING_MODES.get(writing_mode)
|
||||
self.globalcontext['html_tag'] = self.html_tag
|
||||
self.globalcontext['use_meta_charset'] = self.use_meta_charset
|
||||
|
||||
def build_navlist(self, navnodes):
|
||||
# type: (List[nodes.Node]) -> List[NavPoint]
|
||||
@ -216,9 +225,12 @@ class Epub3Builder(EpubBuilder):
|
||||
|
||||
def setup(app):
|
||||
# type: (Sphinx) -> Dict[unicode, Any]
|
||||
app.setup_extension('sphinx.builders.epub')
|
||||
|
||||
app.setup_extension('sphinx.builders.epub2')
|
||||
|
||||
app.add_builder(Epub3Builder)
|
||||
|
||||
# config values
|
||||
app.add_config_value('epub_description', 'unknown', 'epub3', string_classes)
|
||||
app.add_config_value('epub_contributor', 'unknown', 'epub3', string_classes)
|
||||
app.add_config_value('epub_writing_mode', 'horizontal', 'epub3',
|
||||
|
@ -197,7 +197,7 @@ def should_write(filepath, new_content):
|
||||
with open(filepath, 'r', encoding='utf-8') as oldpot: # type: ignore
|
||||
old_content = oldpot.read()
|
||||
old_header_index = old_content.index('"POT-Creation-Date:')
|
||||
new_header_index = old_content.index('"POT-Creation-Date:')
|
||||
new_header_index = new_content.index('"POT-Creation-Date:')
|
||||
old_body_index = old_content.index('"PO-Revision-Date:')
|
||||
new_body_index = new_content.index('"PO-Revision-Date:')
|
||||
return ((old_content[:old_header_index] != new_content[:new_header_index]) or
|
||||
|
@ -40,7 +40,7 @@ from sphinx.util.matching import patmatch, Matcher, DOTFILES
|
||||
from sphinx.config import string_classes
|
||||
from sphinx.locale import _, l_
|
||||
from sphinx.search import js_index
|
||||
from sphinx.theming import Theme
|
||||
from sphinx.theming import HTMLThemeFactory
|
||||
from sphinx.builders import Builder
|
||||
from sphinx.application import ENV_PICKLE_FILENAME
|
||||
from sphinx.highlighting import PygmentsBridge
|
||||
@ -87,6 +87,23 @@ def get_stable_hash(obj):
|
||||
return md5(text_type(obj).encode('utf8')).hexdigest()
|
||||
|
||||
|
||||
class Stylesheet(text_type):
|
||||
"""The metadata of stylesheet.
|
||||
|
||||
To keep compatibility with old themes, an instance of stylesheet behaves as
|
||||
its filename (str).
|
||||
"""
|
||||
|
||||
def __new__(cls, filename, title, rel):
|
||||
# type: (unicode, unicode, unicode) -> None
|
||||
self = text_type.__new__(cls, filename) # type: ignore
|
||||
self.filename = filename
|
||||
self.title = title
|
||||
self.rel = rel
|
||||
|
||||
return self
|
||||
|
||||
|
||||
class StandaloneHTMLBuilder(Builder):
|
||||
"""
|
||||
Builds standalone HTML docs.
|
||||
@ -112,6 +129,8 @@ class StandaloneHTMLBuilder(Builder):
|
||||
search = True # for things like HTML help and Apple help: suppress search
|
||||
use_index = False
|
||||
download_support = True # enable download role
|
||||
# use html5 translator by default
|
||||
default_html5_translator = False
|
||||
|
||||
# This is a class attribute because it is mutated by Sphinx.add_javascript.
|
||||
script_files = ['_static/jquery.js', '_static/underscore.js',
|
||||
@ -180,9 +199,9 @@ class StandaloneHTMLBuilder(Builder):
|
||||
|
||||
def init_templates(self):
|
||||
# type: () -> None
|
||||
Theme.init_themes(self.confdir, self.config.html_theme_path)
|
||||
theme_factory = HTMLThemeFactory(self.app)
|
||||
themename, themeoptions = self.get_theme_config()
|
||||
self.theme = Theme(themename)
|
||||
self.theme = theme_factory.create(themename)
|
||||
self.theme_options = themeoptions.copy()
|
||||
self.create_template_bridge()
|
||||
self.templates.init(self, self.theme)
|
||||
@ -193,7 +212,7 @@ class StandaloneHTMLBuilder(Builder):
|
||||
if self.config.pygments_style is not None:
|
||||
style = self.config.pygments_style
|
||||
elif self.theme:
|
||||
style = self.theme.get_confstr('theme', 'pygments_style', 'none')
|
||||
style = self.theme.get_config('theme', 'pygments_style', 'none')
|
||||
else:
|
||||
style = 'sphinx'
|
||||
self.highlighter = PygmentsBridge('html', style,
|
||||
@ -202,7 +221,11 @@ class StandaloneHTMLBuilder(Builder):
|
||||
def init_translator_class(self):
|
||||
# type: () -> None
|
||||
if self.translator_class is None:
|
||||
if self.config.html_experimental_html5_writer and html5_ready:
|
||||
use_html5_writer = self.config.html_experimental_html5_writer
|
||||
if use_html5_writer is None:
|
||||
use_html5_writer = self.default_html5_translator and html5_ready
|
||||
|
||||
if use_html5_writer and html5_ready:
|
||||
if self.config.html_use_smartypants:
|
||||
self.translator_class = SmartyPantsHTML5Translator
|
||||
else:
|
||||
@ -367,7 +390,7 @@ class StandaloneHTMLBuilder(Builder):
|
||||
if self.config.html_style is not None:
|
||||
stylename = self.config.html_style
|
||||
elif self.theme:
|
||||
stylename = self.theme.get_confstr('theme', 'stylesheet')
|
||||
stylename = self.theme.get_config('theme', 'stylesheet')
|
||||
else:
|
||||
stylename = 'default.css'
|
||||
|
||||
@ -671,7 +694,7 @@ class StandaloneHTMLBuilder(Builder):
|
||||
|
||||
# then, copy over theme-supplied static files
|
||||
if self.theme:
|
||||
for theme_path in self.theme.get_dirchain()[::-1]:
|
||||
for theme_path in self.theme.get_theme_dirs()[::-1]:
|
||||
entry = path.join(theme_path, 'static')
|
||||
copy_asset(entry, path.join(self.outdir, '_static'), excluded=DOTFILES,
|
||||
context=ctx, renderer=self.templates)
|
||||
@ -1326,7 +1349,7 @@ def setup(app):
|
||||
app.add_config_value('html_search_options', {}, 'html')
|
||||
app.add_config_value('html_search_scorer', '', None)
|
||||
app.add_config_value('html_scaled_image_link', True, 'html')
|
||||
app.add_config_value('html_experimental_html5_writer', False, 'html')
|
||||
app.add_config_value('html_experimental_html5_writer', None, 'html')
|
||||
|
||||
return {
|
||||
'version': 'builtin',
|
||||
|
@ -16,7 +16,7 @@ import sys
|
||||
import inspect
|
||||
import traceback
|
||||
import warnings
|
||||
from types import FunctionType, BuiltinFunctionType, MethodType, ModuleType
|
||||
from types import FunctionType, MethodType, ModuleType
|
||||
|
||||
from six import PY2, iterkeys, iteritems, itervalues, text_type, class_types, \
|
||||
string_types, StringIO
|
||||
@ -1341,7 +1341,7 @@ class FunctionDocumenter(DocstringSignatureMixin, ModuleLevelDocumenter): # typ
|
||||
@classmethod
|
||||
def can_document_member(cls, member, membername, isattr, parent):
|
||||
# type: (Any, unicode, bool, Any) -> bool
|
||||
return isinstance(member, (FunctionType, BuiltinFunctionType))
|
||||
return inspect.isfunction(member) or inspect.isbuiltin(member)
|
||||
|
||||
def format_args(self):
|
||||
# type: () -> unicode
|
||||
@ -1637,13 +1637,16 @@ class AttributeDocumenter(DocstringStripSignatureMixin, ClassLevelDocumenter):
|
||||
# some non-data descriptors as methods
|
||||
priority = 10
|
||||
|
||||
method_types = (FunctionType, BuiltinFunctionType, MethodType)
|
||||
@staticmethod
|
||||
def is_function_or_method(obj):
|
||||
return inspect.isfunction(obj) or inspect.isbuiltin(obj) or inspect.ismethod(obj)
|
||||
|
||||
@classmethod
|
||||
def can_document_member(cls, member, membername, isattr, parent):
|
||||
# type: (Any, unicode, bool, Any) -> bool
|
||||
non_attr_types = cls.method_types + (type, MethodDescriptorType)
|
||||
non_attr_types = (type, MethodDescriptorType)
|
||||
isdatadesc = isdescriptor(member) and not \
|
||||
cls.is_function_or_method(member) and not \
|
||||
isinstance(member, non_attr_types) and not \
|
||||
type(member).__name__ == "instancemethod"
|
||||
# That last condition addresses an obscure case of C-defined
|
||||
@ -1663,7 +1666,7 @@ class AttributeDocumenter(DocstringStripSignatureMixin, ClassLevelDocumenter):
|
||||
if isenumattribute(self.object):
|
||||
self.object = self.object.value
|
||||
if isdescriptor(self.object) and \
|
||||
not isinstance(self.object, self.method_types):
|
||||
not self.is_function_or_method(self.object):
|
||||
self._datadescriptor = True
|
||||
else:
|
||||
# if it's not a data descriptor
|
||||
|
@ -93,8 +93,8 @@ class Graphviz(Directive):
|
||||
document = self.state.document
|
||||
if self.content:
|
||||
return [document.reporter.warning(
|
||||
'Graphviz directive cannot have both content and '
|
||||
'a filename argument', line=self.lineno)]
|
||||
_('Graphviz directive cannot have both content and '
|
||||
'a filename argument'), line=self.lineno)]
|
||||
env = self.state.document.settings.env
|
||||
argument = search_image_for_language(self.arguments[0], env)
|
||||
rel_filename, filename = env.relfn2path(argument)
|
||||
@ -104,13 +104,13 @@ class Graphviz(Directive):
|
||||
dotcode = fp.read()
|
||||
except (IOError, OSError):
|
||||
return [document.reporter.warning(
|
||||
'External Graphviz file %r not found or reading '
|
||||
'it failed' % filename, line=self.lineno)]
|
||||
_('External Graphviz file %r not found or reading '
|
||||
'it failed') % filename, line=self.lineno)]
|
||||
else:
|
||||
dotcode = '\n'.join(self.content)
|
||||
if not dotcode.strip():
|
||||
return [self.state_machine.reporter.warning(
|
||||
'Ignoring "graphviz" directive without content.',
|
||||
_('Ignoring "graphviz" directive without content.'),
|
||||
line=self.lineno)]
|
||||
node = graphviz()
|
||||
node['code'] = dotcode
|
||||
@ -201,8 +201,8 @@ def render_dot(self, code, options, format, prefix='graphviz'):
|
||||
except OSError as err:
|
||||
if err.errno != ENOENT: # No such file or directory
|
||||
raise
|
||||
logger.warning('dot command %r cannot be run (needed for graphviz '
|
||||
'output), check the graphviz_dot setting', graphviz_dot)
|
||||
logger.warning(_('dot command %r cannot be run (needed for graphviz '
|
||||
'output), check the graphviz_dot setting'), graphviz_dot)
|
||||
if not hasattr(self.builder, '_graphviz_warned_dot'):
|
||||
self.builder._graphviz_warned_dot = {}
|
||||
self.builder._graphviz_warned_dot[graphviz_dot] = True
|
||||
@ -219,11 +219,11 @@ def render_dot(self, code, options, format, prefix='graphviz'):
|
||||
stdout, stderr = p.stdout.read(), p.stderr.read()
|
||||
p.wait()
|
||||
if p.returncode != 0:
|
||||
raise GraphvizError('dot exited with error:\n[stderr]\n%s\n'
|
||||
'[stdout]\n%s' % (stderr, stdout))
|
||||
raise GraphvizError(_('dot exited with error:\n[stderr]\n%s\n'
|
||||
'[stdout]\n%s') % (stderr, stdout))
|
||||
if not path.isfile(outfn):
|
||||
raise GraphvizError('dot did not produce an output file:\n[stderr]\n%s\n'
|
||||
'[stdout]\n%s' % (stderr, stdout))
|
||||
raise GraphvizError(_('dot did not produce an output file:\n[stderr]\n%s\n'
|
||||
'[stdout]\n%s') % (stderr, stdout))
|
||||
return relfn, outfn
|
||||
|
||||
|
||||
@ -233,8 +233,8 @@ def render_dot_html(self, node, code, options, prefix='graphviz',
|
||||
format = self.builder.config.graphviz_output_format
|
||||
try:
|
||||
if format not in ('png', 'svg'):
|
||||
raise GraphvizError("graphviz_output_format must be one of 'png', "
|
||||
"'svg', but is %r" % format)
|
||||
raise GraphvizError(_("graphviz_output_format must be one of 'png', "
|
||||
"'svg', but is %r") % format)
|
||||
fname, outfn = render_dot(self, code, options, format, prefix)
|
||||
except GraphvizError as exc:
|
||||
logger.warning('dot code %r: ' % code + str(exc))
|
||||
|
@ -27,7 +27,7 @@ if False:
|
||||
from typing import Any, Callable, Dict, List, Iterator, Tuple # NOQA
|
||||
from jinja2.environment import Environment # NOQA
|
||||
from sphinx.builders import Builder # NOQA
|
||||
from sphinx.themes import Theme # NOQA
|
||||
from sphinx.theming import Theme # NOQA
|
||||
|
||||
|
||||
def _tobool(val):
|
||||
@ -134,7 +134,7 @@ class BuiltinTemplateLoader(TemplateBridge, BaseLoader):
|
||||
# create a chain of paths to search
|
||||
if theme:
|
||||
# the theme's own dir and its bases' dirs
|
||||
pathchain = theme.get_dirchain()
|
||||
pathchain = theme.get_theme_dirs()
|
||||
# the loader dirs: pathchain + the parent directories for all themes
|
||||
loaderchain = pathchain + [path.join(p, '..') for p in pathchain]
|
||||
elif dirs:
|
||||
|
@ -110,9 +110,17 @@
|
||||
{%- endfor %}
|
||||
{%- endmacro %}
|
||||
|
||||
{%- if html_tag %}
|
||||
{{ html_tag }}
|
||||
{%- else %}
|
||||
<html xmlns="http://www.w3.org/1999/xhtml"{% if language is not none %} lang="{{ language }}"{% endif %}>
|
||||
{%- endif %}
|
||||
<head>
|
||||
{%- if use_meta_charset %}
|
||||
<meta charset="{{ encoding }}" />
|
||||
{%- else %}
|
||||
<meta http-equiv="Content-Type" content="text/html; charset={{ encoding }}" />
|
||||
{%- endif %}
|
||||
{{ metatags }}
|
||||
{%- block htmltitle %}
|
||||
<title>{{ title|striptags|e }}{{ titlesuffix }}</title>
|
||||
|
@ -334,6 +334,8 @@ div.figure p.caption span.caption-text {
|
||||
|
||||
/* -- field list styles ----------------------------------------------------- */
|
||||
|
||||
/* -- for html4 -- */
|
||||
|
||||
table.field-list td, table.field-list th {
|
||||
border: 0 !important;
|
||||
}
|
||||
@ -347,7 +349,74 @@ table.field-list td, table.field-list th {
|
||||
margin: 0;
|
||||
}
|
||||
|
||||
/* -- other body styles ----------------------------------------------------- */
|
||||
/* -- for html5 -- */
|
||||
|
||||
/* bold field name, content starts on the same line */
|
||||
|
||||
dl.field-list > dt,
|
||||
dl.option-list > dt,
|
||||
dl.docinfo > dt,
|
||||
dl.footnote > dt,
|
||||
dl.citation > dt {
|
||||
font-weight: bold;
|
||||
clear: left;
|
||||
float: left;
|
||||
margin: 0;
|
||||
padding: 0;
|
||||
padding-right: 0.5em;
|
||||
}
|
||||
|
||||
/* Offset for field content (corresponds to the --field-name-limit option) */
|
||||
|
||||
dl.field-list > dd,
|
||||
dl.option-list > dd,
|
||||
dl.docinfo > dd {
|
||||
margin-left: 9em; /* ca. 14 chars in the test examples */
|
||||
}
|
||||
|
||||
/* start field-body on a new line after long field names */
|
||||
|
||||
dl.field-list > dd > *:first-child,
|
||||
dl.option-list > dd > *:first-child
|
||||
{
|
||||
display: inline-block;
|
||||
width: 100%;
|
||||
margin: 0;
|
||||
}
|
||||
|
||||
dl.field-list > dt:after,
|
||||
dl.docinfo > dt:after {
|
||||
content: ":";
|
||||
}
|
||||
|
||||
/* -- option lists ---------------------------------------------------------- */
|
||||
|
||||
dl.option-list {
|
||||
margin-left: 40px;
|
||||
}
|
||||
|
||||
dl.option-list > dt {
|
||||
font-weight: normal;
|
||||
}
|
||||
|
||||
span.option {
|
||||
white-space: nowrap;
|
||||
}
|
||||
|
||||
/* -- lists ----------------------------------------------------------------- */
|
||||
|
||||
/* -- compact and simple lists: no margin between items -- */
|
||||
|
||||
.simple li, .compact li,
|
||||
.simple ul, .compact ul,
|
||||
.simple ol, .compact ol,
|
||||
.simple > li p, .compact > li p,
|
||||
dl.simple > dd, dl.compact > dd {
|
||||
margin-top: 0;
|
||||
margin-bottom: 0;
|
||||
}
|
||||
|
||||
/* -- enumerated lists ------------------------------------------------------ */
|
||||
|
||||
ol.arabic {
|
||||
list-style: decimal;
|
||||
@ -369,6 +438,18 @@ ol.upperroman {
|
||||
list-style: upper-roman;
|
||||
}
|
||||
|
||||
dt span.classifier {
|
||||
font-style: italic;
|
||||
}
|
||||
|
||||
dt span.classifier:before {
|
||||
font-style: normal;
|
||||
margin: 0.5em;
|
||||
content: ":";
|
||||
}
|
||||
|
||||
/* -- other body styles ----------------------------------------------------- */
|
||||
|
||||
dl {
|
||||
margin-bottom: 15px;
|
||||
}
|
||||
@ -414,10 +495,53 @@ dl.glossary dt {
|
||||
border: 3px solid red;
|
||||
}
|
||||
|
||||
/* -- footnotes and citations ----------------------------------------------- */
|
||||
|
||||
/* -- for html4 -- */
|
||||
.footnote:target {
|
||||
background-color: #dddddd;
|
||||
}
|
||||
|
||||
/* -- for html5 -- */
|
||||
|
||||
dl.footnote.superscript > dd {
|
||||
margin-left: 1em;
|
||||
}
|
||||
|
||||
dl.footnote.brackets > dd {
|
||||
margin-left: 2em;
|
||||
}
|
||||
|
||||
dl > dt.label {
|
||||
font-weight: normal;
|
||||
}
|
||||
|
||||
a.footnote-reference.brackets:before,
|
||||
dt.label > span.brackets:before {
|
||||
content: "[";
|
||||
}
|
||||
|
||||
a.footnote-reference.brackets:after,
|
||||
dt.label > span.brackets:after {
|
||||
content: "]";
|
||||
}
|
||||
|
||||
a.footnote-reference.superscript,
|
||||
dl.footnote.superscript > dt.label {
|
||||
vertical-align: super;
|
||||
font-size: smaller;
|
||||
}
|
||||
|
||||
dt.label > span.fn-backref {
|
||||
margin-left: 0.2em;
|
||||
}
|
||||
|
||||
dt.label > span.fn-backref > a {
|
||||
font-style: italic;
|
||||
}
|
||||
|
||||
/* -- line blocks ----------------------------------------------------------- */
|
||||
|
||||
.line-block {
|
||||
display: block;
|
||||
margin-top: 1em;
|
||||
|
@ -11,9 +11,9 @@
|
||||
|
||||
import os
|
||||
import shutil
|
||||
import zipfile
|
||||
import tempfile
|
||||
from os import path
|
||||
from zipfile import ZipFile
|
||||
|
||||
import pkg_resources
|
||||
from six import string_types, iteritems
|
||||
@ -21,6 +21,7 @@ from six.moves import configparser
|
||||
|
||||
from sphinx import package_dir
|
||||
from sphinx.errors import ThemeError
|
||||
from sphinx.locale import _
|
||||
from sphinx.util import logging
|
||||
from sphinx.util.osutil import ensuredir
|
||||
|
||||
@ -28,195 +29,115 @@ logger = logging.getLogger(__name__)
|
||||
|
||||
if False:
|
||||
# For type annotation
|
||||
from typing import Any, Callable, Dict, List, Tuple # NOQA
|
||||
from typing import Any, Dict, Iterator, List, Tuple # NOQA
|
||||
from sphinx.application import Sphinx # NOQA
|
||||
|
||||
NODEFAULT = object()
|
||||
THEMECONF = 'theme.conf'
|
||||
|
||||
|
||||
class Theme(object):
|
||||
"""
|
||||
Represents the theme chosen in the configuration.
|
||||
"""
|
||||
themes = {} # type: Dict[unicode, Tuple[unicode, zipfile.ZipFile]]
|
||||
themepath = [] # type: List[unicode]
|
||||
def extract_zip(filename, targetdir):
|
||||
# type: (unicode, unicode) -> None
|
||||
"""Extract zip file to target directory."""
|
||||
ensuredir(targetdir)
|
||||
|
||||
@classmethod
|
||||
def init_themes(cls, confdir, theme_path):
|
||||
# type: (unicode, unicode) -> None
|
||||
"""Search all theme paths for available themes."""
|
||||
cls.themepath = list(theme_path)
|
||||
cls.themepath.append(path.join(package_dir, 'themes'))
|
||||
|
||||
for themedir in cls.themepath[::-1]:
|
||||
themedir = path.join(confdir, themedir)
|
||||
if not path.isdir(themedir):
|
||||
with ZipFile(filename) as archive: # type: ignore
|
||||
for name in archive.namelist():
|
||||
if name.endswith('/'):
|
||||
continue
|
||||
for theme in os.listdir(themedir):
|
||||
if theme.lower().endswith('.zip'):
|
||||
try:
|
||||
zfile = zipfile.ZipFile(path.join(themedir, theme)) # type: ignore
|
||||
if THEMECONF not in zfile.namelist():
|
||||
continue
|
||||
tname = theme[:-4]
|
||||
tinfo = zfile
|
||||
except Exception:
|
||||
logger.warning('file %r on theme path is not a valid '
|
||||
'zipfile or contains no theme', theme)
|
||||
continue
|
||||
else:
|
||||
if not path.isfile(path.join(themedir, theme, THEMECONF)):
|
||||
continue
|
||||
tname = theme
|
||||
tinfo = None
|
||||
cls.themes[tname] = (path.join(themedir, theme), tinfo)
|
||||
entry = path.join(targetdir, name)
|
||||
ensuredir(path.dirname(entry))
|
||||
with open(path.join(entry), 'wb') as fp:
|
||||
fp.write(archive.read(name))
|
||||
|
||||
@classmethod
|
||||
def load_extra_theme(cls, name):
|
||||
# type: (unicode) -> None
|
||||
themes = ['alabaster']
|
||||
try:
|
||||
import sphinx_rtd_theme
|
||||
themes.append('sphinx_rtd_theme')
|
||||
except ImportError:
|
||||
pass
|
||||
if name in themes:
|
||||
if name == 'alabaster':
|
||||
import alabaster
|
||||
themedir = alabaster.get_path()
|
||||
# alabaster theme also requires 'alabaster' extension, it will be loaded
|
||||
# at sphinx.application module.
|
||||
elif name == 'sphinx_rtd_theme':
|
||||
themedir = sphinx_rtd_theme.get_html_theme_path()
|
||||
else:
|
||||
raise NotImplementedError('Programming Error')
|
||||
|
||||
else:
|
||||
for themedir in load_theme_plugins():
|
||||
if path.isfile(path.join(themedir, name, THEMECONF)):
|
||||
break
|
||||
else:
|
||||
# specified theme is not found
|
||||
return
|
||||
class Theme(object):
|
||||
"""A Theme is a set of HTML templates and configurations.
|
||||
|
||||
cls.themepath.append(themedir)
|
||||
cls.themes[name] = (path.join(themedir, name), None)
|
||||
return
|
||||
This class supports both theme directory and theme archive (zipped theme)."""
|
||||
|
||||
def __init__(self, name):
|
||||
# type: (unicode) -> None
|
||||
if name not in self.themes:
|
||||
self.load_extra_theme(name)
|
||||
if name not in self.themes:
|
||||
if name == 'sphinx_rtd_theme':
|
||||
raise ThemeError('sphinx_rtd_theme is no longer a hard dependency '
|
||||
'since version 1.4.0. Please install it manually.'
|
||||
'(pip install sphinx_rtd_theme)')
|
||||
else:
|
||||
raise ThemeError('no theme named %r found '
|
||||
'(missing theme.conf?)' % name)
|
||||
def __init__(self, name, theme_path, factory):
|
||||
# type: (unicode, unicode, HTMLThemeFactory) -> None
|
||||
self.name = name
|
||||
self.base = None
|
||||
self.rootdir = None
|
||||
|
||||
# Do not warn yet -- to be compatible with old Sphinxes, people *have*
|
||||
# to use "default".
|
||||
# if name == 'default' and warn:
|
||||
# warn("'default' html theme has been renamed to 'classic'. "
|
||||
# "Please change your html_theme setting either to "
|
||||
# "the new 'alabaster' default theme, or to 'classic' "
|
||||
# "to keep using the old default.")
|
||||
|
||||
tdir, tinfo = self.themes[name]
|
||||
if tinfo is None:
|
||||
if path.isdir(theme_path):
|
||||
# already a directory, do nothing
|
||||
self.rootdir = None
|
||||
self.themedir = tdir
|
||||
self.themedir_created = False
|
||||
self.themedir = theme_path
|
||||
else:
|
||||
# extract the theme to a temp directory
|
||||
self.rootdir = tempfile.mkdtemp('sxt')
|
||||
self.themedir = path.join(self.rootdir, name)
|
||||
self.themedir_created = True
|
||||
ensuredir(self.themedir)
|
||||
for name in tinfo.namelist():
|
||||
if name.endswith('/'):
|
||||
continue
|
||||
dirname = path.dirname(name)
|
||||
if not path.isdir(path.join(self.themedir, dirname)):
|
||||
os.makedirs(path.join(self.themedir, dirname))
|
||||
with open(path.join(self.themedir, name), 'wb') as fp:
|
||||
fp.write(tinfo.read(name))
|
||||
extract_zip(theme_path, self.themedir)
|
||||
|
||||
self.themeconf = configparser.RawConfigParser()
|
||||
self.themeconf.read(path.join(self.themedir, THEMECONF)) # type: ignore
|
||||
self.config = configparser.RawConfigParser()
|
||||
self.config.read(path.join(self.themedir, THEMECONF)) # type: ignore
|
||||
|
||||
try:
|
||||
inherit = self.themeconf.get('theme', 'inherit')
|
||||
inherit = self.config.get('theme', 'inherit')
|
||||
except configparser.NoOptionError:
|
||||
raise ThemeError('theme %r doesn\'t have "inherit" setting' % name)
|
||||
raise ThemeError(_('theme %r doesn\'t have "inherit" setting') % name)
|
||||
|
||||
# load inherited theme automatically #1794, #1884, #1885
|
||||
self.load_extra_theme(inherit)
|
||||
if inherit != 'none':
|
||||
try:
|
||||
self.base = factory.create(inherit)
|
||||
except ThemeError:
|
||||
raise ThemeError(_('no theme named %r found, inherited by %r') %
|
||||
(inherit, name))
|
||||
|
||||
if inherit == 'none':
|
||||
self.base = None
|
||||
elif inherit not in self.themes:
|
||||
raise ThemeError('no theme named %r found, inherited by %r' %
|
||||
(inherit, name))
|
||||
def get_theme_dirs(self):
|
||||
# type: () -> List[unicode]
|
||||
"""Return a list of theme directories, beginning with this theme's,
|
||||
then the base theme's, then that one's base theme's, etc.
|
||||
"""
|
||||
if self.base is None:
|
||||
return [self.themedir]
|
||||
else:
|
||||
self.base = Theme(inherit)
|
||||
return [self.themedir] + self.base.get_theme_dirs()
|
||||
|
||||
def get_confstr(self, section, name, default=NODEFAULT):
|
||||
def get_config(self, section, name, default=NODEFAULT):
|
||||
# type: (unicode, unicode, Any) -> Any
|
||||
"""Return the value for a theme configuration setting, searching the
|
||||
base theme chain.
|
||||
"""
|
||||
try:
|
||||
return self.themeconf.get(section, name) # type: ignore
|
||||
return self.config.get(section, name) # type: ignore
|
||||
except (configparser.NoOptionError, configparser.NoSectionError):
|
||||
if self.base is not None:
|
||||
return self.base.get_confstr(section, name, default)
|
||||
if self.base:
|
||||
return self.base.get_config(section, name, default)
|
||||
|
||||
if default is NODEFAULT:
|
||||
raise ThemeError('setting %s.%s occurs in none of the '
|
||||
'searched theme configs' % (section, name))
|
||||
raise ThemeError(_('setting %s.%s occurs in none of the '
|
||||
'searched theme configs') % (section, name))
|
||||
else:
|
||||
return default
|
||||
|
||||
def get_options(self, overrides):
|
||||
# type: (Dict) -> Any
|
||||
def get_options(self, overrides={}):
|
||||
# type: (Dict[unicode, Any]) -> Dict[unicode, Any]
|
||||
"""Return a dictionary of theme options and their values."""
|
||||
chain = [self.themeconf]
|
||||
base = self.base
|
||||
while base is not None:
|
||||
chain.append(base.themeconf)
|
||||
base = base.base
|
||||
options = {} # type: Dict[unicode, Any]
|
||||
for conf in reversed(chain):
|
||||
try:
|
||||
options.update(conf.items('options'))
|
||||
except configparser.NoSectionError:
|
||||
pass
|
||||
if self.base:
|
||||
options = self.base.get_options()
|
||||
else:
|
||||
options = {}
|
||||
|
||||
try:
|
||||
options.update(self.config.items('options'))
|
||||
except configparser.NoSectionError:
|
||||
pass
|
||||
|
||||
for option, value in iteritems(overrides):
|
||||
if option not in options:
|
||||
raise ThemeError('unsupported theme option %r given' % option)
|
||||
options[option] = value
|
||||
return options
|
||||
|
||||
def get_dirchain(self):
|
||||
# type: () -> List[unicode]
|
||||
"""Return a list of theme directories, beginning with this theme's,
|
||||
then the base theme's, then that one's base theme's, etc.
|
||||
"""
|
||||
chain = [self.themedir]
|
||||
base = self.base
|
||||
while base is not None:
|
||||
chain.append(base.themedir)
|
||||
base = base.base
|
||||
return chain
|
||||
return options
|
||||
|
||||
def cleanup(self):
|
||||
# type: () -> None
|
||||
"""Remove temporary directories."""
|
||||
if self.themedir_created:
|
||||
if self.rootdir:
|
||||
try:
|
||||
shutil.rmtree(self.rootdir)
|
||||
except Exception:
|
||||
@ -225,24 +146,125 @@ class Theme(object):
|
||||
self.base.cleanup()
|
||||
|
||||
|
||||
def load_theme_plugins():
|
||||
# type: () -> List[unicode]
|
||||
"""load plugins by using``sphinx_themes`` section in setuptools entry_points.
|
||||
This API will return list of directory that contain some theme directory.
|
||||
"""
|
||||
theme_paths = [] # type: List[unicode]
|
||||
def is_archived_theme(filename):
|
||||
# type: (unicode) -> bool
|
||||
"""Check the specified file is an archived theme file or not."""
|
||||
try:
|
||||
with ZipFile(filename) as f: # type: ignore
|
||||
return THEMECONF in f.namelist()
|
||||
except:
|
||||
return False
|
||||
|
||||
for plugin in pkg_resources.iter_entry_points('sphinx_themes'):
|
||||
func_or_path = plugin.load()
|
||||
try:
|
||||
path = func_or_path()
|
||||
except Exception:
|
||||
path = func_or_path
|
||||
|
||||
if isinstance(path, string_types):
|
||||
theme_paths.append(path)
|
||||
class HTMLThemeFactory(object):
|
||||
"""A factory class for HTML Themes."""
|
||||
|
||||
def __init__(self, app):
|
||||
# type: (Sphinx) -> None
|
||||
self.confdir = app.confdir
|
||||
self.themes = app.html_themes
|
||||
self.load_builtin_themes()
|
||||
if getattr(app.config, 'html_theme_path', None):
|
||||
self.load_additional_themes(app.config.html_theme_path)
|
||||
|
||||
def load_builtin_themes(self):
|
||||
# type: () -> None
|
||||
"""Load built-in themes."""
|
||||
themes = self.find_themes(path.join(package_dir, 'themes'))
|
||||
for name, theme in iteritems(themes):
|
||||
self.themes[name] = theme
|
||||
|
||||
def load_additional_themes(self, theme_paths):
|
||||
# type: (unicode) -> None
|
||||
"""Load additional themes placed at specified directories."""
|
||||
for theme_path in theme_paths:
|
||||
abs_theme_path = path.abspath(path.join(self.confdir, theme_path))
|
||||
themes = self.find_themes(abs_theme_path)
|
||||
for name, theme in iteritems(themes):
|
||||
self.themes[name] = theme
|
||||
|
||||
def load_extra_theme(self, name):
|
||||
# type: (unicode) -> None
|
||||
"""Try to load a theme having specifed name."""
|
||||
if name == 'alabaster':
|
||||
self.load_alabaster_theme()
|
||||
elif name == 'sphinx_rtd_theme':
|
||||
self.load_sphinx_rtd_theme()
|
||||
else:
|
||||
raise ThemeError('Plugin %r does not response correctly.' %
|
||||
plugin.module_name)
|
||||
self.load_external_theme(name)
|
||||
|
||||
return theme_paths
|
||||
def load_alabaster_theme(self):
|
||||
# type: () -> None
|
||||
"""Load alabaster theme."""
|
||||
import alabaster
|
||||
self.themes['alabaster'] = path.join(alabaster.get_path(), 'alabaster')
|
||||
|
||||
def load_sphinx_rtd_theme(self):
|
||||
# type: () -> None
|
||||
"""Load sphinx_rtd_theme theme (if exists)."""
|
||||
try:
|
||||
import sphinx_rtd_theme
|
||||
theme_path = sphinx_rtd_theme.get_html_theme_path()
|
||||
self.themes['sphinx_rtd_theme'] = path.join(theme_path, 'sphinx_rtd_theme')
|
||||
except ImportError:
|
||||
pass
|
||||
|
||||
def load_external_theme(self, name):
|
||||
# type: (unicode) -> None
|
||||
"""Try to load a theme using entry_points.
|
||||
|
||||
Sphinx refers to ``sphinx_themes`` entry_points.
|
||||
"""
|
||||
for entry_point in pkg_resources.iter_entry_points('sphinx_themes'):
|
||||
target = entry_point.load()
|
||||
if callable(target):
|
||||
themedir = target()
|
||||
if not isinstance(path, string_types):
|
||||
logger.warning(_('Theme extension %r does not response correctly.') %
|
||||
entry_point.module_name)
|
||||
else:
|
||||
themedir = target
|
||||
|
||||
themes = self.find_themes(themedir)
|
||||
for entry, theme in iteritems(themes):
|
||||
if name == entry:
|
||||
self.themes[name] = theme
|
||||
|
||||
def find_themes(self, theme_path):
|
||||
# type: (unicode) -> Dict[unicode, unicode]
|
||||
"""Search themes from specified directory."""
|
||||
themes = {} # type: Dict[unicode, unicode]
|
||||
if not path.isdir(theme_path):
|
||||
return themes
|
||||
|
||||
for entry in os.listdir(theme_path):
|
||||
pathname = path.join(theme_path, entry)
|
||||
if path.isfile(pathname) and entry.lower().endswith('.zip'):
|
||||
if is_archived_theme(pathname):
|
||||
name = entry[:-4]
|
||||
themes[name] = pathname
|
||||
else:
|
||||
logger.warning(_('file %r on theme path is not a valid '
|
||||
'zipfile or contains no theme'), entry)
|
||||
else:
|
||||
if path.isfile(path.join(pathname, THEMECONF)):
|
||||
themes[entry] = pathname
|
||||
|
||||
return themes
|
||||
|
||||
def create(self, name):
|
||||
# type: (unicode) -> Theme
|
||||
"""Create an instance of theme."""
|
||||
if name not in self.themes:
|
||||
self.load_extra_theme(name)
|
||||
|
||||
if name not in self.themes:
|
||||
if name == 'sphinx_rtd_theme':
|
||||
raise ThemeError(_('sphinx_rtd_theme is no longer a hard dependency '
|
||||
'since version 1.4.0. Please install it manually.'
|
||||
'(pip install sphinx_rtd_theme)'))
|
||||
else:
|
||||
raise ThemeError(_('no theme named %r found '
|
||||
'(missing theme.conf?)') % name)
|
||||
|
||||
return Theme(name, self.themes[name], factory=self)
|
||||
|
@ -28,41 +28,73 @@ memory_address_re = re.compile(r' at 0x[0-9a-f]{8,16}(?=>)', re.IGNORECASE)
|
||||
|
||||
|
||||
if PY3:
|
||||
from functools import partial
|
||||
|
||||
# Copied from the definition of inspect.getfullargspec from Python master,
|
||||
# and modified to remove the use of special flags that break decorated
|
||||
# callables and bound methods in the name of backwards compatibility. Used
|
||||
# under the terms of PSF license v2, which requires the above statement
|
||||
# and the following:
|
||||
#
|
||||
# Copyright (c) 2001, 2002, 2003, 2004, 2005, 2006, 2007, 2008, 2009,
|
||||
# 2010, 2011, 2012, 2013, 2014, 2015, 2016, 2017 Python Software
|
||||
# Foundation; All Rights Reserved
|
||||
def getargspec(func):
|
||||
"""Like inspect.getargspec but supports functools.partial as well."""
|
||||
if inspect.ismethod(func):
|
||||
func = func.__func__
|
||||
if type(func) is partial:
|
||||
orig_func = func.func
|
||||
argspec = getargspec(orig_func)
|
||||
args = list(argspec[0])
|
||||
defaults = list(argspec[3] or ())
|
||||
kwoargs = list(argspec[4])
|
||||
kwodefs = dict(argspec[5] or {})
|
||||
if func.args:
|
||||
args = args[len(func.args):]
|
||||
for arg in func.keywords or ():
|
||||
try:
|
||||
i = args.index(arg) - len(args)
|
||||
del args[i]
|
||||
try:
|
||||
del defaults[i]
|
||||
except IndexError:
|
||||
pass
|
||||
except ValueError: # must be a kwonly arg
|
||||
i = kwoargs.index(arg)
|
||||
del kwoargs[i]
|
||||
del kwodefs[arg]
|
||||
return inspect.FullArgSpec(args, argspec[1], argspec[2],
|
||||
tuple(defaults), kwoargs,
|
||||
kwodefs, argspec[6])
|
||||
while hasattr(func, '__wrapped__'):
|
||||
func = func.__wrapped__
|
||||
if not inspect.isfunction(func):
|
||||
raise TypeError('%r is not a Python function' % func)
|
||||
return inspect.getfullargspec(func)
|
||||
"""Like inspect.getfullargspec but supports bound methods, and wrapped
|
||||
methods."""
|
||||
# On 3.5+, signature(int) or similar raises ValueError. On 3.4, it
|
||||
# succeeds with a bogus signature. We want a TypeError uniformly, to
|
||||
# match historical behavior.
|
||||
if (isinstance(func, type) and
|
||||
is_builtin_class_method(func, "__new__") and
|
||||
is_builtin_class_method(func, "__init__")):
|
||||
raise TypeError(
|
||||
"can't compute signature for built-in type {}".format(func))
|
||||
|
||||
sig = inspect.signature(func)
|
||||
|
||||
args = []
|
||||
varargs = None
|
||||
varkw = None
|
||||
kwonlyargs = []
|
||||
defaults = ()
|
||||
annotations = {}
|
||||
defaults = ()
|
||||
kwdefaults = {}
|
||||
|
||||
if sig.return_annotation is not sig.empty:
|
||||
annotations['return'] = sig.return_annotation
|
||||
|
||||
for param in sig.parameters.values():
|
||||
kind = param.kind
|
||||
name = param.name
|
||||
|
||||
if kind is inspect.Parameter.POSITIONAL_ONLY:
|
||||
args.append(name)
|
||||
elif kind is inspect.Parameter.POSITIONAL_OR_KEYWORD:
|
||||
args.append(name)
|
||||
if param.default is not param.empty:
|
||||
defaults += (param.default,)
|
||||
elif kind is inspect.Parameter.VAR_POSITIONAL:
|
||||
varargs = name
|
||||
elif kind is inspect.Parameter.KEYWORD_ONLY:
|
||||
kwonlyargs.append(name)
|
||||
if param.default is not param.empty:
|
||||
kwdefaults[name] = param.default
|
||||
elif kind is inspect.Parameter.VAR_KEYWORD:
|
||||
varkw = name
|
||||
|
||||
if param.annotation is not param.empty:
|
||||
annotations[name] = param.annotation
|
||||
|
||||
if not kwdefaults:
|
||||
# compatibility with 'func.__kwdefaults__'
|
||||
kwdefaults = None
|
||||
|
||||
if not defaults:
|
||||
# compatibility with 'func.__defaults__'
|
||||
defaults = None
|
||||
|
||||
return inspect.FullArgSpec(args, varargs, varkw, defaults,
|
||||
kwonlyargs, kwdefaults, annotations)
|
||||
|
||||
else: # 2.7
|
||||
from functools import partial
|
||||
|
@ -324,7 +324,11 @@ class WarningIsErrorFilter(logging.Filter):
|
||||
def filter(self, record):
|
||||
# type: (logging.LogRecord) -> bool
|
||||
if self.app.warningiserror:
|
||||
raise SphinxWarning(record.msg % record.args)
|
||||
location = getattr(record, 'location', '')
|
||||
if location:
|
||||
raise SphinxWarning(location + ":" + record.msg % record.args)
|
||||
else:
|
||||
raise SphinxWarning(record.msg % record.args)
|
||||
else:
|
||||
return True
|
||||
|
||||
@ -434,8 +438,8 @@ def setup(app, status, warning):
|
||||
|
||||
warning_handler = WarningStreamHandler(SafeEncodingWriter(warning)) # type: ignore
|
||||
warning_handler.addFilter(WarningSuppressor(app))
|
||||
warning_handler.addFilter(WarningIsErrorFilter(app))
|
||||
warning_handler.addFilter(WarningLogRecordTranslator(app))
|
||||
warning_handler.addFilter(WarningIsErrorFilter(app))
|
||||
warning_handler.setLevel(logging.WARNING)
|
||||
warning_handler.setFormatter(ColorizeFormatter())
|
||||
|
||||
|
@ -5,4 +5,5 @@ import sys, os
|
||||
templates_path = ['_templates']
|
||||
master_doc = 'index'
|
||||
html_theme = 'base_theme2'
|
||||
html_theme_path = ['base_themes_dir']
|
||||
exclude_patterns = ['_build']
|
||||
|
@ -10,6 +10,8 @@
|
||||
:license: BSD, see LICENSE for details.
|
||||
"""
|
||||
|
||||
from six import PY3
|
||||
|
||||
from util import SphinxTestApp, Struct # NOQA
|
||||
import pytest
|
||||
|
||||
@ -752,6 +754,10 @@ def test_generate():
|
||||
|
||||
# test autodoc_member_order == 'source'
|
||||
directive.env.ref_context['py:module'] = 'test_autodoc'
|
||||
if PY3:
|
||||
roger_line = ' .. py:classmethod:: Class.roger(a, *, b=2, c=3, d=4, e=5, f=6)'
|
||||
else:
|
||||
roger_line = ' .. py:classmethod:: Class.roger(a, e=5, f=6)'
|
||||
assert_order(['.. py:class:: Class(arg)',
|
||||
' .. py:attribute:: Class.descr',
|
||||
' .. py:method:: Class.meth()',
|
||||
@ -761,7 +767,7 @@ def test_generate():
|
||||
' .. py:attribute:: Class.docattr',
|
||||
' .. py:attribute:: Class.udocattr',
|
||||
' .. py:attribute:: Class.mdocattr',
|
||||
' .. py:classmethod:: Class.roger(a, e=5, f=6)',
|
||||
roger_line,
|
||||
' .. py:classmethod:: Class.moore(a, e, f) -> happiness',
|
||||
' .. py:attribute:: Class.inst_attr_comment',
|
||||
' .. py:attribute:: Class.inst_attr_string',
|
||||
|
@ -586,7 +586,7 @@ def test_numfig_without_numbered_toctree_warn(app, warning):
|
||||
app.build()
|
||||
# remove :numbered: option
|
||||
index = (app.srcdir / 'index.rst').text()
|
||||
index = re.sub(':numbered:.*', '', index, re.MULTILINE)
|
||||
index = re.sub(':numbered:.*', '', index)
|
||||
(app.srcdir / 'index.rst').write_text(index, encoding='utf-8')
|
||||
app.builder.build_all()
|
||||
|
||||
@ -684,7 +684,7 @@ def test_numfig_without_numbered_toctree_warn(app, warning):
|
||||
def test_numfig_without_numbered_toctree(app, cached_etree_parse, fname, expect):
|
||||
# remove :numbered: option
|
||||
index = (app.srcdir / 'index.rst').text()
|
||||
index = re.sub(':numbered:.*', '', index, re.MULTILINE)
|
||||
index = re.sub(':numbered:.*', '', index)
|
||||
(app.srcdir / 'index.rst').write_text(index, encoding='utf-8')
|
||||
|
||||
if not app.outdir.listdir():
|
||||
|
@ -10,14 +10,10 @@
|
||||
"""
|
||||
|
||||
import os
|
||||
import zipfile
|
||||
|
||||
import mock
|
||||
import pytest
|
||||
|
||||
from sphinx.theming import Theme, ThemeError
|
||||
|
||||
from util import path
|
||||
from sphinx.theming import ThemeError
|
||||
|
||||
|
||||
@pytest.mark.sphinx(
|
||||
@ -27,29 +23,28 @@ def test_theme_api(app, status, warning):
|
||||
cfg = app.config
|
||||
|
||||
# test Theme class API
|
||||
assert set(Theme.themes.keys()) == \
|
||||
assert set(app.html_themes.keys()) == \
|
||||
set(['basic', 'default', 'scrolls', 'agogo', 'sphinxdoc', 'haiku',
|
||||
'traditional', 'testtheme', 'ziptheme', 'epub', 'nature',
|
||||
'pyramid', 'bizstyle', 'classic', 'nonav'])
|
||||
assert Theme.themes['testtheme'][1] is None
|
||||
assert isinstance(Theme.themes['ziptheme'][1], zipfile.ZipFile)
|
||||
assert app.html_themes['testtheme'] == app.srcdir / 'testtheme'
|
||||
assert app.html_themes['ziptheme'] == app.srcdir / 'ziptheme.zip'
|
||||
|
||||
# test Theme instance API
|
||||
theme = app.builder.theme
|
||||
assert theme.name == 'ziptheme'
|
||||
assert theme.themedir_created
|
||||
themedir = theme.themedir
|
||||
assert theme.base.name == 'basic'
|
||||
assert len(theme.get_dirchain()) == 2
|
||||
assert len(theme.get_theme_dirs()) == 2
|
||||
|
||||
# direct setting
|
||||
assert theme.get_confstr('theme', 'stylesheet') == 'custom.css'
|
||||
assert theme.get_config('theme', 'stylesheet') == 'custom.css'
|
||||
# inherited setting
|
||||
assert theme.get_confstr('options', 'nosidebar') == 'false'
|
||||
assert theme.get_config('options', 'nosidebar') == 'false'
|
||||
# nonexisting setting
|
||||
assert theme.get_confstr('theme', 'foobar', 'def') == 'def'
|
||||
assert theme.get_config('theme', 'foobar', 'def') == 'def'
|
||||
with pytest.raises(ThemeError):
|
||||
theme.get_confstr('theme', 'foobar')
|
||||
theme.get_config('theme', 'foobar')
|
||||
|
||||
# options API
|
||||
with pytest.raises(ThemeError):
|
||||
@ -88,21 +83,13 @@ def test_js_source(app, status, warning):
|
||||
|
||||
|
||||
@pytest.mark.sphinx(testroot='double-inheriting-theme')
|
||||
def test_double_inheriting_theme(make_app, app_params):
|
||||
from sphinx.theming import load_theme_plugins # load original before patching
|
||||
|
||||
def load_themes():
|
||||
roots = path(__file__).abspath().parent / 'roots'
|
||||
yield roots / 'test-double-inheriting-theme' / 'base_themes_dir'
|
||||
for t in load_theme_plugins():
|
||||
yield t
|
||||
|
||||
with mock.patch('sphinx.theming.load_theme_plugins', side_effect=load_themes):
|
||||
args, kwargs = app_params
|
||||
make_app(*args, **kwargs)
|
||||
def test_double_inheriting_theme(app, status, warning):
|
||||
assert app.builder.theme.name == 'base_theme2'
|
||||
app.build() # => not raises TemplateNotFound
|
||||
|
||||
|
||||
@pytest.mark.sphinx(testroot='theming',
|
||||
confoverrides={'html_theme': 'child'})
|
||||
def test_nested_zipped_theme(app, status, warning):
|
||||
assert app.builder.theme.name == 'child'
|
||||
app.build() # => not raises TemplateNotFound
|
||||
|
@ -10,8 +10,68 @@
|
||||
"""
|
||||
from unittest import TestCase
|
||||
|
||||
from six import PY3
|
||||
import functools
|
||||
from textwrap import dedent
|
||||
import pytest
|
||||
|
||||
from sphinx.util import inspect
|
||||
|
||||
class TestGetArgSpec(TestCase):
|
||||
def test_getargspec_builtin_type(self):
|
||||
with pytest.raises(TypeError):
|
||||
inspect.getargspec(int)
|
||||
|
||||
def test_getargspec_partial(self):
|
||||
def fun(a, b, c=1, d=2):
|
||||
pass
|
||||
p = functools.partial(fun, 10, c=11)
|
||||
|
||||
if PY3:
|
||||
# Python 3's partial is rather cleverer than Python 2's, and we
|
||||
# have to jump through some hoops to define an equivalent function
|
||||
# in a way that won't confuse Python 2's parser:
|
||||
ns = {}
|
||||
exec(dedent("""
|
||||
def f_expected(b, *, c=11, d=2):
|
||||
pass
|
||||
"""), ns)
|
||||
f_expected = ns["f_expected"]
|
||||
else:
|
||||
def f_expected(b, d=2):
|
||||
pass
|
||||
expected = inspect.getargspec(f_expected)
|
||||
|
||||
assert expected == inspect.getargspec(p)
|
||||
|
||||
def test_getargspec_bound_methods(self):
|
||||
def f_expected_unbound(self, arg1, **kwargs):
|
||||
pass
|
||||
expected_unbound = inspect.getargspec(f_expected_unbound)
|
||||
|
||||
def f_expected_bound(arg1, **kwargs):
|
||||
pass
|
||||
expected_bound = inspect.getargspec(f_expected_bound)
|
||||
|
||||
class Foo:
|
||||
def method(self, arg1, **kwargs):
|
||||
pass
|
||||
|
||||
bound_method = Foo().method
|
||||
|
||||
@functools.wraps(bound_method)
|
||||
def wrapped_bound_method(*args, **kwargs):
|
||||
pass
|
||||
|
||||
assert expected_unbound == inspect.getargspec(Foo.method)
|
||||
if PY3:
|
||||
# On py2, the inspect functions don't properly handle bound
|
||||
# methods (they include a spurious 'self' argument)
|
||||
assert expected_bound == inspect.getargspec(bound_method)
|
||||
# On py2, the inspect functions can't properly handle wrapped
|
||||
# functions (no __wrapped__ support)
|
||||
assert expected_bound == inspect.getargspec(wrapped_bound_method)
|
||||
|
||||
|
||||
class TestSafeGetAttr(TestCase):
|
||||
def test_safe_getattr_with_default(self):
|
||||
|
@ -24,7 +24,6 @@ from docutils.parsers.rst import directives, roles
|
||||
|
||||
from sphinx import application
|
||||
from sphinx.builders.latex import LaTeXBuilder
|
||||
from sphinx.theming import Theme
|
||||
from sphinx.ext.autodoc import AutoDirective
|
||||
from sphinx.pycode import ModuleAnalyzer
|
||||
from sphinx.deprecation import RemovedInSphinx17Warning
|
||||
@ -168,7 +167,6 @@ class SphinxTestApp(application.Sphinx):
|
||||
raise
|
||||
|
||||
def cleanup(self, doctrees=False):
|
||||
Theme.themes.clear()
|
||||
AutoDirective._registry.clear()
|
||||
ModuleAnalyzer.cache.clear()
|
||||
LaTeXBuilder.usepackages = []
|
||||
|
Loading…
Reference in New Issue
Block a user