Merge branch 'master' into support_remote_images

This commit is contained in:
Takeshi KOMIYA 2017-04-22 09:45:05 +09:00
commit 241c0db7bc
25 changed files with 674 additions and 349 deletions

View File

@ -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

View File

@ -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
----------

View File

@ -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

View File

@ -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)

View File

@ -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

View File

@ -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,
}

View File

@ -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
View 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,
}

View File

@ -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',

View File

@ -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

View File

@ -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',

View File

@ -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

View File

@ -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))

View File

@ -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:

View File

@ -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>

View File

@ -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;

View File

@ -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)

View File

@ -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

View File

@ -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())

View File

@ -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']

View File

@ -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',

View File

@ -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():

View File

@ -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

View File

@ -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):

View File

@ -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 = []