Repository: pypa/pip
Pull request #10511 (2021-09-28 09:34:16)

More pkg_resources refactoring
TODO: This will need review to make sure uninstallation works for PEP 660 editables.

sbidoul: PEP 660 editables are regular wheels so uninstallation should not have to care about them.
uranusjr: The "problem" is old editables do need special treatments, so uninstallation code needs to distinguish whether an editable is legacy or not so PEP 660 editables don't incorrectly fall into the legacy code path :p
sbidoul: > The "problem" is old editables do need special treatments, so uninstallation code needs to distinguish whether an editable is legacy or not so PEP 660 editables don't incorrectly fall into the legacy code path :p

Sure. But do you mean we should worry about uninstallation bugs now that PEP 660 is merged, or is your concern specific to this PR ?
uranusjr: Specific to this PR.
uranusjr: This is rebased against main so I think PEP 660 should be covered correctly.

Patch:
diff --git a/src/pip/_internal/commands/show.py b/src/pip/_internal/commands/show.py
index 872292a2b..d5540d68b 100644
--- a/src/pip/_internal/commands/show.py
+++ b/src/pip/_internal/commands/show.py
@@ -1,8 +1,6 @@
-import csv
 import logging
-import pathlib
 from optparse import Values
-from typing import Iterator, List, NamedTuple, Optional, Tuple
+from typing import Iterator, List, NamedTuple, Optional
 
 from pip._vendor.packaging.utils import canonicalize_name
 
@@ -69,33 +67,6 @@ class _PackageInfo(NamedTuple):
     files: Optional[List[str]]
 
 
-def _convert_legacy_entry(entry: Tuple[str, ...], info: Tuple[str, ...]) -> str:
-    """Convert a legacy installed-files.txt path into modern RECORD path.
-
-    The legacy format stores paths relative to the info directory, while the
-    modern format stores paths relative to the package root, e.g. the
-    site-packages directory.
-
-    :param entry: Path parts of the installed-files.txt entry.
-    :param info: Path parts of the egg-info directory relative to package root.
-    :returns: The converted entry.
-
-    For best compatibility with symlinks, this does not use ``abspath()`` or
-    ``Path.resolve()``, but tries to work with path parts:
-
-    1. While ``entry`` starts with ``..``, remove the equal amounts of parts
-       from ``info``; if ``info`` is empty, start appending ``..`` instead.
-    2. Join the two directly.
-    """
-    while entry and entry[0] == "..":
-        if not info or info[-1] == "..":
-            info += ("..",)
-        else:
-            info = info[:-1]
-        entry = entry[1:]
-    return str(pathlib.Path(*info, *entry))
-
-
 def search_packages_info(query: List[str]) -> Iterator[_PackageInfo]:
     """
     Gather details from installed distributions. Print distribution name,
@@ -121,34 +92,6 @@ def search_packages_info(query: List[str]) -> Iterator[_PackageInfo]:
             in {canonicalize_name(d.name) for d in dist.iter_dependencies()}
         )
 
-    def _files_from_record(dist: BaseDistribution) -> Optional[Iterator[str]]:
-        try:
-            text = dist.read_text("RECORD")
-        except FileNotFoundError:
-            return None
-        # This extra Path-str cast normalizes entries.
-        return (str(pathlib.Path(row[0])) for row in csv.reader(text.splitlines()))
-
-    def _files_from_legacy(dist: BaseDistribution) -> Optional[Iterator[str]]:
-        try:
-            text = dist.read_text("installed-files.txt")
-        except FileNotFoundError:
-            return None
-        paths = (p for p in text.splitlines(keepends=False) if p)
-        root = dist.location
-        info = dist.info_directory
-        if root is None or info is None:
-            return paths
-        try:
-            info_rel = pathlib.Path(info).relative_to(root)
-        except ValueError:  # info is not relative to root.
-            return paths
-        if not info_rel.parts:  # info *is* root.
-            return paths
-        return (
-            _convert_legacy_entry(pathlib.Path(p).parts, info_rel.parts) for p in paths
-        )
-
     for query_name in query_names:
         try:
             dist = installed[query_name]
@@ -164,7 +107,7 @@ def search_packages_info(query: List[str]) -> Iterator[_PackageInfo]:
         except FileNotFoundError:
             entry_points = []
 
-        files_iter = _files_from_record(dist) or _files_from_legacy(dist)
+        files_iter = dist.iter_declared_entries()
         if files_iter is None:
             files: Optional[List[str]] = None
         else:
diff --git a/src/pip/_internal/distributions/installed.py b/src/pip/_internal/distributions/installed.py
index 6c8c17929..be5962f98 100644
--- a/src/pip/_internal/distributions/installed.py
+++ b/src/pip/_internal/distributions/installed.py
@@ -11,10 +11,8 @@ class InstalledDistribution(AbstractDistribution):
     """
 
     def get_metadata_distribution(self) -> BaseDistribution:
-        from pip._internal.metadata.pkg_resources import Distribution as _Dist
-
         assert self.req.satisfied_by is not None, "not actually installed"
-        return _Dist(self.req.satisfied_by)
+        return self.req.satisfied_by
 
     def prepare_distribution_metadata(
         self, finder: PackageFinder, build_isolation: bool
diff --git a/src/pip/_internal/distributions/sdist.py b/src/pip/_internal/distributions/sdist.py
index cd85ac5c4..1d8e55bd2 100644
--- a/src/pip/_internal/distributions/sdist.py
+++ b/src/pip/_internal/distributions/sdist.py
@@ -19,9 +19,7 @@ class SourceDistribution(AbstractDistribution):
     """
 
     def get_metadata_distribution(self) -> BaseDistribution:
-        from pip._internal.metadata.pkg_resources import Distribution as _Dist
-
-        return _Dist(self.req.get_dist())
+        return self.req.get_dist()
 
     def prepare_distribution_metadata(
         self, finder: PackageFinder, build_isolation: bool
diff --git a/src/pip/_internal/index/package_finder.py b/src/pip/_internal/index/package_finder.py
index a2702db7d..82449b488 100644
--- a/src/pip/_internal/index/package_finder.py
+++ b/src/pip/_internal/index/package_finder.py
@@ -881,7 +881,7 @@ class PackageFinder:
 
         installed_version: Optional[_BaseVersion] = None
         if req.satisfied_by is not None:
-            installed_version = parse_version(req.satisfied_by.version)
+            installed_version = req.satisfied_by.version
 
         def _format_versions(cand_iter: Iterable[InstallationCandidate]) -> str:
             # This repeated parse_version and str() conversion is needed to
diff --git a/src/pip/_internal/locations/__init__.py b/src/pip/_internal/locations/__init__.py
index dba182d75..fe5c845c8 100644
--- a/src/pip/_internal/locations/__init__.py
+++ b/src/pip/_internal/locations/__init__.py
@@ -64,8 +64,8 @@ def _looks_like_bpo_44860() -> bool:
 
 def _looks_like_red_hat_patched_platlib_purelib(scheme: Dict[str, str]) -> bool:
     platlib = scheme["platlib"]
-    if "/$platlibdir/" in platlib and hasattr(sys, "platlibdir"):
-        platlib = platlib.replace("/$platlibdir/", f"/{sys.platlibdir}/")
+    if "/$platlibdir/" in platlib:
+        platlib = platlib.replace("/$platlibdir/", f"/{_PLATLIBDIR}/")
     if "/lib64/" not in platlib:
         return False
     unpatched = platlib.replace("/lib64/", "/lib/")
diff --git a/src/pip/_internal/metadata/__init__.py b/src/pip/_internal/metadata/__init__.py
index f4f2a4f6c..cc037c14f 100644
--- a/src/pip/_internal/metadata/__init__.py
+++ b/src/pip/_internal/metadata/__init__.py
@@ -38,6 +38,17 @@ def get_environment(paths: Optional[List[str]]) -> BaseEnvironment:
     return Environment.from_paths(paths)
 
 
+def get_directory_distribution(directory: str) -> BaseDistribution:
+    """Get the distribution metadata representation in the specified directory.
+
+    This returns a Distribution instance from the chosen backend based on
+    the given on-disk ``.dist-info`` directory.
+    """
+    from .pkg_resources import Distribution
+
+    return Distribution.from_directory(directory)
+
+
 def get_wheel_distribution(wheel: Wheel, canonical_name: str) -> BaseDistribution:
     """Get the representation of the specified wheel's distribution metadata.
 
diff --git a/src/pip/_internal/metadata/base.py b/src/pip/_internal/metadata/base.py
index 4788360a0..40fe266d0 100644
--- a/src/pip/_internal/metadata/base.py
+++ b/src/pip/_internal/metadata/base.py
@@ -1,6 +1,8 @@
+import csv
 import email.message
 import json
 import logging
+import pathlib
 import re
 import zipfile
 from typing import (
@@ -12,6 +14,7 @@ from typing import (
     Iterator,
     List,
     Optional,
+    Tuple,
     Union,
 )
 
@@ -36,6 +39,8 @@ else:
 
 DistributionVersion = Union[LegacyVersion, Version]
 
+InfoPath = Union[str, pathlib.PurePosixPath]
+
 logger = logging.getLogger(__name__)
 
 
@@ -53,6 +58,36 @@ class BaseEntryPoint(Protocol):
         raise NotImplementedError()
 
 
+def _convert_installed_files_path(
+    entry: Tuple[str, ...],
+    info: Tuple[str, ...],
+) -> str:
+    """Convert a legacy installed-files.txt path into modern RECORD path.
+
+    The legacy format stores paths relative to the info directory, while the
+    modern format stores paths relative to the package root, e.g. the
+    site-packages directory.
+
+    :param entry: Path parts of the installed-files.txt entry.
+    :param info: Path parts of the egg-info directory relative to package root.
+    :returns: The converted entry.
+
+    For best compatibility with symlinks, this does not use ``abspath()`` or
+    ``Path.resolve()``, but tries to work with path parts:
+
+    1. While ``entry`` starts with ``..``, remove the equal amounts of parts
+       from ``info``; if ``info`` is empty, start appending ``..`` instead.
+    2. Join the two directly.
+    """
+    while entry and entry[0] == "..":
+        if not info or info[-1] == "..":
+            info += ("..",)
+        else:
+            info = info[:-1]
+        entry = entry[1:]
+    return str(pathlib.Path(*info, *entry))
+
+
 class BaseDistribution(Protocol):
     def __repr__(self) -> str:
         return f"{self.raw_name} {self.version} ({self.location})"
@@ -97,8 +132,8 @@ class BaseDistribution(Protocol):
         return None
 
     @property
-    def info_directory(self) -> Optional[str]:
-        """Location of the .[egg|dist]-info directory.
+    def info_location(self) -> Optional[str]:
+        """Location of the .[egg|dist]-info directory or file.
 
         Similarly to ``location``, a string value is not necessarily a
         filesystem path. ``None`` means the distribution is created in-memory.
@@ -112,6 +147,65 @@ class BaseDistribution(Protocol):
         """
         raise NotImplementedError()
 
+    @property
+    def installed_by_distutils(self) -> bool:
+        """Whether this distribution is installed with legacy distutils format.
+
+        A distribution installed with "raw" distutils not patched by setuptools
+        uses one single file at ``info_location`` to store metadata. We need to
+        treat this specially on uninstallation.
+        """
+        info_location = self.info_location
+        if not info_location:
+            return False
+        return pathlib.Path(info_location).is_file()
+
+    @property
+    def installed_as_egg(self) -> bool:
+        """Whether this distribution is installed as an egg.
+
+        This usually indicates the distribution was installed by (older versions
+        of) easy_install.
+        """
+        location = self.location
+        if not location:
+            return False
+        return location.endswith(".egg")
+
+    @property
+    def installed_with_setuptools_egg_info(self) -> bool:
+        """Whether this distribution is installed with the ``.egg-info`` format.
+
+        This usually indicates the distribution was installed with setuptools
+        with an old pip version or with ``single-version-externally-managed``.
+
+        Note that this ensure the metadata store is a directory. distutils can
+        also installs an ``.egg-info``, but as a file, not a directory. This
+        property is *False* for that case. Also see ``installed_by_distutils``.
+        """
+        info_location = self.info_location
+        if not info_location:
+            return False
+        if not info_location.endswith(".egg-info"):
+            return False
+        return pathlib.Path(info_location).is_dir()
+
+    @property
+    def installed_with_dist_info(self) -> bool:
+        """Whether this distribution is installed with the "modern format".
+
+        This indicates a "modern" installation, e.g. storing metadata in the
+        ``.dist-info`` directory. This applies to installations made by
+        setuptools (but through pip, not directly), or anything using the
+        standardized build backend interface (PEP 517).
+        """
+        info_location = self.info_location
+        if not info_location:
+            return False
+        if not info_location.endswith(".dist-info"):
+            return False
+        return pathlib.Path(info_location).is_dir()
+
     @property
     def canonical_name(self) -> NormalizedName:
         raise NotImplementedError()
@@ -120,6 +214,14 @@ class BaseDistribution(Protocol):
     def version(self) -> DistributionVersion:
         raise NotImplementedError()
 
+    @property
+    def setuptools_filename(self) -> str:
+        """Convert a project name to its setuptools-compatible filename.
+
+        This is a copy of ``pkg_resources.to_filename()`` for compatibility.
+        """
+        return self.raw_name.replace("-", "_")
+
     @property
     def direct_url(self) -> Optional[DirectUrl]:
         """Obtain a DirectUrl from this distribution.
@@ -166,11 +268,24 @@ class BaseDistribution(Protocol):
     def in_site_packages(self) -> bool:
         raise NotImplementedError()
 
-    def read_text(self, name: str) -> str:
-        """Read a file in the .dist-info (or .egg-info) directory.
+    def is_file(self, path: InfoPath) -> bool:
+        """Check whether an entry in the info directory is a file."""
+        raise NotImplementedError()
+
+    def iterdir(self, path: InfoPath) -> Iterator[pathlib.PurePosixPath]:
+        """Iterate through a directory in the info directory.
+
+        Each item yielded would be a path relative to the info directory.
 
-        Should raise ``FileNotFoundError`` if ``name`` does not exist in the
-        metadata directory.
+        :raise FileNotFoundError: If ``name`` does not exist in the directory.
+        :raise NotADirectoryError: If ``name`` does not point to a directory.
+        """
+        raise NotImplementedError()
+
+    def read_text(self, path: InfoPath) -> str:
+        """Read a file in the info directory.
+
+        :raise FileNotFoundError: If ``name`` does not exist in the directory.
         """
         raise NotImplementedError()
 
@@ -229,6 +344,51 @@ class BaseDistribution(Protocol):
         """
         raise NotImplementedError()
 
+    def _iter_declared_entries_from_record(self) -> Optional[Iterator[str]]:
+        try:
+            text = self.read_text("RECORD")
+        except FileNotFoundError:
+            return None
+        # This extra Path-str cast normalizes entries.
+        return (str(pathlib.Path(row[0])) for row in csv.reader(text.splitlines()))
+
+    def _iter_declared_entries_from_legacy(self) -> Optional[Iterator[str]]:
+        try:
+            text = self.read_text("installed-files.txt")
+        except FileNotFoundError:
+            return None
+        paths = (p for p in text.splitlines(keepends=False) if p)
+        root = self.location
+        info = self.info_location
+        if root is None or info is None:
+            return paths
+        try:
+            info_rel = pathlib.Path(info).relative_to(root)
+        except ValueError:  # info is not relative to root.
+            return paths
+        if not info_rel.parts:  # info *is* root.
+            return paths
+        return (
+            _convert_installed_files_path(pathlib.Path(p).parts, info_rel.parts)
+            for p in paths
+        )
+
+    def iter_declared_entries(self) -> Optional[Iterator[str]]:
+        """Iterate through file entires declared in this distribution.
+
+        For modern .dist-info distributions, this is the files listed in the
+        ``RECORD`` metadata file. For legacy setuptools distributions, this
+        comes from ``installed-files.txt``, with entries normalized to be
+        compatible with the format used by ``RECORD``.
+
+        :return: An iterator for listed entries, or None if the distribution
+            contains neither ``RECORD`` nor ``installed-files.txt``.
+        """
+        return (
+            self._iter_declared_entries_from_record()
+            or self._iter_declared_entries_from_legacy()
+        )
+
 
 class BaseEnvironment:
     """An environment containing distributions to introspect."""
diff --git a/src/pip/_internal/metadata/pkg_resources.py b/src/pip/_internal/metadata/pkg_resources.py
index 0c054f8c9..ab531532a 100644
--- a/src/pip/_internal/metadata/pkg_resources.py
+++ b/src/pip/_internal/metadata/pkg_resources.py
@@ -1,5 +1,7 @@
 import email.message
 import logging
+import os
+import pathlib
 from typing import Collection, Iterable, Iterator, List, NamedTuple, Optional
 from zipfile import BadZipFile
 
@@ -18,6 +20,7 @@ from .base import (
     BaseEntryPoint,
     BaseEnvironment,
     DistributionVersion,
+    InfoPath,
     Wheel,
 )
 
@@ -34,6 +37,26 @@ class Distribution(BaseDistribution):
     def __init__(self, dist: pkg_resources.Distribution) -> None:
         self._dist = dist
 
+    @classmethod
+    def from_directory(cls, directory: str) -> "Distribution":
+        dist_dir = directory.rstrip(os.sep)
+
+        # Build a PathMetadata object, from path to metadata. :wink:
+        base_dir, dist_dir_name = os.path.split(dist_dir)
+        metadata = pkg_resources.PathMetadata(base_dir, dist_dir)
+
+        # Determine the correct Distribution object type.
+        if dist_dir.endswith(".egg-info"):
+            dist_cls = pkg_resources.Distribution
+            dist_name = os.path.splitext(dist_dir_name)[0]
+        else:
+            assert dist_dir.endswith(".dist-info")
+            dist_cls = pkg_resources.DistInfoDistribution
+            dist_name = os.path.splitext(dist_dir_name)[0].split("-")[0]
+
+        dist = dist_cls(base_dir, project_name=dist_name, metadata=metadata)
+        return cls(dist)
+
     @classmethod
     def from_wheel(cls, wheel: Wheel, name: str) -> "Distribution":
         """Load the distribution from a given wheel.
@@ -53,9 +76,19 @@ class Distribution(BaseDistribution):
         return self._dist.location
 
     @property
-    def info_directory(self) -> Optional[str]:
+    def info_location(self) -> Optional[str]:
         return self._dist.egg_info
 
+    @property
+    def installed_by_distutils(self) -> bool:
+        # A distutils-installed distribution is provided by FileMetadata. This
+        # provider has a "path" attribute not present anywhere else. Not the
+        # best introspection logic, but pip has been doing this for a long time.
+        try:
+            return bool(self._dist._provider.path)
+        except AttributeError:
+            return False
+
     @property
     def canonical_name(self) -> NormalizedName:
         return canonicalize_name(self._dist.project_name)
@@ -66,7 +99,10 @@ class Distribution(BaseDistribution):
 
     @property
     def installer(self) -> str:
-        return get_installer(self._dist)
+        try:
+            return get_installer(self._dist)
+        except (OSError, ValueError):
+            return ""  # Fail silently if the installer file cannot be read.
 
     @property
     def local(self) -> bool:
@@ -80,7 +116,20 @@ class Distribution(BaseDistribution):
     def in_site_packages(self) -> bool:
         return misc.dist_in_site_packages(self._dist)
 
-    def read_text(self, name: str) -> str:
+    def is_file(self, path: InfoPath) -> bool:
+        return self._dist.has_metadata(str(path))
+
+    def iterdir(self, path: InfoPath) -> Iterator[pathlib.PurePosixPath]:
+        name = str(path)
+        if not self._dist.has_metadata(name):
+            raise FileNotFoundError(name)
+        if not self._dist.isdir(name):
+            raise NotADirectoryError(name)
+        for child in self._dist.metadata_listdir(name):
+            yield pathlib.PurePosixPath(path, child)
+
+    def read_text(self, path: InfoPath) -> str:
+        name = str(path)
         if not self._dist.has_metadata(name):
             raise FileNotFoundError(name)
         return self._dist.get_metadata(name)
diff --git a/src/pip/_internal/req/req_install.py b/src/pip/_internal/req/req_install.py
index 19f1904af..46b767b4e 100644
--- a/src/pip/_internal/req/req_install.py
+++ b/src/pip/_internal/req/req_install.py
@@ -18,11 +18,15 @@ from pip._vendor.packaging.utils import canonicalize_name
 from pip._vendor.packaging.version import Version
 from pip._vendor.packaging.version import parse as parse_version
 from pip._vendor.pep517.wrappers import Pep517HookCaller
-from pip._vendor.pkg_resources import Distribution
 
 from pip._internal.build_env import BuildEnvironment, NoOpBuildEnvironment
 from pip._internal.exceptions import InstallationError
 from pip._internal.locations import get_scheme
+from pip._internal.metadata import (
+    BaseDistribution,
+    get_default_environment,
+    get_directory_distribution,
+)
 from pip._internal.models.link import Link
 from pip._internal.operations.build.metadata import generate_metadata
 from pip._internal.operations.build.metadata_editable import generate_editable_metadata
@@ -49,11 +53,9 @@ from pip._internal.utils.misc import (
     display_path,
     dist_in_site_packages,
     dist_in_usersite,
-    get_distribution,
     hide_url,
     redact_auth_from_url,
 )
-from pip._internal.utils.packaging import get_metadata
 from pip._internal.utils.subprocess import runner_with_spinner_message
 from pip._internal.utils.temp_dir import TempDirectory, tempdir_kinds
 from pip._internal.utils.virtualenv import running_under_virtualenv
@@ -62,32 +64,6 @@ from pip._internal.vcs import vcs
 logger = logging.getLogger(__name__)
 
 
-def _get_dist(metadata_directory: str) -> Distribution:
-    """Return a pkg_resources.Distribution for the provided
-    metadata directory.
-    """
-    dist_dir = metadata_directory.rstrip(os.sep)
-
-    # Build a PathMetadata object, from path to metadata. :wink:
-    base_dir, dist_dir_name = os.path.split(dist_dir)
-    metadata = pkg_resources.PathMetadata(base_dir, dist_dir)
-
-    # Determine the correct Distribution object type.
-    if dist_dir.endswith(".egg-info"):
-        dist_cls = pkg_resources.Distribution
-        dist_name = os.path.splitext(dist_dir_name)[0]
-    else:
-        assert dist_dir.endswith(".dist-info")
-        dist_cls = pkg_resources.DistInfoDistribution
-        dist_name = os.path.splitext(dist_dir_name)[0].split("-")[0]
-
-    return dist_cls(
-        base_dir,
-        project_name=dist_name,
-        metadata=metadata,
-    )
-
-
 class InstallRequirement:
     """
     Represents something that may be installed later on, may have information
@@ -154,7 +130,7 @@ class InstallRequirement:
 
         # This holds the pkg_resources.Distribution object if this requirement
         # is already available:
-        self.satisfied_by: Optional[Distribution] = None
+        self.satisfied_by: Optional[BaseDistribution] = None
         # Whether the installation process should try to uninstall an existing
         # distribution before installing this requirement.
         self.should_reinstall = False
@@ -415,18 +391,13 @@ class InstallRequirement:
         """
         if self.req is None:
             return
-        existing_dist = get_distribution(self.req.name)
+        existing_dist = get_default_environment().get_distribution(self.req.name)
         if not existing_dist:
             return
 
-        # pkg_resources may contain a different copy of packaging.version from
-        # pip in if the downstream distributor does a poor job debundling pip.
-        # We avoid existing_dist.parsed_version and let SpecifierSet.contains
-        # parses the version instead.
-        existing_version = existing_dist.version
-        version_compatible = (
-            existing_version is not None
-            and self.req.specifier.contains(existing_version, prereleases=True)
+        version_compatible = self.req.specifier.contains(
+            existing_dist.version,
+            prereleases=True,
         )
         if not version_compatible:
             self.satisfied_by = None
@@ -437,10 +408,9 @@ class InstallRequirement:
                     existing_dist
                 ):
                     raise InstallationError(
-                        "Will not install to the user site because it will "
-                        "lack sys.path precedence to {} in {}".format(
-                            existing_dist.project_name, existing_dist.location
-                        )
+                        f"Will not install to the user site because it will "
+                        f"lack sys.path precedence to {existing_dist.raw_name} "
+                        f"in {existing_dist.location}"
                     )
             else:
                 self.should_reinstall = True
@@ -577,12 +547,12 @@ class InstallRequirement:
     @property
     def metadata(self) -> Any:
         if not hasattr(self, "_metadata"):
-            self._metadata = get_metadata(self.get_dist())
+            self._metadata = self.get_dist().metadata
 
         return self._metadata
 
-    def get_dist(self) -> Distribution:
-        return _get_dist(self.metadata_directory)
+    def get_dist(self) -> BaseDistribution:
+        return get_directory_distribution(self.metadata_directory)
 
     def assert_source_matches_version(self) -> None:
         assert self.source_dir
@@ -661,7 +631,7 @@ class InstallRequirement:
 
         """
         assert self.req
-        dist = get_distribution(self.req.name)
+        dist = get_default_environment().get_distribution(self.req.name)
         if not dist:
             logger.warning("Skipping %s as it is not installed.", self.name)
             return None
diff --git a/src/pip/_internal/req/req_uninstall.py b/src/pip/_internal/req/req_uninstall.py
index 779e93b44..472090a0d 100644
--- a/src/pip/_internal/req/req_uninstall.py
+++ b/src/pip/_internal/req/req_uninstall.py
@@ -1,4 +1,3 @@
-import csv
 import functools
 import os
 import sys
@@ -6,47 +5,33 @@ import sysconfig
 from importlib.util import cache_from_source
 from typing import Any, Callable, Dict, Iterable, Iterator, List, Optional, Set, Tuple
 
-from pip._vendor import pkg_resources
-from pip._vendor.pkg_resources import Distribution
-
 from pip._internal.exceptions import UninstallationError
 from pip._internal.locations import get_bin_prefix, get_bin_user
+from pip._internal.metadata import BaseDistribution
 from pip._internal.utils.compat import WINDOWS
 from pip._internal.utils.egg_link import egg_link_path_from_location
 from pip._internal.utils.logging import getLogger, indent_log
-from pip._internal.utils.misc import (
-    ask,
-    dist_in_usersite,
-    dist_is_local,
-    is_local,
-    normalize_path,
-    renames,
-    rmtree,
-)
+from pip._internal.utils.misc import ask, is_local, normalize_path, renames, rmtree
 from pip._internal.utils.temp_dir import AdjacentTempDirectory, TempDirectory
 
 logger = getLogger(__name__)
 
 
-def _script_names(dist: Distribution, script_name: str, is_gui: bool) -> List[str]:
+def _script_names(bin_dir: str, script_name: str, is_gui: bool) -> Iterator[str]:
     """Create the fully qualified name of the files created by
     {console,gui}_scripts for the given ``dist``.
     Returns the list of file names
     """
-    if dist_in_usersite(dist):
-        bin_dir = get_bin_user()
-    else:
-        bin_dir = get_bin_prefix()
     exe_name = os.path.join(bin_dir, script_name)
-    paths_to_remove = [exe_name]
-    if WINDOWS:
-        paths_to_remove.append(exe_name + ".exe")
-        paths_to_remove.append(exe_name + ".exe.manifest")
-        if is_gui:
-            paths_to_remove.append(exe_name + "-script.pyw")
-        else:
-            paths_to_remove.append(exe_name + "-script.py")
-    return paths_to_remove
+    yield exe_name
+    if not WINDOWS:
+        return
+    yield f"{exe_name}.exe"
+    yield f"{exe_name}.exe.manifest"
+    if is_gui:
+        yield f"{exe_name}-script.pyw"
+    else:
+        yield f"{exe_name}-script.py"
 
 
 def _unique(fn: Callable[..., Iterator[Any]]) -> Callable[..., Iterator[Any]]:
@@ -62,7 +47,7 @@ def _unique(fn: Callable[..., Iterator[Any]]) -> Callable[..., Iterator[Any]]:
 
 
 @_unique
-def uninstallation_paths(dist: Distribution) -> Iterator[str]:
+def uninstallation_paths(dist: BaseDistribution) -> Iterator[str]:
     """
     Yield all the uninstallation paths for dist based on RECORD-without-.py[co]
 
@@ -76,25 +61,25 @@ def uninstallation_paths(dist: Distribution) -> Iterator[str]:
 
     https://packaging.python.org/specifications/recording-installed-packages/
     """
-    try:
-        r = csv.reader(dist.get_metadata_lines("RECORD"))
-    except FileNotFoundError as missing_record_exception:
+    location = dist.location
+    assert location is not None, "not installed"
+
+    entries = dist.iter_declared_entries()
+    if entries is None:
         msg = "Cannot uninstall {dist}, RECORD file not found.".format(dist=dist)
-        try:
-            installer = next(dist.get_metadata_lines("INSTALLER"))
-            if not installer or installer == "pip":
-                raise ValueError()
-        except (OSError, StopIteration, ValueError):
-            dep = "{}=={}".format(dist.project_name, dist.version)
+        installer = dist.installer
+        if not installer or installer == "pip":
+            dep = "{}=={}".format(dist.raw_name, dist.version)
             msg += (
                 " You might be able to recover from this via: "
                 "'pip install --force-reinstall --no-deps {}'.".format(dep)
             )
         else:
             msg += " Hint: The package was installed by {}.".format(installer)
-        raise UninstallationError(msg) from missing_record_exception
-    for row in r:
-        path = os.path.join(dist.location, row[0])
+        raise UninstallationError(msg)
+
+    for entry in entries:
+        path = os.path.join(location, entry)
         yield path
         if path.endswith(".py"):
             dn, fn = os.path.split(path)
@@ -317,11 +302,11 @@ class UninstallPathSet:
     """A set of file paths to be removed in the uninstallation of a
     requirement."""
 
-    def __init__(self, dist: Distribution) -> None:
-        self.paths: Set[str] = set()
+    def __init__(self, dist: BaseDistribution) -> None:
+        self._paths: Set[str] = set()
         self._refuse: Set[str] = set()
-        self.pth: Dict[str, UninstallPthEntries] = {}
-        self.dist = dist
+        self._pth: Dict[str, UninstallPthEntries] = {}
+        self._dist = dist
         self._moved_paths = StashedUninstallPathSet()
 
     def _permitted(self, path: str) -> bool:
@@ -342,7 +327,7 @@ class UninstallPathSet:
         if not os.path.exists(path):
             return
         if self._permitted(path):
-            self.paths.add(path)
+            self._paths.add(path)
         else:
             self._refuse.add(path)
 
@@ -354,37 +339,37 @@ class UninstallPathSet:
     def add_pth(self, pth_file: str, entry: str) -> None:
         pth_file = normalize_path(pth_file)
         if self._permitted(pth_file):
-            if pth_file not in self.pth:
-                self.pth[pth_file] = UninstallPthEntries(pth_file)
-            self.pth[pth_file].add(entry)
+            if pth_file not in self._pth:
+                self._pth[pth_file] = UninstallPthEntries(pth_file)
+            self._pth[pth_file].add(entry)
         else:
             self._refuse.add(pth_file)
 
     def remove(self, auto_confirm: bool = False, verbose: bool = False) -> None:
-        """Remove paths in ``self.paths`` with confirmation (unless
+        """Remove paths in ``self._paths`` with confirmation (unless
         ``auto_confirm`` is True)."""
 
-        if not self.paths:
+        if not self._paths:
             logger.info(
                 "Can't uninstall '%s'. No files were found to uninstall.",
-                self.dist.project_name,
+                self._dist.raw_name,
             )
             return
 
-        dist_name_version = self.dist.project_name + "-" + self.dist.version
+        dist_name_version = f"{self._dist.raw_name}-{self._dist.version}"
         logger.info("Uninstalling %s:", dist_name_version)
 
         with indent_log():
             if auto_confirm or self._allowed_to_proceed(verbose):
                 moved = self._moved_paths
 
-                for_rename = compress_for_rename(self.paths)
+                for_rename = compress_for_rename(self._paths)
 
                 for path in sorted(compact(for_rename)):
                     moved.stash(path)
                     logger.verbose("Removing file or directory %s", path)
 
-                for pth in self.pth.values():
+                for pth in self._pth.values():
                     pth.remove()
 
                 logger.info("Successfully uninstalled %s", dist_name_version)
@@ -402,18 +387,18 @@ class UninstallPathSet:
                     logger.info(path)
 
         if not verbose:
-            will_remove, will_skip = compress_for_output_listing(self.paths)
+            will_remove, will_skip = compress_for_output_listing(self._paths)
         else:
             # In verbose mode, display all the files that are going to be
             # deleted.
-            will_remove = set(self.paths)
+            will_remove = set(self._paths)
             will_skip = set()
 
         _display("Would remove:", will_remove)
         _display("Would not remove (might be manually added):", will_skip)
         _display("Would not remove (outside of prefix):", self._refuse)
         if verbose:
-            _display("Will actually move:", compress_for_rename(self.paths))
+            _display("Will actually move:", compress_for_rename(self._paths))
 
         return ask("Proceed (Y/n)? ", ("y", "n", "")) != "n"
 
@@ -422,12 +407,12 @@ class UninstallPathSet:
         if not self._moved_paths.can_rollback:
             logger.error(
                 "Can't roll back %s; was not uninstalled",
-                self.dist.project_name,
+                self._dist.raw_name,
             )
             return
-        logger.info("Rolling back uninstall of %s", self.dist.project_name)
+        logger.info("Rolling back uninstall of %s", self._dist.raw_name)
         self._moved_paths.rollback()
-        for pth in self.pth.values():
+        for pth in self._pth.values():
             pth.rollback()
 
     def commit(self) -> None:
@@ -435,141 +420,156 @@ class UninstallPathSet:
         self._moved_paths.commit()
 
     @classmethod
-    def from_dist(cls, dist: Distribution) -> "UninstallPathSet":
-        dist_path = normalize_path(dist.location)
-        if not dist_is_local(dist):
+    def from_dist(cls, dist: BaseDistribution) -> "UninstallPathSet":
+        dist_location = dist.location
+        info_location = dist.info_location
+        if dist_location is None:
+            logger.info(
+                "Not uninstalling %s since it is not installed",
+                dist.canonical_name,
+            )
+            return cls(dist)
+
+        normalized_dist_location = normalize_path(dist_location)
+        if not dist.local:
             logger.info(
                 "Not uninstalling %s at %s, outside environment %s",
-                dist.key,
-                dist_path,
+                dist.canonical_name,
+                normalized_dist_location,
                 sys.prefix,
             )
             return cls(dist)
 
-        if dist_path in {
+        if normalized_dist_location in {
             p
             for p in {sysconfig.get_path("stdlib"), sysconfig.get_path("platstdlib")}
             if p
         }:
             logger.info(
                 "Not uninstalling %s at %s, as it is in the standard library.",
-                dist.key,
-                dist_path,
+                dist.canonical_name,
+                normalized_dist_location,
             )
             return cls(dist)
 
         paths_to_remove = cls(dist)
-        develop_egg_link = egg_link_path_from_location(dist.project_name)
-        develop_egg_link_egg_info = "{}.egg-info".format(
-            pkg_resources.to_filename(dist.project_name)
+        develop_egg_link = egg_link_path_from_location(dist.raw_name)
+
+        # Distribution is installed with metadata in a "flat" .egg-info
+        # directory. This means it is not a modern .dist-info installation, an
+        # egg, or legacy editable.
+        setuptools_flat_installation = (
+            dist.installed_with_setuptools_egg_info
+            and info_location is not None
+            and os.path.exists(info_location)
+            # If dist is editable and the location points to a ``.egg-info``,
+            # we are in fact in the legacy editable case.
+            and not info_location.endswith(f"{dist.setuptools_filename}.egg-info")
         )
-        egg_info_exists = dist.egg_info and os.path.exists(dist.egg_info)
-        # Special case for distutils installed package
-        distutils_egg_info = getattr(dist._provider, "path", None)
 
         # Uninstall cases order do matter as in the case of 2 installs of the
         # same package, pip needs to uninstall the currently detected version
-        if (
-            egg_info_exists
-            and dist.egg_info.endswith(".egg-info")
-            and not dist.egg_info.endswith(develop_egg_link_egg_info)
-        ):
-            # if dist.egg_info.endswith(develop_egg_link_egg_info), we
-            # are in fact in the develop_egg_link case
-            paths_to_remove.add(dist.egg_info)
-            if dist.has_metadata("installed-files.txt"):
-                for installed_file in dist.get_metadata(
-                    "installed-files.txt"
-                ).splitlines():
-                    path = os.path.normpath(os.path.join(dist.egg_info, installed_file))
-                    paths_to_remove.add(path)
+        if setuptools_flat_installation:
+            if info_location is not None:
+                paths_to_remove.add(info_location)
+            installed_files = dist.iter_declared_entries()
+            if installed_files is not None:
+                for installed_file in installed_files:
+                    paths_to_remove.add(os.path.join(dist_location, installed_file))
             # FIXME: need a test for this elif block
             # occurs with --single-version-externally-managed/--record outside
             # of pip
-            elif dist.has_metadata("top_level.txt"):
-                if dist.has_metadata("namespace_packages.txt"):
-                    namespaces = dist.get_metadata("namespace_packages.txt")
-                else:
+            elif dist.is_file("top_level.txt"):
+                try:
+                    namespace_packages = dist.read_text("namespace_packages.txt")
+                except FileNotFoundError:
                     namespaces = []
+                else:
+                    namespaces = namespace_packages.splitlines(keepends=False)
                 for top_level_pkg in [
                     p
-                    for p in dist.get_metadata("top_level.txt").splitlines()
+                    for p in dist.read_text("top_level.txt").splitlines()
                     if p and p not in namespaces
                 ]:
-                    path = os.path.join(dist.location, top_level_pkg)
+                    path = os.path.join(dist_location, top_level_pkg)
                     paths_to_remove.add(path)
-                    paths_to_remove.add(path + ".py")
-                    paths_to_remove.add(path + ".pyc")
-                    paths_to_remove.add(path + ".pyo")
+                    paths_to_remove.add(f"{path}.py")
+                    paths_to_remove.add(f"{path}.pyc")
+                    paths_to_remove.add(f"{path}.pyo")
 
-        elif distutils_egg_info:
+        elif dist.installed_by_distutils:
             raise UninstallationError(
                 "Cannot uninstall {!r}. It is a distutils installed project "
                 "and thus we cannot accurately determine which files belong "
                 "to it which would lead to only a partial uninstall.".format(
-                    dist.project_name,
+                    dist.raw_name,
                 )
             )
 
-        elif dist.location.endswith(".egg"):
+        elif dist.installed_as_egg:
             # package installed by easy_install
             # We cannot match on dist.egg_name because it can slightly vary
             # i.e. setuptools-0.6c11-py2.6.egg vs setuptools-0.6rc11-py2.6.egg
-            paths_to_remove.add(dist.location)
-            easy_install_egg = os.path.split(dist.location)[1]
+            paths_to_remove.add(dist_location)
+            easy_install_egg = os.path.split(dist_location)[1]
             easy_install_pth = os.path.join(
-                os.path.dirname(dist.location), "easy-install.pth"
+                os.path.dirname(dist_location),
+                "easy-install.pth",
             )
             paths_to_remove.add_pth(easy_install_pth, "./" + easy_install_egg)
 
-        elif egg_info_exists and dist.egg_info.endswith(".dist-info"):
+        elif dist.installed_with_dist_info:
             for path in uninstallation_paths(dist):
                 paths_to_remove.add(path)
 
         elif develop_egg_link:
-            # develop egg
+            # PEP 660 modern editable is handled in the ``.dist-info`` case
+            # above, so this only covers the setuptools-style editable.
             with open(develop_egg_link) as fh:
                 link_pointer = os.path.normcase(fh.readline().strip())
-            assert (
-                link_pointer == dist.location
-            ), "Egg-link {} does not match installed location of {} (at {})".format(
-                link_pointer, dist.project_name, dist.location
+            assert link_pointer == dist_location, (
+                f"Egg-link {link_pointer} does not match installed location of "
+                f"{dist.raw_name} (at {dist_location})"
             )
             paths_to_remove.add(develop_egg_link)
             easy_install_pth = os.path.join(
                 os.path.dirname(develop_egg_link), "easy-install.pth"
             )
-            paths_to_remove.add_pth(easy_install_pth, dist.location)
+            paths_to_remove.add_pth(easy_install_pth, dist_location)
 
         else:
             logger.debug(
                 "Not sure how to uninstall: %s - Check: %s",
                 dist,
-                dist.location,
+                dist_location,
             )
 
+        if dist.in_usersite:
+            bin_dir = get_bin_user()
+        else:
+            bin_dir = get_bin_prefix()
+
         # find distutils scripts= scripts
-        if dist.has_metadata("scripts") and dist.metadata_isdir("scripts"):
-            for script in dist.metadata_listdir("scripts"):
-                if dist_in_usersite(dist):
-                    bin_dir = get_bin_user()
-                else:
-                    bin_dir = get_bin_prefix()
-                paths_to_remove.add(os.path.join(bin_dir, script))
+        try:
+            for script in dist.iterdir("scripts"):
+                paths_to_remove.add(os.path.join(bin_dir, script.name))
                 if WINDOWS:
-                    paths_to_remove.add(os.path.join(bin_dir, script) + ".bat")
-
-        # find console_scripts
-        _scripts_to_remove = []
-        console_scripts = dist.get_entry_map(group="console_scripts")
-        for name in console_scripts.keys():
-            _scripts_to_remove.extend(_script_names(dist, name, False))
-        # find gui_scripts
-        gui_scripts = dist.get_entry_map(group="gui_scripts")
-        for name in gui_scripts.keys():
-            _scripts_to_remove.extend(_script_names(dist, name, True))
-
-        for s in _scripts_to_remove:
+                    paths_to_remove.add(os.path.join(bin_dir, f"{script.name}.bat"))
+        except (FileNotFoundError, NotADirectoryError):
+            pass
+
+        # find console_scripts and gui_scripts
+        def iter_scripts_to_remove(
+            dist: BaseDistribution,
+            bin_dir: str,
+        ) -> Iterator[str]:
+            for entry_point in dist.iter_entry_points():
+                if entry_point.group == "console_scripts":
+                    yield from _script_names(bin_dir, entry_point.name, False)
+                elif entry_point.group == "gui_scripts":
+                    yield from _script_names(bin_dir, entry_point.name, True)
+
+        for s in iter_scripts_to_remove(dist, bin_dir):
             paths_to_remove.add(s)
 
         return paths_to_remove
diff --git a/src/pip/_internal/resolution/legacy/resolver.py b/src/pip/_internal/resolution/legacy/resolver.py
index 09caaa6d5..8c149d437 100644
--- a/src/pip/_internal/resolution/legacy/resolver.py
+++ b/src/pip/_internal/resolution/legacy/resolver.py
@@ -43,7 +43,7 @@ from pip._internal.req.req_set import RequirementSet
 from pip._internal.resolution.base import BaseResolver, InstallRequirementProvider
 from pip._internal.utils.compatibility_tags import get_supported
 from pip._internal.utils.logging import indent_log
-from pip._internal.utils.misc import dist_in_usersite, normalize_version_info
+from pip._internal.utils.misc import normalize_version_info
 from pip._internal.utils.packaging import check_requires_python
 
 logger = logging.getLogger(__name__)
@@ -203,7 +203,7 @@ class Resolver(BaseResolver):
         """
         # Don't uninstall the conflict if doing a user install and the
         # conflict is not a user install.
-        if not self.use_user_site or dist_in_usersite(req.satisfied_by):
+        if not self.use_user_site or req.satisfied_by.in_usersite:
             req.should_reinstall = True
         req.satisfied_by = None
 
diff --git a/src/pip/_internal/resolution/resolvelib/candidates.py b/src/pip/_internal/resolution/resolvelib/candidates.py
index 60fad55db..c049255fc 100644
--- a/src/pip/_internal/resolution/resolvelib/candidates.py
+++ b/src/pip/_internal/resolution/resolvelib/candidates.py
@@ -94,8 +94,6 @@ def make_install_req_from_editable(
 def _make_install_req_from_dist(
     dist: BaseDistribution, template: InstallRequirement
 ) -> InstallRequirement:
-    from pip._internal.metadata.pkg_resources import Distribution as _Dist
-
     if template.req:
         line = str(template.req)
     elif template.link:
@@ -115,7 +113,7 @@ def _make_install_req_from_dist(
             hashes=template.hash_options,
         ),
     )
-    ireq.satisfied_by = cast(_Dist, dist)._dist
+    ireq.satisfied_by = dist
     return ireq
 
 


Test patch:
diff --git a/tests/functional/test_uninstall.py b/tests/functional/test_uninstall.py
index 3adaf438b..2751cc83b 100644
--- a/tests/functional/test_uninstall.py
+++ b/tests/functional/test_uninstall.py
@@ -458,13 +458,14 @@ def test_uninstallpathset_no_paths(caplog):
     Test UninstallPathSet logs notification when there are no paths to
     uninstall
     """
-    from pkg_resources import get_distribution
-
+    from pip._internal.metadata import get_default_environment
     from pip._internal.req.req_uninstall import UninstallPathSet
 
     caplog.set_level(logging.INFO)
 
-    test_dist = get_distribution("pip")
+    test_dist = get_default_environment().get_distribution("pip")
+    assert test_dist is not None, "pip not installed"
+
     uninstall_set = UninstallPathSet(test_dist)
     uninstall_set.remove()  # with no files added to set
 
diff --git a/tests/unit/test_appdirs.py b/tests/unit/test_appdirs.py
index cdb15e7a1..71301b3bf 100644
--- a/tests/unit/test_appdirs.py
+++ b/tests/unit/test_appdirs.py
@@ -62,7 +62,7 @@ class TestUserCacheDir:
         if sys.platform != "win32":
             return
 
-        def my_get_win_folder(csidl_name):
+        def my_get_win_folder(csidl_name: str) -> str:
             return "\u00DF\u00E4\u03B1\u20AC"
 
         monkeypatch.setattr(platformdirs.windows, "get_win_folder", my_get_win_folder)
diff --git a/tests/unit/test_finder.py b/tests/unit/test_finder.py
index 34720d54e..f8646da4f 100644
--- a/tests/unit/test_finder.py
+++ b/tests/unit/test_finder.py
@@ -5,7 +5,7 @@ from unittest.mock import Mock, patch
 import pytest
 from pip._vendor.packaging.specifiers import SpecifierSet
 from pip._vendor.packaging.tags import Tag
-from pkg_resources import parse_version
+from pip._vendor.packaging.version import parse as parse_version
 
 import pip._internal.utils.compatibility_tags
 from pip._internal.exceptions import BestVersionAlreadyInstalled, DistributionNotFound
@@ -86,8 +86,7 @@ def test_finder_detects_latest_already_satisfied_find_links(data: TestData) -> N
     latest_version = "3.0"
     satisfied_by = Mock(
         location="/path",
-        parsed_version=parse_version(latest_version),
-        version=latest_version,
+        version=parse_version(latest_version),
     )
     req.satisfied_by = satisfied_by
     finder = make_test_finder(find_links=[data.find_links])
@@ -104,8 +103,7 @@ def test_finder_detects_latest_already_satisfied_pypi_links() -> None:
     latest_version = "0.3.1"
     satisfied_by = Mock(
         location="/path",
-        parsed_version=parse_version(latest_version),
-        version=latest_version,
+        version=parse_version(latest_version),
     )
     req.satisfied_by = satisfied_by
     finder = make_test_finder(index_urls=["http://pypi.org/simple/"])
@@ -185,8 +183,7 @@ class TestWheel:
         latest_version = "1.0"
         satisfied_by = Mock(
             location="/path",
-            parsed_version=parse_version(latest_version),
-            version=latest_version,
+            version=parse_version(latest_version),
         )
         req.satisfied_by = satisfied_by
         finder = make_test_finder(find_links=[data.find_links])
diff --git a/tests/unit/test_locations.py b/tests/unit/test_locations.py
index 640be7f0d..bb58e9a13 100644
--- a/tests/unit/test_locations.py
+++ b/tests/unit/test_locations.py
@@ -54,7 +54,7 @@ class TestLocations:
         os.fstat = lambda fd: self.get_mock_fstat(fd)
 
         if sys.platform != "win32":
-            pwd.getpwuid = lambda uid: self.get_mock_getpwuid(uid)
+            pwd.getpwuid = self.get_mock_getpwuid
 
     def revert_patch(self) -> None:
         """revert the patches to python methods"""
@@ -74,7 +74,7 @@ class TestLocations:
         result.st_uid = self.st_uid
         return result
 
-    def get_mock_getpwuid(self, uid: int) -> pwd.struct_passwd:
+    def get_mock_getpwuid(self, uid: int) -> Any:
         """returns a basic mock pwd.getpwuid call result.
         Currently only the pw_name attribute has been set.
         """
diff --git a/tests/unit/test_req.py b/tests/unit/test_req.py
index c4b97cf1c..53d9c9ef3 100644
--- a/tests/unit/test_req.py
+++ b/tests/unit/test_req.py
@@ -9,7 +9,6 @@ from typing import Iterator, Tuple, cast
 from unittest import mock
 
 import pytest
-from pip._vendor import pkg_resources
 from pip._vendor.packaging.markers import Marker
 from pip._vendor.packaging.requirements import Requirement
 
@@ -22,6 +21,7 @@ from pip._internal.exceptions import (
     PreviousBuildDirError,
 )
 from pip._internal.index.package_finder import PackageFinder
+from pip._internal.metadata.pkg_resources import Distribution
 from pip._internal.network.session import PipSession
 from pip._internal.operations.prepare import RequirementPreparer
 from pip._internal.req import InstallRequirement, RequirementSet
@@ -451,8 +451,8 @@ class TestInstallRequirement:
         req = install_req_from_line("foo")
         req.metadata_directory = path
         dist = req.get_dist()
-        assert isinstance(dist, pkg_resources.Distribution)
-        assert dist.project_name == "foo"
+        assert isinstance(dist, Distribution)
+        assert dist.raw_name == dist.canonical_name == "foo"
         assert dist.location == "/path/to".replace("/", os.path.sep)
 
     def test_markers(self) -> None:
diff --git a/tests/unit/test_req_uninstall.py b/tests/unit/test_req_uninstall.py
index ff154a5da..b3049e103 100644
--- a/tests/unit/test_req_uninstall.py
+++ b/tests/unit/test_req_uninstall.py
@@ -1,6 +1,6 @@
 import os
 import sys
-from typing import Iterator, List, Tuple
+from typing import Iterator, List, Optional, Tuple
 from unittest.mock import Mock
 
 import pytest
@@ -27,8 +27,11 @@ def mock_is_local(path: str) -> bool:
 
 def test_uninstallation_paths() -> None:
     class dist:
-        def get_metadata_lines(self, record: str) -> List[str]:
-            return ["file.py,,", "file.pyc,,", "file.so,,", "nopyc.py"]
+        def iter_declared_entries(self) -> Optional[Iterator[str]]:
+            yield "file.py"
+            yield "file.pyc"
+            yield "file.so"
+            yield "nopyc.py"
 
         location = ""
 
@@ -134,12 +137,12 @@ class TestUninstallPathSet:
             pass
 
         ups = UninstallPathSet(dist=Mock())
-        assert ups.paths == set()
+        assert ups._paths == set()
         ups.add(file_extant)
-        assert ups.paths == {file_extant}
+        assert ups._paths == {file_extant}
 
         ups.add(file_nonexistent)
-        assert ups.paths == {file_extant}
+        assert ups._paths == {file_extant}
 
     def test_add_pth(self, tmpdir: str, monkeypatch: pytest.MonkeyPatch) -> None:
         monkeypatch.setattr(pip._internal.req.req_uninstall, "is_local", mock_is_local)
@@ -181,7 +184,7 @@ class TestUninstallPathSet:
 
         ups = UninstallPathSet(dist=Mock())
         ups.add(foo_link)
-        assert ups.paths == {foo_link}
+        assert ups._paths == {foo_link}
 
     def test_compact_shorter_path(self, monkeypatch: pytest.MonkeyPatch) -> None:
         monkeypatch.setattr(pip._internal.req.req_uninstall, "is_local", mock_is_local)
@@ -193,7 +196,7 @@ class TestUninstallPathSet:
         ups = UninstallPathSet(dist=Mock())
         ups.add(short_path)
         ups.add(os.path.join(short_path, "longer"))
-        assert compact(ups.paths) == {short_path}
+        assert compact(ups._paths) == {short_path}
 
     @pytest.mark.skipif("sys.platform == 'win32'")
     def test_detect_symlink_dirs(
@@ -215,7 +218,7 @@ class TestUninstallPathSet:
         ups = UninstallPathSet(dist=Mock())
         ups.add(path1)
         ups.add(path2)
-        assert ups.paths == {path1}
+        assert ups._paths == {path1}
 
 
 class TestStashedUninstallPathSet: