From b39b0191a762ba6997b0b5e8dfef7055c5d52569 Mon Sep 17 00:00:00 2001 From: Takeshi KOMIYA Date: Mon, 30 Jan 2017 01:16:10 +0900 Subject: [PATCH 01/23] Fix #3351: intersphinx does not refers context --- CHANGES | 2 ++ sphinx/domains/__init__.py | 5 +++++ sphinx/domains/python.py | 10 ++++++++++ sphinx/ext/intersphinx.py | 8 ++++++++ tests/test_domain_py.py | 34 ++++++++++++++++++++++++++++++++-- tests/test_ext_intersphinx.py | 12 ++++++++++++ 6 files changed, 69 insertions(+), 2 deletions(-) diff --git a/CHANGES b/CHANGES index 5d605e405..8d1e68d6b 100644 --- a/CHANGES +++ b/CHANGES @@ -117,6 +117,8 @@ Bugs fixed * C++, properly look up ``any`` references. * #3624: sphinx.ext.intersphinx couldn't load inventories compressed with gzip * #3551: PDF information dictionary is lacking author and title data +* #3351: intersphinx does not refers context like ``py:module``, ``py:class`` + and so on. Deprecated ---------- diff --git a/sphinx/domains/__init__.py b/sphinx/domains/__init__.py index 43b6bac04..4085d5e13 100644 --- a/sphinx/domains/__init__.py +++ b/sphinx/domains/__init__.py @@ -308,3 +308,8 @@ class Domain(object): if primary: return type.lname return _('%s %s') % (self.label, type.lname) + + def get_full_qualified_name(self, node): + # type: (nodes.Node) -> unicode + """Return full qualified name for given node.""" + return None diff --git a/sphinx/domains/python.py b/sphinx/domains/python.py index 7439b297b..28ea1a08a 100644 --- a/sphinx/domains/python.py +++ b/sphinx/domains/python.py @@ -885,6 +885,16 @@ class PythonDomain(Domain): if type != 'module': # modules are already handled yield (refname, refname, type, docname, refname, 1) + def get_full_qualified_name(self, node): + # type: (nodes.Node) -> unicode + modname = node.get('py:module') + clsname = node.get('py:class') + target = node.get('reftarget') + if target is None: + return None + else: + return '.'.join(filter(None, [modname, clsname, target])) + def setup(app): # type: (Sphinx) -> Dict[unicode, Any] diff --git a/sphinx/ext/intersphinx.py b/sphinx/ext/intersphinx.py index 1ef4d1d51..d018603e2 100644 --- a/sphinx/ext/intersphinx.py +++ b/sphinx/ext/intersphinx.py @@ -292,6 +292,10 @@ def missing_reference(app, env, node, contnode): # until Sphinx-1.6, cmdoptions are stored as std:option objtypes.append('std:option') to_try = [(inventories.main_inventory, target)] + if domain: + full_qualified_name = env.get_domain(domain).get_full_qualified_name(node) + if full_qualified_name: + to_try.append((inventories.main_inventory, full_qualified_name)) in_set = None if ':' in target: # first part may be the foreign doc set name @@ -299,6 +303,10 @@ def missing_reference(app, env, node, contnode): if setname in inventories.named_inventory: in_set = setname to_try.append((inventories.named_inventory[setname], newtarget)) + if domain: + full_qualified_name = env.get_domain(domain).get_full_qualified_name(node) + if full_qualified_name: + to_try.append((inventories.named_inventory[setname], full_qualified_name)) for inventory, target in to_try: for objtype in objtypes: if objtype not in inventory or target not in inventory[objtype]: diff --git a/tests/test_domain_py.py b/tests/test_domain_py.py index 73766e718..eb8a7a178 100644 --- a/tests/test_domain_py.py +++ b/tests/test_domain_py.py @@ -10,9 +10,12 @@ """ import pytest +from mock import Mock from six import text_type +from docutils import nodes + from sphinx import addnodes -from sphinx.domains.python import py_sig_re, _pseudo_parse_arglist +from sphinx.domains.python import py_sig_re, _pseudo_parse_arglist, PythonDomain from util import assert_node @@ -28,7 +31,6 @@ def parse(sig): def test_function_signatures(): - rv = parse('func(a=1) -> int object') assert text_type(rv) == u'a=1' @@ -165,3 +167,31 @@ def test_domain_py_find_obj(app, status, warning): [(u'NestedParentA.NestedChildA.subchild_1', (u'roles', u'method'))]) assert (find_obj(None, u'NestedParentA.NestedChildA', u'subchild_1', u'meth') == [(u'NestedParentA.NestedChildA.subchild_1', (u'roles', u'method'))]) + + +def test_get_full_qualified_name(): + env = Mock(domaindata={}) + domain = PythonDomain(env) + + # non-python references + node = nodes.reference() + assert domain.get_full_qualified_name(node) is None + + # simple reference + node = nodes.reference(reftarget='func') + assert domain.get_full_qualified_name(node) == 'func' + + # with py:module context + kwargs = {'py:module': 'module1'} + node = nodes.reference(reftarget='func', **kwargs) + assert domain.get_full_qualified_name(node) == 'module1.func' + + # with py:class context + kwargs = {'py:class': 'Class'} + node = nodes.reference(reftarget='func', **kwargs) + assert domain.get_full_qualified_name(node) == 'Class.func' + + # with both py:module and py:class context + kwargs = {'py:module': 'module1', 'py:class': 'Class'} + node = nodes.reference(reftarget='func', **kwargs) + assert domain.get_full_qualified_name(node) == 'module1.Class.func' diff --git a/tests/test_ext_intersphinx.py b/tests/test_ext_intersphinx.py index 86e56fd47..33f5ceae2 100644 --- a/tests/test_ext_intersphinx.py +++ b/tests/test_ext_intersphinx.py @@ -145,6 +145,18 @@ def test_missing_reference(tempdir, app, status, warning): assert rn is None assert contnode[0].astext() == 'py3k:unknown' + # no context data + kwargs = {} + node, contnode = fake_node('py', 'func', 'func', 'func()', **kwargs) + rn = missing_reference(app, app.env, node, contnode) + assert rn is None + + # context data (like py:module) help to search objects + kwargs = {'py:module': 'module1'} + node, contnode = fake_node('py', 'func', 'func', 'func()', **kwargs) + rn = missing_reference(app, app.env, node, contnode) + assert rn[0].astext() == 'func()' + # check relative paths rn = reference_check('py', 'mod', 'py3krel:module1', 'foo') assert rn['refuri'] == 'py3k/foo.html#module-module1' From c5257238075fa4fbcb774fd2905a502d88070d8c Mon Sep 17 00:00:00 2001 From: Takeshi KOMIYA Date: Tue, 7 Mar 2017 14:10:23 +0900 Subject: [PATCH 02/23] Add sphinx.ext.imgconverter --- .travis.yml | 1 + CHANGES | 2 + doc/ext/builtins.rst | 1 + doc/ext/imgconverter.rst | 25 +++ sphinx/ext/imgconverter.py | 88 +++++++++++ sphinx/transforms/post_transforms/images.py | 99 ++++++++++++ tests/roots/test-ext-imgconverter/conf.py | 4 + tests/roots/test-ext-imgconverter/index.rst | 4 + tests/roots/test-ext-imgconverter/svgimg.svg | 158 +++++++++++++++++++ tests/test_ext_imgconverter.py | 22 +++ 10 files changed, 404 insertions(+) create mode 100644 doc/ext/imgconverter.rst create mode 100644 sphinx/ext/imgconverter.py create mode 100644 tests/roots/test-ext-imgconverter/conf.py create mode 100644 tests/roots/test-ext-imgconverter/index.rst create mode 100644 tests/roots/test-ext-imgconverter/svgimg.svg create mode 100644 tests/test_ext_imgconverter.py diff --git a/.travis.yml b/.travis.yml index 3557095dd..c1b320dbc 100644 --- a/.travis.yml +++ b/.travis.yml @@ -43,6 +43,7 @@ addons: - texlive-xetex - lmodern - latex-xcolor + - imagemagick install: - pip install -U pip setuptools - pip install docutils==$DOCUTILS diff --git a/CHANGES b/CHANGES index 99e5e2285..11b3ca5cd 100644 --- a/CHANGES +++ b/CHANGES @@ -117,6 +117,8 @@ Features added * #3641: Epub theme supports HTML structures that are generated by HTML5 writer. * #3644 autodoc uses inspect instead of checking types. Thanks to Jeroen Demeyer. +* Add a new extension; ``sphinx.ext.imgconverter``. It converts images in the + document to appropriate format for builders Bugs fixed ---------- diff --git a/doc/ext/builtins.rst b/doc/ext/builtins.rst index 6d5e59a89..6972a5957 100644 --- a/doc/ext/builtins.rst +++ b/doc/ext/builtins.rst @@ -15,6 +15,7 @@ These extensions are built in and can be activated by respective entries in the githubpages graphviz ifconfig + imgconverter inheritance intersphinx linkcode diff --git a/doc/ext/imgconverter.rst b/doc/ext/imgconverter.rst new file mode 100644 index 000000000..1dfb79cf7 --- /dev/null +++ b/doc/ext/imgconverter.rst @@ -0,0 +1,25 @@ +.. highlight:: rest + +:mod:`sphinx.ext.imgconverter` -- Convert images to appropriate format for builders +=================================================================================== + +.. module:: sphinx.ext.imgconverter + :synopsis: Convert images to appropriate format for builders + +.. versionadded:: 1.6 + +This extension converts images in your document to appropriate format for builders. +For example, it allows you to use SVG images with LaTeX builder. +As a result, you don't mind what image format the builder supports. + +Internally, this extension uses Imagemagick_ to convert images. + +.. _Imagemagick: https://www.imagemagick.org/script/index.php + +Configuration +------------- + +.. confval:: image_converter + + A path to :command:`convert` command. By default, the imgconverter uses + the command from search paths. diff --git a/sphinx/ext/imgconverter.py b/sphinx/ext/imgconverter.py new file mode 100644 index 000000000..bc2564ee0 --- /dev/null +++ b/sphinx/ext/imgconverter.py @@ -0,0 +1,88 @@ +# -*- coding: utf-8 -*- +""" + sphinx.ext.imgconverter + ~~~~~~~~~~~~~~~~~~~~~~~ + + Image converter extension for Sphinx + + :copyright: Copyright 2007-2017 by the Sphinx team, see AUTHORS. + :license: BSD, see LICENSE for details. +""" +import subprocess + +from sphinx.errors import ExtensionError +from sphinx.locale import _ +from sphinx.transforms.post_transforms.images import ImageConverter +from sphinx.util import logging +from sphinx.util.osutil import ENOENT, EPIPE, EINVAL + +if False: + # For type annotation + from typing import Any, Dict # NOQA + from sphinx.application import Sphinx # NOQA + + +logger = logging.getLogger(__name__) + + +class ImagemagickConverter(ImageConverter): + conversion_rules = [ + ('image/svg+xml', 'image/png'), + ('application/pdf', 'image/png'), + ] + + def is_available(self): + # type: () -> bool + """Confirms the converter is available or not.""" + try: + ret = subprocess.call([self.config.image_converter, '-version'], + stdin=subprocess.PIPE, stdout=subprocess.PIPE) + if ret == 0: + return True + else: + return False + except (OSError, IOError): + logger.warning(_('convert command %r cannot be run.' + 'check the image_converter setting'), + self.config.image_converter) + return False + + def convert(self, _from, _to): + # type: (unicode, unicode) -> None + """Converts the image to expected one.""" + try: + p = subprocess.Popen([self.config.image_converter, _from, _to], + stdin=subprocess.PIPE, stdout=subprocess.PIPE) + except OSError as err: + if err.errno != ENOENT: # No such file or directory + raise + logger.warning(_('convert command %r cannot be run.' + 'check the image_converter setting'), + self.config.image_converter) + return False + + try: + stdout, stderr = p.communicate() + except (OSError, IOError) as err: + if err.errno not in (EPIPE, EINVAL): + raise + stdout, stderr = p.stdout.read(), p.stderr.read() + p.wait() + if p.returncode != 0: + raise ExtensionError(_('convert exited with error:\n' + '[stderr]\n%s\n[stdout]\n%s') % + (stderr, stdout)) + + return True + + +def setup(app): + # type: (Sphinx) -> Dict[unicode, Any] + app.add_post_transform(ImagemagickConverter) + app.add_config_value('image_converter', 'convert', 'env') + + return { + 'version': 'builtin', + 'parallel_read_safe': True, + 'parallel_write_safe': True, + } diff --git a/sphinx/transforms/post_transforms/images.py b/sphinx/transforms/post_transforms/images.py index 9a232c1d4..cf481f62e 100644 --- a/sphinx/transforms/post_transforms/images.py +++ b/sphinx/transforms/post_transforms/images.py @@ -136,6 +136,105 @@ class DataURIExtractor(BaseImageConverter): self.app.env.images.add_file(self.env.docname, path) +def get_filename_for(filename, mimetype): + # type: (unicode, unicode) -> unicode + basename = os.path.basename(filename) + return os.path.splitext(basename)[0] + get_image_extension(mimetype) + + +class ImageConverter(BaseImageConverter): + """A base class images converter. + + The concrete image converters should derive this class and + overrides the following methods and attributes: + + * default_priority (if needed) + * conversion_rules + * is_available() + * convert() + """ + default_priority = 200 + + #: A conversion rules between two mimetypes which this converters supports + conversion_rules = [] + + def __init__(self, *args, **kwargs): + # type: (Any, Any) -> None + self.available = None # not checked yet + BaseImageConverter.__init__(self, *args, **kwargs) + + def match(self, node): + # type: (nodes.Node) -> bool + if self.available is None: + self.available = self.is_available() + + if not self.available: + return False + elif set(node['candidates']) & set(self.app.builder.supported_image_types): + # builder supports the image; no need to convert + return False + else: + rule = self.get_conversion_rule(node) + if rule: + return True + else: + return False + + def get_conversion_rule(self, node): + # type: (nodes.Node) -> Tuple[unicode, unicode] + for candidate in self.guess_mimetypes(node): + for supported in self.app.builder.supported_image_types: + rule = (candidate, supported) + if rule in self.conversion_rules: + return rule + + return None + + def is_available(self): + # type: () -> bool + """Confirms the converter is available or not.""" + raise NotImplemented + + def guess_mimetypes(self, node): + # type: (nodes.Node) -> unicode + if '?' in node['candidates']: + return [] + elif '*' in node['candidates']: + from sphinx.util.images import guess_mimetype + return [guess_mimetype(node['uri'])] + else: + return node['candidates'].keys() + + def handle(self, node): + # type: (nodes.Node) -> None + _from, _to = self.get_conversion_rule(node) + + if _from in node['candidates']: + srcpath = node['candidates'][_from] + else: + srcpath = node['candidates']['*'] + + filename = get_filename_for(srcpath, _to) + ensuredir(self.imagedir) + destpath = os.path.join(self.imagedir, filename) + + abs_srcpath = os.path.join(self.app.srcdir, srcpath) + if self.convert(abs_srcpath, destpath): + if '*' in node['candidates']: + node['candidates']['*'] = destpath + else: + node['candidates'][_to] = destpath + node['uri'] = destpath + + self.env.original_image_uri[destpath] = srcpath + self.env.images.add_file(self.env.docname, destpath) + + def convert(self, _from, _to): + # type: (unicode, unicode) -> None + """Converts the image to expected one.""" + raise NotImplemented + + def setup(app): # type: (Sphinx) -> Dict[unicode, Any] app.add_post_transform(ImageDownloader) diff --git a/tests/roots/test-ext-imgconverter/conf.py b/tests/roots/test-ext-imgconverter/conf.py new file mode 100644 index 000000000..67cee152d --- /dev/null +++ b/tests/roots/test-ext-imgconverter/conf.py @@ -0,0 +1,4 @@ +# -*- coding: utf-8 -*- + +master_doc = 'index' +extensions = ['sphinx.ext.imgconverter'] diff --git a/tests/roots/test-ext-imgconverter/index.rst b/tests/roots/test-ext-imgconverter/index.rst new file mode 100644 index 000000000..786c92e8d --- /dev/null +++ b/tests/roots/test-ext-imgconverter/index.rst @@ -0,0 +1,4 @@ +test-ext-imgconverter +===================== + +.. image:: svgimg.svg diff --git a/tests/roots/test-ext-imgconverter/svgimg.svg b/tests/roots/test-ext-imgconverter/svgimg.svg new file mode 100644 index 000000000..10e035b6d --- /dev/null +++ b/tests/roots/test-ext-imgconverter/svgimg.svg @@ -0,0 +1,158 @@ + + + + + + + + + + + + + + Part of the Flat Icon Collection (Thu Aug 26 14:31:40 2004) + + + +
  • + + + + + + </Agent> + </publisher> + <creator + id="creator24"> + <Agent + about="" + id="Agent25"> + <title + id="title26">Danny Allen + + + + + Danny Allen + + + + image/svg+xml + + + + + en + + + + + image/svg+xml + + + + + + + + + + + + + diff --git a/tests/test_ext_imgconverter.py b/tests/test_ext_imgconverter.py new file mode 100644 index 000000000..3ea396093 --- /dev/null +++ b/tests/test_ext_imgconverter.py @@ -0,0 +1,22 @@ +# -*- coding: utf-8 -*- +""" + test_ext_imgconverter + ~~~~~~~~~~~~~~~~~~~~~ + + Test sphinx.ext.imgconverter extension. + + :copyright: Copyright 2007-2017 by the Sphinx team, see AUTHORS. + :license: BSD, see LICENSE for details. +""" + +import pytest + + +@pytest.mark.sphinx('latex', testroot='ext-imgconverter') +def test_ext_imgconverter(app, status, warning): + app.builder.build_all() + + content = (app.outdir / 'Python.tex').text() + assert '\sphinxincludegraphics{{svgimg}.png}' in content + assert not (app.outdir / 'svgimg.svg').exists() + assert (app.outdir / 'svgimg.png').exists() From 2b9b4f92a54563decff6832512a8d239f1ea4be4 Mon Sep 17 00:00:00 2001 From: Takeshi KOMIYA Date: Sat, 22 Apr 2017 13:26:09 +0900 Subject: [PATCH 03/23] Add image_converter_args confval --- doc/ext/imgconverter.rst | 5 +++++ sphinx/ext/imgconverter.py | 7 +++++-- 2 files changed, 10 insertions(+), 2 deletions(-) diff --git a/doc/ext/imgconverter.rst b/doc/ext/imgconverter.rst index 1dfb79cf7..4c43bdf8e 100644 --- a/doc/ext/imgconverter.rst +++ b/doc/ext/imgconverter.rst @@ -23,3 +23,8 @@ Configuration A path to :command:`convert` command. By default, the imgconverter uses the command from search paths. + +.. confval:: image_converter_args + + Additional command-line arguments to give to :command:`convert`, as a list. + The default is an empty list ``[]``. diff --git a/sphinx/ext/imgconverter.py b/sphinx/ext/imgconverter.py index bc2564ee0..f48c647a7 100644 --- a/sphinx/ext/imgconverter.py +++ b/sphinx/ext/imgconverter.py @@ -51,8 +51,10 @@ class ImagemagickConverter(ImageConverter): # type: (unicode, unicode) -> None """Converts the image to expected one.""" try: - p = subprocess.Popen([self.config.image_converter, _from, _to], - stdin=subprocess.PIPE, stdout=subprocess.PIPE) + args = ([self.config.image_converter] + + self.config.image_converter_args + + [_from, _to]) + p = subprocess.Popen(args, stdin=subprocess.PIPE, stdout=subprocess.PIPE) except OSError as err: if err.errno != ENOENT: # No such file or directory raise @@ -80,6 +82,7 @@ def setup(app): # type: (Sphinx) -> Dict[unicode, Any] app.add_post_transform(ImagemagickConverter) app.add_config_value('image_converter', 'convert', 'env') + app.add_config_value('image_converter_args', [], 'env') return { 'version': 'builtin', From 334d527e64e045791c1de48d8ee30e0c208ec57c Mon Sep 17 00:00:00 2001 From: Takeshi KOMIYA Date: Sat, 22 Apr 2017 14:55:43 +0900 Subject: [PATCH 04/23] Fix mypy violations --- sphinx/ext/imgconverter.py | 2 +- sphinx/transforms/post_transforms/images.py | 14 ++++++++------ 2 files changed, 9 insertions(+), 7 deletions(-) diff --git a/sphinx/ext/imgconverter.py b/sphinx/ext/imgconverter.py index f48c647a7..a06b789fb 100644 --- a/sphinx/ext/imgconverter.py +++ b/sphinx/ext/imgconverter.py @@ -48,7 +48,7 @@ class ImagemagickConverter(ImageConverter): return False def convert(self, _from, _to): - # type: (unicode, unicode) -> None + # type: (unicode, unicode) -> bool """Converts the image to expected one.""" try: args = ([self.config.image_converter] + diff --git a/sphinx/transforms/post_transforms/images.py b/sphinx/transforms/post_transforms/images.py index cf481f62e..e1cc9f78b 100644 --- a/sphinx/transforms/post_transforms/images.py +++ b/sphinx/transforms/post_transforms/images.py @@ -24,7 +24,7 @@ from sphinx.util.osutil import ensuredir if False: # For type annotation - from typing import Any, Dict # NOQA + from typing import Any, Dict, List, Tuple # NOQA from sphinx.application import Sphinx # NOQA @@ -156,12 +156,14 @@ class ImageConverter(BaseImageConverter): default_priority = 200 #: A conversion rules between two mimetypes which this converters supports - conversion_rules = [] + conversion_rules = [] # type: List[Tuple[unicode, unicode]] def __init__(self, *args, **kwargs): # type: (Any, Any) -> None - self.available = None # not checked yet - BaseImageConverter.__init__(self, *args, **kwargs) + self.available = None # type: bool + # the converter is available or not. + # Will be checked at first conversion + BaseImageConverter.__init__(self, *args, **kwargs) # type: ignore def match(self, node): # type: (nodes.Node) -> bool @@ -196,7 +198,7 @@ class ImageConverter(BaseImageConverter): raise NotImplemented def guess_mimetypes(self, node): - # type: (nodes.Node) -> unicode + # type: (nodes.Node) -> List[unicode] if '?' in node['candidates']: return [] elif '*' in node['candidates']: @@ -230,7 +232,7 @@ class ImageConverter(BaseImageConverter): self.env.images.add_file(self.env.docname, destpath) def convert(self, _from, _to): - # type: (unicode, unicode) -> None + # type: (unicode, unicode) -> bool """Converts the image to expected one.""" raise NotImplemented From 621a4e6f6182e9ef335e7e4cbc8302ff9caadfc6 Mon Sep 17 00:00:00 2001 From: Takeshi KOMIYA Date: Thu, 20 Apr 2017 21:21:04 +0900 Subject: [PATCH 05/23] Fix #3628: Rename sphinx_themes entry point to sphinx.html_themes --- CHANGES | 2 ++ doc/extdev/appapi.rst | 7 +++++ doc/theming.rst | 70 +++++++++++++++++++++++++++---------------- sphinx/application.py | 5 ++++ sphinx/theming.py | 22 ++++++++++++-- 5 files changed, 78 insertions(+), 28 deletions(-) diff --git a/CHANGES b/CHANGES index 99e5e2285..f1d7eb741 100644 --- a/CHANGES +++ b/CHANGES @@ -162,6 +162,8 @@ Deprecated removed at 1.7, and already its default value has changed from ``True`` to ``False``. * #3221: epub2 builder is deprecated +* #3628: ``sphinx_themes`` entry_point is deprecated. Please use + ``sphinx.html_themes`` instead. Release 1.5.6 (in development) ============================== diff --git a/doc/extdev/appapi.rst b/doc/extdev/appapi.rst index 50b2af0f8..151bb2ed9 100644 --- a/doc/extdev/appapi.rst +++ b/doc/extdev/appapi.rst @@ -370,6 +370,13 @@ package. .. versionadded:: 1.4 +.. method:: Sphinx.add_html_theme(name, theme_path) + + Register a HTML Theme. The *name* is a name of theme, and *path* is a + full path to the theme (refs: :ref:`distribute-your-theme`). + + .. versionadded:: 1.6 + .. method:: Sphinx.add_env_collector(collector) Register an environment collector class (refs: :ref:`collector-api`) diff --git a/doc/theming.rst b/doc/theming.rst index 1d895ec19..1236a85bf 100644 --- a/doc/theming.rst +++ b/doc/theming.rst @@ -46,34 +46,14 @@ file :file:`blue.zip`, you can put it right in the directory containing html_theme = "blue" html_theme_path = ["."] -The third form provides your theme path dynamically to Sphinx if the -``setuptools`` package is installed. You can provide an entry point section -called ``sphinx_themes`` in your setup.py file and write a ``get_path`` function -that has to return the directory with themes in it:: +The third form is a python package. If a theme you want to use is distributed +as a python package, you can use it after installing:: - # 'setup.py' + # installing theme package + $ pip install sphinxjp.themes.dotted - setup( - ... - entry_points = { - 'sphinx_themes': [ - 'path = your_package:get_path', - ] - }, - ... - ) - - # 'your_package.py' - - from os import path - package_dir = path.abspath(path.dirname(__file__)) - template_path = path.join(package_dir, 'themes') - - def get_path(): - return template_path - -.. versionadded:: 1.2 - 'sphinx_themes' entry_points feature. + # use it in your conf.py + html_theme = "dotted" .. _builtin-themes: @@ -310,6 +290,44 @@ Python :mod:`ConfigParser` module) and has the following structure: and are accessible from all templates as ``theme_``. +.. _distribute-your-theme: + +Distribute your theme as a python package +----------------------------------------- + +As a way to distribute your theme, you can use python package. Python package +brings to users easy setting up ways. + +To distribute your theme as a python package, please define an entry point +called ``sphinx.html_themes`` in your setup.py file, and write a ``setup()`` +function to register your themes in it:: + + # 'setup.py' + setup( + ... + entry_points = { + 'sphinx.html_themes': [ + 'name_of_theme = your_package', + ] + }, + ... + ) + + # 'your_package.py' + from os import path + + def setup(app): + app.add_html_theme('name_of_theme', path.abspath(path.dirname(__file__))) + +.. versionadded:: 1.2 + 'sphinx_themes' entry_points feature. + +.. deprecated:: 1.6 + ``sphinx_themes`` entry_points has been deprecated. + +.. versionadded:: 1.6 + ``sphinx.html_themes`` entry_points feature. + Templating ~~~~~~~~~~ diff --git a/sphinx/application.py b/sphinx/application.py index 26126fb9c..edac70a99 100644 --- a/sphinx/application.py +++ b/sphinx/application.py @@ -800,6 +800,11 @@ class Sphinx(object): logger.debug('[app] adding environment collector: %r', collector) collector().enable(self) + def add_html_theme(self, name, theme_path): + # type: (unicode, unicode) -> None + logger.debug('[app] adding HTML theme: %r, %r', name, theme_path) + self.html_themes[name] = theme_path + class TemplateBridge(object): """ diff --git a/sphinx/theming.py b/sphinx/theming.py index f0c7667d3..5e023e5a1 100644 --- a/sphinx/theming.py +++ b/sphinx/theming.py @@ -12,6 +12,7 @@ import os import shutil import tempfile +import warnings from os import path from zipfile import ZipFile @@ -20,7 +21,9 @@ from six import string_types, iteritems from six.moves import configparser from sphinx import package_dir +from sphinx.deprecation import RemovedInSphinx20Warning from sphinx.errors import ThemeError +from sphinx.extension import load_extension from sphinx.locale import _ from sphinx.util import logging from sphinx.util.osutil import ensuredir @@ -77,6 +80,8 @@ class Theme(object): try: inherit = self.config.get('theme', 'inherit') + except configparser.NoSectionError: + raise ThemeError(_('theme %r doesn\'t have "theme" setting') % name) except configparser.NoOptionError: raise ThemeError(_('theme %r doesn\'t have "inherit" setting') % name) @@ -161,7 +166,7 @@ class HTMLThemeFactory(object): def __init__(self, app): # type: (Sphinx) -> None - self.confdir = app.confdir + self.app = app self.themes = app.html_themes self.load_builtin_themes() if getattr(app.config, 'html_theme_path', None): @@ -178,7 +183,7 @@ class HTMLThemeFactory(object): # 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)) + abs_theme_path = path.abspath(path.join(self.app.confdir, theme_path)) themes = self.find_themes(abs_theme_path) for name, theme in iteritems(themes): self.themes[name] = theme @@ -215,6 +220,16 @@ class HTMLThemeFactory(object): Sphinx refers to ``sphinx_themes`` entry_points. """ + # look up for new styled entry_points at first + entry_points = pkg_resources.iter_entry_points('sphinx.html_themes', name) + try: + entry_point = next(entry_points) + load_extension(self.app, entry_point.module_name) + return + except StopIteration: + pass + + # look up for old styled entry_points for entry_point in pkg_resources.iter_entry_points('sphinx_themes'): target = entry_point.load() if callable(target): @@ -228,6 +243,9 @@ class HTMLThemeFactory(object): themes = self.find_themes(themedir) for entry, theme in iteritems(themes): if name == entry: + warnings.warn('``sphinx_themes`` entry point is now deprecated. ' + 'Please use ``sphinx.html_themes`` instead.', + RemovedInSphinx20Warning) self.themes[name] = theme def find_themes(self, theme_path): From 3cf1883356502499de7ba26ca3d4c11d3415fb91 Mon Sep 17 00:00:00 2001 From: Takeshi KOMIYA Date: Sat, 22 Apr 2017 18:14:06 +0900 Subject: [PATCH 06/23] tests: compile by LaTeX only with py36 --- .travis.yml | 2 +- tests/test_build_latex.py | 9 +++++++++ 2 files changed, 10 insertions(+), 1 deletion(-) diff --git a/.travis.yml b/.travis.yml index 3557095dd..fc3cac836 100644 --- a/.travis.yml +++ b/.travis.yml @@ -51,4 +51,4 @@ install: script: - flake8 - if [[ $TRAVIS_PYTHON_VERSION == '3.6' ]]; then make style-check type-check test-async; fi - - if [[ $TRAVIS_PYTHON_VERSION != '3.6' ]]; then make test; fi + - if [[ $TRAVIS_PYTHON_VERSION != '3.6' ]]; then SKIP_LATEX_BUILD=1 make test; fi diff --git a/tests/test_build_latex.py b/tests/test_build_latex.py index c83b9c5f8..ad882758c 100644 --- a/tests/test_build_latex.py +++ b/tests/test_build_latex.py @@ -87,6 +87,14 @@ def compile_latex_document(app): app.config.latex_engine, p.returncode) +def skip_if_requested(testfunc): + if 'SKIP_LATEX_BUILD' in os.environ: + msg = 'Skip LaTeX builds because SKIP_LATEX_BUILD is set' + return skip_if(True, msg)(testfunc) + else: + return testfunc + + def skip_if_stylefiles_notfound(testfunc): if kpsetest(*STYLEFILES) is False: msg = 'not running latex, the required styles do not seem to be installed' @@ -95,6 +103,7 @@ def skip_if_stylefiles_notfound(testfunc): return testfunc +@skip_if_requested @skip_if_stylefiles_notfound @pytest.mark.parametrize( "engine,docclass", From feaf2a793b366a05a140ce0b406cd964a11fdad9 Mon Sep 17 00:00:00 2001 From: Takeshi KOMIYA Date: Thu, 9 Mar 2017 00:21:10 +0900 Subject: [PATCH 07/23] Add SphinxFactory class to simplify application class --- sphinx/application.py | 40 ++++++++--------------------- sphinx/factory.py | 59 +++++++++++++++++++++++++++++++++++++++++++ 2 files changed, 70 insertions(+), 29 deletions(-) create mode 100644 sphinx/factory.py diff --git a/sphinx/application.py b/sphinx/application.py index 26126fb9c..256f9108a 100644 --- a/sphinx/application.py +++ b/sphinx/application.py @@ -26,19 +26,18 @@ from six.moves import cStringIO from docutils import nodes from docutils.parsers.rst import convert_directive_function, \ directives, roles -from pkg_resources import iter_entry_points import sphinx from sphinx import package_dir, locale from sphinx.config import Config -from sphinx.errors import SphinxError, ExtensionError, VersionRequirementError, \ - ConfigError +from sphinx.errors import ConfigError, ExtensionError, VersionRequirementError from sphinx.domains import ObjType from sphinx.domains.std import GenericObject, Target, StandardDomain from sphinx.deprecation import RemovedInSphinx17Warning, RemovedInSphinx20Warning from sphinx.environment import BuildEnvironment from sphinx.events import EventManager from sphinx.extension import load_extension, verify_required_extensions +from sphinx.factory import SphinxFactory from sphinx.io import SphinxStandaloneReader from sphinx.locale import _ from sphinx.roles import XRefRole @@ -124,9 +123,9 @@ class Sphinx(object): self._additional_source_parsers = {} # type: Dict[unicode, Parser] self._setting_up_extension = ['?'] # type: List[unicode] self.domains = {} # type: Dict[unicode, Type[Domain]] - self.builderclasses = {} # type: Dict[unicode, Type[Builder]] self.builder = None # type: Builder self.env = None # type: BuildEnvironment + self.factory = SphinxFactory() self.enumerable_nodes = {} # type: Dict[nodes.Node, Tuple[unicode, Callable]] # NOQA self.post_transforms = [] # type: List[Transform] self.html_themes = {} # type: Dict[unicode, unicode] @@ -301,28 +300,17 @@ class Sphinx(object): logger.info(_('failed: %s'), err) self._init_env(freshenv=True) - def preload_builder(self, buildername): + def preload_builder(self, name): # type: (unicode) -> None - if buildername is None: - return + self.factory.preload_builder(self, name) - if buildername not in self.builderclasses: - entry_points = iter_entry_points('sphinx.builders', buildername) - try: - entry_point = next(entry_points) - except StopIteration: - raise SphinxError(_('Builder name %s not registered or available' - ' through entry point') % buildername) - load_extension(self, entry_point.module_name) - - def create_builder(self, buildername): + def create_builder(self, name): # type: (unicode) -> Builder - if buildername is None: - buildername = 'html' - if buildername not in self.builderclasses: - raise SphinxError(_('Builder name %s not registered') % buildername) + if name is None: + logger.info(_('No builder selected, using default: html')) + name = 'html' - return self.builderclasses[buildername](self) + return self.factory.create_builder(self, name) def _init_builder(self): # type: () -> None @@ -516,13 +504,7 @@ class Sphinx(object): def add_builder(self, builder): # type: (Type[Builder]) -> None logger.debug('[app] adding builder: %r', builder) - if not hasattr(builder, 'name'): - raise ExtensionError(_('Builder class %s has no "name" attribute') - % builder) - if builder.name in self.builderclasses: - raise ExtensionError(_('Builder %r already exists (in module %s)') % - (builder.name, self.builderclasses[builder.name].__module__)) - self.builderclasses[builder.name] = builder + self.factory.add_builder(builder) def add_config_value(self, name, default, rebuild, types=()): # type: (unicode, Any, Union[bool, unicode], Any) -> None diff --git a/sphinx/factory.py b/sphinx/factory.py new file mode 100644 index 000000000..146ea0b40 --- /dev/null +++ b/sphinx/factory.py @@ -0,0 +1,59 @@ +# -*- coding: utf-8 -*- +""" + sphinx.factory + ~~~~~~~~~~~~~~ + + Sphinx component factory. + + Gracefully adapted from the TextPress system by Armin. + + :copyright: Copyright 2007-2016 by the Sphinx team, see AUTHORS. + :license: BSD, see LICENSE for details. +""" +from __future__ import print_function +from pkg_resources import iter_entry_points + +from sphinx.errors import ExtensionError, SphinxError +from sphinx.extension import load_extension +from sphinx.locale import _ + +if False: + # For type annotation + from typing import Dict, Type # NOQA + from sphinx.application import Sphinx # NOQA + from sphinx.builders import Builder # NOQA + + +class SphinxFactory(object): + def __init__(self): + self.builders = {} # type: Dict[unicode, Type[Builder]] + + def add_builder(self, builder): + # type: (Type[Builder]) -> None + if not hasattr(builder, 'name'): + raise ExtensionError(_('Builder class %s has no "name" attribute') % builder) + if builder.name in self.builders: + raise ExtensionError(_('Builder %r already exists (in module %s)') % + (builder.name, self.builders[builder.name].__module__)) + self.builders[builder.name] = builder + + def preload_builder(self, app, name): + # type: (Sphinx, unicode) -> None + if name is None: + return + + if name not in self.builders: + entry_points = iter_entry_points('sphinx.builders', name) + try: + entry_point = next(entry_points) + except StopIteration: + raise SphinxError(_('Builder name %s not registered or available' + ' through entry point') % name) + load_extension(app, entry_point.module_name) + + def create_builder(self, app, name): + # type: (Sphinx, unicode) -> Builder + if name not in self.builders: + raise SphinxError(_('Builder name %s not registered') % name) + + return self.builders[name](app) From 3cc9d8c25029e8f7e0a993280655ec3f2f8bc8c9 Mon Sep 17 00:00:00 2001 From: Takeshi KOMIYA Date: Thu, 9 Mar 2017 18:30:40 +0900 Subject: [PATCH 08/23] Move Sphinx._directive_helper() to sphinx.util.docutils --- CHANGES | 2 ++ sphinx/application.py | 38 +++++++++++++++----------------------- sphinx/util/docutils.py | 19 ++++++++++++++++++- 3 files changed, 35 insertions(+), 24 deletions(-) diff --git a/CHANGES b/CHANGES index 99e5e2285..fb4af9601 100644 --- a/CHANGES +++ b/CHANGES @@ -146,6 +146,8 @@ Deprecated instead (as Sphinx does since 1.5.) * ``Sphinx.status_iterator()`` and ``Sphinx.old_status_iterator()`` is now deprecated. Please use ``sphinx.util:status_iterator()`` intead. +* ``Sphinx._directive_helper()`` is deprecated. Please use + ``sphinx.util.docutils.directive_helper()`` instead. * ``BuildEnvironment.set_warnfunc()`` is now deprecated * Following methods of ``BuildEnvironment`` is now deprecated. diff --git a/sphinx/application.py b/sphinx/application.py index 256f9108a..916871773 100644 --- a/sphinx/application.py +++ b/sphinx/application.py @@ -14,7 +14,6 @@ from __future__ import print_function import os import sys -import types import warnings import posixpath from os import path @@ -24,8 +23,7 @@ from six import iteritems from six.moves import cStringIO from docutils import nodes -from docutils.parsers.rst import convert_directive_function, \ - directives, roles +from docutils.parsers.rst import directives, roles import sphinx from sphinx import package_dir, locale @@ -48,7 +46,7 @@ from sphinx.util import status_iterator, old_status_iterator, display_chunk from sphinx.util.tags import Tags from sphinx.util.osutil import ENOENT from sphinx.util.console import bold, darkgreen # type: ignore -from sphinx.util.docutils import is_html5_writer_available +from sphinx.util.docutils import is_html5_writer_available, directive_helper from sphinx.util.i18n import find_catalog_source_files if False: @@ -575,21 +573,15 @@ class Sphinx(object): self.enumerable_nodes[node] = (figtype, title_getter) self.add_node(node, **kwds) - def _directive_helper(self, obj, content=None, arguments=None, **options): - # type: (Any, unicode, Any, Any) -> Any - if isinstance(obj, (types.FunctionType, types.MethodType)): - obj.content = content # type: ignore - obj.arguments = arguments or (0, 0, False) # type: ignore - obj.options = options # type: ignore - return convert_directive_function(obj) - else: - if content or arguments or options: - raise ExtensionError(_('when adding directive classes, no ' - 'additional arguments may be given')) - return obj + def _directive_helper(self, obj, has_content=None, argument_spec=None, **option_spec): + # type: (Any, bool, Tuple[int, int, bool], Any) -> Any + warnings.warn('_directive_helper() is now deprecated. ' + 'Please use sphinx.util.docutils.directive_helper() instead.', + RemovedInSphinx17Warning) + return directive_helper(obj, has_content, argument_spec, **option_spec) def add_directive(self, name, obj, content=None, arguments=None, **options): - # type: (unicode, Any, unicode, Any, Any) -> None + # type: (unicode, Any, bool, Tuple[int, int, bool], Any) -> None logger.debug('[app] adding directive: %r', (name, obj, content, arguments, options)) if name in directives._directives: @@ -597,8 +589,8 @@ class Sphinx(object): 'already registered, it will be overridden'), self._setting_up_extension[-1], name, type='app', subtype='add_directive') - directives.register_directive( - name, self._directive_helper(obj, content, arguments, **options)) + directive = directive_helper(obj, content, arguments, **options) + directives.register_directive(name, directive) def add_role(self, name, role): # type: (unicode, Any) -> None @@ -641,14 +633,14 @@ class Sphinx(object): self.domains[domain.name] = domain def add_directive_to_domain(self, domain, name, obj, - content=None, arguments=None, **options): - # type: (unicode, unicode, Any, unicode, Any, Any) -> None + has_content=None, argument_spec=None, **option_spec): + # type: (unicode, unicode, Any, bool, Any, Any) -> None logger.debug('[app] adding directive to domain: %r', - (domain, name, obj, content, arguments, options)) + (domain, name, obj, has_content, argument_spec, option_spec)) if domain not in self.domains: raise ExtensionError(_('domain %s not yet registered') % domain) self.domains[domain].directives[name] = \ - self._directive_helper(obj, content, arguments, **options) + directive_helper(obj, has_content, argument_spec, **option_spec) def add_role_to_domain(self, domain, name, role): # type: (unicode, unicode, Any) -> None diff --git a/sphinx/util/docutils.py b/sphinx/util/docutils.py index 4438f9123..d6b9987e3 100644 --- a/sphinx/util/docutils.py +++ b/sphinx/util/docutils.py @@ -11,13 +11,16 @@ from __future__ import absolute_import import re +import types from copy import copy from contextlib import contextmanager import docutils from docutils.utils import Reporter -from docutils.parsers.rst import directives, roles +from docutils.parsers.rst import directives, roles, convert_directive_function +from sphinx.errors import ExtensionError +from sphinx.locale import _ from sphinx.util import logging logger = logging.getLogger(__name__) @@ -154,3 +157,17 @@ class LoggingReporter(Reporter): def is_html5_writer_available(): # type: () -> bool return __version_info__ > (0, 13, 0) + + +def directive_helper(obj, has_content=None, argument_spec=None, **option_spec): + # type: (Any, bool, Tuple[int, int, bool], Any) -> Any + if isinstance(obj, (types.FunctionType, types.MethodType)): + obj.content = has_content # type: ignore + obj.arguments = argument_spec or (0, 0, False) # type: ignore + obj.options = option_spec # type: ignore + return convert_directive_function(obj) + else: + if has_content or argument_spec or option_spec: + raise ExtensionError(_('when adding directive classes, no ' + 'additional arguments may be given')) + return obj From 8ca9bdfbd41cc547ccacbd6a97ea66c6cf4d4cea Mon Sep 17 00:00:00 2001 From: Takeshi KOMIYA Date: Thu, 9 Mar 2017 19:25:42 +0900 Subject: [PATCH 09/23] Move domain class manager to SphinxFactory --- sphinx/application.py | 76 +++++++++++++------------------------- sphinx/factory.py | 85 ++++++++++++++++++++++++++++++++++++++++++- 2 files changed, 110 insertions(+), 51 deletions(-) diff --git a/sphinx/application.py b/sphinx/application.py index 916871773..8a10dedac 100644 --- a/sphinx/application.py +++ b/sphinx/application.py @@ -29,8 +29,6 @@ import sphinx from sphinx import package_dir, locale from sphinx.config import Config from sphinx.errors import ConfigError, ExtensionError, VersionRequirementError -from sphinx.domains import ObjType -from sphinx.domains.std import GenericObject, Target, StandardDomain from sphinx.deprecation import RemovedInSphinx17Warning, RemovedInSphinx20Warning from sphinx.environment import BuildEnvironment from sphinx.events import EventManager @@ -38,7 +36,6 @@ from sphinx.extension import load_extension, verify_required_extensions from sphinx.factory import SphinxFactory from sphinx.io import SphinxStandaloneReader from sphinx.locale import _ -from sphinx.roles import XRefRole from sphinx.util import pycompat # noqa: F401 from sphinx.util import import_object from sphinx.util import logging @@ -120,7 +117,6 @@ class Sphinx(object): self.extensions = {} # type: Dict[unicode, Extension] self._additional_source_parsers = {} # type: Dict[unicode, Parser] self._setting_up_extension = ['?'] # type: List[unicode] - self.domains = {} # type: Dict[unicode, Type[Domain]] self.builder = None # type: Builder self.env = None # type: BuildEnvironment self.factory = SphinxFactory() @@ -223,9 +219,9 @@ class Sphinx(object): verify_required_extensions(self, self.config.needs_extensions) # check primary_domain if requested - if self.config.primary_domain and self.config.primary_domain not in self.domains: - logger.warning(_('primary_domain %r not found, ignored.'), - self.config.primary_domain) + primary_domain = self.config.primary_domain + if primary_domain and not self.factory.has_domain(primary_domain): + logger.warning(_('primary_domain %r not found, ignored.'), primary_domain) # create the builder self.builder = self.create_builder(buildername) @@ -279,17 +275,17 @@ class Sphinx(object): if freshenv: self.env = BuildEnvironment(self) self.env.find_files(self.config, self.builder) - for domain in self.domains.keys(): - self.env.domains[domain] = self.domains[domain](self.env) + for domain in self.factory.create_domains(self.env): + self.env.domains[domain.name] = domain else: try: logger.info(bold(_('loading pickled environment... ')), nonl=True) filename = path.join(self.doctreedir, ENV_PICKLE_FILENAME) self.env = BuildEnvironment.frompickle(filename, self) self.env.domains = {} - for domain in self.domains.keys(): + for domain in self.factory.create_domains(self.env): # this can raise if the data version doesn't fit - self.env.domains[domain] = self.domains[domain](self.env) + self.env.domains[domain.name] = domain logger.info(_('done')) except Exception as err: if isinstance(err, IOError) and err.errno == ENOENT: @@ -618,43 +614,30 @@ class Sphinx(object): def add_domain(self, domain): # type: (Type[Domain]) -> None logger.debug('[app] adding domain: %r', domain) - if domain.name in self.domains: - raise ExtensionError(_('domain %s already registered') % domain.name) - self.domains[domain.name] = domain + self.factory.add_domain(domain) def override_domain(self, domain): # type: (Type[Domain]) -> None logger.debug('[app] overriding domain: %r', domain) - if domain.name not in self.domains: - raise ExtensionError(_('domain %s not yet registered') % domain.name) - if not issubclass(domain, self.domains[domain.name]): - raise ExtensionError(_('new domain not a subclass of registered %s ' - 'domain') % domain.name) - self.domains[domain.name] = domain + self.factory.override_domain(domain) def add_directive_to_domain(self, domain, name, obj, has_content=None, argument_spec=None, **option_spec): # type: (unicode, unicode, Any, bool, Any, Any) -> None logger.debug('[app] adding directive to domain: %r', (domain, name, obj, has_content, argument_spec, option_spec)) - if domain not in self.domains: - raise ExtensionError(_('domain %s not yet registered') % domain) - self.domains[domain].directives[name] = \ - directive_helper(obj, has_content, argument_spec, **option_spec) + self.factory.add_directive_to_domain(domain, name, obj, + has_content, argument_spec, **option_spec) def add_role_to_domain(self, domain, name, role): # type: (unicode, unicode, Any) -> None logger.debug('[app] adding role to domain: %r', (domain, name, role)) - if domain not in self.domains: - raise ExtensionError(_('domain %s not yet registered') % domain) - self.domains[domain].roles[name] = role + self.factory.add_role_to_domain(domain, name, role) def add_index_to_domain(self, domain, index): # type: (unicode, Type[Index]) -> None logger.debug('[app] adding index to domain: %r', (domain, index)) - if domain not in self.domains: - raise ExtensionError(_('domain %s not yet registered') % domain) - self.domains[domain].indices.append(index) + self.factory.add_index_to_domain(domain, index) def add_object_type(self, directivename, rolename, indextemplate='', parse_node=None, ref_nodeclass=None, objname='', @@ -663,19 +646,18 @@ class Sphinx(object): logger.debug('[app] adding object type: %r', (directivename, rolename, indextemplate, parse_node, ref_nodeclass, objname, doc_field_types)) - StandardDomain.object_types[directivename] = \ - ObjType(objname or directivename, rolename) - # create a subclass of GenericObject as the new directive - new_directive = type(directivename, (GenericObject, object), # type: ignore - {'indextemplate': indextemplate, - 'parse_node': staticmethod(parse_node), # type: ignore - 'doc_field_types': doc_field_types}) - StandardDomain.directives[directivename] = new_directive - # XXX support more options? - StandardDomain.roles[rolename] = XRefRole(innernodeclass=ref_nodeclass) + self.factory.add_object_type(directivename, rolename, indextemplate, parse_node, + ref_nodeclass, objname, doc_field_types) - # backwards compatible alias - add_description_unit = add_object_type + def add_description_unit(self, directivename, rolename, indextemplate='', + parse_node=None, ref_nodeclass=None, objname='', + doc_field_types=[]): + # type: (unicode, unicode, unicode, Callable, nodes.Node, unicode, List) -> None + warnings.warn('app.add_description_unit() is now deprecated. ' + 'Use app.add_object_type() instead.', + RemovedInSphinx20Warning) + self.add_object_type(directivename, rolename, indextemplate, parse_node, + ref_nodeclass, objname, doc_field_types) def add_crossref_type(self, directivename, rolename, indextemplate='', ref_nodeclass=None, objname=''): @@ -683,14 +665,8 @@ class Sphinx(object): logger.debug('[app] adding crossref type: %r', (directivename, rolename, indextemplate, ref_nodeclass, objname)) - StandardDomain.object_types[directivename] = \ - ObjType(objname or directivename, rolename) - # create a subclass of Target as the new directive - new_directive = type(directivename, (Target, object), # type: ignore - {'indextemplate': indextemplate}) - StandardDomain.directives[directivename] = new_directive - # XXX support more options? - StandardDomain.roles[rolename] = XRefRole(innernodeclass=ref_nodeclass) + self.factory.add_crossref_type(directivename, rolename, + indextemplate, ref_nodeclass, objname) def add_transform(self, transform): # type: (Type[Transform]) -> None diff --git a/sphinx/factory.py b/sphinx/factory.py index 146ea0b40..2d58c03f9 100644 --- a/sphinx/factory.py +++ b/sphinx/factory.py @@ -11,22 +11,32 @@ :license: BSD, see LICENSE for details. """ from __future__ import print_function + from pkg_resources import iter_entry_points +from six import itervalues from sphinx.errors import ExtensionError, SphinxError +from sphinx.domains import ObjType +from sphinx.domains.std import GenericObject, Target from sphinx.extension import load_extension from sphinx.locale import _ +from sphinx.roles import XRefRole +from sphinx.util.docutils import directive_helper if False: # For type annotation - from typing import Dict, Type # NOQA + from typing import Any, Callable, Dict, Iterator, List, Type # NOQA + from docutils import nodes # NOQA from sphinx.application import Sphinx # NOQA from sphinx.builders import Builder # NOQA + from sphinx.domains import Domain, Index # NOQA + from sphinx.environment import BuildEnvironment # NOQA class SphinxFactory(object): def __init__(self): self.builders = {} # type: Dict[unicode, Type[Builder]] + self.domains = {} # type: Dict[unicode, Type[Domain]] def add_builder(self, builder): # type: (Type[Builder]) -> None @@ -57,3 +67,76 @@ class SphinxFactory(object): raise SphinxError(_('Builder name %s not registered') % name) return self.builders[name](app) + + def add_domain(self, domain): + # type: (Type[Domain]) -> None + if domain.name in self.domains: + raise ExtensionError(_('domain %s already registered') % domain.name) + self.domains[domain.name] = domain + + def has_domain(self, domain): + # type: (unicode) -> bool + return domain in self.domains + + def create_domains(self, env): + # type: (BuildEnvironment) -> Iterator[Domain] + for DomainClass in itervalues(self.domains): + yield DomainClass(env) + + def override_domain(self, domain): + # type: (Type[Domain]) -> None + if domain.name not in self.domains: + raise ExtensionError(_('domain %s not yet registered') % domain.name) + if not issubclass(domain, self.domains[domain.name]): + raise ExtensionError(_('new domain not a subclass of registered %s ' + 'domain') % domain.name) + self.domains[domain.name] = domain + + def add_directive_to_domain(self, domain, name, obj, + has_content=None, argument_spec=None, **option_spec): + # type: (unicode, unicode, Any, bool, Any, Any) -> None + if domain not in self.domains: + raise ExtensionError(_('domain %s not yet registered') % domain) + directive = directive_helper(obj, has_content, argument_spec, **option_spec) + self.domains[domain].directives[name] = directive + + def add_role_to_domain(self, domain, name, role): + # type: (unicode, unicode, Any) -> None + if domain not in self.domains: + raise ExtensionError(_('domain %s not yet registered') % domain) + self.domains[domain].roles[name] = role + + def add_index_to_domain(self, domain, index): + # type: (unicode, Type[Index]) -> None + if domain not in self.domains: + raise ExtensionError(_('domain %s not yet registered') % domain) + self.domains[domain].indices.append(index) + + def add_object_type(self, directivename, rolename, indextemplate='', + parse_node=None, ref_nodeclass=None, objname='', + doc_field_types=[]): + # type: (unicode, unicode, unicode, Callable, nodes.Node, unicode, List) -> None + # create a subclass of GenericObject as the new directive + directive = type(directivename, # type: ignore + (GenericObject, object), + {'indextemplate': indextemplate, + 'parse_node': staticmethod(parse_node), # type: ignore + 'doc_field_types': doc_field_types}) + + stddomain = self.domains['std'] + stddomain.directives[directivename] = directive + stddomain.roles[rolename] = XRefRole(innernodeclass=ref_nodeclass) + stddomain.object_types[directivename] = ObjType(objname or directivename, rolename) + + def add_crossref_type(self, directivename, rolename, indextemplate='', + ref_nodeclass=None, objname=''): + # type: (unicode, unicode, unicode, nodes.Node, unicode) -> None + # create a subclass of Target as the new directive + directive = type(directivename, # type: ignore + (Target, object), + {'indextemplate': indextemplate}) + + stddomain = self.domains['std'] + stddomain.directives[directivename] = directive + stddomain.roles[rolename] = XRefRole(innernodeclass=ref_nodeclass) + stddomain.object_types[directivename] = ObjType(objname or directivename, rolename) From f346e7dc1d2a96060e41fa9927b9638d21a3e3b7 Mon Sep 17 00:00:00 2001 From: Takeshi KOMIYA Date: Thu, 9 Mar 2017 20:26:46 +0900 Subject: [PATCH 10/23] Move source_parsers manager to SphinxFactory --- CHANGES | 1 + sphinx/application.py | 14 ++++---------- sphinx/environment/__init__.py | 3 ++- sphinx/ext/autosummary/__init__.py | 2 +- sphinx/factory.py | 16 ++++++++++++++-- sphinx/io.py | 5 ++--- sphinx/transforms/i18n.py | 2 +- tests/test_application.py | 13 +++---------- 8 files changed, 28 insertions(+), 28 deletions(-) diff --git a/CHANGES b/CHANGES index fb4af9601..8551d08e7 100644 --- a/CHANGES +++ b/CHANGES @@ -45,6 +45,7 @@ Incompatible changes * ``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. +* ``Sphinx.add_source_parser()`` raises an error if duplicated Features removed ---------------- diff --git a/sphinx/application.py b/sphinx/application.py index 8a10dedac..33094e21d 100644 --- a/sphinx/application.py +++ b/sphinx/application.py @@ -115,7 +115,6 @@ class Sphinx(object): # type: (unicode, unicode, unicode, unicode, unicode, Dict, IO, IO, bool, bool, List[unicode], int, int) -> None # NOQA self.verbosity = verbosity self.extensions = {} # type: Dict[unicode, Extension] - self._additional_source_parsers = {} # type: Dict[unicode, Parser] self._setting_up_extension = ['?'] # type: List[unicode] self.builder = None # type: Builder self.env = None # type: BuildEnvironment @@ -264,11 +263,11 @@ class Sphinx(object): def _init_source_parsers(self): # type: () -> None - for suffix, parser in iteritems(self._additional_source_parsers): + for suffix, parser in iteritems(self.config.source_parsers): + self.add_source_parser(suffix, parser) + for suffix, parser in iteritems(self.factory.get_source_parsers()): if suffix not in self.config.source_suffix: self.config.source_suffix.append(suffix) - if suffix not in self.config.source_parsers: - self.config.source_parsers[suffix] = parser def _init_env(self, freshenv): # type: (bool) -> None @@ -738,12 +737,7 @@ class Sphinx(object): def add_source_parser(self, suffix, parser): # type: (unicode, Parser) -> None logger.debug('[app] adding search source_parser: %r, %r', suffix, parser) - if suffix in self._additional_source_parsers: - logger.warning(_('while setting up extension %s: source_parser for %r is ' - 'already registered, it will be overridden'), - self._setting_up_extension[-1], suffix, - type='app', subtype='add_source_parser') - self._additional_source_parsers[suffix] = parser + self.factory.add_source_parser(suffix, parser) def add_env_collector(self, collector): # type: (Type[EnvironmentCollector]) -> None diff --git a/sphinx/environment/__init__.py b/sphinx/environment/__init__.py index 303704773..69bcae354 100644 --- a/sphinx/environment/__init__.py +++ b/sphinx/environment/__init__.py @@ -691,7 +691,8 @@ class BuildEnvironment(object): codecs.register_error('sphinx', self.warn_and_replace) # type: ignore # publish manually - reader = SphinxStandaloneReader(self.app, parsers=self.config.source_parsers) + reader = SphinxStandaloneReader(self.app, + parsers=self.app.factory.get_source_parsers()) pub = Publisher(reader=reader, writer=SphinxDummyWriter(), destination_class=NullOutput) diff --git a/sphinx/ext/autosummary/__init__.py b/sphinx/ext/autosummary/__init__.py index 7a4a65741..d1a979443 100644 --- a/sphinx/ext/autosummary/__init__.py +++ b/sphinx/ext/autosummary/__init__.py @@ -573,7 +573,7 @@ def get_rst_suffix(app): # type: (Sphinx) -> unicode def get_supported_format(suffix): # type: (unicode) -> Tuple[unicode] - parser_class = app.config.source_parsers.get(suffix) + parser_class = app.factory.get_source_parsers().get(suffix) if parser_class is None: return ('restructuredtext',) if isinstance(parser_class, string_types): diff --git a/sphinx/factory.py b/sphinx/factory.py index 2d58c03f9..a08e047db 100644 --- a/sphinx/factory.py +++ b/sphinx/factory.py @@ -27,6 +27,7 @@ if False: # For type annotation from typing import Any, Callable, Dict, Iterator, List, Type # NOQA from docutils import nodes # NOQA + from docutils.parsers import Parser # NOQA from sphinx.application import Sphinx # NOQA from sphinx.builders import Builder # NOQA from sphinx.domains import Domain, Index # NOQA @@ -35,8 +36,9 @@ if False: class SphinxFactory(object): def __init__(self): - self.builders = {} # type: Dict[unicode, Type[Builder]] - self.domains = {} # type: Dict[unicode, Type[Domain]] + self.builders = {} # type: Dict[unicode, Type[Builder]] + self.domains = {} # type: Dict[unicode, Type[Domain]] + self.source_parsers = {} # type: Dict[unicode, Parser] def add_builder(self, builder): # type: (Type[Builder]) -> None @@ -140,3 +142,13 @@ class SphinxFactory(object): stddomain.directives[directivename] = directive stddomain.roles[rolename] = XRefRole(innernodeclass=ref_nodeclass) stddomain.object_types[directivename] = ObjType(objname or directivename, rolename) + + def add_source_parser(self, suffix, parser): + # type: (unicode, Parser) -> None + if suffix in self.source_parsers: + raise ExtensionError(_('source_parser for %r is already registered') % suffix) + self.source_parsers[suffix] = parser + + def get_source_parsers(self): + # type: () -> Dict[unicode, Parser] + return self.source_parsers diff --git a/sphinx/io.py b/sphinx/io.py index b7ae5dc36..c9c9d085a 100644 --- a/sphinx/io.py +++ b/sphinx/io.py @@ -11,7 +11,7 @@ from docutils.io import FileInput from docutils.readers import standalone from docutils.writers import UnfilteredWriter -from six import string_types, text_type +from six import string_types, text_type, iteritems from typing import Any, Union # NOQA from sphinx.transforms import ( @@ -158,9 +158,8 @@ class SphinxFileInput(FileInput): # type: () -> unicode def get_parser_type(source_path): # type: (unicode) -> Tuple[unicode] - for suffix in self.env.config.source_parsers: + for suffix, parser_class in iteritems(self.app.factory.get_source_parsers()): if source_path.endswith(suffix): - parser_class = self.env.config.source_parsers[suffix] if isinstance(parser_class, string_types): parser_class = import_object(parser_class, 'source parser') # type: ignore # NOQA return parser_class.supported diff --git a/sphinx/transforms/i18n.py b/sphinx/transforms/i18n.py index d5ee6927d..be1c80bbd 100644 --- a/sphinx/transforms/i18n.py +++ b/sphinx/transforms/i18n.py @@ -52,7 +52,7 @@ def publish_msgstr(app, source, source_path, source_line, config, settings): from sphinx.io import SphinxI18nReader reader = SphinxI18nReader( app=app, - parsers=config.source_parsers, + parsers=app.factory.get_source_parsers(), parser_name='restructuredtext', # default parser ) reader.set_lineno_for_reporter(source_line) diff --git a/tests/test_application.py b/tests/test_application.py index d285aa03d..84f973353 100644 --- a/tests/test_application.py +++ b/tests/test_application.py @@ -85,13 +85,6 @@ def test_domain_override(app, status, warning): @pytest.mark.sphinx(testroot='add_source_parser') def test_add_source_parser(app, status, warning): assert set(app.config.source_suffix) == set(['.rst', '.md', '.test']) - assert set(app.config.source_parsers.keys()) == set(['.md', '.test']) - assert app.config.source_parsers['.md'].__name__ == 'DummyMarkdownParser' - assert app.config.source_parsers['.test'].__name__ == 'TestSourceParser' - - -@pytest.mark.sphinx(testroot='add_source_parser-conflicts-with-users-setting') -def test_add_source_parser_conflicts_with_users_setting(app, status, warning): - assert set(app.config.source_suffix) == set(['.rst', '.test']) - assert set(app.config.source_parsers.keys()) == set(['.test']) - assert app.config.source_parsers['.test'].__name__ == 'DummyTestParser' + assert set(app.factory.get_source_parsers().keys()) == set(['.md', '.test']) + assert app.factory.get_source_parsers()['.md'].__name__ == 'DummyMarkdownParser' + assert app.factory.get_source_parsers()['.test'].__name__ == 'TestSourceParser' From 78ea36a787a762bb11dcddb00bf7510de43151c3 Mon Sep 17 00:00:00 2001 From: Takeshi KOMIYA Date: Sun, 12 Mar 2017 13:42:12 +0900 Subject: [PATCH 11/23] Move translators to SphinxFactory --- sphinx/application.py | 7 +++--- sphinx/builders/__init__.py | 40 ++++++++++++++++++++++++++++++++--- sphinx/builders/html.py | 29 ++++++++++--------------- sphinx/builders/latex.py | 3 ++- sphinx/builders/manpage.py | 3 ++- sphinx/builders/texinfo.py | 3 ++- sphinx/builders/text.py | 3 ++- sphinx/builders/websupport.py | 6 +----- sphinx/builders/xml.py | 2 ++ sphinx/factory.py | 15 +++++++++++++ sphinx/writers/html.py | 4 ++-- sphinx/writers/latex.py | 4 +--- sphinx/writers/manpage.py | 4 +--- sphinx/writers/texinfo.py | 5 +---- sphinx/writers/text.py | 3 +-- sphinx/writers/xml.py | 3 +-- tests/test_api_translator.py | 24 ++++++++++----------- 17 files changed, 96 insertions(+), 62 deletions(-) diff --git a/sphinx/application.py b/sphinx/application.py index 33094e21d..236fa0516 100644 --- a/sphinx/application.py +++ b/sphinx/application.py @@ -146,7 +146,6 @@ class Sphinx(object): logging.setup(self, self._status, self._warning) self.events = EventManager() - self._translators = {} # type: Dict[unicode, nodes.GenericNodeVisitor] # keep last few messages for traceback # This will be filled by sphinx.util.logging.LastMessagesWriter @@ -515,9 +514,9 @@ class Sphinx(object): self.events.add(name) def set_translator(self, name, translator_class): - # type: (unicode, Any) -> None + # type: (unicode, Type[nodes.NodeVisitor]) -> None logger.info(bold(_('A Translator for the %s builder is changed.') % name)) - self._translators[name] = translator_class + self.factory.add_translator(name, translator_class) def add_node(self, node, **kwds): # type: (nodes.Node, Any) -> None @@ -535,7 +534,7 @@ class Sphinx(object): except ValueError: raise ExtensionError(_('Value for key %r must be a ' '(visit, depart) function tuple') % key) - translator = self._translators.get(key) + translator = self.factory.translators.get(key) translators = [] if translator is not None: translators.append(translator) diff --git a/sphinx/builders/__init__.py b/sphinx/builders/__init__.py index 18f8b0c9c..ab75c5bb8 100644 --- a/sphinx/builders/__init__.py +++ b/sphinx/builders/__init__.py @@ -11,6 +11,7 @@ import os from os import path +import warnings try: import multiprocessing @@ -20,6 +21,7 @@ except ImportError: from six import itervalues from docutils import nodes +from sphinx.deprecation import RemovedInSphinx20Warning from sphinx.util import i18n, path_stabilize, logging, status_iterator from sphinx.util.osutil import SEP, relative_uri from sphinx.util.i18n import find_catalog @@ -53,6 +55,9 @@ class Builder(object): name = '' # type: unicode #: The builder's output format, or '' if no document output is produced. format = '' # type: unicode + # default translator class for the builder. This will be overrided by + # ``app.set_translator()``. + default_translator_class = None # type: nodes.NodeVisitor # doctree versioning method versioning_method = 'none' # type: unicode versioning_compare = False @@ -101,9 +106,6 @@ class Builder(object): self.parallel_ok = False self.finish_tasks = None # type: Any - # load default translator class - self.translator_class = app._translators.get(self.name) - def set_environment(self, env): # type: (BuildEnvironment) -> None """Store BuildEnvironment object.""" @@ -111,6 +113,38 @@ class Builder(object): self.env.set_versioning_method(self.versioning_method, self.versioning_compare) + def get_translator_class(self, *args): + # type: (Any) -> nodes.NodeVisitor + """Return a class of translator.""" + return self.app.factory.get_translator_class(self) + + def create_translator(self, *args): + # type: (Any) -> nodes.NodeVisitor + """Return an instance of translator. + + This method returns an instance of ``default_translator_class`` by default. + Users can replace the translator class with ``app.set_translator()`` API. + """ + translator_class = self.app.factory.get_translator_class(self) + assert translator_class, "translator not found for %s" % self.__class__.__name__ + return translator_class(*args) + + @property + def translator_class(self): + # type: () -> Callable[[Any], nodes.NodeVisitor] + """Return a class of translator. + + .. deprecated:: 1.6 + """ + translator_class = self.app.factory.get_translator_class(self) + if translator_class is None and self.default_translator_class is None: + warnings.warn('builder.translator_class() is now deprecated. ' + 'Please use builder.create_translator() and ' + 'builder.default_translator_class instead.', + RemovedInSphinx20Warning) + return None + return self.create_translator + # helper methods def init(self): # type: () -> None diff --git a/sphinx/builders/html.py b/sphinx/builders/html.py index c5739e896..7369ea23d 100644 --- a/sphinx/builders/html.py +++ b/sphinx/builders/html.py @@ -158,7 +158,6 @@ class StandaloneHTMLBuilder(Builder): self.init_templates() self.init_highlighter() - self.init_translator_class() if self.config.html_file_suffix is not None: self.out_suffix = self.config.html_file_suffix @@ -218,23 +217,18 @@ class StandaloneHTMLBuilder(Builder): self.highlighter = PygmentsBridge('html', style, self.config.trim_doctest_flags) - def init_translator_class(self): - # type: () -> None - if self.translator_class is None: - 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: - self.translator_class = HTML5Translator + @property + def default_translator_class(self): + if self.config.html_experimental_html5_writer and html5_ready: + if self.config.html_use_smartypants: + return SmartyPantsHTML5Translator else: - if self.config.html_use_smartypants: - self.translator_class = SmartyPantsHTMLTranslator - else: - self.translator_class = HTMLTranslator + return HTML5Translator + else: + if self.config.html_use_smartypants: + return SmartyPantsHTMLTranslator + else: + return HTMLTranslator def get_outdated_docs(self): # type: () -> Iterator[unicode] @@ -1200,7 +1194,6 @@ class SerializingHTMLBuilder(StandaloneHTMLBuilder): self.current_docname = None self.theme = None # no theme necessary self.templates = None # no template bridge necessary - self.init_translator_class() self.init_templates() self.init_highlighter() self.use_index = self.get_builder_config('use_index', 'html') diff --git a/sphinx/builders/latex.py b/sphinx/builders/latex.py index ac061e08e..5b4150d1d 100644 --- a/sphinx/builders/latex.py +++ b/sphinx/builders/latex.py @@ -31,7 +31,7 @@ from sphinx.util.nodes import inline_all_toctrees from sphinx.util.fileutil import copy_asset_file from sphinx.util.osutil import SEP, make_filename from sphinx.util.console import bold, darkgreen # type: ignore -from sphinx.writers.latex import LaTeXWriter +from sphinx.writers.latex import LaTeXWriter, LaTeXTranslator if False: # For type annotation @@ -51,6 +51,7 @@ class LaTeXBuilder(Builder): format = 'latex' supported_image_types = ['application/pdf', 'image/png', 'image/jpeg'] supported_remote_images = False + default_translator_class = LaTeXTranslator def init(self): # type: () -> None diff --git a/sphinx/builders/manpage.py b/sphinx/builders/manpage.py index 7ee2957be..83e354601 100644 --- a/sphinx/builders/manpage.py +++ b/sphinx/builders/manpage.py @@ -23,7 +23,7 @@ from sphinx.util import logging from sphinx.util.nodes import inline_all_toctrees from sphinx.util.osutil import make_filename from sphinx.util.console import bold, darkgreen # type: ignore -from sphinx.writers.manpage import ManualPageWriter +from sphinx.writers.manpage import ManualPageWriter, ManualPageTranslator if False: # For type annotation @@ -40,6 +40,7 @@ class ManualPageBuilder(Builder): """ name = 'man' format = 'man' + default_translator_class = ManualPageTranslator supported_image_types = [] # type: List[unicode] def init(self): diff --git a/sphinx/builders/texinfo.py b/sphinx/builders/texinfo.py index 4724aa9c3..3e6816507 100644 --- a/sphinx/builders/texinfo.py +++ b/sphinx/builders/texinfo.py @@ -27,7 +27,7 @@ from sphinx.util.fileutil import copy_asset_file from sphinx.util.nodes import inline_all_toctrees from sphinx.util.osutil import SEP, make_filename from sphinx.util.console import bold, darkgreen # type: ignore -from sphinx.writers.texinfo import TexinfoWriter +from sphinx.writers.texinfo import TexinfoWriter, TexinfoTranslator if False: # For type annotation @@ -99,6 +99,7 @@ class TexinfoBuilder(Builder): format = 'texinfo' supported_image_types = ['image/png', 'image/jpeg', 'image/gif'] + default_translator_class = TexinfoTranslator def init(self): # type: () -> None diff --git a/sphinx/builders/text.py b/sphinx/builders/text.py index 4541e7bee..29ceaa855 100644 --- a/sphinx/builders/text.py +++ b/sphinx/builders/text.py @@ -17,7 +17,7 @@ from docutils.io import StringOutput from sphinx.builders import Builder from sphinx.util import logging from sphinx.util.osutil import ensuredir, os_path -from sphinx.writers.text import TextWriter +from sphinx.writers.text import TextWriter, TextTranslator if False: # For type annotation @@ -33,6 +33,7 @@ class TextBuilder(Builder): format = 'text' out_suffix = '.txt' allow_parallel = True + default_translator_class = TextTranslator current_docname = None # type: unicode diff --git a/sphinx/builders/websupport.py b/sphinx/builders/websupport.py index c2918edfa..20792be90 100644 --- a/sphinx/builders/websupport.py +++ b/sphinx/builders/websupport.py @@ -34,6 +34,7 @@ class WebSupportBuilder(PickleHTMLBuilder): name = 'websupport' versioning_method = 'commentable' versioning_compare = True # for commentable node's uuid stability. + default_translator_class = WebSupportTranslator def init(self): # type: () -> None @@ -54,11 +55,6 @@ class WebSupportBuilder(PickleHTMLBuilder): self.search = search self.storage = storage - def init_translator_class(self): - # type: () -> None - if self.translator_class is None: - self.translator_class = WebSupportTranslator - def prepare_writing(self, docnames): # type: (Iterable[unicode]) -> None PickleHTMLBuilder.prepare_writing(self, docnames) diff --git a/sphinx/builders/xml.py b/sphinx/builders/xml.py index ba1a4850e..d4ebb47ef 100644 --- a/sphinx/builders/xml.py +++ b/sphinx/builders/xml.py @@ -14,6 +14,7 @@ from os import path from docutils import nodes from docutils.io import StringOutput +from docutils.writers.docutils_xml import XMLTranslator from sphinx.builders import Builder from sphinx.util import logging @@ -38,6 +39,7 @@ class XMLBuilder(Builder): allow_parallel = True _writer_class = XMLWriter + default_translator_class = XMLTranslator def init(self): # type: () -> None diff --git a/sphinx/factory.py b/sphinx/factory.py index a08e047db..ce9c52324 100644 --- a/sphinx/factory.py +++ b/sphinx/factory.py @@ -39,6 +39,7 @@ class SphinxFactory(object): self.builders = {} # type: Dict[unicode, Type[Builder]] self.domains = {} # type: Dict[unicode, Type[Domain]] self.source_parsers = {} # type: Dict[unicode, Parser] + self.translators = {} # type: Dict[unicode, nodes.NodeVisitor] def add_builder(self, builder): # type: (Type[Builder]) -> None @@ -152,3 +153,17 @@ class SphinxFactory(object): def get_source_parsers(self): # type: () -> Dict[unicode, Parser] return self.source_parsers + + def add_translator(self, name, translator): + # type: (unicode, Type[nodes.NodeVisitor]) -> None + self.translators[name] = translator + + def get_translator_class(self, builder): + # type: (Builder) -> Type[nodes.NodeVisitor] + return self.translators.get(builder.name, + builder.default_translator_class) + + def create_translator(self, builder, document): + # type: (Builder, nodes.Node) -> nodes.NodeVisitor + translator_class = self.get_translator_class(builder) + return translator_class(builder, document) diff --git a/sphinx/writers/html.py b/sphinx/writers/html.py index 45ec65662..63aba3cd6 100644 --- a/sphinx/writers/html.py +++ b/sphinx/writers/html.py @@ -52,8 +52,8 @@ class HTMLWriter(Writer): def translate(self): # type: () -> None # sadly, this is mostly copied from parent class - self.visitor = visitor = self.builder.translator_class(self.builder, - self.document) + self.visitor = visitor = self.builder.create_translator(self.builder, + self.document) self.document.walkabout(visitor) self.output = visitor.astext() for attr in ('head_prefix', 'stylesheet', 'head', 'body_prefix', diff --git a/sphinx/writers/latex.py b/sphinx/writers/latex.py index 7e666ecff..fc2cbb272 100644 --- a/sphinx/writers/latex.py +++ b/sphinx/writers/latex.py @@ -159,14 +159,12 @@ class LaTeXWriter(writers.Writer): # type: (Builder) -> None writers.Writer.__init__(self) self.builder = builder - self.translator_class = ( - self.builder.translator_class or LaTeXTranslator) def translate(self): # type: () -> None transform = ShowUrlsTransform(self.document) transform.apply() - visitor = self.translator_class(self.document, self.builder) + visitor = self.builder.create_translator(self.document, self.builder) self.document.walkabout(visitor) self.output = visitor.astext() diff --git a/sphinx/writers/manpage.py b/sphinx/writers/manpage.py index 7372230ea..830b1148a 100644 --- a/sphinx/writers/manpage.py +++ b/sphinx/writers/manpage.py @@ -35,14 +35,12 @@ class ManualPageWriter(Writer): # type: (Builder) -> None Writer.__init__(self) self.builder = builder - self.translator_class = ( - self.builder.translator_class or ManualPageTranslator) def translate(self): # type: () -> None transform = NestedInlineTransform(self.document) transform.apply() - visitor = self.translator_class(self.builder, self.document) + visitor = self.builder.create_translator(self.builder, self.document) self.visitor = visitor self.document.walkabout(visitor) self.output = visitor.astext() diff --git a/sphinx/writers/texinfo.py b/sphinx/writers/texinfo.py index a79f34e67..13fac45d3 100644 --- a/sphinx/writers/texinfo.py +++ b/sphinx/writers/texinfo.py @@ -133,13 +133,10 @@ class TexinfoWriter(writers.Writer): # type: (TexinfoBuilder) -> None writers.Writer.__init__(self) self.builder = builder - self.translator_class = ( - self.builder.translator_class or TexinfoTranslator) def translate(self): # type: () -> None - self.visitor = visitor = self.translator_class( - self.document, self.builder) + self.visitor = visitor = self.builder.create_translator(self.document, self.builder) self.document.walkabout(visitor) visitor.finish() for attr in self.visitor_attributes: diff --git a/sphinx/writers/text.py b/sphinx/writers/text.py index 5ab5ec0e6..687eecfd7 100644 --- a/sphinx/writers/text.py +++ b/sphinx/writers/text.py @@ -159,11 +159,10 @@ class TextWriter(writers.Writer): # type: (TextBuilder) -> None writers.Writer.__init__(self) self.builder = builder - self.translator_class = self.builder.translator_class or TextTranslator def translate(self): # type: () -> None - visitor = self.translator_class(self.document, self.builder) + visitor = self.builder.create_translator(self.document, self.builder) self.document.walkabout(visitor) self.output = visitor.body diff --git a/sphinx/writers/xml.py b/sphinx/writers/xml.py index 7d87e0bd6..9cb64216a 100644 --- a/sphinx/writers/xml.py +++ b/sphinx/writers/xml.py @@ -24,8 +24,7 @@ class XMLWriter(BaseXMLWriter): # type: (Builder) -> None BaseXMLWriter.__init__(self) self.builder = builder - if self.builder.translator_class: - self.translator_class = self.builder.translator_class + self.translator_class = self.builder.get_translator_class() def translate(self, *args, **kwargs): # type: (Any, Any) -> None diff --git a/tests/test_api_translator.py b/tests/test_api_translator.py index d987a1b57..02c8c4424 100644 --- a/tests/test_api_translator.py +++ b/tests/test_api_translator.py @@ -26,7 +26,7 @@ def teardown_module(): @pytest.mark.sphinx('html') def test_html_translator(app, status, warning): # no set_translator() - translator_class = app.builder.translator_class + translator_class = app.builder.get_translator_class() assert translator_class assert translator_class.__name__ == 'SmartyPantsHTMLTranslator' @@ -35,7 +35,7 @@ def test_html_translator(app, status, warning): 'html_use_smartypants': False}) def test_html_with_smartypants(app, status, warning): # no set_translator(), html_use_smartypants=False - translator_class = app.builder.translator_class + translator_class = app.builder.get_translator_class() assert translator_class assert translator_class.__name__ == 'HTMLTranslator' @@ -43,7 +43,7 @@ def test_html_with_smartypants(app, status, warning): @pytest.mark.sphinx('html', testroot='api-set-translator') def test_html_with_set_translator_for_html_(app, status, warning): # use set_translator() - translator_class = app.builder.translator_class + translator_class = app.builder.get_translator_class() assert translator_class assert translator_class.__name__ == 'ConfHTMLTranslator' @@ -61,62 +61,62 @@ def test_html_with_set_translator_for_html_(app, status, warning): @pytest.mark.sphinx('singlehtml', testroot='api-set-translator') def test_singlehtml_set_translator_for_singlehtml(app, status, warning): - translator_class = app.builder.translator_class + translator_class = app.builder.get_translator_class() assert translator_class assert translator_class.__name__ == 'ConfSingleHTMLTranslator' @pytest.mark.sphinx('pickle', testroot='api-set-translator') def test_pickle_set_translator_for_pickle(app, status, warning): - translator_class = app.builder.translator_class + translator_class = app.builder.get_translator_class() assert translator_class assert translator_class.__name__ == 'ConfPickleTranslator' @pytest.mark.sphinx('json', testroot='api-set-translator') def test_json_set_translator_for_json(app, status, warning): - translator_class = app.builder.translator_class + translator_class = app.builder.get_translator_class() assert translator_class assert translator_class.__name__ == 'ConfJsonTranslator' @pytest.mark.sphinx('latex', testroot='api-set-translator') def test_html_with_set_translator_for_latex(app, status, warning): - translator_class = app.builder.translator_class + translator_class = app.builder.get_translator_class() assert translator_class assert translator_class.__name__ == 'ConfLaTeXTranslator' @pytest.mark.sphinx('man', testroot='api-set-translator') def test_html_with_set_translator_for_man(app, status, warning): - translator_class = app.builder.translator_class + translator_class = app.builder.get_translator_class() assert translator_class assert translator_class.__name__ == 'ConfManualPageTranslator' @pytest.mark.sphinx('texinfo', testroot='api-set-translator') def test_html_with_set_translator_for_texinfo(app, status, warning): - translator_class = app.builder.translator_class + translator_class = app.builder.get_translator_class() assert translator_class assert translator_class.__name__ == 'ConfTexinfoTranslator' @pytest.mark.sphinx('text', testroot='api-set-translator') def test_html_with_set_translator_for_text(app, status, warning): - translator_class = app.builder.translator_class + translator_class = app.builder.get_translator_class() assert translator_class assert translator_class.__name__ == 'ConfTextTranslator' @pytest.mark.sphinx('xml', testroot='api-set-translator') def test_html_with_set_translator_for_xml(app, status, warning): - translator_class = app.builder.translator_class + translator_class = app.builder.get_translator_class() assert translator_class assert translator_class.__name__ == 'ConfXMLTranslator' @pytest.mark.sphinx('pseudoxml', testroot='api-set-translator') def test_html_with_set_translator_for_pseudoxml(app, status, warning): - translator_class = app.builder.translator_class + translator_class = app.builder.get_translator_class() assert translator_class assert translator_class.__name__ == 'ConfPseudoXMLTranslator' From a87a93ac7164c43c7e805bc5df45c4ff673625d2 Mon Sep 17 00:00:00 2001 From: Takeshi KOMIYA Date: Sun, 16 Apr 2017 17:19:03 +0900 Subject: [PATCH 12/23] Move load_extension() to factory --- sphinx/application.py | 4 +-- sphinx/extension.py | 62 ++--------------------------------------- sphinx/factory.py | 65 +++++++++++++++++++++++++++++++++++++++++-- 3 files changed, 66 insertions(+), 65 deletions(-) diff --git a/sphinx/application.py b/sphinx/application.py index 236fa0516..554b3e960 100644 --- a/sphinx/application.py +++ b/sphinx/application.py @@ -32,7 +32,7 @@ from sphinx.errors import ConfigError, ExtensionError, VersionRequirementError from sphinx.deprecation import RemovedInSphinx17Warning, RemovedInSphinx20Warning from sphinx.environment import BuildEnvironment from sphinx.events import EventManager -from sphinx.extension import load_extension, verify_required_extensions +from sphinx.extension import verify_required_extensions from sphinx.factory import SphinxFactory from sphinx.io import SphinxStandaloneReader from sphinx.locale import _ @@ -452,7 +452,7 @@ class Sphinx(object): # type: (unicode) -> None """Import and setup a Sphinx extension module. No-op if called twice.""" logger.debug('[app] setting up extension: %r', extname) - load_extension(self, extname) + self.factory.load_extension(self, extname) def require_sphinx(self, version): # type: (unicode) -> None diff --git a/sphinx/extension.py b/sphinx/extension.py index 4e40fdee3..82fd2d976 100644 --- a/sphinx/extension.py +++ b/sphinx/extension.py @@ -9,30 +9,20 @@ :license: BSD, see LICENSE for details. """ -import traceback - from six import iteritems -from sphinx.errors import ExtensionError, VersionRequirementError +from sphinx.errors import VersionRequirementError from sphinx.locale import _ from sphinx.util import logging if False: # For type annotation - from typing import Any, Dict # NOQA + from typing import Dict # NOQA from sphinx.application import Sphinx # NOQA - logger = logging.getLogger(__name__) -# list of deprecated extensions. Keys are extension name. -# Values are Sphinx version that merge the extension. -EXTENSION_BLACKLIST = { - "sphinxjp.themecore": "1.2" -} # type: Dict[unicode, unicode] - - class Extension(object): def __init__(self, name, module, **kwargs): self.name = name @@ -51,54 +41,6 @@ class Extension(object): self.parallel_write_safe = kwargs.pop('parallel_read_safe', True) -def load_extension(app, extname): - # type: (Sphinx, unicode) -> None - """Load a Sphinx extension.""" - if extname in app.extensions: # alread loaded - return - if extname in EXTENSION_BLACKLIST: - logger.warning(_('the extension %r was already merged with Sphinx since ' - 'version %s; this extension is ignored.'), - extname, EXTENSION_BLACKLIST[extname]) - return - - # update loading context - app._setting_up_extension.append(extname) - - try: - mod = __import__(extname, None, None, ['setup']) - except ImportError as err: - logger.verbose(_('Original exception:\n') + traceback.format_exc()) - raise ExtensionError(_('Could not import extension %s') % extname, err) - - if not hasattr(mod, 'setup'): - logger.warning(_('extension %r has no setup() function; is it really ' - 'a Sphinx extension module?'), extname) - metadata = {} # type: Dict[unicode, Any] - else: - try: - metadata = mod.setup(app) - except VersionRequirementError as err: - # add the extension name to the version required - raise VersionRequirementError( - _('The %s extension used by this project needs at least ' - 'Sphinx v%s; it therefore cannot be built with this ' - 'version.') % (extname, err) - ) - - if metadata is None: - metadata = {} - if extname == 'rst2pdf.pdfbuilder': - metadata['parallel_read_safe'] = True - elif not isinstance(metadata, dict): - logger.warning(_('extension %r returned an unsupported object from ' - 'its setup() function; it should return None or a ' - 'metadata dictionary'), extname) - - app.extensions[extname] = Extension(extname, mod, **metadata) - app._setting_up_extension.pop() - - def verify_required_extensions(app, requirements): # type: (Sphinx, Dict[unicode, unicode]) -> None """Verify the required Sphinx extensions are loaded.""" diff --git a/sphinx/factory.py b/sphinx/factory.py index ce9c52324..f90d68733 100644 --- a/sphinx/factory.py +++ b/sphinx/factory.py @@ -12,15 +12,18 @@ """ from __future__ import print_function +import traceback + from pkg_resources import iter_entry_points from six import itervalues -from sphinx.errors import ExtensionError, SphinxError +from sphinx.errors import ExtensionError, SphinxError, VersionRequirementError +from sphinx.extension import Extension from sphinx.domains import ObjType from sphinx.domains.std import GenericObject, Target -from sphinx.extension import load_extension from sphinx.locale import _ from sphinx.roles import XRefRole +from sphinx.util import logging from sphinx.util.docutils import directive_helper if False: @@ -33,6 +36,14 @@ if False: from sphinx.domains import Domain, Index # NOQA from sphinx.environment import BuildEnvironment # NOQA +logger = logging.getLogger(__name__) + +# list of deprecated extensions. Keys are extension name. +# Values are Sphinx version that merge the extension. +EXTENSION_BLACKLIST = { + "sphinxjp.themecore": "1.2" +} # type: Dict[unicode, unicode] + class SphinxFactory(object): def __init__(self): @@ -62,7 +73,8 @@ class SphinxFactory(object): except StopIteration: raise SphinxError(_('Builder name %s not registered or available' ' through entry point') % name) - load_extension(app, entry_point.module_name) + + self.load_extension(app, entry_point.module_name) def create_builder(self, app, name): # type: (Sphinx, unicode) -> Builder @@ -167,3 +179,50 @@ class SphinxFactory(object): # type: (Builder, nodes.Node) -> nodes.NodeVisitor translator_class = self.get_translator_class(builder) return translator_class(builder, document) + + def load_extension(self, app, extname): + # type: (Sphinx, unicode) -> None + """Load a Sphinx extension.""" + if extname in app.extensions: # alread loaded + return + if extname in EXTENSION_BLACKLIST: + logger.warning(_('the extension %r was already merged with Sphinx since ' + 'version %s; this extension is ignored.'), + extname, EXTENSION_BLACKLIST[extname]) + return + + # update loading context + app._setting_up_extension.append(extname) + + try: + mod = __import__(extname, None, None, ['setup']) + except ImportError as err: + logger.verbose(_('Original exception:\n') + traceback.format_exc()) + raise ExtensionError(_('Could not import extension %s') % extname, err) + + if not hasattr(mod, 'setup'): + logger.warning(_('extension %r has no setup() function; is it really ' + 'a Sphinx extension module?'), extname) + metadata = {} # type: Dict[unicode, Any] + else: + try: + metadata = mod.setup(app) + except VersionRequirementError as err: + # add the extension name to the version required + raise VersionRequirementError( + _('The %s extension used by this project needs at least ' + 'Sphinx v%s; it therefore cannot be built with this ' + 'version.') % (extname, err) + ) + + if metadata is None: + metadata = {} + if extname == 'rst2pdf.pdfbuilder': + metadata['parallel_read_safe'] = True + elif not isinstance(metadata, dict): + logger.warning(_('extension %r returned an unsupported object from ' + 'its setup() function; it should return None or a ' + 'metadata dictionary'), extname) + + app.extensions[extname] = Extension(extname, mod, **metadata) + app._setting_up_extension.pop() From 94ba265944a72f17c52e63dd2ac0e28de324ba3e Mon Sep 17 00:00:00 2001 From: jfbu Date: Sat, 22 Apr 2017 22:46:16 +0200 Subject: [PATCH 13/23] Move jfbu from contributor list to co-maintainer list --- AUTHORS | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/AUTHORS b/AUTHORS index 0671b31d6..580feeb32 100644 --- a/AUTHORS +++ b/AUTHORS @@ -12,6 +12,7 @@ Other co-maintainers: * Robert Lehmann <@lehmannro> * Roland Meister <@rolmei> * Takeshi Komiya <@tk0miya> +* Jean-François Burnol <@jfbu> * Yoshiki Shibukawa <@shibu_jp> Other contributors, listed alphabetically, are: @@ -21,7 +22,6 @@ Other contributors, listed alphabetically, are: * Jakob Lykke Andersen -- Rewritten C++ domain * Henrique Bastos -- SVG support for graphviz extension * Daniel Bültmann -- todo extension -* Jean-François Burnol -- LaTeX improvements * Marco Buttu -- doctest extension (pyversion option) * Etienne Desautels -- apidoc module * Michael Droettboom -- inheritance_diagram extension From 7176d586cc8323179cf08d026fe08ce39462bff7 Mon Sep 17 00:00:00 2001 From: Takeshi KOMIYA Date: Sun, 23 Apr 2017 11:24:29 +0900 Subject: [PATCH 14/23] Add debug log --- sphinx/ext/imgconverter.py | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/sphinx/ext/imgconverter.py b/sphinx/ext/imgconverter.py index a06b789fb..d69a4411b 100644 --- a/sphinx/ext/imgconverter.py +++ b/sphinx/ext/imgconverter.py @@ -35,8 +35,9 @@ class ImagemagickConverter(ImageConverter): # type: () -> bool """Confirms the converter is available or not.""" try: - ret = subprocess.call([self.config.image_converter, '-version'], - stdin=subprocess.PIPE, stdout=subprocess.PIPE) + args = [self.config.image_converter, '-version'] + logger.debug('Invoking %r ...', args) + ret = subprocess.call(args, stdin=subprocess.PIPE, stdout=subprocess.PIPE) if ret == 0: return True else: @@ -54,6 +55,7 @@ class ImagemagickConverter(ImageConverter): args = ([self.config.image_converter] + self.config.image_converter_args + [_from, _to]) + logger.debug('Invoking %r ...', args) p = subprocess.Popen(args, stdin=subprocess.PIPE, stdout=subprocess.PIPE) except OSError as err: if err.errno != ENOENT: # No such file or directory From 55818b74196c8814ce0ee753cf0fcaad31469856 Mon Sep 17 00:00:00 2001 From: Takeshi KOMIYA Date: Sun, 23 Apr 2017 16:03:46 +0900 Subject: [PATCH 15/23] Update docs --- doc/theming.rst | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/doc/theming.rst b/doc/theming.rst index 1236a85bf..2a7063925 100644 --- a/doc/theming.rst +++ b/doc/theming.rst @@ -300,7 +300,7 @@ brings to users easy setting up ways. To distribute your theme as a python package, please define an entry point called ``sphinx.html_themes`` in your setup.py file, and write a ``setup()`` -function to register your themes in it:: +function to register your themes using ``add_html_theme()`` API in it:: # 'setup.py' setup( @@ -319,6 +319,10 @@ function to register your themes in it:: def setup(app): app.add_html_theme('name_of_theme', path.abspath(path.dirname(__file__))) + +If your theme package contains two or more themes, please call ``add_html_theme()`` +twice or more. + .. versionadded:: 1.2 'sphinx_themes' entry_points feature. From 72f267c6ae27144b46a23725b6634cc35f03fb50 Mon Sep 17 00:00:00 2001 From: Takeshi KOMIYA Date: Sun, 23 Apr 2017 16:29:38 +0900 Subject: [PATCH 16/23] Rename SphinxFactory class to SphinxComponentRegistry --- sphinx/application.py | 46 +++++++++++++++--------------- sphinx/builders/__init__.py | 6 ++-- sphinx/environment/__init__.py | 2 +- sphinx/ext/autosummary/__init__.py | 2 +- sphinx/io.py | 2 +- sphinx/{factory.py => registry.py} | 10 +++---- sphinx/transforms/i18n.py | 2 +- tests/test_application.py | 6 ++-- 8 files changed, 37 insertions(+), 39 deletions(-) rename sphinx/{factory.py => registry.py} (98%) diff --git a/sphinx/application.py b/sphinx/application.py index 554b3e960..eb22bf964 100644 --- a/sphinx/application.py +++ b/sphinx/application.py @@ -33,9 +33,9 @@ from sphinx.deprecation import RemovedInSphinx17Warning, RemovedInSphinx20Warnin from sphinx.environment import BuildEnvironment from sphinx.events import EventManager from sphinx.extension import verify_required_extensions -from sphinx.factory import SphinxFactory from sphinx.io import SphinxStandaloneReader from sphinx.locale import _ +from sphinx.registry import SphinxComponentRegistry from sphinx.util import pycompat # noqa: F401 from sphinx.util import import_object from sphinx.util import logging @@ -118,7 +118,7 @@ class Sphinx(object): self._setting_up_extension = ['?'] # type: List[unicode] self.builder = None # type: Builder self.env = None # type: BuildEnvironment - self.factory = SphinxFactory() + self.registry = SphinxComponentRegistry() self.enumerable_nodes = {} # type: Dict[nodes.Node, Tuple[unicode, Callable]] # NOQA self.post_transforms = [] # type: List[Transform] self.html_themes = {} # type: Dict[unicode, unicode] @@ -218,7 +218,7 @@ class Sphinx(object): # check primary_domain if requested primary_domain = self.config.primary_domain - if primary_domain and not self.factory.has_domain(primary_domain): + if primary_domain and not self.registry.has_domain(primary_domain): logger.warning(_('primary_domain %r not found, ignored.'), primary_domain) # create the builder @@ -264,7 +264,7 @@ class Sphinx(object): # type: () -> None for suffix, parser in iteritems(self.config.source_parsers): self.add_source_parser(suffix, parser) - for suffix, parser in iteritems(self.factory.get_source_parsers()): + for suffix, parser in iteritems(self.registry.get_source_parsers()): if suffix not in self.config.source_suffix: self.config.source_suffix.append(suffix) @@ -273,7 +273,7 @@ class Sphinx(object): if freshenv: self.env = BuildEnvironment(self) self.env.find_files(self.config, self.builder) - for domain in self.factory.create_domains(self.env): + for domain in self.registry.create_domains(self.env): self.env.domains[domain.name] = domain else: try: @@ -281,7 +281,7 @@ class Sphinx(object): filename = path.join(self.doctreedir, ENV_PICKLE_FILENAME) self.env = BuildEnvironment.frompickle(filename, self) self.env.domains = {} - for domain in self.factory.create_domains(self.env): + for domain in self.registry.create_domains(self.env): # this can raise if the data version doesn't fit self.env.domains[domain.name] = domain logger.info(_('done')) @@ -294,7 +294,7 @@ class Sphinx(object): def preload_builder(self, name): # type: (unicode) -> None - self.factory.preload_builder(self, name) + self.registry.preload_builder(self, name) def create_builder(self, name): # type: (unicode) -> Builder @@ -302,7 +302,7 @@ class Sphinx(object): logger.info(_('No builder selected, using default: html')) name = 'html' - return self.factory.create_builder(self, name) + return self.registry.create_builder(self, name) def _init_builder(self): # type: () -> None @@ -452,7 +452,7 @@ class Sphinx(object): # type: (unicode) -> None """Import and setup a Sphinx extension module. No-op if called twice.""" logger.debug('[app] setting up extension: %r', extname) - self.factory.load_extension(self, extname) + self.registry.load_extension(self, extname) def require_sphinx(self, version): # type: (unicode) -> None @@ -496,7 +496,7 @@ class Sphinx(object): def add_builder(self, builder): # type: (Type[Builder]) -> None logger.debug('[app] adding builder: %r', builder) - self.factory.add_builder(builder) + self.registry.add_builder(builder) def add_config_value(self, name, default, rebuild, types=()): # type: (unicode, Any, Union[bool, unicode], Any) -> None @@ -516,7 +516,7 @@ class Sphinx(object): def set_translator(self, name, translator_class): # type: (unicode, Type[nodes.NodeVisitor]) -> None logger.info(bold(_('A Translator for the %s builder is changed.') % name)) - self.factory.add_translator(name, translator_class) + self.registry.add_translator(name, translator_class) def add_node(self, node, **kwds): # type: (nodes.Node, Any) -> None @@ -534,7 +534,7 @@ class Sphinx(object): except ValueError: raise ExtensionError(_('Value for key %r must be a ' '(visit, depart) function tuple') % key) - translator = self.factory.translators.get(key) + translator = self.registry.translators.get(key) translators = [] if translator is not None: translators.append(translator) @@ -612,30 +612,30 @@ class Sphinx(object): def add_domain(self, domain): # type: (Type[Domain]) -> None logger.debug('[app] adding domain: %r', domain) - self.factory.add_domain(domain) + self.registry.add_domain(domain) def override_domain(self, domain): # type: (Type[Domain]) -> None logger.debug('[app] overriding domain: %r', domain) - self.factory.override_domain(domain) + self.registry.override_domain(domain) def add_directive_to_domain(self, domain, name, obj, has_content=None, argument_spec=None, **option_spec): # type: (unicode, unicode, Any, bool, Any, Any) -> None logger.debug('[app] adding directive to domain: %r', (domain, name, obj, has_content, argument_spec, option_spec)) - self.factory.add_directive_to_domain(domain, name, obj, - has_content, argument_spec, **option_spec) + self.registry.add_directive_to_domain(domain, name, obj, + has_content, argument_spec, **option_spec) def add_role_to_domain(self, domain, name, role): # type: (unicode, unicode, Any) -> None logger.debug('[app] adding role to domain: %r', (domain, name, role)) - self.factory.add_role_to_domain(domain, name, role) + self.registry.add_role_to_domain(domain, name, role) def add_index_to_domain(self, domain, index): # type: (unicode, Type[Index]) -> None logger.debug('[app] adding index to domain: %r', (domain, index)) - self.factory.add_index_to_domain(domain, index) + self.registry.add_index_to_domain(domain, index) def add_object_type(self, directivename, rolename, indextemplate='', parse_node=None, ref_nodeclass=None, objname='', @@ -644,8 +644,8 @@ class Sphinx(object): logger.debug('[app] adding object type: %r', (directivename, rolename, indextemplate, parse_node, ref_nodeclass, objname, doc_field_types)) - self.factory.add_object_type(directivename, rolename, indextemplate, parse_node, - ref_nodeclass, objname, doc_field_types) + self.registry.add_object_type(directivename, rolename, indextemplate, parse_node, + ref_nodeclass, objname, doc_field_types) def add_description_unit(self, directivename, rolename, indextemplate='', parse_node=None, ref_nodeclass=None, objname='', @@ -663,8 +663,8 @@ class Sphinx(object): logger.debug('[app] adding crossref type: %r', (directivename, rolename, indextemplate, ref_nodeclass, objname)) - self.factory.add_crossref_type(directivename, rolename, - indextemplate, ref_nodeclass, objname) + self.registry.add_crossref_type(directivename, rolename, + indextemplate, ref_nodeclass, objname) def add_transform(self, transform): # type: (Type[Transform]) -> None @@ -736,7 +736,7 @@ class Sphinx(object): def add_source_parser(self, suffix, parser): # type: (unicode, Parser) -> None logger.debug('[app] adding search source_parser: %r, %r', suffix, parser) - self.factory.add_source_parser(suffix, parser) + self.registry.add_source_parser(suffix, parser) def add_env_collector(self, collector): # type: (Type[EnvironmentCollector]) -> None diff --git a/sphinx/builders/__init__.py b/sphinx/builders/__init__.py index ab75c5bb8..fdf524bf6 100644 --- a/sphinx/builders/__init__.py +++ b/sphinx/builders/__init__.py @@ -116,7 +116,7 @@ class Builder(object): def get_translator_class(self, *args): # type: (Any) -> nodes.NodeVisitor """Return a class of translator.""" - return self.app.factory.get_translator_class(self) + return self.app.registry.get_translator_class(self) def create_translator(self, *args): # type: (Any) -> nodes.NodeVisitor @@ -125,7 +125,7 @@ class Builder(object): This method returns an instance of ``default_translator_class`` by default. Users can replace the translator class with ``app.set_translator()`` API. """ - translator_class = self.app.factory.get_translator_class(self) + translator_class = self.app.registry.get_translator_class(self) assert translator_class, "translator not found for %s" % self.__class__.__name__ return translator_class(*args) @@ -136,7 +136,7 @@ class Builder(object): .. deprecated:: 1.6 """ - translator_class = self.app.factory.get_translator_class(self) + translator_class = self.app.registry.get_translator_class(self) if translator_class is None and self.default_translator_class is None: warnings.warn('builder.translator_class() is now deprecated. ' 'Please use builder.create_translator() and ' diff --git a/sphinx/environment/__init__.py b/sphinx/environment/__init__.py index 69bcae354..066f57209 100644 --- a/sphinx/environment/__init__.py +++ b/sphinx/environment/__init__.py @@ -692,7 +692,7 @@ class BuildEnvironment(object): # publish manually reader = SphinxStandaloneReader(self.app, - parsers=self.app.factory.get_source_parsers()) + parsers=self.app.registry.get_source_parsers()) pub = Publisher(reader=reader, writer=SphinxDummyWriter(), destination_class=NullOutput) diff --git a/sphinx/ext/autosummary/__init__.py b/sphinx/ext/autosummary/__init__.py index d1a979443..67bbf6d91 100644 --- a/sphinx/ext/autosummary/__init__.py +++ b/sphinx/ext/autosummary/__init__.py @@ -573,7 +573,7 @@ def get_rst_suffix(app): # type: (Sphinx) -> unicode def get_supported_format(suffix): # type: (unicode) -> Tuple[unicode] - parser_class = app.factory.get_source_parsers().get(suffix) + parser_class = app.registry.get_source_parsers().get(suffix) if parser_class is None: return ('restructuredtext',) if isinstance(parser_class, string_types): diff --git a/sphinx/io.py b/sphinx/io.py index c9c9d085a..7cb54efff 100644 --- a/sphinx/io.py +++ b/sphinx/io.py @@ -158,7 +158,7 @@ class SphinxFileInput(FileInput): # type: () -> unicode def get_parser_type(source_path): # type: (unicode) -> Tuple[unicode] - for suffix, parser_class in iteritems(self.app.factory.get_source_parsers()): + for suffix, parser_class in iteritems(self.app.registry.get_source_parsers()): if source_path.endswith(suffix): if isinstance(parser_class, string_types): parser_class = import_object(parser_class, 'source parser') # type: ignore # NOQA diff --git a/sphinx/factory.py b/sphinx/registry.py similarity index 98% rename from sphinx/factory.py rename to sphinx/registry.py index f90d68733..72c9b7ae6 100644 --- a/sphinx/factory.py +++ b/sphinx/registry.py @@ -1,11 +1,9 @@ # -*- coding: utf-8 -*- """ - sphinx.factory - ~~~~~~~~~~~~~~ + sphinx.registory + ~~~~~~~~~~~~~~~~ - Sphinx component factory. - - Gracefully adapted from the TextPress system by Armin. + Sphinx component registory. :copyright: Copyright 2007-2016 by the Sphinx team, see AUTHORS. :license: BSD, see LICENSE for details. @@ -45,7 +43,7 @@ EXTENSION_BLACKLIST = { } # type: Dict[unicode, unicode] -class SphinxFactory(object): +class SphinxComponentRegistry(object): def __init__(self): self.builders = {} # type: Dict[unicode, Type[Builder]] self.domains = {} # type: Dict[unicode, Type[Domain]] diff --git a/sphinx/transforms/i18n.py b/sphinx/transforms/i18n.py index be1c80bbd..7776af237 100644 --- a/sphinx/transforms/i18n.py +++ b/sphinx/transforms/i18n.py @@ -52,7 +52,7 @@ def publish_msgstr(app, source, source_path, source_line, config, settings): from sphinx.io import SphinxI18nReader reader = SphinxI18nReader( app=app, - parsers=app.factory.get_source_parsers(), + parsers=app.registry.get_source_parsers(), parser_name='restructuredtext', # default parser ) reader.set_lineno_for_reporter(source_line) diff --git a/tests/test_application.py b/tests/test_application.py index 84f973353..736ce42ac 100644 --- a/tests/test_application.py +++ b/tests/test_application.py @@ -85,6 +85,6 @@ def test_domain_override(app, status, warning): @pytest.mark.sphinx(testroot='add_source_parser') def test_add_source_parser(app, status, warning): assert set(app.config.source_suffix) == set(['.rst', '.md', '.test']) - assert set(app.factory.get_source_parsers().keys()) == set(['.md', '.test']) - assert app.factory.get_source_parsers()['.md'].__name__ == 'DummyMarkdownParser' - assert app.factory.get_source_parsers()['.test'].__name__ == 'TestSourceParser' + assert set(app.registry.get_source_parsers().keys()) == set(['.md', '.test']) + assert app.registry.get_source_parsers()['.md'].__name__ == 'DummyMarkdownParser' + assert app.registry.get_source_parsers()['.test'].__name__ == 'TestSourceParser' From db377ceb5524ec276ac6d18ed3206b8147800ae7 Mon Sep 17 00:00:00 2001 From: Takeshi KOMIYA Date: Sun, 23 Apr 2017 00:21:22 +0900 Subject: [PATCH 17/23] Implement get_full_qualified_name() to StandardDomain (refs: #3630) --- sphinx/domains/std.py | 15 ++++- tests/test_domain_std.py | 18 ++++++ tests/test_ext_intersphinx.py | 111 ++++++++++++++++++++++------------ tests/test_util_inventory.py | 1 + 4 files changed, 107 insertions(+), 38 deletions(-) diff --git a/sphinx/domains/std.py b/sphinx/domains/std.py index 0086d6113..c08842e6c 100644 --- a/sphinx/domains/std.py +++ b/sphinx/domains/std.py @@ -849,7 +849,11 @@ class StandardDomain(Domain): for doc in self.env.all_docs: yield (doc, clean_astext(self.env.titles[doc]), 'doc', doc, '', -1) for (prog, option), info in iteritems(self.data['progoptions']): - yield (option, option, 'cmdoption', info[0], info[1], 1) + if prog: + fullname = ".".join([prog, option]) + yield (fullname, fullname, 'cmdoption', info[0], info[1], 1) + else: + yield (option, option, 'cmdoption', info[0], info[1], 1) for (type, name), info in iteritems(self.data['objects']): yield (name, name, type, info[0], info[1], self.object_types[type].attrs['searchprio']) @@ -925,6 +929,15 @@ class StandardDomain(Domain): # Maybe it is defined in orphaned document. raise ValueError + def get_full_qualified_name(self, node): + # type: (nodes.Node) -> unicode + progname = node.get('std:program') + target = node.get('reftarget') + if progname is None or target is None: + return None + else: + return '.'.join([progname, target]) + def setup(app): # type: (Sphinx) -> Dict[unicode, Any] diff --git a/tests/test_domain_std.py b/tests/test_domain_std.py index 9c476a040..737295c00 100644 --- a/tests/test_domain_std.py +++ b/tests/test_domain_std.py @@ -57,3 +57,21 @@ def test_process_doc_handle_table_title(): assert 'testname' in domain.data['labels'] assert domain.data['labels']['testname'] == ( 'testdoc', 'testid', 'title text') + + +def test_get_full_qualified_name(): + env = mock.Mock(domaindata={}) + domain = StandardDomain(env) + + # normal references + node = nodes.reference() + assert domain.get_full_qualified_name(node) is None + + # simple reference to options + node = nodes.reference(reftype='option', reftarget='-l') + assert domain.get_full_qualified_name(node) is None + + # options with std:program context + kwargs = {'std:program': 'ls'} + node = nodes.reference(reftype='option', reftarget='-l', **kwargs) + assert domain.get_full_qualified_name(node) == 'ls.-l' diff --git a/tests/test_ext_intersphinx.py b/tests/test_ext_intersphinx.py index 33f5ceae2..0fa6c93b7 100644 --- a/tests/test_ext_intersphinx.py +++ b/tests/test_ext_intersphinx.py @@ -26,6 +26,22 @@ from sphinx.ext.intersphinx import ( from test_util_inventory import inventory_v2 +def fake_node(domain, type, target, content, **attrs): + contnode = nodes.emphasis(content, content) + node = addnodes.pending_xref('') + node['reftarget'] = target + node['reftype'] = type + node['refdomain'] = domain + node.attributes.update(attrs) + node += contnode + return node, contnode + + +def reference_check(app, *args, **kwds): + node, contnode = fake_node(*args, **kwds) + return missing_reference(app, app.env, node, contnode) + + @mock.patch('sphinx.ext.intersphinx.InventoryFile') @mock.patch('sphinx.ext.intersphinx._read_from_url') def test_fetch_inventory_redirection(_read_from_url, InventoryFile, app, status, warning): @@ -88,46 +104,30 @@ def test_missing_reference(tempdir, app, status, warning): assert inv['py:module']['module2'] == \ ('foo', '2.0', 'https://docs.python.org/foo.html#module-module2', '-') - # create fake nodes and check referencing - - def fake_node(domain, type, target, content, **attrs): - contnode = nodes.emphasis(content, content) - node = addnodes.pending_xref('') - node['reftarget'] = target - node['reftype'] = type - node['refdomain'] = domain - node.attributes.update(attrs) - node += contnode - return node, contnode - - def reference_check(*args, **kwds): - node, contnode = fake_node(*args, **kwds) - return missing_reference(app, app.env, node, contnode) - # check resolution when a target is found - rn = reference_check('py', 'func', 'module1.func', 'foo') + rn = reference_check(app, 'py', 'func', 'module1.func', 'foo') assert isinstance(rn, nodes.reference) assert rn['refuri'] == 'https://docs.python.org/sub/foo.html#module1.func' assert rn['reftitle'] == '(in foo v2.0)' assert rn[0].astext() == 'foo' # create unresolvable nodes and check None return value - assert reference_check('py', 'foo', 'module1.func', 'foo') is None - assert reference_check('py', 'func', 'foo', 'foo') is None - assert reference_check('py', 'func', 'foo', 'foo') is None + assert reference_check(app, 'py', 'foo', 'module1.func', 'foo') is None + assert reference_check(app, 'py', 'func', 'foo', 'foo') is None + assert reference_check(app, 'py', 'func', 'foo', 'foo') is None # check handling of prefixes # prefix given, target found: prefix is stripped - rn = reference_check('py', 'mod', 'py3k:module2', 'py3k:module2') + rn = reference_check(app, 'py', 'mod', 'py3k:module2', 'py3k:module2') assert rn[0].astext() == 'module2' # prefix given, but not in title: nothing stripped - rn = reference_check('py', 'mod', 'py3k:module2', 'module2') + rn = reference_check(app, 'py', 'mod', 'py3k:module2', 'module2') assert rn[0].astext() == 'module2' # prefix given, but explicit: nothing stripped - rn = reference_check('py', 'mod', 'py3k:module2', 'py3k:module2', + rn = reference_check(app, 'py', 'mod', 'py3k:module2', 'py3k:module2', refexplicit=True) assert rn[0].astext() == 'py3k:module2' @@ -145,34 +145,71 @@ def test_missing_reference(tempdir, app, status, warning): assert rn is None assert contnode[0].astext() == 'py3k:unknown' + # check relative paths + rn = reference_check(app, 'py', 'mod', 'py3krel:module1', 'foo') + assert rn['refuri'] == 'py3k/foo.html#module-module1' + + rn = reference_check(app, 'py', 'mod', 'py3krelparent:module1', 'foo') + assert rn['refuri'] == '../../py3k/foo.html#module-module1' + + rn = reference_check(app, 'py', 'mod', 'py3krel:module1', 'foo', refdoc='sub/dir/test') + assert rn['refuri'] == '../../py3k/foo.html#module-module1' + + rn = reference_check(app, 'py', 'mod', 'py3krelparent:module1', 'foo', + refdoc='sub/dir/test') + assert rn['refuri'] == '../../../../py3k/foo.html#module-module1' + + # check refs of standard domain + rn = reference_check(app, 'std', 'doc', 'docname', 'docname') + assert rn['refuri'] == 'https://docs.python.org/docname.html' + + +def test_missing_reference_pydomain(tempdir, app, status, warning): + inv_file = tempdir / 'inventory' + inv_file.write_bytes(inventory_v2) + app.config.intersphinx_mapping = { + 'https://docs.python.org/': inv_file, + } + app.config.intersphinx_cache_limit = 0 + + # load the inventory and check if it's done correctly + load_mappings(app) + # no context data kwargs = {} node, contnode = fake_node('py', 'func', 'func', 'func()', **kwargs) rn = missing_reference(app, app.env, node, contnode) assert rn is None - # context data (like py:module) help to search objects + # py:module context helps to search objects kwargs = {'py:module': 'module1'} node, contnode = fake_node('py', 'func', 'func', 'func()', **kwargs) rn = missing_reference(app, app.env, node, contnode) - assert rn[0].astext() == 'func()' + assert rn.astext() == 'func()' - # check relative paths - rn = reference_check('py', 'mod', 'py3krel:module1', 'foo') - assert rn['refuri'] == 'py3k/foo.html#module-module1' - rn = reference_check('py', 'mod', 'py3krelparent:module1', 'foo') - assert rn['refuri'] == '../../py3k/foo.html#module-module1' +def test_missing_reference_stddomain(tempdir, app, status, warning): + inv_file = tempdir / 'inventory' + inv_file.write_bytes(inventory_v2) + app.config.intersphinx_mapping = { + 'https://docs.python.org/': inv_file, + } + app.config.intersphinx_cache_limit = 0 - rn = reference_check('py', 'mod', 'py3krel:module1', 'foo', refdoc='sub/dir/test') - assert rn['refuri'] == '../../py3k/foo.html#module-module1' + # load the inventory and check if it's done correctly + load_mappings(app) - rn = reference_check('py', 'mod', 'py3krelparent:module1', 'foo', refdoc='sub/dir/test') - assert rn['refuri'] == '../../../../py3k/foo.html#module-module1' + # no context data + kwargs = {} + node, contnode = fake_node('std', 'option', '-l', '-l', **kwargs) + rn = missing_reference(app, app.env, node, contnode) + assert rn is None - # check refs of standard domain - rn = reference_check('std', 'doc', 'docname', 'docname') - assert rn['refuri'] == 'https://docs.python.org/docname.html' + # std:program context helps to search objects + kwargs = {'std:program': 'ls'} + node, contnode = fake_node('std', 'option', '-l', 'ls -l', **kwargs) + rn = missing_reference(app, app.env, node, contnode) + assert rn.astext() == 'ls -l' def test_load_mappings_warnings(tempdir, app, status, warning): diff --git a/tests/test_util_inventory.py b/tests/test_util_inventory.py index 6114ef513..747fe53cf 100644 --- a/tests/test_util_inventory.py +++ b/tests/test_util_inventory.py @@ -35,6 +35,7 @@ module2 py:module 0 foo.html#module-$ - module1.func py:function 1 sub/foo.html#$ - CFunc c:function 2 cfunc.html#CFunc - a term std:term -1 glossary.html#term-a-term - +ls.-l std:cmdoption 1 index.html#cmdoption-ls-l - docname std:doc -1 docname.html - a term including:colon std:term -1 glossary.html#term-a-term-including-colon - '''.encode('utf-8')) From 96fa6d2972ea7c77525607ec490daed9e303535d Mon Sep 17 00:00:00 2001 From: Takeshi KOMIYA Date: Sun, 23 Apr 2017 01:02:29 +0900 Subject: [PATCH 18/23] Implement get_full_qualified_name() to JavascriptDomain (refs: #3630) --- sphinx/domains/javascript.py | 10 ++++++++++ tests/test_domain_js.py | 32 ++++++++++++++++++++++++++++++++ tests/test_ext_intersphinx.py | 24 ++++++++++++++++++++++++ tests/test_util_inventory.py | 4 ++++ 4 files changed, 70 insertions(+) diff --git a/sphinx/domains/javascript.py b/sphinx/domains/javascript.py index 9d6bf1908..9ecf4a4b0 100644 --- a/sphinx/domains/javascript.py +++ b/sphinx/domains/javascript.py @@ -398,6 +398,16 @@ class JavaScriptDomain(Domain): yield refname, refname, type, docname, \ refname.replace('$', '_S_'), 1 + def get_full_qualified_name(self, node): + # type: (nodes.Node) -> unicode + modname = node.get('js:module') + prefix = node.get('js:object') + target = node.get('reftarget') + if target is None: + return None + else: + return '.'.join(filter(None, [modname, prefix, target])) + def setup(app): # type: (Sphinx) -> Dict[unicode, Any] diff --git a/tests/test_domain_js.py b/tests/test_domain_js.py index e521d00c3..a3915c563 100644 --- a/tests/test_domain_js.py +++ b/tests/test_domain_js.py @@ -10,7 +10,11 @@ """ import pytest +from mock import Mock +from docutils import nodes + from sphinx import addnodes +from sphinx.domains.javascript import JavaScriptDomain from util import assert_node @@ -133,3 +137,31 @@ def test_domain_js_find_obj(app, status, warning): ( u'module_a.submodule.ModTopLevel.mod_child_2', (u'module', u'method'))) assert (find_obj(u'module_b.submodule', u'ModTopLevel', u'module_a.submodule', u'mod') == ( u'module_a.submodule', (u'module', u'module'))) + + +def test_get_full_qualified_name(): + env = Mock(domaindata={}) + domain = JavaScriptDomain(env) + + # non-js references + node = nodes.reference() + assert domain.get_full_qualified_name(node) is None + + # simple reference + node = nodes.reference(reftarget='func') + assert domain.get_full_qualified_name(node) == 'func' + + # with js:module context + kwargs = {'js:module': 'module1'} + node = nodes.reference(reftarget='func', **kwargs) + assert domain.get_full_qualified_name(node) == 'module1.func' + + # with js:object context + kwargs = {'js:object': 'Class'} + node = nodes.reference(reftarget='func', **kwargs) + assert domain.get_full_qualified_name(node) == 'Class.func' + + # with both js:module and js:object context + kwargs = {'js:module': 'module1', 'js:object': 'Class'} + node = nodes.reference(reftarget='func', **kwargs) + assert domain.get_full_qualified_name(node) == 'module1.Class.func' diff --git a/tests/test_ext_intersphinx.py b/tests/test_ext_intersphinx.py index 0fa6c93b7..4774604e8 100644 --- a/tests/test_ext_intersphinx.py +++ b/tests/test_ext_intersphinx.py @@ -212,6 +212,30 @@ def test_missing_reference_stddomain(tempdir, app, status, warning): assert rn.astext() == 'ls -l' +def test_missing_reference_jsdomain(tempdir, app, status, warning): + inv_file = tempdir / 'inventory' + inv_file.write_bytes(inventory_v2) + app.config.intersphinx_mapping = { + 'https://docs.python.org/': inv_file, + } + app.config.intersphinx_cache_limit = 0 + + # load the inventory and check if it's done correctly + load_mappings(app) + + # no context data + kwargs = {} + node, contnode = fake_node('js', 'meth', 'baz', 'baz()', **kwargs) + rn = missing_reference(app, app.env, node, contnode) + assert rn is None + + # js:module and js:object context helps to search objects + kwargs = {'js:module': 'foo', 'js:object': 'bar'} + node, contnode = fake_node('js', 'meth', 'baz', 'baz()', **kwargs) + rn = missing_reference(app, app.env, node, contnode) + assert rn.astext() == 'baz()' + + def test_load_mappings_warnings(tempdir, app, status, warning): """ load_mappings issues a warning if new-style mapping diff --git a/tests/test_util_inventory.py b/tests/test_util_inventory.py index 747fe53cf..35f80d960 100644 --- a/tests/test_util_inventory.py +++ b/tests/test_util_inventory.py @@ -37,6 +37,10 @@ CFunc c:function 2 cfunc.html#CFunc - a term std:term -1 glossary.html#term-a-term - ls.-l std:cmdoption 1 index.html#cmdoption-ls-l - docname std:doc -1 docname.html - +foo js:module 1 index.html#foo - +foo.bar js:class 1 index.html#foo.bar - +foo.bar.baz js:method 1 index.html#foo.bar.baz - +foo.bar.qux js:data 1 index.html#foo.bar.qux - a term including:colon std:term -1 glossary.html#term-a-term-including-colon - '''.encode('utf-8')) From 44b6044d4b5c84a3ec633a7bc50fde23d23ab7cb Mon Sep 17 00:00:00 2001 From: Takeshi KOMIYA Date: Sun, 23 Apr 2017 11:33:49 +0900 Subject: [PATCH 19/23] Implement get_full_qualified_name() to CPPDomain (refs: #3630) --- sphinx/domains/cpp.py | 14 +++++++++++++ .../test-ext-intersphinx-cppdomain/conf.py | 4 ++++ .../test-ext-intersphinx-cppdomain/index.rst | 6 ++++++ tests/test_domain_cpp.py | 2 ++ tests/test_ext_intersphinx.py | 20 +++++++++++++++++++ tests/test_util_inventory.py | 2 ++ 6 files changed, 48 insertions(+) create mode 100644 tests/roots/test-ext-intersphinx-cppdomain/conf.py create mode 100644 tests/roots/test-ext-intersphinx-cppdomain/index.rst diff --git a/sphinx/domains/cpp.py b/sphinx/domains/cpp.py index 2fceeded3..b1e83a041 100644 --- a/sphinx/domains/cpp.py +++ b/sphinx/domains/cpp.py @@ -5032,6 +5032,20 @@ class CPPDomain(Domain): newestId = symbol.declaration.get_newest_id() yield (name, name, objectType, docname, newestId, 1) + def get_full_qualified_name(self, node): + # type: (nodes.Node) -> unicode + target = node.get('reftarget', None) + if target is None: + return None + parentKey = node.get("cpp:parent_key", None) + if parentKey is None: + return None + + rootSymbol = self.data['root_symbol'] + parentSymbol = rootSymbol.direct_lookup(parentKey) + parentName = parentSymbol.get_full_nested_name() + return '::'.join([text_type(parentName), target]) + def setup(app): # type: (Sphinx) -> Dict[unicode, Any] diff --git a/tests/roots/test-ext-intersphinx-cppdomain/conf.py b/tests/roots/test-ext-intersphinx-cppdomain/conf.py new file mode 100644 index 000000000..cbc4a8f43 --- /dev/null +++ b/tests/roots/test-ext-intersphinx-cppdomain/conf.py @@ -0,0 +1,4 @@ +# -*- coding: utf-8 -*- + +extensions = ['sphinx.ext.intersphinx'] +master_doc = 'index' diff --git a/tests/roots/test-ext-intersphinx-cppdomain/index.rst b/tests/roots/test-ext-intersphinx-cppdomain/index.rst new file mode 100644 index 000000000..b5397f856 --- /dev/null +++ b/tests/roots/test-ext-intersphinx-cppdomain/index.rst @@ -0,0 +1,6 @@ +test-ext-intersphinx-cppdomain +============================== + +.. cpp:namespace:: foo + +:cpp:class:`Bar` diff --git a/tests/test_domain_cpp.py b/tests/test_domain_cpp.py index 49d8e3206..3b2b3ba10 100644 --- a/tests/test_domain_cpp.py +++ b/tests/test_domain_cpp.py @@ -13,6 +13,8 @@ import re from six import text_type import pytest +from mock import Mock +from docutils import nodes from sphinx import addnodes from sphinx.domains.cpp import DefinitionParser, DefinitionError, NoOldIdError diff --git a/tests/test_ext_intersphinx.py b/tests/test_ext_intersphinx.py index 4774604e8..47f2029dc 100644 --- a/tests/test_ext_intersphinx.py +++ b/tests/test_ext_intersphinx.py @@ -212,6 +212,26 @@ def test_missing_reference_stddomain(tempdir, app, status, warning): assert rn.astext() == 'ls -l' +@pytest.mark.sphinx('html', testroot='ext-intersphinx-cppdomain') +def test_missing_reference_cppdomain(tempdir, app, status, warning): + inv_file = tempdir / 'inventory' + inv_file.write_bytes(inventory_v2) + app.config.intersphinx_mapping = { + 'https://docs.python.org/': inv_file, + } + app.config.intersphinx_cache_limit = 0 + + # load the inventory and check if it's done correctly + load_mappings(app) + + app.build() + html = (app.outdir / 'index.html').text() + assert ('' + 'Bar' in html) + + def test_missing_reference_jsdomain(tempdir, app, status, warning): inv_file = tempdir / 'inventory' inv_file.write_bytes(inventory_v2) diff --git a/tests/test_util_inventory.py b/tests/test_util_inventory.py index 35f80d960..ecd2be1a6 100644 --- a/tests/test_util_inventory.py +++ b/tests/test_util_inventory.py @@ -34,6 +34,8 @@ module1 py:module 0 foo.html#module-module1 Long Module desc module2 py:module 0 foo.html#module-$ - module1.func py:function 1 sub/foo.html#$ - CFunc c:function 2 cfunc.html#CFunc - +foo::Bar cpp:class 1 index.html#cpp_foo_bar - +foo::Bar::baz cpp:function 1 index.html#cpp_foo_bar_baz - a term std:term -1 glossary.html#term-a-term - ls.-l std:cmdoption 1 index.html#cmdoption-ls-l - docname std:doc -1 docname.html - From bc8646b1f10b975f39ae31a7a4c05ad0ca3e9568 Mon Sep 17 00:00:00 2001 From: Takeshi KOMIYA Date: Sun, 23 Apr 2017 16:54:00 +0900 Subject: [PATCH 20/23] Drop ununsed import --- tests/test_domain_cpp.py | 2 -- 1 file changed, 2 deletions(-) diff --git a/tests/test_domain_cpp.py b/tests/test_domain_cpp.py index 3b2b3ba10..49d8e3206 100644 --- a/tests/test_domain_cpp.py +++ b/tests/test_domain_cpp.py @@ -13,8 +13,6 @@ import re from six import text_type import pytest -from mock import Mock -from docutils import nodes from sphinx import addnodes from sphinx.domains.cpp import DefinitionParser, DefinitionError, NoOldIdError From 10585c5283af312033e1ef5a81a4b12fe4093cb2 Mon Sep 17 00:00:00 2001 From: Takeshi KOMIYA Date: Sun, 23 Apr 2017 17:11:01 +0900 Subject: [PATCH 21/23] Fix to use registry.load_extension() instead --- sphinx/theming.py | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/sphinx/theming.py b/sphinx/theming.py index 5e023e5a1..c769cfda0 100644 --- a/sphinx/theming.py +++ b/sphinx/theming.py @@ -23,7 +23,6 @@ from six.moves import configparser from sphinx import package_dir from sphinx.deprecation import RemovedInSphinx20Warning from sphinx.errors import ThemeError -from sphinx.extension import load_extension from sphinx.locale import _ from sphinx.util import logging from sphinx.util.osutil import ensuredir @@ -224,7 +223,7 @@ class HTMLThemeFactory(object): entry_points = pkg_resources.iter_entry_points('sphinx.html_themes', name) try: entry_point = next(entry_points) - load_extension(self.app, entry_point.module_name) + self.app.registry.load_extension(self.app, entry_point.module_name) return except StopIteration: pass From 6cdc0542a6d84df16f37c77572ca3e3d66b553e1 Mon Sep 17 00:00:00 2001 From: Takeshi KOMIYA Date: Sun, 23 Apr 2017 17:59:39 +0900 Subject: [PATCH 22/23] Fix coding styles --- tests/test_ext_imgconverter.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tests/test_ext_imgconverter.py b/tests/test_ext_imgconverter.py index 3ea396093..9baf0eb2a 100644 --- a/tests/test_ext_imgconverter.py +++ b/tests/test_ext_imgconverter.py @@ -17,6 +17,6 @@ def test_ext_imgconverter(app, status, warning): app.builder.build_all() content = (app.outdir / 'Python.tex').text() - assert '\sphinxincludegraphics{{svgimg}.png}' in content + assert '\\sphinxincludegraphics{{svgimg}.png}' in content assert not (app.outdir / 'svgimg.svg').exists() assert (app.outdir / 'svgimg.png').exists() From 840ae67080750c2f3b8ad67088b7a40dace21853 Mon Sep 17 00:00:00 2001 From: Takeshi KOMIYA Date: Sun, 23 Apr 2017 19:16:39 +0900 Subject: [PATCH 23/23] Fix typo --- sphinx/registry.py | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/sphinx/registry.py b/sphinx/registry.py index 72c9b7ae6..a86fd8915 100644 --- a/sphinx/registry.py +++ b/sphinx/registry.py @@ -1,9 +1,9 @@ # -*- coding: utf-8 -*- """ - sphinx.registory - ~~~~~~~~~~~~~~~~ + sphinx.registry + ~~~~~~~~~~~~~~~ - Sphinx component registory. + Sphinx component registry. :copyright: Copyright 2007-2016 by the Sphinx team, see AUTHORS. :license: BSD, see LICENSE for details.