Merge branch '2.0' into refactor_py_domain2

This commit is contained in:
Takeshi KOMIYA 2019-04-23 01:16:42 +09:00 committed by GitHub
commit a285220778
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
27 changed files with 412 additions and 90 deletions

View File

@ -44,6 +44,9 @@ Deprecated
* ``sphinx.ext.autodoc.importer.MockLoader`` * ``sphinx.ext.autodoc.importer.MockLoader``
* ``sphinx.ext.autodoc.importer.mock()`` * ``sphinx.ext.autodoc.importer.mock()``
* ``sphinx.ext.autosummary.autolink_role()`` * ``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.CitationReferences``
* ``sphinx.transforms.SmartQuotesSkipper`` * ``sphinx.transforms.SmartQuotesSkipper``
* ``sphinx.util.docfields.DocFieldTransformer.preprocess_fieldtypes()`` * ``sphinx.util.docfields.DocFieldTransformer.preprocess_fieldtypes()``
@ -68,8 +71,13 @@ Features added
* ``math`` directive now supports ``:class:`` option * ``math`` directive now supports ``:class:`` option
* todo: ``todo`` directive now supports ``:name:`` option * todo: ``todo`` directive now supports ``:name:`` option
* #6232: Enable CLI override of Makefile variables * #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 * #6212 autosummary: Add :confval:`autosummary_imported_members` to display
imported members on autosummary imported members on autosummary
* #6271: ``make clean`` is catastrophically broken if building into '.'
* Add ``:classmethod:`` and ``:staticmethod:`` options to :rst:dir:`py:method` * Add ``:classmethod:`` and ``:staticmethod:`` options to :rst:dir:`py:method`
directive directive
@ -80,6 +88,7 @@ Bugs fixed
is consisted by non-ASCII characters is consisted by non-ASCII characters
* #6213: ifconfig: contents after headings are not shown * #6213: ifconfig: contents after headings are not shown
* commented term in glossary directive is wrongly recognized * commented term in glossary directive is wrongly recognized
* #6299: rst domain: rst:directive directive generates waste space
Testing Testing
-------- --------

View File

@ -147,7 +147,7 @@ Sphinx core events
------------------ ------------------
These events are known to the core. The arguments shown are given to the 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 function (note that ``conf.py`` can also have a ``setup`` function) to connect
handlers to the events. Example: handlers to the events. Example:

View File

@ -38,3 +38,8 @@ Builder API
.. automethod:: write_doc .. automethod:: write_doc
.. automethod:: finish .. automethod:: finish
**Attributes**
.. attribute:: events
An :class:`.EventManager` object.

View File

@ -177,6 +177,21 @@ The following is a list of deprecated interfaces.
- 4.0 - 4.0
- ``sphinx.ext.autosummary.AutoLink`` - ``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`` * - ``sphinx.transforms.CitationReferences``
- 2.1 - 2.1
- 4.0 - 4.0

View File

@ -27,6 +27,10 @@ Build environment API
Directory for storing pickled doctrees. Directory for storing pickled doctrees.
.. attribute:: events
An :class:`.EventManager` object.
.. attribute:: found_docs .. attribute:: found_docs
A set of all existing docnames. A set of all existing docnames.

View File

@ -29,3 +29,9 @@ components (e.g. :class:`.Config`, :class:`.BuildEnvironment` and so on) easily.
.. autoclass:: sphinx.transforms.post_transforms.images.ImageConverter .. autoclass:: sphinx.transforms.post_transforms.images.ImageConverter
:members: :members:
Utility components
------------------
.. autoclass:: sphinx.events.EventManager
:members:

View File

@ -387,14 +387,17 @@ There are also config values that you can set:
The supported options are ``'members'``, ``'member-order'``, The supported options are ``'members'``, ``'member-order'``,
``'undoc-members'``, ``'private-members'``, ``'special-members'``, ``'undoc-members'``, ``'private-members'``, ``'special-members'``,
``'inherited-members'``, ``'show-inheritance'``, ``'ignore-module-all'`` and ``'inherited-members'``, ``'show-inheritance'``, ``'ignore-module-all'``,
``'exclude-members'``. ``'imported-members'`` and ``'exclude-members'``.
.. versionadded:: 1.8 .. versionadded:: 1.8
.. versionchanged:: 2.0 .. versionchanged:: 2.0
Accepts ``True`` as a value. Accepts ``True`` as a value.
.. versionchanged:: 2.1
Added ``'imported-members'``.
.. confval:: autodoc_docstring_signature .. confval:: autodoc_docstring_signature
Functions imported from C modules cannot be introspected, and therefore the Functions imported from C modules cannot be introspected, and therefore the

View File

@ -187,7 +187,7 @@ class Sphinx:
self.warningiserror = warningiserror self.warningiserror = warningiserror
logging.setup(self, self._status, self._warning) logging.setup(self, self._status, self._warning)
self.events = EventManager() self.events = EventManager(self)
# keep last few messages for traceback # keep last few messages for traceback
# This will be filled by sphinx.util.logging.LastMessagesWriter # 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 # now that we know all config values, collect them from conf.py
self.config.init_values() self.config.init_values()
self.emit('config-inited', self.config) self.events.emit('config-inited', self.config)
# create the project # create the project
self.project = Project(self.srcdir, self.config.source_suffix) self.project = Project(self.srcdir, self.config.source_suffix)
@ -324,7 +324,7 @@ class Sphinx:
# type: () -> None # type: () -> None
self.builder.set_environment(self.env) self.builder.set_environment(self.env)
self.builder.init() self.builder.init()
self.emit('builder-inited') self.events.emit('builder-inited')
# ---- main "build" method ------------------------------------------------- # ---- main "build" method -------------------------------------------------
@ -365,10 +365,10 @@ class Sphinx:
envfile = path.join(self.doctreedir, ENV_PICKLE_FILENAME) envfile = path.join(self.doctreedir, ENV_PICKLE_FILENAME)
if path.isfile(envfile): if path.isfile(envfile):
os.unlink(envfile) os.unlink(envfile)
self.emit('build-finished', err) self.events.emit('build-finished', err)
raise raise
else: else:
self.emit('build-finished', None) self.events.emit('build-finished', None)
self.builder.cleanup() self.builder.cleanup()
# ---- general extensibility interface ------------------------------------- # ---- general extensibility interface -------------------------------------
@ -437,13 +437,7 @@ class Sphinx:
Return the return values of all callbacks as a list. Do not emit core Return the return values of all callbacks as a list. Do not emit core
Sphinx events in extensions! Sphinx events in extensions!
""" """
try: return self.events.emit(event, *args)
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)
def emit_firstresult(self, event, *args): def emit_firstresult(self, event, *args):
# type: (str, Any) -> Any # type: (str, Any) -> Any
@ -453,7 +447,7 @@ class Sphinx:
.. versionadded:: 0.5 .. versionadded:: 0.5
""" """
return self.events.emit_firstresult(event, self, *args) return self.events.emit_firstresult(event, *args)
# registering addon parts # registering addon parts

View File

@ -43,6 +43,7 @@ if False:
from sphinx.application import Sphinx # NOQA from sphinx.application import Sphinx # NOQA
from sphinx.config import Config # NOQA from sphinx.config import Config # NOQA
from sphinx.environment import BuildEnvironment # NOQA from sphinx.environment import BuildEnvironment # NOQA
from sphinx.events import EventManager # NOQA
from sphinx.util.i18n import CatalogInfo # NOQA from sphinx.util.i18n import CatalogInfo # NOQA
from sphinx.util.tags import Tags # NOQA from sphinx.util.tags import Tags # NOQA
@ -93,6 +94,7 @@ class Builder:
self.app = app # type: Sphinx self.app = app # type: Sphinx
self.env = None # type: BuildEnvironment self.env = None # type: BuildEnvironment
self.events = app.events # type: EventManager
self.config = app.config # type: Config self.config = app.config # type: Config
self.tags = app.tags # type: Tags self.tags = app.tags # type: Tags
self.tags.add(self.format) self.tags.add(self.format)
@ -399,7 +401,7 @@ class Builder:
added, changed, removed = self.env.get_outdated_files(updated) added, changed, removed = self.env.get_outdated_files(updated)
# allow user intervention as well # 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) changed.update(set(docs) & self.env.found_docs)
# if files were added or removed, all documents with globbed toctrees # if files were added or removed, all documents with globbed toctrees
@ -416,13 +418,13 @@ class Builder:
# clear all files no longer present # clear all files no longer present
for docname in removed: 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) self.env.clear_doc(docname)
# read all new and changed files # read all new and changed files
docnames = sorted(added | changed) docnames = sorted(added | changed)
# allow changing and reordering the list of docs to read # 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 # check if we should do parallel or serial read
if parallel_available and len(docnames) > 5 and self.app.parallel > 1: 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' % raise SphinxError('master file %s not found' %
self.env.doc2path(self.config.master_doc)) 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: if retval is not None:
docnames.extend(retval) docnames.extend(retval)
@ -453,7 +455,7 @@ class Builder:
for docname in status_iterator(docnames, __('reading sources... '), "purple", for docname in status_iterator(docnames, __('reading sources... '), "purple",
len(docnames), self.app.verbosity): len(docnames), self.app.verbosity):
# remove all inventory entries for that file # 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.env.clear_doc(docname)
self.read_doc(docname) self.read_doc(docname)
@ -461,7 +463,7 @@ class Builder:
# type: (List[str], int) -> None # type: (List[str], int) -> None
# clear all outdated docs at once # clear all outdated docs at once
for docname in docnames: 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) self.env.clear_doc(docname)
def read_process(docs): def read_process(docs):

View File

@ -686,7 +686,7 @@ class StandaloneHTMLBuilder(Builder):
def gen_additional_pages(self): def gen_additional_pages(self):
# type: () -> None # type: () -> None
# pages from extensions # 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: for pagename, context, template in pagelist:
self.handle_page(pagename, context, template) self.handle_page(pagename, context, template)

View File

@ -72,11 +72,19 @@ class Make:
def build_clean(self): def build_clean(self):
# type: () -> int # type: () -> int
srcdir = path.abspath(self.srcdir)
builddir = path.abspath(self.builddir)
if not path.exists(self.builddir): if not path.exists(self.builddir):
return 0 return 0
elif not path.isdir(self.builddir): elif not path.isdir(self.builddir):
print("Error: %r is not a directory!" % self.builddir) print("Error: %r is not a directory!" % self.builddir)
return 1 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) print("Removing everything under %r..." % self.builddir)
for item in os.listdir(self.builddir): for item in os.listdir(self.builddir):
rmtree(self.builddir_join(item)) rmtree(self.builddir_join(item))

View File

@ -9,12 +9,14 @@
""" """
import re import re
from typing import cast
from sphinx import addnodes from sphinx import addnodes
from sphinx.directives import ObjectDescription from sphinx.directives import ObjectDescription
from sphinx.domains import Domain, ObjType from sphinx.domains import Domain, ObjType
from sphinx.locale import _ from sphinx.locale import _, __
from sphinx.roles import XRefRole from sphinx.roles import XRefRole
from sphinx.util import logging
from sphinx.util.nodes import make_refnode from sphinx.util.nodes import make_refnode
if False: if False:
@ -26,6 +28,8 @@ if False:
from sphinx.environment import BuildEnvironment # NOQA from sphinx.environment import BuildEnvironment # NOQA
logger = logging.getLogger(__name__)
dir_sig_re = re.compile(r'\.\. (.+?)::(.*)$') dir_sig_re = re.compile(r'\.\. (.+?)::(.*)$')
@ -43,14 +47,9 @@ class ReSTMarkup(ObjectDescription):
signode['first'] = (not self.names) signode['first'] = (not self.names)
self.state.document.note_explicit_target(signode) self.state.document.note_explicit_target(signode)
objects = self.env.domaindata['rst']['objects'] domain = cast(ReSTDomain, self.env.get_domain('rst'))
key = (self.objtype, name) domain.note_object(self.objtype, name, location=(self.env.docname, self.lineno))
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
indextext = self.get_index_text(self.objtype, name) indextext = self.get_index_text(self.objtype, name)
if indextext: if indextext:
self.indexnode['entries'].append(('single', indextext, self.indexnode['entries'].append(('single', indextext,
@ -58,10 +57,6 @@ class ReSTMarkup(ObjectDescription):
def get_index_text(self, objectname, name): def get_index_text(self, objectname, name):
# type: (str, str) -> str # type: (str, str) -> str
if self.objtype == 'directive':
return _('%s (directive)') % name
elif self.objtype == 'role':
return _('%s (role)') % name
return '' return ''
@ -80,7 +75,10 @@ def parse_directive(d):
if not m: if not m:
return (dir, '') return (dir, '')
parsed_dir, parsed_args = m.groups() 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): class ReSTDirective(ReSTMarkup):
@ -96,6 +94,10 @@ class ReSTDirective(ReSTMarkup):
signode += addnodes.desc_addname(args, args) signode += addnodes.desc_addname(args, args)
return name return name
def get_index_text(self, objectname, name):
# type: (str, str) -> str
return _('%s (directive)') % name
class ReSTRole(ReSTMarkup): class ReSTRole(ReSTMarkup):
""" """
@ -106,6 +108,10 @@ class ReSTRole(ReSTMarkup):
signode += addnodes.desc_name(':%s:' % sig, ':%s:' % sig) signode += addnodes.desc_name(':%s:' % sig, ':%s:' % sig)
return sig return sig
def get_index_text(self, objectname, name):
# type: (str, str) -> str
return _('%s (role)') % name
class ReSTDomain(Domain): class ReSTDomain(Domain):
"""ReStructuredText domain.""" """ReStructuredText domain."""
@ -126,42 +132,54 @@ class ReSTDomain(Domain):
} }
initial_data = { initial_data = {
'objects': {}, # fullname -> docname, objtype '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): def clear_doc(self, docname):
# type: (str) -> None # 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: if doc == docname:
del self.data['objects'][typ, name] del self.objects[typ, name]
def merge_domaindata(self, docnames, otherdata): def merge_domaindata(self, docnames, otherdata):
# type: (List[str], Dict) -> None # type: (List[str], Dict) -> None
# XXX check duplicates # XXX check duplicates
for (typ, name), doc in otherdata['objects'].items(): for (typ, name), doc in otherdata['objects'].items():
if doc in docnames: 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): 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 # type: (BuildEnvironment, str, Builder, str, str, addnodes.pending_xref, nodes.Element) -> nodes.Element # NOQA
objects = self.data['objects']
objtypes = self.objtypes_for_role(typ) objtypes = self.objtypes_for_role(typ)
for objtype in objtypes: for objtype in objtypes:
if (objtype, target) in objects: todocname = self.objects.get((objtype, target))
return make_refnode(builder, fromdocname, if todocname:
objects[objtype, target], return make_refnode(builder, fromdocname, todocname,
objtype + '-' + target, objtype + '-' + target,
contnode, target + ' ' + objtype) contnode, target + ' ' + objtype)
return None return None
def resolve_any_xref(self, env, fromdocname, builder, target, node, contnode): 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 # 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]] results = [] # type: List[Tuple[str, nodes.Element]]
for objtype in self.object_types: 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), results.append(('rst:' + self.role_for_objtype(objtype),
make_refnode(builder, fromdocname, make_refnode(builder, fromdocname, todocname,
objects[objtype, target],
objtype + '-' + target, objtype + '-' + target,
contnode, target + ' ' + objtype))) contnode, target + ' ' + objtype)))
return results return results

View File

@ -37,6 +37,7 @@ if False:
from sphinx.application import Sphinx # NOQA from sphinx.application import Sphinx # NOQA
from sphinx.builders import Builder # NOQA from sphinx.builders import Builder # NOQA
from sphinx.config import Config # NOQA from sphinx.config import Config # NOQA
from sphinx.event import EventManager # NOQA
from sphinx.domains import Domain # NOQA from sphinx.domains import Domain # NOQA
from sphinx.project import Project # NOQA from sphinx.project import Project # NOQA
@ -98,6 +99,7 @@ class BuildEnvironment:
self.srcdir = None # type: str self.srcdir = None # type: str
self.config = None # type: Config self.config = None # type: Config
self.config_status = None # type: int self.config_status = None # type: int
self.events = None # type: EventManager
self.project = None # type: Project self.project = None # type: Project
self.version = None # type: Dict[str, str] self.version = None # type: Dict[str, str]
@ -193,7 +195,7 @@ class BuildEnvironment:
# type: () -> Dict # type: () -> Dict
"""Obtains serializable data for pickling.""" """Obtains serializable data for pickling."""
__dict__ = self.__dict__.copy() __dict__ = self.__dict__.copy()
__dict__.update(app=None, domains={}) # clear unpickable attributes __dict__.update(app=None, domains={}, events=None) # clear unpickable attributes
return __dict__ return __dict__
def __setstate__(self, state): def __setstate__(self, state):
@ -213,6 +215,7 @@ class BuildEnvironment:
self.app = app self.app = app
self.doctreedir = app.doctreedir self.doctreedir = app.doctreedir
self.events = app.events
self.srcdir = app.srcdir self.srcdir = app.srcdir
self.project = app.project self.project = app.project
self.version = app.registry.get_envversion(app) self.version = app.registry.get_envversion(app)
@ -310,7 +313,7 @@ class BuildEnvironment:
for domainname, domain in self.domains.items(): for domainname, domain in self.domains.items():
domain.merge_domaindata(docnames, other.domaindata[domainname]) 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): def path2doc(self, filename):
# type: (str) -> Optional[str] # type: (str) -> Optional[str]
@ -452,7 +455,7 @@ class BuildEnvironment:
def check_dependents(self, app, already): def check_dependents(self, app, already):
# type: (Sphinx, Set[str]) -> Iterator[str] # type: (Sphinx, Set[str]) -> Iterator[str]
to_rewrite = [] # type: List[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) to_rewrite.extend(docnames)
for docname in set(to_rewrite): for docname in set(to_rewrite):
if docname not in already: if docname not in already:
@ -600,7 +603,7 @@ class BuildEnvironment:
self.temp_data = backup self.temp_data = backup
# allow custom references to be resolved # allow custom references to be resolved
self.app.emit('doctree-resolved', doctree, docname) self.events.emit('doctree-resolved', doctree, docname)
def collect_relations(self): def collect_relations(self):
# type: () -> Dict[str, List[str]] # type: () -> Dict[str, List[str]]
@ -656,7 +659,7 @@ class BuildEnvironment:
# call check-consistency for all extensions # call check-consistency for all extensions
for domain in self.domains.values(): for domain in self.domains.values():
domain.check_consistency() domain.check_consistency()
self.app.emit('env-check-consistency', self) self.events.emit('env-check-consistency', self)
# --------- METHODS FOR COMPATIBILITY -------------------------------------- # --------- METHODS FOR COMPATIBILITY --------------------------------------

View File

@ -10,14 +10,20 @@
:license: BSD, see LICENSE for details. :license: BSD, see LICENSE for details.
""" """
import warnings
from collections import OrderedDict, defaultdict from collections import OrderedDict, defaultdict
from sphinx.deprecation import RemovedInSphinx40Warning
from sphinx.errors import ExtensionError from sphinx.errors import ExtensionError
from sphinx.locale import __ from sphinx.locale import __
from sphinx.util import logging
if False: if False:
# For type annotation # For type annotation
from typing import Any, Callable, Dict, List # NOQA 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. # List of all known core events. Maps name to arguments description.
@ -42,20 +48,28 @@ core_events = {
class EventManager: class EventManager:
def __init__(self): """Event manager for Sphinx."""
# type: () -> None
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.events = core_events.copy()
self.listeners = defaultdict(OrderedDict) # type: Dict[str, Dict[int, Callable]] self.listeners = defaultdict(OrderedDict) # type: Dict[str, Dict[int, Callable]]
self.next_listener_id = 0 self.next_listener_id = 0
def add(self, name): def add(self, name):
# type: (str) -> None # type: (str) -> None
"""Register a custom Sphinx event."""
if name in self.events: if name in self.events:
raise ExtensionError(__('Event %r already present') % name) raise ExtensionError(__('Event %r already present') % name)
self.events[name] = '' self.events[name] = ''
def connect(self, name, callback): def connect(self, name, callback):
# type: (str, Callable) -> int # type: (str, Callable) -> int
"""Connect a handler to specific event."""
if name not in self.events: if name not in self.events:
raise ExtensionError(__('Unknown event name: %s') % name) raise ExtensionError(__('Unknown event name: %s') % name)
@ -66,18 +80,35 @@ class EventManager:
def disconnect(self, listener_id): def disconnect(self, listener_id):
# type: (int) -> None # type: (int) -> None
"""Disconnect a handler."""
for event in self.listeners.values(): for event in self.listeners.values():
event.pop(listener_id, None) event.pop(listener_id, None)
def emit(self, name, *args): def emit(self, name, *args):
# type: (str, Any) -> List # 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 = [] results = []
for callback in self.listeners[name].values(): for callback in self.listeners[name].values():
results.append(callback(*args)) if self.app is None:
# for compatibility; RemovedInSphinx40Warning
results.append(callback(*args))
else:
results.append(callback(self.app, *args))
return results return results
def emit_firstresult(self, name, *args): def emit_firstresult(self, name, *args):
# type: (str, Any) -> Any # 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): for result in self.emit(name, *args):
if result is not None: if result is not None:
return result return result

View File

@ -405,9 +405,9 @@ class Documenter:
retann = self.retann retann = self.retann
result = self.env.app.emit_firstresult( result = self.env.events.emit_firstresult('autodoc-process-signature',
'autodoc-process-signature', self.objtype, self.fullname, self.objtype, self.fullname,
self.object, self.options, args, retann) self.object, self.options, args, retann)
if result: if result:
args, retann = result args, retann = result
@ -993,7 +993,9 @@ class FunctionDocumenter(DocstringSignatureMixin, ModuleLevelDocumenter): # typ
@classmethod @classmethod
def can_document_member(cls, member, membername, isattr, parent): def can_document_member(cls, member, membername, isattr, parent):
# type: (Any, str, bool, Any) -> bool # type: (Any, str, bool, Any) -> bool
return inspect.isfunction(member) or inspect.isbuiltin(member) # 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)))
def format_args(self): def format_args(self):
# type: () -> str # type: () -> str
@ -1347,17 +1349,14 @@ class AttributeDocumenter(DocstringStripSignatureMixin, ClassLevelDocumenter):
@classmethod @classmethod
def can_document_member(cls, member, membername, isattr, parent): def can_document_member(cls, member, membername, isattr, parent):
# type: (Any, str, bool, Any) -> bool # type: (Any, str, bool, Any) -> bool
non_attr_types = (type, MethodDescriptorType) if inspect.isattributedescriptor(member):
isdatadesc = inspect.isdescriptor(member) and not \ return True
cls.is_function_or_method(member) and not \ elif (not isinstance(parent, ModuleDocumenter) and
isinstance(member, non_attr_types) and not \ not inspect.isroutine(member) and
type(member).__name__ == "instancemethod" not isinstance(member, type)):
# That last condition addresses an obscure case of C-defined return True
# methods using a deprecated type in Python 3, that is not otherwise else:
# exported anywhere by Python return False
return isdatadesc or (not isinstance(parent, ModuleDocumenter) and
not inspect.isroutine(member) and
not isinstance(member, type))
def document_members(self, all_members=False): def document_members(self, all_members=False):
# type: (bool) -> None # type: (bool) -> None
@ -1368,8 +1367,7 @@ class AttributeDocumenter(DocstringStripSignatureMixin, ClassLevelDocumenter):
ret = super().import_object() ret = super().import_object()
if inspect.isenumattribute(self.object): if inspect.isenumattribute(self.object):
self.object = self.object.value self.object = self.object.value
if inspect.isdescriptor(self.object) and \ if inspect.isattributedescriptor(self.object):
not self.is_function_or_method(self.object):
self._datadescriptor = True self._datadescriptor = True
else: else:
# if it's not a data descriptor # if it's not a data descriptor

View File

@ -30,7 +30,8 @@ logger = logging.getLogger(__name__)
# common option names for autodoc directives # common option names for autodoc directives
AUTODOC_DEFAULT_OPTIONS = ['members', 'undoc-members', 'inherited-members', AUTODOC_DEFAULT_OPTIONS = ['members', 'undoc-members', 'inherited-members',
'show-inheritance', 'private-members', 'special-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): class DummyOptionSpec(dict):

View File

@ -21,12 +21,15 @@ from subprocess import CalledProcessError, PIPE
from docutils import nodes from docutils import nodes
import sphinx import sphinx
from sphinx import package_dir
from sphinx.deprecation import RemovedInSphinx40Warning, deprecated_alias
from sphinx.errors import SphinxError from sphinx.errors import SphinxError
from sphinx.locale import _, __ from sphinx.locale import _, __
from sphinx.util import logging from sphinx.util import logging
from sphinx.util.math import get_node_equation_number, wrap_displaymath from sphinx.util.math import get_node_equation_number, wrap_displaymath
from sphinx.util.osutil import ensuredir from sphinx.util.osutil import ensuredir
from sphinx.util.png import read_png_depth, write_png_depth from sphinx.util.png import read_png_depth, write_png_depth
from sphinx.util.template import LaTeXRenderer
if False: if False:
# For type annotation # For type annotation
@ -38,6 +41,8 @@ if False:
logger = logging.getLogger(__name__) logger = logging.getLogger(__name__)
templates_path = path.join(package_dir, 'templates', 'imgmath')
class MathExtError(SphinxError): class MathExtError(SphinxError):
category = 'Math extension error' category = 'Math extension error'
@ -87,19 +92,27 @@ DOC_BODY_PREVIEW = r'''
depth_re = re.compile(br'\[\d+ depth=(-?\d+)\]') depth_re = re.compile(br'\[\d+ depth=(-?\d+)\]')
def generate_latex_macro(math, config): def generate_latex_macro(math, config, confdir=''):
# type: (str, Config) -> str # type: (str, Config, str) -> str
"""Generate LaTeX macro.""" """Generate LaTeX macro."""
fontsize = config.imgmath_font_size variables = {
baselineskip = int(round(fontsize * 1.2)) '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: if config.imgmath_use_preview:
latex += DOC_BODY_PREVIEW % (fontsize, baselineskip, math) template_name = 'preview.tex_t'
else: 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): def ensure_tempdir(builder):
@ -220,7 +233,7 @@ def render_math(self, math):
if image_format not in SUPPORT_FORMAT: if image_format not in SUPPORT_FORMAT:
raise MathExtError('imgmath_image_format must be either "png" or "svg"') 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) filename = "%s.%s" % (sha1(latex.encode()).hexdigest(), image_format)
relfn = posixpath.join(self.builder.imgpath, 'math', filename) relfn = posixpath.join(self.builder.imgpath, 'math', filename)
@ -332,6 +345,15 @@ def html_visit_displaymath(self, node):
raise nodes.SkipNode 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): def setup(app):
# type: (Sphinx) -> Dict[str, Any] # type: (Sphinx) -> Dict[str, Any]
app.add_html_math_renderer('imgmath', app.add_html_math_renderer('imgmath',

View File

@ -86,7 +86,7 @@ def process_todos(app, doctree):
if not hasattr(env, 'todo_all_todos'): if not hasattr(env, 'todo_all_todos'):
env.todo_all_todos = [] # type: ignore env.todo_all_todos = [] # type: ignore
for node in doctree.traverse(todo_node): for node in doctree.traverse(todo_node):
app.emit('todo-defined', node) app.events.emit('todo-defined', node)
newnode = node.deepcopy() newnode = node.deepcopy()
newnode['ids'] = [] newnode['ids'] = []

View File

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

View File

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

View File

@ -29,7 +29,7 @@ def deprecate_source_parsers(app, config):
# type: (Sphinx, Config) -> None # type: (Sphinx, Config) -> None
if config.source_parsers: if config.source_parsers:
warnings.warn('The config variable "source_parsers" is deprecated. ' 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) RemovedInSphinx30Warning)
for suffix, parser in config.source_parsers.items(): for suffix, parser in config.source_parsers.items():
if isinstance(parser, str): if isinstance(parser, str):

View File

@ -29,6 +29,17 @@ if False:
# For type annotation # For type annotation
from typing import Any, Callable, Mapping, List, Tuple, Type # NOQA 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__) logger = logging.getLogger(__name__)
memory_address_re = re.compile(r' at 0x[0-9a-f]{8,16}(?=>)', re.IGNORECASE) memory_address_re = re.compile(r' at 0x[0-9a-f]{8,16}(?=>)', re.IGNORECASE)
@ -161,6 +172,34 @@ def isdescriptor(x):
return False 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): def isfunction(obj):
# type: (Any) -> bool # type: (Any) -> bool
"""Check if the object is function.""" """Check if the object is function."""

View File

@ -67,9 +67,10 @@ class SphinxRenderer(FileRenderer):
class LaTeXRenderer(SphinxRenderer): class LaTeXRenderer(SphinxRenderer):
def __init__(self): def __init__(self, template_path=None):
# type: () -> None # type: (str) -> None
template_path = os.path.join(package_dir, 'templates', 'latex') if template_path is None:
template_path = os.path.join(package_dir, 'templates', 'latex')
super().__init__(template_path) super().__init__(template_path)
# use texescape as escape filter # use texescape as escape filter

View File

@ -0,0 +1,7 @@
class Cls:
def method(self):
"""Method docstring"""
pass
bound_method = Cls().method

View File

@ -259,6 +259,11 @@ def test_format_signature():
assert formatsig('method', 'H.foo', H.foo2, None, None) == '(*c)' assert formatsig('method', 'H.foo', H.foo2, None, None) == '(*c)'
assert formatsig('method', 'H.foo', H.foo3, None, None) == r"(d='\\n')" 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 '') # test exception handling (exception is caught and args is '')
directive.env.config.autodoc_docstring_signature = False directive.env.config.autodoc_docstring_signature = False
assert formatsig('function', 'int', int, None, None) == '' assert formatsig('function', 'int', int, None, None) == ''
@ -451,6 +456,14 @@ def test_get_doc():
directive.env.config.autoclass_content = 'both' directive.env.config.autoclass_content = 'both'
assert getdocl('class', I) == ['Class docstring', '', 'New docstring'] 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 from target import Base, Derived
# NOTE: inspect.getdoc seems not to work with locally defined classes # NOTE: inspect.getdoc seems not to work with locally defined classes
@ -1491,6 +1504,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') @pytest.mark.usefixtures('setup_test')
def test_coroutine(): def test_coroutine():
options = {"members": None} options = {"members": None}
@ -1579,6 +1609,8 @@ def test_autodoc_default_options(app):
assert ' .. py:attribute:: EnumCls.val4' not in actual assert ' .. py:attribute:: EnumCls.val4' not in actual
actual = do_autodoc(app, 'class', 'target.CustomIter') actual = do_autodoc(app, 'class', 'target.CustomIter')
assert ' .. py:method:: target.CustomIter' not in actual 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: # with :members:
app.config.autodoc_default_options = {'members': None} app.config.autodoc_default_options = {'members': None}
@ -1642,6 +1674,15 @@ def test_autodoc_default_options(app):
assert ' .. py:method:: CustomIter.snafucate()' in actual assert ' .. py:method:: CustomIter.snafucate()' in actual
assert ' Makes this snafucated.' 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') @pytest.mark.sphinx('html', testroot='ext-autodoc')
def test_autodoc_default_options_with_values(app): def test_autodoc_default_options_with_values(app):

View File

@ -8,7 +8,14 @@
:license: BSD, see LICENSE for details. :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.domains.rst import parse_directive
from sphinx.testing import restructuredtext
from sphinx.testing.util import assert_node
def test_parse_directive(): def test_parse_directive():
@ -16,10 +23,59 @@ def test_parse_directive():
assert s == ('foö', '') assert s == ('foö', '')
s = parse_directive(' .. foö :: ') s = parse_directive(' .. foö :: ')
assert s == ('foö', ' ') assert s == ('foö', '')
s = parse_directive('.. foö:: args1 args2') s = parse_directive('.. foö:: args1 args2')
assert s == ('foö', ' args1 args2') assert s == ('foö', ' args1 args2')
s = parse_directive('.. :: bar') s = parse_directive('.. :: bar')
assert s == ('.. :: 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_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)

View File

@ -7,8 +7,12 @@
:copyright: Copyright 2007-2019 by the Sphinx team, see AUTHORS. :copyright: Copyright 2007-2019 by the Sphinx team, see AUTHORS.
:license: BSD, see LICENSE for details. :license: BSD, see LICENSE for details.
""" """
import _testcapi
import datetime
import functools import functools
import sys import sys
import types
from textwrap import dedent from textwrap import dedent
import pytest 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 class
assert inspect.isdescriptor(Base().meth) is True # method of instance assert inspect.isdescriptor(Base().meth) is True # method of instance
assert inspect.isdescriptor(func) is True # function 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