mirror of
https://github.com/sphinx-doc/sphinx.git
synced 2025-02-25 18:55:22 -06:00
Merge branch '4.x'
This commit is contained in:
commit
3027a2f867
2
.github/workflows/main.yml
vendored
2
.github/workflows/main.yml
vendored
@ -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
65
CHANGES
@ -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)
|
||||
=====================================
|
||||
|
||||
|
2
EXAMPLES
2
EXAMPLES
@ -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/>`__
|
||||
|
@ -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
|
||||
|
@ -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
|
||||
|
@ -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).
|
||||
|
||||
|
5
setup.py
5
setup.py
@ -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',
|
||||
|
@ -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))
|
||||
|
||||
if attrs:
|
||||
return '<script %s>%s</script>' % (' '.join(attrs), body)
|
||||
else:
|
||||
return '<script>%s</script>' % body
|
||||
|
||||
context['js_tag'] = js_tag
|
||||
|
||||
|
@ -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':
|
||||
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]:
|
||||
|
@ -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,10 +1328,10 @@ class FunctionDocumenter(DocstringSignatureMixin, ModuleLevelDocumenter): # typ
|
||||
if typ is object:
|
||||
pass # default implementation. skipped.
|
||||
else:
|
||||
self.annotate_to_first_argument(func, typ)
|
||||
|
||||
dispatchfunc = self.annotate_to_first_argument(func, typ)
|
||||
if dispatchfunc:
|
||||
documenter = FunctionDocumenter(self.directive, '')
|
||||
documenter.object = func
|
||||
documenter.object = dispatchfunc
|
||||
documenter.objpath = [None]
|
||||
sigs.append(documenter.format_signature())
|
||||
if overloaded:
|
||||
@ -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,11 +2124,11 @@ class MethodDocumenter(DocstringSignatureMixin, ClassLevelDocumenter): # type:
|
||||
if typ is object:
|
||||
pass # default implementation. skipped.
|
||||
else:
|
||||
self.annotate_to_first_argument(func, typ)
|
||||
|
||||
dispatchmeth = self.annotate_to_first_argument(func, typ)
|
||||
if dispatchmeth:
|
||||
documenter = MethodDocumenter(self.directive, '')
|
||||
documenter.parent = self.parent
|
||||
documenter.object = func
|
||||
documenter.object = dispatchmeth
|
||||
documenter.objpath = [None]
|
||||
sigs.append(documenter.format_signature())
|
||||
if overloaded:
|
||||
@ -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
|
||||
|
||||
|
@ -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']
|
||||
|
@ -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 */
|
||||
|
@ -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
|
||||
|
||||
|
||||
|
@ -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,11 +623,18 @@ 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:
|
||||
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,
|
||||
|
@ -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
|
||||
|
@ -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
|
2
tests/roots/test-ext-autodoc/target/metadata.py
Normal file
2
tests/roots/test-ext-autodoc/target/metadata.py
Normal file
@ -0,0 +1,2 @@
|
||||
def foo():
|
||||
""":meta metadata-only-docstring:"""
|
@ -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
|
||||
|
@ -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
|
||||
|
@ -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
|
||||
|
@ -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"
|
||||
|
@ -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',
|
||||
|
@ -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.
|
||||
|
@ -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',
|
||||
|
@ -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'
|
||||
|
@ -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)
|
||||
|
@ -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"
|
||||
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"
|
||||
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"
|
||||
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"
|
||||
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'}
|
||||
|
||||
|
||||
|
@ -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():
|
||||
|
Loading…
Reference in New Issue
Block a user