Repository: pypa/pip
Pull request #12707 (2024-05-17 20:00:26)

Upgrade truststore to 0.9.1
Follow-up from https://github.com/pypa/pip/pull/12662 which fixes the issue discovered after 0.9.0 was vendored (thanks @notatallshaw for reporting that issue!)

sethmlarson: Thank you for the review @uranusjr!

Patch:
diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml
index 4125549ab..dc306b0d6 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.4.7
   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..f06ca7bf9 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
@@ -511,6 +512,7 @@ Miro Hrončok
 Monica Baluna
 montefra
 Monty Taylor
+mrKazzila
 Muha Ajjan
 Nadav Wexler
 Nahuel Ambrosini
@@ -665,6 +667,7 @@ Shantanu
 shenxianpeng
 shireenrao
 Shivansh-007
+Shixian Sheng
 Shlomi Fish
 Shovan Maity
 Simeon Visser
diff --git a/NEWS.rst b/NEWS.rst
index eea12074d..f3baf4b12 100644
--- a/NEWS.rst
+++ b/NEWS.rst
@@ -9,6 +9,33 @@
 
 .. towncrier release notes start
 
+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/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/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/9be41686-c40a-4e08-84e1-936b65b74fc7.trivial.rst b/news/9be41686-c40a-4e08-84e1-936b65b74fc7.trivial.rst
deleted file mode 100644
index e69de29bb..000000000
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/truststore.vendor.rst b/news/truststore.vendor.rst
new file mode 100644
index 000000000..08841296f
--- /dev/null
+++ b/news/truststore.vendor.rst
@@ -0,0 +1,1 @@
+Upgraded truststore to 0.9.1.
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/src/pip/__init__.py b/src/pip/__init__.py
index cf95985fa..0865ecd8a 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.1.dev2"
 
 
 def main(args: Optional[List[str]] = None) -> int:
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/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/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/_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/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/vendor.txt b/src/pip/_vendor/vendor.txt
index 00d81549c..74eb58ae7 100644
--- a/src/pip/_vendor/vendor.txt
+++ b/src/pip/_vendor/vendor.txt
@@ -2,7 +2,7 @@ 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
@@ -16,4 +16,4 @@ 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/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/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 = ""