diff options
author | Takeshi KOMIYA <i.tkomiya@gmail.com> | 2021-05-03 22:33:12 +0900 |
---|---|---|
committer | Takeshi KOMIYA <i.tkomiya@gmail.com> | 2021-05-03 22:33:12 +0900 |
commit | 3027a2f8675e6140f2d8b83d19bec4159a09af5c (patch) | |
tree | d4db4de45fe517f165425014dd1d90fc9bb6816e | |
parent | f5e7b9b815977790f940ffcc374550bb70fc99d4 (diff) | |
parent | f31af4b8158e6142d918366aa0026e40575af914 (diff) | |
download | sphinx-git-3027a2f8675e6140f2d8b83d19bec4159a09af5c.tar.gz |
Merge branch '4.x'
28 files changed, 467 insertions, 152 deletions
diff --git a/.github/workflows/main.yml b/.github/workflows/main.yml index f49117e1a..f32468576 100644 --- a/.github/workflows/main.yml +++ b/.github/workflows/main.yml @@ -21,7 +21,7 @@ jobs: docutils: du16 - name: py39 python: 3.9 - docutils: du16 + docutils: du17 coverage: "--cov ./ --cov-append --cov-config setup.cfg" # - name: py310-dev # python: 3.10-dev @@ -31,20 +31,34 @@ Incompatible changes Deprecated ---------- +* ``sphinx.util.docstrings.extract_metadata()`` + Features added -------------- +* #8107: autodoc: Add ``class-doc-from`` option to :rst:dir:`autoclass` + directive to control the content of the specific class like + :confval:`autoclass_content` +* #8588: autodoc: :confval:`autodoc_type_aliases` now supports dotted name. It + allows you to define an alias for a class with module name like + ``foo.bar.BazClass`` * #9129: html search: Show search summaries when html_copy_source = False +* #9120: html theme: Eliminate prompt characters of code-block from copyable + text * #9097: Optimize the paralell build Bugs fixed ---------- +* #8872: autodoc: stacked singledispatches are wrongly rendered +* #8597: autodoc: a docsting having metadata only should be treated as + undocumented + Testing -------- -Release 4.0.0 beta2 (in development) +Release 4.0.0 beta3 (in development) ==================================== Dependencies @@ -53,15 +67,37 @@ Dependencies Incompatible changes -------------------- -* #9023: Change the CSS classes on :rst:role:`cpp:expr` and - :rst:role:`cpp:texpr`. - Deprecated ---------- Features added -------------- +Bugs fixed +---------- + +Testing +-------- + +Release 4.0.0 beta2 (released Apr 29, 2021) +=========================================== + +Dependencies +------------ + +* Support docutils-0.17. Please notice it changes the output of HTML builder. + Some themes do not support it, and you need to update your custom CSS to + upgrade it. + +Incompatible changes +-------------------- + +* #9023: Change the CSS classes on :rst:role:`cpp:expr` and + :rst:role:`cpp:texpr`. + +Features added +-------------- + * #8818: autodoc: Super class having ``Any`` arguments causes nit-picky warning * #9095: autodoc: TypeError is raised on processing broken metaclass * #9110: autodoc: metadata of GenericAlias is not rendered as a reference in @@ -81,9 +117,6 @@ Bugs fixed * C, C++, fix ``KeyError`` when an ``alias`` directive is the first C/C++ directive in a file with another C/C++ directive later. -Testing --------- - Release 4.0.0 beta1 (released Apr 12, 2021) =========================================== @@ -222,24 +255,6 @@ Bugs fixed Release 3.5.5 (in development) ============================== -Dependencies ------------- - -Incompatible changes --------------------- - -Deprecated ----------- - -Features added --------------- - -Bugs fixed ----------- - -Testing --------- - Release 3.5.4 (released Apr 11, 2021) ===================================== @@ -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/doc/extdev/deprecated.rst b/doc/extdev/deprecated.rst index 9e17b9fb4..514a80541 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 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/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). @@ -21,9 +21,10 @@ install_requires = [ 'sphinxcontrib-htmlhelp', 'sphinxcontrib-serializinghtml', 'sphinxcontrib-qthelp', - 'Jinja2>=2.3', + 'Jinja2>=2.3,<3.0', + 'MarkupSafe<2.0', 'Pygments>=2.0', - 'docutils>=0.14,<0.17', + 'docutils>=0.14,<0.18', 'snowballstemmer>=1.1', 'babel>=1.3', 'alabaster>=0.7,<0.8', diff --git a/sphinx/builders/html/__init__.py b/sphinx/builders/html/__init__.py index a78d54a16..87b9c5c45 100644 --- a/sphinx/builders/html/__init__.py +++ b/sphinx/builders/html/__init__.py @@ -1166,7 +1166,11 @@ def setup_js_tag_helper(app: Sphinx, pagename: str, templatename: str, else: # str value (old styled) attrs.append('src="%s"' % pathto(js, resource=True)) - return '<script %s>%s</script>' % (' '.join(attrs), body) + + if attrs: + return '<script %s>%s</script>' % (' '.join(attrs), body) + else: + return '<script>%s</script>' % body context['js_tag'] = js_tag 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/ext/autodoc/__init__.py b/sphinx/ext/autodoc/__init__.py index c92709deb..ff6475c94 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() @@ -722,9 +730,9 @@ class Documenter: # hack for ClassDocumenter to inject docstring via ObjectMember doc = obj.docstring + doc, metadata = separate_metadata(doc) has_doc = bool(doc) - metadata = extract_metadata(doc) if 'private' in metadata: # consider a member private if docstring has "private" metadata isprivate = True @@ -1320,12 +1328,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 +1358,34 @@ class FunctionDocumenter(DocstringSignatureMixin, ModuleLevelDocumenter): # typ return overload.replace(parameters=parameters) - def annotate_to_first_argument(self, func: Callable, typ: Type) -> None: + def annotate_to_first_argument(self, func: Callable, typ: Type) -> Optional[Callable]: """Annotate type hint to the first argument of function if needed.""" try: sig = inspect.signature(func, type_aliases=self.config.autodoc_type_aliases) except TypeError as exc: logger.warning(__("Failed to get a function signature for %s: %s"), self.fullname, exc) - return + return None except ValueError: - return + return None if len(sig.parameters) == 0: - return + return None + + def dummy(): + pass params = list(sig.parameters.values()) if params[0].annotation is Parameter.empty: params[0] = params[0].replace(annotation=typ) try: - func.__signature__ = sig.replace(parameters=params) # type: ignore + dummy.__signature__ = sig.replace(parameters=params) # type: ignore + return dummy except (AttributeError, TypeError): # failed to update signature (ex. built-in or extension types) - return + return None + else: + return None class DecoratorDocumenter(FunctionDocumenter): @@ -1417,6 +1431,7 @@ class ClassDocumenter(DocstringSignatureMixin, ModuleLevelDocumenter): # type: 'show-inheritance': bool_option, 'member-order': member_order_option, 'exclude-members': exclude_members_option, 'private-members': members_option, 'special-members': members_option, + 'class-doc-from': class_doc_from_option, } _signature_class: Any = None @@ -1651,7 +1666,7 @@ class ClassDocumenter(DocstringSignatureMixin, ModuleLevelDocumenter): # type: if lines is not None: return lines - content = self.config.autoclass_content + classdoc_from = self.options.get('class-doc-from', self.config.autoclass_content) docstrings = [] attrdocstring = self.get_attr(self.object, '__doc__', None) @@ -1660,7 +1675,7 @@ class ClassDocumenter(DocstringSignatureMixin, ModuleLevelDocumenter): # type: # for classes, what the "docstring" is can be controlled via a # config value; the default is only the class docstring - if content in ('both', 'init'): + if classdoc_from in ('both', 'init'): __init__ = self.get_attr(self.object, '__init__', None) initdocstring = getdoc(__init__, self.get_attr, self.config.autodoc_inherit_docstrings, @@ -1682,7 +1697,7 @@ class ClassDocumenter(DocstringSignatureMixin, ModuleLevelDocumenter): # type: initdocstring.strip() == object.__new__.__doc__)): # for !pypy initdocstring = None if initdocstring: - if content == 'init': + if classdoc_from == 'init': docstrings = [initdocstring] else: docstrings.append(initdocstring) @@ -1918,7 +1933,7 @@ class DataDocumenter(GenericAliasMixin, NewTypeMixin, TypeVarMixin, return True else: doc = self.get_doc() - metadata = extract_metadata('\n'.join(sum(doc, []))) + docstring, metadata = separate_metadata('\n'.join(sum(doc, []))) if 'hide-value' in metadata: return True @@ -2109,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): @@ -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 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/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/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/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/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/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/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-root/conf.py b/tests/roots/test-root/conf.py index 34cafa767..687445a70 100644 --- a/tests/roots/test-root/conf.py +++ b/tests/roots/test-root/conf.py @@ -42,7 +42,7 @@ latex_additional_files = ['svgimg.svg'] coverage_c_path = ['special/*.h'] coverage_c_regexes = {'function': r'^PyAPI_FUNC\(.*\)\s+([^_][\w_]+)'} -extlinks = {'issue': ('http://bugs.python.org/issue%s', 'issue '), +extlinks = {'issue': ('http://bugs.python.org/issue%s', 'issue %s'), 'pyurl': ('http://python.org/%s', None)} # modify tags from conf.py 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_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_math.py b/tests/test_ext_math.py index bd124c8c6..ebe2c0f38 100644 --- a/tests/test_ext_math.py +++ b/tests/test_ext_math.py @@ -215,11 +215,23 @@ def test_math_compat(app, status, warning): @pytest.mark.sphinx('html', testroot='ext-math', confoverrides={'extensions': ['sphinx.ext.mathjax'], - 'mathjax_config': {'extensions': ['tex2jax.js']}}) -def test_mathjax_config(app, status, warning): + 'mathjax3_config': {'extensions': ['tex2jax.js']}}) +def test_mathjax3_config(app, status, warning): app.builder.build_all() content = (app.outdir / 'index.html').read_text() + assert MATHJAX_URL in content + assert ('<script>window.MathJax = {"extensions": ["tex2jax.js"]}</script>' in content) + + +@pytest.mark.sphinx('html', testroot='ext-math', + confoverrides={'extensions': ['sphinx.ext.mathjax'], + 'mathjax2_config': {'extensions': ['tex2jax.js']}}) +def test_mathjax2_config(app, status, warning): + app.builder.build_all() + + content = (app.outdir / 'index.html').read_text() + assert MATHJAX_URL in content assert ('<script type="text/x-mathjax-config">' 'MathJax.Hub.Config({"extensions": ["tex2jax.js"]})' '</script>' in content) 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..fbf243ba1 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(): |