Merge branch '4.x'

This commit is contained in:
Takeshi KOMIYA 2021-05-03 22:33:12 +09:00
commit 3027a2f867
28 changed files with 467 additions and 152 deletions

View File

@ -21,7 +21,7 @@ jobs:
docutils: du16
- name: py39
python: 3.9
docutils: du16
docutils: du17
coverage: "--cov ./ --cov-append --cov-config setup.cfg"
# - name: py310-dev
# python: 3.10-dev

65
CHANGES
View File

@ -31,20 +31,34 @@ Incompatible changes
Deprecated
----------
* ``sphinx.util.docstrings.extract_metadata()``
Features added
--------------
* #8107: autodoc: Add ``class-doc-from`` option to :rst:dir:`autoclass`
directive to control the content of the specific class like
:confval:`autoclass_content`
* #8588: autodoc: :confval:`autodoc_type_aliases` now supports dotted name. It
allows you to define an alias for a class with module name like
``foo.bar.BazClass``
* #9129: html search: Show search summaries when html_copy_source = False
* #9120: html theme: Eliminate prompt characters of code-block from copyable
text
* #9097: Optimize the paralell build
Bugs fixed
----------
* #8872: autodoc: stacked singledispatches are wrongly rendered
* #8597: autodoc: a docsting having metadata only should be treated as
undocumented
Testing
--------
Release 4.0.0 beta2 (in development)
Release 4.0.0 beta3 (in development)
====================================
Dependencies
@ -53,15 +67,37 @@ Dependencies
Incompatible changes
--------------------
* #9023: Change the CSS classes on :rst:role:`cpp:expr` and
:rst:role:`cpp:texpr`.
Deprecated
----------
Features added
--------------
Bugs fixed
----------
Testing
--------
Release 4.0.0 beta2 (released Apr 29, 2021)
===========================================
Dependencies
------------
* Support docutils-0.17. Please notice it changes the output of HTML builder.
Some themes do not support it, and you need to update your custom CSS to
upgrade it.
Incompatible changes
--------------------
* #9023: Change the CSS classes on :rst:role:`cpp:expr` and
:rst:role:`cpp:texpr`.
Features added
--------------
* #8818: autodoc: Super class having ``Any`` arguments causes nit-picky warning
* #9095: autodoc: TypeError is raised on processing broken metaclass
* #9110: autodoc: metadata of GenericAlias is not rendered as a reference in
@ -81,9 +117,6 @@ Bugs fixed
* C, C++, fix ``KeyError`` when an ``alias`` directive is the first C/C++
directive in a file with another C/C++ directive later.
Testing
--------
Release 4.0.0 beta1 (released Apr 12, 2021)
===========================================
@ -222,24 +255,6 @@ Bugs fixed
Release 3.5.5 (in development)
==============================
Dependencies
------------
Incompatible changes
--------------------
Deprecated
----------
Features added
--------------
Bugs fixed
----------
Testing
--------
Release 3.5.4 (released Apr 11, 2021)
=====================================

View File

@ -12,7 +12,6 @@ interesting examples.
Documentation using the alabaster theme
---------------------------------------
* `AIOHTTP <https://docs.aiohttp.org/>`__
* `Alabaster <https://alabaster.readthedocs.io/>`__
* `Blinker <https://pythonhosted.org/blinker/>`__
* `Calibre <https://manual.calibre-ebook.com/>`__
@ -311,6 +310,7 @@ Documentation using sphinx_bootstrap_theme
Documentation using a custom theme or integrated in a website
-------------------------------------------------------------
* `AIOHTTP <https://docs.aiohttp.org/>`__
* `Apache Cassandra <https://cassandra.apache.org/doc/>`__
* `Astropy <http://docs.astropy.org/>`__
* `Bokeh <https://bokeh.pydata.org/>`__

View File

@ -22,6 +22,11 @@ The following is a list of deprecated interfaces.
- (will be) Removed
- Alternatives
* - ``sphinx.util.docstrings.extract_metadata()``
- 4.1
- 6.0
- ``sphinx.util.docstrings.separate_metadata()``
* - ``favicon`` variable in HTML templates
- 4.0
- TBD

View File

@ -343,6 +343,10 @@ inserting them into the page source under a suitable :rst:dir:`py:module`,
.. autoclass:: module.name::Noodle
* :rst:dir:`autoclass` also recognizes the ``class-doc-from`` option that
can be used to override the global value of :confval:`autoclass_content`.
.. versionadded:: 4.1
.. rst:directive:: autofunction
autodecorator
@ -507,7 +511,7 @@ There are also config values that you can set:
The supported options are ``'members'``, ``'member-order'``,
``'undoc-members'``, ``'private-members'``, ``'special-members'``,
``'inherited-members'``, ``'show-inheritance'``, ``'ignore-module-all'``,
``'imported-members'`` and ``'exclude-members'``.
``'imported-members'``, ``'exclude-members'`` and ``'class-doc-from'``.
.. versionadded:: 1.8
@ -517,6 +521,9 @@ There are also config values that you can set:
.. versionchanged:: 2.1
Added ``'imported-members'``.
.. versionchanged:: 4.1
Added ``'class-doc-from'``.
.. confval:: autodoc_docstring_signature
Functions imported from C modules cannot be introspected, and therefore the

View File

@ -1680,6 +1680,9 @@ There is a set of directives allowing documenting command-line programs:
then ``:option:`rm -r``` would refer to the first option, while
``:option:`svn -r``` would refer to the second one.
If ``None`` is passed to the argument, the directive will reset the
current program name.
The program name may contain spaces (in case you want to document
subcommands like ``svn add`` and ``svn commit`` separately).

View File

@ -21,9 +21,10 @@ install_requires = [
'sphinxcontrib-htmlhelp',
'sphinxcontrib-serializinghtml',
'sphinxcontrib-qthelp',
'Jinja2>=2.3',
'Jinja2>=2.3,<3.0',
'MarkupSafe<2.0',
'Pygments>=2.0',
'docutils>=0.14,<0.17',
'docutils>=0.14,<0.18',
'snowballstemmer>=1.1',
'babel>=1.3',
'alabaster>=0.7,<0.8',

View File

@ -1166,7 +1166,11 @@ def setup_js_tag_helper(app: Sphinx, pagename: str, templatename: str,
else:
# str value (old styled)
attrs.append('src="%s"' % pathto(js, resource=True))
return '<script %s>%s</script>' % (' '.join(attrs), body)
if attrs:
return '<script %s>%s</script>' % (' '.join(attrs), body)
else:
return '<script>%s</script>' % body
context['js_tag'] = js_tag

View File

@ -129,7 +129,7 @@ class CheckExternalLinksBuilder(DummyBuilder):
# create queues and worker threads
self._wqueue: PriorityQueue[CheckRequestType] = PriorityQueue()
self._rqueue: Queue = Queue()
self._rqueue: Queue[CheckResult] = Queue()
@property
def anchors_ignore(self) -> List[Pattern]:
@ -228,43 +228,39 @@ class CheckExternalLinksBuilder(DummyBuilder):
)
return self._wqueue
def process_result(self, result: Tuple[str, str, int, str, str, int]) -> None:
uri, docname, lineno, status, info, code = result
def process_result(self, result: CheckResult) -> None:
filename = self.env.doc2path(result.docname, None)
filename = self.env.doc2path(docname, None)
linkstat = dict(filename=filename, lineno=lineno,
status=status, code=code, uri=uri,
info=info)
if status == 'unchecked':
self.write_linkstat(linkstat)
linkstat = dict(filename=filename, lineno=result.lineno,
status=result.status, code=result.code, uri=result.uri,
info=result.message)
self.write_linkstat(linkstat)
if result.status == 'unchecked':
return
if status == 'working' and info == 'old':
self.write_linkstat(linkstat)
if result.status == 'working' and result.message == 'old':
return
if lineno:
logger.info('(%16s: line %4d) ', docname, lineno, nonl=True)
if status == 'ignored':
if info:
logger.info(darkgray('-ignored- ') + uri + ': ' + info)
if result.lineno:
logger.info('(%16s: line %4d) ', result.docname, result.lineno, nonl=True)
if result.status == 'ignored':
if result.message:
logger.info(darkgray('-ignored- ') + result.uri + ': ' + result.message)
else:
logger.info(darkgray('-ignored- ') + uri)
self.write_linkstat(linkstat)
elif status == 'local':
logger.info(darkgray('-local- ') + uri)
self.write_entry('local', docname, filename, lineno, uri)
self.write_linkstat(linkstat)
elif status == 'working':
logger.info(darkgreen('ok ') + uri + info)
self.write_linkstat(linkstat)
elif status == 'broken':
logger.info(darkgray('-ignored- ') + result.uri)
elif result.status == 'local':
logger.info(darkgray('-local- ') + result.uri)
self.write_entry('local', result.docname, filename, result.lineno, result.uri)
elif result.status == 'working':
logger.info(darkgreen('ok ') + result.uri + result.message)
elif result.status == 'broken':
if self.app.quiet or self.app.warningiserror:
logger.warning(__('broken link: %s (%s)'), uri, info,
location=(filename, lineno))
logger.warning(__('broken link: %s (%s)'), result.uri, result.message,
location=(filename, result.lineno))
else:
logger.info(red('broken ') + uri + red(' - ' + info))
self.write_entry('broken', docname, filename, lineno, uri + ': ' + info)
self.write_linkstat(linkstat)
elif status == 'redirected':
logger.info(red('broken ') + result.uri + red(' - ' + result.message))
self.write_entry('broken', result.docname, filename, result.lineno,
result.uri + ': ' + result.message)
elif result.status == 'redirected':
try:
text, color = {
301: ('permanently', purple),
@ -272,16 +268,16 @@ class CheckExternalLinksBuilder(DummyBuilder):
303: ('with See Other', purple),
307: ('temporarily', turquoise),
308: ('permanently', purple),
}[code]
}[result.code]
except KeyError:
text, color = ('with unknown code', purple)
linkstat['text'] = text
logger.info(color('redirect ') + uri + color(' - ' + text + ' to ' + info))
self.write_entry('redirected ' + text, docname, filename,
lineno, uri + ' to ' + info)
self.write_linkstat(linkstat)
logger.info(color('redirect ') + result.uri +
color(' - ' + text + ' to ' + result.message))
self.write_entry('redirected ' + text, result.docname, filename,
result.lineno, result.uri + ' to ' + result.message)
else:
raise ValueError("Unknown status %s." % status)
raise ValueError("Unknown status %s." % result.status)
def write_entry(self, what: str, docname: str, filename: str, line: int,
uri: str) -> None:
@ -576,7 +572,7 @@ class HyperlinkAvailabilityCheckWorker(Thread):
if status == 'rate-limited':
logger.info(darkgray('-rate limited- ') + uri + darkgray(' | sleeping...'))
else:
self.rqueue.put((uri, docname, lineno, status, info, code))
self.rqueue.put(CheckResult(uri, docname, lineno, status, info, code))
self.wqueue.task_done()
def limit_rate(self, response: Response) -> Optional[float]:

View File

@ -30,7 +30,7 @@ from sphinx.ext.autodoc.mock import ismock, mock, undecorate
from sphinx.locale import _, __
from sphinx.pycode import ModuleAnalyzer, PycodeError
from sphinx.util import inspect, logging
from sphinx.util.docstrings import extract_metadata, prepare_docstring
from sphinx.util.docstrings import prepare_docstring, separate_metadata
from sphinx.util.inspect import (evaluate_signature, getdoc, object_description, safe_getattr,
stringify_signature)
from sphinx.util.typing import OptionSpec, get_type_hints, restify
@ -129,6 +129,14 @@ def member_order_option(arg: Any) -> Optional[str]:
raise ValueError(__('invalid value for member-order option: %s') % arg)
def class_doc_from_option(arg: Any) -> Optional[str]:
"""Used to convert the :class-doc-from: option to autoclass directives."""
if arg in ('both', 'class', 'init'):
return arg
else:
raise ValueError(__('invalid value for class-doc-from option: %s') % arg)
SUPPRESS = object()
@ -722,9 +730,9 @@ class Documenter:
# hack for ClassDocumenter to inject docstring via ObjectMember
doc = obj.docstring
doc, metadata = separate_metadata(doc)
has_doc = bool(doc)
metadata = extract_metadata(doc)
if 'private' in metadata:
# consider a member private if docstring has "private" metadata
isprivate = True
@ -1320,12 +1328,12 @@ class FunctionDocumenter(DocstringSignatureMixin, ModuleLevelDocumenter): # typ
if typ is object:
pass # default implementation. skipped.
else:
self.annotate_to_first_argument(func, typ)
documenter = FunctionDocumenter(self.directive, '')
documenter.object = func
documenter.objpath = [None]
sigs.append(documenter.format_signature())
dispatchfunc = self.annotate_to_first_argument(func, typ)
if dispatchfunc:
documenter = FunctionDocumenter(self.directive, '')
documenter.object = dispatchfunc
documenter.objpath = [None]
sigs.append(documenter.format_signature())
if overloaded:
actual = inspect.signature(self.object,
type_aliases=self.config.autodoc_type_aliases)
@ -1350,28 +1358,34 @@ class FunctionDocumenter(DocstringSignatureMixin, ModuleLevelDocumenter): # typ
return overload.replace(parameters=parameters)
def annotate_to_first_argument(self, func: Callable, typ: Type) -> None:
def annotate_to_first_argument(self, func: Callable, typ: Type) -> Optional[Callable]:
"""Annotate type hint to the first argument of function if needed."""
try:
sig = inspect.signature(func, type_aliases=self.config.autodoc_type_aliases)
except TypeError as exc:
logger.warning(__("Failed to get a function signature for %s: %s"),
self.fullname, exc)
return
return None
except ValueError:
return
return None
if len(sig.parameters) == 0:
return
return None
def dummy():
pass
params = list(sig.parameters.values())
if params[0].annotation is Parameter.empty:
params[0] = params[0].replace(annotation=typ)
try:
func.__signature__ = sig.replace(parameters=params) # type: ignore
dummy.__signature__ = sig.replace(parameters=params) # type: ignore
return dummy
except (AttributeError, TypeError):
# failed to update signature (ex. built-in or extension types)
return
return None
else:
return None
class DecoratorDocumenter(FunctionDocumenter):
@ -1417,6 +1431,7 @@ class ClassDocumenter(DocstringSignatureMixin, ModuleLevelDocumenter): # type:
'show-inheritance': bool_option, 'member-order': member_order_option,
'exclude-members': exclude_members_option,
'private-members': members_option, 'special-members': members_option,
'class-doc-from': class_doc_from_option,
}
_signature_class: Any = None
@ -1651,7 +1666,7 @@ class ClassDocumenter(DocstringSignatureMixin, ModuleLevelDocumenter): # type:
if lines is not None:
return lines
content = self.config.autoclass_content
classdoc_from = self.options.get('class-doc-from', self.config.autoclass_content)
docstrings = []
attrdocstring = self.get_attr(self.object, '__doc__', None)
@ -1660,7 +1675,7 @@ class ClassDocumenter(DocstringSignatureMixin, ModuleLevelDocumenter): # type:
# for classes, what the "docstring" is can be controlled via a
# config value; the default is only the class docstring
if content in ('both', 'init'):
if classdoc_from in ('both', 'init'):
__init__ = self.get_attr(self.object, '__init__', None)
initdocstring = getdoc(__init__, self.get_attr,
self.config.autodoc_inherit_docstrings,
@ -1682,7 +1697,7 @@ class ClassDocumenter(DocstringSignatureMixin, ModuleLevelDocumenter): # type:
initdocstring.strip() == object.__new__.__doc__)): # for !pypy
initdocstring = None
if initdocstring:
if content == 'init':
if classdoc_from == 'init':
docstrings = [initdocstring]
else:
docstrings.append(initdocstring)
@ -1918,7 +1933,7 @@ class DataDocumenter(GenericAliasMixin, NewTypeMixin, TypeVarMixin,
return True
else:
doc = self.get_doc()
metadata = extract_metadata('\n'.join(sum(doc, [])))
docstring, metadata = separate_metadata('\n'.join(sum(doc, [])))
if 'hide-value' in metadata:
return True
@ -2109,13 +2124,13 @@ class MethodDocumenter(DocstringSignatureMixin, ClassLevelDocumenter): # type:
if typ is object:
pass # default implementation. skipped.
else:
self.annotate_to_first_argument(func, typ)
documenter = MethodDocumenter(self.directive, '')
documenter.parent = self.parent
documenter.object = func
documenter.objpath = [None]
sigs.append(documenter.format_signature())
dispatchmeth = self.annotate_to_first_argument(func, typ)
if dispatchmeth:
documenter = MethodDocumenter(self.directive, '')
documenter.parent = self.parent
documenter.object = dispatchmeth
documenter.objpath = [None]
sigs.append(documenter.format_signature())
if overloaded:
if inspect.isstaticmethod(self.object, cls=self.parent, name=self.object_name):
actual = inspect.signature(self.object, bound_method=False,
@ -2149,27 +2164,34 @@ class MethodDocumenter(DocstringSignatureMixin, ClassLevelDocumenter): # type:
return overload.replace(parameters=parameters)
def annotate_to_first_argument(self, func: Callable, typ: Type) -> None:
def annotate_to_first_argument(self, func: Callable, typ: Type) -> Optional[Callable]:
"""Annotate type hint to the first argument of function if needed."""
try:
sig = inspect.signature(func, type_aliases=self.config.autodoc_type_aliases)
except TypeError as exc:
logger.warning(__("Failed to get a method signature for %s: %s"),
self.fullname, exc)
return
return None
except ValueError:
return
return None
if len(sig.parameters) == 1:
return
return None
def dummy():
pass
params = list(sig.parameters.values())
if params[1].annotation is Parameter.empty:
params[1] = params[1].replace(annotation=typ)
try:
func.__signature__ = sig.replace(parameters=params) # type: ignore
dummy.__signature__ = sig.replace(parameters=params) # type: ignore
return dummy
except (AttributeError, TypeError):
# failed to update signature (ex. built-in or extension types)
return
return None
else:
return None
class NonDataDescriptorMixin(DataDocumenterMixinBase):
@ -2456,7 +2478,7 @@ class AttributeDocumenter(GenericAliasMixin, NewTypeMixin, SlotsMixin, # type:
else:
doc = self.get_doc()
if doc:
metadata = extract_metadata('\n'.join(sum(doc, [])))
docstring, metadata = separate_metadata('\n'.join(sum(doc, [])))
if 'hide-value' in metadata:
return True

View File

@ -30,7 +30,7 @@ logger = logging.getLogger(__name__)
AUTODOC_DEFAULT_OPTIONS = ['members', 'undoc-members', 'inherited-members',
'show-inheritance', 'private-members', 'special-members',
'ignore-module-all', 'exclude-members', 'member-order',
'imported-members']
'imported-members', 'class-doc-from']
AUTODOC_EXTENDABLE_OPTIONS = ['members', 'private-members', 'special-members',
'exclude-members']

View File

@ -819,7 +819,7 @@ div.code-block-caption code {
table.highlighttable td.linenos,
span.linenos,
div.doctest > div.highlight span.gp { /* gp: Generic.Prompt */
div.highlight span.gp { /* gp: Generic.Prompt */
user-select: none;
-webkit-user-select: text; /* Safari fallback only */
-webkit-user-select: none; /* Chrome/Safari */

View File

@ -11,26 +11,28 @@
import re
import sys
import warnings
from typing import Dict, List
from typing import Dict, List, Tuple
from docutils.parsers.rst.states import Body
from sphinx.deprecation import RemovedInSphinx50Warning
from sphinx.deprecation import RemovedInSphinx50Warning, RemovedInSphinx60Warning
field_list_item_re = re.compile(Body.patterns['field_marker'])
def extract_metadata(s: str) -> Dict[str, str]:
"""Extract metadata from docstring."""
def separate_metadata(s: str) -> Tuple[str, Dict[str, str]]:
"""Separate docstring into metadata and others."""
in_other_element = False
metadata: Dict[str, str] = {}
lines = []
if not s:
return metadata
return s, metadata
for line in prepare_docstring(s):
if line.strip() == '':
in_other_element = False
lines.append(line)
else:
matched = field_list_item_re.match(line)
if matched and not in_other_element:
@ -38,9 +40,20 @@ def extract_metadata(s: str) -> Dict[str, str]:
if field_name.startswith('meta '):
name = field_name[5:].strip()
metadata[name] = line[matched.end():].strip()
else:
lines.append(line)
else:
in_other_element = True
lines.append(line)
return '\n'.join(lines), metadata
def extract_metadata(s: str) -> Dict[str, str]:
warnings.warn("extract_metadata() is deprecated.",
RemovedInSphinx60Warning, stacklevel=2)
docstring, metadata = separate_metadata(s)
return metadata

View File

@ -18,8 +18,10 @@ import types
import typing
import warnings
from functools import partial, partialmethod
from importlib import import_module
from inspect import Parameter, isclass, ismethod, ismethoddescriptor, ismodule # NOQA
from io import StringIO
from types import ModuleType
from typing import Any, Callable, Dict, Mapping, Optional, Sequence, Tuple, Type, cast
from sphinx.deprecation import RemovedInSphinx50Warning
@ -501,6 +503,78 @@ class DefaultValue:
return self.value
class TypeAliasForwardRef:
"""Pseudo typing class for autodoc_type_aliases.
This avoids the error on evaluating the type inside `get_type_hints()`.
"""
def __init__(self, name: str) -> None:
self.name = name
def __call__(self) -> None:
# Dummy method to imitate special typing classes
pass
def __eq__(self, other: Any) -> bool:
return self.name == other
class TypeAliasModule:
"""Pseudo module class for autodoc_type_aliases."""
def __init__(self, modname: str, mapping: Dict[str, str]) -> None:
self.__modname = modname
self.__mapping = mapping
self.__module: Optional[ModuleType] = None
def __getattr__(self, name: str) -> Any:
fullname = '.'.join(filter(None, [self.__modname, name]))
if fullname in self.__mapping:
# exactly matched
return TypeAliasForwardRef(self.__mapping[fullname])
else:
prefix = fullname + '.'
nested = {k: v for k, v in self.__mapping.items() if k.startswith(prefix)}
if nested:
# sub modules or classes found
return TypeAliasModule(fullname, nested)
else:
# no sub modules or classes found.
try:
# return the real submodule if exists
return import_module(fullname)
except ImportError:
# return the real class
if self.__module is None:
self.__module = import_module(self.__modname)
return getattr(self.__module, name)
class TypeAliasNamespace(Dict[str, Any]):
"""Pseudo namespace class for autodoc_type_aliases.
This enables to look up nested modules and classes like `mod1.mod2.Class`.
"""
def __init__(self, mapping: Dict[str, str]) -> None:
self.__mapping = mapping
def __getitem__(self, key: str) -> Any:
if key in self.__mapping:
# exactly matched
return TypeAliasForwardRef(self.__mapping[key])
else:
prefix = key + '.'
nested = {k: v for k, v in self.__mapping.items() if k.startswith(prefix)}
if nested:
# sub modules or classes found
return TypeAliasModule(key, nested)
else:
raise KeyError
def _should_unwrap(subject: Callable) -> bool:
"""Check the function should be unwrapped on getting signature."""
__globals__ = getglobals(subject)
@ -549,12 +623,19 @@ def signature(subject: Callable, bound_method: bool = False, follow_wrapped: boo
try:
# Resolve annotations using ``get_type_hints()`` and type_aliases.
annotations = typing.get_type_hints(subject, None, type_aliases)
localns = TypeAliasNamespace(type_aliases)
annotations = typing.get_type_hints(subject, None, localns)
for i, param in enumerate(parameters):
if param.name in annotations:
parameters[i] = param.replace(annotation=annotations[param.name])
annotation = annotations[param.name]
if isinstance(annotation, TypeAliasForwardRef):
annotation = annotation.name
parameters[i] = param.replace(annotation=annotation)
if 'return' in annotations:
return_annotation = annotations['return']
if isinstance(annotations['return'], TypeAliasForwardRef):
return_annotation = annotations['return'].name
else:
return_annotation = annotations['return']
except Exception:
# ``get_type_hints()`` does not support some kind of objects like partial,
# ForwardRef and so on.

View File

@ -29,6 +29,8 @@ tex_replacements = [
# map special Unicode characters to TeX commands
('', r'\(\checkmark\)'),
('', r'\(\pmb{\checkmark}\)'),
('', r'\(\times\)'),
('', r'\(\pmb{\times}\)'),
# used to separate -- in options
('', r'{}'),
# map some special Unicode characters to similar ASCII ones

View File

@ -1,5 +1,6 @@
from __future__ import annotations
import io
from typing import overload
myint = int
@ -11,6 +12,10 @@ variable: myint
variable2 = None # type: myint
def read(r: io.BytesIO) -> io.StringIO:
"""docstring"""
def sum(x: myint, y: myint) -> myint:
"""docstring"""
return x + y

View File

@ -0,0 +1,2 @@
def foo():
""":meta metadata-only-docstring:"""

View File

@ -15,6 +15,7 @@ def func(arg, kwarg=None):
@func.register(int)
@func.register(float)
def _func_int(arg, kwarg=None):
"""A function for int."""
pass

View File

@ -10,6 +10,7 @@ class Foo:
pass
@meth.register(int)
@meth.register(float)
def _meth_int(self, arg, kwarg=None):
"""A method for int."""
pass

View File

@ -42,7 +42,7 @@ latex_additional_files = ['svgimg.svg']
coverage_c_path = ['special/*.h']
coverage_c_regexes = {'function': r'^PyAPI_FUNC\(.*\)\s+([^_][\w_]+)'}
extlinks = {'issue': ('http://bugs.python.org/issue%s', 'issue '),
extlinks = {'issue': ('http://bugs.python.org/issue%s', 'issue %s'),
'pyurl': ('http://python.org/%s', None)}
# modify tags from conf.py

View File

@ -324,6 +324,23 @@ def test_cmdoption(app):
assert domain.progoptions[('ls', '-l')] == ('index', 'cmdoption-ls-l')
def test_cmdoption_for_None(app):
text = (".. program:: ls\n"
".. program:: None\n"
"\n"
".. option:: -l\n")
domain = app.env.get_domain('std')
doctree = restructuredtext.parse(app, text)
assert_node(doctree, (addnodes.index,
[desc, ([desc_signature, ([desc_name, "-l"],
[desc_addname, ()])],
[desc_content, ()])]))
assert_node(doctree[0], addnodes.index,
entries=[('pair', 'command line option; -l', 'cmdoption-l', '', None)])
assert (None, '-l') in domain.progoptions
assert domain.progoptions[(None, '-l')] == ('index', 'cmdoption-l')
def test_multiple_cmdoptions(app):
text = (".. program:: cmd\n"
"\n"

View File

@ -735,6 +735,34 @@ def test_autodoc_undoc_members(app):
]
@pytest.mark.sphinx('html', testroot='ext-autodoc')
def test_autodoc_undoc_members_for_metadata_only(app):
# metadata only member is not displayed
options = {"members": None}
actual = do_autodoc(app, 'module', 'target.metadata', options)
assert list(actual) == [
'',
'.. py:module:: target.metadata',
'',
]
# metadata only member is displayed when undoc-member given
options = {"members": None,
"undoc-members": None}
actual = do_autodoc(app, 'module', 'target.metadata', options)
assert list(actual) == [
'',
'.. py:module:: target.metadata',
'',
'',
'.. py:function:: foo()',
' :module: target.metadata',
'',
' :meta metadata-only-docstring:',
'',
]
@pytest.mark.sphinx('html', testroot='ext-autodoc')
def test_autodoc_inherited_members(app):
options = {"members": None,
@ -2080,6 +2108,7 @@ def test_singledispatch(app):
'',
'',
'.. py:function:: func(arg, kwarg=None)',
' func(arg: float, kwarg=None)',
' func(arg: int, kwarg=None)',
' func(arg: str, kwarg=None)',
' :module: target.singledispatch',
@ -2107,6 +2136,7 @@ def test_singledispatchmethod(app):
'',
'',
' .. py:method:: Foo.meth(arg, kwarg=None)',
' Foo.meth(arg: float, kwarg=None)',
' Foo.meth(arg: int, kwarg=None)',
' Foo.meth(arg: str, kwarg=None)',
' :module: target.singledispatchmethod',
@ -2125,6 +2155,7 @@ def test_singledispatchmethod_automethod(app):
assert list(actual) == [
'',
'.. py:method:: Foo.meth(arg, kwarg=None)',
' Foo.meth(arg: float, kwarg=None)',
' Foo.meth(arg: int, kwarg=None)',
' Foo.meth(arg: str, kwarg=None)',
' :module: target.singledispatchmethod',

View File

@ -264,6 +264,53 @@ def test_show_inheritance_for_subclass_of_generic_type(app):
]
@pytest.mark.sphinx('html', testroot='ext-autodoc')
def test_class_doc_from_class(app):
options = {"members": None,
"class-doc-from": "class"}
actual = do_autodoc(app, 'class', 'target.autoclass_content.C', options)
assert list(actual) == [
'',
'.. py:class:: C()',
' :module: target.autoclass_content',
'',
' A class having __init__, no __new__',
'',
]
@pytest.mark.sphinx('html', testroot='ext-autodoc')
def test_class_doc_from_init(app):
options = {"members": None,
"class-doc-from": "init"}
actual = do_autodoc(app, 'class', 'target.autoclass_content.C', options)
assert list(actual) == [
'',
'.. py:class:: C()',
' :module: target.autoclass_content',
'',
' __init__ docstring',
'',
]
@pytest.mark.sphinx('html', testroot='ext-autodoc')
def test_class_doc_from_both(app):
options = {"members": None,
"class-doc-from": "both"}
actual = do_autodoc(app, 'class', 'target.autoclass_content.C', options)
assert list(actual) == [
'',
'.. py:class:: C()',
' :module: target.autoclass_content',
'',
' A class having __init__, no __new__',
'',
' __init__ docstring',
'',
]
def test_class_alias(app):
def autodoc_process_docstring(*args):
"""A handler always raises an error.

View File

@ -119,6 +119,7 @@ def test_singledispatch(app):
assert list(actual) == [
'',
'.. py:function:: func(arg, kwarg=None)',
' func(arg: float, kwarg=None)',
' func(arg: int, kwarg=None)',
' func(arg: str, kwarg=None)',
' :module: target.singledispatch',

View File

@ -792,27 +792,27 @@ def test_autodoc_typehints_description_for_invalid_node(app):
def test_autodoc_type_aliases(app):
# default
options = {"members": None}
actual = do_autodoc(app, 'module', 'target.annotations', options)
actual = do_autodoc(app, 'module', 'target.autodoc_type_aliases', options)
assert list(actual) == [
'',
'.. py:module:: target.annotations',
'.. py:module:: target.autodoc_type_aliases',
'',
'',
'.. py:class:: Foo()',
' :module: target.annotations',
' :module: target.autodoc_type_aliases',
'',
' docstring',
'',
'',
' .. py:attribute:: Foo.attr1',
' :module: target.annotations',
' :module: target.autodoc_type_aliases',
' :type: int',
'',
' docstring',
'',
'',
' .. py:attribute:: Foo.attr2',
' :module: target.annotations',
' :module: target.autodoc_type_aliases',
' :type: int',
'',
' docstring',
@ -820,26 +820,32 @@ def test_autodoc_type_aliases(app):
'',
'.. py:function:: mult(x: int, y: int) -> int',
' mult(x: float, y: float) -> float',
' :module: target.annotations',
' :module: target.autodoc_type_aliases',
'',
' docstring',
'',
'',
'.. py:function:: read(r: _io.BytesIO) -> _io.StringIO',
' :module: target.autodoc_type_aliases',
'',
' docstring',
'',
'',
'.. py:function:: sum(x: int, y: int) -> int',
' :module: target.annotations',
' :module: target.autodoc_type_aliases',
'',
' docstring',
'',
'',
'.. py:data:: variable',
' :module: target.annotations',
' :module: target.autodoc_type_aliases',
' :type: int',
'',
' docstring',
'',
'',
'.. py:data:: variable2',
' :module: target.annotations',
' :module: target.autodoc_type_aliases',
' :type: int',
' :value: None',
'',
@ -848,28 +854,29 @@ def test_autodoc_type_aliases(app):
]
# define aliases
app.config.autodoc_type_aliases = {'myint': 'myint'}
actual = do_autodoc(app, 'module', 'target.annotations', options)
app.config.autodoc_type_aliases = {'myint': 'myint',
'io.StringIO': 'my.module.StringIO'}
actual = do_autodoc(app, 'module', 'target.autodoc_type_aliases', options)
assert list(actual) == [
'',
'.. py:module:: target.annotations',
'.. py:module:: target.autodoc_type_aliases',
'',
'',
'.. py:class:: Foo()',
' :module: target.annotations',
' :module: target.autodoc_type_aliases',
'',
' docstring',
'',
'',
' .. py:attribute:: Foo.attr1',
' :module: target.annotations',
' :module: target.autodoc_type_aliases',
' :type: myint',
'',
' docstring',
'',
'',
' .. py:attribute:: Foo.attr2',
' :module: target.annotations',
' :module: target.autodoc_type_aliases',
' :type: myint',
'',
' docstring',
@ -877,26 +884,32 @@ def test_autodoc_type_aliases(app):
'',
'.. py:function:: mult(x: myint, y: myint) -> myint',
' mult(x: float, y: float) -> float',
' :module: target.annotations',
' :module: target.autodoc_type_aliases',
'',
' docstring',
'',
'',
'.. py:function:: read(r: _io.BytesIO) -> my.module.StringIO',
' :module: target.autodoc_type_aliases',
'',
' docstring',
'',
'',
'.. py:function:: sum(x: myint, y: myint) -> myint',
' :module: target.annotations',
' :module: target.autodoc_type_aliases',
'',
' docstring',
'',
'',
'.. py:data:: variable',
' :module: target.annotations',
' :module: target.autodoc_type_aliases',
' :type: myint',
'',
' docstring',
'',
'',
'.. py:data:: variable2',
' :module: target.annotations',
' :module: target.autodoc_type_aliases',
' :type: myint',
' :value: None',
'',
@ -911,10 +924,10 @@ def test_autodoc_type_aliases(app):
confoverrides={'autodoc_typehints': "description",
'autodoc_type_aliases': {'myint': 'myint'}})
def test_autodoc_typehints_description_and_type_aliases(app):
(app.srcdir / 'annotations.rst').write_text('.. autofunction:: target.annotations.sum')
(app.srcdir / 'autodoc_type_aliases.rst').write_text('.. autofunction:: target.autodoc_type_aliases.sum')
app.build()
context = (app.outdir / 'annotations.txt').read_text()
assert ('target.annotations.sum(x, y)\n'
context = (app.outdir / 'autodoc_type_aliases.txt').read_text()
assert ('target.autodoc_type_aliases.sum(x, y)\n'
'\n'
' docstring\n'
'\n'

View File

@ -215,11 +215,23 @@ def test_math_compat(app, status, warning):
@pytest.mark.sphinx('html', testroot='ext-math',
confoverrides={'extensions': ['sphinx.ext.mathjax'],
'mathjax_config': {'extensions': ['tex2jax.js']}})
def test_mathjax_config(app, status, warning):
'mathjax3_config': {'extensions': ['tex2jax.js']}})
def test_mathjax3_config(app, status, warning):
app.builder.build_all()
content = (app.outdir / 'index.html').read_text()
assert MATHJAX_URL in content
assert ('<script>window.MathJax = {"extensions": ["tex2jax.js"]}</script>' in content)
@pytest.mark.sphinx('html', testroot='ext-math',
confoverrides={'extensions': ['sphinx.ext.mathjax'],
'mathjax2_config': {'extensions': ['tex2jax.js']}})
def test_mathjax2_config(app, status, warning):
app.builder.build_all()
content = (app.outdir / 'index.html').read_text()
assert MATHJAX_URL in content
assert ('<script type="text/x-mathjax-config">'
'MathJax.Hub.Config({"extensions": ["tex2jax.js"]})'
'</script>' in content)

View File

@ -8,31 +8,48 @@
:license: BSD, see LICENSE for details.
"""
from sphinx.util.docstrings import extract_metadata, prepare_commentdoc, prepare_docstring
from sphinx.util.docstrings import prepare_commentdoc, prepare_docstring, separate_metadata
def test_extract_metadata():
metadata = extract_metadata(":meta foo: bar\n"
":meta baz:\n")
def test_separate_metadata():
# metadata only
text = (":meta foo: bar\n"
":meta baz:\n")
docstring, metadata = separate_metadata(text)
assert docstring == ''
assert metadata == {'foo': 'bar', 'baz': ''}
# non metadata field list item
text = (":meta foo: bar\n"
":param baz:\n")
docstring, metadata = separate_metadata(text)
assert docstring == ':param baz:\n'
assert metadata == {'foo': 'bar'}
# field_list like text following just after paragaph is not a field_list
metadata = extract_metadata("blah blah blah\n"
":meta foo: bar\n"
":meta baz:\n")
text = ("blah blah blah\n"
":meta foo: bar\n"
":meta baz:\n")
docstring, metadata = separate_metadata(text)
assert docstring == text
assert metadata == {}
# field_list like text following after blank line is a field_list
metadata = extract_metadata("blah blah blah\n"
"\n"
":meta foo: bar\n"
":meta baz:\n")
text = ("blah blah blah\n"
"\n"
":meta foo: bar\n"
":meta baz:\n")
docstring, metadata = separate_metadata(text)
assert docstring == "blah blah blah\n\n"
assert metadata == {'foo': 'bar', 'baz': ''}
# non field_list item breaks field_list
metadata = extract_metadata(":meta foo: bar\n"
"blah blah blah\n"
":meta baz:\n")
text = (":meta foo: bar\n"
"blah blah blah\n"
":meta baz:\n")
docstring, metadata = separate_metadata(text)
assert docstring == ("blah blah blah\n"
":meta baz:\n")
assert metadata == {'foo': 'bar'}

View File

@ -19,7 +19,26 @@ import _testcapi
import pytest
from sphinx.util import inspect
from sphinx.util.inspect import stringify_signature
from sphinx.util.inspect import TypeAliasNamespace, stringify_signature
def test_TypeAliasNamespace():
import logging.config
type_alias = TypeAliasNamespace({'logging.Filter': 'MyFilter',
'logging.Handler': 'MyHandler',
'logging.handlers.SyslogHandler': 'MySyslogHandler'})
assert type_alias['logging'].Filter == 'MyFilter'
assert type_alias['logging'].Handler == 'MyHandler'
assert type_alias['logging'].handlers.SyslogHandler == 'MySyslogHandler'
assert type_alias['logging'].Logger == logging.Logger
assert type_alias['logging'].config == logging.config
with pytest.raises(KeyError):
assert type_alias['log']
with pytest.raises(KeyError):
assert type_alias['unknown']
def test_signature():