diff --git a/src/codemodder/dependency_management/base_dependency_writer.py b/src/codemodder/dependency_management/base_dependency_writer.py index aafa5bdff..11f89d280 100644 --- a/src/codemodder/dependency_management/base_dependency_writer.py +++ b/src/codemodder/dependency_management/base_dependency_writer.py @@ -17,7 +17,7 @@ def __init__(self, dependency_store: PackageStore, parent_directory: Path): @abstractmethod def write( - self, dependencies: list[Requirement], dry_run: bool = False + self, dependencies: list[Dependency], dry_run: bool = False ) -> Optional[ChangeSet]: pass diff --git a/src/codemodder/dependency_management/requirements_txt_writer.py b/src/codemodder/dependency_management/requirements_txt_writer.py index 2a0d935b6..1c59c2788 100644 --- a/src/codemodder/dependency_management/requirements_txt_writer.py +++ b/src/codemodder/dependency_management/requirements_txt_writer.py @@ -1,13 +1,14 @@ from typing import Optional from codemodder.dependency_management.base_dependency_writer import DependencyWriter from codemodder.change import Action, Change, ChangeSet, PackageAction, Result -from packaging.requirements import Requirement from codemodder.diff import create_diff +from codemodder.dependency import Dependency +from packaging.requirements import Requirement class RequirementsTxtWriter(DependencyWriter): def write( - self, dependencies: list[Requirement], dry_run: bool = False + self, dependencies: list[Dependency], dry_run: bool = False ) -> Optional[ChangeSet]: new_dependencies = self.add(dependencies) if new_dependencies: @@ -15,13 +16,13 @@ def write( return None def add_to_file(self, dependencies: list[Requirement], dry_run: bool): - original_lines = self._parse_file() - updated_lines = original_lines.copy() + lines = self._parse_file() + original_lines = lines.copy() if not original_lines[-1].endswith("\n"): - updated_lines[-1] += "\n" + original_lines[-1] += "\n" requirement_lines = [f"{dep.requirement}\n" for dep in dependencies] - updated_lines += requirement_lines + updated_lines = original_lines + requirement_lines diff = create_diff(original_lines, updated_lines) @@ -43,8 +44,7 @@ def add_to_file(self, dependencies: list[Requirement], dry_run: bool): if not dry_run: with open(self.path, "w", encoding="utf-8") as f: - f.writelines(original_lines) - f.writelines(requirement_lines) + f.writelines(updated_lines) return ChangeSet( str(self.path.relative_to(self.parent_directory)), diff --git a/tests/dependency_management/test_dependency_manager.py b/tests/dependency_management/test_dependency_manager.py index 354740994..ee24ac3d8 100644 --- a/tests/dependency_management/test_dependency_manager.py +++ b/tests/dependency_management/test_dependency_manager.py @@ -1,9 +1,10 @@ -from pathlib import Path - import pytest +from codemodder.change import ChangeSet from codemodder.dependency import DefusedXML, Security -from codemodder.dependency_management import DependencyManager, Requirement +from codemodder.dependency_management import DependencyManager +from codemodder.project_analysis.file_parsers import RequirementsTxtParser +from codemodder.project_analysis.file_parsers.package_store import PackageStore @pytest.fixture(autouse=True, scope="module") @@ -12,106 +13,23 @@ def disable_write_dependencies(): class TestDependencyManager: - TEST_DIR = "tests/" - - def test_read_dependency_file(self, tmpdir): - dependency_file = Path(tmpdir) / "requirements.txt" - dependency_file.write_text("requests\n", encoding="utf-8") - - dm = DependencyManager(Path(tmpdir)) - assert dm.dependencies == {"requests": Requirement("requests")} - - @pytest.mark.parametrize("dry_run", [True, False]) - def test_add_dependency_preserve_comments(self, tmpdir, dry_run): - contents = "# comment\n\nrequests\n" - dependency_file = Path(tmpdir) / "requirements.txt" - dependency_file.write_text(contents, encoding="utf-8") - - dm = DependencyManager(Path(tmpdir)) - dm.add([DefusedXML]) - changeset = dm.write(dry_run=dry_run) - - assert dependency_file.read_text(encoding="utf-8") == ( - contents if dry_run else "# comment\n\nrequests\ndefusedxml~=0.7.1\n" - ) - - assert changeset is not None - assert changeset.path == dependency_file.name - assert changeset.diff == ( - "--- \n" - "+++ \n" - "@@ -1,3 +1,4 @@\n" - " # comment\n" - " \n" - " requests\n" - "+defusedxml~=0.7.1\n" - ) - assert len(changeset.changes) == 1 - assert changeset.changes[0].lineNumber == 4 - assert changeset.changes[0].description == DefusedXML.build_description() - assert changeset.changes[0].properties == { - "contextual_description": True, - "contextual_description_position": "right", - } - - def test_add_multiple_dependencies(self, tmpdir): - dependency_file = Path(tmpdir) / "requirements.txt" - dependency_file.write_text("requests\n", encoding="utf-8") - - for dep in [DefusedXML, Security]: - dm = DependencyManager(Path(tmpdir)) - dm.add([dep]) - dm.write() - - assert dependency_file.read_text(encoding="utf-8") == ( - "requests\ndefusedxml~=0.7.1\nsecurity~=1.2.0\n" - ) - - def test_add_same_dependency_only_once(self, tmpdir): - dependency_file = Path(tmpdir) / "requirements.txt" - dependency_file.write_text("requests\n", encoding="utf-8") - - for dep in [Security, Security]: - dm = DependencyManager(Path(tmpdir)) - dm.add([dep]) - dm.write() - - assert dependency_file.read_text(encoding="utf-8") == ( - "requests\nsecurity~=1.2.0\n" + def test_cant_write_unknown_store(self, tmpdir): + store = PackageStore( + type="unknown", file="idk.txt", dependencies=[], py_versions=[] ) - @pytest.mark.parametrize("version", ["1.2.0", "1.0.1"]) - def test_dont_add_existing_dependency(self, version, tmpdir): - dependency_file = Path(tmpdir) / "requirements.txt" - dependency_file.write_text(f"requests\nsecurity~={version}\n", encoding="utf-8") + dm = DependencyManager(store, tmpdir) + dependencies = [DefusedXML, Security] - dm = DependencyManager(Path(tmpdir)) - dm.add([Security]) - dm.write() + changeset = dm.write(dependencies) + assert changeset is None - assert dependency_file.read_text(encoding="utf-8") == ( - f"requests\nsecurity~={version}\n" - ) + def test_write_for_requirements_txt(self, pkg_with_reqs_txt): + parser = RequirementsTxtParser(pkg_with_reqs_txt) + stores = parser.parse() + assert len(stores) == 1 + dm = DependencyManager(stores[0], pkg_with_reqs_txt) + dependencies = [DefusedXML, Security] - def test_dependency_file_no_terminating_newline(self, tmpdir): - dependency_file = Path(tmpdir) / "requirements.txt" - dependency_file.write_text("requests\nwhatever", encoding="utf-8") - - dm = DependencyManager(Path(tmpdir)) - dm.add([Security]) - changeset = dm.write() - - assert changeset is not None - assert changeset.diff == ( - "--- \n" - "+++ \n" - "@@ -1,2 +1,3 @@\n" - " requests\n" - "-whatever\n" - "+whatever\n" - "+security~=1.2.0\n" - ) - - assert dependency_file.read_text(encoding="utf-8") == ( - "requests\nwhatever\nsecurity~=1.2.0\n" - ) + changeset = dm.write(dependencies) + assert isinstance(changeset, ChangeSet) diff --git a/tests/dependency_management/test_requirements_txt_writer.py b/tests/dependency_management/test_requirements_txt_writer.py new file mode 100644 index 000000000..99a8de820 --- /dev/null +++ b/tests/dependency_management/test_requirements_txt_writer.py @@ -0,0 +1,132 @@ +import pytest +from pathlib import Path +from codemodder.dependency_management.requirements_txt_writer import ( + RequirementsTxtWriter, +) +from codemodder.project_analysis.file_parsers.package_store import PackageStore +from codemodder.dependency import DefusedXML, Security + + +@pytest.fixture(autouse=True, scope="module") +def disable_write_dependencies(): + """Override fixture from conftest.py in order to allow testing""" + + +class TestRequirementsTxtWriter: + @pytest.mark.parametrize("dry_run", [True, False]) + def test_add_dependencies_preserve_comments(self, tmpdir, dry_run): + contents = "# comment\n\nrequests\n" + dependency_file = Path(tmpdir) / "requirements.txt" + dependency_file.write_text(contents, encoding="utf-8") + store = PackageStore( + type="requirements.txt", + file=str(dependency_file), + dependencies=[], + py_versions=[], + ) + writer = RequirementsTxtWriter(store, Path(tmpdir)) + dependencies = [DefusedXML, Security] + changeset = writer.write(dependencies, dry_run=dry_run) + + assert dependency_file.read_text(encoding="utf-8") == ( + contents + if dry_run + else "# comment\n\nrequests\ndefusedxml~=0.7.1\nsecurity~=1.2.0\n" + ) + + assert changeset is not None + assert changeset.path == dependency_file.name + assert changeset.diff == ( + "--- \n" + "+++ \n" + "@@ -1,3 +1,5 @@\n" + " # comment\n" + " \n" + " requests\n" + "+defusedxml~=0.7.1\n" + "+security~=1.2.0\n" + ) + assert len(changeset.changes) == 2 + change_one = changeset.changes[0] + assert change_one.lineNumber == 4 + assert change_one.description == DefusedXML.build_description() + assert change_one.properties == { + "contextual_description": True, + "contextual_description_position": "right", + } + change_two = changeset.changes[1] + assert change_two.lineNumber == 5 + assert change_two.description == Security.build_description() + assert change_two.properties == { + "contextual_description": True, + "contextual_description_position": "right", + } + + def test_add_same_dependency_only_once(self, tmpdir): + dependency_file = Path(tmpdir) / "requirements.txt" + dependency_file.write_text("requests\n", encoding="utf-8") + + store = PackageStore( + type="requirements.txt", + file=str(dependency_file), + dependencies=[], + py_versions=[], + ) + writer = RequirementsTxtWriter(store, Path(tmpdir)) + dependencies = [Security, Security] + changeset = writer.write(dependencies) + assert len(changeset.changes) == 1 + + assert dependency_file.read_text(encoding="utf-8") == ( + "requests\nsecurity~=1.2.0\n" + ) + + def test_dont_add_existing_dependency(self, tmpdir): + dependency_file = Path(tmpdir) / "requirements.txt" + contents = f"requests\nsecurity~=1.2.0\n" + dependency_file.write_text(contents, encoding="utf-8") + + store = PackageStore( + type="requirements.txt", + file=str(dependency_file), + dependencies=[Security.requirement], + py_versions=[], + ) + writer = RequirementsTxtWriter(store, Path(tmpdir)) + dependencies = [Security] + changeset = writer.write(dependencies) + assert changeset is None + assert dependency_file.read_text(encoding="utf-8") == contents + + def test_dependency_file_no_terminating_newline(self, tmpdir): + contents = "# comment\n\nrequests" + dependency_file = Path(tmpdir) / "requirements.txt" + dependency_file.write_text(contents, encoding="utf-8") + + store = PackageStore( + type="requirements.txt", + file=str(dependency_file), + dependencies=[], + py_versions=[], + ) + writer = RequirementsTxtWriter(store, Path(tmpdir)) + dependencies = [DefusedXML, Security] + changeset = writer.write(dependencies) + + assert ( + dependency_file.read_text(encoding="utf-8") + == "# comment\n\nrequests\ndefusedxml~=0.7.1\nsecurity~=1.2.0\n" + ) + + assert changeset is not None + assert changeset.path == dependency_file.name + assert changeset.diff == ( + "--- \n" + "+++ \n" + "@@ -1,3 +1,5 @@\n" + " # comment\n" + " \n" + " requests\n" + "+defusedxml~=0.7.1\n" + "+security~=1.2.0\n" + )