Skip to content

Commit 3a3bc16

Browse files
committed
Add a new option --json-pdt
Signed-off-by: Tushar Goel <tushar.goel.dav@gmail.com>
1 parent 91a9d22 commit 3a3bc16

5 files changed

Lines changed: 261 additions & 5 deletions

File tree

src/python_inspector/resolution.py

Lines changed: 38 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -507,13 +507,45 @@ def format_resolution(results, environment, repos, as_tree=False):
507507
return dependencies
508508

509509

510+
def pdt_dfs(mapping, graph, src):
511+
"""
512+
Return a nested mapping of dependencies.
513+
"""
514+
children = list(graph.iter_children(src))
515+
if not children:
516+
return dict(
517+
key=src, package_name=src, installed_version=str(mapping[src].version), dependencies=[]
518+
)
519+
520+
return dict(
521+
key=src,
522+
package_name=src,
523+
installed_version=str(mapping[src].version),
524+
dependencies=sorted([pdt_dfs(mapping, graph, c) for c in children], key=lambda d: d["key"]),
525+
)
526+
527+
528+
def format_pdt_tree(results):
529+
"""
530+
Return a formatted tree of dependencies.
531+
"""
532+
mapping = results.mapping
533+
graph = results.graph
534+
dependencies = []
535+
for src in get_all_srcs(mapping=mapping, graph=graph):
536+
dependencies.append(pdt_dfs(mapping=mapping, graph=graph, src=src))
537+
dependencies.sort(key=lambda d: d["key"])
538+
return dependencies
539+
540+
510541
def get_resolved_dependencies(
511542
requirements: List[Requirement],
512543
environment: utils_pypi.Environment = None,
513544
repos: Sequence[utils_pypi.PypiSimpleRepository] = tuple(),
514545
as_tree: bool = False,
515546
max_rounds: int = 200000,
516547
debug: bool = False,
548+
pdt_output: bool = False,
517549
):
518550
"""
519551
Return resolved dependencies of a ``requirements`` list of Requirement for
@@ -535,9 +567,12 @@ def get_resolved_dependencies(
535567
),
536568
reporter=BaseReporter(),
537569
)
538-
results = resolver.resolve(requirements=requirements, max_rounds=max_rounds)
539-
results = format_resolution(results, as_tree=as_tree, environment=environment, repos=repos)
540-
return results
570+
resolver_results = resolver.resolve(requirements=requirements, max_rounds=max_rounds)
571+
if pdt_output:
572+
return format_pdt_tree(resolver_results)
573+
return format_resolution(
574+
resolver_results, as_tree=as_tree, environment=environment, repos=repos
575+
)
541576
except Exception as e:
542577
if debug:
543578
click.secho(f"{e!r}", err=True)

src/python_inspector/resolve_cli.py

Lines changed: 18 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -101,6 +101,13 @@
101101
help="Write output as pretty-printed JSON to FILE. "
102102
"Use the special '-' file name to print results on screen/stdout.",
103103
)
104+
@click.option(
105+
"--json-pdt",
106+
"pdt_output",
107+
is_flag=True,
108+
help="Write output as pretty-printed JSON to FILE. "
109+
"Use the special '-' file name to print results on screen/stdout.",
110+
)
104111
@click.option(
105112
"--max-rounds",
106113
"max_rounds",
@@ -135,6 +142,7 @@ def resolve_dependencies(
135142
operating_system,
136143
index_urls,
137144
json_output,
145+
pdt_output,
138146
max_rounds,
139147
use_cached_index=False,
140148
use_pypi_json_api=False,
@@ -234,6 +242,7 @@ def resolve_dependencies(
234242
as_tree=False,
235243
max_rounds=max_rounds,
236244
debug=debug,
245+
pdt_output=pdt_output,
237246
)
238247

239248
cli_options = [f"--requirement {rf}" for rf in requirement_files]
@@ -271,7 +280,13 @@ def resolve_dependencies(
271280

272281

273282
def resolve(
274-
direct_dependencies, environment, repos=tuple(), as_tree=False, max_rounds=200000, debug=False
283+
direct_dependencies,
284+
environment,
285+
repos=tuple(),
286+
as_tree=False,
287+
max_rounds=200000,
288+
debug=False,
289+
pdt_output=False,
275290
):
276291
"""
277292
Resolve dependencies given a ``direct_dependencies`` list of
@@ -284,7 +299,7 @@ def resolve(
284299
requirements = []
285300

286301
for dependency in direct_dependencies:
287-
# FIXME We are skipping editable requirements for now
302+
# FIXME We are skipping editable requirements for now
288303
# https://github.com/nexB/python-inspector/issues/41
289304
if dependency.extra_data.get("is_editable"):
290305
continue
@@ -299,6 +314,7 @@ def resolve(
299314
as_tree=as_tree,
300315
max_rounds=max_rounds,
301316
debug=debug,
317+
pdt_output=pdt_output,
302318
)
303319

304320
initial_requirements = [d.to_dict() for d in direct_dependencies]

tests/data/pdt-expected.json

Lines changed: 184 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,184 @@
1+
{
2+
"headers": {
3+
"tool_name": "dad",
4+
"tool_homepageurl": "https://github.com/nexB/python-inspector",
5+
"tool_version": "0.5.0",
6+
"options": [
7+
"--index-url https://pypi.org/simple",
8+
"--python-version 38",
9+
"--operating-system linux",
10+
"--json <file>"
11+
],
12+
"notice": "Dependency tree generated with python-inspector.\npython-inspector is a free software tool from nexB Inc. and others.\nVisit https://github.com/nexB/scancode-toolkit/ for support and download.",
13+
"warnings": [],
14+
"errors": []
15+
},
16+
"requirements": [
17+
{
18+
"purl": "pkg:pypi/click@6.7",
19+
"extracted_requirement": "click==6.7",
20+
"scope": "install",
21+
"is_runtime": true,
22+
"is_optional": false,
23+
"is_resolved": true,
24+
"resolved_package": {},
25+
"extra_data": {
26+
"is_editable": false,
27+
"link": null,
28+
"hash_options": [],
29+
"is_constraint": false,
30+
"is_archive": null,
31+
"is_wheel": false,
32+
"is_url": null,
33+
"is_vcs_url": null,
34+
"is_name_at_url": false,
35+
"is_local_path": null
36+
}
37+
},
38+
{
39+
"purl": "pkg:pypi/flask@1.0",
40+
"extracted_requirement": "Flask==1.0",
41+
"scope": "install",
42+
"is_runtime": true,
43+
"is_optional": false,
44+
"is_resolved": true,
45+
"resolved_package": {},
46+
"extra_data": {
47+
"is_editable": false,
48+
"link": null,
49+
"hash_options": [],
50+
"is_constraint": false,
51+
"is_archive": null,
52+
"is_wheel": false,
53+
"is_url": null,
54+
"is_vcs_url": null,
55+
"is_name_at_url": false,
56+
"is_local_path": null
57+
}
58+
},
59+
{
60+
"purl": "pkg:pypi/itsdangerous@0.24",
61+
"extracted_requirement": "itsdangerous==0.24",
62+
"scope": "install",
63+
"is_runtime": true,
64+
"is_optional": false,
65+
"is_resolved": true,
66+
"resolved_package": {},
67+
"extra_data": {
68+
"is_editable": false,
69+
"link": null,
70+
"hash_options": [],
71+
"is_constraint": false,
72+
"is_archive": null,
73+
"is_wheel": false,
74+
"is_url": null,
75+
"is_vcs_url": null,
76+
"is_name_at_url": false,
77+
"is_local_path": null
78+
}
79+
},
80+
{
81+
"purl": "pkg:pypi/jinja2@2.11.3",
82+
"extracted_requirement": "Jinja2==2.11.3",
83+
"scope": "install",
84+
"is_runtime": true,
85+
"is_optional": false,
86+
"is_resolved": true,
87+
"resolved_package": {},
88+
"extra_data": {
89+
"is_editable": false,
90+
"link": null,
91+
"hash_options": [],
92+
"is_constraint": false,
93+
"is_archive": null,
94+
"is_wheel": false,
95+
"is_url": null,
96+
"is_vcs_url": null,
97+
"is_name_at_url": false,
98+
"is_local_path": null
99+
}
100+
},
101+
{
102+
"purl": "pkg:pypi/markupsafe@1.0",
103+
"extracted_requirement": "MarkupSafe==1.0",
104+
"scope": "install",
105+
"is_runtime": true,
106+
"is_optional": false,
107+
"is_resolved": true,
108+
"resolved_package": {},
109+
"extra_data": {
110+
"is_editable": false,
111+
"link": null,
112+
"hash_options": [],
113+
"is_constraint": false,
114+
"is_archive": null,
115+
"is_wheel": false,
116+
"is_url": null,
117+
"is_vcs_url": null,
118+
"is_name_at_url": false,
119+
"is_local_path": null
120+
}
121+
},
122+
{
123+
"purl": "pkg:pypi/werkzeug@0.15.3",
124+
"extracted_requirement": "Werkzeug==0.15.3",
125+
"scope": "install",
126+
"is_runtime": true,
127+
"is_optional": false,
128+
"is_resolved": true,
129+
"resolved_package": {},
130+
"extra_data": {
131+
"is_editable": false,
132+
"link": null,
133+
"hash_options": [],
134+
"is_constraint": false,
135+
"is_archive": null,
136+
"is_wheel": false,
137+
"is_url": null,
138+
"is_vcs_url": null,
139+
"is_name_at_url": false,
140+
"is_local_path": null
141+
}
142+
}
143+
],
144+
"resolved_dependencies": [
145+
{
146+
"key": "flask",
147+
"package_name": "flask",
148+
"installed_version": "1.0",
149+
"dependencies": [
150+
{
151+
"key": "click",
152+
"package_name": "click",
153+
"installed_version": "6.7",
154+
"dependencies": []
155+
},
156+
{
157+
"key": "itsdangerous",
158+
"package_name": "itsdangerous",
159+
"installed_version": "0.24",
160+
"dependencies": []
161+
},
162+
{
163+
"key": "jinja2",
164+
"package_name": "jinja2",
165+
"installed_version": "2.11.3",
166+
"dependencies": [
167+
{
168+
"key": "markupsafe",
169+
"package_name": "markupsafe",
170+
"installed_version": "1.0",
171+
"dependencies": []
172+
}
173+
]
174+
},
175+
{
176+
"key": "werkzeug",
177+
"package_name": "werkzeug",
178+
"installed_version": "0.15.3",
179+
"dependencies": []
180+
}
181+
]
182+
}
183+
]
184+
}

tests/data/pdt.txt

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,6 @@
1+
click==6.7
2+
Flask==1.0
3+
itsdangerous==0.24
4+
Jinja2==2.11.3
5+
MarkupSafe==1.0
6+
Werkzeug==0.15.3

tests/test_cli.py

Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -40,6 +40,21 @@ def test_cli_with_default_urls():
4040
)
4141

4242

43+
@pytest.mark.online
44+
def test_pdt_output():
45+
requirements_file = test_env.get_test_loc("pdt.txt")
46+
expected_file = test_env.get_test_loc("pdt-expected.json", must_exist=False)
47+
extra_options = [
48+
"--json-pdt",
49+
]
50+
check_requirements_resolution(
51+
requirements_file=requirements_file,
52+
expected_file=expected_file,
53+
extra_options=extra_options,
54+
regen=REGEN_TEST_FIXTURES,
55+
)
56+
57+
4358
@pytest.mark.online
4459
def test_cli_with_single_index_url():
4560
expected_file = test_env.get_test_loc("single-url-expected.json", must_exist=False)

0 commit comments

Comments
 (0)