Repository: tox-dev/pipdeptree
Pull request #402 (2024-09-01 18:27:55)

Move pip's frozen requirement API into a single module
This snapshot moves the remaining pip API usage into the `freeze` module since we only use it to render dependency trees in the requirements.txt file format.

It also makes changes to related tests to make them true unit tests and creates an integration test for dist_to_frozen_repr().


Patch:
diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml
index 9e82b25..55c7df6 100644
--- a/.github/workflows/release.yml
+++ b/.github/workflows/release.yml
@@ -24,4 +24,4 @@ jobs:
       - name: Build package
         run: pyproject-build -s -w . -o dist
       - name: Publish to PyPI
-        uses: pypa/gh-action-pypi-publish@v1.9.0
+        uses: pypa/gh-action-pypi-publish@v1.10.1
diff --git a/src/pipdeptree/_adapter.py b/src/pipdeptree/_freeze.py
similarity index 85%
rename from src/pipdeptree/_adapter.py
rename to src/pipdeptree/_freeze.py
index 2593662..e6aea91 100644
--- a/src/pipdeptree/_adapter.py
+++ b/src/pipdeptree/_freeze.py
@@ -16,6 +16,16 @@ if TYPE_CHECKING:
     from importlib.metadata import Distribution
 
 
+def dist_to_frozen_repr(dist: Distribution) -> str:
+    """Return the frozen requirement repr of a `importlib.metadata.Distribution` object."""
+    from pip._internal.operations.freeze import FrozenRequirement  # noqa: PLC0415, PLC2701
+
+    adapter = PipBaseDistributionAdapter(dist)
+    fr = FrozenRequirement.from_dist(adapter)  # type: ignore[arg-type]
+
+    return str(fr).strip()
+
+
 class PipBaseDistributionAdapter:
     """
     An adapter class for pip's `pip._internal.metadata.BaseDistribution` abstract class.
@@ -74,3 +84,6 @@ class PipBaseDistributionAdapter:
             with Path(egg_link_path).open("r", encoding=locale.getpreferredencoding(False)) as f:  # noqa: FBT003
                 result = f.readline().rstrip()
         return result
+
+
+__all__ = ["dist_to_frozen_repr"]
diff --git a/src/pipdeptree/_models/package.py b/src/pipdeptree/_models/package.py
index 9d40a44..c62dd27 100644
--- a/src/pipdeptree/_models/package.py
+++ b/src/pipdeptree/_models/package.py
@@ -9,11 +9,11 @@ from typing import TYPE_CHECKING, Iterator
 from packaging.requirements import InvalidRequirement, Requirement
 from packaging.utils import canonicalize_name
 
+from pipdeptree._freeze import dist_to_frozen_repr
+
 if TYPE_CHECKING:
     from importlib.metadata import Distribution
 
-from pipdeptree._adapter import PipBaseDistributionAdapter
-
 
 class InvalidRequirementError(ValueError):
     """
@@ -75,12 +75,7 @@ class Package(ABC):
 
     @staticmethod
     def as_frozen_repr(dist: Distribution) -> str:
-        from pip._internal.operations.freeze import FrozenRequirement  # noqa: PLC0415, PLC2701 # pragma: no cover
-
-        adapter = PipBaseDistributionAdapter(dist)
-        fr = FrozenRequirement.from_dist(adapter)  # type: ignore[arg-type]
-
-        return str(fr).strip()
+        return dist_to_frozen_repr(dist)
 
     def __repr__(self) -> str:
         return f'<{self.__class__.__name__}("{self.key}")>'


Test patch:
diff --git a/tests/_models/test_package.py b/tests/_models/test_package.py
index 6ad7721..43f1f4a 100644
--- a/tests/_models/test_package.py
+++ b/tests/_models/test_package.py
@@ -1,9 +1,6 @@
 from __future__ import annotations
 
-import locale
-import sys
 from importlib.metadata import PackageNotFoundError
-from pathlib import Path
 from typing import TYPE_CHECKING, Any
 from unittest.mock import MagicMock, Mock
 
@@ -29,18 +26,11 @@ def test_guess_version_setuptools(mocker: MockerFixture) -> None:
     assert result == "?"
 
 
-def test_package_as_frozen_repr(tmp_path: Path, mocker: MockerFixture) -> None:
-    file_path = tmp_path / "foo.egg-link"
-    with Path(file_path).open("w", encoding=locale.getpreferredencoding(False)) as f:
-        f.write("/A/B/foo")
-    mock_path = sys.path.copy()
-    mock_path.append(str(tmp_path))
-    mocker.patch("pipdeptree._discovery.sys.path", mock_path)
-    json_text = '{"dir_info": {"editable": true}}'
-    foo = Mock(metadata={"Name": "foo"}, version="20.4.1")
-    foo.read_text = Mock(return_value=json_text)
+def test_package_as_frozen_repr(mocker: MockerFixture) -> None:
+    foo = Mock(metadata={"Name": "foo"}, version="1.2.3")
     dp = DistPackage(foo)
-    expected = "# Editable install with no version control (foo==20.4.1)\n-e /A/B/foo"
+    expected = "test"
+    mocker.patch("pipdeptree._models.package.dist_to_frozen_repr", Mock(return_value=expected))
     assert Package.as_frozen_repr(dp.unwrap()) == expected
 
 
@@ -67,8 +57,7 @@ def test_dist_package_requires_with_environment_markers_that_eval_to_false() ->
 def test_dist_package_render_as_root() -> None:
     foo = Mock(metadata={"Name": "foo"}, version="20.4.1")
     dp = DistPackage(foo)
-    is_frozen = False
-    assert dp.render_as_root(frozen=is_frozen) == "foo==20.4.1"
+    assert dp.render_as_root(frozen=False) == "foo==20.4.1"
 
 
 def test_dist_package_render_as_branch() -> None:
@@ -78,18 +67,15 @@ def test_dist_package_render_as_branch() -> None:
     bar_req.name = "bar"
     rp = ReqPackage(bar_req, dist=bar)
     dp = DistPackage(foo).as_parent_of(rp)
-    is_frozen = False
-    assert dp.render_as_branch(frozen=is_frozen) == "foo==20.4.1 [requires: bar>=4.0]"
+    assert dp.render_as_branch(frozen=False) == "foo==20.4.1 [requires: bar>=4.0]"
 
 
-def test_dist_package_render_as_root_with_frozen() -> None:
-    json_text = '{"dir_info": {"editable": true}, "url": "file:///A/B/foo"}'
-    foo = Mock(metadata={"Name": "foo"}, version="20.4.1")
-    foo.read_text = Mock(return_value=json_text)
+def test_dist_package_render_as_root_with_frozen(mocker: MockerFixture) -> None:
+    foo = Mock(metadata={"Name": "foo"}, version="1.2.3")
     dp = DistPackage(foo)
-    is_frozen = True
-    expect = "# Editable install with no version control (foo==20.4.1)\n-e /A/B/foo"
-    assert dp.render_as_root(frozen=is_frozen) == expect
+    expected = "test"
+    mocker.patch("pipdeptree._models.package.dist_to_frozen_repr", Mock(return_value=expected))
+    assert dp.render_as_root(frozen=True) == expected
 
 
 def test_dist_package_as_parent_of() -> None:
@@ -181,21 +167,18 @@ def test_req_package_render_as_root() -> None:
     bar_req = MagicMock(specifier=[">=4.0"])
     bar_req.name = "bar"
     rp = ReqPackage(bar_req, dist=bar)
-    is_frozen = False
-    assert rp.render_as_root(frozen=is_frozen) == "bar==4.1.0"
+    assert rp.render_as_root(frozen=False) == "bar==4.1.0"
 
 
-def test_req_package_render_as_root_with_frozen() -> None:
-    json_text = '{"dir_info": {"editable": true}, "url": "file:///A/B/bar"}'
+def test_req_package_render_as_root_with_frozen(mocker: MockerFixture) -> None:
     bar = Mock(metadata={"Name": "bar"}, version="4.1.0")
-    bar.read_text = Mock(return_value=json_text)
-    d = DistPackage(bar)
+    dp = DistPackage(bar)
     bar_req = MagicMock(specifier=[">=4.0"])
     bar_req.name = "bar"
-    rp = ReqPackage(bar_req, dist=d)
-    is_frozen = True
-    expect = "# Editable install with no version control (bar==4.1.0)\n-e /A/B/bar"
-    assert rp.render_as_root(frozen=is_frozen) == expect
+    rp = ReqPackage(bar_req, dp)
+    expected = "test"
+    mocker.patch("pipdeptree._models.package.dist_to_frozen_repr", Mock(return_value=expected))
+    assert rp.render_as_root(frozen=True) == expected
 
 
 def test_req_package_render_as_branch() -> None:
@@ -203,8 +186,7 @@ def test_req_package_render_as_branch() -> None:
     bar_req = MagicMock(specifier=[">=4.0"])
     bar_req.name = "bar"
     rp = ReqPackage(bar_req, dist=bar)
-    is_frozen = False
-    assert rp.render_as_branch(frozen=is_frozen) == "bar [required: >=4.0, installed: 4.1.0]"
+    assert rp.render_as_branch(frozen=False) == "bar [required: >=4.0, installed: 4.1.0]"
 
 
 def test_req_package_is_conflicting_handle_dev_versions() -> None:
diff --git a/tests/test_freeze.py b/tests/test_freeze.py
new file mode 100644
index 0000000..6d6a76e
--- /dev/null
+++ b/tests/test_freeze.py
@@ -0,0 +1,12 @@
+from __future__ import annotations
+
+from unittest.mock import Mock
+
+from pipdeptree._freeze import dist_to_frozen_repr
+
+
+def test_dist_to_frozen_repr() -> None:
+    foo = Mock(metadata={"Name": "foo"}, version="20.4.1")
+    foo.read_text = Mock(return_value=None)
+    expected = "foo==20.4.1"
+    assert dist_to_frozen_repr(foo) == expected