diff options
72 files changed, 926 insertions, 308 deletions
diff --git a/.github/workflows/main.yml b/.github/workflows/main.yml index f32468576..62a3d1139 100644 --- a/.github/workflows/main.yml +++ b/.github/workflows/main.yml @@ -23,9 +23,9 @@ jobs: python: 3.9 docutils: du17 coverage: "--cov ./ --cov-append --cov-config setup.cfg" - # - name: py310-dev - # python: 3.10-dev - # docutils: du16 + - name: py310-dev + python: 3.10-dev + docutils: du17 env: PYTEST_ADDOPTS: ${{ matrix.coverage }} @@ -1,3 +1,50 @@ +Release 4.1.0 (in development) +============================== + +Dependencies +------------ + +* Support jinja2-3.0 + +Incompatible changes +-------------------- + +Deprecated +---------- + +* ``sphinx.util.docstrings.extract_metadata()`` + +Features added +-------------- + +* #8107: autodoc: Add ``class-doc-from`` option to :rst:dir:`autoclass` + directive to control the content of the specific class like + :confval:`autoclass_content` +* #8588: autodoc: :confval:`autodoc_type_aliases` now supports dotted name. It + allows you to define an alias for a class with module name like + ``foo.bar.BazClass`` +* #9175: autodoc: Special member is not documented in the module +* #3257: autosummary: Support instance attributes for classes +* #9129: html search: Show search summaries when html_copy_source = False +* #9120: html theme: Eliminate prompt characters of code-block from copyable + text +* #9176: i18n: Emit a debug message if message catalog file not found under + :confval:`locale_dirs` +* #9097: Optimize the paralell build +* #9131: Add :confval:`nitpick_ignore_regex` to ignore nitpicky warnings using + regular expressions + + +Bugs fixed +---------- + +* #8872: autodoc: stacked singledispatches are wrongly rendered +* #8597: autodoc: a docsting having metadata only should be treated as + undocumented + +Testing +-------- + Release 4.0.2 (in development) ============================== @@ -12,7 +12,6 @@ interesting examples. Documentation using the alabaster theme --------------------------------------- -* `AIOHTTP <https://docs.aiohttp.org/>`__ * `Alabaster <https://alabaster.readthedocs.io/>`__ * `Blinker <https://pythonhosted.org/blinker/>`__ * `Calibre <https://manual.calibre-ebook.com/>`__ @@ -311,6 +310,7 @@ Documentation using sphinx_bootstrap_theme Documentation using a custom theme or integrated in a website ------------------------------------------------------------- +* `AIOHTTP <https://docs.aiohttp.org/>`__ * `Apache Cassandra <https://cassandra.apache.org/doc/>`__ * `Astropy <http://docs.astropy.org/>`__ * `Bokeh <https://bokeh.pydata.org/>`__ diff --git a/README.rst b/README.rst index 13bbab99d..c60e58780 100644 --- a/README.rst +++ b/README.rst @@ -10,10 +10,6 @@ :target: http://www.sphinx-doc.org/ :alt: Documentation Status -.. image:: https://travis-ci.org/sphinx-doc/sphinx.svg?branch=master - :target: https://travis-ci.org/sphinx-doc/sphinx - :alt: Build Status (Travis CI) - .. image:: https://ci.appveyor.com/api/projects/status/github/sphinx-doc/sphinx?branch=master&svg=true :target: https://ci.appveyor.com/project/sphinxdoc/sphinx :alt: Build Status (AppVeyor) diff --git a/doc/extdev/deprecated.rst b/doc/extdev/deprecated.rst index 9e17b9fb4..952258a2b 100644 --- a/doc/extdev/deprecated.rst +++ b/doc/extdev/deprecated.rst @@ -22,6 +22,11 @@ The following is a list of deprecated interfaces. - (will be) Removed - Alternatives + * - ``sphinx.util.docstrings.extract_metadata()`` + - 4.1 + - 6.0 + - ``sphinx.util.docstrings.separate_metadata()`` + * - ``favicon`` variable in HTML templates - 4.0 - TBD @@ -35,12 +40,12 @@ The following is a list of deprecated interfaces. * - ``sphinx.directives.patches.ListTable`` - 4.0 - 6.0 - - ``docutils.parsers.rst.diretives.tables.ListSVTable`` + - ``docutils.parsers.rst.directives.tables.ListSVTable`` * - ``sphinx.directives.patches.RSTTable`` - 4.0 - 6.0 - - ``docutils.parsers.rst.diretives.tables.RSTTable`` + - ``docutils.parsers.rst.directives.tables.RSTTable`` * - ``sphinx.ext.autodoc.directive.DocumenterBridge.filename_set`` - 4.0 @@ -80,7 +85,7 @@ The following is a list of deprecated interfaces. * - ``sphinx.util.smartypants`` - 4.0 - 6.0 - - ``docutils.utils.smartyquotes`` + - ``docutils.utils.smartquotes`` * - ``sphinx.util.typing.DirectiveOption`` - 4.0 diff --git a/doc/usage/configuration.rst b/doc/usage/configuration.rst index 7b7a0a1d4..0b2bd4e3e 100644 --- a/doc/usage/configuration.rst +++ b/doc/usage/configuration.rst @@ -419,6 +419,20 @@ General configuration .. versionadded:: 1.1 +.. confval:: nitpick_ignore_regex + + An extended version of :confval:`nitpick_ignore`, which instead interprets + the ``type`` and ``target`` strings as regular expressions. Note, that the + regular expression must match the whole string (as if the ``^`` and ``$`` + markers were inserted). + + For example, ``(r'py:.*', r'foo.*bar\.B.*')`` will ignore nitpicky warnings + for all python entities that start with ``'foo'`` and have ``'bar.B'`` in + them, such as ``('py:const', 'foo_package.bar.BAZ_VALUE')`` or + ``('py:class', 'food.bar.Barman')``. + + .. versionadded:: 4.1 + .. confval:: numfig If true, figures, tables and code-blocks are automatically numbered if they @@ -780,6 +794,10 @@ documentation on :ref:`intl` for details. The default is ``['locales']``. + .. note:: The :option:`-v option for sphinx-build command <sphinx-build -v>` + is useful to check the locale_dirs config works as expected. It + emits debug messages if message catalog directory not found. + .. versionchanged:: 1.5 Use ``locales`` directory as a default value diff --git a/doc/usage/extensions/autodoc.rst b/doc/usage/extensions/autodoc.rst index da0ff7c99..13a2b3010 100644 --- a/doc/usage/extensions/autodoc.rst +++ b/doc/usage/extensions/autodoc.rst @@ -343,6 +343,10 @@ inserting them into the page source under a suitable :rst:dir:`py:module`, .. autoclass:: module.name::Noodle + * :rst:dir:`autoclass` also recognizes the ``class-doc-from`` option that + can be used to override the global value of :confval:`autoclass_content`. + + .. versionadded:: 4.1 .. rst:directive:: autofunction autodecorator @@ -507,7 +511,7 @@ There are also config values that you can set: The supported options are ``'members'``, ``'member-order'``, ``'undoc-members'``, ``'private-members'``, ``'special-members'``, ``'inherited-members'``, ``'show-inheritance'``, ``'ignore-module-all'``, - ``'imported-members'`` and ``'exclude-members'``. + ``'imported-members'``, ``'exclude-members'`` and ``'class-doc-from'``. .. versionadded:: 1.8 @@ -517,6 +521,9 @@ There are also config values that you can set: .. versionchanged:: 2.1 Added ``'imported-members'``. + .. versionchanged:: 4.1 + Added ``'class-doc-from'``. + .. confval:: autodoc_docstring_signature Functions imported from C modules cannot be introspected, and therefore the diff --git a/doc/usage/markdown.rst b/doc/usage/markdown.rst index 3ab88ddad..ac24f8d62 100644 --- a/doc/usage/markdown.rst +++ b/doc/usage/markdown.rst @@ -8,13 +8,13 @@ Markdown `Markdown`__ is a lightweight markup language with a simplistic plain text formatting syntax. It exists in many syntactically different *flavors*. To -support Markdown-based documentation, Sphinx can use `recommonmark`__. -recommonmark is a Docutils bridge to `CommonMark-py`__, a Python package for +support Markdown-based documentation, Sphinx can use `MyST-Parser`__. +MyST-Parser is a Docutils bridge to `markdown-it-py`__, a Python package for parsing the `CommonMark`__ Markdown flavor. __ https://daringfireball.net/projects/markdown/ -__ https://recommonmark.readthedocs.io/en/latest/index.html -__ https://github.com/rtfd/CommonMark-py +__ https://myst-parser.readthedocs.io/en/latest/ +__ https://github.com/executablebooks/markdown-it-py __ https://commonmark.org/ Configuration @@ -22,23 +22,17 @@ Configuration To configure your Sphinx project for Markdown support, proceed as follows: -#. Install the Markdown parser *recommonmark*:: +#. Install the Markdown parser *MyST-Parser*:: - pip install --upgrade recommonmark + pip install --upgrade myst-parser - .. note:: - - The configuration as explained here requires recommonmark version - 0.5.0 or later. - -#. Add *recommonmark* to the +#. Add *myst_parser* to the :confval:`list of configured extensions <extensions>`:: - extensions = ['recommonmark'] + extensions = ['myst_parser'] - .. versionchanged:: 1.8 - Version 1.8 deprecates and version 3.0 removes the ``source_parsers`` - configuration variable that was used by older *recommonmark* versions. + .. note:: + MyST-Parser requires Sphinx 2.1 or newer. #. If you want to use Markdown files with extensions other than ``.md``, adjust the :confval:`source_suffix` variable. The following example configures @@ -51,8 +45,8 @@ To configure your Sphinx project for Markdown support, proceed as follows: '.md': 'markdown', } -#. You can further configure *recommonmark* to allow custom syntax that - standard *CommonMark* doesn't support. Read more in the `recommonmark +#. You can further configure *MyST-Parser* to allow custom syntax that + standard *CommonMark* doesn't support. Read more in the `MyST-Parser documentation`__. -__ https://recommonmark.readthedocs.io/en/latest/auto_structify.html +__ https://myst-parser.readthedocs.io/en/latest/using/syntax-optional.html diff --git a/doc/usage/restructuredtext/domains.rst b/doc/usage/restructuredtext/domains.rst index 65a32b6c8..dfd347327 100644 --- a/doc/usage/restructuredtext/domains.rst +++ b/doc/usage/restructuredtext/domains.rst @@ -1680,6 +1680,9 @@ There is a set of directives allowing documenting command-line programs: then ``:option:`rm -r``` would refer to the first option, while ``:option:`svn -r``` would refer to the second one. + If ``None`` is passed to the argument, the directive will reset the + current program name. + The program name may contain spaces (in case you want to document subcommands like ``svn add`` and ``svn commit`` separately). diff --git a/package-lock.json b/package-lock.json index 087afcf3e..6fd89f9db 100644 --- a/package-lock.json +++ b/package-lock.json @@ -704,9 +704,9 @@ } }, "lodash": { - "version": "4.17.19", - "resolved": "https://registry.npmjs.org/lodash/-/lodash-4.17.19.tgz", - "integrity": "sha512-JNvd8XER9GQX0v2qJgsaN/mzFCNA5BRe/j8JN9d+tWyGLSodKQHKFicdwNYzWwI3wjRnaKPsGj1XkBjx/F96DQ==", + "version": "4.17.21", + "resolved": "https://registry.npmjs.org/lodash/-/lodash-4.17.21.tgz", + "integrity": "sha512-v2kDEe57lecTulaDIuNTPy3Ry4gLGJ6Z1O3vE1krgXZNrsQ+LFTGHVxVjcXPs17LhbZVGedAJv8XZ1tvj5FvSg==", "dev": true }, "log4js": { @@ -21,8 +21,7 @@ install_requires = [ 'sphinxcontrib-htmlhelp', 'sphinxcontrib-serializinghtml', 'sphinxcontrib-qthelp', - 'Jinja2>=2.3,<3.0', - 'MarkupSafe<2.0', + 'Jinja2>=2.3', 'Pygments>=2.0', 'docutils>=0.14,<0.18', 'snowballstemmer>=1.1', diff --git a/sphinx/__init__.py b/sphinx/__init__.py index 016e84c27..a1de1402b 100644 --- a/sphinx/__init__.py +++ b/sphinx/__init__.py @@ -27,8 +27,8 @@ if 'PYTHONWARNINGS' not in os.environ: warnings.filterwarnings('ignore', "'U' mode is deprecated", DeprecationWarning, module='docutils.io') -__version__ = '4.0.2+' -__released__ = '4.0.2' # used when Sphinx builds its own docs +__version__ = '4.1.0' +__released__ = '4.1.0' # used when Sphinx builds its own docs #: Version info for better programmatic use. #: @@ -38,7 +38,7 @@ __released__ = '4.0.2' # used when Sphinx builds its own docs #: #: .. versionadded:: 1.2 #: Before version 1.2, check the string ``sphinx.__version__``. -version_info = (4, 0, 2, 'beta', 0) +version_info = (4, 1, 0, 'final', 0) package_dir = path.abspath(path.dirname(__file__)) diff --git a/sphinx/application.py b/sphinx/application.py index 4735beffd..afbb0f981 100644 --- a/sphinx/application.py +++ b/sphinx/application.py @@ -141,9 +141,9 @@ class Sphinx: self.phase = BuildPhase.INITIALIZATION self.verbosity = verbosity self.extensions: Dict[str, Extension] = {} - self.builder: Builder = None - self.env: BuildEnvironment = None - self.project: Project = None + self.builder: Optional[Builder] = None + self.env: Optional[BuildEnvironment] = None + self.project: Optional[Project] = None self.registry = SphinxComponentRegistry() self.html_themes: Dict[str, str] = {} @@ -174,7 +174,7 @@ class Sphinx: if status is None: self._status: IO = StringIO() - self.quiet = True + self.quiet: bool = True else: self._status = status self.quiet = False diff --git a/sphinx/builders/__init__.py b/sphinx/builders/__init__.py index bedc65b61..722f9a280 100644 --- a/sphinx/builders/__init__.py +++ b/sphinx/builders/__init__.py @@ -11,7 +11,8 @@ import pickle import time from os import path -from typing import TYPE_CHECKING, Any, Dict, Iterable, List, Sequence, Set, Tuple, Type, Union +from typing import (TYPE_CHECKING, Any, Dict, Iterable, List, Optional, Sequence, Set, Tuple, + Type, Union) from docutils import nodes from docutils.nodes import Node @@ -88,7 +89,7 @@ class Builder: ensuredir(self.doctreedir) self.app: Sphinx = app - self.env: BuildEnvironment = None + self.env: Optional[BuildEnvironment] = None self.events: EventManager = app.events self.config: Config = app.config self.tags: Tags = app.tags @@ -225,7 +226,7 @@ class Builder: self.compile_catalogs(set(repo.catalogs), message) def compile_specific_catalogs(self, specified_files: List[str]) -> None: - def to_domain(fpath: str) -> str: + def to_domain(fpath: str) -> Optional[str]: docname = self.env.path2doc(path.abspath(fpath)) if docname: return docname_to_domain(docname, self.config.gettext_compact) diff --git a/sphinx/builders/linkcheck.py b/sphinx/builders/linkcheck.py index 05e12c173..a46b80c08 100644 --- a/sphinx/builders/linkcheck.py +++ b/sphinx/builders/linkcheck.py @@ -129,7 +129,7 @@ class CheckExternalLinksBuilder(DummyBuilder): # create queues and worker threads self._wqueue: PriorityQueue[CheckRequestType] = PriorityQueue() - self._rqueue: Queue = Queue() + self._rqueue: Queue[CheckResult] = Queue() @property def anchors_ignore(self) -> List[Pattern]: @@ -228,43 +228,39 @@ class CheckExternalLinksBuilder(DummyBuilder): ) return self._wqueue - def process_result(self, result: Tuple[str, str, int, str, str, int]) -> None: - uri, docname, lineno, status, info, code = result + def process_result(self, result: CheckResult) -> None: + filename = self.env.doc2path(result.docname, None) - filename = self.env.doc2path(docname, None) - linkstat = dict(filename=filename, lineno=lineno, - status=status, code=code, uri=uri, - info=info) - if status == 'unchecked': - self.write_linkstat(linkstat) + linkstat = dict(filename=filename, lineno=result.lineno, + status=result.status, code=result.code, uri=result.uri, + info=result.message) + self.write_linkstat(linkstat) + + if result.status == 'unchecked': return - if status == 'working' and info == 'old': - self.write_linkstat(linkstat) + if result.status == 'working' and result.message == 'old': return - if lineno: - logger.info('(%16s: line %4d) ', docname, lineno, nonl=True) - if status == 'ignored': - if info: - logger.info(darkgray('-ignored- ') + uri + ': ' + info) + if result.lineno: + logger.info('(%16s: line %4d) ', result.docname, result.lineno, nonl=True) + if result.status == 'ignored': + if result.message: + logger.info(darkgray('-ignored- ') + result.uri + ': ' + result.message) else: - logger.info(darkgray('-ignored- ') + uri) - self.write_linkstat(linkstat) - elif status == 'local': - logger.info(darkgray('-local- ') + uri) - self.write_entry('local', docname, filename, lineno, uri) - self.write_linkstat(linkstat) - elif status == 'working': - logger.info(darkgreen('ok ') + uri + info) - self.write_linkstat(linkstat) - elif status == 'broken': + logger.info(darkgray('-ignored- ') + result.uri) + elif result.status == 'local': + logger.info(darkgray('-local- ') + result.uri) + self.write_entry('local', result.docname, filename, result.lineno, result.uri) + elif result.status == 'working': + logger.info(darkgreen('ok ') + result.uri + result.message) + elif result.status == 'broken': if self.app.quiet or self.app.warningiserror: - logger.warning(__('broken link: %s (%s)'), uri, info, - location=(filename, lineno)) + logger.warning(__('broken link: %s (%s)'), result.uri, result.message, + location=(filename, result.lineno)) else: - logger.info(red('broken ') + uri + red(' - ' + info)) - self.write_entry('broken', docname, filename, lineno, uri + ': ' + info) - self.write_linkstat(linkstat) - elif status == 'redirected': + logger.info(red('broken ') + result.uri + red(' - ' + result.message)) + self.write_entry('broken', result.docname, filename, result.lineno, + result.uri + ': ' + result.message) + elif result.status == 'redirected': try: text, color = { 301: ('permanently', purple), @@ -272,16 +268,16 @@ class CheckExternalLinksBuilder(DummyBuilder): 303: ('with See Other', purple), 307: ('temporarily', turquoise), 308: ('permanently', purple), - }[code] + }[result.code] except KeyError: text, color = ('with unknown code', purple) linkstat['text'] = text - logger.info(color('redirect ') + uri + color(' - ' + text + ' to ' + info)) - self.write_entry('redirected ' + text, docname, filename, - lineno, uri + ' to ' + info) - self.write_linkstat(linkstat) + logger.info(color('redirect ') + result.uri + + color(' - ' + text + ' to ' + result.message)) + self.write_entry('redirected ' + text, result.docname, filename, + result.lineno, result.uri + ' to ' + result.message) else: - raise ValueError("Unknown status %s." % status) + raise ValueError("Unknown status %s." % result.status) def write_entry(self, what: str, docname: str, filename: str, line: int, uri: str) -> None: @@ -576,7 +572,7 @@ class HyperlinkAvailabilityCheckWorker(Thread): if status == 'rate-limited': logger.info(darkgray('-rate limited- ') + uri + darkgray(' | sleeping...')) else: - self.rqueue.put((uri, docname, lineno, status, info, code)) + self.rqueue.put(CheckResult(uri, docname, lineno, status, info, code)) self.wqueue.task_done() def limit_rate(self, response: Response) -> Optional[float]: diff --git a/sphinx/config.py b/sphinx/config.py index 418ecf4fb..1ba98d007 100644 --- a/sphinx/config.py +++ b/sphinx/config.py @@ -131,6 +131,7 @@ class Config: 'manpages_url': (None, 'env', []), 'nitpicky': (False, None, []), 'nitpick_ignore': ([], None, []), + 'nitpick_ignore_regex': ([], None, []), 'numfig': (False, 'env', []), 'numfig_secnum_depth': (1, 'env', []), 'numfig_format': ({}, 'env', []), # will be initialized in init_numfig_format() @@ -309,7 +310,7 @@ class Config: self.__dict__.update(state) -def eval_config_file(filename: str, tags: Tags) -> Dict[str, Any]: +def eval_config_file(filename: str, tags: Optional[Tags]) -> Dict[str, Any]: """Evaluate a config file.""" namespace: Dict[str, Any] = {} namespace['__file__'] = filename diff --git a/sphinx/domains/__init__.py b/sphinx/domains/__init__.py index 8f3e6a955..dbfad258c 100644 --- a/sphinx/domains/__init__.py +++ b/sphinx/domains/__init__.py @@ -11,8 +11,8 @@ import copy from abc import ABC, abstractmethod -from typing import (TYPE_CHECKING, Any, Callable, Dict, Iterable, List, NamedTuple, Tuple, - Type, Union, cast) +from typing import (TYPE_CHECKING, Any, Callable, Dict, Iterable, List, NamedTuple, Optional, + Tuple, Type, Union, cast) from docutils import nodes from docutils.nodes import Element, Node, system_message @@ -196,7 +196,7 @@ class Domain: #: data value for a fresh environment initial_data: Dict = {} #: data value - data: Dict = None + data: Dict #: data version, bump this when the format of `self.data` changes data_version = 0 @@ -251,7 +251,7 @@ class Domain: for role in objtype.roles: self._role2type.setdefault(role, []).append(name) - def role(self, name: str) -> RoleFunction: + def role(self, name: str) -> Optional[RoleFunction]: """Return a role adapter function that always gives the registered role its full name ('domain:name') as the first argument. """ @@ -269,7 +269,7 @@ class Domain: self._role_cache[name] = role_adapter return role_adapter - def directive(self, name: str) -> Callable: + def directive(self, name: str) -> Optional[Callable]: """Return a directive adapter class that always gives the registered directive its full name ('domain:name') as ``self.name``. """ @@ -318,7 +318,7 @@ class Domain: def resolve_xref(self, env: "BuildEnvironment", fromdocname: str, builder: "Builder", typ: str, target: str, node: pending_xref, contnode: Element - ) -> Element: + ) -> Optional[Element]: """Resolve the pending_xref *node* with the given *typ* and *target*. This method should return a new node, to replace the xref node, @@ -393,11 +393,11 @@ class Domain: return type.lname return _('%s %s') % (self.label, type.lname) - def get_enumerable_node_type(self, node: Node) -> str: + def get_enumerable_node_type(self, node: Node) -> Optional[str]: """Get type of enumerable nodes (experimental).""" enum_node_type, _ = self.enumerable_nodes.get(node.__class__, (None, None)) return enum_node_type - def get_full_qualified_name(self, node: Element) -> str: + def get_full_qualified_name(self, node: Element) -> Optional[str]: """Return full qualified name for given node.""" return None diff --git a/sphinx/domains/c.py b/sphinx/domains/c.py index b0711f68e..ed908c875 100644 --- a/sphinx/domains/c.py +++ b/sphinx/domains/c.py @@ -9,7 +9,8 @@ """ import re -from typing import Any, Callable, Dict, Generator, Iterator, List, Tuple, TypeVar, Union, cast +from typing import (Any, Callable, Dict, Generator, Iterator, List, Optional, Tuple, TypeVar, + Union, cast) from docutils import nodes from docutils.nodes import Element, Node, TextElement, system_message @@ -3807,7 +3808,7 @@ class CDomain(Domain): def _resolve_xref_inner(self, env: BuildEnvironment, fromdocname: str, builder: Builder, typ: str, target: str, node: pending_xref, - contnode: Element) -> Tuple[Element, str]: + contnode: Element) -> Tuple[Optional[Element], Optional[str]]: parser = DefinitionParser(target, location=node, config=env.config) try: name = parser.parse_xref_object() @@ -3844,7 +3845,7 @@ class CDomain(Domain): def resolve_xref(self, env: BuildEnvironment, fromdocname: str, builder: Builder, typ: str, target: str, node: pending_xref, - contnode: Element) -> Element: + contnode: Element) -> Optional[Element]: return self._resolve_xref_inner(env, fromdocname, builder, typ, target, node, contnode)[0] diff --git a/sphinx/domains/citation.py b/sphinx/domains/citation.py index 772d486af..0e859acb4 100644 --- a/sphinx/domains/citation.py +++ b/sphinx/domains/citation.py @@ -8,7 +8,7 @@ :license: BSD, see LICENSE for details. """ -from typing import TYPE_CHECKING, Any, Dict, List, Set, Tuple, cast +from typing import TYPE_CHECKING, Any, Dict, List, Optional, Set, Tuple, cast from docutils import nodes from docutils.nodes import Element @@ -88,7 +88,7 @@ class CitationDomain(Domain): def resolve_xref(self, env: "BuildEnvironment", fromdocname: str, builder: "Builder", typ: str, target: str, node: pending_xref, contnode: Element - ) -> Element: + ) -> Optional[Element]: docname, labelid, lineno = self.citations.get(target, ('', '', 0)) if not docname: return None diff --git a/sphinx/domains/cpp.py b/sphinx/domains/cpp.py index d57c6e257..6c551b4a7 100644 --- a/sphinx/domains/cpp.py +++ b/sphinx/domains/cpp.py @@ -2292,6 +2292,10 @@ class ASTDeclarator(ASTBase): def name(self) -> ASTNestedName: raise NotImplementedError(repr(self)) + @name.setter + def name(self, name: ASTNestedName) -> None: + raise NotImplementedError(repr(self)) + @property def isPack(self) -> bool: raise NotImplementedError(repr(self)) @@ -2339,6 +2343,10 @@ class ASTDeclaratorNameParamQual(ASTDeclarator): def name(self) -> ASTNestedName: return self.declId + @name.setter + def name(self, name: ASTNestedName) -> None: + self.declId = name + @property def isPack(self) -> bool: return False @@ -2420,6 +2428,10 @@ class ASTDeclaratorNameBitField(ASTDeclarator): def name(self) -> ASTNestedName: return self.declId + @name.setter + def name(self, name: ASTNestedName) -> None: + self.declId = name + def get_param_id(self, version: int) -> str: # only the parameters (if any) return '' @@ -2466,6 +2478,10 @@ class ASTDeclaratorPtr(ASTDeclarator): def name(self) -> ASTNestedName: return self.next.name + @name.setter + def name(self, name: ASTNestedName) -> None: + self.next.name = name + @property def function_params(self) -> List[ASTFunctionParameter]: return self.next.function_params @@ -2565,6 +2581,10 @@ class ASTDeclaratorRef(ASTDeclarator): def name(self) -> ASTNestedName: return self.next.name + @name.setter + def name(self, name: ASTNestedName) -> None: + self.next.name = name + @property def isPack(self) -> bool: return True @@ -2629,6 +2649,10 @@ class ASTDeclaratorParamPack(ASTDeclarator): def name(self) -> ASTNestedName: return self.next.name + @name.setter + def name(self, name: ASTNestedName) -> None: + self.next.name = name + @property def function_params(self) -> List[ASTFunctionParameter]: return self.next.function_params @@ -2689,6 +2713,10 @@ class ASTDeclaratorMemPtr(ASTDeclarator): def name(self) -> ASTNestedName: return self.next.name + @name.setter + def name(self, name: ASTNestedName) -> None: + self.next.name = name + @property def function_params(self) -> List[ASTFunctionParameter]: return self.next.function_params @@ -2782,6 +2810,10 @@ class ASTDeclaratorParen(ASTDeclarator): def name(self) -> ASTNestedName: return self.inner.name + @name.setter + def name(self, name: ASTNestedName) -> None: + self.inner.name = name + @property def function_params(self) -> List[ASTFunctionParameter]: return self.inner.function_params @@ -2913,6 +2945,10 @@ class ASTType(ASTBase): def name(self) -> ASTNestedName: return self.decl.name + @name.setter + def name(self, name: ASTNestedName) -> None: + self.decl.name = name + @property def isPack(self) -> bool: return self.decl.isPack @@ -7558,7 +7594,7 @@ class CPPDomain(Domain): def _resolve_xref_inner(self, env: BuildEnvironment, fromdocname: str, builder: Builder, typ: str, target: str, node: pending_xref, - contnode: Element) -> Tuple[Element, str]: + contnode: Element) -> Tuple[Optional[Element], Optional[str]]: # add parens again for those that could be functions if typ == 'any' or typ == 'func': target += '()' @@ -7707,7 +7743,7 @@ class CPPDomain(Domain): def resolve_xref(self, env: BuildEnvironment, fromdocname: str, builder: Builder, typ: str, target: str, node: pending_xref, contnode: Element - ) -> Element: + ) -> Optional[Element]: return self._resolve_xref_inner(env, fromdocname, builder, typ, target, node, contnode)[0] diff --git a/sphinx/domains/javascript.py b/sphinx/domains/javascript.py index 92d0e1d61..8511a1d6a 100644 --- a/sphinx/domains/javascript.py +++ b/sphinx/domains/javascript.py @@ -8,7 +8,7 @@ :license: BSD, see LICENSE for details. """ -from typing import Any, Dict, Iterator, List, Tuple, cast +from typing import Any, Dict, Iterator, List, Optional, Tuple, cast from docutils import nodes from docutils.nodes import Element, Node @@ -413,7 +413,7 @@ class JavaScriptDomain(Domain): def resolve_xref(self, env: BuildEnvironment, fromdocname: str, builder: Builder, typ: str, target: str, node: pending_xref, contnode: Element - ) -> Element: + ) -> Optional[Element]: mod_name = node.get('js:module') prefix = node.get('js:object') searchorder = 1 if node.hasattr('refspecific') else 0 diff --git a/sphinx/domains/math.py b/sphinx/domains/math.py index 88db1ad0e..aa6f9422d 100644 --- a/sphinx/domains/math.py +++ b/sphinx/domains/math.py @@ -8,7 +8,7 @@ :license: BSD, see LICENSE for details. """ -from typing import TYPE_CHECKING, Any, Dict, Iterable, List, Tuple +from typing import TYPE_CHECKING, Any, Dict, Iterable, List, Optional, Tuple from docutils import nodes from docutils.nodes import Element, Node, make_id, system_message @@ -97,7 +97,7 @@ class MathDomain(Domain): def resolve_xref(self, env: BuildEnvironment, fromdocname: str, builder: "Builder", typ: str, target: str, node: pending_xref, contnode: Element - ) -> Element: + ) -> Optional[Element]: assert typ in ('eq', 'numref') docname, number = self.equations.get(target, (None, None)) if docname: diff --git a/sphinx/domains/python.py b/sphinx/domains/python.py index 069737bde..7d39d80ed 100644 --- a/sphinx/domains/python.py +++ b/sphinx/domains/python.py @@ -15,7 +15,7 @@ import sys import typing import warnings from inspect import Parameter -from typing import Any, Dict, Iterable, Iterator, List, NamedTuple, Tuple, Type, cast +from typing import Any, Dict, Iterable, Iterator, List, NamedTuple, Optional, Tuple, Type, cast from docutils import nodes from docutils.nodes import Element, Node @@ -1246,7 +1246,7 @@ class PythonDomain(Domain): def resolve_xref(self, env: BuildEnvironment, fromdocname: str, builder: Builder, type: str, target: str, node: pending_xref, contnode: Element - ) -> Element: + ) -> Optional[Element]: modname = node.get('py:module') clsname = node.get('py:class') searchmode = 1 if node.hasattr('refspecific') else 0 @@ -1344,7 +1344,7 @@ class PythonDomain(Domain): else: yield (refname, refname, obj.objtype, obj.docname, obj.node_id, 1) - def get_full_qualified_name(self, node: Element) -> str: + def get_full_qualified_name(self, node: Element) -> Optional[str]: modname = node.get('py:module') clsname = node.get('py:class') target = node.get('reftarget') diff --git a/sphinx/domains/rst.py b/sphinx/domains/rst.py index d048c2dfb..539a610bb 100644 --- a/sphinx/domains/rst.py +++ b/sphinx/domains/rst.py @@ -9,7 +9,7 @@ """ import re -from typing import Any, Dict, Iterator, List, Tuple, cast +from typing import Any, Dict, Iterator, List, Optional, Tuple, cast from docutils.nodes import Element from docutils.parsers.rst import directives @@ -247,7 +247,7 @@ class ReSTDomain(Domain): def resolve_xref(self, env: BuildEnvironment, fromdocname: str, builder: Builder, typ: str, target: str, node: pending_xref, contnode: Element - ) -> Element: + ) -> Optional[Element]: objtypes = self.objtypes_for_role(typ) for objtype in objtypes: todocname, node_id = self.objects.get((objtype, target), (None, None)) diff --git a/sphinx/domains/std.py b/sphinx/domains/std.py index 5e10646f0..7660f84c9 100644 --- a/sphinx/domains/std.py +++ b/sphinx/domains/std.py @@ -285,7 +285,7 @@ class OptionXRefRole(XRefRole): def split_term_classifiers(line: str) -> List[Optional[str]]: # split line into a term and classifiers. if no classifier, None is used.. - parts = re.split(' +: +', line) + [None] + parts: List[Optional[str]] = re.split(' +: +', line) + [None] return parts @@ -621,7 +621,7 @@ class StandardDomain(Domain): } # node_class -> (figtype, title_getter) - enumerable_nodes: Dict[Type[Node], Tuple[str, Callable]] = { + enumerable_nodes: Dict[Type[Node], Tuple[str, Optional[Callable]]] = { nodes.figure: ('figure', None), nodes.table: ('table', None), nodes.container: ('code-block', None), @@ -812,7 +812,8 @@ class StandardDomain(Domain): return newnode def resolve_xref(self, env: "BuildEnvironment", fromdocname: str, builder: "Builder", - typ: str, target: str, node: pending_xref, contnode: Element) -> Element: + typ: str, target: str, node: pending_xref, contnode: Element + ) -> Optional[Element]: if typ == 'ref': resolver = self._resolve_ref_xref elif typ == 'numref': @@ -832,7 +833,7 @@ class StandardDomain(Domain): def _resolve_ref_xref(self, env: "BuildEnvironment", fromdocname: str, builder: "Builder", typ: str, target: str, node: pending_xref, - contnode: Element) -> Element: + contnode: Element) -> Optional[Element]: if node['refexplicit']: # reference to anonymous label; the reference uses # the supplied link caption @@ -850,7 +851,7 @@ class StandardDomain(Domain): def _resolve_numref_xref(self, env: "BuildEnvironment", fromdocname: str, builder: "Builder", typ: str, target: str, - node: pending_xref, contnode: Element) -> Element: + node: pending_xref, contnode: Element) -> Optional[Element]: if target in self.labels: docname, labelid, figname = self.labels.get(target, ('', '', '')) else: @@ -913,7 +914,7 @@ class StandardDomain(Domain): def _resolve_keyword_xref(self, env: "BuildEnvironment", fromdocname: str, builder: "Builder", typ: str, target: str, - node: pending_xref, contnode: Element) -> Element: + node: pending_xref, contnode: Element) -> Optional[Element]: # keywords are oddballs: they are referenced by named labels docname, labelid, _ = self.labels.get(target, ('', '', '')) if not docname: @@ -923,7 +924,7 @@ class StandardDomain(Domain): def _resolve_doc_xref(self, env: "BuildEnvironment", fromdocname: str, builder: "Builder", typ: str, target: str, - node: pending_xref, contnode: Element) -> Element: + node: pending_xref, contnode: Element) -> Optional[Element]: # directly reference to document by source name; can be absolute or relative refdoc = node.get('refdoc', fromdocname) docname = docname_join(refdoc, node['reftarget']) @@ -940,7 +941,7 @@ class StandardDomain(Domain): def _resolve_option_xref(self, env: "BuildEnvironment", fromdocname: str, builder: "Builder", typ: str, target: str, - node: pending_xref, contnode: Element) -> Element: + node: pending_xref, contnode: Element) -> Optional[Element]: progname = node.get('std:program') target = target.strip() docname, labelid = self.progoptions.get((progname, target), ('', '')) @@ -977,7 +978,7 @@ class StandardDomain(Domain): def _resolve_obj_xref(self, env: "BuildEnvironment", fromdocname: str, builder: "Builder", typ: str, target: str, - node: pending_xref, contnode: Element) -> Element: + node: pending_xref, contnode: Element) -> Optional[Element]: objtypes = self.objtypes_for_role(typ) or [] for objtype in objtypes: if (objtype, target) in self.objects: @@ -1041,7 +1042,7 @@ class StandardDomain(Domain): def is_enumerable_node(self, node: Node) -> bool: return node.__class__ in self.enumerable_nodes - def get_numfig_title(self, node: Node) -> str: + def get_numfig_title(self, node: Node) -> Optional[str]: """Get the title of enumerable nodes to refer them using its title""" if self.is_enumerable_node(node): elem = cast(Element, node) @@ -1055,7 +1056,7 @@ class StandardDomain(Domain): return None - def get_enumerable_node_type(self, node: Node) -> str: + def get_enumerable_node_type(self, node: Node) -> Optional[str]: """Get type of enumerable nodes.""" def has_child(node: Element, cls: Type) -> bool: return any(isinstance(child, cls) for child in node) @@ -1094,7 +1095,7 @@ class StandardDomain(Domain): # Maybe it is defined in orphaned document. raise ValueError from exc - def get_full_qualified_name(self, node: Element) -> str: + def get_full_qualified_name(self, node: Element) -> Optional[str]: if node.get('reftype') == 'option': progname = node.get('std:program') command = ws_re.split(node.get('reftarget')) @@ -1109,7 +1110,8 @@ class StandardDomain(Domain): return None -def warn_missing_reference(app: "Sphinx", domain: Domain, node: pending_xref) -> bool: +def warn_missing_reference(app: "Sphinx", domain: Domain, node: pending_xref + ) -> Optional[bool]: if (domain and domain.name != 'std') or node['reftype'] != 'ref': return None else: diff --git a/sphinx/environment/__init__.py b/sphinx/environment/__init__.py index 0483dcdf9..2914a78af 100644 --- a/sphinx/environment/__init__.py +++ b/sphinx/environment/__init__.py @@ -14,8 +14,8 @@ from collections import defaultdict from copy import copy from datetime import datetime from os import path -from typing import (TYPE_CHECKING, Any, Callable, Dict, Generator, Iterator, List, Set, Tuple, - Union) +from typing import (TYPE_CHECKING, Any, Callable, Dict, Generator, Iterator, List, Optional, + Set, Tuple, Union) from docutils import nodes from docutils.nodes import Node @@ -87,7 +87,7 @@ class BuildEnvironment: transformations to resolve links to them. """ - domains: Dict[str, Domain] = None + domains: Dict[str, Domain] # --------- ENVIRONMENT INITIALIZATION ------------------------------------- @@ -266,7 +266,7 @@ class BuildEnvironment: raise an exception if the user tries to use an environment with an incompatible versioning method. """ - condition: Union[bool, Callable] = None + condition: Union[bool, Callable] if callable(method): condition = method else: @@ -309,7 +309,7 @@ class BuildEnvironment: domain.merge_domaindata(docnames, other.domaindata[domainname]) self.events.emit('env-merge-info', self, docnames, other) - def path2doc(self, filename: str) -> str: + def path2doc(self, filename: str) -> Optional[str]: """Return the docname for the filename if the file is document. *filename* should be absolute or relative to the source directory. @@ -577,7 +577,7 @@ class BuildEnvironment: # allow custom references to be resolved self.events.emit('doctree-resolved', doctree, docname) - def collect_relations(self) -> Dict[str, List[str]]: + def collect_relations(self) -> Dict[str, List[Optional[str]]]: traversed = set() def traverse_toctree(parent: str, docname: str) -> Iterator[Tuple[str, str]]: diff --git a/sphinx/environment/adapters/toctree.py b/sphinx/environment/adapters/toctree.py index 3ee2f9ff0..a62e951d7 100644 --- a/sphinx/environment/adapters/toctree.py +++ b/sphinx/environment/adapters/toctree.py @@ -8,7 +8,7 @@ :license: BSD, see LICENSE for details. """ -from typing import TYPE_CHECKING, Any, Iterable, List, cast +from typing import TYPE_CHECKING, Any, Iterable, List, Optional, cast from docutils import nodes from docutils.nodes import Element, Node @@ -48,7 +48,7 @@ class TocTree: def resolve(self, docname: str, builder: "Builder", toctree: addnodes.toctree, prune: bool = True, maxdepth: int = 0, titles_only: bool = False, - collapse: bool = False, includehidden: bool = False) -> Element: + collapse: bool = False, includehidden: bool = False) -> Optional[Element]: """Resolve a *toctree* node into individual bullet lists with titles as items, returning None (if no containing titles are found) or a new node. @@ -313,7 +313,7 @@ class TocTree: return toc def get_toctree_for(self, docname: str, builder: "Builder", collapse: bool, - **kwargs: Any) -> Element: + **kwargs: Any) -> Optional[Element]: """Return the global TOC nodetree.""" doctree = self.env.get_doctree(self.env.config.root_doc) toctrees: List[Element] = [] diff --git a/sphinx/environment/collectors/__init__.py b/sphinx/environment/collectors/__init__.py index e27091018..f4020431f 100644 --- a/sphinx/environment/collectors/__init__.py +++ b/sphinx/environment/collectors/__init__.py @@ -8,7 +8,7 @@ :license: BSD, see LICENSE for details. """ -from typing import TYPE_CHECKING, Dict, List, Set +from typing import TYPE_CHECKING, Dict, List, Optional, Set from docutils import nodes @@ -27,7 +27,7 @@ class EnvironmentCollector: entries and toctrees, etc. """ - listener_ids: Dict[str, int] = None + listener_ids: Optional[Dict[str, int]] = None def enable(self, app: "Sphinx") -> None: assert self.listener_ids is None diff --git a/sphinx/ext/autodoc/__init__.py b/sphinx/ext/autodoc/__init__.py index de94ff1c5..95aba45d8 100644 --- a/sphinx/ext/autodoc/__init__.py +++ b/sphinx/ext/autodoc/__init__.py @@ -30,7 +30,7 @@ from sphinx.ext.autodoc.mock import ismock, mock, undecorate from sphinx.locale import _, __ from sphinx.pycode import ModuleAnalyzer, PycodeError from sphinx.util import inspect, logging -from sphinx.util.docstrings import extract_metadata, prepare_docstring +from sphinx.util.docstrings import prepare_docstring, separate_metadata from sphinx.util.inspect import (evaluate_signature, getdoc, object_description, safe_getattr, stringify_signature) from sphinx.util.typing import OptionSpec, get_type_hints, restify @@ -129,6 +129,14 @@ def member_order_option(arg: Any) -> Optional[str]: raise ValueError(__('invalid value for member-order option: %s') % arg) +def class_doc_from_option(arg: Any) -> Optional[str]: + """Used to convert the :class-doc-from: option to autoclass directives.""" + if arg in ('both', 'class', 'init'): + return arg + else: + raise ValueError(__('invalid value for class-doc-from option: %s') % arg) + + SUPPRESS = object() @@ -381,8 +389,8 @@ class Documenter: # functions can contain a signature which is then used instead of # an autogenerated one try: - explicit_modname, path, base, args, retann = \ - py_ext_sig_re.match(self.name).groups() + matched = py_ext_sig_re.match(self.name) + explicit_modname, path, base, args, retann = matched.groups() except AttributeError: logger.warning(__('invalid signature for auto%s (%r)') % (self.objtype, self.name), type='autodoc') @@ -404,8 +412,8 @@ class Documenter: self.args = args self.retann = retann - self.fullname = (self.modname or '') + \ - ('.' + '.'.join(self.objpath) if self.objpath else '') + self.fullname = ((self.modname or '') + + ('.' + '.'.join(self.objpath) if self.objpath else '')) return True def import_object(self, raiseerror: bool = False) -> bool: @@ -701,6 +709,8 @@ class Documenter: # if isattr is True, the member is documented as an attribute if member is INSTANCEATTR: isattr = True + elif (namespace, membername) in attr_docs: + isattr = True else: isattr = False @@ -722,9 +732,9 @@ class Documenter: # hack for ClassDocumenter to inject docstring via ObjectMember doc = obj.docstring + doc, metadata = separate_metadata(doc) has_doc = bool(doc) - metadata = extract_metadata(doc) if 'private' in metadata: # consider a member private if docstring has "private" metadata isprivate = True @@ -761,7 +771,6 @@ class Documenter: else: # keep documented attributes keep = True - isattr = True elif want_all and isprivate: if has_doc or self.options.undoc_members: if self.options.private_members is None: @@ -816,8 +825,9 @@ class Documenter: if self.objpath: self.env.temp_data['autodoc:class'] = self.objpath[0] - want_all = all_members or self.options.inherited_members or \ - self.options.members is ALL + want_all = (all_members or + self.options.inherited_members or + self.options.members is ALL) # find out which members are documentable members_check_module, members = self.get_object_members(want_all) @@ -833,8 +843,7 @@ class Documenter: classes.sort(key=lambda cls: cls.priority) # give explicitly separated module name, so that members # of inner classes can be documented - full_mname = self.modname + '::' + \ - '.'.join(self.objpath + [mname]) + full_mname = self.modname + '::' + '.'.join(self.objpath + [mname]) documenter = classes[-1](self.directive, full_mname, self.indent) memberdocumenters.append((documenter, isattr)) @@ -1320,12 +1329,12 @@ class FunctionDocumenter(DocstringSignatureMixin, ModuleLevelDocumenter): # typ if typ is object: pass # default implementation. skipped. else: - self.annotate_to_first_argument(func, typ) - - documenter = FunctionDocumenter(self.directive, '') - documenter.object = func - documenter.objpath = [None] - sigs.append(documenter.format_signature()) + dispatchfunc = self.annotate_to_first_argument(func, typ) + if dispatchfunc: + documenter = FunctionDocumenter(self.directive, '') + documenter.object = dispatchfunc + documenter.objpath = [None] + sigs.append(documenter.format_signature()) if overloaded: actual = inspect.signature(self.object, type_aliases=self.config.autodoc_type_aliases) @@ -1350,28 +1359,34 @@ class FunctionDocumenter(DocstringSignatureMixin, ModuleLevelDocumenter): # typ return overload.replace(parameters=parameters) - def annotate_to_first_argument(self, func: Callable, typ: Type) -> None: + def annotate_to_first_argument(self, func: Callable, typ: Type) -> Optional[Callable]: """Annotate type hint to the first argument of function if needed.""" try: sig = inspect.signature(func, type_aliases=self.config.autodoc_type_aliases) except TypeError as exc: logger.warning(__("Failed to get a function signature for %s: %s"), self.fullname, exc) - return + return None except ValueError: - return + return None if len(sig.parameters) == 0: - return + return None + + def dummy(): + pass params = list(sig.parameters.values()) if params[0].annotation is Parameter.empty: params[0] = params[0].replace(annotation=typ) try: - func.__signature__ = sig.replace(parameters=params) # type: ignore + dummy.__signature__ = sig.replace(parameters=params) # type: ignore + return dummy except (AttributeError, TypeError): # failed to update signature (ex. built-in or extension types) - return + return None + else: + return None class DecoratorDocumenter(FunctionDocumenter): @@ -1417,6 +1432,7 @@ class ClassDocumenter(DocstringSignatureMixin, ModuleLevelDocumenter): # type: 'show-inheritance': bool_option, 'member-order': member_order_option, 'exclude-members': exclude_members_option, 'private-members': members_option, 'special-members': members_option, + 'class-doc-from': class_doc_from_option, } _signature_class: Any = None @@ -1651,7 +1667,7 @@ class ClassDocumenter(DocstringSignatureMixin, ModuleLevelDocumenter): # type: if lines is not None: return lines - content = self.config.autoclass_content + classdoc_from = self.options.get('class-doc-from', self.config.autoclass_content) docstrings = [] attrdocstring = self.get_attr(self.object, '__doc__', None) @@ -1660,7 +1676,7 @@ class ClassDocumenter(DocstringSignatureMixin, ModuleLevelDocumenter): # type: # for classes, what the "docstring" is can be controlled via a # config value; the default is only the class docstring - if content in ('both', 'init'): + if classdoc_from in ('both', 'init'): __init__ = self.get_attr(self.object, '__init__', None) initdocstring = getdoc(__init__, self.get_attr, self.config.autodoc_inherit_docstrings, @@ -1682,7 +1698,7 @@ class ClassDocumenter(DocstringSignatureMixin, ModuleLevelDocumenter): # type: initdocstring.strip() == object.__new__.__doc__)): # for !pypy initdocstring = None if initdocstring: - if content == 'init': + if classdoc_from == 'init': docstrings = [initdocstring] else: docstrings.append(initdocstring) @@ -1918,7 +1934,7 @@ class DataDocumenter(GenericAliasMixin, NewTypeMixin, TypeVarMixin, return True else: doc = self.get_doc() - metadata = extract_metadata('\n'.join(sum(doc, []))) + docstring, metadata = separate_metadata('\n'.join(sum(doc, []))) if 'hide-value' in metadata: return True @@ -1952,8 +1968,8 @@ class DataDocumenter(GenericAliasMixin, NewTypeMixin, TypeVarMixin, pass def get_real_modname(self) -> str: - return self.get_attr(self.parent or self.object, '__module__', None) \ - or self.modname + real_modname = self.get_attr(self.parent or self.object, '__module__', None) + return real_modname or self.modname def get_module_comment(self, attrname: str) -> Optional[List[str]]: try: @@ -2018,8 +2034,7 @@ class MethodDocumenter(DocstringSignatureMixin, ClassLevelDocumenter): # type: @classmethod def can_document_member(cls, member: Any, membername: str, isattr: bool, parent: Any ) -> bool: - return inspect.isroutine(member) and \ - not isinstance(parent, ModuleDocumenter) + return inspect.isroutine(member) and not isinstance(parent, ModuleDocumenter) def import_object(self, raiseerror: bool = False) -> bool: ret = super().import_object(raiseerror) @@ -2109,13 +2124,13 @@ class MethodDocumenter(DocstringSignatureMixin, ClassLevelDocumenter): # type: if typ is object: pass # default implementation. skipped. else: - self.annotate_to_first_argument(func, typ) - - documenter = MethodDocumenter(self.directive, '') - documenter.parent = self.parent - documenter.object = func - documenter.objpath = [None] - sigs.append(documenter.format_signature()) + dispatchmeth = self.annotate_to_first_argument(func, typ) + if dispatchmeth: + documenter = MethodDocumenter(self.directive, '') + documenter.parent = self.parent + documenter.object = dispatchmeth + documenter.objpath = [None] + sigs.append(documenter.format_signature()) if overloaded: if inspect.isstaticmethod(self.object, cls=self.parent, name=self.object_name): actual = inspect.signature(self.object, bound_method=False, @@ -2149,27 +2164,34 @@ class MethodDocumenter(DocstringSignatureMixin, ClassLevelDocumenter): # type: return overload.replace(parameters=parameters) - def annotate_to_first_argument(self, func: Callable, typ: Type) -> None: + def annotate_to_first_argument(self, func: Callable, typ: Type) -> Optional[Callable]: """Annotate type hint to the first argument of function if needed.""" try: sig = inspect.signature(func, type_aliases=self.config.autodoc_type_aliases) except TypeError as exc: logger.warning(__("Failed to get a method signature for %s: %s"), self.fullname, exc) - return + return None except ValueError: - return + return None + if len(sig.parameters) == 1: - return + return None + + def dummy(): + pass params = list(sig.parameters.values()) if params[1].annotation is Parameter.empty: params[1] = params[1].replace(annotation=typ) try: - func.__signature__ = sig.replace(parameters=params) # type: ignore + dummy.__signature__ = sig.replace(parameters=params) # type: ignore + return dummy except (AttributeError, TypeError): # failed to update signature (ex. built-in or extension types) - return + return None + else: + return None class NonDataDescriptorMixin(DataDocumenterMixinBase): @@ -2447,8 +2469,8 @@ class AttributeDocumenter(GenericAliasMixin, NewTypeMixin, SlotsMixin, # type: return ret def get_real_modname(self) -> str: - return self.get_attr(self.parent or self.object, '__module__', None) \ - or self.modname + real_modname = self.get_attr(self.parent or self.object, '__module__', None) + return real_modname or self.modname def should_suppress_value_header(self) -> bool: if super().should_suppress_value_header(): @@ -2456,7 +2478,7 @@ class AttributeDocumenter(GenericAliasMixin, NewTypeMixin, SlotsMixin, # type: else: doc = self.get_doc() if doc: - metadata = extract_metadata('\n'.join(sum(doc, []))) + docstring, metadata = separate_metadata('\n'.join(sum(doc, []))) if 'hide-value' in metadata: return True @@ -2549,8 +2571,8 @@ class PropertyDocumenter(DocstringStripSignatureMixin, ClassLevelDocumenter): # pass def get_real_modname(self) -> str: - return self.get_attr(self.parent or self.object, '__module__', None) \ - or self.modname + real_modname = self.get_attr(self.parent or self.object, '__module__', None) + return real_modname or self.modname def add_directive_header(self, sig: str) -> None: super().add_directive_header(sig) diff --git a/sphinx/ext/autodoc/directive.py b/sphinx/ext/autodoc/directive.py index c58d0c411..a554adf68 100644 --- a/sphinx/ext/autodoc/directive.py +++ b/sphinx/ext/autodoc/directive.py @@ -30,7 +30,7 @@ logger = logging.getLogger(__name__) AUTODOC_DEFAULT_OPTIONS = ['members', 'undoc-members', 'inherited-members', 'show-inheritance', 'private-members', 'special-members', 'ignore-module-all', 'exclude-members', 'member-order', - 'imported-members'] + 'imported-members', 'class-doc-from'] AUTODOC_EXTENDABLE_OPTIONS = ['members', 'private-members', 'special-members', 'exclude-members'] diff --git a/sphinx/ext/autosummary/__init__.py b/sphinx/ext/autosummary/__init__.py index cacff8c5c..be9f0cc65 100644 --- a/sphinx/ext/autosummary/__init__.py +++ b/sphinx/ext/autosummary/__init__.py @@ -662,8 +662,10 @@ def import_ivar_by_name(name: str, prefixes: List[str] = [None]) -> Tuple[str, A name, attr = name.rsplit(".", 1) real_name, obj, parent, modname = import_by_name(name, prefixes) qualname = real_name.replace(modname + ".", "") - analyzer = ModuleAnalyzer.for_module(modname) - if (qualname, attr) in analyzer.find_attr_docs(): + analyzer = ModuleAnalyzer.for_module(getattr(obj, '__module__', modname)) + analyzer.analyze() + # check for presence in `annotations` to include dataclass attributes + if (qualname, attr) in analyzer.attr_docs or (qualname, attr) in analyzer.annotations: return real_name + "." + attr, INSTANCEATTR, obj, modname except (ImportError, ValueError, PycodeError): pass diff --git a/sphinx/ext/autosummary/generate.py b/sphinx/ext/autosummary/generate.py index d1130d096..4f3493659 100644 --- a/sphinx/ext/autosummary/generate.py +++ b/sphinx/ext/autosummary/generate.py @@ -239,15 +239,33 @@ def generate_autosummary_content(name: str, obj: Any, parent: Any, name, exc, type='autosummary') return False - def get_members(obj: Any, types: Set[str], include_public: List[str] = [], - imported: bool = True) -> Tuple[List[str], List[str]]: - items: List[str] = [] - public: List[str] = [] + def get_class_members(obj: Any) -> Dict[str, Any]: + members = sphinx.ext.autodoc.get_class_members(obj, [qualname], safe_getattr) + return {name: member.object for name, member in members.items()} + + def get_module_members(obj: Any) -> Dict[str, Any]: + members = {} for name in dir(obj): try: - value = safe_getattr(obj, name) + members[name] = safe_getattr(obj, name) except AttributeError: continue + return members + + def get_all_members(obj: Any) -> Dict[str, Any]: + if doc.objtype == "module": + return get_module_members(obj) + elif doc.objtype == "class": + return get_class_members(obj) + return {} + + def get_members(obj: Any, types: Set[str], include_public: List[str] = [], + imported: bool = True) -> Tuple[List[str], List[str]]: + items: List[str] = [] + public: List[str] = [] + + all_members = get_all_members(obj) + for name, value in all_members.items(): documenter = get_documenter(app, value, obj) if documenter.objtype in types: # skip imported members if expected diff --git a/sphinx/jinja2glue.py b/sphinx/jinja2glue.py index c239f5a4a..cd6e8066b 100644 --- a/sphinx/jinja2glue.py +++ b/sphinx/jinja2glue.py @@ -12,7 +12,7 @@ from os import path from pprint import pformat from typing import TYPE_CHECKING, Any, Callable, Dict, Iterator, List, Tuple, Union -from jinja2 import BaseLoader, FileSystemLoader, TemplateNotFound, contextfunction +from jinja2 import BaseLoader, FileSystemLoader, TemplateNotFound from jinja2.environment import Environment from jinja2.sandbox import SandboxedEnvironment from jinja2.utils import open_if_exists @@ -22,6 +22,11 @@ from sphinx.theming import Theme from sphinx.util import logging from sphinx.util.osutil import mtimes_of_files +try: + from jinja2.utils import pass_context # type: ignore # jinja2-3.0 or above +except ImportError: + from jinja2 import contextfunction as pass_context + if TYPE_CHECKING: from sphinx.builders import Builder @@ -101,7 +106,7 @@ class idgen: next = __next__ # Python 2/Jinja compatibility -@contextfunction +@pass_context def warning(context: Dict, message: str, *args: Any, **kwargs: Any) -> str: if 'pagename' in context: filename = context.get('pagename') + context.get('file_suffix', '') @@ -180,9 +185,9 @@ class BuiltinTemplateLoader(TemplateBridge, BaseLoader): self.environment.filters['toint'] = _toint self.environment.filters['todim'] = _todim self.environment.filters['slice_index'] = _slice_index - self.environment.globals['debug'] = contextfunction(pformat) + self.environment.globals['debug'] = pass_context(pformat) self.environment.globals['warning'] = warning - self.environment.globals['accesskey'] = contextfunction(accesskey) + self.environment.globals['accesskey'] = pass_context(accesskey) self.environment.globals['idgen'] = idgen if use_i18n: self.environment.install_gettext_translations(builder.app.translator) diff --git a/sphinx/project.py b/sphinx/project.py index bb2314c63..d4293cdc4 100644 --- a/sphinx/project.py +++ b/sphinx/project.py @@ -10,7 +10,7 @@ import os from glob import glob -from typing import Dict, List, Set +from typing import Dict, List, Optional, Set from sphinx.locale import __ from sphinx.util import get_matching_files, logging, path_stabilize @@ -60,7 +60,7 @@ class Project: return self.docnames - def path2doc(self, filename: str) -> str: + def path2doc(self, filename: str) -> Optional[str]: """Return the docname for the filename if the file is document. *filename* should be absolute or relative to the source directory. diff --git a/sphinx/pycode/__init__.py b/sphinx/pycode/__init__.py index c55a4fe4a..8e79385e2 100644 --- a/sphinx/pycode/__init__.py +++ b/sphinx/pycode/__init__.py @@ -25,6 +25,13 @@ from sphinx.pycode.parser import Parser class ModuleAnalyzer: + annotations: Dict[Tuple[str, str], str] + attr_docs: Dict[Tuple[str, str], List[str]] + finals: List[str] + overloads: Dict[str, List[Signature]] + tagorder: Dict[str, int] + tags: Dict[str, Tuple[str, int, int]] + # cache for analyzer objects -- caches both by module and file name cache: Dict[Tuple[str, str], Any] = {} @@ -134,13 +141,6 @@ class ModuleAnalyzer: # cache the source code as well self.code = source.read() - # will be filled by analyze() - self.annotations: Dict[Tuple[str, str], str] = None - self.attr_docs: Dict[Tuple[str, str], List[str]] = None - self.finals: List[str] = None - self.overloads: Dict[str, List[Signature]] = None - self.tagorder: Dict[str, int] = None - self.tags: Dict[str, Tuple[str, int, int]] = None self._analyzed = False def parse(self) -> None: diff --git a/sphinx/pycode/parser.py b/sphinx/pycode/parser.py index fa249d8c5..a00b481ce 100644 --- a/sphinx/pycode/parser.py +++ b/sphinx/pycode/parser.py @@ -129,8 +129,8 @@ class TokenProcessor: lines = iter(buffers) self.buffers = buffers self.tokens = tokenize.generate_tokens(lambda: next(lines)) - self.current: Token = None - self.previous: Token = None + self.current: Optional[Token] = None + self.previous: Optional[Token] = None def get_line(self, lineno: int) -> str: """Returns specified line.""" @@ -178,7 +178,7 @@ class AfterCommentParser(TokenProcessor): def __init__(self, lines: List[str]) -> None: super().__init__(lines) - self.comment: str = None + self.comment: Optional[str] = None def fetch_rvalue(self) -> List[Token]: """Fetch right-hand value of assignment.""" @@ -223,16 +223,16 @@ class VariableCommentPicker(ast.NodeVisitor): self.encoding = encoding self.context: List[str] = [] self.current_classes: List[str] = [] - self.current_function: ast.FunctionDef = None + self.current_function: Optional[ast.FunctionDef] = None self.comments: Dict[Tuple[str, str], str] = OrderedDict() self.annotations: Dict[Tuple[str, str], str] = {} - self.previous: ast.AST = None + self.previous: Optional[ast.AST] = None self.deforders: Dict[str, int] = {} self.finals: List[str] = [] self.overloads: Dict[str, List[Signature]] = {} - self.typing: str = None - self.typing_final: str = None - self.typing_overload: str = None + self.typing: Optional[str] = None + self.typing_final: Optional[str] = None + self.typing_overload: Optional[str] = None super().__init__() def get_qualname_for(self, name: str) -> Optional[List[str]]: @@ -308,7 +308,7 @@ class VariableCommentPicker(ast.NodeVisitor): return False - def get_self(self) -> ast.arg: + def get_self(self) -> Optional[ast.arg]: """Returns the name of first argument if in function.""" if self.current_function and self.current_function.args.args: return self.current_function.args.args[0] @@ -466,7 +466,7 @@ class DefinitionFinder(TokenProcessor): def __init__(self, lines: List[str]) -> None: super().__init__(lines) - self.decorator: Token = None + self.decorator: Optional[Token] = None self.context: List[str] = [] self.indents: List = [] self.definitions: Dict[str, Tuple[str, int, int]] = {} diff --git a/sphinx/search/__init__.py b/sphinx/search/__init__.py index 2a3420244..4b0567969 100644 --- a/sphinx/search/__init__.py +++ b/sphinx/search/__init__.py @@ -12,7 +12,7 @@ import pickle import re from importlib import import_module from os import path -from typing import IO, Any, Dict, Iterable, List, Set, Tuple, Type +from typing import IO, Any, Dict, Iterable, List, Optional, Set, Tuple, Type from docutils import nodes from docutils.nodes import Node @@ -440,7 +440,7 @@ class IndexBuilder: else: return [] - def get_js_stemmer_rawcode(self) -> str: + def get_js_stemmer_rawcode(self) -> Optional[str]: return None def get_js_stemmer_code(self) -> str: diff --git a/sphinx/themes/basic/static/basic.css_t b/sphinx/themes/basic/static/basic.css_t index db43499ad..45815bac0 100644 --- a/sphinx/themes/basic/static/basic.css_t +++ b/sphinx/themes/basic/static/basic.css_t @@ -819,7 +819,7 @@ div.code-block-caption code { table.highlighttable td.linenos, span.linenos, -div.doctest > div.highlight span.gp { /* gp: Generic.Prompt */ +div.highlight span.gp { /* gp: Generic.Prompt */ user-select: none; -webkit-user-select: text; /* Safari fallback only */ -webkit-user-select: none; /* Chrome/Safari */ diff --git a/sphinx/themes/basic/static/searchtools.js b/sphinx/themes/basic/static/searchtools.js index e09f9263f..8eb14218b 100644 --- a/sphinx/themes/basic/static/searchtools.js +++ b/sphinx/themes/basic/static/searchtools.js @@ -276,7 +276,7 @@ var Search = { setTimeout(function() { displayNextItem(); }, 5); - } else if (DOCUMENTATION_OPTIONS.HAS_SOURCE) { + } else { $.ajax({url: requestUrl, dataType: "text", complete: function(jqxhr, textstatus) { @@ -289,12 +289,6 @@ var Search = { displayNextItem(); }, 5); }}); - } else { - // no source available, just display title - Search.output.append(listItem); - setTimeout(function() { - displayNextItem(); - }, 5); } } // search finished, update title and status message diff --git a/sphinx/transforms/__init__.py b/sphinx/transforms/__init__.py index 213742636..66882e2b2 100644 --- a/sphinx/transforms/__init__.py +++ b/sphinx/transforms/__init__.py @@ -10,7 +10,7 @@ import re import warnings -from typing import TYPE_CHECKING, Any, Dict, Generator, List, Tuple +from typing import TYPE_CHECKING, Any, Dict, Generator, List, Optional, Tuple from docutils import nodes from docutils.nodes import Element, Node, Text @@ -72,8 +72,8 @@ class SphinxTransformer(Transformer): A transformer for Sphinx. """ - document: nodes.document = None - env: "BuildEnvironment" = None + document: nodes.document + env: Optional["BuildEnvironment"] = None def set_environment(self, env: "BuildEnvironment") -> None: self.env = env diff --git a/sphinx/transforms/i18n.py b/sphinx/transforms/i18n.py index 8f6d37d8d..2a5d6f121 100644 --- a/sphinx/transforms/i18n.py +++ b/sphinx/transforms/i18n.py @@ -10,7 +10,7 @@ from os import path from textwrap import indent -from typing import TYPE_CHECKING, Any, Dict, List, Tuple, Type, TypeVar +from typing import TYPE_CHECKING, Any, Dict, List, Optional, Tuple, Type, TypeVar from docutils import nodes from docutils.io import StringInput @@ -416,7 +416,7 @@ class Locale(SphinxTransform): .format(old_xref_rawsources, new_xref_rawsources), location=node) - def get_ref_key(node: addnodes.pending_xref) -> Tuple[str, str, str]: + def get_ref_key(node: addnodes.pending_xref) -> Optional[Tuple[str, str, str]]: case = node["refdomain"], node["reftype"] if case == ('std', 'term'): return None diff --git a/sphinx/transforms/post_transforms/__init__.py b/sphinx/transforms/post_transforms/__init__.py index e2899d994..281407983 100644 --- a/sphinx/transforms/post_transforms/__init__.py +++ b/sphinx/transforms/post_transforms/__init__.py @@ -8,6 +8,7 @@ :license: BSD, see LICENSE for details. """ +import re from typing import Any, Dict, List, Optional, Tuple, Type, cast from docutils import nodes @@ -171,14 +172,27 @@ class ReferencesResolver(SphinxPostTransform): warn = node.get('refwarn') if self.config.nitpicky: warn = True + dtype = '%s:%s' % (domain.name, typ) if domain else typ if self.config.nitpick_ignore: - dtype = '%s:%s' % (domain.name, typ) if domain else typ if (dtype, target) in self.config.nitpick_ignore: warn = False # for "std" types also try without domain name if (not domain or domain.name == 'std') and \ (typ, target) in self.config.nitpick_ignore: warn = False + if self.config.nitpick_ignore_regex: + def matches_ignore(entry_type: str, entry_target: str) -> bool: + for ignore_type, ignore_target in self.config.nitpick_ignore_regex: + if re.fullmatch(ignore_type, entry_type) and \ + re.fullmatch(ignore_target, entry_target): + return True + return False + if matches_ignore(dtype, target): + warn = False + # for "std" types also try without domain name + if (not domain or domain.name == 'std') and \ + matches_ignore(typ, target): + warn = False if not warn: return diff --git a/sphinx/util/__init__.py b/sphinx/util/__init__.py index d420f4f77..f1a6f1af6 100644 --- a/sphinx/util/__init__.py +++ b/sphinx/util/__init__.py @@ -22,8 +22,8 @@ from datetime import datetime from importlib import import_module from os import path from time import mktime, strptime -from typing import (IO, TYPE_CHECKING, Any, Callable, Dict, Iterable, Iterator, List, Pattern, - Set, Tuple, Type) +from typing import (IO, TYPE_CHECKING, Any, Callable, Dict, Iterable, Iterator, List, Optional, + Pattern, Set, Tuple, Type) from urllib.parse import parse_qsl, quote_plus, urlencode, urlsplit, urlunsplit from sphinx.deprecation import RemovedInSphinx50Warning @@ -408,7 +408,7 @@ def import_object(objname: str, source: str = None) -> Any: raise ExtensionError('Could not import %s' % objname, exc) from exc -def split_full_qualified_name(name: str) -> Tuple[str, str]: +def split_full_qualified_name(name: str) -> Tuple[Optional[str], str]: """Split full qualified name to a pair of modname and qualname. A qualname is an abbreviation for "Qualified name" introduced at PEP-3155 diff --git a/sphinx/util/docfields.py b/sphinx/util/docfields.py index f74f03265..3a3367ebe 100644 --- a/sphinx/util/docfields.py +++ b/sphinx/util/docfields.py @@ -209,7 +209,7 @@ class DocFieldTransformer: Transforms field lists in "doc field" syntax into better-looking equivalents, using the field type definitions given on a domain. """ - typemap: Dict[str, Tuple[Field, bool]] = None + typemap: Dict[str, Tuple[Field, bool]] def __init__(self, directive: "ObjectDescription") -> None: self.directive = directive diff --git a/sphinx/util/docstrings.py b/sphinx/util/docstrings.py index 46bb5b9b8..d81d7dd99 100644 --- a/sphinx/util/docstrings.py +++ b/sphinx/util/docstrings.py @@ -11,26 +11,28 @@ import re import sys import warnings -from typing import Dict, List +from typing import Dict, List, Tuple from docutils.parsers.rst.states import Body -from sphinx.deprecation import RemovedInSphinx50Warning +from sphinx.deprecation import RemovedInSphinx50Warning, RemovedInSphinx60Warning field_list_item_re = re.compile(Body.patterns['field_marker']) -def extract_metadata(s: str) -> Dict[str, str]: - """Extract metadata from docstring.""" +def separate_metadata(s: str) -> Tuple[str, Dict[str, str]]: + """Separate docstring into metadata and others.""" in_other_element = False metadata: Dict[str, str] = {} + lines = [] if not s: - return metadata + return s, metadata for line in prepare_docstring(s): if line.strip() == '': in_other_element = False + lines.append(line) else: matched = field_list_item_re.match(line) if matched and not in_other_element: @@ -38,9 +40,20 @@ def extract_metadata(s: str) -> Dict[str, str]: if field_name.startswith('meta '): name = field_name[5:].strip() metadata[name] = line[matched.end():].strip() + else: + lines.append(line) else: in_other_element = True + lines.append(line) + + return '\n'.join(lines), metadata + + +def extract_metadata(s: str) -> Dict[str, str]: + warnings.warn("extract_metadata() is deprecated.", + RemovedInSphinx60Warning, stacklevel=2) + docstring, metadata = separate_metadata(s) return metadata diff --git a/sphinx/util/docutils.py b/sphinx/util/docutils.py index 44483bdd8..c2e12e152 100644 --- a/sphinx/util/docutils.py +++ b/sphinx/util/docutils.py @@ -176,8 +176,8 @@ class sphinx_domains: """ def __init__(self, env: "BuildEnvironment") -> None: self.env = env - self.directive_func: Callable = None - self.roles_func: Callable = None + self.directive_func: Callable = lambda *args: (None, []) + self.roles_func: Callable = lambda *args: (None, []) def __enter__(self) -> None: self.enable() @@ -372,7 +372,7 @@ class SphinxRole: if name: self.name = name.lower() else: - self.name = self.env.temp_data.get('default_role') + self.name = self.env.temp_data.get('default_role', '') if not self.name: self.name = self.env.config.default_role if not self.name: @@ -491,7 +491,7 @@ class SphinxTranslator(nodes.NodeVisitor): # cache a vanilla instance of nodes.document # Used in new_document() function -__document_cache__: nodes.document = None +__document_cache__: Optional[nodes.document] = None def new_document(source_path: str, settings: Any = None) -> nodes.document: diff --git a/sphinx/util/i18n.py b/sphinx/util/i18n.py index 1e469d135..f7298e2e8 100644 --- a/sphinx/util/i18n.py +++ b/sphinx/util/i18n.py @@ -91,8 +91,11 @@ class CatalogRepository: for locale_dir in self._locale_dirs: locale_dir = path.join(self.basedir, locale_dir) - if path.exists(path.join(locale_dir, self.language, 'LC_MESSAGES')): + locale_path = path.join(locale_dir, self.language, 'LC_MESSAGES') + if path.exists(locale_path): yield locale_dir + else: + logger.verbose(__('locale_dir %s does not exists'), locale_path) @property def pofiles(self) -> Generator[Tuple[str, str], None, None]: diff --git a/sphinx/util/inspect.py b/sphinx/util/inspect.py index 7c9adb0bf..f216e8797 100644 --- a/sphinx/util/inspect.py +++ b/sphinx/util/inspect.py @@ -18,8 +18,10 @@ import types import typing import warnings from functools import partial, partialmethod +from importlib import import_module from inspect import Parameter, isclass, ismethod, ismethoddescriptor, ismodule # NOQA from io import StringIO +from types import ModuleType from typing import Any, Callable, Dict, Mapping, Optional, Sequence, Tuple, Type, cast from sphinx.deprecation import RemovedInSphinx50Warning @@ -501,6 +503,78 @@ class DefaultValue: return self.value +class TypeAliasForwardRef: + """Pseudo typing class for autodoc_type_aliases. + + This avoids the error on evaluating the type inside `get_type_hints()`. + """ + def __init__(self, name: str) -> None: + self.name = name + + def __call__(self) -> None: + # Dummy method to imitate special typing classes + pass + + def __eq__(self, other: Any) -> bool: + return self.name == other + + +class TypeAliasModule: + """Pseudo module class for autodoc_type_aliases.""" + + def __init__(self, modname: str, mapping: Dict[str, str]) -> None: + self.__modname = modname + self.__mapping = mapping + + self.__module: Optional[ModuleType] = None + + def __getattr__(self, name: str) -> Any: + fullname = '.'.join(filter(None, [self.__modname, name])) + if fullname in self.__mapping: + # exactly matched + return TypeAliasForwardRef(self.__mapping[fullname]) + else: + prefix = fullname + '.' + nested = {k: v for k, v in self.__mapping.items() if k.startswith(prefix)} + if nested: + # sub modules or classes found + return TypeAliasModule(fullname, nested) + else: + # no sub modules or classes found. + try: + # return the real submodule if exists + return import_module(fullname) + except ImportError: + # return the real class + if self.__module is None: + self.__module = import_module(self.__modname) + + return getattr(self.__module, name) + + +class TypeAliasNamespace(Dict[str, Any]): + """Pseudo namespace class for autodoc_type_aliases. + + This enables to look up nested modules and classes like `mod1.mod2.Class`. + """ + + def __init__(self, mapping: Dict[str, str]) -> None: + self.__mapping = mapping + + def __getitem__(self, key: str) -> Any: + if key in self.__mapping: + # exactly matched + return TypeAliasForwardRef(self.__mapping[key]) + else: + prefix = key + '.' + nested = {k: v for k, v in self.__mapping.items() if k.startswith(prefix)} + if nested: + # sub modules or classes found + return TypeAliasModule(key, nested) + else: + raise KeyError + + def _should_unwrap(subject: Callable) -> bool: """Check the function should be unwrapped on getting signature.""" __globals__ = getglobals(subject) @@ -549,12 +623,19 @@ def signature(subject: Callable, bound_method: bool = False, follow_wrapped: boo try: # Resolve annotations using ``get_type_hints()`` and type_aliases. - annotations = typing.get_type_hints(subject, None, type_aliases) + localns = TypeAliasNamespace(type_aliases) + annotations = typing.get_type_hints(subject, None, localns) for i, param in enumerate(parameters): if param.name in annotations: - parameters[i] = param.replace(annotation=annotations[param.name]) + annotation = annotations[param.name] + if isinstance(annotation, TypeAliasForwardRef): + annotation = annotation.name + parameters[i] = param.replace(annotation=annotation) if 'return' in annotations: - return_annotation = annotations['return'] + if isinstance(annotations['return'], TypeAliasForwardRef): + return_annotation = annotations['return'].name + else: + return_annotation = annotations['return'] except Exception: # ``get_type_hints()`` does not support some kind of objects like partial, # ForwardRef and so on. diff --git a/sphinx/util/logging.py b/sphinx/util/logging.py index 64b7d8fb4..e0f4f0e70 100644 --- a/sphinx/util/logging.py +++ b/sphinx/util/logging.py @@ -320,8 +320,8 @@ def prefixed_warnings(prefix: str) -> Generator[None, None, None]: prefix_filter.prefix = previous else: # not prefixed yet + prefix_filter = MessagePrefixFilter(prefix) try: - prefix_filter = MessagePrefixFilter(prefix) warning_handler.addFilter(prefix_filter) yield finally: @@ -472,7 +472,7 @@ class SphinxLogRecordTranslator(logging.Filter): * Make a instance of SphinxLogRecord * docname to path if location given """ - LogRecordClass: Type[logging.LogRecord] = None + LogRecordClass: Type[logging.LogRecord] def __init__(self, app: "Sphinx") -> None: self.app = app diff --git a/sphinx/util/nodes.py b/sphinx/util/nodes.py index 44eb5d303..36db149b9 100644 --- a/sphinx/util/nodes.py +++ b/sphinx/util/nodes.py @@ -10,7 +10,8 @@ import re import unicodedata -from typing import TYPE_CHECKING, Any, Callable, Iterable, List, Set, Tuple, Type, Union, cast +from typing import (TYPE_CHECKING, Any, Callable, Iterable, List, Optional, Set, Tuple, Type, + Union, cast) from docutils import nodes from docutils.nodes import Element, Node @@ -170,7 +171,7 @@ def apply_source_workaround(node: Element) -> None: ))): logger.debug('[i18n] PATCH: %r to have source and line: %s', get_full_module_name(node), repr_domxml(node)) - node.source = get_node_source(node) + node.source = get_node_source(node) or '' node.line = 0 # need fix docutils to get `node.line` return @@ -266,7 +267,7 @@ def extract_messages(doctree: Element) -> Iterable[Tuple[Element, str]]: if node.get('translatable'): msg = '.. image:: %s' % node['uri'] else: - msg = None + msg = '' elif isinstance(node, META_TYPE_NODES): msg = node.rawcontent elif isinstance(node, nodes.pending) and is_pending_meta(node): @@ -279,14 +280,14 @@ def extract_messages(doctree: Element) -> Iterable[Tuple[Element, str]]: yield node, msg -def get_node_source(node: Element) -> str: +def get_node_source(node: Element) -> Optional[str]: for pnode in traverse_parent(node): if pnode.source: return pnode.source return None -def get_node_line(node: Element) -> int: +def get_node_line(node: Element) -> Optional[int]: for pnode in traverse_parent(node): if pnode.line: return pnode.line @@ -300,7 +301,7 @@ def traverse_parent(node: Element, cls: Any = None) -> Iterable[Element]: node = node.parent -def get_prev_node(node: Node) -> Node: +def get_prev_node(node: Node) -> Optional[Node]: pos = node.parent.index(node) if pos > 0: return node.parent[pos - 1] @@ -360,10 +361,11 @@ indextypes = [ ] -def process_index_entry(entry: str, targetid: str) -> List[Tuple[str, str, str, str, str]]: +def process_index_entry(entry: str, targetid: str + ) -> List[Tuple[str, str, str, str, Optional[str]]]: from sphinx.domains.python import pairindextypes - indexentries: List[Tuple[str, str, str, str, str]] = [] + indexentries: List[Tuple[str, str, str, str, Optional[str]]] = [] entry = entry.strip() oentry = entry main = '' @@ -531,7 +533,8 @@ def make_id(env: "BuildEnvironment", document: nodes.document, return node_id -def find_pending_xref_condition(node: addnodes.pending_xref, condition: str) -> Element: +def find_pending_xref_condition(node: addnodes.pending_xref, condition: str + ) -> Optional[Element]: """Pick matched pending_xref_condition node up from the pending_xref.""" for subnode in node: if (isinstance(subnode, addnodes.pending_xref_condition) and diff --git a/sphinx/util/parallel.py b/sphinx/util/parallel.py index 7ad7f81e7..2a83d6297 100644 --- a/sphinx/util/parallel.py +++ b/sphinx/util/parallel.py @@ -14,7 +14,7 @@ import sys import time import traceback from math import sqrt -from typing import Any, Callable, Dict, List, Sequence +from typing import Any, Callable, Dict, List, Optional, Sequence try: import multiprocessing @@ -62,7 +62,7 @@ class ParallelTasks: # (optional) function performed by each task on the result of main task self._result_funcs: Dict[int, Callable] = {} # task arguments - self._args: Dict[int, List[Any]] = {} + self._args: Dict[int, Optional[List[Any]]] = {} # list of subprocesses (both started and waiting) self._procs: Dict[int, multiprocessing.Process] = {} # list of receiving pipe connections of running subprocesses @@ -105,7 +105,8 @@ class ParallelTasks: def join(self) -> None: try: while self._pworking: - self._join_one() + if not self._join_one(): + time.sleep(0.02) except Exception: # shutdown other child processes on failure self.terminate() @@ -119,7 +120,8 @@ class ParallelTasks: self._precvs.pop(tid) self._pworking -= 1 - def _join_one(self) -> None: + def _join_one(self) -> bool: + joined_any = False for tid, pipe in self._precvs.items(): if pipe.poll(): exc, logs, result = pipe.recv() @@ -131,15 +133,17 @@ class ParallelTasks: self._procs[tid].join() self._precvs.pop(tid) self._pworking -= 1 + joined_any = True break - else: - time.sleep(0.02) + while self._precvsWaiting and self._pworking < self.nproc: newtid, newprecv = self._precvsWaiting.popitem() self._precvs[newtid] = newprecv self._procs[newtid].start() self._pworking += 1 + return joined_any + def make_chunks(arguments: Sequence[str], nproc: int, maxbatch: int = 10) -> List[Any]: # determine how many documents to read in one go diff --git a/sphinx/util/rst.py b/sphinx/util/rst.py index 82b3f6bda..8103a8b10 100644 --- a/sphinx/util/rst.py +++ b/sphinx/util/rst.py @@ -18,11 +18,17 @@ from docutils.parsers.rst import roles from docutils.parsers.rst.languages import en as english from docutils.statemachine import StringList from docutils.utils import Reporter -from jinja2 import Environment, environmentfilter +from jinja2 import Environment from sphinx.locale import __ from sphinx.util import docutils, logging +try: + from jinja2.utils import pass_environment # type: ignore # jinja2-3.0 or above +except ImportError: + from jinja2 import environmentfilter as pass_environment + + logger = logging.getLogger(__name__) docinfo_re = re.compile(':\\w+:.*?') @@ -51,7 +57,7 @@ def textwidth(text: str, widechars: str = 'WF') -> int: return sum(charwidth(c, widechars) for c in text) -@environmentfilter +@pass_environment def heading(env: Environment, text: str, level: int = 1) -> str: """Create a heading for *level*.""" assert level <= 3 diff --git a/sphinx/util/texescape.py b/sphinx/util/texescape.py index 417a963a7..8dcc08a9b 100644 --- a/sphinx/util/texescape.py +++ b/sphinx/util/texescape.py @@ -29,6 +29,8 @@ tex_replacements = [ # map special Unicode characters to TeX commands ('✓', r'\(\checkmark\)'), ('✔', r'\(\pmb{\checkmark}\)'), + ('✕', r'\(\times\)'), + ('✖', r'\(\pmb{\times}\)'), # used to separate -- in options ('', r'{}'), # map some special Unicode characters to similar ASCII ones diff --git a/sphinx/util/typing.py b/sphinx/util/typing.py index 957f8a332..fb8daa623 100644 --- a/sphinx/util/typing.py +++ b/sphinx/util/typing.py @@ -64,7 +64,7 @@ RoleFunction = Callable[[str, str, str, int, Inliner, Dict[str, Any], List[str]] Tuple[List[nodes.Node], List[nodes.system_message]]] # A option spec for directive -OptionSpec = Dict[str, Callable[[Optional[str]], Any]] +OptionSpec = Dict[str, Callable[[str], Any]] # title getter functions for enumerable nodes (see sphinx.domains.std) TitleGetter = Callable[[nodes.Node], str] diff --git a/tests/roots/test-autosummary/dummy_module.py b/tests/roots/test-autosummary/dummy_module.py index 93d482b59..4adc0313e 100644 --- a/tests/roots/test-autosummary/dummy_module.py +++ b/tests/roots/test-autosummary/dummy_module.py @@ -3,6 +3,7 @@ module_attr C.class_attr + C.instance_attr C.prop_attr1 C.prop_attr2 C.C2 @@ -51,6 +52,12 @@ class C: #: value is integer. class_attr = 42 + def __init__(self): + #: This is an instance attribute + #: + #: value is a string + self.instance_attr = "42" + def _prop_attr_get(self): """ This is a function docstring diff --git a/tests/roots/test-ext-autodoc/target/annotations.py b/tests/roots/test-ext-autodoc/target/autodoc_type_aliases.py index ef600e2af..d8a2fecef 100644 --- a/tests/roots/test-ext-autodoc/target/annotations.py +++ b/tests/roots/test-ext-autodoc/target/autodoc_type_aliases.py @@ -1,5 +1,6 @@ from __future__ import annotations +import io from typing import overload myint = int @@ -11,6 +12,10 @@ variable: myint variable2 = None # type: myint +def read(r: io.BytesIO) -> io.StringIO: + """docstring""" + + def sum(x: myint, y: myint) -> myint: """docstring""" return x + y diff --git a/tests/roots/test-ext-autodoc/target/metadata.py b/tests/roots/test-ext-autodoc/target/metadata.py new file mode 100644 index 000000000..7a4488f67 --- /dev/null +++ b/tests/roots/test-ext-autodoc/target/metadata.py @@ -0,0 +1,2 @@ +def foo(): + """:meta metadata-only-docstring:""" diff --git a/tests/roots/test-ext-autodoc/target/module.py b/tests/roots/test-ext-autodoc/target/module.py new file mode 100644 index 000000000..fe3b490a9 --- /dev/null +++ b/tests/roots/test-ext-autodoc/target/module.py @@ -0,0 +1,14 @@ +undocumented = 1 + +#: docstring +documented = 1 + +undoc_annotated: int + +#: docstring +annotated: int + +__special__ = 1 + +#: docstring +__documented_special__ = 1 diff --git a/tests/roots/test-ext-autodoc/target/singledispatch.py b/tests/roots/test-ext-autodoc/target/singledispatch.py index 3fa81dcae..fca2b6683 100644 --- a/tests/roots/test-ext-autodoc/target/singledispatch.py +++ b/tests/roots/test-ext-autodoc/target/singledispatch.py @@ -15,6 +15,7 @@ def func(arg, kwarg=None): @func.register(int) +@func.register(float) def _func_int(arg, kwarg=None): """A function for int.""" pass diff --git a/tests/roots/test-ext-autodoc/target/singledispatchmethod.py b/tests/roots/test-ext-autodoc/target/singledispatchmethod.py index b5ccbb2f0..086c7fe66 100644 --- a/tests/roots/test-ext-autodoc/target/singledispatchmethod.py +++ b/tests/roots/test-ext-autodoc/target/singledispatchmethod.py @@ -10,6 +10,7 @@ class Foo: pass @meth.register(int) + @meth.register(float) def _meth_int(self, arg, kwarg=None): """A method for int.""" pass diff --git a/tests/roots/test-nitpicky-warnings/conf.py b/tests/roots/test-nitpicky-warnings/conf.py new file mode 100644 index 000000000..2db221cc6 --- /dev/null +++ b/tests/roots/test-nitpicky-warnings/conf.py @@ -0,0 +1 @@ +nitpicky = True diff --git a/tests/roots/test-nitpicky-warnings/index.rst b/tests/roots/test-nitpicky-warnings/index.rst new file mode 100644 index 000000000..e73840d4f --- /dev/null +++ b/tests/roots/test-nitpicky-warnings/index.rst @@ -0,0 +1,7 @@ +test-nitpicky-warnings +====================== + +:py:const:`prefix.anything.postfix` +:py:class:`prefix.anything` +:py:class:`anything.postfix` +:js:class:`prefix.anything.postfix` diff --git a/tests/test_config.py b/tests/test_config.py index a48e7ce30..b9b4f612a 100644 --- a/tests/test_config.py +++ b/tests/test_config.py @@ -311,3 +311,77 @@ def test_check_enum_for_list_failed(logger): config.init_values() check_confval_types(None, config) assert logger.warning.called + + +nitpick_warnings = [ + "WARNING: py:const reference target not found: prefix.anything.postfix", + "WARNING: py:class reference target not found: prefix.anything", + "WARNING: py:class reference target not found: anything.postfix", + "WARNING: js:class reference target not found: prefix.anything.postfix", +] + + +@pytest.mark.sphinx(testroot='nitpicky-warnings') +def test_nitpick_base(app, status, warning): + app.builder.build_all() + + warning = warning.getvalue().strip().split('\n') + assert len(warning) == len(nitpick_warnings) + for actual, expected in zip(warning, nitpick_warnings): + assert expected in actual + + +@pytest.mark.sphinx(testroot='nitpicky-warnings', confoverrides={ + 'nitpick_ignore': [ + ('py:const', 'prefix.anything.postfix'), + ('py:class', 'prefix.anything'), + ('py:class', 'anything.postfix'), + ('js:class', 'prefix.anything.postfix'), + ], +}) +def test_nitpick_ignore(app, status, warning): + app.builder.build_all() + assert not len(warning.getvalue().strip()) + + +@pytest.mark.sphinx(testroot='nitpicky-warnings', confoverrides={ + 'nitpick_ignore_regex': [ + (r'py:.*', r'.*postfix'), + (r'.*:class', r'prefix.*'), + ] +}) +def test_nitpick_ignore_regex1(app, status, warning): + app.builder.build_all() + assert not len(warning.getvalue().strip()) + + +@pytest.mark.sphinx(testroot='nitpicky-warnings', confoverrides={ + 'nitpick_ignore_regex': [ + (r'py:.*', r'prefix.*'), + (r'.*:class', r'.*postfix'), + ] +}) +def test_nitpick_ignore_regex2(app, status, warning): + app.builder.build_all() + assert not len(warning.getvalue().strip()) + + +@pytest.mark.sphinx(testroot='nitpicky-warnings', confoverrides={ + 'nitpick_ignore_regex': [ + # None of these should match + (r'py:', r'.*'), + (r':class', r'.*'), + (r'', r'.*'), + (r'.*', r'anything'), + (r'.*', r'prefix'), + (r'.*', r'postfix'), + (r'.*', r''), + ] +}) +def test_nitpick_ignore_regex_fullmatch(app, status, warning): + app.builder.build_all() + + warning = warning.getvalue().strip().split('\n') + assert len(warning) == len(nitpick_warnings) + for actual, expected in zip(warning, nitpick_warnings): + assert expected in actual diff --git a/tests/test_domain_std.py b/tests/test_domain_std.py index 9d9e27bd0..011c82f6a 100644 --- a/tests/test_domain_std.py +++ b/tests/test_domain_std.py @@ -324,6 +324,23 @@ def test_cmdoption(app): assert domain.progoptions[('ls', '-l')] == ('index', 'cmdoption-ls-l') +def test_cmdoption_for_None(app): + text = (".. program:: ls\n" + ".. program:: None\n" + "\n" + ".. option:: -l\n") + domain = app.env.get_domain('std') + doctree = restructuredtext.parse(app, text) + assert_node(doctree, (addnodes.index, + [desc, ([desc_signature, ([desc_name, "-l"], + [desc_addname, ()])], + [desc_content, ()])])) + assert_node(doctree[0], addnodes.index, + entries=[('pair', 'command line option; -l', 'cmdoption-l', '', None)]) + assert (None, '-l') in domain.progoptions + assert domain.progoptions[(None, '-l')] == ('index', 'cmdoption-l') + + def test_multiple_cmdoptions(app): text = (".. program:: cmd\n" "\n" diff --git a/tests/test_ext_autodoc.py b/tests/test_ext_autodoc.py index 2becf5de2..4c16886b3 100644 --- a/tests/test_ext_autodoc.py +++ b/tests/test_ext_autodoc.py @@ -736,6 +736,34 @@ def test_autodoc_undoc_members(app): @pytest.mark.sphinx('html', testroot='ext-autodoc') +def test_autodoc_undoc_members_for_metadata_only(app): + # metadata only member is not displayed + options = {"members": None} + actual = do_autodoc(app, 'module', 'target.metadata', options) + assert list(actual) == [ + '', + '.. py:module:: target.metadata', + '', + ] + + # metadata only member is displayed when undoc-member given + options = {"members": None, + "undoc-members": None} + actual = do_autodoc(app, 'module', 'target.metadata', options) + assert list(actual) == [ + '', + '.. py:module:: target.metadata', + '', + '', + '.. py:function:: foo()', + ' :module: target.metadata', + '', + ' :meta metadata-only-docstring:', + '', + ] + + +@pytest.mark.sphinx('html', testroot='ext-autodoc') def test_autodoc_inherited_members(app): options = {"members": None, "inherited-members": None} @@ -2080,6 +2108,7 @@ def test_singledispatch(app): '', '', '.. py:function:: func(arg, kwarg=None)', + ' func(arg: float, kwarg=None)', ' func(arg: int, kwarg=None)', ' func(arg: str, kwarg=None)', ' :module: target.singledispatch', @@ -2107,6 +2136,7 @@ def test_singledispatchmethod(app): '', '', ' .. py:method:: Foo.meth(arg, kwarg=None)', + ' Foo.meth(arg: float, kwarg=None)', ' Foo.meth(arg: int, kwarg=None)', ' Foo.meth(arg: str, kwarg=None)', ' :module: target.singledispatchmethod', @@ -2125,6 +2155,7 @@ def test_singledispatchmethod_automethod(app): assert list(actual) == [ '', '.. py:method:: Foo.meth(arg, kwarg=None)', + ' Foo.meth(arg: float, kwarg=None)', ' Foo.meth(arg: int, kwarg=None)', ' Foo.meth(arg: str, kwarg=None)', ' :module: target.singledispatchmethod', diff --git a/tests/test_ext_autodoc_autoclass.py b/tests/test_ext_autodoc_autoclass.py index d879f8e14..096dc9397 100644 --- a/tests/test_ext_autodoc_autoclass.py +++ b/tests/test_ext_autodoc_autoclass.py @@ -264,6 +264,53 @@ def test_show_inheritance_for_subclass_of_generic_type(app): ] +@pytest.mark.sphinx('html', testroot='ext-autodoc') +def test_class_doc_from_class(app): + options = {"members": None, + "class-doc-from": "class"} + actual = do_autodoc(app, 'class', 'target.autoclass_content.C', options) + assert list(actual) == [ + '', + '.. py:class:: C()', + ' :module: target.autoclass_content', + '', + ' A class having __init__, no __new__', + '', + ] + + +@pytest.mark.sphinx('html', testroot='ext-autodoc') +def test_class_doc_from_init(app): + options = {"members": None, + "class-doc-from": "init"} + actual = do_autodoc(app, 'class', 'target.autoclass_content.C', options) + assert list(actual) == [ + '', + '.. py:class:: C()', + ' :module: target.autoclass_content', + '', + ' __init__ docstring', + '', + ] + + +@pytest.mark.sphinx('html', testroot='ext-autodoc') +def test_class_doc_from_both(app): + options = {"members": None, + "class-doc-from": "both"} + actual = do_autodoc(app, 'class', 'target.autoclass_content.C', options) + assert list(actual) == [ + '', + '.. py:class:: C()', + ' :module: target.autoclass_content', + '', + ' A class having __init__, no __new__', + '', + ' __init__ docstring', + '', + ] + + def test_class_alias(app): def autodoc_process_docstring(*args): """A handler always raises an error. diff --git a/tests/test_ext_autodoc_autofunction.py b/tests/test_ext_autodoc_autofunction.py index 615091889..ca2429b5e 100644 --- a/tests/test_ext_autodoc_autofunction.py +++ b/tests/test_ext_autodoc_autofunction.py @@ -119,6 +119,7 @@ def test_singledispatch(app): assert list(actual) == [ '', '.. py:function:: func(arg, kwarg=None)', + ' func(arg: float, kwarg=None)', ' func(arg: int, kwarg=None)', ' func(arg: str, kwarg=None)', ' :module: target.singledispatch', diff --git a/tests/test_ext_autodoc_automodule.py b/tests/test_ext_autodoc_automodule.py index 3332704bb..59296a981 100644 --- a/tests/test_ext_autodoc_automodule.py +++ b/tests/test_ext_autodoc_automodule.py @@ -29,6 +29,95 @@ def test_empty_all(app): ] +@pytest.mark.sphinx('html', testroot='ext-autodoc') +def test_automodule(app): + options = {'members': None} + actual = do_autodoc(app, 'module', 'target.module', options) + assert list(actual) == [ + '', + '.. py:module:: target.module', + '', + '', + '.. py:data:: annotated', + ' :module: target.module', + ' :type: int', + '', + ' docstring', + '', + '', + '.. py:data:: documented', + ' :module: target.module', + ' :value: 1', + '', + ' docstring', + '', + ] + + +@pytest.mark.sphinx('html', testroot='ext-autodoc') +def test_automodule_undoc_members(app): + options = {'members': None, + 'undoc-members': None} + actual = do_autodoc(app, 'module', 'target.module', options) + assert list(actual) == [ + '', + '.. py:module:: target.module', + '', + '', + '.. py:data:: annotated', + ' :module: target.module', + ' :type: int', + '', + ' docstring', + '', + '', + '.. py:data:: documented', + ' :module: target.module', + ' :value: 1', + '', + ' docstring', + '', + '', + '.. py:data:: undoc_annotated', + ' :module: target.module', + ' :type: int', + '', + ] + + +@pytest.mark.sphinx('html', testroot='ext-autodoc') +def test_automodule_special_members(app): + options = {'members': None, + 'special-members': None} + actual = do_autodoc(app, 'module', 'target.module', options) + assert list(actual) == [ + '', + '.. py:module:: target.module', + '', + '', + '.. py:data:: __documented_special__', + ' :module: target.module', + ' :value: 1', + '', + ' docstring', + '', + '', + '.. py:data:: annotated', + ' :module: target.module', + ' :type: int', + '', + ' docstring', + '', + '', + '.. py:data:: documented', + ' :module: target.module', + ' :value: 1', + '', + ' docstring', + '', + ] + + @pytest.mark.sphinx('html', testroot='ext-autodoc', confoverrides={'autodoc_mock_imports': ['missing_module', 'missing_package1', diff --git a/tests/test_ext_autodoc_configs.py b/tests/test_ext_autodoc_configs.py index bc8c01fbd..04d35e335 100644 --- a/tests/test_ext_autodoc_configs.py +++ b/tests/test_ext_autodoc_configs.py @@ -792,27 +792,27 @@ def test_autodoc_typehints_description_for_invalid_node(app): def test_autodoc_type_aliases(app): # default options = {"members": None} - actual = do_autodoc(app, 'module', 'target.annotations', options) + actual = do_autodoc(app, 'module', 'target.autodoc_type_aliases', options) assert list(actual) == [ '', - '.. py:module:: target.annotations', + '.. py:module:: target.autodoc_type_aliases', '', '', '.. py:class:: Foo()', - ' :module: target.annotations', + ' :module: target.autodoc_type_aliases', '', ' docstring', '', '', ' .. py:attribute:: Foo.attr1', - ' :module: target.annotations', + ' :module: target.autodoc_type_aliases', ' :type: int', '', ' docstring', '', '', ' .. py:attribute:: Foo.attr2', - ' :module: target.annotations', + ' :module: target.autodoc_type_aliases', ' :type: int', '', ' docstring', @@ -820,26 +820,32 @@ def test_autodoc_type_aliases(app): '', '.. py:function:: mult(x: int, y: int) -> int', ' mult(x: float, y: float) -> float', - ' :module: target.annotations', + ' :module: target.autodoc_type_aliases', + '', + ' docstring', + '', + '', + '.. py:function:: read(r: _io.BytesIO) -> _io.StringIO', + ' :module: target.autodoc_type_aliases', '', ' docstring', '', '', '.. py:function:: sum(x: int, y: int) -> int', - ' :module: target.annotations', + ' :module: target.autodoc_type_aliases', '', ' docstring', '', '', '.. py:data:: variable', - ' :module: target.annotations', + ' :module: target.autodoc_type_aliases', ' :type: int', '', ' docstring', '', '', '.. py:data:: variable2', - ' :module: target.annotations', + ' :module: target.autodoc_type_aliases', ' :type: int', ' :value: None', '', @@ -848,28 +854,29 @@ def test_autodoc_type_aliases(app): ] # define aliases - app.config.autodoc_type_aliases = {'myint': 'myint'} - actual = do_autodoc(app, 'module', 'target.annotations', options) + app.config.autodoc_type_aliases = {'myint': 'myint', + 'io.StringIO': 'my.module.StringIO'} + actual = do_autodoc(app, 'module', 'target.autodoc_type_aliases', options) assert list(actual) == [ '', - '.. py:module:: target.annotations', + '.. py:module:: target.autodoc_type_aliases', '', '', '.. py:class:: Foo()', - ' :module: target.annotations', + ' :module: target.autodoc_type_aliases', '', ' docstring', '', '', ' .. py:attribute:: Foo.attr1', - ' :module: target.annotations', + ' :module: target.autodoc_type_aliases', ' :type: myint', '', ' docstring', '', '', ' .. py:attribute:: Foo.attr2', - ' :module: target.annotations', + ' :module: target.autodoc_type_aliases', ' :type: myint', '', ' docstring', @@ -877,26 +884,32 @@ def test_autodoc_type_aliases(app): '', '.. py:function:: mult(x: myint, y: myint) -> myint', ' mult(x: float, y: float) -> float', - ' :module: target.annotations', + ' :module: target.autodoc_type_aliases', + '', + ' docstring', + '', + '', + '.. py:function:: read(r: _io.BytesIO) -> my.module.StringIO', + ' :module: target.autodoc_type_aliases', '', ' docstring', '', '', '.. py:function:: sum(x: myint, y: myint) -> myint', - ' :module: target.annotations', + ' :module: target.autodoc_type_aliases', '', ' docstring', '', '', '.. py:data:: variable', - ' :module: target.annotations', + ' :module: target.autodoc_type_aliases', ' :type: myint', '', ' docstring', '', '', '.. py:data:: variable2', - ' :module: target.annotations', + ' :module: target.autodoc_type_aliases', ' :type: myint', ' :value: None', '', @@ -911,10 +924,10 @@ def test_autodoc_type_aliases(app): confoverrides={'autodoc_typehints': "description", 'autodoc_type_aliases': {'myint': 'myint'}}) def test_autodoc_typehints_description_and_type_aliases(app): - (app.srcdir / 'annotations.rst').write_text('.. autofunction:: target.annotations.sum') + (app.srcdir / 'autodoc_type_aliases.rst').write_text('.. autofunction:: target.autodoc_type_aliases.sum') app.build() - context = (app.outdir / 'annotations.txt').read_text() - assert ('target.annotations.sum(x, y)\n' + context = (app.outdir / 'autodoc_type_aliases.txt').read_text() + assert ('target.autodoc_type_aliases.sum(x, y)\n' '\n' ' docstring\n' '\n' diff --git a/tests/test_ext_autosummary.py b/tests/test_ext_autosummary.py index 5d92b6afd..71868d492 100644 --- a/tests/test_ext_autosummary.py +++ b/tests/test_ext_autosummary.py @@ -161,6 +161,7 @@ def test_get_items_summary(make_app, app_params): 'emptyLine': "This is the real summary", 'module_attr': 'This is a module attribute', 'C.class_attr': 'This is a class attribute', + 'C.instance_attr': 'This is an instance attribute', 'C.prop_attr1': 'This is a function docstring', 'C.prop_attr2': 'This is a attribute docstring', 'C.C2': 'This is a nested inner class docstring', @@ -329,6 +330,7 @@ def test_autosummary_generate(app, status, warning): ' ~Foo.CONSTANT3\n' ' ~Foo.CONSTANT4\n' ' ~Foo.baz\n' + ' ~Foo.value\n' ' \n' in Foo) FooBar = (app.srcdir / 'generated' / 'autosummary_dummy_module.Foo.Bar.rst').read_text() diff --git a/tests/test_util_docstrings.py b/tests/test_util_docstrings.py index 543feca2a..2d406b81c 100644 --- a/tests/test_util_docstrings.py +++ b/tests/test_util_docstrings.py @@ -8,31 +8,48 @@ :license: BSD, see LICENSE for details. """ -from sphinx.util.docstrings import extract_metadata, prepare_commentdoc, prepare_docstring +from sphinx.util.docstrings import prepare_commentdoc, prepare_docstring, separate_metadata -def test_extract_metadata(): - metadata = extract_metadata(":meta foo: bar\n" - ":meta baz:\n") +def test_separate_metadata(): + # metadata only + text = (":meta foo: bar\n" + ":meta baz:\n") + docstring, metadata = separate_metadata(text) + assert docstring == '' assert metadata == {'foo': 'bar', 'baz': ''} + # non metadata field list item + text = (":meta foo: bar\n" + ":param baz:\n") + docstring, metadata = separate_metadata(text) + assert docstring == ':param baz:\n' + assert metadata == {'foo': 'bar'} + # field_list like text following just after paragaph is not a field_list - metadata = extract_metadata("blah blah blah\n" - ":meta foo: bar\n" - ":meta baz:\n") + text = ("blah blah blah\n" + ":meta foo: bar\n" + ":meta baz:\n") + docstring, metadata = separate_metadata(text) + assert docstring == text assert metadata == {} # field_list like text following after blank line is a field_list - metadata = extract_metadata("blah blah blah\n" - "\n" - ":meta foo: bar\n" - ":meta baz:\n") + text = ("blah blah blah\n" + "\n" + ":meta foo: bar\n" + ":meta baz:\n") + docstring, metadata = separate_metadata(text) + assert docstring == "blah blah blah\n\n" assert metadata == {'foo': 'bar', 'baz': ''} # non field_list item breaks field_list - metadata = extract_metadata(":meta foo: bar\n" - "blah blah blah\n" - ":meta baz:\n") + text = (":meta foo: bar\n" + "blah blah blah\n" + ":meta baz:\n") + docstring, metadata = separate_metadata(text) + assert docstring == ("blah blah blah\n" + ":meta baz:\n") assert metadata == {'foo': 'bar'} diff --git a/tests/test_util_inspect.py b/tests/test_util_inspect.py index 7b86c6ade..95e36ba14 100644 --- a/tests/test_util_inspect.py +++ b/tests/test_util_inspect.py @@ -19,7 +19,26 @@ import _testcapi import pytest from sphinx.util import inspect -from sphinx.util.inspect import stringify_signature +from sphinx.util.inspect import TypeAliasNamespace, stringify_signature + + +def test_TypeAliasNamespace(): + import logging.config + type_alias = TypeAliasNamespace({'logging.Filter': 'MyFilter', + 'logging.Handler': 'MyHandler', + 'logging.handlers.SyslogHandler': 'MySyslogHandler'}) + + assert type_alias['logging'].Filter == 'MyFilter' + assert type_alias['logging'].Handler == 'MyHandler' + assert type_alias['logging'].handlers.SyslogHandler == 'MySyslogHandler' + assert type_alias['logging'].Logger == logging.Logger + assert type_alias['logging'].config == logging.config + + with pytest.raises(KeyError): + assert type_alias['log'] + + with pytest.raises(KeyError): + assert type_alias['unknown'] def test_signature(): @@ -183,10 +202,7 @@ def test_signature_annotations(): # Instance annotations sig = inspect.signature(f11) - if sys.version_info < (3, 10): - assert stringify_signature(sig) == '(x: CustomAnnotation, y: 123) -> None' - else: - assert stringify_signature(sig) == '(x: CustomAnnotation(), y: 123) -> None' + assert stringify_signature(sig) == '(x: CustomAnnotation, y: 123) -> None' # tuple with more than two items sig = inspect.signature(f12) |
