Repository: pypa/pip
Pull request #13032 (2024-10-18 22:35:33)

Correct typos in docs and code comments
Corrected multiple errors in documentation.


JoshuaPerdue: Resolves issue https://github.com/pypa/pip/issues/13031
JoshuaPerdue: pre-commit.ci autofix

Patch:
diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml
index 3a0319c23..3b3f73e2b 100644
--- a/.pre-commit-config.yaml
+++ b/.pre-commit-config.yaml
@@ -2,7 +2,7 @@ exclude: 'src/pip/_vendor/'
 
 repos:
 - repo: https://github.com/pre-commit/pre-commit-hooks
-  rev: v4.6.0
+  rev: v5.0.0
   hooks:
   - id: check-builtin-literals
   - id: check-added-large-files
@@ -17,18 +17,18 @@ repos:
     exclude: .patch
 
 - repo: https://github.com/psf/black-pre-commit-mirror
-  rev: 24.4.2
+  rev: 24.10.0
   hooks:
   - id: black
 
 - repo: https://github.com/astral-sh/ruff-pre-commit
-  rev: v0.5.6
+  rev: v0.8.2
   hooks:
     - id: ruff
       args: [--fix, --exit-non-zero-on-fix]
 
 - repo: https://github.com/pre-commit/mirrors-mypy
-  rev: v1.12.1
+  rev: v1.13.0
   hooks:
   - id: mypy
     exclude: tests/data
diff --git a/AUTHORS.txt b/AUTHORS.txt
index dda2ac30f..8ccefbc6e 100644
--- a/AUTHORS.txt
+++ b/AUTHORS.txt
@@ -57,6 +57,7 @@ Anthony Sottile
 Antoine Musso
 Anton Ovchinnikov
 Anton Patrushev
+Anton Zelenov
 Antonio Alvarado Hernandez
 Antony Lee
 Antti Kaihola
@@ -225,6 +226,7 @@ Diego Ramirez
 DiegoCaraballo
 Dimitri Merejkowsky
 Dimitri Papadopoulos
+Dimitri Papadopoulos Orfanos
 Dirk Stolle
 Dmitry Gladkov
 Dmitry Volodin
@@ -690,6 +692,7 @@ snook92
 socketubs
 Sorin Sbarnea
 Srinivas Nyayapati
+Srishti Hegde
 Stavros Korokithakis
 Stefan Scherfke
 Stefano Rivera
diff --git a/NEWS.rst b/NEWS.rst
index 397277444..5ebf7141b 100644
--- a/NEWS.rst
+++ b/NEWS.rst
@@ -9,6 +9,45 @@
 
 .. towncrier release notes start
 
+24.3.1 (2024-10-27)
+===================
+
+Bug Fixes
+---------
+
+- Allow multiple nested inclusions of the same requirements file again. (`#13046 <https://github.com/pypa/pip/issues/13046>`_)
+
+24.3 (2024-10-27)
+=================
+
+Deprecations and Removals
+-------------------------
+
+- Deprecate wheel filenames that are not compliant with :pep:`440`. (`#12918 <https://github.com/pypa/pip/issues/12918>`_)
+
+Features
+--------
+
+- Detect recursively referencing requirements files and help users identify
+  the source. (`#12653 <https://github.com/pypa/pip/issues/12653>`_)
+- Support for :pep:`730` iOS wheels. (`#12961 <https://github.com/pypa/pip/issues/12961>`_)
+
+Bug Fixes
+---------
+
+- Display a better error message when an already installed package has an invalid requirement. (`#12953 <https://github.com/pypa/pip/issues/12953>`_)
+- Ignore ``PIP_TARGET`` and ``pip.conf`` ``global.target`` when preparing a build environment. (`#8438 <https://github.com/pypa/pip/issues/8438>`_)
+- Restore support for macOS 10.12 and older (via truststore). (`#12901 <https://github.com/pypa/pip/issues/12901>`_)
+- Allow installing pip in editable mode in a virtual environment on Windows. (`#12666 <https://github.com/pypa/pip/issues/12666>`_)
+
+Vendored Libraries
+------------------
+
+- Upgrade certifi to 2024.8.30
+- Upgrade distlib to 0.3.9
+- Upgrade truststore to 0.10.0
+- Upgrade urllib3 to 1.26.20
+
 24.2 (2024-07-28)
 =================
 
diff --git a/docs/html/development/architecture/command-line-interface.rst b/docs/html/development/architecture/command-line-interface.rst
index 725199bf5..283adc7c1 100644
--- a/docs/html/development/architecture/command-line-interface.rst
+++ b/docs/html/development/architecture/command-line-interface.rst
@@ -154,7 +154,7 @@ Its main addition consists of the following function:
 
   .. py:method:: get_default_values()
 
-    Overrides the original method to allow updating the defaults ater the instantiation of the
+    Overrides the original method to allow updating the defaults after the instantiation of the
     option parser.
 
 It allows overriding the default options and arguments using the ``Configuration`` class
diff --git a/docs/html/development/architecture/overview.rst b/docs/html/development/architecture/overview.rst
index 63445754a..a003d9a9e 100644
--- a/docs/html/development/architecture/overview.rst
+++ b/docs/html/development/architecture/overview.rst
@@ -134,7 +134,7 @@ Once it has those, it selects one file and downloads it.
 cannot….should not be …. ? I want only the Flask …. Why am I getting the
 whole list?
 
-Answer: It's not every file, just files of Flask. No API for getting alllllll
+Answer: It's not every file, just files of Flask. No API for getting all
 files on PyPI. It’s for getting all files of Flask.)
 
 .. _`tracking issue`: https://github.com/pypa/pip/issues/6831
diff --git a/docs/html/ux-research-design/research-results/improving-pips-documentation.md b/docs/html/ux-research-design/research-results/improving-pips-documentation.md
index 765a8f106..d7f547668 100644
--- a/docs/html/ux-research-design/research-results/improving-pips-documentation.md
+++ b/docs/html/ux-research-design/research-results/improving-pips-documentation.md
@@ -512,7 +512,7 @@ _Suggested content:_
 _Page purpose:_
 
 - To onboard people who want to contribute to pip's docs
-- To share previous research and recommendataions related to pip's docs
+- To share previous research and recommendations related to pip's docs
 
 _Suggested content:_
 
diff --git a/docs/html/ux-research-design/research-results/personas.md b/docs/html/ux-research-design/research-results/personas.md
index e4f1a3df4..afdac02a3 100644
--- a/docs/html/ux-research-design/research-results/personas.md
+++ b/docs/html/ux-research-design/research-results/personas.md
@@ -2,7 +2,7 @@
 
 ## Problem
 
-We want to develop personas for pip's user to facilate faster user-centered decision making for the pip development team.
+We want to develop personas for pip's user to facilitate faster user-centered decision making for the pip development team.
 
 [Skip to recommendations](#recommendations)
 
diff --git a/docs/html/ux-research-design/research-results/pip-force-reinstall.md b/docs/html/ux-research-design/research-results/pip-force-reinstall.md
index 4ba84b876..2f3e2339c 100644
--- a/docs/html/ux-research-design/research-results/pip-force-reinstall.md
+++ b/docs/html/ux-research-design/research-results/pip-force-reinstall.md
@@ -66,7 +66,7 @@ Most respondents use `--force-reinstall` "almost never" (65.6%):
 ![screenshot of survey question of how often users use --force-reinstall](https://i.imgur.com/fjLQUPV.png)
 ![bar chart of how often users use --force-reinstall](https://i.imgur.com/Xe1XDkI.png)
 
-Amongst respondents who said they use `--force-resinstall` often or very often:
+Amongst respondents who said they use `--force-reinstall` often or very often:
 
 - 54.54% (6/11) of respondents thought that pip should install the same version of requests - i.e. that `--force-reinstall` should _not_ implicitly upgrade
 - 45.45% (5/11) of respondents thought that pip should upgrade requests to the latest version - i.e that `--force-reinstall` _should_ implicitly upgrade
@@ -76,7 +76,7 @@ Respondents find `--force-reinstall` less useful than useful:
 ![screenshot of survey question of how useful users find --force-reinstall](https://i.imgur.com/6cv4lFn.png)
 ![bar chart of how useful users find --force-reinstall](https://i.imgur.com/gMUBDBo.png)
 
-Amongst respondents who said they find `--force-resinstall` useful or very useful:
+Amongst respondents who said they find `--force-reinstall` useful or very useful:
 
 - 38.46% (20/52) of respondents thought that pip should install the same version of requests - i.e. that `--force-reinstall` should _not_ implicitly upgrade
 - 50% (26/52) of respondents thought that pip should upgrade requests to the latest version - i.e that `--force-reinstall` _should_ implicitly upgrade
@@ -89,7 +89,7 @@ In this case, we recommend showing the following message when a user tries to us
 
 > Error: the pip install --force-reinstall option no longer exists. Use pip uninstall then pip install to replace up-to-date packages, or pip install --upgrade to update your packages to the latest available versions.
 
-Should the pip development team wish to keep `--force-resintall`, we recommend maintaining the current (implicit upgrade) behaviour, as pip's users have not expressed a clear preference for a different behaviour.
+Should the pip development team wish to keep `--force-reinstall`, we recommend maintaining the current (implicit upgrade) behaviour, as pip's users have not expressed a clear preference for a different behaviour.
 
 In this case, we recommend upgrading the [help text](https://pip.pypa.io/en/stable/reference/pip_install/#cmdoption-force-reinstall) to be more explicit:
 
diff --git a/docs/html/ux-research-design/research-results/prioritizing-features.md b/docs/html/ux-research-design/research-results/prioritizing-features.md
index 3642042f3..4e0d0250c 100644
--- a/docs/html/ux-research-design/research-results/prioritizing-features.md
+++ b/docs/html/ux-research-design/research-results/prioritizing-features.md
@@ -109,9 +109,9 @@ Results varied by the amount of Python experience the user had.
 
 ![Screenshot of Install a package from wheels](https://i.imgur.com/9DMBfNL.png)
 
-#### Install apackage from a local directory
+#### Install a package from a local directory
 
-![Screenshot of Install apackage from a local directory](https://i.imgur.com/Jp95rak.png)
+![Screenshot of Install a package from a local directory](https://i.imgur.com/Jp95rak.png)
 
 #### Control where you want your installed package to live on your computer
 
diff --git a/docs/html/ux-research-design/research-results/users-and-security.md b/docs/html/ux-research-design/research-results/users-and-security.md
index fbc8f492f..8e3a12d96 100644
--- a/docs/html/ux-research-design/research-results/users-and-security.md
+++ b/docs/html/ux-research-design/research-results/users-and-security.md
@@ -56,6 +56,8 @@ Both of these groups identified their "sphere of influence" and did their best t
 
 ### User thoughts about security
 
+Selected quotes from research participants
+
 #### Responsibility as author
 
 Participants who spent a lot of their time writing Python code - either for community or as part of their job - expressed a responsibility to their users for the code they wrote - people who wrote code which was made public expressed a stronger responsibility.
@@ -76,8 +78,7 @@ Participants also explained they rely on code security scanning and checking sof
 
 #### Reliance on good software development practices
 
-A small number of participants e### Selected quotes from research participants
-xplained they have good software practices in place, which help with writing secure software.
+A small number of participants explained they have good software practices in place, which help with writing secure software.
 
 > "We have a book about ethics of code - we have mandatory certification."
 
diff --git a/news/12653.feature.rst b/news/12653.feature.rst
deleted file mode 100644
index 83e1a46e6..000000000
--- a/news/12653.feature.rst
+++ /dev/null
@@ -1,2 +0,0 @@
-Detect recursively referencing requirements files and help users identify
-the source.
diff --git a/news/12678.trivial.rst b/news/12678.trivial.rst
deleted file mode 100644
index 57f38781b..000000000
--- a/news/12678.trivial.rst
+++ /dev/null
@@ -1,1 +0,0 @@
-Update classifier to support Python 3.13
diff --git a/news/12893.trivial.rst b/news/12893.trivial.rst
deleted file mode 100644
index d0a0de143..000000000
--- a/news/12893.trivial.rst
+++ /dev/null
@@ -1,1 +0,0 @@
-Update ruff in pre-commit to 0.5.6
diff --git a/news/12894.trivial.rst b/news/12894.trivial.rst
deleted file mode 100644
index 51e4c8b03..000000000
--- a/news/12894.trivial.rst
+++ /dev/null
@@ -1,1 +0,0 @@
-Add Ruff's Pytest rules to the tests directory.
diff --git a/news/12895.trivial.rst b/news/12895.trivial.rst
deleted file mode 100644
index ab159a6cc..000000000
--- a/news/12895.trivial.rst
+++ /dev/null
@@ -1,1 +0,0 @@
-Remove IRC link from create issue page.
diff --git a/news/12918.removal.rst b/news/12918.removal.rst
deleted file mode 100644
index c5a52cf66..000000000
--- a/news/12918.removal.rst
+++ /dev/null
@@ -1,1 +0,0 @@
-Deprecate wheel filenames that are not compliant with PEP 440.
diff --git a/news/12939.trivial.rst b/news/12939.trivial.rst
deleted file mode 100644
index c9acd4f8c..000000000
--- a/news/12939.trivial.rst
+++ /dev/null
@@ -1,1 +0,0 @@
-Update non PEP 440 wheel filename deprecation notice.
diff --git a/news/12953.bugfix.rst b/news/12953.bugfix.rst
deleted file mode 100644
index f2d0521b2..000000000
--- a/news/12953.bugfix.rst
+++ /dev/null
@@ -1,1 +0,0 @@
-Display disagnostic error message when already installed package has an invalid requirement.
diff --git a/news/12961.feature.rst b/news/12961.feature.rst
deleted file mode 100644
index e4e982db1..000000000
--- a/news/12961.feature.rst
+++ /dev/null
@@ -1,1 +0,0 @@
-Support for PEP 730 iOS wheels was added.
diff --git a/news/12964.trivial.rst b/news/12964.trivial.rst
deleted file mode 100644
index 1467743ff..000000000
--- a/news/12964.trivial.rst
+++ /dev/null
@@ -1,1 +0,0 @@
-A valid, but non-existent URL used in a test case was corrected to be a valid URL.
diff --git a/news/12974.trivial.rst b/news/12974.trivial.rst
deleted file mode 100644
index d1c31fb17..000000000
--- a/news/12974.trivial.rst
+++ /dev/null
@@ -1,1 +0,0 @@
-Create two new import groups, "vendored" and "import", this only affects tests.
diff --git a/news/13012.trivial.rst b/news/13012.trivial.rst
deleted file mode 100644
index 623523a9e..000000000
--- a/news/13012.trivial.rst
+++ /dev/null
@@ -1,1 +0,0 @@
-Clean up non PEP 440 wheel filename deprecation language.
diff --git a/news/13013.trivial.rst b/news/13013.trivial.rst
deleted file mode 100644
index 75e724363..000000000
--- a/news/13013.trivial.rst
+++ /dev/null
@@ -1,1 +0,0 @@
-Remove InvalidVersion exception catch for parse_{sdist,wheel}_filename.
diff --git a/news/13031.trivial.rst b/news/13031.trivial.rst
new file mode 100644
index 000000000..d765e810e
--- /dev/null
+++ b/news/13031.trivial.rst
@@ -0,0 +1,1 @@
+Correct documentation errors.
diff --git a/news/10EC0CC0-C535-4B04-8924-CBF3DAA3315D.trivial.rst b/news/13072.trivial.rst
similarity index 100%
rename from news/10EC0CC0-C535-4B04-8924-CBF3DAA3315D.trivial.rst
rename to news/13072.trivial.rst
diff --git a/news/13079.bugfix.rst b/news/13079.bugfix.rst
new file mode 100644
index 000000000..5b297f5a1
--- /dev/null
+++ b/news/13079.bugfix.rst
@@ -0,0 +1,1 @@
+This change fixes a security bug allowing a wheel to execute code during installation.
diff --git a/news/8438.bugfix.rst b/news/8438.bugfix.rst
deleted file mode 100644
index cedd5d799..000000000
--- a/news/8438.bugfix.rst
+++ /dev/null
@@ -1,10 +0,0 @@
-If the user's pip.conf includes a target directory setting,
-attempting to install a package in editable mode or from source
-results in a fatal error during the installation of setuptools.
-
-The following assertion triggers:
-assert not (home and prefix), "home={} prefix={}".format(home, prefix)
-
-To avoid this issue when building a package, the target
-setting should be ignored. This can be achieved by passing an empty
-target when installing dependencies in the BuildEnvironment class.
diff --git a/news/F2CF2568-49B1-49EF-8B16-70491B9C8FDA.trivial.rst b/news/F2CF2568-49B1-49EF-8B16-70491B9C8FDA.trivial.rst
deleted file mode 100644
index e69de29bb..000000000
diff --git a/news/5ab8c2b8-cdb9-4b08-9292-24ce5a135e04.trivial.rst b/news/ba0bd1bb-2dcc-43d0-83ff-c762e7e55bf9.trivial.rst
similarity index 100%
rename from news/5ab8c2b8-cdb9-4b08-9292-24ce5a135e04.trivial.rst
rename to news/ba0bd1bb-2dcc-43d0-83ff-c762e7e55bf9.trivial.rst
diff --git a/news/A454782D-6619-44D8-811F-2C1973A430BB.trivial.rst b/news/c33bb4df-d6ab-4d9b-8113-55c27a237dfd.trivial.rst
similarity index 100%
rename from news/A454782D-6619-44D8-811F-2C1973A430BB.trivial.rst
rename to news/c33bb4df-d6ab-4d9b-8113-55c27a237dfd.trivial.rst
diff --git a/news/certifi.vendor.rst b/news/certifi.vendor.rst
deleted file mode 100644
index ad9c985c3..000000000
--- a/news/certifi.vendor.rst
+++ /dev/null
@@ -1,1 +0,0 @@
-Upgrade certifi to 2024.8.30
diff --git a/news/distlib.vendor.rst b/news/distlib.vendor.rst
deleted file mode 100644
index 428113e8b..000000000
--- a/news/distlib.vendor.rst
+++ /dev/null
@@ -1,1 +0,0 @@
-Upgrade distlib to 0.3.9
diff --git a/news/truststore.vendor.rst b/news/truststore.vendor.rst
deleted file mode 100644
index d5d1e1582..000000000
--- a/news/truststore.vendor.rst
+++ /dev/null
@@ -1,1 +0,0 @@
-Upgrade truststore to 0.10.0
diff --git a/news/urllib3.vendor.rst b/news/urllib3.vendor.rst
deleted file mode 100644
index 6b66de2fd..000000000
--- a/news/urllib3.vendor.rst
+++ /dev/null
@@ -1,1 +0,0 @@
-Upgrade urllib3 to 1.26.20
diff --git a/src/pip/__init__.py b/src/pip/__init__.py
index bfc5eb111..4eff4299c 100644
--- a/src/pip/__init__.py
+++ b/src/pip/__init__.py
@@ -1,6 +1,6 @@
 from typing import List, Optional
 
-__version__ = "24.3.dev0"
+__version__ = "25.0.dev0"
 
 
 def main(args: Optional[List[str]] = None) -> int:
diff --git a/src/pip/_internal/cli/progress_bars.py b/src/pip/_internal/cli/progress_bars.py
index 1236180c0..3d9dde8ed 100644
--- a/src/pip/_internal/cli/progress_bars.py
+++ b/src/pip/_internal/cli/progress_bars.py
@@ -63,7 +63,7 @@ def _raw_progress_bar(
     size: Optional[int],
 ) -> Generator[bytes, None, None]:
     def write_progress(current: int, total: int) -> None:
-        sys.stdout.write("Progress %d of %d\n" % (current, total))
+        sys.stdout.write(f"Progress {current} of {total}\n")
         sys.stdout.flush()
 
     current = 0
diff --git a/src/pip/_internal/commands/install.py b/src/pip/_internal/commands/install.py
index ad45a2f2a..70acf202b 100644
--- a/src/pip/_internal/commands/install.py
+++ b/src/pip/_internal/commands/install.py
@@ -10,6 +10,13 @@ from typing import List, Optional
 from pip._vendor.packaging.utils import canonicalize_name
 from pip._vendor.rich import print_json
 
+# Eagerly import self_outdated_check to avoid crashes. Otherwise,
+# this module would be imported *after* pip was replaced, resulting
+# in crashes if the new self_outdated_check module was incompatible
+# with the rest of pip that's already imported, or allowing a
+# wheel to execute arbitrary code on install by replacing
+# self_outdated_check.
+import pip._internal.self_outdated_check  # noqa: F401
 from pip._internal.cache import WheelCache
 from pip._internal.cli import cmdoptions
 from pip._internal.cli.cmdoptions import make_target_python
@@ -408,12 +415,6 @@ class InstallCommand(RequirementCommand):
                 # If we're not replacing an already installed pip,
                 # we're not modifying it.
                 modifying_pip = pip_req.satisfied_by is None
-                if modifying_pip:
-                    # Eagerly import this module to avoid crashes. Otherwise, this
-                    # module would be imported *after* pip was replaced, resulting in
-                    # crashes if the new self_outdated_check module was incompatible
-                    # with the rest of pip that's already imported.
-                    import pip._internal.self_outdated_check  # noqa: F401
             protect_pip_from_modification_on_windows(modifying_pip=modifying_pip)
 
             reqs_to_build = [
diff --git a/src/pip/_internal/configuration.py b/src/pip/_internal/configuration.py
index c25273d5f..ffeda1d47 100644
--- a/src/pip/_internal/configuration.py
+++ b/src/pip/_internal/configuration.py
@@ -330,7 +330,7 @@ class Configuration:
         This should be treated like items of a dictionary. The order
         here doesn't affect what gets overridden. That is controlled
         by OVERRIDE_ORDER. However this does control the order they are
-        displayed to the user. It's probably most ergononmic to display
+        displayed to the user. It's probably most ergonomic to display
         things in the same order as OVERRIDE_ORDER
         """
         # SMELL: Move the conditions out of this function
diff --git a/src/pip/_internal/index/package_finder.py b/src/pip/_internal/index/package_finder.py
index 0d65ce35f..8b89e7682 100644
--- a/src/pip/_internal/index/package_finder.py
+++ b/src/pip/_internal/index/package_finder.py
@@ -334,44 +334,30 @@ class CandidatePreferences:
     allow_all_prereleases: bool = False
 
 
+@dataclass(frozen=True)
 class BestCandidateResult:
     """A collection of candidates, returned by `PackageFinder.find_best_candidate`.
 
     This class is only intended to be instantiated by CandidateEvaluator's
     `compute_best_candidate()` method.
-    """
-
-    def __init__(
-        self,
-        candidates: List[InstallationCandidate],
-        applicable_candidates: List[InstallationCandidate],
-        best_candidate: Optional[InstallationCandidate],
-    ) -> None:
-        """
-        :param candidates: A sequence of all available candidates found.
-        :param applicable_candidates: The applicable candidates.
-        :param best_candidate: The most preferred candidate found, or None
-            if no applicable candidates were found.
-        """
-        assert set(applicable_candidates) <= set(candidates)
-
-        if best_candidate is None:
-            assert not applicable_candidates
-        else:
-            assert best_candidate in applicable_candidates
 
-        self._applicable_candidates = applicable_candidates
-        self._candidates = candidates
+    :param all_candidates: A sequence of all available candidates found.
+    :param applicable_candidates: The applicable candidates.
+    :param best_candidate: The most preferred candidate found, or None
+        if no applicable candidates were found.
+    """
 
-        self.best_candidate = best_candidate
+    all_candidates: List[InstallationCandidate]
+    applicable_candidates: List[InstallationCandidate]
+    best_candidate: Optional[InstallationCandidate]
 
-    def iter_all(self) -> Iterable[InstallationCandidate]:
-        """Iterate through all candidates."""
-        return iter(self._candidates)
+    def __post_init__(self) -> None:
+        assert set(self.applicable_candidates) <= set(self.all_candidates)
 
-    def iter_applicable(self) -> Iterable[InstallationCandidate]:
-        """Iterate through the applicable candidates."""
-        return iter(self._applicable_candidates)
+        if self.best_candidate is None:
+            assert not self.applicable_candidates
+        else:
+            assert self.best_candidate in self.applicable_candidates
 
 
 class CandidateEvaluator:
@@ -929,7 +915,7 @@ class PackageFinder:
                 "Could not find a version that satisfies the requirement %s "
                 "(from versions: %s)",
                 req,
-                _format_versions(best_candidate_result.iter_all()),
+                _format_versions(best_candidate_result.all_candidates),
             )
 
             raise DistributionNotFound(f"No matching distribution found for {req}")
@@ -963,7 +949,7 @@ class PackageFinder:
             logger.debug(
                 "Using version %s (newest of versions: %s)",
                 best_candidate.version,
-                _format_versions(best_candidate_result.iter_applicable()),
+                _format_versions(best_candidate_result.applicable_candidates),
             )
             return best_candidate
 
@@ -971,7 +957,7 @@ class PackageFinder:
         logger.debug(
             "Installed version (%s) is most up-to-date (past versions: %s)",
             installed_version,
-            _format_versions(best_candidate_result.iter_applicable()),
+            _format_versions(best_candidate_result.applicable_candidates),
         )
         raise BestVersionAlreadyInstalled
 
diff --git a/src/pip/_internal/metadata/__init__.py b/src/pip/_internal/metadata/__init__.py
index aa232b6ca..1ea1e7fd2 100644
--- a/src/pip/_internal/metadata/__init__.py
+++ b/src/pip/_internal/metadata/__init__.py
@@ -30,7 +30,7 @@ def _should_use_importlib_metadata() -> bool:
     """Whether to use the ``importlib.metadata`` or ``pkg_resources`` backend.
 
     By default, pip uses ``importlib.metadata`` on Python 3.11+, and
-    ``pkg_resourcess`` otherwise. This can be overridden by a couple of ways:
+    ``pkg_resources`` otherwise. This can be overridden by a couple of ways:
 
     * If environment variable ``_PIP_USE_IMPORTLIB_METADATA`` is set, it
       dictates whether ``importlib.metadata`` is used, regardless of Python
@@ -71,7 +71,7 @@ def get_default_environment() -> BaseEnvironment:
 
     This returns an Environment instance from the chosen backend. The default
     Environment instance should be built from ``sys.path`` and may use caching
-    to share instance state accorss calls.
+    to share instance state across calls.
     """
     return select_backend().Environment.default()
 
diff --git a/src/pip/_internal/metadata/importlib/_dists.py b/src/pip/_internal/metadata/importlib/_dists.py
index 36cd32623..5e92b1275 100644
--- a/src/pip/_internal/metadata/importlib/_dists.py
+++ b/src/pip/_internal/metadata/importlib/_dists.py
@@ -190,7 +190,7 @@ class Distribution(BaseDistribution):
         return content
 
     def iter_entry_points(self) -> Iterable[BaseEntryPoint]:
-        # importlib.metadata's EntryPoint structure sasitfies BaseEntryPoint.
+        # importlib.metadata's EntryPoint structure satisfies BaseEntryPoint.
         return self._dist.entry_points
 
     def _metadata_impl(self) -> email.message.Message:
diff --git a/src/pip/_internal/operations/freeze.py b/src/pip/_internal/operations/freeze.py
index bb1039fb7..ae5dd37f9 100644
--- a/src/pip/_internal/operations/freeze.py
+++ b/src/pip/_internal/operations/freeze.py
@@ -1,9 +1,10 @@
 import collections
 import logging
 import os
+from dataclasses import dataclass, field
 from typing import Container, Dict, Generator, Iterable, List, NamedTuple, Optional, Set
 
-from pip._vendor.packaging.utils import canonicalize_name
+from pip._vendor.packaging.utils import NormalizedName, canonicalize_name
 from pip._vendor.packaging.version import InvalidVersion
 
 from pip._internal.exceptions import BadCommand, InstallationError
@@ -220,19 +221,16 @@ def _get_editable_info(dist: BaseDistribution) -> _EditableInfo:
     )
 
 
+@dataclass(frozen=True)
 class FrozenRequirement:
-    def __init__(
-        self,
-        name: str,
-        req: str,
-        editable: bool,
-        comments: Iterable[str] = (),
-    ) -> None:
-        self.name = name
-        self.canonical_name = canonicalize_name(name)
-        self.req = req
-        self.editable = editable
-        self.comments = comments
+    name: str
+    req: str
+    editable: bool
+    comments: Iterable[str] = field(default_factory=tuple)
+
+    @property
+    def canonical_name(self) -> NormalizedName:
+        return canonicalize_name(self.name)
 
     @classmethod
     def from_dist(cls, dist: BaseDistribution) -> "FrozenRequirement":
diff --git a/src/pip/_internal/pyproject.py b/src/pip/_internal/pyproject.py
index 2a9cad480..0e8452f39 100644
--- a/src/pip/_internal/pyproject.py
+++ b/src/pip/_internal/pyproject.py
@@ -73,7 +73,7 @@ def load_pyproject_toml(
         build_system = None
 
     # The following cases must use PEP 517
-    # We check for use_pep517 being non-None and falsey because that means
+    # We check for use_pep517 being non-None and falsy because that means
     # the user explicitly requested --no-use-pep517.  The value 0 as
     # opposed to False can occur when the value is provided via an
     # environment variable or config file option (due to the quirk of
diff --git a/src/pip/_internal/req/req_file.py b/src/pip/_internal/req/req_file.py
index 55c0726f3..dee7f2fe8 100644
--- a/src/pip/_internal/req/req_file.py
+++ b/src/pip/_internal/req/req_file.py
@@ -8,6 +8,7 @@ import os
 import re
 import shlex
 import urllib.parse
+from dataclasses import dataclass
 from optparse import Values
 from typing import (
     TYPE_CHECKING,
@@ -84,49 +85,48 @@ SUPPORTED_OPTIONS_EDITABLE_REQ_DEST = [
 logger = logging.getLogger(__name__)
 
 
+@dataclass(frozen=True)
 class ParsedRequirement:
-    def __init__(
-        self,
-        requirement: str,
-        is_editable: bool,
-        comes_from: str,
-        constraint: bool,
-        options: Optional[Dict[str, Any]] = None,
-        line_source: Optional[str] = None,
-    ) -> None:
-        self.requirement = requirement
-        self.is_editable = is_editable
-        self.comes_from = comes_from
-        self.options = options
-        self.constraint = constraint
-        self.line_source = line_source
+    # TODO: replace this with slots=True when dropping Python 3.9 support.
+    __slots__ = (
+        "requirement",
+        "is_editable",
+        "comes_from",
+        "constraint",
+        "options",
+        "line_source",
+    )
 
+    requirement: str
+    is_editable: bool
+    comes_from: str
+    constraint: bool
+    options: Optional[Dict[str, Any]]
+    line_source: Optional[str]
 
+
+@dataclass(frozen=True)
 class ParsedLine:
-    def __init__(
-        self,
-        filename: str,
-        lineno: int,
-        args: str,
-        opts: Values,
-        constraint: bool,
-    ) -> None:
-        self.filename = filename
-        self.lineno = lineno
-        self.opts = opts
-        self.constraint = constraint
-
-        if args:
-            self.is_requirement = True
-            self.is_editable = False
-            self.requirement = args
-        elif opts.editables:
-            self.is_requirement = True
-            self.is_editable = True
+    __slots__ = ("filename", "lineno", "args", "opts", "constraint")
+
+    filename: str
+    lineno: int
+    args: str
+    opts: Values
+    constraint: bool
+
+    @property
+    def is_editable(self) -> bool:
+        return bool(self.opts.editables)
+
+    @property
+    def requirement(self) -> Optional[str]:
+        if self.args:
+            return self.args
+        elif self.is_editable:
             # We don't support multiple -e on one line
-            self.requirement = opts.editables[0]
-        else:
-            self.is_requirement = False
+            return self.opts.editables[0]
+        return None
 
 
 def parse_requirements(
@@ -179,7 +179,7 @@ def handle_requirement_line(
         line.lineno,
     )
 
-    assert line.is_requirement
+    assert line.requirement is not None
 
     # get the options that apply to requirements
     if line.is_editable:
@@ -301,7 +301,7 @@ def handle_line(
     affect the finder.
     """
 
-    if line.is_requirement:
+    if line.requirement is not None:
         parsed_req = handle_requirement_line(line, options)
         return parsed_req
     else:
@@ -324,22 +324,23 @@ class RequirementsFileParser:
     ) -> None:
         self._session = session
         self._line_parser = line_parser
-        self._parsed_files: dict[str, Optional[str]] = {}
 
     def parse(
         self, filename: str, constraint: bool
     ) -> Generator[ParsedLine, None, None]:
         """Parse a given file, yielding parsed lines."""
-        self._parsed_files[os.path.abspath(filename)] = (
-            None  # The primary requirements file passed
+        yield from self._parse_and_recurse(
+            filename, constraint, [{os.path.abspath(filename): None}]
         )
-        yield from self._parse_and_recurse(filename, constraint)
 
     def _parse_and_recurse(
-        self, filename: str, constraint: bool
+        self,
+        filename: str,
+        constraint: bool,
+        parsed_files_stack: List[Dict[str, Optional[str]]],
     ) -> Generator[ParsedLine, None, None]:
         for line in self._parse_file(filename, constraint):
-            if not line.is_requirement and (
+            if line.requirement is None and (
                 line.opts.requirements or line.opts.constraints
             ):
                 # parse a nested requirements file
@@ -364,8 +365,9 @@ class RequirementsFileParser:
                             req_path,
                         )
                     )
-                if req_path in self._parsed_files:
-                    initial_file = self._parsed_files[req_path]
+                parsed_files = parsed_files_stack[0]
+                if req_path in parsed_files:
+                    initial_file = parsed_files[req_path]
                     tail = (
                         f" and again in {initial_file}"
                         if initial_file is not None
@@ -375,8 +377,11 @@ class RequirementsFileParser:
                         f"{req_path} recursively references itself in {filename}{tail}"
                     )
                 # Keeping a track where was each file first included in
-                self._parsed_files[req_path] = filename
-                yield from self._parse_and_recurse(req_path, nested_constraint)
+                new_parsed_files = parsed_files.copy()
+                new_parsed_files[req_path] = filename
+                yield from self._parse_and_recurse(
+                    req_path, nested_constraint, [new_parsed_files, *parsed_files_stack]
+                )
             else:
                 yield line
 
diff --git a/src/pip/_internal/resolution/resolvelib/factory.py b/src/pip/_internal/resolution/resolvelib/factory.py
index dc6e2e12e..6c273eb88 100644
--- a/src/pip/_internal/resolution/resolvelib/factory.py
+++ b/src/pip/_internal/resolution/resolvelib/factory.py
@@ -309,7 +309,7 @@ class Factory:
                 specifier=specifier,
                 hashes=hashes,
             )
-            icans = list(result.iter_applicable())
+            icans = result.applicable_candidates
 
             # PEP 592: Yanked releases are ignored unless the specifier
             # explicitly pins a version (via '==' or '===') that can be
diff --git a/src/pip/_internal/utils/logging.py b/src/pip/_internal/utils/logging.py
index 41f6eb51a..62035fc40 100644
--- a/src/pip/_internal/utils/logging.py
+++ b/src/pip/_internal/utils/logging.py
@@ -137,12 +137,19 @@ class IndentedRenderable:
             yield Segment("\n")
 
 
+class PipConsole(Console):
+    def on_broken_pipe(self) -> None:
+        # Reraise the original exception, rich 13.8.0+ exits by default
+        # instead, preventing our handler from firing.
+        raise BrokenPipeError() from None
+
+
 class RichPipStreamHandler(RichHandler):
     KEYWORDS: ClassVar[Optional[List[str]]] = []
 
     def __init__(self, stream: Optional[TextIO], no_color: bool) -> None:
         super().__init__(
-            console=Console(file=stream, no_color=no_color, soft_wrap=True),
+            console=PipConsole(file=stream, no_color=no_color, soft_wrap=True),
             show_time=False,
             show_level=False,
             show_path=False,


Test patch:
diff --git a/tests/lib/__init__.py b/tests/lib/__init__.py
index df142ba44..44fa4053b 100644
--- a/tests/lib/__init__.py
+++ b/tests/lib/__init__.py
@@ -29,6 +29,7 @@ from typing import (
     cast,
 )
 from urllib.parse import urlparse, urlunparse
+from urllib.request import pathname2url
 from zipfile import ZipFile
 
 import pytest
@@ -1379,6 +1380,10 @@ class ScriptFactory(Protocol):
 
 CertFactory = Callable[[], str]
 
+# -------------------------------------------------------------------------
+# Accommodations for Windows path and URL changes in recent Python releases
+# -------------------------------------------------------------------------
+
 # 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"
@@ -1394,3 +1399,15 @@ skip_needs_old_urlun_behavior_win = pytest.mark.skipif(
     sys.platform != "win32" or has_new_urlun_behavior,
     reason="testing windows behavior for older CPython",
 )
+
+# Trailing slashes are now preserved on Windows, matching POSIX behaviour.
+# BPO: https://github.com/python/cpython/issues/126212
+does_pathname2url_preserve_trailing_slash = pathname2url("C:/foo/").endswith("/")
+skip_needs_new_pathname2url_trailing_slash_behavior_win = pytest.mark.skipif(
+    sys.platform != "win32" or not does_pathname2url_preserve_trailing_slash,
+    reason="testing windows (pathname2url) behavior for newer CPython",
+)
+skip_needs_old_pathname2url_trailing_slash_behavior_win = pytest.mark.skipif(
+    sys.platform != "win32" or does_pathname2url_preserve_trailing_slash,
+    reason="testing windows (pathname2url) behavior for older CPython",
+)
diff --git a/tests/unit/test_collector.py b/tests/unit/test_collector.py
index c527d5610..c7760a238 100644
--- a/tests/unit/test_collector.py
+++ b/tests/unit/test_collector.py
@@ -40,7 +40,9 @@ from pip._internal.network.session import PipSession
 from tests.lib import (
     TestData,
     make_test_link_collector,
+    skip_needs_new_pathname2url_trailing_slash_behavior_win,
     skip_needs_new_urlun_behavior_win,
+    skip_needs_old_pathname2url_trailing_slash_behavior_win,
     skip_needs_old_urlun_behavior_win,
 )
 
@@ -390,12 +392,26 @@ 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=skip_needs_old_urlun_behavior_win,
+            marks=[
+                skip_needs_old_urlun_behavior_win,
+                skip_needs_old_pathname2url_trailing_slash_behavior_win,
+            ],
         ),
         pytest.param(
             "file:///T:/path/with spaces/",
             "file://///T:/path/with%20spaces",
-            marks=skip_needs_new_urlun_behavior_win,
+            marks=[
+                skip_needs_new_urlun_behavior_win,
+                skip_needs_old_pathname2url_trailing_slash_behavior_win,
+            ],
+        ),
+        pytest.param(
+            "file:///T:/path/with spaces/",
+            "file://///T:/path/with%20spaces/",
+            marks=[
+                skip_needs_new_urlun_behavior_win,
+                skip_needs_new_pathname2url_trailing_slash_behavior_win,
+            ],
         ),
         # URL with Windows drive letter, running on non-windows
         # platform. The `:` after the drive should be quoted.
diff --git a/tests/unit/test_index.py b/tests/unit/test_index.py
index 8faa4a56a..cbbca0381 100644
--- a/tests/unit/test_index.py
+++ b/tests/unit/test_index.py
@@ -467,13 +467,13 @@ class TestCandidateEvaluator:
         )
         result = evaluator.compute_best_candidate(candidates)
 
-        assert result._candidates == candidates
+        assert result.all_candidates == candidates
         expected_applicable = candidates[:2]
         assert [str(c.version) for c in expected_applicable] == [
             "1.10",
             "1.11",
         ]
-        assert result._applicable_candidates == expected_applicable
+        assert result.applicable_candidates == expected_applicable
 
         assert result.best_candidate is expected_applicable[1]
 
@@ -490,8 +490,8 @@ class TestCandidateEvaluator:
         )
         result = evaluator.compute_best_candidate(candidates)
 
-        assert result._candidates == candidates
-        assert result._applicable_candidates == []
+        assert result.all_candidates == candidates
+        assert result.applicable_candidates == []
         assert result.best_candidate is None
 
     @pytest.mark.parametrize(
diff --git a/tests/unit/test_req_file.py b/tests/unit/test_req_file.py
index 7593e55d6..1cc030681 100644
--- a/tests/unit/test_req_file.py
+++ b/tests/unit/test_req_file.py
@@ -347,6 +347,19 @@ class TestProcessLine:
         assert reqs[0].name == req_name
         assert reqs[0].constraint
 
+    def test_repeated_requirement_files(
+        self, tmp_path: Path, session: PipSession
+    ) -> None:
+        # Test that the same requirements file can be included multiple times
+        # as long as there is no recursion. https://github.com/pypa/pip/issues/13046
+        tmp_path.joinpath("a.txt").write_text("requests")
+        tmp_path.joinpath("b.txt").write_text("-r a.txt")
+        tmp_path.joinpath("c.txt").write_text("-r a.txt\n-r b.txt")
+        parsed = parse_requirements(
+            filename=os.fspath(tmp_path.joinpath("c.txt")), session=session
+        )
+        assert [r.requirement for r in parsed] == ["requests", "requests"]
+
     def test_recursive_requirements_file(
         self, tmpdir: Path, session: PipSession
     ) -> None:
diff --git a/tests/unit/test_urls.py b/tests/unit/test_urls.py
index 539bf091f..0c1452550 100644
--- a/tests/unit/test_urls.py
+++ b/tests/unit/test_urls.py
@@ -6,11 +6,6 @@ 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'")
 def test_path_to_url_unix() -> None:
@@ -25,23 +20,22 @@ def test_path_to_url_unix() -> None:
     [
         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_unc_path_to_url_win() -> None:
+    # The two and four slash forms are both acceptable for our purposes. CPython's
+    # behaviour has changed several times here, so blindly accept either.
+    # - https://github.com/python/cpython/issues/78457
+    # - https://github.com/python/cpython/issues/126205
+    url = path_to_url(r"\\unc\as\path")
+    assert url in ["file://unc/as/path", "file:////unc/as/path"]
+
+
 @pytest.mark.skipif("sys.platform != 'win32'")
 def test_relative_path_to_url_win() -> None:
     resolved_path = os.path.join(os.getcwd(), "file")