Skip to content

Commit

Permalink
Implement --license option (#318)
Browse files Browse the repository at this point in the history
  • Loading branch information
kemzeb committed Feb 15, 2024
1 parent 312818a commit 134ca6e
Show file tree
Hide file tree
Showing 9 changed files with 157 additions and 6 deletions.
1 change: 1 addition & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -234,6 +234,7 @@ options:
warning control: suppress will show warnings but return 0 whether or not they are present; silence will not show warnings at all and always return 0; fail will show warnings and return 1 if any are present (default:
suppress)
-r, --reverse render the dependency tree in the reverse fashion ie. the sub-dependencies are listed with the list of packages that need them under them (default: False)
--license list the license(s) of a package (text render only) (default: False)
select:
choose what to render
Expand Down
9 changes: 9 additions & 0 deletions src/pipdeptree/_cli.py
Original file line number Diff line number Diff line change
Expand Up @@ -26,6 +26,7 @@ class Options(Namespace):
output_format: str | None
depth: float
encoding: str
license: bool


class _Formatter(ArgumentDefaultsHelpFormatter):
Expand Down Expand Up @@ -60,6 +61,12 @@ def build_parser() -> ArgumentParser:
),
)

parser.add_argument(
"--license",
action="store_true",
help="list the license(s) of a package (text render only)",
)

select = parser.add_argument_group(title="select", description="choose what to render")
select.add_argument("--python", default=sys.executable, help="Python interpreter to inspect")
select.add_argument(
Expand Down Expand Up @@ -142,6 +149,8 @@ def get_options(args: Sequence[str] | None) -> Options:

if parsed_args.exclude and (parsed_args.all or parsed_args.packages):
return parser.error("cannot use --exclude with --packages or --all")
if parsed_args.license and parsed_args.freeze:
return parser.error("cannot use --license with --freeze")

return cast(Options, parsed_args)

Expand Down
24 changes: 23 additions & 1 deletion src/pipdeptree/_models/package.py
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@

from abc import ABC, abstractmethod
from importlib import import_module
from importlib.metadata import PackageNotFoundError, version
from importlib.metadata import PackageNotFoundError, metadata, version
from inspect import ismodule
from typing import TYPE_CHECKING

Expand Down Expand Up @@ -94,13 +94,35 @@ class DistPackage(Package):
"""

UNKNOWN_LICENSE_STR = "(Unknown license)"

def __init__(self, obj: DistInfoDistribution, req: ReqPackage | None = None) -> None:
super().__init__(obj)
self.req = req

def requires(self) -> list[Requirement]:
return self._obj.requires() # type: ignore[no-untyped-call,no-any-return]

def licenses(self) -> str:
try:
dist_metadata = metadata(self.key)
except PackageNotFoundError:
return self.UNKNOWN_LICENSE_STR

license_strs: list[str] = []
classifiers = dist_metadata.get_all("Classifier", [])

for classifier in classifiers:
line = str(classifier)
if line.startswith("License"):
license_str = line.split(":: ")[-1]
license_strs.append(license_str)

if len(license_strs) == 0:
return self.UNKNOWN_LICENSE_STR

return f'({", ".join(license_strs)})'

@property
def version(self) -> str:
return self._obj.version # type: ignore[no-any-return]
Expand Down
1 change: 1 addition & 0 deletions src/pipdeptree/_render/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -30,6 +30,7 @@ def render(options: Options, tree: PackageDAG) -> None:
encoding=options.encoding_type,
list_all=options.all,
frozen=options.freeze,
include_license=options.license,
)


Expand Down
23 changes: 19 additions & 4 deletions src/pipdeptree/_render/text.py
Original file line number Diff line number Diff line change
Expand Up @@ -3,17 +3,20 @@
from itertools import chain
from typing import TYPE_CHECKING, Any

from pipdeptree._models import DistPackage

if TYPE_CHECKING:
from pipdeptree._models import DistPackage, PackageDAG, ReqPackage
from pipdeptree._models import PackageDAG, ReqPackage


def render_text(
def render_text( # noqa: PLR0913
tree: PackageDAG,
*,
max_depth: float,
encoding: str,
list_all: bool = True,
frozen: bool = False,
include_license: bool = False,
) -> None:
"""
Print tree as text on console.
Expand All @@ -32,17 +35,20 @@ def render_text(
nodes = [p for p in nodes if p.key not in branch_keys]

if encoding in {"utf-8", "utf-16", "utf-32"}:
_render_text_with_unicode(tree, nodes, max_depth, frozen)
_render_text_with_unicode(tree, nodes, max_depth, frozen, include_license)
else:
_render_text_without_unicode(tree, nodes, max_depth, frozen)
_render_text_without_unicode(tree, nodes, max_depth, frozen, include_license)


def _render_text_with_unicode(
tree: PackageDAG,
nodes: list[DistPackage],
max_depth: float,
frozen: bool, # noqa: FBT001
include_license: bool, # noqa: FBT001
) -> None:
assert not (frozen and include_license)

use_bullets = not frozen

def aux( # noqa: PLR0913, PLR0917
Expand Down Expand Up @@ -83,6 +89,10 @@ def aux( # noqa: PLR0913, PLR0917
prefix += " " if use_bullets else ""
next_prefix = prefix
node_str = prefix + bullet + node_str

if include_license and isinstance(node, DistPackage):
node_str += " " + node.licenses()

result = [node_str]

children = tree.get_children(node.key)
Expand Down Expand Up @@ -114,7 +124,10 @@ def _render_text_without_unicode(
nodes: list[DistPackage],
max_depth: float,
frozen: bool, # noqa: FBT001
include_license: bool, # noqa: FBT001
) -> None:
assert not (frozen and include_license)

use_bullets = not frozen

def aux(
Expand All @@ -129,6 +142,8 @@ def aux(
if parent:
prefix = " " * indent + ("- " if use_bullets else "")
node_str = prefix + node_str
if include_license and isinstance(node, DistPackage):
node_str += " " + node.licenses()
result = [node_str]
children = [
aux(c, node, indent=indent + 2, cur_chain=[*cur_chain, c.project_name], depth=depth + 1)
Expand Down
48 changes: 48 additions & 0 deletions tests/_models/test_package.py
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,8 @@
from typing import TYPE_CHECKING, Any
from unittest.mock import Mock

import pytest

from pipdeptree._models import DistPackage, ReqPackage

if TYPE_CHECKING:
Expand Down Expand Up @@ -61,6 +63,52 @@ def test_dist_package_as_dict() -> None:
assert expected == result


@pytest.mark.parametrize(
("mocked_metadata", "expected_output"),
[
pytest.param(
Mock(get_all=lambda *args, **kwargs: []), # noqa: ARG005
DistPackage.UNKNOWN_LICENSE_STR,
id="no-license",
),
pytest.param(
Mock(
get_all=lambda *args, **kwargs: [ # noqa: ARG005
"License :: OSI Approved :: GNU General Public License v2 (GPLv2)",
"Operating System :: OS Independent",
]
),
"(GNU General Public License v2 (GPLv2))",
id="one-license-with-one-non-license",
),
pytest.param(
Mock(
get_all=lambda *args, **kwargs: [ # noqa: ARG005
"License :: OSI Approved :: GNU General Public License v2 (GPLv2)",
"License :: OSI Approved :: Apache Software License",
]
),
"(GNU General Public License v2 (GPLv2), Apache Software License)",
id="more-than-one-license",
),
],
)
def test_dist_package_licenses(mocked_metadata: Mock, expected_output: str, monkeypatch: pytest.MonkeyPatch) -> None:
monkeypatch.setattr("pipdeptree._models.package.metadata", lambda _: mocked_metadata)
dist = DistPackage(Mock(project_name="a"))
licenses_str = dist.licenses()

assert licenses_str == expected_output


def test_dist_package_licenses_importlib_cant_find_package(monkeypatch: pytest.MonkeyPatch) -> None:
monkeypatch.setattr("pipdeptree._models.package.metadata", Mock(side_effect=PackageNotFoundError()))
dist = DistPackage(Mock(project_name="a"))
licenses_str = dist.licenses()

assert licenses_str == DistPackage.UNKNOWN_LICENSE_STR


def test_req_package_render_as_root() -> None:
bar = Mock(key="bar", project_name="bar", version="4.1.0")
bar_req = Mock(key="bar", project_name="bar", version="4.1.0", specs=[(">=", "4.0")])
Expand Down
4 changes: 3 additions & 1 deletion tests/render/test_render.py
Original file line number Diff line number Diff line change
Expand Up @@ -37,4 +37,6 @@ def test_grahpviz_routing(mocker: MockerFixture) -> None:
def test_text_routing(mocker: MockerFixture) -> None:
render = mocker.patch("pipdeptree._render.render_text")
main([])
render.assert_called_once_with(ANY, encoding="utf-8", frozen=False, list_all=False, max_depth=inf)
render.assert_called_once_with(
ANY, encoding="utf-8", frozen=False, list_all=False, max_depth=inf, include_license=False
)
44 changes: 44 additions & 0 deletions tests/render/test_text.py
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@
import pytest

from pipdeptree._models import PackageDAG
from pipdeptree._models.package import DistPackage
from pipdeptree._render.text import render_text

if TYPE_CHECKING:
Expand Down Expand Up @@ -467,3 +468,46 @@ def test_render_text_list_all_and_packages_options_used(
]

assert "\n".join(expected_output).strip() == captured.out.strip()


@pytest.mark.parametrize(
("encoding", "expected_output"),
[
(
"utf-8",
[
"a==3.4.0 (TEST)",
"└── c [required: ==1.0.0, installed: 1.0.0]",
"b==2.3.1 (TEST)",
"c==1.0.0 (TEST)",
],
),
(
"ascii",
[
"a==3.4.0 (TEST)",
" - c [required: ==1.0.0, installed: 1.0.0]",
"b==2.3.1 (TEST)",
"c==1.0.0 (TEST)",
],
),
],
)
def test_render_text_with_license_info(
encoding: str,
expected_output: str,
mock_pkgs: Callable[[MockGraph], Iterator[Mock]],
capsys: pytest.CaptureFixture[str],
monkeypatch: pytest.MonkeyPatch,
) -> None:
graph: dict[tuple[str, str], list[tuple[str, list[tuple[str, str]]]]] = {
("a", "3.4.0"): [("c", [("==", "1.0.0")])],
("b", "2.3.1"): [],
("c", "1.0.0"): [],
}
dag = PackageDAG.from_pkgs(list(mock_pkgs(graph)))
monkeypatch.setattr(DistPackage, "licenses", lambda _: "(TEST)")

render_text(dag, max_depth=float("inf"), encoding=encoding, include_license=True)
captured = capsys.readouterr()
assert "\n".join(expected_output).strip() == captured.out.strip()
9 changes: 9 additions & 0 deletions tests/test_cli.py
Original file line number Diff line number Diff line change
Expand Up @@ -97,3 +97,12 @@ def test_parser_get_options_exclude_combine_not_supported(args: list[str], capsy
def test_parser_get_options_exclude_only() -> None:
parsed_args = get_options(["--exclude", "py"])
assert parsed_args.exclude == "py"


def test_parser_get_options_license_and_freeze_together_not_supported(capsys: pytest.CaptureFixture[str]) -> None:
with pytest.raises(SystemExit, match="2"):
get_options(["--license", "--freeze"])

out, err = capsys.readouterr()
assert not out
assert "cannot use --license with --freeze" in err

0 comments on commit 134ca6e

Please sign in to comment.