We are currently solving the following issue within our repository. Here is the issue text:
--- BEGIN ISSUE ---
pip-sync errors when merging multiple requirements.txt files that point at the same editable install
pip-sync raises an `AttributeError: 'NoneType' object has no attribute 'specifier'` error when you try and run it with multiple requirements files that each include an editable install for the same package.

#### Environment Versions

1. OS Type: Linux
1. Python version: Python 3.9.11
1. pip version: pip 22.3
1. pip-tools version: pip-compile, version 6.9.0

#### Steps to replicate
1. Create a setup.py, dev_requirements.in, requirements.in file
```
# setup.py
from setuptools import find_packages, setup
setup(name="a", version="0.0.1", packages=find_packages())

# dev_requirements.in
-e file:.

# requirements.in
-e file:.
```
2. Run pip-compile
```
pip-compile requirements.in
pip-compile dev_requirements.in
```
3. Run pip-sync
```
pip-sync requirements.in  dev_requirements.in
```

#### Expected result
I expected the editable package to install once. Similar to running `pip install -r requirements.txt -r dev_requirements.txt`


#### Actual result
`pip-sync` raised an error
Full stack trace
```
➜  scratch pip-sync  requirements.txt dev_requirements.txt 
Traceback (most recent call last):
  File "/home/vivek/.pyenv/versions/3.9.11/bin/pip-sync", line 8, in <module>
    sys.exit(cli())
  File "/home/vivek/.pyenv/versions/3.9.11/lib/python3.9/site-packages/click/core.py", line 1130, in __call__
    return self.main(*args, **kwargs)
  File "/home/vivek/.pyenv/versions/3.9.11/lib/python3.9/site-packages/click/core.py", line 1055, in main
    rv = self.invoke(ctx)
  File "/home/vivek/.pyenv/versions/3.9.11/lib/python3.9/site-packages/click/core.py", line 1404, in invoke
    return ctx.invoke(self.callback, **ctx.params)
  File "/home/vivek/.pyenv/versions/3.9.11/lib/python3.9/site-packages/click/core.py", line 760, in invoke
    return __callback(*args, **kwargs)
  File "/home/vivek/.pyenv/versions/3.9.11/lib/python3.9/site-packages/piptools/scripts/sync.py", line 146, in cli
    merged_requirements = sync.merge(requirements, ignore_conflicts=force)
  File "/home/vivek/.pyenv/versions/3.9.11/lib/python3.9/site-packages/piptools/sync.py", line 115, in merge
    if ireq.specifier != existing_ireq.specifier:
  File "/home/vivek/.pyenv/versions/3.9.11/lib/python3.9/site-packages/pip/_internal/req/req_install.py", line 245, in specifier
    return self.req.specifier
AttributeError: 'NoneType' object has no attribute 'specifier'
```

...


--- END ISSUE ---

Below are some code segments, each from a relevant file. One or more of these files may contain bugs.
--- BEGIN FILES ---
Path: `piptools/sync.py`
Content:
```
1 from __future__ import annotations
2 
3 import collections
4 import os
5 import sys
6 import tempfile
7 from subprocess import run  # nosec
8 from typing import Deque, Iterable, Mapping, ValuesView
9 
10 import click
11 from pip._internal.commands.freeze import DEV_PKGS
12 from pip._internal.req import InstallRequirement
13 from pip._internal.utils.compat import stdlib_pkgs
14 
15 from ._compat.pip_compat import Distribution, dist_requires
16 from .exceptions import IncompatibleRequirements
17 from .logging import log
18 from .utils import (
19     flat_map,
20     format_requirement,
21     get_hashes_from_ireq,
22     is_url_requirement,
23     key_from_ireq,
24     key_from_req,
25 )
26 
27 PACKAGES_TO_IGNORE = [
28     "-markerlib",
29     "pip",
30     "pip-tools",
31     "pip-review",
32     "pkg-resources",
33     *stdlib_pkgs,
34     *DEV_PKGS,
35 ]
36 
37 
38 def dependency_tree(
39     installed_keys: Mapping[str, Distribution], root_key: str
40 ) -> set[str]:
41     """
42     Calculate the dependency tree for the package `root_key` and return
43     a collection of all its dependencies.  Uses a DFS traversal algorithm.
44 
45     `installed_keys` should be a {key: requirement} mapping, e.g.
46         {'django': from_line('django==1.8')}
47     `root_key` should be the key to return the dependency tree for.
48     """
49     dependencies = set()
50     queue: Deque[Distribution] = collections.deque()
51 
52     if root_key in installed_keys:
53         dep = installed_keys[root_key]
54         queue.append(dep)
55 
56     while queue:
57         v = queue.popleft()
58         key = key_from_req(v)
59         if key in dependencies:
60             continue
61 
62         dependencies.add(key)
63 
64         for dep_specifier in dist_requires(v):
65             dep_name = key_from_req(dep_specifier)
66             if dep_name in installed_keys:
67                 dep = installed_keys[dep_name]
68 
69                 if dep_specifier.specifier.contains(dep.version):
70                     queue.append(dep)
71 
72     return dependencies
73 
74 
75 def get_dists_to_ignore(installed: Iterable[Distribution]) -> list[str]:
76     """
77     Returns a collection of package names to ignore when performing pip-sync,
78     based on the currently installed environment.  For example, when pip-tools
79     is installed in the local environment, it should be ignored, including all
80     of its dependencies (e.g. click).  When pip-tools is not installed
81     locally, click should also be installed/uninstalled depending on the given
82     requirements.
83     """
84     installed_keys = {key_from_req(r): r for r in installed}
85     return list(
86         flat_map(lambda req: dependency_tree(installed_keys, req), PACKAGES_TO_IGNORE)
87     )
88 
89 
90 def merge(
91     requirements: Iterable[InstallRequirement], ignore_conflicts: bool
92 ) -> ValuesView[InstallRequirement]:
93     by_key: dict[str, InstallRequirement] = {}
94 
95     for ireq in requirements:
96         # Limitation: URL requirements are merged by precise string match, so
97         # "file:///example.zip#egg=example", "file:///example.zip", and
98         # "example==1.0" will not merge with each other
99         if ireq.match_markers():
100             key = key_from_ireq(ireq)
101 
102             if not ignore_conflicts:
103                 existing_ireq = by_key.get(key)
104                 if existing_ireq:
105                     # NOTE: We check equality here since we can assume that the
106                     # requirements are all pinned
107                     if ireq.specifier != existing_ireq.specifier:
108                         raise IncompatibleRequirements(ireq, existing_ireq)
109 
110             # TODO: Always pick the largest specifier in case of a conflict
111             by_key[key] = ireq
112     return by_key.values()
113 
114 
115 def diff_key_from_ireq(ireq: InstallRequirement) -> str:
116     """
117     Calculate a key for comparing a compiled requirement with installed modules.
118     For URL requirements, only provide a useful key if the url includes
119     #egg=name==version, which will set ireq.req.name and ireq.specifier.
120     Otherwise return ireq.link so the key will not match and the package will
121     reinstall. Reinstall is necessary to ensure that packages will reinstall
122     if the URL is changed but the version is not.
123     """
124     if is_url_requirement(ireq):
125         if (
126             ireq.req
127             and (getattr(ireq.req, "key", None) or getattr(ireq.req, "name", None))
128             and ireq.specifier
129         ):
130             return key_from_ireq(ireq)
131         return str(ireq.link)
132     return key_from_ireq(ireq)
133 
134 
135 def diff(
136     compiled_requirements: Iterable[InstallRequirement],
137     installed_dists: Iterable[Distribution],
138 ) -> tuple[set[InstallRequirement], set[str]]:
139     """
140     Calculate which packages should be installed or uninstalled, given a set
141     of compiled requirements and a list of currently installed modules.
142     """
143     requirements_lut = {diff_key_from_ireq(r): r for r in compiled_requirements}
144 
145     satisfied = set()  # holds keys
146     to_install = set()  # holds InstallRequirement objects
147     to_uninstall = set()  # holds keys
148 
149     pkgs_to_ignore = get_dists_to_ignore(installed_dists)
150     for dist in installed_dists:
151         key = key_from_req(dist)
152         if key not in requirements_lut or not requirements_lut[key].match_markers():
153             to_uninstall.add(key)
154         elif requirements_lut[key].specifier.contains(dist.version):
155             satisfied.add(key)
156 
157     for key, requirement in requirements_lut.items():
158         if key not in satisfied and requirement.match_markers():
159             to_install.add(requirement)
160 
161     # Make sure to not uninstall any packages that should be ignored
162     to_uninstall -= set(pkgs_to_ignore)
163 
164     return (to_install, to_uninstall)
165 
166 
167 def sync(
168     to_install: Iterable[InstallRequirement],
169     to_uninstall: Iterable[InstallRequirement],
170     dry_run: bool = False,
171     install_flags: list[str] | None = None,
172     ask: bool = False,
173     python_executable: str | None = None,
174 ) -> int:
175     """
176     Install and uninstalls the given sets of modules.
177     """
178     exit_code = 0
179 
180     python_executable = python_executable or sys.executable
181 
182     if not to_uninstall and not to_install:
183         log.info("Everything up-to-date", err=False)
184         return exit_code
185 
186     pip_flags = []
187     if log.verbosity < 0:
188         pip_flags += ["-q"]
189 
190     if ask:
191         dry_run = True
192 
193     if dry_run:
194         if to_uninstall:
195             click.echo("Would uninstall:")
196             for pkg in sorted(to_uninstall):
197                 click.echo(f"  {pkg}")
198 
199         if to_install:
200             click.echo("Would install:")
201             for ireq in sorted(to_install, key=key_from_ireq):
202                 click.echo(f"  {format_requirement(ireq)}")
203 
204         exit_code = 1
205 
206     if ask and click.confirm("Would you like to proceed with these changes?"):
207         dry_run = False
208         exit_code = 0
209 
210     if not dry_run:
211         if to_uninstall:
212             run(  # nosec
213                 [
214                     python_executable,
215                     "-m",
216                     "pip",
217                     "uninstall",
218                     "-y",
219                     *pip_flags,
220                     *sorted(to_uninstall),
221                 ],
222                 check=True,
223             )
224 
225         if to_install:
226             if install_flags is None:
227                 install_flags = []
228             # prepare requirement lines
229             req_lines = []
230             for ireq in sorted(to_install, key=key_from_ireq):
231                 ireq_hashes = get_hashes_from_ireq(ireq)
232                 req_lines.append(format_requirement(ireq, hashes=ireq_hashes))
233 
234             # save requirement lines to a temporary file
235             tmp_req_file = tempfile.NamedTemporaryFile(mode="wt", delete=False)
236             tmp_req_file.write("\n".join(req_lines))
237             tmp_req_file.close()
238 
239             try:
240                 run(  # nosec
241                     [
242                         python_executable,
243                         "-m",
244                         "pip",
245                         "install",
246                         "-r",
247                         tmp_req_file.name,
248                         *pip_flags,
249                         *install_flags,
250                     ],
251                     check=True,
252                 )
253             finally:
254                 os.unlink(tmp_req_file.name)
255 
256     return exit_code
257
```
--- END FILES ---

Please first localize the bug based on the issue statement, and then generate a patch according to the `git diff` format fenced by three backticks.

Here is an example:

```diff
diff --git a/examples/server_async.py b/examples/server_async.py
--- a/examples/server_async.py
+++ b/examples/server_async.py
@@ -313,4 +313,4 @@
 
 
 if __name__ == "__main__":
-    asyncio.run(run_async_server("."), debug=True)
+    asyncio.run(run_async_server(), debug=True)
diff --git a/examples/server_sync.py b/examples/server_sync.py
--- a/examples/server_sync.py
+++ b/examples/server_sync.py
@@ -313,5 +313,5 @@
 
 
 if __name__ == "__main__":
-    server = run_sync_server(".")
+    server = run_sync_server()
     server.shutdown()

```

diff --git a/piptools/sync.py b/piptools/sync.py
--- a/piptools/sync.py
+++ b/piptools/sync.py
@@ -104,7 +104,11 @@
                 if existing_ireq:
                     # NOTE: We check equality here since we can assume that the
                     # requirements are all pinned
-                    if ireq.specifier != existing_ireq.specifier:
+                    if (
+                        ireq.req
+                        and existing_ireq.req
+                        and ireq.specifier != existing_ireq.specifier
+                    ):
                         raise IncompatibleRequirements(ireq, existing_ireq)
 
             # TODO: Always pick the largest specifier in case of a conflict

{"golden_diff": "diff --git a/piptools/sync.py b/piptools/sync.py\n--- a/piptools/sync.py\n+++ b/piptools/sync.py\n@@ -104,7 +104,11 @@\n                 if existing_ireq:\n                     # NOTE: We check equality here since we can assume that the\n                     # requirements are all pinned\n-                    if ireq.specifier != existing_ireq.specifier:\n+                    if (\n+                        ireq.req\n+                        and existing_ireq.req\n+                        and ireq.specifier != existing_ireq.specifier\n+                    ):\n                         raise IncompatibleRequirements(ireq, existing_ireq)\n \n             # TODO: Always pick the largest specifier in case of a conflict\n", "issue": "pip-sync errors when merging multiple requirements.txt files that point at the same editable install\npip-sync raises an `AttributeError: 'NoneType' object has no attribute 'specifier'` error when you try and run it with multiple requirements files that each include an editable install for the same package.\r\n\r\n#### Environment Versions\r\n\r\n1. OS Type: Linux\r\n1. Python version: Python 3.9.11\r\n1. pip version: pip 22.3\r\n1. pip-tools version: pip-compile, version 6.9.0\r\n\r\n#### Steps to replicate\r\n1. Create a setup.py, dev_requirements.in, requirements.in file\r\n```\r\n# setup.py\r\nfrom setuptools import find_packages, setup\r\nsetup(name=\"a\", version=\"0.0.1\", packages=find_packages())\r\n\r\n# dev_requirements.in\r\n-e file:.\r\n\r\n# requirements.in\r\n-e file:.\r\n```\r\n2. Run pip-compile\r\n```\r\npip-compile requirements.in\r\npip-compile dev_requirements.in\r\n```\r\n3. Run pip-sync\r\n```\r\npip-sync requirements.in  dev_requirements.in\r\n```\r\n\r\n#### Expected result\r\nI expected the editable package to install once. Similar to running `pip install -r requirements.txt -r dev_requirements.txt`\r\n\r\n\r\n#### Actual result\r\n`pip-sync` raised an error\r\nFull stack trace\r\n```\r\n\u279c  scratch pip-sync  requirements.txt dev_requirements.txt \r\nTraceback (most recent call last):\r\n  File \"/home/vivek/.pyenv/versions/3.9.11/bin/pip-sync\", line 8, in <module>\r\n    sys.exit(cli())\r\n  File \"/home/vivek/.pyenv/versions/3.9.11/lib/python3.9/site-packages/click/core.py\", line 1130, in __call__\r\n    return self.main(*args, **kwargs)\r\n  File \"/home/vivek/.pyenv/versions/3.9.11/lib/python3.9/site-packages/click/core.py\", line 1055, in main\r\n    rv = self.invoke(ctx)\r\n  File \"/home/vivek/.pyenv/versions/3.9.11/lib/python3.9/site-packages/click/core.py\", line 1404, in invoke\r\n    return ctx.invoke(self.callback, **ctx.params)\r\n  File \"/home/vivek/.pyenv/versions/3.9.11/lib/python3.9/site-packages/click/core.py\", line 760, in invoke\r\n    return __callback(*args, **kwargs)\r\n  File \"/home/vivek/.pyenv/versions/3.9.11/lib/python3.9/site-packages/piptools/scripts/sync.py\", line 146, in cli\r\n    merged_requirements = sync.merge(requirements, ignore_conflicts=force)\r\n  File \"/home/vivek/.pyenv/versions/3.9.11/lib/python3.9/site-packages/piptools/sync.py\", line 115, in merge\r\n    if ireq.specifier != existing_ireq.specifier:\r\n  File \"/home/vivek/.pyenv/versions/3.9.11/lib/python3.9/site-packages/pip/_internal/req/req_install.py\", line 245, in specifier\r\n    return self.req.specifier\r\nAttributeError: 'NoneType' object has no attribute 'specifier'\r\n```\r\n\r\n...\r\n\n", "before_files": [{"content": "from __future__ import annotations\n\nimport collections\nimport os\nimport sys\nimport tempfile\nfrom subprocess import run  # nosec\nfrom typing import Deque, Iterable, Mapping, ValuesView\n\nimport click\nfrom pip._internal.commands.freeze import DEV_PKGS\nfrom pip._internal.req import InstallRequirement\nfrom pip._internal.utils.compat import stdlib_pkgs\n\nfrom ._compat.pip_compat import Distribution, dist_requires\nfrom .exceptions import IncompatibleRequirements\nfrom .logging import log\nfrom .utils import (\n    flat_map,\n    format_requirement,\n    get_hashes_from_ireq,\n    is_url_requirement,\n    key_from_ireq,\n    key_from_req,\n)\n\nPACKAGES_TO_IGNORE = [\n    \"-markerlib\",\n    \"pip\",\n    \"pip-tools\",\n    \"pip-review\",\n    \"pkg-resources\",\n    *stdlib_pkgs,\n    *DEV_PKGS,\n]\n\n\ndef dependency_tree(\n    installed_keys: Mapping[str, Distribution], root_key: str\n) -> set[str]:\n    \"\"\"\n    Calculate the dependency tree for the package `root_key` and return\n    a collection of all its dependencies.  Uses a DFS traversal algorithm.\n\n    `installed_keys` should be a {key: requirement} mapping, e.g.\n        {'django': from_line('django==1.8')}\n    `root_key` should be the key to return the dependency tree for.\n    \"\"\"\n    dependencies = set()\n    queue: Deque[Distribution] = collections.deque()\n\n    if root_key in installed_keys:\n        dep = installed_keys[root_key]\n        queue.append(dep)\n\n    while queue:\n        v = queue.popleft()\n        key = key_from_req(v)\n        if key in dependencies:\n            continue\n\n        dependencies.add(key)\n\n        for dep_specifier in dist_requires(v):\n            dep_name = key_from_req(dep_specifier)\n            if dep_name in installed_keys:\n                dep = installed_keys[dep_name]\n\n                if dep_specifier.specifier.contains(dep.version):\n                    queue.append(dep)\n\n    return dependencies\n\n\ndef get_dists_to_ignore(installed: Iterable[Distribution]) -> list[str]:\n    \"\"\"\n    Returns a collection of package names to ignore when performing pip-sync,\n    based on the currently installed environment.  For example, when pip-tools\n    is installed in the local environment, it should be ignored, including all\n    of its dependencies (e.g. click).  When pip-tools is not installed\n    locally, click should also be installed/uninstalled depending on the given\n    requirements.\n    \"\"\"\n    installed_keys = {key_from_req(r): r for r in installed}\n    return list(\n        flat_map(lambda req: dependency_tree(installed_keys, req), PACKAGES_TO_IGNORE)\n    )\n\n\ndef merge(\n    requirements: Iterable[InstallRequirement], ignore_conflicts: bool\n) -> ValuesView[InstallRequirement]:\n    by_key: dict[str, InstallRequirement] = {}\n\n    for ireq in requirements:\n        # Limitation: URL requirements are merged by precise string match, so\n        # \"file:///example.zip#egg=example\", \"file:///example.zip\", and\n        # \"example==1.0\" will not merge with each other\n        if ireq.match_markers():\n            key = key_from_ireq(ireq)\n\n            if not ignore_conflicts:\n                existing_ireq = by_key.get(key)\n                if existing_ireq:\n                    # NOTE: We check equality here since we can assume that the\n                    # requirements are all pinned\n                    if ireq.specifier != existing_ireq.specifier:\n                        raise IncompatibleRequirements(ireq, existing_ireq)\n\n            # TODO: Always pick the largest specifier in case of a conflict\n            by_key[key] = ireq\n    return by_key.values()\n\n\ndef diff_key_from_ireq(ireq: InstallRequirement) -> str:\n    \"\"\"\n    Calculate a key for comparing a compiled requirement with installed modules.\n    For URL requirements, only provide a useful key if the url includes\n    #egg=name==version, which will set ireq.req.name and ireq.specifier.\n    Otherwise return ireq.link so the key will not match and the package will\n    reinstall. Reinstall is necessary to ensure that packages will reinstall\n    if the URL is changed but the version is not.\n    \"\"\"\n    if is_url_requirement(ireq):\n        if (\n            ireq.req\n            and (getattr(ireq.req, \"key\", None) or getattr(ireq.req, \"name\", None))\n            and ireq.specifier\n        ):\n            return key_from_ireq(ireq)\n        return str(ireq.link)\n    return key_from_ireq(ireq)\n\n\ndef diff(\n    compiled_requirements: Iterable[InstallRequirement],\n    installed_dists: Iterable[Distribution],\n) -> tuple[set[InstallRequirement], set[str]]:\n    \"\"\"\n    Calculate which packages should be installed or uninstalled, given a set\n    of compiled requirements and a list of currently installed modules.\n    \"\"\"\n    requirements_lut = {diff_key_from_ireq(r): r for r in compiled_requirements}\n\n    satisfied = set()  # holds keys\n    to_install = set()  # holds InstallRequirement objects\n    to_uninstall = set()  # holds keys\n\n    pkgs_to_ignore = get_dists_to_ignore(installed_dists)\n    for dist in installed_dists:\n        key = key_from_req(dist)\n        if key not in requirements_lut or not requirements_lut[key].match_markers():\n            to_uninstall.add(key)\n        elif requirements_lut[key].specifier.contains(dist.version):\n            satisfied.add(key)\n\n    for key, requirement in requirements_lut.items():\n        if key not in satisfied and requirement.match_markers():\n            to_install.add(requirement)\n\n    # Make sure to not uninstall any packages that should be ignored\n    to_uninstall -= set(pkgs_to_ignore)\n\n    return (to_install, to_uninstall)\n\n\ndef sync(\n    to_install: Iterable[InstallRequirement],\n    to_uninstall: Iterable[InstallRequirement],\n    dry_run: bool = False,\n    install_flags: list[str] | None = None,\n    ask: bool = False,\n    python_executable: str | None = None,\n) -> int:\n    \"\"\"\n    Install and uninstalls the given sets of modules.\n    \"\"\"\n    exit_code = 0\n\n    python_executable = python_executable or sys.executable\n\n    if not to_uninstall and not to_install:\n        log.info(\"Everything up-to-date\", err=False)\n        return exit_code\n\n    pip_flags = []\n    if log.verbosity < 0:\n        pip_flags += [\"-q\"]\n\n    if ask:\n        dry_run = True\n\n    if dry_run:\n        if to_uninstall:\n            click.echo(\"Would uninstall:\")\n            for pkg in sorted(to_uninstall):\n                click.echo(f\"  {pkg}\")\n\n        if to_install:\n            click.echo(\"Would install:\")\n            for ireq in sorted(to_install, key=key_from_ireq):\n                click.echo(f\"  {format_requirement(ireq)}\")\n\n        exit_code = 1\n\n    if ask and click.confirm(\"Would you like to proceed with these changes?\"):\n        dry_run = False\n        exit_code = 0\n\n    if not dry_run:\n        if to_uninstall:\n            run(  # nosec\n                [\n                    python_executable,\n                    \"-m\",\n                    \"pip\",\n                    \"uninstall\",\n                    \"-y\",\n                    *pip_flags,\n                    *sorted(to_uninstall),\n                ],\n                check=True,\n            )\n\n        if to_install:\n            if install_flags is None:\n                install_flags = []\n            # prepare requirement lines\n            req_lines = []\n            for ireq in sorted(to_install, key=key_from_ireq):\n                ireq_hashes = get_hashes_from_ireq(ireq)\n                req_lines.append(format_requirement(ireq, hashes=ireq_hashes))\n\n            # save requirement lines to a temporary file\n            tmp_req_file = tempfile.NamedTemporaryFile(mode=\"wt\", delete=False)\n            tmp_req_file.write(\"\\n\".join(req_lines))\n            tmp_req_file.close()\n\n            try:\n                run(  # nosec\n                    [\n                        python_executable,\n                        \"-m\",\n                        \"pip\",\n                        \"install\",\n                        \"-r\",\n                        tmp_req_file.name,\n                        *pip_flags,\n                        *install_flags,\n                    ],\n                    check=True,\n                )\n            finally:\n                os.unlink(tmp_req_file.name)\n\n    return exit_code\n", "path": "piptools/sync.py"}], "after_files": [{"content": "from __future__ import annotations\n\nimport collections\nimport os\nimport sys\nimport tempfile\nfrom subprocess import run  # nosec\nfrom typing import Deque, Iterable, Mapping, ValuesView\n\nimport click\nfrom pip._internal.commands.freeze import DEV_PKGS\nfrom pip._internal.req import InstallRequirement\nfrom pip._internal.utils.compat import stdlib_pkgs\n\nfrom ._compat.pip_compat import Distribution, dist_requires\nfrom .exceptions import IncompatibleRequirements\nfrom .logging import log\nfrom .utils import (\n    flat_map,\n    format_requirement,\n    get_hashes_from_ireq,\n    is_url_requirement,\n    key_from_ireq,\n    key_from_req,\n)\n\nPACKAGES_TO_IGNORE = [\n    \"-markerlib\",\n    \"pip\",\n    \"pip-tools\",\n    \"pip-review\",\n    \"pkg-resources\",\n    *stdlib_pkgs,\n    *DEV_PKGS,\n]\n\n\ndef dependency_tree(\n    installed_keys: Mapping[str, Distribution], root_key: str\n) -> set[str]:\n    \"\"\"\n    Calculate the dependency tree for the package `root_key` and return\n    a collection of all its dependencies.  Uses a DFS traversal algorithm.\n\n    `installed_keys` should be a {key: requirement} mapping, e.g.\n        {'django': from_line('django==1.8')}\n    `root_key` should be the key to return the dependency tree for.\n    \"\"\"\n    dependencies = set()\n    queue: Deque[Distribution] = collections.deque()\n\n    if root_key in installed_keys:\n        dep = installed_keys[root_key]\n        queue.append(dep)\n\n    while queue:\n        v = queue.popleft()\n        key = key_from_req(v)\n        if key in dependencies:\n            continue\n\n        dependencies.add(key)\n\n        for dep_specifier in dist_requires(v):\n            dep_name = key_from_req(dep_specifier)\n            if dep_name in installed_keys:\n                dep = installed_keys[dep_name]\n\n                if dep_specifier.specifier.contains(dep.version):\n                    queue.append(dep)\n\n    return dependencies\n\n\ndef get_dists_to_ignore(installed: Iterable[Distribution]) -> list[str]:\n    \"\"\"\n    Returns a collection of package names to ignore when performing pip-sync,\n    based on the currently installed environment.  For example, when pip-tools\n    is installed in the local environment, it should be ignored, including all\n    of its dependencies (e.g. click).  When pip-tools is not installed\n    locally, click should also be installed/uninstalled depending on the given\n    requirements.\n    \"\"\"\n    installed_keys = {key_from_req(r): r for r in installed}\n    return list(\n        flat_map(lambda req: dependency_tree(installed_keys, req), PACKAGES_TO_IGNORE)\n    )\n\n\ndef merge(\n    requirements: Iterable[InstallRequirement], ignore_conflicts: bool\n) -> ValuesView[InstallRequirement]:\n    by_key: dict[str, InstallRequirement] = {}\n\n    for ireq in requirements:\n        # Limitation: URL requirements are merged by precise string match, so\n        # \"file:///example.zip#egg=example\", \"file:///example.zip\", and\n        # \"example==1.0\" will not merge with each other\n        if ireq.match_markers():\n            key = key_from_ireq(ireq)\n\n            if not ignore_conflicts:\n                existing_ireq = by_key.get(key)\n                if existing_ireq:\n                    # NOTE: We check equality here since we can assume that the\n                    # requirements are all pinned\n                    if (\n                        ireq.req\n                        and existing_ireq.req\n                        and ireq.specifier != existing_ireq.specifier\n                    ):\n                        raise IncompatibleRequirements(ireq, existing_ireq)\n\n            # TODO: Always pick the largest specifier in case of a conflict\n            by_key[key] = ireq\n    return by_key.values()\n\n\ndef diff_key_from_ireq(ireq: InstallRequirement) -> str:\n    \"\"\"\n    Calculate a key for comparing a compiled requirement with installed modules.\n    For URL requirements, only provide a useful key if the url includes\n    #egg=name==version, which will set ireq.req.name and ireq.specifier.\n    Otherwise return ireq.link so the key will not match and the package will\n    reinstall. Reinstall is necessary to ensure that packages will reinstall\n    if the URL is changed but the version is not.\n    \"\"\"\n    if is_url_requirement(ireq):\n        if (\n            ireq.req\n            and (getattr(ireq.req, \"key\", None) or getattr(ireq.req, \"name\", None))\n            and ireq.specifier\n        ):\n            return key_from_ireq(ireq)\n        return str(ireq.link)\n    return key_from_ireq(ireq)\n\n\ndef diff(\n    compiled_requirements: Iterable[InstallRequirement],\n    installed_dists: Iterable[Distribution],\n) -> tuple[set[InstallRequirement], set[str]]:\n    \"\"\"\n    Calculate which packages should be installed or uninstalled, given a set\n    of compiled requirements and a list of currently installed modules.\n    \"\"\"\n    requirements_lut = {diff_key_from_ireq(r): r for r in compiled_requirements}\n\n    satisfied = set()  # holds keys\n    to_install = set()  # holds InstallRequirement objects\n    to_uninstall = set()  # holds keys\n\n    pkgs_to_ignore = get_dists_to_ignore(installed_dists)\n    for dist in installed_dists:\n        key = key_from_req(dist)\n        if key not in requirements_lut or not requirements_lut[key].match_markers():\n            to_uninstall.add(key)\n        elif requirements_lut[key].specifier.contains(dist.version):\n            satisfied.add(key)\n\n    for key, requirement in requirements_lut.items():\n        if key not in satisfied and requirement.match_markers():\n            to_install.add(requirement)\n\n    # Make sure to not uninstall any packages that should be ignored\n    to_uninstall -= set(pkgs_to_ignore)\n\n    return (to_install, to_uninstall)\n\n\ndef sync(\n    to_install: Iterable[InstallRequirement],\n    to_uninstall: Iterable[InstallRequirement],\n    dry_run: bool = False,\n    install_flags: list[str] | None = None,\n    ask: bool = False,\n    python_executable: str | None = None,\n) -> int:\n    \"\"\"\n    Install and uninstalls the given sets of modules.\n    \"\"\"\n    exit_code = 0\n\n    python_executable = python_executable or sys.executable\n\n    if not to_uninstall and not to_install:\n        log.info(\"Everything up-to-date\", err=False)\n        return exit_code\n\n    pip_flags = []\n    if log.verbosity < 0:\n        pip_flags += [\"-q\"]\n\n    if ask:\n        dry_run = True\n\n    if dry_run:\n        if to_uninstall:\n            click.echo(\"Would uninstall:\")\n            for pkg in sorted(to_uninstall):\n                click.echo(f\"  {pkg}\")\n\n        if to_install:\n            click.echo(\"Would install:\")\n            for ireq in sorted(to_install, key=key_from_ireq):\n                click.echo(f\"  {format_requirement(ireq)}\")\n\n        exit_code = 1\n\n    if ask and click.confirm(\"Would you like to proceed with these changes?\"):\n        dry_run = False\n        exit_code = 0\n\n    if not dry_run:\n        if to_uninstall:\n            run(  # nosec\n                [\n                    python_executable,\n                    \"-m\",\n                    \"pip\",\n                    \"uninstall\",\n                    \"-y\",\n                    *pip_flags,\n                    *sorted(to_uninstall),\n                ],\n                check=True,\n            )\n\n        if to_install:\n            if install_flags is None:\n                install_flags = []\n            # prepare requirement lines\n            req_lines = []\n            for ireq in sorted(to_install, key=key_from_ireq):\n                ireq_hashes = get_hashes_from_ireq(ireq)\n                req_lines.append(format_requirement(ireq, hashes=ireq_hashes))\n\n            # save requirement lines to a temporary file\n            tmp_req_file = tempfile.NamedTemporaryFile(mode=\"wt\", delete=False)\n            tmp_req_file.write(\"\\n\".join(req_lines))\n            tmp_req_file.close()\n\n            try:\n                run(  # nosec\n                    [\n                        python_executable,\n                        \"-m\",\n                        \"pip\",\n                        \"install\",\n                        \"-r\",\n                        tmp_req_file.name,\n                        *pip_flags,\n                        *install_flags,\n                    ],\n                    check=True,\n                )\n            finally:\n                os.unlink(tmp_req_file.name)\n\n    return exit_code\n", "path": "piptools/sync.py"}]}