Repository: pypa/pip

Issue:
packaging does not expect 'extra' in marker (need vendor upgrade)
### Description

Hi, I've been doing some hacks on pip recently, like taking extra requirements into account when `checking_install_conflicts`. 
I have rewritten the `method` of `pkg_resources.Distribution`, and every time I get the dependency, I will return the extra mim requirements.
Here are the sample code:

```python3
from contextlib import contextmanager
from pip._internal.commands import create_command


@contextmanager
def patch_pkg_resources_distribution() -> Generator:
    from pip._vendor.pkg_resources import Distribution, parse_requirements

    origin_requires = Distribution.requires

    def patched_requires(self, extras=()):
        deps = origin_requires(self, extras)
        if 'mim' in self.extras:
            mim_extra_requires = origin_requires(self, ('mim',))
            filter_invalid_marker(mim_extra_requires)
            deps += mim_extra_requires
        return deps

    Distribution.requires = patched_requires  # type: ignore
    yield

    Distribution.requires = origin_requires  # type: ignore
    return


install_args = [...]
with patch_pkg_resources_distribution():
    create_command('install')(install_args)
```

I got this error during `check_install_conflicts`:

```
ERROR: Error while checking for conflicts. Please file an issue on pip's issue tracker: https://github.com/pypa/pip/issues/new
Traceback (most recent call last):
  File "/mnt/data/xiaoyancong/miniconda3/envs/open-mmlab/lib/python3.7/site-packages/pip/_internal/commands/install.py", line 531, in _determine_conflicts
    return check_install_conflicts(to_install)
  File "/mnt/data/xiaoyancong/miniconda3/envs/open-mmlab/lib/python3.7/site-packages/pip/_internal/operations/check.py", line 111, in check_install_conflicts
    package_set, should_ignore=lambda name: name not in whitelist
  File "/mnt/data/xiaoyancong/miniconda3/envs/open-mmlab/lib/python3.7/site-packages/pip/_internal/operations/check.py", line 78, in check_package_set
    missed = req.marker.evaluate()
  File "/mnt/data/xiaoyancong/miniconda3/envs/open-mmlab/lib/python3.7/site-packages/pip/_vendor/packaging/markers.py", line 304, in evaluate
    return _evaluate_markers(self._markers, current_environment)
  File "/mnt/data/xiaoyancong/miniconda3/envs/open-mmlab/lib/python3.7/site-packages/pip/_vendor/packaging/markers.py", line 234, in _evaluate_markers
    lhs_value = _get_env(environment, lhs.value)
  File "/mnt/data/xiaoyancong/miniconda3/envs/open-mmlab/lib/python3.7/site-packages/pip/_vendor/packaging/markers.py", line 216, in _get_env
    f"{name!r} does not exist in evaluation environment."
pip._vendor.packaging.markers.UndefinedEnvironmentName: 'extra' does not exist in evaluation environment.
```

The reason for the above error is that this [code](https://github.com/pypa/pip/blob/22.1.2/src/pip/_internal/operations/check.py#L78) does not consider that the marker of the requirement obtained from METADATA may contain extras. Is this behavior a bug?

A sample METADTA that contains Requires-Dist:
```
...
Requires-Dist: matplotlib
Requires-Dist: mmcls (>=0.20.1)
Requires-Dist: numpy
Requires-Dist: packaging
Requires-Dist: prettytable
Provides-Extra: all
Requires-Dist: cityscapesscripts ; extra == 'all'
Requires-Dist: matplotlib ; extra == 'all'
Requires-Dist: mmcls (>=0.20.1) ; extra == 'all'
Requires-Dist: numpy ; extra == 'all'
Requires-Dist: packaging ; extra == 'all'
Requires-Dist: prettytable ; extra == 'all'
Requires-Dist: codecov ; extra == 'all'
Requires-Dist: flake8 ; extra == 'all'
Requires-Dist: interrogate ; extra == 'all'
Requires-Dist: pytest ; extra == 'all'
Requires-Dist: xdoctest (>=0.10.0) ; extra == 'all'
Requires-Dist: yapf ; extra == 'all'
Provides-Extra: build
Provides-Extra: mim
Requires-Dist: mmcls (>=0.20.1) ; extra == 'mim'
Requires-Dist: mmcv-full (<=1.6.0,>=1.4.4) ; extra == 'mim'
Provides-Extra: optional
Requires-Dist: cityscapesscripts ; extra == 'optional'
Provides-Extra: tests
Requires-Dist: codecov ; extra == 'tests'
Requires-Dist: flake8 ; extra == 'tests'
Requires-Dist: interrogate ; extra == 'tests'
Requires-Dist: pytest ; extra == 'tests'
Requires-Dist: xdoctest (>=0.10.0) ; extra == 'tests'
Requires-Dist: yapf ; extra == 'tests'
...
```

### Expected behavior

The requirement parsed from METADATA should consider the invalid situation of the marker? 
For example, in [src/pip/_internal/operations/check.py#L78](https://github.com/pypa/pip/blob/22.1.2/src/pip/_internal/operations/check.py#L78) we can handle the invalid marker?

```python3
...
for req in package_detail.dependencies:
    name = canonicalize_name(req.name)
    
    # Check if it's missing
    if name not in package_set:
        missed = True
        if req.marker is not None:
            try:
                missed = req.marker.evaluate()
            except:
                missed = False
            if missed:
                missing_deps.add((name, req))
        continue
    ....

```

### pip version

22.1.2

### Python version

3.7

### OS

ubuntu

### How to Reproduce

See above

### Output

```sh-session
see above
```


### Code of Conduct

- [x] I agree to follow the [PSF Code of Conduct](https://www.python.org/psf/conduct/).

Discussion:

Patch:
diff --git a/.readthedocs-custom-redirects.yml b/.readthedocs-custom-redirects.yml
index d0c072fbf..46c1f819c 100644
--- a/.readthedocs-custom-redirects.yml
+++ b/.readthedocs-custom-redirects.yml
@@ -2,9 +2,9 @@
 # It is related to Read the Docs, but is not a file processed by the platform.
 
 /dev/news-entry-failure: >-
-  https://pip.pypa.io/en/latest/development/contributing/#news-entries
+  https://pip.pypa.io/en/stable/development/contributing/#news-entries
 /errors/resolution-impossible: >-
-  https://pip.pypa.io/en/stable/topics/dependency-resolution/#dealing-with-dependency-conflicts
+  https://pip.pypa.io/en/latest/topics/dependency-resolution/#dealing-with-dependency-conflicts
 /surveys/backtracking: >-
   https://forms.gle/LkZP95S4CfqBAU1N6
 /warnings/backtracking: >-
diff --git a/docs/html/topics/local-project-installs.md b/docs/html/topics/local-project-installs.md
index bd70dbe2f..151035b00 100644
--- a/docs/html/topics/local-project-installs.md
+++ b/docs/html/topics/local-project-installs.md
@@ -34,7 +34,7 @@ Editable installs allow you to install your project without copying any files. I
 With an editable install, you only need to perform a re-installation if you change the project metadata (eg: version, what scripts need to be generated etc). You will still need to run build commands when you need to perform a compilation for non-Python code in the project (eg: C extensions).
 
 ```{caution}
-It is possible to see behaviour differences between regular installs vs editable installs. These differences depend on the build-backend, and you should check the build-backend documentation for the details. In case you distribute the project as a "distribution package", users will see the behaviour of regular installs -- thus, it is important to ensure that regular installs work correctly.
+It is possible to see behaviour differences between regular installs vs editable installs. In case you distribute the project as a "distribution package", users will see the behaviour of regular installs -- thus, it is important to ensure that regular installs work correctly.
 ```
 
 ```{note}
diff --git a/news/11527.bugfix.rst b/news/11527.bugfix.rst
deleted file mode 100644
index 0185a804f..000000000
--- a/news/11527.bugfix.rst
+++ /dev/null
@@ -1,2 +0,0 @@
-Wheel cache behavior is restored to match previous versions, allowing the
-cache to find existing entries.
diff --git a/news/11623.bugfix.rst b/news/11623.bugfix.rst
deleted file mode 100644
index 45b8fe192..000000000
--- a/news/11623.bugfix.rst
+++ /dev/null
@@ -1,1 +0,0 @@
-Fix scripts path in isolated build environment on Debian.
diff --git a/src/pip/_internal/build_env.py b/src/pip/_internal/build_env.py
index 24bfa870b..e67b868e8 100644
--- a/src/pip/_internal/build_env.py
+++ b/src/pip/_internal/build_env.py
@@ -8,6 +8,7 @@ import site
 import sys
 import textwrap
 from collections import OrderedDict
+from sysconfig import get_paths
 from types import TracebackType
 from typing import TYPE_CHECKING, Iterable, List, Optional, Set, Tuple, Type
 
@@ -18,7 +19,6 @@ from pip._vendor.packaging.version import Version
 from pip import __file__ as pip_location
 from pip._internal.cli.spinners import open_spinner
 from pip._internal.locations import (
-    get_isolated_environment_bin_path,
     get_isolated_environment_lib_paths,
     get_platlib,
     get_purelib,
@@ -37,7 +37,10 @@ class _Prefix:
     def __init__(self, path: str) -> None:
         self.path = path
         self.setup = False
-        self.bin_dir = get_isolated_environment_bin_path(path)
+        self.bin_dir = get_paths(
+            "nt" if os.name == "nt" else "posix_prefix",
+            vars={"base": path, "platbase": path},
+        )["scripts"]
         self.lib_dirs = get_isolated_environment_lib_paths(path)
 
 
diff --git a/src/pip/_internal/index/package_finder.py b/src/pip/_internal/index/package_finder.py
index b6f8d57e8..9bf247f02 100644
--- a/src/pip/_internal/index/package_finder.py
+++ b/src/pip/_internal/index/package_finder.py
@@ -1,11 +1,14 @@
 """Routines related to PyPI, indexes"""
 
+# The following comment should be removed at some point in the future.
+# mypy: strict-optional=False
+
 import enum
 import functools
 import itertools
 import logging
 import re
-from typing import TYPE_CHECKING, FrozenSet, Iterable, List, Optional, Set, Tuple, Union
+from typing import FrozenSet, Iterable, List, Optional, Set, Tuple, Union
 
 from pip._vendor.packaging import specifiers
 from pip._vendor.packaging.tags import Tag
@@ -36,9 +39,6 @@ from pip._internal.utils.misc import build_netloc
 from pip._internal.utils.packaging import check_requires_python
 from pip._internal.utils.unpacking import SUPPORTED_EXTENSIONS
 
-if TYPE_CHECKING:
-    from pip._vendor.typing_extensions import TypeGuard
-
 __all__ = ["FormatControl", "BestCandidateResult", "PackageFinder"]
 
 
@@ -251,7 +251,7 @@ class LinkEvaluator:
 
 def filter_unallowed_hashes(
     candidates: List[InstallationCandidate],
-    hashes: Optional[Hashes],
+    hashes: Hashes,
     project_name: str,
 ) -> List[InstallationCandidate]:
     """
@@ -540,7 +540,6 @@ class CandidateEvaluator:
                 binary_preference = 1
             if wheel.build_tag is not None:
                 match = re.match(r"^(\d+)(.*)$", wheel.build_tag)
-                assert match is not None, "guaranteed by filename validation"
                 build_tag_groups = match.groups()
                 build_tag = (int(build_tag_groups[0]), build_tag_groups[1])
         else:  # sdist
@@ -943,46 +942,43 @@ class PackageFinder:
                 "No matching distribution found for {}".format(req)
             )
 
-        def _should_install_candidate(
-            candidate: Optional[InstallationCandidate],
-        ) -> "TypeGuard[InstallationCandidate]":
-            if installed_version is None:
-                return True
-            if best_candidate is None:
-                return False
-            return best_candidate.version > installed_version
+        best_installed = False
+        if installed_version and (
+            best_candidate is None or best_candidate.version <= installed_version
+        ):
+            best_installed = True
 
         if not upgrade and installed_version is not None:
-            if _should_install_candidate(best_candidate):
+            if best_installed:
                 logger.debug(
-                    "Existing installed version (%s) satisfies requirement "
-                    "(most up-to-date version is %s)",
+                    "Existing installed version (%s) is most up-to-date and "
+                    "satisfies requirement",
                     installed_version,
-                    best_candidate.version,
                 )
             else:
                 logger.debug(
-                    "Existing installed version (%s) is most up-to-date and "
-                    "satisfies requirement",
+                    "Existing installed version (%s) satisfies requirement "
+                    "(most up-to-date version is %s)",
                     installed_version,
+                    best_candidate.version,
                 )
             return None
 
-        if _should_install_candidate(best_candidate):
+        if best_installed:
+            # We have an existing version, and its the best version
             logger.debug(
-                "Using version %s (newest of versions: %s)",
-                best_candidate.version,
+                "Installed version (%s) is most up-to-date (past versions: %s)",
+                installed_version,
                 _format_versions(best_candidate_result.iter_applicable()),
             )
-            return best_candidate
+            raise BestVersionAlreadyInstalled
 
-        # We have an existing version, and its the best version
         logger.debug(
-            "Installed version (%s) is most up-to-date (past versions: %s)",
-            installed_version,
+            "Using version %s (newest of versions: %s)",
+            best_candidate.version,
             _format_versions(best_candidate_result.iter_applicable()),
         )
-        raise BestVersionAlreadyInstalled
+        return best_candidate
 
 
 def _find_name_version_sep(fragment: str, canonical_name: str) -> int:
diff --git a/src/pip/_internal/locations/__init__.py b/src/pip/_internal/locations/__init__.py
index 496844be1..516bd6078 100644
--- a/src/pip/_internal/locations/__init__.py
+++ b/src/pip/_internal/locations/__init__.py
@@ -25,10 +25,9 @@ __all__ = [
     "USER_CACHE_DIR",
     "get_bin_prefix",
     "get_bin_user",
-    "get_isolated_environment_bin_path",
-    "get_isolated_environment_lib_paths",
     "get_major_minor_version",
     "get_platlib",
+    "get_isolated_environment_lib_paths",
     "get_purelib",
     "get_scheme",
     "get_src_prefix",
@@ -527,7 +526,3 @@ def get_isolated_environment_lib_paths(prefix: str) -> List[str]:
         _log_context(prefix=prefix)
 
     return old_lib_paths
-
-
-def get_isolated_environment_bin_path(prefix: str) -> str:
-    return _sysconfig.get_isolated_environment_paths(prefix)["scripts"]
diff --git a/src/pip/_internal/locations/_sysconfig.py b/src/pip/_internal/locations/_sysconfig.py
index 38e400f94..698215720 100644
--- a/src/pip/_internal/locations/_sysconfig.py
+++ b/src/pip/_internal/locations/_sysconfig.py
@@ -213,13 +213,10 @@ def get_platlib() -> str:
     return sysconfig.get_paths()["platlib"]
 
 
-def get_isolated_environment_paths(prefix: str) -> typing.Dict[str, str]:
-    variables = {"base": prefix, "platbase": prefix}
-    if "venv" in sysconfig.get_scheme_names():
-        return sysconfig.get_paths(vars=variables, scheme="venv")
-    return sysconfig.get_paths(vars=variables)
-
-
 def get_isolated_environment_lib_paths(prefix: str) -> typing.Tuple[str, str]:
-    paths = get_isolated_environment_paths(prefix)
+    vars = {"base": prefix, "platbase": prefix}
+    if "venv" in sysconfig.get_scheme_names():
+        paths = sysconfig.get_paths(vars=vars, scheme="venv")
+    else:
+        paths = sysconfig.get_paths(vars=vars)
     return (paths["purelib"], paths["platlib"])
diff --git a/src/pip/_internal/models/link.py b/src/pip/_internal/models/link.py
index a1e4d5a08..c7c4b0e9b 100644
--- a/src/pip/_internal/models/link.py
+++ b/src/pip/_internal/models/link.py
@@ -79,9 +79,6 @@ class LinkHash:
         name, value = match.groups()
         return cls(name=name, value=value)
 
-    def as_dict(self) -> Dict[str, str]:
-        return {self.name: self.value}
-
     def as_hashes(self) -> Hashes:
         """Return a Hashes instance which checks only for the current hash."""
         return Hashes({self.name: [self.value]})
@@ -168,6 +165,7 @@ class Link(KeyBasedCompareMixin):
         "requires_python",
         "yanked_reason",
         "dist_info_metadata",
+        "link_hash",
         "cache_link_parsing",
         "egg_fragment",
     ]
@@ -179,6 +177,7 @@ class Link(KeyBasedCompareMixin):
         requires_python: Optional[str] = None,
         yanked_reason: Optional[str] = None,
         dist_info_metadata: Optional[str] = None,
+        link_hash: Optional[LinkHash] = None,
         cache_link_parsing: bool = True,
         hashes: Optional[Mapping[str, str]] = None,
     ) -> None:
@@ -201,11 +200,16 @@ class Link(KeyBasedCompareMixin):
             attribute, if present, in a simple repository HTML link. This may be parsed
             into its own `Link` by `self.metadata_link()`. See PEP 658 for more
             information and the specification.
+        :param link_hash: a checksum for the content the link points to. If not
+            provided, this will be extracted from the link URL, if the URL has
+            any checksum.
         :param cache_link_parsing: A flag that is used elsewhere to determine
-            whether resources retrieved from this link should be cached. PyPI
-            URLs should generally have this set to False, for example.
+                                   whether resources retrieved from this link
+                                   should be cached. PyPI index urls should
+                                   generally have this set to False, for
+                                   example.
         :param hashes: A mapping of hash names to digests to allow us to
-            determine the validity of a download.
+                       determine the validity of a download.
         """
 
         # url can be a UNC windows share
@@ -216,18 +220,13 @@ class Link(KeyBasedCompareMixin):
         # Store the url as a private attribute to prevent accidentally
         # trying to set a new value.
         self._url = url
-
-        link_hash = LinkHash.split_hash_name_and_value(url)
-        hashes_from_link = {} if link_hash is None else link_hash.as_dict()
-        if hashes is None:
-            self._hashes = hashes_from_link
-        else:
-            self._hashes = {**hashes, **hashes_from_link}
+        self._hashes = hashes if hashes is not None else {}
 
         self.comes_from = comes_from
         self.requires_python = requires_python if requires_python else None
         self.yanked_reason = yanked_reason
         self.dist_info_metadata = dist_info_metadata
+        self.link_hash = link_hash or LinkHash.split_hash_name_and_value(self._url)
 
         super().__init__(key=url, defining_class=Link)
 
@@ -402,26 +401,29 @@ class Link(KeyBasedCompareMixin):
         if self.dist_info_metadata is None:
             return None
         metadata_url = f"{self.url_without_fragment}.metadata"
+        link_hash: Optional[LinkHash] = None
         # If data-dist-info-metadata="true" is set, then the metadata file exists,
         # but there is no information about its checksum or anything else.
         if self.dist_info_metadata != "true":
             link_hash = LinkHash.split_hash_name_and_value(self.dist_info_metadata)
-        else:
-            link_hash = None
-        if link_hash is None:
-            return Link(metadata_url)
-        return Link(metadata_url, hashes=link_hash.as_dict())
+        return Link(metadata_url, link_hash=link_hash)
 
-    def as_hashes(self) -> Hashes:
-        return Hashes({k: [v] for k, v in self._hashes.items()})
+    def as_hashes(self) -> Optional[Hashes]:
+        if self.link_hash is not None:
+            return self.link_hash.as_hashes()
+        return None
 
     @property
     def hash(self) -> Optional[str]:
-        return next(iter(self._hashes.values()), None)
+        if self.link_hash is not None:
+            return self.link_hash.value
+        return None
 
     @property
     def hash_name(self) -> Optional[str]:
-        return next(iter(self._hashes), None)
+        if self.link_hash is not None:
+            return self.link_hash.name
+        return None
 
     @property
     def show_url(self) -> str:
@@ -450,15 +452,15 @@ class Link(KeyBasedCompareMixin):
 
     @property
     def has_hash(self) -> bool:
-        return bool(self._hashes)
+        return self.link_hash is not None
 
     def is_hash_allowed(self, hashes: Optional[Hashes]) -> bool:
         """
         Return True if the link has a hash and it is allowed by `hashes`.
         """
-        if hashes is None:
+        if self.link_hash is None:
             return False
-        return any(hashes.is_hash_allowed(k, v) for k, v in self._hashes.items())
+        return self.link_hash.is_hash_allowed(hashes)
 
 
 class _CleanResult(NamedTuple):
diff --git a/src/pip/_internal/operations/check.py b/src/pip/_internal/operations/check.py
index fb3ac8b9c..e3bce69b2 100644
--- a/src/pip/_internal/operations/check.py
+++ b/src/pip/_internal/operations/check.py
@@ -75,7 +75,7 @@ def check_package_set(
             if name not in package_set:
                 missed = True
                 if req.marker is not None:
-                    missed = req.marker.evaluate()
+                    missed = req.marker.evaluate({"extra": ""})
                 if missed:
                     missing_deps.add((name, req))
                 continue
diff --git a/src/pip/_internal/self_outdated_check.py b/src/pip/_internal/self_outdated_check.py
index 41cc42c56..9e2149c52 100644
--- a/src/pip/_internal/self_outdated_check.py
+++ b/src/pip/_internal/self_outdated_check.py
@@ -133,7 +133,7 @@ class UpgradePrompt:
         return Group(
             Text(),
             Text.from_markup(
-                f"{notice} A new release of pip is available: "
+                f"{notice} A new release of pip available: "
                 f"[red]{self.old}[reset] -> [green]{self.new}[reset]"
             ),
             Text.from_markup(
@@ -155,7 +155,7 @@ def was_installed_by_pip(pkg: str) -> bool:
 
 def _get_current_remote_pip_version(
     session: PipSession, options: optparse.Values
-) -> Optional[str]:
+) -> str:
     # Lets use PackageFinder to see what the latest pip version is
     link_collector = LinkCollector.create(
         session,
@@ -176,7 +176,7 @@ def _get_current_remote_pip_version(
     )
     best_candidate = finder.find_best_candidate("pip").best_candidate
     if best_candidate is None:
-        return None
+        return
 
     return str(best_candidate.version)
 
@@ -186,14 +186,11 @@ def _self_version_check_logic(
     state: SelfCheckState,
     current_time: datetime.datetime,
     local_version: DistributionVersion,
-    get_remote_version: Callable[[], Optional[str]],
+    get_remote_version: Callable[[], str],
 ) -> Optional[UpgradePrompt]:
     remote_version_str = state.get(current_time)
     if remote_version_str is None:
         remote_version_str = get_remote_version()
-        if remote_version_str is None:
-            logger.debug("No remote pip version found")
-            return None
         state.set(remote_version_str, current_time)
 
     remote_version = parse_version(remote_version_str)


Test patch:
diff --git a/tests/unit/test_cache.py b/tests/unit/test_cache.py
index f27daa266..f1f0141c7 100644
--- a/tests/unit/test_cache.py
+++ b/tests/unit/test_cache.py
@@ -1,7 +1,7 @@
 import os
 from pathlib import Path
 
-from pip._vendor.packaging.tags import Tag, interpreter_name, interpreter_version
+from pip._vendor.packaging.tags import Tag
 
 from pip._internal.cache import WheelCache, _hash_dict
 from pip._internal.models.format_control import FormatControl
@@ -52,48 +52,6 @@ def test_cache_hash() -> None:
     assert h == "f83b32dfa27a426dec08c21bf006065dd003d0aac78e7fc493d9014d"
 
 
-def test_link_to_cache(tmpdir: Path) -> None:
-    """
-    Test that Link.from_json() produces Links with consistent cache
-    locations
-    """
-    wc = WheelCache(os.fspath(tmpdir), FormatControl())
-    # Define our expectations for stable cache path.
-    i_name = interpreter_name()
-    i_version = interpreter_version()
-    key_parts = {
-        "url": "https://files.pythonhosted.org/packages/a6/91/"
-        "86a6eac449ddfae239e93ffc1918cf33fd9bab35c04d1e963b311e347a73/"
-        "netifaces-0.11.0.tar.gz",
-        "sha256": "043a79146eb2907edf439899f262b3dfe41717d34124298ed281139a8b93ca32",
-        "interpreter_name": i_name,
-        "interpreter_version": i_version,
-    }
-    expected_hash = _hash_dict(key_parts)
-    parts = [
-        expected_hash[:2],
-        expected_hash[2:4],
-        expected_hash[4:6],
-        expected_hash[6:],
-    ]
-    pathed_hash = os.path.join(*parts)
-    # Check working from a Link produces the same result.
-    file_data = {
-        "filename": "netifaces-0.11.0.tar.gz",
-        "hashes": {
-            "sha256": key_parts["sha256"],
-        },
-        "requires-python": "",
-        "url": key_parts["url"],
-        "yanked": False,
-    }
-    page_url = "https://pypi.org/simple/netifaces/"
-    link = Link.from_json(file_data=file_data, page_url=page_url)
-    assert link
-    path = wc.get_path_for_link(link)
-    assert pathed_hash in path
-
-
 def test_get_cache_entry(tmpdir: Path) -> None:
     wc = WheelCache(os.fspath(tmpdir), FormatControl())
     persi_link = Link("https://g.c/o/r/persi")
diff --git a/tests/unit/test_collector.py b/tests/unit/test_collector.py
index 47307c00e..55676a4fc 100644
--- a/tests/unit/test_collector.py
+++ b/tests/unit/test_collector.py
@@ -6,7 +6,7 @@ import re
 import uuid
 from pathlib import Path
 from textwrap import dedent
-from typing import Dict, List, Optional, Tuple
+from typing import List, Optional, Tuple
 from unittest import mock
 
 import pytest
@@ -538,7 +538,7 @@ def test_parse_links_json() -> None:
         metadata_link.url
         == "https://example.com/files/holygrail-1.0-py3-none-any.whl.metadata"
     )
-    assert metadata_link._hashes == {"sha512": "aabdd41"}
+    assert metadata_link.link_hash == LinkHash("sha512", "aabdd41")
 
 
 @pytest.mark.parametrize(
@@ -575,41 +575,41 @@ _pkg1_requirement = Requirement("pkg1==1.0")
 
 
 @pytest.mark.parametrize(
-    "anchor_html, expected, hashes",
+    "anchor_html, expected, link_hash",
     [
         # Test not present.
         (
             '<a href="/pkg1-1.0.tar.gz"></a>',
             None,
-            {},
+            None,
         ),
         # Test with value "true".
         (
             '<a href="/pkg1-1.0.tar.gz" data-dist-info-metadata="true"></a>',
             "true",
-            {},
+            None,
         ),
         # Test with a provided hash value.
         (
             '<a href="/pkg1-1.0.tar.gz" data-dist-info-metadata="sha256=aa113592bbe"></a>',  # noqa: E501
             "sha256=aa113592bbe",
-            {},
+            None,
         ),
         # Test with a provided hash value for both the requirement as well as metadata.
         (
             '<a href="/pkg1-1.0.tar.gz#sha512=abc132409cb" data-dist-info-metadata="sha256=aa113592bbe"></a>',  # noqa: E501
             "sha256=aa113592bbe",
-            {"sha512": "abc132409cb"},
+            LinkHash("sha512", "abc132409cb"),
         ),
     ],
 )
 def test_parse_links__dist_info_metadata(
     anchor_html: str,
     expected: Optional[str],
-    hashes: Dict[str, str],
+    link_hash: Optional[LinkHash],
 ) -> None:
     link = _test_parse_links_data_attribute(anchor_html, "dist_info_metadata", expected)
-    assert link._hashes == hashes
+    assert link.link_hash == link_hash
 
 
 def test_parse_links_caches_same_page_by_url() -> None: