diff options
Diffstat (limited to 'sphinx')
-rw-r--r-- | sphinx/__init__.py | 6 | ||||
-rw-r--r-- | sphinx/application.py | 70 | ||||
-rw-r--r-- | sphinx/builders/html/__init__.py | 36 | ||||
-rw-r--r-- | sphinx/builders/html/transforms.py | 5 | ||||
-rw-r--r-- | sphinx/builders/texinfo.py | 3 | ||||
-rw-r--r-- | sphinx/config.py | 3 | ||||
-rw-r--r-- | sphinx/directives/__init__.py | 12 | ||||
-rw-r--r-- | sphinx/domains/c.py | 2 | ||||
-rw-r--r-- | sphinx/domains/cpp.py | 2 | ||||
-rw-r--r-- | sphinx/domains/javascript.py | 2 | ||||
-rw-r--r-- | sphinx/domains/math.py | 7 | ||||
-rw-r--r-- | sphinx/domains/python.py | 2 | ||||
-rw-r--r-- | sphinx/domains/rst.py | 2 | ||||
-rw-r--r-- | sphinx/domains/std.py | 4 | ||||
-rw-r--r-- | sphinx/ext/autodoc/__init__.py | 375 | ||||
-rw-r--r-- | sphinx/ext/autodoc/importer.py | 41 | ||||
-rw-r--r-- | sphinx/ext/autosummary/generate.py | 11 | ||||
-rw-r--r-- | sphinx/ext/mathjax.py | 27 | ||||
-rw-r--r-- | sphinx/registry.py | 8 | ||||
-rw-r--r-- | sphinx/transforms/post_transforms/images.py | 6 | ||||
-rw-r--r-- | sphinx/util/typing.py | 7 |
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) |