From d45c0d33f235e549e10fdaae4a018082061a592d Mon Sep 17 00:00:00 2001 From: Takeshi KOMIYA Date: Fri, 12 Apr 2019 01:46:13 +0900 Subject: [PATCH 01/17] Fix #6271: make clean is catastrophically broken if building into '.' --- CHANGES | 1 + sphinx/cmd/make_mode.py | 8 ++++++++ 2 files changed, 9 insertions(+) diff --git a/CHANGES b/CHANGES index 6d043f802..bef23d24b 100644 --- a/CHANGES +++ b/CHANGES @@ -68,6 +68,7 @@ Features added * #6232: Enable CLI override of Makefile variables * #6212 autosummary: Add :confval:`autosummary_imported_members` to display imported members on autosummary +* #6271: ``make clean`` is catastrophically broken if building into '.' Bugs fixed ---------- diff --git a/sphinx/cmd/make_mode.py b/sphinx/cmd/make_mode.py index 82a88933d..e87aa02fc 100644 --- a/sphinx/cmd/make_mode.py +++ b/sphinx/cmd/make_mode.py @@ -72,11 +72,19 @@ class Make: def build_clean(self): # type: () -> int + srcdir = path.abspath(self.srcdir) + builddir = path.abspath(self.builddir) if not path.exists(self.builddir): return 0 elif not path.isdir(self.builddir): print("Error: %r is not a directory!" % self.builddir) return 1 + elif srcdir == builddir: + print("Error: %r is same as source directory!" % self.builddir) + return 1 + elif path.commonpath([srcdir, builddir]) == builddir: + print("Error: %r directory contains source directory!" % self.builddir) + return 1 print("Removing everything under %r..." % self.builddir) for item in os.listdir(self.builddir): rmtree(self.builddir_join(item)) From 830da28e42b89ef2d372781f3a1fc0a09c3e20c3 Mon Sep 17 00:00:00 2001 From: Takeshi KOMIYA Date: Fri, 12 Apr 2019 20:58:10 +0900 Subject: [PATCH 02/17] Add testcases for rst domain --- tests/test_domain_rst.py | 57 ++++++++++++++++++++++++++++++++++++++++ 1 file changed, 57 insertions(+) diff --git a/tests/test_domain_rst.py b/tests/test_domain_rst.py index 70004dcdd..f6ea11619 100644 --- a/tests/test_domain_rst.py +++ b/tests/test_domain_rst.py @@ -8,7 +8,14 @@ :license: BSD, see LICENSE for details. """ +from sphinx import addnodes +from sphinx.addnodes import ( + desc, desc_addname, desc_content, desc_name, desc_optional, desc_parameter, + desc_parameterlist, desc_returns, desc_signature +) from sphinx.domains.rst import parse_directive +from sphinx.testing import restructuredtext +from sphinx.testing.util import assert_node def test_parse_directive(): @@ -23,3 +30,53 @@ def test_parse_directive(): s = parse_directive('.. :: bar') assert s == ('.. :: bar', '') + + +def test_rst_directive(app): + # bare + text = ".. rst:directive:: toctree" + doctree = restructuredtext.parse(app, text) + assert_node(doctree, (addnodes.index, + [desc, ([desc_signature, desc_name, ".. toctree::"], + [desc_content, ()])])) + assert_node(doctree[0], + entries=[("single", "toctree (directive)", "directive-toctree", "", None)]) + assert_node(doctree[1], addnodes.desc, desctype="directive", + domain="rst", objtype="directive", noindex=False) + + # decorated + text = ".. rst:directive:: .. toctree::" + doctree = restructuredtext.parse(app, text) + assert_node(doctree, (addnodes.index, + [desc, ([desc_signature, ([desc_name, ".. toctree::"], + [desc_addname, " "])], + [desc_content, ()])])) + assert_node(doctree[0], + entries=[("single", "toctree (directive)", "directive-toctree", "", None)]) + assert_node(doctree[1], addnodes.desc, desctype="directive", + domain="rst", objtype="directive", noindex=False) + + +def test_rst_directive_with_argument(app): + text = ".. rst:directive:: .. toctree:: foo bar baz" + doctree = restructuredtext.parse(app, text) + assert_node(doctree, (addnodes.index, + [desc, ([desc_signature, ([desc_name, ".. toctree::"], + [desc_addname, " foo bar baz"])], + [desc_content, ()])])) + assert_node(doctree[0], + entries=[("single", "toctree (directive)", "directive-toctree", "", None)]) + assert_node(doctree[1], addnodes.desc, desctype="directive", + domain="rst", objtype="directive", noindex=False) + + +def test_rst_role(app): + text = ".. rst:role:: ref" + doctree = restructuredtext.parse(app, text) + assert_node(doctree, (addnodes.index, + [desc, ([desc_signature, desc_name, ":ref:"], + [desc_content, ()])])) + assert_node(doctree[0], + entries=[("single", "ref (role)", "role-ref", "", None)]) + assert_node(doctree[1], addnodes.desc, desctype="role", + domain="rst", objtype="role", noindex=False) From d41cae328ec139b4d65b60041f561657cfef86ab Mon Sep 17 00:00:00 2001 From: Takeshi KOMIYA Date: Sat, 13 Apr 2019 23:13:00 +0900 Subject: [PATCH 03/17] Add sphinx.util.inspect:isattributedescriptor() --- sphinx/ext/autodoc/__init__.py | 22 ++++++++----------- sphinx/util/inspect.py | 39 ++++++++++++++++++++++++++++++++++ tests/test_util_inspect.py | 27 +++++++++++++++++++++++ 3 files changed, 75 insertions(+), 13 deletions(-) diff --git a/sphinx/ext/autodoc/__init__.py b/sphinx/ext/autodoc/__init__.py index 4c1032db5..3eeec4523 100644 --- a/sphinx/ext/autodoc/__init__.py +++ b/sphinx/ext/autodoc/__init__.py @@ -1340,17 +1340,14 @@ class AttributeDocumenter(DocstringStripSignatureMixin, ClassLevelDocumenter): @classmethod def can_document_member(cls, member, membername, isattr, parent): # type: (Any, str, bool, Any) -> bool - non_attr_types = (type, MethodDescriptorType) - isdatadesc = inspect.isdescriptor(member) and not \ - cls.is_function_or_method(member) and not \ - isinstance(member, non_attr_types) and not \ - type(member).__name__ == "instancemethod" - # That last condition addresses an obscure case of C-defined - # methods using a deprecated type in Python 3, that is not otherwise - # exported anywhere by Python - return isdatadesc or (not isinstance(parent, ModuleDocumenter) and - not inspect.isroutine(member) and - not isinstance(member, type)) + if inspect.isattributedescriptor(member): + return True + elif (not isinstance(parent, ModuleDocumenter) and + not inspect.isroutine(member) and + not isinstance(member, type)): + return True + else: + return False def document_members(self, all_members=False): # type: (bool) -> None @@ -1361,8 +1358,7 @@ class AttributeDocumenter(DocstringStripSignatureMixin, ClassLevelDocumenter): ret = super().import_object() if inspect.isenumattribute(self.object): self.object = self.object.value - if inspect.isdescriptor(self.object) and \ - not self.is_function_or_method(self.object): + if inspect.isattributedescriptor(self.object): self._datadescriptor = True else: # if it's not a data descriptor diff --git a/sphinx/util/inspect.py b/sphinx/util/inspect.py index 0cf7c1084..877f727d4 100644 --- a/sphinx/util/inspect.py +++ b/sphinx/util/inspect.py @@ -29,6 +29,17 @@ if False: # For type annotation from typing import Any, Callable, Mapping, List, Tuple, Type # NOQA +if sys.version_info > (3, 7): + from types import ( + ClassMethodDescriptorType, + MethodDescriptorType, + WrapperDescriptorType + ) +else: + ClassMethodDescriptorType = type(object.__init__) + MethodDescriptorType = type(str.join) + WrapperDescriptorType = type(dict.__dict__['fromkeys']) + logger = logging.getLogger(__name__) memory_address_re = re.compile(r' at 0x[0-9a-f]{8,16}(?=>)', re.IGNORECASE) @@ -161,6 +172,34 @@ def isdescriptor(x): return False +def isattributedescriptor(obj): + # type: (Any) -> bool + """Check if the object is an attribute like descriptor.""" + if inspect.isdatadescriptor(object): + # data descriptor is kind of attribute + return True + elif isdescriptor(obj): + # non data descriptor + if isfunction(obj) or isbuiltin(obj) or inspect.ismethod(obj): + # attribute must not be either function, builtin and method + return False + elif inspect.isclass(obj): + # attribute must not be a class + return False + elif isinstance(obj, (ClassMethodDescriptorType, + MethodDescriptorType, + WrapperDescriptorType)): + # attribute must not be a method descriptor + return False + elif type(obj).__name__ == "instancemethod": + # attribute must not be an instancemethod (C-API) + return False + else: + return True + else: + return False + + def isfunction(obj): # type: (Any) -> bool """Check if the object is function.""" diff --git a/tests/test_util_inspect.py b/tests/test_util_inspect.py index d167c1740..275206526 100644 --- a/tests/test_util_inspect.py +++ b/tests/test_util_inspect.py @@ -7,8 +7,12 @@ :copyright: Copyright 2007-2019 by the Sphinx team, see AUTHORS. :license: BSD, see LICENSE for details. """ + +import _testcapi +import datetime import functools import sys +import types from textwrap import dedent import pytest @@ -432,3 +436,26 @@ def test_isdescriptor(app): assert inspect.isdescriptor(Base.meth) is True # method of class assert inspect.isdescriptor(Base().meth) is True # method of instance assert inspect.isdescriptor(func) is True # function + + +@pytest.mark.sphinx(testroot='ext-autodoc') +def test_isattributedescriptor(app): + from target.methods import Base + + class Descriptor: + def __get__(self, obj, typ=None): + pass + + testinstancemethod = _testcapi.instancemethod(str.__repr__) + + assert inspect.isattributedescriptor(Base.prop) is True # property + assert inspect.isattributedescriptor(Base.meth) is False # method + assert inspect.isattributedescriptor(Base.staticmeth) is False # staticmethod + assert inspect.isattributedescriptor(Base.classmeth) is False # classmetho + assert inspect.isattributedescriptor(Descriptor) is False # custom descriptor class # NOQA + assert inspect.isattributedescriptor(str.join) is False # MethodDescriptorType # NOQA + assert inspect.isattributedescriptor(object.__init__) is False # WrapperDescriptorType # NOQA + assert inspect.isattributedescriptor(dict.__dict__['fromkeys']) is False # ClassMethodDescriptorType # NOQA + assert inspect.isattributedescriptor(types.FrameType.f_locals) is True # GetSetDescriptorType # NOQA + assert inspect.isattributedescriptor(datetime.timedelta.days) is True # MemberDescriptorType # NOQA + assert inspect.isattributedescriptor(testinstancemethod) is False # instancemethod (C-API) # NOQA From 0f0ca79e06b3b6aa9e22327eb263b148574825eb Mon Sep 17 00:00:00 2001 From: Alex Sergeev Date: Fri, 12 Apr 2019 20:32:20 -0700 Subject: [PATCH 04/17] Add imported-members to the directive whitelist --- sphinx/ext/autodoc/directive.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/sphinx/ext/autodoc/directive.py b/sphinx/ext/autodoc/directive.py index 8b41d7fe1..42415433b 100644 --- a/sphinx/ext/autodoc/directive.py +++ b/sphinx/ext/autodoc/directive.py @@ -30,7 +30,8 @@ logger = logging.getLogger(__name__) # common option names for autodoc directives AUTODOC_DEFAULT_OPTIONS = ['members', 'undoc-members', 'inherited-members', 'show-inheritance', 'private-members', 'special-members', - 'ignore-module-all', 'exclude-members', 'member-order'] + 'ignore-module-all', 'exclude-members', 'member-order', + 'imported-members'] class DummyOptionSpec(dict): From 4bad5bd419c56c063e75641888dd5f03bd0f9eba Mon Sep 17 00:00:00 2001 From: Alex Sergeev Date: Sat, 13 Apr 2019 20:47:05 -1000 Subject: [PATCH 05/17] Address feedback --- doc/usage/extensions/autodoc.rst | 4 ++-- tests/test_autodoc.py | 11 +++++++++++ 2 files changed, 13 insertions(+), 2 deletions(-) diff --git a/doc/usage/extensions/autodoc.rst b/doc/usage/extensions/autodoc.rst index 6d7ba8272..e2c3b28cb 100644 --- a/doc/usage/extensions/autodoc.rst +++ b/doc/usage/extensions/autodoc.rst @@ -359,8 +359,8 @@ There are also config values that you can set: This value is a list of autodoc directive flags that should be automatically applied to all autodoc directives. The supported flags are ``'members'``, ``'undoc-members'``, ``'private-members'``, ``'special-members'``, - ``'inherited-members'``, ``'show-inheritance'``, ``'ignore-module-all'`` - and ``'exclude-members'``. + ``'inherited-members'``, ``'show-inheritance'``, ``'ignore-module-all'``, + ``'imported-members'`` and ``'exclude-members'``. .. versionadded:: 1.0 diff --git a/tests/test_autodoc.py b/tests/test_autodoc.py index 27412a9da..6512ca66c 100644 --- a/tests/test_autodoc.py +++ b/tests/test_autodoc.py @@ -1551,6 +1551,8 @@ def test_autodoc_default_options(app): assert ' .. py:attribute:: EnumCls.val4' not in actual actual = do_autodoc(app, 'class', 'target.CustomIter') assert ' .. py:method:: target.CustomIter' not in actual + actual = do_autodoc(app, 'module', 'target') + assert '.. py:function:: save_traceback(app)' not in actual # with :members: app.config.autodoc_default_options = {'members': None} @@ -1614,6 +1616,15 @@ def test_autodoc_default_options(app): assert ' .. py:method:: CustomIter.snafucate()' in actual assert ' Makes this snafucated.' in actual + # with :imported-members: + app.config.autodoc_default_options = { + 'members': None, + 'imported-members': None, + 'ignore-module-all': None, + } + actual = do_autodoc(app, 'module', 'target') + assert '.. py:function:: save_traceback(app)' in actual + @pytest.mark.sphinx('html', testroot='ext-autodoc') def test_autodoc_default_options_with_values(app): From 15daf84f1ad9b952bbb608bf8ee7012996049645 Mon Sep 17 00:00:00 2001 From: Alex Sergeev Date: Fri, 12 Apr 2019 20:12:55 -0700 Subject: [PATCH 06/17] Add support for bound methods posing as functions in the module --- sphinx/ext/autodoc/__init__.py | 3 ++- tests/test_autodoc.py | 14 ++++++++++++++ 2 files changed, 16 insertions(+), 1 deletion(-) diff --git a/sphinx/ext/autodoc/__init__.py b/sphinx/ext/autodoc/__init__.py index 4c1032db5..37bdbc09c 100644 --- a/sphinx/ext/autodoc/__init__.py +++ b/sphinx/ext/autodoc/__init__.py @@ -993,7 +993,8 @@ class FunctionDocumenter(DocstringSignatureMixin, ModuleLevelDocumenter): # typ @classmethod def can_document_member(cls, member, membername, isattr, parent): # type: (Any, str, bool, Any) -> bool - return inspect.isfunction(member) or inspect.isbuiltin(member) + return (inspect.isfunction(member) or inspect.isbuiltin(member) or + (inspect.isroutine(member) and isinstance(parent, ModuleDocumenter))) def format_args(self): # type: () -> str diff --git a/tests/test_autodoc.py b/tests/test_autodoc.py index 27412a9da..3faf5d9db 100644 --- a/tests/test_autodoc.py +++ b/tests/test_autodoc.py @@ -259,6 +259,11 @@ def test_format_signature(): assert formatsig('method', 'H.foo', H.foo2, None, None) == '(*c)' assert formatsig('method', 'H.foo', H.foo3, None, None) == r"(d='\\n')" + # test bound methods interpreted as functions + assert formatsig('function', 'foo', H().foo1, None, None) == '(b, *c)' + assert formatsig('function', 'foo', H().foo2, None, None) == '(*c)' + assert formatsig('function', 'foo', H().foo3, None, None) == r"(d='\\n')" + # test exception handling (exception is caught and args is '') directive.env.config.autodoc_docstring_signature = False assert formatsig('function', 'int', int, None, None) == '' @@ -282,6 +287,7 @@ def test_format_signature(): '(b, c=42, *d, **e)' + @pytest.mark.usefixtures('setup_test') def test_get_doc(): def getdocl(objtype, obj): @@ -451,6 +457,14 @@ def test_get_doc(): directive.env.config.autoclass_content = 'both' assert getdocl('class', I) == ['Class docstring', '', 'New docstring'] + # verify that method docstrings get extracted in both normal case + # and in case of bound method posing as a function + class J: # NOQA + def foo(self): + """Method docstring""" + assert getdocl('method', J.foo) == ['Method docstring'] + assert getdocl('function', J().foo) == ['Method docstring'] + from target import Base, Derived # NOTE: inspect.getdoc seems not to work with locally defined classes From b7f6657dd11b9131ff301cdc0066f14055fa83c0 Mon Sep 17 00:00:00 2001 From: Alex Sergeev Date: Sat, 13 Apr 2019 22:08:18 -1000 Subject: [PATCH 07/17] Address feedback --- sphinx/ext/autodoc/__init__.py | 1 + .../test-ext-autodoc/target/bound_method.py | 7 +++++++ tests/test_autodoc.py | 18 +++++++++++++++++- 3 files changed, 25 insertions(+), 1 deletion(-) create mode 100644 tests/roots/test-ext-autodoc/target/bound_method.py diff --git a/sphinx/ext/autodoc/__init__.py b/sphinx/ext/autodoc/__init__.py index 37bdbc09c..392c4e3b4 100644 --- a/sphinx/ext/autodoc/__init__.py +++ b/sphinx/ext/autodoc/__init__.py @@ -993,6 +993,7 @@ class FunctionDocumenter(DocstringSignatureMixin, ModuleLevelDocumenter): # typ @classmethod def can_document_member(cls, member, membername, isattr, parent): # type: (Any, str, bool, Any) -> bool + # supports functions, builtins and bound methods exported at the module level return (inspect.isfunction(member) or inspect.isbuiltin(member) or (inspect.isroutine(member) and isinstance(parent, ModuleDocumenter))) diff --git a/tests/roots/test-ext-autodoc/target/bound_method.py b/tests/roots/test-ext-autodoc/target/bound_method.py new file mode 100644 index 000000000..d48b9ee1c --- /dev/null +++ b/tests/roots/test-ext-autodoc/target/bound_method.py @@ -0,0 +1,7 @@ +class Cls: + def method(self): + """Method docstring""" + pass + + +bound_method = Cls().method diff --git a/tests/test_autodoc.py b/tests/test_autodoc.py index 3faf5d9db..07e82b54a 100644 --- a/tests/test_autodoc.py +++ b/tests/test_autodoc.py @@ -287,7 +287,6 @@ def test_format_signature(): '(b, c=42, *d, **e)' - @pytest.mark.usefixtures('setup_test') def test_get_doc(): def getdocl(objtype, obj): @@ -1477,6 +1476,23 @@ def test_partialfunction(): ] +@pytest.mark.usefixtures('setup_test') +def test_bound_method(): + options = {"members": None} + actual = do_autodoc(app, 'module', 'target.bound_method', options) + assert list(actual) == [ + '', + '.. py:module:: target.bound_method', + '', + '', + '.. py:function:: bound_method()', + ' :module: target.bound_method', + '', + ' Method docstring', + ' ', + ] + + @pytest.mark.usefixtures('setup_test') def test_coroutine(): options = {"members": None} From 1900c729a3e940af49706dabb36addbc6a076ecb Mon Sep 17 00:00:00 2001 From: Takeshi KOMIYA Date: Fri, 12 Apr 2019 21:06:15 +0900 Subject: [PATCH 08/17] Fix #6299: rst:directive directive generates waste space --- CHANGES | 1 + sphinx/domains/rst.py | 5 ++++- tests/test_domain_rst.py | 5 ++--- 3 files changed, 7 insertions(+), 4 deletions(-) diff --git a/CHANGES b/CHANGES index 4246d035f..2b11b4613 100644 --- a/CHANGES +++ b/CHANGES @@ -79,6 +79,7 @@ Bugs fixed is consisted by non-ASCII characters * #6213: ifconfig: contents after headings are not shown * commented term in glossary directive is wrongly recognized +* #6299: rst domain: rst:directive directive generates waste space Testing -------- diff --git a/sphinx/domains/rst.py b/sphinx/domains/rst.py index 716b50105..60f6cf5a8 100644 --- a/sphinx/domains/rst.py +++ b/sphinx/domains/rst.py @@ -80,7 +80,10 @@ def parse_directive(d): if not m: return (dir, '') parsed_dir, parsed_args = m.groups() - return (parsed_dir.strip(), ' ' + parsed_args.strip()) + if parsed_args.strip(): + return (parsed_dir.strip(), ' ' + parsed_args.strip()) + else: + return (parsed_dir.strip(), '') class ReSTDirective(ReSTMarkup): diff --git a/tests/test_domain_rst.py b/tests/test_domain_rst.py index f6ea11619..3310b5752 100644 --- a/tests/test_domain_rst.py +++ b/tests/test_domain_rst.py @@ -23,7 +23,7 @@ def test_parse_directive(): assert s == ('foö', '') s = parse_directive(' .. foö :: ') - assert s == ('foö', ' ') + assert s == ('foö', '') s = parse_directive('.. foö:: args1 args2') assert s == ('foö', ' args1 args2') @@ -48,8 +48,7 @@ def test_rst_directive(app): text = ".. rst:directive:: .. toctree::" doctree = restructuredtext.parse(app, text) assert_node(doctree, (addnodes.index, - [desc, ([desc_signature, ([desc_name, ".. toctree::"], - [desc_addname, " "])], + [desc, ([desc_signature, desc_name, ".. toctree::"], [desc_content, ()])])) assert_node(doctree[0], entries=[("single", "toctree (directive)", "directive-toctree", "", None)]) From ddf7a9b0dade323f7ee604692e85b06492787903 Mon Sep 17 00:00:00 2001 From: Takeshi KOMIYA Date: Fri, 12 Apr 2019 20:28:57 +0900 Subject: [PATCH 09/17] refactor: rst domain: Add objects property and note_object() method --- sphinx/domains/rst.py | 53 ++++++++++++++++++++++++++----------------- 1 file changed, 32 insertions(+), 21 deletions(-) diff --git a/sphinx/domains/rst.py b/sphinx/domains/rst.py index 716b50105..09b330c61 100644 --- a/sphinx/domains/rst.py +++ b/sphinx/domains/rst.py @@ -9,12 +9,14 @@ """ import re +from typing import cast from sphinx import addnodes from sphinx.directives import ObjectDescription from sphinx.domains import Domain, ObjType -from sphinx.locale import _ +from sphinx.locale import _, __ from sphinx.roles import XRefRole +from sphinx.util import logging from sphinx.util.nodes import make_refnode if False: @@ -26,6 +28,8 @@ if False: from sphinx.environment import BuildEnvironment # NOQA +logger = logging.getLogger(__name__) + dir_sig_re = re.compile(r'\.\. (.+?)::(.*)$') @@ -43,14 +47,9 @@ class ReSTMarkup(ObjectDescription): signode['first'] = (not self.names) self.state.document.note_explicit_target(signode) - objects = self.env.domaindata['rst']['objects'] - key = (self.objtype, name) - if key in objects: - self.state_machine.reporter.warning( - 'duplicate description of %s %s, ' % (self.objtype, name) + - 'other instance in ' + self.env.doc2path(objects[key]), - line=self.lineno) - objects[key] = self.env.docname + domain = cast(ReSTDomain, self.env.get_domain('rst')) + domain.note_object(self.objtype, name, location=(self.env.docname, self.lineno)) + indextext = self.get_index_text(self.objtype, name) if indextext: self.indexnode['entries'].append(('single', indextext, @@ -126,42 +125,54 @@ class ReSTDomain(Domain): } initial_data = { 'objects': {}, # fullname -> docname, objtype - } # type: Dict[str, Dict[str, Tuple[str, ObjType]]] + } # type: Dict[str, Dict[Tuple[str, str], str]] + + @property + def objects(self): + # type: () -> Dict[Tuple[str, str], str] + return self.data.setdefault('objects', {}) # (objtype, fullname) -> docname + + def note_object(self, objtype, name, location=None): + # type: (str, str, Any) -> None + if (objtype, name) in self.objects: + docname = self.objects[objtype, name] + logger.warning(__('duplicate description of %s %s, other instance in %s') % + (objtype, name, docname), location=location) + + self.objects[objtype, name] = self.env.docname def clear_doc(self, docname): # type: (str) -> None - for (typ, name), doc in list(self.data['objects'].items()): + for (typ, name), doc in list(self.objects.items()): if doc == docname: - del self.data['objects'][typ, name] + del self.objects[typ, name] def merge_domaindata(self, docnames, otherdata): # type: (List[str], Dict) -> None # XXX check duplicates for (typ, name), doc in otherdata['objects'].items(): if doc in docnames: - self.data['objects'][typ, name] = doc + self.objects[typ, name] = doc def resolve_xref(self, env, fromdocname, builder, typ, target, node, contnode): # type: (BuildEnvironment, str, Builder, str, str, addnodes.pending_xref, nodes.Element) -> nodes.Element # NOQA - objects = self.data['objects'] objtypes = self.objtypes_for_role(typ) for objtype in objtypes: - if (objtype, target) in objects: - return make_refnode(builder, fromdocname, - objects[objtype, target], + todocname = self.objects.get((objtype, target)) + if todocname: + return make_refnode(builder, fromdocname, todocname, objtype + '-' + target, contnode, target + ' ' + objtype) return None def resolve_any_xref(self, env, fromdocname, builder, target, node, contnode): # type: (BuildEnvironment, str, Builder, str, addnodes.pending_xref, nodes.Element) -> List[Tuple[str, nodes.Element]] # NOQA - objects = self.data['objects'] results = [] # type: List[Tuple[str, nodes.Element]] for objtype in self.object_types: - if (objtype, target) in self.data['objects']: + todocname = self.objects.get((objtype, target)) + if todocname: results.append(('rst:' + self.role_for_objtype(objtype), - make_refnode(builder, fromdocname, - objects[objtype, target], + make_refnode(builder, fromdocname, todocname, objtype + '-' + target, contnode, target + ' ' + objtype))) return results From c26d5d7ecd61a83f4e75035d205519a0bd5502f3 Mon Sep 17 00:00:00 2001 From: Takeshi KOMIYA Date: Sun, 14 Apr 2019 19:19:02 +0900 Subject: [PATCH 10/17] refactor: separate ReSTMarkup.get_index_text() to subclasses According to the principle, the parent class; ReSTMarkup should not know about children. --- sphinx/domains/rst.py | 12 ++++++++---- 1 file changed, 8 insertions(+), 4 deletions(-) diff --git a/sphinx/domains/rst.py b/sphinx/domains/rst.py index 716b50105..a7a863373 100644 --- a/sphinx/domains/rst.py +++ b/sphinx/domains/rst.py @@ -58,10 +58,6 @@ class ReSTMarkup(ObjectDescription): def get_index_text(self, objectname, name): # type: (str, str) -> str - if self.objtype == 'directive': - return _('%s (directive)') % name - elif self.objtype == 'role': - return _('%s (role)') % name return '' @@ -96,6 +92,10 @@ class ReSTDirective(ReSTMarkup): signode += addnodes.desc_addname(args, args) return name + def get_index_text(self, objectname, name): + # type: (str, str) -> str + return _('%s (directive)') % name + class ReSTRole(ReSTMarkup): """ @@ -106,6 +106,10 @@ class ReSTRole(ReSTMarkup): signode += addnodes.desc_name(':%s:' % sig, ':%s:' % sig) return sig + def get_index_text(self, objectname, name): + # type: (str, str) -> str + return _('%s (role)') % name + class ReSTDomain(Domain): """ReStructuredText domain.""" From 4f1aa2c3296deaf27878c9c9cf4d8c0ca4c3715b Mon Sep 17 00:00:00 2001 From: Takeshi KOMIYA Date: Sun, 14 Apr 2019 19:34:13 +0900 Subject: [PATCH 11/17] Update CHANGES for PR #6288 --- CHANGES | 2 ++ 1 file changed, 2 insertions(+) diff --git a/CHANGES b/CHANGES index 2b11b4613..7f257dae9 100644 --- a/CHANGES +++ b/CHANGES @@ -68,6 +68,8 @@ Features added * ``math`` directive now supports ``:class:`` option * todo: ``todo`` directive now supports ``:name:`` option * #6232: Enable CLI override of Makefile variables +* #6287: autodoc: Unable to document bound instance methods exported as module + functions * #6212 autosummary: Add :confval:`autosummary_imported_members` to display imported members on autosummary * #6271: ``make clean`` is catastrophically broken if building into '.' From 98691baf85745922a2f0d87a9a73a8da44e9fbc6 Mon Sep 17 00:00:00 2001 From: Alex Sergeev Date: Sun, 14 Apr 2019 10:47:50 -1000 Subject: [PATCH 12/17] Address comments --- doc/usage/extensions/autodoc.rst | 11 +++++++---- 1 file changed, 7 insertions(+), 4 deletions(-) diff --git a/doc/usage/extensions/autodoc.rst b/doc/usage/extensions/autodoc.rst index e2c3b28cb..0b6061e78 100644 --- a/doc/usage/extensions/autodoc.rst +++ b/doc/usage/extensions/autodoc.rst @@ -359,8 +359,8 @@ There are also config values that you can set: This value is a list of autodoc directive flags that should be automatically applied to all autodoc directives. The supported flags are ``'members'``, ``'undoc-members'``, ``'private-members'``, ``'special-members'``, - ``'inherited-members'``, ``'show-inheritance'``, ``'ignore-module-all'``, - ``'imported-members'`` and ``'exclude-members'``. + ``'inherited-members'``, ``'show-inheritance'``, ``'ignore-module-all'`` + and ``'exclude-members'``. .. versionadded:: 1.0 @@ -387,14 +387,17 @@ There are also config values that you can set: The supported options are ``'members'``, ``'member-order'``, ``'undoc-members'``, ``'private-members'``, ``'special-members'``, - ``'inherited-members'``, ``'show-inheritance'``, ``'ignore-module-all'`` and - ``'exclude-members'``. + ``'inherited-members'``, ``'show-inheritance'``, ``'ignore-module-all'``, + ``'imported-members'`` and ``'exclude-members'``. .. versionadded:: 1.8 .. versionchanged:: 2.0 Accepts ``True`` as a value. + .. versionchanged:: 2.1 + Added ``'imported-members'``. + .. confval:: autodoc_docstring_signature Functions imported from C modules cannot be introspected, and therefore the From d7025cd1a73fd47e8f415be0e0d1fbcb4a796157 Mon Sep 17 00:00:00 2001 From: Takeshi KOMIYA Date: Mon, 15 Apr 2019 10:25:31 +0900 Subject: [PATCH 13/17] Update CHANGES for PR #6289 --- CHANGES | 2 ++ 1 file changed, 2 insertions(+) diff --git a/CHANGES b/CHANGES index 7f257dae9..fa996f62c 100644 --- a/CHANGES +++ b/CHANGES @@ -70,6 +70,8 @@ Features added * #6232: Enable CLI override of Makefile variables * #6287: autodoc: Unable to document bound instance methods exported as module functions +* #6289: autodoc: :confval:`autodoc_default_options` now supports + ``imported-members`` option * #6212 autosummary: Add :confval:`autosummary_imported_members` to display imported members on autosummary * #6271: ``make clean`` is catastrophically broken if building into '.' From 8e0da4ee382ea952fb2f7918f613e87b51761195 Mon Sep 17 00:00:00 2001 From: Takeshi KOMIYA Date: Tue, 16 Apr 2019 00:05:28 +0900 Subject: [PATCH 14/17] Update deprecation message for source_suffix (refs: #6283) --- sphinx/util/compat.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/sphinx/util/compat.py b/sphinx/util/compat.py index 5d8cbb3db..492c313d0 100644 --- a/sphinx/util/compat.py +++ b/sphinx/util/compat.py @@ -29,7 +29,7 @@ def deprecate_source_parsers(app, config): # type: (Sphinx, Config) -> None if config.source_parsers: warnings.warn('The config variable "source_parsers" is deprecated. ' - 'Please use app.add_source_parser() API instead.', + 'Please update your extension for the parser and remove the setting.', RemovedInSphinx30Warning) for suffix, parser in config.source_parsers.items(): if isinstance(parser, str): From aabeb711233ef0243457487ec41ab1c21785e3af Mon Sep 17 00:00:00 2001 From: Takeshi KOMIYA Date: Tue, 16 Apr 2019 01:27:11 +0900 Subject: [PATCH 15/17] imgmath: Use template file for LaTeX --- CHANGES | 3 ++ doc/extdev/deprecated.rst | 15 ++++++++++ sphinx/ext/imgmath.py | 40 +++++++++++++++++++------ sphinx/templates/imgmath/preview.tex_t | 18 +++++++++++ sphinx/templates/imgmath/template.tex_t | 14 +++++++++ sphinx/util/template.py | 7 +++-- 6 files changed, 85 insertions(+), 12 deletions(-) create mode 100644 sphinx/templates/imgmath/preview.tex_t create mode 100644 sphinx/templates/imgmath/template.tex_t diff --git a/CHANGES b/CHANGES index fa996f62c..c76efe299 100644 --- a/CHANGES +++ b/CHANGES @@ -44,6 +44,9 @@ Deprecated * ``sphinx.ext.autodoc.importer.MockLoader`` * ``sphinx.ext.autodoc.importer.mock()`` * ``sphinx.ext.autosummary.autolink_role()`` +* ``sphinx.ext.imgmath.DOC_BODY`` +* ``sphinx.ext.imgmath.DOC_BODY_PREVIEW`` +* ``sphinx.ext.imgmath.DOC_HEAD`` * ``sphinx.transforms.CitationReferences`` * ``sphinx.transforms.SmartQuotesSkipper`` * ``sphinx.util.docfields.DocFieldTransformer.preprocess_fieldtypes()`` diff --git a/doc/extdev/deprecated.rst b/doc/extdev/deprecated.rst index 97eb0689a..52fdf9f7a 100644 --- a/doc/extdev/deprecated.rst +++ b/doc/extdev/deprecated.rst @@ -177,6 +177,21 @@ The following is a list of deprecated interfaces. - 4.0 - ``sphinx.ext.autosummary.AutoLink`` + * - ``sphinx.ext.imgmath.DOC_BODY`` + - 2.1 + - 4.0 + - N/A + + * - ``sphinx.ext.imgmath.DOC_BODY_PREVIEW`` + - 2.1 + - 4.0 + - N/A + + * - ``sphinx.ext.imgmath.DOC_HEAD`` + - 2.1 + - 4.0 + - N/A + * - ``sphinx.transforms.CitationReferences`` - 2.1 - 4.0 diff --git a/sphinx/ext/imgmath.py b/sphinx/ext/imgmath.py index eb0d35c25..c71db8b12 100644 --- a/sphinx/ext/imgmath.py +++ b/sphinx/ext/imgmath.py @@ -21,12 +21,15 @@ from subprocess import CalledProcessError, PIPE from docutils import nodes import sphinx +from sphinx import package_dir +from sphinx.deprecation import RemovedInSphinx40Warning, deprecated_alias from sphinx.errors import SphinxError from sphinx.locale import _, __ from sphinx.util import logging from sphinx.util.math import get_node_equation_number, wrap_displaymath from sphinx.util.osutil import ensuredir from sphinx.util.png import read_png_depth, write_png_depth +from sphinx.util.template import LaTeXRenderer if False: # For type annotation @@ -38,6 +41,8 @@ if False: logger = logging.getLogger(__name__) +templates_path = path.join(package_dir, 'templates', 'imgmath') + class MathExtError(SphinxError): category = 'Math extension error' @@ -87,19 +92,27 @@ DOC_BODY_PREVIEW = r''' depth_re = re.compile(br'\[\d+ depth=(-?\d+)\]') -def generate_latex_macro(math, config): - # type: (str, Config) -> str +def generate_latex_macro(math, config, confdir=''): + # type: (str, Config, str) -> str """Generate LaTeX macro.""" - fontsize = config.imgmath_font_size - baselineskip = int(round(fontsize * 1.2)) + variables = { + 'fontsize': config.imgmath_font_size, + 'baselineskip': int(round(config.imgmath_font_size * 1.2)), + 'preamble': config.imgmath_latex_preamble, + 'math': math + } - latex = DOC_HEAD + config.imgmath_latex_preamble if config.imgmath_use_preview: - latex += DOC_BODY_PREVIEW % (fontsize, baselineskip, math) + template_name = 'preview.tex_t' else: - latex += DOC_BODY % (fontsize, baselineskip, math) + template_name = 'template.tex_t' - return latex + for template_dir in config.templates_path: + template = path.join(confdir, template_dir, template_name) + if path.exists(template): + return LaTeXRenderer().render(template, variables) + + return LaTeXRenderer(templates_path).render(template_name, variables) def ensure_tempdir(builder): @@ -220,7 +233,7 @@ def render_math(self, math): if image_format not in SUPPORT_FORMAT: raise MathExtError('imgmath_image_format must be either "png" or "svg"') - latex = generate_latex_macro(math, self.builder.config) + latex = generate_latex_macro(math, self.builder.config, self.builder.confdir) filename = "%s.%s" % (sha1(latex.encode()).hexdigest(), image_format) relfn = posixpath.join(self.builder.imgpath, 'math', filename) @@ -332,6 +345,15 @@ def html_visit_displaymath(self, node): raise nodes.SkipNode +deprecated_alias('sphinx.ext.imgmath', + { + 'DOC_BODY': DOC_BODY, + 'DOC_BODY_PREVIEW': DOC_BODY_PREVIEW, + 'DOC_HEAD': DOC_HEAD, + }, + RemovedInSphinx40Warning) + + def setup(app): # type: (Sphinx) -> Dict[str, Any] app.add_html_math_renderer('imgmath', diff --git a/sphinx/templates/imgmath/preview.tex_t b/sphinx/templates/imgmath/preview.tex_t new file mode 100644 index 000000000..719dbd5f7 --- /dev/null +++ b/sphinx/templates/imgmath/preview.tex_t @@ -0,0 +1,18 @@ +\documentclass[12pt]{article} +\usepackage[utf8x]{inputenc} +\usepackage{amsmath} +\usepackage{amsthm} +\usepackage{amssymb} +\usepackage{amsfonts} +\usepackage{anyfontsize} +\usepackage{bm} +\pagestyle{empty} +<%= preamble %> + +\usepackage[active]{preview} + +\begin{document} +\begin{preview} +\fontsize{<%= fontsize %>}{<%= baselineskip %}}\selectfont <%= math %> +\end{preview} +\end{document} diff --git a/sphinx/templates/imgmath/template.tex_t b/sphinx/templates/imgmath/template.tex_t new file mode 100644 index 000000000..19834bdc8 --- /dev/null +++ b/sphinx/templates/imgmath/template.tex_t @@ -0,0 +1,14 @@ +\documentclass[12pt]{article} +\usepackage[utf8x]{inputenc} +\usepackage{amsmath} +\usepackage{amsthm} +\usepackage{amssymb} +\usepackage{amsfonts} +\usepackage{anyfontsize} +\usepackage{bm} +\pagestyle{empty} +<%= preamble %> + +\begin{document} +\fontsize{<%= fontsize %>}{<%= baselineskip %>}\selectfont <%= math %> +\end{document} diff --git a/sphinx/util/template.py b/sphinx/util/template.py index 704a42c05..c33e16819 100644 --- a/sphinx/util/template.py +++ b/sphinx/util/template.py @@ -67,9 +67,10 @@ class SphinxRenderer(FileRenderer): class LaTeXRenderer(SphinxRenderer): - def __init__(self): - # type: () -> None - template_path = os.path.join(package_dir, 'templates', 'latex') + def __init__(self, template_path=None): + # type: (str) -> None + if template_path is None: + template_path = os.path.join(package_dir, 'templates', 'latex') super().__init__(template_path) # use texescape as escape filter From 06f86b06399937bb2018841b5dd5fcbf4e54d7a0 Mon Sep 17 00:00:00 2001 From: Takeshi KOMIYA Date: Sat, 9 Mar 2019 16:46:41 +0900 Subject: [PATCH 16/17] Make EventManager portable So far, we need to bypass application object for modules to emit a event. This make EventManager portable and easy to pass event emitter. This brings modules less coupled with application object. --- doc/extdev/appapi.rst | 2 +- doc/extdev/builderapi.rst | 5 +++++ doc/extdev/envapi.rst | 4 ++++ doc/extdev/utils.rst | 6 ++++++ sphinx/application.py | 20 +++++++------------- sphinx/builders/__init__.py | 14 ++++++++------ sphinx/builders/html.py | 2 +- sphinx/environment/__init__.py | 13 ++++++++----- sphinx/events.py | 28 +++++++++++++++++++++++++--- sphinx/ext/autodoc/__init__.py | 6 +++--- sphinx/ext/todo.py | 2 +- 11 files changed, 69 insertions(+), 33 deletions(-) diff --git a/doc/extdev/appapi.rst b/doc/extdev/appapi.rst index fe64628a4..bb4994909 100644 --- a/doc/extdev/appapi.rst +++ b/doc/extdev/appapi.rst @@ -147,7 +147,7 @@ Sphinx core events ------------------ These events are known to the core. The arguments shown are given to the -registered event handlers. Use :meth:`.connect` in an extension's ``setup`` +registered event handlers. Use :meth:`.Sphinx.connect` in an extension's ``setup`` function (note that ``conf.py`` can also have a ``setup`` function) to connect handlers to the events. Example: diff --git a/doc/extdev/builderapi.rst b/doc/extdev/builderapi.rst index 2c2cf12e3..0ab7a30f4 100644 --- a/doc/extdev/builderapi.rst +++ b/doc/extdev/builderapi.rst @@ -38,3 +38,8 @@ Builder API .. automethod:: write_doc .. automethod:: finish + **Attributes** + + .. attribute:: events + + An :class:`.EventManager` object. diff --git a/doc/extdev/envapi.rst b/doc/extdev/envapi.rst index 1dee6a576..d7ec23925 100644 --- a/doc/extdev/envapi.rst +++ b/doc/extdev/envapi.rst @@ -27,6 +27,10 @@ Build environment API Directory for storing pickled doctrees. + .. attribute:: events + + An :class:`.EventManager` object. + .. attribute:: found_docs A set of all existing docnames. diff --git a/doc/extdev/utils.rst b/doc/extdev/utils.rst index 2a94a34bb..e842f3032 100644 --- a/doc/extdev/utils.rst +++ b/doc/extdev/utils.rst @@ -29,3 +29,9 @@ components (e.g. :class:`.Config`, :class:`.BuildEnvironment` and so on) easily. .. autoclass:: sphinx.transforms.post_transforms.images.ImageConverter :members: + +Utility components +------------------ + +.. autoclass:: sphinx.events.EventManager + :members: diff --git a/sphinx/application.py b/sphinx/application.py index eabc35c14..8590aa4d2 100644 --- a/sphinx/application.py +++ b/sphinx/application.py @@ -187,7 +187,7 @@ class Sphinx: self.warningiserror = warningiserror logging.setup(self, self._status, self._warning) - self.events = EventManager() + self.events = EventManager(self) # keep last few messages for traceback # This will be filled by sphinx.util.logging.LastMessagesWriter @@ -254,7 +254,7 @@ class Sphinx: # now that we know all config values, collect them from conf.py self.config.init_values() - self.emit('config-inited', self.config) + self.events.emit('config-inited', self.config) # create the project self.project = Project(self.srcdir, self.config.source_suffix) @@ -324,7 +324,7 @@ class Sphinx: # type: () -> None self.builder.set_environment(self.env) self.builder.init() - self.emit('builder-inited') + self.events.emit('builder-inited') # ---- main "build" method ------------------------------------------------- @@ -365,10 +365,10 @@ class Sphinx: envfile = path.join(self.doctreedir, ENV_PICKLE_FILENAME) if path.isfile(envfile): os.unlink(envfile) - self.emit('build-finished', err) + self.events.emit('build-finished', err) raise else: - self.emit('build-finished', None) + self.events.emit('build-finished', None) self.builder.cleanup() # ---- general extensibility interface ------------------------------------- @@ -437,13 +437,7 @@ class Sphinx: Return the return values of all callbacks as a list. Do not emit core Sphinx events in extensions! """ - try: - logger.debug('[app] emitting event: %r%s', event, repr(args)[:100]) - except Exception: - # not every object likes to be repr()'d (think - # random stuff coming via autodoc) - pass - return self.events.emit(event, self, *args) + return self.events.emit(event, *args) def emit_firstresult(self, event, *args): # type: (str, Any) -> Any @@ -453,7 +447,7 @@ class Sphinx: .. versionadded:: 0.5 """ - return self.events.emit_firstresult(event, self, *args) + return self.events.emit_firstresult(event, *args) # registering addon parts diff --git a/sphinx/builders/__init__.py b/sphinx/builders/__init__.py index 1b29fa983..8eaa0e215 100644 --- a/sphinx/builders/__init__.py +++ b/sphinx/builders/__init__.py @@ -43,6 +43,7 @@ if False: from sphinx.application import Sphinx # NOQA from sphinx.config import Config # NOQA from sphinx.environment import BuildEnvironment # NOQA + from sphinx.events import EventManager # NOQA from sphinx.util.i18n import CatalogInfo # NOQA from sphinx.util.tags import Tags # NOQA @@ -93,6 +94,7 @@ class Builder: self.app = app # type: Sphinx self.env = None # type: BuildEnvironment + self.events = app.events # type: EventManager self.config = app.config # type: Config self.tags = app.tags # type: Tags self.tags.add(self.format) @@ -399,7 +401,7 @@ class Builder: added, changed, removed = self.env.get_outdated_files(updated) # allow user intervention as well - for docs in self.app.emit('env-get-outdated', self, added, changed, removed): + for docs in self.events.emit('env-get-outdated', self, added, changed, removed): changed.update(set(docs) & self.env.found_docs) # if files were added or removed, all documents with globbed toctrees @@ -416,13 +418,13 @@ class Builder: # clear all files no longer present for docname in removed: - self.app.emit('env-purge-doc', self.env, docname) + self.events.emit('env-purge-doc', self.env, docname) self.env.clear_doc(docname) # read all new and changed files docnames = sorted(added | changed) # allow changing and reordering the list of docs to read - self.app.emit('env-before-read-docs', self.env, docnames) + self.events.emit('env-before-read-docs', self.env, docnames) # check if we should do parallel or serial read if parallel_available and len(docnames) > 5 and self.app.parallel > 1: @@ -439,7 +441,7 @@ class Builder: raise SphinxError('master file %s not found' % self.env.doc2path(self.config.master_doc)) - for retval in self.app.emit('env-updated', self.env): + for retval in self.events.emit('env-updated', self.env): if retval is not None: docnames.extend(retval) @@ -453,7 +455,7 @@ class Builder: for docname in status_iterator(docnames, __('reading sources... '), "purple", len(docnames), self.app.verbosity): # remove all inventory entries for that file - self.app.emit('env-purge-doc', self.env, docname) + self.events.emit('env-purge-doc', self.env, docname) self.env.clear_doc(docname) self.read_doc(docname) @@ -461,7 +463,7 @@ class Builder: # type: (List[str], int) -> None # clear all outdated docs at once for docname in docnames: - self.app.emit('env-purge-doc', self.env, docname) + self.events.emit('env-purge-doc', self.env, docname) self.env.clear_doc(docname) def read_process(docs): diff --git a/sphinx/builders/html.py b/sphinx/builders/html.py index 287b96a92..504bbc598 100644 --- a/sphinx/builders/html.py +++ b/sphinx/builders/html.py @@ -686,7 +686,7 @@ class StandaloneHTMLBuilder(Builder): def gen_additional_pages(self): # type: () -> None # pages from extensions - for pagelist in self.app.emit('html-collect-pages'): + for pagelist in self.events.emit('html-collect-pages'): for pagename, context, template in pagelist: self.handle_page(pagename, context, template) diff --git a/sphinx/environment/__init__.py b/sphinx/environment/__init__.py index e3d2c1f49..af3b6cf1f 100644 --- a/sphinx/environment/__init__.py +++ b/sphinx/environment/__init__.py @@ -37,6 +37,7 @@ if False: from sphinx.application import Sphinx # NOQA from sphinx.builders import Builder # NOQA from sphinx.config import Config # NOQA + from sphinx.event import EventManager # NOQA from sphinx.domains import Domain # NOQA from sphinx.project import Project # NOQA @@ -98,6 +99,7 @@ class BuildEnvironment: self.srcdir = None # type: str self.config = None # type: Config self.config_status = None # type: int + self.events = None # type: EventManager self.project = None # type: Project self.version = None # type: Dict[str, str] @@ -193,7 +195,7 @@ class BuildEnvironment: # type: () -> Dict """Obtains serializable data for pickling.""" __dict__ = self.__dict__.copy() - __dict__.update(app=None, domains={}) # clear unpickable attributes + __dict__.update(app=None, domains={}, events=None) # clear unpickable attributes return __dict__ def __setstate__(self, state): @@ -213,6 +215,7 @@ class BuildEnvironment: self.app = app self.doctreedir = app.doctreedir + self.events = app.events self.srcdir = app.srcdir self.project = app.project self.version = app.registry.get_envversion(app) @@ -310,7 +313,7 @@ class BuildEnvironment: for domainname, domain in self.domains.items(): domain.merge_domaindata(docnames, other.domaindata[domainname]) - app.emit('env-merge-info', self, docnames, other) + self.events.emit('env-merge-info', self, docnames, other) def path2doc(self, filename): # type: (str) -> Optional[str] @@ -452,7 +455,7 @@ class BuildEnvironment: def check_dependents(self, app, already): # type: (Sphinx, Set[str]) -> Iterator[str] to_rewrite = [] # type: List[str] - for docnames in app.emit('env-get-updated', self): + for docnames in self.events.emit('env-get-updated', self): to_rewrite.extend(docnames) for docname in set(to_rewrite): if docname not in already: @@ -600,7 +603,7 @@ class BuildEnvironment: self.temp_data = backup # allow custom references to be resolved - self.app.emit('doctree-resolved', doctree, docname) + self.events.emit('doctree-resolved', doctree, docname) def collect_relations(self): # type: () -> Dict[str, List[str]] @@ -656,7 +659,7 @@ class BuildEnvironment: # call check-consistency for all extensions for domain in self.domains.values(): domain.check_consistency() - self.app.emit('env-check-consistency', self) + self.events.emit('env-check-consistency', self) # --------- METHODS FOR COMPATIBILITY -------------------------------------- diff --git a/sphinx/events.py b/sphinx/events.py index 25a378d7c..334b4e053 100644 --- a/sphinx/events.py +++ b/sphinx/events.py @@ -14,10 +14,14 @@ from collections import OrderedDict, defaultdict from sphinx.errors import ExtensionError from sphinx.locale import __ +from sphinx.util import logging if False: # For type annotation from typing import Any, Callable, Dict, List # NOQA + from sphinx.application import Sphinx # NOQA + +logger = logging.getLogger(__name__) # List of all known core events. Maps name to arguments description. @@ -42,20 +46,25 @@ core_events = { class EventManager: - def __init__(self): - # type: () -> None + """Event manager for Sphinx.""" + + def __init__(self, app): + # type: (Sphinx) -> None + self.app = app self.events = core_events.copy() self.listeners = defaultdict(OrderedDict) # type: Dict[str, Dict[int, Callable]] self.next_listener_id = 0 def add(self, name): # type: (str) -> None + """Register a custom Sphinx event.""" if name in self.events: raise ExtensionError(__('Event %r already present') % name) self.events[name] = '' def connect(self, name, callback): # type: (str, Callable) -> int + """Connect a handler to specific event.""" if name not in self.events: raise ExtensionError(__('Unknown event name: %s') % name) @@ -66,18 +75,31 @@ class EventManager: def disconnect(self, listener_id): # type: (int) -> None + """Disconnect a handler.""" for event in self.listeners.values(): event.pop(listener_id, None) def emit(self, name, *args): # type: (str, Any) -> List + """Emit a Sphinx event.""" + try: + logger.debug('[app] emitting event: %r%s', name, repr(args)[:100]) + except Exception: + # not every object likes to be repr()'d (think + # random stuff coming via autodoc) + pass + results = [] for callback in self.listeners[name].values(): - results.append(callback(*args)) + results.append(callback(self.app, *args)) return results def emit_firstresult(self, name, *args): # type: (str, Any) -> Any + """Emit a Sphinx event and returns first result. + + This returns the result of the first handler that doesn't return ``None``. + """ for result in self.emit(name, *args): if result is not None: return result diff --git a/sphinx/ext/autodoc/__init__.py b/sphinx/ext/autodoc/__init__.py index 5b260c6fe..d10ddb768 100644 --- a/sphinx/ext/autodoc/__init__.py +++ b/sphinx/ext/autodoc/__init__.py @@ -405,9 +405,9 @@ class Documenter: retann = self.retann - result = self.env.app.emit_firstresult( - 'autodoc-process-signature', self.objtype, self.fullname, - self.object, self.options, args, retann) + result = self.env.events.emit_firstresult('autodoc-process-signature', + self.objtype, self.fullname, + self.object, self.options, args, retann) if result: args, retann = result diff --git a/sphinx/ext/todo.py b/sphinx/ext/todo.py index 1922bb49c..f43520036 100644 --- a/sphinx/ext/todo.py +++ b/sphinx/ext/todo.py @@ -86,7 +86,7 @@ def process_todos(app, doctree): if not hasattr(env, 'todo_all_todos'): env.todo_all_todos = [] # type: ignore for node in doctree.traverse(todo_node): - app.emit('todo-defined', node) + app.events.emit('todo-defined', node) newnode = node.deepcopy() newnode['ids'] = [] From 114f73435bfbbe5d4d61442ea18e2b579eac4a92 Mon Sep 17 00:00:00 2001 From: Takeshi KOMIYA Date: Tue, 16 Apr 2019 13:57:46 +0900 Subject: [PATCH 17/17] refactor: Make app argument for EventManager optional to keep compatibility --- sphinx/events.py | 13 +++++++++++-- 1 file changed, 11 insertions(+), 2 deletions(-) diff --git a/sphinx/events.py b/sphinx/events.py index 334b4e053..df72f8f21 100644 --- a/sphinx/events.py +++ b/sphinx/events.py @@ -10,8 +10,10 @@ :license: BSD, see LICENSE for details. """ +import warnings from collections import OrderedDict, defaultdict +from sphinx.deprecation import RemovedInSphinx40Warning from sphinx.errors import ExtensionError from sphinx.locale import __ from sphinx.util import logging @@ -48,8 +50,11 @@ core_events = { class EventManager: """Event manager for Sphinx.""" - def __init__(self, app): + def __init__(self, app=None): # type: (Sphinx) -> None + if app is None: + warnings.warn('app argument is required for EventManager.', + RemovedInSphinx40Warning) self.app = app self.events = core_events.copy() self.listeners = defaultdict(OrderedDict) # type: Dict[str, Dict[int, Callable]] @@ -91,7 +96,11 @@ class EventManager: results = [] for callback in self.listeners[name].values(): - results.append(callback(self.app, *args)) + if self.app is None: + # for compatibility; RemovedInSphinx40Warning + results.append(callback(*args)) + else: + results.append(callback(self.app, *args)) return results def emit_firstresult(self, name, *args):