1import os
2import shutil
3import subprocess
4import sys
5from textwrap import dedent
6from unittest import mock
7
8import pytest
9from pip._internal.utils.urls import path_to_url
10
11from piptools.scripts.compile import cli
12
13from .constants import MINIMAL_WHEELS_PATH, PACKAGES_PATH
14
15is_pypy = "__pypy__" in sys.builtin_module_names
16is_windows = sys.platform == "win32"
17
18
19@pytest.fixture(autouse=True)
20def _temp_dep_cache(tmpdir, monkeypatch):
21    monkeypatch.setenv("PIP_TOOLS_CACHE_DIR", str(tmpdir / "cache"))
22
23
24def test_default_pip_conf_read(pip_with_index_conf, runner):
25    # preconditions
26    with open("requirements.in", "w"):
27        pass
28    out = runner.invoke(cli, ["-v"])
29
30    # check that we have our index-url as specified in pip.conf
31    assert "Using indexes:\n  http://example.com" in out.stderr
32    assert "--index-url http://example.com" in out.stderr
33
34
35def test_command_line_overrides_pip_conf(pip_with_index_conf, runner):
36    # preconditions
37    with open("requirements.in", "w"):
38        pass
39    out = runner.invoke(cli, ["-v", "-i", "http://override.com"])
40
41    # check that we have our index-url as specified in pip.conf
42    assert "Using indexes:\n  http://override.com" in out.stderr
43
44
45@pytest.mark.network
46@pytest.mark.parametrize(
47    ("install_requires", "expected_output"),
48    (
49        pytest.param("small-fake-a==0.1", "small-fake-a==0.1", id="regular"),
50        pytest.param(
51            "pip-tools @ https://github.com/jazzband/pip-tools/archive/7d86c8d3.zip",
52            "pip-tools @ https://github.com/jazzband/pip-tools/archive/7d86c8d3.zip",
53            id="zip URL",
54        ),
55        pytest.param(
56            "pip-tools @ git+https://github.com/jazzband/pip-tools@7d86c8d3",
57            "pip-tools @ git+https://github.com/jazzband/pip-tools@7d86c8d3",
58            id="scm URL",
59        ),
60        pytest.param(
61            "pip-tools @ https://files.pythonhosted.org/packages/06/96/"
62            "89872db07ae70770fba97205b0737c17ef013d0d1c790"
63            "899c16bb8bac419/pip_tools-3.6.1-py2.py3-none-any.whl",
64            "pip-tools @ https://files.pythonhosted.org/packages/06/96/"
65            "89872db07ae70770fba97205b0737c17ef013d0d1c790"
66            "899c16bb8bac419/pip_tools-3.6.1-py2.py3-none-any.whl",
67            id="wheel URL",
68        ),
69    ),
70)
71def test_command_line_setuptools_read(
72    runner, make_pip_conf, make_package, install_requires, expected_output
73):
74    package_dir = make_package(
75        name="fake-setuptools-a",
76        install_requires=(install_requires,),
77    )
78
79    out = runner.invoke(
80        cli,
81        (str(package_dir / "setup.py"), "--find-links", MINIMAL_WHEELS_PATH),
82    )
83
84    assert expected_output in out.stderr.splitlines()
85
86    # check that pip-compile generated a configuration file
87    assert (package_dir / "requirements.txt").exists()
88
89
90@pytest.mark.network
91@pytest.mark.parametrize(
92    ("options", "expected_output_file"),
93    (
94        # For the `pip-compile` output file should be "requirements.txt"
95        ([], "requirements.txt"),
96        # For the `pip-compile --output-file=output.txt`
97        # output file should be "output.txt"
98        (["--output-file", "output.txt"], "output.txt"),
99        # For the `pip-compile setup.py` output file should be "requirements.txt"
100        (["setup.py"], "requirements.txt"),
101        # For the `pip-compile setup.py --output-file=output.txt`
102        # output file should be "output.txt"
103        (["setup.py", "--output-file", "output.txt"], "output.txt"),
104    ),
105)
106def test_command_line_setuptools_output_file(runner, options, expected_output_file):
107    """
108    Test the output files for setup.py as a requirement file.
109    """
110
111    with open("setup.py", "w") as package:
112        package.write(
113            dedent(
114                """\
115                from setuptools import setup
116                setup(install_requires=[])
117                """
118            )
119        )
120
121    out = runner.invoke(cli, options)
122    assert out.exit_code == 0
123    assert os.path.exists(expected_output_file)
124
125
126@pytest.mark.network
127def test_command_line_setuptools_nested_output_file(tmpdir, runner):
128    """
129    Test the output file for setup.py in nested folder as a requirement file.
130    """
131    proj_dir = tmpdir.mkdir("proj")
132
133    with open(str(proj_dir / "setup.py"), "w") as package:
134        package.write(
135            dedent(
136                """\
137                from setuptools import setup
138                setup(install_requires=[])
139                """
140            )
141        )
142
143    out = runner.invoke(cli, [str(proj_dir / "setup.py")])
144    assert out.exit_code == 0
145    assert (proj_dir / "requirements.txt").exists()
146
147
148@pytest.mark.network
149def test_setuptools_preserves_environment_markers(
150    runner, make_package, make_wheel, make_pip_conf, tmpdir
151):
152    make_pip_conf(
153        dedent(
154            """\
155            [global]
156            disable-pip-version-check = True
157            """
158        )
159    )
160
161    dists_dir = tmpdir / "dists"
162
163    foo_dir = make_package(name="foo", version="1.0")
164    make_wheel(foo_dir, dists_dir)
165
166    bar_dir = make_package(
167        name="bar", version="2.0", install_requires=['foo ; python_version >= "1"']
168    )
169    out = runner.invoke(
170        cli,
171        [
172            str(bar_dir / "setup.py"),
173            "--no-header",
174            "--no-annotate",
175            "--no-emit-find-links",
176            "--find-links",
177            str(dists_dir),
178        ],
179    )
180
181    assert out.exit_code == 0, out.stderr
182    assert out.stderr == 'foo==1.0 ; python_version >= "1"\n'
183
184
185def test_find_links_option(runner):
186    with open("requirements.in", "w") as req_in:
187        req_in.write("-f ./libs3")
188
189    out = runner.invoke(cli, ["-v", "-f", "./libs1", "-f", "./libs2"])
190
191    # Check that find-links has been passed to pip
192    assert "Using links:\n  ./libs1\n  ./libs2\n  ./libs3\n" in out.stderr
193
194    # Check that find-links has been written to a requirements.txt
195    with open("requirements.txt") as req_txt:
196        assert (
197            "--find-links ./libs1\n--find-links ./libs2\n--find-links ./libs3\n"
198            in req_txt.read()
199        )
200
201
202def test_find_links_envvar(monkeypatch, runner):
203    with open("requirements.in", "w") as req_in:
204        req_in.write("-f ./libs3")
205
206    monkeypatch.setenv("PIP_FIND_LINKS", "./libs1 ./libs2")
207    out = runner.invoke(cli, ["-v"])
208
209    # Check that find-links has been passed to pip
210    assert "Using links:\n  ./libs1\n  ./libs2\n  ./libs3\n" in out.stderr
211
212    # Check that find-links has been written to a requirements.txt
213    with open("requirements.txt") as req_txt:
214        assert (
215            "--find-links ./libs1\n--find-links ./libs2\n--find-links ./libs3\n"
216            in req_txt.read()
217        )
218
219
220def test_extra_index_option(pip_with_index_conf, runner):
221    with open("requirements.in", "w"):
222        pass
223    out = runner.invoke(
224        cli,
225        [
226            "-v",
227            "--extra-index-url",
228            "http://extraindex1.com",
229            "--extra-index-url",
230            "http://extraindex2.com",
231        ],
232    )
233    assert (
234        "Using indexes:\n"
235        "  http://example.com\n"
236        "  http://extraindex1.com\n"
237        "  http://extraindex2.com" in out.stderr
238    )
239    assert (
240        "--index-url http://example.com\n"
241        "--extra-index-url http://extraindex1.com\n"
242        "--extra-index-url http://extraindex2.com" in out.stderr
243    )
244
245
246def test_extra_index_envvar(monkeypatch, runner):
247    with open("requirements.in", "w"):
248        pass
249
250    monkeypatch.setenv("PIP_INDEX_URL", "http://example.com")
251    monkeypatch.setenv(
252        "PIP_EXTRA_INDEX_URL", "http://extraindex1.com http://extraindex2.com"
253    )
254    out = runner.invoke(cli, ["-v"])
255    assert (
256        "Using indexes:\n"
257        "  http://example.com\n"
258        "  http://extraindex1.com\n"
259        "  http://extraindex2.com" in out.stderr
260    )
261    assert (
262        "--index-url http://example.com\n"
263        "--extra-index-url http://extraindex1.com\n"
264        "--extra-index-url http://extraindex2.com" in out.stderr
265    )
266
267
268@pytest.mark.parametrize("option", ("--extra-index-url", "--find-links"))
269def test_redacted_urls_in_verbose_output(runner, option):
270    """
271    Test that URLs with sensitive data don't leak to the output.
272    """
273    with open("requirements.in", "w"):
274        pass
275
276    out = runner.invoke(
277        cli,
278        [
279            "--no-header",
280            "--no-emit-index-url",
281            "--no-emit-find-links",
282            "--verbose",
283            option,
284            "http://username:password@example.com",
285        ],
286    )
287
288    assert "http://username:****@example.com" in out.stderr
289    assert "password" not in out.stderr
290
291
292def test_trusted_host_option(pip_conf, runner):
293    with open("requirements.in", "w"):
294        pass
295    out = runner.invoke(
296        cli, ["-v", "--trusted-host", "example.com", "--trusted-host", "example2.com"]
297    )
298    assert "--trusted-host example.com\n--trusted-host example2.com\n" in out.stderr
299
300
301def test_trusted_host_envvar(monkeypatch, pip_conf, runner):
302    with open("requirements.in", "w"):
303        pass
304    monkeypatch.setenv("PIP_TRUSTED_HOST", "example.com example2.com")
305    out = runner.invoke(cli, ["-v"])
306    assert "--trusted-host example.com\n--trusted-host example2.com\n" in out.stderr
307
308
309@pytest.mark.parametrize(
310    "options",
311    (
312        pytest.param(
313            ["--trusted-host", "example.com", "--no-emit-trusted-host"],
314            id="trusted host",
315        ),
316        pytest.param(
317            ["--find-links", "wheels", "--no-emit-find-links"], id="find links"
318        ),
319        pytest.param(
320            ["--index-url", "https://index-url", "--no-emit-index-url"], id="index url"
321        ),
322    ),
323)
324def test_all_no_emit_options(runner, options):
325    with open("requirements.in", "w"):
326        pass
327    out = runner.invoke(cli, ["--no-header", *options])
328    assert out.stderr.strip().splitlines() == []
329
330
331@pytest.mark.parametrize(
332    ("option", "expected_output"),
333    (
334        pytest.param(
335            "--emit-index-url", ["--index-url https://index-url"], id="index url"
336        ),
337        pytest.param("--no-emit-index-url", [], id="no index"),
338    ),
339)
340def test_emit_index_url_option(runner, option, expected_output):
341    with open("requirements.in", "w"):
342        pass
343
344    out = runner.invoke(
345        cli, ["--no-header", "--index-url", "https://index-url", option]
346    )
347
348    assert out.stderr.strip().splitlines() == expected_output
349
350
351@pytest.mark.network
352@pytest.mark.xfail(
353    is_pypy and is_windows, reason="https://github.com/jazzband/pip-tools/issues/1148"
354)
355def test_realistic_complex_sub_dependencies(runner):
356    wheels_dir = "wheels"
357
358    # make a temporary wheel of a fake package
359    subprocess.run(
360        [
361            "pip",
362            "wheel",
363            "--no-deps",
364            "-w",
365            wheels_dir,
366            os.path.join(PACKAGES_PATH, "fake_with_deps", "."),
367        ],
368        check=True,
369    )
370
371    with open("requirements.in", "w") as req_in:
372        req_in.write("fake_with_deps")  # require fake package
373
374    out = runner.invoke(cli, ["-n", "--rebuild", "-f", wheels_dir])
375
376    assert out.exit_code == 0
377
378
379def test_run_as_module_compile():
380    """piptools can be run as ``python -m piptools ...``."""
381
382    result = subprocess.run(
383        [sys.executable, "-m", "piptools", "compile", "--help"],
384        stdout=subprocess.PIPE,
385        check=True,
386    )
387
388    # Should have run pip-compile successfully.
389    assert result.stdout.startswith(b"Usage:")
390    assert b"Compiles requirements.txt from requirements.in" in result.stdout
391
392
393def test_editable_package(pip_conf, runner):
394    """piptools can compile an editable"""
395    fake_package_dir = os.path.join(PACKAGES_PATH, "small_fake_with_deps")
396    fake_package_dir = path_to_url(fake_package_dir)
397    with open("requirements.in", "w") as req_in:
398        req_in.write("-e " + fake_package_dir)  # require editable fake package
399
400    out = runner.invoke(cli, ["-n"])
401
402    assert out.exit_code == 0
403    assert fake_package_dir in out.stderr
404    assert "small-fake-a==0.1" in out.stderr
405
406
407def test_editable_package_without_non_editable_duplicate(pip_conf, runner):
408    """
409    piptools keeps editable requirement,
410    without also adding a duplicate "non-editable" requirement variation
411    """
412    fake_package_dir = os.path.join(PACKAGES_PATH, "small_fake_a")
413    fake_package_dir = path_to_url(fake_package_dir)
414    with open("requirements.in", "w") as req_in:
415        # small_fake_with_unpinned_deps also requires small_fake_a
416        req_in.write(
417            "-e "
418            + fake_package_dir
419            + "\nsmall_fake_with_unpinned_deps"  # require editable fake package
420        )
421
422    out = runner.invoke(cli, ["-n"])
423
424    assert out.exit_code == 0
425    assert fake_package_dir in out.stderr
426    # Shouldn't include a non-editable small-fake-a==<version>.
427    assert "small-fake-a==" not in out.stderr
428
429
430def test_editable_package_constraint_without_non_editable_duplicate(pip_conf, runner):
431    """
432    piptools keeps editable constraint,
433    without also adding a duplicate "non-editable" requirement variation
434    """
435    fake_package_dir = os.path.join(PACKAGES_PATH, "small_fake_a")
436    fake_package_dir = path_to_url(fake_package_dir)
437    with open("constraints.txt", "w") as constraints:
438        constraints.write("-e " + fake_package_dir)  # require editable fake package
439
440    with open("requirements.in", "w") as req_in:
441        req_in.write(
442            "-c constraints.txt"  # require editable fake package
443            "\nsmall_fake_with_unpinned_deps"  # This one also requires small_fake_a
444        )
445
446    out = runner.invoke(cli, ["-n"])
447
448    assert out.exit_code == 0
449    assert fake_package_dir in out.stderr
450    # Shouldn't include a non-editable small-fake-a==<version>.
451    assert "small-fake-a==" not in out.stderr
452
453
454@pytest.mark.parametrize("req_editable", ((True,), (False,)))
455def test_editable_package_in_constraints(pip_conf, runner, req_editable):
456    """
457    piptools can compile an editable that appears in both primary requirements
458    and constraints
459    """
460    fake_package_dir = os.path.join(PACKAGES_PATH, "small_fake_with_deps")
461    fake_package_dir = path_to_url(fake_package_dir)
462
463    with open("constraints.txt", "w") as constraints_in:
464        constraints_in.write("-e " + fake_package_dir)
465
466    with open("requirements.in", "w") as req_in:
467        prefix = "-e " if req_editable else ""
468        req_in.write(prefix + fake_package_dir + "\n-c constraints.txt")
469
470    out = runner.invoke(cli, ["-n"])
471
472    assert out.exit_code == 0
473    assert fake_package_dir in out.stderr
474    assert "small-fake-a==0.1" in out.stderr
475
476
477@pytest.mark.network
478def test_editable_package_vcs(runner):
479    vcs_package = (
480        "git+git://github.com/jazzband/pip-tools@"
481        "f97e62ecb0d9b70965c8eff952c001d8e2722e94"
482        "#egg=pip-tools"
483    )
484    with open("requirements.in", "w") as req_in:
485        req_in.write("-e " + vcs_package)
486    out = runner.invoke(cli, ["-n", "--rebuild"])
487    assert out.exit_code == 0
488    assert vcs_package in out.stderr
489    assert "click" in out.stderr  # dependency of pip-tools
490
491
492def test_locally_available_editable_package_is_not_archived_in_cache_dir(
493    pip_conf, tmpdir, runner
494):
495    """
496    piptools will not create an archive for a locally available editable requirement
497    """
498    cache_dir = tmpdir.mkdir("cache_dir")
499
500    fake_package_dir = os.path.join(PACKAGES_PATH, "small_fake_with_deps")
501    fake_package_dir = path_to_url(fake_package_dir)
502
503    with open("requirements.in", "w") as req_in:
504        req_in.write("-e " + fake_package_dir)  # require editable fake package
505
506    out = runner.invoke(cli, ["-n", "--rebuild", "--cache-dir", str(cache_dir)])
507
508    assert out.exit_code == 0
509    assert fake_package_dir in out.stderr
510    assert "small-fake-a==0.1" in out.stderr
511
512    # we should not find any archived file in {cache_dir}/pkgs
513    assert not os.listdir(os.path.join(str(cache_dir), "pkgs"))
514
515
516@pytest.mark.parametrize(
517    ("line", "dependency"),
518    (
519        # use pip-tools version prior to its use of setuptools_scm,
520        # which is incompatible with https: install
521        pytest.param(
522            "https://github.com/jazzband/pip-tools/archive/"
523            "7d86c8d3ecd1faa6be11c7ddc6b29a30ffd1dae3.zip",
524            "\nclick==",
525            id="Zip URL",
526        ),
527        pytest.param(
528            "git+git://github.com/jazzband/pip-tools@"
529            "7d86c8d3ecd1faa6be11c7ddc6b29a30ffd1dae3",
530            "\nclick==",
531            id="VCS URL",
532        ),
533        pytest.param(
534            "https://files.pythonhosted.org/packages/06/96/"
535            "89872db07ae70770fba97205b0737c17ef013d0d1c790"
536            "899c16bb8bac419/pip_tools-3.6.1-py2.py3-none-any.whl",
537            "\nclick==",
538            id="Wheel URL",
539        ),
540        pytest.param(
541            "pytest-django @ git+git://github.com/pytest-dev/pytest-django"
542            "@21492afc88a19d4ca01cd0ac392a5325b14f95c7"
543            "#egg=pytest-django",
544            "pytest-django @ git+git://github.com/pytest-dev/pytest-django"
545            "@21492afc88a19d4ca01cd0ac392a5325b14f95c7",
546            id="VCS with direct reference and egg",
547        ),
548    ),
549)
550@pytest.mark.parametrize("generate_hashes", ((True,), (False,)))
551@pytest.mark.network
552def test_url_package(runner, line, dependency, generate_hashes):
553    with open("requirements.in", "w") as req_in:
554        req_in.write(line)
555    out = runner.invoke(
556        cli, ["-n", "--rebuild"] + (["--generate-hashes"] if generate_hashes else [])
557    )
558    assert out.exit_code == 0
559    assert dependency in out.stderr
560
561
562@pytest.mark.parametrize(
563    ("line", "dependency", "rewritten_line"),
564    (
565        pytest.param(
566            path_to_url(
567                os.path.join(
568                    MINIMAL_WHEELS_PATH, "small_fake_with_deps-0.1-py2.py3-none-any.whl"
569                )
570            ),
571            "\nsmall-fake-a==0.1",
572            None,
573            id="Wheel URI",
574        ),
575        pytest.param(
576            path_to_url(os.path.join(PACKAGES_PATH, "small_fake_with_deps")),
577            "\nsmall-fake-a==0.1",
578            None,
579            id="Local project URI",
580        ),
581        pytest.param(
582            os.path.join(
583                MINIMAL_WHEELS_PATH, "small_fake_with_deps-0.1-py2.py3-none-any.whl"
584            ),
585            "\nsmall-fake-a==0.1",
586            path_to_url(
587                os.path.join(
588                    MINIMAL_WHEELS_PATH, "small_fake_with_deps-0.1-py2.py3-none-any.whl"
589                )
590            ),
591            id="Bare path to file URI",
592        ),
593        pytest.param(
594            os.path.join(
595                MINIMAL_WHEELS_PATH, "small_fake_with_deps-0.1-py2.py3-none-any.whl"
596            ),
597            "\nsmall-fake-with-deps @ "
598            + path_to_url(
599                os.path.join(
600                    MINIMAL_WHEELS_PATH, "small_fake_with_deps-0.1-py2.py3-none-any.whl"
601                )
602            ),
603            "\nsmall-fake-with-deps @ "
604            + path_to_url(
605                os.path.join(
606                    MINIMAL_WHEELS_PATH, "small_fake_with_deps-0.1-py2.py3-none-any.whl"
607                )
608            ),
609            id="Local project with absolute URI",
610        ),
611        pytest.param(
612            path_to_url(os.path.join(PACKAGES_PATH, "small_fake_with_subdir"))
613            + "#subdirectory=subdir&egg=small_fake_a",
614            "small-fake-a @ "
615            + path_to_url(os.path.join(PACKAGES_PATH, "small_fake_with_subdir"))
616            + "#subdirectory=subdir",
617            "small-fake-a @ "
618            + path_to_url(os.path.join(PACKAGES_PATH, "small_fake_with_subdir"))
619            + "#subdirectory=subdir",
620            id="Local project with subdirectory",
621        ),
622    ),
623)
624@pytest.mark.parametrize("generate_hashes", ((True,), (False,)))
625def test_local_file_uri_package(
626    pip_conf, runner, line, dependency, rewritten_line, generate_hashes
627):
628    if rewritten_line is None:
629        rewritten_line = line
630    with open("requirements.in", "w") as req_in:
631        req_in.write(line)
632    out = runner.invoke(
633        cli, ["-n", "--rebuild"] + (["--generate-hashes"] if generate_hashes else [])
634    )
635    assert out.exit_code == 0
636    assert rewritten_line in out.stderr
637    assert dependency in out.stderr
638
639
640def test_relative_file_uri_package(pip_conf, runner):
641    # Copy wheel into temp dir
642    shutil.copy(
643        os.path.join(
644            MINIMAL_WHEELS_PATH, "small_fake_with_deps-0.1-py2.py3-none-any.whl"
645        ),
646        ".",
647    )
648    with open("requirements.in", "w") as req_in:
649        req_in.write("file:small_fake_with_deps-0.1-py2.py3-none-any.whl")
650    out = runner.invoke(cli, ["-n", "--rebuild"])
651    assert out.exit_code == 0
652    assert "file:small_fake_with_deps-0.1-py2.py3-none-any.whl" in out.stderr
653
654
655def test_direct_reference_with_extras(runner):
656    with open("requirements.in", "w") as req_in:
657        req_in.write(
658            "piptools[testing,coverage] @ git+https://github.com/jazzband/pip-tools@6.2.0"
659        )
660    out = runner.invoke(cli, ["-n", "--rebuild"])
661    assert out.exit_code == 0
662    assert "pip-tools @ git+https://github.com/jazzband/pip-tools@6.2.0" in out.stderr
663    assert "pytest==" in out.stderr
664    assert "pytest-cov==" in out.stderr
665
666
667def test_input_file_without_extension(pip_conf, runner):
668    """
669    piptools can compile a file without an extension,
670    and add .txt as the defaut output file extension.
671    """
672    with open("requirements", "w") as req_in:
673        req_in.write("small-fake-a==0.1")
674
675    out = runner.invoke(cli, ["requirements"])
676
677    assert out.exit_code == 0
678    assert "small-fake-a==0.1" in out.stderr
679    assert os.path.exists("requirements.txt")
680
681
682def test_upgrade_packages_option(pip_conf, runner):
683    """
684    piptools respects --upgrade-package/-P inline list.
685    """
686    with open("requirements.in", "w") as req_in:
687        req_in.write("small-fake-a\nsmall-fake-b")
688    with open("requirements.txt", "w") as req_in:
689        req_in.write("small-fake-a==0.1\nsmall-fake-b==0.1")
690
691    out = runner.invoke(cli, ["--no-annotate", "-P", "small-fake-b"])
692
693    assert out.exit_code == 0
694    assert "small-fake-a==0.1" in out.stderr.splitlines()
695    assert "small-fake-b==0.3" in out.stderr.splitlines()
696
697
698def test_upgrade_packages_option_irrelevant(pip_conf, runner):
699    """
700    piptools ignores --upgrade-package/-P items not already constrained.
701    """
702    with open("requirements.in", "w") as req_in:
703        req_in.write("small-fake-a")
704    with open("requirements.txt", "w") as req_in:
705        req_in.write("small-fake-a==0.1")
706
707    out = runner.invoke(cli, ["--no-annotate", "--upgrade-package", "small-fake-b"])
708
709    assert out.exit_code == 0
710    assert "small-fake-a==0.1" in out.stderr.splitlines()
711    assert "small-fake-b==0.3" not in out.stderr.splitlines()
712
713
714def test_upgrade_packages_option_no_existing_file(pip_conf, runner):
715    """
716    piptools respects --upgrade-package/-P inline list when the output file
717    doesn't exist.
718    """
719    with open("requirements.in", "w") as req_in:
720        req_in.write("small-fake-a\nsmall-fake-b")
721
722    out = runner.invoke(cli, ["--no-annotate", "-P", "small-fake-b"])
723
724    assert out.exit_code == 0
725    assert "small-fake-a==0.2" in out.stderr.splitlines()
726    assert "small-fake-b==0.3" in out.stderr.splitlines()
727
728
729@pytest.mark.parametrize(
730    ("current_package", "upgraded_package"),
731    (
732        pytest.param("small-fake-b==0.1", "small-fake-b==0.3", id="upgrade"),
733        pytest.param("small-fake-b==0.3", "small-fake-b==0.1", id="downgrade"),
734    ),
735)
736def test_upgrade_packages_version_option(
737    pip_conf, runner, current_package, upgraded_package
738):
739    """
740    piptools respects --upgrade-package/-P inline list with specified versions.
741    """
742    with open("requirements.in", "w") as req_in:
743        req_in.write("small-fake-a\nsmall-fake-b")
744    with open("requirements.txt", "w") as req_in:
745        req_in.write("small-fake-a==0.1\n" + current_package)
746
747    out = runner.invoke(cli, ["--no-annotate", "--upgrade-package", upgraded_package])
748
749    assert out.exit_code == 0
750    stderr_lines = out.stderr.splitlines()
751    assert "small-fake-a==0.1" in stderr_lines
752    assert upgraded_package in stderr_lines
753
754
755def test_upgrade_packages_version_option_no_existing_file(pip_conf, runner):
756    """
757    piptools respects --upgrade-package/-P inline list with specified versions.
758    """
759    with open("requirements.in", "w") as req_in:
760        req_in.write("small-fake-a\nsmall-fake-b")
761
762    out = runner.invoke(cli, ["-P", "small-fake-b==0.2"])
763
764    assert out.exit_code == 0
765    assert "small-fake-a==0.2" in out.stderr
766    assert "small-fake-b==0.2" in out.stderr
767
768
769def test_upgrade_packages_version_option_and_upgrade(pip_conf, runner):
770    """
771    piptools respects --upgrade-package/-P inline list with specified versions
772    whilst also doing --upgrade.
773    """
774    with open("requirements.in", "w") as req_in:
775        req_in.write("small-fake-a\nsmall-fake-b")
776    with open("requirements.txt", "w") as req_in:
777        req_in.write("small-fake-a==0.1\nsmall-fake-b==0.1")
778
779    out = runner.invoke(cli, ["--upgrade", "-P", "small-fake-b==0.1"])
780
781    assert out.exit_code == 0
782    assert "small-fake-a==0.2" in out.stderr
783    assert "small-fake-b==0.1" in out.stderr
784
785
786def test_upgrade_packages_version_option_and_upgrade_no_existing_file(pip_conf, runner):
787    """
788    piptools respects --upgrade-package/-P inline list with specified versions
789    whilst also doing --upgrade and the output file doesn't exist.
790    """
791    with open("requirements.in", "w") as req_in:
792        req_in.write("small-fake-a\nsmall-fake-b")
793
794    out = runner.invoke(cli, ["--upgrade", "-P", "small-fake-b==0.1"])
795
796    assert out.exit_code == 0
797    assert "small-fake-a==0.2" in out.stderr
798    assert "small-fake-b==0.1" in out.stderr
799
800
801def test_quiet_option(runner):
802    with open("requirements", "w"):
803        pass
804    out = runner.invoke(cli, ["--quiet", "-n", "requirements"])
805    # Pinned requirements result has not been written to output.
806    assert not out.stderr_bytes
807
808
809def test_dry_run_noisy_option(runner):
810    with open("requirements", "w"):
811        pass
812    out = runner.invoke(cli, ["--dry-run", "requirements"])
813    # Dry-run message has been written to output
814    assert "Dry-run, so nothing updated." in out.stderr.splitlines()
815
816
817def test_dry_run_quiet_option(runner):
818    with open("requirements", "w"):
819        pass
820    out = runner.invoke(cli, ["--dry-run", "--quiet", "requirements"])
821    # Dry-run message has not been written to output.
822    assert not out.stderr_bytes
823
824
825def test_generate_hashes_with_editable(pip_conf, runner):
826    small_fake_package_dir = os.path.join(PACKAGES_PATH, "small_fake_with_deps")
827    small_fake_package_url = path_to_url(small_fake_package_dir)
828    with open("requirements.in", "w") as fp:
829        fp.write(f"-e {small_fake_package_url}\n")
830    out = runner.invoke(cli, ["--no-annotate", "--generate-hashes"])
831    expected = (
832        "-e {}\n"
833        "small-fake-a==0.1 \\\n"
834        "    --hash=sha256:5e6071ee6e4c59e0d0408d366f"
835        "e9b66781d2cf01be9a6e19a2433bb3c5336330\n"
836        "small-fake-b==0.1 \\\n"
837        "    --hash=sha256:acdba8f8b8a816213c30d5310c"
838        "3fe296c0107b16ed452062f7f994a5672e3b3f\n"
839    ).format(small_fake_package_url)
840    assert out.exit_code == 0
841    assert expected in out.stderr
842
843
844@pytest.mark.network
845def test_generate_hashes_with_url(runner):
846    with open("requirements.in", "w") as fp:
847        fp.write(
848            "https://github.com/jazzband/pip-tools/archive/"
849            "7d86c8d3ecd1faa6be11c7ddc6b29a30ffd1dae3.zip#egg=pip-tools\n"
850        )
851    out = runner.invoke(cli, ["--no-annotate", "--generate-hashes"])
852    expected = (
853        "pip-tools @ https://github.com/jazzband/pip-tools/archive/"
854        "7d86c8d3ecd1faa6be11c7ddc6b29a30ffd1dae3.zip \\\n"
855        "    --hash=sha256:d24de92e18ad5bf291f25cfcdcf"
856        "0171be6fa70d01d0bef9eeda356b8549715e7\n"
857    )
858    assert out.exit_code == 0
859    assert expected in out.stderr
860
861
862def test_generate_hashes_verbose(pip_conf, runner):
863    """
864    The hashes generation process should show a progress.
865    """
866    with open("requirements.in", "w") as fp:
867        fp.write("small-fake-a==0.1")
868
869    out = runner.invoke(cli, ["--generate-hashes", "-v"])
870    expected_verbose_text = "Generating hashes:\n  small-fake-a\n"
871    assert expected_verbose_text in out.stderr
872
873
874@pytest.mark.network
875def test_generate_hashes_with_annotations(runner):
876    with open("requirements.in", "w") as fp:
877        fp.write("six==1.15.0")
878
879    out = runner.invoke(cli, ["--generate-hashes"])
880    assert out.stderr == dedent(
881        f"""\
882        #
883        # This file is autogenerated by pip-compile with python \
884{sys.version_info.major}.{sys.version_info.minor}
885        # To update, run:
886        #
887        #    pip-compile --generate-hashes
888        #
889        six==1.15.0 \\
890            --hash=sha256:30639c035cdb23534cd4aa2dd52c3bf48f06e5f4a941509c8bafd8ce11080259 \\
891            --hash=sha256:8b74bedcbbbaca38ff6d7491d76f2b06b3592611af620f8426e82dddb04a5ced
892            # via -r requirements.in
893        """
894    )
895
896
897@pytest.mark.network
898def test_generate_hashes_with_split_style_annotations(runner):
899    with open("requirements.in", "w") as fp:
900        fp.write("Django==1.11.29\n")
901        fp.write("django-debug-toolbar==1.11\n")
902        fp.write("django-storages==1.9.1\n")
903        fp.write("django-taggit==0.24.0\n")
904        fp.write("pytz==2020.4\n")
905        fp.write("sqlparse==0.3.1\n")
906
907    out = runner.invoke(cli, ["--generate-hashes", "--annotation-style", "split"])
908    assert out.stderr == dedent(
909        f"""\
910        #
911        # This file is autogenerated by pip-compile with python \
912{sys.version_info.major}.{sys.version_info.minor}
913        # To update, run:
914        #
915        #    pip-compile --generate-hashes
916        #
917        django==1.11.29 \\
918            --hash=sha256:014e3392058d94f40569206a24523ce254d55ad2f9f46c6550b0fe2e4f94cf3f \\
919            --hash=sha256:4200aefb6678019a0acf0005cd14cfce3a5e6b9b90d06145fcdd2e474ad4329c
920            # via
921            #   -r requirements.in
922            #   django-debug-toolbar
923            #   django-storages
924            #   django-taggit
925        django-debug-toolbar==1.11 \\
926            --hash=sha256:89d75b60c65db363fb24688d977e5fbf0e73386c67acf562d278402a10fc3736 \\
927            --hash=sha256:c2b0134119a624f4ac9398b44f8e28a01c7686ac350a12a74793f3dd57a9eea0
928            # via -r requirements.in
929        django-storages==1.9.1 \\
930            --hash=sha256:3103991c2ee8cef8a2ff096709973ffe7106183d211a79f22cf855f33533d924 \\
931            --hash=sha256:a59e9923cbce7068792f75344ed7727021ee4ac20f227cf17297d0d03d141e91
932            # via -r requirements.in
933        django-taggit==0.24.0 \\
934            --hash=sha256:710b4d15ec1996550cc68a0abbc41903ca7d832540e52b1336e6858737e410d8 \\
935            --hash=sha256:bb8f27684814cd1414b2af75b857b5e26a40912631904038a7ecacd2bfafc3ac
936            # via -r requirements.in
937        pytz==2020.4 \\
938            --hash=sha256:3e6b7dd2d1e0a59084bcee14a17af60c5c562cdc16d828e8eba2e683d3a7e268 \\
939            --hash=sha256:5c55e189b682d420be27c6995ba6edce0c0a77dd67bfbe2ae6607134d5851ffd
940            # via
941            #   -r requirements.in
942            #   django
943        sqlparse==0.3.1 \\
944            --hash=sha256:022fb9c87b524d1f7862b3037e541f68597a730a8843245c349fc93e1643dc4e \\
945            --hash=sha256:e162203737712307dfe78860cc56c8da8a852ab2ee33750e33aeadf38d12c548
946            # via
947            #   -r requirements.in
948            #   django-debug-toolbar
949        """
950    )
951
952
953@pytest.mark.network
954def test_generate_hashes_with_line_style_annotations(runner):
955    with open("requirements.in", "w") as fp:
956        fp.write("Django==1.11.29\n")
957        fp.write("django-debug-toolbar==1.11\n")
958        fp.write("django-storages==1.9.1\n")
959        fp.write("django-taggit==0.24.0\n")
960        fp.write("pytz==2020.4\n")
961        fp.write("sqlparse==0.3.1\n")
962
963    out = runner.invoke(cli, ["--generate-hashes", "--annotation-style", "line"])
964    assert out.stderr == dedent(
965        f"""\
966        #
967        # This file is autogenerated by pip-compile with python \
968{sys.version_info.major}.{sys.version_info.minor}
969        # To update, run:
970        #
971        #    pip-compile --annotation-style=line --generate-hashes
972        #
973        django==1.11.29 \\
974            --hash=sha256:014e3392058d94f40569206a24523ce254d55ad2f9f46c6550b0fe2e4f94cf3f \\
975            --hash=sha256:4200aefb6678019a0acf0005cd14cfce3a5e6b9b90d06145fcdd2e474ad4329c
976            # via -r requirements.in, django-debug-toolbar, django-storages, django-taggit
977        django-debug-toolbar==1.11 \\
978            --hash=sha256:89d75b60c65db363fb24688d977e5fbf0e73386c67acf562d278402a10fc3736 \\
979            --hash=sha256:c2b0134119a624f4ac9398b44f8e28a01c7686ac350a12a74793f3dd57a9eea0
980            # via -r requirements.in
981        django-storages==1.9.1 \\
982            --hash=sha256:3103991c2ee8cef8a2ff096709973ffe7106183d211a79f22cf855f33533d924 \\
983            --hash=sha256:a59e9923cbce7068792f75344ed7727021ee4ac20f227cf17297d0d03d141e91
984            # via -r requirements.in
985        django-taggit==0.24.0 \\
986            --hash=sha256:710b4d15ec1996550cc68a0abbc41903ca7d832540e52b1336e6858737e410d8 \\
987            --hash=sha256:bb8f27684814cd1414b2af75b857b5e26a40912631904038a7ecacd2bfafc3ac
988            # via -r requirements.in
989        pytz==2020.4 \\
990            --hash=sha256:3e6b7dd2d1e0a59084bcee14a17af60c5c562cdc16d828e8eba2e683d3a7e268 \\
991            --hash=sha256:5c55e189b682d420be27c6995ba6edce0c0a77dd67bfbe2ae6607134d5851ffd
992            # via -r requirements.in, django
993        sqlparse==0.3.1 \\
994            --hash=sha256:022fb9c87b524d1f7862b3037e541f68597a730a8843245c349fc93e1643dc4e \\
995            --hash=sha256:e162203737712307dfe78860cc56c8da8a852ab2ee33750e33aeadf38d12c548
996            # via -r requirements.in, django-debug-toolbar
997        """
998    )
999
1000
1001def test_filter_pip_markers(pip_conf, runner):
1002    """
1003    Check that pip-compile works with pip environment markers (PEP496)
1004    """
1005    with open("requirements", "w") as req_in:
1006        req_in.write("small-fake-a==0.1\nunknown_package==0.1; python_version == '1'")
1007
1008    out = runner.invoke(cli, ["-n", "requirements"])
1009
1010    assert out.exit_code == 0
1011    assert "small-fake-a==0.1" in out.stderr
1012    assert "unknown_package" not in out.stderr
1013
1014
1015def test_no_candidates(pip_conf, runner):
1016    with open("requirements", "w") as req_in:
1017        req_in.write("small-fake-a>0.3b1,<0.3b2")
1018
1019    out = runner.invoke(cli, ["-n", "requirements"])
1020
1021    assert out.exit_code == 2
1022    assert "Skipped pre-versions:" in out.stderr
1023
1024
1025def test_no_candidates_pre(pip_conf, runner):
1026    with open("requirements", "w") as req_in:
1027        req_in.write("small-fake-a>0.3b1,<0.3b1")
1028
1029    out = runner.invoke(cli, ["-n", "requirements", "--pre"])
1030
1031    assert out.exit_code == 2
1032    assert "Tried pre-versions:" in out.stderr
1033
1034
1035@pytest.mark.parametrize(
1036    ("url", "expected_url"),
1037    (
1038        pytest.param("https://example.com", b"https://example.com", id="regular url"),
1039        pytest.param(
1040            "https://username:password@example.com",
1041            b"https://username:****@example.com",
1042            id="url with credentials",
1043        ),
1044    ),
1045)
1046def test_default_index_url(make_pip_conf, url, expected_url):
1047    """
1048    Test help's output with default index URL.
1049    """
1050    make_pip_conf(
1051        dedent(
1052            f"""\
1053            [global]
1054            index-url = {url}
1055            """
1056        )
1057    )
1058
1059    result = subprocess.run(
1060        [sys.executable, "-m", "piptools", "compile", "--help"],
1061        stdout=subprocess.PIPE,
1062        check=True,
1063    )
1064
1065    assert expected_url in result.stdout
1066
1067
1068def test_stdin_without_output_file(runner):
1069    """
1070    The --output-file option is required for STDIN.
1071    """
1072    out = runner.invoke(cli, ["-n", "-"])
1073
1074    assert out.exit_code == 2
1075    assert "--output-file is required if input is from stdin" in out.stderr
1076
1077
1078def test_not_specified_input_file(runner):
1079    """
1080    It should raise an error if there are no input files or default input files
1081    such as "setup.py" or "requirements.in".
1082    """
1083    out = runner.invoke(cli)
1084    assert "If you do not specify an input file" in out.stderr
1085    assert out.exit_code == 2
1086
1087
1088def test_stdin(pip_conf, runner):
1089    """
1090    Test compile requirements from STDIN.
1091    """
1092    out = runner.invoke(
1093        cli,
1094        ["-", "--output-file", "requirements.txt", "-n", "--no-emit-find-links"],
1095        input="small-fake-a==0.1",
1096    )
1097
1098    assert out.stderr == dedent(
1099        f"""\
1100        #
1101        # This file is autogenerated by pip-compile with python \
1102{sys.version_info.major}.{sys.version_info.minor}
1103        # To update, run:
1104        #
1105        #    pip-compile --no-emit-find-links --output-file=requirements.txt -
1106        #
1107        small-fake-a==0.1
1108            # via -r -
1109        Dry-run, so nothing updated.
1110        """
1111    )
1112
1113
1114def test_multiple_input_files_without_output_file(runner):
1115    """
1116    The --output-file option is required for multiple requirement input files.
1117    """
1118    with open("src_file1.in", "w") as req_in:
1119        req_in.write("six==1.10.0")
1120
1121    with open("src_file2.in", "w") as req_in:
1122        req_in.write("django==2.1")
1123
1124    out = runner.invoke(cli, ["src_file1.in", "src_file2.in"])
1125
1126    assert (
1127        "--output-file is required if two or more input files are given" in out.stderr
1128    )
1129    assert out.exit_code == 2
1130
1131
1132@pytest.mark.parametrize(
1133    ("options", "expected"),
1134    (
1135        pytest.param(
1136            ("--annotate",),
1137            f"""\
1138            #
1139            # This file is autogenerated by pip-compile with python \
1140{sys.version_info.major}.{sys.version_info.minor}
1141            # To update, run:
1142            #
1143            #    pip-compile --no-emit-find-links
1144            #
1145            small-fake-a==0.1
1146                # via
1147                #   -c constraints.txt
1148                #   small-fake-with-deps
1149            small-fake-with-deps==0.1
1150                # via -r requirements.in
1151            Dry-run, so nothing updated.
1152            """,
1153            id="annotate",
1154        ),
1155        pytest.param(
1156            ("--annotate", "--annotation-style", "line"),
1157            f"""\
1158            #
1159            # This file is autogenerated by pip-compile with python \
1160{sys.version_info.major}.{sys.version_info.minor}
1161            # To update, run:
1162            #
1163            #    pip-compile --annotation-style=line --no-emit-find-links
1164            #
1165            small-fake-a==0.1         # via -c constraints.txt, small-fake-with-deps
1166            small-fake-with-deps==0.1  # via -r requirements.in
1167            Dry-run, so nothing updated.
1168            """,
1169            id="annotate line style",
1170        ),
1171        pytest.param(
1172            ("--no-annotate",),
1173            f"""\
1174            #
1175            # This file is autogenerated by pip-compile with python \
1176{sys.version_info.major}.{sys.version_info.minor}
1177            # To update, run:
1178            #
1179            #    pip-compile --no-annotate --no-emit-find-links
1180            #
1181            small-fake-a==0.1
1182            small-fake-with-deps==0.1
1183            Dry-run, so nothing updated.
1184            """,
1185            id="no annotate",
1186        ),
1187    ),
1188)
1189def test_annotate_option(pip_conf, runner, options, expected):
1190    """
1191    The output lines have annotations if the option is turned on.
1192    """
1193    with open("constraints.txt", "w") as constraints_in:
1194        constraints_in.write("small-fake-a==0.1")
1195    with open("requirements.in", "w") as req_in:
1196        req_in.write("-c constraints.txt\n")
1197        req_in.write("small_fake_with_deps")
1198
1199    out = runner.invoke(cli, [*options, "-n", "--no-emit-find-links"])
1200
1201    assert out.stderr == dedent(expected)
1202    assert out.exit_code == 0
1203
1204
1205@pytest.mark.parametrize(
1206    ("option", "expected"),
1207    (
1208        ("--allow-unsafe", "small-fake-a==0.1"),
1209        ("--no-allow-unsafe", "# small-fake-a"),
1210        (None, "# small-fake-a"),
1211    ),
1212)
1213def test_allow_unsafe_option(pip_conf, monkeypatch, runner, option, expected):
1214    """
1215    Unsafe packages are printed as expected with and without --allow-unsafe.
1216    """
1217    monkeypatch.setattr("piptools.resolver.UNSAFE_PACKAGES", {"small-fake-a"})
1218    with open("requirements.in", "w") as req_in:
1219        req_in.write(path_to_url(os.path.join(PACKAGES_PATH, "small_fake_with_deps")))
1220
1221    out = runner.invoke(cli, ["--no-annotate", option] if option else [])
1222
1223    assert expected in out.stderr.splitlines()
1224    assert out.exit_code == 0
1225
1226
1227@pytest.mark.parametrize(
1228    ("option", "attr", "expected"),
1229    (("--cert", "cert", "foo.crt"), ("--client-cert", "client_cert", "bar.pem")),
1230)
1231@mock.patch("piptools.scripts.compile.parse_requirements")
1232def test_cert_option(parse_requirements, runner, option, attr, expected):
1233    """
1234    The options --cert and --client-cert have to be passed to the PyPIRepository.
1235    """
1236    with open("requirements.in", "w"):
1237        pass
1238
1239    runner.invoke(cli, [option, expected])
1240
1241    # Ensure the options in parse_requirements has the expected option
1242    args, kwargs = parse_requirements.call_args
1243    assert getattr(kwargs["options"], attr) == expected
1244
1245
1246@pytest.mark.parametrize(
1247    ("option", "expected"),
1248    (("--build-isolation", True), ("--no-build-isolation", False)),
1249)
1250@mock.patch("piptools.scripts.compile.parse_requirements")
1251def test_build_isolation_option(parse_requirements, runner, option, expected):
1252    """
1253    A value of the --build-isolation/--no-build-isolation flag
1254    must be passed to parse_requirements().
1255    """
1256    with open("requirements.in", "w"):
1257        pass
1258
1259    runner.invoke(cli, [option])
1260
1261    # Ensure the options in parse_requirements has the expected build_isolation option
1262    args, kwargs = parse_requirements.call_args
1263    assert kwargs["options"].build_isolation is expected
1264
1265
1266@mock.patch("piptools.scripts.compile.PyPIRepository")
1267def test_forwarded_args(PyPIRepository, runner):
1268    """
1269    Test the forwarded cli args (--pip-args 'arg...') are passed to the pip command.
1270    """
1271    with open("requirements.in", "w"):
1272        pass
1273
1274    cli_args = ("--no-annotate", "--generate-hashes")
1275    pip_args = ("--no-color", "--isolated", "--disable-pip-version-check")
1276    runner.invoke(cli, [*cli_args, "--pip-args", " ".join(pip_args)])
1277    args, kwargs = PyPIRepository.call_args
1278    assert set(pip_args).issubset(set(args[0]))
1279
1280
1281@pytest.mark.parametrize(
1282    ("cli_option", "infile_option", "expected_package"),
1283    (
1284        # no --pre pip-compile should resolve to the last stable version
1285        (False, False, "small-fake-a==0.2"),
1286        # pip-compile --pre should resolve to the last pre-released version
1287        (True, False, "small-fake-a==0.3b1"),
1288        (False, True, "small-fake-a==0.3b1"),
1289        (True, True, "small-fake-a==0.3b1"),
1290    ),
1291)
1292def test_pre_option(pip_conf, runner, cli_option, infile_option, expected_package):
1293    """
1294    Tests pip-compile respects --pre option.
1295    """
1296    with open("requirements.in", "w") as req_in:
1297        if infile_option:
1298            req_in.write("--pre\n")
1299        req_in.write("small-fake-a\n")
1300
1301    out = runner.invoke(cli, ["--no-annotate", "-n"] + (["-p"] if cli_option else []))
1302
1303    assert out.exit_code == 0, out.stderr
1304    assert expected_package in out.stderr.splitlines(), out.stderr
1305
1306
1307@pytest.mark.parametrize(
1308    "add_options",
1309    (
1310        [],
1311        ["--output-file", "requirements.txt"],
1312        ["--upgrade"],
1313        ["--upgrade", "--output-file", "requirements.txt"],
1314        ["--upgrade-package", "small-fake-a"],
1315        ["--upgrade-package", "small-fake-a", "--output-file", "requirements.txt"],
1316    ),
1317)
1318def test_dry_run_option(pip_conf, runner, add_options):
1319    """
1320    Tests pip-compile doesn't create requirements.txt file on dry-run.
1321    """
1322    with open("requirements.in", "w") as req_in:
1323        req_in.write("small-fake-a\n")
1324
1325    out = runner.invoke(cli, ["--no-annotate", "--dry-run", *add_options])
1326
1327    assert out.exit_code == 0, out.stderr
1328    assert "small-fake-a==0.2" in out.stderr.splitlines()
1329    assert not os.path.exists("requirements.txt")
1330
1331
1332@pytest.mark.parametrize(
1333    ("add_options", "expected_cli_output_package"),
1334    (
1335        ([], "small-fake-a==0.1"),
1336        (["--output-file", "requirements.txt"], "small-fake-a==0.1"),
1337        (["--upgrade"], "small-fake-a==0.2"),
1338        (["--upgrade", "--output-file", "requirements.txt"], "small-fake-a==0.2"),
1339        (["--upgrade-package", "small-fake-a"], "small-fake-a==0.2"),
1340        (
1341            ["--upgrade-package", "small-fake-a", "--output-file", "requirements.txt"],
1342            "small-fake-a==0.2",
1343        ),
1344    ),
1345)
1346def test_dry_run_doesnt_touch_output_file(
1347    pip_conf, runner, add_options, expected_cli_output_package
1348):
1349    """
1350    Tests pip-compile doesn't touch requirements.txt file on dry-run.
1351    """
1352    with open("requirements.in", "w") as req_in:
1353        req_in.write("small-fake-a\n")
1354
1355    with open("requirements.txt", "w") as req_txt:
1356        req_txt.write("small-fake-a==0.1\n")
1357
1358    before_compile_mtime = os.stat("requirements.txt").st_mtime
1359
1360    out = runner.invoke(cli, ["--no-annotate", "--dry-run", *add_options])
1361
1362    assert out.exit_code == 0, out.stderr
1363    assert expected_cli_output_package in out.stderr.splitlines()
1364
1365    # The package version must NOT be updated in the output file
1366    with open("requirements.txt") as req_txt:
1367        assert "small-fake-a==0.1" in req_txt.read().splitlines()
1368
1369    # The output file must not be touched
1370    after_compile_mtime = os.stat("requirements.txt").st_mtime
1371    assert after_compile_mtime == before_compile_mtime
1372
1373
1374@pytest.mark.parametrize(
1375    ("empty_input_pkg", "prior_output_pkg"),
1376    (
1377        ("", ""),
1378        ("", "small-fake-a==0.1\n"),
1379        ("# Nothing to see here", ""),
1380        ("# Nothing to see here", "small-fake-a==0.1\n"),
1381    ),
1382)
1383def test_empty_input_file_no_header(runner, empty_input_pkg, prior_output_pkg):
1384    """
1385    Tests pip-compile creates an empty requirements.txt file,
1386    given --no-header and empty requirements.in
1387    """
1388    with open("requirements.in", "w") as req_in:
1389        req_in.write(empty_input_pkg)  # empty input file
1390
1391    with open("requirements.txt", "w") as req_txt:
1392        req_txt.write(prior_output_pkg)
1393
1394    runner.invoke(cli, ["--no-header", "requirements.in"])
1395
1396    with open("requirements.txt") as req_txt:
1397        assert req_txt.read().strip() == ""
1398
1399
1400def test_upgrade_package_doesnt_remove_annotation(pip_conf, runner):
1401    """
1402    Tests pip-compile --upgrade-package shouldn't remove "via" annotation.
1403    See: GH-929
1404    """
1405    with open("requirements.in", "w") as req_in:
1406        req_in.write("small-fake-with-deps\n")
1407
1408    runner.invoke(cli)
1409
1410    # Downgrade small-fake-a to 0.1
1411    with open("requirements.txt", "w") as req_txt:
1412        req_txt.write(
1413            "small-fake-with-deps==0.1\n"
1414            "small-fake-a==0.1         # via small-fake-with-deps\n"
1415        )
1416
1417    runner.invoke(cli, ["-P", "small-fake-a", "--no-emit-find-links"])
1418    with open("requirements.txt") as req_txt:
1419        assert req_txt.read() == dedent(
1420            f"""\
1421            #
1422            # This file is autogenerated by pip-compile with python \
1423{sys.version_info.major}.{sys.version_info.minor}
1424            # To update, run:
1425            #
1426            #    pip-compile --no-emit-find-links
1427            #
1428            small-fake-a==0.1
1429                # via small-fake-with-deps
1430            small-fake-with-deps==0.1
1431                # via -r requirements.in
1432            """
1433        )
1434
1435
1436@pytest.mark.parametrize(
1437    "options",
1438    (
1439        "--index-url https://example.com",
1440        "--extra-index-url https://example.com",
1441        "--find-links ./libs1",
1442        "--trusted-host example.com",
1443        "--no-binary :all:",
1444        "--only-binary :all:",
1445    ),
1446)
1447def test_options_in_requirements_file(runner, options):
1448    """
1449    Test the options from requirements.in is copied to requirements.txt.
1450    """
1451    with open("requirements.in", "w") as reqs_in:
1452        reqs_in.write(options)
1453
1454    out = runner.invoke(cli)
1455    assert out.exit_code == 0, out
1456
1457    with open("requirements.txt") as reqs_txt:
1458        assert options in reqs_txt.read().splitlines()
1459
1460
1461@pytest.mark.parametrize(
1462    ("cli_options", "expected_message"),
1463    (
1464        pytest.param(
1465            ["--index-url", "scheme://foo"],
1466            "Was scheme://foo reachable?",
1467            id="single index url",
1468        ),
1469        pytest.param(
1470            ["--index-url", "scheme://foo", "--extra-index-url", "scheme://bar"],
1471            "Were scheme://foo or scheme://bar reachable?",
1472            id="multiple index urls",
1473        ),
1474        pytest.param(
1475            ["--index-url", "scheme://username:password@host"],
1476            "Was scheme://username:****@host reachable?",
1477            id="index url with credentials",
1478        ),
1479    ),
1480)
1481def test_unreachable_index_urls(runner, cli_options, expected_message):
1482    """
1483    Test pip-compile raises an error if index URLs are not reachable.
1484    """
1485    with open("requirements.in", "w") as reqs_in:
1486        reqs_in.write("some-package")
1487
1488    out = runner.invoke(cli, cli_options)
1489
1490    assert out.exit_code == 2, out
1491
1492    stderr_lines = out.stderr.splitlines()
1493    assert "No versions found" in stderr_lines
1494    assert expected_message in stderr_lines
1495
1496
1497@pytest.mark.parametrize(
1498    ("current_package", "upgraded_package"),
1499    (
1500        pytest.param("small-fake-b==0.1", "small-fake-b==0.2", id="upgrade"),
1501        pytest.param("small-fake-b==0.2", "small-fake-b==0.1", id="downgrade"),
1502    ),
1503)
1504def test_upgrade_packages_option_subdependency(
1505    pip_conf, runner, current_package, upgraded_package
1506):
1507    """
1508    Test that pip-compile --upgrade-package/-P upgrades/dpwngrades subdependencies.
1509    """
1510
1511    with open("requirements.in", "w") as reqs:
1512        reqs.write("small-fake-with-unpinned-deps\n")
1513
1514    with open("requirements.txt", "w") as reqs:
1515        reqs.write("small-fake-a==0.1\n")
1516        reqs.write(current_package + "\n")
1517        reqs.write("small-fake-with-unpinned-deps==0.1\n")
1518
1519    out = runner.invoke(
1520        cli, ["--no-annotate", "--dry-run", "--upgrade-package", upgraded_package]
1521    )
1522
1523    stderr_lines = out.stderr.splitlines()
1524    assert "small-fake-a==0.1" in stderr_lines, "small-fake-a must keep its version"
1525    assert (
1526        upgraded_package in stderr_lines
1527    ), f"{current_package} must be upgraded/downgraded to {upgraded_package}"
1528
1529
1530@pytest.mark.parametrize(
1531    ("input_opts", "output_opts"),
1532    (
1533        # Test that input options overwrite output options
1534        pytest.param(
1535            "--index-url https://index-url",
1536            "--index-url https://another-index-url",
1537            id="index url",
1538        ),
1539        pytest.param(
1540            "--extra-index-url https://extra-index-url",
1541            "--extra-index-url https://another-extra-index-url",
1542            id="extra index url",
1543        ),
1544        pytest.param("--find-links dir", "--find-links another-dir", id="find links"),
1545        pytest.param(
1546            "--trusted-host hostname",
1547            "--trusted-host another-hostname",
1548            id="trusted host",
1549        ),
1550        pytest.param(
1551            "--no-binary :package:", "--no-binary :another-package:", id="no binary"
1552        ),
1553        pytest.param(
1554            "--only-binary :package:",
1555            "--only-binary :another-package:",
1556            id="only binary",
1557        ),
1558        # Test misc corner cases
1559        pytest.param("", "--index-url https://index-url", id="empty input options"),
1560        pytest.param(
1561            "--index-url https://index-url",
1562            (
1563                "--index-url https://index-url\n"
1564                "--extra-index-url https://another-extra-index-url"
1565            ),
1566            id="partially matched options",
1567        ),
1568    ),
1569)
1570def test_remove_outdated_options(runner, input_opts, output_opts):
1571    """
1572    Test that the options from the current requirements.txt wouldn't stay
1573    after compile if they were removed from requirements.in file.
1574    """
1575    with open("requirements.in", "w") as req_in:
1576        req_in.write(input_opts)
1577    with open("requirements.txt", "w") as req_txt:
1578        req_txt.write(output_opts)
1579
1580    out = runner.invoke(cli, ["--no-header"])
1581
1582    assert out.exit_code == 0, out
1583    assert out.stderr.strip() == input_opts
1584
1585
1586def test_sub_dependencies_with_constraints(pip_conf, runner):
1587    # Write constraints file
1588    with open("constraints.txt", "w") as constraints_in:
1589        constraints_in.write("small-fake-a==0.1\n")
1590        constraints_in.write("small-fake-b==0.2\n")
1591        constraints_in.write("small-fake-with-unpinned-deps==0.1")
1592
1593    with open("requirements.in", "w") as req_in:
1594        req_in.write("-c constraints.txt\n")
1595        req_in.write("small_fake_with_deps_and_sub_deps")  # require fake package
1596
1597    out = runner.invoke(cli, ["--no-annotate"])
1598
1599    assert out.exit_code == 0
1600
1601    req_out_lines = set(out.stderr.splitlines())
1602    assert {
1603        "small-fake-a==0.1",
1604        "small-fake-b==0.2",
1605        "small-fake-with-deps-and-sub-deps==0.1",
1606        "small-fake-with-unpinned-deps==0.1",
1607    }.issubset(req_out_lines)
1608
1609
1610def test_preserve_compiled_prerelease_version(pip_conf, runner):
1611    with open("requirements.in", "w") as req_in:
1612        req_in.write("small-fake-a")
1613
1614    with open("requirements.txt", "w") as req_txt:
1615        req_txt.write("small-fake-a==0.3b1")
1616
1617    out = runner.invoke(cli, ["--no-annotate", "--no-header"])
1618
1619    assert out.exit_code == 0, out
1620    assert "small-fake-a==0.3b1" in out.stderr.splitlines()
1621
1622
1623def test_prefer_binary_dist(
1624    pip_conf, make_package, make_sdist, make_wheel, tmpdir, runner
1625):
1626    """
1627    Test pip-compile chooses a correct version of a package with
1628    a binary distribution when PIP_PREFER_BINARY environment variable is on.
1629    """
1630    dists_dir = tmpdir / "dists"
1631
1632    # Make first-package==1.0 and wheels
1633    first_package_v1 = make_package(name="first-package", version="1.0")
1634    make_wheel(first_package_v1, dists_dir)
1635
1636    # Make first-package==2.0 and sdists
1637    first_package_v2 = make_package(name="first-package", version="2.0")
1638    make_sdist(first_package_v2, dists_dir)
1639
1640    # Make second-package==1.0 which depends on first-package, and wheels
1641    second_package_v1 = make_package(
1642        name="second-package", version="1.0", install_requires=["first-package"]
1643    )
1644    make_wheel(second_package_v1, dists_dir)
1645
1646    with open("requirements.in", "w") as req_in:
1647        req_in.write("second-package")
1648
1649    out = runner.invoke(
1650        cli,
1651        ["--no-annotate", "--find-links", str(dists_dir)],
1652        env={"PIP_PREFER_BINARY": "1"},
1653    )
1654
1655    assert out.exit_code == 0, out
1656    assert "first-package==1.0" in out.stderr.splitlines(), out.stderr
1657    assert "second-package==1.0" in out.stderr.splitlines(), out.stderr
1658
1659
1660@pytest.mark.parametrize("prefer_binary", (True, False))
1661def test_prefer_binary_dist_even_there_is_source_dists(
1662    pip_conf, make_package, make_sdist, make_wheel, tmpdir, runner, prefer_binary
1663):
1664    """
1665    Test pip-compile chooses a correct version of a package with a binary distribution
1666    (despite a source dist existing) when PIP_PREFER_BINARY environment variable is on
1667    or off.
1668
1669    Regression test for issue GH-1118.
1670    """
1671    dists_dir = tmpdir / "dists"
1672
1673    # Make first version of package with only wheels
1674    package_v1 = make_package(name="test-package", version="1.0")
1675    make_wheel(package_v1, dists_dir)
1676
1677    # Make seconds version with wheels and sdists
1678    package_v2 = make_package(name="test-package", version="2.0")
1679    make_wheel(package_v2, dists_dir)
1680    make_sdist(package_v2, dists_dir)
1681
1682    with open("requirements.in", "w") as req_in:
1683        req_in.write("test-package")
1684
1685    out = runner.invoke(
1686        cli,
1687        ["--no-annotate", "--find-links", str(dists_dir)],
1688        env={"PIP_PREFER_BINARY": str(int(prefer_binary))},
1689    )
1690
1691    assert out.exit_code == 0, out
1692    assert "test-package==2.0" in out.stderr.splitlines(), out.stderr
1693
1694
1695@pytest.mark.parametrize("output_content", ("test-package-1==0.1", ""))
1696def test_duplicate_reqs_combined(
1697    pip_conf, make_package, make_sdist, tmpdir, runner, output_content
1698):
1699    """
1700    Test pip-compile tracks dependencies properly when install requirements are
1701    combined, especially when an output file already exists.
1702
1703    Regression test for issue GH-1154.
1704    """
1705    test_package_1 = make_package("test_package_1", version="0.1")
1706    test_package_2 = make_package(
1707        "test_package_2", version="0.1", install_requires=["test-package-1"]
1708    )
1709
1710    dists_dir = tmpdir / "dists"
1711
1712    for pkg in (test_package_1, test_package_2):
1713        make_sdist(pkg, dists_dir)
1714
1715    with open("requirements.in", "w") as reqs_in:
1716        reqs_in.write(f"file:{test_package_2}\n")
1717        reqs_in.write(f"file:{test_package_2}#egg=test-package-2\n")
1718
1719    if output_content:
1720        with open("requirements.txt", "w") as reqs_out:
1721            reqs_out.write(output_content)
1722
1723    out = runner.invoke(cli, ["--find-links", str(dists_dir)])
1724
1725    assert out.exit_code == 0, out
1726    assert str(test_package_2) in out.stderr
1727    assert "test-package-1==0.1" in out.stderr
1728
1729
1730def test_combine_extras(pip_conf, runner, make_package):
1731    """
1732    Ensure that multiple declarations of a dependency that specify different
1733    extras produces a requirement for that package with the union of the extras
1734    """
1735    package_with_extras = make_package(
1736        "package_with_extras",
1737        extras_require={
1738            "extra1": ["small-fake-a==0.1"],
1739            "extra2": ["small-fake-b==0.1"],
1740        },
1741    )
1742
1743    with open("requirements.in", "w") as req_in:
1744        req_in.writelines(
1745            [
1746                "-r ./requirements-second.in\n",
1747                f"{package_with_extras}[extra1]",
1748            ]
1749        )
1750
1751    with open("requirements-second.in", "w") as req_sec_in:
1752        req_sec_in.write(f"{package_with_extras}[extra2]")
1753
1754    out = runner.invoke(cli, ["-n"])
1755
1756    assert out.exit_code == 0
1757    assert "package-with-extras" in out.stderr
1758    assert "small-fake-a==" in out.stderr
1759    assert "small-fake-b==" in out.stderr
1760
1761
1762@pytest.mark.parametrize(
1763    ("pkg2_install_requires", "req_in_content", "out_expected_content"),
1764    (
1765        pytest.param(
1766            "",
1767            ["test-package-1===0.1.0\n"],
1768            ["test-package-1===0.1.0"],
1769            id="pin package with ===",
1770        ),
1771        pytest.param(
1772            "",
1773            ["test-package-1==0.1.0\n"],
1774            ["test-package-1==0.1.0"],
1775            id="pin package with ==",
1776        ),
1777        pytest.param(
1778            "test-package-1==0.1.0",
1779            ["test-package-1===0.1.0\n", "test-package-2==0.1.0\n"],
1780            ["test-package-1===0.1.0", "test-package-2==0.1.0"],
1781            id="dep === pin preferred over == pin, main package == pin",
1782        ),
1783        pytest.param(
1784            "test-package-1==0.1.0",
1785            ["test-package-1===0.1.0\n", "test-package-2===0.1.0\n"],
1786            ["test-package-1===0.1.0", "test-package-2===0.1.0"],
1787            id="dep === pin preferred over == pin, main package === pin",
1788        ),
1789        pytest.param(
1790            "test-package-1==0.1.0",
1791            ["test-package-2===0.1.0\n"],
1792            ["test-package-1==0.1.0", "test-package-2===0.1.0"],
1793            id="dep == pin conserved, main package === pin",
1794        ),
1795    ),
1796)
1797def test_triple_equal_pinned_dependency_is_used(
1798    runner,
1799    make_package,
1800    make_wheel,
1801    tmpdir,
1802    pkg2_install_requires,
1803    req_in_content,
1804    out_expected_content,
1805):
1806    """
1807    Test that pip-compile properly emits the pinned requirement with ===
1808    torchvision 0.8.2 requires torch==1.7.1 which can resolve to versions with
1809    patches (e.g. torch 1.7.1+cu110), we want torch===1.7.1 without patches
1810    """
1811
1812    dists_dir = tmpdir / "dists"
1813
1814    test_package_1 = make_package("test_package_1", version="0.1.0")
1815    make_wheel(test_package_1, dists_dir)
1816
1817    test_package_2 = make_package(
1818        "test_package_2", version="0.1.0", install_requires=[pkg2_install_requires]
1819    )
1820    make_wheel(test_package_2, dists_dir)
1821
1822    with open("requirements.in", "w") as reqs_in:
1823        for line in req_in_content:
1824            reqs_in.write(line)
1825
1826    out = runner.invoke(cli, ["--find-links", str(dists_dir)])
1827
1828    assert out.exit_code == 0, out
1829    for line in out_expected_content:
1830        assert line in out.stderr
1831
1832
1833METADATA_TEST_CASES = (
1834    pytest.param(
1835        "setup.cfg",
1836        """
1837            [metadata]
1838            name = sample_lib
1839            author = Vincent Driessen
1840            author_email = me@nvie.com
1841
1842            [options]
1843            packages = find:
1844            install_requires =
1845                small-fake-a==0.1
1846                small-fake-b==0.2
1847
1848            [options.extras_require]
1849            dev =
1850                small-fake-c==0.3
1851                small-fake-d==0.4
1852            test =
1853                small-fake-e==0.5
1854                small-fake-f==0.6
1855        """,
1856        id="setup.cfg",
1857    ),
1858    pytest.param(
1859        "setup.py",
1860        """
1861            from setuptools import setup
1862
1863            setup(
1864                name="sample_lib",
1865                version=0.1,
1866                install_requires=["small-fake-a==0.1", "small-fake-b==0.2"],
1867                extras_require={
1868                    "dev": ["small-fake-c==0.3", "small-fake-d==0.4"],
1869                    "test": ["small-fake-e==0.5", "small-fake-f==0.6"],
1870                },
1871            )
1872        """,
1873        id="setup.py",
1874    ),
1875    pytest.param(
1876        "pyproject.toml",
1877        """
1878            [build-system]
1879            requires = ["flit_core >=2,<4"]
1880            build-backend = "flit_core.buildapi"
1881
1882            [tool.flit.metadata]
1883            module = "sample_lib"
1884            author = "Vincent Driessen"
1885            author-email = "me@nvie.com"
1886
1887            requires = ["small-fake-a==0.1", "small-fake-b==0.2"]
1888
1889            [tool.flit.metadata.requires-extra]
1890            dev  = ["small-fake-c==0.3", "small-fake-d==0.4"]
1891            test = ["small-fake-e==0.5", "small-fake-f==0.6"]
1892        """,
1893        id="flit",
1894    ),
1895    pytest.param(
1896        "pyproject.toml",
1897        """
1898            [build-system]
1899            requires = ["poetry_core>=1.0.0"]
1900            build-backend = "poetry.core.masonry.api"
1901
1902            [tool.poetry]
1903            name = "sample_lib"
1904            version = "0.1.0"
1905            description = ""
1906            authors = ["Vincent Driessen <me@nvie.com>"]
1907
1908            [tool.poetry.dependencies]
1909            python = "*"
1910            small-fake-a = "0.1"
1911            small-fake-b = "0.2"
1912
1913            small-fake-c = "0.3"
1914            small-fake-d = "0.4"
1915            small-fake-e = "0.5"
1916            small-fake-f = "0.6"
1917
1918            [tool.poetry.extras]
1919            dev  = ["small-fake-c", "small-fake-d"]
1920            test = ["small-fake-e", "small-fake-f"]
1921        """,
1922        id="poetry",
1923    ),
1924)
1925
1926
1927@pytest.mark.network
1928@pytest.mark.parametrize(("fname", "content"), METADATA_TEST_CASES)
1929@pytest.mark.xfail(is_pypy, reason="https://github.com/jazzband/pip-tools/issues/1375")
1930def test_input_formats(fake_dists, runner, make_module, fname, content):
1931    """
1932    Test different dependency formats as input file.
1933    """
1934    meta_path = make_module(fname=fname, content=content)
1935    out = runner.invoke(cli, ["-n", "--find-links", fake_dists, meta_path])
1936    assert out.exit_code == 0, out.stderr
1937    assert "small-fake-a==0.1" in out.stderr
1938    assert "small-fake-b==0.2" in out.stderr
1939    assert "small-fake-c" not in out.stderr
1940    assert "small-fake-d" not in out.stderr
1941    assert "small-fake-e" not in out.stderr
1942    assert "small-fake-f" not in out.stderr
1943    assert "extra ==" not in out.stderr
1944
1945
1946@pytest.mark.network
1947@pytest.mark.parametrize(("fname", "content"), METADATA_TEST_CASES)
1948@pytest.mark.xfail(is_pypy, reason="https://github.com/jazzband/pip-tools/issues/1375")
1949def test_one_extra(fake_dists, runner, make_module, fname, content):
1950    """
1951    Test one `--extra` (dev) passed, other extras (test) must be ignored.
1952    """
1953    meta_path = make_module(fname=fname, content=content)
1954    out = runner.invoke(
1955        cli, ["-n", "--extra", "dev", "--find-links", fake_dists, meta_path]
1956    )
1957    assert out.exit_code == 0, out.stderr
1958    assert "small-fake-a==0.1" in out.stderr
1959    assert "small-fake-b==0.2" in out.stderr
1960    assert "small-fake-c==0.3" in out.stderr
1961    assert "small-fake-d==0.4" in out.stderr
1962    assert "small-fake-e" not in out.stderr
1963    assert "small-fake-f" not in out.stderr
1964    assert "extra ==" not in out.stderr
1965
1966
1967@pytest.mark.network
1968@pytest.mark.parametrize(
1969    "extra_opts",
1970    (
1971        pytest.param(("--extra", "dev", "--extra", "test"), id="singular"),
1972        pytest.param(("--extra", "dev,test"), id="comma-separated"),
1973    ),
1974)
1975@pytest.mark.parametrize(("fname", "content"), METADATA_TEST_CASES)
1976@pytest.mark.xfail(is_pypy, reason="https://github.com/jazzband/pip-tools/issues/1375")
1977def test_multiple_extras(fake_dists, runner, make_module, fname, content, extra_opts):
1978    """
1979    Test passing multiple `--extra` params.
1980    """
1981    meta_path = make_module(fname=fname, content=content)
1982    out = runner.invoke(
1983        cli,
1984        [
1985            "-n",
1986            *extra_opts,
1987            "--find-links",
1988            fake_dists,
1989            meta_path,
1990        ],
1991    )
1992    assert out.exit_code == 0, out.stderr
1993    assert "small-fake-a==0.1" in out.stderr
1994    assert "small-fake-b==0.2" in out.stderr
1995    assert "small-fake-c==0.3" in out.stderr
1996    assert "small-fake-d==0.4" in out.stderr
1997    assert "small-fake-e==0.5" in out.stderr
1998    assert "small-fake-f==0.6" in out.stderr
1999    assert "extra ==" not in out.stderr
2000
2001
2002def test_extras_fail_with_requirements_in(runner, tmpdir):
2003    """
2004    Test that passing `--extra` with `requirements.in` input file fails.
2005    """
2006    path = os.path.join(tmpdir, "requirements.in")
2007    with open(path, "w") as stream:
2008        stream.write("\n")
2009    out = runner.invoke(cli, ["-n", "--extra", "something", path])
2010    assert out.exit_code == 2
2011    exp = "--extra has effect only with setup.py and PEP-517 input formats"
2012    assert exp in out.stderr
2013
2014
2015def test_cli_compile_strip_extras(runner, make_package, make_sdist, tmpdir):
2016    """
2017    Assures that --strip-extras removes mention of extras from output.
2018    """
2019    test_package_1 = make_package(
2020        "test_package_1", version="0.1", extras_require={"more": "test_package_2"}
2021    )
2022    test_package_2 = make_package(
2023        "test_package_2",
2024        version="0.1",
2025    )
2026    dists_dir = tmpdir / "dists"
2027
2028    for pkg in (test_package_1, test_package_2):
2029        make_sdist(pkg, dists_dir)
2030
2031    with open("requirements.in", "w") as reqs_out:
2032        reqs_out.write("test_package_1[more]")
2033
2034    out = runner.invoke(cli, ["--strip-extras", "--find-links", str(dists_dir)])
2035
2036    assert out.exit_code == 0, out
2037    assert "test-package-2==0.1" in out.stderr
2038    assert "[more]" not in out.stderr
2039