1import os 2import subprocess 3import sys 4from textwrap import dedent 5 6import mock 7import pytest 8from pytest import mark 9 10from .constants import MINIMAL_WHEELS_PATH, PACKAGES_PATH 11from .utils import invoke 12 13from piptools._compat.pip_compat import PIP_VERSION, path_to_url 14from piptools.scripts.compile import cli 15 16 17@pytest.fixture(autouse=True) 18def temp_dep_cache(tmpdir, monkeypatch): 19 monkeypatch.setenv("PIP_TOOLS_CACHE_DIR", str(tmpdir / "cache")) 20 21 22def test_default_pip_conf_read(pip_with_index_conf, runner): 23 # preconditions 24 with open("requirements.in", "w"): 25 pass 26 out = runner.invoke(cli, ["-v"]) 27 28 # check that we have our index-url as specified in pip.conf 29 assert "Using indexes:\n http://example.com" in out.stderr 30 assert "--index-url http://example.com" in out.stderr 31 32 33def test_command_line_overrides_pip_conf(pip_with_index_conf, runner): 34 # preconditions 35 with open("requirements.in", "w"): 36 pass 37 out = runner.invoke(cli, ["-v", "-i", "http://override.com"]) 38 39 # check that we have our index-url as specified in pip.conf 40 assert "Using indexes:\n http://override.com" in out.stderr 41 42 43def test_command_line_setuptools_read(pip_conf, runner): 44 with open("setup.py", "w") as package: 45 package.write( 46 dedent( 47 """\ 48 from setuptools import setup 49 setup( 50 name="fake-setuptools-a", 51 install_requires=["small-fake-a==0.1"] 52 ) 53 """ 54 ) 55 ) 56 out = runner.invoke(cli) 57 58 assert "This file is autogenerated by pip-compile" in out.stderr 59 assert ( 60 "small-fake-a==0.1 # via fake-setuptools-a (setup.py)" 61 in out.stderr.splitlines() 62 ) 63 64 # check that pip-compile generated a configuration file 65 assert os.path.exists("requirements.txt") 66 67 68@pytest.mark.parametrize( 69 "options, expected_output_file", 70 [ 71 # For the `pip-compile` output file should be "requirements.txt" 72 ([], "requirements.txt"), 73 # For the `pip-compile --output-file=output.txt` 74 # output file should be "output.txt" 75 (["--output-file", "output.txt"], "output.txt"), 76 # For the `pip-compile setup.py` output file should be "requirements.txt" 77 (["setup.py"], "requirements.txt"), 78 # For the `pip-compile setup.py --output-file=output.txt` 79 # output file should be "output.txt" 80 (["setup.py", "--output-file", "output.txt"], "output.txt"), 81 ], 82) 83def test_command_line_setuptools_output_file( 84 pip_conf, runner, options, expected_output_file 85): 86 """ 87 Test the output files for setup.py as a requirement file. 88 """ 89 with open("setup.py", "w") as package: 90 package.write( 91 dedent( 92 """\ 93 from setuptools import setup 94 setup(install_requires=[]) 95 """ 96 ) 97 ) 98 99 out = runner.invoke(cli, options) 100 assert out.exit_code == 0 101 assert os.path.exists(expected_output_file) 102 103 104def test_find_links_option(runner): 105 with open("requirements.in", "w") as req_in: 106 req_in.write("-f ./libs3") 107 108 out = runner.invoke(cli, ["-v", "-f", "./libs1", "-f", "./libs2"]) 109 110 # Check that find-links has been passed to pip 111 assert "Configuration:\n -f ./libs1\n -f ./libs2\n -f ./libs3\n" in out.stderr 112 113 # Check that find-links has been written to a requirements.txt 114 with open("requirements.txt", "r") as req_txt: 115 assert ( 116 "--find-links ./libs1\n--find-links ./libs2\n--find-links ./libs3\n" 117 in req_txt.read() 118 ) 119 120 121def test_extra_index_option(pip_with_index_conf, runner): 122 with open("requirements.in", "w"): 123 pass 124 out = runner.invoke( 125 cli, 126 [ 127 "-v", 128 "--extra-index-url", 129 "http://extraindex1.com", 130 "--extra-index-url", 131 "http://extraindex2.com", 132 ], 133 ) 134 assert ( 135 "Using indexes:\n" 136 " http://example.com\n" 137 " http://extraindex1.com\n" 138 " http://extraindex2.com" in out.stderr 139 ) 140 assert ( 141 "--index-url http://example.com\n" 142 "--extra-index-url http://extraindex1.com\n" 143 "--extra-index-url http://extraindex2.com" in out.stderr 144 ) 145 146 147def test_trusted_host(pip_conf, runner): 148 with open("requirements.in", "w"): 149 pass 150 out = runner.invoke( 151 cli, ["-v", "--trusted-host", "example.com", "--trusted-host", "example2.com"] 152 ) 153 assert "--trusted-host example.com\n" "--trusted-host example2.com\n" in out.stderr 154 155 156def test_trusted_host_no_emit(pip_conf, runner): 157 with open("requirements.in", "w"): 158 pass 159 out = runner.invoke( 160 cli, ["-v", "--trusted-host", "example.com", "--no-emit-trusted-host"] 161 ) 162 assert "--trusted-host example.com" not in out.stderr 163 164 165def test_find_links_no_emit(pip_conf, runner): 166 with open("requirements.in", "w"): 167 pass 168 out = runner.invoke(cli, ["-v", "--no-emit-find-links"]) 169 assert "--find-links" not in out.stderr 170 171 172@pytest.mark.network 173def test_realistic_complex_sub_dependencies(runner): 174 wheels_dir = "wheels" 175 176 # make a temporary wheel of a fake package 177 subprocess.check_output( 178 [ 179 "pip", 180 "wheel", 181 "--no-deps", 182 "-w", 183 wheels_dir, 184 os.path.join(PACKAGES_PATH, "fake_with_deps", "."), 185 ] 186 ) 187 188 with open("requirements.in", "w") as req_in: 189 req_in.write("fake_with_deps") # require fake package 190 191 out = runner.invoke(cli, ["-v", "-n", "--rebuild", "-f", wheels_dir]) 192 193 assert out.exit_code == 0 194 195 196def test_run_as_module_compile(): 197 """piptools can be run as ``python -m piptools ...``.""" 198 199 status, output = invoke([sys.executable, "-m", "piptools", "compile", "--help"]) 200 201 # Should have run pip-compile successfully. 202 output = output.decode("utf-8") 203 assert output.startswith("Usage:") 204 assert "Compiles requirements.txt from requirements.in" in output 205 assert status == 0 206 207 208def test_editable_package(pip_conf, runner): 209 """ piptools can compile an editable """ 210 fake_package_dir = os.path.join(PACKAGES_PATH, "small_fake_with_deps") 211 fake_package_dir = path_to_url(fake_package_dir) 212 with open("requirements.in", "w") as req_in: 213 req_in.write("-e " + fake_package_dir) # require editable fake package 214 215 out = runner.invoke(cli, ["-n"]) 216 217 assert out.exit_code == 0 218 assert fake_package_dir in out.stderr 219 assert "small-fake-a==0.1" in out.stderr 220 221 222@pytest.mark.network 223def test_editable_package_vcs(runner): 224 vcs_package = ( 225 "git+git://github.com/jazzband/pip-tools@" 226 "f97e62ecb0d9b70965c8eff952c001d8e2722e94" 227 "#egg=pip-tools" 228 ) 229 with open("requirements.in", "w") as req_in: 230 req_in.write("-e " + vcs_package) 231 out = runner.invoke(cli, ["-n", "--rebuild"]) 232 assert out.exit_code == 0 233 assert vcs_package in out.stderr 234 assert "click" in out.stderr # dependency of pip-tools 235 236 237def test_locally_available_editable_package_is_not_archived_in_cache_dir( 238 pip_conf, tmpdir, runner 239): 240 """ 241 piptools will not create an archive for a locally available editable requirement 242 """ 243 cache_dir = tmpdir.mkdir("cache_dir") 244 245 fake_package_dir = os.path.join(PACKAGES_PATH, "small_fake_with_deps") 246 fake_package_dir = path_to_url(fake_package_dir) 247 248 with open("requirements.in", "w") as req_in: 249 req_in.write("-e " + fake_package_dir) # require editable fake package 250 251 out = runner.invoke(cli, ["-n", "--rebuild", "--cache-dir", str(cache_dir)]) 252 253 assert out.exit_code == 0 254 assert fake_package_dir in out.stderr 255 assert "small-fake-a==0.1" in out.stderr 256 257 # we should not find any archived file in {cache_dir}/pkgs 258 assert not os.listdir(os.path.join(str(cache_dir), "pkgs")) 259 260 261@mark.parametrize( 262 ("line", "dependency"), 263 [ 264 # zip URL 265 # use pip-tools version prior to its use of setuptools_scm, 266 # which is incompatible with https: install 267 ( 268 "https://github.com/jazzband/pip-tools/archive/" 269 "7d86c8d3ecd1faa6be11c7ddc6b29a30ffd1dae3.zip", 270 "\nclick==", 271 ), 272 # scm URL 273 ( 274 "git+git://github.com/jazzband/pip-tools@" 275 "7d86c8d3ecd1faa6be11c7ddc6b29a30ffd1dae3", 276 "\nclick==", 277 ), 278 # wheel URL 279 ( 280 "https://files.pythonhosted.org/packages/06/96/" 281 "89872db07ae70770fba97205b0737c17ef013d0d1c790" 282 "899c16bb8bac419/pip_tools-3.6.1-py2.py3-none-any.whl", 283 "\nclick==", 284 ), 285 ], 286) 287@mark.parametrize(("generate_hashes",), [(True,), (False,)]) 288@pytest.mark.network 289def test_url_package(runner, line, dependency, generate_hashes): 290 with open("requirements.in", "w") as req_in: 291 req_in.write(line) 292 out = runner.invoke( 293 cli, ["-n", "--rebuild"] + (["--generate-hashes"] if generate_hashes else []) 294 ) 295 assert out.exit_code == 0 296 assert dependency in out.stderr 297 298 299@mark.parametrize( 300 ("line", "dependency", "rewritten_line"), 301 [ 302 # file:// wheel URL 303 ( 304 path_to_url( 305 os.path.join( 306 MINIMAL_WHEELS_PATH, "small_fake_with_deps-0.1-py2.py3-none-any.whl" 307 ) 308 ), 309 "\nsmall-fake-a==0.1", 310 None, 311 ), 312 # file:// directory 313 ( 314 path_to_url(os.path.join(PACKAGES_PATH, "small_fake_with_deps")), 315 "\nsmall-fake-a==0.1", 316 None, 317 ), 318 # bare path 319 # will be rewritten to file:// URL 320 ( 321 os.path.join( 322 MINIMAL_WHEELS_PATH, "small_fake_with_deps-0.1-py2.py3-none-any.whl" 323 ), 324 "\nsmall-fake-a==0.1", 325 path_to_url( 326 os.path.join( 327 MINIMAL_WHEELS_PATH, "small_fake_with_deps-0.1-py2.py3-none-any.whl" 328 ) 329 ), 330 ), 331 ], 332) 333@mark.parametrize(("generate_hashes",), [(True,), (False,)]) 334def test_local_url_package( 335 pip_conf, runner, line, dependency, rewritten_line, generate_hashes 336): 337 if rewritten_line is None: 338 rewritten_line = line 339 with open("requirements.in", "w") as req_in: 340 req_in.write(line) 341 out = runner.invoke( 342 cli, ["-n", "--rebuild"] + (["--generate-hashes"] if generate_hashes else []) 343 ) 344 assert out.exit_code == 0 345 assert rewritten_line in out.stderr 346 assert dependency in out.stderr 347 348 349def test_input_file_without_extension(pip_conf, runner): 350 """ 351 piptools can compile a file without an extension, 352 and add .txt as the defaut output file extension. 353 """ 354 with open("requirements", "w") as req_in: 355 req_in.write("small-fake-a==0.1") 356 357 out = runner.invoke(cli, ["requirements"]) 358 359 assert out.exit_code == 0 360 assert "small-fake-a==0.1" in out.stderr 361 assert os.path.exists("requirements.txt") 362 363 364def test_upgrade_packages_option(pip_conf, runner): 365 """ 366 piptools respects --upgrade-package/-P inline list. 367 """ 368 with open("requirements.in", "w") as req_in: 369 req_in.write("small-fake-a\nsmall-fake-b") 370 with open("requirements.txt", "w") as req_in: 371 req_in.write("small-fake-a==0.1\nsmall-fake-b==0.1") 372 373 out = runner.invoke(cli, ["--no-annotate", "-P", "small-fake-b"]) 374 375 assert out.exit_code == 0 376 assert "small-fake-a==0.1" in out.stderr.splitlines() 377 assert "small-fake-b==0.3" in out.stderr.splitlines() 378 379 380def test_upgrade_packages_option_irrelevant(pip_conf, runner): 381 """ 382 piptools ignores --upgrade-package/-P items not already constrained. 383 """ 384 with open("requirements.in", "w") as req_in: 385 req_in.write("small-fake-a") 386 with open("requirements.txt", "w") as req_in: 387 req_in.write("small-fake-a==0.1") 388 389 out = runner.invoke(cli, ["--no-annotate", "--upgrade-package", "small-fake-b"]) 390 391 assert out.exit_code == 0 392 assert "small-fake-a==0.1" in out.stderr.splitlines() 393 assert "small-fake-b==0.3" not in out.stderr.splitlines() 394 395 396def test_upgrade_packages_option_no_existing_file(pip_conf, runner): 397 """ 398 piptools respects --upgrade-package/-P inline list when the output file 399 doesn't exist. 400 """ 401 with open("requirements.in", "w") as req_in: 402 req_in.write("small-fake-a\nsmall-fake-b") 403 404 out = runner.invoke(cli, ["--no-annotate", "-P", "small-fake-b"]) 405 406 assert out.exit_code == 0 407 assert "small-fake-a==0.2" in out.stderr.splitlines() 408 assert "small-fake-b==0.3" in out.stderr.splitlines() 409 410 411@pytest.mark.parametrize( 412 "current_package, upgraded_package", 413 ( 414 pytest.param("small-fake-b==0.1", "small-fake-b==0.3", id="upgrade"), 415 pytest.param("small-fake-b==0.3", "small-fake-b==0.1", id="downgrade"), 416 ), 417) 418def test_upgrade_packages_version_option( 419 pip_conf, runner, current_package, upgraded_package 420): 421 """ 422 piptools respects --upgrade-package/-P inline list with specified versions. 423 """ 424 with open("requirements.in", "w") as req_in: 425 req_in.write("small-fake-a\nsmall-fake-b") 426 with open("requirements.txt", "w") as req_in: 427 req_in.write("small-fake-a==0.1\n" + current_package) 428 429 out = runner.invoke(cli, ["--no-annotate", "--upgrade-package", upgraded_package]) 430 431 assert out.exit_code == 0 432 stderr_lines = out.stderr.splitlines() 433 assert "small-fake-a==0.1" in stderr_lines 434 assert upgraded_package in stderr_lines 435 436 437def test_upgrade_packages_version_option_no_existing_file(pip_conf, runner): 438 """ 439 piptools respects --upgrade-package/-P inline list with specified versions. 440 """ 441 with open("requirements.in", "w") as req_in: 442 req_in.write("small-fake-a\nsmall-fake-b") 443 444 out = runner.invoke(cli, ["-P", "small-fake-b==0.2"]) 445 446 assert out.exit_code == 0 447 assert "small-fake-a==0.2" in out.stderr 448 assert "small-fake-b==0.2" in out.stderr 449 450 451def test_upgrade_packages_version_option_and_upgrade(pip_conf, runner): 452 """ 453 piptools respects --upgrade-package/-P inline list with specified versions 454 whilst also doing --upgrade. 455 """ 456 with open("requirements.in", "w") as req_in: 457 req_in.write("small-fake-a\nsmall-fake-b") 458 with open("requirements.txt", "w") as req_in: 459 req_in.write("small-fake-a==0.1\nsmall-fake-b==0.1") 460 461 out = runner.invoke(cli, ["--upgrade", "-P", "small-fake-b==0.1"]) 462 463 assert out.exit_code == 0 464 assert "small-fake-a==0.2" in out.stderr 465 assert "small-fake-b==0.1" in out.stderr 466 467 468def test_upgrade_packages_version_option_and_upgrade_no_existing_file(pip_conf, runner): 469 """ 470 piptools respects --upgrade-package/-P inline list with specified versions 471 whilst also doing --upgrade and the output file doesn't exist. 472 """ 473 with open("requirements.in", "w") as req_in: 474 req_in.write("small-fake-a\nsmall-fake-b") 475 476 out = runner.invoke(cli, ["--upgrade", "-P", "small-fake-b==0.1"]) 477 478 assert out.exit_code == 0 479 assert "small-fake-a==0.2" in out.stderr 480 assert "small-fake-b==0.1" in out.stderr 481 482 483def test_quiet_option(runner): 484 with open("requirements", "w"): 485 pass 486 out = runner.invoke(cli, ["--quiet", "-n", "requirements"]) 487 # Pinned requirements result has not been written to output. 488 assert not out.stderr_bytes 489 490 491def test_dry_run_noisy_option(runner): 492 with open("requirements", "w"): 493 pass 494 out = runner.invoke(cli, ["--dry-run", "requirements"]) 495 # Dry-run message has been written to output 496 assert "Dry-run, so nothing updated." in out.stderr.splitlines() 497 498 499def test_dry_run_quiet_option(runner): 500 with open("requirements", "w"): 501 pass 502 out = runner.invoke(cli, ["--dry-run", "--quiet", "requirements"]) 503 # Dry-run message has not been written to output. 504 assert not out.stderr_bytes 505 506 507def test_generate_hashes_with_editable(pip_conf, runner): 508 small_fake_package_dir = os.path.join(PACKAGES_PATH, "small_fake_with_deps") 509 small_fake_package_url = path_to_url(small_fake_package_dir) 510 with open("requirements.in", "w") as fp: 511 fp.write("-e {}\n".format(small_fake_package_url)) 512 out = runner.invoke(cli, ["--no-annotate", "--generate-hashes"]) 513 expected = ( 514 "-e {}\n" 515 "small-fake-a==0.1 \\\n" 516 " --hash=sha256:5e6071ee6e4c59e0d0408d366f" 517 "e9b66781d2cf01be9a6e19a2433bb3c5336330\n" 518 "small-fake-b==0.1 \\\n" 519 " --hash=sha256:acdba8f8b8a816213c30d5310c" 520 "3fe296c0107b16ed452062f7f994a5672e3b3f\n" 521 ).format(small_fake_package_url) 522 assert out.exit_code == 0 523 assert expected in out.stderr 524 525 526@pytest.mark.network 527def test_generate_hashes_with_url(runner): 528 with open("requirements.in", "w") as fp: 529 fp.write( 530 "https://github.com/jazzband/pip-tools/archive/" 531 "7d86c8d3ecd1faa6be11c7ddc6b29a30ffd1dae3.zip#egg=pip-tools\n" 532 ) 533 out = runner.invoke(cli, ["--no-annotate", "--generate-hashes"]) 534 expected = ( 535 "https://github.com/jazzband/pip-tools/archive/" 536 "7d86c8d3ecd1faa6be11c7ddc6b29a30ffd1dae3.zip#egg=pip-tools \\\n" 537 " --hash=sha256:d24de92e18ad5bf291f25cfcdcf" 538 "0171be6fa70d01d0bef9eeda356b8549715e7\n" 539 ) 540 assert out.exit_code == 0 541 assert expected in out.stderr 542 543 544def test_generate_hashes_verbose(pip_conf, runner): 545 """ 546 The hashes generation process should show a progress. 547 """ 548 with open("requirements.in", "w") as fp: 549 fp.write("small-fake-a==0.1") 550 551 out = runner.invoke(cli, ["--generate-hashes", "-v"]) 552 expected_verbose_text = "Generating hashes:\n small-fake-a\n" 553 assert expected_verbose_text in out.stderr 554 555 556@pytest.mark.skipif(PIP_VERSION < (9,), reason="needs pip 9 or greater") 557def test_filter_pip_markers(pip_conf, runner): 558 """ 559 Check that pip-compile works with pip environment markers (PEP496) 560 """ 561 with open("requirements", "w") as req_in: 562 req_in.write( 563 "small-fake-a==0.1\n" "unknown_package==0.1; python_version == '1'" 564 ) 565 566 out = runner.invoke(cli, ["-n", "requirements"]) 567 568 assert out.exit_code == 0 569 assert "small-fake-a==0.1" in out.stderr 570 assert "unknown_package" not in out.stderr 571 572 573def test_no_candidates(pip_conf, runner): 574 with open("requirements", "w") as req_in: 575 req_in.write("small-fake-a>0.3b1,<0.3b2") 576 577 out = runner.invoke(cli, ["-n", "requirements"]) 578 579 assert out.exit_code == 2 580 assert "Skipped pre-versions:" in out.stderr 581 582 583def test_no_candidates_pre(pip_conf, runner): 584 with open("requirements", "w") as req_in: 585 req_in.write("small-fake-a>0.3b1,<0.3b1") 586 587 out = runner.invoke(cli, ["-n", "requirements", "--pre"]) 588 589 assert out.exit_code == 2 590 assert "Tried pre-versions:" in out.stderr 591 592 593def test_default_index_url(pip_with_index_conf): 594 status, output = invoke([sys.executable, "-m", "piptools", "compile", "--help"]) 595 output = output.decode("utf-8") 596 597 # Click's subprocess output has \r\r\n line endings on win py27. Fix it. 598 output = output.replace("\r\r", "\r") 599 600 assert status == 0 601 expected = ( 602 " -i, --index-url TEXT Change index URL (defaults to" 603 + os.linesep 604 + " http://example.com)" 605 + os.linesep 606 ) 607 assert expected in output 608 609 610def test_stdin_without_output_file(runner): 611 """ 612 The --output-file option is required for STDIN. 613 """ 614 out = runner.invoke(cli, ["-n", "-"]) 615 616 assert out.exit_code == 2 617 assert "--output-file is required if input is from stdin" in out.stderr 618 619 620def test_not_specified_input_file(runner): 621 """ 622 It should raise an error if there are no input files or default input files 623 such as "setup.py" or "requirements.in". 624 """ 625 out = runner.invoke(cli) 626 assert "If you do not specify an input file" in out.stderr 627 assert out.exit_code == 2 628 629 630def test_stdin(pip_conf, runner): 631 """ 632 Test compile requirements from STDIN. 633 """ 634 out = runner.invoke( 635 cli, ["-", "--output-file", "requirements.txt", "-n"], input="small-fake-a==0.1" 636 ) 637 638 assert "small-fake-a==0.1 # via -r -" in out.stderr.splitlines() 639 640 641def test_multiple_input_files_without_output_file(runner): 642 """ 643 The --output-file option is required for multiple requirement input files. 644 """ 645 with open("src_file1.in", "w") as req_in: 646 req_in.write("six==1.10.0") 647 648 with open("src_file2.in", "w") as req_in: 649 req_in.write("django==2.1") 650 651 out = runner.invoke(cli, ["src_file1.in", "src_file2.in"]) 652 653 assert ( 654 "--output-file is required if two or more input files are given" in out.stderr 655 ) 656 assert out.exit_code == 2 657 658 659@pytest.mark.parametrize( 660 "option, expected", 661 [ 662 ( 663 "--annotate", 664 "small-fake-a==0.1 " 665 "# via -c constraints.txt, small-fake-with-deps\n", 666 ), 667 ("--no-annotate", "small-fake-a==0.1\n"), 668 ], 669) 670def test_annotate_option(pip_conf, runner, option, expected): 671 """ 672 The output lines has have annotations if option is turned on. 673 """ 674 with open("constraints.txt", "w") as constraints_in: 675 constraints_in.write("small-fake-a==0.1") 676 with open("requirements.in", "w") as req_in: 677 req_in.write("-c constraints.txt\n") 678 req_in.write("small_fake_with_deps") 679 680 out = runner.invoke(cli, [option, "-n"]) 681 682 assert expected in out.stderr 683 assert out.exit_code == 0 684 685 686@pytest.mark.parametrize( 687 "option, expected", 688 [("--allow-unsafe", "small-fake-a==0.1"), (None, "# small-fake-a")], 689) 690def test_allow_unsafe_option(pip_conf, monkeypatch, runner, option, expected): 691 """ 692 Unsafe packages are printed as expected with and without --allow-unsafe. 693 """ 694 monkeypatch.setattr("piptools.resolver.UNSAFE_PACKAGES", {"small-fake-a"}) 695 with open("requirements.in", "w") as req_in: 696 req_in.write(path_to_url(os.path.join(PACKAGES_PATH, "small_fake_with_deps"))) 697 698 out = runner.invoke(cli, ["--no-annotate", option] if option else []) 699 700 assert expected in out.stderr.splitlines() 701 assert out.exit_code == 0 702 703 704@pytest.mark.parametrize( 705 "option, attr, expected", 706 [("--cert", "cert", "foo.crt"), ("--client-cert", "client_cert", "bar.pem")], 707) 708@mock.patch("piptools.scripts.compile.parse_requirements") 709def test_cert_option(parse_requirements, runner, option, attr, expected): 710 """ 711 The options --cert and --client-crt have to be passed to the PyPIRepository. 712 """ 713 with open("requirements.in", "w"): 714 pass 715 716 runner.invoke(cli, [option, expected]) 717 718 # Ensure the options in parse_requirements has the expected option 719 assert getattr(parse_requirements.call_args.kwargs["options"], attr) == expected 720 721 722@pytest.mark.parametrize( 723 "option, expected", [("--build-isolation", True), ("--no-build-isolation", False)] 724) 725@mock.patch("piptools.scripts.compile.PyPIRepository") 726@mock.patch("piptools.scripts.compile.parse_requirements") # prevent to parse 727def test_build_isolation_option( 728 parse_requirements, PyPIRepository, runner, option, expected 729): 730 """ 731 A value of the --build-isolation/--no-build-isolation flag 732 must be passed to the PyPIRepository. 733 """ 734 with open("requirements.in", "w"): 735 pass 736 737 runner.invoke(cli, [option]) 738 739 # Ensure the build_isolation option in PyPIRepository has the expected value. 740 assert PyPIRepository.call_args.kwargs["build_isolation"] is expected 741 742 743@pytest.mark.parametrize( 744 "cli_option, infile_option, expected_package", 745 [ 746 # no --pre pip-compile should resolve to the last stable version 747 (False, False, "small-fake-a==0.2"), 748 # pip-compile --pre should resolve to the last pre-released version 749 (True, False, "small-fake-a==0.3b1"), 750 (False, True, "small-fake-a==0.3b1"), 751 (True, True, "small-fake-a==0.3b1"), 752 ], 753) 754def test_pre_option(pip_conf, runner, cli_option, infile_option, expected_package): 755 """ 756 Tests pip-compile respects --pre option. 757 """ 758 with open("requirements.in", "w") as req_in: 759 if infile_option: 760 req_in.write("--pre\n") 761 req_in.write("small-fake-a\n") 762 763 out = runner.invoke(cli, ["--no-annotate", "-n"] + (["-p"] if cli_option else [])) 764 765 assert out.exit_code == 0, out.stderr 766 assert expected_package in out.stderr.splitlines(), out.stderr 767 768 769@pytest.mark.parametrize( 770 "add_options", 771 [ 772 [], 773 ["--output-file", "requirements.txt"], 774 ["--upgrade"], 775 ["--upgrade", "--output-file", "requirements.txt"], 776 ["--upgrade-package", "small-fake-a"], 777 ["--upgrade-package", "small-fake-a", "--output-file", "requirements.txt"], 778 ], 779) 780def test_dry_run_option(pip_conf, runner, add_options): 781 """ 782 Tests pip-compile doesn't create requirements.txt file on dry-run. 783 """ 784 with open("requirements.in", "w") as req_in: 785 req_in.write("small-fake-a\n") 786 787 out = runner.invoke(cli, ["--no-annotate", "--dry-run"] + add_options) 788 789 assert out.exit_code == 0, out.stderr 790 assert "small-fake-a==0.2" in out.stderr.splitlines() 791 assert not os.path.exists("requirements.txt") 792 793 794@pytest.mark.parametrize( 795 "add_options, expected_cli_output_package", 796 [ 797 ([], "small-fake-a==0.1"), 798 (["--output-file", "requirements.txt"], "small-fake-a==0.1"), 799 (["--upgrade"], "small-fake-a==0.2"), 800 (["--upgrade", "--output-file", "requirements.txt"], "small-fake-a==0.2"), 801 (["--upgrade-package", "small-fake-a"], "small-fake-a==0.2"), 802 ( 803 ["--upgrade-package", "small-fake-a", "--output-file", "requirements.txt"], 804 "small-fake-a==0.2", 805 ), 806 ], 807) 808def test_dry_run_doesnt_touch_output_file( 809 pip_conf, runner, add_options, expected_cli_output_package 810): 811 """ 812 Tests pip-compile doesn't touch requirements.txt file on dry-run. 813 """ 814 with open("requirements.in", "w") as req_in: 815 req_in.write("small-fake-a\n") 816 817 with open("requirements.txt", "w") as req_txt: 818 req_txt.write("small-fake-a==0.1\n") 819 820 before_compile_mtime = os.stat("requirements.txt").st_mtime 821 822 out = runner.invoke(cli, ["--no-annotate", "--dry-run"] + add_options) 823 824 assert out.exit_code == 0, out.stderr 825 assert expected_cli_output_package in out.stderr.splitlines() 826 827 # The package version must NOT be updated in the output file 828 with open("requirements.txt", "r") as req_txt: 829 assert "small-fake-a==0.1" in req_txt.read().splitlines() 830 831 # The output file must not be touched 832 after_compile_mtime = os.stat("requirements.txt").st_mtime 833 assert after_compile_mtime == before_compile_mtime 834 835 836@pytest.mark.parametrize( 837 "empty_input_pkg, prior_output_pkg", 838 [ 839 ("", ""), 840 ("", "small-fake-a==0.1\n"), 841 ("# Nothing to see here", ""), 842 ("# Nothing to see here", "small-fake-a==0.1\n"), 843 ], 844) 845def test_empty_input_file_no_header(runner, empty_input_pkg, prior_output_pkg): 846 """ 847 Tests pip-compile creates an empty requirements.txt file, 848 given --no-header and empty requirements.in 849 """ 850 with open("requirements.in", "w") as req_in: 851 req_in.write(empty_input_pkg) # empty input file 852 853 with open("requirements.txt", "w") as req_txt: 854 req_txt.write(prior_output_pkg) 855 856 runner.invoke(cli, ["--no-header", "requirements.in"]) 857 858 with open("requirements.txt", "r") as req_txt: 859 assert req_txt.read().strip() == "" 860 861 862def test_upgrade_package_doesnt_remove_annotation(pip_conf, runner): 863 """ 864 Tests pip-compile --upgrade-package shouldn't remove "via" annotation. 865 See: GH-929 866 """ 867 with open("requirements.in", "w") as req_in: 868 req_in.write("small-fake-with-deps\n") 869 870 runner.invoke(cli) 871 872 # Downgrade small-fake-a to 0.1 873 with open("requirements.txt", "w") as req_txt: 874 req_txt.write( 875 "small-fake-with-deps==0.1\n" 876 "small-fake-a==0.1 # via small-fake-with-deps\n" 877 ) 878 879 runner.invoke(cli, ["-P", "small-fake-a"]) 880 with open("requirements.txt", "r") as req_txt: 881 assert ( 882 "small-fake-a==0.1 # via small-fake-with-deps" 883 in req_txt.read().splitlines() 884 ) 885 886 887@pytest.mark.parametrize( 888 "options", 889 [ 890 # TODO add --no-index support in OutputWriter 891 # "--no-index", 892 "--index-url https://example.com", 893 "--extra-index-url https://example.com", 894 "--find-links ./libs1", 895 "--trusted-host example.com", 896 "--no-binary :all:", 897 "--only-binary :all:", 898 ], 899) 900def test_options_in_requirements_file(runner, options): 901 """ 902 Test the options from requirements.in is copied to requirements.txt. 903 """ 904 with open("requirements.in", "w") as reqs_in: 905 reqs_in.write(options) 906 907 out = runner.invoke(cli) 908 assert out.exit_code == 0, out 909 910 with open("requirements.txt") as reqs_txt: 911 assert options in reqs_txt.read().splitlines() 912 913 914@pytest.mark.parametrize( 915 "cli_options, expected_message", 916 ( 917 pytest.param( 918 ["--index-url", "file:foo"], 919 "Was file:foo reachable?", 920 id="single index url", 921 ), 922 pytest.param( 923 ["--index-url", "file:foo", "--extra-index-url", "file:bar"], 924 "Were file:foo or file:bar reachable?", 925 id="multiple index urls", 926 ), 927 ), 928) 929def test_unreachable_index_urls(runner, cli_options, expected_message): 930 """ 931 Test pip-compile raises an error if index URLs are not reachable. 932 """ 933 with open("requirements.in", "w") as reqs_in: 934 reqs_in.write("some-package") 935 936 out = runner.invoke(cli, cli_options) 937 938 assert out.exit_code == 2, out 939 940 stderr_lines = out.stderr.splitlines() 941 assert "No versions found" in stderr_lines 942 assert expected_message in stderr_lines 943 944 945@pytest.mark.parametrize( 946 "current_package, upgraded_package", 947 ( 948 pytest.param("small-fake-b==0.1", "small-fake-b==0.2", id="upgrade"), 949 pytest.param("small-fake-b==0.2", "small-fake-b==0.1", id="downgrade"), 950 ), 951) 952def test_upgrade_packages_option_subdependency( 953 pip_conf, runner, current_package, upgraded_package 954): 955 """ 956 Test that pip-compile --upgrade-package/-P upgrades/dpwngrades subdependencies. 957 """ 958 959 with open("requirements.in", "w") as reqs: 960 reqs.write("small-fake-with-unpinned-deps\n") 961 962 with open("requirements.txt", "w") as reqs: 963 reqs.write("small-fake-a==0.1\n") 964 reqs.write(current_package + "\n") 965 reqs.write("small-fake-with-unpinned-deps==0.1\n") 966 967 out = runner.invoke( 968 cli, ["--no-annotate", "--dry-run", "--upgrade-package", upgraded_package] 969 ) 970 971 stderr_lines = out.stderr.splitlines() 972 assert "small-fake-a==0.1" in stderr_lines, "small-fake-a must keep its version" 973 assert ( 974 upgraded_package in stderr_lines 975 ), "{} must be upgraded/downgraded to {}".format(current_package, upgraded_package) 976 977 978@pytest.mark.parametrize( 979 "input_opts, output_opts", 980 ( 981 # Test that input options overwrite output options 982 pytest.param( 983 "--index-url https://index-url", 984 "--index-url https://another-index-url", 985 id="index url", 986 ), 987 pytest.param( 988 "--extra-index-url https://extra-index-url", 989 "--extra-index-url https://another-extra-index-url", 990 id="extra index url", 991 ), 992 pytest.param("--find-links dir", "--find-links another-dir", id="find links"), 993 pytest.param( 994 "--trusted-host hostname", 995 "--trusted-host another-hostname", 996 id="trusted host", 997 ), 998 pytest.param( 999 "--no-binary :package:", "--no-binary :another-package:", id="no binary" 1000 ), 1001 pytest.param( 1002 "--only-binary :package:", 1003 "--only-binary :another-package:", 1004 id="only binary", 1005 ), 1006 # Test misc corner cases 1007 pytest.param("", "--index-url https://index-url", id="empty input options"), 1008 pytest.param( 1009 "--index-url https://index-url", 1010 ( 1011 "--index-url https://index-url\n" 1012 "--extra-index-url https://another-extra-index-url" 1013 ), 1014 id="partially matched options", 1015 ), 1016 ), 1017) 1018def test_remove_outdated_options(runner, input_opts, output_opts): 1019 """ 1020 Test that the options from the current requirements.txt wouldn't stay 1021 after compile if they were removed from requirements.in file. 1022 """ 1023 with open("requirements.in", "w") as req_in: 1024 req_in.write(input_opts) 1025 with open("requirements.txt", "w") as req_txt: 1026 req_txt.write(output_opts) 1027 1028 out = runner.invoke(cli, ["--no-header"]) 1029 1030 assert out.exit_code == 0, out 1031 assert out.stderr.strip() == input_opts 1032 1033 1034def test_sub_dependencies_with_constraints(pip_conf, runner): 1035 # Write constraints file 1036 with open("constraints.txt", "w") as constraints_in: 1037 constraints_in.write("small-fake-a==0.1\n") 1038 constraints_in.write("small-fake-b==0.2\n") 1039 constraints_in.write("small-fake-with-unpinned-deps==0.1") 1040 1041 with open("requirements.in", "w") as req_in: 1042 req_in.write("-c constraints.txt\n") 1043 req_in.write("small_fake_with_deps_and_sub_deps") # require fake package 1044 1045 out = runner.invoke(cli, ["--no-annotate"]) 1046 1047 assert out.exit_code == 0 1048 1049 req_out_lines = set(out.stderr.splitlines()) 1050 assert { 1051 "small-fake-a==0.1", 1052 "small-fake-b==0.2", 1053 "small-fake-with-deps-and-sub-deps==0.1", 1054 "small-fake-with-unpinned-deps==0.1", 1055 }.issubset(req_out_lines) 1056