Repository: sphinx-doc/sphinx

Issue:
sphinx.ext.doctest: Doctest inside of project fails (params reversed)
### Describe the bug

`sphinx.ext.doctest`'s [`is_allowed_version`](https://github.com/sphinx-doc/sphinx/blob/5.x/sphinx/ext/doctest.py#L40-L55) ([usage](https://github.com/sphinx-doc/sphinx/blob/5.x/sphinx/ext/doctest.py#L131-L133)) has its versions flipped

The params are reversed

### How to Reproduce

1. Extract doctest from sphinx.ext.doctest:

    _example.py_:
    
    ```python
    from packaging.specifiers import InvalidSpecifier, SpecifierSet
    from packaging.version import Version
    
    
    def is_allowed_version(spec: str, version: str) -> bool:
        """Check `spec` satisfies `version` or not.
    
        This obeys PEP-440 specifiers:
        https://peps.python.org/pep-0440/#version-specifiers
    
        Some examples:
    
            >>> is_allowed_version('3.3', '<=3.5')
            True
            >>> is_allowed_version('3.3', '<=3.2')
            False
            >>> is_allowed_version('3.3', '>3.2, <4.0')
            True
        """
        return Version(version) in SpecifierSet(spec)
    ```
2. Run through `doctest`:

    ```console
    $ python -m doctest example.py
    ```

### Expected behavior

Test should pass silently

### Your project

https://gist.github.com/tony/05c66199c003aaf712a39b5dbc103eba

### Screenshots

```
**********************************************************************
File "~/sphinx/example.py", line 13, in example.is_allowed_version
Failed example:
    is_allowed_version('3.3', '<=3.5')
Exception raised:
    Traceback (most recent call last):
      File "~/.python/installs/3.8.13/lib/python3.8/doctest.py", line 1336, in __run
        exec(compile(example.source, filename, "single",
      File "<doctest example.is_allowed_version[0]>", line 1, in <module>
        is_allowed_version('3.3', '<=3.5')
      File "~/sphinx/example.py", line 20, in is_allowed_version
        return Version(version) in SpecifierSet(spec)
      File "~/sphinx/.venv/lib/python3.8/site-packages/packaging/version.py", line 266, in __init__
        raise InvalidVersion(f"Invalid version: '{version}'")
    packaging.version.InvalidVersion: Invalid version: '<=3.5'
**********************************************************************
File "~/sphinx/example.py", line 15, in example.is_allowed_version
Failed example:
    is_allowed_version('3.3', '<=3.2')
Exception raised:
    Traceback (most recent call last):
      File "~/.python/installs/3.8.13/lib/python3.8/doctest.py", line 1336, in __run
        exec(compile(example.source, filename, "single",
      File "<doctest example.is_allowed_version[1]>", line 1, in <module>
        is_allowed_version('3.3', '<=3.2')
      File "~/sphinx/example.py", line 20, in is_allowed_version
        return Version(version) in SpecifierSet(spec)
      File "~/sphinx/.venv/lib/python3.8/site-packages/packaging/version.py", line 266, in __init__
        raise InvalidVersion(f"Invalid version: '{version}'")
    packaging.version.InvalidVersion: Invalid version: '<=3.2'
**********************************************************************
File "~/sphinx/example.py", line 17, in example.is_allowed_version
Failed example:
    is_allowed_version('3.3', '>3.2, <4.0')
Exception raised:
    Traceback (most recent call last):
      File "~/.python/installs/3.8.13/lib/python3.8/doctest.py", line 1336, in __run
        exec(compile(example.source, filename, "single",
      File "<doctest example.is_allowed_version[2]>", line 1, in <module>
        is_allowed_version('3.3', '>3.2, <4.0')
      File "~/sphinx/example.py", line 20, in is_allowed_version
        return Version(version) in SpecifierSet(spec)
      File "~/sphinx/.venv/lib/python3.8/site-packages/packaging/version.py", line 266, in __init__
        raise InvalidVersion(f"Invalid version: '{version}'")
    packaging.version.InvalidVersion: Invalid version: '>3.2, <4.0'
**********************************************************************
1 items had failures:
   3 of   3 in example.is_allowed_version
***Test Failed*** 3 failures.
```

### OS

WSL 2, Ubuntu, Windows 11

### Python version

3.8

### Sphinx version

5.2.0.dev20220905

### Sphinx extensions

sphinx.ext.doctest

### Extra tools

doctest

### Additional context

To fix, reverse:

```python
from packaging.specifiers import InvalidSpecifier, SpecifierSet
from packaging.version import Version


def is_allowed_version(spec: str, version: str) -> bool:
    """Check `spec` satisfies `version` or not.

    This obeys PEP-440 specifiers:
    https://peps.python.org/pep-0440/#version-specifiers

    Some examples:

        >>> is_allowed_version('<=3.5', '3.3')
        True
        >>> is_allowed_version('<=3.2', '3.3')
        False
        >>> is_allowed_version('>3.2, <4.0', '3.3')
        True
    """
    return Version(version) in SpecifierSet(spec)
```

```console
$ python -m doctest example.py -v
```

Output:

```
Trying:
    is_allowed_version('<=3.5', '3.3')
Expecting:
    True
ok
Trying:
    is_allowed_version('<=3.2', '3.3')
Expecting:
    False
ok
Trying:
    is_allowed_version('>3.2, <4.0', '3.3')
Expecting:
    True
ok
1 items had no tests:
    example
1 items passed all tests:
   3 tests in example.is_allowed_version
3 tests in 2 items.
3 passed and 0 failed.
Test passed.
```

Discussion:

Patch:
diff --git a/CHANGES b/CHANGES
index df90f5388..f1c57a24d 100644
--- a/CHANGES
+++ b/CHANGES
@@ -13,18 +13,21 @@ Deprecated
 Features added
 --------------
 
-- #10286: C++, support requires clauses not just between the template
+* #10286: C++, support requires clauses not just between the template
   parameter lists and the declaration.
-- #10755: linkcheck: Check the source URL of raw directives that use the ``url``
+* #10755: linkcheck: Check the source URL of raw directives that use the ``url``
   option.
+* #10781: Allow :rst:role:`ref` role to be used with definitions and fields.
+* #10717: HTML Search: Increase priority for full title and 
+  subtitle matches in search results
 
 Bugs fixed
 ----------
 
-- #10257: C++, ensure consistent non-specialization template argument
+* #10257: C++, ensure consistent non-specialization template argument
   representation.
-- #10729: C++, fix parsing of certain non-type template parameter packs.
-- #10715: Revert #10520: "Fix" use of sidebar classes in ``agogo.css_t``
+* #10729: C++, fix parsing of certain non-type template parameter packs.
+* #10715: Revert #10520: "Fix" use of sidebar classes in ``agogo.css_t``
 
 Testing
 --------
diff --git a/sphinx/domains/std.py b/sphinx/domains/std.py
index d985e5d3a..b0d21b492 100644
--- a/sphinx/domains/std.py
+++ b/sphinx/domains/std.py
@@ -757,12 +757,21 @@ def process_doc(self, env: "BuildEnvironment", docname: str, document: nodes.doc
                 if not sectname:
                     continue
             else:
-                toctree = next(node.findall(addnodes.toctree), None)
-                if toctree and toctree.get('caption'):
-                    sectname = toctree.get('caption')
+                if (isinstance(node, (nodes.definition_list,
+                                      nodes.field_list)) and
+                        node.children):
+                    node = cast(nodes.Element, node.children[0])
+                if isinstance(node, (nodes.field, nodes.definition_list_item)):
+                    node = cast(nodes.Element, node.children[0])
+                if isinstance(node, (nodes.term, nodes.field_name)):
+                    sectname = clean_astext(node)
                 else:
-                    # anonymous-only labels
-                    continue
+                    toctree = next(node.findall(addnodes.toctree), None)
+                    if toctree and toctree.get('caption'):
+                        sectname = toctree.get('caption')
+                    else:
+                        # anonymous-only labels
+                        continue
             self.labels[name] = docname, labelid, sectname
 
     def add_program_option(self, program: str, name: str, docname: str, labelid: str) -> None:
diff --git a/sphinx/environment/__init__.py b/sphinx/environment/__init__.py
index 430e03358..1a1dd755f 100644
--- a/sphinx/environment/__init__.py
+++ b/sphinx/environment/__init__.py
@@ -59,7 +59,7 @@
 
 # This is increased every time an environment attribute is added
 # or changed to properly invalidate pickle files.
-ENV_VERSION = 56
+ENV_VERSION = 57
 
 # config status
 CONFIG_OK = 1
diff --git a/sphinx/search/__init__.py b/sphinx/search/__init__.py
index ac8a44dcc..a3dd07d3f 100644
--- a/sphinx/search/__init__.py
+++ b/sphinx/search/__init__.py
@@ -183,6 +183,7 @@ class WordCollector(nodes.NodeVisitor):
     def __init__(self, document: nodes.document, lang: SearchLanguage) -> None:
         super().__init__(document)
         self.found_words: List[str] = []
+        self.found_titles: List[Tuple[str, str]] = []
         self.found_title_words: List[str] = []
         self.lang = lang
 
@@ -213,7 +214,10 @@ def dispatch_visit(self, node: Node) -> None:
         elif isinstance(node, nodes.Text):
             self.found_words.extend(self.lang.split(node.astext()))
         elif isinstance(node, nodes.title):
-            self.found_title_words.extend(self.lang.split(node.astext()))
+            title = node.astext()
+            ids = node.parent['ids']
+            self.found_titles.append((title, ids[0] if ids else None))
+            self.found_title_words.extend(self.lang.split(title))
         elif isinstance(node, Element) and self.is_meta_keywords(node):
             keywords = node['content']
             keywords = [keyword.strip() for keyword in keywords.split(',')]
@@ -237,6 +241,7 @@ def __init__(self, env: BuildEnvironment, lang: str, options: Dict, scoring: str
         self._mapping: Dict[str, Set[str]] = {}     # stemmed word -> set(docname)
         # stemmed words in titles -> set(docname)
         self._title_mapping: Dict[str, Set[str]] = {}
+        self._all_titles: Dict[str, List[Tuple[str, str]]] = {}  # docname -> all titles
         self._stem_cache: Dict[str, str] = {}       # word -> stemmed word
         self._objtypes: Dict[Tuple[str, str], int] = {}     # objtype -> index
         # objtype index -> (domain, type, objname (localized))
@@ -281,6 +286,11 @@ def load(self, stream: IO, format: Any) -> None:
         index2fn = frozen['docnames']
         self._filenames = dict(zip(index2fn, frozen['filenames']))
         self._titles = dict(zip(index2fn, frozen['titles']))
+        self._all_titles = {}
+
+        for title, doc_tuples in frozen['alltitles'].items():
+            for doc, titleid in doc_tuples:
+                self._all_titles.setdefault(index2fn[doc], []).append((title, titleid))
 
         def load_terms(mapping: Dict[str, Any]) -> Dict[str, Set[str]]:
             rv = {}
@@ -364,9 +374,16 @@ def freeze(self) -> Dict[str, Any]:
         objects = self.get_objects(fn2index)  # populates _objtypes
         objtypes = {v: k[0] + ':' + k[1] for (k, v) in self._objtypes.items()}
         objnames = self._objnames
+
+        alltitles: Dict[str, List[Tuple[int, str]]] = {}
+        for docname, titlelist in self._all_titles.items():
+            for title, titleid in titlelist:
+                alltitles.setdefault(title.lower(), []).append((fn2index[docname],  titleid))
+
         return dict(docnames=docnames, filenames=filenames, titles=titles, terms=terms,
                     objects=objects, objtypes=objtypes, objnames=objnames,
-                    titleterms=title_terms, envversion=self.env.version)
+                    titleterms=title_terms, envversion=self.env.version,
+                    alltitles=alltitles)
 
     def label(self) -> str:
         return "%s (code: %s)" % (self.lang.language_name, self.lang.lang)
@@ -374,13 +391,16 @@ def label(self) -> str:
     def prune(self, docnames: Iterable[str]) -> None:
         """Remove data for all docnames not in the list."""
         new_titles = {}
+        new_alltitles = {}
         new_filenames = {}
         for docname in docnames:
             if docname in self._titles:
                 new_titles[docname] = self._titles[docname]
+                new_alltitles[docname] = self._all_titles[docname]
                 new_filenames[docname] = self._filenames[docname]
         self._titles = new_titles
         self._filenames = new_filenames
+        self._all_titles = new_alltitles
         for wordnames in self._mapping.values():
             wordnames.intersection_update(docnames)
         for wordnames in self._title_mapping.values():
@@ -403,6 +423,8 @@ def stem(word: str) -> str:
                 return self._stem_cache[word]
         _filter = self.lang.word_filter
 
+        self._all_titles[docname] = visitor.found_titles
+
         for word in visitor.found_title_words:
             stemmed_word = stem(word)
             if _filter(stemmed_word):
diff --git a/sphinx/themes/basic/static/searchtools.js b/sphinx/themes/basic/static/searchtools.js
index f2fb7d5cf..30872aa8f 100644
--- a/sphinx/themes/basic/static/searchtools.js
+++ b/sphinx/themes/basic/static/searchtools.js
@@ -237,6 +237,11 @@ const Search = {
    * execute search (requires search index to be loaded)
    */
   query: (query) => {
+    const filenames = Search._index.filenames;
+    const docNames = Search._index.docnames;
+    const titles = Search._index.titles;
+    const allTitles = Search._index.alltitles;
+
     // stem the search terms and add them to the correct list
     const stemmer = new Stemmer();
     const searchTerms = new Set();
@@ -272,6 +277,23 @@ const Search = {
     let results = [];
     _removeChildren(document.getElementById("search-progress"));
 
+    const queryLower = query.toLowerCase();
+    for (const [title, foundTitles] of Object.entries(allTitles)) {
+      if (title.includes(queryLower) && (queryLower.length >= title.length/2)) {
+        for (const [file, id] of foundTitles) {
+          let score = Math.round(100 * queryLower.length / title.length)
+          results.push([
+            docNames[file],
+            titles[file],
+            id !== null ? "#" + id : "",
+            null,
+            score,
+            filenames[file],
+          ]);
+        }
+      }
+    }
+
     // lookup as object
     objectTerms.forEach((term) =>
       results.push(...Search.performObjectSearch(term, objectTerms))
@@ -399,8 +421,8 @@ const Search = {
     // prepare search
     const terms = Search._index.terms;
     const titleTerms = Search._index.titleterms;
-    const docNames = Search._index.docnames;
     const filenames = Search._index.filenames;
+    const docNames = Search._index.docnames;
     const titles = Search._index.titles;
 
     const scoreMap = new Map();
diff --git a/sphinx/util/docutils.py b/sphinx/util/docutils.py
index 3ca75deea..cde09058e 100644
--- a/sphinx/util/docutils.py
+++ b/sphinx/util/docutils.py
@@ -30,6 +30,8 @@
 report_re = re.compile('^(.+?:(?:\\d+)?): \\((DEBUG|INFO|WARNING|ERROR|SEVERE)/(\\d+)?\\) ')
 
 if TYPE_CHECKING:
+    from docutils.frontend import Values
+
     from sphinx.builders import Builder
     from sphinx.config import Config
     from sphinx.environment import BuildEnvironment
@@ -598,7 +600,7 @@ def findall(self, *args, **kwargs):
 
 # cache a vanilla instance of nodes.document
 # Used in new_document() function
-__document_cache__: Optional[nodes.document] = None
+__document_cache__: Tuple["Values", Reporter]
 
 
 def new_document(source_path: str, settings: Any = None) -> nodes.document:
@@ -609,15 +611,18 @@ def new_document(source_path: str, settings: Any = None) -> nodes.document:
     This makes an instantiation of document nodes much faster.
     """
     global __document_cache__
-    if __document_cache__ is None:
-        __document_cache__ = docutils.utils.new_document(source_path)
+    try:
+        cached_settings, reporter = __document_cache__
+    except NameError:
+        doc = docutils.utils.new_document(source_path)
+        __document_cache__ = cached_settings, reporter = doc.settings, doc.reporter
 
     if settings is None:
-        # Make a copy of ``settings`` from cache to accelerate instantiation
-        settings = copy(__document_cache__.settings)
+        # Make a copy of the cached settings to accelerate instantiation
+        settings = copy(cached_settings)
 
     # Create a new instance of nodes.document using cached reporter
     from sphinx import addnodes
-    document = addnodes.document(settings, __document_cache__.reporter, source=source_path)
+    document = addnodes.document(settings, reporter, source=source_path)
     document.note_source(source_path, -1)
     return document


Test patch:
diff --git a/sphinx/ext/doctest.py b/sphinx/ext/doctest.py
index 4168e998a..8e060907f 100644
--- a/sphinx/ext/doctest.py
+++ b/sphinx/ext/doctest.py
@@ -45,11 +45,11 @@ def is_allowed_version(spec: str, version: str) -> bool:
 
     Some examples:
 
-        >>> is_allowed_version('3.3', '<=3.5')
+        >>> is_allowed_version('<=3.5', '3.3')
         True
-        >>> is_allowed_version('3.3', '<=3.2')
+        >>> is_allowed_version('<=3.2', '3.3')
         False
-        >>> is_allowed_version('3.3', '>3.2, <4.0')
+        >>> is_allowed_version('>3.2, <4.0', '3.3')
         True
     """
     return Version(version) in SpecifierSet(spec)
diff --git a/tests/test_domain_std.py b/tests/test_domain_std.py
index 866938526..d5c3e63d8 100644
--- a/tests/test_domain_std.py
+++ b/tests/test_domain_std.py
@@ -445,3 +445,43 @@ def test_labeled_rubric(app):
     domain = app.env.get_domain("std")
     assert 'label' in domain.labels
     assert domain.labels['label'] == ('index', 'label', 'blah blah blah')
+
+
+def test_labeled_definition(app):
+    text = (".. _label1:\n"
+            "\n"
+            "Foo blah *blah* blah\n"
+            "  Definition\n"
+            "\n"
+            ".. _label2:\n"
+            "\n"
+            "Bar blah *blah* blah\n"
+            "  Definition\n"
+            "\n")
+    restructuredtext.parse(app, text)
+
+    domain = app.env.get_domain("std")
+    assert 'label1' in domain.labels
+    assert domain.labels['label1'] == ('index', 'label1', 'Foo blah blah blah')
+    assert 'label2' in domain.labels
+    assert domain.labels['label2'] == ('index', 'label2', 'Bar blah blah blah')
+
+
+def test_labeled_field(app):
+    text = (".. _label1:\n"
+            "\n"
+            ":Foo blah *blah* blah:\n"
+            "  Definition\n"
+            "\n"
+            ".. _label2:\n"
+            "\n"
+            ":Bar blah *blah* blah:\n"
+            "  Definition\n"
+            "\n")
+    restructuredtext.parse(app, text)
+
+    domain = app.env.get_domain("std")
+    assert 'label1' in domain.labels
+    assert domain.labels['label1'] == ('index', 'label1', 'Foo blah blah blah')
+    assert 'label2' in domain.labels
+    assert domain.labels['label2'] == ('index', 'label2', 'Bar blah blah blah')
diff --git a/tests/test_search.py b/tests/test_search.py
index 0330bfbae..cbe1be54f 100644
--- a/tests/test_search.py
+++ b/tests/test_search.py
@@ -177,7 +177,8 @@ def test_IndexBuilder():
                   'non': [0, 1, 2, 3],
                   'test': [0, 1, 2, 3]},
         'titles': ('title1_1', 'title1_2', 'title2_1', 'title2_2'),
-        'titleterms': {'section_titl': [0, 1, 2, 3]}
+        'titleterms': {'section_titl': [0, 1, 2, 3]},
+        'alltitles': {'section_title': [(0, 'section-title'), (1, 'section-title'), (2, 'section-title'), (3, 'section-title')]}
     }
     assert index._objtypes == {('dummy1', 'objtype1'): 0, ('dummy2', 'objtype1'): 1}
     assert index._objnames == {0: ('dummy1', 'objtype1', 'objtype1'),
@@ -234,7 +235,8 @@ def test_IndexBuilder():
                   'non': [0, 1],
                   'test': [0, 1]},
         'titles': ('title1_2', 'title2_2'),
-        'titleterms': {'section_titl': [0, 1]}
+        'titleterms': {'section_titl': [0, 1]},
+        'alltitles': {'section_title': [(0, 'section-title'), (1, 'section-title')]}
     }
     assert index._objtypes == {('dummy1', 'objtype1'): 0, ('dummy2', 'objtype1'): 1}
     assert index._objnames == {0: ('dummy1', 'objtype1', 'objtype1'),