Repository: pypa/pip
Pull request #12728 (2024-05-24 12:59:41)

Stop explicitly listing wheel as a build dependency
setuptools' get_requires_for_build_wheel() hook already injects this dependency when building wheels is requested.

See also 64d89385ce4b5ae2d05d176e7746bff0baaabafd.

<!---
Thank you for your soon to be pull request. Before you submit this, please
double check to make sure that you've added a news file fragment. In pip we
generate our NEWS.rst from multiple news fragment files, and all pull requests
require either a news file fragment or a marker to indicate they don't require
one.

To read more about adding a news file fragment for your PR, please check out
our documentation at: https://pip.pypa.io/en/latest/development/contributing/#news-entries
-->

cc @webknjaz 


Patch:
diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml
index 4125549ab..7880e0cb4 100644
--- a/.pre-commit-config.yaml
+++ b/.pre-commit-config.yaml
@@ -17,18 +17,18 @@ repos:
     exclude: .patch
 
 - repo: https://github.com/psf/black-pre-commit-mirror
-  rev: 24.4.0
+  rev: 24.4.2
   hooks:
   - id: black
 
 - repo: https://github.com/astral-sh/ruff-pre-commit
-  rev: v0.4.1
+  rev: v0.5.0
   hooks:
     - id: ruff
       args: [--fix, --exit-non-zero-on-fix]
 
 - repo: https://github.com/pre-commit/mirrors-mypy
-  rev: v1.9.0
+  rev: v1.10.0
   hooks:
   - id: mypy
     exclude: tests/data
@@ -54,7 +54,7 @@ repos:
     exclude: NEWS.rst  # The errors flagged in NEWS.rst are old.
 
 - repo: https://github.com/codespell-project/codespell
-  rev: v2.2.6
+  rev: v2.3.0
   hooks:
     - id: codespell
       exclude: AUTHORS.txt|tests/data
diff --git a/AUTHORS.txt b/AUTHORS.txt
index 1b80e97c3..10317a284 100644
--- a/AUTHORS.txt
+++ b/AUTHORS.txt
@@ -275,6 +275,7 @@ Florian Briand
 Florian Rathgeber
 Francesco
 Francesco Montesano
+Fredrik Orderud
 Frost Ming
 Gabriel Curio
 Gabriel de Perthuis
@@ -477,6 +478,7 @@ matthew
 Matthew Einhorn
 Matthew Feickert
 Matthew Gilliard
+Matthew Hughes
 Matthew Iversen
 Matthew Treinish
 Matthew Trumbell
@@ -511,6 +513,7 @@ Miro Hrončok
 Monica Baluna
 montefra
 Monty Taylor
+mrKazzila
 Muha Ajjan
 Nadav Wexler
 Nahuel Ambrosini
@@ -665,6 +668,7 @@ Shantanu
 shenxianpeng
 shireenrao
 Shivansh-007
+Shixian Sheng
 Shlomi Fish
 Shovan Maity
 Simeon Visser
diff --git a/NEWS.rst b/NEWS.rst
index eea12074d..3e7940c5f 100644
--- a/NEWS.rst
+++ b/NEWS.rst
@@ -9,6 +9,64 @@
 
 .. towncrier release notes start
 
+24.1.2 (2024-07-07)
+===================
+
+Bug Fixes
+---------
+
+- Fix finding hardlink targets in tar files with an ignored top-level directory. (`#12781 <https://github.com/pypa/pip/issues/12781>`_)
+
+24.1.1 (2024-06-26)
+===================
+
+Bug Fixes
+---------
+
+- Actually use system trust stores when the truststore feature is enabled.
+
+Vendored Libraries
+------------------
+
+- Upgrade requests to 2.32.3
+
+
+24.1 (2024-06-20)
+=================
+
+Vendored Libraries
+------------------
+
+- Upgrade truststore to 0.9.1.
+
+
+24.1b2 (2024-06-12)
+===================
+
+Features
+--------
+
+- Report informative messages about invalid requirements. (`#12713 <https://github.com/pypa/pip/issues/12713>`_)
+
+Bug Fixes
+---------
+
+- Eagerly import the self version check logic to avoid crashes while upgrading or downgrading pip at the same time. (`#12675 <https://github.com/pypa/pip/issues/12675>`_)
+- Accommodate for mismatches between different sources of truth for extra names, for packages generated by ``setuptools``. (`#12688 <https://github.com/pypa/pip/issues/12688>`_)
+- Accommodate for development versions of CPython ending in ``+`` in the version string. (`#12691 <https://github.com/pypa/pip/issues/12691>`_)
+
+Vendored Libraries
+------------------
+
+- Upgrade packaging to 24.1
+- Upgrade requests to 2.32.0
+- Remove vendored colorama
+- Remove vendored six
+- Remove vendored webencodings
+- Remove vendored charset_normalizer
+
+  ``requests`` provides optional character detection support on some APIs when processing ambiguous bytes. This isn't relevant for pip to function and we're able to remove it due to recent upstream changes.
+
 24.1b1 (2024-05-06)
 ===================
 
diff --git a/docs/html/development/release-process.rst b/docs/html/development/release-process.rst
index 21a6ca562..5bf0d278b 100644
--- a/docs/html/development/release-process.rst
+++ b/docs/html/development/release-process.rst
@@ -18,6 +18,9 @@ that month will be up to the release manager for that release. If there are
 no changes, then that release month is skipped and the next release will be
 3 months later.
 
+The version number of pip is ``YY.N``, where ``YY`` is the year of the release
+and ``N`` identifies the quarter of the year (0-3).
+
 The release manager may, at their discretion, choose whether or not there
 will be a pre-release period for a release, and if there is may extend that
 period into the next month if needed.
diff --git a/docs/html/ux-research-design/contribute.md b/docs/html/ux-research-design/contribute.md
index 48aab68e6..2e91d60c4 100644
--- a/docs/html/ux-research-design/contribute.md
+++ b/docs/html/ux-research-design/contribute.md
@@ -21,4 +21,4 @@ You can help the team by testing new features as they are released to the commun
 If you believe that you have found a user experience bug in pip, or you have ideas for how pip could be made better for all users, please file an issue on the [pip issue tracker](https://github.com/pypa/pip/issues/new).
 
 You can also help improve pip’s user experience by [working on UX issues](https://github.com/pypa/pip/issues?q=is%3Aissue+label%3AUX+is%3Aopen). Issues that are ideal for new contributors are marked with “[good first issue](https://github.com/pypa/pip/issues?q=is%3Aopen+is%3Aissue+label%3A%22good+first+issue%22)”. Explore the
-[UX Guidance](guidance) if you have questions.
+[UX Guidance](guidance.md) if you have questions.
diff --git a/news/12675.bugfix.rst b/news/12675.bugfix.rst
deleted file mode 100644
index 0d448f38b..000000000
--- a/news/12675.bugfix.rst
+++ /dev/null
@@ -1,2 +0,0 @@
-Eagerly import the self version check logic to avoid crashes while upgrading
-or downgrading pip at the same time.
diff --git a/news/12683.feature.rst b/news/12683.feature.rst
new file mode 100644
index 000000000..2e949cd07
--- /dev/null
+++ b/news/12683.feature.rst
@@ -0,0 +1,2 @@
+Disable pip's self version check when invoking a pip subprocess to install
+PEP 517 build requirements.
diff --git a/news/12702.feature.rst b/news/12702.feature.rst
deleted file mode 100644
index 881906af6..000000000
--- a/news/12702.feature.rst
+++ /dev/null
@@ -1,1 +0,0 @@
-Remove vendored colorama.
diff --git a/news/12712.feature.rst b/news/12712.feature.rst
new file mode 100644
index 000000000..3a08cdb10
--- /dev/null
+++ b/news/12712.feature.rst
@@ -0,0 +1,2 @@
+Improve dependency resolution performance by caching platform compatibility
+tags during wheel cache lookup.
diff --git a/news/12728.feature.rst b/news/12728.feature.rst
new file mode 100644
index 000000000..923d18707
--- /dev/null
+++ b/news/12728.feature.rst
@@ -0,0 +1,5 @@
+``wheel`` is no longer explicitly listed as a build depepndency of ``pip``.
+``setuptools`` already injects this dependency in the ``get_requires_for_build_wheel()`` hook.
+This makes no difference for users of ``pip``.
+This makes no difference when building wheels of ``pip``.
+This avoids an unnecessary dependency on ``wheel`` when building the source distribution of ``pip``.
diff --git a/news/12776.trivial.rst b/news/12776.trivial.rst
new file mode 100644
index 000000000..87d2f8e79
--- /dev/null
+++ b/news/12776.trivial.rst
@@ -0,0 +1,1 @@
+Use prerelease version of CFFI for Python 3.13 testing
diff --git a/news/12781.bugfix.rst b/news/12781.bugfix.rst
new file mode 100644
index 000000000..6bd43d347
--- /dev/null
+++ b/news/12781.bugfix.rst
@@ -0,0 +1,1 @@
+Fix finding hardlink targets in tar files with an ignored top-level directory.
diff --git a/news/12791.bugfix.rst b/news/12791.bugfix.rst
new file mode 100644
index 000000000..c19345d43
--- /dev/null
+++ b/news/12791.bugfix.rst
@@ -0,0 +1,2 @@
+Improve pip install performance. The installed packages printout is
+now calculated in linear time instead of quadratic time.
diff --git a/news/12796.vendor.rst b/news/12796.vendor.rst
new file mode 100644
index 000000000..6384a5b14
--- /dev/null
+++ b/news/12796.vendor.rst
@@ -0,0 +1,1 @@
+Update the preload list for the ``DEBUNDLED`` case, to replace ``pep517`` that has been renamed to ``pyproject_hooks``.
diff --git a/news/12797.vendor.rst b/news/12797.vendor.rst
new file mode 100644
index 000000000..7842883dd
--- /dev/null
+++ b/news/12797.vendor.rst
@@ -0,0 +1,1 @@
+Use tomllib from the stdlib if available, rather than tomli
diff --git a/news/12805.trivial.rst b/news/12805.trivial.rst
new file mode 100644
index 000000000..56f61f772
--- /dev/null
+++ b/news/12805.trivial.rst
@@ -0,0 +1,1 @@
+Update ruff to 0.5.0
diff --git a/news/charset_normalizer.vendor.rst b/news/charset_normalizer.vendor.rst
deleted file mode 100644
index b4a64c7c1..000000000
--- a/news/charset_normalizer.vendor.rst
+++ /dev/null
@@ -1,5 +0,0 @@
-Remove vendored charset_normalizer.
-
-Requests provides optional character detection support on some APIs
-when processing ambiguous bytes. This isn't relevant for pip to function
-and we're able to remove it due to recent upstream changes.
diff --git a/news/requests.vendor.rst b/news/requests.vendor.rst
deleted file mode 100644
index edae29fdb..000000000
--- a/news/requests.vendor.rst
+++ /dev/null
@@ -1,1 +0,0 @@
-Upgrade Requests to 2.32.0
diff --git a/news/six.vendor.rst b/news/six.vendor.rst
deleted file mode 100644
index 9b332be92..000000000
--- a/news/six.vendor.rst
+++ /dev/null
@@ -1,1 +0,0 @@
-Remove vendored six.
diff --git a/news/typing_extensions.vendor.rst b/news/typing_extensions.vendor.rst
new file mode 100644
index 000000000..580ac5dfb
--- /dev/null
+++ b/news/typing_extensions.vendor.rst
@@ -0,0 +1,1 @@
+Upgrade typing_extensions to 4.12.2
diff --git a/news/webencodings.vendor.rst b/news/webencodings.vendor.rst
deleted file mode 100644
index 35b9f9b9e..000000000
--- a/news/webencodings.vendor.rst
+++ /dev/null
@@ -1,1 +0,0 @@
-Remove vendored webencodings.
diff --git a/noxfile.py b/noxfile.py
index 92d0e2438..370e2fe63 100644
--- a/noxfile.py
+++ b/noxfile.py
@@ -294,6 +294,11 @@ def prepare_release(session: nox.Session) -> None:
 
     session.log("# Generating NEWS")
     release.generate_news(session, version)
+    if sys.stdin.isatty():
+        input(
+            "Please review the NEWS file, make necessary edits, and stage them.\n"
+            "Press Enter to continue..."
+        )
 
     session.log(f"# Bumping for release {version}")
     release.update_version_file(version, VERSION_FILE)
diff --git a/pyproject.toml b/pyproject.toml
index 8cbf9c9f4..cf098eea0 100644
--- a/pyproject.toml
+++ b/pyproject.toml
@@ -41,7 +41,7 @@ Changelog = "https://pip.pypa.io/en/stable/news/"
 
 [build-system]
 # The lower bound is for <https://github.com/pypa/setuptools/issues/3865>.
-requires = ["setuptools>=67.6.1", "wheel"]
+requires = ["setuptools>=67.6.1"]
 build-backend = "setuptools.build_meta"
 
 [tool.setuptools]
@@ -151,7 +151,6 @@ distlib = "https://bitbucket.org/pypa/distlib/raw/master/LICENSE.txt"
 
 [tool.ruff]
 src = ["src"]
-target-version = "py38"
 line-length = 88
 extend-exclude = [
     "_vendor",
diff --git a/src/pip/__init__.py b/src/pip/__init__.py
index cf95985fa..531eec2d9 100644
--- a/src/pip/__init__.py
+++ b/src/pip/__init__.py
@@ -1,6 +1,6 @@
 from typing import List, Optional
 
-__version__ = "24.1.dev1"
+__version__ = "24.2.dev0"
 
 
 def main(args: Optional[List[str]] = None) -> int:
diff --git a/src/pip/_internal/build_env.py b/src/pip/_internal/build_env.py
index 838de8647..5cb903fa5 100644
--- a/src/pip/_internal/build_env.py
+++ b/src/pip/_internal/build_env.py
@@ -241,6 +241,7 @@ class BuildEnvironment:
             "--prefix",
             prefix.path,
             "--no-warn-script-location",
+            "--disable-pip-version-check",
         ]
         if logger.getEffectiveLevel() <= logging.DEBUG:
             args.append("-vv")
diff --git a/src/pip/_internal/cli/parser.py b/src/pip/_internal/cli/parser.py
index ae554b24c..b7d7c1f60 100644
--- a/src/pip/_internal/cli/parser.py
+++ b/src/pip/_internal/cli/parser.py
@@ -6,7 +6,7 @@ import shutil
 import sys
 import textwrap
 from contextlib import suppress
-from typing import Any, Dict, Generator, List, Tuple
+from typing import Any, Dict, Generator, List, Optional, Tuple
 
 from pip._internal.cli.status_codes import UNKNOWN_ERROR
 from pip._internal.configuration import Configuration, ConfigurationError
@@ -67,7 +67,7 @@ class PrettyHelpFormatter(optparse.IndentedHelpFormatter):
         msg = "\nUsage: {}\n".format(self.indent_lines(textwrap.dedent(usage), "  "))
         return msg
 
-    def format_description(self, description: str) -> str:
+    def format_description(self, description: Optional[str]) -> str:
         # leave full control over description to us
         if description:
             if hasattr(self.parser, "main"):
@@ -85,7 +85,7 @@ class PrettyHelpFormatter(optparse.IndentedHelpFormatter):
         else:
             return ""
 
-    def format_epilog(self, epilog: str) -> str:
+    def format_epilog(self, epilog: Optional[str]) -> str:
         # leave full control over epilog to us
         if epilog:
             return epilog
diff --git a/src/pip/_internal/commands/install.py b/src/pip/_internal/commands/install.py
index d5b06c8c7..5e076a2d1 100644
--- a/src/pip/_internal/commands/install.py
+++ b/src/pip/_internal/commands/install.py
@@ -7,6 +7,7 @@ import site
 from optparse import SUPPRESS_HELP, Values
 from typing import List, Optional
 
+from pip._vendor.packaging.utils import canonicalize_name
 from pip._vendor.rich import print_json
 
 from pip._internal.cache import WheelCache
@@ -472,17 +473,21 @@ class InstallCommand(RequirementCommand):
             )
             env = get_environment(lib_locations)
 
+            # Display a summary of installed packages, with extra care to
+            # display a package name as it was requested by the user.
             installed.sort(key=operator.attrgetter("name"))
-            items = []
-            for result in installed:
-                item = result.name
-                try:
-                    installed_dist = env.get_distribution(item)
-                    if installed_dist is not None:
-                        item = f"{item}-{installed_dist.version}"
-                except Exception:
-                    pass
-                items.append(item)
+            summary = []
+            installed_versions = {}
+            for distribution in env.iter_all_distributions():
+                installed_versions[distribution.canonical_name] = distribution.version
+            for package in installed:
+                display_name = package.name
+                version = installed_versions.get(canonicalize_name(display_name), None)
+                if version:
+                    text = f"{display_name}-{version}"
+                else:
+                    text = display_name
+                summary.append(text)
 
             if conflicts is not None:
                 self._warn_about_conflicts(
@@ -490,7 +495,7 @@ class InstallCommand(RequirementCommand):
                     resolver_variant=self.determine_resolver_variant(options),
                 )
 
-            installed_desc = " ".join(items)
+            installed_desc = " ".join(summary)
             if installed_desc:
                 write_output(
                     "Successfully installed %s",
diff --git a/src/pip/_internal/metadata/importlib/_envs.py b/src/pip/_internal/metadata/importlib/_envs.py
index 2df738fc7..c82e1ffc9 100644
--- a/src/pip/_internal/metadata/importlib/_envs.py
+++ b/src/pip/_internal/metadata/importlib/_envs.py
@@ -181,9 +181,10 @@ class Environment(BaseEnvironment):
             yield from finder.find_linked(location)
 
     def get_distribution(self, name: str) -> Optional[BaseDistribution]:
+        canonical_name = canonicalize_name(name)
         matches = (
             distribution
             for distribution in self.iter_all_distributions()
-            if distribution.canonical_name == canonicalize_name(name)
+            if distribution.canonical_name == canonical_name
         )
         return next(matches, None)
diff --git a/src/pip/_internal/metadata/pkg_resources.py b/src/pip/_internal/metadata/pkg_resources.py
index 235556781..4ea84f93a 100644
--- a/src/pip/_internal/metadata/pkg_resources.py
+++ b/src/pip/_internal/metadata/pkg_resources.py
@@ -11,7 +11,6 @@ from typing import (
     Mapping,
     NamedTuple,
     Optional,
-    cast,
 )
 
 from pip._vendor import pkg_resources
@@ -92,8 +91,7 @@ class Distribution(BaseDistribution):
     def _extra_mapping(self) -> Mapping[NormalizedName, str]:
         if self.__extra_mapping is None:
             self.__extra_mapping = {
-                canonicalize_name(extra): pkg_resources.safe_extra(cast(str, extra))
-                for extra in self.metadata.get_all("Provides-Extra", [])
+                canonicalize_name(extra): extra for extra in self._dist.extras
             }
 
         return self.__extra_mapping
diff --git a/src/pip/_internal/pyproject.py b/src/pip/_internal/pyproject.py
index 8de36b873..b9b9e3f19 100644
--- a/src/pip/_internal/pyproject.py
+++ b/src/pip/_internal/pyproject.py
@@ -1,9 +1,14 @@
 import importlib.util
 import os
+import sys
 from collections import namedtuple
 from typing import Any, List, Optional
 
-from pip._vendor import tomli
+if sys.version_info >= (3, 11):
+    import tomllib
+else:
+    from pip._vendor import tomli as tomllib
+
 from pip._vendor.packaging.requirements import InvalidRequirement, Requirement
 
 from pip._internal.exceptions import (
@@ -61,7 +66,7 @@ def load_pyproject_toml(
 
     if has_pyproject:
         with open(pyproject_toml, encoding="utf-8") as f:
-            pp_toml = tomli.loads(f.read())
+            pp_toml = tomllib.loads(f.read())
         build_system = pp_toml.get("build-system")
     else:
         build_system = None
diff --git a/src/pip/_internal/req/constructors.py b/src/pip/_internal/req/constructors.py
index b2c9505a9..b8e170f2a 100644
--- a/src/pip/_internal/req/constructors.py
+++ b/src/pip/_internal/req/constructors.py
@@ -206,8 +206,8 @@ def parse_req_from_editable(editable_req: str) -> RequirementParts:
     if name is not None:
         try:
             req: Optional[Requirement] = Requirement(name)
-        except InvalidRequirement:
-            raise InstallationError(f"Invalid requirement: '{name}'")
+        except InvalidRequirement as exc:
+            raise InstallationError(f"Invalid requirement: {name!r}: {exc}")
     else:
         req = None
 
@@ -360,7 +360,7 @@ def parse_req_from_line(name: str, line_source: Optional[str]) -> RequirementPar
     def _parse_req_string(req_as_string: str) -> Requirement:
         try:
             return get_requirement(req_as_string)
-        except InvalidRequirement:
+        except InvalidRequirement as exc:
             if os.path.sep in req_as_string:
                 add_msg = "It looks like a path."
                 add_msg += deduce_helpful_msg(req_as_string)
@@ -370,7 +370,7 @@ def parse_req_from_line(name: str, line_source: Optional[str]) -> RequirementPar
                 add_msg = "= is not a valid operator. Did you mean == ?"
             else:
                 add_msg = ""
-            msg = with_source(f"Invalid requirement: {req_as_string!r}")
+            msg = with_source(f"Invalid requirement: {req_as_string!r}: {exc}")
             if add_msg:
                 msg += f"\nHint: {add_msg}"
             raise InstallationError(msg)
@@ -429,8 +429,8 @@ def install_req_from_req_string(
 ) -> InstallRequirement:
     try:
         req = get_requirement(req_string)
-    except InvalidRequirement:
-        raise InstallationError(f"Invalid requirement: '{req_string}'")
+    except InvalidRequirement as exc:
+        raise InstallationError(f"Invalid requirement: {req_string!r}: {exc}")
 
     domains_not_allowed = [
         PyPI.file_storage_domain,
diff --git a/src/pip/_internal/req/req_install.py b/src/pip/_internal/req/req_install.py
index 213278588..afd0a9e5e 100644
--- a/src/pip/_internal/req/req_install.py
+++ b/src/pip/_internal/req/req_install.py
@@ -909,7 +909,7 @@ def check_legacy_setup_py_options(
             reason="--build-option and --global-option are deprecated.",
             issue=11859,
             replacement="to use --config-settings",
-            gone_in="24.2",
+            gone_in="25.0",
         )
         logger.warning(
             "Implying --no-binary=:all: due to the presence of "
diff --git a/src/pip/_internal/resolution/resolvelib/factory.py b/src/pip/_internal/resolution/resolvelib/factory.py
index 1f31d834b..145bdbf71 100644
--- a/src/pip/_internal/resolution/resolvelib/factory.py
+++ b/src/pip/_internal/resolution/resolvelib/factory.py
@@ -121,6 +121,7 @@ class Factory:
         self._extras_candidate_cache: Dict[
             Tuple[int, FrozenSet[NormalizedName]], ExtrasCandidate
         ] = {}
+        self._supported_tags_cache = get_supported()
 
         if not ignore_installed:
             env = get_default_environment()
@@ -608,7 +609,7 @@ class Factory:
         return self._wheel_cache.get_cache_entry(
             link=link,
             package_name=name,
-            supported_tags=get_supported(),
+            supported_tags=self._supported_tags_cache,
         )
 
     def get_dist_to_uninstall(self, candidate: Candidate) -> Optional[BaseDistribution]:
diff --git a/src/pip/_internal/utils/misc.py b/src/pip/_internal/utils/misc.py
index 48771c099..0a5c67b3b 100644
--- a/src/pip/_internal/utils/misc.py
+++ b/src/pip/_internal/utils/misc.py
@@ -149,7 +149,7 @@ def _onerror_ignore(*_args: Any) -> None:
 
 
 def _onerror_reraise(*_args: Any) -> None:
-    raise
+    raise  # noqa: PLE0704 - Bare exception used to reraise existing exception
 
 
 def rmtree_errorhandler(
diff --git a/src/pip/_internal/utils/unpacking.py b/src/pip/_internal/utils/unpacking.py
index 341269550..875e30e13 100644
--- a/src/pip/_internal/utils/unpacking.py
+++ b/src/pip/_internal/utils/unpacking.py
@@ -190,9 +190,19 @@ def untar_file(filename: str, location: str) -> None:
         else:
             default_mode_plus_executable = _get_default_mode_plus_executable()
 
+            if leading:
+                # Strip the leading directory from all files in the archive,
+                # including hardlink targets (which are relative to the
+                # unpack location).
+                for member in tar.getmembers():
+                    name_lead, name_rest = split_leading_dir(member.name)
+                    member.name = name_rest
+                    if member.islnk():
+                        lnk_lead, lnk_rest = split_leading_dir(member.linkname)
+                        if lnk_lead == name_lead:
+                            member.linkname = lnk_rest
+
             def pip_filter(member: tarfile.TarInfo, path: str) -> tarfile.TarInfo:
-                if leading:
-                    member.name = split_leading_dir(member.name)[1]
                 orig_mode = member.mode
                 try:
                     try:
diff --git a/src/pip/_vendor/README.rst b/src/pip/_vendor/README.rst
index a21314543..e3fe8041c 100644
--- a/src/pip/_vendor/README.rst
+++ b/src/pip/_vendor/README.rst
@@ -80,12 +80,7 @@ instead opt to patch the software they distribute to debundle it and make it
 rely on the global versions of the software that they already have packaged
 (which may have its own patches applied to it). We (the pip team) would prefer
 it if pip was *not* debundled in this manner due to the above reasons and
-instead we would prefer it if pip would be left intact as it is now. The one
-exception to this, is it is acceptable to remove the
-``pip/_vendor/requests/cacert.pem`` file provided you ensure that the
-``ssl.get_default_verify_paths().cafile`` API returns the correct CA bundle for
-your system. This will ensure that pip will use your system provided CA bundle
-instead of the copy bundled with pip.
+instead we would prefer it if pip would be left intact as it is now.
 
 In the longer term, if someone has a *portable* solution to the above problems,
 other than the bundling method we currently use, that doesn't add additional
diff --git a/src/pip/_vendor/__init__.py b/src/pip/_vendor/__init__.py
index 50537ab9d..12be4fd9a 100644
--- a/src/pip/_vendor/__init__.py
+++ b/src/pip/_vendor/__init__.py
@@ -65,10 +65,10 @@ if DEBUNDLED:
     vendored("packaging")
     vendored("packaging.version")
     vendored("packaging.specifiers")
-    vendored("pep517")
     vendored("pkg_resources")
     vendored("platformdirs")
     vendored("progress")
+    vendored("pyproject_hooks")
     vendored("requests")
     vendored("requests.exceptions")
     vendored("requests.packages")
@@ -111,6 +111,7 @@ if DEBUNDLED:
     vendored("rich.text")
     vendored("rich.traceback")
     vendored("tenacity")
-    vendored("tomli")
+    if sys.version_info < (3, 11):
+        vendored("tomli")
     vendored("truststore")
     vendored("urllib3")
diff --git a/src/pip/_vendor/packaging/__init__.py b/src/pip/_vendor/packaging/__init__.py
index e7c0aa12c..9ba41d835 100644
--- a/src/pip/_vendor/packaging/__init__.py
+++ b/src/pip/_vendor/packaging/__init__.py
@@ -6,7 +6,7 @@ __title__ = "packaging"
 __summary__ = "Core utilities for Python packages"
 __uri__ = "https://github.com/pypa/packaging"
 
-__version__ = "24.0"
+__version__ = "24.1"
 
 __author__ = "Donald Stufft and individual contributors"
 __email__ = "donald@stufft.io"
diff --git a/src/pip/_vendor/packaging/_elffile.py b/src/pip/_vendor/packaging/_elffile.py
index 6fb19b30b..f7a02180b 100644
--- a/src/pip/_vendor/packaging/_elffile.py
+++ b/src/pip/_vendor/packaging/_elffile.py
@@ -8,10 +8,12 @@ Based on: https://gist.github.com/lyssdod/f51579ae8d93c8657a5564aefc2ffbca
 ELF header: https://refspecs.linuxfoundation.org/elf/gabi4+/ch4.eheader.html
 """
 
+from __future__ import annotations
+
 import enum
 import os
 import struct
-from typing import IO, Optional, Tuple
+from typing import IO
 
 
 class ELFInvalid(ValueError):
@@ -87,11 +89,11 @@ class ELFFile:
         except struct.error as e:
             raise ELFInvalid("unable to parse machine and section information") from e
 
-    def _read(self, fmt: str) -> Tuple[int, ...]:
+    def _read(self, fmt: str) -> tuple[int, ...]:
         return struct.unpack(fmt, self._f.read(struct.calcsize(fmt)))
 
     @property
-    def interpreter(self) -> Optional[str]:
+    def interpreter(self) -> str | None:
         """
         The path recorded in the ``PT_INTERP`` section header.
         """
diff --git a/src/pip/_vendor/packaging/_manylinux.py b/src/pip/_vendor/packaging/_manylinux.py
index ad62505f3..08f651fbd 100644
--- a/src/pip/_vendor/packaging/_manylinux.py
+++ b/src/pip/_vendor/packaging/_manylinux.py
@@ -1,3 +1,5 @@
+from __future__ import annotations
+
 import collections
 import contextlib
 import functools
@@ -5,7 +7,7 @@ import os
 import re
 import sys
 import warnings
-from typing import Dict, Generator, Iterator, NamedTuple, Optional, Sequence, Tuple
+from typing import Generator, Iterator, NamedTuple, Sequence
 
 from ._elffile import EIClass, EIData, ELFFile, EMachine
 
@@ -17,7 +19,7 @@ EF_ARM_ABI_FLOAT_HARD = 0x00000400
 # `os.PathLike` not a generic type until Python 3.9, so sticking with `str`
 # as the type for `path` until then.
 @contextlib.contextmanager
-def _parse_elf(path: str) -> Generator[Optional[ELFFile], None, None]:
+def _parse_elf(path: str) -> Generator[ELFFile | None, None, None]:
     try:
         with open(path, "rb") as f:
             yield ELFFile(f)
@@ -72,7 +74,7 @@ def _have_compatible_abi(executable: str, archs: Sequence[str]) -> bool:
 # For now, guess what the highest minor version might be, assume it will
 # be 50 for testing. Once this actually happens, update the dictionary
 # with the actual value.
-_LAST_GLIBC_MINOR: Dict[int, int] = collections.defaultdict(lambda: 50)
+_LAST_GLIBC_MINOR: dict[int, int] = collections.defaultdict(lambda: 50)
 
 
 class _GLibCVersion(NamedTuple):
@@ -80,7 +82,7 @@ class _GLibCVersion(NamedTuple):
     minor: int
 
 
-def _glibc_version_string_confstr() -> Optional[str]:
+def _glibc_version_string_confstr() -> str | None:
     """
     Primary implementation of glibc_version_string using os.confstr.
     """
@@ -90,7 +92,7 @@ def _glibc_version_string_confstr() -> Optional[str]:
     # https://github.com/python/cpython/blob/fcf1d003bf4f0100c/Lib/platform.py#L175-L183
     try:
         # Should be a string like "glibc 2.17".
-        version_string: Optional[str] = os.confstr("CS_GNU_LIBC_VERSION")
+        version_string: str | None = os.confstr("CS_GNU_LIBC_VERSION")
         assert version_string is not None
         _, version = version_string.rsplit()
     except (AssertionError, AttributeError, OSError, ValueError):
@@ -99,7 +101,7 @@ def _glibc_version_string_confstr() -> Optional[str]:
     return version
 
 
-def _glibc_version_string_ctypes() -> Optional[str]:
+def _glibc_version_string_ctypes() -> str | None:
     """
     Fallback implementation of glibc_version_string using ctypes.
     """
@@ -143,12 +145,12 @@ def _glibc_version_string_ctypes() -> Optional[str]:
     return version_str
 
 
-def _glibc_version_string() -> Optional[str]:
+def _glibc_version_string() -> str | None:
     """Returns glibc version string, or None if not using glibc."""
     return _glibc_version_string_confstr() or _glibc_version_string_ctypes()
 
 
-def _parse_glibc_version(version_str: str) -> Tuple[int, int]:
+def _parse_glibc_version(version_str: str) -> tuple[int, int]:
     """Parse glibc version.
 
     We use a regexp instead of str.split because we want to discard any
@@ -167,8 +169,8 @@ def _parse_glibc_version(version_str: str) -> Tuple[int, int]:
     return int(m.group("major")), int(m.group("minor"))
 
 
-@functools.lru_cache()
-def _get_glibc_version() -> Tuple[int, int]:
+@functools.lru_cache
+def _get_glibc_version() -> tuple[int, int]:
     version_str = _glibc_version_string()
     if version_str is None:
         return (-1, -1)
diff --git a/src/pip/_vendor/packaging/_musllinux.py b/src/pip/_vendor/packaging/_musllinux.py
index 86419df9d..d2bf30b56 100644
--- a/src/pip/_vendor/packaging/_musllinux.py
+++ b/src/pip/_vendor/packaging/_musllinux.py
@@ -4,11 +4,13 @@ This module implements logic to detect if the currently running Python is
 linked against musl, and what musl version is used.
 """
 
+from __future__ import annotations
+
 import functools
 import re
 import subprocess
 import sys
-from typing import Iterator, NamedTuple, Optional, Sequence
+from typing import Iterator, NamedTuple, Sequence
 
 from ._elffile import ELFFile
 
@@ -18,7 +20,7 @@ class _MuslVersion(NamedTuple):
     minor: int
 
 
-def _parse_musl_version(output: str) -> Optional[_MuslVersion]:
+def _parse_musl_version(output: str) -> _MuslVersion | None:
     lines = [n for n in (n.strip() for n in output.splitlines()) if n]
     if len(lines) < 2 or lines[0][:4] != "musl":
         return None
@@ -28,8 +30,8 @@ def _parse_musl_version(output: str) -> Optional[_MuslVersion]:
     return _MuslVersion(major=int(m.group(1)), minor=int(m.group(2)))
 
 
-@functools.lru_cache()
-def _get_musl_version(executable: str) -> Optional[_MuslVersion]:
+@functools.lru_cache
+def _get_musl_version(executable: str) -> _MuslVersion | None:
     """Detect currently-running musl runtime version.
 
     This is done by checking the specified executable's dynamic linking
diff --git a/src/pip/_vendor/packaging/_parser.py b/src/pip/_vendor/packaging/_parser.py
index 684df7545..c1238c06e 100644
--- a/src/pip/_vendor/packaging/_parser.py
+++ b/src/pip/_vendor/packaging/_parser.py
@@ -1,11 +1,13 @@
 """Handwritten parser of dependency specifiers.
 
-The docstring for each __parse_* function contains ENBF-inspired grammar representing
+The docstring for each __parse_* function contains EBNF-inspired grammar representing
 the implementation.
 """
 
+from __future__ import annotations
+
 import ast
-from typing import Any, List, NamedTuple, Optional, Tuple, Union
+from typing import NamedTuple, Sequence, Tuple, Union
 
 from ._tokenizer import DEFAULT_RULES, Tokenizer
 
@@ -41,20 +43,16 @@ class Op(Node):
 
 MarkerVar = Union[Variable, Value]
 MarkerItem = Tuple[MarkerVar, Op, MarkerVar]
-# MarkerAtom = Union[MarkerItem, List["MarkerAtom"]]
-# MarkerList = List[Union["MarkerList", MarkerAtom, str]]
-# mypy does not support recursive type definition
-# https://github.com/python/mypy/issues/731
-MarkerAtom = Any
-MarkerList = List[Any]
+MarkerAtom = Union[MarkerItem, Sequence["MarkerAtom"]]
+MarkerList = Sequence[Union["MarkerList", MarkerAtom, str]]
 
 
 class ParsedRequirement(NamedTuple):
     name: str
     url: str
-    extras: List[str]
+    extras: list[str]
     specifier: str
-    marker: Optional[MarkerList]
+    marker: MarkerList | None
 
 
 # --------------------------------------------------------------------------------------
@@ -87,7 +85,7 @@ def _parse_requirement(tokenizer: Tokenizer) -> ParsedRequirement:
 
 def _parse_requirement_details(
     tokenizer: Tokenizer,
-) -> Tuple[str, str, Optional[MarkerList]]:
+) -> tuple[str, str, MarkerList | None]:
     """
     requirement_details = AT URL (WS requirement_marker?)?
                         | specifier WS? (requirement_marker)?
@@ -156,7 +154,7 @@ def _parse_requirement_marker(
     return marker
 
 
-def _parse_extras(tokenizer: Tokenizer) -> List[str]:
+def _parse_extras(tokenizer: Tokenizer) -> list[str]:
     """
     extras = (LEFT_BRACKET wsp* extras_list? wsp* RIGHT_BRACKET)?
     """
@@ -175,11 +173,11 @@ def _parse_extras(tokenizer: Tokenizer) -> List[str]:
     return extras
 
 
-def _parse_extras_list(tokenizer: Tokenizer) -> List[str]:
+def _parse_extras_list(tokenizer: Tokenizer) -> list[str]:
     """
     extras_list = identifier (wsp* ',' wsp* identifier)*
     """
-    extras: List[str] = []
+    extras: list[str] = []
 
     if not tokenizer.check("IDENTIFIER"):
         return extras
diff --git a/src/pip/_vendor/packaging/_tokenizer.py b/src/pip/_vendor/packaging/_tokenizer.py
index dd0d648d4..89d041605 100644
--- a/src/pip/_vendor/packaging/_tokenizer.py
+++ b/src/pip/_vendor/packaging/_tokenizer.py
@@ -1,7 +1,9 @@
+from __future__ import annotations
+
 import contextlib
 import re
 from dataclasses import dataclass
-from typing import Dict, Iterator, NoReturn, Optional, Tuple, Union
+from typing import Iterator, NoReturn
 
 from .specifiers import Specifier
 
@@ -21,7 +23,7 @@ class ParserSyntaxError(Exception):
         message: str,
         *,
         source: str,
-        span: Tuple[int, int],
+        span: tuple[int, int],
     ) -> None:
         self.span = span
         self.message = message
@@ -34,7 +36,7 @@ class ParserSyntaxError(Exception):
         return "\n    ".join([self.message, self.source, marker])
 
 
-DEFAULT_RULES: "Dict[str, Union[str, re.Pattern[str]]]" = {
+DEFAULT_RULES: dict[str, str | re.Pattern[str]] = {
     "LEFT_PARENTHESIS": r"\(",
     "RIGHT_PARENTHESIS": r"\)",
     "LEFT_BRACKET": r"\[",
@@ -96,13 +98,13 @@ class Tokenizer:
         self,
         source: str,
         *,
-        rules: "Dict[str, Union[str, re.Pattern[str]]]",
+        rules: dict[str, str | re.Pattern[str]],
     ) -> None:
         self.source = source
-        self.rules: Dict[str, re.Pattern[str]] = {
+        self.rules: dict[str, re.Pattern[str]] = {
             name: re.compile(pattern) for name, pattern in rules.items()
         }
-        self.next_token: Optional[Token] = None
+        self.next_token: Token | None = None
         self.position = 0
 
     def consume(self, name: str) -> None:
@@ -154,8 +156,8 @@ class Tokenizer:
         self,
         message: str,
         *,
-        span_start: Optional[int] = None,
-        span_end: Optional[int] = None,
+        span_start: int | None = None,
+        span_end: int | None = None,
     ) -> NoReturn:
         """Raise ParserSyntaxError at the given position."""
         span = (
diff --git a/src/pip/_vendor/packaging/markers.py b/src/pip/_vendor/packaging/markers.py
index 8b98fca72..7ac7bb69a 100644
--- a/src/pip/_vendor/packaging/markers.py
+++ b/src/pip/_vendor/packaging/markers.py
@@ -2,20 +2,16 @@
 # 2.0, and the BSD License. See the LICENSE file in the root of this repository
 # for complete details.
 
+from __future__ import annotations
+
 import operator
 import os
 import platform
 import sys
-from typing import Any, Callable, Dict, List, Optional, Tuple, Union
-
-from ._parser import (
-    MarkerAtom,
-    MarkerList,
-    Op,
-    Value,
-    Variable,
-    parse_marker as _parse_marker,
-)
+from typing import Any, Callable, TypedDict, cast
+
+from ._parser import MarkerAtom, MarkerList, Op, Value, Variable
+from ._parser import parse_marker as _parse_marker
 from ._tokenizer import ParserSyntaxError
 from .specifiers import InvalidSpecifier, Specifier
 from .utils import canonicalize_name
@@ -50,6 +46,78 @@ class UndefinedEnvironmentName(ValueError):
     """
 
 
+class Environment(TypedDict):
+    implementation_name: str
+    """The implementation's identifier, e.g. ``'cpython'``."""
+
+    implementation_version: str
+    """
+    The implementation's version, e.g. ``'3.13.0a2'`` for CPython 3.13.0a2, or
+    ``'7.3.13'`` for PyPy3.10 v7.3.13.
+    """
+
+    os_name: str
+    """
+    The value of :py:data:`os.name`. The name of the operating system dependent module
+    imported, e.g. ``'posix'``.
+    """
+
+    platform_machine: str
+    """
+    Returns the machine type, e.g. ``'i386'``.
+
+    An empty string if the value cannot be determined.
+    """
+
+    platform_release: str
+    """
+    The system's release, e.g. ``'2.2.0'`` or ``'NT'``.
+
+    An empty string if the value cannot be determined.
+    """
+
+    platform_system: str
+    """
+    The system/OS name, e.g. ``'Linux'``, ``'Windows'`` or ``'Java'``.
+
+    An empty string if the value cannot be determined.
+    """
+
+    platform_version: str
+    """
+    The system's release version, e.g. ``'#3 on degas'``.
+
+    An empty string if the value cannot be determined.
+    """
+
+    python_full_version: str
+    """
+    The Python version as string ``'major.minor.patchlevel'``.
+
+    Note that unlike the Python :py:data:`sys.version`, this value will always include
+    the patchlevel (it defaults to 0).
+    """
+
+    platform_python_implementation: str
+    """
+    A string identifying the Python implementation, e.g. ``'CPython'``.
+    """
+
+    python_version: str
+    """The Python version as string ``'major.minor'``."""
+
+    sys_platform: str
+    """
+    This string contains a platform identifier that can be used to append
+    platform-specific components to :py:data:`sys.path`, for instance.
+
+    For Unix systems, except on Linux and AIX, this is the lowercased OS name as
+    returned by ``uname -s`` with the first part of the version as returned by
+    ``uname -r`` appended, e.g. ``'sunos5'`` or ``'freebsd8'``, at the time when Python
+    was built.
+    """
+
+
 def _normalize_extra_values(results: Any) -> Any:
     """
     Normalize extra values.
@@ -67,9 +135,8 @@ def _normalize_extra_values(results: Any) -> Any:
 
 
 def _format_marker(
-    marker: Union[List[str], MarkerAtom, str], first: Optional[bool] = True
+    marker: list[str] | MarkerAtom | str, first: bool | None = True
 ) -> str:
-
     assert isinstance(marker, (list, tuple, str))
 
     # Sometimes we have a structure like [[...]] which is a single item list
@@ -95,7 +162,7 @@ def _format_marker(
         return marker
 
 
-_operators: Dict[str, Operator] = {
+_operators: dict[str, Operator] = {
     "in": lambda lhs, rhs: lhs in rhs,
     "not in": lambda lhs, rhs: lhs not in rhs,
     "<": operator.lt,
@@ -115,14 +182,14 @@ def _eval_op(lhs: str, op: Op, rhs: str) -> bool:
     else:
         return spec.contains(lhs, prereleases=True)
 
-    oper: Optional[Operator] = _operators.get(op.serialize())
+    oper: Operator | None = _operators.get(op.serialize())
     if oper is None:
         raise UndefinedComparison(f"Undefined {op!r} on {lhs!r} and {rhs!r}.")
 
     return oper(lhs, rhs)
 
 
-def _normalize(*values: str, key: str) -> Tuple[str, ...]:
+def _normalize(*values: str, key: str) -> tuple[str, ...]:
     # PEP 685 – Comparison of extra names for optional distribution dependencies
     # https://peps.python.org/pep-0685/
     # > When comparing extra names, tools MUST normalize the names being
@@ -134,8 +201,8 @@ def _normalize(*values: str, key: str) -> Tuple[str, ...]:
     return values
 
 
-def _evaluate_markers(markers: MarkerList, environment: Dict[str, str]) -> bool:
-    groups: List[List[bool]] = [[]]
+def _evaluate_markers(markers: MarkerList, environment: dict[str, str]) -> bool:
+    groups: list[list[bool]] = [[]]
 
     for marker in markers:
         assert isinstance(marker, (list, tuple, str))
@@ -164,7 +231,7 @@ def _evaluate_markers(markers: MarkerList, environment: Dict[str, str]) -> bool:
     return any(all(item) for item in groups)
 
 
-def format_full_version(info: "sys._version_info") -> str:
+def format_full_version(info: sys._version_info) -> str:
     version = "{0.major}.{0.minor}.{0.micro}".format(info)
     kind = info.releaselevel
     if kind != "final":
@@ -172,7 +239,7 @@ def format_full_version(info: "sys._version_info") -> str:
     return version
 
 
-def default_environment() -> Dict[str, str]:
+def default_environment() -> Environment:
     iver = format_full_version(sys.implementation.version)
     implementation_name = sys.implementation.name
     return {
@@ -231,7 +298,7 @@ class Marker:
 
         return str(self) == str(other)
 
-    def evaluate(self, environment: Optional[Dict[str, str]] = None) -> bool:
+    def evaluate(self, environment: dict[str, str] | None = None) -> bool:
         """Evaluate a marker.
 
         Return the boolean from evaluating the given marker against the
@@ -240,8 +307,14 @@ class Marker:
 
         The environment is determined from the current Python process.
         """
-        current_environment = default_environment()
+        current_environment = cast("dict[str, str]", default_environment())
         current_environment["extra"] = ""
+        # Work around platform.python_version() returning something that is not PEP 440
+        # compliant for non-tagged Python builds. We preserve default_environment()'s
+        # behavior of returning platform.python_version() verbatim, and leave it to the
+        # caller to provide a syntactically valid version if they want to override it.
+        if current_environment["python_full_version"].endswith("+"):
+            current_environment["python_full_version"] += "local"
         if environment is not None:
             current_environment.update(environment)
             # The API used to allow setting extra to None. We need to handle this
diff --git a/src/pip/_vendor/packaging/metadata.py b/src/pip/_vendor/packaging/metadata.py
index 5e4c106c5..eb8dc844d 100644
--- a/src/pip/_vendor/packaging/metadata.py
+++ b/src/pip/_vendor/packaging/metadata.py
@@ -1,50 +1,31 @@
+from __future__ import annotations
+
 import email.feedparser
 import email.header
 import email.message
 import email.parser
 import email.policy
-import sys
 import typing
 from typing import (
     Any,
     Callable,
-    Dict,
     Generic,
-    List,
-    Optional,
-    Tuple,
-    Type,
-    Union,
+    Literal,
+    TypedDict,
     cast,
 )
 
-from . import requirements, specifiers, utils, version as version_module
+from . import requirements, specifiers, utils
+from . import version as version_module
 
 T = typing.TypeVar("T")
-if sys.version_info[:2] >= (3, 8):  # pragma: no cover
-    from typing import Literal, TypedDict
-else:  # pragma: no cover
-    if typing.TYPE_CHECKING:
-        from pip._vendor.typing_extensions import Literal, TypedDict
-    else:
-        try:
-            from pip._vendor.typing_extensions import Literal, TypedDict
-        except ImportError:
-
-            class Literal:
-                def __init_subclass__(*_args, **_kwargs):
-                    pass
-
-            class TypedDict:
-                def __init_subclass__(*_args, **_kwargs):
-                    pass
 
 
 try:
     ExceptionGroup
 except NameError:  # pragma: no cover
 
-    class ExceptionGroup(Exception):  # noqa: N818
+    class ExceptionGroup(Exception):
         """A minimal implementation of :external:exc:`ExceptionGroup` from Python 3.11.
 
         If :external:exc:`ExceptionGroup` is already defined by Python itself,
@@ -52,9 +33,9 @@ except NameError:  # pragma: no cover
         """
 
         message: str
-        exceptions: List[Exception]
+        exceptions: list[Exception]
 
-        def __init__(self, message: str, exceptions: List[Exception]) -> None:
+        def __init__(self, message: str, exceptions: list[Exception]) -> None:
             self.message = message
             self.exceptions = exceptions
 
@@ -100,32 +81,32 @@ class RawMetadata(TypedDict, total=False):
     metadata_version: str
     name: str
     version: str
-    platforms: List[str]
+    platforms: list[str]
     summary: str
     description: str
-    keywords: List[str]
+    keywords: list[str]
     home_page: str
     author: str
     author_email: str
     license: str
 
     # Metadata 1.1 - PEP 314
-    supported_platforms: List[str]
+    supported_platforms: list[str]
     download_url: str
-    classifiers: List[str]
-    requires: List[str]
-    provides: List[str]
-    obsoletes: List[str]
+    classifiers: list[str]
+    requires: list[str]
+    provides: list[str]
+    obsoletes: list[str]
 
     # Metadata 1.2 - PEP 345
     maintainer: str
     maintainer_email: str
-    requires_dist: List[str]
-    provides_dist: List[str]
-    obsoletes_dist: List[str]
+    requires_dist: list[str]
+    provides_dist: list[str]
+    obsoletes_dist: list[str]
     requires_python: str
-    requires_external: List[str]
-    project_urls: Dict[str, str]
+    requires_external: list[str]
+    project_urls: dict[str, str]
 
     # Metadata 2.0
     # PEP 426 attempted to completely revamp the metadata format
@@ -138,10 +119,10 @@ class RawMetadata(TypedDict, total=False):
 
     # Metadata 2.1 - PEP 566
     description_content_type: str
-    provides_extra: List[str]
+    provides_extra: list[str]
 
     # Metadata 2.2 - PEP 643
-    dynamic: List[str]
+    dynamic: list[str]
 
     # Metadata 2.3 - PEP 685
     # No new fields were added in PEP 685, just some edge case were
@@ -185,12 +166,12 @@ _DICT_FIELDS = {
 }
 
 
-def _parse_keywords(data: str) -> List[str]:
+def _parse_keywords(data: str) -> list[str]:
     """Split a string of comma-separate keyboards into a list of keywords."""
     return [k.strip() for k in data.split(",")]
 
 
-def _parse_project_urls(data: List[str]) -> Dict[str, str]:
+def _parse_project_urls(data: list[str]) -> dict[str, str]:
     """Parse a list of label/URL string pairings separated by a comma."""
     urls = {}
     for pair in data:
@@ -230,7 +211,7 @@ def _parse_project_urls(data: List[str]) -> Dict[str, str]:
     return urls
 
 
-def _get_payload(msg: email.message.Message, source: Union[bytes, str]) -> str:
+def _get_payload(msg: email.message.Message, source: bytes | str) -> str:
     """Get the body of the message."""
     # If our source is a str, then our caller has managed encodings for us,
     # and we don't need to deal with it.
@@ -292,7 +273,7 @@ _EMAIL_TO_RAW_MAPPING = {
 _RAW_TO_EMAIL_MAPPING = {raw: email for email, raw in _EMAIL_TO_RAW_MAPPING.items()}
 
 
-def parse_email(data: Union[bytes, str]) -> Tuple[RawMetadata, Dict[str, List[str]]]:
+def parse_email(data: bytes | str) -> tuple[RawMetadata, dict[str, list[str]]]:
     """Parse a distribution's metadata stored as email headers (e.g. from ``METADATA``).
 
     This function returns a two-item tuple of dicts. The first dict is of
@@ -308,8 +289,8 @@ def parse_email(data: Union[bytes, str]) -> Tuple[RawMetadata, Dict[str, List[st
     included in this dict.
 
     """
-    raw: Dict[str, Union[str, List[str], Dict[str, str]]] = {}
-    unparsed: Dict[str, List[str]] = {}
+    raw: dict[str, str | list[str] | dict[str, str]] = {}
+    unparsed: dict[str, list[str]] = {}
 
     if isinstance(data, str):
         parsed = email.parser.Parser(policy=email.policy.compat32).parsestr(data)
@@ -357,7 +338,7 @@ def parse_email(data: Union[bytes, str]) -> Tuple[RawMetadata, Dict[str, List[st
                 # The Header object stores it's data as chunks, and each chunk
                 # can be independently encoded, so we'll need to check each
                 # of them.
-                chunks: List[Tuple[bytes, Optional[str]]] = []
+                chunks: list[tuple[bytes, str | None]] = []
                 for bin, encoding in email.header.decode_header(h):
                     try:
                         bin.decode("utf8", "strict")
@@ -499,11 +480,11 @@ class _Validator(Generic[T]):
     ) -> None:
         self.added = added
 
-    def __set_name__(self, _owner: "Metadata", name: str) -> None:
+    def __set_name__(self, _owner: Metadata, name: str) -> None:
         self.name = name
         self.raw_name = _RAW_TO_EMAIL_MAPPING[name]
 
-    def __get__(self, instance: "Metadata", _owner: Type["Metadata"]) -> T:
+    def __get__(self, instance: Metadata, _owner: type[Metadata]) -> T:
         # With Python 3.8, the caching can be replaced with functools.cached_property().
         # No need to check the cache as attribute lookup will resolve into the
         # instance's __dict__ before __get__ is called.
@@ -531,7 +512,7 @@ class _Validator(Generic[T]):
         return cast(T, value)
 
     def _invalid_metadata(
-        self, msg: str, cause: Optional[Exception] = None
+        self, msg: str, cause: Exception | None = None
     ) -> InvalidMetadata:
         exc = InvalidMetadata(
             self.raw_name, msg.format_map({"field": repr(self.raw_name)})
@@ -606,7 +587,7 @@ class _Validator(Generic[T]):
             )
         return value
 
-    def _process_dynamic(self, value: List[str]) -> List[str]:
+    def _process_dynamic(self, value: list[str]) -> list[str]:
         for dynamic_field in map(str.lower, value):
             if dynamic_field in {"name", "version", "metadata-version"}:
                 raise self._invalid_metadata(
@@ -618,8 +599,8 @@ class _Validator(Generic[T]):
 
     def _process_provides_extra(
         self,
-        value: List[str],
-    ) -> List[utils.NormalizedName]:
+        value: list[str],
+    ) -> list[utils.NormalizedName]:
         normalized_names = []
         try:
             for name in value:
@@ -641,8 +622,8 @@ class _Validator(Generic[T]):
 
     def _process_requires_dist(
         self,
-        value: List[str],
-    ) -> List[requirements.Requirement]:
+        value: list[str],
+    ) -> list[requirements.Requirement]:
         reqs = []
         try:
             for req in value:
@@ -665,7 +646,7 @@ class Metadata:
     _raw: RawMetadata
 
     @classmethod
-    def from_raw(cls, data: RawMetadata, *, validate: bool = True) -> "Metadata":
+    def from_raw(cls, data: RawMetadata, *, validate: bool = True) -> Metadata:
         """Create an instance from :class:`RawMetadata`.
 
         If *validate* is true, all metadata will be validated. All exceptions
@@ -675,7 +656,7 @@ class Metadata:
         ins._raw = data.copy()  # Mutations occur due to caching enriched values.
 
         if validate:
-            exceptions: List[Exception] = []
+            exceptions: list[Exception] = []
             try:
                 metadata_version = ins.metadata_version
                 metadata_age = _VALID_METADATA_VERSIONS.index(metadata_version)
@@ -722,9 +703,7 @@ class Metadata:
         return ins
 
     @classmethod
-    def from_email(
-        cls, data: Union[bytes, str], *, validate: bool = True
-    ) -> "Metadata":
+    def from_email(cls, data: bytes | str, *, validate: bool = True) -> Metadata:
         """Parse metadata from email headers.
 
         If *validate* is true, the metadata will be validated. All exceptions
@@ -760,66 +739,66 @@ class Metadata:
     *validate* parameter)"""
     version: _Validator[version_module.Version] = _Validator()
     """:external:ref:`core-metadata-version` (required)"""
-    dynamic: _Validator[Optional[List[str]]] = _Validator(
+    dynamic: _Validator[list[str] | None] = _Validator(
         added="2.2",
     )
     """:external:ref:`core-metadata-dynamic`
     (validated against core metadata field names and lowercased)"""
-    platforms: _Validator[Optional[List[str]]] = _Validator()
+    platforms: _Validator[list[str] | None] = _Validator()
     """:external:ref:`core-metadata-platform`"""
-    supported_platforms: _Validator[Optional[List[str]]] = _Validator(added="1.1")
+    supported_platforms: _Validator[list[str] | None] = _Validator(added="1.1")
     """:external:ref:`core-metadata-supported-platform`"""
-    summary: _Validator[Optional[str]] = _Validator()
+    summary: _Validator[str | None] = _Validator()
     """:external:ref:`core-metadata-summary` (validated to contain no newlines)"""
-    description: _Validator[Optional[str]] = _Validator()  # TODO 2.1: can be in body
+    description: _Validator[str | None] = _Validator()  # TODO 2.1: can be in body
     """:external:ref:`core-metadata-description`"""
-    description_content_type: _Validator[Optional[str]] = _Validator(added="2.1")
+    description_content_type: _Validator[str | None] = _Validator(added="2.1")
     """:external:ref:`core-metadata-description-content-type` (validated)"""
-    keywords: _Validator[Optional[List[str]]] = _Validator()
+    keywords: _Validator[list[str] | None] = _Validator()
     """:external:ref:`core-metadata-keywords`"""
-    home_page: _Validator[Optional[str]] = _Validator()
+    home_page: _Validator[str | None] = _Validator()
     """:external:ref:`core-metadata-home-page`"""
-    download_url: _Validator[Optional[str]] = _Validator(added="1.1")
+    download_url: _Validator[str | None] = _Validator(added="1.1")
     """:external:ref:`core-metadata-download-url`"""
-    author: _Validator[Optional[str]] = _Validator()
+    author: _Validator[str | None] = _Validator()
     """:external:ref:`core-metadata-author`"""
-    author_email: _Validator[Optional[str]] = _Validator()
+    author_email: _Validator[str | None] = _Validator()
     """:external:ref:`core-metadata-author-email`"""
-    maintainer: _Validator[Optional[str]] = _Validator(added="1.2")
+    maintainer: _Validator[str | None] = _Validator(added="1.2")
     """:external:ref:`core-metadata-maintainer`"""
-    maintainer_email: _Validator[Optional[str]] = _Validator(added="1.2")
+    maintainer_email: _Validator[str | None] = _Validator(added="1.2")
     """:external:ref:`core-metadata-maintainer-email`"""
-    license: _Validator[Optional[str]] = _Validator()
+    license: _Validator[str | None] = _Validator()
     """:external:ref:`core-metadata-license`"""
-    classifiers: _Validator[Optional[List[str]]] = _Validator(added="1.1")
+    classifiers: _Validator[list[str] | None] = _Validator(added="1.1")
     """:external:ref:`core-metadata-classifier`"""
-    requires_dist: _Validator[Optional[List[requirements.Requirement]]] = _Validator(
+    requires_dist: _Validator[list[requirements.Requirement] | None] = _Validator(
         added="1.2"
     )
     """:external:ref:`core-metadata-requires-dist`"""
-    requires_python: _Validator[Optional[specifiers.SpecifierSet]] = _Validator(
+    requires_python: _Validator[specifiers.SpecifierSet | None] = _Validator(
         added="1.2"
     )
     """:external:ref:`core-metadata-requires-python`"""
     # Because `Requires-External` allows for non-PEP 440 version specifiers, we
     # don't do any processing on the values.
-    requires_external: _Validator[Optional[List[str]]] = _Validator(added="1.2")
+    requires_external: _Validator[list[str] | None] = _Validator(added="1.2")
     """:external:ref:`core-metadata-requires-external`"""
-    project_urls: _Validator[Optional[Dict[str, str]]] = _Validator(added="1.2")
+    project_urls: _Validator[dict[str, str] | None] = _Validator(added="1.2")
     """:external:ref:`core-metadata-project-url`"""
     # PEP 685 lets us raise an error if an extra doesn't pass `Name` validation
     # regardless of metadata version.
-    provides_extra: _Validator[Optional[List[utils.NormalizedName]]] = _Validator(
+    provides_extra: _Validator[list[utils.NormalizedName] | None] = _Validator(
         added="2.1",
     )
     """:external:ref:`core-metadata-provides-extra`"""
-    provides_dist: _Validator[Optional[List[str]]] = _Validator(added="1.2")
+    provides_dist: _Validator[list[str] | None] = _Validator(added="1.2")
     """:external:ref:`core-metadata-provides-dist`"""
-    obsoletes_dist: _Validator[Optional[List[str]]] = _Validator(added="1.2")
+    obsoletes_dist: _Validator[list[str] | None] = _Validator(added="1.2")
     """:external:ref:`core-metadata-obsoletes-dist`"""
-    requires: _Validator[Optional[List[str]]] = _Validator(added="1.1")
+    requires: _Validator[list[str] | None] = _Validator(added="1.1")
     """``Requires`` (deprecated)"""
-    provides: _Validator[Optional[List[str]]] = _Validator(added="1.1")
+    provides: _Validator[list[str] | None] = _Validator(added="1.1")
     """``Provides`` (deprecated)"""
-    obsoletes: _Validator[Optional[List[str]]] = _Validator(added="1.1")
+    obsoletes: _Validator[list[str] | None] = _Validator(added="1.1")
     """``Obsoletes`` (deprecated)"""
diff --git a/src/pip/_vendor/packaging/requirements.py b/src/pip/_vendor/packaging/requirements.py
index bdc43a7e9..4e068c956 100644
--- a/src/pip/_vendor/packaging/requirements.py
+++ b/src/pip/_vendor/packaging/requirements.py
@@ -1,8 +1,9 @@
 # This file is dual licensed under the terms of the Apache License, Version
 # 2.0, and the BSD License. See the LICENSE file in the root of this repository
 # for complete details.
+from __future__ import annotations
 
-from typing import Any, Iterator, Optional, Set
+from typing import Any, Iterator
 
 from ._parser import parse_requirement as _parse_requirement
 from ._tokenizer import ParserSyntaxError
@@ -37,10 +38,10 @@ class Requirement:
             raise InvalidRequirement(str(e)) from e
 
         self.name: str = parsed.name
-        self.url: Optional[str] = parsed.url or None
-        self.extras: Set[str] = set(parsed.extras or [])
+        self.url: str | None = parsed.url or None
+        self.extras: set[str] = set(parsed.extras or [])
         self.specifier: SpecifierSet = SpecifierSet(parsed.specifier)
-        self.marker: Optional[Marker] = None
+        self.marker: Marker | None = None
         if parsed.marker is not None:
             self.marker = Marker.__new__(Marker)
             self.marker._markers = _normalize_extra_values(parsed.marker)
diff --git a/src/pip/_vendor/packaging/specifiers.py b/src/pip/_vendor/packaging/specifiers.py
index 3a6497e44..f3ac480fa 100644
--- a/src/pip/_vendor/packaging/specifiers.py
+++ b/src/pip/_vendor/packaging/specifiers.py
@@ -8,10 +8,12 @@
     from pip._vendor.packaging.version import Version
 """
 
+from __future__ import annotations
+
 import abc
 import itertools
 import re
-from typing import Callable, Iterable, Iterator, List, Optional, Tuple, TypeVar, Union
+from typing import Callable, Iterable, Iterator, TypeVar, Union
 
 from .utils import canonicalize_version
 from .version import Version
@@ -64,7 +66,7 @@ class BaseSpecifier(metaclass=abc.ABCMeta):
 
     @property
     @abc.abstractmethod
-    def prereleases(self) -> Optional[bool]:
+    def prereleases(self) -> bool | None:
         """Whether or not pre-releases as a whole are allowed.
 
         This can be set to either ``True`` or ``False`` to explicitly enable or disable
@@ -79,14 +81,14 @@ class BaseSpecifier(metaclass=abc.ABCMeta):
         """
 
     @abc.abstractmethod
-    def contains(self, item: str, prereleases: Optional[bool] = None) -> bool:
+    def contains(self, item: str, prereleases: bool | None = None) -> bool:
         """
         Determines if the given item is contained within this specifier.
         """
 
     @abc.abstractmethod
     def filter(
-        self, iterable: Iterable[UnparsedVersionVar], prereleases: Optional[bool] = None
+        self, iterable: Iterable[UnparsedVersionVar], prereleases: bool | None = None
     ) -> Iterator[UnparsedVersionVar]:
         """
         Takes an iterable of items and filters them so that only items which
@@ -217,7 +219,7 @@ class Specifier(BaseSpecifier):
         "===": "arbitrary",
     }
 
-    def __init__(self, spec: str = "", prereleases: Optional[bool] = None) -> None:
+    def __init__(self, spec: str = "", prereleases: bool | None = None) -> None:
         """Initialize a Specifier instance.
 
         :param spec:
@@ -234,7 +236,7 @@ class Specifier(BaseSpecifier):
         if not match:
             raise InvalidSpecifier(f"Invalid specifier: '{spec}'")
 
-        self._spec: Tuple[str, str] = (
+        self._spec: tuple[str, str] = (
             match.group("operator").strip(),
             match.group("version").strip(),
         )
@@ -318,7 +320,7 @@ class Specifier(BaseSpecifier):
         return "{}{}".format(*self._spec)
 
     @property
-    def _canonical_spec(self) -> Tuple[str, str]:
+    def _canonical_spec(self) -> tuple[str, str]:
         canonical_version = canonicalize_version(
             self._spec[1],
             strip_trailing_zero=(self._spec[0] != "~="),
@@ -364,7 +366,6 @@ class Specifier(BaseSpecifier):
         return operator_callable
 
     def _compare_compatible(self, prospective: Version, spec: str) -> bool:
-
         # Compatible releases have an equivalent combination of >= and ==. That
         # is that ~=2.2 is equivalent to >=2.2,==2.*. This allows us to
         # implement this in terms of the other specifiers instead of
@@ -385,7 +386,6 @@ class Specifier(BaseSpecifier):
         )
 
     def _compare_equal(self, prospective: Version, spec: str) -> bool:
-
         # We need special logic to handle prefix matching
         if spec.endswith(".*"):
             # In the case of prefix matching we want to ignore local segment.
@@ -429,21 +429,18 @@ class Specifier(BaseSpecifier):
         return not self._compare_equal(prospective, spec)
 
     def _compare_less_than_equal(self, prospective: Version, spec: str) -> bool:
-
         # NB: Local version identifiers are NOT permitted in the version
         # specifier, so local version labels can be universally removed from
         # the prospective version.
         return Version(prospective.public) <= Version(spec)
 
     def _compare_greater_than_equal(self, prospective: Version, spec: str) -> bool:
-
         # NB: Local version identifiers are NOT permitted in the version
         # specifier, so local version labels can be universally removed from
         # the prospective version.
         return Version(prospective.public) >= Version(spec)
 
     def _compare_less_than(self, prospective: Version, spec_str: str) -> bool:
-
         # Convert our spec to a Version instance, since we'll want to work with
         # it as a version.
         spec = Version(spec_str)
@@ -468,7 +465,6 @@ class Specifier(BaseSpecifier):
         return True
 
     def _compare_greater_than(self, prospective: Version, spec_str: str) -> bool:
-
         # Convert our spec to a Version instance, since we'll want to work with
         # it as a version.
         spec = Version(spec_str)
@@ -501,7 +497,7 @@ class Specifier(BaseSpecifier):
     def _compare_arbitrary(self, prospective: Version, spec: str) -> bool:
         return str(prospective).lower() == str(spec).lower()
 
-    def __contains__(self, item: Union[str, Version]) -> bool:
+    def __contains__(self, item: str | Version) -> bool:
         """Return whether or not the item is contained in this specifier.
 
         :param item: The item to check for.
@@ -522,9 +518,7 @@ class Specifier(BaseSpecifier):
         """
         return self.contains(item)
 
-    def contains(
-        self, item: UnparsedVersion, prereleases: Optional[bool] = None
-    ) -> bool:
+    def contains(self, item: UnparsedVersion, prereleases: bool | None = None) -> bool:
         """Return whether or not the item is contained in this specifier.
 
         :param item:
@@ -569,7 +563,7 @@ class Specifier(BaseSpecifier):
         return operator_callable(normalized_item, self.version)
 
     def filter(
-        self, iterable: Iterable[UnparsedVersionVar], prereleases: Optional[bool] = None
+        self, iterable: Iterable[UnparsedVersionVar], prereleases: bool | None = None
     ) -> Iterator[UnparsedVersionVar]:
         """Filter items in the given iterable, that match the specifier.
 
@@ -633,7 +627,7 @@ class Specifier(BaseSpecifier):
 _prefix_regex = re.compile(r"^([0-9]+)((?:a|b|c|rc)[0-9]+)$")
 
 
-def _version_split(version: str) -> List[str]:
+def _version_split(version: str) -> list[str]:
     """Split version into components.
 
     The split components are intended for version comparison. The logic does
@@ -641,7 +635,7 @@ def _version_split(version: str) -> List[str]:
     components back with :func:`_version_join` may not produce the original
     version string.
     """
-    result: List[str] = []
+    result: list[str] = []
 
     epoch, _, rest = version.rpartition("!")
     result.append(epoch or "0")
@@ -655,7 +649,7 @@ def _version_split(version: str) -> List[str]:
     return result
 
 
-def _version_join(components: List[str]) -> str:
+def _version_join(components: list[str]) -> str:
     """Join split version components into a version string.
 
     This function assumes the input came from :func:`_version_split`, where the
@@ -672,7 +666,7 @@ def _is_not_suffix(segment: str) -> bool:
     )
 
 
-def _pad_version(left: List[str], right: List[str]) -> Tuple[List[str], List[str]]:
+def _pad_version(left: list[str], right: list[str]) -> tuple[list[str], list[str]]:
     left_split, right_split = [], []
 
     # Get the release segment of our versions
@@ -700,9 +694,7 @@ class SpecifierSet(BaseSpecifier):
     specifiers (``>=3.0,!=3.1``), or no specifier at all.
     """
 
-    def __init__(
-        self, specifiers: str = "", prereleases: Optional[bool] = None
-    ) -> None:
+    def __init__(self, specifiers: str = "", prereleases: bool | None = None) -> None:
         """Initialize a SpecifierSet instance.
 
         :param specifiers:
@@ -730,7 +722,7 @@ class SpecifierSet(BaseSpecifier):
         self._prereleases = prereleases
 
     @property
-    def prereleases(self) -> Optional[bool]:
+    def prereleases(self) -> bool | None:
         # If we have been given an explicit prerelease modifier, then we'll
         # pass that through here.
         if self._prereleases is not None:
@@ -787,7 +779,7 @@ class SpecifierSet(BaseSpecifier):
     def __hash__(self) -> int:
         return hash(self._specs)
 
-    def __and__(self, other: Union["SpecifierSet", str]) -> "SpecifierSet":
+    def __and__(self, other: SpecifierSet | str) -> SpecifierSet:
         """Return a SpecifierSet which is a combination of the two sets.
 
         :param other: The other object to combine with.
@@ -883,8 +875,8 @@ class SpecifierSet(BaseSpecifier):
     def contains(
         self,
         item: UnparsedVersion,
-        prereleases: Optional[bool] = None,
-        installed: Optional[bool] = None,
+        prereleases: bool | None = None,
+        installed: bool | None = None,
     ) -> bool:
         """Return whether or not the item is contained in this SpecifierSet.
 
@@ -938,7 +930,7 @@ class SpecifierSet(BaseSpecifier):
         return all(s.contains(item, prereleases=prereleases) for s in self._specs)
 
     def filter(
-        self, iterable: Iterable[UnparsedVersionVar], prereleases: Optional[bool] = None
+        self, iterable: Iterable[UnparsedVersionVar], prereleases: bool | None = None
     ) -> Iterator[UnparsedVersionVar]:
         """Filter items in the given iterable, that match the specifiers in this set.
 
@@ -995,8 +987,8 @@ class SpecifierSet(BaseSpecifier):
         # which will filter out any pre-releases, unless there are no final
         # releases.
         else:
-            filtered: List[UnparsedVersionVar] = []
-            found_prereleases: List[UnparsedVersionVar] = []
+            filtered: list[UnparsedVersionVar] = []
+            found_prereleases: list[UnparsedVersionVar] = []
 
             for item in iterable:
                 parsed_version = _coerce_version(item)
diff --git a/src/pip/_vendor/packaging/tags.py b/src/pip/_vendor/packaging/tags.py
index 89f192613..6667d2990 100644
--- a/src/pip/_vendor/packaging/tags.py
+++ b/src/pip/_vendor/packaging/tags.py
@@ -2,6 +2,8 @@
 # 2.0, and the BSD License. See the LICENSE file in the root of this repository
 # for complete details.
 
+from __future__ import annotations
+
 import logging
 import platform
 import re
@@ -11,15 +13,10 @@ import sys
 import sysconfig
 from importlib.machinery import EXTENSION_SUFFIXES
 from typing import (
-    Dict,
-    FrozenSet,
     Iterable,
     Iterator,
-    List,
-    Optional,
     Sequence,
     Tuple,
-    Union,
     cast,
 )
 
@@ -30,7 +27,7 @@ logger = logging.getLogger(__name__)
 PythonVersion = Sequence[int]
 MacVersion = Tuple[int, int]
 
-INTERPRETER_SHORT_NAMES: Dict[str, str] = {
+INTERPRETER_SHORT_NAMES: dict[str, str] = {
     "python": "py",  # Generic.
     "cpython": "cp",
     "pypy": "pp",
@@ -96,7 +93,7 @@ class Tag:
         return f"<{self} @ {id(self)}>"
 
 
-def parse_tag(tag: str) -> FrozenSet[Tag]:
+def parse_tag(tag: str) -> frozenset[Tag]:
     """
     Parses the provided tag (e.g. `py3-none-any`) into a frozenset of Tag instances.
 
@@ -112,8 +109,8 @@ def parse_tag(tag: str) -> FrozenSet[Tag]:
     return frozenset(tags)
 
 
-def _get_config_var(name: str, warn: bool = False) -> Union[int, str, None]:
-    value: Union[int, str, None] = sysconfig.get_config_var(name)
+def _get_config_var(name: str, warn: bool = False) -> int | str | None:
+    value: int | str | None = sysconfig.get_config_var(name)
     if value is None and warn:
         logger.debug(
             "Config variable '%s' is unset, Python ABI tag may be incorrect", name
@@ -125,7 +122,7 @@ def _normalize_string(string: str) -> str:
     return string.replace(".", "_").replace("-", "_").replace(" ", "_")
 
 
-def _is_threaded_cpython(abis: List[str]) -> bool:
+def _is_threaded_cpython(abis: list[str]) -> bool:
     """
     Determine if the ABI corresponds to a threaded (`--disable-gil`) build.
 
@@ -151,7 +148,7 @@ def _abi3_applies(python_version: PythonVersion, threading: bool) -> bool:
     return len(python_version) > 1 and tuple(python_version) >= (3, 2) and not threading
 
 
-def _cpython_abis(py_version: PythonVersion, warn: bool = False) -> List[str]:
+def _cpython_abis(py_version: PythonVersion, warn: bool = False) -> list[str]:
     py_version = tuple(py_version)  # To allow for version comparison.
     abis = []
     version = _version_nodot(py_version[:2])
@@ -185,9 +182,9 @@ def _cpython_abis(py_version: PythonVersion, warn: bool = False) -> List[str]:
 
 
 def cpython_tags(
-    python_version: Optional[PythonVersion] = None,
-    abis: Optional[Iterable[str]] = None,
-    platforms: Optional[Iterable[str]] = None,
+    python_version: PythonVersion | None = None,
+    abis: Iterable[str] | None = None,
+    platforms: Iterable[str] | None = None,
     *,
     warn: bool = False,
 ) -> Iterator[Tag]:
@@ -244,7 +241,7 @@ def cpython_tags(
                 yield Tag(interpreter, "abi3", platform_)
 
 
-def _generic_abi() -> List[str]:
+def _generic_abi() -> list[str]:
     """
     Return the ABI tag based on EXT_SUFFIX.
     """
@@ -286,9 +283,9 @@ def _generic_abi() -> List[str]:
 
 
 def generic_tags(
-    interpreter: Optional[str] = None,
-    abis: Optional[Iterable[str]] = None,
-    platforms: Optional[Iterable[str]] = None,
+    interpreter: str | None = None,
+    abis: Iterable[str] | None = None,
+    platforms: Iterable[str] | None = None,
     *,
     warn: bool = False,
 ) -> Iterator[Tag]:
@@ -332,9 +329,9 @@ def _py_interpreter_range(py_version: PythonVersion) -> Iterator[str]:
 
 
 def compatible_tags(
-    python_version: Optional[PythonVersion] = None,
-    interpreter: Optional[str] = None,
-    platforms: Optional[Iterable[str]] = None,
+    python_version: PythonVersion | None = None,
+    interpreter: str | None = None,
+    platforms: Iterable[str] | None = None,
 ) -> Iterator[Tag]:
     """
     Yields the sequence of tags that are compatible with a specific version of Python.
@@ -366,7 +363,7 @@ def _mac_arch(arch: str, is_32bit: bool = _32_BIT_INTERPRETER) -> str:
     return "i386"
 
 
-def _mac_binary_formats(version: MacVersion, cpu_arch: str) -> List[str]:
+def _mac_binary_formats(version: MacVersion, cpu_arch: str) -> list[str]:
     formats = [cpu_arch]
     if cpu_arch == "x86_64":
         if version < (10, 4):
@@ -399,7 +396,7 @@ def _mac_binary_formats(version: MacVersion, cpu_arch: str) -> List[str]:
 
 
 def mac_platforms(
-    version: Optional[MacVersion] = None, arch: Optional[str] = None
+    version: MacVersion | None = None, arch: str | None = None
 ) -> Iterator[str]:
     """
     Yields the platform tags for a macOS system.
diff --git a/src/pip/_vendor/packaging/utils.py b/src/pip/_vendor/packaging/utils.py
index c2c2f75aa..d33da5bb8 100644
--- a/src/pip/_vendor/packaging/utils.py
+++ b/src/pip/_vendor/packaging/utils.py
@@ -2,8 +2,10 @@
 # 2.0, and the BSD License. See the LICENSE file in the root of this repository
 # for complete details.
 
+from __future__ import annotations
+
 import re
-from typing import FrozenSet, NewType, Tuple, Union, cast
+from typing import NewType, Tuple, Union, cast
 
 from .tags import Tag, parse_tag
 from .version import InvalidVersion, Version
@@ -53,7 +55,7 @@ def is_normalized_name(name: str) -> bool:
 
 
 def canonicalize_version(
-    version: Union[Version, str], *, strip_trailing_zero: bool = True
+    version: Version | str, *, strip_trailing_zero: bool = True
 ) -> str:
     """
     This is very similar to Version.__str__, but has one subtle difference
@@ -102,7 +104,7 @@ def canonicalize_version(
 
 def parse_wheel_filename(
     filename: str,
-) -> Tuple[NormalizedName, Version, BuildTag, FrozenSet[Tag]]:
+) -> tuple[NormalizedName, Version, BuildTag, frozenset[Tag]]:
     if not filename.endswith(".whl"):
         raise InvalidWheelFilename(
             f"Invalid wheel filename (extension must be '.whl'): {filename}"
@@ -143,7 +145,7 @@ def parse_wheel_filename(
     return (name, version, build, tags)
 
 
-def parse_sdist_filename(filename: str) -> Tuple[NormalizedName, Version]:
+def parse_sdist_filename(filename: str) -> tuple[NormalizedName, Version]:
     if filename.endswith(".tar.gz"):
         file_stem = filename[: -len(".tar.gz")]
     elif filename.endswith(".zip"):
diff --git a/src/pip/_vendor/packaging/version.py b/src/pip/_vendor/packaging/version.py
index 6978e2843..8b0a04084 100644
--- a/src/pip/_vendor/packaging/version.py
+++ b/src/pip/_vendor/packaging/version.py
@@ -7,9 +7,11 @@
     from pip._vendor.packaging.version import parse, Version
 """
 
+from __future__ import annotations
+
 import itertools
 import re
-from typing import Any, Callable, NamedTuple, Optional, SupportsInt, Tuple, Union
+from typing import Any, Callable, NamedTuple, SupportsInt, Tuple, Union
 
 from ._structures import Infinity, InfinityType, NegativeInfinity, NegativeInfinityType
 
@@ -35,14 +37,14 @@ VersionComparisonMethod = Callable[[CmpKey, CmpKey], bool]
 
 class _Version(NamedTuple):
     epoch: int
-    release: Tuple[int, ...]
-    dev: Optional[Tuple[str, int]]
-    pre: Optional[Tuple[str, int]]
-    post: Optional[Tuple[str, int]]
-    local: Optional[LocalType]
+    release: tuple[int, ...]
+    dev: tuple[str, int] | None
+    pre: tuple[str, int] | None
+    post: tuple[str, int] | None
+    local: LocalType | None
 
 
-def parse(version: str) -> "Version":
+def parse(version: str) -> Version:
     """Parse the given version string.
 
     >>> parse('1.0.dev1')
@@ -65,7 +67,7 @@ class InvalidVersion(ValueError):
 
 
 class _BaseVersion:
-    _key: Tuple[Any, ...]
+    _key: tuple[Any, ...]
 
     def __hash__(self) -> int:
         return hash(self._key)
@@ -73,13 +75,13 @@ class _BaseVersion:
     # Please keep the duplicated `isinstance` check
     # in the six comparisons hereunder
     # unless you find a way to avoid adding overhead function calls.
-    def __lt__(self, other: "_BaseVersion") -> bool:
+    def __lt__(self, other: _BaseVersion) -> bool:
         if not isinstance(other, _BaseVersion):
             return NotImplemented
 
         return self._key < other._key
 
-    def __le__(self, other: "_BaseVersion") -> bool:
+    def __le__(self, other: _BaseVersion) -> bool:
         if not isinstance(other, _BaseVersion):
             return NotImplemented
 
@@ -91,13 +93,13 @@ class _BaseVersion:
 
         return self._key == other._key
 
-    def __ge__(self, other: "_BaseVersion") -> bool:
+    def __ge__(self, other: _BaseVersion) -> bool:
         if not isinstance(other, _BaseVersion):
             return NotImplemented
 
         return self._key >= other._key
 
-    def __gt__(self, other: "_BaseVersion") -> bool:
+    def __gt__(self, other: _BaseVersion) -> bool:
         if not isinstance(other, _BaseVersion):
             return NotImplemented
 
@@ -274,7 +276,7 @@ class Version(_BaseVersion):
         return self._version.epoch
 
     @property
-    def release(self) -> Tuple[int, ...]:
+    def release(self) -> tuple[int, ...]:
         """The components of the "release" segment of the version.
 
         >>> Version("1.2.3").release
@@ -290,7 +292,7 @@ class Version(_BaseVersion):
         return self._version.release
 
     @property
-    def pre(self) -> Optional[Tuple[str, int]]:
+    def pre(self) -> tuple[str, int] | None:
         """The pre-release segment of the version.
 
         >>> print(Version("1.2.3").pre)
@@ -305,7 +307,7 @@ class Version(_BaseVersion):
         return self._version.pre
 
     @property
-    def post(self) -> Optional[int]:
+    def post(self) -> int | None:
         """The post-release number of the version.
 
         >>> print(Version("1.2.3").post)
@@ -316,7 +318,7 @@ class Version(_BaseVersion):
         return self._version.post[1] if self._version.post else None
 
     @property
-    def dev(self) -> Optional[int]:
+    def dev(self) -> int | None:
         """The development number of the version.
 
         >>> print(Version("1.2.3").dev)
@@ -327,7 +329,7 @@ class Version(_BaseVersion):
         return self._version.dev[1] if self._version.dev else None
 
     @property
-    def local(self) -> Optional[str]:
+    def local(self) -> str | None:
         """The local version segment of the version.
 
         >>> print(Version("1.2.3").local)
@@ -450,9 +452,8 @@ class Version(_BaseVersion):
 
 
 def _parse_letter_version(
-    letter: Optional[str], number: Union[str, bytes, SupportsInt, None]
-) -> Optional[Tuple[str, int]]:
-
+    letter: str | None, number: str | bytes | SupportsInt | None
+) -> tuple[str, int] | None:
     if letter:
         # We consider there to be an implicit 0 in a pre-release if there is
         # not a numeral associated with it.
@@ -488,7 +489,7 @@ def _parse_letter_version(
 _local_version_separators = re.compile(r"[\._-]")
 
 
-def _parse_local_version(local: Optional[str]) -> Optional[LocalType]:
+def _parse_local_version(local: str | None) -> LocalType | None:
     """
     Takes a string like abc.1.twelve and turns it into ("abc", 1, "twelve").
     """
@@ -502,13 +503,12 @@ def _parse_local_version(local: Optional[str]) -> Optional[LocalType]:
 
 def _cmpkey(
     epoch: int,
-    release: Tuple[int, ...],
-    pre: Optional[Tuple[str, int]],
-    post: Optional[Tuple[str, int]],
-    dev: Optional[Tuple[str, int]],
-    local: Optional[LocalType],
+    release: tuple[int, ...],
+    pre: tuple[str, int] | None,
+    post: tuple[str, int] | None,
+    dev: tuple[str, int] | None,
+    local: LocalType | None,
 ) -> CmpKey:
-
     # When we compare a release version, we want to compare it with all of the
     # trailing zeros removed. So we'll use a reverse the list, drop all the now
     # leading zeros until we come to something non zero, then take the rest
diff --git a/src/pip/_vendor/requests/__version__.py b/src/pip/_vendor/requests/__version__.py
index 1ac168734..2c105aca7 100644
--- a/src/pip/_vendor/requests/__version__.py
+++ b/src/pip/_vendor/requests/__version__.py
@@ -5,8 +5,8 @@
 __title__ = "requests"
 __description__ = "Python HTTP for Humans."
 __url__ = "https://requests.readthedocs.io"
-__version__ = "2.32.0"
-__build__ = 0x023200
+__version__ = "2.32.3"
+__build__ = 0x023203
 __author__ = "Kenneth Reitz"
 __author_email__ = "me@kennethreitz.org"
 __license__ = "Apache-2.0"
diff --git a/src/pip/_vendor/requests/adapters.py b/src/pip/_vendor/requests/adapters.py
index 4d9d9c630..703077746 100644
--- a/src/pip/_vendor/requests/adapters.py
+++ b/src/pip/_vendor/requests/adapters.py
@@ -9,6 +9,7 @@ and maintain connections.
 import os.path
 import socket  # noqa: F401
 import typing
+import warnings
 
 from pip._vendor.urllib3.exceptions import ClosedPoolError, ConnectTimeoutError
 from pip._vendor.urllib3.exceptions import HTTPError as _HTTPError
@@ -72,26 +73,44 @@ DEFAULT_POOLSIZE = 10
 DEFAULT_RETRIES = 0
 DEFAULT_POOL_TIMEOUT = None
 
-_preloaded_ssl_context = create_urllib3_context()
-_preloaded_ssl_context.load_verify_locations(
-    extract_zipped_paths(DEFAULT_CA_BUNDLE_PATH)
-)
+
+try:
+    import ssl  # noqa: F401
+
+    _preloaded_ssl_context = create_urllib3_context()
+    _preloaded_ssl_context.load_verify_locations(
+        extract_zipped_paths(DEFAULT_CA_BUNDLE_PATH)
+    )
+except ImportError:
+    # Bypass default SSLContext creation when Python
+    # interpreter isn't built with the ssl module.
+    _preloaded_ssl_context = None
 
 
 def _urllib3_request_context(
     request: "PreparedRequest",
     verify: "bool | str | None",
     client_cert: "typing.Tuple[str, str] | str | None",
+    poolmanager: "PoolManager",
 ) -> "(typing.Dict[str, typing.Any], typing.Dict[str, typing.Any])":
     host_params = {}
     pool_kwargs = {}
     parsed_request_url = urlparse(request.url)
     scheme = parsed_request_url.scheme.lower()
     port = parsed_request_url.port
+
+    # Determine if we have and should use our default SSLContext
+    # to optimize performance on standard requests.
+    poolmanager_kwargs = getattr(poolmanager, "connection_pool_kw", {})
+    has_poolmanager_ssl_context = poolmanager_kwargs.get("ssl_context")
+    should_use_default_ssl_context = (
+        _preloaded_ssl_context is not None and not has_poolmanager_ssl_context
+    )
+
     cert_reqs = "CERT_REQUIRED"
     if verify is False:
         cert_reqs = "CERT_NONE"
-    elif verify is True:
+    elif verify is True and should_use_default_ssl_context:
         pool_kwargs["ssl_context"] = _preloaded_ssl_context
     elif isinstance(verify, str):
         if not os.path.isdir(verify):
@@ -374,13 +393,83 @@ class HTTPAdapter(BaseAdapter):
 
         return response
 
-    def _get_connection(self, request, verify, proxies=None, cert=None):
-        # Replace the existing get_connection without breaking things and
-        # ensure that TLS settings are considered when we interact with
-        # urllib3 HTTP Pools
+    def build_connection_pool_key_attributes(self, request, verify, cert=None):
+        """Build the PoolKey attributes used by urllib3 to return a connection.
+
+        This looks at the PreparedRequest, the user-specified verify value,
+        and the value of the cert parameter to determine what PoolKey values
+        to use to select a connection from a given urllib3 Connection Pool.
+
+        The SSL related pool key arguments are not consistently set. As of
+        this writing, use the following to determine what keys may be in that
+        dictionary:
+
+        * If ``verify`` is ``True``, ``"ssl_context"`` will be set and will be the
+          default Requests SSL Context
+        * If ``verify`` is ``False``, ``"ssl_context"`` will not be set but
+          ``"cert_reqs"`` will be set
+        * If ``verify`` is a string, (i.e., it is a user-specified trust bundle)
+          ``"ca_certs"`` will be set if the string is not a directory recognized
+          by :py:func:`os.path.isdir`, otherwise ``"ca_certs_dir"`` will be
+          set.
+        * If ``"cert"`` is specified, ``"cert_file"`` will always be set. If
+          ``"cert"`` is a tuple with a second item, ``"key_file"`` will also
+          be present
+
+        To override these settings, one may subclass this class, call this
+        method and use the above logic to change parameters as desired. For
+        example, if one wishes to use a custom :py:class:`ssl.SSLContext` one
+        must both set ``"ssl_context"`` and based on what else they require,
+        alter the other keys to ensure the desired behaviour.
+
+        :param request:
+            The PreparedReqest being sent over the connection.
+        :type request:
+            :class:`~requests.models.PreparedRequest`
+        :param verify:
+            Either a boolean, in which case it controls whether
+            we verify the server's TLS certificate, or a string, in which case it
+            must be a path to a CA bundle to use.
+        :param cert:
+            (optional) Any user-provided SSL certificate for client
+            authentication (a.k.a., mTLS). This may be a string (i.e., just
+            the path to a file which holds both certificate and key) or a
+            tuple of length 2 with the certificate file path and key file
+            path.
+        :returns:
+            A tuple of two dictionaries. The first is the "host parameters"
+            portion of the Pool Key including scheme, hostname, and port. The
+            second is a dictionary of SSLContext related parameters.
+        """
+        return _urllib3_request_context(request, verify, cert, self.poolmanager)
+
+    def get_connection_with_tls_context(self, request, verify, proxies=None, cert=None):
+        """Returns a urllib3 connection for the given request and TLS settings.
+        This should not be called from user code, and is only exposed for use
+        when subclassing the :class:`HTTPAdapter <requests.adapters.HTTPAdapter>`.
+
+        :param request:
+            The :class:`PreparedRequest <PreparedRequest>` object to be sent
+            over the connection.
+        :param verify:
+            Either a boolean, in which case it controls whether we verify the
+            server's TLS certificate, or a string, in which case it must be a
+            path to a CA bundle to use.
+        :param proxies:
+            (optional) The proxies dictionary to apply to the request.
+        :param cert:
+            (optional) Any user-provided SSL certificate to be used for client
+            authentication (a.k.a., mTLS).
+        :rtype:
+            urllib3.ConnectionPool
+        """
         proxy = select_proxy(request.url, proxies)
         try:
-            host_params, pool_kwargs = _urllib3_request_context(request, verify, cert)
+            host_params, pool_kwargs = self.build_connection_pool_key_attributes(
+                request,
+                verify,
+                cert,
+            )
         except ValueError as e:
             raise InvalidURL(e, request=request)
         if proxy:
@@ -404,7 +493,10 @@ class HTTPAdapter(BaseAdapter):
         return conn
 
     def get_connection(self, url, proxies=None):
-        """Returns a urllib3 connection for the given URL. This should not be
+        """DEPRECATED: Users should move to `get_connection_with_tls_context`
+        for all subclasses of HTTPAdapter using Requests>=2.32.2.
+
+        Returns a urllib3 connection for the given URL. This should not be
         called from user code, and is only exposed for use when subclassing the
         :class:`HTTPAdapter <requests.adapters.HTTPAdapter>`.
 
@@ -412,6 +504,15 @@ class HTTPAdapter(BaseAdapter):
         :param proxies: (optional) A Requests-style dictionary of proxies used on this request.
         :rtype: urllib3.ConnectionPool
         """
+        warnings.warn(
+            (
+                "`get_connection` has been deprecated in favor of "
+                "`get_connection_with_tls_context`. Custom HTTPAdapter subclasses "
+                "will need to migrate for Requests>=2.32.2. Please see "
+                "https://github.com/psf/requests/pull/6710 for more details."
+            ),
+            DeprecationWarning,
+        )
         proxy = select_proxy(url, proxies)
 
         if proxy:
@@ -529,7 +630,9 @@ class HTTPAdapter(BaseAdapter):
         """
 
         try:
-            conn = self._get_connection(request, verify, proxies=proxies, cert=cert)
+            conn = self.get_connection_with_tls_context(
+                request, verify, proxies=proxies, cert=cert
+            )
         except LocationValueError as e:
             raise InvalidURL(e, request=request)
 
diff --git a/src/pip/_vendor/truststore/__init__.py b/src/pip/_vendor/truststore/__init__.py
index 59930f455..86368145a 100644
--- a/src/pip/_vendor/truststore/__init__.py
+++ b/src/pip/_vendor/truststore/__init__.py
@@ -10,4 +10,4 @@ from ._api import SSLContext, extract_from_ssl, inject_into_ssl  # noqa: E402
 del _api, _sys  # type: ignore[name-defined] # noqa: F821
 
 __all__ = ["SSLContext", "inject_into_ssl", "extract_from_ssl"]
-__version__ = "0.8.0"
+__version__ = "0.9.1"
diff --git a/src/pip/_vendor/truststore/_api.py b/src/pip/_vendor/truststore/_api.py
index 829aff726..b1ea3b05c 100644
--- a/src/pip/_vendor/truststore/_api.py
+++ b/src/pip/_vendor/truststore/_api.py
@@ -2,9 +2,10 @@ import os
 import platform
 import socket
 import ssl
+import sys
 import typing
 
-import _ssl  # type: ignore[import]
+import _ssl  # type: ignore[import-not-found]
 
 from ._ssl_constants import (
     _original_SSLContext,
@@ -49,7 +50,7 @@ def extract_from_ssl() -> None:
     try:
         import pip._vendor.urllib3.util.ssl_ as urllib3_ssl
 
-        urllib3_ssl.SSLContext = _original_SSLContext
+        urllib3_ssl.SSLContext = _original_SSLContext  # type: ignore[assignment]
     except ImportError:
         pass
 
@@ -171,16 +172,13 @@ class SSLContext(_truststore_SSLContext_super_class):  # type: ignore[misc]
     @typing.overload
     def get_ca_certs(
         self, binary_form: typing.Literal[False] = ...
-    ) -> list[typing.Any]:
-        ...
+    ) -> list[typing.Any]: ...
 
     @typing.overload
-    def get_ca_certs(self, binary_form: typing.Literal[True] = ...) -> list[bytes]:
-        ...
+    def get_ca_certs(self, binary_form: typing.Literal[True] = ...) -> list[bytes]: ...
 
     @typing.overload
-    def get_ca_certs(self, binary_form: bool = ...) -> typing.Any:
-        ...
+    def get_ca_certs(self, binary_form: bool = ...) -> typing.Any: ...
 
     def get_ca_certs(self, binary_form: bool = False) -> list[typing.Any] | list[bytes]:
         raise NotImplementedError()
@@ -276,6 +274,25 @@ class SSLContext(_truststore_SSLContext_super_class):  # type: ignore[misc]
         )
 
 
+# Python 3.13+ makes get_unverified_chain() a public API that only returns DER
+# encoded certificates. We detect whether we need to call public_bytes() for 3.10->3.12
+# Pre-3.13 returned None instead of an empty list from get_unverified_chain()
+if sys.version_info >= (3, 13):
+
+    def _get_unverified_chain_bytes(sslobj: ssl.SSLObject) -> list[bytes]:
+        unverified_chain = sslobj.get_unverified_chain() or ()  # type: ignore[attr-defined]
+        return [
+            cert if isinstance(cert, bytes) else cert.public_bytes(_ssl.ENCODING_DER)
+            for cert in unverified_chain
+        ]
+
+else:
+
+    def _get_unverified_chain_bytes(sslobj: ssl.SSLObject) -> list[bytes]:
+        unverified_chain = sslobj.get_unverified_chain() or ()  # type: ignore[attr-defined]
+        return [cert.public_bytes(_ssl.ENCODING_DER) for cert in unverified_chain]
+
+
 def _verify_peercerts(
     sock_or_sslobj: ssl.SSLSocket | ssl.SSLObject, server_hostname: str | None
 ) -> None:
@@ -290,13 +307,7 @@ def _verify_peercerts(
     except AttributeError:
         pass
 
-    # SSLObject.get_unverified_chain() returns 'None'
-    # if the peer sends no certificates. This is common
-    # for the server-side scenario.
-    unverified_chain: typing.Sequence[_ssl.Certificate] = (
-        sslobj.get_unverified_chain() or ()  # type: ignore[attr-defined]
-    )
-    cert_bytes = [cert.public_bytes(_ssl.ENCODING_DER) for cert in unverified_chain]
+    cert_bytes = _get_unverified_chain_bytes(sslobj)
     _verify_peercerts_impl(
         sock_or_sslobj.context, cert_bytes, server_hostname=server_hostname
     )
diff --git a/src/pip/_vendor/truststore/_macos.py b/src/pip/_vendor/truststore/_macos.py
index 7dc440bf3..b234ffec7 100644
--- a/src/pip/_vendor/truststore/_macos.py
+++ b/src/pip/_vendor/truststore/_macos.py
@@ -96,9 +96,6 @@ try:
     Security.SecTrustSetAnchorCertificatesOnly.argtypes = [SecTrustRef, Boolean]
     Security.SecTrustSetAnchorCertificatesOnly.restype = OSStatus
 
-    Security.SecTrustEvaluate.argtypes = [SecTrustRef, POINTER(SecTrustResultType)]
-    Security.SecTrustEvaluate.restype = OSStatus
-
     Security.SecPolicyCreateRevocation.argtypes = [CFOptionFlags]
     Security.SecPolicyCreateRevocation.restype = SecPolicyRef
 
@@ -259,6 +256,7 @@ def _handle_osstatus(result: OSStatus, _: typing.Any, args: typing.Any) -> typin
 
 Security.SecTrustCreateWithCertificates.errcheck = _handle_osstatus  # type: ignore[assignment]
 Security.SecTrustSetAnchorCertificates.errcheck = _handle_osstatus  # type: ignore[assignment]
+Security.SecTrustSetAnchorCertificatesOnly.errcheck = _handle_osstatus  # type: ignore[assignment]
 Security.SecTrustGetTrustResult.errcheck = _handle_osstatus  # type: ignore[assignment]
 
 
@@ -417,21 +415,21 @@ def _verify_peercerts_impl(
                 CoreFoundation.CFRelease(certs)
 
         # If there are additional trust anchors to load we need to transform
-        # the list of DER-encoded certificates into a CFArray. Otherwise
-        # pass 'None' to signal that we only want system / fetched certificates.
+        # the list of DER-encoded certificates into a CFArray.
         ctx_ca_certs_der: list[bytes] | None = ssl_context.get_ca_certs(
             binary_form=True
         )
         if ctx_ca_certs_der:
             ctx_ca_certs = None
             try:
-                ctx_ca_certs = _der_certs_to_cf_cert_array(cert_chain)
+                ctx_ca_certs = _der_certs_to_cf_cert_array(ctx_ca_certs_der)
                 Security.SecTrustSetAnchorCertificates(trust, ctx_ca_certs)
             finally:
                 if ctx_ca_certs:
                     CoreFoundation.CFRelease(ctx_ca_certs)
-        else:
-            Security.SecTrustSetAnchorCertificates(trust, None)
+
+        # We always want system certificates.
+        Security.SecTrustSetAnchorCertificatesOnly(trust, False)
 
         cf_error = CoreFoundation.CFErrorRef()
         sec_trust_eval_result = Security.SecTrustEvaluateWithError(
diff --git a/src/pip/_vendor/truststore/_windows.py b/src/pip/_vendor/truststore/_windows.py
index 3de4960a1..3d00d467f 100644
--- a/src/pip/_vendor/truststore/_windows.py
+++ b/src/pip/_vendor/truststore/_windows.py
@@ -325,6 +325,12 @@ def _verify_peercerts_impl(
     server_hostname: str | None = None,
 ) -> None:
     """Verify the cert_chain from the server using Windows APIs."""
+
+    # If the peer didn't send any certificates then
+    # we can't do verification. Raise an error.
+    if not cert_chain:
+        raise ssl.SSLCertVerificationError("Peer sent no certificates to verify")
+
     pCertContext = None
     hIntermediateCertStore = CertOpenStore(CERT_STORE_PROV_MEMORY, 0, None, 0, None)
     try:
@@ -375,7 +381,7 @@ def _verify_peercerts_impl(
                 server_hostname,
                 chain_flags=chain_flags,
             )
-        except ssl.SSLCertVerificationError:
+        except ssl.SSLCertVerificationError as e:
             # If that fails but custom CA certs have been added
             # to the SSLContext using load_verify_locations,
             # try verifying using a custom chain engine
@@ -384,15 +390,19 @@ def _verify_peercerts_impl(
                 binary_form=True
             )
             if custom_ca_certs:
-                _verify_using_custom_ca_certs(
-                    ssl_context,
-                    custom_ca_certs,
-                    hIntermediateCertStore,
-                    pCertContext,
-                    pChainPara,
-                    server_hostname,
-                    chain_flags=chain_flags,
-                )
+                try:
+                    _verify_using_custom_ca_certs(
+                        ssl_context,
+                        custom_ca_certs,
+                        hIntermediateCertStore,
+                        pCertContext,
+                        pChainPara,
+                        server_hostname,
+                        chain_flags=chain_flags,
+                    )
+                # Raise the original error, not the new error.
+                except ssl.SSLCertVerificationError:
+                    raise e from None
             else:
                 raise
     finally:
diff --git a/src/pip/_vendor/typing_extensions.py b/src/pip/_vendor/typing_extensions.py
index d60315a6a..e429384e7 100644
--- a/src/pip/_vendor/typing_extensions.py
+++ b/src/pip/_vendor/typing_extensions.py
@@ -1,6 +1,7 @@
 import abc
 import collections
 import collections.abc
+import contextlib
 import functools
 import inspect
 import operator
@@ -116,6 +117,7 @@ __all__ = [
     'MutableMapping',
     'MutableSequence',
     'MutableSet',
+    'NoDefault',
     'Optional',
     'Pattern',
     'Reversible',
@@ -134,6 +136,7 @@ __all__ = [
 # for backward compatibility
 PEP_560 = True
 GenericMeta = type
+_PEP_696_IMPLEMENTED = sys.version_info >= (3, 13, 0, "beta")
 
 # The functions below are modified copies of typing internal helpers.
 # They are needed by _ProtocolMeta and they provide support for PEP 646.
@@ -406,17 +409,96 @@ Coroutine = typing.Coroutine
 AsyncIterable = typing.AsyncIterable
 AsyncIterator = typing.AsyncIterator
 Deque = typing.Deque
-ContextManager = typing.ContextManager
-AsyncContextManager = typing.AsyncContextManager
 DefaultDict = typing.DefaultDict
 OrderedDict = typing.OrderedDict
 Counter = typing.Counter
 ChainMap = typing.ChainMap
-AsyncGenerator = typing.AsyncGenerator
 Text = typing.Text
 TYPE_CHECKING = typing.TYPE_CHECKING
 
 
+if sys.version_info >= (3, 13, 0, "beta"):
+    from typing import AsyncContextManager, AsyncGenerator, ContextManager, Generator
+else:
+    def _is_dunder(attr):
+        return attr.startswith('__') and attr.endswith('__')
+
+    # Python <3.9 doesn't have typing._SpecialGenericAlias
+    _special_generic_alias_base = getattr(
+        typing, "_SpecialGenericAlias", typing._GenericAlias
+    )
+
+    class _SpecialGenericAlias(_special_generic_alias_base, _root=True):
+        def __init__(self, origin, nparams, *, inst=True, name=None, defaults=()):
+            if _special_generic_alias_base is typing._GenericAlias:
+                # Python <3.9
+                self.__origin__ = origin
+                self._nparams = nparams
+                super().__init__(origin, nparams, special=True, inst=inst, name=name)
+            else:
+                # Python >= 3.9
+                super().__init__(origin, nparams, inst=inst, name=name)
+            self._defaults = defaults
+
+        def __setattr__(self, attr, val):
+            allowed_attrs = {'_name', '_inst', '_nparams', '_defaults'}
+            if _special_generic_alias_base is typing._GenericAlias:
+                # Python <3.9
+                allowed_attrs.add("__origin__")
+            if _is_dunder(attr) or attr in allowed_attrs:
+                object.__setattr__(self, attr, val)
+            else:
+                setattr(self.__origin__, attr, val)
+
+        @typing._tp_cache
+        def __getitem__(self, params):
+            if not isinstance(params, tuple):
+                params = (params,)
+            msg = "Parameters to generic types must be types."
+            params = tuple(typing._type_check(p, msg) for p in params)
+            if (
+                self._defaults
+                and len(params) < self._nparams
+                and len(params) + len(self._defaults) >= self._nparams
+            ):
+                params = (*params, *self._defaults[len(params) - self._nparams:])
+            actual_len = len(params)
+
+            if actual_len != self._nparams:
+                if self._defaults:
+                    expected = f"at least {self._nparams - len(self._defaults)}"
+                else:
+                    expected = str(self._nparams)
+                if not self._nparams:
+                    raise TypeError(f"{self} is not a generic class")
+                raise TypeError(
+                    f"Too {'many' if actual_len > self._nparams else 'few'}"
+                    f" arguments for {self};"
+                    f" actual {actual_len}, expected {expected}"
+                )
+            return self.copy_with(params)
+
+    _NoneType = type(None)
+    Generator = _SpecialGenericAlias(
+        collections.abc.Generator, 3, defaults=(_NoneType, _NoneType)
+    )
+    AsyncGenerator = _SpecialGenericAlias(
+        collections.abc.AsyncGenerator, 2, defaults=(_NoneType,)
+    )
+    ContextManager = _SpecialGenericAlias(
+        contextlib.AbstractContextManager,
+        2,
+        name="ContextManager",
+        defaults=(typing.Optional[bool],)
+    )
+    AsyncContextManager = _SpecialGenericAlias(
+        contextlib.AbstractAsyncContextManager,
+        2,
+        name="AsyncContextManager",
+        defaults=(typing.Optional[bool],)
+    )
+
+
 _PROTO_ALLOWLIST = {
     'collections.abc': [
         'Callable', 'Awaitable', 'Iterable', 'Iterator', 'AsyncIterable',
@@ -427,23 +509,11 @@ _PROTO_ALLOWLIST = {
 }
 
 
-_EXCLUDED_ATTRS = {
-    "__abstractmethods__", "__annotations__", "__weakref__", "_is_protocol",
-    "_is_runtime_protocol", "__dict__", "__slots__", "__parameters__",
-    "__orig_bases__", "__module__", "_MutableMapping__marker", "__doc__",
-    "__subclasshook__", "__orig_class__", "__init__", "__new__",
-    "__protocol_attrs__", "__non_callable_proto_members__",
-    "__match_args__",
+_EXCLUDED_ATTRS = frozenset(typing.EXCLUDED_ATTRIBUTES) | {
+    "__match_args__", "__protocol_attrs__", "__non_callable_proto_members__",
+    "__final__",
 }
 
-if sys.version_info >= (3, 9):
-    _EXCLUDED_ATTRS.add("__class_getitem__")
-
-if sys.version_info >= (3, 12):
-    _EXCLUDED_ATTRS.add("__type_params__")
-
-_EXCLUDED_ATTRS = frozenset(_EXCLUDED_ATTRS)
-
 
 def _get_protocol_attrs(cls):
     attrs = set()
@@ -669,13 +739,18 @@ else:
         not their type signatures!
         """
         if not issubclass(cls, typing.Generic) or not getattr(cls, '_is_protocol', False):
-            raise TypeError('@runtime_checkable can be only applied to protocol classes,'
-                            ' got %r' % cls)
+            raise TypeError(f'@runtime_checkable can be only applied to protocol classes,'
+                            f' got {cls!r}')
         cls._is_runtime_protocol = True
 
-        # Only execute the following block if it's a typing_extensions.Protocol class.
-        # typing.Protocol classes don't need it.
-        if isinstance(cls, _ProtocolMeta):
+        # typing.Protocol classes on <=3.11 break if we execute this block,
+        # because typing.Protocol classes on <=3.11 don't have a
+        # `__protocol_attrs__` attribute, and this block relies on the
+        # `__protocol_attrs__` attribute. Meanwhile, typing.Protocol classes on 3.12.2+
+        # break if we *don't* execute this block, because *they* assume that all
+        # protocol classes have a `__non_callable_proto_members__` attribute
+        # (which this block sets)
+        if isinstance(cls, _ProtocolMeta) or sys.version_info >= (3, 12, 2):
             # PEP 544 prohibits using issubclass()
             # with protocols that have non-method members.
             # See gh-113320 for why we compute this attribute here,
@@ -867,7 +942,13 @@ else:
                 tp_dict.__orig_bases__ = bases
 
             annotations = {}
-            own_annotations = ns.get('__annotations__', {})
+            if "__annotations__" in ns:
+                own_annotations = ns["__annotations__"]
+            elif "__annotate__" in ns:
+                # TODO: Use inspect.VALUE here, and make the annotations lazily evaluated
+                own_annotations = ns["__annotate__"](1)
+            else:
+                own_annotations = {}
             msg = "TypedDict('Name', {f0: t0, f1: t1, ...}); each t must be a type"
             if _TAKES_MODULE:
                 own_annotations = {
@@ -1190,7 +1271,7 @@ else:
 
         def __reduce__(self):
             return operator.getitem, (
-                Annotated, (self.__origin__,) + self.__metadata__
+                Annotated, (self.__origin__, *self.__metadata__)
             )
 
         def __eq__(self, other):
@@ -1316,7 +1397,7 @@ else:
             get_args(Callable[[], T][int]) == ([], int)
         """
         if isinstance(tp, _AnnotatedAlias):
-            return (tp.__origin__,) + tp.__metadata__
+            return (tp.__origin__, *tp.__metadata__)
         if isinstance(tp, (typing._GenericAlias, _typing_GenericAlias)):
             if getattr(tp, "_special", False):
                 return ()
@@ -1362,17 +1443,37 @@ else:
     )
 
 
+if hasattr(typing, "NoDefault"):
+    NoDefault = typing.NoDefault
+else:
+    class NoDefaultTypeMeta(type):
+        def __setattr__(cls, attr, value):
+            # TypeError is consistent with the behavior of NoneType
+            raise TypeError(
+                f"cannot set {attr!r} attribute of immutable type {cls.__name__!r}"
+            )
+
+    class NoDefaultType(metaclass=NoDefaultTypeMeta):
+        """The type of the NoDefault singleton."""
+
+        __slots__ = ()
+
+        def __new__(cls):
+            return globals().get("NoDefault") or object.__new__(cls)
+
+        def __repr__(self):
+            return "typing_extensions.NoDefault"
+
+        def __reduce__(self):
+            return "NoDefault"
+
+    NoDefault = NoDefaultType()
+    del NoDefaultType, NoDefaultTypeMeta
+
+
 def _set_default(type_param, default):
-    if isinstance(default, (tuple, list)):
-        type_param.__default__ = tuple((typing._type_check(d, "Default must be a type")
-                                        for d in default))
-    elif default != _marker:
-        if isinstance(type_param, ParamSpec) and default is ...:  # ... not valid <3.11
-            type_param.__default__ = default
-        else:
-            type_param.__default__ = typing._type_check(default, "Default must be a type")
-    else:
-        type_param.__default__ = None
+    type_param.has_default = lambda: default is not NoDefault
+    type_param.__default__ = default
 
 
 def _set_module(typevarlike):
@@ -1395,32 +1496,46 @@ class _TypeVarLikeMeta(type):
         return isinstance(__instance, cls._backported_typevarlike)
 
 
-# Add default and infer_variance parameters from PEP 696 and 695
-class TypeVar(metaclass=_TypeVarLikeMeta):
-    """Type variable."""
+if _PEP_696_IMPLEMENTED:
+    from typing import TypeVar
+else:
+    # Add default and infer_variance parameters from PEP 696 and 695
+    class TypeVar(metaclass=_TypeVarLikeMeta):
+        """Type variable."""
 
-    _backported_typevarlike = typing.TypeVar
+        _backported_typevarlike = typing.TypeVar
 
-    def __new__(cls, name, *constraints, bound=None,
-                covariant=False, contravariant=False,
-                default=_marker, infer_variance=False):
-        if hasattr(typing, "TypeAliasType"):
-            # PEP 695 implemented (3.12+), can pass infer_variance to typing.TypeVar
-            typevar = typing.TypeVar(name, *constraints, bound=bound,
-                                     covariant=covariant, contravariant=contravariant,
-                                     infer_variance=infer_variance)
-        else:
-            typevar = typing.TypeVar(name, *constraints, bound=bound,
-                                     covariant=covariant, contravariant=contravariant)
-            if infer_variance and (covariant or contravariant):
-                raise ValueError("Variance cannot be specified with infer_variance.")
-            typevar.__infer_variance__ = infer_variance
-        _set_default(typevar, default)
-        _set_module(typevar)
-        return typevar
+        def __new__(cls, name, *constraints, bound=None,
+                    covariant=False, contravariant=False,
+                    default=NoDefault, infer_variance=False):
+            if hasattr(typing, "TypeAliasType"):
+                # PEP 695 implemented (3.12+), can pass infer_variance to typing.TypeVar
+                typevar = typing.TypeVar(name, *constraints, bound=bound,
+                                         covariant=covariant, contravariant=contravariant,
+                                         infer_variance=infer_variance)
+            else:
+                typevar = typing.TypeVar(name, *constraints, bound=bound,
+                                         covariant=covariant, contravariant=contravariant)
+                if infer_variance and (covariant or contravariant):
+                    raise ValueError("Variance cannot be specified with infer_variance.")
+                typevar.__infer_variance__ = infer_variance
+
+            _set_default(typevar, default)
+            _set_module(typevar)
+
+            def _tvar_prepare_subst(alias, args):
+                if (
+                    typevar.has_default()
+                    and alias.__parameters__.index(typevar) == len(args)
+                ):
+                    args += (typevar.__default__,)
+                return args
 
-    def __init_subclass__(cls) -> None:
-        raise TypeError(f"type '{__name__}.TypeVar' is not an acceptable base type")
+            typevar.__typing_prepare_subst__ = _tvar_prepare_subst
+            return typevar
+
+        def __init_subclass__(cls) -> None:
+            raise TypeError(f"type '{__name__}.TypeVar' is not an acceptable base type")
 
 
 # Python 3.10+ has PEP 612
@@ -1485,8 +1600,12 @@ else:
                 return NotImplemented
             return self.__origin__ == other.__origin__
 
+
+if _PEP_696_IMPLEMENTED:
+    from typing import ParamSpec
+
 # 3.10+
-if hasattr(typing, 'ParamSpec'):
+elif hasattr(typing, 'ParamSpec'):
 
     # Add default parameter - PEP 696
     class ParamSpec(metaclass=_TypeVarLikeMeta):
@@ -1496,7 +1615,7 @@ if hasattr(typing, 'ParamSpec'):
 
         def __new__(cls, name, *, bound=None,
                     covariant=False, contravariant=False,
-                    infer_variance=False, default=_marker):
+                    infer_variance=False, default=NoDefault):
             if hasattr(typing, "TypeAliasType"):
                 # PEP 695 implemented, can pass infer_variance to typing.TypeVar
                 paramspec = typing.ParamSpec(name, bound=bound,
@@ -1511,6 +1630,24 @@ if hasattr(typing, 'ParamSpec'):
 
             _set_default(paramspec, default)
             _set_module(paramspec)
+
+            def _paramspec_prepare_subst(alias, args):
+                params = alias.__parameters__
+                i = params.index(paramspec)
+                if i == len(args) and paramspec.has_default():
+                    args = [*args, paramspec.__default__]
+                if i >= len(args):
+                    raise TypeError(f"Too few arguments for {alias}")
+                # Special case where Z[[int, str, bool]] == Z[int, str, bool] in PEP 612.
+                if len(params) == 1 and not typing._is_param_expr(args[0]):
+                    assert i == 0
+                    args = (args,)
+                # Convert lists to tuples to help other libraries cache the results.
+                elif isinstance(args[i], list):
+                    args = (*args[:i], tuple(args[i]), *args[i + 1:])
+                return args
+
+            paramspec.__typing_prepare_subst__ = _paramspec_prepare_subst
             return paramspec
 
         def __init_subclass__(cls) -> None:
@@ -1579,8 +1716,8 @@ else:
             return ParamSpecKwargs(self)
 
         def __init__(self, name, *, bound=None, covariant=False, contravariant=False,
-                     infer_variance=False, default=_marker):
-            super().__init__([self])
+                     infer_variance=False, default=NoDefault):
+            list.__init__(self, [self])
             self.__name__ = name
             self.__covariant__ = bool(covariant)
             self.__contravariant__ = bool(contravariant)
@@ -1674,7 +1811,7 @@ def _concatenate_getitem(self, parameters):
 # 3.10+
 if hasattr(typing, 'Concatenate'):
     Concatenate = typing.Concatenate
-    _ConcatenateGenericAlias = typing._ConcatenateGenericAlias  # noqa: F811
+    _ConcatenateGenericAlias = typing._ConcatenateGenericAlias
 # 3.9
 elif sys.version_info[:2] >= (3, 9):
     @_ExtensionsSpecialForm
@@ -2209,6 +2346,17 @@ elif sys.version_info[:2] >= (3, 9):  # 3.9+
     class _UnpackAlias(typing._GenericAlias, _root=True):
         __class__ = typing.TypeVar
 
+        @property
+        def __typing_unpacked_tuple_args__(self):
+            assert self.__origin__ is Unpack
+            assert len(self.__args__) == 1
+            arg, = self.__args__
+            if isinstance(arg, (typing._GenericAlias, _types.GenericAlias)):
+                if arg.__origin__ is not tuple:
+                    raise TypeError("Unpack[...] must be used with a tuple type")
+                return arg.__args__
+            return None
+
     @_UnpackSpecialForm
     def Unpack(self, parameters):
         item = typing._type_check(parameters, f'{self._name} accepts only a single type.')
@@ -2233,7 +2381,20 @@ else:  # 3.8
         return isinstance(obj, _UnpackAlias)
 
 
-if hasattr(typing, "TypeVarTuple"):  # 3.11+
+if _PEP_696_IMPLEMENTED:
+    from typing import TypeVarTuple
+
+elif hasattr(typing, "TypeVarTuple"):  # 3.11+
+
+    def _unpack_args(*args):
+        newargs = []
+        for arg in args:
+            subargs = getattr(arg, '__typing_unpacked_tuple_args__', None)
+            if subargs is not None and not (subargs and subargs[-1] is ...):
+                newargs.extend(subargs)
+            else:
+                newargs.append(arg)
+        return newargs
 
     # Add default parameter - PEP 696
     class TypeVarTuple(metaclass=_TypeVarLikeMeta):
@@ -2241,10 +2402,57 @@ if hasattr(typing, "TypeVarTuple"):  # 3.11+
 
         _backported_typevarlike = typing.TypeVarTuple
 
-        def __new__(cls, name, *, default=_marker):
+        def __new__(cls, name, *, default=NoDefault):
             tvt = typing.TypeVarTuple(name)
             _set_default(tvt, default)
             _set_module(tvt)
+
+            def _typevartuple_prepare_subst(alias, args):
+                params = alias.__parameters__
+                typevartuple_index = params.index(tvt)
+                for param in params[typevartuple_index + 1:]:
+                    if isinstance(param, TypeVarTuple):
+                        raise TypeError(
+                            f"More than one TypeVarTuple parameter in {alias}"
+                        )
+
+                alen = len(args)
+                plen = len(params)
+                left = typevartuple_index
+                right = plen - typevartuple_index - 1
+                var_tuple_index = None
+                fillarg = None
+                for k, arg in enumerate(args):
+                    if not isinstance(arg, type):
+                        subargs = getattr(arg, '__typing_unpacked_tuple_args__', None)
+                        if subargs and len(subargs) == 2 and subargs[-1] is ...:
+                            if var_tuple_index is not None:
+                                raise TypeError(
+                                    "More than one unpacked "
+                                    "arbitrary-length tuple argument"
+                                )
+                            var_tuple_index = k
+                            fillarg = subargs[0]
+                if var_tuple_index is not None:
+                    left = min(left, var_tuple_index)
+                    right = min(right, alen - var_tuple_index - 1)
+                elif left + right > alen:
+                    raise TypeError(f"Too few arguments for {alias};"
+                                    f" actual {alen}, expected at least {plen - 1}")
+                if left == alen - right and tvt.has_default():
+                    replacement = _unpack_args(tvt.__default__)
+                else:
+                    replacement = args[left: alen - right]
+
+                return (
+                    *args[:left],
+                    *([fillarg] * (typevartuple_index - left)),
+                    replacement,
+                    *([fillarg] * (plen - right - left - typevartuple_index - 1)),
+                    *args[alen - right:],
+                )
+
+            tvt.__typing_prepare_subst__ = _typevartuple_prepare_subst
             return tvt
 
         def __init_subclass__(self, *args, **kwds):
@@ -2301,7 +2509,7 @@ else:  # <=3.10
         def __iter__(self):
             yield self.__unpacked__
 
-        def __init__(self, name, *, default=_marker):
+        def __init__(self, name, *, default=NoDefault):
             self.__name__ = name
             _DefaultMixin.__init__(self, default)
 
@@ -2352,6 +2560,12 @@ else:  # <=3.10
         return obj
 
 
+if hasattr(typing, "_ASSERT_NEVER_REPR_MAX_LENGTH"):  # 3.11+
+    _ASSERT_NEVER_REPR_MAX_LENGTH = typing._ASSERT_NEVER_REPR_MAX_LENGTH
+else:  # <=3.10
+    _ASSERT_NEVER_REPR_MAX_LENGTH = 100
+
+
 if hasattr(typing, "assert_never"):  # 3.11+
     assert_never = typing.assert_never
 else:  # <=3.10
@@ -2375,7 +2589,10 @@ else:  # <=3.10
         At runtime, this throws an exception when called.
 
         """
-        raise AssertionError("Expected code to be unreachable")
+        value = repr(arg)
+        if len(value) > _ASSERT_NEVER_REPR_MAX_LENGTH:
+            value = value[:_ASSERT_NEVER_REPR_MAX_LENGTH] + '...'
+        raise AssertionError(f"Expected code to be unreachable, but got: {value}")
 
 
 if sys.version_info >= (3, 12):  # 3.12+
@@ -2677,11 +2894,14 @@ if not hasattr(typing, "TypeVarTuple"):
                 if alen < elen:
                     # since we validate TypeVarLike default in _collect_type_vars
                     # or _collect_parameters we can safely check parameters[alen]
-                    if getattr(parameters[alen], '__default__', None) is not None:
+                    if (
+                        getattr(parameters[alen], '__default__', NoDefault)
+                        is not NoDefault
+                    ):
                         return
 
-                    num_default_tv = sum(getattr(p, '__default__', None)
-                                         is not None for p in parameters)
+                    num_default_tv = sum(getattr(p, '__default__', NoDefault)
+                                         is not NoDefault for p in parameters)
 
                     elen -= num_default_tv
 
@@ -2711,11 +2931,14 @@ else:
                 if alen < elen:
                     # since we validate TypeVarLike default in _collect_type_vars
                     # or _collect_parameters we can safely check parameters[alen]
-                    if getattr(parameters[alen], '__default__', None) is not None:
+                    if (
+                        getattr(parameters[alen], '__default__', NoDefault)
+                        is not NoDefault
+                    ):
                         return
 
-                    num_default_tv = sum(getattr(p, '__default__', None)
-                                         is not None for p in parameters)
+                    num_default_tv = sum(getattr(p, '__default__', NoDefault)
+                                         is not NoDefault for p in parameters)
 
                     elen -= num_default_tv
 
@@ -2724,7 +2947,42 @@ else:
             raise TypeError(f"Too {'many' if alen > elen else 'few'} arguments"
                             f" for {cls}; actual {alen}, expected {expect_val}")
 
-typing._check_generic = _check_generic
+if not _PEP_696_IMPLEMENTED:
+    typing._check_generic = _check_generic
+
+
+def _has_generic_or_protocol_as_origin() -> bool:
+    try:
+        frame = sys._getframe(2)
+    # - Catch AttributeError: not all Python implementations have sys._getframe()
+    # - Catch ValueError: maybe we're called from an unexpected module
+    #   and the call stack isn't deep enough
+    except (AttributeError, ValueError):
+        return False  # err on the side of leniency
+    else:
+        # If we somehow get invoked from outside typing.py,
+        # also err on the side of leniency
+        if frame.f_globals.get("__name__") != "typing":
+            return False
+        origin = frame.f_locals.get("origin")
+        # Cannot use "in" because origin may be an object with a buggy __eq__ that
+        # throws an error.
+        return origin is typing.Generic or origin is Protocol or origin is typing.Protocol
+
+
+_TYPEVARTUPLE_TYPES = {TypeVarTuple, getattr(typing, "TypeVarTuple", None)}
+
+
+def _is_unpacked_typevartuple(x) -> bool:
+    if get_origin(x) is not Unpack:
+        return False
+    args = get_args(x)
+    return (
+        bool(args)
+        and len(args) == 1
+        and type(args[0]) in _TYPEVARTUPLE_TYPES
+    )
+
 
 # Python 3.11+ _collect_type_vars was renamed to _collect_parameters
 if hasattr(typing, '_collect_type_vars'):
@@ -2737,19 +2995,29 @@ if hasattr(typing, '_collect_type_vars'):
         if typevar_types is None:
             typevar_types = typing.TypeVar
         tvars = []
-        # required TypeVarLike cannot appear after TypeVarLike with default
+
+        # A required TypeVarLike cannot appear after a TypeVarLike with a default
+        # if it was a direct call to `Generic[]` or `Protocol[]`
+        enforce_default_ordering = _has_generic_or_protocol_as_origin()
         default_encountered = False
+
+        # Also, a TypeVarLike with a default cannot appear after a TypeVarTuple
+        type_var_tuple_encountered = False
+
         for t in types:
-            if (
-                isinstance(t, typevar_types) and
-                t not in tvars and
-                not _is_unpack(t)
-            ):
-                if getattr(t, '__default__', None) is not None:
-                    default_encountered = True
-                elif default_encountered:
-                    raise TypeError(f'Type parameter {t!r} without a default'
-                                    ' follows type parameter with a default')
+            if _is_unpacked_typevartuple(t):
+                type_var_tuple_encountered = True
+            elif isinstance(t, typevar_types) and t not in tvars:
+                if enforce_default_ordering:
+                    has_default = getattr(t, '__default__', NoDefault) is not NoDefault
+                    if has_default:
+                        if type_var_tuple_encountered:
+                            raise TypeError('Type parameter with a default'
+                                            ' follows TypeVarTuple')
+                        default_encountered = True
+                    elif default_encountered:
+                        raise TypeError(f'Type parameter {t!r} without a default'
+                                        ' follows type parameter with a default')
 
                 tvars.append(t)
             if _should_collect_from_parameters(t):
@@ -2767,8 +3035,15 @@ else:
             assert _collect_parameters((T, Callable[P, T])) == (T, P)
         """
         parameters = []
-        # required TypeVarLike cannot appear after TypeVarLike with default
+
+        # A required TypeVarLike cannot appear after a TypeVarLike with default
+        # if it was a direct call to `Generic[]` or `Protocol[]`
+        enforce_default_ordering = _has_generic_or_protocol_as_origin()
         default_encountered = False
+
+        # Also, a TypeVarLike with a default cannot appear after a TypeVarTuple
+        type_var_tuple_encountered = False
+
         for t in args:
             if isinstance(t, type):
                 # We don't want __parameters__ descriptor of a bare Python class.
@@ -2782,21 +3057,33 @@ else:
                             parameters.append(collected)
             elif hasattr(t, '__typing_subst__'):
                 if t not in parameters:
-                    if getattr(t, '__default__', None) is not None:
-                        default_encountered = True
-                    elif default_encountered:
-                        raise TypeError(f'Type parameter {t!r} without a default'
-                                        ' follows type parameter with a default')
+                    if enforce_default_ordering:
+                        has_default = (
+                            getattr(t, '__default__', NoDefault) is not NoDefault
+                        )
+
+                        if type_var_tuple_encountered and has_default:
+                            raise TypeError('Type parameter with a default'
+                                            ' follows TypeVarTuple')
+
+                        if has_default:
+                            default_encountered = True
+                        elif default_encountered:
+                            raise TypeError(f'Type parameter {t!r} without a default'
+                                            ' follows type parameter with a default')
 
                     parameters.append(t)
             else:
+                if _is_unpacked_typevartuple(t):
+                    type_var_tuple_encountered = True
                 for x in getattr(t, '__parameters__', ()):
                     if x not in parameters:
                         parameters.append(x)
 
         return tuple(parameters)
 
-    typing._collect_parameters = _collect_parameters
+    if not _PEP_696_IMPLEMENTED:
+        typing._collect_parameters = _collect_parameters
 
 # Backport typing.NamedTuple as it exists in Python 3.13.
 # In 3.11, the ability to define generic `NamedTuple`s was supported.
@@ -2830,7 +3117,13 @@ else:
                     raise TypeError(
                         'can only inherit from a NamedTuple type and Generic')
             bases = tuple(tuple if base is _NamedTuple else base for base in bases)
-            types = ns.get('__annotations__', {})
+            if "__annotations__" in ns:
+                types = ns["__annotations__"]
+            elif "__annotate__" in ns:
+                # TODO: Use inspect.VALUE here, and make the annotations lazily evaluated
+                types = ns["__annotate__"](1)
+            else:
+                types = {}
             default_names = []
             for field_name in types:
                 if field_name in ns:
@@ -2962,7 +3255,7 @@ else:
 if hasattr(collections.abc, "Buffer"):
     Buffer = collections.abc.Buffer
 else:
-    class Buffer(abc.ABC):
+    class Buffer(abc.ABC):  # noqa: B024
         """Base class for classes that implement the buffer protocol.
 
         The buffer protocol allows Python objects to expose a low-level
@@ -3289,6 +3582,23 @@ else:
             return self.documentation == other.documentation
 
 
+_CapsuleType = getattr(_types, "CapsuleType", None)
+
+if _CapsuleType is None:
+    try:
+        import _socket
+    except ImportError:
+        pass
+    else:
+        _CAPI = getattr(_socket, "CAPI", None)
+        if _CAPI is not None:
+            _CapsuleType = type(_CAPI)
+
+if _CapsuleType is not None:
+    CapsuleType = _CapsuleType
+    __all__.append("CapsuleType")
+
+
 # Aliases for items that have always been in typing.
 # Explicitly assign these (rather than using `from typing import *` at the top),
 # so that we get a CI error if one of these is deleted from typing.py
@@ -3302,7 +3612,6 @@ Container = typing.Container
 Dict = typing.Dict
 ForwardRef = typing.ForwardRef
 FrozenSet = typing.FrozenSet
-Generator = typing.Generator
 Generic = typing.Generic
 Hashable = typing.Hashable
 IO = typing.IO
diff --git a/src/pip/_vendor/vendor.txt b/src/pip/_vendor/vendor.txt
index 00d81549c..7e5b4e5df 100644
--- a/src/pip/_vendor/vendor.txt
+++ b/src/pip/_vendor/vendor.txt
@@ -2,18 +2,18 @@ CacheControl==0.14.0
 distlib==0.3.8
 distro==1.9.0
 msgpack==1.0.8
-packaging==24.0
+packaging==24.1
 platformdirs==4.2.1
 pyproject-hooks==1.0.0
-requests==2.32.0
+requests==2.32.3
     certifi==2024.2.2
     idna==3.7
     urllib3==1.26.18
 rich==13.7.1
     pygments==2.17.2
-    typing_extensions==4.11.0
+    typing_extensions==4.12.2
 resolvelib==1.0.1
 setuptools==69.5.1
 tenacity==8.2.3
 tomli==2.0.1
-truststore==0.8.0
+truststore==0.9.1
diff --git a/tools/codespell-ignore.txt b/tools/codespell-ignore.txt
index 288f59735..6856939bf 100644
--- a/tools/codespell-ignore.txt
+++ b/tools/codespell-ignore.txt
@@ -3,6 +3,7 @@ lousily
 followings
 # A contributor first name
 wil
+Whit
 # Codebase variable or class names
 uptodate
 afile


Test patch:
diff --git a/tests/conftest.py b/tests/conftest.py
index 35101cef2..5934e9f95 100644
--- a/tests/conftest.py
+++ b/tests/conftest.py
@@ -395,6 +395,7 @@ def pip_editable_parts(
             "-m",
             "pip",
             "install",
+            "--no-build-isolation",
             "--target",
             pip_self_install_path,
             "-e",
diff --git a/tests/functional/test_config_settings.py b/tests/functional/test_config_settings.py
index 3f88d9c39..857722dd1 100644
--- a/tests/functional/test_config_settings.py
+++ b/tests/functional/test_config_settings.py
@@ -118,6 +118,7 @@ def test_config_settings_implies_pep517(
     )
     result = script.pip(
         "wheel",
+        "--no-build-isolation",
         "--config-settings",
         "FOO=Hello",
         pkg_path,
diff --git a/tests/functional/test_install.py b/tests/functional/test_install.py
index eaea12a16..0b377167b 100644
--- a/tests/functional/test_install.py
+++ b/tests/functional/test_install.py
@@ -1,9 +1,11 @@
 import hashlib
+import io
 import os
 import re
 import ssl
 import sys
 import sysconfig
+import tarfile
 import textwrap
 from os.path import curdir, join, pardir
 from pathlib import Path
@@ -685,7 +687,9 @@ def test_link_hash_in_dep_fails_require_hashes(
     # Build a wheel for pkga and compute its hash.
     wheelhouse = tmp_path / "wheehouse"
     wheelhouse.mkdir()
-    script.pip("wheel", "--no-deps", "-w", wheelhouse, project_path)
+    script.pip(
+        "wheel", "--no-build-isolation", "--no-deps", "-w", wheelhouse, project_path
+    )
     digest = hashlib.sha256(
         wheelhouse.joinpath("pkga-1.0-py3-none-any.whl").read_bytes()
     ).hexdigest()
@@ -903,7 +907,14 @@ def test_editable_install__local_dir_setup_requires_with_pyproject(
         "setup(name='dummy', setup_requires=['simplewheel'])\n"
     )
 
-    script.pip("install", "--find-links", shared_data.find_links, "-e", local_dir)
+    script.pip(
+        "install",
+        "--no-build-isolation",
+        "--find-links",
+        shared_data.find_links,
+        "-e",
+        local_dir,
+    )
 
 
 def test_install_pre__setup_requires_with_pyproject(
@@ -2590,3 +2601,130 @@ def test_install_pip_prints_req_chain_pypi(script: PipTestEnvironment) -> None:
         f"Collecting python-openid "
         f"(from Paste[openid]==1.7.5.1->-r {req_path} (line 1))" in result.stdout
     )
+
+
+@pytest.mark.parametrize("common_prefix", ("", "linktest-1.0/"))
+def test_install_sdist_links(script: PipTestEnvironment, common_prefix: str) -> None:
+    """
+    Test installing an sdist with hard and symbolic links.
+    """
+
+    # Build an unpack an sdist that contains data files:
+    # - root.dat
+    # - sub/inner.dat
+    # and links (symbolic and hard) to both of those, both in the top-level
+    # and 'sub/' directories. That's 8 links total.
+
+    # We build the sdist from in-memory data, since the filesystem
+    # might not support both kinds of links.
+
+    sdist_path = script.scratch_path.joinpath("linktest-1.0.tar.gz")
+
+    def add_file(tar: tarfile.TarFile, name: str, content: str) -> None:
+        info = tarfile.TarInfo(common_prefix + name)
+        content_bytes = content.encode("utf-8")
+        info.size = len(content_bytes)
+        tar.addfile(info, io.BytesIO(content_bytes))
+
+    def add_link(tar: tarfile.TarFile, name: str, linktype: str, target: str) -> None:
+        info = tarfile.TarInfo(common_prefix + name)
+        info.type = {"sym": tarfile.SYMTYPE, "hard": tarfile.LNKTYPE}[linktype]
+        info.linkname = target
+        tar.addfile(info)
+
+    with tarfile.open(sdist_path, "w:gz") as sdist_tar:
+        add_file(
+            sdist_tar,
+            "PKG-INFO",
+            textwrap.dedent(
+                """
+                    Metadata-Version: 2.1
+                    Name: linktest
+                    Version: 1.0
+                """
+            ),
+        )
+
+        add_file(sdist_tar, "src/linktest/__init__.py", "")
+        add_file(sdist_tar, "src/linktest/root.dat", "Data")
+        add_file(sdist_tar, "src/linktest/sub/__init__.py", "")
+        add_file(sdist_tar, "src/linktest/sub/inner.dat", "Data")
+        linknames = []
+
+        # Windows requires native path separators in symlink targets.
+        # (see https://github.com/python/cpython/issues/57911)
+        # (This is not needed for hardlinks, nor for the workaround tarfile
+        # uses if symlinking is disabled.)
+        SEP = os.path.sep
+
+        pkg_root = f"{common_prefix}src/linktest"
+        for prefix, target_tag, linktype, target in [
+            ("", "root", "sym", "root.dat"),
+            ("", "root", "hard", f"{pkg_root}/root.dat"),
+            ("", "inner", "sym", f"sub{SEP}inner.dat"),
+            ("", "inner", "hard", f"{pkg_root}/sub/inner.dat"),
+            ("sub/", "root", "sym", f"..{SEP}root.dat"),
+            ("sub/", "root", "hard", f"{pkg_root}/root.dat"),
+            ("sub/", "inner", "sym", "inner.dat"),
+            ("sub/", "inner", "hard", f"{pkg_root}/sub/inner.dat"),
+        ]:
+            name = f"{prefix}link.{target_tag}.{linktype}.dat"
+            add_link(sdist_tar, "src/linktest/" + name, linktype, target)
+            linknames.append(name)
+
+        add_file(
+            sdist_tar,
+            "pyproject.toml",
+            textwrap.dedent(
+                """
+                    [build-system]
+                    requires = ["setuptools"]
+                    build-backend = "setuptools.build_meta"
+                    [project]
+                    name = "linktest"
+                    version = "1.0"
+                    [tool.setuptools]
+                    include-package-data = true
+                    [tool.setuptools.packages.find]
+                    where = ["src"]
+                    [tool.setuptools.package-data]
+                    "*" = ["*.dat"]
+                """
+            ),
+        )
+
+        add_file(
+            sdist_tar,
+            "src/linktest/__main__.py",
+            textwrap.dedent(
+                f"""
+                    from pathlib import Path
+                    linknames = {linknames!r}
+
+                    # we could use importlib.resources here once
+                    # it has stable convenient API across supported versions
+                    res_path = Path(__file__).parent
+
+                    for name in linknames:
+                        data_text = res_path.joinpath(name).read_text()
+                        assert data_text == "Data"
+                    print(str(len(linknames)) + ' files checked')
+                """
+            ),
+        )
+
+    # Show sdist content, for debugging the test
+    result = script.run("python", "-m", "tarfile", "-vl", str(sdist_path))
+    print(result)
+
+    # Install the package
+    result = script.pip("install", str(sdist_path))
+    print(result)
+
+    # Show installed content, for debugging the test
+    result = script.pip("show", "-f", "linktest")
+    print(result)
+
+    # Run the internal test
+    result = script.run("python", "-m", "linktest")
+    assert result.stdout.strip() == "8 files checked"
diff --git a/tests/functional/test_install_extras.py b/tests/functional/test_install_extras.py
index 4dd27bb9d..4e74f54a2 100644
--- a/tests/functional/test_install_extras.py
+++ b/tests/functional/test_install_extras.py
@@ -1,6 +1,7 @@
 import re
 import textwrap
 from os.path import join
+from pathlib import Path
 
 import pytest
 
@@ -252,3 +253,23 @@ def test_install_extras(script: PipTestEnvironment) -> None:
         "a",
     )
     script.assert_installed(a="1", b="1", dep="1", meh="1")
+
+
+def test_install_setuptools_extras_inconsistency(
+    script: PipTestEnvironment, tmp_path: Path
+) -> None:
+    test_project_path = tmp_path.joinpath("test")
+    test_project_path.mkdir()
+    test_project_path.joinpath("setup.py").write_text(
+        textwrap.dedent(
+            """
+                from setuptools import setup
+                setup(
+                    name='test',
+                    version='0.1',
+                    extras_require={'extra_underscored': ['packaging']},
+                )
+            """
+        )
+    )
+    script.pip("install", "--dry-run", test_project_path)
diff --git a/tests/functional/test_self_update.py b/tests/functional/test_self_update.py
index c50755220..1331a87c3 100644
--- a/tests/functional/test_self_update.py
+++ b/tests/functional/test_self_update.py
@@ -11,12 +11,12 @@ def test_self_update_editable(script: Any, pip_src: Any) -> None:
     # Step 1. Install pip as non-editable. This is expected to succeed as
     # the existing pip in the environment is installed in editable mode, so
     # it only places a .pth file in the environment.
-    proc = script.pip("install", pip_src)
+    proc = script.pip("install", "--no-build-isolation", pip_src)
     assert proc.returncode == 0
     # Step 2. Using the pip we just installed, install pip *again*, but
     # in editable mode. This could fail, as we'll need to uninstall the running
     # pip in order to install the new copy, and uninstalling pip while it's
     # running could fail. This test is specifically to ensure that doesn't
     # happen...
-    proc = script.pip("install", "-e", pip_src)
+    proc = script.pip("install", "--no-build-isolation", "-e", pip_src)
     assert proc.returncode == 0
diff --git a/tests/lib/__init__.py b/tests/lib/__init__.py
index bd31b59ff..e318f7155 100644
--- a/tests/lib/__init__.py
+++ b/tests/lib/__init__.py
@@ -28,6 +28,7 @@ from typing import (
     Union,
     cast,
 )
+from urllib.parse import urlparse, urlunparse
 from zipfile import ZipFile
 
 import pytest
@@ -1375,3 +1376,19 @@ class ScriptFactory(Protocol):
 
 
 CertFactory = Callable[[], str]
+
+# versions containing fix/backport from https://github.com/python/cpython/pull/113563
+# which changed the behavior of `urllib.parse.urlun{parse,split}`
+url = "////path/to/file"
+has_new_urlun_behavior = url == urlunparse(urlparse(url))
+
+# the above change seems to only impact tests on Windows, so just add skips for that
+skip_needs_new_urlun_behavior_win = pytest.mark.skipif(
+    sys.platform != "win32" or not has_new_urlun_behavior,
+    reason="testing windows behavior for newer CPython",
+)
+
+skip_needs_old_urlun_behavior_win = pytest.mark.skipif(
+    sys.platform != "win32" or has_new_urlun_behavior,
+    reason="testing windows behavior for older CPython",
+)
diff --git a/tests/requirements.txt b/tests/requirements.txt
index 8eb02d6ca..5c3ef4188 100644
--- a/tests/requirements.txt
+++ b/tests/requirements.txt
@@ -1,4 +1,4 @@
-cffi @ https://github.com/python-cffi/cffi/archive/refs/heads/main.zip ; python_version > "3.12" # Temporary workaround for Python 3.13 until next CFFI release
+cffi >= 1.17.0rc1 ; python_version > "3.12" # Temporary workaround for Python 3.13 until final CFFI release
 cryptography
 freezegun
 installer
diff --git a/tests/unit/test_collector.py b/tests/unit/test_collector.py
index 2aaedeedf..89da25b73 100644
--- a/tests/unit/test_collector.py
+++ b/tests/unit/test_collector.py
@@ -35,7 +35,12 @@ from pip._internal.models.link import (
     _ensure_quoted_url,
 )
 from pip._internal.network.session import PipSession
-from tests.lib import TestData, make_test_link_collector
+from tests.lib import (
+    TestData,
+    make_test_link_collector,
+    skip_needs_new_urlun_behavior_win,
+    skip_needs_old_urlun_behavior_win,
+)
 
 ACCEPT = ", ".join(
     [
@@ -383,7 +388,12 @@ def test_clean_url_path_with_local_path(path: str, expected: str) -> None:
         pytest.param(
             "file:///T:/path/with spaces/",
             "file:///T:/path/with%20spaces",
-            marks=pytest.mark.skipif("sys.platform != 'win32'"),
+            marks=skip_needs_old_urlun_behavior_win,
+        ),
+        pytest.param(
+            "file:///T:/path/with spaces/",
+            "file://///T:/path/with%20spaces",
+            marks=skip_needs_new_urlun_behavior_win,
         ),
         # URL with Windows drive letter, running on non-windows
         # platform. The `:` after the drive should be quoted.
@@ -396,7 +406,12 @@ def test_clean_url_path_with_local_path(path: str, expected: str) -> None:
         pytest.param(
             "git+file:///T:/with space/repo.git@1.0#egg=my-package-1.0",
             "git+file:///T:/with%20space/repo.git@1.0#egg=my-package-1.0",
-            marks=pytest.mark.skipif("sys.platform != 'win32'"),
+            marks=skip_needs_old_urlun_behavior_win,
+        ),
+        pytest.param(
+            "git+file:///T:/with space/repo.git@1.0#egg=my-package-1.0",
+            "git+file://///T:/with%20space/repo.git@1.0#egg=my-package-1.0",
+            marks=skip_needs_new_urlun_behavior_win,
         ),
         # Test a VCS URL with a Windows drive letter and revision,
         # running on non-windows platform.
diff --git a/tests/unit/test_req.py b/tests/unit/test_req.py
index 5e3c640a5..dd0af289e 100644
--- a/tests/unit/test_req.py
+++ b/tests/unit/test_req.py
@@ -734,7 +734,7 @@ class TestInstallRequirement:
         with pytest.raises(InstallationError) as e:
             install_req_from_line(test_name)
         err_msg = e.value.args[0]
-        assert f"Invalid requirement: '{test_name}'" == err_msg
+        assert err_msg.startswith(f"Invalid requirement: '{test_name}'")
 
     def test_requirement_file(self) -> None:
         req_file_path = os.path.join(self.tempdir, "test.txt")
diff --git a/tests/unit/test_req_file.py b/tests/unit/test_req_file.py
index ce751afe2..f4f98b190 100644
--- a/tests/unit/test_req_file.py
+++ b/tests/unit/test_req_file.py
@@ -269,8 +269,10 @@ class TestProcessLine:
             )
 
         expected = (
-            "Invalid requirement: 'my-package=1.0' "
-            "(from line 3 of path/requirements.txt)\n"
+            "Invalid requirement: 'my-package=1.0': "
+            "Expected end or semicolon (after name and no valid version specifier)\n"
+            "    my-package=1.0\n"
+            "              ^ (from line 3 of path/requirements.txt)\n"
             "Hint: = is not a valid operator. Did you mean == ?"
         )
         assert str(exc.value) == expected
diff --git a/tests/unit/test_req_install.py b/tests/unit/test_req_install.py
index 4bb71a743..22b98f86d 100644
--- a/tests/unit/test_req_install.py
+++ b/tests/unit/test_req_install.py
@@ -60,7 +60,12 @@ class TestInstallRequirementFrom:
         with pytest.raises(InstallationError) as excinfo:
             install_req_from_req_string("http:/this/is/invalid")
 
-        assert str(excinfo.value) == ("Invalid requirement: 'http:/this/is/invalid'")
+        assert str(excinfo.value) == (
+            "Invalid requirement: 'http:/this/is/invalid': "
+            "Expected end or semicolon (after name and no valid version specifier)\n"
+            "    http:/this/is/invalid\n"
+            "        ^"
+        )
 
     def test_install_req_from_string_without_comes_from(self) -> None:
         """
diff --git a/tests/unit/test_req_uninstall.py b/tests/unit/test_req_uninstall.py
index 6a846e202..b61babc34 100644
--- a/tests/unit/test_req_uninstall.py
+++ b/tests/unit/test_req_uninstall.py
@@ -28,10 +28,7 @@ def mock_permitted(ups: UninstallPathSet, path: str) -> bool:
 def test_uninstallation_paths() -> None:
     class dist:
         def iter_declared_entries(self) -> Optional[Iterator[str]]:
-            yield "file.py"
-            yield "file.pyc"
-            yield "file.so"
-            yield "nopyc.py"
+            return iter(["file.py", "file.pyc", "file.so", "nopyc.py"])
 
         location = ""
 
diff --git a/tests/unit/test_urls.py b/tests/unit/test_urls.py
index 746d02224..c4b8db681 100644
--- a/tests/unit/test_urls.py
+++ b/tests/unit/test_urls.py
@@ -5,6 +5,10 @@ import urllib.request
 import pytest
 
 from pip._internal.utils.urls import path_to_url, url_to_path
+from tests.lib import (
+    skip_needs_new_urlun_behavior_win,
+    skip_needs_old_urlun_behavior_win,
+)
 
 
 @pytest.mark.skipif("sys.platform == 'win32'")
@@ -15,12 +19,32 @@ def test_path_to_url_unix() -> None:
 
 
 @pytest.mark.skipif("sys.platform != 'win32'")
-def test_path_to_url_win() -> None:
-    assert path_to_url("c:/tmp/file") == "file:///C:/tmp/file"
-    assert path_to_url("c:\\tmp\\file") == "file:///C:/tmp/file"
-    assert path_to_url(r"\\unc\as\path") == "file://unc/as/path"
-    path = os.path.join(os.getcwd(), "file")
-    assert path_to_url("file") == "file:" + urllib.request.pathname2url(path)
+@pytest.mark.parametrize(
+    "path, url",
+    [
+        pytest.param("c:/tmp/file", "file:///C:/tmp/file", id="posix-path"),
+        pytest.param("c:\\tmp\\file", "file:///C:/tmp/file", id="nt-path"),
+        pytest.param(
+            r"\\unc\as\path",
+            "file://unc/as/path",
+            marks=skip_needs_old_urlun_behavior_win,
+            id="unc-path",
+        ),
+        pytest.param(
+            r"\\unc\as\path",
+            "file:////unc/as/path",
+            marks=skip_needs_new_urlun_behavior_win,
+        ),
+    ],
+)
+def test_path_to_url_win(path: str, url: str) -> None:
+    assert path_to_url(path) == url
+
+
+@pytest.mark.skipif("sys.platform != 'win32'")
+def test_relative_path_to_url_win() -> None:
+    resolved_path = os.path.join(os.getcwd(), "file")
+    assert path_to_url("file") == "file:" + urllib.request.pathname2url(resolved_path)
 
 
 @pytest.mark.parametrize(
diff --git a/tests/unit/test_utils.py b/tests/unit/test_utils.py
index 102b0340a..aa9e7fd56 100644
--- a/tests/unit/test_utils.py
+++ b/tests/unit/test_utils.py
@@ -248,10 +248,10 @@ def test_rmtree_errorhandler_reraises_error(tmpdir: Path) -> None:
     by the given unreadable directory.
     """
     # Create directory without read permission
-    subdir_path = tmpdir / "subdir"
-    subdir_path.mkdir()
-    path = str(subdir_path)
-    os.chmod(path, stat.S_IWRITE)
+    path = tmpdir / "subdir"
+    path.mkdir()
+    old_mode = path.stat().st_mode
+    path.chmod(stat.S_IWRITE)
 
     mock_func = Mock()
 
@@ -267,6 +267,9 @@ def test_rmtree_errorhandler_reraises_error(tmpdir: Path) -> None:
             rmtree_errorhandler(
                 mock_func, path, sys.exc_info()  # type: ignore[arg-type]
             )
+    finally:
+        # Restore permissions to let pytest to clean up temp dirs
+        path.chmod(old_mode)
 
     mock_func.assert_not_called()
 
diff --git a/tests/unit/test_utils_unpacking.py b/tests/unit/test_utils_unpacking.py
index 3fdd822e7..efccbdccb 100644
--- a/tests/unit/test_utils_unpacking.py
+++ b/tests/unit/test_utils_unpacking.py
@@ -197,6 +197,47 @@ class TestUnpackArchives:
 
         assert "is outside the destination" in str(e.value)
 
+    @pytest.mark.parametrize(
+        ("input_prefix", "unpack_prefix"),
+        [
+            ("", ""),
+            ("dir/", ""),  # pip ignores a common leading directory
+            ("dir/sub/", "sub/"),  # pip ignores *one* common leading directory
+        ],
+    )
+    def test_unpack_tar_links(self, input_prefix: str, unpack_prefix: str) -> None:
+        """
+        Test unpacking a *.tar with file containing hard & soft links
+        """
+        test_tar = os.path.join(self.tempdir, "test_tar_links.tar")
+        content = b"file content"
+        with tarfile.open(test_tar, "w") as mytar:
+            file_tarinfo = tarfile.TarInfo(input_prefix + "regular_file.txt")
+            file_tarinfo.size = len(content)
+            mytar.addfile(file_tarinfo, io.BytesIO(content))
+
+            hardlink_tarinfo = tarfile.TarInfo(input_prefix + "hardlink.txt")
+            hardlink_tarinfo.type = tarfile.LNKTYPE
+            hardlink_tarinfo.linkname = input_prefix + "regular_file.txt"
+            mytar.addfile(hardlink_tarinfo)
+
+            symlink_tarinfo = tarfile.TarInfo(input_prefix + "symlink.txt")
+            symlink_tarinfo.type = tarfile.SYMTYPE
+            symlink_tarinfo.linkname = "regular_file.txt"
+            mytar.addfile(symlink_tarinfo)
+
+        untar_file(test_tar, self.tempdir)
+
+        unpack_dir = os.path.join(self.tempdir, unpack_prefix)
+        with open(os.path.join(unpack_dir, "regular_file.txt"), "rb") as f:
+            assert f.read() == content
+
+        with open(os.path.join(unpack_dir, "hardlink.txt"), "rb") as f:
+            assert f.read() == content
+
+        with open(os.path.join(unpack_dir, "symlink.txt"), "rb") as f:
+            assert f.read() == content
+
 
 def test_unpack_tar_unicode(tmpdir: Path) -> None:
     test_tar = tmpdir / "test.tar"