Skip to content

Commit dc439a3

Browse files
committed
Update CLI help and validation
* rename cmd.cli() to cmd.about() * Use a common validation callback function for --mapping and --mapping-file options * Streamline and standardize help text and METAVAR usage * Standardize option definitions in particular for Path options Signed-off-by: Philippe Ombredanne <[email protected]>
1 parent d0e061e commit dc439a3

File tree

7 files changed

+133
-90
lines changed

7 files changed

+133
-90
lines changed

setup.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -79,7 +79,7 @@ def read(*names, **kwargs):
7979
},
8080
entry_points={
8181
'console_scripts': [
82-
'about=attributecode.cmd:cli',
82+
'about=attributecode.cmd:about',
8383
]
8484
},
8585
)

src/attributecode/cmd.py

Lines changed: 95 additions & 58 deletions
Original file line numberDiff line numberDiff line change
@@ -92,7 +92,7 @@ def main(self, args=None, prog_name=None, complete_var=None,
9292
@click.group(name='about')
9393
@click.version_option(version=__version__, prog_name=prog_name, message=intro)
9494
@click.help_option('-h', '--help')
95-
def cli():
95+
def about():
9696
"""
9797
Generate licensing attribution and credit notices from .ABOUT files and inventories.
9898
@@ -122,16 +122,32 @@ def validate_filter(ctx, param, value):
122122
return kvals
123123

124124

125-
@cli.command(cls=AboutCommand,
126-
short_help='Collect .ABOUT files and write an inventory as CSV or JSON.')
125+
def validate_mapping(mapping, mapping_file):
126+
"""
127+
Return a mapping_file or None.
128+
Raise a UsageError on errors.
129+
"""
130+
if mapping and mapping_file:
131+
raise click.UsageError(
132+
'Invalid options combination: '
133+
'--mapping and --mapping-file are ultually exclusive.')
134+
if mapping:
135+
return DEFAULT_MAPPING
136+
return mapping_file or None
137+
138+
139+
@about.command(cls=AboutCommand,
140+
short_help='Collect the inventory of .ABOUT files to a CSV or JSON file.')
127141

128142
@click.argument('location',
129143
required=True,
144+
metavar='LOCATION',
130145
type=click.Path(
131146
exists=True, file_okay=True, dir_okay=True, readable=True, resolve_path=True))
132147

133148
@click.argument('output',
134149
required=True,
150+
metavar='OUTPUT',
135151
type=click.Path(exists=False, dir_okay=False, writable=True, resolve_path=True))
136152

137153
# fIXME: this is too complex and should be removed
@@ -150,13 +166,16 @@ def validate_filter(ctx, param, value):
150166

151167
@click.option('--mapping',
152168
is_flag=True,
153-
help='Use the default file mapping.config (./attributecode/mapping.config) '
154-
'with mapping between input keys and ABOUT field names.')
169+
help='Use the default built-in "mapping.config" file '
170+
'with mapping between input keys and .ABOUT field names.'
171+
'Cannot be combined with the --mapping-file option.')
155172

156173
@click.option('--mapping-file',
157174
metavar='FILE',
158-
type=click.Path(exists=True, dir_okay=True, readable=True, resolve_path=True),
159-
help='Use a custom mapping file with mapping between input keys and ABOUT field names.')
175+
type=click.Path(exists=True, dir_okay=False, readable=True, resolve_path=True),
176+
help='Path to an optional custom mapping FILE '
177+
'with mapping between input keys and .ABOUT field names. '
178+
'Cannot be combined with the --mapping option.')
160179

161180
@click.option('-q', '--quiet',
162181
is_flag=True,
@@ -171,7 +190,7 @@ def validate_filter(ctx, param, value):
171190
def inventory(location, output, mapping, mapping_file,
172191
format, filter, quiet, verbose): # NOQA
173192
"""
174-
Collect a JSON or CSV inventory of packages from .ABOUT files.
193+
Collect the inventory of .ABOUT file data as CSV or JSON.
175194
176195
LOCATION: Path to an .ABOUT file or a directory with .ABOUT files.
177196
@@ -181,20 +200,12 @@ def inventory(location, output, mapping, mapping_file,
181200
print_version()
182201
click.echo('Collecting inventory from ABOUT files...')
183202

184-
if not os.path.exists(os.path.dirname(output)):
185-
# FIXME: there is likely a better way to return an error
186-
raise click.UsageError('ERROR: <OUTPUT> path does not exists.')
187-
188203
# FIXME: do we really want to continue support zip as an input?
189204
if location.lower().endswith('.zip'):
190205
# accept zipped ABOUT files as input
191206
location = extract_zip(location)
192207

193-
if mapping and mapping_file:
194-
raise click.UsageError('Invalid combination of options: --mapping and --mapping-file')
195-
196-
if mapping:
197-
mapping_file = DEFAULT_MAPPING
208+
mapping_file = validate_mapping(mapping, mapping_file)
198209

199210
errors, abouts = collect_inventory(location, mapping_file=mapping_file)
200211

@@ -203,7 +214,7 @@ def inventory(location, output, mapping, mapping_file,
203214
abouts = inventory_filter(abouts, filter)
204215

205216
# Do not write the output if one of the ABOUT files has duplicated keys
206-
# TODO: why stop only for this message??????
217+
# TODO: why do this check here?? Also if this is the place, we should list what the errors are.
207218
dup_error_msg = u'Duplicated keys'
208219
halt_output = False
209220
for err in errors:
@@ -231,16 +242,28 @@ def inventory(location, output, mapping, mapping_file,
231242
# gen subcommand
232243
######################################################################
233244

234-
@cli.command(cls=AboutCommand,
245+
def validate_location_extension(ctx, param, value):
246+
if not value:
247+
return
248+
if not value.endswith(('.csv', '.json',)):
249+
raise click.UsageError(
250+
'Invalid input file extension: must be one .csv or .json.')
251+
return value
252+
253+
254+
@about.command(cls=AboutCommand,
235255
short_help='Generate .ABOUT files from an inventory as CSV or JSON.')
236256

237257
@click.argument('location',
238258
required=True,
239-
type=click.Path(exists=True, file_okay=True, dir_okay=False, readable=True, resolve_path=True))
259+
metavar='LOCATION',
260+
type=click.Path(
261+
exists=True, file_okay=True, dir_okay=True, readable=True, resolve_path=True))
240262

241263
@click.argument('output',
242264
required=True,
243-
type=click.Path(exists=True, writable=True, file_okay=False, dir_okay=True, resolve_path=True))
265+
metavar='OUTPUT',
266+
type=click.Path(exists=True, file_okay=False, writable=True, resolve_path=True))
244267

245268
# FIXME: the CLI UX should be improved with two separate options for API key and URL
246269
@click.option('--fetch-license',
@@ -257,13 +280,16 @@ def inventory(location, output, mapping, mapping_file,
257280

258281
@click.option('--mapping',
259282
is_flag=True,
260-
help='Use the default file mapping.config (./attributecode/mapping.config) '
261-
'with mapping between input keys and ABOUT field names.')
283+
help='Use the default built-in "mapping.config" file '
284+
'with mapping between input keys and .ABOUT field names.'
285+
'Cannot be combined with the --mapping-file option.')
262286

263287
@click.option('--mapping-file',
264288
metavar='FILE',
265-
type=click.Path(exists=True, dir_okay=True, readable=True, resolve_path=True),
266-
help='Use a custom mapping file with mapping between input keys and ABOUT field names.')
289+
type=click.Path(exists=True, dir_okay=False, readable=True, resolve_path=True),
290+
help='Path to an optional custom mapping FILE '
291+
'with mapping between input keys and .ABOUT field names. '
292+
'Cannot be combined with the --mapping option.')
267293

268294
@click.option('-q', '--quiet',
269295
is_flag=True,
@@ -281,7 +307,7 @@ def gen(location, output,
281307
mapping, mapping_file,
282308
quiet, verbose):
283309
"""
284-
Generate .ABOUT files in OUTPUT directory from a JSON or CSV inventory of .ABOUT files at LOCATION.
310+
Generate .ABOUT files in OUTPUT from an inventory of .ABOUT files at LOCATION.
285311
286312
LOCATION: Path to a JSON or CSV inventory file.
287313
@@ -291,10 +317,7 @@ def gen(location, output,
291317
print_version()
292318
click.echo('Generating .ABOUT files...')
293319

294-
if mapping and mapping_file:
295-
raise click.UsageError('Invalid combination of options: --mapping and --mapping-file')
296-
if mapping:
297-
mapping_file = DEFAULT_MAPPING
320+
mapping_file = validate_mapping(mapping, mapping_file)
298321

299322
if not location.endswith(('.csv', '.json',)):
300323
raise click.UsageError('ERROR: Invalid input file extension: must be one .csv or .json.')
@@ -335,21 +358,41 @@ def validate_variables(ctx, param, value):
335358
return kvals
336359

337360

338-
@cli.command(cls=AboutCommand,
361+
def validate_template(ctx, param, value):
362+
if not value:
363+
return DEFAULT_TEMPLATE_FILE
364+
365+
with io.open(value, encoding='utf-8') as templatef:
366+
template_error = check_template(templatef.read())
367+
368+
if template_error:
369+
lineno, message = template_error
370+
raise click.UsageError(
371+
'Template syntax error at line: '
372+
'{lineno}: "{message}"'.format(**locals()))
373+
return value
374+
375+
376+
@about.command(cls=AboutCommand,
339377
short_help='Generate an attribution document from .ABOUT files.')
340378

341379
@click.argument('location',
342380
required=True,
343-
type=click.Path(exists=True, readable=True, resolve_path=True))
381+
metavar='LOCATION',
382+
type=click.Path(
383+
exists=True, file_okay=True, dir_okay=True, readable=True, resolve_path=True))
344384

345385
@click.argument('output',
346386
required=True,
347-
type=click.Path(exists=False, writable=True, dir_okay=False, resolve_path=True))
387+
metavar='OUTPUT',
388+
type=click.Path(exists=False, dir_okay=False, writable=True, resolve_path=True))
348389

349390
@click.option('--template',
350-
metavar='TEMPLATE_FILE_PATH',
391+
metavar='FILE',
392+
callback=validate_template,
351393
type=click.Path(exists=True, dir_okay=False, readable=True, resolve_path=True),
352-
help='Path to an optional custom attribution template to generate the attribution document.')
394+
help='Path to an optional custom attribution template to generate the '
395+
'attribution document. If not provided the default built-in template is used.')
353396

354397
@click.option('--variable',
355398
multiple=True,
@@ -358,19 +401,23 @@ def validate_variables(ctx, param, value):
358401
help='Add variable(s) as key=value for use in a custom attribution template.')
359402

360403
@click.option('--inventory',
404+
metavar='FILE',
361405
type=click.Path(exists=True, dir_okay=False, resolve_path=True),
362-
help='Path to an optional JSON or CSV inventory file listing the '
406+
help='Path to an optional JSON or CSV inventory FILE listing the '
363407
'subset of .ABOUT files paths to consider when generating the attribution document.')
364408

365409
@click.option('--mapping',
366410
is_flag=True,
367-
help='Use the default file mapping.config (./attributecode/mapping.config) '
368-
'with mapping between input keys and ABOUT field names.')
411+
help='Use the default built-in "mapping.config" file '
412+
'with mapping between input keys and .ABOUT field names.'
413+
'Cannot be combined with the --mapping-file option.')
369414

370415
@click.option('--mapping-file',
371416
metavar='FILE',
372-
type=click.Path(exists=True, dir_okay=True, readable=True, resolve_path=True),
373-
help='Use a custom mapping file with mapping between input keys and ABOUT field names.')
417+
type=click.Path(exists=True, dir_okay=False, readable=True, resolve_path=True),
418+
help='Path to an optional custom mapping FILE '
419+
'with mapping between input keys and .ABOUT field names. '
420+
'Cannot be combined with the --mapping option.')
374421

375422
@click.option('-q', '--quiet',
376423
is_flag=True,
@@ -396,21 +443,7 @@ def attrib(location, output, template, variable,
396443
print_version()
397444
click.echo('Generating attribution...')
398445

399-
if mapping and mapping_file:
400-
raise click.UsageError('Invalid combination of options: --mapping and --mapping-file')
401-
if mapping:
402-
mapping_file = DEFAULT_MAPPING
403-
404-
# Check template syntax early
405-
if template:
406-
with io.open(template, encoding='utf-8') as templatef:
407-
template_error = check_template(templatef.read())
408-
if template_error:
409-
lineno, message = template_error
410-
raise click.UsageError(
411-
'Template validation error at line: {lineno}: "{message}"'.format(**locals()))
412-
else:
413-
template = DEFAULT_TEMPLATE_FILE
446+
mapping_file = validate_mapping(mapping, mapping_file)
414447

415448
# accept zipped ABOUT files as input
416449
if location.lower().endswith('.zip'):
@@ -440,13 +473,17 @@ def attrib(location, output, template, variable,
440473
# check subcommand
441474
######################################################################
442475

443-
@cli.command(cls=AboutCommand,
476+
# FIXME: This is really only a dupe of the Inventory command
477+
478+
@about.command(cls=AboutCommand,
444479
short_help='Validate that the format of .ABOUT files is correct and report '
445480
'errors and warnings.')
446481

447482
@click.argument('location',
448483
required=True,
449-
type=click.Path(exists=True, readable=True, resolve_path=True))
484+
metavar='LOCATION',
485+
type=click.Path(
486+
exists=True, file_okay=True, dir_okay=True, readable=True, resolve_path=True))
450487

451488
@click.option('--verbose',
452489
is_flag=True,
@@ -563,4 +600,4 @@ def parse_key_values(key_values):
563600

564601

565602
if __name__ == '__main__':
566-
cli()
603+
about()

tests/testdata/test_cmd/help/about_attrib_help.txt

Lines changed: 18 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -7,19 +7,21 @@ Usage: about attrib [OPTIONS] LOCATION OUTPUT
77
OUTPUT: Path where to write the attribution document.
88

99
Options:
10-
--template TEMPLATE_FILE_PATH Path to an optional custom attribution template
11-
to generate the attribution document.
12-
--variable <key>=<value> Add variable(s) as key=value for use in a
13-
custom attribution template.
14-
--inventory PATH Path to an optional JSON or CSV inventory file
15-
listing the subset of .ABOUT files paths to
16-
consider when generating the attribution
17-
document.
18-
--mapping Use the default file mapping.config
19-
(./attributecode/mapping.config) with mapping
20-
between input keys and ABOUT field names.
21-
--mapping-file FILE Use a custom mapping file with mapping between
22-
input keys and ABOUT field names.
23-
-q, --quiet Do not print error or warning messages.
24-
--verbose Show all error and warning messages.
25-
-h, --help Show this message and exit.
10+
--template FILE Path to an optional custom attribution template to
11+
generate the attribution document. If not provided
12+
the default built-in template is used.
13+
--variable <key>=<value> Add variable(s) as key=value for use in a custom
14+
attribution template.
15+
--inventory FILE Path to an optional JSON or CSV inventory FILE
16+
listing the subset of .ABOUT files paths to consider
17+
when generating the attribution document.
18+
--mapping Use the default built-in "mapping.config" file with
19+
mapping between input keys and .ABOUT field
20+
names.Cannot be combined with the --mapping-file
21+
option.
22+
--mapping-file FILE Path to an optional custom mapping FILE with mapping
23+
between input keys and .ABOUT field names. Cannot be
24+
combined with the --mapping option.
25+
-q, --quiet Do not print error or warning messages.
26+
--verbose Show all error and warning messages.
27+
-h, --help Show this message and exit.
Lines changed: 9 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
Usage: about gen [OPTIONS] LOCATION OUTPUT
22

3-
Generate .ABOUT files in OUTPUT directory from a JSON or CSV inventory of
4-
.ABOUT files at LOCATION.
3+
Generate .ABOUT files in OUTPUT from an inventory of .ABOUT files at
4+
LOCATION.
55

66
LOCATION: Path to a JSON or CSV inventory file.
77

@@ -12,11 +12,13 @@ Options:
1212
License Library API URL using the API KEY.
1313
--reference DIR Path to a directory with reference license data and
1414
text files.
15-
--mapping Use the default file mapping.config
16-
(./attributecode/mapping.config) with mapping between
17-
input keys and ABOUT field names.
18-
--mapping-file FILE Use a custom mapping file with mapping between input
19-
keys and ABOUT field names.
15+
--mapping Use the default built-in "mapping.config" file with
16+
mapping between input keys and .ABOUT field
17+
names.Cannot be combined with the --mapping-file
18+
option.
19+
--mapping-file FILE Path to an optional custom mapping FILE with mapping
20+
between input keys and .ABOUT field names. Cannot be
21+
combined with the --mapping option.
2022
-q, --quiet Do not print error or warning messages.
2123
--verbose Show all error and warning messages.
2224
-h, --help Show this message and exit.

tests/testdata/test_cmd/help/about_help.txt

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -17,4 +17,4 @@ Commands:
1717
check Validate that the format of .ABOUT files is correct and report
1818
errors and warnings.
1919
gen Generate .ABOUT files from an inventory as CSV or JSON.
20-
inventory Collect .ABOUT files and write an inventory as CSV or JSON.
20+
inventory Collect the inventory of .ABOUT files to a CSV or JSON file.

0 commit comments

Comments
 (0)