generated from aboutcode-org/skeleton
-
-
Notifications
You must be signed in to change notification settings - Fork 28
Expand file tree
/
Copy pathresolve_cli.py
More file actions
402 lines (350 loc) · 11.8 KB
/
resolve_cli.py
File metadata and controls
402 lines (350 loc) · 11.8 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
#!/usr/bin/env python
# -*- coding: utf-8 -*-
#
# Copyright (c) nexB Inc. and others. All rights reserved.
# ScanCode is a trademark of nexB Inc.
# SPDX-License-Identifier: Apache-2.0
# See http://www.apache.org/licenses/LICENSE-2.0 for the license text.
# See https://github.com/aboutcode-org/python-inspector for support or download.
# See https://aboutcode.org for more information about nexB OSS projects.
#
from typing import Dict
import click
from python_inspector import pyinspector_settings
from python_inspector import settings
from python_inspector import utils_pypi
from python_inspector.cli_utils import FileOptionType
from python_inspector.utils import write_output_in_file
TRACE = False
__version__ = "0.14.4"
DEFAULT_PYTHON_VERSION = settings.DEFAULT_PYTHON_VERSION
PYPI_SIMPLE_URL = settings.PYPI_SIMPLE_URL
def print_version(ctx, param, value):
if not value or ctx.resilient_parsing:
return
click.echo(f"Python-inspector version: {__version__}")
ctx.exit()
@click.command()
@click.pass_context
@click.option(
"-r",
"--requirement",
"requirement_files",
type=click.Path(exists=True, readable=True, path_type=str, dir_okay=False),
metavar="REQUIREMENT-FILE",
multiple=True,
required=False,
help="Path to pip requirements file listing thirdparty packages. "
"This option can be used multiple times.",
)
@click.option(
"-s",
"--setup-py",
"setup_py_file",
type=click.Path(exists=True, readable=True, path_type=str, dir_okay=False),
metavar="SETUP-PY-FILE",
multiple=False,
required=False,
help="Path to setuptools setup.py file listing dependencies and metadata.",
)
@click.option(
"--spec",
"--specifier",
"specifiers",
type=str,
metavar="SPECIFIER",
multiple=True,
required=False,
help="Package specifier such as django==1.2.3. This option can be used multiple times.",
)
@click.option(
"-p",
"--python-version",
"python_version",
type=click.Choice(utils_pypi.valid_python_versions),
metavar="PYVER",
show_default=True,
required=True,
help="Python version to use for dependency resolution. One of "
+ ", ".join(utils_pypi.PYTHON_DOT_VERSIONS_BY_VER.values()),
)
@click.option(
"-o",
"--operating-system",
"operating_system",
type=click.Choice(utils_pypi.PLATFORMS_BY_OS),
metavar="OS",
show_default=True,
required=True,
help="OS to use for dependency resolution. One of " + ", ".join(utils_pypi.PLATFORMS_BY_OS),
)
@click.option(
"--index-url",
"index_urls",
type=str,
metavar="INDEX",
show_default=True,
default=pyinspector_settings.INDEX_URL,
multiple=True,
help="PyPI simple index URL(s) to use in order of preference. "
"This option can be used multiple times.",
)
@click.option(
"--json",
"json_output",
type=FileOptionType(mode="w", encoding="utf-8", lazy=True),
required=False,
metavar="FILE",
help="Write output as pretty-printed JSON to FILE. "
"Use the special '-' file name to print results on screen/stdout.",
)
@click.option(
"--json-pdt",
"pdt_output",
type=FileOptionType(mode="w", encoding="utf-8", lazy=True),
required=False,
metavar="FILE",
help="Write output as pretty-printed JSON to FILE as a tree in the style of pipdeptree. "
"Use the special '-' file name to print results on screen/stdout.",
)
@click.option(
"-n",
"--netrc",
"netrc_file",
type=click.Path(exists=True, readable=True, path_type=str, dir_okay=False),
metavar="NETRC-FILE",
hidden=True,
required=False,
help="Netrc file to use for authentication.",
)
@click.option(
"--max-rounds",
"max_rounds",
hidden=True,
type=int,
default=200000,
help="Increase the maximum number of resolution rounds. "
"Use in the rare cases where the resolution graph is very deep.",
)
@click.option(
"--use-cached-index",
is_flag=True,
hidden=True,
help="Use cached on-disk PyPI simple package indexes "
"and do not refetch package index if cache is present.",
)
@click.option(
"--use-pypi-json-api",
is_flag=True,
help="Use PyPI JSON API to fetch dependency data. Faster but not always correct. "
"--index-url are ignored when this option is active.",
)
@click.option(
"--analyze-setup-py-insecurely",
is_flag=True,
help="Enable collection of requirements in setup.py that compute these "
"dynamically. This is an insecure operation as it can run arbitrary code.",
)
@click.option(
"--prefer-source",
is_flag=True,
help="Prefer source distributions over binary distributions if no source "
"distribution is available then binary distributions are used",
)
@click.option(
"--verbose",
is_flag=True,
help="Enable verbose debug output.",
)
@click.option(
"-V",
"--version",
is_flag=True,
is_eager=True,
expose_value=False,
callback=print_version,
help="Show the version and exit.",
)
@click.option(
"--ignore-errors", is_flag=True, default=False, help="Ignore errors and continue execution."
)
@click.help_option("-h", "--help")
@click.option(
"--generic-paths",
is_flag=True,
hidden=True,
help="Use generic or truncated paths in the JSON output header and files sections. "
"Used only for testing to avoid absolute paths and paths changing at each run.",
)
def resolve_dependencies(
ctx,
requirement_files,
setup_py_file,
specifiers,
python_version,
operating_system,
index_urls,
json_output,
pdt_output,
netrc_file,
max_rounds,
use_cached_index=False,
use_pypi_json_api=False,
analyze_setup_py_insecurely=False,
prefer_source=False,
verbose=TRACE,
generic_paths=False,
ignore_errors=False,
):
"""
Resolve the dependencies for the package requirements listed in one or
more REQUIREMENT-FILE file, one or more SPECIFIER and one setuptools
SETUP-PY-FILE file and save the results as JSON to FILE.
Resolve the dependencies for the requested ``--python-version`` PYVER and
``--operating_system`` OS combination.
Download from the provided PyPI simple --index-url INDEX(s) URLs defaulting
to PyPI.org.
Provide source distributions over binary distributions with the --prefer-source
option. If no source distribution is available then binary distributions are used.
Error and progress are printed to stderr.
For example, display the results of resolving the dependencies for flask==2.1.2
on screen::
python-inspector --spec "flask==2.1.2" --json -
"""
from python_inspector.api import resolve_dependencies as resolver_api
if not (json_output or pdt_output):
click.secho("No output file specified. Use --json or --json-pdt.", err=True)
ctx.exit(1)
if json_output and pdt_output:
click.secho("Only one of --json or --json-pdt can be used.", err=True)
ctx.exit(1)
options = get_pretty_options(ctx, generic_paths=generic_paths)
notice = (
"Dependency tree generated with python-inspector.\n"
"python-inspector is a free software tool from nexB Inc. and others.\n"
"Visit https://github.com/aboutcode-org/python-inspector/ for support and download."
)
headers = dict(
tool_name="python-inspector",
tool_homepageurl="https://github.com/aboutcode-org/python-inspector",
tool_version=__version__,
options=options,
notice=notice,
warnings=[],
errors=[],
)
try:
resolution_result: Dict = resolver_api(
requirement_files=requirement_files,
setup_py_file=setup_py_file,
specifiers=specifiers,
python_version=python_version,
operating_system=operating_system,
index_urls=index_urls,
pdt_output=pdt_output,
netrc_file=netrc_file,
max_rounds=max_rounds,
use_cached_index=use_cached_index,
use_pypi_json_api=use_pypi_json_api,
verbose=verbose,
analyze_setup_py_insecurely=analyze_setup_py_insecurely,
printer=click.secho,
prefer_source=prefer_source,
ignore_errors=ignore_errors,
generic_paths=generic_paths,
)
files = resolution_result.files or []
output = dict(
headers=headers,
files=files,
packages=resolution_result.packages,
resolved_dependencies_graph=resolution_result.resolution,
)
write_output_in_file(
output=output,
location=json_output or pdt_output,
)
except Exception:
import traceback
click.secho(traceback.format_exc(), err=True)
ctx.exit(1)
def get_pretty_options(ctx, generic_paths=False):
"""
Return a sorted list of formatted strings for the selected CLI options of
the `ctx` Click.context, putting arguments first then options:
["~/some/path", "--license", ...]
Skip options that are hidden or flags that are not set.
If ``generic_paths`` is True, click.File and click.Path parameters are made
"generic" replacing their value with a placeholder. This is used mostly for
testing.
"""
args = []
options = []
param_values = ctx.params
for param in ctx.command.params:
name = param.name
value = param_values.get(name)
if param.is_eager:
continue
if getattr(param, "hidden", False):
continue
if value in (None, False):
continue
if value in (tuple(), []):
# option with multiple values, the value is a emoty tuple
continue
# opts is a list of CLI options as in "--verbose": the last opt is
# the CLI option long form by convention
cli_opt = param.opts[-1]
if not isinstance(value, (tuple, list)):
value = [value]
for val in value:
val = get_pretty_value(param_type=param.type, value=val, generic_paths=generic_paths)
if isinstance(param, click.Argument):
args.append(val)
else:
# an option
if val is True:
# mere flag... do not add the "true" value
options.append(f"{cli_opt}")
else:
options.append(f"{cli_opt} {val}")
return sorted(args) + sorted(options)
def get_pretty_value(param_type, value, generic_paths=False):
"""
Return pretty formatted string extracted from a parameter ``value``.
Make paths generic (by using a placeholder or truncating the path) if
``generic_paths`` is True.
"""
if isinstance(param_type, (click.Path, click.File)):
return get_pretty_path(param_type, value, generic_paths)
elif not (value is None or isinstance(value, (str, bytes, tuple, list, dict, bool))):
# coerce to string for non-basic types
return repr(value)
else:
return value
def get_pretty_path(param_type, value, generic_paths=False):
"""
Return a pretty path value for a Path or File option. Truncate the path or
use a placeholder as needed if ``generic_paths`` is True. Used for testing.
"""
from python_inspector.utils import remove_test_data_dir_variable_prefix
if value == "-":
return value
if isinstance(param_type, click.Path):
if generic_paths:
return remove_test_data_dir_variable_prefix(path=value)
return value
elif isinstance(param_type, click.File):
# the value cannot be displayed as-is as this may be an opened file-
# like object
vname = getattr(value, "name", None)
if not vname:
return "<file>"
else:
value = vname
if generic_paths:
return remove_test_data_dir_variable_prefix(path=value, placeholder="<file>")
return value
if __name__ == "__main__":
resolve_dependencies()