Repository: jazzband/pip-tools
Pull request #2087 (2024-05-07 05:50:54)

Preserve relative paths for editable dependencies in output
Accomplishes preservation of relative dependency paths by reverting back to input requirements definition for editable dependencies.

There are a couple prior attempts of this. The two I read through treated this like a formatting problem, I'm not sure it is. The issue, as has been pointed out in #204 is that paths are converted into urls in the form of absolute paths within pip.

If we maintain knowledge of the original relative paths within pip-tools, the values will be maintained more faithfully, and the complexity should be lower.

This PR accomplishes it by forcing the return value from `parse_requirements()` to give the values we need elsewhere. A better approach may be to return something different than `InstallRequirement` or preserve the relationship with the parsed requirement and provide both where it makes sense.

##### Contributor checklist

- [x] Included tests for the changes.
- [x] PR title is short, clear, and ready to be included in the user-facing changelog.

##### Maintainer checklist

- [x] Verified one of these labels is present: `backwards incompatible`, `feature`, `enhancement`, `deprecation`, `bug`, `dependency`, `docs` or `skip-changelog` as they determine changelog listing.
- [ ] Assign the PR to an existing or new milestone for the target version (following [Semantic Versioning](https://blog.versioneye.com/2014/01/16/semantic-versioning/)).



Patch:
diff --git a/piptools/_compat/pip_compat.py b/piptools/_compat/pip_compat.py
index 6409fbc..a5f843a 100644
--- a/piptools/_compat/pip_compat.py
+++ b/piptools/_compat/pip_compat.py
@@ -10,6 +10,7 @@ from pip._internal.index.package_finder import PackageFinder
 from pip._internal.metadata import BaseDistribution
 from pip._internal.metadata.pkg_resources import Distribution as _PkgResourcesDist
 from pip._internal.models.direct_url import DirectUrl
+from pip._internal.models.link import Link
 from pip._internal.network.session import PipSession
 from pip._internal.req import InstallRequirement
 from pip._internal.req import parse_requirements as _parse_requirements
@@ -63,6 +64,14 @@ class Distribution:
         return cls(dist._dist.name, dist._dist.version, requires, dist.direct_url)
 
 
+class FileLink(Link):
+
+    @property
+    def file_path(self) -> str:
+        # overriding the actual property to bypass some validation
+        return self._url
+
+
 def parse_requirements(
     filename: str,
     session: PipSession,
@@ -74,7 +83,14 @@ def parse_requirements(
     for parsed_req in _parse_requirements(
         filename, session, finder=finder, options=options, constraint=constraint
     ):
-        yield install_req_from_parsed_requirement(parsed_req, isolated=isolated)
+        install_req = install_req_from_parsed_requirement(parsed_req, isolated=isolated)
+        if install_req.editable and not parsed_req.requirement.startswith("file://"):
+            # ``Link.url`` is what is saved to the output file
+            # we set the url directly to undo the transformation in pip's Link class
+            file_link = FileLink(install_req.link.url)
+            file_link._url = parsed_req.requirement
+            install_req.link = file_link
+        yield install_req
 
 
 def create_wheel_cache(cache_dir: str, format_control: str | None = None) -> WheelCache:


Test patch:
diff --git a/tests/constants.py b/tests/constants.py
index 0a33a27..3bbfa0b 100644
--- a/tests/constants.py
+++ b/tests/constants.py
@@ -5,3 +5,6 @@ import os
 TEST_DATA_PATH = os.path.join(os.path.dirname(__file__), "test_data")
 MINIMAL_WHEELS_PATH = os.path.join(TEST_DATA_PATH, "minimal_wheels")
 PACKAGES_PATH = os.path.join(TEST_DATA_PATH, "packages")
+PACKAGES_RELATIVE_PATH = os.path.relpath(
+    PACKAGES_PATH, os.path.commonpath([os.getcwd(), PACKAGES_PATH])
+)
diff --git a/tests/test_pip_compat.py b/tests/test_pip_compat.py
new file mode 100644
index 0000000..583addf
--- /dev/null
+++ b/tests/test_pip_compat.py
@@ -0,0 +1,29 @@
+from __future__ import annotations
+
+import os
+import tempfile
+from pathlib import Path, PurePosixPath
+
+import pytest
+
+from piptools._compat.pip_compat import parse_requirements
+from piptools.repositories import PyPIRepository
+
+from .constants import PACKAGES_RELATIVE_PATH
+
+
+def test_parse_requirements_preserve_editable_relative_path(tmp_path, repository):
+    test_package_path = str(
+        PurePosixPath(Path(PACKAGES_RELATIVE_PATH)) / "small_fake_a"
+    )
+    requirements_in_path = str(tmp_path / "requirements.in")
+
+    with open(requirements_in_path, "w") as requirements_in_file:
+        requirements_in_file.write(f"-e {test_package_path}")
+
+    [install_requirement] = parse_requirements(
+        requirements_in_path, session=repository.session
+    )
+
+    assert install_requirement.link.url == test_package_path
+    assert install_requirement.link.file_path == test_package_path