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.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()``
@ -68,8 +71,13 @@ 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
* #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 '.'
* Add ``:classmethod:`` and ``:staticmethod:`` options to :rst:dir:`py:method`
directive
@ -80,6 +88,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
--------

View File

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

View File

@ -38,3 +38,8 @@ Builder API
.. automethod:: write_doc
.. 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
- ``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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -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,
@ -58,10 +57,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 ''
@ -80,7 +75,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):
@ -96,6 +94,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 +108,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."""
@ -126,42 +132,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

View File

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

View File

@ -10,14 +10,20 @@
: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
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 +48,28 @@ core_events = {
class EventManager:
def __init__(self):
# type: () -> None
"""Event manager for Sphinx."""
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]]
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 +80,35 @@ 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))
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):
# 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

View File

@ -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
@ -993,7 +993,9 @@ 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)
# 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):
# type: () -> str
@ -1347,17 +1349,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
@ -1368,8 +1367,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

View File

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

View File

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

View File

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

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

View File

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

View File

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

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.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) == ''
@ -451,6 +456,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
@ -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')
def test_coroutine():
options = {"members": None}
@ -1579,6 +1609,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}
@ -1642,6 +1674,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):

View File

@ -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():
@ -16,10 +23,59 @@ 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')
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_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.
: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