summaryrefslogtreecommitdiff
path: root/sphinx
diff options
context:
space:
mode:
Diffstat (limited to 'sphinx')
-rw-r--r--sphinx/__init__.py6
-rw-r--r--sphinx/application.py70
-rw-r--r--sphinx/builders/html/__init__.py36
-rw-r--r--sphinx/builders/html/transforms.py5
-rw-r--r--sphinx/builders/texinfo.py3
-rw-r--r--sphinx/config.py3
-rw-r--r--sphinx/directives/__init__.py12
-rw-r--r--sphinx/domains/c.py2
-rw-r--r--sphinx/domains/cpp.py2
-rw-r--r--sphinx/domains/javascript.py2
-rw-r--r--sphinx/domains/math.py7
-rw-r--r--sphinx/domains/python.py2
-rw-r--r--sphinx/domains/rst.py2
-rw-r--r--sphinx/domains/std.py4
-rw-r--r--sphinx/ext/autodoc/__init__.py375
-rw-r--r--sphinx/ext/autodoc/importer.py41
-rw-r--r--sphinx/ext/autosummary/generate.py11
-rw-r--r--sphinx/ext/mathjax.py27
-rw-r--r--sphinx/registry.py8
-rw-r--r--sphinx/transforms/post_transforms/images.py6
-rw-r--r--sphinx/util/typing.py7
21 files changed, 405 insertions, 226 deletions
diff --git a/sphinx/__init__.py b/sphinx/__init__.py
index 06ba4fb92..23a867fa0 100644
--- a/sphinx/__init__.py
+++ b/sphinx/__init__.py
@@ -32,8 +32,8 @@ if 'PYTHONWARNINGS' not in os.environ:
warnings.filterwarnings('ignore', "'U' mode is deprecated",
DeprecationWarning, module='docutils.io')
-__version__ = '3.4.4+'
-__released__ = '3.4.4' # used when Sphinx builds its own docs
+__version__ = '3.5.0+'
+__released__ = '3.5.0' # used when Sphinx builds its own docs
#: Version info for better programmatic use.
#:
@@ -43,7 +43,7 @@ __released__ = '3.4.4' # used when Sphinx builds its own docs
#:
#: .. versionadded:: 1.2
#: Before version 1.2, check the string ``sphinx.__version__``.
-version_info = (3, 4, 4, 'beta', 0)
+version_info = (3, 5, 0, 'beta', 0)
package_dir = path.abspath(path.dirname(__file__))
diff --git a/sphinx/application.py b/sphinx/application.py
index 2253ce306..54a2603aa 100644
--- a/sphinx/application.py
+++ b/sphinx/application.py
@@ -916,22 +916,24 @@ class Sphinx:
"""
self.registry.add_post_transform(transform)
- def add_javascript(self, filename: str, **kwargs: str) -> None:
+ def add_javascript(self, filename: str, **kwargs: Any) -> None:
"""An alias of :meth:`add_js_file`."""
warnings.warn('The app.add_javascript() is deprecated. '
'Please use app.add_js_file() instead.',
RemovedInSphinx40Warning, stacklevel=2)
self.add_js_file(filename, **kwargs)
- def add_js_file(self, filename: str, **kwargs: str) -> None:
+ def add_js_file(self, filename: str, priority: int = 500, **kwargs: Any) -> None:
"""Register a JavaScript file to include in the HTML output.
Add *filename* to the list of JavaScript files that the default HTML
- template will include. The filename must be relative to the HTML
- static path , or a full URI with scheme. If the keyword argument
- ``body`` is given, its value will be added between the
- ``<script>`` tags. Extra keyword arguments are included as
- attributes of the ``<script>`` tag.
+ template will include in order of *priority* (ascending). The filename
+ must be relative to the HTML static path , or a full URI with scheme.
+ If the priority of JavaScript file is the same as others, the JavaScript
+ files will be included in order of the registration. If the keyword
+ argument ``body`` is given, its value will be added between the
+ ``<script>`` tags. Extra keyword arguments are included as attributes of
+ the ``<script>`` tag.
Example::
@@ -944,23 +946,43 @@ class Sphinx:
app.add_js_file(None, body="var myVariable = 'foo';")
# => <script>var myVariable = 'foo';</script>
+ .. list-table:: priority range for JavaScript files
+ :widths: 20,80
+
+ * - Priority
+ - Main purpose in Sphinx
+ * - 200
+ - default priority for built-in JavaScript files
+ * - 500
+ - default priority for extensions
+ * - 800
+ - default priority for :confval:`html_js_files`
+
+ A JavaScript file can be added to the specific HTML page when on extension
+ calls this method on :event:`html-page-context` event.
+
.. versionadded:: 0.5
.. versionchanged:: 1.8
Renamed from ``app.add_javascript()``.
And it allows keyword arguments as attributes of script tag.
+
+ .. versionchanged:: 3.5
+ Take priority argument. Allow to add a JavaScript file to the specific page.
"""
- self.registry.add_js_file(filename, **kwargs)
+ self.registry.add_js_file(filename, priority=priority, **kwargs)
if hasattr(self.builder, 'add_js_file'):
- self.builder.add_js_file(filename, **kwargs) # type: ignore
+ self.builder.add_js_file(filename, priority=priority, **kwargs) # type: ignore
- def add_css_file(self, filename: str, **kwargs: str) -> None:
+ def add_css_file(self, filename: str, priority: int = 500, **kwargs: Any) -> None:
"""Register a stylesheet to include in the HTML output.
Add *filename* to the list of CSS files that the default HTML template
- will include. The filename must be relative to the HTML static path,
- or a full URI with scheme. The keyword arguments are also accepted for
- attributes of ``<link>`` tag.
+ will include in order of *priority* (ascending). The filename must be
+ relative to the HTML static path, or a full URI with scheme. If the
+ priority of CSS file is the same as others, the CSS files will be
+ included in order of the registration. The keyword arguments are also
+ accepted for attributes of ``<link>`` tag.
Example::
@@ -975,6 +997,19 @@ class Sphinx:
# => <link rel="alternate stylesheet" href="_static/fancy.css"
# type="text/css" title="fancy" />
+ .. list-table:: priority range for CSS files
+ :widths: 20,80
+
+ * - Priority
+ - Main purpose in Sphinx
+ * - 500
+ - default priority for extensions
+ * - 800
+ - default priority for :confval:`html_css_files`
+
+ A CSS file can be added to the specific HTML page when on extension calls
+ this method on :event:`html-page-context` event.
+
.. versionadded:: 1.0
.. versionchanged:: 1.6
@@ -987,11 +1022,14 @@ class Sphinx:
.. versionchanged:: 1.8
Renamed from ``app.add_stylesheet()``.
And it allows keyword arguments as attributes of link tag.
+
+ .. versionchanged:: 3.5
+ Take priority argument. Allow to add a CSS file to the specific page.
"""
logger.debug('[app] adding stylesheet: %r', filename)
- self.registry.add_css_files(filename, **kwargs)
+ self.registry.add_css_files(filename, priority=priority, **kwargs)
if hasattr(self.builder, 'add_css_file'):
- self.builder.add_css_file(filename, **kwargs) # type: ignore
+ self.builder.add_css_file(filename, priority=priority, **kwargs) # type: ignore
def add_stylesheet(self, filename: str, alternate: bool = False, title: str = None
) -> None:
@@ -1000,7 +1038,7 @@ class Sphinx:
'Please use app.add_css_file() instead.',
RemovedInSphinx40Warning, stacklevel=2)
- attributes = {} # type: Dict[str, str]
+ attributes = {} # type: Dict[str, Any]
if alternate:
attributes['rel'] = 'alternate stylesheet'
else:
diff --git a/sphinx/builders/html/__init__.py b/sphinx/builders/html/__init__.py
index 2c96ede32..4bb7ee510 100644
--- a/sphinx/builders/html/__init__.py
+++ b/sphinx/builders/html/__init__.py
@@ -90,10 +90,13 @@ class Stylesheet(str):
attributes = None # type: Dict[str, str]
filename = None # type: str
+ priority = None # type: int
- def __new__(cls, filename: str, *args: str, **attributes: str) -> "Stylesheet":
+ def __new__(cls, filename: str, *args: str, priority: int = 500, **attributes: Any
+ ) -> "Stylesheet":
self = str.__new__(cls, filename) # type: ignore
self.filename = filename
+ self.priority = priority
self.attributes = attributes
self.attributes.setdefault('rel', 'stylesheet')
self.attributes.setdefault('type', 'text/css')
@@ -113,10 +116,12 @@ class JavaScript(str):
attributes = None # type: Dict[str, str]
filename = None # type: str
+ priority = None # type: int
- def __new__(cls, filename: str, **attributes: str) -> "JavaScript":
+ def __new__(cls, filename: str, priority: int = 500, **attributes: str) -> "JavaScript":
self = str.__new__(cls, filename) # type: ignore
self.filename = filename
+ self.priority = priority
self.attributes = attributes
return self
@@ -290,29 +295,31 @@ class StandaloneHTMLBuilder(Builder):
self.add_css_file(filename, **attrs)
for filename, attrs in self.get_builder_config('css_files', 'html'):
+ attrs.setdefault('priority', 800) # User's CSSs are loaded after extensions'
self.add_css_file(filename, **attrs)
- def add_css_file(self, filename: str, **kwargs: str) -> None:
+ def add_css_file(self, filename: str, **kwargs: Any) -> None:
if '://' not in filename:
filename = posixpath.join('_static', filename)
self.css_files.append(Stylesheet(filename, **kwargs)) # type: ignore
def init_js_files(self) -> None:
- self.add_js_file('jquery.js')
- self.add_js_file('underscore.js')
- self.add_js_file('doctools.js')
+ self.add_js_file('jquery.js', priority=200)
+ self.add_js_file('underscore.js', priority=200)
+ self.add_js_file('doctools.js', priority=200)
for filename, attrs in self.app.registry.js_files:
self.add_js_file(filename, **attrs)
for filename, attrs in self.get_builder_config('js_files', 'html'):
+ attrs.setdefault('priority', 800) # User's JSs are loaded after extensions'
self.add_js_file(filename, **attrs)
if self.config.language and self._get_translations_js():
self.add_js_file('translations.js')
- def add_js_file(self, filename: str, **kwargs: str) -> None:
+ def add_js_file(self, filename: str, **kwargs: Any) -> None:
if filename and '://' not in filename:
filename = posixpath.join('_static', filename)
@@ -448,9 +455,6 @@ class StandaloneHTMLBuilder(Builder):
logo = path.basename(self.config.html_logo) if self.config.html_logo else ''
favicon = path.basename(self.config.html_favicon) if self.config.html_favicon else ''
- if not isinstance(self.config.html_use_opensearch, str):
- logger.warning(__('html_use_opensearch config value must now be a string'))
-
self.relations = self.env.collect_relations()
rellinks = [] # type: List[Tuple[str, str, str, str]]
@@ -462,6 +466,10 @@ class StandaloneHTMLBuilder(Builder):
rellinks.append((indexname, indexcls.localname,
'', indexcls.shortname))
+ # back up script_files and css_files to allow adding JS/CSS files to a specific page.
+ self._script_files = list(self.script_files)
+ self._css_files = list(self.css_files)
+
if self.config.html_style is not None:
stylename = self.config.html_style
elif self.theme:
@@ -1012,12 +1020,20 @@ class StandaloneHTMLBuilder(Builder):
self.add_sidebars(pagename, ctx)
ctx.update(addctx)
+ # revert script_files and css_files
+ self.script_files[:] = self._script_files
+ self.css_files[:] = self.css_files
+
self.update_page_context(pagename, templatename, ctx, event_arg)
newtmpl = self.app.emit_firstresult('html-page-context', pagename,
templatename, ctx, event_arg)
if newtmpl:
templatename = newtmpl
+ # sort JS/CSS before rendering HTML
+ ctx['script_files'].sort(key=lambda js: js.priority)
+ ctx['css_files'].sort(key=lambda js: js.priority)
+
try:
output = self.templates.render(templatename, ctx)
except UnicodeError:
diff --git a/sphinx/builders/html/transforms.py b/sphinx/builders/html/transforms.py
index e0b3f6e08..29a989936 100644
--- a/sphinx/builders/html/transforms.py
+++ b/sphinx/builders/html/transforms.py
@@ -28,7 +28,7 @@ class KeyboardTransform(SphinxPostTransform):
After::
- <literal class="kbd">
+ <literal class="kbd compound">
<literal class="kbd">
Control
-
@@ -37,7 +37,7 @@ class KeyboardTransform(SphinxPostTransform):
"""
default_priority = 400
builders = ('html',)
- pattern = re.compile(r'(-|\+|\^|\s+)')
+ pattern = re.compile(r'(?<=.)(-|\+|\^|\s+)(?=.)')
def run(self, **kwargs: Any) -> None:
matcher = NodeMatcher(nodes.literal, classes=["kbd"])
@@ -46,6 +46,7 @@ class KeyboardTransform(SphinxPostTransform):
if len(parts) == 1:
continue
+ node['classes'].append('compound')
node.pop()
while parts:
key = parts.pop(0)
diff --git a/sphinx/builders/texinfo.py b/sphinx/builders/texinfo.py
index 0c2d3e014..1a56be0f9 100644
--- a/sphinx/builders/texinfo.py
+++ b/sphinx/builders/texinfo.py
@@ -179,7 +179,8 @@ class TexinfoBuilder(Builder):
try:
imagedir = path.join(self.outdir, targetname + '-figures')
ensuredir(imagedir)
- copy_asset_file(path.join(self.srcdir, dest), imagedir)
+ copy_asset_file(path.join(self.srcdir, src),
+ path.join(imagedir, dest))
except Exception as err:
logger.warning(__('cannot copy image file %r: %s'),
path.join(self.srcdir, src), err)
diff --git a/sphinx/config.py b/sphinx/config.py
index 8517fb4e4..645b09272 100644
--- a/sphinx/config.py
+++ b/sphinx/config.py
@@ -98,7 +98,8 @@ class Config:
# general options
'project': ('Python', 'env', []),
'author': ('unknown', 'env', []),
- 'copyright': ('', 'html', []),
+ 'project_copyright': ('', 'html', [str]),
+ 'copyright': (lambda c: c.project_copyright, 'html', [str]),
'version': ('', 'env', []),
'release': ('', 'env', []),
'today': ('', 'env', []),
diff --git a/sphinx/directives/__init__.py b/sphinx/directives/__init__.py
index 592921535..e386b3eaa 100644
--- a/sphinx/directives/__init__.py
+++ b/sphinx/directives/__init__.py
@@ -9,7 +9,7 @@
"""
import re
-from typing import Any, Dict, List, Tuple, cast
+from typing import Any, Dict, Generic, List, Tuple, TypeVar, cast
from docutils import nodes
from docutils.nodes import Node
@@ -33,6 +33,8 @@ if False:
nl_escape_re = re.compile(r'\\\n')
strip_backslash_re = re.compile(r'\\(.)')
+T = TypeVar('T')
+
def optional_int(argument: str) -> int:
"""
@@ -47,7 +49,7 @@ def optional_int(argument: str) -> int:
return value
-class ObjectDescription(SphinxDirective):
+class ObjectDescription(SphinxDirective, Generic[T]):
"""
Directive to describe a class, function or similar object. Not used
directly, but subclassed (in domain-specific directives) to add custom
@@ -97,7 +99,7 @@ class ObjectDescription(SphinxDirective):
else:
return [line.strip() for line in lines]
- def handle_signature(self, sig: str, signode: desc_signature) -> Any:
+ def handle_signature(self, sig: str, signode: desc_signature) -> T:
"""
Parse the signature *sig* into individual nodes and append them to
*signode*. If ValueError is raised, parsing is aborted and the whole
@@ -109,7 +111,7 @@ class ObjectDescription(SphinxDirective):
"""
raise ValueError
- def add_target_and_index(self, name: Any, sig: str, signode: desc_signature) -> None:
+ def add_target_and_index(self, name: T, sig: str, signode: desc_signature) -> None:
"""
Add cross-reference IDs and entries to self.indexnode, if applicable.
@@ -173,7 +175,7 @@ class ObjectDescription(SphinxDirective):
if self.domain:
node['classes'].append(self.domain)
- self.names = [] # type: List[Any]
+ self.names = [] # type: List[T]
signatures = self.get_signatures()
for i, sig in enumerate(signatures):
# add a signature node for each signature in the current unit
diff --git a/sphinx/domains/c.py b/sphinx/domains/c.py
index 87f115c4a..fb4da502d 100644
--- a/sphinx/domains/c.py
+++ b/sphinx/domains/c.py
@@ -3099,7 +3099,7 @@ def _make_phony_error_name() -> ASTNestedName:
return ASTNestedName([ASTIdentifier("PhonyNameDueToError")], rooted=False)
-class CObject(ObjectDescription):
+class CObject(ObjectDescription[ASTDeclaration]):
"""
Description of a C language object.
"""
diff --git a/sphinx/domains/cpp.py b/sphinx/domains/cpp.py
index 389630a32..f6e746809 100644
--- a/sphinx/domains/cpp.py
+++ b/sphinx/domains/cpp.py
@@ -6670,7 +6670,7 @@ def _make_phony_error_name() -> ASTNestedName:
return ASTNestedName([nne], [False], rooted=False)
-class CPPObject(ObjectDescription):
+class CPPObject(ObjectDescription[ASTDeclaration]):
"""Description of a C++ language object."""
doc_field_types = [
diff --git a/sphinx/domains/javascript.py b/sphinx/domains/javascript.py
index a4b2eca2e..f612fb914 100644
--- a/sphinx/domains/javascript.py
+++ b/sphinx/domains/javascript.py
@@ -32,7 +32,7 @@ from sphinx.util.nodes import make_id, make_refnode
logger = logging.getLogger(__name__)
-class JSObject(ObjectDescription):
+class JSObject(ObjectDescription[Tuple[str, str]]):
"""
Description of a JavaScript object.
"""
diff --git a/sphinx/domains/math.py b/sphinx/domains/math.py
index 6bd93e7b5..248a7a2a6 100644
--- a/sphinx/domains/math.py
+++ b/sphinx/domains/math.py
@@ -157,8 +157,11 @@ class MathDomain(Domain):
targets = [eq for eq in self.equations.values() if eq[0] == docname]
return len(targets) + 1
- def has_equations(self) -> bool:
- return any(self.data['has_equations'].values())
+ def has_equations(self, docname: str = None) -> bool:
+ if docname:
+ return self.data['has_equations'].get(docname, False)
+ else:
+ return any(self.data['has_equations'].values())
def setup(app: "Sphinx") -> Dict[str, Any]:
diff --git a/sphinx/domains/python.py b/sphinx/domains/python.py
index dff4f9580..c2af9886f 100644
--- a/sphinx/domains/python.py
+++ b/sphinx/domains/python.py
@@ -334,7 +334,7 @@ class PyTypedField(PyXrefMixin, TypedField):
return super().make_xref(rolename, domain, target, innernode, contnode, env)
-class PyObject(ObjectDescription):
+class PyObject(ObjectDescription[Tuple[str, str]]):
"""
Description of a general Python object.
diff --git a/sphinx/domains/rst.py b/sphinx/domains/rst.py
index 0539197bc..07bf46b75 100644
--- a/sphinx/domains/rst.py
+++ b/sphinx/domains/rst.py
@@ -31,7 +31,7 @@ logger = logging.getLogger(__name__)
dir_sig_re = re.compile(r'\.\. (.+?)::(.*)$')
-class ReSTMarkup(ObjectDescription):
+class ReSTMarkup(ObjectDescription[str]):
"""
Description of generic reST markup.
"""
diff --git a/sphinx/domains/std.py b/sphinx/domains/std.py
index bdce2406e..33acdb3f5 100644
--- a/sphinx/domains/std.py
+++ b/sphinx/domains/std.py
@@ -48,7 +48,7 @@ option_desc_re = re.compile(r'((?:/|--|-|\+)?[^\s=]+)(=?\s*.*)')
token_re = re.compile(r'`(\w+)`', re.U)
-class GenericObject(ObjectDescription):
+class GenericObject(ObjectDescription[str]):
"""
A generic x-ref directive registered with Sphinx.add_object_type().
"""
@@ -178,7 +178,7 @@ class Target(SphinxDirective):
return self.name + '-' + name
-class Cmdoption(ObjectDescription):
+class Cmdoption(ObjectDescription[str]):
"""
Description of a command-line option (.. option).
"""
diff --git a/sphinx/ext/autodoc/__init__.py b/sphinx/ext/autodoc/__init__.py
index 54e91a1cd..bf80ef4a8 100644
--- a/sphinx/ext/autodoc/__init__.py
+++ b/sphinx/ext/autodoc/__init__.py
@@ -10,7 +10,6 @@
:license: BSD, see LICENSE for details.
"""
-import importlib
import re
import warnings
from inspect import Parameter, Signature
@@ -26,8 +25,8 @@ from sphinx.config import ENUM, Config
from sphinx.deprecation import (RemovedInSphinx40Warning, RemovedInSphinx50Warning,
RemovedInSphinx60Warning)
from sphinx.environment import BuildEnvironment
-from sphinx.ext.autodoc.importer import (get_class_members, get_module_members,
- get_object_members, import_object)
+from sphinx.ext.autodoc.importer import (get_class_members, get_object_members, import_module,
+ import_object)
from sphinx.ext.autodoc.mock import ismock, mock
from sphinx.locale import _, __
from sphinx.pycode import ModuleAnalyzer, PycodeError
@@ -92,7 +91,7 @@ SLOTSATTR = object()
def members_option(arg: Any) -> Union[object, List[str]]:
"""Used to convert the :members: option to auto directives."""
- if arg is None or arg is True:
+ if arg in (None, True):
return ALL
elif arg is False:
return None
@@ -111,14 +110,14 @@ def members_set_option(arg: Any) -> Union[object, Set[str]]:
def exclude_members_option(arg: Any) -> Union[object, Set[str]]:
"""Used to convert the :exclude-members: option."""
- if arg is None:
+ if arg in (None, True):
return EMPTY
return {x.strip() for x in arg.split(',') if x.strip()}
def inherited_members_option(arg: Any) -> Union[object, Set[str]]:
"""Used to convert the :members: option to auto directives."""
- if arg is None:
+ if arg in (None, True):
return 'object'
else:
return arg
@@ -126,7 +125,7 @@ def inherited_members_option(arg: Any) -> Union[object, Set[str]]:
def member_order_option(arg: Any) -> Optional[str]:
"""Used to convert the :members: option to auto directives."""
- if arg is None:
+ if arg in (None, True):
return None
elif arg in ('alphabetical', 'bysource', 'groupwise'):
return arg
@@ -138,7 +137,7 @@ SUPPRESS = object()
def annotation_option(arg: Any) -> Any:
- if arg is None:
+ if arg in (None, True):
# suppress showing the representation of the object
return SUPPRESS
else:
@@ -276,11 +275,12 @@ class ObjectMember(tuple):
return super().__new__(cls, (name, obj)) # type: ignore
def __init__(self, name: str, obj: Any, docstring: Optional[str] = None,
- skipped: bool = False) -> None:
+ class_: Any = None, skipped: bool = False) -> None:
self.__name__ = name
self.object = obj
self.docstring = docstring
self.skipped = skipped
+ self.class_ = class_
ObjectMembers = Union[List[ObjectMember], List[Tuple[str, Any]]]
@@ -538,8 +538,12 @@ class Documenter:
# etc. don't support a prepended module name
self.add_line(' :module: %s' % self.modname, sourcename)
- def get_doc(self, encoding: str = None, ignore: int = None) -> List[List[str]]:
- """Decode and return lines of the docstring(s) for the object."""
+ def get_doc(self, encoding: str = None, ignore: int = None) -> Optional[List[List[str]]]:
+ """Decode and return lines of the docstring(s) for the object.
+
+ When it returns None value, autodoc-process-docstring will not be called for this
+ object.
+ """
if encoding is not None:
warnings.warn("The 'encoding' argument to autodoc.%s.get_doc() is deprecated."
% self.__class__.__name__,
@@ -587,12 +591,10 @@ class Documenter:
def add_content(self, more_content: Optional[StringList], no_docstring: bool = False
) -> None:
"""Add content from docstrings, attribute documentation and user."""
- # Suspended temporarily (see https://github.com/sphinx-doc/sphinx/pull/8533)
- #
- # if no_docstring:
- # warnings.warn("The 'no_docstring' argument to %s.add_content() is deprecated."
- # % self.__class__.__name__,
- # RemovedInSphinx50Warning, stacklevel=2)
+ if no_docstring:
+ warnings.warn("The 'no_docstring' argument to %s.add_content() is deprecated."
+ % self.__class__.__name__,
+ RemovedInSphinx50Warning, stacklevel=2)
# set sourcename and add content from attribute documentation
sourcename = self.get_sourcename()
@@ -612,13 +614,17 @@ class Documenter:
# add content from docstrings
if not no_docstring:
docstrings = self.get_doc()
- if not docstrings:
- # append at least a dummy docstring, so that the event
- # autodoc-process-docstring is fired and can add some
- # content if desired
- docstrings.append([])
- for i, line in enumerate(self.process_doc(docstrings)):
- self.add_line(line, sourcename, i)
+ if docstrings is None:
+ # Do not call autodoc-process-docstring on get_doc() returns None.
+ pass
+ else:
+ if not docstrings:
+ # append at least a dummy docstring, so that the event
+ # autodoc-process-docstring is fired and can add some
+ # content if desired
+ docstrings.append([])
+ for i, line in enumerate(self.process_doc(docstrings)):
+ self.add_line(line, sourcename, i)
# add additional content (e.g. from document), if present
if more_content:
@@ -668,7 +674,7 @@ class Documenter:
The user can override the skipping decision by connecting to the
``autodoc-skip-member`` event.
"""
- def is_filtered_inherited_member(name: str) -> bool:
+ def is_filtered_inherited_member(name: str, obj: Any) -> bool:
if inspect.isclass(self.object):
for cls in self.object.__mro__:
if cls.__name__ == self.options.inherited_members and cls != self.object:
@@ -678,6 +684,8 @@ class Documenter:
return False
elif name in self.get_attr(cls, '__annotations__', {}):
return False
+ elif isinstance(obj, ObjectMember) and obj.class_ is cls:
+ return False
return False
@@ -742,7 +750,7 @@ class Documenter:
if self.options.special_members and membername in self.options.special_members:
if membername == '__doc__':
keep = False
- elif is_filtered_inherited_member(membername):
+ elif is_filtered_inherited_member(membername, obj):
keep = False
else:
keep = has_doc or self.options.undoc_members
@@ -762,14 +770,15 @@ class Documenter:
if has_doc or self.options.undoc_members:
if self.options.private_members is None:
keep = False
- elif is_filtered_inherited_member(membername):
+ elif is_filtered_inherited_member(membername, obj):
keep = False
else:
keep = membername in self.options.private_members
else:
keep = False
else:
- if self.options.members is ALL and is_filtered_inherited_member(membername):
+ if (self.options.members is ALL and
+ is_filtered_inherited_member(membername, obj)):
keep = False
else:
# ignore undocumented members if :undoc-members: is not given
@@ -1034,30 +1043,54 @@ class ModuleDocumenter(Documenter):
if self.options.deprecated:
self.add_line(' :deprecated:', sourcename)
+ def get_module_members(self) -> Dict[str, ObjectMember]:
+ """Get members of target module."""
+ if self.analyzer:
+ attr_docs = self.analyzer.attr_docs
+ else:
+ attr_docs = {}
+
+ members = {} # type: Dict[str, ObjectMember]
+ for name in dir(self.object):
+ try:
+ value = safe_getattr(self.object, name, None)
+ docstring = attr_docs.get(('', name), [])
+ members[name] = ObjectMember(name, value, docstring="\n".join(docstring))
+ except AttributeError:
+ continue
+
+ # annotation only member (ex. attr: int)
+ try:
+ for name in inspect.getannotations(self.object):
+ if name not in members:
+ docstring = attr_docs.get(('', name), [])
+ members[name] = ObjectMember(name, INSTANCEATTR,
+ docstring="\n".join(docstring))
+ except AttributeError:
+ pass
+
+ return members
+
def get_object_members(self, want_all: bool) -> Tuple[bool, ObjectMembers]:
+ members = self.get_module_members()
if want_all:
- members = get_module_members(self.object)
- if not self.__all__:
+ if self.__all__ is None:
# for implicit module members, check __module__ to avoid
# documenting imported objects
- return True, members
+ return True, list(members.values())
else:
- ret = []
- for name, value in members:
- if name in self.__all__:
- ret.append(ObjectMember(name, value))
- else:
- ret.append(ObjectMember(name, value, skipped=True))
+ for member in members.values():
+ if member.__name__ not in self.__all__:
+ member.skipped = True
- return False, ret
+ return False, list(members.values())
else:
memberlist = self.options.members or []
ret = []
for name in memberlist:
- try:
- value = safe_getattr(self.object, name)
- ret.append(ObjectMember(name, value))
- except AttributeError:
+ if name in members:
+ ret.append(members[name])
+ else:
logger.warning(__('missing attribute mentioned in :members: option: '
'module %s, attribute %s') %
(safe_getattr(self.object, '__name__', '???'), name),
@@ -1160,6 +1193,8 @@ class DocstringSignatureMixin:
valid_names.extend(cls.__name__ for cls in self.object.__mro__)
docstrings = self.get_doc()
+ if docstrings is None:
+ return None, None
self._new_docstrings = docstrings[:]
self._signatures = []
result = None
@@ -1210,7 +1245,7 @@ class DocstringSignatureMixin:
return result
- def get_doc(self, encoding: str = None, ignore: int = None) -> List[List[str]]:
+ def get_doc(self, encoding: str = None, ignore: int = None) -> Optional[List[List[str]]]:
if encoding is not None:
warnings.warn("The 'encoding' argument to autodoc.%s.get_doc() is deprecated."
% self.__class__.__name__,
@@ -1594,27 +1629,24 @@ class ClassDocumenter(DocstringSignatureMixin, ModuleLevelDocumenter): # type:
selected = []
for name in self.options.members: # type: str
if name in members:
- selected.append(ObjectMember(name, members[name].value,
- docstring=members[name].docstring))
+ selected.append(members[name])
else:
logger.warning(__('missing attribute %s in object %s') %
(name, self.fullname), type='autodoc')
return False, selected
elif self.options.inherited_members:
- return False, [ObjectMember(m.name, m.value, docstring=m.docstring)
- for m in members.values()]
+ return False, list(members.values())
else:
- return False, [ObjectMember(m.name, m.value, docstring=m.docstring)
- for m in members.values() if m.class_ == self.object]
+ return False, [m for m in members.values() if m.class_ == self.object]
- def get_doc(self, encoding: str = None, ignore: int = None) -> List[List[str]]:
+ def get_doc(self, encoding: str = None, ignore: int = None) -> Optional[List[List[str]]]:
if encoding is not None:
warnings.warn("The 'encoding' argument to autodoc.%s.get_doc() is deprecated."
% self.__class__.__name__,
RemovedInSphinx40Warning, stacklevel=2)
if self.doc_as_attr:
# Don't show the docstring of the class when it is an alias.
- return []
+ return None
lines = getattr(self, '_new_docstrings', None)
if lines is not None:
@@ -1667,9 +1699,7 @@ class ClassDocumenter(DocstringSignatureMixin, ModuleLevelDocumenter): # type:
except AttributeError:
pass # Invalid class object is passed.
- super().add_content(more_content, no_docstring=True)
- else:
- super().add_content(more_content)
+ super().add_content(more_content)
def document_members(self, all_members: bool = False) -> None:
if self.doc_as_attr:
@@ -1774,7 +1804,7 @@ class TypeVarMixin(DataDocumenterMixinBase):
return (isinstance(self.object, TypeVar) or
super().should_suppress_directive_header())
- def get_doc(self, encoding: str = None, ignore: int = None) -> List[List[str]]:
+ def get_doc(self, encoding: str = None, ignore: int = None) -> Optional[List[List[str]]]:
if ignore is not None:
warnings.warn("The 'ignore' argument to autodoc.%s.get_doc() is deprecated."
% self.__class__.__name__,
@@ -1816,12 +1846,14 @@ class UninitializedGlobalVariableMixin(DataDocumenterMixinBase):
except ImportError as exc:
# annotation only instance variable (PEP-526)
try:
- self.parent = importlib.import_module(self.modname)
- annotations = get_type_hints(self.parent, None,
- self.config.autodoc_type_aliases)
- if self.objpath[-1] in annotations:
- self.object = UNINITIALIZED_ATTR
- return True
+ with mock(self.config.autodoc_mock_imports):
+ parent = import_module(self.modname, self.config.autodoc_warningiserror)
+ annotations = get_type_hints(parent, None,
+ self.config.autodoc_type_aliases)
+ if self.objpath[-1] in annotations:
+ self.object = UNINITIALIZED_ATTR
+ self.parent = parent
+ return True
except ImportError:
pass
@@ -1836,7 +1868,7 @@ class UninitializedGlobalVariableMixin(DataDocumenterMixinBase):
return (self.object is UNINITIALIZED_ATTR or
super().should_suppress_value_header())
- def get_doc(self, encoding: str = None, ignore: int = None) -> List[List[str]]:
+ def get_doc(self, encoding: str = None, ignore: int = None) -> Optional[List[List[str]]]:
if self.object is UNINITIALIZED_ATTR:
return []
else:
@@ -1881,6 +1913,17 @@ class DataDocumenter(GenericAliasMixin, NewTypeMixin, TypeVarMixin,
return ret
+ def should_suppress_value_header(self) -> bool:
+ if super().should_suppress_value_header():
+ return True
+ else:
+ doc = self.get_doc()
+ metadata = extract_metadata('\n'.join(sum(doc, [])))
+ if 'hide-value' in metadata:
+ return True
+
+ return False
+
def add_directive_header(self, sig: str) -> None:
super().add_directive_header(sig)
sourcename = self.get_sourcename()
@@ -1912,8 +1955,32 @@ class DataDocumenter(GenericAliasMixin, NewTypeMixin, TypeVarMixin,
return self.get_attr(self.parent or self.object, '__module__', None) \
or self.modname
+ def get_module_comment(self, attrname: str) -> Optional[List[str]]:
+ try:
+ analyzer = ModuleAnalyzer.for_module(self.modname)
+ analyzer.analyze()
+ key = ('', attrname)
+ if key in analyzer.attr_docs:
+ return list(analyzer.attr_docs[key])
+ except PycodeError:
+ pass
+
+ return None
+
+ def get_doc(self, encoding: str = None, ignore: int = None) -> List[List[str]]:
+ # Check the variable has a docstring-comment
+ comment = self.get_module_comment(self.objpath[-1])
+ if comment:
+ return [comment]
+ else:
+ return super().get_doc(encoding, ignore)
+
def add_content(self, more_content: Optional[StringList], no_docstring: bool = False
) -> None:
+ # Disable analyzing variable comment on Documenter.add_content() to control it on
+ # DataDocumenter.add_content()
+ self.analyzer = None
+
if not more_content:
more_content = StringList()
@@ -2109,23 +2176,14 @@ class NonDataDescriptorMixin(DataDocumenterMixinBase):
return (not getattr(self, 'non_data_descriptor', False) or
super().should_suppress_directive_header())
- def get_doc(self, encoding: str = None, ignore: int = None) -> List[List[str]]:
+ def get_doc(self, encoding: str = None, ignore: int = None) -> Optional[List[List[str]]]:
if getattr(self, 'non_data_descriptor', False):
# the docstring of non datadescriptor is very probably the wrong thing
# to display
- return []
+ return None
else:
return super().get_doc(encoding, ignore) # type: ignore
- def add_content(self, more_content: Optional[StringList], no_docstring: bool = False
- ) -> None:
- if getattr(self, 'non_data_descriptor', False):
- # the docstring of non datadescriptor is very probably the wrong thing
- # to display
- no_docstring = True
-
- super().add_content(more_content, no_docstring=no_docstring) # type: ignore
-
class SlotsMixin(DataDocumenterMixinBase):
"""
@@ -2157,7 +2215,7 @@ class SlotsMixin(DataDocumenterMixinBase):
else:
return super().should_suppress_directive_header()
- def get_doc(self, encoding: str = None, ignore: int = None) -> List[List[str]]:
+ def get_doc(self, encoding: str = None, ignore: int = None) -> Optional[List[List[str]]]:
if self.object is SLOTSATTR:
try:
__slots__ = inspect.getslots(self.parent)
@@ -2174,9 +2232,9 @@ class SlotsMixin(DataDocumenterMixinBase):
return super().get_doc(encoding, ignore) # type: ignore
-class UninitializedInstanceAttributeMixin(DataDocumenterMixinBase):
+class RuntimeInstanceAttributeMixin(DataDocumenterMixinBase):
"""
- Mixin for AttributeDocumenter to provide the feature for supporting uninitialized
+ Mixin for AttributeDocumenter to provide the feature for supporting runtime
instance attributes (that are defined in __init__() methods with doc-comments).
Example:
@@ -2186,38 +2244,69 @@ class UninitializedInstanceAttributeMixin(DataDocumenterMixinBase):
self.attr = None #: This is a target of this mix-in.
"""
- def get_attribute_comment(self, parent: Any) -> Optional[List[str]]:
+ RUNTIME_INSTANCE_ATTRIBUTE = object()
+
+ def is_runtime_instance_attribute(self, parent: Any) -> bool:
+ """Check the subject is an attribute defined in __init__()."""
+ # An instance variable defined in __init__().
+ if self.get_attribute_comment(parent, self.objpath[-1]): # type: ignore
+ return True
+ else:
+ return False
+
+ def import_object(self, raiseerror: bool = False) -> bool:
+ """Check the existence of runtime instance attribute when failed to import the
+ attribute."""
try:
- for cls in inspect.getmro(parent):
- try:
- module = safe_getattr(cls, '__module__')
- qualname = safe_getattr(cls, '__qualname__')
+ return super().import_object(raiseerror=True) # type: ignore
+ except ImportError as exc:
+ try:
+ with mock(self.config.autodoc_mock_imports):
+ ret = import_object(self.modname, self.objpath[:-1], 'class',
+ attrgetter=self.get_attr, # type: ignore
+ warningiserror=self.config.autodoc_warningiserror)
+ parent = ret[3]
+ if self.is_runtime_instance_attribute(parent):
+ self.object = self.RUNTIME_INSTANCE_ATTRIBUTE
+ self.parent = parent
+ return True
+ except ImportError:
+ pass
- analyzer = ModuleAnalyzer.for_module(module)
- analyzer.analyze()
- if qualname and self.objpath:
- key = (qualname, self.objpath[-1])
- if key in analyzer.attr_docs:
- return list(analyzer.attr_docs[key])
- except (AttributeError, PycodeError):
- pass
- except (AttributeError, PycodeError):
- pass
+ if raiseerror:
+ raise
+ else:
+ logger.warning(exc.args[0], type='autodoc', subtype='import_object')
+ self.env.note_reread()
+ return False
+
+ def should_suppress_value_header(self) -> bool:
+ return (self.object is self.RUNTIME_INSTANCE_ATTRIBUTE or
+ super().should_suppress_value_header())
- return None
+
+class UninitializedInstanceAttributeMixin(DataDocumenterMixinBase):
+ """
+ Mixin for AttributeDocumenter to provide the feature for supporting uninitialized
+ instance attributes (PEP-526 styled, annotation only attributes).
+
+ Example:
+
+ class Foo:
+ attr: int #: This is a target of this mix-in.
+ """
def is_uninitialized_instance_attribute(self, parent: Any) -> bool:
- """Check the subject is an attribute defined in __init__()."""
- # An instance variable defined in __init__().
- if self.get_attribute_comment(parent):
+ """Check the subject is an annotation only attribute."""
+ annotations = get_type_hints(parent, None, self.config.autodoc_type_aliases)
+ if self.objpath[-1] in annotations:
return True
else:
return False
def import_object(self, raiseerror: bool = False) -> bool:
- """Check the exisitence of uninitizlied instance attribute when failed to import
- the attribute.
- """
+ """Check the exisitence of uninitialized instance attribute when failed to import
+ the attribute."""
try:
return super().import_object(raiseerror=True) # type: ignore
except ImportError as exc:
@@ -2244,26 +2333,17 @@ class UninitializedInstanceAttributeMixin(DataDocumenterMixinBase):
return (self.object is UNINITIALIZED_ATTR or
super().should_suppress_value_header())
- def get_doc(self, encoding: str = None, ignore: int = None) -> List[List[str]]:
- if self.object is UNINITIALIZED_ATTR:
- comment = self.get_attribute_comment(self.parent)
- if comment:
- return [comment]
-
- return super().get_doc(encoding, ignore) # type: ignore
-
- def add_content(self, more_content: Optional[StringList], no_docstring: bool = False
- ) -> None:
+ def get_doc(self, encoding: str = None, ignore: int = None) -> Optional[List[List[str]]]:
if self.object is UNINITIALIZED_ATTR:
- self.analyzer = None
-
- super().add_content(more_content, no_docstring=no_docstring) # type: ignore
+ return None
+ else:
+ return super().get_doc(encoding, ignore) # type: ignore
class AttributeDocumenter(GenericAliasMixin, NewTypeMixin, SlotsMixin, # type: ignore
- TypeVarMixin, UninitializedInstanceAttributeMixin,
- NonDataDescriptorMixin, DocstringStripSignatureMixin,
- ClassLevelDocumenter):
+ TypeVarMixin, RuntimeInstanceAttributeMixin,
+ UninitializedInstanceAttributeMixin, NonDataDescriptorMixin,
+ DocstringStripSignatureMixin, ClassLevelDocumenter):
"""
Specialized Documenter subclass for attributes.
"""
@@ -2298,6 +2378,8 @@ class AttributeDocumenter(GenericAliasMixin, NewTypeMixin, SlotsMixin, # type:
def isinstanceattribute(self) -> bool:
"""Check the subject is an instance attribute."""
+ warnings.warn('AttributeDocumenter.isinstanceattribute() is deprecated.',
+ RemovedInSphinx50Warning)
# uninitialized instance variable (PEP-526)
with mock(self.config.autodoc_mock_imports):
try:
@@ -2340,21 +2422,9 @@ class AttributeDocumenter(GenericAliasMixin, NewTypeMixin, SlotsMixin, # type:
pass
def import_object(self, raiseerror: bool = False) -> bool:
- try:
- ret = super().import_object(raiseerror=True)
- if inspect.isenumattribute(self.object):
- self.object = self.object.value
- except ImportError as exc:
- if self.isinstanceattribute():
- self.object = INSTANCEATTR
- ret = True
- elif raiseerror:
- raise
- else:
- logger.warning(exc.args[0], type='autodoc', subtype='import_object')
- self.env.note_reread()
- ret = False
-
+ ret = super().import_object(raiseerror)
+ if inspect.isenumattribute(self.object):
+ self.object = self.object.value
if self.parent:
self.update_annotations(self.parent)
@@ -2364,6 +2434,18 @@ class AttributeDocumenter(GenericAliasMixin, NewTypeMixin, SlotsMixin, # type:
return self.get_attr(self.parent or self.object, '__module__', None) \
or self.modname
+ def should_suppress_value_header(self) -> bool:
+ if super().should_suppress_value_header():
+ return True
+ else:
+ doc = self.get_doc()
+ if doc:
+ metadata = extract_metadata('\n'.join(sum(doc, [])))
+ if 'hide-value' in metadata:
+ return True
+
+ return False
+
def add_directive_header(self, sig: str) -> None:
super().add_directive_header(sig)
sourcename = self.get_sourcename()
@@ -2379,8 +2461,7 @@ class AttributeDocumenter(GenericAliasMixin, NewTypeMixin, SlotsMixin, # type:
self.add_line(' :type: ' + objrepr, sourcename)
try:
- if (self.object is INSTANCEATTR or self.options.no_value or
- self.should_suppress_value_header()):
+ if self.options.no_value or self.should_suppress_value_header():
pass
else:
objrepr = object_description(self.object)
@@ -2388,9 +2469,31 @@ class AttributeDocumenter(GenericAliasMixin, NewTypeMixin, SlotsMixin, # type:
except ValueError:
pass
- def get_doc(self, encoding: str = None, ignore: int = None) -> List[List[str]]:
- if self.object is INSTANCEATTR:
- return []
+ def get_attribute_comment(self, parent: Any, attrname: str) -> Optional[List[str]]:
+ try:
+ for cls in inspect.getmro(parent):
+ try:
+ module = safe_getattr(cls, '__module__')
+ qualname = safe_getattr(cls, '__qualname__')
+
+ analyzer = ModuleAnalyzer.for_module(module)
+ analyzer.analyze()
+ if qualname and self.objpath:
+ key = (qualname, attrname)
+ if key in analyzer.attr_docs:
+ return list(analyzer.attr_docs[key])
+ except (AttributeError, PycodeError):
+ pass
+ except (AttributeError, PycodeError):
+ pass
+
+ return None
+
+ def get_doc(self, encoding: str = None, ignore: int = None) -> Optional[List[List[str]]]:
+ # Check the attribute has a docstring-comment
+ comment = self.get_attribute_comment(self.parent, self.objpath[-1])
+ if comment:
+ return [comment]
try:
# Disable `autodoc_inherit_docstring` temporarily to avoid to obtain
@@ -2404,6 +2507,10 @@ class AttributeDocumenter(GenericAliasMixin, NewTypeMixin, SlotsMixin, # type:
def add_content(self, more_content: Optional[StringList], no_docstring: bool = False
) -> None:
+ # Disable analyzing attribute comment on Documenter.add_content() to control it on
+ # AttributeDocumenter.add_content()
+ self.analyzer = None
+
if more_content is None:
more_content = StringList()
self.update_content(more_content)
diff --git a/sphinx/ext/autodoc/importer.py b/sphinx/ext/autodoc/importer.py
index 89aab8f0e..ffcb27ecc 100644
--- a/sphinx/ext/autodoc/importer.py
+++ b/sphinx/ext/autodoc/importer.py
@@ -13,7 +13,8 @@ import traceback
import warnings
from typing import Any, Callable, Dict, List, Mapping, NamedTuple, Optional, Tuple
-from sphinx.deprecation import RemovedInSphinx40Warning, deprecated_alias
+from sphinx.deprecation import (RemovedInSphinx40Warning, RemovedInSphinx50Warning,
+ deprecated_alias)
from sphinx.pycode import ModuleAnalyzer, PycodeError
from sphinx.util import logging
from sphinx.util.inspect import (getannotations, getmro, getslots, isclass, isenumclass,
@@ -23,6 +24,8 @@ if False:
# For type annotation
from typing import Type # NOQA
+ from sphinx.ext.autodoc import ObjectMember
+
logger = logging.getLogger(__name__)
@@ -141,6 +144,9 @@ def get_module_members(module: Any) -> List[Tuple[str, Any]]:
"""Get members of target module."""
from sphinx.ext.autodoc import INSTANCEATTR
+ warnings.warn('sphinx.ext.autodoc.importer.get_module_members() is deprecated.',
+ RemovedInSphinx50Warning)
+
members = {} # type: Dict[str, Tuple[str, Any]]
for name in dir(module):
try:
@@ -241,37 +247,27 @@ def get_object_members(subject: Any, objpath: List[str], attrgetter: Callable,
return members
-class ClassAttribute:
- """The attribute of the class."""
-
- def __init__(self, cls: Any, name: str, value: Any, docstring: Optional[str] = None):
- self.class_ = cls
- self.name = name
- self.value = value
- self.docstring = docstring
-
-
def get_class_members(subject: Any, objpath: List[str], attrgetter: Callable
- ) -> Dict[str, ClassAttribute]:
+ ) -> Dict[str, "ObjectMember"]:
"""Get members and attributes of target class."""
- from sphinx.ext.autodoc import INSTANCEATTR
+ from sphinx.ext.autodoc import INSTANCEATTR, ObjectMember
# the members directly defined in the class
obj_dict = attrgetter(subject, '__dict__', {})
- members = {} # type: Dict[str, ClassAttribute]
+ members = {} # type: Dict[str, ObjectMember]
# enum members
if isenumclass(subject):
for name, value in subject.__members__.items():
if name not in members:
- members[name] = ClassAttribute(subject, name, value)
+ members[name] = ObjectMember(name, value, class_=subject)
superclass = subject.__mro__[1]
for name in obj_dict:
if name not in superclass.__dict__:
value = safe_getattr(subject, name)
- members[name] = ClassAttribute(subject, name, value)
+ members[name] = ObjectMember(name, value, class_=subject)
# members in __slots__
try:
@@ -280,7 +276,8 @@ def get_class_members(subject: Any, objpath: List[str], attrgetter: Callable
from sphinx.ext.autodoc import SLOTSATTR
for name, docstring in __slots__.items():
- members[name] = ClassAttribute(subject, name, SLOTSATTR, docstring)
+ members[name] = ObjectMember(name, SLOTSATTR, class_=subject,
+ docstring=docstring)
except (AttributeError, TypeError, ValueError):
pass
@@ -291,9 +288,9 @@ def get_class_members(subject: Any, objpath: List[str], attrgetter: Callable
unmangled = unmangle(subject, name)
if unmangled and unmangled not in members:
if name in obj_dict:
- members[unmangled] = ClassAttribute(subject, unmangled, value)
+ members[unmangled] = ObjectMember(unmangled, value, class_=subject)
else:
- members[unmangled] = ClassAttribute(None, unmangled, value)
+ members[unmangled] = ObjectMember(unmangled, value)
except AttributeError:
continue
@@ -304,7 +301,7 @@ def get_class_members(subject: Any, objpath: List[str], attrgetter: Callable
for name in getannotations(cls):
name = unmangle(cls, name)
if name and name not in members:
- members[name] = ClassAttribute(cls, name, INSTANCEATTR)
+ members[name] = ObjectMember(name, INSTANCEATTR, class_=cls)
except AttributeError:
pass
@@ -316,8 +313,8 @@ def get_class_members(subject: Any, objpath: List[str], attrgetter: Callable
analyzer.analyze()
for (ns, name), docstring in analyzer.attr_docs.items():
if ns == qualname and name not in members:
- members[name] = ClassAttribute(cls, name, INSTANCEATTR,
- '\n'.join(docstring))
+ members[name] = ObjectMember(name, INSTANCEATTR, class_=cls,
+ docstring='\n'.join(docstring))
except (AttributeError, PycodeError):
pass
except AttributeError:
diff --git a/sphinx/ext/autosummary/generate.py b/sphinx/ext/autosummary/generate.py
index c1e5c225b..e21e1d94e 100644
--- a/sphinx/ext/autosummary/generate.py
+++ b/sphinx/ext/autosummary/generate.py
@@ -40,6 +40,7 @@ from sphinx.builders import Builder
from sphinx.config import Config
from sphinx.deprecation import RemovedInSphinx40Warning, RemovedInSphinx50Warning
from sphinx.ext.autodoc import Documenter
+from sphinx.ext.autodoc.importer import import_module
from sphinx.ext.autosummary import get_documenter, import_by_name, import_ivar_by_name
from sphinx.locale import __
from sphinx.pycode import ModuleAnalyzer, PycodeError
@@ -89,12 +90,11 @@ def setup_documenters(app: Any) -> None:
DecoratorDocumenter, ExceptionDocumenter,
FunctionDocumenter, MethodDocumenter, ModuleDocumenter,
NewTypeAttributeDocumenter, NewTypeDataDocumenter,
- PropertyDocumenter, SingledispatchFunctionDocumenter)
+ PropertyDocumenter)
documenters = [
ModuleDocumenter, ClassDocumenter, ExceptionDocumenter, DataDocumenter,
FunctionDocumenter, MethodDocumenter, NewTypeAttributeDocumenter,
NewTypeDataDocumenter, AttributeDocumenter, DecoratorDocumenter, PropertyDocumenter,
- SingledispatchFunctionDocumenter,
] # type: List[Type[Documenter]]
for documenter in documenters:
app.registry.add_documenter(documenter.objtype, documenter)
@@ -285,6 +285,13 @@ def generate_autosummary_content(name: str, obj: Any, parent: Any,
items = [] # type: List[str]
for _, modname, ispkg in pkgutil.iter_modules(obj.__path__):
fullname = name + '.' + modname
+ try:
+ module = import_module(fullname)
+ if module and hasattr(module, '__sphinx_mock__'):
+ continue
+ except ImportError:
+ pass
+
items.append(fullname)
public = [x for x in items if not x.split('.')[-1].startswith('_')]
return public, items
diff --git a/sphinx/ext/mathjax.py b/sphinx/ext/mathjax.py
index e4318f35d..ff8ef3718 100644
--- a/sphinx/ext/mathjax.py
+++ b/sphinx/ext/mathjax.py
@@ -17,14 +17,17 @@ from docutils import nodes
import sphinx
from sphinx.application import Sphinx
-from sphinx.builders.html import StandaloneHTMLBuilder
from sphinx.domains.math import MathDomain
-from sphinx.environment import BuildEnvironment
from sphinx.errors import ExtensionError
from sphinx.locale import _
from sphinx.util.math import get_node_equation_number
from sphinx.writers.html import HTMLTranslator
+# more information for mathjax secure url is here:
+# https://docs.mathjax.org/en/latest/start.html#secure-access-to-the-cdn
+MATHJAX_URL = ('https://cdnjs.cloudflare.com/ajax/libs/mathjax/2.7.7/latest.js?'
+ 'config=TeX-AMS-MML_HTMLorMML')
+
def html_visit_math(self: HTMLTranslator, node: nodes.math) -> None:
self.body.append(self.starttag(node, 'span', '', CLASS='math notranslate nohighlight'))
@@ -66,25 +69,25 @@ def html_visit_displaymath(self: HTMLTranslator, node: nodes.math_block) -> None
raise nodes.SkipNode
-def install_mathjax(app: Sphinx, env: BuildEnvironment) -> None:
+def install_mathjax(app: Sphinx, pagename: str, templatename: str, context: Dict,
+ event_arg: Any) -> None:
if app.builder.format != 'html' or app.builder.math_renderer_name != 'mathjax': # type: ignore # NOQA
return
if not app.config.mathjax_path:
raise ExtensionError('mathjax_path config value must be set for the '
'mathjax extension to work')
- builder = cast(StandaloneHTMLBuilder, app.builder)
- domain = cast(MathDomain, env.get_domain('math'))
- if domain.has_equations():
+ domain = cast(MathDomain, app.env.get_domain('math'))
+ if domain.has_equations(pagename):
# Enable mathjax only if equations exists
options = {'async': 'async'}
if app.config.mathjax_options:
options.update(app.config.mathjax_options)
- builder.add_js_file(app.config.mathjax_path, **options)
+ app.add_js_file(app.config.mathjax_path, **options) # type: ignore
if app.config.mathjax_config:
body = "MathJax.Hub.Config(%s)" % json.dumps(app.config.mathjax_config)
- builder.add_js_file(None, type="text/x-mathjax-config", body=body)
+ app.add_js_file(None, type="text/x-mathjax-config", body=body)
def setup(app: Sphinx) -> Dict[str, Any]:
@@ -92,15 +95,11 @@ def setup(app: Sphinx) -> Dict[str, Any]:
(html_visit_math, None),
(html_visit_displaymath, None))
- # more information for mathjax secure url is here:
- # https://docs.mathjax.org/en/latest/start.html#secure-access-to-the-cdn
- app.add_config_value('mathjax_path',
- 'https://cdnjs.cloudflare.com/ajax/libs/mathjax/2.7.7/latest.js?'
- 'config=TeX-AMS-MML_HTMLorMML', 'html')
+ app.add_config_value('mathjax_path', MATHJAX_URL, 'html')
app.add_config_value('mathjax_options', {}, 'html')
app.add_config_value('mathjax_inline', [r'\(', r'\)'], 'html')
app.add_config_value('mathjax_display', [r'\[', r'\]'], 'html')
app.add_config_value('mathjax_config', None, 'html')
- app.connect('env-updated', install_mathjax)
+ app.connect('html-page-context', install_mathjax)
return {'version': sphinx.__display_version__, 'parallel_read_safe': True}
diff --git a/sphinx/registry.py b/sphinx/registry.py
index 8a988cb98..c6a249e74 100644
--- a/sphinx/registry.py
+++ b/sphinx/registry.py
@@ -63,7 +63,7 @@ class SphinxComponentRegistry:
self.documenters = {} # type: Dict[str, Type[Documenter]]
#: css_files; a list of tuple of filename and attributes
- self.css_files = [] # type: List[Tuple[str, Dict[str, str]]]
+ self.css_files = [] # type: List[Tuple[str, Dict[str, Any]]]
#: domains; a dict of domain name -> domain class
self.domains = {} # type: Dict[str, Type[Domain]]
@@ -94,7 +94,7 @@ class SphinxComponentRegistry:
self.html_block_math_renderers = {} # type: Dict[str, Tuple[Callable, Callable]]
#: js_files; list of JS paths or URLs
- self.js_files = [] # type: List[Tuple[str, Dict[str, str]]]
+ self.js_files = [] # type: List[Tuple[str, Dict[str, Any]]]
#: LaTeX packages; list of package names and its options
self.latex_packages = [] # type: List[Tuple[str, str]]
@@ -361,10 +361,10 @@ class SphinxComponentRegistry:
attrgetter: Callable[[Any, str, Any], Any]) -> None:
self.autodoc_attrgettrs[typ] = attrgetter
- def add_css_files(self, filename: str, **attributes: str) -> None:
+ def add_css_files(self, filename: str, **attributes: Any) -> None:
self.css_files.append((filename, attributes))
- def add_js_file(self, filename: str, **attributes: str) -> None:
+ def add_js_file(self, filename: str, **attributes: Any) -> None:
logger.debug('[app] adding js_file: %r, %r', filename, attributes)
self.js_files.append((filename, attributes))
diff --git a/sphinx/transforms/post_transforms/images.py b/sphinx/transforms/post_transforms/images.py
index fb4c3ca20..2603e0458 100644
--- a/sphinx/transforms/post_transforms/images.py
+++ b/sphinx/transforms/post_transforms/images.py
@@ -197,15 +197,15 @@ class ImageConverter(BaseImageConverter):
def match(self, node: nodes.image) -> bool:
if not self.app.builder.supported_image_types:
return False
+ elif set(node['candidates']) & set(self.app.builder.supported_image_types):
+ # builder supports the image; no need to convert
+ return False
elif self.available is None:
# store the value to the class variable to share it during the build
self.__class__.available = self.is_available()
if not self.available:
return False
- elif set(node['candidates']) & set(self.app.builder.supported_image_types):
- # builder supports the image; no need to convert
- return False
else:
rule = self.get_conversion_rule(node)
if rule:
diff --git a/sphinx/util/typing.py b/sphinx/util/typing.py
index 80f1acc48..e85c40cdf 100644
--- a/sphinx/util/typing.py
+++ b/sphinx/util/typing.py
@@ -10,6 +10,7 @@
import sys
import typing
+from struct import Struct
from typing import Any, Callable, Dict, Generator, List, Optional, Tuple, TypeVar, Union
from docutils import nodes
@@ -94,6 +95,9 @@ def restify(cls: Optional["Type"]) -> str:
return ':obj:`None`'
elif cls is Ellipsis:
return '...'
+ elif cls is Struct:
+ # Before Python 3.9, struct.Struct class has incorrect __module__.
+ return ':class:`struct.Struct`'
elif inspect.isNewType(cls):
return ':class:`%s`' % cls.__name__
elif cls.__module__ in ('__builtin__', 'builtins'):
@@ -305,6 +309,9 @@ def stringify(annotation: Any) -> str:
return annotation.__qualname__
elif annotation is Ellipsis:
return '...'
+ elif annotation is Struct:
+ # Before Python 3.9, struct.Struct class has incorrect __module__.
+ return 'struct.Struct'
if sys.version_info >= (3, 7): # py37+
return _stringify_py37(annotation)