From ef9dad27188094a94e7aa4ffcfab38989b540658 Mon Sep 17 00:00:00 2001 From: Andrew Nesbitt Date: Sun, 25 Jan 2026 22:57:38 +0000 Subject: [PATCH] Add pipdeptree.json and pipenv.graph.json filename support Both pipdeptree.json and pipenv.graph.json use the same format as pip-dependency-graph.json (pipdeptree --json output). This adds filename aliases so the existing parser handles these files. --- internal/pypi/pypi.go | 5 +- internal/pypi/pypi_test.go | 83 +++++++++++ testdata/pypi/pipdeptree.json | 244 ++++++++++++++++++++++++++++++++ testdata/pypi/pipenv.graph.json | 67 +++++++++ 4 files changed, 397 insertions(+), 2 deletions(-) create mode 100644 testdata/pypi/pipdeptree.json create mode 100644 testdata/pypi/pipenv.graph.json diff --git a/internal/pypi/pypi.go b/internal/pypi/pypi.go index c6eab06..f712836 100644 --- a/internal/pypi/pypi.go +++ b/internal/pypi/pypi.go @@ -36,8 +36,9 @@ func init() { // uv.lock - lockfile core.Register("pypi", core.Lockfile, &uvLockParser{}, core.ExactMatch("uv.lock")) - // pip-dependency-graph.json - lockfile (pipdeptree --json output) - core.Register("pypi", core.Lockfile, &pipDependencyGraphParser{}, core.ExactMatch("pip-dependency-graph.json")) + // pip-dependency-graph.json, pipdeptree.json, pipenv.graph.json - lockfile (pipdeptree --json output) + core.Register("pypi", core.Lockfile, &pipDependencyGraphParser{}, + core.ExactMatch("pip-dependency-graph.json", "pipdeptree.json", "pipenv.graph.json")) // pip-resolved-dependencies.txt - lockfile (pip freeze output) core.Register("pypi", core.Lockfile, &pipResolvedDepsParser{}, core.ExactMatch("pip-resolved-dependencies.txt")) diff --git a/internal/pypi/pypi_test.go b/internal/pypi/pypi_test.go index 22ba5f9..37a81d6 100644 --- a/internal/pypi/pypi_test.go +++ b/internal/pypi/pypi_test.go @@ -925,3 +925,86 @@ func TestPylockToml(t *testing.T) { } } } + +func TestPipdeptreeJSON(t *testing.T) { + content, err := os.ReadFile("../../testdata/pypi/pipdeptree.json") + if err != nil { + t.Fatalf("failed to read fixture: %v", err) + } + + parser := &pipDependencyGraphParser{} + deps, err := parser.Parse("pipdeptree.json", content) + if err != nil { + t.Fatalf("Parse failed: %v", err) + } + + // Same as pip-dependency-graph.json test since file is a copy + if len(deps) != 17 { + t.Fatalf("expected 17 dependencies, got %d", len(deps)) + } + + depMap := make(map[string]core.Dependency) + for _, d := range deps { + depMap[d.Name] = d + } + + // Verify some packages + samples := map[string]string{ + "aiohttp": "3.9.5", + "black": "23.12.0", + "click": "8.1.7", + } + + for name, wantVer := range samples { + dep, ok := depMap[name] + if !ok { + t.Errorf("expected %s dependency", name) + continue + } + if dep.Version != wantVer { + t.Errorf("%s version = %q, want %q", name, dep.Version, wantVer) + } + } +} + +func TestPipenvGraphJSON(t *testing.T) { + content, err := os.ReadFile("../../testdata/pypi/pipenv.graph.json") + if err != nil { + t.Fatalf("failed to read fixture: %v", err) + } + + parser := &pipDependencyGraphParser{} + deps, err := parser.Parse("pipenv.graph.json", content) + if err != nil { + t.Fatalf("Parse failed: %v", err) + } + + if len(deps) != 5 { + t.Fatalf("expected 5 dependencies, got %d", len(deps)) + } + + depMap := make(map[string]core.Dependency) + for _, d := range deps { + depMap[d.Name] = d + } + + // Verify all packages + expected := map[string]string{ + "requests": "2.31.0", + "charset-normalizer": "3.3.2", + "idna": "3.7", + "urllib3": "2.2.1", + "certifi": "2024.2.2", + } + + for name, wantVer := range expected { + dep, ok := depMap[name] + if !ok { + t.Errorf("expected %s dependency", name) + continue + } + if dep.Version != wantVer { + t.Errorf("%s version = %q, want %q", name, dep.Version, wantVer) + } + } +} diff --git a/testdata/pypi/pipdeptree.json b/testdata/pypi/pipdeptree.json new file mode 100644 index 0000000..1aac0b7 --- /dev/null +++ b/testdata/pypi/pipdeptree.json @@ -0,0 +1,244 @@ +[ + { + "package": { + "key": "aiohttp", + "package_name": "aiohttp", + "installed_version": "3.9.5" + }, + "dependencies": [ + { + "key": "aiosignal", + "package_name": "aiosignal", + "installed_version": "1.3.1", + "required_version": ">=1.1.2" + }, + { + "key": "async-timeout", + "package_name": "async-timeout", + "installed_version": "4.0.3", + "required_version": ">=4.0,<5.0" + }, + { + "key": "attrs", + "package_name": "attrs", + "installed_version": "23.2.0", + "required_version": ">=17.3.0" + }, + { + "key": "frozenlist", + "package_name": "frozenlist", + "installed_version": "1.4.1", + "required_version": ">=1.1.1" + }, + { + "key": "multidict", + "package_name": "multidict", + "installed_version": "6.0.5", + "required_version": ">=4.5,<7.0" + }, + { + "key": "yarl", + "package_name": "yarl", + "installed_version": "1.9.4", + "required_version": ">=1.0,<2.0" + } + ] + }, + { + "package": { + "key": "aiosignal", + "package_name": "aiosignal", + "installed_version": "1.3.1" + }, + "dependencies": [ + { + "key": "frozenlist", + "package_name": "frozenlist", + "installed_version": "1.4.1", + "required_version": ">=1.1.0" + } + ] + }, + { + "package": { + "key": "async-timeout", + "package_name": "async-timeout", + "installed_version": "4.0.3" + }, + "dependencies": [] + }, + { + "package": { + "key": "attrs", + "package_name": "attrs", + "installed_version": "23.2.0" + }, + "dependencies": [] + }, + { + "package": { + "key": "black", + "package_name": "black", + "installed_version": "23.12.0" + }, + "dependencies": [ + { + "key": "aiohttp", + "package_name": "aiohttp", + "installed_version": "3.9.5", + "required_version": ">=3.7.4" + }, + { + "key": "click", + "package_name": "click", + "installed_version": "8.1.7", + "required_version": ">=8.0.0" + }, + { + "key": "mypy-extensions", + "package_name": "mypy-extensions", + "installed_version": "1.0.0", + "required_version": ">=0.4.3" + }, + { + "key": "packaging", + "package_name": "packaging", + "installed_version": "24.0", + "required_version": ">=22.0" + }, + { + "key": "pathspec", + "package_name": "pathspec", + "installed_version": "0.12.1", + "required_version": ">=0.9.0" + }, + { + "key": "platformdirs", + "package_name": "platformdirs", + "installed_version": "4.2.2", + "required_version": ">=2" + }, + { + "key": "tomli", + "package_name": "tomli", + "installed_version": "2.0.1", + "required_version": ">=1.1.0" + }, + { + "key": "typing-extensions", + "package_name": "typing_extensions", + "installed_version": "4.12.0", + "required_version": ">=4.0.1" + } + ] + }, + { + "package": { + "key": "click", + "package_name": "click", + "installed_version": "8.1.7" + }, + "dependencies": [] + }, + { + "package": { + "key": "frozenlist", + "package_name": "frozenlist", + "installed_version": "1.4.1" + }, + "dependencies": [] + }, + { + "package": { + "key": "idna", + "package_name": "idna", + "installed_version": "3.7" + }, + "dependencies": [] + }, + { + "package": { + "key": "multidict", + "package_name": "multidict", + "installed_version": "6.0.5" + }, + "dependencies": [] + }, + { + "package": { + "key": "mypy-extensions", + "package_name": "mypy-extensions", + "installed_version": "1.0.0" + }, + "dependencies": [] + }, + { + "package": { + "key": "packaging", + "package_name": "packaging", + "installed_version": "24.0" + }, + "dependencies": [] + }, + { + "package": { + "key": "pathspec", + "package_name": "pathspec", + "installed_version": "0.12.1" + }, + "dependencies": [] + }, + { + "package": { + "key": "platformdirs", + "package_name": "platformdirs", + "installed_version": "4.2.2" + }, + "dependencies": [] + }, + { + "package": { + "key": "termcolor", + "package_name": "termcolor", + "installed_version": "2.4.0" + }, + "dependencies": [] + }, + { + "package": { + "key": "tomli", + "package_name": "tomli", + "installed_version": "2.0.1" + }, + "dependencies": [] + }, + { + "package": { + "key": "typing-extensions", + "package_name": "typing_extensions", + "installed_version": "4.12.0" + }, + "dependencies": [] + }, + { + "package": { + "key": "yarl", + "package_name": "yarl", + "installed_version": "1.9.4" + }, + "dependencies": [ + { + "key": "idna", + "package_name": "idna", + "installed_version": "3.7", + "required_version": ">=2.0" + }, + { + "key": "multidict", + "package_name": "multidict", + "installed_version": "6.0.5", + "required_version": ">=4.0" + } + ] + } +] \ No newline at end of file diff --git a/testdata/pypi/pipenv.graph.json b/testdata/pypi/pipenv.graph.json new file mode 100644 index 0000000..0517bfc --- /dev/null +++ b/testdata/pypi/pipenv.graph.json @@ -0,0 +1,67 @@ +[ + { + "package": { + "key": "requests", + "package_name": "requests", + "installed_version": "2.31.0" + }, + "dependencies": [ + { + "key": "charset-normalizer", + "package_name": "charset-normalizer", + "installed_version": "3.3.2", + "required_version": ">=2,<4" + }, + { + "key": "idna", + "package_name": "idna", + "installed_version": "3.7", + "required_version": ">=2.5,<4" + }, + { + "key": "urllib3", + "package_name": "urllib3", + "installed_version": "2.2.1", + "required_version": ">=1.21.1,<3" + }, + { + "key": "certifi", + "package_name": "certifi", + "installed_version": "2024.2.2", + "required_version": ">=2017.4.17" + } + ] + }, + { + "package": { + "key": "charset-normalizer", + "package_name": "charset-normalizer", + "installed_version": "3.3.2" + }, + "dependencies": [] + }, + { + "package": { + "key": "idna", + "package_name": "idna", + "installed_version": "3.7" + }, + "dependencies": [] + }, + { + "package": { + "key": "urllib3", + "package_name": "urllib3", + "installed_version": "2.2.1" + }, + "dependencies": [] + }, + { + "package": { + "key": "certifi", + "package_name": "certifi", + "installed_version": "2024.2.2" + }, + "dependencies": [] + } +]