1import os 2import sys 3from json import loads 4 5import pytest 6 7from pdm.cli import actions 8from pdm.exceptions import InvalidPyVersion, PdmException, PdmUsageError 9from pdm.models.requirements import parse_requirement 10from pdm.models.specifiers import PySpecSet 11 12 13@pytest.mark.usefixtures("repository", "working_set", "vcs") 14def test_remove_both_normal_and_editable_packages(project, is_dev): 15 project.environment.python_requires = PySpecSet(">=3.6") 16 actions.do_add(project, is_dev, packages=["demo"]) 17 actions.do_add( 18 project, 19 is_dev, 20 editables=["git+https://github.com/test-root/demo.git#egg=demo"], 21 ) 22 group = ( 23 project.tool_settings["dev-dependencies"]["dev"] 24 if is_dev 25 else project.meta["dependencies"] 26 ) 27 actions.do_remove(project, is_dev, packages=["demo"]) 28 assert not group 29 assert "demo" not in project.locked_repository.all_candidates 30 31 32@pytest.mark.usefixtures("working_set") 33def test_update_all_packages(project, repository, capsys): 34 actions.do_add(project, packages=["requests", "pytz"]) 35 repository.add_candidate("pytz", "2019.6") 36 repository.add_candidate("chardet", "3.0.5") 37 repository.add_candidate("requests", "2.20.0") 38 repository.add_dependencies( 39 "requests", 40 "2.20.0", 41 [ 42 "certifi>=2017.4.17", 43 "chardet<3.1.0,>=3.0.2", 44 "idna<2.8,>=2.5", 45 "urllib3<1.24,>=1.21.1", 46 ], 47 ) 48 actions.do_update(project) 49 locked_candidates = project.locked_repository.all_candidates 50 assert locked_candidates["requests"].version == "2.20.0" 51 assert locked_candidates["chardet"].version == "3.0.5" 52 assert locked_candidates["pytz"].version == "2019.6" 53 out, _ = capsys.readouterr() 54 assert "3 to update" in out, out 55 56 actions.do_sync(project) 57 out, _ = capsys.readouterr() 58 assert "All packages are synced to date" in out 59 60 61@pytest.mark.usefixtures("working_set") 62def test_update_dry_run(project, repository, capsys): 63 actions.do_add(project, packages=["requests", "pytz"]) 64 repository.add_candidate("pytz", "2019.6") 65 repository.add_candidate("chardet", "3.0.5") 66 repository.add_candidate("requests", "2.20.0") 67 repository.add_dependencies( 68 "requests", 69 "2.20.0", 70 [ 71 "certifi>=2017.4.17", 72 "chardet<3.1.0,>=3.0.2", 73 "idna<2.8,>=2.5", 74 "urllib3<1.24,>=1.21.1", 75 ], 76 ) 77 actions.do_update(project, dry_run=True) 78 project.lockfile = None 79 locked_candidates = project.locked_repository.all_candidates 80 assert locked_candidates["requests"].version == "2.19.1" 81 assert locked_candidates["chardet"].version == "3.0.4" 82 assert locked_candidates["pytz"].version == "2019.3" 83 out, _ = capsys.readouterr() 84 assert "requests 2.19.1 -> 2.20.0" in out 85 86 87@pytest.mark.usefixtures("working_set") 88def test_update_top_packages_dry_run(project, repository, capsys): 89 actions.do_add(project, packages=["requests", "pytz"]) 90 repository.add_candidate("pytz", "2019.6") 91 repository.add_candidate("chardet", "3.0.5") 92 repository.add_candidate("requests", "2.20.0") 93 repository.add_dependencies( 94 "requests", 95 "2.20.0", 96 [ 97 "certifi>=2017.4.17", 98 "chardet<3.1.0,>=3.0.2", 99 "idna<2.8,>=2.5", 100 "urllib3<1.24,>=1.21.1", 101 ], 102 ) 103 actions.do_update(project, top=True, dry_run=True) 104 out, _ = capsys.readouterr() 105 assert "requests 2.19.1 -> 2.20.0" in out 106 assert "- chardet 3.0.4 -> 3.0.5" not in out 107 108 109@pytest.mark.usefixtures("working_set") 110def test_update_specified_packages(project, repository): 111 actions.do_add(project, sync=False, packages=["requests", "pytz"]) 112 repository.add_candidate("pytz", "2019.6") 113 repository.add_candidate("chardet", "3.0.5") 114 repository.add_candidate("requests", "2.20.0") 115 repository.add_dependencies( 116 "requests", 117 "2.20.0", 118 [ 119 "certifi>=2017.4.17", 120 "chardet<3.1.0,>=3.0.2", 121 "idna<2.8,>=2.5", 122 "urllib3<1.24,>=1.21.1", 123 ], 124 ) 125 actions.do_update(project, packages=["requests"]) 126 locked_candidates = project.locked_repository.all_candidates 127 assert locked_candidates["requests"].version == "2.20.0" 128 assert locked_candidates["chardet"].version == "3.0.4" 129 130 131@pytest.mark.usefixtures("working_set") 132def test_update_specified_packages_eager_mode(project, repository): 133 actions.do_add(project, sync=False, packages=["requests", "pytz"]) 134 repository.add_candidate("pytz", "2019.6") 135 repository.add_candidate("chardet", "3.0.5") 136 repository.add_candidate("requests", "2.20.0") 137 repository.add_dependencies( 138 "requests", 139 "2.20.0", 140 [ 141 "certifi>=2017.4.17", 142 "chardet<3.1.0,>=3.0.2", 143 "idna<2.8,>=2.5", 144 "urllib3<1.24,>=1.21.1", 145 ], 146 ) 147 actions.do_update(project, strategy="eager", packages=["requests"]) 148 locked_candidates = project.locked_repository.all_candidates 149 assert locked_candidates["requests"].version == "2.20.0" 150 assert locked_candidates["chardet"].version == "3.0.5" 151 assert locked_candidates["pytz"].version == "2019.3" 152 153 154@pytest.mark.usefixtures("repository") 155def test_remove_package(project, working_set, is_dev): 156 actions.do_add(project, dev=is_dev, packages=["requests", "pytz"]) 157 actions.do_remove(project, dev=is_dev, packages=["pytz"]) 158 locked_candidates = project.locked_repository.all_candidates 159 assert "pytz" not in locked_candidates 160 assert "pytz" not in working_set 161 162 163@pytest.mark.usefixtures("repository") 164def test_remove_package_with_dry_run(project, working_set, capsys): 165 actions.do_add(project, packages=["requests"]) 166 actions.do_remove(project, packages=["requests"], dry_run=True) 167 out, _ = capsys.readouterr() 168 project._lockfile = None 169 locked_candidates = project.locked_repository.all_candidates 170 assert "urllib3" in locked_candidates 171 assert "urllib3" in working_set 172 assert "- urllib3 1.22" in out 173 174 175@pytest.mark.usefixtures("repository") 176def test_remove_package_no_sync(project, working_set): 177 actions.do_add(project, packages=["requests", "pytz"]) 178 actions.do_remove(project, sync=False, packages=["pytz"]) 179 locked_candidates = project.locked_repository.all_candidates 180 assert "pytz" not in locked_candidates 181 assert "pytz" in working_set 182 183 184@pytest.mark.usefixtures("repository", "working_set") 185def test_remove_package_not_exist(project): 186 actions.do_add(project, packages=["requests", "pytz"]) 187 with pytest.raises(PdmException): 188 actions.do_remove(project, sync=False, packages=["django"]) 189 190 191@pytest.mark.usefixtures("repository") 192def test_remove_package_exist_in_multi_groups(project, working_set): 193 actions.do_add(project, packages=["requests"]) 194 actions.do_add(project, dev=True, packages=["urllib3"]) 195 actions.do_remove(project, dev=True, packages=["urllib3"]) 196 assert all( 197 "urllib3" not in line 198 for line in project.tool_settings["dev-dependencies"]["dev"] 199 ) 200 assert "urllib3" in working_set 201 assert "requests" in working_set 202 203 204@pytest.mark.usefixtures("repository") 205def test_add_remove_no_package(project): 206 with pytest.raises(PdmUsageError): 207 actions.do_add(project, packages=()) 208 209 with pytest.raises(PdmUsageError): 210 actions.do_remove(project, packages=()) 211 212 213@pytest.mark.usefixtures("repository", "working_set") 214def test_update_with_package_and_groups_argument(project): 215 actions.do_add(project, packages=["requests", "pytz"]) 216 with pytest.raises(PdmUsageError): 217 actions.do_update(project, groups=("default", "dev"), packages=("requests",)) 218 219 with pytest.raises(PdmUsageError): 220 actions.do_update(project, default=False, packages=("requests",)) 221 222 223@pytest.mark.usefixtures("repository") 224def test_lock_dependencies(project): 225 project.add_dependencies({"requests": parse_requirement("requests")}) 226 actions.do_lock(project) 227 assert project.lockfile_file.exists() 228 locked = project.locked_repository.all_candidates 229 for package in ("requests", "idna", "chardet", "certifi"): 230 assert package in locked 231 232 233def test_build_distributions(tmp_path, core): 234 project = core.create_project() 235 actions.do_build(project, dest=tmp_path.as_posix()) 236 wheel = next(tmp_path.glob("*.whl")) 237 assert wheel.name.startswith("pdm-") 238 tarball = next(tmp_path.glob("*.tar.gz")) 239 assert tarball.exists() 240 241 242def test_project_no_init_error(project_no_init): 243 244 for handler in ( 245 actions.do_add, 246 actions.do_list, 247 actions.do_lock, 248 actions.do_update, 249 ): 250 with pytest.raises( 251 PdmException, match="The pyproject.toml has not been initialized yet" 252 ): 253 handler(project_no_init) 254 255 256@pytest.mark.usefixtures("repository", "working_set") 257def test_list_dependency_graph(project, capsys): 258 actions.do_add(project, packages=["requests"]) 259 actions.do_list(project, graph=True) 260 content, _ = capsys.readouterr() 261 assert "└── urllib3 1.22 [ required: <1.24,>=1.21.1 ]" in content 262 263 264@pytest.mark.usefixtures("working_set") 265def test_list_dependency_graph_with_circular_forward(project, capsys, repository): 266 repository.add_candidate("foo", "0.1.0") 267 repository.add_candidate("foo-bar", "0.1.0") 268 repository.add_dependencies("foo", "0.1.0", ["foo-bar"]) 269 repository.add_dependencies("foo-bar", "0.1.0", ["foo"]) 270 actions.do_add(project, packages=["foo"]) 271 capsys.readouterr() 272 actions.do_list(project, graph=True) 273 content, _ = capsys.readouterr() 274 assert "foo [circular]" in content 275 276 277@pytest.mark.usefixtures("working_set") 278def test_list_dependency_graph_with_circular_reverse(project, capsys, repository): 279 repository.add_candidate("foo", "0.1.0") 280 repository.add_candidate("foo-bar", "0.1.0") 281 repository.add_candidate("baz", "0.1.0") 282 repository.add_dependencies("foo", "0.1.0", ["foo-bar"]) 283 repository.add_dependencies("foo-bar", "0.1.0", ["foo", "baz"]) 284 repository.add_dependencies("baz", "0.1.0", []) 285 actions.do_add(project, packages=["foo"]) 286 capsys.readouterr() 287 actions.do_list(project, graph=True, reverse=True) 288 content, _ = capsys.readouterr() 289 expected = """ 290 └── foo 0.1.0 [ requires: Any ] 291 ├── foo-bar [circular] [ requires: Any ] 292 └── test-project 0.0.0 [ requires: ~=0.1 ]""" 293 assert expected in content 294 295 296@pytest.mark.usefixtures("repository", "working_set") 297def test_freeze_dependencies_list(project, capsys, mocker): 298 actions.do_add(project, packages=["requests"]) 299 capsys.readouterr() 300 mocker.patch( 301 "pdm.models.requirements.Requirement.from_dist", 302 side_effect=lambda d: d.as_req(), 303 ) 304 actions.do_list(project, freeze=True) 305 content, _ = capsys.readouterr() 306 assert "requests==2.19.1" in content 307 assert "urllib3==1.22" in content 308 309 310def test_list_reverse_without_graph_flag(project): 311 with pytest.raises(PdmException): 312 actions.do_list(project, reverse=True) 313 314 315@pytest.mark.usefixtures("repository", "working_set") 316def test_list_reverse_dependency_graph(project, capsys): 317 actions.do_add(project, packages=["requests"]) 318 capsys.readouterr() 319 actions.do_list(project, True, True) 320 content, _ = capsys.readouterr() 321 assert "└── requests 2.19.1 [ requires: <1.24,>=1.21.1 ]" in content 322 323 324def test_list_json_without_graph_flag(project): 325 with pytest.raises(PdmException): 326 actions.do_list(project, json=True) 327 328 329@pytest.mark.usefixtures("repository", "working_set") 330def test_list_json(project, capsys): 331 actions.do_add(project, packages=["requests"], no_self=True) 332 content, _ = capsys.readouterr() 333 actions.do_list(project, graph=True, json=True) 334 content, _ = capsys.readouterr() 335 expected = [ 336 { 337 "package": "requests", 338 "version": "2.19.1", 339 "required": "~=2.19", 340 "dependencies": [ 341 { 342 "package": "certifi", 343 "version": "2018.11.17", 344 "required": ">=2017.4.17", 345 "dependencies": [], 346 }, 347 { 348 "package": "chardet", 349 "version": "3.0.4", 350 "required": "<3.1.0,>=3.0.2", 351 "dependencies": [], 352 }, 353 { 354 "package": "idna", 355 "version": "2.7", 356 "required": "<2.8,>=2.5", 357 "dependencies": [], 358 }, 359 { 360 "package": "urllib3", 361 "version": "1.22", 362 "required": "<1.24,>=1.21.1", 363 "dependencies": [], 364 }, 365 ], 366 } 367 ] 368 assert expected == loads(content) 369 370 371@pytest.mark.usefixtures("repository", "working_set") 372def test_list_json_reverse(project, capsys): 373 actions.do_add(project, packages=["requests"], no_self=True) 374 capsys.readouterr() 375 actions.do_list(project, graph=True, reverse=True, json=True) 376 content, _ = capsys.readouterr() 377 expected = [ 378 { 379 "package": "certifi", 380 "version": "2018.11.17", 381 "requires": None, 382 "dependents": [ 383 { 384 "package": "requests", 385 "version": "2.19.1", 386 "requires": ">=2017.4.17", 387 "dependents": [], 388 } 389 ], 390 }, 391 { 392 "package": "chardet", 393 "version": "3.0.4", 394 "requires": None, 395 "dependents": [ 396 { 397 "package": "requests", 398 "version": "2.19.1", 399 "requires": "<3.1.0,>=3.0.2", 400 "dependents": [], 401 } 402 ], 403 }, 404 { 405 "package": "idna", 406 "version": "2.7", 407 "requires": None, 408 "dependents": [ 409 { 410 "package": "requests", 411 "version": "2.19.1", 412 "requires": "<2.8,>=2.5", 413 "dependents": [], 414 } 415 ], 416 }, 417 { 418 "package": "urllib3", 419 "version": "1.22", 420 "requires": None, 421 "dependents": [ 422 { 423 "package": "requests", 424 "version": "2.19.1", 425 "requires": "<1.24,>=1.21.1", 426 "dependents": [], 427 } 428 ], 429 }, 430 ] 431 432 assert expected == loads(content) 433 434 435@pytest.mark.usefixtures("working_set") 436def test_list_json_with_circular_forward(project, capsys, repository): 437 repository.add_candidate("foo", "0.1.0") 438 repository.add_candidate("foo-bar", "0.1.0") 439 repository.add_candidate("baz", "0.1.0") 440 repository.add_dependencies("baz", "0.1.0", ["foo"]) 441 repository.add_dependencies("foo", "0.1.0", ["foo-bar"]) 442 repository.add_dependencies("foo-bar", "0.1.0", ["foo"]) 443 actions.do_add(project, packages=["baz"], no_self=True) 444 capsys.readouterr() 445 actions.do_list(project, graph=True, json=True) 446 content, _ = capsys.readouterr() 447 expected = [ 448 { 449 "package": "baz", 450 "version": "0.1.0", 451 "required": "~=0.1", 452 "dependencies": [ 453 { 454 "package": "foo", 455 "version": "0.1.0", 456 "required": "Any", 457 "dependencies": [ 458 { 459 "package": "foo-bar", 460 "version": "0.1.0", 461 "required": "Any", 462 "dependencies": [ 463 { 464 "package": "foo", 465 "version": "0.1.0", 466 "required": "Any", 467 "dependencies": [], 468 } 469 ], 470 } 471 ], 472 } 473 ], 474 }, 475 ] 476 assert expected == loads(content) 477 478 479@pytest.mark.usefixtures("working_set") 480def test_list_json_with_circular_reverse(project, capsys, repository): 481 repository.add_candidate("foo", "0.1.0") 482 repository.add_candidate("foo-bar", "0.1.0") 483 repository.add_candidate("baz", "0.1.0") 484 repository.add_dependencies("foo", "0.1.0", ["foo-bar"]) 485 repository.add_dependencies("foo-bar", "0.1.0", ["foo", "baz"]) 486 repository.add_dependencies("baz", "0.1.0", []) 487 actions.do_add(project, packages=["foo"], no_self=True) 488 capsys.readouterr() 489 actions.do_list(project, graph=True, json=True, reverse=True) 490 content, _ = capsys.readouterr() 491 expected = [ 492 { 493 "package": "baz", 494 "version": "0.1.0", 495 "requires": None, 496 "dependents": [ 497 { 498 "package": "foo-bar", 499 "version": "0.1.0", 500 "requires": "Any", 501 "dependents": [ 502 { 503 "package": "foo", 504 "version": "0.1.0", 505 "requires": "Any", 506 "dependents": [ 507 { 508 "package": "foo-bar", 509 "version": "0.1.0", 510 "requires": "Any", 511 "dependents": [], 512 } 513 ], 514 } 515 ], 516 } 517 ], 518 }, 519 ] 520 assert expected == loads(content) 521 522 523@pytest.mark.usefixtures("repository", "working_set") 524def test_update_packages_with_top(project): 525 actions.do_add(project, packages=("requests",)) 526 with pytest.raises(PdmUsageError): 527 actions.do_update(project, packages=("requests",), top=True) 528 529 530@pytest.mark.usefixtures("working_set") 531def test_update_ignore_constraints(project, repository): 532 actions.do_add(project, packages=("pytz",)) 533 assert project.meta.dependencies == ["pytz~=2019.3"] 534 repository.add_candidate("pytz", "2020.2") 535 536 actions.do_update(project, unconstrained=False, packages=("pytz",)) 537 assert project.meta.dependencies == ["pytz~=2019.3"] 538 assert project.locked_repository.all_candidates["pytz"].version == "2019.3" 539 540 actions.do_update(project, unconstrained=True, packages=("pytz",)) 541 assert project.meta.dependencies == ["pytz~=2020.2"] 542 assert project.locked_repository.all_candidates["pytz"].version == "2020.2" 543 544 545def test_init_validate_python_requires(project_no_init): 546 with pytest.raises(ValueError): 547 actions.do_init(project_no_init, python_requires="3.7") 548 549 550@pytest.mark.skipif(os.name != "posix", reason="Run on POSIX platforms only") 551def test_use_wrapper_python(project): 552 wrapper_script = """#!/bin/bash 553exec "{}" "$@" 554""".format( 555 sys.executable 556 ) 557 shim_path = project.root.joinpath("python_shim.sh") 558 shim_path.write_text(wrapper_script) 559 shim_path.chmod(0o755) 560 561 actions.do_use(project, shim_path.as_posix()) 562 assert project.python.executable == sys.executable 563 564 565@pytest.mark.skipif(os.name != "posix", reason="Run on POSIX platforms only") 566def test_use_invalid_wrapper_python(project): 567 wrapper_script = """#!/bin/bash 568echo hello 569""" 570 shim_path = project.root.joinpath("python_shim.sh") 571 shim_path.write_text(wrapper_script) 572 shim_path.chmod(0o755) 573 with pytest.raises(InvalidPyVersion): 574 actions.do_use(project, shim_path.as_posix()) 575