Repository: jazzband/pip-tools

Issue:
pip-sync fails to run on pip >= 21.3
It seems that pip 21.3 has removed `get_installed_distributions` from `_internal.utils.misc`. `pip-sync` is importing this, and that is causing a catastrophic error on startup.

#### Environment Versions

1. Linux
1. Python: 3.8.6
1. pip version: 21.3
1. pip-tools version: 6.3.1

#### Steps to replicate

1. Create a virtual environment and activate it
2. `pip install -U pip`
3. `pip install pip-tools`
4. `pip-sync -h`

#### Expected result

Help

#### Actual result

```
Traceback (most recent call last):
  File "/home/smmorton/playground/demo/bin/pip-sync", line 5, in <module>
    from piptools.scripts.sync import cli
  File "/home/smmorton/playground/demo/lib/python3.8/site-packages/piptools/scripts/sync.py", line 12, in <module>
    from pip._internal.utils.misc import get_installed_distributions
ImportError: cannot import name 'get_installed_distributions' from 'pip._internal.utils.misc' (/home/smmorton/playground/demo/lib/python3.8/site-packages/pip/_internal/utils/misc.py)
```



Discussion:

Resolving patch:
diff --git a/piptools/scripts/sync.py b/piptools/scripts/sync.py
--- a/piptools/scripts/sync.py
+++ b/piptools/scripts/sync.py
@@ -9,7 +9,8 @@
 from pip._internal.commands import create_command
 from pip._internal.commands.install import InstallCommand
 from pip._internal.index.package_finder import PackageFinder
-from pip._internal.utils.misc import get_installed_distributions
+from pip._internal.metadata import get_environment
+from pip._vendor.pkg_resources import Distribution
 
 from .. import sync
 from .._compat import IS_CLICK_VER_8_PLUS, parse_requirements
@@ -152,9 +153,10 @@ def cli(
         if python_executable is None
         else get_sys_path_for_python_executable(python_executable)
     )
-
-    installed_dists = get_installed_distributions(
-        skip=[], user_only=user_only, paths=paths, local_only=python_executable is None
+    installed_dists = _get_installed_distributions(
+        user_only=user_only,
+        local_only=python_executable is None,
+        paths=paths,
     )
     to_install, to_uninstall = sync.diff(merged_requirements, installed_dists)
 
@@ -268,3 +270,20 @@ def _compose_install_flags(
         result.extend(["--client-cert", client_cert])
 
     return result
+
+
+def _get_installed_distributions(
+    local_only: bool = True,
+    user_only: bool = False,
+    paths: Optional[List[str]] = None,
+) -> List[Distribution]:
+    """Return a list of installed Distribution objects."""
+    from pip._internal.metadata.pkg_resources import Distribution as _Dist
+
+    env = get_environment(paths)
+    dists = env.iter_installed_distributions(
+        local_only=local_only,
+        user_only=user_only,
+        skip=[],
+    )
+    return [cast(_Dist, dist)._dist for dist in dists]
diff --git a/piptools/sync.py b/piptools/sync.py
--- a/piptools/sync.py
+++ b/piptools/sync.py
@@ -19,7 +19,7 @@
 from pip._internal.commands.freeze import DEV_PKGS
 from pip._internal.req import InstallRequirement
 from pip._internal.utils.compat import stdlib_pkgs
-from pip._vendor.packaging.requirements import Requirement
+from pip._vendor.pkg_resources import Distribution
 
 from .exceptions import IncompatibleRequirements
 from .logging import log
@@ -44,7 +44,7 @@
 
 
 def dependency_tree(
-    installed_keys: Mapping[str, Requirement], root_key: str
+    installed_keys: Mapping[str, Distribution], root_key: str
 ) -> Set[str]:
     """
     Calculate the dependency tree for the package `root_key` and return
@@ -55,7 +55,7 @@ def dependency_tree(
     `root_key` should be the key to return the dependency tree for.
     """
     dependencies = set()
-    queue: Deque[Requirement] = collections.deque()
+    queue: Deque[Distribution] = collections.deque()
 
     if root_key in installed_keys:
         dep = installed_keys[root_key]
@@ -80,7 +80,7 @@ def dependency_tree(
     return dependencies
 
 
-def get_dists_to_ignore(installed: Iterable[Requirement]) -> List[str]:
+def get_dists_to_ignore(installed: Iterable[Distribution]) -> List[str]:
     """
     Returns a collection of package names to ignore when performing pip-sync,
     based on the currently installed environment.  For example, when pip-tools
@@ -142,7 +142,7 @@ def diff_key_from_ireq(ireq: InstallRequirement) -> str:
 
 def diff(
     compiled_requirements: Iterable[InstallRequirement],
-    installed_dists: Iterable[Requirement],
+    installed_dists: Iterable[Distribution],
 ) -> Tuple[Set[InstallRequirement], Set[str]]:
     """
     Calculate which packages should be installed or uninstalled, given a set
diff --git a/piptools/utils.py b/piptools/utils.py
--- a/piptools/utils.py
+++ b/piptools/utils.py
@@ -3,6 +3,7 @@
 import json
 import os
 import shlex
+import typing
 from typing import (
     Callable,
     Dict,
@@ -26,7 +27,7 @@
 from pip._vendor.packaging.markers import Marker
 from pip._vendor.packaging.specifiers import SpecifierSet
 from pip._vendor.packaging.version import Version
-from pip._vendor.pkg_resources import get_distribution
+from pip._vendor.pkg_resources import Distribution, Requirement, get_distribution
 
 from piptools.subprocess_utils import run_python_snippet
 
@@ -56,7 +57,9 @@ def key_from_ireq(ireq: InstallRequirement) -> str:
         return key_from_req(ireq.req)
 
 
-def key_from_req(req: InstallRequirement) -> str:
+def key_from_req(
+    req: typing.Union[InstallRequirement, Distribution, Requirement]
+) -> str:
     """Get an all-lowercase version of the requirement's name."""
     if hasattr(req, "key"):
         # from pkg_resources, such as installed dists for pip-sync


Test patch: