diff --git a/.circleci/config.yml b/.circleci/config.yml index 6b5c7379b..9ded24cff 100644 --- a/.circleci/config.yml +++ b/.circleci/config.yml @@ -3,6 +3,8 @@ jobs: build: docker: - image: sphinxdoc/docker-ci + environment: + DO_EPUBCHECK: 1 working_directory: /sphinx steps: - checkout diff --git a/CHANGES b/CHANGES index 1f2d2f569..a09e19137 100644 --- a/CHANGES +++ b/CHANGES @@ -51,36 +51,69 @@ Incompatible changes Deprecated ---------- +* C, parsing of pre-v3 style type directives and roles, along with the options + :confval:`c_allow_pre_v3` and :confval:`c_warn_on_allowed_pre_v3`. + Features added -------------- +* #2076: autodoc: Allow overriding of exclude-members in skip-member function +* #2024: autosummary: Add :confval:`autosummary_filename_map` to avoid conflict + of filenames between two object with different case * #7849: html: Add :confval:`html_codeblock_linenos_style` to change the style of line numbers for code-blocks * #7853: C and C++, support parameterized GNU style attributes. * #7888: napoleon: Add aliases Warn and Raise. * C, added :rst:dir:`c:alias` directive for inserting copies of existing declarations. +* #7745: html: inventory is broken if the docname contains a space +* #7902: html theme: Add a new option :confval:`globaltoc_maxdepth` to control + the behavior of globaltoc in sidebar +* #7840: i18n: Optimize the dependencies check on bootstrap +* #5208: linkcheck: Support checks for local links +* #5090: setuptools: Link verbosity to distutils' -v and -q option * #7052: add ``:noindexentry:`` to the Python, C, C++, and Javascript domains. Update the documentation to better reflect the relationship between this option and the ``:noindex:`` option. +* #7899: C, add possibility of parsing of some pre-v3 style type directives and + roles and try to convert them to equivalent v3 directives/roles. + Set the new option :confval:`c_allow_pre_v3` to ``True`` to enable this. + The warnings printed from this functionality can be suppressed by setting + :confval:`c_warn_on_allowed_pre_v3`` to ``True``. + The functionality is immediately deprecated. Bugs fixed ---------- * #7886: autodoc: TypeError is raised on mocking generic-typed classes +* #7935: autodoc: function signature is not shown when the function has a + parameter having ``inspect._empty`` as its default value +* #7901: autodoc: type annotations for overloaded functions are not resolved +* #904: autodoc: An instance attribute cause a crash of autofunction directive +* #1362: autodoc: ``private-members`` option does not work for class attributes +* #7983: autodoc: Generator type annotation is wrongly rendered in py36 * #7839: autosummary: cannot handle umlauts in function names * #7865: autosummary: Failed to extract summary line when abbreviations found * #7866: autosummary: Failed to extract correct summary line when docstring contains a hyperlink target +* #7469: autosummary: "Module attributes" header is not translatable +* #7940: apidoc: An extra newline is generated at the end of the rst file if a + module has submodules * #4258: napoleon: decorated special methods are not shown * #7715: LaTeX: ``numfig_secnum_depth > 1`` leads to wrong figure links * #7846: html theme: XML-invalid files were generated * #7894: gettext: Wrong source info is shown when using rst_epilog +* #7691: linkcheck: HEAD requests are not used for checking +* #4888: i18n: Failed to add an explicit title to ``:ref:`` role on translation +* #7928: py domain: failed to resolve a type annotation for the attribute +* #7968: i18n: The content of ``math`` directive is interpreted as reST on + translation * #7869: :rst:role:`abbr` role without an explanation will show the explanation from the previous abbr role * C and C++, removed ``noindex`` directive option as it did nothing. * #7619: Duplicated node IDs are generated if node has multiple IDs +* #2050: Symbols sections are appeared twice in the index page Testing -------- @@ -103,6 +136,9 @@ Features added Bugs fixed ---------- +* C, don't deepcopy the entire symbol table and make a mess every time an + enumerator is handled. + Testing -------- @@ -626,7 +662,7 @@ Release 2.4.1 (released Feb 11, 2020) Bugs fixed ---------- -* #7120: html: crashed when on scaling SVG images which have float dimentions +* #7120: html: crashed when on scaling SVG images which have float dimensions * #7126: autodoc: TypeError: 'getset_descriptor' object is not iterable Release 2.4.0 (released Feb 09, 2020) @@ -772,7 +808,7 @@ Features added * #6548: html: Use favicon for OpenSearch if available * #6729: html theme: agogo theme now supports ``rightsidebar`` option * #6780: Add PEP-561 Support -* #6762: latex: Allow to load additonal LaTeX packages via ``extrapackages`` key +* #6762: latex: Allow to load additional LaTeX packages via ``extrapackages`` key of :confval:`latex_elements` * #1331: Add new config variable: :confval:`user_agent` * #6000: LaTeX: have backslash also be an inline literal word wrap break diff --git a/EXAMPLES b/EXAMPLES index 87bf8feb6..3332c0b33 100644 --- a/EXAMPLES +++ b/EXAMPLES @@ -363,7 +363,7 @@ Documentation using a custom theme or integrated in a website * `Roundup `__ * `SaltStack `__ * `scikit-learn `__ -* `SciPy `__ +* `SciPy `__ * `Scrapy `__ * `Seaborn `__ * `Selenium `__ @@ -390,6 +390,7 @@ Homepages and other non-documentation sites * `Pylearn2 `__ (sphinxdoc, customized) * `PyXLL `__ (sphinx_bootstrap_theme, customized) * `SciPy Cookbook `__ (sphinx_rtd_theme) +* `Tech writer at work blog `__ (custom theme) * `The Wine Cellar Book `__ (sphinxdoc) * `Thomas Cokelaer's Python, Sphinx and reStructuredText tutorials `__ (standard) * `UC Berkeley ME233 Advanced Control Systems II course `__ (sphinxdoc) diff --git a/README.rst b/README.rst index b99b79702..13bbab99d 100644 --- a/README.rst +++ b/README.rst @@ -30,6 +30,10 @@ :target: https://opensource.org/licenses/BSD-3-Clause :alt: BSD 3 Clause +.. image:: https://codetriage.com/sphinx-doc/sphinx/badges/users.svg + :target: https://codetriage.com/sphinx-doc/sphinx + :alt: Open Source Helpers badge + Sphinx is a tool that makes it easy to create intelligent and beautiful documentation for Python projects (or other documents consisting of multiple reStructuredText sources), written by Georg Brandl. It was originally created diff --git a/doc/usage/configuration.rst b/doc/usage/configuration.rst index 382e4091e..309d6e700 100644 --- a/doc/usage/configuration.rst +++ b/doc/usage/configuration.rst @@ -2546,6 +2546,23 @@ Options for the C domain .. versionadded:: 3.0 +.. confval:: c_allow_pre_v3 + + A boolean (default ``False``) controlling whether to parse and try to + convert pre-v3 style type directives and type roles. + + .. versionadded:: 3.2 + .. deprecated:: 3.2 + Use the directives and roles added in v3. + +.. confval:: c_warn_on_allowed_pre_v3 + + A boolean (default ``True``) controlling whether to warn when a pre-v3 + style type directive/role is parsed and converted. + + .. versionadded:: 3.2 + .. deprecated:: 3.2 + Use the directives and roles added in v3. .. _cpp-config: diff --git a/doc/usage/extensions/autosummary.rst b/doc/usage/extensions/autosummary.rst index 38d18361e..d50abd89e 100644 --- a/doc/usage/extensions/autosummary.rst +++ b/doc/usage/extensions/autosummary.rst @@ -175,7 +175,7 @@ also use these config values: .. confval:: autosummary_generate_overwrite - If true, autosummary already overwrites stub files by generated contents. + If true, autosummary overwrites existing files by generated stub pages. Defaults to true (enabled). .. versionadded:: 3.0 @@ -195,6 +195,15 @@ also use these config values: .. versionadded:: 2.1 +.. confval:: autosummary_filename_map + + A dict mapping object names to filenames. This is necessary to avoid + filename conflicts where multiple objects have names that are + indistinguishable when case is ignored, on file systems where filenames + are case-insensitive. + + .. versionadded:: 3.2 + Customizing templates --------------------- diff --git a/doc/usage/theming.rst b/doc/usage/theming.rst index 792a4a53d..5474e9620 100644 --- a/doc/usage/theming.rst +++ b/doc/usage/theming.rst @@ -172,6 +172,12 @@ These themes are: .. versionadded:: 3.1 + - **globaltoc_maxdepth** (int): The maximum depth of the toctree in + ``globaltoc.html`` (see :confval:`html_sidebars`). Set it to -1 to allow + unlimited depth. Defaults to the max depth selected in the toctree directive. + + .. versionadded:: 3.2 + **alabaster** `Alabaster theme`_ is a modified "Kr" Sphinx theme from @kennethreitz (especially as used in his Requests project), which was itself originally diff --git a/package-lock.json b/package-lock.json index 469127026..e3fb91e21 100644 --- a/package-lock.json +++ b/package-lock.json @@ -702,9 +702,9 @@ } }, "lodash": { - "version": "4.17.14", - "resolved": "https://registry.npmjs.org/lodash/-/lodash-4.17.14.tgz", - "integrity": "sha512-mmKYbW3GLuJeX+iGP+Y7Gp1AiGHGbXHCOh/jZmrawMmsE7MS4znI3RL2FsjbqOyMayHInjOeykW7PEajUk1/xw==", + "version": "4.17.19", + "resolved": "https://registry.npmjs.org/lodash/-/lodash-4.17.19.tgz", + "integrity": "sha512-JNvd8XER9GQX0v2qJgsaN/mzFCNA5BRe/j8JN9d+tWyGLSodKQHKFicdwNYzWwI3wjRnaKPsGj1XkBjx/F96DQ==", "dev": true }, "log4js": { diff --git a/sphinx/builders/_epub_base.py b/sphinx/builders/_epub_base.py index 7976b4f7c..7a2c10207 100644 --- a/sphinx/builders/_epub_base.py +++ b/sphinx/builders/_epub_base.py @@ -393,7 +393,7 @@ class EpubBuilder(StandaloneHTMLBuilder): return ext in VECTOR_GRAPHICS_EXTENSIONS def copy_image_files_pil(self) -> None: - """Copy images using Pillow, the Python Imaging Libary. + """Copy images using Pillow, the Python Imaging Library. The method tries to read and write the files with Pillow, converting the format and resizing the image if necessary/possible. """ diff --git a/sphinx/builders/dirhtml.py b/sphinx/builders/dirhtml.py index ba60c923c..6fab8cf82 100644 --- a/sphinx/builders/dirhtml.py +++ b/sphinx/builders/dirhtml.py @@ -49,9 +49,12 @@ class DirectoryHTMLBuilder(StandaloneHTMLBuilder): # for compatibility deprecated_alias('sphinx.builders.html', { - 'DirectoryHTMLBuilder': DirectoryHTMLBuilder, + 'DirectoryHTMLBuilder': DirectoryHTMLBuilder, }, - RemovedInSphinx40Warning) + RemovedInSphinx40Warning, + { + 'DirectoryHTMLBuilder': 'sphinx.builders.dirhtml.DirectoryHTMLBuilder', + }) def setup(app: Sphinx) -> Dict[str, Any]: diff --git a/sphinx/builders/html/__init__.py b/sphinx/builders/html/__init__.py index 97981988d..1767782ad 100644 --- a/sphinx/builders/html/__init__.py +++ b/sphinx/builders/html/__init__.py @@ -15,6 +15,7 @@ import sys from datetime import datetime from os import path from typing import Any, Dict, IO, Iterable, Iterator, List, Set, Tuple, Type +from urllib.parse import quote from docutils import nodes from docutils.core import publish_parts @@ -881,6 +882,8 @@ class StandaloneHTMLBuilder(Builder): def _get_local_toctree(self, docname: str, collapse: bool = True, **kwargs: Any) -> str: if 'includehidden' not in kwargs: kwargs['includehidden'] = False + if kwargs.get('maxdepth') == '': + kwargs.pop('maxdepth') return self.render_partial(TocTree(self.env).get_toctree_for( docname, self, collapse, **kwargs))['fragment'] @@ -940,7 +943,7 @@ class StandaloneHTMLBuilder(Builder): # --------- these are overwritten by the serialization builder def get_target_uri(self, docname: str, typ: str = None) -> str: - return docname + self.link_suffix + return quote(docname) + self.link_suffix def handle_page(self, pagename: str, addctx: Dict, templatename: str = 'page.html', outfilename: str = None, event_arg: Any = None) -> None: diff --git a/sphinx/builders/linkcheck.py b/sphinx/builders/linkcheck.py index dd5317087..ef8f9d902 100644 --- a/sphinx/builders/linkcheck.py +++ b/sphinx/builders/linkcheck.py @@ -35,6 +35,8 @@ from sphinx.util.requests import is_ssl_error logger = logging.getLogger(__name__) +uri_re = re.compile('[a-z]+://') + DEFAULT_REQUEST_HEADERS = { 'Accept': 'text/html,application/xhtml+xml;q=0.9,*/*;q=0.8', @@ -210,10 +212,21 @@ class CheckExternalLinksBuilder(Builder): def check() -> Tuple[str, str, int]: # check for various conditions without bothering the network - if len(uri) == 0 or uri.startswith(('#', 'mailto:', 'ftp:')): + if len(uri) == 0 or uri.startswith(('#', 'mailto:')): return 'unchecked', '', 0 elif not uri.startswith(('http:', 'https:')): - return 'local', '', 0 + if uri_re.match(uri): + # non supported URI schemes (ex. ftp) + return 'unchecked', '', 0 + else: + if path.exists(path.join(self.srcdir, uri)): + return 'working', '', 0 + else: + for rex in self.to_ignore: + if rex.match(uri): + return 'ignored', '', 0 + else: + return 'broken', '', 0 elif uri in self.good: return 'working', 'old', 0 elif uri in self.broken: diff --git a/sphinx/builders/singlehtml.py b/sphinx/builders/singlehtml.py index b145109a6..df90b4c73 100644 --- a/sphinx/builders/singlehtml.py +++ b/sphinx/builders/singlehtml.py @@ -193,7 +193,11 @@ deprecated_alias('sphinx.builders.html', { 'SingleFileHTMLBuilder': SingleFileHTMLBuilder, }, - RemovedInSphinx40Warning) + RemovedInSphinx40Warning, + { + 'SingleFileHTMLBuilder': + 'sphinx.builders.singlehtml.SingleFileHTMLBuilder', + }) def setup(app: Sphinx) -> Dict[str, Any]: diff --git a/sphinx/deprecation.py b/sphinx/deprecation.py index 77b462da8..aeefc4f9f 100644 --- a/sphinx/deprecation.py +++ b/sphinx/deprecation.py @@ -29,27 +29,39 @@ class RemovedInSphinx60Warning(PendingDeprecationWarning): RemovedInNextVersionWarning = RemovedInSphinx50Warning -def deprecated_alias(modname: str, objects: Dict, warning: "Type[Warning]") -> None: +def deprecated_alias(modname: str, objects: Dict[str, object], + warning: "Type[Warning]", names: Dict[str, str] = None) -> None: module = import_module(modname) - sys.modules[modname] = _ModuleWrapper(module, modname, objects, warning) # type: ignore + sys.modules[modname] = _ModuleWrapper( # type: ignore + module, modname, objects, warning, names) class _ModuleWrapper: - def __init__(self, module: Any, modname: str, objects: Dict, warning: "Type[Warning]" - ) -> None: + def __init__(self, module: Any, modname: str, + objects: Dict[str, object], + warning: "Type[Warning]", + names: Dict[str, str]) -> None: self._module = module self._modname = modname self._objects = objects self._warning = warning + self._names = names def __getattr__(self, name: str) -> Any: - if name in self._objects: - warnings.warn("%s.%s is deprecated. Check CHANGES for Sphinx " - "API modifications." % (self._modname, name), - self._warning, stacklevel=3) - return self._objects[name] + if name not in self._objects: + return getattr(self._module, name) - return getattr(self._module, name) + canonical_name = self._names.get(name, None) + if canonical_name is not None: + warnings.warn( + "The alias '{}.{}' is deprecated, use '{}' instead. Check CHANGES for " + "Sphinx API modifications.".format(self._modname, name, canonical_name), + self._warning, stacklevel=3) + else: + warnings.warn("{}.{} is deprecated. Check CHANGES for Sphinx " + "API modifications.".format(self._modname, name), + self._warning, stacklevel=3) + return self._objects[name] class DeprecatedDict(dict): diff --git a/sphinx/directives/__init__.py b/sphinx/directives/__init__.py index 7684931c7..e2b0d3342 100644 --- a/sphinx/directives/__init__.py +++ b/sphinx/directives/__init__.py @@ -268,7 +268,10 @@ deprecated_alias('sphinx.directives', { 'DescDirective': ObjectDescription, }, - RemovedInSphinx50Warning) + RemovedInSphinx50Warning, + { + 'DescDirective': 'sphinx.directives.ObjectDescription', + }) def setup(app: "Sphinx") -> Dict[str, Any]: diff --git a/sphinx/domains/c.py b/sphinx/domains/c.py index 10ced026d..642fee55e 100644 --- a/sphinx/domains/c.py +++ b/sphinx/domains/c.py @@ -22,6 +22,7 @@ from sphinx import addnodes from sphinx.addnodes import pending_xref from sphinx.application import Sphinx from sphinx.builders import Builder +from sphinx.deprecation import RemovedInSphinx50Warning from sphinx.directives import ObjectDescription from sphinx.domains import Domain, ObjType from sphinx.environment import BuildEnvironment @@ -111,6 +112,9 @@ class ASTIdentifier(ASTBaseBase): assert len(identifier) != 0 self.identifier = identifier + def __eq__(self, other: Any) -> bool: + return type(other) is ASTIdentifier and self.identifier == other.identifier + def is_anon(self) -> bool: return self.identifier[0] == '@' @@ -1335,6 +1339,10 @@ class ASTDeclaration(ASTBaseBase): # set by CObject._add_enumerator_to_parent self.enumeratorScopedSymbol = None # type: Symbol + def clone(self) -> "ASTDeclaration": + return ASTDeclaration(self.objectType, self.directiveType, + self.declaration.clone(), self.semicolon) + @property def name(self) -> ASTNestedName: return self.declaration.name @@ -1424,6 +1432,16 @@ class Symbol: debug_lookup = False debug_show_tree = False + def __copy__(self): + assert False # shouldn't happen + + def __deepcopy__(self, memo): + if self.parent: + assert False # shouldn't happen + else: + # the domain base class makes a copy of the initial data, which is fine + return Symbol(None, None, None, None) + @staticmethod def debug_print(*args: Any) -> None: print(Symbol.debug_indent_string * Symbol.debug_indent, end="") @@ -1512,7 +1530,6 @@ class Symbol: self.parent = None def clear_doc(self, docname: str) -> None: - newChildren = [] # type: List[Symbol] for sChild in self._children: sChild.clear_doc(docname) if sChild.declaration and sChild.docname == docname: @@ -1524,8 +1541,6 @@ class Symbol: sChild.siblingBelow.siblingAbove = sChild.siblingAbove sChild.siblingAbove = None sChild.siblingBelow = None - newChildren.append(sChild) - self._children = newChildren def get_all_symbols(self) -> Iterator["Symbol"]: yield self @@ -2937,6 +2952,23 @@ class DefinitionParser(BaseParser): init = ASTInitializer(initVal) return ASTEnumerator(name, init) + def parse_pre_v3_type_definition(self) -> ASTDeclaration: + self.skip_ws() + declaration = None # type: Any + if self.skip_word('struct'): + typ = 'struct' + declaration = self._parse_struct() + elif self.skip_word('union'): + typ = 'union' + declaration = self._parse_union() + elif self.skip_word('enum'): + typ = 'enum' + declaration = self._parse_enum() + else: + self.fail("Could not parse pre-v3 type directive." + " Must start with 'struct', 'union', or 'enum'.") + return ASTDeclaration(typ, typ, declaration, False) + def parse_declaration(self, objectType: str, directiveType: str) -> ASTDeclaration: if objectType not in ('function', 'member', 'macro', 'struct', 'union', 'enum', 'enumerator', 'type'): @@ -3114,6 +3146,9 @@ class CObject(ObjectDescription): def parse_definition(self, parser: DefinitionParser) -> ASTDeclaration: return parser.parse_declaration(self.object_type, self.objtype) + def parse_pre_v3_type_definition(self, parser: DefinitionParser) -> ASTDeclaration: + return parser.parse_pre_v3_type_definition() + def describe_signature(self, signode: TextElement, ast: Any, options: Dict) -> None: ast.describe_signature(signode, 'lastIsName', self.env, options) @@ -3135,8 +3170,27 @@ class CObject(ObjectDescription): parser = DefinitionParser(sig, location=signode, config=self.env.config) try: - ast = self.parse_definition(parser) - parser.assert_end() + try: + ast = self.parse_definition(parser) + parser.assert_end() + except DefinitionError as eOrig: + if not self.env.config['c_allow_pre_v3']: + raise + if self.objtype != 'type': + raise + try: + ast = self.parse_pre_v3_type_definition(parser) + parser.assert_end() + except DefinitionError: + raise eOrig + self.object_type = ast.objectType # type: ignore + if self.env.config['c_warn_on_allowed_pre_v3']: + msg = "{}: Pre-v3 C type directive '.. c:type:: {}' converted to " \ + "'.. c:{}:: {}'." \ + "\nThe original parsing error was:\n{}" + msg = msg.format(RemovedInSphinx50Warning.__name__, + sig, ast.objectType, ast, eOrig) + logger.warning(msg, location=signode) except DefinitionError as e: logger.warning(e, location=signode) # It is easier to assume some phony name than handling the error in @@ -3445,6 +3499,39 @@ class CXRefRole(XRefRole): title = title[dot + 1:] return title, target + def run(self) -> Tuple[List[Node], List[system_message]]: + if not self.env.config['c_allow_pre_v3']: + return super().run() + + text = self.text.replace('\n', ' ') + parser = DefinitionParser(text, location=self.get_source_info(), + config=self.env.config) + try: + parser.parse_xref_object() + # it succeeded, so let it through + return super().run() + except DefinitionError as eOrig: + # try as if it was an c:expr + parser.pos = 0 + try: + ast = parser.parse_expression() + except DefinitionError: + # that didn't go well, just default back + return super().run() + classes = ['xref', 'c', 'c-texpr'] + parentSymbol = self.env.temp_data.get('cpp:parent_symbol', None) + if parentSymbol is None: + parentSymbol = self.env.domaindata['c']['root_symbol'] + signode = nodes.inline(classes=classes) + ast.describe_signature(signode, 'markType', self.env, parentSymbol) + + if self.env.config['c_warn_on_allowed_pre_v3']: + msg = "{}: Pre-v3 C type role ':c:type:`{}`' converted to ':c:expr:`{}`'." + msg += "\nThe original parsing error was:\n{}" + msg = msg.format(RemovedInSphinx50Warning.__name__, text, text, eOrig) + logger.warning(msg, location=self.get_source_info()) + return [signode], [] + class CExprRole(SphinxRole): def __init__(self, asCode: bool) -> None: @@ -3646,6 +3733,9 @@ def setup(app: Sphinx) -> Dict[str, Any]: app.add_config_value("c_paren_attributes", [], 'env') app.add_post_transform(AliasTransform) + app.add_config_value("c_allow_pre_v3", False, 'env') + app.add_config_value("c_warn_on_allowed_pre_v3", True, 'env') + return { 'version': 'builtin', 'env_version': 2, diff --git a/sphinx/domains/cpp.py b/sphinx/domains/cpp.py index c840423aa..2348fb76b 100644 --- a/sphinx/domains/cpp.py +++ b/sphinx/domains/cpp.py @@ -141,7 +141,7 @@ T = TypeVar('T') visibility storage-class-specifier function-specifier "friend" "constexpr" "volatile" "const" trailing-type-specifier # where trailing-type-specifier can no be cv-qualifier - # Inside e.g., template paramters a strict subset is used + # Inside e.g., template parameters a strict subset is used # (see type-specifier-seq) trailing-type-specifier -> simple-type-specifier -> @@ -3782,6 +3782,16 @@ class Symbol: debug_lookup = False # overridden by the corresponding config value debug_show_tree = False # overridden by the corresponding config value + def __copy__(self): + assert False # shouldn't happen + + def __deepcopy__(self, memo): + if self.parent: + assert False # shouldn't happen + else: + # the domain base class makes a copy of the initial data, which is fine + return Symbol(None, None, None, None, None, None) + @staticmethod def debug_print(*args: Any) -> None: print(Symbol.debug_indent_string * Symbol.debug_indent, end="") diff --git a/sphinx/domains/javascript.py b/sphinx/domains/javascript.py index 6c75f8dd7..dc7d610c2 100644 --- a/sphinx/domains/javascript.py +++ b/sphinx/domains/javascript.py @@ -149,7 +149,7 @@ class JSObject(ObjectDescription): :py:class:`JSObject` represents JavaScript language constructs. For constructs that are nestable, this method will build up a stack of the - nesting heirarchy so that it can be later de-nested correctly, in + nesting hierarchy so that it can be later de-nested correctly, in :py:meth:`after_content`. For constructs that aren't nestable, the stack is bypassed, and instead diff --git a/sphinx/domains/python.py b/sphinx/domains/python.py index ba07b8847..4f2afd0b3 100644 --- a/sphinx/domains/python.py +++ b/sphinx/domains/python.py @@ -79,18 +79,24 @@ class ModuleEntry(NamedTuple): deprecated: bool -def type_to_xref(text: str) -> addnodes.pending_xref: +def type_to_xref(text: str, env: BuildEnvironment = None) -> addnodes.pending_xref: """Convert a type string to a cross reference node.""" if text == 'None': reftype = 'obj' else: reftype = 'class' + if env: + kwargs = {'py:module': env.ref_context.get('py:module'), + 'py:class': env.ref_context.get('py:class')} + else: + kwargs = {} + return pending_xref('', nodes.Text(text), - refdomain='py', reftype=reftype, reftarget=text) + refdomain='py', reftype=reftype, reftarget=text, **kwargs) -def _parse_annotation(annotation: str) -> List[Node]: +def _parse_annotation(annotation: str, env: BuildEnvironment = None) -> List[Node]: """Parse type annotation.""" def unparse(node: ast.AST) -> List[Node]: if isinstance(node, ast.Attribute): @@ -132,18 +138,22 @@ def _parse_annotation(annotation: str) -> List[Node]: else: raise SyntaxError # unsupported syntax + if env is None: + warnings.warn("The env parameter for _parse_annotation becomes required now.", + RemovedInSphinx50Warning, stacklevel=2) + try: tree = ast_parse(annotation) result = unparse(tree) for i, node in enumerate(result): if isinstance(node, nodes.Text): - result[i] = type_to_xref(str(node)) + result[i] = type_to_xref(str(node), env) return result except SyntaxError: - return [type_to_xref(annotation)] + return [type_to_xref(annotation, env)] -def _parse_arglist(arglist: str) -> addnodes.desc_parameterlist: +def _parse_arglist(arglist: str, env: BuildEnvironment = None) -> addnodes.desc_parameterlist: """Parse a list of arguments using AST parser""" params = addnodes.desc_parameterlist(arglist) sig = signature_from_str('(%s)' % arglist) @@ -169,7 +179,7 @@ def _parse_arglist(arglist: str) -> addnodes.desc_parameterlist: node += addnodes.desc_sig_name('', param.name) if param.annotation is not param.empty: - children = _parse_annotation(param.annotation) + children = _parse_annotation(param.annotation, env) node += addnodes.desc_sig_punctuation('', ':') node += nodes.Text(' ') node += addnodes.desc_sig_name('', '', *children) # type: ignore @@ -418,7 +428,7 @@ class PyObject(ObjectDescription): signode += addnodes.desc_name(name, name) if arglist: try: - signode += _parse_arglist(arglist) + signode += _parse_arglist(arglist, self.env) except SyntaxError: # fallback to parse arglist original parser. # it supports to represent optional arguments (ex. "func(foo [, bar])") @@ -433,7 +443,7 @@ class PyObject(ObjectDescription): signode += addnodes.desc_parameterlist() if retann: - children = _parse_annotation(retann) + children = _parse_annotation(retann, self.env) signode += addnodes.desc_returns(retann, '', *children) anno = self.options.get('annotation') @@ -478,7 +488,7 @@ class PyObject(ObjectDescription): :py:class:`PyObject` represents Python language constructs. For constructs that are nestable, such as a Python classes, this method will - build up a stack of the nesting heirarchy so that it can be later + build up a stack of the nesting hierarchy so that it can be later de-nested correctly, in :py:meth:`after_content`. For constructs that aren't nestable, the stack is bypassed, and instead @@ -600,7 +610,7 @@ class PyVariable(PyObject): typ = self.options.get('type') if typ: - annotations = _parse_annotation(typ) + annotations = _parse_annotation(typ, self.env) signode += addnodes.desc_annotation(typ, '', nodes.Text(': '), *annotations) value = self.options.get('value') @@ -761,7 +771,7 @@ class PyAttribute(PyObject): typ = self.options.get('type') if typ: - annotations = _parse_annotation(typ) + annotations = _parse_annotation(typ, self.env) signode += addnodes.desc_annotation(typ, '', nodes.Text(': '), *annotations) value = self.options.get('value') diff --git a/sphinx/environment/__init__.py b/sphinx/environment/__init__.py index 6de352498..71cbcfbdf 100644 --- a/sphinx/environment/__init__.py +++ b/sphinx/environment/__init__.py @@ -370,11 +370,11 @@ class BuildEnvironment: # add catalog mo file dependency repo = CatalogRepository(self.srcdir, self.config.locale_dirs, self.config.language, self.config.source_encoding) + mo_paths = {c.domain: c.mo_path for c in repo.catalogs} for docname in self.found_docs: domain = docname_to_domain(docname, self.config.gettext_compact) - for catalog in repo.catalogs: - if catalog.domain == domain: - self.dependencies[docname].add(catalog.mo_path) + if domain in mo_paths: + self.dependencies[docname].add(mo_paths[domain]) except OSError as exc: raise DocumentError(__('Failed to scan documents in %s: %r') % (self.srcdir, exc)) from exc diff --git a/sphinx/environment/adapters/indexentries.py b/sphinx/environment/adapters/indexentries.py index 5af213932..b13ac97c3 100644 --- a/sphinx/environment/adapters/indexentries.py +++ b/sphinx/environment/adapters/indexentries.py @@ -98,9 +98,8 @@ class IndexEntries: for subentry in indexentry[1].values(): subentry[0].sort(key=keyfunc0) # type: ignore - # sort the index entries; put all symbols at the front, even those - # following the letters in ASCII, this is where the chr(127) comes from - def keyfunc(entry: Tuple[str, List]) -> Tuple[str, str]: + # sort the index entries + def keyfunc(entry: Tuple[str, List]) -> Tuple[Tuple[int, str], str]: key, (void, void, category_key) = entry if category_key: # using specified category key to sort @@ -108,11 +107,16 @@ class IndexEntries: lckey = unicodedata.normalize('NFD', key.lower()) if lckey.startswith('\N{RIGHT-TO-LEFT MARK}'): lckey = lckey[1:] + if lckey[0:1].isalpha() or lckey.startswith('_'): - lckey = chr(127) + lckey + # put non-symbol characters at the folloing group (1) + sortkey = (1, lckey) + else: + # put symbols at the front of the index (0) + sortkey = (0, lckey) # ensure a determinstic order *within* letters by also sorting on # the entry itself - return (lckey, entry[0]) + return (sortkey, entry[0]) newlist = sorted(new.items(), key=keyfunc) if group_entries: diff --git a/sphinx/events.py b/sphinx/events.py index 9d7402554..ff753b3b6 100644 --- a/sphinx/events.py +++ b/sphinx/events.py @@ -107,7 +107,7 @@ class EventManager: raise except Exception as exc: raise ExtensionError(__("Handler %r for event %r threw an exception") % - (listener.handler, name)) from exc + (listener.handler, name), exc) from exc return results def emit_firstresult(self, name: str, *args: Any, diff --git a/sphinx/ext/autodoc/__init__.py b/sphinx/ext/autodoc/__init__.py index 319fe06bb..472002cea 100644 --- a/sphinx/ext/autodoc/__init__.py +++ b/sphinx/ext/autodoc/__init__.py @@ -34,7 +34,9 @@ from sphinx.pycode import ModuleAnalyzer, PycodeError from sphinx.util import inspect from sphinx.util import logging from sphinx.util.docstrings import extract_metadata, prepare_docstring -from sphinx.util.inspect import getdoc, object_description, safe_getattr, stringify_signature +from sphinx.util.inspect import ( + evaluate_signature, getdoc, object_description, safe_getattr, stringify_signature +) from sphinx.util.typing import stringify as stringify_typehint if TYPE_CHECKING: @@ -333,7 +335,7 @@ class Documenter: ('.' + '.'.join(self.objpath) if self.objpath else '') return True - def import_object(self) -> bool: + def import_object(self, raiseerror: bool = False) -> bool: """Import the object given by *self.modname* and *self.objpath* and set it as *self.object*. @@ -347,9 +349,12 @@ class Documenter: self.module, self.parent, self.object_name, self.object = ret return True except ImportError as exc: - logger.warning(exc.args[0], type='autodoc', subtype='import_object') - self.env.note_reread() - return False + if raiseerror: + raise + else: + logger.warning(exc.args[0], type='autodoc', subtype='import_object') + self.env.note_reread() + return False def get_real_modname(self) -> str: """Get the real module name of an object to document. @@ -619,6 +624,10 @@ class Documenter: if safe_getattr(member, '__sphinx_mock__', False): # mocked module or object pass + elif (self.options.exclude_members not in (None, ALL) and + membername in self.options.exclude_members): + # remove members given by exclude-members + keep = False elif want_all and membername.startswith('__') and \ membername.endswith('__') and len(membername) > 4: # special __methods__ @@ -688,16 +697,6 @@ class Documenter: # find out which members are documentable members_check_module, members = self.get_object_members(want_all) - # remove members given by exclude-members - if self.options.exclude_members: - members = [ - (membername, member) for (membername, member) in members - if ( - self.options.exclude_members is ALL or - membername not in self.options.exclude_members - ) - ] - # document non-skipped members memberdocumenters = [] # type: List[Tuple[Documenter, bool]] for (mname, member, isattr) in self.filter_members(members, want_all): @@ -885,7 +884,7 @@ class ModuleDocumenter(Documenter): type='autodoc') return ret - def import_object(self) -> Any: + def import_object(self, raiseerror: bool = False) -> bool: def is_valid_module_all(__all__: Any) -> bool: """Check the given *__all__* is valid for a module.""" if (isinstance(__all__, (list, tuple)) and @@ -894,7 +893,7 @@ class ModuleDocumenter(Documenter): else: return False - ret = super().import_object() + ret = super().import_object(raiseerror) if not self.options.ignore_module_all: __all__ = getattr(self.object, '__all__', None) @@ -1190,7 +1189,9 @@ class FunctionDocumenter(DocstringSignatureMixin, ModuleLevelDocumenter): # typ documenter.objpath = [None] sigs.append(documenter.format_signature()) if overloaded: + __globals__ = safe_getattr(self.object, '__globals__', {}) for overload in self.analyzer.overloads.get('.'.join(self.objpath)): + overload = evaluate_signature(overload, __globals__) sig = stringify_signature(overload, **kwargs) sigs.append(sig) @@ -1279,8 +1280,8 @@ class ClassDocumenter(DocstringSignatureMixin, ModuleLevelDocumenter): # type: ) -> bool: return isinstance(member, type) - def import_object(self) -> Any: - ret = super().import_object() + def import_object(self, raiseerror: bool = False) -> bool: + ret = super().import_object(raiseerror) # if the class is documented under another name, document it # as data/attribute if ret: @@ -1389,7 +1390,11 @@ class ClassDocumenter(DocstringSignatureMixin, ModuleLevelDocumenter): # type: sigs = [] if overloaded: # Use signatures for overloaded methods instead of the implementation method. + method = safe_getattr(self._signature_class, self._signature_method_name, None) + __globals__ = safe_getattr(method, '__globals__', {}) for overload in self.analyzer.overloads.get(qualname): + overload = evaluate_signature(overload, __globals__) + parameters = list(overload.parameters.values()) overload = overload.replace(parameters=parameters[1:], return_annotation=Parameter.empty) @@ -1586,7 +1591,7 @@ class DataDeclarationDocumenter(DataDocumenter): isattr and member is INSTANCEATTR) - def import_object(self) -> bool: + def import_object(self, raiseerror: bool = False) -> bool: """Never import anything.""" # disguise as a data self.objtype = 'data' @@ -1685,8 +1690,8 @@ class MethodDocumenter(DocstringSignatureMixin, ClassLevelDocumenter): # type: return inspect.isroutine(member) and \ not isinstance(parent, ModuleDocumenter) - def import_object(self) -> Any: - ret = super().import_object() + def import_object(self, raiseerror: bool = False) -> bool: + ret = super().import_object(raiseerror) if not ret: return ret @@ -1778,7 +1783,9 @@ class MethodDocumenter(DocstringSignatureMixin, ClassLevelDocumenter): # type: documenter.objpath = [None] sigs.append(documenter.format_signature()) if overloaded: + __globals__ = safe_getattr(self.object, '__globals__', {}) for overload in self.analyzer.overloads.get('.'.join(self.objpath)): + overload = evaluate_signature(overload, __globals__) if not inspect.isstaticmethod(self.object, cls=self.parent, name=self.object_name): parameters = list(overload.parameters.values()) @@ -1851,15 +1858,42 @@ class AttributeDocumenter(DocstringStripSignatureMixin, ClassLevelDocumenter): def document_members(self, all_members: bool = False) -> None: pass - def import_object(self) -> Any: - ret = super().import_object() - if inspect.isenumattribute(self.object): - self.object = self.object.value - if inspect.isattributedescriptor(self.object): - self._datadescriptor = True - else: - # if it's not a data descriptor - self._datadescriptor = False + def isinstanceattribute(self) -> bool: + """Check the subject is an instance attribute.""" + try: + analyzer = ModuleAnalyzer.for_module(self.modname) + attr_docs = analyzer.find_attr_docs() + if self.objpath: + key = ('.'.join(self.objpath[:-1]), self.objpath[-1]) + if key in attr_docs: + return True + + return False + except PycodeError: + return False + + 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 + if inspect.isattributedescriptor(self.object): + self._datadescriptor = True + else: + # if it's not a data descriptor + self._datadescriptor = False + except ImportError as exc: + if self.isinstanceattribute(): + self.object = INSTANCEATTR + self._datadescriptor = False + ret = True + elif raiseerror: + raise + else: + logger.warning(exc.args[0], type='autodoc', subtype='import_object') + self.env.note_reread() + ret = False + return ret def get_real_modname(self) -> str: @@ -1966,7 +2000,7 @@ class InstanceAttributeDocumenter(AttributeDocumenter): isattr and member is INSTANCEATTR) - def import_object(self) -> bool: + def import_object(self, raiseerror: bool = False) -> bool: """Never import anything.""" # disguise as an attribute self.objtype = 'attribute' @@ -1997,7 +2031,7 @@ class SlotsAttributeDocumenter(AttributeDocumenter): """This documents only SLOTSATTR members.""" return member is SLOTSATTR - def import_object(self) -> Any: + def import_object(self, raiseerror: bool = False) -> bool: """Never import anything.""" # disguise as an attribute self.objtype = 'attribute' @@ -2011,9 +2045,12 @@ class SlotsAttributeDocumenter(AttributeDocumenter): self.module, _, _, self.parent = ret return True except ImportError as exc: - logger.warning(exc.args[0], type='autodoc', subtype='import_object') - self.env.note_reread() - return False + if raiseerror: + raise + else: + logger.warning(exc.args[0], type='autodoc', subtype='import_object') + self.env.note_reread() + return False def get_doc(self, ignore: int = None) -> List[List[str]]: """Decode and return lines of the docstring(s) for the object.""" diff --git a/sphinx/ext/autodoc/importer.py b/sphinx/ext/autodoc/importer.py index 637c69814..1a961e84a 100644 --- a/sphinx/ext/autodoc/importer.py +++ b/sphinx/ext/autodoc/importer.py @@ -11,7 +11,7 @@ import importlib import traceback import warnings -from typing import Any, Callable, Dict, List, Mapping, NamedTuple, Tuple +from typing import Any, Callable, Dict, List, Mapping, NamedTuple, Optional, Tuple from sphinx.pycode import ModuleAnalyzer from sphinx.util import logging @@ -20,6 +20,36 @@ from sphinx.util.inspect import isclass, isenumclass, safe_getattr logger = logging.getLogger(__name__) +def mangle(subject: Any, name: str) -> str: + """mangle the given name.""" + try: + if isclass(subject) and name.startswith('__') and not name.endswith('__'): + return "_%s%s" % (subject.__name__, name) + except AttributeError: + pass + + return name + + +def unmangle(subject: Any, name: str) -> Optional[str]: + """unmangle the given name.""" + try: + if isclass(subject) and not name.endswith('__'): + prefix = "_%s__" % subject.__name__ + if name.startswith(prefix): + return name.replace(prefix, "__", 1) + else: + for cls in subject.__mro__: + prefix = "_%s__" % cls.__name__ + if name.startswith(prefix): + # mangled attribute defined in parent class + return None + except AttributeError: + pass + + return name + + def import_module(modname: str, warningiserror: bool = False) -> Any: """ Call importlib.import_module(modname), convert exceptions to ImportError @@ -67,7 +97,8 @@ def import_object(modname: str, objpath: List[str], objtype: str = '', for attrname in objpath: parent = obj logger.debug('[autodoc] getattr(_, %r)', attrname) - obj = attrgetter(obj, attrname) + mangled_name = mangle(obj, attrname) + obj = attrgetter(obj, mangled_name) logger.debug('[autodoc] => %r', obj) object_name = attrname return [module, parent, object_name, obj] @@ -161,7 +192,8 @@ def get_object_members(subject: Any, objpath: List[str], attrgetter: Callable, try: value = attrgetter(subject, name) directly_defined = name in obj_dict - if name not in members: + name = unmangle(subject, name) + if name and name not in members: members[name] = Attribute(name, directly_defined, value) except AttributeError: continue @@ -169,7 +201,8 @@ def get_object_members(subject: Any, objpath: List[str], attrgetter: Callable, # annotation only member (ex. attr: int) if hasattr(subject, '__annotations__') and isinstance(subject.__annotations__, Mapping): for name in subject.__annotations__: - if name not in members: + name = unmangle(subject, name) + if name and name not in members: members[name] = Attribute(name, True, INSTANCEATTR) if analyzer: diff --git a/sphinx/ext/autosummary/__init__.py b/sphinx/ext/autosummary/__init__.py index 38a5e7e85..c1d0febe0 100644 --- a/sphinx/ext/autosummary/__init__.py +++ b/sphinx/ext/autosummary/__init__.py @@ -248,7 +248,9 @@ class Autosummary(SphinxDirective): tree_prefix = self.options['toctree'].strip() docnames = [] excluded = Matcher(self.config.exclude_patterns) + filename_map = self.config.autosummary_filename_map for name, sig, summary, real_name in items: + real_name = filename_map.get(real_name, real_name) docname = posixpath.join(tree_prefix, real_name) docname = posixpath.normpath(posixpath.join(dirname, docname)) if docname not in self.env.found_docs: @@ -731,6 +733,7 @@ def setup(app: Sphinx) -> Dict[str, Any]: app.add_role('autolink', AutoLink()) app.connect('builder-inited', process_generate_options) app.add_config_value('autosummary_context', {}, True) + app.add_config_value('autosummary_filename_map', {}, 'html') app.add_config_value('autosummary_generate', [], True, [bool]) app.add_config_value('autosummary_generate_overwrite', True, False) app.add_config_value('autosummary_mock_imports', diff --git a/sphinx/ext/autosummary/generate.py b/sphinx/ext/autosummary/generate.py index c45b90cdd..3c38775fe 100644 --- a/sphinx/ext/autosummary/generate.py +++ b/sphinx/ext/autosummary/generate.py @@ -70,6 +70,7 @@ class DummyApplication: self.warningiserror = False self.config.add('autosummary_context', {}, True, None) + self.config.add('autosummary_filename_map', {}, True, None) self.config.init_values() def emit_firstresult(self, *args: Any) -> None: @@ -375,6 +376,11 @@ def generate_autosummary_docs(sources: List[str], output_dir: str = None, # keep track of new files new_files = [] + if app: + filename_map = app.config.autosummary_filename_map + else: + filename_map = {} + # write for entry in sorted(set(items), key=str): if entry.path is None: @@ -400,7 +406,7 @@ def generate_autosummary_docs(sources: List[str], output_dir: str = None, imported_members, app, entry.recursive, context, modname, qualname) - filename = os.path.join(path, name + suffix) + filename = os.path.join(path, filename_map.get(name, name) + suffix) if os.path.isfile(filename): with open(filename, encoding=encoding) as f: old_content = f.read() diff --git a/sphinx/ext/autosummary/templates/autosummary/module.rst b/sphinx/ext/autosummary/templates/autosummary/module.rst index 3a93e872a..e74c012f4 100644 --- a/sphinx/ext/autosummary/templates/autosummary/module.rst +++ b/sphinx/ext/autosummary/templates/autosummary/module.rst @@ -4,7 +4,7 @@ {% block attributes %} {% if attributes %} - .. rubric:: Module Attributes + .. rubric:: {{ _('Module Attributes') }} .. autosummary:: {% for item in attributes %} diff --git a/sphinx/ext/ifconfig.py b/sphinx/ext/ifconfig.py index 0e652509f..bae4554cf 100644 --- a/sphinx/ext/ifconfig.py +++ b/sphinx/ext/ifconfig.py @@ -62,7 +62,7 @@ def process_ifconfig_nodes(app: Sphinx, doctree: nodes.document, docname: str) - # handle exceptions in a clean fashion from traceback import format_exception_only msg = ''.join(format_exception_only(err.__class__, err)) - newnode = doctree.reporter.error('Exception occured in ' + newnode = doctree.reporter.error('Exception occurred in ' 'ifconfig expression: \n%s' % msg, base_node=node) node.replace_self(newnode) diff --git a/sphinx/pycode/__init__.py b/sphinx/pycode/__init__.py index 295963ca3..943c77244 100644 --- a/sphinx/pycode/__init__.py +++ b/sphinx/pycode/__init__.py @@ -10,6 +10,7 @@ import re import tokenize +from collections import OrderedDict from importlib import import_module from inspect import Signature from io import StringIO @@ -145,7 +146,7 @@ class ModuleAnalyzer: parser = Parser(self.code) parser.parse() - self.attr_docs = {} + self.attr_docs = OrderedDict() for (scope, comment) in parser.comments.items(): if comment: self.attr_docs[scope] = comment.splitlines() + [''] diff --git a/sphinx/pycode/parser.py b/sphinx/pycode/parser.py index a6f9b1643..a417b5a1b 100644 --- a/sphinx/pycode/parser.py +++ b/sphinx/pycode/parser.py @@ -12,6 +12,7 @@ import itertools import re import sys import tokenize +from collections import OrderedDict from inspect import Signature from token import NAME, NEWLINE, INDENT, DEDENT, NUMBER, OP, STRING from tokenize import COMMENT, NL @@ -231,7 +232,7 @@ class VariableCommentPicker(ast.NodeVisitor): self.context = [] # type: List[str] self.current_classes = [] # type: List[str] self.current_function = None # type: ast.FunctionDef - self.comments = {} # type: Dict[Tuple[str, str], str] + self.comments = OrderedDict() # type: Dict[Tuple[str, str], str] self.annotations = {} # type: Dict[Tuple[str, str], str] self.previous = None # type: ast.AST self.deforders = {} # type: Dict[str, int] diff --git a/sphinx/setup_command.py b/sphinx/setup_command.py index 2b0e4b74b..9965aa15a 100644 --- a/sphinx/setup_command.py +++ b/sphinx/setup_command.py @@ -105,7 +105,8 @@ class BuildDoc(Command): self.config_dir = None # type: str self.link_index = False self.copyright = '' - self.verbosity = 0 + # Link verbosity to distutils' (which uses 1 by default). + self.verbosity = self.distribution.verbose - 1 # type: ignore self.traceback = False self.nitpicky = False self.keep_going = False diff --git a/sphinx/templates/apidoc/package.rst_t b/sphinx/templates/apidoc/package.rst_t index 8630a87b7..b7380e8a0 100644 --- a/sphinx/templates/apidoc/package.rst_t +++ b/sphinx/templates/apidoc/package.rst_t @@ -35,7 +35,7 @@ Submodules ---------- {% if separatemodules %} {{ toctree(submodules) }} -{%- else %} +{% else %} {%- for submodule in submodules %} {% if show_headings %} {{- [submodule, "module"] | join(" ") | e | heading(2) }} @@ -43,7 +43,7 @@ Submodules {{ automodule(submodule, automodule_options) }} {% endfor %} {%- endif %} -{% endif %} +{%- endif %} {%- if not modulefirst and not is_namespace %} Module contents diff --git a/sphinx/themes/basic/globaltoc.html b/sphinx/themes/basic/globaltoc.html index 2f16655cf..1b2f9baee 100644 --- a/sphinx/themes/basic/globaltoc.html +++ b/sphinx/themes/basic/globaltoc.html @@ -8,4 +8,4 @@ :license: BSD, see LICENSE for details. #}

{{ _('Table of Contents') }}

-{{ toctree(includehidden=theme_globaltoc_includehidden, collapse=theme_globaltoc_collapse) }} +{{ toctree(includehidden=theme_globaltoc_includehidden, collapse=theme_globaltoc_collapse, maxdepth=theme_globaltoc_maxdepth) }} diff --git a/sphinx/themes/basic/theme.conf b/sphinx/themes/basic/theme.conf index 3c289d5dc..ff378cab4 100644 --- a/sphinx/themes/basic/theme.conf +++ b/sphinx/themes/basic/theme.conf @@ -12,3 +12,4 @@ body_max_width = 800 navigation_with_keys = False globaltoc_collapse = true globaltoc_includehidden = false +globaltoc_maxdepth = diff --git a/sphinx/themes/bizstyle/static/css3-mediaqueries_src.js b/sphinx/themes/bizstyle/static/css3-mediaqueries_src.js index f21dd4949..787862027 100644 --- a/sphinx/themes/bizstyle/static/css3-mediaqueries_src.js +++ b/sphinx/themes/bizstyle/static/css3-mediaqueries_src.js @@ -1035,7 +1035,7 @@ domReady(function enableCssMediaQueries() { var vpw = cssHelper.getViewportWidth(); var vph = cssHelper.getViewportHeight(); // check whether vp size has really changed, because IE also triggers resize event when body size changes - // 20px allowance to accomodate short appearance of scrollbars in IE in some cases + // 20px allowance to accommodate short appearance of scrollbars in IE in some cases if (Math.abs(vpw - cvpw) > scrollbarWidth || Math.abs(vph - cvph) > scrollbarWidth) { cvpw = vpw; cvph = vph; diff --git a/sphinx/transforms/i18n.py b/sphinx/transforms/i18n.py index 028044de2..b016ba418 100644 --- a/sphinx/transforms/i18n.py +++ b/sphinx/transforms/i18n.py @@ -36,6 +36,13 @@ if TYPE_CHECKING: logger = logging.getLogger(__name__) +# The attributes not copied to the translated node +# +# * refexplict: For allow to give (or not to give) an explicit title +# to the pending_xref on translation +EXCLUDED_PENDING_XREF_ATTRIBUTES = ('refexplicit',) + + N = TypeVar('N', bound=nodes.Node) @@ -429,11 +436,8 @@ class Locale(SphinxTransform): # Copy attributes to keep original node behavior. Especially # copying 'reftarget', 'py:module', 'py:class' are needed. for k, v in xref_reftarget_map.get(key, {}).items(): - # Note: This implementation overwrite all attributes. - # if some attributes `k` should not be overwritten, - # you should provide exclude list as: - # `if k not in EXCLUDE_LIST: new[k] = v` - new[k] = v + if k not in EXCLUDED_PENDING_XREF_ATTRIBUTES: + new[k] = v # update leaves for child in patch.children: diff --git a/sphinx/util/__init__.py b/sphinx/util/__init__.py index e02e8adce..ab9c1c20e 100644 --- a/sphinx/util/__init__.py +++ b/sphinx/util/__init__.py @@ -258,7 +258,7 @@ def get_full_modname(modname: str, attribute: str) -> str: return None module = import_module(modname) - # Allow an attribute to have multiple parts and incidentially allow + # Allow an attribute to have multiple parts and incidentally allow # repeated .s in the attribute. value = module for attr in attribute.split('.'): diff --git a/sphinx/util/cfamily.py b/sphinx/util/cfamily.py index f8ce4c827..d4c21bfdb 100644 --- a/sphinx/util/cfamily.py +++ b/sphinx/util/cfamily.py @@ -103,7 +103,6 @@ class ASTBaseBase: __hash__ = None # type: Callable[[], int] def clone(self) -> Any: - """Clone a definition expression node.""" return deepcopy(self) def _stringify(self, transform: StringifyTransform) -> str: diff --git a/sphinx/util/compat.py b/sphinx/util/compat.py index 2c38f668b..6893efaf9 100644 --- a/sphinx/util/compat.py +++ b/sphinx/util/compat.py @@ -20,7 +20,7 @@ def register_application_for_autosummary(app: "Sphinx") -> None: """Register application object to autosummary module. Since Sphinx-1.7, documenters and attrgetters are registered into - applicaiton object. As a result, the arguments of + application object. As a result, the arguments of ``get_documenter()`` has been changed. To keep compatibility, this handler registers application object to the module. """ diff --git a/sphinx/util/docstrings.py b/sphinx/util/docstrings.py index 64fdbf1d7..67a008643 100644 --- a/sphinx/util/docstrings.py +++ b/sphinx/util/docstrings.py @@ -57,7 +57,7 @@ def prepare_docstring(s: str, ignore: int = None, tabsize: int = 8) -> List[str] if ignore is None: ignore = 1 else: - warnings.warn("The 'ignore' argument to parepare_docstring() is deprecated.", + warnings.warn("The 'ignore' argument to prepare_docstring() is deprecated.", RemovedInSphinx50Warning, stacklevel=2) lines = s.expandtabs(tabsize).splitlines() diff --git a/sphinx/util/docutils.py b/sphinx/util/docutils.py index aa310c546..7ea9649ac 100644 --- a/sphinx/util/docutils.py +++ b/sphinx/util/docutils.py @@ -495,7 +495,7 @@ def new_document(source_path: str, settings: Any = None) -> nodes.document: """Return a new empty document object. This is an alternative of docutils'. This is a simple wrapper for ``docutils.utils.new_document()``. It - caches the result of docutils' and use it on second call for instanciation. + caches the result of docutils' and use it on second call for instantiation. This makes an instantiation of document nodes much faster. """ global __document_cache__ diff --git a/sphinx/util/inspect.py b/sphinx/util/inspect.py index 8e85ea392..6bc3018e1 100644 --- a/sphinx/util/inspect.py +++ b/sphinx/util/inspect.py @@ -22,13 +22,14 @@ from inspect import ( # NOQA Parameter, isclass, ismethod, ismethoddescriptor, ismodule ) from io import StringIO -from typing import Any, Callable +from typing import Any, Callable, Dict from typing import cast from sphinx.deprecation import RemovedInSphinx50Warning from sphinx.pycode.ast import ast # for py36-37 from sphinx.pycode.ast import unparse as ast_unparse from sphinx.util import logging +from sphinx.util.typing import ForwardRef from sphinx.util.typing import stringify as stringify_annotation if sys.version_info > (3, 7): @@ -467,7 +468,53 @@ def signature(subject: Callable, bound_method: bool = False, follow_wrapped: boo if len(parameters) > 0: parameters.pop(0) - return inspect.Signature(parameters, return_annotation=return_annotation) + # To allow to create signature object correctly for pure python functions, + # pass an internal parameter __validate_parameters__=False to Signature + # + # For example, this helps a function having a default value `inspect._empty`. + # refs: https://github.com/sphinx-doc/sphinx/issues/7935 + return inspect.Signature(parameters, return_annotation=return_annotation, # type: ignore + __validate_parameters__=False) + + +def evaluate_signature(sig: inspect.Signature, globalns: Dict = None, localns: Dict = None + ) -> inspect.Signature: + """Evaluate unresolved type annotations in a signature object.""" + def evaluate(annotation: Any, globalns: Dict, localns: Dict) -> Any: + """Evaluate unresolved type annotation.""" + try: + if isinstance(annotation, str): + ref = ForwardRef(annotation, True) + annotation = ref._evaluate(globalns, localns) + + if isinstance(annotation, ForwardRef): + annotation = annotation._evaluate(globalns, localns) + elif isinstance(annotation, str): + # might be a ForwardRef'ed annotation in overloaded functions + ref = ForwardRef(annotation, True) + annotation = ref._evaluate(globalns, localns) + except (NameError, TypeError): + # failed to evaluate type. skipped. + pass + + return annotation + + if globalns is None: + globalns = {} + if localns is None: + localns = globalns + + parameters = list(sig.parameters.values()) + for i, param in enumerate(parameters): + if param.annotation: + annotation = evaluate(param.annotation, globalns, localns) + parameters[i] = param.replace(annotation=annotation) + + return_annotation = sig.return_annotation + if return_annotation: + return_annotation = evaluate(return_annotation, globalns, localns) + + return sig.replace(parameters=parameters, return_annotation=return_annotation) def stringify_signature(sig: inspect.Signature, show_annotation: bool = True, diff --git a/sphinx/util/nodes.py b/sphinx/util/nodes.py index 099866d66..f757bc9c3 100644 --- a/sphinx/util/nodes.py +++ b/sphinx/util/nodes.py @@ -239,6 +239,7 @@ def is_translatable(node: Node) -> bool: LITERAL_TYPE_NODES = ( nodes.literal_block, nodes.doctest_block, + nodes.math_block, nodes.raw, ) IMAGE_TYPE_NODES = ( diff --git a/sphinx/util/requests.py b/sphinx/util/requests.py index d9d1d1b2c..b3fc8bc35 100644 --- a/sphinx/util/requests.py +++ b/sphinx/util/requests.py @@ -124,4 +124,4 @@ def head(url: str, **kwargs: Any) -> requests.Response: headers.setdefault('User-Agent', useragent_header[0][1]) with ignore_insecure_warning(**kwargs): - return requests.get(url, **kwargs) + return requests.head(url, **kwargs) diff --git a/sphinx/util/typing.py b/sphinx/util/typing.py index 6f12a453a..e91939ec2 100644 --- a/sphinx/util/typing.py +++ b/sphinx/util/typing.py @@ -10,12 +10,27 @@ import sys import typing -from typing import Any, Callable, Dict, List, Tuple, TypeVar, Union +from typing import Any, Callable, Dict, Generator, List, Tuple, TypeVar, Union from docutils import nodes from docutils.parsers.rst.states import Inliner +if sys.version_info > (3, 7): + from typing import ForwardRef +else: + from typing import _ForwardRef # type: ignore + + class ForwardRef: + """A pseudo ForwardRef class for py35 and py36.""" + def __init__(self, arg: Any, is_argument: bool = True) -> None: + self.arg = arg + + def _evaluate(self, globalns: Dict, localns: Dict) -> Any: + ref = _ForwardRef(self.arg) + return ref._eval_type(globalns, localns) + + # An entry of Directive.option_spec DirectiveOption = Callable[[str], Any] @@ -147,6 +162,8 @@ def _stringify_py36(annotation: Any) -> str: params = None if annotation.__args__ is None or len(annotation.__args__) <= 2: # type: ignore # NOQA params = annotation.__args__ # type: ignore + elif annotation.__origin__ == Generator: # type: ignore + params = annotation.__args__ # type: ignore else: # typing.Callable args = ', '.join(stringify(arg) for arg in annotation.__args__[:-1]) # type: ignore diff --git a/sphinx/versioning.py b/sphinx/versioning.py index 3c6a43c19..4f925741c 100644 --- a/sphinx/versioning.py +++ b/sphinx/versioning.py @@ -116,7 +116,7 @@ def merge_doctrees(old: Node, new: Node, condition: Any) -> Iterator[Node]: def get_ratio(old: str, new: str) -> float: - """Return a "similiarity ratio" (in percent) representing the similarity + """Return a "similarity ratio" (in percent) representing the similarity between the two strings where 0 is equal and anything above less than equal. """ if not all([old, new]): diff --git a/sphinx/writers/text.py b/sphinx/writers/text.py index 858c68806..0759b4860 100644 --- a/sphinx/writers/text.py +++ b/sphinx/writers/text.py @@ -73,7 +73,7 @@ class Table: Cell spanning on multiple rows or multiple columns (having a colspan or rowspan greater than one) are automatically referenced - by all the table cells they covers. This is a usefull + by all the table cells they covers. This is a useful representation as we can simply check ``if self[x, y] is self[x, y+1]`` to recognize a rowspan. diff --git a/tests/roots/test-ext-autodoc/target/name_mangling.py b/tests/roots/test-ext-autodoc/target/name_mangling.py new file mode 100644 index 000000000..269b51d93 --- /dev/null +++ b/tests/roots/test-ext-autodoc/target/name_mangling.py @@ -0,0 +1,11 @@ +class Foo: + #: name of Foo + __name = None + __age = None + + +class Bar(Foo): + __address = None + + #: a member having mangled-like name + _Baz__email = None diff --git a/tests/roots/test-ext-autodoc/target/overload.py b/tests/roots/test-ext-autodoc/target/overload.py index da43d32eb..cc4e509f2 100644 --- a/tests/roots/test-ext-autodoc/target/overload.py +++ b/tests/roots/test-ext-autodoc/target/overload.py @@ -7,7 +7,7 @@ def sum(x: int, y: int) -> int: @overload -def sum(x: float, y: float) -> float: +def sum(x: "float", y: "float") -> "float": ... @@ -29,7 +29,7 @@ class Math: ... @overload - def sum(self, x: float, y: float) -> float: + def sum(self, x: "float", y: "float") -> "float": ... @overload @@ -49,7 +49,7 @@ class Foo: ... @overload - def __new__(cls, x: str, y: str) -> "Foo": + def __new__(cls, x: "str", y: "str") -> "Foo": ... def __new__(cls, x, y): @@ -64,7 +64,7 @@ class Bar: ... @overload - def __init__(cls, x: str, y: str) -> None: + def __init__(cls, x: "str", y: "str") -> "None": ... def __init__(cls, x, y): @@ -77,7 +77,7 @@ class Meta(type): ... @overload - def __call__(cls, x: str, y: str) -> Any: + def __call__(cls, x: "str", y: "str") -> "Any": ... def __call__(cls, x, y): diff --git a/tests/roots/test-ext-autosummary-filename-map/autosummary_dummy_module.py b/tests/roots/test-ext-autosummary-filename-map/autosummary_dummy_module.py new file mode 100644 index 000000000..1f57eeb25 --- /dev/null +++ b/tests/roots/test-ext-autosummary-filename-map/autosummary_dummy_module.py @@ -0,0 +1,21 @@ +from os import path # NOQA +from typing import Union + + +class Foo: + class Bar: + pass + + def __init__(self): + pass + + def bar(self): + pass + + @property + def baz(self): + pass + + +def bar(x: Union[int, str], y: int = 1) -> None: + pass diff --git a/tests/roots/test-ext-autosummary-filename-map/conf.py b/tests/roots/test-ext-autosummary-filename-map/conf.py new file mode 100644 index 000000000..17e2fa445 --- /dev/null +++ b/tests/roots/test-ext-autosummary-filename-map/conf.py @@ -0,0 +1,11 @@ +import os +import sys + +sys.path.insert(0, os.path.abspath('.')) + +extensions = ['sphinx.ext.autosummary'] +autosummary_generate = True +autosummary_filename_map = { + "autosummary_dummy_module": "module_mangled", + "autosummary_dummy_module.bar": "bar" +} diff --git a/tests/roots/test-ext-autosummary-filename-map/index.rst b/tests/roots/test-ext-autosummary-filename-map/index.rst new file mode 100644 index 000000000..57d902b6a --- /dev/null +++ b/tests/roots/test-ext-autosummary-filename-map/index.rst @@ -0,0 +1,9 @@ + +.. autosummary:: + :toctree: generated + :caption: An autosummary + + autosummary_dummy_module + autosummary_dummy_module.Foo + autosummary_dummy_module.Foo.bar + autosummary_dummy_module.bar diff --git a/tests/roots/test-ext-autosummary/autosummary_dummy_module.py b/tests/roots/test-ext-autosummary/autosummary_dummy_module.py index 85981a0f8..76158b6b9 100644 --- a/tests/roots/test-ext-autosummary/autosummary_dummy_module.py +++ b/tests/roots/test-ext-autosummary/autosummary_dummy_module.py @@ -2,7 +2,16 @@ from os import path # NOQA from typing import Union +#: module variable +CONSTANT1 = None +CONSTANT2 = None + + class Foo: + #: class variable + CONSTANT3 = None + CONSTANT4 = None + class Bar: pass diff --git a/tests/roots/test-intl/role_xref.txt b/tests/roots/test-intl/role_xref.txt index 875af4667..2919b5c7f 100644 --- a/tests/roots/test-intl/role_xref.txt +++ b/tests/roots/test-intl/role_xref.txt @@ -14,7 +14,7 @@ same type links link to :term:`Some term` and :term:`Some other term`. -link to :ref:`i18n-role-xref` and :ref:`same-type-links`. +link to :ref:`i18n-role-xref`, :ref:`same-type-links` and :ref:`label `. link to :doc:`index` and :doc:`glossary_terms`. diff --git a/tests/roots/test-intl/xx/LC_MESSAGES/role_xref.po b/tests/roots/test-intl/xx/LC_MESSAGES/role_xref.po index 81ee22c6e..96d821fdb 100644 --- a/tests/roots/test-intl/xx/LC_MESSAGES/role_xref.po +++ b/tests/roots/test-intl/xx/LC_MESSAGES/role_xref.po @@ -28,8 +28,8 @@ msgstr "SAME TYPE LINKS" msgid "link to :term:`Some term` and :term:`Some other term`." msgstr "LINK TO :term:`SOME OTHER NEW TERM` AND :term:`SOME NEW TERM`." -msgid "link to :ref:`i18n-role-xref` and :ref:`same-type-links`." -msgstr "LINK TO :ref:`same-type-links` AND :ref:`i18n-role-xref`." +msgid "link to :ref:`i18n-role-xref`, :ref:`same-type-links` and :ref:`label `." +msgstr "LINK TO :ref:`LABEL ` AND :ref:`same-type-links` AND :ref:`same-type-links`." msgid "link to :doc:`index` and :doc:`glossary_terms`." msgstr "LINK TO :doc:`glossary_terms` AND :doc:`index`." diff --git a/tests/roots/test-latex-labels/index.rst b/tests/roots/test-latex-labels/index.rst index 781db5a01..f3c421750 100644 --- a/tests/roots/test-latex-labels/index.rst +++ b/tests/roots/test-latex-labels/index.rst @@ -69,4 +69,4 @@ subsubsection otherdoc -* Embeded standalone hyperlink reference(refs: #5948): `subsection `_. +* Embedded standalone hyperlink reference(refs: #5948): `subsection `_. diff --git a/tests/roots/test-linkcheck/links.txt b/tests/roots/test-linkcheck/links.txt index fa8f11e4c..90759ee63 100644 --- a/tests/roots/test-linkcheck/links.txt +++ b/tests/roots/test-linkcheck/links.txt @@ -11,6 +11,8 @@ Some additional anchors to exercise ignore code * `Example Bar invalid `_ * `Example anchor invalid `_ * `Complete nonsense `_ +* `Example valid local file `_ +* `Example invalid local file `_ .. image:: https://www.google.com/image.png .. figure:: https://www.google.com/image2.png diff --git a/tests/roots/test-root/index.txt b/tests/roots/test-root/index.txt index 27f90f357..e39c958b3 100644 --- a/tests/roots/test-root/index.txt +++ b/tests/roots/test-root/index.txt @@ -32,14 +32,11 @@ Contents: Latest reference Python - self - Indices and tables ================== * :ref:`genindex` * :ref:`modindex` -* :ref:`search` References ========== diff --git a/tests/roots/test-toctree/index.rst b/tests/roots/test-toctree/index.rst index dc7fd2e4a..adf1b84dd 100644 --- a/tests/roots/test-toctree/index.rst +++ b/tests/roots/test-toctree/index.rst @@ -16,6 +16,7 @@ Contents: foo bar http://sphinx-doc.org/ + self .. only:: html diff --git a/tests/test_build_epub.py b/tests/test_build_epub.py index 193cd9024..759090ce3 100644 --- a/tests/test_build_epub.py +++ b/tests/test_build_epub.py @@ -387,6 +387,6 @@ def test_run_epubcheck(app): subprocess.run(['java', '-jar', epubcheck, app.outdir / 'SphinxTests.epub'], stdout=PIPE, stderr=PIPE, check=True) except CalledProcessError as exc: - print(exc.stdout) - print(exc.stderr) + print(exc.stdout.decode('utf-8')) + print(exc.stderr.decode('utf-8')) assert False, 'epubcheck exited with return code %s' % exc.returncode diff --git a/tests/test_build_html.py b/tests/test_build_html.py index 667d6fbdd..00a4c20da 100644 --- a/tests/test_build_html.py +++ b/tests/test_build_html.py @@ -357,7 +357,6 @@ def test_html4_output(app, status, warning): "[@class='reference external']", ''), (".//li/p/a[@href='genindex.html']/span", 'Index'), (".//li/p/a[@href='py-modindex.html']/span", 'Module Index'), - (".//li/p/a[@href='search.html']/span", 'Search Page'), # custom sidebar only for contents (".//h4", 'Contents sidebar'), # custom JavaScript diff --git a/tests/test_build_latex.py b/tests/test_build_latex.py index 818a493a5..f97dbe9d9 100644 --- a/tests/test_build_latex.py +++ b/tests/test_build_latex.py @@ -1470,7 +1470,7 @@ def test_latex_labels(app, status, warning): r'\label{\detokenize{otherdoc:otherdoc}}' r'\label{\detokenize{otherdoc::doc}}' in result) - # Embeded standalone hyperlink reference (refs: #5948) + # Embedded standalone hyperlink reference (refs: #5948) assert result.count(r'\label{\detokenize{index:section1}}') == 1 diff --git a/tests/test_build_linkcheck.py b/tests/test_build_linkcheck.py index d1fec550f..7d85f10c5 100644 --- a/tests/test_build_linkcheck.py +++ b/tests/test_build_linkcheck.py @@ -30,7 +30,9 @@ def test_defaults(app, status, warning): # images should fail assert "Not Found for url: https://www.google.com/image.png" in content assert "Not Found for url: https://www.google.com/image2.png" in content - assert len(content.splitlines()) == 5 + # looking for local file should fail + assert "[broken] path/to/notfound" in content + assert len(content.splitlines()) == 6 @pytest.mark.sphinx('linkcheck', testroot='linkcheck', freshenv=True) @@ -47,8 +49,8 @@ def test_defaults_json(app, status, warning): "info"]: assert attr in row - assert len(content.splitlines()) == 8 - assert len(rows) == 8 + assert len(content.splitlines()) == 10 + assert len(rows) == 10 # the output order of the rows is not stable # due to possible variance in network latency rowsby = {row["uri"]:row for row in rows} @@ -69,7 +71,7 @@ def test_defaults_json(app, status, warning): assert dnerow['uri'] == 'https://localhost:7777/doesnotexist' assert rowsby['https://www.google.com/image2.png'] == { 'filename': 'links.txt', - 'lineno': 16, + 'lineno': 18, 'status': 'broken', 'code': 0, 'uri': 'https://www.google.com/image2.png', @@ -92,7 +94,8 @@ def test_defaults_json(app, status, warning): 'https://localhost:7777/doesnotexist', 'http://www.sphinx-doc.org/en/1.7/intro.html#', 'https://www.google.com/image.png', - 'https://www.google.com/image2.png'] + 'https://www.google.com/image2.png', + 'path/to/notfound'] }) def test_anchors_ignored(app, status, warning): app.builder.build_all() diff --git a/tests/test_domain_py.py b/tests/test_domain_py.py index 43128ece1..d53c6a658 100644 --- a/tests/test_domain_py.py +++ b/tests/test_domain_py.py @@ -238,18 +238,18 @@ def test_get_full_qualified_name(): assert domain.get_full_qualified_name(node) == 'module1.Class.func' -def test_parse_annotation(): - doctree = _parse_annotation("int") +def test_parse_annotation(app): + doctree = _parse_annotation("int", app.env) assert_node(doctree, ([pending_xref, "int"],)) assert_node(doctree[0], pending_xref, refdomain="py", reftype="class", reftarget="int") - doctree = _parse_annotation("List[int]") + doctree = _parse_annotation("List[int]", app.env) assert_node(doctree, ([pending_xref, "List"], [desc_sig_punctuation, "["], [pending_xref, "int"], [desc_sig_punctuation, "]"])) - doctree = _parse_annotation("Tuple[int, int]") + doctree = _parse_annotation("Tuple[int, int]", app.env) assert_node(doctree, ([pending_xref, "Tuple"], [desc_sig_punctuation, "["], [pending_xref, "int"], @@ -257,14 +257,14 @@ def test_parse_annotation(): [pending_xref, "int"], [desc_sig_punctuation, "]"])) - doctree = _parse_annotation("Tuple[()]") + doctree = _parse_annotation("Tuple[()]", app.env) assert_node(doctree, ([pending_xref, "Tuple"], [desc_sig_punctuation, "["], [desc_sig_punctuation, "("], [desc_sig_punctuation, ")"], [desc_sig_punctuation, "]"])) - doctree = _parse_annotation("Callable[[int, int], int]") + doctree = _parse_annotation("Callable[[int, int], int]", app.env) assert_node(doctree, ([pending_xref, "Callable"], [desc_sig_punctuation, "["], [desc_sig_punctuation, "["], @@ -277,12 +277,11 @@ def test_parse_annotation(): [desc_sig_punctuation, "]"])) # None type makes an object-reference (not a class reference) - doctree = _parse_annotation("None") + doctree = _parse_annotation("None", app.env) assert_node(doctree, ([pending_xref, "None"],)) assert_node(doctree[0], pending_xref, refdomain="py", reftype="obj", reftarget="None") - def test_pyfunction_signature(app): text = ".. py:function:: hello(name: str) -> str" doctree = restructuredtext.parse(app, text) @@ -460,14 +459,22 @@ def test_pyobject_prefix(app): def test_pydata(app): - text = ".. py:data:: var\n" + text = (".. py:module:: example\n" + ".. py:data:: var\n" + " :type: int\n") domain = app.env.get_domain('py') doctree = restructuredtext.parse(app, text) - assert_node(doctree, (addnodes.index, - [desc, ([desc_signature, desc_name, "var"], + assert_node(doctree, (nodes.target, + addnodes.index, + addnodes.index, + [desc, ([desc_signature, ([desc_addname, "example."], + [desc_name, "var"], + [desc_annotation, (": ", + [pending_xref, "int"])])], [desc_content, ()])])) - assert 'var' in domain.objects - assert domain.objects['var'] == ('index', 'var', 'data', False) + assert_node(doctree[3][0][2][1], pending_xref, **{"py:module": "example"}) + assert 'example.var' in domain.objects + assert domain.objects['example.var'] == ('index', 'example.var', 'data', False) def test_pyfunction(app): @@ -700,6 +707,8 @@ def test_pyattribute(app): [desc_sig_punctuation, "]"])], [desc_annotation, " = ''"])], [desc_content, ()])) + assert_node(doctree[1][1][1][0][1][1], pending_xref, **{"py:class": "Class"}) + assert_node(doctree[1][1][1][0][1][3], pending_xref, **{"py:class": "Class"}) assert 'Class.attr' in domain.objects assert domain.objects['Class.attr'] == ('index', 'Class.attr', 'attribute', False) diff --git a/tests/test_environment_indexentries.py b/tests/test_environment_indexentries.py index 7f6003d54..c15226d85 100644 --- a/tests/test_environment_indexentries.py +++ b/tests/test_environment_indexentries.py @@ -25,12 +25,14 @@ def test_create_single_index(app): ".. index:: ёлка\n" ".. index:: ‏תירבע‎\n" ".. index:: 9-symbol\n" - ".. index:: &-symbol\n") + ".. index:: &-symbol\n" + ".. index:: £100\n") restructuredtext.parse(app, text) index = IndexEntries(app.env).create_index(app.builder) assert len(index) == 6 assert index[0] == ('Symbols', [('&-symbol', [[('', '#index-9')], [], None]), - ('9-symbol', [[('', '#index-8')], [], None])]) + ('9-symbol', [[('', '#index-8')], [], None]), + ('£100', [[('', '#index-10')], [], None])]) assert index[1] == ('D', [('docutils', [[('', '#index-0')], [], None])]) assert index[2] == ('P', [('pip', [[], [('install', [('', '#index-2')]), ('upgrade', [('', '#index-3')])], None]), diff --git a/tests/test_environment_toctree.py b/tests/test_environment_toctree.py index a8c7da62e..4059e5cb2 100644 --- a/tests/test_environment_toctree.py +++ b/tests/test_environment_toctree.py @@ -41,7 +41,8 @@ def test_process_doc(app): assert_node(toctree[0][1][0], addnodes.toctree, caption="Table of Contents", glob=False, hidden=False, titlesonly=False, maxdepth=2, numbered=999, - entries=[(None, 'foo'), (None, 'bar'), (None, 'http://sphinx-doc.org/')], + entries=[(None, 'foo'), (None, 'bar'), (None, 'http://sphinx-doc.org/'), + (None, 'self')], includefiles=['foo', 'bar']) # only branch @@ -219,7 +220,9 @@ def test_get_toctree_for(app): ([list_item, ([compact_paragraph, reference, "foo"], bullet_list)], [list_item, compact_paragraph, reference, "bar"], - [list_item, compact_paragraph, reference, "http://sphinx-doc.org/"])) + [list_item, compact_paragraph, reference, "http://sphinx-doc.org/"], + [list_item, compact_paragraph, reference, + "Welcome to Sphinx Tests’s documentation!"])) assert_node(toctree[1][0][1], ([list_item, compact_paragraph, reference, "quux"], [list_item, compact_paragraph, reference, "foo.1"], @@ -231,6 +234,7 @@ def test_get_toctree_for(app): assert_node(toctree[1][0][1][2][0][0], reference, refuri="foo#foo-2", secnumber=[1, 3]) assert_node(toctree[1][1][0][0], reference, refuri="bar", secnumber=[2]) assert_node(toctree[1][2][0][0], reference, refuri="http://sphinx-doc.org/") + assert_node(toctree[1][3][0][0], reference, refuri="") assert_node(toctree[2], [bullet_list, list_item, compact_paragraph, reference, "baz"]) @@ -255,10 +259,13 @@ def test_get_toctree_for_collapse(app): assert_node(toctree[1], ([list_item, compact_paragraph, reference, "foo"], [list_item, compact_paragraph, reference, "bar"], - [list_item, compact_paragraph, reference, "http://sphinx-doc.org/"])) + [list_item, compact_paragraph, reference, "http://sphinx-doc.org/"], + [list_item, compact_paragraph, reference, + "Welcome to Sphinx Tests’s documentation!"])) assert_node(toctree[1][0][0][0], reference, refuri="foo", secnumber=[1]) assert_node(toctree[1][1][0][0], reference, refuri="bar", secnumber=[2]) assert_node(toctree[1][2][0][0], reference, refuri="http://sphinx-doc.org/") + assert_node(toctree[1][3][0][0], reference, refuri="") assert_node(toctree[2], [bullet_list, list_item, compact_paragraph, reference, "baz"]) @@ -285,7 +292,9 @@ def test_get_toctree_for_maxdepth(app): ([list_item, ([compact_paragraph, reference, "foo"], bullet_list)], [list_item, compact_paragraph, reference, "bar"], - [list_item, compact_paragraph, reference, "http://sphinx-doc.org/"])) + [list_item, compact_paragraph, reference, "http://sphinx-doc.org/"], + [list_item, compact_paragraph, reference, + "Welcome to Sphinx Tests’s documentation!"])) assert_node(toctree[1][0][1], ([list_item, compact_paragraph, reference, "quux"], [list_item, ([compact_paragraph, reference, "foo.1"], @@ -302,6 +311,7 @@ def test_get_toctree_for_maxdepth(app): assert_node(toctree[1][0][1][2][0][0], reference, refuri="foo#foo-2", secnumber=[1, 3]) assert_node(toctree[1][1][0][0], reference, refuri="bar", secnumber=[2]) assert_node(toctree[1][2][0][0], reference, refuri="http://sphinx-doc.org/") + assert_node(toctree[1][3][0][0], reference, refuri="") assert_node(toctree[2], [bullet_list, list_item, compact_paragraph, reference, "baz"]) @@ -327,7 +337,9 @@ def test_get_toctree_for_includehidden(app): ([list_item, ([compact_paragraph, reference, "foo"], bullet_list)], [list_item, compact_paragraph, reference, "bar"], - [list_item, compact_paragraph, reference, "http://sphinx-doc.org/"])) + [list_item, compact_paragraph, reference, "http://sphinx-doc.org/"], + [list_item, compact_paragraph, reference, + "Welcome to Sphinx Tests’s documentation!"])) assert_node(toctree[1][0][1], ([list_item, compact_paragraph, reference, "quux"], [list_item, compact_paragraph, reference, "foo.1"], diff --git a/tests/test_ext_apidoc.py b/tests/test_ext_apidoc.py index e8d923b71..e19a1d7ba 100644 --- a/tests/test_ext_apidoc.py +++ b/tests/test_ext_apidoc.py @@ -121,7 +121,6 @@ def test_pep_0420_enabled_separate(make_app, apidoc): with open(outdir / 'a.b.c.rst') as f: rst = f.read() - assert ".. toctree::\n :maxdepth: 4\n\n a.b.c.d\n" in rst with open(outdir / 'a.b.e.rst') as f: @@ -509,7 +508,6 @@ def test_package_file(tempdir): " :undoc-members:\n" " :show-inheritance:\n" "\n" - "\n" "Module contents\n" "---------------\n" "\n" @@ -595,8 +593,7 @@ def test_package_file_module_first(tempdir): ".. automodule:: testpkg.example\n" " :members:\n" " :undoc-members:\n" - " :show-inheritance:\n" - "\n") + " :show-inheritance:\n") def test_package_file_without_submodules(tempdir): @@ -639,5 +636,4 @@ def test_namespace_package_file(tempdir): ".. automodule:: testpkg.example\n" " :members:\n" " :undoc-members:\n" - " :show-inheritance:\n" - "\n") + " :show-inheritance:\n") diff --git a/tests/test_ext_autodoc.py b/tests/test_ext_autodoc.py index 7b4823a2f..be46c1490 100644 --- a/tests/test_ext_autodoc.py +++ b/tests/test_ext_autodoc.py @@ -1047,7 +1047,7 @@ def test_class_attributes(app): @pytest.mark.sphinx('html', testroot='ext-autodoc') -def test_instance_attributes(app): +def test_autoclass_instance_attributes(app): options = {"members": None} actual = do_autodoc(app, 'class', 'target.InstAttCls', options) assert list(actual) == [ @@ -1120,6 +1120,19 @@ def test_instance_attributes(app): ] +@pytest.mark.sphinx('html', testroot='ext-autodoc') +def test_autoattribute_instance_attributes(app): + actual = do_autodoc(app, 'attribute', 'target.InstAttCls.ia1') + assert list(actual) == [ + '', + '.. py:attribute:: InstAttCls.ia1', + ' :module: target', + '', + ' Doc comment for instance attribute InstAttCls.ia1', + '' + ] + + @pytest.mark.sphinx('html', testroot='ext-autodoc') def test_slots(app): options = {"members": None, @@ -1960,3 +1973,48 @@ def test_name_conflict(app): ' docstring of target.name_conflict.foo::bar.', '', ] + + +@pytest.mark.sphinx('html', testroot='ext-autodoc') +def test_name_mangling(app): + options = {"members": None, + "undoc-members": None, + "private-members": None} + actual = do_autodoc(app, 'module', 'target.name_mangling', options) + assert list(actual) == [ + '', + '.. py:module:: target.name_mangling', + '', + '', + '.. py:class:: Bar()', + ' :module: target.name_mangling', + '', + '', + ' .. py:attribute:: Bar._Baz__email', + ' :module: target.name_mangling', + ' :value: None', + '', + ' a member having mangled-like name', + '', + '', + ' .. py:attribute:: Bar.__address', + ' :module: target.name_mangling', + ' :value: None', + '', + '', + '.. py:class:: Foo()', + ' :module: target.name_mangling', + '', + '', + ' .. py:attribute:: Foo.__age', + ' :module: target.name_mangling', + ' :value: None', + '', + '', + ' .. py:attribute:: Foo.__name', + ' :module: target.name_mangling', + ' :value: None', + '', + ' name of Foo', + '', + ] diff --git a/tests/test_ext_autosummary.py b/tests/test_ext_autosummary.py index 3dd90b8ce..567a8caea 100644 --- a/tests/test_ext_autosummary.py +++ b/tests/test_ext_autosummary.py @@ -208,17 +208,17 @@ def test_autosummary_generate_content_for_module(app): assert template.render.call_args[0][0] == 'module' context = template.render.call_args[0][1] - assert context['members'] == ['Exc', 'Foo', '_Baz', '_Exc', '__builtins__', - '__cached__', '__doc__', '__file__', '__name__', - '__package__', '_quux', 'bar', 'qux'] + assert context['members'] == ['CONSTANT1', 'CONSTANT2', 'Exc', 'Foo', '_Baz', '_Exc', + '__builtins__', '__cached__', '__doc__', '__file__', + '__name__', '__package__', '_quux', 'bar', 'qux'] assert context['functions'] == ['bar'] assert context['all_functions'] == ['_quux', 'bar'] assert context['classes'] == ['Foo'] assert context['all_classes'] == ['Foo', '_Baz'] assert context['exceptions'] == ['Exc'] assert context['all_exceptions'] == ['Exc', '_Exc'] - assert context['attributes'] == ['qux'] - assert context['all_attributes'] == ['qux'] + assert context['attributes'] == ['CONSTANT1', 'qux'] + assert context['all_attributes'] == ['CONSTANT1', 'qux'] assert context['fullname'] == 'autosummary_dummy_module' assert context['module'] == 'autosummary_dummy_module' assert context['objname'] == '' @@ -239,8 +239,9 @@ def test_autosummary_generate_content_for_module_skipped(app): generate_autosummary_content('autosummary_dummy_module', autosummary_dummy_module, None, template, None, False, app, False, {}) context = template.render.call_args[0][1] - assert context['members'] == ['_Baz', '_Exc', '__builtins__', '__cached__', '__doc__', - '__file__', '__name__', '__package__', '_quux', 'qux'] + assert context['members'] == ['CONSTANT1', 'CONSTANT2', '_Baz', '_Exc', '__builtins__', + '__cached__', '__doc__', '__file__', '__name__', + '__package__', '_quux', 'qux'] assert context['functions'] == [] assert context['classes'] == [] assert context['exceptions'] == [] @@ -256,18 +257,18 @@ def test_autosummary_generate_content_for_module_imported_members(app): assert template.render.call_args[0][0] == 'module' context = template.render.call_args[0][1] - assert context['members'] == ['Exc', 'Foo', 'Union', '_Baz', '_Exc', '__builtins__', - '__cached__', '__doc__', '__file__', '__loader__', - '__name__', '__package__', '__spec__', '_quux', - 'bar', 'path', 'qux'] + assert context['members'] == ['CONSTANT1', 'CONSTANT2', 'Exc', 'Foo', 'Union', '_Baz', + '_Exc', '__builtins__', '__cached__', '__doc__', + '__file__', '__loader__', '__name__', '__package__', + '__spec__', '_quux', 'bar', 'path', 'qux'] assert context['functions'] == ['bar'] assert context['all_functions'] == ['_quux', 'bar'] assert context['classes'] == ['Foo'] assert context['all_classes'] == ['Foo', '_Baz'] assert context['exceptions'] == ['Exc'] assert context['all_exceptions'] == ['Exc', '_Exc'] - assert context['attributes'] == ['qux'] - assert context['all_attributes'] == ['qux'] + assert context['attributes'] == ['CONSTANT1', 'qux'] + assert context['all_attributes'] == ['CONSTANT1', 'qux'] assert context['fullname'] == 'autosummary_dummy_module' assert context['module'] == 'autosummary_dummy_module' assert context['objname'] == '' @@ -307,6 +308,11 @@ def test_autosummary_generate(app, status, warning): ' \n' ' Foo\n' ' \n' in module) + assert (' .. autosummary::\n' + ' \n' + ' CONSTANT1\n' + ' qux\n' + ' \n' in module) Foo = (app.srcdir / 'generated' / 'autosummary_dummy_module.Foo.rst').read_text() assert '.. automethod:: __init__' in Foo @@ -317,6 +323,8 @@ def test_autosummary_generate(app, status, warning): ' \n' in Foo) assert (' .. autosummary::\n' ' \n' + ' ~Foo.CONSTANT3\n' + ' ~Foo.CONSTANT4\n' ' ~Foo.baz\n' ' \n' in Foo) @@ -386,6 +394,20 @@ def test_autosummary_recursive(app, status, warning): assert 'package.package.module' in content +@pytest.mark.sphinx('dummy', testroot='ext-autosummary-filename-map') +def test_autosummary_filename_map(app, status, warning): + app.build() + + assert (app.srcdir / 'generated' / 'module_mangled.rst').exists() + assert not (app.srcdir / 'generated' / 'autosummary_dummy_module.rst').exists() + assert (app.srcdir / 'generated' / 'bar.rst').exists() + assert not (app.srcdir / 'generated' / 'autosummary_dummy_module.bar.rst').exists() + assert (app.srcdir / 'generated' / 'autosummary_dummy_module.Foo.rst').exists() + + html_warnings = app._warning.getvalue() + assert html_warnings == '' + + @pytest.mark.sphinx('latex', **default_kw) def test_autosummary_latex_table_colspec(app, status, warning): app.builder.build_all() diff --git a/tests/test_ext_inheritance_diagram.py b/tests/test_ext_inheritance_diagram.py index 3125f2c6e..2ecb3f4e4 100644 --- a/tests/test_ext_inheritance_diagram.py +++ b/tests/test_ext_inheritance_diagram.py @@ -109,7 +109,7 @@ def test_inheritance_diagram(app, status, warning): ('dummy.test.B', 'dummy.test.B', [], None) ] - # inheritance diagram with 2 top classes and specifiying the entire module + # inheritance diagram with 2 top classes and specifying the entire module # rendering should be # # A diff --git a/tests/test_intl.py b/tests/test_intl.py index d9701343e..8cb978913 100644 --- a/tests/test_intl.py +++ b/tests/test_intl.py @@ -956,9 +956,9 @@ def test_xml_role_xref(app): 'glossary_terms#term-Some-term']) assert_elem( para2[1], - ['LINK TO', 'SAME TYPE LINKS', 'AND', - "I18N ROCK'N ROLE XREF", '.'], - ['same-type-links', 'i18n-role-xref']) + ['LINK TO', 'LABEL', 'AND', + 'SAME TYPE LINKS', 'AND', 'SAME TYPE LINKS', '.'], + ['i18n-role-xref', 'same-type-links', 'same-type-links']) assert_elem( para2[2], ['LINK TO', 'I18N WITH GLOSSARY TERMS', 'AND', 'CONTENTS', '.'], diff --git a/tests/test_util_inspect.py b/tests/test_util_inspect.py index 6a14dc1ac..c21eaaa16 100644 --- a/tests/test_util_inspect.py +++ b/tests/test_util_inspect.py @@ -130,7 +130,7 @@ def test_signature_partialmethod(): def test_signature_annotations(): from typing_test_data import (f0, f1, f2, f3, f4, f5, f6, f7, f8, f9, f10, - f11, f12, f13, f14, f15, f16, f17, f18, f19, f20, Node) + f11, f12, f13, f14, f15, f16, f17, f18, f19, f20, f21, Node) # Class annotations sig = inspect.signature(f0) @@ -214,6 +214,10 @@ def test_signature_annotations(): sig = inspect.signature(f19) assert stringify_signature(sig) == '(*args: int, **kwargs: str)' + # default value is inspect.Signature.empty + sig = inspect.signature(f21) + assert stringify_signature(sig) == "(arg1='whatever', arg2)" + # type hints by string sig = inspect.signature(Node.children) if (3, 5, 0) <= sys.version_info < (3, 5, 3): diff --git a/tests/test_util_typing.py b/tests/test_util_typing.py index 41d2a19c2..932fdbfc0 100644 --- a/tests/test_util_typing.py +++ b/tests/test_util_typing.py @@ -10,7 +10,9 @@ import sys from numbers import Integral -from typing import Any, Dict, List, TypeVar, Union, Callable, Tuple, Optional, Generic +from typing import ( + Any, Dict, Generator, List, TypeVar, Union, Callable, Tuple, Optional, Generic +) import pytest @@ -48,6 +50,7 @@ def test_stringify_type_hints_containers(): assert stringify(Tuple[str, ...]) == "Tuple[str, ...]" assert stringify(List[Dict[str, Tuple]]) == "List[Dict[str, Tuple]]" assert stringify(MyList[Tuple[int, int]]) == "test_util_typing.MyList[Tuple[int, int]]" + assert stringify(Generator[None, None, None]) == "Generator[None, None, None]" @pytest.mark.skipif(sys.version_info < (3, 9), reason='python 3.9+ is required.') diff --git a/tests/typing_test_data.py b/tests/typing_test_data.py index 70c3144a0..6c0357911 100644 --- a/tests/typing_test_data.py +++ b/tests/typing_test_data.py @@ -1,3 +1,4 @@ +from inspect import Signature from numbers import Integral from typing import Any, Dict, List, TypeVar, Union, Callable, Tuple, Optional @@ -100,6 +101,9 @@ def f20() -> Optional[Union[int, str]]: pass +def f21(arg1='whatever', arg2=Signature.empty): + pass + class Node: def __init__(self, parent: Optional['Node']) -> None: diff --git a/utils/doclinter.py b/utils/doclinter.py index 52b2fe892..bb11decaf 100644 --- a/utils/doclinter.py +++ b/utils/doclinter.py @@ -50,6 +50,9 @@ def lint(path: str) -> int: if re.match(r'^\s*\.\. ', line): # ignore directives and hyperlink targets pass + elif re.match(r'^\s*``[^`]+``$', line): + # ignore a very long literal string + pass else: print('%s:%d: the line is too long (%d > %d).' % (path, i + 1, len(line), MAX_LINE_LENGTH)) diff --git a/utils/pylintrc b/utils/pylintrc index cf71db595..c2b338b79 100644 --- a/utils/pylintrc +++ b/utils/pylintrc @@ -1,6 +1,6 @@ # lint Python modules using external checkers. # -# This is the main checker controling the other ones and the reports +# This is the main checker controlling the other ones and the reports # generation. It is itself both a raw checker and an astng checker in order # to: # * handle message activation / deactivation at the module level @@ -71,7 +71,7 @@ reports=yes # Python expression which should return a note less than 10 (10 is the highest # note).You have access to the variables errors warning, statement which -# respectivly contain the number of errors / warnings messages and the total +# respectively contain the number of errors / warnings messages and the total # number of statements analyzed. This is used by the global evaluation report # (R0004). evaluation=10.0 - ((float(5 * error + warning + refactor + convention) / statement) * 10)