diff --git a/news/370.bugfix.md b/news/370.bugfix.md new file mode 100644 index 0000000000000000000000000000000000000000..43bf865658248dcd22952fcadf8b2d8d74b5ce7a --- /dev/null +++ b/news/370.bugfix.md @@ -0,0 +1 @@ +Don't overwrite existing dependencies when importing from requirements.txt. diff --git a/pdm/cli/actions.py b/pdm/cli/actions.py index c1da2d3f5d4d74e68745e51823e267fd47c8e538..14f8edca9851caa359d802c53c31122991baf053 100644 --- a/pdm/cli/actions.py +++ b/pdm/cli/actions.py @@ -17,6 +17,7 @@ from pdm.cli.utils import ( find_importable_files, format_lockfile, format_resolution_impossible, + merge_dictionary, save_version_specifiers, set_env_in_reg, translate_sections, @@ -537,8 +538,8 @@ def do_import( tomlkit.comment("See https://www.python.org/dev/peps/pep-0621/") ) - pyproject["project"].update(project_data) - pyproject["tool"]["pdm"].update(settings) + merge_dictionary(pyproject["project"], project_data) + merge_dictionary(pyproject["tool"]["pdm"], settings) pyproject["build-system"] = { "requires": ["pdm-pep517"], "build-backend": "pdm.pep517.api", diff --git a/pdm/cli/options.py b/pdm/cli/options.py index bb3e1c869efd32ba23a6f1f88ac5cd94810e76b1..dddc9d91d00b9a942975b78f3e8bc5c5b26e1477 100644 --- a/pdm/cli/options.py +++ b/pdm/cli/options.py @@ -102,7 +102,7 @@ sections_group.add_argument( dest="default", action="store_false", default=True, - help="Don't include dependencies from default seciton", + help="Don't include dependencies from default section", ) diff --git a/pdm/cli/utils.py b/pdm/cli/utils.py index 387cba7c543e982bd275de0928511a032d3b7b0d..5a97b0f1b25b06f980d9aa906bdb10893d398792 100644 --- a/pdm/cli/utils.py +++ b/pdm/cli/utils.py @@ -462,3 +462,16 @@ def translate_sections( if default: sections.add("default") return sections + + +def merge_dictionary(target: dict, input: dict) -> None: + """Merge the input dict with the target while preserving the existing values + properly. This will update the target dictionary in place. + """ + for key, value in input.items(): + if key not in target: + target[key] = value + elif isinstance(value, dict): + target[key].update(value) + elif isinstance(value, list): + target[key].extend(value) diff --git a/pdm/formats/requirements.py b/pdm/formats/requirements.py index 0b73312734ca587071c7943a37b48f32bf9edfa0..189c6e2e1704ce6f209f174e78eea7f3d9aeed95 100644 --- a/pdm/formats/requirements.py +++ b/pdm/formats/requirements.py @@ -7,7 +7,6 @@ from typing import Any, Dict, List, Optional, Tuple, Union from distlib.wheel import Wheel from pip._vendor.packaging.requirements import Requirement as PRequirement -from pdm.exceptions import PdmUsageError from pdm.formats.base import make_array from pdm.models.candidates import Candidate from pdm.models.environment import Environment @@ -109,12 +108,10 @@ def convert( reqs = [ireq_as_line(ireq, project.environment) for ireq in ireqs] deps = make_array(reqs, True) - data = {"dependencies": []} + data = {} settings = {} - if options.dev and options.section: - raise PdmUsageError("Can't specify --dev and --section at the same time") - elif options.dev: - settings["dev-dependencies"] = {"dev": deps} + if options.dev: + settings["dev-dependencies"] = {options.section or "dev": deps} elif options.section: data["optional-dependencies"] = {options.section: deps} else: diff --git a/tests/cli/test_cli.py b/tests/cli/test_cli.py index ee132006dbedf10fcd86e7702355864fc4009187..1ac15b5e46087da73a6ba5d95031f42a539e4680 100644 --- a/tests/cli/test_cli.py +++ b/tests/cli/test_cli.py @@ -291,6 +291,17 @@ def test_import_other_format_file(project, invoke, filename): assert result.exit_code == 0 +def test_import_requirement_no_overwrite(project, invoke, tmp_path): + project.add_dependencies({"requests": parse_requirement("requests")}) + tmp_path.joinpath("reqs.txt").write_text("flask\nflask-login\n") + result = invoke( + ["import", "-dsweb", str(tmp_path.joinpath("reqs.txt"))], obj=project + ) + assert result.exit_code == 0, result.stderr + assert list(project.get_dependencies()) == ["requests"] + assert list(project.get_dependencies("web")) == ["flask", "flask-login"] + + @pytest.mark.pypi def test_search_package(project, invoke): result = invoke(["search", "requests"], obj=project) diff --git a/tests/test_formats.py b/tests/test_formats.py index e39fcf32b0308a37cc8e6cf36fe7b821ab8e16d8..02bea85fa614b06777fa775f4b105e90a913427e 100644 --- a/tests/test_formats.py +++ b/tests/test_formats.py @@ -151,5 +151,5 @@ def test_import_requirements_with_section(project): assert "webassets==2.0" in section assert 'whoosh==2.7.4; sys_platform == "win32"' in section assert "-e git+https://github.com/pypa/pip.git@master#egg=pip" in section - assert not result["dependencies"] + assert not result.get("dependencies") assert not result.get("dev-dependencies", {}).get("dev") diff --git a/tests/test_utils.py b/tests/test_utils.py index 72d7bad97472d9d6e6e00fa3ec0ab2a613eefaf0..357ca1dec5ba3d3436e7f4a95839dc07f22fcae5 100644 --- a/tests/test_utils.py +++ b/tests/test_utils.py @@ -5,6 +5,7 @@ import sys import pytest from pdm import utils +from pdm.cli import utils as cli_utils @pytest.mark.parametrize( @@ -66,3 +67,21 @@ def test_find_python_in_path(tmp_path): ) assert not utils.find_python_in_path(tmp_path) + + +def test_merge_dictionary(): + target = { + "existing_dict": {"foo": "bar", "hello": "world"}, + "existing_list": ["hello"], + } + input_dict = { + "existing_dict": {"foo": "baz"}, + "existing_list": ["world"], + "new_dict": {"name": "Sam"}, + } + cli_utils.merge_dictionary(target, input_dict) + assert target == { + "existing_dict": {"foo": "baz", "hello": "world"}, + "existing_list": ["hello", "world"], + "new_dict": {"name": "Sam"}, + }