Repository: sphinx-doc/sphinx

Issue:
PEP 646-style annotation of `*args` as a Type Variable Tuple
### Describe the bug

The simple
```
.. py.function:: func(*args: *tuple[str, int])
```
emits a warning
```
WARNING: could not parse arglist ('*args: *tuple[int, str]'): Unable to parse Starred object
```
The culprit is the absence of a `visit_Starred` method in the class `sphinx.pycode.ast._UnparseVisitor`. After fixing that with the inclusion of
```python
def visit_Starred(self, node: ast.Starred) -> str:
    return f'*{self.visit(node.value)}'
```
a further warning is emitted:
```
WARNING: py:class reference target not found: *tuple[int, str] [ref.class]
```
In this case the problem is that `sphinx.domains.python._annotations._parse_annotation` tries to find a cross-reference without first splitting off the star in front. A solution is the inclusion of the following block in the body of the `unparse` function within that function:
```python
if isinstance(node, ast.Starred):
    result = [addnodes.desc_sig_operator('', '*')]
    result.extend(unparse(node.value))
    return result
```

PR to follow.


### How to Reproduce

`index.rst`:
```
.. py.function:: func(*args: *tuple[str, int]
```
`conf.py`:
```python
extensions = [
    "sphinx.ext.intersphinx",
]
intersphinx_mapping = {
    "python": ("https://docs.python.org/3/", None),
}
```

### Environment Information

```text
Platform:              darwin; (macOS-14.7-arm64-arm-64bit-Mach-O)
Python version:        3.13.1 (main, Dec  3 2024, 17:59:52) [Clang 15.0.0 (clang-1500.3.9.4)])
Python implementation: CPython
Sphinx version:        8.2.0
Docutils version:      0.21.2
Jinja2 version:        3.1.5
Pygments version:      2.19.1
```

### Sphinx extensions

```python
["sphinx.ext.intersphinx"]
```

### Additional context

None.

Discussion:

Solution patch:
diff --git a/.github/workflows/builddoc.yml b/.github/workflows/builddoc.yml
index 7f8471deecb..8955cf2988a 100644
--- a/.github/workflows/builddoc.yml
+++ b/.github/workflows/builddoc.yml
@@ -36,7 +36,7 @@ jobs:
         version: latest
         enable-cache: false
     - name: Install dependencies
-      run: uv pip install .[docs]
+      run: uv pip install . --group docs
     - name: Render the documentation
       run: >
         sphinx-build
diff --git a/.github/workflows/create-release.yml b/.github/workflows/create-release.yml
index 6f3ebf264a8..5d07fcbd18d 100644
--- a/.github/workflows/create-release.yml
+++ b/.github/workflows/create-release.yml
@@ -41,10 +41,7 @@ jobs:
           enable-cache: false
 
       - name: Install build dependencies (pypa/build, twine)
-        run: |
-          uv pip install build "twine>=5.1"
-          # resolution fails without betterproto
-          uv pip install pypi-attestations==0.0.21 betterproto==2.0.0b6
+        run: uv pip install --group package
 
       - name: Build distribution
         run: python -m build
diff --git a/.github/workflows/lint.yml b/.github/workflows/lint.yml
index a3b5cf7ae52..7e72b6f6fd0 100644
--- a/.github/workflows/lint.yml
+++ b/.github/workflows/lint.yml
@@ -55,7 +55,7 @@ jobs:
         version: latest
         enable-cache: false
     - name: Install dependencies
-      run: uv pip install ".[lint,test]"
+      run: uv pip install -r pyproject.toml --group package --group test --group types
     - name: Type check with mypy
       run: mypy
 
@@ -76,7 +76,7 @@ jobs:
         version: latest
         enable-cache: false
     - name: Install dependencies
-      run: uv pip install ".[lint,test]"
+      run: uv pip install -r pyproject.toml --group package --group test --group types
     - name: Type check with pyright
       run: pyright
 
@@ -97,7 +97,7 @@ jobs:
         version: latest
         enable-cache: false
     - name: Install dependencies
-      run: uv pip install --upgrade sphinx-lint
+      run: uv pip install --group lint
     - name: Lint documentation with sphinx-lint
       run: make doclinter
 
@@ -118,7 +118,7 @@ jobs:
         version: latest
         enable-cache: false
     - name: Install dependencies
-      run: uv pip install --upgrade twine build
+      run: uv pip install --group package
     - name: Lint with twine
       run: |
         python -m build .
diff --git a/.github/workflows/main.yml b/.github/workflows/main.yml
index 1758254c633..7e7bdb6dab2 100644
--- a/.github/workflows/main.yml
+++ b/.github/workflows/main.yml
@@ -67,7 +67,7 @@ jobs:
         version: latest
         enable-cache: false
     - name: Install dependencies
-      run: uv pip install .[test]
+      run: uv pip install . --group test
     - name: Install Docutils ${{ matrix.docutils }}
       run: uv pip install --upgrade "docutils~=${{ matrix.docutils }}.0"
     - name: Test with pytest
@@ -198,7 +198,7 @@ jobs:
         version: latest
         enable-cache: false
     - name: Install dependencies
-      run: uv pip install .[test]
+      run: uv pip install . --group test
     - name: Test with pytest
       run: python -m pytest -vv --durations 25
       env:
@@ -227,7 +227,7 @@ jobs:
         version: latest
         enable-cache: false
     - name: Install dependencies
-      run: uv pip install .[test]
+      run: uv pip install . --group test
     - name: Test with pytest
       run: python -m pytest -vv --durations 25
       env:
@@ -262,7 +262,7 @@ jobs:
         version: latest
         enable-cache: false
     - name: Install dependencies
-      run: uv pip install .[test]
+      run: uv pip install . --group test
     - name: Install Docutils' HEAD
       run: uv pip install "docutils @ git+https://repo.or.cz/docutils.git#subdirectory=docutils"
     - name: Test with pytest
@@ -296,7 +296,7 @@ jobs:
         enable-cache: false
     - name: Install dependencies
       run: |
-        uv pip install .[test] --resolution lowest-direct
+        uv pip install . --group test --resolution lowest-direct
         uv pip install alabaster==1.0.0
     - name: Test with pytest
       run: python -m pytest -n logical --dist=worksteal -vv --durations 25
@@ -326,7 +326,7 @@ jobs:
         version: latest
         enable-cache: false
     - name: Install dependencies
-      run: uv pip install .[test]
+      run: uv pip install . --group test
     - name: Test with pytest
       run: python -m pytest -vv --durations 25
       env:
@@ -357,7 +357,7 @@ jobs:
         version: latest
         enable-cache: false
     - name: Install dependencies
-      run: uv pip install .[test] pytest-cov
+      run: uv pip install . --group test pytest-cov
     - name: Test with pytest
       run: python -m pytest -vv --cov . --cov-append --cov-config pyproject.toml
       env:
diff --git a/.github/workflows/transifex.yml b/.github/workflows/transifex.yml
index 09437cb7ece..56246266515 100644
--- a/.github/workflows/transifex.yml
+++ b/.github/workflows/transifex.yml
@@ -41,7 +41,7 @@ jobs:
         version: latest
         enable-cache: false
     - name: Install dependencies
-      run: uv pip install --upgrade babel jinja2
+      run: uv pip install --group translations
     - name: Extract translations from source code
       run: python utils/babel_runner.py extract
     - name: Push translations to transifex.com
@@ -77,7 +77,7 @@ jobs:
         version: latest
         enable-cache: false
     - name: Install dependencies
-      run: uv pip install --upgrade babel jinja2
+      run: uv pip install --group translations
     - name: Extract translations from source code
       run: python utils/babel_runner.py extract
     - name: Pull translations from transifex.com
diff --git a/AUTHORS.rst b/AUTHORS.rst
index ff92ab7eab7..eed59b68cbd 100644
--- a/AUTHORS.rst
+++ b/AUTHORS.rst
@@ -107,6 +107,7 @@ Contributors
 * Thomas Lamb -- linkcheck builder
 * Thomas Waldmann -- apidoc module fixes
 * Tim Hoffmann -- theme improvements
+* Victor Wheeler -- documentation improvements
 * Vince Salvino -- JavaScript search improvements
 * Will Maier -- directory HTML builder
 * Zac Hatfield-Dodds -- doctest reporting improvements, intersphinx performance
diff --git a/CHANGES.rst b/CHANGES.rst
index c257b3b11b1..b37ce847c49 100644
--- a/CHANGES.rst
+++ b/CHANGES.rst
@@ -16,5 +16,8 @@ Features added
 Bugs fixed
 ----------
 
+* #13369: Correctly parse and cross-reference unpacked type annotations.
+  Patch by Alicia Garcia-Raboso.
+
 Testing
 -------
diff --git a/doc/development/html_themes/templating.rst b/doc/development/html_themes/templating.rst
index e7c1d11f453..77b43882f86 100644
--- a/doc/development/html_themes/templating.rst
+++ b/doc/development/html_themes/templating.rst
@@ -6,6 +6,32 @@
 Templating
 ==========
 
+What Is Templating?
+-------------------
+
+Templating is a method of generating HTML pages by combining static templates
+with variable data.
+The template files contain the static parts of the desired HTML output
+and include special syntax describing how variable content will be inserted.
+For example, this can be used to insert the current date in the footer of each page,
+or to surround the main content of the document with a scaffold of HTML for layout
+and formatting purposes.
+Doing so only requires an understanding of HTML and the templating syntax.
+Knowledge of Python can be helpful, but is not required.
+
+Templating uses an inheritance mechanism which allows child templates files
+(e.g. in a theme) to override as much (or as little) of their 'parents' as desired.
+Likewise, content authors can use their own local templates to override as much (or
+as little) of the theme templates as desired.
+
+The result is that the Sphinx core, without needing to be changed, provides basic
+HTML generation, independent of the structure and appearance of the final output,
+while granting a great deal of flexibility to theme and content authors.
+
+
+Sphinx Templating
+-----------------
+
 Sphinx uses the `Jinja <https://jinja.palletsprojects.com/>`_ templating engine
 for its HTML templates.  Jinja is a text-based engine, inspired by Django
 templates, so anyone having used Django will already be familiar with it. It
diff --git a/pyproject.toml b/pyproject.toml
index 5d480cf3f76..d841d10747d 100644
--- a/pyproject.toml
+++ b/pyproject.toml
@@ -92,19 +92,19 @@ docs = [
     "sphinxcontrib-websupport",
 ]
 lint = [
-    "ruff==0.11.0",
+    "ruff==0.11.2",
     "mypy==1.15.0",
     "sphinx-lint>=0.9",
     "types-colorama==0.4.15.20240311",
     "types-defusedxml==0.7.0.20240218",
     "types-docutils==0.21.0.20241128",
     "types-Pillow==10.2.0.20240822",
-    "types-Pygments==2.19.0.20250219",
-    "types-requests==2.32.0.20250301",  # align with requests
+    "types-Pygments==2.19.0.20250305",
+    "types-requests==2.32.0.20250306",  # align with requests
     "types-urllib3==1.26.25.14",
-    "pyright==1.1.396",
+    "pyright==1.1.397",
     "pytest>=8.0",
-    "pypi-attestations==0.0.21",
+    "pypi-attestations==0.0.22",
     "betterproto==2.0.0b6",
 ]
 test = [
@@ -130,6 +130,48 @@ sphinx-quickstart = "sphinx.cmd.quickstart:main"
 sphinx-apidoc = "sphinx.ext.apidoc:main"
 sphinx-autogen = "sphinx.ext.autosummary.generate:main"
 
+[dependency-groups]
+docs = [
+    "sphinxcontrib-websupport",
+]
+lint = [
+    "ruff==0.11.2",
+    "sphinx-lint>=0.9",
+]
+package = [
+    "betterproto==2.0.0b6",  # resolution fails without betterproto
+    "build",
+    "pypi-attestations==0.0.22",
+    "twine>=5.1",
+]
+test = [
+    "pytest>=8.0",
+    "pytest-xdist[psutil]>=3.4",
+    "cython>=3.0",  # for Cython compilation
+    "defusedxml>=0.7.1",  # for secure XML/HTML parsing
+    "setuptools>=70.0",  # for Cython compilation
+    "typing_extensions>=4.9",  # for typing_extensions.Unpack
+]
+translations = [
+    "babel>=2.13",
+    "Jinja2>=3.1",
+]
+types = [
+    "mypy==1.15.0",
+    "pyright==1.1.397",
+    { include-group = "type-stubs" },
+]
+type-stubs = [
+    # align with versions used elsewhere
+    "types-colorama==0.4.15.20240311",
+    "types-defusedxml==0.7.0.20240218",
+    "types-docutils==0.21.0.20241128",
+    "types-Pillow==10.2.0.20240822",
+    "types-Pygments==2.19.0.20250305",
+    "types-requests==2.32.0.20250306",
+    "types-urllib3==1.26.25.14",
+]
+
 [tool.flit.module]
 name = "sphinx"
 
@@ -203,9 +245,6 @@ ignore_missing_imports = true
 [[tool.mypy.overrides]]
 module = [
     # tests/
-    "tests.test_addnodes",
-    "tests.test_application",
-    "tests.test_events",
     "tests.test_highlighting",
     "tests.test_project",
     "tests.test_versioning",
@@ -253,7 +292,6 @@ module = [
     "tests.test_util.test_util_docutils",
     "tests.test_util.test_util_images",
     "tests.test_util.test_util_inventory",
-    "tests.test_util.test_util_matching",
     # tests/test_writers
     "tests.test_writers.test_docutilsconf",
 ]
@@ -422,3 +460,6 @@ reportUnusedFunction = "none"
 reportUnusedImport = "none"
 reportUnusedVariable = "none"
 reportWildcardImportFromLibrary = "none"
+
+[tool.uv]
+default-groups = "all"
diff --git a/sphinx/builders/html/__init__.py b/sphinx/builders/html/__init__.py
index 5e6acdeaf9d..1ba026a61d0 100644
--- a/sphinx/builders/html/__init__.py
+++ b/sphinx/builders/html/__init__.py
@@ -17,6 +17,7 @@
 from urllib.parse import quote
 
 import docutils.readers.doctree
+import jinja2.exceptions
 from docutils import nodes
 from docutils.core import Publisher
 from docutils.frontend import OptionParser
@@ -1221,6 +1222,19 @@ def js_tag(js: _JavaScript | str) -> str:
             )
             return
         except Exception as exc:
+            if (
+                isinstance(exc, jinja2.exceptions.UndefinedError)
+                and exc.message == "'style' is undefined"
+            ):
+                msg = __(
+                    "The '%s' theme does not support this version of Sphinx, "
+                    "because it uses the 'style' field in HTML templates, "
+                    'which was  was deprecated in Sphinx 5.1 and removed in Sphinx 7.0. '
+                    "The theme must be updated to use the 'styles' field instead. "
+                    'See https://www.sphinx-doc.org/en/master/development/html_themes/templating.html#styles'
+                )
+                raise ThemeError(msg % self.config.html_theme) from None
+
             msg = __('An error happened in rendering the page %s.\nReason: %r') % (
                 pagename,
                 exc,
diff --git a/sphinx/domains/python/_annotations.py b/sphinx/domains/python/_annotations.py
index 823aac01316..29e47fa7151 100644
--- a/sphinx/domains/python/_annotations.py
+++ b/sphinx/domains/python/_annotations.py
@@ -124,6 +124,10 @@ def unparse(node: ast.AST) -> list[Node]:
                 return [nodes.Text(repr(node.value))]
         if isinstance(node, ast.Expr):
             return unparse(node.value)
+        if isinstance(node, ast.Starred):
+            result = [addnodes.desc_sig_operator('', '*')]
+            result.extend(unparse(node.value))
+            return result
         if isinstance(node, ast.Invert):
             return [addnodes.desc_sig_punctuation('', '~')]
         if isinstance(node, ast.USub):
diff --git a/sphinx/ext/autodoc/importer.py b/sphinx/ext/autodoc/importer.py
index ea5b47e41e6..ca9c7ca7778 100644
--- a/sphinx/ext/autodoc/importer.py
+++ b/sphinx/ext/autodoc/importer.py
@@ -164,6 +164,7 @@ def import_module(modname: str, try_reload: bool = False) -> Any:
     if modname in sys.modules:
         return sys.modules[modname]
 
+    skip_pyi = bool(os.getenv('SPHINX_AUTODOC_IGNORE_NATIVE_MODULE_TYPE_STUBS', ''))
     original_module_names = frozenset(sys.modules)
     try:
         spec = find_spec(modname)
@@ -171,7 +172,7 @@ def import_module(modname: str, try_reload: bool = False) -> Any:
             msg = f'No module named {modname!r}'
             raise ModuleNotFoundError(msg, name=modname)  # NoQA: TRY301
         spec, pyi_path = _find_type_stub_spec(spec, modname)
-        if pyi_path is None:
+        if skip_pyi or pyi_path is None:
             module = importlib.import_module(modname)
         else:
             if spec.loader is None:
diff --git a/sphinx/pycode/ast.py b/sphinx/pycode/ast.py
index b1521595b49..640864e467a 100644
--- a/sphinx/pycode/ast.py
+++ b/sphinx/pycode/ast.py
@@ -202,5 +202,8 @@ def visit_Tuple(self, node: ast.Tuple) -> str:
         else:
             return '(' + ', '.join(self.visit(e) for e in node.elts) + ')'
 
+    def visit_Starred(self, node: ast.Starred) -> str:
+        return f'*{self.visit(node.value)}'
+
     def generic_visit(self, node: ast.AST) -> NoReturn:
         raise NotImplementedError('Unable to parse %s object' % type(node).__name__)
diff --git a/sphinx/util/i18n.py b/sphinx/util/i18n.py
index 6cc4b31766e..05542876fc3 100644
--- a/sphinx/util/i18n.py
+++ b/sphinx/util/i18n.py
@@ -228,6 +228,14 @@ def babel_format_date(
     if not hasattr(date, 'tzinfo'):
         formatter = babel.dates.format_date
 
+    if not locale:
+        # Babel would not accept a falsy locale
+        # (or would try to fall back to the LC_TIME
+        # locale, which would be not what was requested),
+        # so we can just short-cut to English, as we
+        # would for the `"fallback to English"` case.
+        locale = 'en'
+
     try:
         return formatter(date, format, locale=locale)
     except (ValueError, babel.core.UnknownLocaleError):
diff --git a/tox.ini b/tox.ini
index 674013fdc08..23b239c7ffc 100644
--- a/tox.ini
+++ b/tox.ini
@@ -20,7 +20,7 @@ passenv =
     READTHEDOCS
 description =
     py{311,312,313,314}: Run unit tests against {envname}.
-extras =
+dependency_groups =
     test
 setenv =
     PYTHONWARNINGS = error
@@ -31,8 +31,9 @@ commands=
 [testenv:lint]
 description =
     Run linters.
-extras =
+dependency_groups =
     lint
+    types
 # If you update any of these commands, don't forget to update the equivalent
 # GitHub Workflow step
 commands =
@@ -43,7 +44,7 @@ commands =
 [testenv:docs]
 description =
     Build documentation.
-extras =
+dependency_groups =
     docs
 commands =
     python -c "import shutil; shutil.rmtree('./build/sphinx', ignore_errors=True) if '{env:CLEAN:}' else None"
@@ -52,7 +53,7 @@ commands =
 [testenv:docs-live]
 description =
     Build documentation.
-extras =
+dependency_groups =
     docs
 deps =
     sphinx-autobuild
@@ -70,7 +71,7 @@ commands =
 [testenv:ruff]
 description =
     Run ruff formatting and linting.
-extras =
+dependency_groups =
     lint
 commands =
     ruff format .
@@ -79,8 +80,7 @@ commands =
 [testenv:mypy]
 description =
     Run mypy type checking.
-extras =
-    lint
-    test
+dependency_groups =
+    types
 commands =
     mypy {posargs}
diff --git a/utils/convert_attestations.py b/utils/convert_attestations.py
index 7e227c21358..0d013bf97ce 100644
--- a/utils/convert_attestations.py
+++ b/utils/convert_attestations.py
@@ -7,7 +7,7 @@
 # /// script
 # requires-python = ">=3.11"
 # dependencies = [
-#     "pypi-attestations==0.0.21",
+#     "pypi-attestations==0.0.22",
 #     "betterproto==2.0.0b6",
 # ]
 # ///


Test patch:
diff --git a/tests/test_addnodes.py b/tests/test_addnodes.py
index b3f77ad2bb9..8cac53b2828 100644
--- a/tests/test_addnodes.py
+++ b/tests/test_addnodes.py
@@ -20,7 +20,9 @@ def sig_elements() -> Iterator[set[type[addnodes.desc_sig_element]]]:
     addnodes.SIG_ELEMENTS = original  # restore the previous value
 
 
-def test_desc_sig_element_nodes(sig_elements):
+def test_desc_sig_element_nodes(
+    sig_elements: set[type[addnodes.desc_sig_element]],
+) -> None:
     """Test the registration of ``desc_sig_element`` subclasses."""
     # expected desc_sig_* node classes (must be declared *after* reloading
     # the module since otherwise the objects are not the correct ones)
diff --git a/tests/test_application.py b/tests/test_application.py
index b2bd7bbc66c..73c3f3556ca 100644
--- a/tests/test_application.py
+++ b/tests/test_application.py
@@ -20,6 +20,7 @@
 
 if TYPE_CHECKING:
     import os
+    from typing import Any
 
 
 def test_instantiation(
@@ -50,7 +51,7 @@ def test_instantiation(
 
 @pytest.mark.sphinx('html', testroot='root')
 def test_events(app: SphinxTestApp) -> None:
-    def empty():
+    def empty() -> None:
         pass
 
     with pytest.raises(ExtensionError) as excinfo:
@@ -62,7 +63,7 @@ def empty():
         app.add_event('my_event')
     assert "Event 'my_event' already present" in str(excinfo.value)
 
-    def mock_callback(a_app, *args):
+    def mock_callback(a_app: SphinxTestApp, *args: Any) -> str:
         assert a_app is app
         assert emit_args == args
         return 'ret'
diff --git a/tests/test_domains/test_domain_py.py b/tests/test_domains/test_domain_py.py
index b3c246297ad..26c79ffd8fb 100644
--- a/tests/test_domains/test_domain_py.py
+++ b/tests/test_domains/test_domain_py.py
@@ -508,6 +508,28 @@ def test_parse_annotation(app):
         ),
     )
 
+    doctree = _parse_annotation('*tuple[str, int]', app.env)
+    assert_node(
+        doctree,
+        (
+            [desc_sig_operator, '*'],
+            [pending_xref, 'tuple'],
+            [desc_sig_punctuation, '['],
+            [pending_xref, 'str'],
+            [desc_sig_punctuation, ','],
+            desc_sig_space,
+            [pending_xref, 'int'],
+            [desc_sig_punctuation, ']'],
+        ),
+    )
+    assert_node(
+        doctree[1],
+        pending_xref,
+        refdomain='py',
+        reftype='class',
+        reftarget='tuple',
+    )
+
 
 @pytest.mark.sphinx('html', testroot='_blank')
 def test_parse_annotation_suppress(app):
diff --git a/tests/test_events.py b/tests/test_events.py
index 56f76511dcb..412116c9f4b 100644
--- a/tests/test_events.py
+++ b/tests/test_events.py
@@ -3,12 +3,18 @@
 from __future__ import annotations
 
 from types import SimpleNamespace
+from typing import TYPE_CHECKING
 
 import pytest
 
 from sphinx.errors import ExtensionError
 from sphinx.events import EventManager
 
+if TYPE_CHECKING:
+    from typing import NoReturn
+
+    from sphinx.application import Sphinx
+
 
 def test_event_priority() -> None:
     result = []
@@ -27,7 +33,7 @@ def test_event_priority() -> None:
 
 
 def test_event_allowed_exceptions() -> None:
-    def raise_error(app):
+    def raise_error(app: Sphinx) -> NoReturn:
         raise RuntimeError
 
     app = SimpleNamespace(pdb=False)  # pass a dummy object as an app
@@ -44,7 +50,7 @@ def raise_error(app):
 
 
 def test_event_pdb() -> None:
-    def raise_error(app):
+    def raise_error(app: Sphinx) -> NoReturn:
         raise RuntimeError
 
     app = SimpleNamespace(pdb=True)  # pass a dummy object as an app
diff --git a/tests/test_pycode/test_pycode_ast.py b/tests/test_pycode/test_pycode_ast.py
index 6ebc1a91099..409e5806d1b 100644
--- a/tests/test_pycode/test_pycode_ast.py
+++ b/tests/test_pycode/test_pycode_ast.py
@@ -62,6 +62,7 @@
          'x[:, np.newaxis, :, :]'),                # Index, Subscript, numpy extended syntax
         ('y[:, 1:3][np.array([0, 2, 4]), :]',
          'y[:, 1:3][np.array([0, 2, 4]), :]'),     # Index, 2x Subscript, numpy extended syntax
+        ('*tuple[str, int]', '*tuple[str, int]'),  # Starred
     ],
 )  # fmt: skip
 def test_unparse(source, expected):
diff --git a/tests/test_util/test_util_i18n.py b/tests/test_util/test_util_i18n.py
index 4326b4382dd..31e774b7932 100644
--- a/tests/test_util/test_util_i18n.py
+++ b/tests/test_util/test_util_i18n.py
@@ -4,12 +4,10 @@
 
 import datetime
 import os
-import sys
 import time
 from pathlib import Path
 from typing import TYPE_CHECKING
 
-import babel
 import pytest
 from babel.messages.mofile import read_mo
 
@@ -60,11 +58,6 @@ def test_catalog_write_mo(tmp_path):
         assert read_mo(f) is not None
 
 
-# https://github.com/python-babel/babel/issues/1183
-@pytest.mark.xfail(
-    sys.platform == 'win32' and babel.__version__ == '2.17.0',
-    reason='Windows tests fail with Babel 2.17',
-)
 def test_format_date():
     date = datetime.date(2016, 2, 7)
 
diff --git a/tests/test_util/test_util_matching.py b/tests/test_util/test_util_matching.py
index 0c17280aa6e..a2ad1ff5ba6 100644
--- a/tests/test_util/test_util_matching.py
+++ b/tests/test_util/test_util_matching.py
@@ -2,8 +2,13 @@
 
 from __future__ import annotations
 
+from typing import TYPE_CHECKING
+
 from sphinx.util.matching import Matcher, compile_matchers, get_matching_files
 
+if TYPE_CHECKING:
+    from pathlib import Path
+
 
 def test_compile_matchers() -> None:
     # exact matching
@@ -85,7 +90,7 @@ def test_Matcher() -> None:
     assert matcher('subdir/world.py')
 
 
-def test_get_matching_files_all(rootdir):
+def test_get_matching_files_all(rootdir: Path) -> None:
     files = get_matching_files(rootdir / 'test-root')
     assert sorted(files) == [
         'Makefile',
@@ -133,7 +138,7 @@ def test_get_matching_files_all(rootdir):
     ]
 
 
-def test_get_matching_files_all_exclude_single(rootdir):
+def test_get_matching_files_all_exclude_single(rootdir: Path) -> None:
     files = get_matching_files(rootdir / 'test-root', exclude_patterns=['**.html'])
     assert sorted(files) == [
         'Makefile',
@@ -178,7 +183,7 @@ def test_get_matching_files_all_exclude_single(rootdir):
     ]
 
 
-def test_get_matching_files_all_exclude_multiple(rootdir):
+def test_get_matching_files_all_exclude_multiple(rootdir: Path) -> None:
     files = get_matching_files(
         rootdir / 'test-root', exclude_patterns=['**.html', '**.inc']
     )
@@ -218,7 +223,7 @@ def test_get_matching_files_all_exclude_multiple(rootdir):
     ]
 
 
-def test_get_matching_files_all_exclude_nonexistent(rootdir):
+def test_get_matching_files_all_exclude_nonexistent(rootdir: Path) -> None:
     files = get_matching_files(rootdir / 'test-root', exclude_patterns=['halibut/**'])
     assert sorted(files) == [
         'Makefile',
@@ -266,7 +271,7 @@ def test_get_matching_files_all_exclude_nonexistent(rootdir):
     ]
 
 
-def test_get_matching_files_all_include_single(rootdir):
+def test_get_matching_files_all_include_single(rootdir: Path) -> None:
     files = get_matching_files(rootdir / 'test-root', include_patterns=['subdir/**'])
     assert sorted(files) == [
         'subdir/excluded.txt',
@@ -278,7 +283,7 @@ def test_get_matching_files_all_include_single(rootdir):
     ]
 
 
-def test_get_matching_files_all_include_multiple(rootdir):
+def test_get_matching_files_all_include_multiple(rootdir: Path) -> None:
     files = get_matching_files(
         rootdir / 'test-root', include_patterns=['special/**', 'subdir/**']
     )
@@ -294,12 +299,12 @@ def test_get_matching_files_all_include_multiple(rootdir):
     ]
 
 
-def test_get_matching_files_all_include_nonexistent(rootdir):
+def test_get_matching_files_all_include_nonexistent(rootdir: Path) -> None:
     files = get_matching_files(rootdir / 'test-root', include_patterns=['halibut/**'])
     assert sorted(files) == []
 
 
-def test_get_matching_files_all_include_prefix(rootdir):
+def test_get_matching_files_all_include_prefix(rootdir: Path) -> None:
     files = get_matching_files(rootdir / 'test-root', include_patterns=['autodoc*'])
     assert sorted(files) == [
         'autodoc.txt',
@@ -307,7 +312,7 @@ def test_get_matching_files_all_include_prefix(rootdir):
     ]
 
 
-def test_get_matching_files_all_include_question_mark(rootdir):
+def test_get_matching_files_all_include_question_mark(rootdir: Path) -> None:
     files = get_matching_files(rootdir / 'test-root', include_patterns=['img.???'])
     assert sorted(files) == [
         'img.gif',