Repository: pypa/pip
Pull request #10717 (2021-12-08 19:39:30)

Fallback to pep517 if setup.py is present and setuptools cannot be imported
This is WIP PR without any documentation or tests. I'd like to first know your opinion whether the idea is acceptable. Thank you. 

<!---
Thank you for your soon to be pull request. Before you submit this, please
double check to make sure that you've added a news file fragment. In pip we
generate our NEWS.rst from multiple news fragment files, and all pull requests
require either a news file fragment or a marker to indicate they don't require
one.

To read more about adding a news file fragment for your PR, please check out
our documentation at: https://pip.pypa.io/en/latest/development/contributing/#news-entries
-->


pfmoore: I'm inclined to agree with the suggestion @uranusjr made on Discourse - if there's no `setup.py` and `setuptools` cannot be imported, the only possible thing that will work is to use PEP 517, so why not do that? Which means this PR can simply be "use PEP 517 if setuptools cannot be imported".
hroncok: I'd argue that when the user explicitly used `--no-pep517` and setuptools is not installed, that deserves an error.
hroncok: That combined basically means something like:

```
... elif not has_setuptools:
    if use_pep517 is not None and not use_pep517:
            raise InstallationError(
                "Disabling PEP 517 processing is invalid: "
                "setuptools not installed"
            )
    use_pep517 = True
```
uranusjr: The “can’t do `--no-use-pep517` because setuptools is not found” error can even be raised as early as command argument parsing, instead of half-way into package installation.
pradyunsg: FWIW, I was hoping to go down a different avenue here: https://github.com/pypa/pip/commit/dc217bbb69d29e4cfae35eb0a0a744701c83b7bd

That present a clearer error message instead of opting these projects into PEP 517.
hroncok: At least to me, opting to PEP 517 when setuptools is not installed sounds like a more user-friendly thing to do than erroring out (and gradually downloading a lesser and lesser version of the package only to error again and again).
pradyunsg: Yea... #10655 is annoying.

It seems to me that there's reasonable agreement that we should stop backtracking on build failures, which combined with [better presentation of build failures](https://github.com/pypa/pip/compare/main...pradyunsg:better-subprocess-errors) (I'll file this as a PR once #10703 merges) could be a more "complete" solution here; rather than needing to change this one special case?

hroncok: Even without backtracking, I still think that using PEP 517 when setuptools is not present is the nicer thing to do, but I won't fight you on that. A better error report and no backtracking makes this good enough for me.
pradyunsg: Thinking about this a bit more, I think we should do all three things: this PR, #10722 and #10723. They're all better behaviours, and I think it's totally reasonable for us to do all of them.
hrnciar: I was trying to solve the CI issues, but I ran into some test failures. I don't think they are caused by this PR, because I am getting the same failures when I run the tests on the main branch locally. If that's the case, I think PR is ready for review.
uranusjr: Those are caused by #10742, don’t mind them.
hroncok: Is this ready to be merged, or do we need to change something?
hroncok: What si the next step here?
uranusjr: Anyone else wants to take a look? If not I’m inclined to include this in the 22.1 release.
hroncok: > I would still prefer this not be wrapped. 

So, use a long line or split the string literal?
pfmoore: > use a long line or split the string literal?

Ultimately whatever stops black whining about the layout, I don't really care 🙂 If I were writing this without regard for black, I'd use a long line.
uranusjr: Come one Black why can’t you do all the recommendations in one go.

Patch:
diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml
index 85b575c3e..f0b8ba644 100644
--- a/.github/workflows/ci.yml
+++ b/.github/workflows/ci.yml
@@ -218,3 +218,38 @@ jobs:
           --verbose --numprocesses auto --showlocals
         env:
           TEMP: "R:\\Temp"
+
+  tests-importlib-metadata:
+    name: tests for importlib.metadata backend
+    runs-on: ubuntu-latest
+    env:
+      _PIP_METADATA_BACKEND_IMPORTLIB: egg-compat
+
+    needs: [pre-commit, packaging, determine-changes]
+    if: >-
+      needs.determine-changes.outputs.tests == 'true' ||
+      github.event_name != 'pull_request'
+
+    steps:
+      - uses: actions/checkout@v2
+      - uses: actions/setup-python@v2
+        with:
+          python-version: '3.10'
+
+      - name: Install Ubuntu dependencies
+        run: sudo apt-get install bzr
+
+      - run: pip install nox 'virtualenv<20'
+
+      # Main check
+      - name: Run unit tests
+        run: >-
+          nox -s test-3.10 --
+          -m unit
+          --verbose --numprocesses auto --showlocals
+      - name: Run integration tests
+        run: >-
+          nox -s test-3.10 --
+          -m integration
+          --verbose --numprocesses auto --showlocals
+          --durations=5
diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml
index 37a987b30..4d88637ff 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.1.0
+  rev: v4.2.0
   hooks:
   - id: check-builtin-literals
   - id: check-added-large-files
@@ -26,9 +26,9 @@ repos:
   hooks:
   - id: flake8
     additional_dependencies: [
-        'flake8-bugbear==20.1.4',
+        'flake8-bugbear==22.3.23',
         'flake8-logging-format==0.6.0',
-        'flake8-implicit-str-concat==0.2.0',
+        'flake8-implicit-str-concat==0.3.0',
     ]
     exclude: tests/data
 
@@ -47,10 +47,11 @@ repos:
     additional_dependencies: [
         'keyring==23.0.1',
         'nox==2021.6.12',
-        'pytest==6.2.5',
-        'types-docutils==0.1.8',
-        'types-setuptools==57.0.2',
-        'types-six==0.1.9',
+        'pytest==7.1.1',
+        'types-docutils==0.18.3',
+        'types-setuptools==57.4.14',
+        'types-freezegun==1.1.9',
+        'types-six==1.16.15',
     ]
 
 - repo: https://github.com/pre-commit/pygrep-hooks
diff --git a/docs/html/user_guide.rst b/docs/html/user_guide.rst
index 13e8ec423..6a25a6e6a 100644
--- a/docs/html/user_guide.rst
+++ b/docs/html/user_guide.rst
@@ -496,6 +496,10 @@ To setup for fish::
 
     python -m pip completion --fish > ~/.config/fish/completions/pip.fish
 
+To setup for powershell::
+
+   python -m pip completion --powershell | Out-File -Encoding default -Append $PROFILE
+
 Alternatively, you can use the result of the ``completion`` command directly
 with the eval function of your shell, e.g. by adding the following to your
 startup file::
diff --git a/news/10990.feature.rst b/news/10556.feature.rst
similarity index 100%
rename from news/10990.feature.rst
rename to news/10556.feature.rst
diff --git a/news/10709.process.rst b/news/10709.process.rst
new file mode 100644
index 000000000..538671e66
--- /dev/null
+++ b/news/10709.process.rst
@@ -0,0 +1,3 @@
+Start migration of distribution metadata implementation from ``pkg_resources``
+to ``importlib.metadata``. The new implementation is currently not exposed in
+any user-facing way, but included in the code base for easier development.
diff --git a/news/10717.bugfix.rst b/news/10717.bugfix.rst
new file mode 100644
index 000000000..950a45217
--- /dev/null
+++ b/news/10717.bugfix.rst
@@ -0,0 +1,1 @@
+Fallback to pyproject.toml-based builds if ``setup.py`` is present in a project, but ``setuptools`` cannot be imported.
diff --git a/news/10883.bugfix.rst b/news/10883.bugfix.rst
new file mode 100644
index 000000000..3807524c1
--- /dev/null
+++ b/news/10883.bugfix.rst
@@ -0,0 +1,2 @@
+When checking for conflicts in the build environment, correctly skip requirements
+containing markers that do not match the current environment.
diff --git a/news/9024.feature.rst b/news/9024.feature.rst
new file mode 100644
index 000000000..2ec0a5ab9
--- /dev/null
+++ b/news/9024.feature.rst
@@ -0,0 +1,1 @@
+Add support for Powershell autocompletion.
diff --git a/news/pyparsing.vendor.rst b/news/pyparsing.vendor.rst
new file mode 100644
index 000000000..b81eb3390
--- /dev/null
+++ b/news/pyparsing.vendor.rst
@@ -0,0 +1,1 @@
+Upgrade ``pyparsing`` to 3.0.8 for startup performance improvements.
diff --git a/setup.cfg b/setup.cfg
index ae6aa38d8..98f8a5f92 100644
--- a/setup.cfg
+++ b/setup.cfg
@@ -24,6 +24,10 @@ extend-ignore =
     G200, G202,
     # black adds spaces around ':'
     E203,
+    # using a cache
+    B019,
+    # reassigning variables in a loop
+    B020,
 per-file-ignores =
     # G: The plugin logging-format treats every .log and .error as logging.
     noxfile.py: G
diff --git a/src/pip/_internal/build_env.py b/src/pip/_internal/build_env.py
index 65d3f2176..7783c9acd 100644
--- a/src/pip/_internal/build_env.py
+++ b/src/pip/_internal/build_env.py
@@ -175,6 +175,8 @@ class BuildEnvironment:
             )
             for req_str in reqs:
                 req = Requirement(req_str)
+                if req.marker is not None and not req.marker.evaluate():
+                    continue  # FIXME: Consider extras?
                 dist = env.get_distribution(req.name)
                 if not dist:
                     missing.add(req_str)
diff --git a/src/pip/_internal/cli/cmdoptions.py b/src/pip/_internal/cli/cmdoptions.py
index 3612dc456..91b94e302 100644
--- a/src/pip/_internal/cli/cmdoptions.py
+++ b/src/pip/_internal/cli/cmdoptions.py
@@ -10,6 +10,7 @@ pass on state. To be consistent, all options will follow this design.
 # The following comment should be removed at some point in the future.
 # mypy: strict-optional=False
 
+import importlib.util
 import logging
 import os
 import textwrap
@@ -770,6 +771,12 @@ def _handle_no_use_pep517(
         """
         raise_option_error(parser, option=option, msg=msg)
 
+    # If user doesn't wish to use pep517, we check if setuptools is installed
+    # and raise error if it is not.
+    if not importlib.util.find_spec("setuptools"):
+        msg = "It is not possible to use --no-use-pep517 without setuptools installed."
+        raise_option_error(parser, option=option, msg=msg)
+
     # Otherwise, --no-use-pep517 was passed via the command-line.
     parser.values.use_pep517 = False
 
@@ -853,13 +860,13 @@ disable_pip_version_check: Callable[..., Option] = partial(
     "of pip is available for download. Implied with --no-index.",
 )
 
-warn_about_root_user: Callable[..., Option] = partial(
+root_user_action: Callable[..., Option] = partial(
     Option,
-    "--no-warn-when-using-as-a-root-user",
-    dest="warn_about_root_user",
-    default=True,
-    action="store_false",
-    help="Do not warn when used as a root user",
+    "--root-user-action",
+    dest="root_user_action",
+    default="warn",
+    choices=["warn", "ignore"],
+    help="Action if pip is run as a root user. By default, a warning message is shown.",
 )
 
 
diff --git a/src/pip/_internal/commands/completion.py b/src/pip/_internal/commands/completion.py
index c0fb4caf8..deaa30899 100644
--- a/src/pip/_internal/commands/completion.py
+++ b/src/pip/_internal/commands/completion.py
@@ -43,6 +43,28 @@ COMPLETION_SCRIPTS = {
         end
         complete -fa "(__fish_complete_pip)" -c {prog}
     """,
+    "powershell": """
+        if ((Test-Path Function:\\TabExpansion) -and -not `
+            (Test-Path Function:\\_pip_completeBackup)) {{
+            Rename-Item Function:\\TabExpansion _pip_completeBackup
+        }}
+        function TabExpansion($line, $lastWord) {{
+            $lastBlock = [regex]::Split($line, '[|;]')[-1].TrimStart()
+            if ($lastBlock.StartsWith("{prog} ")) {{
+                $Env:COMP_WORDS=$lastBlock
+                $Env:COMP_CWORD=$lastBlock.Split().Length - 1
+                $Env:PIP_AUTO_COMPLETE=1
+                (& {prog}).Split()
+                Remove-Item Env:COMP_WORDS
+                Remove-Item Env:COMP_CWORD
+                Remove-Item Env:PIP_AUTO_COMPLETE
+            }}
+            elseif (Test-Path Function:\\_pip_completeBackup) {{
+                # Fall back on existing tab expansion
+                _pip_completeBackup $line $lastWord
+            }}
+        }}
+    """,
 }
 
 
@@ -76,6 +98,14 @@ class CompletionCommand(Command):
             dest="shell",
             help="Emit completion code for fish",
         )
+        self.cmd_opts.add_option(
+            "--powershell",
+            "-p",
+            action="store_const",
+            const="powershell",
+            dest="shell",
+            help="Emit completion code for powershell",
+        )
 
         self.parser.insert_option_group(0, self.cmd_opts)
 
diff --git a/src/pip/_internal/commands/install.py b/src/pip/_internal/commands/install.py
index 1a5b40b56..75c8b143c 100644
--- a/src/pip/_internal/commands/install.py
+++ b/src/pip/_internal/commands/install.py
@@ -227,7 +227,7 @@ class InstallCommand(RequirementCommand):
         self.cmd_opts.add_option(cmdoptions.prefer_binary())
         self.cmd_opts.add_option(cmdoptions.require_hashes())
         self.cmd_opts.add_option(cmdoptions.progress_bar())
-        self.cmd_opts.add_option(cmdoptions.warn_about_root_user())
+        self.cmd_opts.add_option(cmdoptions.root_user_action())
 
         index_opts = cmdoptions.make_option_group(
             cmdoptions.index_group,
@@ -464,7 +464,7 @@ class InstallCommand(RequirementCommand):
             self._handle_target_dir(
                 options.target_dir, target_temp_dir, options.upgrade
             )
-        if options.warn_about_root_user:
+        if options.root_user_action == "warn":
             warn_if_run_as_root()
         return SUCCESS
 
diff --git a/src/pip/_internal/commands/show.py b/src/pip/_internal/commands/show.py
index d1e7daef0..fe51c3f7d 100644
--- a/src/pip/_internal/commands/show.py
+++ b/src/pip/_internal/commands/show.py
@@ -76,7 +76,7 @@ def search_packages_info(query: List[str]) -> Generator[_PackageInfo, None, None
     """
     env = get_default_environment()
 
-    installed = {dist.canonical_name: dist for dist in env.iter_distributions()}
+    installed = {dist.canonical_name: dist for dist in env.iter_all_distributions()}
     query_names = [canonicalize_name(name) for name in query]
     missing = sorted(
         [name for name, pkg in zip(query, query_names) if pkg not in installed]
diff --git a/src/pip/_internal/commands/uninstall.py b/src/pip/_internal/commands/uninstall.py
index 45cf2e7f4..dea8077e7 100644
--- a/src/pip/_internal/commands/uninstall.py
+++ b/src/pip/_internal/commands/uninstall.py
@@ -54,7 +54,7 @@ class UninstallCommand(Command, SessionCommandMixin):
             action="store_true",
             help="Don't ask for confirmation of uninstall deletions.",
         )
-        self.cmd_opts.add_option(cmdoptions.warn_about_root_user())
+        self.cmd_opts.add_option(cmdoptions.root_user_action())
         self.parser.insert_option_group(0, self.cmd_opts)
 
     def run(self, options: Values, args: List[str]) -> int:
@@ -101,6 +101,6 @@ class UninstallCommand(Command, SessionCommandMixin):
             )
             if uninstall_pathset:
                 uninstall_pathset.commit()
-        if options.warn_about_root_user:
+        if options.root_user_action == "warn":
             warn_if_run_as_root()
         return SUCCESS
diff --git a/src/pip/_internal/metadata/__init__.py b/src/pip/_internal/metadata/__init__.py
index cc037c14f..aa9a80822 100644
--- a/src/pip/_internal/metadata/__init__.py
+++ b/src/pip/_internal/metadata/__init__.py
@@ -1,7 +1,14 @@
-from typing import List, Optional
+import functools
+import os
+from typing import TYPE_CHECKING, List, Optional, Type, cast
 
 from .base import BaseDistribution, BaseEnvironment, FilesystemWheel, MemoryWheel, Wheel
 
+if TYPE_CHECKING:
+    from typing import Protocol
+else:
+    Protocol = object
+
 __all__ = [
     "BaseDistribution",
     "BaseEnvironment",
@@ -11,9 +18,26 @@ __all__ = [
     "get_default_environment",
     "get_environment",
     "get_wheel_distribution",
+    "select_backend",
 ]
 
 
+class Backend(Protocol):
+    Distribution: Type[BaseDistribution]
+    Environment: Type[BaseEnvironment]
+
+
+@functools.lru_cache(maxsize=None)
+def select_backend() -> Backend:
+    if os.environ.get("_PIP_METADATA_BACKEND_IMPORTLIB"):
+        from . import importlib
+
+        return cast(Backend, importlib)
+    from . import pkg_resources
+
+    return cast(Backend, pkg_resources)
+
+
 def get_default_environment() -> BaseEnvironment:
     """Get the default representation for the current environment.
 
@@ -21,9 +45,7 @@ def get_default_environment() -> BaseEnvironment:
     Environment instance should be built from ``sys.path`` and may use caching
     to share instance state accorss calls.
     """
-    from .pkg_resources import Environment
-
-    return Environment.default()
+    return select_backend().Environment.default()
 
 
 def get_environment(paths: Optional[List[str]]) -> BaseEnvironment:
@@ -33,9 +55,7 @@ def get_environment(paths: Optional[List[str]]) -> BaseEnvironment:
     given import paths. The backend must build a fresh instance representing
     the state of installed distributions when this function is called.
     """
-    from .pkg_resources import Environment
-
-    return Environment.from_paths(paths)
+    return select_backend().Environment.from_paths(paths)
 
 
 def get_directory_distribution(directory: str) -> BaseDistribution:
@@ -44,9 +64,7 @@ def get_directory_distribution(directory: str) -> BaseDistribution:
     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)
+    return select_backend().Distribution.from_directory(directory)
 
 
 def get_wheel_distribution(wheel: Wheel, canonical_name: str) -> BaseDistribution:
@@ -57,6 +75,4 @@ def get_wheel_distribution(wheel: Wheel, canonical_name: str) -> BaseDistributio
 
     :param canonical_name: Normalized project name of the given wheel.
     """
-    from .pkg_resources import Distribution
-
-    return Distribution.from_wheel(wheel, canonical_name)
+    return select_backend().Distribution.from_wheel(wheel, canonical_name)
diff --git a/src/pip/_internal/metadata/base.py b/src/pip/_internal/metadata/base.py
index 7586dfc46..f1a1ee62f 100644
--- a/src/pip/_internal/metadata/base.py
+++ b/src/pip/_internal/metadata/base.py
@@ -10,7 +10,6 @@ from typing import (
     TYPE_CHECKING,
     Collection,
     Container,
-    Generator,
     Iterable,
     Iterator,
     List,
@@ -32,10 +31,7 @@ from pip._internal.models.direct_url import (
     DirectUrlValidationError,
 )
 from pip._internal.utils.compat import stdlib_pkgs  # TODO: Move definition here.
-from pip._internal.utils.egg_link import (
-    egg_link_path_from_location,
-    egg_link_path_from_sys_path,
-)
+from pip._internal.utils.egg_link import egg_link_path_from_sys_path
 from pip._internal.utils.misc import is_local, normalize_path
 from pip._internal.utils.urls import url_to_path
 
@@ -46,7 +42,7 @@ else:
 
 DistributionVersion = Union[LegacyVersion, Version]
 
-InfoPath = Union[str, pathlib.PurePosixPath]
+InfoPath = Union[str, pathlib.PurePath]
 
 logger = logging.getLogger(__name__)
 
@@ -96,6 +92,28 @@ def _convert_installed_files_path(
 
 
 class BaseDistribution(Protocol):
+    @classmethod
+    def from_directory(cls, directory: str) -> "BaseDistribution":
+        """Load the distribution from a metadata directory.
+
+        :param directory: Path to a metadata directory, e.g. ``.dist-info``.
+        """
+        raise NotImplementedError()
+
+    @classmethod
+    def from_wheel(cls, wheel: "Wheel", name: str) -> "BaseDistribution":
+        """Load the distribution from a given wheel.
+
+        :param wheel: A concrete wheel definition.
+        :param name: File name of the wheel.
+
+        :raises InvalidWheel: Whenever loading of the wheel causes a
+            :py:exc:`zipfile.BadZipFile` exception to be thrown.
+        :raises UnsupportedWheel: If the wheel is a valid zip, but malformed
+            internally.
+        """
+        raise NotImplementedError()
+
     def __repr__(self) -> str:
         return f"{self.raw_name} {self.version} ({self.location})"
 
@@ -149,14 +167,7 @@ class BaseDistribution(Protocol):
 
         The returned location is normalized (in particular, with symlinks removed).
         """
-        egg_link = egg_link_path_from_location(self.raw_name)
-        if egg_link:
-            location = egg_link
-        elif self.location:
-            location = self.location
-        else:
-            return None
-        return normalize_path(location)
+        raise NotImplementedError()
 
     @property
     def info_location(self) -> Optional[str]:
@@ -317,21 +328,19 @@ class BaseDistribution(Protocol):
         """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.
+    def iter_distutils_script_names(self) -> Iterator[str]:
+        """Find distutils 'scripts' entries metadata.
 
-        Each item yielded would be a path relative to the info directory.
-
-        :raise FileNotFoundError: If ``name`` does not exist in the directory.
-        :raise NotADirectoryError: If ``name`` does not point to a directory.
+        If 'scripts' is supplied in ``setup.py``, distutils records those in the
+        installed distribution's ``scripts`` directory, a file for each script.
         """
         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 NoneMetadataError: If ``name`` exists in the info directory, but
+        :raise FileNotFoundError: If ``path`` does not exist in the directory.
+        :raise NoneMetadataError: If ``path`` exists in the info directory, but
             cannot be read.
         """
         raise NotImplementedError()
@@ -471,8 +480,8 @@ class BaseEnvironment:
         """
         raise NotImplementedError()
 
-    def iter_distributions(self) -> Generator["BaseDistribution", None, None]:
-        """Iterate through installed distributions."""
+    def iter_all_distributions(self) -> Iterator[BaseDistribution]:
+        """Iterate through all installed distributions without any filtering."""
         for dist in self._iter_distributions():
             # Make sure the distribution actually comes from a valid Python
             # packaging distribution. Pip's AdjacentTempDirectory leaves folders
@@ -502,6 +511,11 @@ class BaseEnvironment:
     ) -> Iterator[BaseDistribution]:
         """Return a list of installed distributions.
 
+        This is based on ``iter_all_distributions()`` with additional filtering
+        options. Note that ``iter_installed_distributions()`` without arguments
+        is *not* equal to ``iter_all_distributions()``, since some of the
+        configurations exclude packages by default.
+
         :param local_only: If True (default), only return installations
         local to the current virtualenv, if in a virtualenv.
         :param skip: An iterable of canonicalized project names to ignore;
@@ -511,7 +525,7 @@ class BaseEnvironment:
         :param user_only: If True, only report installations in the user
         site directory.
         """
-        it = self.iter_distributions()
+        it = self.iter_all_distributions()
         if local_only:
             it = (d for d in it if d.local)
         if not include_editables:
diff --git a/src/pip/_internal/metadata/importlib/__init__.py b/src/pip/_internal/metadata/importlib/__init__.py
new file mode 100644
index 000000000..5e7af9fe5
--- /dev/null
+++ b/src/pip/_internal/metadata/importlib/__init__.py
@@ -0,0 +1,4 @@
+from ._dists import Distribution
+from ._envs import Environment
+
+__all__ = ["Distribution", "Environment"]
diff --git a/src/pip/_internal/metadata/importlib/_compat.py b/src/pip/_internal/metadata/importlib/_compat.py
new file mode 100644
index 000000000..2bc6bfd27
--- /dev/null
+++ b/src/pip/_internal/metadata/importlib/_compat.py
@@ -0,0 +1,41 @@
+import importlib.metadata
+from typing import Any, Optional, Protocol, cast
+
+
+class BasePath(Protocol):
+    """A protocol that various path objects conform.
+
+    This exists because importlib.metadata uses both ``pathlib.Path`` and
+    ``zipfile.Path``, and we need a common base for type hints (Union does not
+    work well since ``zipfile.Path`` is too new for our linter setup).
+
+    This does not mean to be exhaustive, but only contains things that present
+    in both classes *that we need*.
+    """
+
+    name: str
+
+    @property
+    def parent(self) -> "BasePath":
+        raise NotImplementedError()
+
+
+def get_info_location(d: importlib.metadata.Distribution) -> Optional[BasePath]:
+    """Find the path to the distribution's metadata directory.
+
+    HACK: This relies on importlib.metadata's private ``_path`` attribute. Not
+    all distributions exist on disk, so importlib.metadata is correct to not
+    expose the attribute as public. But pip's code base is old and not as clean,
+    so we do this to avoid having to rewrite too many things. Hopefully we can
+    eliminate this some day.
+    """
+    return getattr(d, "_path", None)
+
+
+def get_dist_name(dist: importlib.metadata.Distribution) -> str:
+    """Get the distribution's project name.
+
+    The ``name`` attribute is only available in Python 3.10 or later. We are
+    targeting exactly that, but Mypy does not know this.
+    """
+    return cast(Any, dist).name
diff --git a/src/pip/_internal/metadata/importlib/_dists.py b/src/pip/_internal/metadata/importlib/_dists.py
new file mode 100644
index 000000000..cf66de54d
--- /dev/null
+++ b/src/pip/_internal/metadata/importlib/_dists.py
@@ -0,0 +1,274 @@
+import email.message
+import importlib.metadata
+import os
+import pathlib
+import zipfile
+from typing import (
+    Collection,
+    Dict,
+    Iterable,
+    Iterator,
+    Mapping,
+    NamedTuple,
+    Optional,
+    Sequence,
+)
+
+from pip._vendor.packaging.requirements import Requirement
+from pip._vendor.packaging.utils import NormalizedName, canonicalize_name
+from pip._vendor.packaging.version import parse as parse_version
+
+from pip._internal.exceptions import InvalidWheel, UnsupportedWheel
+from pip._internal.metadata.base import (
+    BaseDistribution,
+    BaseEntryPoint,
+    DistributionVersion,
+    InfoPath,
+    Wheel,
+)
+from pip._internal.utils.misc import normalize_path
+from pip._internal.utils.packaging import safe_extra
+from pip._internal.utils.wheel import parse_wheel, read_wheel_metadata_file
+
+from ._compat import BasePath, get_dist_name
+
+
+class WheelDistribution(importlib.metadata.Distribution):
+    """An ``importlib.metadata.Distribution`` read from a wheel.
+
+    Although ``importlib.metadata.PathDistribution`` accepts ``zipfile.Path``,
+    its implementation is too "lazy" for pip's needs (we can't keep the ZipFile
+    handle open for the entire lifetime of the distribution object).
+
+    This implementation eagerly reads the entire metadata directory into the
+    memory instead, and operates from that.
+    """
+
+    def __init__(
+        self,
+        files: Mapping[pathlib.PurePosixPath, bytes],
+        info_location: pathlib.PurePosixPath,
+    ) -> None:
+        self._files = files
+        self.info_location = info_location
+
+    @classmethod
+    def from_zipfile(
+        cls,
+        zf: zipfile.ZipFile,
+        name: str,
+        location: str,
+    ) -> "WheelDistribution":
+        info_dir, _ = parse_wheel(zf, name)
+        paths = (
+            (name, pathlib.PurePosixPath(name.split("/", 1)[-1]))
+            for name in zf.namelist()
+            if name.startswith(f"{info_dir}/")
+        )
+        files = {
+            relpath: read_wheel_metadata_file(zf, fullpath)
+            for fullpath, relpath in paths
+        }
+        info_location = pathlib.PurePosixPath(location, info_dir)
+        return cls(files, info_location)
+
+    def iterdir(self, path: InfoPath) -> Iterator[pathlib.PurePosixPath]:
+        # Only allow iterating through the metadata directory.
+        if pathlib.PurePosixPath(str(path)) in self._files:
+            return iter(self._files)
+        raise FileNotFoundError(path)
+
+    def read_text(self, filename: str) -> Optional[str]:
+        try:
+            data = self._files[pathlib.PurePosixPath(filename)]
+        except KeyError:
+            return None
+        try:
+            text = data.decode("utf-8")
+        except UnicodeDecodeError as e:
+            wheel = self.info_location.parent
+            error = f"Error decoding metadata for {wheel}: {e} in {filename} file"
+            raise UnsupportedWheel(error)
+        return text
+
+
+class RequiresEntry(NamedTuple):
+    requirement: str
+    extra: str
+    marker: str
+
+
+class Distribution(BaseDistribution):
+    def __init__(
+        self,
+        dist: importlib.metadata.Distribution,
+        info_location: Optional[BasePath],
+        installed_location: Optional[BasePath],
+    ) -> None:
+        self._dist = dist
+        self._info_location = info_location
+        self._installed_location = installed_location
+
+    @classmethod
+    def from_directory(cls, directory: str) -> BaseDistribution:
+        info_location = pathlib.Path(directory)
+        dist = importlib.metadata.Distribution.at(info_location)
+        return cls(dist, info_location, info_location.parent)
+
+    @classmethod
+    def from_wheel(cls, wheel: Wheel, name: str) -> BaseDistribution:
+        try:
+            with wheel.as_zipfile() as zf:
+                dist = WheelDistribution.from_zipfile(zf, name, wheel.location)
+        except zipfile.BadZipFile as e:
+            raise InvalidWheel(wheel.location, name) from e
+        except UnsupportedWheel as e:
+            raise UnsupportedWheel(f"{name} has an invalid wheel, {e}")
+        return cls(dist, dist.info_location, pathlib.PurePosixPath(wheel.location))
+
+    @property
+    def location(self) -> Optional[str]:
+        if self._info_location is None:
+            return None
+        return str(self._info_location.parent)
+
+    @property
+    def info_location(self) -> Optional[str]:
+        if self._info_location is None:
+            return None
+        return str(self._info_location)
+
+    @property
+    def installed_location(self) -> Optional[str]:
+        if self._installed_location is None:
+            return None
+        return normalize_path(str(self._installed_location))
+
+    def _get_dist_name_from_location(self) -> Optional[str]:
+        """Try to get the name from the metadata directory name.
+
+        This is much faster than reading metadata.
+        """
+        if self._info_location is None:
+            return None
+        stem, suffix = os.path.splitext(self._info_location.name)
+        if suffix not in (".dist-info", ".egg-info"):
+            return None
+        return stem.split("-", 1)[0]
+
+    @property
+    def canonical_name(self) -> NormalizedName:
+        name = self._get_dist_name_from_location() or get_dist_name(self._dist)
+        return canonicalize_name(name)
+
+    @property
+    def version(self) -> DistributionVersion:
+        return parse_version(self._dist.version)
+
+    def is_file(self, path: InfoPath) -> bool:
+        return self._dist.read_text(str(path)) is not None
+
+    def iter_distutils_script_names(self) -> Iterator[str]:
+        # A distutils installation is always "flat" (not in e.g. egg form), so
+        # if this distribution's info location is NOT a pathlib.Path (but e.g.
+        # zipfile.Path), it can never contain any distutils scripts.
+        if not isinstance(self._info_location, pathlib.Path):
+            return
+        for child in self._info_location.joinpath("scripts").iterdir():
+            yield child.name
+
+    def read_text(self, path: InfoPath) -> str:
+        content = self._dist.read_text(str(path))
+        if content is None:
+            raise FileNotFoundError(path)
+        return content
+
+    def iter_entry_points(self) -> Iterable[BaseEntryPoint]:
+        # importlib.metadata's EntryPoint structure sasitfies BaseEntryPoint.
+        return self._dist.entry_points
+
+    @property
+    def metadata(self) -> email.message.Message:
+        return self._dist.metadata
+
+    def _iter_requires_txt_entries(self) -> Iterator[RequiresEntry]:
+        """Parse a ``requires.txt`` in an egg-info directory.
+
+        This is an INI-ish format where an egg-info stores dependencies. A
+        section name describes extra other environment markers, while each entry
+        is an arbitrary string (not a key-value pair) representing a dependency
+        as a requirement string (no markers).
+
+        There is a construct in ``importlib.metadata`` called ``Sectioned`` that
+        does mostly the same, but the format is currently considered private.
+        """
+        content = self._dist.read_text("requires.txt")
+        if content is None:
+            return
+        extra = marker = ""  # Section-less entries don't have markers.
+        for line in content.splitlines():
+            line = line.strip()
+            if not line or line.startswith("#"):  # Comment; ignored.
+                continue
+            if line.startswith("[") and line.endswith("]"):  # A section header.
+                extra, _, marker = line.strip("[]").partition(":")
+                continue
+            yield RequiresEntry(requirement=line, extra=extra, marker=marker)
+
+    def _iter_egg_info_extras(self) -> Iterable[str]:
+        """Get extras from the egg-info directory."""
+        known_extras = {""}
+        for entry in self._iter_requires_txt_entries():
+            if entry.extra in known_extras:
+                continue
+            known_extras.add(entry.extra)
+            yield entry.extra
+
+    def iter_provided_extras(self) -> Iterable[str]:
+        iterator = (
+            self._dist.metadata.get_all("Provides-Extra")
+            or self._iter_egg_info_extras()
+        )
+        return (safe_extra(extra) for extra in iterator)
+
+    def _iter_egg_info_dependencies(self) -> Iterable[str]:
+        """Get distribution dependencies from the egg-info directory.
+
+        To ease parsing, this converts a legacy dependency entry into a PEP 508
+        requirement string. Like ``_iter_requires_txt_entries()``, there is code
+        in ``importlib.metadata`` that does mostly the same, but not do exactly
+        what we need.
+
+        Namely, ``importlib.metadata`` does not normalize the extra name before
+        putting it into the requirement string, which causes marker comparison
+        to fail because the dist-info format do normalize. This is consistent in
+        all currently available PEP 517 backends, although not standardized.
+        """
+        for entry in self._iter_requires_txt_entries():
+            if entry.extra and entry.marker:
+                marker = f'({entry.marker}) and extra == "{safe_extra(entry.extra)}"'
+            elif entry.extra:
+                marker = f'extra == "{safe_extra(entry.extra)}"'
+            elif entry.marker:
+                marker = entry.marker
+            else:
+                marker = ""
+            if marker:
+                yield f"{entry.requirement} ; {marker}"
+            else:
+                yield entry.requirement
+
+    def iter_dependencies(self, extras: Collection[str] = ()) -> Iterable[Requirement]:
+        req_string_iterator = (
+            self._dist.metadata.get_all("Requires-Dist")
+            or self._iter_egg_info_dependencies()
+        )
+        contexts: Sequence[Dict[str, str]] = [{"extra": safe_extra(e)} for e in extras]
+        for req_string in req_string_iterator:
+            req = Requirement(req_string)
+            if not req.marker:
+                yield req
+            elif not extras and req.marker.evaluate({"extra": ""}):
+                yield req
+            elif any(req.marker.evaluate(context) for context in contexts):
+                yield req
diff --git a/src/pip/_internal/metadata/importlib/_envs.py b/src/pip/_internal/metadata/importlib/_envs.py
new file mode 100644
index 000000000..25dbdeaeb
--- /dev/null
+++ b/src/pip/_internal/metadata/importlib/_envs.py
@@ -0,0 +1,163 @@
+import functools
+import importlib.metadata
+import os
+import pathlib
+import sys
+import zipfile
+import zipimport
+from typing import Iterator, List, Optional, Sequence, Set, Tuple
+
+from pip._vendor.packaging.utils import NormalizedName, canonicalize_name
+
+from pip._internal.metadata.base import BaseDistribution, BaseEnvironment
+from pip._internal.utils.deprecation import deprecated
+
+from ._compat import BasePath, get_dist_name, get_info_location
+from ._dists import Distribution
+
+
+class _DistributionFinder:
+    """Finder to locate distributions.
+
+    The main purpose of this class is to memoize found distributions' names, so
+    only one distribution is returned for each package name. At lot of pip code
+    assumes this (because it is setuptools's behavior), and not doing the same
+    can potentially cause a distribution in lower precedence path to override a
+    higher precedence one if the caller is not careful.
+
+    Eventually we probably want to make it possible to see lower precedence
+    installations as well. It's useful feature, after all.
+    """
+
+    FoundResult = Tuple[importlib.metadata.Distribution, Optional[BasePath]]
+
+    def __init__(self) -> None:
+        self._found_names: Set[NormalizedName] = set()
+
+    def _find_impl(self, location: str) -> Iterator[FoundResult]:
+        """Find distributions in a location."""
+        # To know exactly where we find a distribution, we have to feed in the
+        # paths one by one, instead of dumping the list to importlib.metadata.
+        for dist in importlib.metadata.distributions(path=[location]):
+            normalized_name = canonicalize_name(get_dist_name(dist))
+            if normalized_name in self._found_names:
+                continue
+            self._found_names.add(normalized_name)
+            info_location = get_info_location(dist)
+            yield dist, info_location
+
+    def find(self, location: str) -> Iterator[BaseDistribution]:
+        """Find distributions in a location.
+
+        The path can be either a directory, or a ZIP archive.
+        """
+        for dist, info_location in self._find_impl(location):
+            if info_location is None:
+                installed_location: Optional[BasePath] = None
+            else:
+                installed_location = info_location.parent
+            yield Distribution(dist, info_location, installed_location)
+
+    def find_linked(self, location: str) -> Iterator[BaseDistribution]:
+        """Read location in egg-link files and return distributions in there.
+
+        The path should be a directory; otherwise this returns nothing. This
+        follows how setuptools does this for compatibility. The first non-empty
+        line in the egg-link is read as a path (resolved against the egg-link's
+        containing directory if relative). Distributions found at that linked
+        location are returned.
+        """
+        path = pathlib.Path(location)
+        if not path.is_dir():
+            return
+        for child in path.iterdir():
+            if child.suffix != ".egg-link":
+                continue
+            with child.open() as f:
+                lines = (line.strip() for line in f)
+                target_rel = next((line for line in lines if line), "")
+            if not target_rel:
+                continue
+            target_location = str(path.joinpath(target_rel))
+            for dist, info_location in self._find_impl(target_location):
+                yield Distribution(dist, info_location, path)
+
+    def _find_eggs_in_dir(self, location: str) -> Iterator[BaseDistribution]:
+        from pip._vendor.pkg_resources import find_distributions
+
+        from pip._internal.metadata import pkg_resources as legacy
+
+        with os.scandir(location) as it:
+            for entry in it:
+                if not entry.name.endswith(".egg"):
+                    continue
+                for dist in find_distributions(entry.path):
+                    yield legacy.Distribution(dist)
+
+    def _find_eggs_in_zip(self, location: str) -> Iterator[BaseDistribution]:
+        from pip._vendor.pkg_resources import find_eggs_in_zip
+
+        from pip._internal.metadata import pkg_resources as legacy
+
+        try:
+            importer = zipimport.zipimporter(location)
+        except zipimport.ZipImportError:
+            return
+        for dist in find_eggs_in_zip(importer, location):
+            yield legacy.Distribution(dist)
+
+    def find_eggs(self, location: str) -> Iterator[BaseDistribution]:
+        """Find eggs in a location.
+
+        This actually uses the old *pkg_resources* backend. We likely want to
+        deprecate this so we can eventually remove the *pkg_resources*
+        dependency entirely. Before that, this should first emit a deprecation
+        warning for some versions when using the fallback since importing
+        *pkg_resources* is slow for those who don't need it.
+        """
+        if os.path.isdir(location):
+            yield from self._find_eggs_in_dir(location)
+        if zipfile.is_zipfile(location):
+            yield from self._find_eggs_in_zip(location)
+
+
+@functools.lru_cache(maxsize=None)  # Warn a distribution exactly once.
+def _emit_egg_deprecation(location: Optional[str]) -> None:
+    deprecated(
+        reason=f"Loading egg at {location} is deprecated.",
+        replacement="to use pip for package installation.",
+        gone_in=None,
+    )
+
+
+class Environment(BaseEnvironment):
+    def __init__(self, paths: Sequence[str]) -> None:
+        self._paths = paths
+
+    @classmethod
+    def default(cls) -> BaseEnvironment:
+        return cls(sys.path)
+
+    @classmethod
+    def from_paths(cls, paths: Optional[List[str]]) -> BaseEnvironment:
+        if paths is None:
+            return cls(sys.path)
+        return cls(paths)
+
+    def _iter_distributions(self) -> Iterator[BaseDistribution]:
+        finder = _DistributionFinder()
+        for location in self._paths:
+            yield from finder.find(location)
+            for dist in finder.find_eggs(location):
+                # _emit_egg_deprecation(dist.location)  # TODO: Enable this.
+                yield dist
+            # This must go last because that's how pkg_resources tie-breaks.
+            yield from finder.find_linked(location)
+
+    def get_distribution(self, name: str) -> Optional[BaseDistribution]:
+        matches = (
+            distribution
+            for distribution in self.iter_all_distributions()
+            if distribution.canonical_name == canonicalize_name(name)
+        )
+        return next(matches, None)
diff --git a/src/pip/_internal/metadata/pkg_resources.py b/src/pip/_internal/metadata/pkg_resources.py
index 0e56f98e2..ffde8c77e 100644
--- a/src/pip/_internal/metadata/pkg_resources.py
+++ b/src/pip/_internal/metadata/pkg_resources.py
@@ -2,9 +2,8 @@ import email.message
 import email.parser
 import logging
 import os
-import pathlib
 import zipfile
-from typing import Collection, Generator, Iterable, List, Mapping, NamedTuple, Optional
+from typing import Collection, Iterable, Iterator, List, Mapping, NamedTuple, Optional
 
 from pip._vendor import pkg_resources
 from pip._vendor.packaging.requirements import Requirement
@@ -12,7 +11,8 @@ from pip._vendor.packaging.utils import NormalizedName, canonicalize_name
 from pip._vendor.packaging.version import parse as parse_version
 
 from pip._internal.exceptions import InvalidWheel, NoneMetadataError, UnsupportedWheel
-from pip._internal.utils.misc import display_path
+from pip._internal.utils.egg_link import egg_link_path_from_location
+from pip._internal.utils.misc import display_path, normalize_path
 from pip._internal.utils.wheel import parse_wheel, read_wheel_metadata_file
 
 from .base import (
@@ -73,7 +73,7 @@ class Distribution(BaseDistribution):
         self._dist = dist
 
     @classmethod
-    def from_directory(cls, directory: str) -> "Distribution":
+    def from_directory(cls, directory: str) -> BaseDistribution:
         dist_dir = directory.rstrip(os.sep)
 
         # Build a PathMetadata object, from path to metadata. :wink:
@@ -93,14 +93,7 @@ class Distribution(BaseDistribution):
         return cls(dist)
 
     @classmethod
-    def from_wheel(cls, wheel: Wheel, name: str) -> "Distribution":
-        """Load the distribution from a given wheel.
-
-        :raises InvalidWheel: Whenever loading of the wheel causes a
-            :py:exc:`zipfile.BadZipFile` exception to be thrown.
-        :raises UnsupportedWheel: If the wheel is a valid zip, but malformed
-            internally.
-        """
+    def from_wheel(cls, wheel: Wheel, name: str) -> BaseDistribution:
         try:
             with wheel.as_zipfile() as zf:
                 info_dir, _ = parse_wheel(zf, name)
@@ -124,6 +117,17 @@ class Distribution(BaseDistribution):
     def location(self) -> Optional[str]:
         return self._dist.location
 
+    @property
+    def installed_location(self) -> Optional[str]:
+        egg_link = egg_link_path_from_location(self.raw_name)
+        if egg_link:
+            location = egg_link
+        elif self.location:
+            location = self.location
+        else:
+            return None
+        return normalize_path(location)
+
     @property
     def info_location(self) -> Optional[str]:
         return self._dist.egg_info
@@ -149,14 +153,8 @@ class Distribution(BaseDistribution):
     def is_file(self, path: InfoPath) -> bool:
         return self._dist.has_metadata(str(path))
 
-    def iterdir(self, path: InfoPath) -> Generator[pathlib.PurePosixPath, None, None]:
-        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 iter_distutils_script_names(self) -> Iterator[str]:
+        yield from self._dist.metadata_listdir("scripts")
 
     def read_text(self, path: InfoPath) -> str:
         name = str(path)
@@ -217,6 +215,10 @@ class Environment(BaseEnvironment):
     def from_paths(cls, paths: Optional[List[str]]) -> BaseEnvironment:
         return cls(pkg_resources.WorkingSet(paths))
 
+    def _iter_distributions(self) -> Iterator[BaseDistribution]:
+        for dist in self._ws:
+            yield Distribution(dist)
+
     def _search_distribution(self, name: str) -> Optional[BaseDistribution]:
         """Find a distribution matching the ``name`` in the environment.
 
@@ -224,7 +226,7 @@ class Environment(BaseEnvironment):
         match the behavior of ``pkg_resources.get_distribution()``.
         """
         canonical_name = canonicalize_name(name)
-        for dist in self.iter_distributions():
+        for dist in self.iter_all_distributions():
             if dist.canonical_name == canonical_name:
                 return dist
         return None
@@ -250,7 +252,3 @@ class Environment(BaseEnvironment):
         except pkg_resources.DistributionNotFound:
             return None
         return self._search_distribution(name)
-
-    def _iter_distributions(self) -> Generator[BaseDistribution, None, None]:
-        for dist in self._ws:
-            yield Distribution(dist)
diff --git a/src/pip/_internal/models/direct_url.py b/src/pip/_internal/models/direct_url.py
index 92060d45d..e75feda9c 100644
--- a/src/pip/_internal/models/direct_url.py
+++ b/src/pip/_internal/models/direct_url.py
@@ -74,14 +74,10 @@ class VcsInfo:
         vcs: str,
         commit_id: str,
         requested_revision: Optional[str] = None,
-        resolved_revision: Optional[str] = None,
-        resolved_revision_type: Optional[str] = None,
     ) -> None:
         self.vcs = vcs
         self.requested_revision = requested_revision
         self.commit_id = commit_id
-        self.resolved_revision = resolved_revision
-        self.resolved_revision_type = resolved_revision_type
 
     @classmethod
     def _from_dict(cls, d: Optional[Dict[str, Any]]) -> Optional["VcsInfo"]:
@@ -91,8 +87,6 @@ class VcsInfo:
             vcs=_get_required(d, str, "vcs"),
             commit_id=_get_required(d, str, "commit_id"),
             requested_revision=_get(d, str, "requested_revision"),
-            resolved_revision=_get(d, str, "resolved_revision"),
-            resolved_revision_type=_get(d, str, "resolved_revision_type"),
         )
 
     def _to_dict(self) -> Dict[str, Any]:
@@ -100,8 +94,6 @@ class VcsInfo:
             vcs=self.vcs,
             requested_revision=self.requested_revision,
             commit_id=self.commit_id,
-            resolved_revision=self.resolved_revision,
-            resolved_revision_type=self.resolved_revision_type,
         )
 
 
diff --git a/src/pip/_internal/pyproject.py b/src/pip/_internal/pyproject.py
index e183eaf86..1e9119f3e 100644
--- a/src/pip/_internal/pyproject.py
+++ b/src/pip/_internal/pyproject.py
@@ -1,3 +1,4 @@
+import importlib.util
 import os
 from collections import namedtuple
 from typing import Any, List, Optional
@@ -89,9 +90,15 @@ def load_pyproject_toml(
 
     # If we haven't worked out whether to use PEP 517 yet,
     # and the user hasn't explicitly stated a preference,
-    # we do so if the project has a pyproject.toml file.
+    # we do so if the project has a pyproject.toml file
+    # or if we cannot import setuptools.
+
+    # We fallback to PEP 517 when without setuptools,
+    # so setuptools can be installed as a default build backend.
+    # For more info see:
+    # https://discuss.python.org/t/pip-without-setuptools-could-the-experience-be-improved/11810/9
     elif use_pep517 is None:
-        use_pep517 = has_pyproject
+        use_pep517 = has_pyproject or not importlib.util.find_spec("setuptools")
 
     # At this point, we know whether we're going to use PEP 517.
     assert use_pep517 is not None
diff --git a/src/pip/_internal/req/req_uninstall.py b/src/pip/_internal/req/req_uninstall.py
index 7aaa3233c..15b67385c 100644
--- a/src/pip/_internal/req/req_uninstall.py
+++ b/src/pip/_internal/req/req_uninstall.py
@@ -558,10 +558,10 @@ class UninstallPathSet:
 
         # find distutils scripts= scripts
         try:
-            for script in dist.iterdir("scripts"):
-                paths_to_remove.add(os.path.join(bin_dir, script.name))
+            for script in dist.iter_distutils_script_names():
+                paths_to_remove.add(os.path.join(bin_dir, script))
                 if WINDOWS:
-                    paths_to_remove.add(os.path.join(bin_dir, f"{script.name}.bat"))
+                    paths_to_remove.add(os.path.join(bin_dir, f"{script}.bat"))
         except (FileNotFoundError, NotADirectoryError):
             pass
 
diff --git a/src/pip/_vendor/pyparsing.pyi b/src/pip/_vendor/pyparsing.pyi
deleted file mode 100644
index 8e9de6b02..000000000
--- a/src/pip/_vendor/pyparsing.pyi
+++ /dev/null
@@ -1,1 +0,0 @@
-from pyparsing import *
\ No newline at end of file
diff --git a/src/pip/_vendor/pyparsing/__init__.py b/src/pip/_vendor/pyparsing/__init__.py
index fbc6d8cf2..24ba1b9b0 100644
--- a/src/pip/_vendor/pyparsing/__init__.py
+++ b/src/pip/_vendor/pyparsing/__init__.py
@@ -1,6 +1,6 @@
 # module pyparsing.py
 #
-# Copyright (c) 2003-2021  Paul T. McGuire
+# Copyright (c) 2003-2022  Paul T. McGuire
 #
 # Permission is hereby granted, free of charge, to any person obtaining
 # a copy of this software and associated documentation files (the
@@ -105,14 +105,17 @@ class version_info(NamedTuple):
 
     @property
     def __version__(self):
-        return "{}.{}.{}".format(self.major, self.minor, self.micro) + (
-            "{}{}{}".format(
-                "r" if self.releaselevel[0] == "c" else "",
-                self.releaselevel[0],
-                self.serial,
-            ),
-            "",
-        )[self.releaselevel == "final"]
+        return (
+            "{}.{}.{}".format(self.major, self.minor, self.micro)
+            + (
+                "{}{}{}".format(
+                    "r" if self.releaselevel[0] == "c" else "",
+                    self.releaselevel[0],
+                    self.serial,
+                ),
+                "",
+            )[self.releaselevel == "final"]
+        )
 
     def __str__(self):
         return "{} {} / {}".format(__name__, self.__version__, __version_time__)
@@ -125,8 +128,8 @@ class version_info(NamedTuple):
         )
 
 
-__version_info__ = version_info(3, 0, 7, "final", 0)
-__version_time__ = "15 Jan 2022 04:10 UTC"
+__version_info__ = version_info(3, 0, 8, "final", 0)
+__version_time__ = "09 Apr 2022 23:29 UTC"
 __version__ = __version_info__.__version__
 __versionTime__ = __version_time__
 __author__ = "Paul McGuire <ptmcg.gm+pyparsing@gmail.com>"
diff --git a/src/pip/_vendor/pyparsing/core.py b/src/pip/_vendor/pyparsing/core.py
index 63118154a..5afbfd0bd 100644
--- a/src/pip/_vendor/pyparsing/core.py
+++ b/src/pip/_vendor/pyparsing/core.py
@@ -23,7 +23,6 @@ import string
 import copy
 import warnings
 import re
-import sre_constants
 import sys
 from collections.abc import Iterable
 import traceback
@@ -53,7 +52,7 @@ _MAX_INT = sys.maxsize
 str_type: Tuple[type, ...] = (str, bytes)
 
 #
-# Copyright (c) 2003-2021  Paul T. McGuire
+# Copyright (c) 2003-2022  Paul T. McGuire
 #
 # Permission is hereby granted, free of charge, to any person obtaining
 # a copy of this software and associated documentation files (the
@@ -76,6 +75,19 @@ str_type: Tuple[type, ...] = (str, bytes)
 #
 
 
+if sys.version_info >= (3, 8):
+    from functools import cached_property
+else:
+
+    class cached_property:
+        def __init__(self, func):
+            self._func = func
+
+        def __get__(self, instance, owner=None):
+            ret = instance.__dict__[self._func.__name__] = self._func(instance)
+            return ret
+
+
 class __compat__(__config_flags):
     """
     A cross-version compatibility configuration for pyparsing features that will be
@@ -246,10 +258,10 @@ hexnums = nums + "ABCDEFabcdef"
 alphanums = alphas + nums
 printables = "".join([c for c in string.printable if c not in string.whitespace])
 
-_trim_arity_call_line = None
+_trim_arity_call_line: traceback.StackSummary = None
 
 
-def _trim_arity(func, maxargs=2):
+def _trim_arity(func, max_limit=3):
     """decorator to trim function calls to match the arity of the target"""
     global _trim_arity_call_line
 
@@ -267,16 +279,12 @@ def _trim_arity(func, maxargs=2):
     # synthesize what would be returned by traceback.extract_stack at the call to
     # user's parse action 'func', so that we don't incur call penalty at parse time
 
-    LINE_DIFF = 11
+    # fmt: off
+    LINE_DIFF = 7
     # IF ANY CODE CHANGES, EVEN JUST COMMENTS OR BLANK LINES, BETWEEN THE NEXT LINE AND
     # THE CALL TO FUNC INSIDE WRAPPER, LINE_DIFF MUST BE MODIFIED!!!!
-    _trim_arity_call_line = (
-        _trim_arity_call_line or traceback.extract_stack(limit=2)[-1]
-    )
-    pa_call_line_synth = (
-        _trim_arity_call_line[0],
-        _trim_arity_call_line[1] + LINE_DIFF,
-    )
+    _trim_arity_call_line = (_trim_arity_call_line or traceback.extract_stack(limit=2)[-1])
+    pa_call_line_synth = (_trim_arity_call_line[0], _trim_arity_call_line[1] + LINE_DIFF)
 
     def wrapper(*args):
         nonlocal found_arity, limit
@@ -297,16 +305,18 @@ def _trim_arity(func, maxargs=2):
                     del tb
 
                     if trim_arity_type_error:
-                        if limit <= maxargs:
+                        if limit < max_limit:
                             limit += 1
                             continue
 
                     raise
+    # fmt: on
 
     # copy func name to wrapper for sensible debug output
     # (can't use functools.wraps, since that messes with function signature)
     func_name = getattr(func, "__name__", getattr(func, "__class__").__name__)
     wrapper.__name__ = func_name
+    wrapper.__doc__ = func.__doc__
 
     return wrapper
 
@@ -467,7 +477,6 @@ class ParserElement(ABC):
         self.modalResults = True
         # custom debug actions
         self.debugActions = self.DebugActions(None, None, None)
-        self.re = None
         # avoid redundant calls to preParse
         self.callPreparse = True
         self.callDuringTry = False
@@ -1342,7 +1351,7 @@ class ParserElement(ABC):
             last = e
         yield instring[last:]
 
-    def __add__(self, other):
+    def __add__(self, other) -> "ParserElement":
         """
         Implementation of ``+`` operator - returns :class:`And`. Adding strings to a :class:`ParserElement`
         converts them to :class:`Literal`s by default.
@@ -1382,7 +1391,7 @@ class ParserElement(ABC):
             )
         return And([self, other])
 
-    def __radd__(self, other):
+    def __radd__(self, other) -> "ParserElement":
         """
         Implementation of ``+`` operator when left operand is not a :class:`ParserElement`
         """
@@ -1399,7 +1408,7 @@ class ParserElement(ABC):
             )
         return other + self
 
-    def __sub__(self, other):
+    def __sub__(self, other) -> "ParserElement":
         """
         Implementation of ``-`` operator, returns :class:`And` with error stop
         """
@@ -1413,7 +1422,7 @@ class ParserElement(ABC):
             )
         return self + And._ErrorStop() + other
 
-    def __rsub__(self, other):
+    def __rsub__(self, other) -> "ParserElement":
         """
         Implementation of ``-`` operator when left operand is not a :class:`ParserElement`
         """
@@ -1427,7 +1436,7 @@ class ParserElement(ABC):
             )
         return other - self
 
-    def __mul__(self, other):
+    def __mul__(self, other) -> "ParserElement":
         """
         Implementation of ``*`` operator, allows use of ``expr * 3`` in place of
         ``expr + expr + expr``.  Expressions may also be multiplied by a 2-integer
@@ -1513,10 +1522,10 @@ class ParserElement(ABC):
                 ret = And([self] * minElements)
         return ret
 
-    def __rmul__(self, other):
+    def __rmul__(self, other) -> "ParserElement":
         return self.__mul__(other)
 
-    def __or__(self, other):
+    def __or__(self, other) -> "ParserElement":
         """
         Implementation of ``|`` operator - returns :class:`MatchFirst`
         """
@@ -1533,7 +1542,7 @@ class ParserElement(ABC):
             )
         return MatchFirst([self, other])
 
-    def __ror__(self, other):
+    def __ror__(self, other) -> "ParserElement":
         """
         Implementation of ``|`` operator when left operand is not a :class:`ParserElement`
         """
@@ -1547,7 +1556,7 @@ class ParserElement(ABC):
             )
         return other | self
 
-    def __xor__(self, other):
+    def __xor__(self, other) -> "ParserElement":
         """
         Implementation of ``^`` operator - returns :class:`Or`
         """
@@ -1561,7 +1570,7 @@ class ParserElement(ABC):
             )
         return Or([self, other])
 
-    def __rxor__(self, other):
+    def __rxor__(self, other) -> "ParserElement":
         """
         Implementation of ``^`` operator when left operand is not a :class:`ParserElement`
         """
@@ -1575,7 +1584,7 @@ class ParserElement(ABC):
             )
         return other ^ self
 
-    def __and__(self, other):
+    def __and__(self, other) -> "ParserElement":
         """
         Implementation of ``&`` operator - returns :class:`Each`
         """
@@ -1589,7 +1598,7 @@ class ParserElement(ABC):
             )
         return Each([self, other])
 
-    def __rand__(self, other):
+    def __rand__(self, other) -> "ParserElement":
         """
         Implementation of ``&`` operator when left operand is not a :class:`ParserElement`
         """
@@ -1603,7 +1612,7 @@ class ParserElement(ABC):
             )
         return other & self
 
-    def __invert__(self):
+    def __invert__(self) -> "ParserElement":
         """
         Implementation of ``~`` operator - returns :class:`NotAny`
         """
@@ -1653,7 +1662,7 @@ class ParserElement(ABC):
         ret = self * tuple(key[:2])
         return ret
 
-    def __call__(self, name: str = None):
+    def __call__(self, name: str = None) -> "ParserElement":
         """
         Shortcut for :class:`set_results_name`, with ``list_all_matches=False``.
 
@@ -2140,6 +2149,7 @@ class ParserElement(ABC):
         output_html: Union[TextIO, Path, str],
         vertical: int = 3,
         show_results_names: bool = False,
+        show_groups: bool = False,
         **kwargs,
     ) -> None:
         """
@@ -2152,7 +2162,7 @@ class ParserElement(ABC):
           instead of horizontally (default=3)
         - show_results_names - bool flag whether diagram should show annotations for
           defined results names
-
+        - show_groups - bool flag whether groups should be highlighted with an unlabeled surrounding box
         Additional diagram-formatting keyword arguments can also be included;
         see railroad.Diagram class.
         """
@@ -2170,6 +2180,7 @@ class ParserElement(ABC):
             self,
             vertical=vertical,
             show_results_names=show_results_names,
+            show_groups=show_groups,
             diagram_kwargs=kwargs,
         )
         if isinstance(output_html, (str, Path)):
@@ -2219,7 +2230,7 @@ class _PendingSkip(ParserElement):
     def _generateDefaultName(self):
         return str(self.anchor + Empty()).replace("Empty", "...")
 
-    def __add__(self, other):
+    def __add__(self, other) -> "ParserElement":
         skipper = SkipTo(other).set_name("...")("_skipped*")
         if self.must_skip:
 
@@ -2773,7 +2784,7 @@ class Word(Token):
 
             try:
                 self.re = re.compile(self.reString)
-            except sre_constants.error:
+            except re.error:
                 self.re = None
             else:
                 self.re_match = self.re.match
@@ -2926,19 +2937,12 @@ class Regex(Token):
             if not pattern:
                 raise ValueError("null string passed to Regex; use Empty() instead")
 
-            self.pattern = pattern
+            self._re = None
+            self.reString = self.pattern = pattern
             self.flags = flags
 
-            try:
-                self.re = re.compile(self.pattern, self.flags)
-                self.reString = self.pattern
-            except sre_constants.error:
-                raise ValueError(
-                    "invalid pattern ({!r}) passed to Regex".format(pattern)
-                )
-
         elif hasattr(pattern, "pattern") and hasattr(pattern, "match"):
-            self.re = pattern
+            self._re = pattern
             self.pattern = self.reString = pattern.pattern
             self.flags = flags
 
@@ -2947,11 +2951,8 @@ class Regex(Token):
                 "Regex may only be constructed with a string or a compiled RE object"
             )
 
-        self.re_match = self.re.match
-
         self.errmsg = "Expected " + self.name
         self.mayIndexError = False
-        self.mayReturnEmpty = self.re_match("") is not None
         self.asGroupList = asGroupList
         self.asMatch = asMatch
         if self.asGroupList:
@@ -2959,6 +2960,26 @@ class Regex(Token):
         if self.asMatch:
             self.parseImpl = self.parseImplAsMatch
 
+    @cached_property
+    def re(self):
+        if self._re:
+            return self._re
+        else:
+            try:
+                return re.compile(self.pattern, self.flags)
+            except re.error:
+                raise ValueError(
+                    "invalid pattern ({!r}) passed to Regex".format(self.pattern)
+                )
+
+    @cached_property
+    def re_match(self):
+        return self.re.match
+
+    @cached_property
+    def mayReturnEmpty(self):
+        return self.re_match("") is not None
+
     def _generateDefaultName(self):
         return "Re:({})".format(repr(self.pattern).replace("\\\\", "\\"))
 
@@ -3168,7 +3189,7 @@ class QuotedString(Token):
             self.re = re.compile(self.pattern, self.flags)
             self.reString = self.pattern
             self.re_match = self.re.match
-        except sre_constants.error:
+        except re.error:
             raise ValueError(
                 "invalid pattern {!r} passed to Regex".format(self.pattern)
             )
@@ -3826,7 +3847,9 @@ class And(ParseExpression):
                 seen.add(id(cur))
                 if isinstance(cur, IndentedBlock):
                     prev.add_parse_action(
-                        lambda s, l, t, cur_=cur: setattr(cur_, "parent_anchor", col(l, s))
+                        lambda s, l, t, cur_=cur: setattr(
+                            cur_, "parent_anchor", col(l, s)
+                        )
                     )
                     break
                 subs = cur.recurse()
@@ -5002,20 +5025,20 @@ class SkipTo(ParseElementEnhance):
     prints::
 
         ['101', 'Critical', 'Intermittent system crash', '6']
-        - days_open: 6
-        - desc: Intermittent system crash
-        - issue_num: 101
-        - sev: Critical
+        - days_open: '6'
+        - desc: 'Intermittent system crash'
+        - issue_num: '101'
+        - sev: 'Critical'
         ['94', 'Cosmetic', "Spelling error on Login ('log|n')", '14']
-        - days_open: 14
-        - desc: Spelling error on Login ('log|n')
-        - issue_num: 94
-        - sev: Cosmetic
+        - days_open: '14'
+        - desc: "Spelling error on Login ('log|n')"
+        - issue_num: '94'
+        - sev: 'Cosmetic'
         ['79', 'Minor', 'System slow when running too many reports', '47']
-        - days_open: 47
-        - desc: System slow when running too many reports
-        - issue_num: 79
-        - sev: Minor
+        - days_open: '47'
+        - desc: 'System slow when running too many reports'
+        - issue_num: '79'
+        - sev: 'Minor'
     """
 
     def __init__(
@@ -5473,10 +5496,10 @@ class Dict(TokenConverter):
 
         ['shape', 'SQUARE', 'posn', 'upper left', 'color', 'light blue', 'texture', 'burlap']
         [['shape', 'SQUARE'], ['posn', 'upper left'], ['color', 'light blue'], ['texture', 'burlap']]
-        - color: light blue
-        - posn: upper left
-        - shape: SQUARE
-        - texture: burlap
+        - color: 'light blue'
+        - posn: 'upper left'
+        - shape: 'SQUARE'
+        - texture: 'burlap'
         SQUARE
         {'color': 'light blue', 'posn': 'upper left', 'texture': 'burlap', 'shape': 'SQUARE'}
 
@@ -5564,13 +5587,13 @@ class Suppress(TokenConverter):
             expr = _PendingSkip(NoMatch())
         super().__init__(expr)
 
-    def __add__(self, other):
+    def __add__(self, other) -> "ParserElement":
         if isinstance(self.expr, _PendingSkip):
             return Suppress(SkipTo(other)) + other
         else:
             return super().__add__(other)
 
-    def __sub__(self, other):
+    def __sub__(self, other) -> "ParserElement":
         if isinstance(self.expr, _PendingSkip):
             return Suppress(SkipTo(other)) - other
         else:
diff --git a/src/pip/_vendor/pyparsing/diagram/__init__.py b/src/pip/_vendor/pyparsing/diagram/__init__.py
index 895b97b80..93c8c081e 100644
--- a/src/pip/_vendor/pyparsing/diagram/__init__.py
+++ b/src/pip/_vendor/pyparsing/diagram/__init__.py
@@ -16,6 +16,7 @@ from jinja2 import Template
 from io import StringIO
 import inspect
 
+
 with open(resource_filename(__name__, "template.jinja2"), encoding="utf-8") as fp:
     template = Template(fp.read())
 
@@ -54,7 +55,7 @@ class AnnotatedItem(railroad.Group):
     """
 
     def __init__(self, label: str, item):
-        super().__init__(item=item, label="[{}]".format(label))
+        super().__init__(item=item, label="[{}]".format(label) if label else label)
 
 
 class EditablePartial(Generic[T]):
@@ -137,6 +138,7 @@ def to_railroad(
     diagram_kwargs: Optional[dict] = None,
     vertical: int = 3,
     show_results_names: bool = False,
+    show_groups: bool = False,
 ) -> List[NamedDiagram]:
     """
     Convert a pyparsing element tree into a list of diagrams. This is the recommended entrypoint to diagram
@@ -147,6 +149,8 @@ def to_railroad(
        shown vertically instead of horizontally
     :param show_results_names - bool to indicate whether results name annotations should be
        included in the diagram
+    :param show_groups - bool to indicate whether groups should be highlighted with an unlabeled
+       surrounding box
     """
     # Convert the whole tree underneath the root
     lookup = ConverterState(diagram_kwargs=diagram_kwargs or {})
@@ -156,6 +160,7 @@ def to_railroad(
         parent=None,
         vertical=vertical,
         show_results_names=show_results_names,
+        show_groups=show_groups,
     )
 
     root_id = id(element)
@@ -362,6 +367,7 @@ def _apply_diagram_item_enhancements(fn):
         index: int = 0,
         name_hint: str = None,
         show_results_names: bool = False,
+        show_groups: bool = False,
     ) -> Optional[EditablePartial]:
 
         ret = fn(
@@ -372,6 +378,7 @@ def _apply_diagram_item_enhancements(fn):
             index,
             name_hint,
             show_results_names,
+            show_groups,
         )
 
         # apply annotation for results name, if present
@@ -411,6 +418,7 @@ def _to_diagram_element(
     index: int = 0,
     name_hint: str = None,
     show_results_names: bool = False,
+    show_groups: bool = False,
 ) -> Optional[EditablePartial]:
     """
     Recursively converts a PyParsing Element to a railroad Element
@@ -423,6 +431,7 @@ def _to_diagram_element(
     :param name_hint: If provided, this will override the generated name
     :param show_results_names: bool flag indicating whether to add annotations for results names
     :returns: The converted version of the input element, but as a Partial that hasn't yet been constructed
+    :param show_groups: bool flag indicating whether to show groups using bounding box
     """
     exprs = element.recurse()
     name = name_hint or element.customName or element.__class__.__name__
@@ -437,7 +446,7 @@ def _to_diagram_element(
         if isinstance(
             element,
             (
-                pyparsing.TokenConverter,
+                # pyparsing.TokenConverter,
                 # pyparsing.Forward,
                 pyparsing.Located,
             ),
@@ -457,6 +466,7 @@ def _to_diagram_element(
                     index=index,
                     name_hint=propagated_name,
                     show_results_names=show_results_names,
+                    show_groups=show_groups,
                 )
 
     # If the element isn't worth extracting, we always treat it as the first time we say it
@@ -510,6 +520,13 @@ def _to_diagram_element(
         ret = EditablePartial.from_call(AnnotatedItem, label="LOOKAHEAD", item="")
     elif isinstance(element, pyparsing.PrecededBy):
         ret = EditablePartial.from_call(AnnotatedItem, label="LOOKBEHIND", item="")
+    elif isinstance(element, pyparsing.Group):
+        if show_groups:
+            ret = EditablePartial.from_call(AnnotatedItem, label="", item="")
+        else:
+            ret = EditablePartial.from_call(railroad.Group, label="", item="")
+    elif isinstance(element, pyparsing.TokenConverter):
+        ret = EditablePartial.from_call(AnnotatedItem, label=type(element).__name__.lower(), item="")
     elif isinstance(element, pyparsing.Opt):
         ret = EditablePartial.from_call(railroad.Optional, item="")
     elif isinstance(element, pyparsing.OneOrMore):
@@ -558,6 +575,7 @@ def _to_diagram_element(
             vertical=vertical,
             index=i,
             show_results_names=show_results_names,
+            show_groups=show_groups,
         )
 
         # Some elements don't need to be shown in the diagram
diff --git a/src/pip/_vendor/pyparsing/helpers.py b/src/pip/_vendor/pyparsing/helpers.py
index 5e7b3ad05..be8a36578 100644
--- a/src/pip/_vendor/pyparsing/helpers.py
+++ b/src/pip/_vendor/pyparsing/helpers.py
@@ -185,7 +185,9 @@ def match_previous_expr(expr: ParserElement) -> ParserElement:
         def must_match_these_tokens(s, l, t):
             theseTokens = _flatten(t.as_list())
             if theseTokens != matchTokens:
-                raise ParseException(s, l, "Expected {}, found{}".format(matchTokens, theseTokens))
+                raise ParseException(
+                    s, l, "Expected {}, found{}".format(matchTokens, theseTokens)
+                )
 
         rep.set_parse_action(must_match_these_tokens, callDuringTry=True)
 
@@ -310,7 +312,7 @@ def one_of(
 
             return ret
 
-        except sre_constants.error:
+        except re.error:
             warnings.warn(
                 "Exception creating Regex for one_of, building MatchFirst", stacklevel=2
             )
@@ -350,10 +352,10 @@ def dict_of(key: ParserElement, value: ParserElement) -> ParserElement:
     prints::
 
         [['shape', 'SQUARE'], ['posn', 'upper left'], ['color', 'light blue'], ['texture', 'burlap']]
-        - color: light blue
-        - posn: upper left
-        - shape: SQUARE
-        - texture: burlap
+        - color: 'light blue'
+        - posn: 'upper left'
+        - shape: 'SQUARE'
+        - texture: 'burlap'
         SQUARE
         SQUARE
         {'color': 'light blue', 'shape': 'SQUARE', 'posn': 'upper left', 'texture': 'burlap'}
@@ -758,10 +760,14 @@ def infix_notation(
         a tuple or list of functions, this is equivalent to calling
         ``set_parse_action(*fn)``
         (:class:`ParserElement.set_parse_action`)
-    - ``lpar`` - expression for matching left-parentheses
-      (default= ``Suppress('(')``)
-    - ``rpar`` - expression for matching right-parentheses
-      (default= ``Suppress(')')``)
+    - ``lpar`` - expression for matching left-parentheses; if passed as a
+      str, then will be parsed as Suppress(lpar). If lpar is passed as
+      an expression (such as ``Literal('(')``), then it will be kept in
+      the parsed results, and grouped with them. (default= ``Suppress('(')``)
+    - ``rpar`` - expression for matching right-parentheses; if passed as a
+      str, then will be parsed as Suppress(rpar). If rpar is passed as
+      an expression (such as ``Literal(')')``), then it will be kept in
+      the parsed results, and grouped with them. (default= ``Suppress(')')``)
 
     Example::
 
@@ -803,9 +809,17 @@ def infix_notation(
     _FB.__name__ = "FollowedBy>"
 
     ret = Forward()
-    lpar = Suppress(lpar)
-    rpar = Suppress(rpar)
-    lastExpr = base_expr | (lpar + ret + rpar)
+    if isinstance(lpar, str):
+        lpar = Suppress(lpar)
+    if isinstance(rpar, str):
+        rpar = Suppress(rpar)
+
+    # if lpar and rpar are not suppressed, wrap in group
+    if not (isinstance(rpar, Suppress) and isinstance(rpar, Suppress)):
+        lastExpr = base_expr | Group(lpar + ret + rpar)
+    else:
+        lastExpr = base_expr | (lpar + ret + rpar)
+
     for i, operDef in enumerate(op_list):
         opExpr, arity, rightLeftAssoc, pa = (operDef + (None,))[:4]
         if isinstance(opExpr, str_type):
diff --git a/src/pip/_vendor/pyparsing/py.typed b/src/pip/_vendor/pyparsing/py.typed
new file mode 100644
index 000000000..e69de29bb
diff --git a/src/pip/_vendor/pyparsing/results.py b/src/pip/_vendor/pyparsing/results.py
index 9676f45b8..bb444df4e 100644
--- a/src/pip/_vendor/pyparsing/results.py
+++ b/src/pip/_vendor/pyparsing/results.py
@@ -65,9 +65,9 @@ class ParseResults:
         'month' in result -> True
         'minutes' in result -> False
         result.dump() -> ['1999', '/', '12', '/', '31']
-        - day: 31
-        - month: 12
-        - year: 1999
+        - day: '31'
+        - month: '12'
+        - year: '1999'
     """
 
     _null_values: Tuple[Any, ...] = (None, [], "", ())
@@ -301,7 +301,7 @@ class ParseResults:
         prints::
 
             ['AAB', '123', '321']
-            - LABEL: AAB
+            - LABEL: 'AAB'
 
             ['AAB', '123', '321']
         """
@@ -603,15 +603,15 @@ class ParseResults:
             integer = Word(nums)
             date_str = integer("year") + '/' + integer("month") + '/' + integer("day")
 
-            result = date_str.parse_string('12/31/1999')
+            result = date_str.parse_string('1999/12/31')
             print(result.dump())
 
         prints::
 
-            ['12', '/', '31', '/', '1999']
-            - day: 1999
-            - month: 31
-            - year: 12
+            ['1999', '/', '12', '/', '31']
+            - day: '31'
+            - month: '12'
+            - year: '1999'
         """
         out = []
         NL = "\n"
diff --git a/src/pip/_vendor/vendor.txt b/src/pip/_vendor/vendor.txt
index b544ae13d..4ff67780c 100644
--- a/src/pip/_vendor/vendor.txt
+++ b/src/pip/_vendor/vendor.txt
@@ -7,7 +7,7 @@ msgpack==1.0.3
 packaging==21.3
 pep517==0.12.0
 platformdirs==2.4.1
-pyparsing==3.0.7
+pyparsing==3.0.8
 requests==2.27.1
     certifi==2021.10.08
     chardet==4.0.0


Test patch:
diff --git a/tests/functional/test_build_env.py b/tests/functional/test_build_env.py
index 285f21fda..a7f89687c 100644
--- a/tests/functional/test_build_env.py
+++ b/tests/functional/test_build_env.py
@@ -165,6 +165,25 @@ def test_build_env_requirements_check(script: PipTestEnvironment) -> None:
         """,
     )
 
+    run_with_build_env(
+        script,
+        """
+        build_env.install_requirements(
+            finder,
+            ["bar==3.0"],
+            "normal",
+            kind="installing bar in normal",
+        )
+        r = build_env.check_requirements(
+            [
+                "bar==2.0; python_version < '3.0'",
+                "bar==3.0; python_version >= '3.0'",
+            ],
+        )
+        assert r == (set(), set()), repr(r)
+        """,
+    )
+
 
 def test_build_env_overlay_prefix_has_priority(script: PipTestEnvironment) -> None:
     create_basic_wheel_for_package(script, "pkg", "2.0")
diff --git a/tests/functional/test_completion.py b/tests/functional/test_completion.py
index 27167ccf8..ba302a69b 100644
--- a/tests/functional/test_completion.py
+++ b/tests/functional/test_completion.py
@@ -54,6 +54,30 @@ function _pip_completion {
 }
 compctl -K _pip_completion pip""",
     ),
+    (
+        "powershell",
+        """\
+if ((Test-Path Function:\\TabExpansion) -and -not `
+    (Test-Path Function:\\_pip_completeBackup)) {
+    Rename-Item Function:\\TabExpansion _pip_completeBackup
+}
+function TabExpansion($line, $lastWord) {
+    $lastBlock = [regex]::Split($line, '[|;]')[-1].TrimStart()
+    if ($lastBlock.StartsWith("pip ")) {
+        $Env:COMP_WORDS=$lastBlock
+        $Env:COMP_CWORD=$lastBlock.Split().Length - 1
+        $Env:PIP_AUTO_COMPLETE=1
+        (& pip).Split()
+        Remove-Item Env:COMP_WORDS
+        Remove-Item Env:COMP_CWORD
+        Remove-Item Env:PIP_AUTO_COMPLETE
+    }
+    elseif (Test-Path Function:\\_pip_completeBackup) {
+        # Fall back on existing tab expansion
+        _pip_completeBackup $line $lastWord
+    }
+}""",
+    ),
 )
 
 
@@ -141,9 +165,10 @@ def test_completion_alone(autocomplete_script: PipTestEnvironment) -> None:
     Test getting completion for none shell, just pip completion
     """
     result = autocomplete_script.pip("completion", allow_stderr_error=True)
-    assert "ERROR: You must pass --bash or --fish or --zsh" in result.stderr, (
-        "completion alone failed -- " + result.stderr
-    )
+    assert (
+        "ERROR: You must pass --bash or --fish or --powershell or --zsh"
+        in result.stderr
+    ), ("completion alone failed -- " + result.stderr)
 
 
 def test_completion_for_un_snippet(autocomplete: DoAutocomplete) -> None:
@@ -362,7 +387,7 @@ def test_completion_path_after_option(
     )
 
 
-@pytest.mark.parametrize("flag", ["--bash", "--zsh", "--fish"])
+@pytest.mark.parametrize("flag", ["--bash", "--zsh", "--fish", "--powershell"])
 def test_completion_uses_same_executable_name(
     autocomplete_script: PipTestEnvironment, flag: str, deprecated_python: bool
 ) -> None:
diff --git a/tests/functional/test_new_resolver.py b/tests/functional/test_new_resolver.py
index 21b9e0b7f..7f35406f7 100644
--- a/tests/functional/test_new_resolver.py
+++ b/tests/functional/test_new_resolver.py
@@ -1233,7 +1233,7 @@ def test_new_resolver_presents_messages_when_backtracking_a_lot(
         # cannot handle it correctly. Nobody is complaining about it right now,
         # we're probably dropping it for importlib.metadata soon(tm), so let's
         # ignore it for the time being.
-        pytest.param("0.1.0+local-1", marks=pytest.mark.xfail),
+        pytest.param("0.1.0+local-1", marks=pytest.mark.xfail(strict=False)),
     ],
     ids=["meta_dot", "meta_underscore", "meta_dash"],
 )
diff --git a/tests/functional/test_uninstall.py b/tests/functional/test_uninstall.py
index a9d6a828b..431ea8dd5 100644
--- a/tests/functional/test_uninstall.py
+++ b/tests/functional/test_uninstall.py
@@ -4,7 +4,7 @@ import sys
 import textwrap
 from os.path import join, normpath
 from tempfile import mkdtemp
-from typing import Any
+from typing import Any, Iterator
 from unittest.mock import Mock
 
 import pytest
@@ -615,11 +615,13 @@ def test_uninstall_setuptools_develop_install(
     script.assert_installed(FSPkg="0.1.dev0")
     # Uninstall both develop and install
     uninstall = script.pip("uninstall", "FSPkg", "-y")
-    assert any(filename.endswith(".egg") for filename in uninstall.files_deleted.keys())
+    assert any(filename.endswith(".egg") for filename in uninstall.files_deleted), str(
+        uninstall
+    )
     uninstall2 = script.pip("uninstall", "FSPkg", "-y")
     assert (
         join(script.site_packages, "FSPkg.egg-link") in uninstall2.files_deleted
-    ), list(uninstall2.files_deleted.keys())
+    ), str(uninstall2)
     script.assert_not_installed("FSPkg")
 
 
@@ -649,6 +651,17 @@ def test_uninstall_editable_and_pip_install(
     script.assert_not_installed("FSPkg")
 
 
+@pytest.fixture()
+def move_easy_install_pth(script: PipTestEnvironment) -> Iterator[None]:
+    """Move easy-install.pth out of the way for testing easy_install."""
+    easy_install_pth = join(script.site_packages_path, "easy-install.pth")
+    pip_test_pth = join(script.site_packages_path, "pip-test.pth")
+    os.rename(easy_install_pth, pip_test_pth)
+    yield
+    os.rename(pip_test_pth, easy_install_pth)
+
+
+@pytest.mark.usefixtures("move_easy_install_pth")
 def test_uninstall_editable_and_pip_install_easy_install_remove(
     script: PipTestEnvironment, data: TestData
 ) -> None:
@@ -659,16 +672,12 @@ def test_uninstall_editable_and_pip_install_easy_install_remove(
     # This becomes the default behavior in setuptools 25.
     script.environ["SETUPTOOLS_SYS_PATH_TECHNIQUE"] = "raw"
 
-    # Rename easy-install.pth to pip-test.pth
-    easy_install_pth = join(script.site_packages_path, "easy-install.pth")
-    pip_test_pth = join(script.site_packages_path, "pip-test.pth")
-    os.rename(easy_install_pth, pip_test_pth)
-
     # Install FSPkg
     pkg_path = data.packages.joinpath("FSPkg")
     script.pip("install", "-e", ".", expect_stderr=True, cwd=pkg_path)
 
     # Rename easy-install.pth to pip-test-fspkg.pth
+    easy_install_pth = join(script.site_packages_path, "easy-install.pth")
     pip_test_fspkg_pth = join(script.site_packages_path, "pip-test-fspkg.pth")
     os.rename(easy_install_pth, pip_test_fspkg_pth)
 
@@ -689,9 +698,6 @@ def test_uninstall_editable_and_pip_install_easy_install_remove(
     # Confirm that FSPkg is uninstalled
     script.assert_not_installed("FSPkg")
 
-    # Rename pip-test.pth back to easy-install.pth
-    os.rename(pip_test_pth, easy_install_pth)
-
 
 def test_uninstall_ignores_missing_packages(
     script: PipTestEnvironment, data: TestData
diff --git a/tests/unit/test_req.py b/tests/unit/test_req.py
index 075d12688..74f4a8bab 100644
--- a/tests/unit/test_req.py
+++ b/tests/unit/test_req.py
@@ -21,7 +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.metadata import select_backend
 from pip._internal.network.session import PipSession
 from pip._internal.operations.build.build_tracker import get_build_tracker
 from pip._internal.operations.prepare import RequirementPreparer
@@ -451,7 +451,7 @@ class TestInstallRequirement:
         req = install_req_from_line("foo")
         req.metadata_directory = path
         dist = req.get_dist()
-        assert isinstance(dist, Distribution)
+        assert isinstance(dist, select_backend().Distribution)
         assert dist.raw_name == dist.canonical_name == "foo"
         assert dist.location == "/path/to".replace("/", os.path.sep)
 
diff --git a/tests/unit/test_self_check_outdated.py b/tests/unit/test_self_check_outdated.py
index d313f3fd0..d41de249e 100644
--- a/tests/unit/test_self_check_outdated.py
+++ b/tests/unit/test_self_check_outdated.py
@@ -6,7 +6,7 @@ import sys
 from typing import Any, Optional, cast
 from unittest import mock
 
-import freezegun  # type: ignore
+import freezegun
 import pytest
 from pip._vendor.packaging.version import parse as parse_version