14
14
import click
15
15
16
16
from python_inspector import utils_pypi
17
- from python_inspector .api import resolve_dependencies as resolver_api
18
17
from python_inspector .cli_utils import FileOptionType
19
18
from python_inspector .utils import write_output_in_file
20
19
@@ -52,9 +51,9 @@ def print_version(ctx, param, value):
52
51
"setup_py_file" ,
53
52
type = click .Path (exists = True , readable = True , path_type = str , dir_okay = False ),
54
53
metavar = "SETUP-PY-FILE" ,
54
+ multiple = False ,
55
55
required = False ,
56
- help = "Path to setuptools setup.py file listing dependencies and metadata. "
57
- "This option can be used multiple times." ,
56
+ help = "Path to setuptools setup.py file listing dependencies and metadata." ,
58
57
)
59
58
@click .option (
60
59
"--spec" ,
@@ -74,7 +73,8 @@ def print_version(ctx, param, value):
74
73
metavar = "PYVER" ,
75
74
show_default = True ,
76
75
required = True ,
77
- help = "Python version to use for dependency resolution." ,
76
+ help = "Python version to use for dependency resolution. One of "
77
+ + ", " .join (utils_pypi .PYTHON_DOT_VERSIONS_BY_VER .values ()),
78
78
)
79
79
@click .option (
80
80
"-o" ,
@@ -84,7 +84,7 @@ def print_version(ctx, param, value):
84
84
metavar = "OS" ,
85
85
show_default = True ,
86
86
required = True ,
87
- help = "OS to use for dependency resolution." ,
87
+ help = "OS to use for dependency resolution. One of " + ", " . join ( utils_pypi . PLATFORMS_BY_OS ) ,
88
88
)
89
89
@click .option (
90
90
"--index-url" ,
@@ -123,21 +123,23 @@ def print_version(ctx, param, value):
123
123
metavar = "NETRC-FILE" ,
124
124
hidden = True ,
125
125
required = False ,
126
- help = "Netrc file to use for authentication. " ,
126
+ help = "Netrc file to use for authentication." ,
127
127
)
128
128
@click .option (
129
129
"--max-rounds" ,
130
130
"max_rounds" ,
131
131
hidden = True ,
132
132
type = int ,
133
133
default = 200000 ,
134
- help = "Increase the max rounds whenever the resolution is too deep" ,
134
+ help = "Increase the maximum number of resolution rounds. "
135
+ "Use in the rare cases where the resolution graph is very deep." ,
135
136
)
136
137
@click .option (
137
138
"--use-cached-index" ,
138
139
is_flag = True ,
139
140
hidden = True ,
140
- help = "Use cached on-disk PyPI simple package indexes and do not refetch if present." ,
141
+ help = "Use cached on-disk PyPI simple package indexes "
142
+ "and do not refetch package index if cache is present." ,
141
143
)
142
144
@click .option (
143
145
"--use-pypi-json-api" ,
@@ -148,20 +150,19 @@ def print_version(ctx, param, value):
148
150
@click .option (
149
151
"--analyze-setup-py-insecurely" ,
150
152
is_flag = True ,
151
- help = "Enable collection of requirements in setup.py that compute these"
152
- " dynamically. This is an insecure operation as it can run arbitrary code." ,
153
+ help = "Enable collection of requirements in setup.py that compute these "
154
+ "dynamically. This is an insecure operation as it can run arbitrary code." ,
153
155
)
154
156
@click .option (
155
157
"--prefer-source" ,
156
158
is_flag = True ,
157
- help = "Prefer source distributions over binary distributions"
158
- " if no source distribution is available then binary distributions are used" ,
159
+ help = "Prefer source distributions over binary distributions if no source "
160
+ "distribution is available then binary distributions are used" ,
159
161
)
160
162
@click .option (
161
163
"--verbose" ,
162
164
is_flag = True ,
163
- hidden = True ,
164
- help = "Enable debug output." ,
165
+ help = "Enable verbose debug output." ,
165
166
)
166
167
@click .option (
167
168
"-V" ,
@@ -173,6 +174,13 @@ def print_version(ctx, param, value):
173
174
help = "Show the version and exit." ,
174
175
)
175
176
@click .help_option ("-h" , "--help" )
177
+ @click .option (
178
+ "--generic-paths" ,
179
+ is_flag = True ,
180
+ hidden = True ,
181
+ help = "Use generic or truncated paths in the JSON output header and files sections. "
182
+ "Used only for testing to avoid absolute paths and paths changing at each run." ,
183
+ )
176
184
def resolve_dependencies (
177
185
ctx ,
178
186
requirement_files ,
@@ -190,6 +198,7 @@ def resolve_dependencies(
190
198
analyze_setup_py_insecurely = False ,
191
199
prefer_source = False ,
192
200
verbose = TRACE ,
201
+ generic_paths = False ,
193
202
):
194
203
"""
195
204
Resolve the dependencies for the package requirements listed in one or
@@ -212,6 +221,8 @@ def resolve_dependencies(
212
221
213
222
python-inspector --spec "flask==2.1.2" --json -
214
223
"""
224
+ from python_inspector .api import resolve_dependencies as resolver_api
225
+
215
226
if not (json_output or pdt_output ):
216
227
click .secho ("No output file specified. Use --json or --json-pdt." , err = True )
217
228
ctx .exit (1 )
@@ -220,12 +231,7 @@ def resolve_dependencies(
220
231
click .secho ("Only one of --json or --json-pdt can be used." , err = True )
221
232
ctx .exit (1 )
222
233
223
- options = [f"--requirement { rf } " for rf in requirement_files ]
224
- options += [f"--specifier { sp } " for sp in specifiers ]
225
- options += [f"--index-url { iu } " for iu in index_urls ]
226
- options += [f"--python-version { python_version } " ]
227
- options += [f"--operating-system { operating_system } " ]
228
- options += ["--json <file>" ]
234
+ options = get_pretty_options (ctx , generic_paths = generic_paths )
229
235
230
236
notice = (
231
237
"Dependency tree generated with python-inspector.\n "
@@ -260,23 +266,133 @@ def resolve_dependencies(
260
266
analyze_setup_py_insecurely = analyze_setup_py_insecurely ,
261
267
printer = click .secho ,
262
268
prefer_source = prefer_source ,
269
+ generic_paths = generic_paths ,
263
270
)
271
+
272
+ files = resolution_result .files or []
264
273
output = dict (
265
274
headers = headers ,
266
- files = resolution_result . files ,
275
+ files = files ,
267
276
packages = resolution_result .packages ,
268
277
resolved_dependencies_graph = resolution_result .resolution ,
269
278
)
270
279
write_output_in_file (
271
280
output = output ,
272
281
location = json_output or pdt_output ,
273
282
)
274
- except Exception as exc :
283
+ except Exception :
275
284
import traceback
276
285
277
286
click .secho (traceback .format_exc (), err = True )
278
287
ctx .exit (1 )
279
288
280
289
290
+ def get_pretty_options (ctx , generic_paths = False ):
291
+ """
292
+ Return a sorted list of formatted strings for the selected CLI options of
293
+ the `ctx` Click.context, putting arguments first then options:
294
+
295
+ ["~/some/path", "--license", ...]
296
+
297
+ Skip options that are hidden or flags that are not set.
298
+ If ``generic_paths`` is True, click.File and click.Path parameters are made
299
+ "generic" replacing their value with a placeholder. This is used mostly for
300
+ testing.
301
+ """
302
+
303
+ args = []
304
+ options = []
305
+
306
+ param_values = ctx .params
307
+ for param in ctx .command .params :
308
+ name = param .name
309
+ value = param_values .get (name )
310
+
311
+ if param .is_eager :
312
+ continue
313
+
314
+ if getattr (param , "hidden" , False ):
315
+ continue
316
+
317
+ if value == param .default :
318
+ continue
319
+
320
+ if value in (None , False ):
321
+ continue
322
+
323
+ if value in (tuple (), []):
324
+ # option with multiple values, the value is a emoty tuple
325
+ continue
326
+
327
+ # opts is a list of CLI options as in "--verbose": the last opt is
328
+ # the CLI option long form by convention
329
+ cli_opt = param .opts [- 1 ]
330
+
331
+ if not isinstance (value , (tuple , list )):
332
+ value = [value ]
333
+
334
+ for val in value :
335
+ val = get_pretty_value (param_type = param .type , value = val , generic_paths = generic_paths )
336
+
337
+ if isinstance (param , click .Argument ):
338
+ args .append (val )
339
+ else :
340
+ # an option
341
+ if val is True :
342
+ # mere flag... do not add the "true" value
343
+ options .append (f"{ cli_opt } " )
344
+ else :
345
+ options .append (f"{ cli_opt } { val } " )
346
+
347
+ return sorted (args ) + sorted (options )
348
+
349
+
350
+ def get_pretty_value (param_type , value , generic_paths = False ):
351
+ """
352
+ Return pretty formatted string extracted from a parameter ``value``.
353
+ Make paths generic (by using a placeholder or truncating the path) if
354
+ ``generic_paths`` is True.
355
+ """
356
+ if isinstance (param_type , (click .Path , click .File )):
357
+ return get_pretty_path (param_type , value , generic_paths )
358
+
359
+ elif not (value is None or isinstance (value , (str , bytes , tuple , list , dict , bool ))):
360
+ # coerce to string for non-basic types
361
+ return repr (value )
362
+
363
+ else :
364
+ return value
365
+
366
+
367
+ def get_pretty_path (param_type , value , generic_paths = False ):
368
+ """
369
+ Return a pretty path value for a Path or File option. Truncate the path or
370
+ use a placeholder as needed if ``generic_paths`` is True. Used for testing.
371
+ """
372
+ from python_inspector .utils import remove_test_data_dir_variable_prefix
373
+
374
+ if value == "-" :
375
+ return value
376
+
377
+ if isinstance (param_type , click .Path ):
378
+ if generic_paths :
379
+ return remove_test_data_dir_variable_prefix (path = value )
380
+ return value
381
+
382
+ elif isinstance (param_type , click .File ):
383
+ # the value cannot be displayed as-is as this may be an opened file-
384
+ # like object
385
+ vname = getattr (value , "name" , None )
386
+ if not vname :
387
+ return "<file>"
388
+ else :
389
+ value = vname
390
+
391
+ if generic_paths :
392
+ return remove_test_data_dir_variable_prefix (path = value , placeholder = "<file>" )
393
+
394
+ return value
395
+
396
+
281
397
if __name__ == "__main__" :
282
398
resolve_dependencies ()
0 commit comments