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