mirror of
https://github.com/sphinx-doc/sphinx.git
synced 2025-02-25 18:55:22 -06:00
Merge branch '2.0'
This commit is contained in:
9
CHANGES
9
CHANGES
@@ -39,6 +39,7 @@ Deprecated
|
||||
----------
|
||||
|
||||
* ``sphinx.builders.latex.LaTeXBuilder.apply_transforms()``
|
||||
* ``sphinx.builders._epub_base.EpubBuilder.esc()``
|
||||
* ``sphinx.directives.Acks``
|
||||
* ``sphinx.directives.Author``
|
||||
* ``sphinx.directives.Centered``
|
||||
@@ -63,6 +64,8 @@ Deprecated
|
||||
* ``sphinx.domains.std.StandardDomain.note_citation_refs()``
|
||||
* ``sphinx.domains.std.StandardDomain.note_labels()``
|
||||
* ``sphinx.environment.NoUri``
|
||||
* ``sphinx.ext.apidoc.format_directive()``
|
||||
* ``sphinx.ext.apidoc.format_heading()``
|
||||
* ``sphinx.ext.autodoc.importer.MockFinder``
|
||||
* ``sphinx.ext.autodoc.importer.MockLoader``
|
||||
* ``sphinx.ext.autodoc.importer.mock()``
|
||||
@@ -101,11 +104,13 @@ Features added
|
||||
functions
|
||||
* #6289: autodoc: :confval:`autodoc_default_options` now supports
|
||||
``imported-members`` option
|
||||
* #4777: autodoc: Support coroutine
|
||||
* #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
|
||||
* #4777: py domain: Add ``:async:`` option to :rst:dir:`py:function` directive
|
||||
* py domain: Add ``:async:``, ``:classmethod:`` and ``:staticmethod:`` options
|
||||
to :rst:dir:`py:method` directive
|
||||
|
||||
Bugs fixed
|
||||
----------
|
||||
|
||||
@@ -31,6 +31,11 @@ The following is a list of deprecated interfaces.
|
||||
- 4.0
|
||||
- N/A
|
||||
|
||||
* - ``sphinx.builders._epub_base.EpubBuilder.esc()``
|
||||
- 2.1
|
||||
- 4.0
|
||||
- ``html.escape()``
|
||||
|
||||
* - ``sphinx.directives.Acks``
|
||||
- 2.1
|
||||
- 4.0
|
||||
@@ -156,6 +161,15 @@ The following is a list of deprecated interfaces.
|
||||
- 2.1
|
||||
- 4.0
|
||||
- ``sphinx.errors.NoUri``
|
||||
* - ``sphinx.ext.apidoc.format_directive()``
|
||||
- 2.1
|
||||
- 4.0
|
||||
- N/A
|
||||
|
||||
* - ``sphinx.ext.apidoc.format_heading()``
|
||||
- 2.1
|
||||
- 4.0
|
||||
- N/A
|
||||
|
||||
* - ``sphinx.ext.autodoc.importer.MockFinder``
|
||||
- 2.1
|
||||
|
||||
@@ -169,6 +169,13 @@ The following directives are provided for module and class contents:
|
||||
This information can (in any ``py`` directive) optionally be given in a
|
||||
structured form, see :ref:`info-field-lists`.
|
||||
|
||||
The ``async`` option can be given (with no value) to indicate the function is
|
||||
an async method.
|
||||
|
||||
.. versionchanged:: 2.1
|
||||
|
||||
``:async:`` option added.
|
||||
|
||||
.. rst:directive:: .. py:data:: name
|
||||
|
||||
Describes global data in a module, including both variables and values used
|
||||
@@ -216,12 +223,15 @@ The following directives are provided for module and class contents:
|
||||
described for ``function``. See also :ref:`signatures` and
|
||||
:ref:`info-field-lists`.
|
||||
|
||||
The ``async`` option can be given (with no value) to indicate the method is
|
||||
an async method.
|
||||
|
||||
The ``classmethod`` option and ``staticmethod`` option can be given (with
|
||||
no value) to indicate the method is a class method (or a static method).
|
||||
|
||||
.. versionchanged:: 2.1
|
||||
|
||||
``:classmethod:`` and ``:staticmethod:`` options added.
|
||||
``:async:``, ``:classmethod:`` and ``:staticmethod:`` options added.
|
||||
|
||||
.. rst:directive:: .. py:staticmethod:: name(parameters)
|
||||
|
||||
|
||||
3
setup.py
3
setup.py
@@ -38,6 +38,9 @@ extras_require = {
|
||||
':sys_platform=="win32"': [
|
||||
'colorama>=0.3.5',
|
||||
],
|
||||
'docs': [
|
||||
'sphinxcontrib-websupport',
|
||||
],
|
||||
'test': [
|
||||
'pytest',
|
||||
'pytest-cov',
|
||||
|
||||
@@ -8,6 +8,7 @@
|
||||
:license: BSD, see LICENSE for details.
|
||||
"""
|
||||
|
||||
import html
|
||||
import os
|
||||
import re
|
||||
import warnings
|
||||
@@ -178,7 +179,9 @@ class EpubBuilder(StandaloneHTMLBuilder):
|
||||
def esc(self, name):
|
||||
# type: (str) -> str
|
||||
"""Replace all characters not allowed in text an attribute values."""
|
||||
# Like cgi.escape, but also replace apostrophe
|
||||
warnings.warn(
|
||||
'%s.esc() is deprecated. Use html.escape() instead.' % self.__class__.__name__,
|
||||
RemovedInSphinx40Warning)
|
||||
name = name.replace('&', '&')
|
||||
name = name.replace('<', '<')
|
||||
name = name.replace('>', '>')
|
||||
@@ -201,8 +204,8 @@ class EpubBuilder(StandaloneHTMLBuilder):
|
||||
if (self.toctree_template % level) in classes:
|
||||
result.append({
|
||||
'level': level,
|
||||
'refuri': self.esc(refuri),
|
||||
'text': ssp(self.esc(doctree.astext()))
|
||||
'refuri': html.escape(refuri),
|
||||
'text': ssp(html.escape(doctree.astext()))
|
||||
})
|
||||
break
|
||||
elif isinstance(doctree, nodes.Element):
|
||||
@@ -241,21 +244,21 @@ class EpubBuilder(StandaloneHTMLBuilder):
|
||||
"""
|
||||
refnodes.insert(0, {
|
||||
'level': 1,
|
||||
'refuri': self.esc(self.config.master_doc + self.out_suffix),
|
||||
'text': ssp(self.esc(
|
||||
'refuri': html.escape(self.config.master_doc + self.out_suffix),
|
||||
'text': ssp(html.escape(
|
||||
self.env.titles[self.config.master_doc].astext()))
|
||||
})
|
||||
for file, text in reversed(self.config.epub_pre_files):
|
||||
refnodes.insert(0, {
|
||||
'level': 1,
|
||||
'refuri': self.esc(file),
|
||||
'text': ssp(self.esc(text))
|
||||
'refuri': html.escape(file),
|
||||
'text': ssp(html.escape(text))
|
||||
})
|
||||
for file, text in self.config.epub_post_files:
|
||||
refnodes.append({
|
||||
'level': 1,
|
||||
'refuri': self.esc(file),
|
||||
'text': ssp(self.esc(text))
|
||||
'refuri': html.escape(file),
|
||||
'text': ssp(html.escape(text))
|
||||
})
|
||||
|
||||
def fix_fragment(self, prefix, fragment):
|
||||
@@ -511,15 +514,15 @@ class EpubBuilder(StandaloneHTMLBuilder):
|
||||
file properly escaped.
|
||||
"""
|
||||
metadata = {} # type: Dict[str, Any]
|
||||
metadata['title'] = self.esc(self.config.epub_title)
|
||||
metadata['author'] = self.esc(self.config.epub_author)
|
||||
metadata['uid'] = self.esc(self.config.epub_uid)
|
||||
metadata['lang'] = self.esc(self.config.epub_language)
|
||||
metadata['publisher'] = self.esc(self.config.epub_publisher)
|
||||
metadata['copyright'] = self.esc(self.config.epub_copyright)
|
||||
metadata['scheme'] = self.esc(self.config.epub_scheme)
|
||||
metadata['id'] = self.esc(self.config.epub_identifier)
|
||||
metadata['date'] = self.esc(format_date("%Y-%m-%d"))
|
||||
metadata['title'] = html.escape(self.config.epub_title)
|
||||
metadata['author'] = html.escape(self.config.epub_author)
|
||||
metadata['uid'] = html.escape(self.config.epub_uid)
|
||||
metadata['lang'] = html.escape(self.config.epub_language)
|
||||
metadata['publisher'] = html.escape(self.config.epub_publisher)
|
||||
metadata['copyright'] = html.escape(self.config.epub_copyright)
|
||||
metadata['scheme'] = html.escape(self.config.epub_scheme)
|
||||
metadata['id'] = html.escape(self.config.epub_identifier)
|
||||
metadata['date'] = html.escape(format_date("%Y-%m-%d"))
|
||||
metadata['manifest_items'] = []
|
||||
metadata['spines'] = []
|
||||
metadata['guides'] = []
|
||||
@@ -566,9 +569,9 @@ class EpubBuilder(StandaloneHTMLBuilder):
|
||||
type='epub', subtype='unknown_project_files')
|
||||
continue
|
||||
filename = filename.replace(os.sep, '/')
|
||||
item = ManifestItem(self.esc(filename),
|
||||
self.esc(self.make_id(filename)),
|
||||
self.esc(self.media_types[ext]))
|
||||
item = ManifestItem(html.escape(filename),
|
||||
html.escape(self.make_id(filename)),
|
||||
html.escape(self.media_types[ext]))
|
||||
metadata['manifest_items'].append(item)
|
||||
self.files.append(filename)
|
||||
|
||||
@@ -579,21 +582,21 @@ class EpubBuilder(StandaloneHTMLBuilder):
|
||||
continue
|
||||
if refnode['refuri'] in self.ignored_files:
|
||||
continue
|
||||
spine = Spine(self.esc(self.make_id(refnode['refuri'])), True)
|
||||
spine = Spine(html.escape(self.make_id(refnode['refuri'])), True)
|
||||
metadata['spines'].append(spine)
|
||||
spinefiles.add(refnode['refuri'])
|
||||
for info in self.domain_indices:
|
||||
spine = Spine(self.esc(self.make_id(info[0] + self.out_suffix)), True)
|
||||
spine = Spine(html.escape(self.make_id(info[0] + self.out_suffix)), True)
|
||||
metadata['spines'].append(spine)
|
||||
spinefiles.add(info[0] + self.out_suffix)
|
||||
if self.use_index:
|
||||
spine = Spine(self.esc(self.make_id('genindex' + self.out_suffix)), True)
|
||||
spine = Spine(html.escape(self.make_id('genindex' + self.out_suffix)), True)
|
||||
metadata['spines'].append(spine)
|
||||
spinefiles.add('genindex' + self.out_suffix)
|
||||
# add auto generated files
|
||||
for name in self.files:
|
||||
if name not in spinefiles and name.endswith(self.out_suffix):
|
||||
spine = Spine(self.esc(self.make_id(name)), False)
|
||||
spine = Spine(html.escape(self.make_id(name)), False)
|
||||
metadata['spines'].append(spine)
|
||||
|
||||
# add the optional cover
|
||||
@@ -601,18 +604,18 @@ class EpubBuilder(StandaloneHTMLBuilder):
|
||||
if self.config.epub_cover:
|
||||
image, html_tmpl = self.config.epub_cover
|
||||
image = image.replace(os.sep, '/')
|
||||
metadata['cover'] = self.esc(self.make_id(image))
|
||||
metadata['cover'] = html.escape(self.make_id(image))
|
||||
if html_tmpl:
|
||||
spine = Spine(self.esc(self.make_id(self.coverpage_name)), True)
|
||||
spine = Spine(html.escape(self.make_id(self.coverpage_name)), True)
|
||||
metadata['spines'].insert(0, spine)
|
||||
if self.coverpage_name not in self.files:
|
||||
ext = path.splitext(self.coverpage_name)[-1]
|
||||
self.files.append(self.coverpage_name)
|
||||
item = ManifestItem(self.esc(self.coverpage_name),
|
||||
self.esc(self.make_id(self.coverpage_name)),
|
||||
self.esc(self.media_types[ext]))
|
||||
item = ManifestItem(html.escape(self.coverpage_name),
|
||||
html.escape(self.make_id(self.coverpage_name)),
|
||||
html.escape(self.media_types[ext]))
|
||||
metadata['manifest_items'].append(item)
|
||||
ctx = {'image': self.esc(image), 'title': self.config.project}
|
||||
ctx = {'image': html.escape(image), 'title': self.config.project}
|
||||
self.handle_page(
|
||||
path.splitext(self.coverpage_name)[0], ctx, html_tmpl)
|
||||
spinefiles.add(self.coverpage_name)
|
||||
@@ -628,17 +631,17 @@ class EpubBuilder(StandaloneHTMLBuilder):
|
||||
auto_add_cover = False
|
||||
if type == 'toc':
|
||||
auto_add_toc = False
|
||||
metadata['guides'].append(Guide(self.esc(type),
|
||||
self.esc(title),
|
||||
self.esc(uri)))
|
||||
metadata['guides'].append(Guide(html.escape(type),
|
||||
html.escape(title),
|
||||
html.escape(uri)))
|
||||
if auto_add_cover and html_tmpl:
|
||||
metadata['guides'].append(Guide('cover',
|
||||
self.guide_titles['cover'],
|
||||
self.esc(self.coverpage_name)))
|
||||
html.escape(self.coverpage_name)))
|
||||
if auto_add_toc and self.refnodes:
|
||||
metadata['guides'].append(Guide('toc',
|
||||
self.guide_titles['toc'],
|
||||
self.esc(self.refnodes[0]['refuri'])))
|
||||
html.escape(self.refnodes[0]['refuri'])))
|
||||
|
||||
# write the project file
|
||||
copy_asset_file(path.join(self.template_dir, 'content.opf_t'),
|
||||
@@ -707,7 +710,7 @@ class EpubBuilder(StandaloneHTMLBuilder):
|
||||
"""
|
||||
metadata = {} # type: Dict[str, Any]
|
||||
metadata['uid'] = self.config.epub_uid
|
||||
metadata['title'] = self.esc(self.config.epub_title)
|
||||
metadata['title'] = html.escape(self.config.epub_title)
|
||||
metadata['level'] = level
|
||||
metadata['navpoints'] = navpoints
|
||||
return metadata
|
||||
|
||||
@@ -9,6 +9,7 @@
|
||||
:license: BSD, see LICENSE for details.
|
||||
"""
|
||||
|
||||
import html
|
||||
import warnings
|
||||
from collections import namedtuple
|
||||
from os import path
|
||||
@@ -98,12 +99,12 @@ class Epub3Builder(_epub_base.EpubBuilder):
|
||||
writing_mode = self.config.epub_writing_mode
|
||||
|
||||
metadata = super().content_metadata()
|
||||
metadata['description'] = self.esc(self.config.epub_description)
|
||||
metadata['contributor'] = self.esc(self.config.epub_contributor)
|
||||
metadata['description'] = html.escape(self.config.epub_description)
|
||||
metadata['contributor'] = html.escape(self.config.epub_contributor)
|
||||
metadata['page_progression_direction'] = PAGE_PROGRESSION_DIRECTIONS.get(writing_mode)
|
||||
metadata['ibook_scroll_axis'] = IBOOK_SCROLL_AXIS.get(writing_mode)
|
||||
metadata['date'] = self.esc(format_date("%Y-%m-%dT%H:%M:%SZ"))
|
||||
metadata['version'] = self.esc(self.config.version)
|
||||
metadata['date'] = html.escape(format_date("%Y-%m-%dT%H:%M:%SZ"))
|
||||
metadata['version'] = html.escape(self.config.version)
|
||||
metadata['epub_version'] = self.config.epub_version
|
||||
return metadata
|
||||
|
||||
@@ -166,8 +167,8 @@ class Epub3Builder(_epub_base.EpubBuilder):
|
||||
properly escaped.
|
||||
"""
|
||||
metadata = {} # type: Dict
|
||||
metadata['lang'] = self.esc(self.config.epub_language)
|
||||
metadata['toc_locale'] = self.esc(self.guide_titles['toc'])
|
||||
metadata['lang'] = html.escape(self.config.epub_language)
|
||||
metadata['toc_locale'] = html.escape(self.guide_titles['toc'])
|
||||
metadata['navlist'] = navlist
|
||||
return metadata
|
||||
|
||||
|
||||
@@ -429,6 +429,18 @@ class PyModulelevel(PyObject):
|
||||
class PyFunction(PyObject):
|
||||
"""Description of a function."""
|
||||
|
||||
option_spec = PyObject.option_spec.copy()
|
||||
option_spec.update({
|
||||
'async': directives.flag,
|
||||
})
|
||||
|
||||
def get_signature_prefix(self, sig):
|
||||
# type: (str) -> str
|
||||
if 'async' in self.options:
|
||||
return 'async '
|
||||
else:
|
||||
return ''
|
||||
|
||||
def needs_arglist(self):
|
||||
# type: () -> bool
|
||||
return True
|
||||
@@ -564,6 +576,7 @@ class PyMethod(PyObject):
|
||||
|
||||
option_spec = PyObject.option_spec.copy()
|
||||
option_spec.update({
|
||||
'async': directives.flag,
|
||||
'classmethod': directives.flag,
|
||||
'staticmethod': directives.flag,
|
||||
})
|
||||
@@ -574,10 +587,16 @@ class PyMethod(PyObject):
|
||||
|
||||
def get_signature_prefix(self, sig):
|
||||
# type: (str) -> str
|
||||
prefix = []
|
||||
if 'async' in self.options:
|
||||
prefix.append('async')
|
||||
if 'staticmethod' in self.options:
|
||||
return 'static '
|
||||
elif 'classmethod' in self.options:
|
||||
return 'classmethod '
|
||||
prefix.append('static')
|
||||
if 'classmethod' in self.options:
|
||||
prefix.append('classmethod')
|
||||
|
||||
if prefix:
|
||||
return ' '.join(prefix) + ' '
|
||||
else:
|
||||
return ''
|
||||
|
||||
|
||||
@@ -19,15 +19,18 @@ import glob
|
||||
import locale
|
||||
import os
|
||||
import sys
|
||||
import warnings
|
||||
from fnmatch import fnmatch
|
||||
from os import path
|
||||
|
||||
import sphinx.locale
|
||||
from sphinx import __display_version__, package_dir
|
||||
from sphinx.cmd.quickstart import EXTENSIONS
|
||||
from sphinx.deprecation import RemovedInSphinx40Warning
|
||||
from sphinx.locale import __
|
||||
from sphinx.util import rst
|
||||
from sphinx.util.osutil import FileAvoidWrite, ensuredir
|
||||
from sphinx.util.template import ReSTRenderer
|
||||
|
||||
if False:
|
||||
# For type annotation
|
||||
@@ -47,6 +50,8 @@ else:
|
||||
INITPY = '__init__.py'
|
||||
PY_SUFFIXES = {'.py', '.pyx'}
|
||||
|
||||
template_dir = path.join(package_dir, 'templates', 'apidoc')
|
||||
|
||||
|
||||
def makename(package, module):
|
||||
# type: (str, str) -> str
|
||||
@@ -79,6 +84,8 @@ def write_file(name, text, opts):
|
||||
def format_heading(level, text, escape=True):
|
||||
# type: (int, str, bool) -> str
|
||||
"""Create a heading of <level> [1, 2 or 3 supported]."""
|
||||
warnings.warn('format_warning() is deprecated.',
|
||||
RemovedInSphinx40Warning)
|
||||
if escape:
|
||||
text = rst.escape(text)
|
||||
underlining = ['=', '-', '~', ][level - 1] * len(text)
|
||||
@@ -88,100 +95,79 @@ def format_heading(level, text, escape=True):
|
||||
def format_directive(module, package=None):
|
||||
# type: (str, str) -> str
|
||||
"""Create the automodule directive and add the options."""
|
||||
warnings.warn('format_directive() is deprecated.',
|
||||
RemovedInSphinx40Warning)
|
||||
directive = '.. automodule:: %s\n' % makename(package, module)
|
||||
for option in OPTIONS:
|
||||
directive += ' :%s:\n' % option
|
||||
return directive
|
||||
|
||||
|
||||
def create_module_file(package, module, opts):
|
||||
def create_module_file(package, basename, opts):
|
||||
# type: (str, str, Any) -> None
|
||||
"""Build the text of the file and write the file."""
|
||||
if not opts.noheadings:
|
||||
text = format_heading(1, '%s module' % module)
|
||||
else:
|
||||
text = ''
|
||||
# text += format_heading(2, ':mod:`%s` Module' % module)
|
||||
text += format_directive(module, package)
|
||||
write_file(makename(package, module), text, opts)
|
||||
qualname = makename(package, basename)
|
||||
context = {
|
||||
'show_headings': not opts.noheadings,
|
||||
'basename': basename,
|
||||
'qualname': qualname,
|
||||
'automodule_options': OPTIONS,
|
||||
}
|
||||
text = ReSTRenderer(template_dir).render('module.rst', context)
|
||||
write_file(qualname, text, opts)
|
||||
|
||||
|
||||
def create_package_file(root, master_package, subroot, py_files, opts, subs, is_namespace, excludes=[]): # NOQA
|
||||
# type: (str, str, str, List[str], Any, List[str], bool, List[str]) -> None
|
||||
"""Build the text of the file and write the file."""
|
||||
text = format_heading(1, ('%s package' if not is_namespace else "%s namespace")
|
||||
% makename(master_package, subroot))
|
||||
|
||||
if opts.modulefirst and not is_namespace:
|
||||
text += format_directive(subroot, master_package)
|
||||
text += '\n'
|
||||
|
||||
# build a list of directories that are szvpackages (contain an INITPY file)
|
||||
# and also checks the INITPY file is not empty, or there are other python
|
||||
# source files in that folder.
|
||||
# (depending on settings - but shall_skip() takes care of that)
|
||||
subs = [sub for sub in subs if not
|
||||
shall_skip(path.join(root, sub, INITPY), opts, excludes)]
|
||||
# if there are some package directories, add a TOC for theses subpackages
|
||||
if subs:
|
||||
text += format_heading(2, 'Subpackages')
|
||||
text += '.. toctree::\n\n'
|
||||
for sub in subs:
|
||||
text += ' %s.%s\n' % (makename(master_package, subroot), sub)
|
||||
text += '\n'
|
||||
|
||||
submods = [path.splitext(sub)[0] for sub in py_files
|
||||
if not shall_skip(path.join(root, sub), opts, excludes) and
|
||||
sub != INITPY]
|
||||
if submods:
|
||||
text += format_heading(2, 'Submodules')
|
||||
if opts.separatemodules:
|
||||
text += '.. toctree::\n\n'
|
||||
for submod in submods:
|
||||
modfile = makename(master_package, makename(subroot, submod))
|
||||
text += ' %s\n' % modfile
|
||||
|
||||
# generate separate file for this module
|
||||
if not opts.noheadings:
|
||||
filetext = format_heading(1, '%s module' % modfile)
|
||||
else:
|
||||
filetext = ''
|
||||
filetext += format_directive(makename(subroot, submod),
|
||||
master_package)
|
||||
write_file(modfile, filetext, opts)
|
||||
else:
|
||||
for submod in submods:
|
||||
modfile = makename(master_package, makename(subroot, submod))
|
||||
if not opts.noheadings:
|
||||
text += format_heading(2, '%s module' % modfile)
|
||||
text += format_directive(makename(subroot, submod),
|
||||
master_package)
|
||||
text += '\n'
|
||||
text += '\n'
|
||||
|
||||
if not opts.modulefirst and not is_namespace:
|
||||
text += format_heading(2, 'Module contents')
|
||||
text += format_directive(subroot, master_package)
|
||||
# build a list of sub packages (directories containing an INITPY file)
|
||||
subpackages = [sub for sub in subs if not
|
||||
shall_skip(path.join(root, sub, INITPY), opts, excludes)]
|
||||
subpackages = [makename(makename(master_package, subroot), pkgname)
|
||||
for pkgname in subpackages]
|
||||
# build a list of sub modules
|
||||
submodules = [path.splitext(sub)[0] for sub in py_files
|
||||
if not shall_skip(path.join(root, sub), opts, excludes) and
|
||||
sub != INITPY]
|
||||
submodules = [makename(master_package, makename(subroot, modname))
|
||||
for modname in submodules]
|
||||
|
||||
context = {
|
||||
'pkgname': makename(master_package, subroot),
|
||||
'subpackages': subpackages,
|
||||
'submodules': submodules,
|
||||
'is_namespace': is_namespace,
|
||||
'modulefirst': opts.modulefirst,
|
||||
'separatemodules': opts.separatemodules,
|
||||
'automodule_options': OPTIONS,
|
||||
'show_headings': not opts.noheadings,
|
||||
}
|
||||
text = ReSTRenderer(template_dir).render('package.rst', context)
|
||||
write_file(makename(master_package, subroot), text, opts)
|
||||
|
||||
if submodules and opts.separatemodules:
|
||||
for submodule in submodules:
|
||||
create_module_file(None, submodule, opts)
|
||||
|
||||
|
||||
def create_modules_toc_file(modules, opts, name='modules'):
|
||||
# type: (List[str], Any, str) -> None
|
||||
"""Create the module's index."""
|
||||
text = format_heading(1, '%s' % opts.header, escape=False)
|
||||
text += '.. toctree::\n'
|
||||
text += ' :maxdepth: %s\n\n' % opts.maxdepth
|
||||
|
||||
modules.sort()
|
||||
prev_module = ''
|
||||
for module in modules:
|
||||
for module in modules[:]:
|
||||
# look if the module is a subpackage and, if yes, ignore it
|
||||
if module.startswith(prev_module + '.'):
|
||||
continue
|
||||
prev_module = module
|
||||
text += ' %s\n' % module
|
||||
modules.remove(module)
|
||||
else:
|
||||
prev_module = module
|
||||
|
||||
context = {
|
||||
'header': opts.header,
|
||||
'maxdepth': opts.maxdepth,
|
||||
'docnames': modules,
|
||||
}
|
||||
text = ReSTRenderer(template_dir).render('toc.rst', context)
|
||||
write_file(name, text, opts)
|
||||
|
||||
|
||||
|
||||
@@ -1032,6 +1032,14 @@ class FunctionDocumenter(DocstringSignatureMixin, ModuleLevelDocumenter): # typ
|
||||
# type: (bool) -> None
|
||||
pass
|
||||
|
||||
def add_directive_header(self, sig):
|
||||
# type: (str) -> None
|
||||
sourcename = self.get_sourcename()
|
||||
super().add_directive_header(sig)
|
||||
|
||||
if inspect.iscoroutinefunction(self.object):
|
||||
self.add_line(' :async:', sourcename)
|
||||
|
||||
|
||||
class DecoratorDocumenter(FunctionDocumenter):
|
||||
"""
|
||||
@@ -1316,9 +1324,11 @@ class MethodDocumenter(DocstringSignatureMixin, ClassLevelDocumenter): # type:
|
||||
|
||||
sourcename = self.get_sourcename()
|
||||
obj = self.parent.__dict__.get(self.object_name, self.object)
|
||||
if inspect.iscoroutinefunction(obj):
|
||||
self.add_line(' :async:', sourcename)
|
||||
if inspect.isclassmethod(obj):
|
||||
self.add_line(' :classmethod:', sourcename)
|
||||
elif inspect.isstaticmethod(obj, cls=self.parent, name=self.object_name):
|
||||
if inspect.isstaticmethod(obj, cls=self.parent, name=self.object_name):
|
||||
self.add_line(' :staticmethod:', sourcename)
|
||||
|
||||
def document_members(self, all_members=False):
|
||||
|
||||
@@ -381,8 +381,17 @@ class VariableCommentPicker(ast.NodeVisitor):
|
||||
self.context.pop()
|
||||
self.current_function = None
|
||||
|
||||
def visit_AsyncFunctionDef(self, node):
|
||||
# type: (ast.AsyncFunctionDef) -> None
|
||||
"""Handles AsyncFunctionDef node and set context."""
|
||||
self.visit_FunctionDef(node) # type: ignore
|
||||
|
||||
|
||||
class DefinitionFinder(TokenProcessor):
|
||||
"""Python source code parser to detect location of functions,
|
||||
classes and methods.
|
||||
"""
|
||||
|
||||
def __init__(self, lines):
|
||||
# type: (List[str]) -> None
|
||||
super().__init__(lines)
|
||||
@@ -393,6 +402,7 @@ class DefinitionFinder(TokenProcessor):
|
||||
|
||||
def add_definition(self, name, entry):
|
||||
# type: (str, Tuple[str, int, int]) -> None
|
||||
"""Add a location of definition."""
|
||||
if self.indents and self.indents[-1][0] == 'def' and entry[0] == 'def':
|
||||
# ignore definition of inner function
|
||||
pass
|
||||
@@ -401,6 +411,7 @@ class DefinitionFinder(TokenProcessor):
|
||||
|
||||
def parse(self):
|
||||
# type: () -> None
|
||||
"""Parse the code to obtain location of definitions."""
|
||||
while True:
|
||||
token = self.fetch_token()
|
||||
if token is None:
|
||||
@@ -422,6 +433,7 @@ class DefinitionFinder(TokenProcessor):
|
||||
|
||||
def parse_definition(self, typ):
|
||||
# type: (str) -> None
|
||||
"""Parse AST of definition."""
|
||||
name = self.fetch_token()
|
||||
self.context.append(name.value)
|
||||
funcname = '.'.join(self.context)
|
||||
@@ -443,6 +455,7 @@ class DefinitionFinder(TokenProcessor):
|
||||
|
||||
def finalize_block(self):
|
||||
# type: () -> None
|
||||
"""Finalize definition block."""
|
||||
definition = self.indents.pop()
|
||||
if definition[0] != 'other':
|
||||
typ, funcname, start_pos = definition
|
||||
|
||||
9
sphinx/templates/apidoc/module.rst
Normal file
9
sphinx/templates/apidoc/module.rst
Normal file
@@ -0,0 +1,9 @@
|
||||
{%- if show_headings %}
|
||||
{{- [basename, "module"] | join(' ') | e | heading }}
|
||||
|
||||
{% endif -%}
|
||||
.. automodule:: {{ qualname }}
|
||||
{%- for option in automodule_options %}
|
||||
:{{ option }}:
|
||||
{%- endfor %}
|
||||
|
||||
52
sphinx/templates/apidoc/package.rst
Normal file
52
sphinx/templates/apidoc/package.rst
Normal file
@@ -0,0 +1,52 @@
|
||||
{%- macro automodule(modname, options) -%}
|
||||
.. automodule:: {{ modname }}
|
||||
{%- for option in options %}
|
||||
:{{ option }}:
|
||||
{%- endfor %}
|
||||
{%- endmacro %}
|
||||
|
||||
{%- macro toctree(docnames) -%}
|
||||
.. toctree::
|
||||
{% for docname in docnames %}
|
||||
{{ docname }}
|
||||
{%- endfor %}
|
||||
{%- endmacro %}
|
||||
|
||||
{%- if is_namespace %}
|
||||
{{- [pkgname, "namespace"] | join(" ") | e | heading }}
|
||||
{% else %}
|
||||
{{- [pkgname, "package"] | join(" ") | e | heading }}
|
||||
{% endif %}
|
||||
|
||||
{%- if modulefirst and not is_namespace %}
|
||||
{{ automodule(pkgname, automodule_options) }}
|
||||
{% endif %}
|
||||
|
||||
{%- if subpackages %}
|
||||
Subpackages
|
||||
-----------
|
||||
|
||||
{{ toctree(subpackages) }}
|
||||
{% endif %}
|
||||
|
||||
{%- if submodules %}
|
||||
Submodules
|
||||
----------
|
||||
{% if separatemodules %}
|
||||
{{ toctree(submodules) }}
|
||||
{%- else %}
|
||||
{%- for submodule in submodules %}
|
||||
{% if show_headings %}
|
||||
{{- [submodule, "module"] | join(" ") | e | heading(2) }}
|
||||
{% endif %}
|
||||
{{ automodule(submodule, automodule_options) }}
|
||||
{%- endfor %}
|
||||
{% endif %}
|
||||
{% endif %}
|
||||
|
||||
{%- if not modulefirst and not is_namespace %}
|
||||
Module contents
|
||||
---------------
|
||||
|
||||
{{ automodule(pkgname, automodule_options) }}
|
||||
{% endif %}
|
||||
8
sphinx/templates/apidoc/toc.rst
Normal file
8
sphinx/templates/apidoc/toc.rst
Normal file
@@ -0,0 +1,8 @@
|
||||
{{ header | heading }}
|
||||
|
||||
.. toctree::
|
||||
:maxdepth: {{ maxdepth }}
|
||||
{% for docname in docnames %}
|
||||
{{ docname }}
|
||||
{%- endfor %}
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
\documentclass[12pt]{article}
|
||||
\usepackage[utf8x]{inputenc}
|
||||
\usepackage[utf8]{inputenc}
|
||||
\usepackage{amsmath}
|
||||
\usepackage{amsthm}
|
||||
\usepackage{amssymb}
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
\documentclass[12pt]{article}
|
||||
\usepackage[utf8x]{inputenc}
|
||||
\usepackage[utf8]{inputenc}
|
||||
\usepackage{amsmath}
|
||||
\usepackage{amsthm}
|
||||
\usepackage{amssymb}
|
||||
|
||||
@@ -14,7 +14,8 @@ import inspect
|
||||
import re
|
||||
import sys
|
||||
import typing
|
||||
from functools import partial
|
||||
import warnings
|
||||
from functools import partial, partialmethod
|
||||
from inspect import ( # NOQA
|
||||
isclass, ismethod, ismethoddescriptor, isroutine
|
||||
)
|
||||
@@ -127,7 +128,7 @@ def isenumattribute(x):
|
||||
def ispartial(obj):
|
||||
# type: (Any) -> bool
|
||||
"""Check if the object is partial."""
|
||||
return isinstance(obj, partial)
|
||||
return isinstance(obj, (partial, partialmethod))
|
||||
|
||||
|
||||
def isclassmethod(obj):
|
||||
@@ -210,6 +211,18 @@ def isbuiltin(obj):
|
||||
return inspect.isbuiltin(obj) or ispartial(obj) and inspect.isbuiltin(obj.func)
|
||||
|
||||
|
||||
def iscoroutinefunction(obj):
|
||||
# type: (Any) -> bool
|
||||
"""Check if the object is coroutine-function."""
|
||||
if inspect.iscoroutinefunction(obj):
|
||||
return True
|
||||
elif ispartial(obj) and inspect.iscoroutinefunction(obj.func):
|
||||
# partialed
|
||||
return True
|
||||
else:
|
||||
return False
|
||||
|
||||
|
||||
def safe_getattr(obj, name, *defargs):
|
||||
# type: (Any, str, str) -> object
|
||||
"""A getattr() that turns all exceptions into AttributeErrors."""
|
||||
|
||||
@@ -9,11 +9,14 @@
|
||||
"""
|
||||
|
||||
import re
|
||||
from collections import defaultdict
|
||||
from contextlib import contextmanager
|
||||
from unicodedata import east_asian_width
|
||||
|
||||
from docutils.parsers.rst import roles
|
||||
from docutils.parsers.rst.languages import en as english
|
||||
from docutils.utils import Reporter
|
||||
from jinja2 import environmentfilter
|
||||
|
||||
from sphinx.locale import __
|
||||
from sphinx.util import docutils
|
||||
@@ -21,13 +24,20 @@ from sphinx.util import logging
|
||||
|
||||
if False:
|
||||
# For type annotation
|
||||
from typing import Generator # NOQA
|
||||
from typing import Callable, Dict, Generator # NOQA
|
||||
from docutils.statemachine import StringList # NOQA
|
||||
from jinja2 import Environment # NOQA
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
docinfo_re = re.compile(':\\w+:.*?')
|
||||
symbols_re = re.compile(r'([!-\-/:-@\[-`{-~])') # symbols without dot(0x2e)
|
||||
SECTIONING_CHARS = ['=', '-', '~']
|
||||
|
||||
# width of characters
|
||||
WIDECHARS = defaultdict(lambda: "WF") # type: Dict[str, str]
|
||||
# WF: Wide + Full-width
|
||||
WIDECHARS["ja"] = "WFA" # In Japanese, Ambiguous characters also have double width
|
||||
|
||||
|
||||
def escape(text):
|
||||
@@ -37,6 +47,29 @@ def escape(text):
|
||||
return text
|
||||
|
||||
|
||||
def textwidth(text, widechars='WF'):
|
||||
# type: (str, str) -> int
|
||||
"""Get width of text."""
|
||||
def charwidth(char, widechars):
|
||||
# type: (str, str) -> int
|
||||
if east_asian_width(char) in widechars:
|
||||
return 2
|
||||
else:
|
||||
return 1
|
||||
|
||||
return sum(charwidth(c, widechars) for c in text)
|
||||
|
||||
|
||||
@environmentfilter
|
||||
def heading(env, text, level=1):
|
||||
# type: (Environment, str, int) -> str
|
||||
"""Create a heading for *level*."""
|
||||
assert level <= 3
|
||||
width = textwidth(text, WIDECHARS[env.language]) # type: ignore
|
||||
sectioning_char = SECTIONING_CHARS[level - 1]
|
||||
return '%s\n%s' % (text, sectioning_char * width)
|
||||
|
||||
|
||||
@contextmanager
|
||||
def default_role(docname, name):
|
||||
# type: (str, str) -> Generator
|
||||
|
||||
@@ -15,7 +15,7 @@ from jinja2.sandbox import SandboxedEnvironment
|
||||
from sphinx import package_dir
|
||||
from sphinx.jinja2glue import SphinxFileSystemLoader
|
||||
from sphinx.locale import get_translator
|
||||
from sphinx.util import texescape
|
||||
from sphinx.util import rst, texescape
|
||||
|
||||
if False:
|
||||
# For type annotation
|
||||
@@ -84,3 +84,17 @@ class LaTeXRenderer(SphinxRenderer):
|
||||
self.env.variable_end_string = '%>'
|
||||
self.env.block_start_string = '<%'
|
||||
self.env.block_end_string = '%>'
|
||||
|
||||
|
||||
class ReSTRenderer(SphinxRenderer):
|
||||
def __init__(self, template_path=None, language=None):
|
||||
# type: (str, str) -> None
|
||||
super().__init__(template_path)
|
||||
|
||||
# add language to environment
|
||||
self.env.extend(language=language)
|
||||
|
||||
# use texescape as escape filter
|
||||
self.env.filters['e'] = rst.escape
|
||||
self.env.filters['escape'] = rst.escape
|
||||
self.env.filters['heading'] = rst.heading
|
||||
|
||||
@@ -5,7 +5,11 @@ def func():
|
||||
pass
|
||||
|
||||
|
||||
async def coroutinefunc():
|
||||
pass
|
||||
|
||||
partial_func = partial(func)
|
||||
partial_coroutinefunc = partial(coroutinefunc)
|
||||
|
||||
builtin_func = print
|
||||
partial_builtin_func = partial(print)
|
||||
|
||||
@@ -19,6 +19,11 @@ class Base():
|
||||
|
||||
partialmeth = partialmethod(meth)
|
||||
|
||||
async def coroutinemeth(self):
|
||||
pass
|
||||
|
||||
partial_coroutinemeth = partialmethod(coroutinemeth)
|
||||
|
||||
|
||||
class Inherited(Base):
|
||||
pass
|
||||
|
||||
@@ -1520,6 +1520,15 @@ def test_bound_method():
|
||||
|
||||
@pytest.mark.usefixtures('setup_test')
|
||||
def test_coroutine():
|
||||
actual = do_autodoc(app, 'function', 'target.functions.coroutinefunc')
|
||||
assert list(actual) == [
|
||||
'',
|
||||
'.. py:function:: coroutinefunc()',
|
||||
' :module: target.functions',
|
||||
' :async:',
|
||||
'',
|
||||
]
|
||||
|
||||
options = {"members": None}
|
||||
actual = do_autodoc(app, 'class', 'target.coroutine.AsyncClass', options)
|
||||
assert list(actual) == [
|
||||
@@ -1530,6 +1539,7 @@ def test_coroutine():
|
||||
' ',
|
||||
' .. py:method:: AsyncClass.do_coroutine()',
|
||||
' :module: target.coroutine',
|
||||
' :async:',
|
||||
' ',
|
||||
' A documented coroutine function',
|
||||
' '
|
||||
|
||||
@@ -304,15 +304,24 @@ def test_pydata(app):
|
||||
|
||||
|
||||
def test_pyfunction(app):
|
||||
text = ".. py:function:: func\n"
|
||||
text = (".. py:function:: func1\n"
|
||||
".. py:function:: func2\n"
|
||||
" :async:\n")
|
||||
domain = app.env.get_domain('py')
|
||||
doctree = restructuredtext.parse(app, text)
|
||||
assert_node(doctree, (addnodes.index,
|
||||
[desc, ([desc_signature, ([desc_name, "func"],
|
||||
[desc, ([desc_signature, ([desc_name, "func1"],
|
||||
[desc_parameterlist, ()])],
|
||||
[desc_content, ()])],
|
||||
addnodes.index,
|
||||
[desc, ([desc_signature, ([desc_annotation, "async "],
|
||||
[desc_name, "func2"],
|
||||
[desc_parameterlist, ()])],
|
||||
[desc_content, ()])]))
|
||||
assert 'func' in domain.objects
|
||||
assert domain.objects['func'] == ('index', 'function')
|
||||
assert 'func1' in domain.objects
|
||||
assert domain.objects['func1'] == ('index', 'function')
|
||||
assert 'func2' in domain.objects
|
||||
assert domain.objects['func2'] == ('index', 'function')
|
||||
|
||||
|
||||
def test_pymethod_options(app):
|
||||
@@ -322,7 +331,9 @@ def test_pymethod_options(app):
|
||||
" .. py:method:: meth2\n"
|
||||
" :classmethod:\n"
|
||||
" .. py:method:: meth3\n"
|
||||
" :staticmethod:\n")
|
||||
" :staticmethod:\n"
|
||||
" .. py:method:: meth4\n"
|
||||
" :async:\n")
|
||||
domain = app.env.get_domain('py')
|
||||
doctree = restructuredtext.parse(app, text)
|
||||
assert_node(doctree, (addnodes.index,
|
||||
@@ -333,6 +344,8 @@ def test_pymethod_options(app):
|
||||
addnodes.index,
|
||||
desc,
|
||||
addnodes.index,
|
||||
desc,
|
||||
addnodes.index,
|
||||
desc)])]))
|
||||
|
||||
# method
|
||||
@@ -364,6 +377,16 @@ def test_pymethod_options(app):
|
||||
assert 'Class.meth3' in domain.objects
|
||||
assert domain.objects['Class.meth3'] == ('index', 'method')
|
||||
|
||||
# :async:
|
||||
assert_node(doctree[1][1][6], addnodes.index,
|
||||
entries=[('single', 'meth4() (Class method)', 'Class.meth4', '', None)])
|
||||
assert_node(doctree[1][1][7], ([desc_signature, ([desc_annotation, "async "],
|
||||
[desc_name, "meth4"],
|
||||
[desc_parameterlist, ()])],
|
||||
[desc_content, ()]))
|
||||
assert 'Class.meth4' in domain.objects
|
||||
assert domain.objects['Class.meth4'] == ('index', 'method')
|
||||
|
||||
|
||||
def test_pyclassmethod(app):
|
||||
text = (".. py:class:: Class\n"
|
||||
|
||||
@@ -13,6 +13,7 @@ from collections import namedtuple
|
||||
import pytest
|
||||
|
||||
from sphinx.ext.apidoc import main as apidoc_main
|
||||
from sphinx.testing.path import path
|
||||
|
||||
|
||||
@pytest.fixture()
|
||||
@@ -398,3 +399,216 @@ def test_subpackage_in_toc(make_app, apidoc):
|
||||
assert 'parent.child.foo' in parent_child
|
||||
|
||||
assert (outdir / 'parent.child.foo.rst').isfile()
|
||||
|
||||
|
||||
def test_toc_file(tempdir):
|
||||
outdir = path(tempdir)
|
||||
(outdir / 'module').makedirs()
|
||||
(outdir / 'example.py').write_text('')
|
||||
(outdir / 'module' / 'example.py').write_text('')
|
||||
apidoc_main(['-o', tempdir, tempdir])
|
||||
assert (outdir / 'modules.rst').exists()
|
||||
|
||||
content = (outdir / 'modules.rst').text()
|
||||
assert content == ("test_toc_file0\n"
|
||||
"==============\n"
|
||||
"\n"
|
||||
".. toctree::\n"
|
||||
" :maxdepth: 4\n"
|
||||
"\n"
|
||||
" example\n")
|
||||
|
||||
|
||||
def test_module_file(tempdir):
|
||||
outdir = path(tempdir)
|
||||
(outdir / 'example.py').write_text('')
|
||||
apidoc_main(['-o', tempdir, tempdir])
|
||||
assert (outdir / 'example.rst').exists()
|
||||
|
||||
content = (outdir / 'example.rst').text()
|
||||
assert content == ("example module\n"
|
||||
"==============\n"
|
||||
"\n"
|
||||
".. automodule:: example\n"
|
||||
" :members:\n"
|
||||
" :undoc-members:\n"
|
||||
" :show-inheritance:\n")
|
||||
|
||||
|
||||
def test_module_file_noheadings(tempdir):
|
||||
outdir = path(tempdir)
|
||||
(outdir / 'example.py').write_text('')
|
||||
apidoc_main(['--no-headings', '-o', tempdir, tempdir])
|
||||
assert (outdir / 'example.rst').exists()
|
||||
|
||||
content = (outdir / 'example.rst').text()
|
||||
assert content == (".. automodule:: example\n"
|
||||
" :members:\n"
|
||||
" :undoc-members:\n"
|
||||
" :show-inheritance:\n")
|
||||
|
||||
|
||||
def test_package_file(tempdir):
|
||||
outdir = path(tempdir)
|
||||
(outdir / 'testpkg').makedirs()
|
||||
(outdir / 'testpkg' / '__init__.py').write_text('')
|
||||
(outdir / 'testpkg' / 'example.py').write_text('')
|
||||
(outdir / 'testpkg' / 'subpkg').makedirs()
|
||||
(outdir / 'testpkg' / 'subpkg' / '__init__.py').write_text('')
|
||||
apidoc_main(['-o', tempdir, tempdir / 'testpkg'])
|
||||
assert (outdir / 'testpkg.rst').exists()
|
||||
assert (outdir / 'testpkg.subpkg.rst').exists()
|
||||
|
||||
content = (outdir / 'testpkg.rst').text()
|
||||
assert content == ("testpkg package\n"
|
||||
"===============\n"
|
||||
"\n"
|
||||
"Subpackages\n"
|
||||
"-----------\n"
|
||||
"\n"
|
||||
".. toctree::\n"
|
||||
"\n"
|
||||
" testpkg.subpkg\n"
|
||||
"\n"
|
||||
"Submodules\n"
|
||||
"----------\n"
|
||||
"\n"
|
||||
"testpkg.example module\n"
|
||||
"----------------------\n"
|
||||
"\n"
|
||||
".. automodule:: testpkg.example\n"
|
||||
" :members:\n"
|
||||
" :undoc-members:\n"
|
||||
" :show-inheritance:\n"
|
||||
"\n"
|
||||
"\n"
|
||||
"Module contents\n"
|
||||
"---------------\n"
|
||||
"\n"
|
||||
".. automodule:: testpkg\n"
|
||||
" :members:\n"
|
||||
" :undoc-members:\n"
|
||||
" :show-inheritance:\n")
|
||||
|
||||
content = (outdir / 'testpkg.subpkg.rst').text()
|
||||
assert content == ("testpkg.subpkg package\n"
|
||||
"======================\n"
|
||||
"\n"
|
||||
"Module contents\n"
|
||||
"---------------\n"
|
||||
"\n"
|
||||
".. automodule:: testpkg.subpkg\n"
|
||||
" :members:\n"
|
||||
" :undoc-members:\n"
|
||||
" :show-inheritance:\n")
|
||||
|
||||
|
||||
def test_package_file_separate(tempdir):
|
||||
outdir = path(tempdir)
|
||||
(outdir / 'testpkg').makedirs()
|
||||
(outdir / 'testpkg' / '__init__.py').write_text('')
|
||||
(outdir / 'testpkg' / 'example.py').write_text('')
|
||||
apidoc_main(['--separate', '-o', tempdir, tempdir / 'testpkg'])
|
||||
assert (outdir / 'testpkg.rst').exists()
|
||||
assert (outdir / 'testpkg.example.rst').exists()
|
||||
|
||||
content = (outdir / 'testpkg.rst').text()
|
||||
assert content == ("testpkg package\n"
|
||||
"===============\n"
|
||||
"\n"
|
||||
"Submodules\n"
|
||||
"----------\n"
|
||||
"\n"
|
||||
".. toctree::\n"
|
||||
"\n"
|
||||
" testpkg.example\n"
|
||||
"\n"
|
||||
"Module contents\n"
|
||||
"---------------\n"
|
||||
"\n"
|
||||
".. automodule:: testpkg\n"
|
||||
" :members:\n"
|
||||
" :undoc-members:\n"
|
||||
" :show-inheritance:\n")
|
||||
|
||||
content = (outdir / 'testpkg.example.rst').text()
|
||||
assert content == ("testpkg.example module\n"
|
||||
"======================\n"
|
||||
"\n"
|
||||
".. automodule:: testpkg.example\n"
|
||||
" :members:\n"
|
||||
" :undoc-members:\n"
|
||||
" :show-inheritance:\n")
|
||||
|
||||
|
||||
def test_package_file_module_first(tempdir):
|
||||
outdir = path(tempdir)
|
||||
(outdir / 'testpkg').makedirs()
|
||||
(outdir / 'testpkg' / '__init__.py').write_text('')
|
||||
(outdir / 'testpkg' / 'example.py').write_text('')
|
||||
apidoc_main(['--module-first', '-o', tempdir, tempdir])
|
||||
|
||||
content = (outdir / 'testpkg.rst').text()
|
||||
assert content == ("testpkg package\n"
|
||||
"===============\n"
|
||||
"\n"
|
||||
".. automodule:: testpkg\n"
|
||||
" :members:\n"
|
||||
" :undoc-members:\n"
|
||||
" :show-inheritance:\n"
|
||||
"\n"
|
||||
"Submodules\n"
|
||||
"----------\n"
|
||||
"\n"
|
||||
"testpkg.example module\n"
|
||||
"----------------------\n"
|
||||
"\n"
|
||||
".. automodule:: testpkg.example\n"
|
||||
" :members:\n"
|
||||
" :undoc-members:\n"
|
||||
" :show-inheritance:\n"
|
||||
"\n")
|
||||
|
||||
|
||||
def test_package_file_without_submodules(tempdir):
|
||||
outdir = path(tempdir)
|
||||
(outdir / 'testpkg').makedirs()
|
||||
(outdir / 'testpkg' / '__init__.py').write_text('')
|
||||
apidoc_main(['-o', tempdir, tempdir / 'testpkg'])
|
||||
assert (outdir / 'testpkg.rst').exists()
|
||||
|
||||
content = (outdir / 'testpkg.rst').text()
|
||||
assert content == ("testpkg package\n"
|
||||
"===============\n"
|
||||
"\n"
|
||||
"Module contents\n"
|
||||
"---------------\n"
|
||||
"\n"
|
||||
".. automodule:: testpkg\n"
|
||||
" :members:\n"
|
||||
" :undoc-members:\n"
|
||||
" :show-inheritance:\n")
|
||||
|
||||
|
||||
def test_namespace_package_file(tempdir):
|
||||
outdir = path(tempdir)
|
||||
(outdir / 'testpkg').makedirs()
|
||||
(outdir / 'testpkg' / 'example.py').write_text('')
|
||||
apidoc_main(['--implicit-namespace', '-o', tempdir, tempdir / 'testpkg'])
|
||||
assert (outdir / 'testpkg.rst').exists()
|
||||
|
||||
content = (outdir / 'testpkg.rst').text()
|
||||
assert content == ("testpkg namespace\n"
|
||||
"=================\n"
|
||||
"\n"
|
||||
"Submodules\n"
|
||||
"----------\n"
|
||||
"\n"
|
||||
"testpkg.example module\n"
|
||||
"----------------------\n"
|
||||
"\n"
|
||||
".. automodule:: testpkg.example\n"
|
||||
" :members:\n"
|
||||
" :undoc-members:\n"
|
||||
" :show-inheritance:\n"
|
||||
"\n")
|
||||
|
||||
@@ -1,129 +0,0 @@
|
||||
"""
|
||||
test_inheritance
|
||||
~~~~~~~~~~~~~~~~
|
||||
|
||||
Tests for :mod:`sphinx.ext.inheritance_diagram` module.
|
||||
|
||||
:copyright: Copyright 2015 by the Sphinx team, see AUTHORS.
|
||||
:license: BSD, see LICENSE for details.
|
||||
"""
|
||||
|
||||
import os
|
||||
|
||||
import pytest
|
||||
|
||||
from sphinx.ext.inheritance_diagram import InheritanceDiagram
|
||||
|
||||
|
||||
@pytest.mark.sphinx(buildername="html", testroot="inheritance")
|
||||
@pytest.mark.usefixtures('if_graphviz_found')
|
||||
def test_inheritance_diagram(app, status, warning):
|
||||
# monkey-patch InheritaceDiagram.run() so we can get access to its
|
||||
# results.
|
||||
orig_run = InheritanceDiagram.run
|
||||
graphs = {}
|
||||
|
||||
def new_run(self):
|
||||
result = orig_run(self)
|
||||
node = result[0]
|
||||
source = os.path.basename(node.document.current_source).replace(".rst", "")
|
||||
graphs[source] = node['graph']
|
||||
return result
|
||||
|
||||
InheritanceDiagram.run = new_run
|
||||
|
||||
try:
|
||||
app.builder.build_all()
|
||||
finally:
|
||||
InheritanceDiagram.run = orig_run
|
||||
|
||||
assert app.statuscode == 0
|
||||
|
||||
html_warnings = warning.getvalue()
|
||||
assert html_warnings == ""
|
||||
|
||||
# note: it is better to split these asserts into separate test functions
|
||||
# but I can't figure out how to build only a specific .rst file
|
||||
|
||||
# basic inheritance diagram showing all classes
|
||||
for cls in graphs['basic_diagram'].class_info:
|
||||
# use in b/c traversing order is different sometimes
|
||||
assert cls in [
|
||||
('dummy.test.A', 'dummy.test.A', [], None),
|
||||
('dummy.test.F', 'dummy.test.F', ['dummy.test.C'], None),
|
||||
('dummy.test.C', 'dummy.test.C', ['dummy.test.A'], None),
|
||||
('dummy.test.E', 'dummy.test.E', ['dummy.test.B'], None),
|
||||
('dummy.test.D', 'dummy.test.D', ['dummy.test.B', 'dummy.test.C'], None),
|
||||
('dummy.test.B', 'dummy.test.B', ['dummy.test.A'], None)
|
||||
]
|
||||
|
||||
# inheritance diagram using :parts: 1 option
|
||||
for cls in graphs['diagram_w_parts'].class_info:
|
||||
assert cls in [
|
||||
('A', 'dummy.test.A', [], None),
|
||||
('F', 'dummy.test.F', ['C'], None),
|
||||
('C', 'dummy.test.C', ['A'], None),
|
||||
('E', 'dummy.test.E', ['B'], None),
|
||||
('D', 'dummy.test.D', ['B', 'C'], None),
|
||||
('B', 'dummy.test.B', ['A'], None)
|
||||
]
|
||||
|
||||
# inheritance diagram with 1 top class
|
||||
# :top-classes: dummy.test.B
|
||||
# rendering should be
|
||||
# A
|
||||
# \
|
||||
# B C
|
||||
# / \ / \
|
||||
# E D F
|
||||
#
|
||||
for cls in graphs['diagram_w_1_top_class'].class_info:
|
||||
assert cls in [
|
||||
('dummy.test.A', 'dummy.test.A', [], None),
|
||||
('dummy.test.F', 'dummy.test.F', ['dummy.test.C'], None),
|
||||
('dummy.test.C', 'dummy.test.C', ['dummy.test.A'], None),
|
||||
('dummy.test.E', 'dummy.test.E', ['dummy.test.B'], None),
|
||||
('dummy.test.D', 'dummy.test.D', ['dummy.test.B', 'dummy.test.C'], None),
|
||||
('dummy.test.B', 'dummy.test.B', [], None)
|
||||
]
|
||||
|
||||
# inheritance diagram with 2 top classes
|
||||
# :top-classes: dummy.test.B, dummy.test.C
|
||||
# Note: we're specifying separate classes, not the entire module here
|
||||
# rendering should be
|
||||
#
|
||||
# B C
|
||||
# / \ / \
|
||||
# E D F
|
||||
#
|
||||
for cls in graphs['diagram_w_2_top_classes'].class_info:
|
||||
assert cls in [
|
||||
('dummy.test.F', 'dummy.test.F', ['dummy.test.C'], None),
|
||||
('dummy.test.C', 'dummy.test.C', [], None),
|
||||
('dummy.test.E', 'dummy.test.E', ['dummy.test.B'], None),
|
||||
('dummy.test.D', 'dummy.test.D', ['dummy.test.B', 'dummy.test.C'], None),
|
||||
('dummy.test.B', 'dummy.test.B', [], None)
|
||||
]
|
||||
|
||||
# inheritance diagram with 2 top classes and specifiying the entire module
|
||||
# rendering should be
|
||||
#
|
||||
# A
|
||||
# B C
|
||||
# / \ / \
|
||||
# E D F
|
||||
#
|
||||
# Note: dummy.test.A is included in the graph before its descendants are even processed
|
||||
# b/c we've specified to load the entire module. The way InheritanceGraph works it is very
|
||||
# hard to exclude parent classes once after they have been included in the graph.
|
||||
# If you'd like to not show class A in the graph don't specify the entire module.
|
||||
# this is a known issue.
|
||||
for cls in graphs['diagram_module_w_2_top_classes'].class_info:
|
||||
assert cls in [
|
||||
('dummy.test.F', 'dummy.test.F', ['dummy.test.C'], None),
|
||||
('dummy.test.C', 'dummy.test.C', [], None),
|
||||
('dummy.test.E', 'dummy.test.E', ['dummy.test.B'], None),
|
||||
('dummy.test.D', 'dummy.test.D', ['dummy.test.B', 'dummy.test.C'], None),
|
||||
('dummy.test.B', 'dummy.test.B', [], None),
|
||||
('dummy.test.A', 'dummy.test.A', [], None),
|
||||
]
|
||||
@@ -9,11 +9,128 @@
|
||||
"""
|
||||
|
||||
import re
|
||||
import os
|
||||
import sys
|
||||
|
||||
import pytest
|
||||
|
||||
from sphinx.ext.inheritance_diagram import InheritanceException, import_classes
|
||||
from sphinx.ext.inheritance_diagram import (
|
||||
InheritanceDiagram, InheritanceException, import_classes
|
||||
)
|
||||
|
||||
|
||||
@pytest.mark.sphinx(buildername="html", testroot="inheritance")
|
||||
@pytest.mark.usefixtures('if_graphviz_found')
|
||||
def test_inheritance_diagram(app, status, warning):
|
||||
# monkey-patch InheritaceDiagram.run() so we can get access to its
|
||||
# results.
|
||||
orig_run = InheritanceDiagram.run
|
||||
graphs = {}
|
||||
|
||||
def new_run(self):
|
||||
result = orig_run(self)
|
||||
node = result[0]
|
||||
source = os.path.basename(node.document.current_source).replace(".rst", "")
|
||||
graphs[source] = node['graph']
|
||||
return result
|
||||
|
||||
InheritanceDiagram.run = new_run
|
||||
|
||||
try:
|
||||
app.builder.build_all()
|
||||
finally:
|
||||
InheritanceDiagram.run = orig_run
|
||||
|
||||
assert app.statuscode == 0
|
||||
|
||||
html_warnings = warning.getvalue()
|
||||
assert html_warnings == ""
|
||||
|
||||
# note: it is better to split these asserts into separate test functions
|
||||
# but I can't figure out how to build only a specific .rst file
|
||||
|
||||
# basic inheritance diagram showing all classes
|
||||
for cls in graphs['basic_diagram'].class_info:
|
||||
# use in b/c traversing order is different sometimes
|
||||
assert cls in [
|
||||
('dummy.test.A', 'dummy.test.A', [], None),
|
||||
('dummy.test.F', 'dummy.test.F', ['dummy.test.C'], None),
|
||||
('dummy.test.C', 'dummy.test.C', ['dummy.test.A'], None),
|
||||
('dummy.test.E', 'dummy.test.E', ['dummy.test.B'], None),
|
||||
('dummy.test.D', 'dummy.test.D', ['dummy.test.B', 'dummy.test.C'], None),
|
||||
('dummy.test.B', 'dummy.test.B', ['dummy.test.A'], None)
|
||||
]
|
||||
|
||||
# inheritance diagram using :parts: 1 option
|
||||
for cls in graphs['diagram_w_parts'].class_info:
|
||||
assert cls in [
|
||||
('A', 'dummy.test.A', [], None),
|
||||
('F', 'dummy.test.F', ['C'], None),
|
||||
('C', 'dummy.test.C', ['A'], None),
|
||||
('E', 'dummy.test.E', ['B'], None),
|
||||
('D', 'dummy.test.D', ['B', 'C'], None),
|
||||
('B', 'dummy.test.B', ['A'], None)
|
||||
]
|
||||
|
||||
# inheritance diagram with 1 top class
|
||||
# :top-classes: dummy.test.B
|
||||
# rendering should be
|
||||
# A
|
||||
# \
|
||||
# B C
|
||||
# / \ / \
|
||||
# E D F
|
||||
#
|
||||
for cls in graphs['diagram_w_1_top_class'].class_info:
|
||||
assert cls in [
|
||||
('dummy.test.A', 'dummy.test.A', [], None),
|
||||
('dummy.test.F', 'dummy.test.F', ['dummy.test.C'], None),
|
||||
('dummy.test.C', 'dummy.test.C', ['dummy.test.A'], None),
|
||||
('dummy.test.E', 'dummy.test.E', ['dummy.test.B'], None),
|
||||
('dummy.test.D', 'dummy.test.D', ['dummy.test.B', 'dummy.test.C'], None),
|
||||
('dummy.test.B', 'dummy.test.B', [], None)
|
||||
]
|
||||
|
||||
# inheritance diagram with 2 top classes
|
||||
# :top-classes: dummy.test.B, dummy.test.C
|
||||
# Note: we're specifying separate classes, not the entire module here
|
||||
# rendering should be
|
||||
#
|
||||
# B C
|
||||
# / \ / \
|
||||
# E D F
|
||||
#
|
||||
for cls in graphs['diagram_w_2_top_classes'].class_info:
|
||||
assert cls in [
|
||||
('dummy.test.F', 'dummy.test.F', ['dummy.test.C'], None),
|
||||
('dummy.test.C', 'dummy.test.C', [], None),
|
||||
('dummy.test.E', 'dummy.test.E', ['dummy.test.B'], None),
|
||||
('dummy.test.D', 'dummy.test.D', ['dummy.test.B', 'dummy.test.C'], None),
|
||||
('dummy.test.B', 'dummy.test.B', [], None)
|
||||
]
|
||||
|
||||
# inheritance diagram with 2 top classes and specifiying the entire module
|
||||
# rendering should be
|
||||
#
|
||||
# A
|
||||
# B C
|
||||
# / \ / \
|
||||
# E D F
|
||||
#
|
||||
# Note: dummy.test.A is included in the graph before its descendants are even processed
|
||||
# b/c we've specified to load the entire module. The way InheritanceGraph works it is very
|
||||
# hard to exclude parent classes once after they have been included in the graph.
|
||||
# If you'd like to not show class A in the graph don't specify the entire module.
|
||||
# this is a known issue.
|
||||
for cls in graphs['diagram_module_w_2_top_classes'].class_info:
|
||||
assert cls in [
|
||||
('dummy.test.F', 'dummy.test.F', ['dummy.test.C'], None),
|
||||
('dummy.test.C', 'dummy.test.C', [], None),
|
||||
('dummy.test.E', 'dummy.test.E', ['dummy.test.B'], None),
|
||||
('dummy.test.D', 'dummy.test.D', ['dummy.test.B', 'dummy.test.C'], None),
|
||||
('dummy.test.B', 'dummy.test.B', [], None),
|
||||
('dummy.test.A', 'dummy.test.A', [], None),
|
||||
]
|
||||
|
||||
|
||||
@pytest.mark.sphinx('html', testroot='ext-inheritance_diagram')
|
||||
|
||||
@@ -314,6 +314,21 @@ def test_decorators():
|
||||
'Foo.method': ('def', 13, 15)}
|
||||
|
||||
|
||||
def test_async_function_and_method():
|
||||
source = ('async def some_function():\n'
|
||||
' """docstring"""\n'
|
||||
' a = 1 + 1 #: comment1\n'
|
||||
'\n'
|
||||
'class Foo:\n'
|
||||
' async def method(self):\n'
|
||||
' pass\n')
|
||||
parser = Parser(source)
|
||||
parser.parse()
|
||||
assert parser.definitions == {'some_function': ('def', 1, 3),
|
||||
'Foo': ('class', 5, 7),
|
||||
'Foo.method': ('def', 6, 7)}
|
||||
|
||||
|
||||
def test_formfeed_char():
|
||||
source = ('class Foo:\n'
|
||||
'\f\n'
|
||||
|
||||
@@ -397,6 +397,22 @@ def test_isstaticmethod(app):
|
||||
assert inspect.isstaticmethod(Inherited.meth, Inherited, 'meth') is False
|
||||
|
||||
|
||||
@pytest.mark.sphinx(testroot='ext-autodoc')
|
||||
def test_iscoroutinefunction(app):
|
||||
from target.functions import coroutinefunc, func, partial_coroutinefunc
|
||||
from target.methods import Base
|
||||
|
||||
assert inspect.iscoroutinefunction(func) is False # function
|
||||
assert inspect.iscoroutinefunction(coroutinefunc) is True # coroutine
|
||||
assert inspect.iscoroutinefunction(partial_coroutinefunc) is True # partial-ed coroutine
|
||||
assert inspect.iscoroutinefunction(Base.meth) is False # method
|
||||
assert inspect.iscoroutinefunction(Base.coroutinemeth) is True # coroutine-method
|
||||
|
||||
# partial-ed coroutine-method
|
||||
partial_coroutinemeth = Base.__dict__['partial_coroutinemeth']
|
||||
assert inspect.iscoroutinefunction(partial_coroutinemeth) is True
|
||||
|
||||
|
||||
@pytest.mark.sphinx(testroot='ext-autodoc')
|
||||
def test_isfunction(app):
|
||||
from target.functions import builtin_func, partial_builtin_func
|
||||
|
||||
@@ -9,8 +9,11 @@
|
||||
"""
|
||||
|
||||
from docutils.statemachine import StringList
|
||||
from jinja2 import Environment
|
||||
|
||||
from sphinx.util.rst import append_epilog, escape, prepend_prolog
|
||||
from sphinx.util.rst import (
|
||||
append_epilog, escape, heading, prepend_prolog, textwidth
|
||||
)
|
||||
|
||||
|
||||
def test_escape():
|
||||
@@ -83,3 +86,34 @@ def test_prepend_prolog_without_CR(app):
|
||||
('<generated>', 0, ''),
|
||||
('dummy.rst', 0, 'hello Sphinx world'),
|
||||
('dummy.rst', 1, 'Sphinx is a document generator')]
|
||||
|
||||
|
||||
def test_textwidth():
|
||||
assert textwidth('Hello') == 5
|
||||
assert textwidth('русский язык') == 12
|
||||
assert textwidth('русский язык', 'WFA') == 23 # Cyrillic are ambiguous chars
|
||||
|
||||
|
||||
def test_heading():
|
||||
env = Environment()
|
||||
env.extend(language=None)
|
||||
|
||||
assert heading(env, 'Hello') == ('Hello\n'
|
||||
'=====')
|
||||
assert heading(env, 'Hello', 1) == ('Hello\n'
|
||||
'=====')
|
||||
assert heading(env, 'Hello', 2) == ('Hello\n'
|
||||
'-----')
|
||||
assert heading(env, 'Hello', 3) == ('Hello\n'
|
||||
'~~~~~')
|
||||
assert heading(env, 'русский язык', 1) == (
|
||||
'русский язык\n'
|
||||
'============'
|
||||
)
|
||||
|
||||
# language=ja: ambiguous
|
||||
env.language = 'ja'
|
||||
assert heading(env, 'русский язык', 1) == (
|
||||
'русский язык\n'
|
||||
'======================='
|
||||
)
|
||||
|
||||
37
tests/test_util_template.py
Normal file
37
tests/test_util_template.py
Normal file
@@ -0,0 +1,37 @@
|
||||
"""
|
||||
test_util_template
|
||||
~~~~~~~~~~~~~~~~~~
|
||||
|
||||
Tests sphinx.util.template functions.
|
||||
|
||||
:copyright: Copyright 2007-2019 by the Sphinx team, see AUTHORS.
|
||||
:license: BSD, see LICENSE for details.
|
||||
"""
|
||||
|
||||
from sphinx.util.template import ReSTRenderer
|
||||
|
||||
|
||||
def test_ReSTRenderer_escape():
|
||||
r = ReSTRenderer()
|
||||
template = '{{ "*hello*" | e }}'
|
||||
assert r.render_string(template, {}) == r'\*hello\*'
|
||||
|
||||
|
||||
def test_ReSTRenderer_heading():
|
||||
r = ReSTRenderer()
|
||||
|
||||
template = '{{ "hello" | heading }}'
|
||||
assert r.render_string(template, {}) == 'hello\n====='
|
||||
|
||||
template = '{{ "hello" | heading(1) }}'
|
||||
assert r.render_string(template, {}) == 'hello\n====='
|
||||
|
||||
template = '{{ "русский язык" | heading(2) }}'
|
||||
assert r.render_string(template, {}) == ('русский язык\n'
|
||||
'------------')
|
||||
|
||||
# language: ja
|
||||
r.env.language = 'ja'
|
||||
template = '{{ "русский язык" | heading }}'
|
||||
assert r.render_string(template, {}) == ('русский язык\n'
|
||||
'=======================')
|
||||
5
tox.ini
5
tox.ini
@@ -15,7 +15,6 @@ deps =
|
||||
du14: docutils==0.14
|
||||
extras =
|
||||
test
|
||||
websupport
|
||||
setenv =
|
||||
PYTHONWARNINGS = all,ignore::ImportWarning:pkgutil,ignore::ImportWarning:importlib._bootstrap,ignore::ImportWarning:importlib._bootstrap_external,ignore::ImportWarning:pytest_cov.plugin,ignore::DeprecationWarning:site,ignore::DeprecationWarning:_pytest.assertion.rewrite,ignore::DeprecationWarning:_pytest.fixtures,ignore::DeprecationWarning:distutils
|
||||
commands=
|
||||
@@ -62,8 +61,8 @@ commands=
|
||||
basepython = python3
|
||||
description =
|
||||
Build documentation.
|
||||
deps =
|
||||
sphinxcontrib-websupport
|
||||
extras =
|
||||
docs
|
||||
commands =
|
||||
python setup.py build_sphinx {posargs}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user