Merge branch '2.0'

This commit is contained in:
Takeshi KOMIYA
2019-05-06 21:09:43 +09:00
30 changed files with 802 additions and 264 deletions

View File

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

View File

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

View File

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

View File

@@ -38,6 +38,9 @@ extras_require = {
':sys_platform=="win32"': [
'colorama>=0.3.5',
],
'docs': [
'sphinxcontrib-websupport',
],
'test': [
'pytest',
'pytest-cov',

View File

@@ -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('<', '&lt;')
name = name.replace('>', '&gt;')
@@ -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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@@ -0,0 +1,9 @@
{%- if show_headings %}
{{- [basename, "module"] | join(' ') | e | heading }}
{% endif -%}
.. automodule:: {{ qualname }}
{%- for option in automodule_options %}
:{{ option }}:
{%- endfor %}

View 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 %}

View File

@@ -0,0 +1,8 @@
{{ header | heading }}
.. toctree::
:maxdepth: {{ maxdepth }}
{% for docname in docnames %}
{{ docname }}
{%- endfor %}

View File

@@ -1,5 +1,5 @@
\documentclass[12pt]{article}
\usepackage[utf8x]{inputenc}
\usepackage[utf8]{inputenc}
\usepackage{amsmath}
\usepackage{amsthm}
\usepackage{amssymb}

View File

@@ -1,5 +1,5 @@
\documentclass[12pt]{article}
\usepackage[utf8x]{inputenc}
\usepackage[utf8]{inputenc}
\usepackage{amsmath}
\usepackage{amsthm}
\usepackage{amssymb}

View File

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

View File

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

View File

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

View File

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

View File

@@ -19,6 +19,11 @@ class Base():
partialmeth = partialmethod(meth)
async def coroutinemeth(self):
pass
partial_coroutinemeth = partialmethod(coroutinemeth)
class Inherited(Base):
pass

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View 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'
'=======================')

View File

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