1414import click
1515
1616from python_inspector import utils_pypi
17- from python_inspector .api import resolve_dependencies as resolver_api
1817from python_inspector .cli_utils import FileOptionType
1918from python_inspector .utils import write_output_in_file
2019
@@ -52,9 +51,9 @@ def print_version(ctx, param, value):
5251 "setup_py_file" ,
5352 type = click .Path (exists = True , readable = True , path_type = str , dir_okay = False ),
5453 metavar = "SETUP-PY-FILE" ,
54+ multiple = False ,
5555 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." ,
5857)
5958@click .option (
6059 "--spec" ,
@@ -74,7 +73,8 @@ def print_version(ctx, param, value):
7473 metavar = "PYVER" ,
7574 show_default = True ,
7675 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 ()),
7878)
7979@click .option (
8080 "-o" ,
@@ -84,7 +84,7 @@ def print_version(ctx, param, value):
8484 metavar = "OS" ,
8585 show_default = True ,
8686 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 ) ,
8888)
8989@click .option (
9090 "--index-url" ,
@@ -123,21 +123,23 @@ def print_version(ctx, param, value):
123123 metavar = "NETRC-FILE" ,
124124 hidden = True ,
125125 required = False ,
126- help = "Netrc file to use for authentication. " ,
126+ help = "Netrc file to use for authentication." ,
127127)
128128@click .option (
129129 "--max-rounds" ,
130130 "max_rounds" ,
131131 hidden = True ,
132132 type = int ,
133133 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." ,
135136)
136137@click .option (
137138 "--use-cached-index" ,
138139 is_flag = True ,
139140 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." ,
141143)
142144@click .option (
143145 "--use-pypi-json-api" ,
@@ -148,20 +150,19 @@ def print_version(ctx, param, value):
148150@click .option (
149151 "--analyze-setup-py-insecurely" ,
150152 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." ,
153155)
154156@click .option (
155157 "--prefer-source" ,
156158 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" ,
159161)
160162@click .option (
161163 "--verbose" ,
162164 is_flag = True ,
163- hidden = True ,
164- help = "Enable debug output." ,
165+ help = "Enable verbose debug output." ,
165166)
166167@click .option (
167168 "-V" ,
@@ -173,6 +174,13 @@ def print_version(ctx, param, value):
173174 help = "Show the version and exit." ,
174175)
175176@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+ )
176184def resolve_dependencies (
177185 ctx ,
178186 requirement_files ,
@@ -190,6 +198,7 @@ def resolve_dependencies(
190198 analyze_setup_py_insecurely = False ,
191199 prefer_source = False ,
192200 verbose = TRACE ,
201+ generic_paths = False ,
193202):
194203 """
195204 Resolve the dependencies for the package requirements listed in one or
@@ -212,6 +221,8 @@ def resolve_dependencies(
212221
213222 python-inspector --spec "flask==2.1.2" --json -
214223 """
224+ from python_inspector .api import resolve_dependencies as resolver_api
225+
215226 if not (json_output or pdt_output ):
216227 click .secho ("No output file specified. Use --json or --json-pdt." , err = True )
217228 ctx .exit (1 )
@@ -220,12 +231,7 @@ def resolve_dependencies(
220231 click .secho ("Only one of --json or --json-pdt can be used." , err = True )
221232 ctx .exit (1 )
222233
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 )
229235
230236 notice = (
231237 "Dependency tree generated with python-inspector.\n "
@@ -260,23 +266,133 @@ def resolve_dependencies(
260266 analyze_setup_py_insecurely = analyze_setup_py_insecurely ,
261267 printer = click .secho ,
262268 prefer_source = prefer_source ,
269+ generic_paths = generic_paths ,
263270 )
271+
272+ files = resolution_result .files or []
264273 output = dict (
265274 headers = headers ,
266- files = resolution_result . files ,
275+ files = files ,
267276 packages = resolution_result .packages ,
268277 resolved_dependencies_graph = resolution_result .resolution ,
269278 )
270279 write_output_in_file (
271280 output = output ,
272281 location = json_output or pdt_output ,
273282 )
274- except Exception as exc :
283+ except Exception :
275284 import traceback
276285
277286 click .secho (traceback .format_exc (), err = True )
278287 ctx .exit (1 )
279288
280289
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+
281397if __name__ == "__main__" :
282398 resolve_dependencies ()
0 commit comments