diff --git a/CHANGES b/CHANGES index c3be86a31..9b777a51c 100644 --- a/CHANGES +++ b/CHANGES @@ -122,6 +122,7 @@ Features added :event:`html-page-context` event * #6550: html: Allow to use HTML permalink texts via :confval:`html_permalinks_icon` +* #1638: html: Add permalink icons to glossary terms * #8649: imgconverter: Skip availability check if builder supports the image type * #8573: napoleon: Allow to change the style of custom sections using @@ -130,6 +131,7 @@ Features added references when :confval:`napoleon_preprocess_types` enabled * #6241: mathjax: Include mathjax.js only on the document using equations * #8651: std domain: cross-reference for a rubric having inline item is broken +* #7642: std domain: Optimize case-insensitive match of term * #8681: viewcode: Support incremental build * #8132: Add :confval:`project_copyright` as an alias of :confval:`copyright` * #207: Now :confval:`highlight_language` supports multiple languages @@ -150,19 +152,26 @@ Bugs fixed * #8652: autodoc: All variable comments in the module are ignored if the module contains invalid type comments * #8693: autodoc: Default values for overloaded functions are rendered as string +* #8134: autodoc: crashes when mocked decorator takes arguments * #8306: autosummary: mocked modules are documented as empty page when using :recursive: option * #8618: html: kbd role produces incorrect HTML when compound-key separators (-, + or ^) are used as keystrokes * #8629: html: A type warning for html_use_opensearch is shown twice * #8714: html: kbd role with "Caps Lock" rendered incorrectly +* #8123: html search: fix searching for terms containing + (Requires a custom + search language that does not split on +) * #8665: html theme: Could not override globaltoc_maxdepth in theme.conf +* #8745: i18n: crashes with KeyError when translation message adds a new auto + footnote reference * #4304: linkcheck: Fix race condition that could lead to checking the availability of the same URL twice +* #7118: sphinx-quickstart: questionare got Mojibake if libreadline unavailable * #8094: texinfo: image files on the different directory with document are not copied * #8720: viewcode: module pages are generated for epub on incremental build * #8704: viewcode: anchors are generated in incremental build after singlehtml +* #8756: viewcode: highlighted code is generated even if not referenced * #8671: :confval:`highlight_options` is not working * #8341: C, fix intersphinx lookup types for names in declarations. * C, C++: in general fix intersphinx and role lookup types. @@ -170,8 +179,14 @@ Bugs fixed * #8683: :confval:`html_last_updated_fmt` generates wrong time zone for %Z * #1112: ``download`` role creates duplicated copies when relative path is specified +* #7576: LaTeX with French babel and memoir crash: "Illegal parameter number + in definition of ``\FNH@prefntext``" +* #8214: LaTeX: The :rst:role:`index` role and the glossary generate duplicate + entries in the LaTeX index (if both used for same term) * #8735: LaTeX: wrong internal links in pdf to captioned code-blocks when :confval:`numfig` is not True +* #8442: LaTeX: some indexed terms are ignored when using xelatex engine + (or pdflatex and :confval:`latex_use_xindy` set to True) with memoir class Testing -------- diff --git a/doc/changes.rst b/doc/changes.rst index b4872784a..829c7f7ed 100644 --- a/doc/changes.rst +++ b/doc/changes.rst @@ -8,4 +8,9 @@ Changelog ========= +.. raw:: latex + + \hypersetup{bookmarksdepth=1}% pdf bookmarks + \addtocontents{toc}{\protect\setcounter{tocdepth}{1}}% + .. include:: ../CHANGES diff --git a/doc/conf.py b/doc/conf.py index deccc2e9a..53f036d3e 100644 --- a/doc/conf.py +++ b/doc/conf.py @@ -58,8 +58,27 @@ latex_documents = [('contents', 'sphinx.tex', 'Sphinx Documentation', latex_logo = '_static/sphinx.png' latex_elements = { 'fontenc': r'\usepackage[LGR,X2,T1]{fontenc}', - 'passoptionstopackages': '\\PassOptionsToPackage{svgnames}{xcolor}', - 'preamble': '\\DeclareUnicodeCharacter{229E}{\\ensuremath{\\boxplus}}', + 'fontpkg': r''' +\usepackage[sc]{mathpazo} +\usepackage[scaled]{helvet} +\usepackage{courier} +\substitutefont{LGR}{\rmdefault}{cmr} +\substitutefont{LGR}{\sfdefault}{cmss} +\substitutefont{LGR}{\ttdefault}{cmtt} +\substitutefont{X2}{\rmdefault}{cmr} +\substitutefont{X2}{\sfdefault}{cmss} +\substitutefont{X2}{\ttdefault}{cmtt} +''', + 'passoptionstopackages': r''' +\PassOptionsToPackage{svgnames}{xcolor} +\PassOptionsToPackage{bookmarksdepth=3}{hyperref}% depth of pdf bookmarks +''', + 'preamble': r''' +\DeclareUnicodeCharacter{229E}{\ensuremath{\boxplus}} +\setcounter{tocdepth}{3}% depth of what is kept from toc file +\setcounter{secnumdepth}{1}% depth of section numbering +''', + 'fvset': '\\fvset{fontsize=auto}', # fix missing index entry due to RTD doing only once pdflatex after makeindex 'printindex': r''' \IfFileExists{\jobname.ind} diff --git a/doc/usage/extensions/index.rst b/doc/usage/extensions/index.rst index 3d076fb3a..0d446cb61 100644 --- a/doc/usage/extensions/index.rst +++ b/doc/usage/extensions/index.rst @@ -46,14 +46,15 @@ organization. If you wish to include your extension in this organization, simply follow the instructions provided in the `github-administration`__ project. This is optional and there are several extensions hosted elsewhere. The `awesome-sphinxdoc`__ project contains a curated list of Sphinx packages, -and many packages use the ``Framework :: Sphinx :: Extension`` and -``Framework :: Sphinx :: Theme`` `trove classifiers`__ for Sphinx extensions -and themes, respectively. +and many packages use the `Framework :: Sphinx :: Extension`__ and +`Framework :: Sphinx :: Theme`__ trove classifiers for Sphinx extensions and +themes, respectively. .. __: https://github.com/sphinx-contrib/ .. __: https://github.com/sphinx-contrib/github-administration .. __: https://github.com/yoloseem/awesome-sphinxdoc -.. __: https://pypi.org/classifiers/ +.. __: https://pypi.org/search/?c=Framework+%3A%3A+Sphinx+%3A%3A+Extension +.. __: https://pypi.org/search/?c=Framework+%3A%3A+Sphinx+%3A%3A+Theme Where to put your own extensions? ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ diff --git a/karma.conf.js b/karma.conf.js index d0c11aa21..82be18a71 100644 --- a/karma.conf.js +++ b/karma.conf.js @@ -18,6 +18,7 @@ module.exports = function(config) { 'sphinx/themes/basic/static/underscore.js', 'sphinx/themes/basic/static/jquery.js', 'sphinx/themes/basic/static/doctools.js', + 'sphinx/themes/basic/static/searchtools.js', 'tests/js/*.js' ], diff --git a/sphinx/application.py b/sphinx/application.py index acc512694..c4fc9c212 100644 --- a/sphinx/application.py +++ b/sphinx/application.py @@ -494,31 +494,34 @@ class Sphinx: """Register a configuration value. This is necessary for Sphinx to recognize new values and set default - values accordingly. The *name* should be prefixed with the extension - name, to avoid clashes. The *default* value can be any Python object. - The string value *rebuild* must be one of those values: + values accordingly. - * ``'env'`` if a change in the setting only takes effect when a - document is parsed -- this means that the whole environment must be - rebuilt. - * ``'html'`` if a change in the setting needs a full rebuild of HTML - documents. - * ``''`` if a change in the setting will not need any special rebuild. - The *types* value takes a list of types that describes the type of - configuration value. For example, ``[str]`` is used to describe a - configuration that takes string value. + :param name: The name of configuration value. It is recommended to be prefixed + with the extension name (ex. ``html_logo``, ``epub_title``) + :param default: The default value of the configuration. + :param rebuild: The condition of rebuild. It must be one of those values: - .. versionchanged:: 0.6 - Changed *rebuild* from a simple boolean (equivalent to ``''`` or - ``'env'``) to a string. However, booleans are still accepted and - converted internally. + * ``'env'`` if a change in the setting only takes effect when a + document is parsed -- this means that the whole environment must be + rebuilt. + * ``'html'`` if a change in the setting needs a full rebuild of HTML + documents. + * ``''`` if a change in the setting will not need any special rebuild. + :param types: The type of configuration value. A list of types can be specified. For + example, ``[str]`` is used to describe a configuration that takes string + value. .. versionchanged:: 0.4 If the *default* value is a callable, it will be called with the config object as its argument in order to get the default value. This can be used to implement config values whose default depends on other values. + + .. versionchanged:: 0.6 + Changed *rebuild* from a simple boolean (equivalent to ``''`` or + ``'env'``) to a string. However, booleans are still accepted and + converted internally. """ logger.debug('[app] adding config value: %r', (name, default, rebuild) + ((types,) if types else ())) @@ -530,6 +533,8 @@ class Sphinx: """Register an event called *name*. This is needed to be able to emit it. + + :param name: The name of the event """ logger.debug('[app] adding event: %r', name) self.events.add(name) @@ -560,6 +565,11 @@ class Sphinx: This is necessary for Docutils internals. It may also be used in the future to validate nodes in the parsed documents. + :param node: A node class + :param kwargs: Visitor functions for each builder (see below) + :param override: If true, install the node forcedly even if another node is already + installed as the same name + Node visitor functions for the Sphinx HTML, LaTeX, text and manpage writers can be given as keyword arguments: the keyword should be one or more of ``'html'``, ``'latex'``, ``'text'``, ``'man'``, ``'texinfo'`` @@ -581,9 +591,6 @@ class Sphinx: Obviously, translators for which you don't specify visitor methods will choke on the node when encountered in a document to translate. - If *override* is True, the given *node* is forcedly installed even if - a node having the same name is already installed. - .. versionchanged:: 0.5 Added the support for keyword arguments giving visit functions. """ @@ -603,24 +610,21 @@ class Sphinx: Sphinx numbers the node automatically. And then the users can refer it using :rst:role:`numref`. - *figtype* is a type of enumerable nodes. Each figtypes have individual - numbering sequences. As a system figtypes, ``figure``, ``table`` and - ``code-block`` are defined. It is able to add custom nodes to these - default figtypes. It is also able to define new custom figtype if new - figtype is given. - - *title_getter* is a getter function to obtain the title of node. It - takes an instance of the enumerable node, and it must return its title - as string. The title is used to the default title of references for - :rst:role:`ref`. By default, Sphinx searches - ``docutils.nodes.caption`` or ``docutils.nodes.title`` from the node as - a title. - - Other keyword arguments are used for node visitor functions. See the - :meth:`.Sphinx.add_node` for details. - - If *override* is True, the given *node* is forcedly installed even if - a node having the same name is already installed. + :param node: A node class + :param figtype: The type of enumerable nodes. Each figtypes have individual numbering + sequences. As a system figtypes, ``figure``, ``table`` and + ``code-block`` are defined. It is able to add custom nodes to these + default figtypes. It is also able to define new custom figtype if new + figtype is given. + :param title_getter: A getter function to obtain the title of node. It takes an + instance of the enumerable node, and it must return its title as + string. The title is used to the default title of references for + :rst:role:`ref`. By default, Sphinx searches + ``docutils.nodes.caption`` or ``docutils.nodes.title`` from the + node as a title. + :param kwargs: Visitor functions for each builder (same as :meth:`add_node`) + :param override: If true, install the node forcedly even if another node is already + installed as the same name .. versionadded:: 1.4 """ @@ -679,7 +683,7 @@ class Sphinx: """Register a Docutils role. :param name: The name of role - :param cls: A role function + :param role: A role function :param override: If true, install the role forcedly even if another role is already installed as the same name @@ -720,11 +724,9 @@ class Sphinx: def add_domain(self, domain: "Type[Domain]", override: bool = False) -> None: """Register a domain. - Make the given *domain* (which must be a class; more precisely, a - subclass of :class:`~sphinx.domains.Domain`) known to Sphinx. - - If *override* is True, the given *domain* is forcedly installed even if - a domain having the same name is already installed. + :param domain: A domain class + :param override: If true, install the domain forcedly even if another domain + is already installed as the same name .. versionadded:: 1.0 .. versionchanged:: 1.8 @@ -739,8 +741,11 @@ class Sphinx: Like :meth:`add_directive`, but the directive is added to the domain named *domain*. - If *override* is True, the given *directive* is forcedly installed even if - a directive named as *name* is already installed. + :param domain: The name of target domain + :param name: A name of directive + :param cls: A directive class + :param override: If true, install the directive forcedly even if another directive + is already installed as the same name .. versionadded:: 1.0 .. versionchanged:: 1.8 @@ -755,8 +760,11 @@ class Sphinx: Like :meth:`add_role`, but the role is added to the domain named *domain*. - If *override* is True, the given *role* is forcedly installed even if - a role named as *name* is already installed. + :param domain: The name of target domain + :param name: A name of role + :param role: A role function + :param override: If true, install the role forcedly even if another role is already + installed as the same name .. versionadded:: 1.0 .. versionchanged:: 1.8 @@ -768,11 +776,12 @@ class Sphinx: ) -> None: """Register a custom index for a domain. - Add a custom *index* class to the domain named *domain*. *index* must - be a subclass of :class:`~sphinx.domains.Index`. + Add a custom *index* class to the domain named *domain*. - If *override* is True, the given *index* is forcedly installed even if - an index having the same name is already installed. + :param domain: The name of target domain + :param index: A index class + :param override: If true, install the index forcedly even if another index is + already installed as the same name .. versionadded:: 1.0 .. versionchanged:: 1.8 @@ -893,6 +902,8 @@ class Sphinx: the list of transforms that are applied after Sphinx parses a reST document. + :param transform: A transform class + .. list-table:: priority range categories for Sphinx transforms :widths: 20,80 @@ -925,6 +936,8 @@ class Sphinx: Add the standard docutils :class:`Transform` subclass *transform* to the list of transforms that are applied before Sphinx writes a document. + + :param transform: A transform class """ self.registry.add_post_transform(transform) @@ -1196,9 +1209,10 @@ class Sphinx: def add_message_catalog(self, catalog: str, locale_dir: str) -> None: """Register a message catalog. - The *catalog* is a name of catalog, and *locale_dir* is a base path - of message catalog. For more details, see - :func:`sphinx.locale.get_translation()`. + :param catalog: A name of catalog + :param locale_dir: The base path of message catalog + + For more details, see :func:`sphinx.locale.get_translation()`. .. versionadded:: 1.8 """ @@ -1209,7 +1223,7 @@ class Sphinx: def is_parallel_allowed(self, typ: str) -> bool: """Check parallel processing is allowed or not. - ``typ`` is a type of processing; ``'read'`` or ``'write'``. + :param typ: A type of processing; ``'read'`` or ``'write'``. """ if typ == 'read': attrname = 'parallel_read_safe' diff --git a/sphinx/builders/linkcheck.py b/sphinx/builders/linkcheck.py index f813922ec..106303bd6 100644 --- a/sphinx/builders/linkcheck.py +++ b/sphinx/builders/linkcheck.py @@ -118,10 +118,6 @@ class CheckExternalLinksBuilder(DummyBuilder): self._redirected = {} # type: Dict[str, Tuple[str, int]] # set a timeout for non-responding servers socket.setdefaulttimeout(5.0) - # create output file - open(path.join(self.outdir, 'output.txt'), 'w').close() - # create JSON output file - open(path.join(self.outdir, 'output.json'), 'w').close() # create queues and worker threads self.rate_limits = {} # type: Dict[str, RateLimit] @@ -435,26 +431,25 @@ class CheckExternalLinksBuilder(DummyBuilder): def write_entry(self, what: str, docname: str, filename: str, line: int, uri: str) -> None: - with open(path.join(self.outdir, 'output.txt'), 'a') as output: - output.write("%s:%s: [%s] %s\n" % (filename, line, what, uri)) + self.txt_outfile.write("%s:%s: [%s] %s\n" % (filename, line, what, uri)) def write_linkstat(self, data: dict) -> None: - with open(path.join(self.outdir, 'output.json'), 'a') as output: - output.write(json.dumps(data)) - output.write('\n') + self.json_outfile.write(json.dumps(data)) + self.json_outfile.write('\n') def finish(self) -> None: logger.info('') - n = 0 for hyperlink in self.hyperlinks.values(): self.wqueue.put(hyperlink, False) - n += 1 + total_links = len(self.hyperlinks) done = 0 - while done < n: - self.process_result(self.rqueue.get()) - done += 1 + with open(path.join(self.outdir, 'output.txt'), 'w') as self.txt_outfile,\ + open(path.join(self.outdir, 'output.json'), 'w') as self.json_outfile: + while done < total_links: + self.process_result(self.rqueue.get()) + done += 1 if self._broken: self.app.statuscode = 1 diff --git a/sphinx/cmd/quickstart.py b/sphinx/cmd/quickstart.py index 0972769ee..4ef9335a4 100644 --- a/sphinx/cmd/quickstart.py +++ b/sphinx/cmd/quickstart.py @@ -27,6 +27,7 @@ try: readline.parse_and_bind("tab: complete") USE_LIBEDIT = False except ImportError: + readline = None USE_LIBEDIT = False from docutils.utils import column_width @@ -139,8 +140,11 @@ def do_prompt(text: str, default: str = None, validator: Callable[[str], Any] = # sequence (see #5335). To avoid the problem, all prompts are not colored # on libedit. pass - else: + elif readline: + # pass input_mode=True if readline available prompt = colorize(COLOR_QUESTION, prompt, input_mode=True) + else: + prompt = colorize(COLOR_QUESTION, prompt, input_mode=False) x = term_input(prompt).strip() if default and not x: x = default diff --git a/sphinx/domains/std.py b/sphinx/domains/std.py index c4577ba63..8b10c8547 100644 --- a/sphinx/domains/std.py +++ b/sphinx/domains/std.py @@ -308,7 +308,7 @@ def make_glossary_term(env: "BuildEnvironment", textnodes: Iterable[Node], index document.note_explicit_target(term) std = cast(StandardDomain, env.get_domain('std')) - std.note_object('term', termtext, node_id, location=term) + std._note_term(termtext, node_id, location=term) # add an index entry too indexnode = addnodes.index() @@ -679,6 +679,20 @@ class StandardDomain(Domain): RemovedInSphinx50Warning, stacklevel=2) self.objects[objtype, name] = (docname, labelid) + @property + def _terms(self) -> Dict[str, Tuple[str, str]]: + """.. note:: Will be removed soon. internal use only.""" + return self.data.setdefault('terms', {}) # (name) -> docname, labelid + + def _note_term(self, term: str, labelid: str, location: Any = None) -> None: + """Note a term for cross reference. + + .. note:: Will be removed soon. internal use only. + """ + self.note_object('term', term, labelid, location) + + self._terms[term.lower()] = (self.env.docname, labelid) + @property def progoptions(self) -> Dict[Tuple[str, str], Tuple[str, str]]: return self.data.setdefault('progoptions', {}) # (program, name) -> docname, labelid @@ -699,6 +713,9 @@ class StandardDomain(Domain): for key, (fn, _l) in list(self.objects.items()): if fn == docname: del self.objects[key] + for key, (fn, _l) in list(self._terms.items()): + if fn == docname: + del self._terms[key] for key, (fn, _l, _l) in list(self.labels.items()): if fn == docname: del self.labels[key] @@ -714,6 +731,9 @@ class StandardDomain(Domain): for key, data in otherdata['objects'].items(): if data[0] in docnames: self.objects[key] = data + for key, data in otherdata['terms'].items(): + if data[0] in docnames: + self._terms[key] = data for key, data in otherdata['labels'].items(): if data[0] in docnames: self.labels[key] = data @@ -947,19 +967,12 @@ class StandardDomain(Domain): if result: return result else: - for objtype, term in self.objects: - if objtype == 'term' and term.lower() == target.lower(): - docname, labelid = self.objects[objtype, term] - logger.warning(__('term %s not found in case sensitive match.' - 'made a reference to %s instead.'), - target, term, location=node, type='ref', subtype='term') - break + # fallback to case insentive match + if target.lower() in self._terms: + docname, labelid = self._terms[target.lower()] + return make_refnode(builder, fromdocname, docname, labelid, contnode) else: - docname, labelid = '', '' - if not docname: return None - return make_refnode(builder, fromdocname, docname, - labelid, contnode) def _resolve_obj_xref(self, env: "BuildEnvironment", fromdocname: str, builder: "Builder", typ: str, target: str, @@ -1115,7 +1128,7 @@ def setup(app: "Sphinx") -> Dict[str, Any]: return { 'version': 'builtin', - 'env_version': 1, + 'env_version': 2, 'parallel_read_safe': True, 'parallel_write_safe': True, } diff --git a/sphinx/ext/autodoc/__init__.py b/sphinx/ext/autodoc/__init__.py index ee74db370..b4d5a4581 100644 --- a/sphinx/ext/autodoc/__init__.py +++ b/sphinx/ext/autodoc/__init__.py @@ -26,7 +26,7 @@ from sphinx.deprecation import RemovedInSphinx50Warning, RemovedInSphinx60Warnin from sphinx.environment import BuildEnvironment from sphinx.ext.autodoc.importer import (get_class_members, get_object_members, import_module, import_object) -from sphinx.ext.autodoc.mock import ismock, mock +from sphinx.ext.autodoc.mock import ismock, mock, undecorate from sphinx.locale import _, __ from sphinx.pycode import ModuleAnalyzer, PycodeError from sphinx.util import inspect, logging @@ -418,6 +418,8 @@ class Documenter: attrgetter=self.get_attr, warningiserror=self.config.autodoc_warningiserror) self.module, self.parent, self.object_name, self.object = ret + if ismock(self.object): + self.object = undecorate(self.object) return True except ImportError as exc: if raiseerror: @@ -1046,6 +1048,8 @@ class ModuleDocumenter(Documenter): for name in dir(self.object): try: value = safe_getattr(self.object, name, None) + if ismock(value): + value = undecorate(value) docstring = attr_docs.get(('', name), []) members[name] = ObjectMember(name, value, docstring="\n".join(docstring)) except AttributeError: diff --git a/sphinx/ext/autodoc/importer.py b/sphinx/ext/autodoc/importer.py index 097dc6d07..a60feed7e 100644 --- a/sphinx/ext/autodoc/importer.py +++ b/sphinx/ext/autodoc/importer.py @@ -14,6 +14,7 @@ import warnings from typing import Any, Callable, Dict, List, NamedTuple, Optional, Tuple from sphinx.deprecation import RemovedInSphinx50Warning +from sphinx.ext.autodoc.mock import ismock, undecorate from sphinx.pycode import ModuleAnalyzer, PycodeError from sphinx.util import logging from sphinx.util.inspect import (getannotations, getmro, getslots, isclass, isenumclass, @@ -273,6 +274,9 @@ def get_class_members(subject: Any, objpath: List[str], attrgetter: Callable for name in dir(subject): try: value = attrgetter(subject, name) + if ismock(value): + value = undecorate(value) + unmangled = unmangle(subject, name) if unmangled and unmangled not in members: if name in obj_dict: diff --git a/sphinx/ext/autodoc/mock.py b/sphinx/ext/autodoc/mock.py index 513ce523f..80e3d2b50 100644 --- a/sphinx/ext/autodoc/mock.py +++ b/sphinx/ext/autodoc/mock.py @@ -13,7 +13,7 @@ import os import sys from importlib.abc import Loader, MetaPathFinder from importlib.machinery import ModuleSpec -from types import FunctionType, MethodType, ModuleType +from types import ModuleType from typing import Any, Generator, Iterator, List, Optional, Sequence, Tuple, Union from sphinx.util import logging @@ -27,6 +27,7 @@ class _MockObject: __display_name__ = '_MockObject' __sphinx_mock__ = True + __sphinx_decorator_args__ = () # type: Tuple[Any, ...] def __new__(cls, *args: Any, **kwargs: Any) -> Any: if len(args) == 3 and isinstance(args[1], tuple): @@ -60,18 +61,19 @@ class _MockObject: return _make_subclass(key, self.__display_name__, self.__class__)() def __call__(self, *args: Any, **kwargs: Any) -> Any: - if args and type(args[0]) in [type, FunctionType, MethodType]: - # Appears to be a decorator, pass through unchanged - return args[0] - return self + call = self.__class__() + call.__sphinx_decorator_args__ = args + return call def __repr__(self) -> str: return self.__display_name__ def _make_subclass(name: str, module: str, superclass: Any = _MockObject, - attributes: Any = None) -> Any: - attrs = {'__module__': module, '__display_name__': module + '.' + name} + attributes: Any = None, decorator_args: Tuple = ()) -> Any: + attrs = {'__module__': module, + '__display_name__': module + '.' + name, + '__sphinx_decorator_args__': decorator_args} attrs.update(attributes or {}) return type(name, (superclass,), attrs) @@ -172,3 +174,14 @@ def ismock(subject: Any) -> bool: pass return False + + +def undecorate(subject: _MockObject) -> Any: + """Unwrap mock if *subject* is decorated by mocked object. + + If not decorated, returns given *subject* itself. + """ + if ismock(subject) and subject.__sphinx_decorator_args__: + return subject.__sphinx_decorator_args__[0] + else: + return subject diff --git a/sphinx/ext/viewcode.py b/sphinx/ext/viewcode.py index baf86dbbf..21cff6a03 100644 --- a/sphinx/ext/viewcode.py +++ b/sphinx/ext/viewcode.py @@ -149,6 +149,18 @@ def env_merge_info(app: Sphinx, env: BuildEnvironment, docnames: Iterable[str], env._viewcode_modules.update(other._viewcode_modules) # type: ignore +def env_purge_doc(app: Sphinx, env: BuildEnvironment, docname: str) -> None: + modules = getattr(env, '_viewcode_modules', {}) + + for modname, (code, tags, used, refname) in list(modules.items()): + for fullname in list(used): + if used[fullname] == docname: + used.pop(fullname) + + if len(used) == 0: + modules.pop(modname) + + class ViewcodeAnchorTransform(SphinxPostTransform): """Convert or remove viewcode_anchor nodes depends on builder.""" default_priority = 100 @@ -323,6 +335,7 @@ def setup(app: Sphinx) -> Dict[str, Any]: app.add_config_value('viewcode_follow_imported_members', True, False) app.connect('doctree-read', doctree_read) app.connect('env-merge-info', env_merge_info) + app.connect('env-purge-doc', env_purge_doc) app.connect('html-collect-pages', collect_pages) app.connect('missing-reference', missing_reference) # app.add_config_value('viewcode_include_modules', [], 'env') diff --git a/sphinx/templates/latex/latex.tex_t b/sphinx/templates/latex/latex.tex_t index 9bfb3c3ff..679660fdd 100644 --- a/sphinx/templates/latex/latex.tex_t +++ b/sphinx/templates/latex/latex.tex_t @@ -20,6 +20,9 @@ %% turn off hyperref patch of \index as sphinx.xdy xindy module takes care of %% suitable \hyperpage mark-up, working around hyperref-xindy incompatibility \PassOptionsToPackage{hyperindex=false}{hyperref} +%% memoir class requires extra handling +\makeatletter\@ifclassloaded{memoir} +{\ifdefined\memhyperindexfalse\memhyperindexfalse\fi}{}\makeatother <% endif -%> <%= passoptionstopackages %> \PassOptionsToPackage{warn}{textcomp} diff --git a/sphinx/texinputs/footnotehyper-sphinx.sty b/sphinx/texinputs/footnotehyper-sphinx.sty index b6692cfb8..c66e9a548 100644 --- a/sphinx/texinputs/footnotehyper-sphinx.sty +++ b/sphinx/texinputs/footnotehyper-sphinx.sty @@ -1,9 +1,9 @@ \NeedsTeXFormat{LaTeX2e} \ProvidesPackage{footnotehyper-sphinx}% - [2017/10/27 v1.7 hyperref aware footnote.sty for sphinx (JFB)] + [2021/01/26 v1.1b hyperref aware footnote.sty for sphinx (JFB)] %% %% Package: footnotehyper-sphinx -%% Version: based on footnotehyper.sty 2017/03/07 v1.0 +%% Version: based on footnotehyper.sty 2021/01/26 v1.1b %% as available at https://www.ctan.org/pkg/footnotehyper %% License: the one applying to Sphinx %% @@ -16,7 +16,7 @@ %% 3. use of \sphinxunactivateextrasandspace from sphinx.sty, %% 4. macro definition \sphinxfootnotemark, %% 5. macro definition \sphinxlongtablepatch -%% 6. replaced an \undefined by \@undefined +%% 6. replaced some \undefined by \@undefined \DeclareOption*{\PackageWarning{footnotehyper-sphinx}{Option `\CurrentOption' is unknown}}% \ProcessOptions\relax \newbox\FNH@notes @@ -206,9 +206,20 @@ \FNH@@@1.2!3?4,\FNH@@@\relax }% \long\def\FNH@check@a #11.2!3?4,#2\FNH@@@#3{% - \ifx\relax#3\expandafter\@firstoftwo\else\expandafter\@secondoftwo\fi - \FNH@bad@makefntext@alert - {\def\FNH@prefntext{#1}\def\FNH@postfntext{#2}\FNH@check@b}% + \ifx\relax#3\FNH@bad@makefntext@alert + \else + \edef\FNH@restore@{\catcode`\noexpand\@\the\catcode`\@\relax}% + \makeatletter + \ifx\@makefntextFB\@undefined + \expandafter\@gobble\else\expandafter\@firstofone\fi + {\@ifclassloaded{memoir}% + {\ifFBFrenchFootnotes\expandafter\@gobble\fi}% + {}}% + \@secondoftwo + \scantokens{\def\FNH@prefntext{#1}\def\FNH@postfntext{#2}}% + \FNH@restore@ + \expandafter\FNH@check@b + \fi }% \def\FNH@check@b #1\relax{% \expandafter\expandafter\expandafter\FNH@check@c diff --git a/sphinx/texinputs/sphinx.sty b/sphinx/texinputs/sphinx.sty index 343924753..f406b1089 100644 --- a/sphinx/texinputs/sphinx.sty +++ b/sphinx/texinputs/sphinx.sty @@ -1859,8 +1859,8 @@ \def\sphinxstyleindexextra #1{ (\emph{#1})} \def\sphinxstyleindexpageref #1{, \pageref{#1}} \def\sphinxstyleindexpagemain#1{\textbf{#1}} -\protected\def\spxentry#1{#1}% will get \let to \sphinxstyleindexentry in index -\protected\def\spxextra#1{#1}% will get \let to \sphinxstyleindexextra in index +\def\spxentry{\@backslashchar spxentry}% let to \sphinxstyleindexentry in index +\def\spxextra{\@backslashchar spxextra}% let to \sphinxstyleindexextra in index \def\sphinxstyleindexlettergroup #1% {{\Large\sffamily#1}\nopagebreak\vspace{1mm}} \def\sphinxstyleindexlettergroupDefault #1% diff --git a/sphinx/themes/basic/static/doctools.js b/sphinx/themes/basic/static/doctools.js index 144884ea6..61ac9d266 100644 --- a/sphinx/themes/basic/static/doctools.js +++ b/sphinx/themes/basic/static/doctools.js @@ -29,9 +29,14 @@ if (!window.console || !console.firebug) { /** * small helper function to urldecode strings + * + * See https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/decodeURIComponent#Decoding_query_parameters_from_a_URL */ jQuery.urldecode = function(x) { - return decodeURIComponent(x).replace(/\+/g, ' '); + if (!x) { + return x + } + return decodeURIComponent(x.replace(/\+/g, ' ')); }; /** diff --git a/sphinx/themes/basic/static/searchtools.js b/sphinx/themes/basic/static/searchtools.js index 2cb28b330..847e879d2 100644 --- a/sphinx/themes/basic/static/searchtools.js +++ b/sphinx/themes/basic/static/searchtools.js @@ -379,6 +379,13 @@ var Search = { return results; }, + /** + * See https://developer.mozilla.org/en-US/docs/Web/JavaScript/Guide/Regular_Expressions + */ + escapeRegExp : function(string) { + return string.replace(/[.*+\-?^${}()|[\]\\]/g, '\\$&'); // $& means the whole matched string + }, + /** * search for full-text terms in the index */ @@ -402,13 +409,14 @@ var Search = { ]; // add support for partial matches if (word.length > 2) { + var word_regex = this.escapeRegExp(word); for (var w in terms) { - if (w.match(word) && !terms[word]) { + if (w.match(word_regex) && !terms[word]) { _o.push({files: terms[w], score: Scorer.partialTerm}) } } for (var w in titleterms) { - if (w.match(word) && !titleterms[word]) { + if (w.match(word_regex) && !titleterms[word]) { _o.push({files: titleterms[w], score: Scorer.partialTitle}) } } diff --git a/sphinx/transforms/i18n.py b/sphinx/transforms/i18n.py index d00c5a144..1cacd0ccd 100644 --- a/sphinx/transforms/i18n.py +++ b/sphinx/transforms/i18n.py @@ -312,6 +312,7 @@ class Locale(SphinxTransform): refname = newf.get('refname') refs = old_foot_namerefs.get(refname, []) if not refs: + newf.parent.remove(newf) continue oldf = refs.pop(0) diff --git a/sphinx/writers/html.py b/sphinx/writers/html.py index 13c6820ba..cf89bd2d1 100644 --- a/sphinx/writers/html.py +++ b/sphinx/writers/html.py @@ -386,6 +386,10 @@ class HTMLTranslator(SphinxTranslator, BaseTranslator): # there's a classifier. pass else: + if isinstance(node.parent.parent.parent, addnodes.glossary): + # add permalink if glossary terms + self.add_permalink_ref(node, _('Permalink to this term')) + self.body.append('') # overwritten diff --git a/sphinx/writers/html5.py b/sphinx/writers/html5.py index 53960f637..3ae6a914c 100644 --- a/sphinx/writers/html5.py +++ b/sphinx/writers/html5.py @@ -337,6 +337,10 @@ class HTML5Translator(SphinxTranslator, BaseTranslator): # there's a classifier. pass else: + if isinstance(node.parent.parent.parent, addnodes.glossary): + # add permalink if glossary terms + self.add_permalink_ref(node, _('Permalink to this term')) + self.body.append('') # overwritten diff --git a/tests/js/doctools.js b/tests/js/doctools.js index 54246f635..a28f3ceb3 100644 --- a/tests/js/doctools.js +++ b/tests/js/doctools.js @@ -12,6 +12,10 @@ describe('jQuery extensions', function() { expect(jQuery.urldecode(test_encoded_string)).toEqual(test_decoded_string); }); + it('+ should result in " "', function() { + expect(jQuery.urldecode('+')).toEqual(' '); + }); + }); describe('getQueryParameters', function() { diff --git a/tests/js/searchtools.js b/tests/js/searchtools.js new file mode 100644 index 000000000..007eeb7db --- /dev/null +++ b/tests/js/searchtools.js @@ -0,0 +1,32 @@ +describe('Basic html theme search', function() { + + describe('terms search', function() { + + it('should find "C++" when in index', function() { + index = { + docnames:["index"], + filenames:["index.rst"], + terms:{'c++':0}, + titles:["<no title>"], + titleterms:{} + } + Search.setIndex(index); + searchterms = ['c++']; + excluded = []; + terms = index.terms; + titleterms = index.titleterms; + + hits = [[ + "index", + "<no title>", + "", + null, + 2, + "index.rst" + ]]; + expect(Search.performTermsSearch(searchterms, excluded, terms, titleterms)).toEqual(hits); + }); + + }); + +}); diff --git a/tests/roots/test-ext-autodoc/target/need_mocks.py b/tests/roots/test-ext-autodoc/target/need_mocks.py index bc227f246..a29954184 100644 --- a/tests/roots/test-ext-autodoc/target/need_mocks.py +++ b/tests/roots/test-ext-autodoc/target/need_mocks.py @@ -9,7 +9,7 @@ import sphinx.missing_module4 # NOQA from sphinx.missing_module4 import missing_name2 # NOQA -@missing_name +@missing_name(int) def decoratedFunction(): """decoratedFunction docstring""" return None diff --git a/tests/test_build_html.py b/tests/test_build_html.py index 9f8b09ee6..d337f4337 100644 --- a/tests/test_build_html.py +++ b/tests/test_build_html.py @@ -254,6 +254,7 @@ def test_html4_output(app, status, warning): (".//p[@class='centered']/strong", 'LICENSE'), # a glossary (".//dl/dt[@id='term-boson']", 'boson'), + (".//dl/dt[@id='term-boson']/a", '¶'), # a production list (".//pre/strong", 'try_stmt'), (".//pre/a[@href='#grammar-token-try1_stmt']/code/span", 'try1_stmt'), diff --git a/tests/test_ext_autodoc_mock.py b/tests/test_ext_autodoc_mock.py index 08157ac45..497bd8a6b 100644 --- a/tests/test_ext_autodoc_mock.py +++ b/tests/test_ext_autodoc_mock.py @@ -15,7 +15,7 @@ from typing import TypeVar import pytest -from sphinx.ext.autodoc.mock import _MockModule, _MockObject, ismock, mock +from sphinx.ext.autodoc.mock import _MockModule, _MockObject, ismock, mock, undecorate def test_MockModule(): @@ -115,20 +115,25 @@ def test_mock_decorator(): @mock.function_deco def func(): - """docstring""" + pass class Foo: @mock.method_deco def meth(self): - """docstring""" + pass @mock.class_deco class Bar: - """docstring""" + pass - assert func.__doc__ == "docstring" - assert Foo.meth.__doc__ == "docstring" - assert Bar.__doc__ == "docstring" + @mock.funcion_deco(Foo) + class Baz: + pass + + assert undecorate(func).__name__ == "func" + assert undecorate(Foo.meth).__name__ == "meth" + assert undecorate(Bar).__name__ == "Bar" + assert undecorate(Baz).__name__ == "Baz" def test_ismock(): diff --git a/tests/test_markup.py b/tests/test_markup.py index 8341b8826..18c40ec1d 100644 --- a/tests/test_markup.py +++ b/tests/test_markup.py @@ -363,8 +363,12 @@ def get_verifier(verify, verify_re): # glossary (description list): multiple terms 'verify', '.. glossary::\n\n term1\n term2\n description', - ('
\n
term1
' - '
term2
description
\n
'), + ('
\n' + '
term1
' + '
term2
' + '
description
\n
'), None, ), ])