|
1 #!/usr/bin/env python |
|
2 # -*- coding: utf-8 -*- |
|
3 # vim:fenc=utf-8 ff=unix ft=python ts=4 sw=4 sts=4 si et |
|
4 """ |
|
5 pip-licenses |
|
6 |
|
7 MIT License |
|
8 |
|
9 Copyright (c) 2018 raimon |
|
10 |
|
11 Permission is hereby granted, free of charge, to any person obtaining a copy |
|
12 of this software and associated documentation files (the "Software"), to deal |
|
13 in the Software without restriction, including without limitation the rights |
|
14 to use, copy, modify, merge, publish, distribute, sublicense, and/or sell |
|
15 copies of the Software, and to permit persons to whom the Software is |
|
16 furnished to do so, subject to the following conditions: |
|
17 |
|
18 The above copyright notice and this permission notice shall be included in all |
|
19 copies or substantial portions of the Software. |
|
20 |
|
21 THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR |
|
22 IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, |
|
23 FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE |
|
24 AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER |
|
25 LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, |
|
26 OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE |
|
27 SOFTWARE. |
|
28 """ |
|
29 |
|
30 # |
|
31 # Modified to be used within the eric-ide project. |
|
32 # |
|
33 # Copyright (c) 2022 Detlev Offenbach <detlev@die-offenbachs.de> |
|
34 # |
|
35 |
|
36 import argparse |
|
37 import codecs |
|
38 import glob |
|
39 import json |
|
40 import os |
|
41 import sys |
|
42 from collections import Counter |
|
43 from email import message_from_string |
|
44 from email.parser import FeedParser |
|
45 from enum import Enum, auto |
|
46 from typing import List, Optional, Sequence, Text |
|
47 |
|
48 |
|
49 def get_installed_distributions(local_only=True, user_only=False): |
|
50 try: |
|
51 from pip._internal.metadata import get_environment |
|
52 except ImportError: |
|
53 # For backward compatibility with pip version 20.3.4 |
|
54 from pip._internal.utils import misc |
|
55 return misc.get_installed_distributions( |
|
56 local_only=local_only, |
|
57 user_only=user_only |
|
58 ) |
|
59 else: |
|
60 from pip._internal.utils.compat import stdlib_pkgs |
|
61 dists = get_environment(None).iter_installed_distributions( |
|
62 local_only=local_only, |
|
63 user_only=user_only, |
|
64 skip=stdlib_pkgs, |
|
65 include_editables=True, |
|
66 editables_only=False, |
|
67 ) |
|
68 return [d._dist for d in dists] |
|
69 |
|
70 |
|
71 __pkgname__ = 'pip-licenses' |
|
72 __version__ = '3.5.4' |
|
73 __author__ = 'raimon' |
|
74 __license__ = 'MIT' |
|
75 __summary__ = ('Dump the software license list of ' |
|
76 'Python packages installed with pip.') |
|
77 __url__ = 'https://github.com/raimon49/pip-licenses' |
|
78 |
|
79 |
|
80 FIELD_NAMES = ( |
|
81 'Name', |
|
82 'Version', |
|
83 'License', |
|
84 'LicenseFile', |
|
85 'LicenseText', |
|
86 'NoticeFile', |
|
87 'NoticeText', |
|
88 'Author', |
|
89 'Description', |
|
90 'URL', |
|
91 ) |
|
92 |
|
93 |
|
94 DEFAULT_OUTPUT_FIELDS = ( |
|
95 'Name', |
|
96 'Version', |
|
97 ) |
|
98 |
|
99 |
|
100 SUMMARY_OUTPUT_FIELDS = ( |
|
101 'Count', |
|
102 'License', |
|
103 ) |
|
104 |
|
105 |
|
106 METADATA_KEYS = ( |
|
107 'home-page', |
|
108 'author', |
|
109 'license', |
|
110 'summary', |
|
111 'license_classifier', |
|
112 ) |
|
113 |
|
114 # Mapping of FIELD_NAMES to METADATA_KEYS where they differ by more than case |
|
115 FIELDS_TO_METADATA_KEYS = { |
|
116 'URL': 'home-page', |
|
117 'Description': 'summary', |
|
118 'License-Metadata': 'license', |
|
119 'License-Classifier': 'license_classifier', |
|
120 } |
|
121 |
|
122 |
|
123 SYSTEM_PACKAGES = ( |
|
124 __pkgname__, |
|
125 'pip', |
|
126 'setuptools', |
|
127 'wheel', |
|
128 ) |
|
129 |
|
130 LICENSE_UNKNOWN = 'UNKNOWN' |
|
131 |
|
132 |
|
133 def get_packages(args: "CustomNamespace"): |
|
134 |
|
135 def get_pkg_included_file(pkg, file_names): |
|
136 """ |
|
137 Attempt to find the package's included file on disk and return the |
|
138 tuple (included_file_path, included_file_contents). |
|
139 """ |
|
140 included_file = LICENSE_UNKNOWN |
|
141 included_text = LICENSE_UNKNOWN |
|
142 pkg_dirname = "{}-{}.dist-info".format( |
|
143 pkg.project_name.replace("-", "_"), pkg.version) |
|
144 patterns = [] |
|
145 [patterns.extend(sorted(glob.glob(os.path.join(pkg.location, |
|
146 pkg_dirname, |
|
147 f)))) |
|
148 for f in file_names] |
|
149 for test_file in patterns: |
|
150 if os.path.exists(test_file) and not os.path.isdir(test_file): |
|
151 included_file = test_file |
|
152 with open(test_file, encoding='utf-8', |
|
153 errors='backslashreplace') as included_file_handle: |
|
154 included_text = included_file_handle.read() |
|
155 break |
|
156 return (included_file, included_text) |
|
157 |
|
158 def get_pkg_info(pkg): |
|
159 (license_file, license_text) = get_pkg_included_file( |
|
160 pkg, |
|
161 ('LICENSE*', 'LICENCE*', 'COPYING*') |
|
162 ) |
|
163 (notice_file, notice_text) = get_pkg_included_file( |
|
164 pkg, |
|
165 ('NOTICE*',) |
|
166 ) |
|
167 pkg_info = { |
|
168 'name': pkg.project_name, |
|
169 'version': pkg.version, |
|
170 'namever': str(pkg), |
|
171 'licensefile': license_file, |
|
172 'licensetext': license_text, |
|
173 'noticefile': notice_file, |
|
174 'noticetext': notice_text, |
|
175 } |
|
176 metadata = None |
|
177 if pkg.has_metadata('METADATA'): |
|
178 metadata = pkg.get_metadata('METADATA') |
|
179 |
|
180 if pkg.has_metadata('PKG-INFO') and metadata is None: |
|
181 metadata = pkg.get_metadata('PKG-INFO') |
|
182 |
|
183 if metadata is None: |
|
184 for key in METADATA_KEYS: |
|
185 pkg_info[key] = LICENSE_UNKNOWN |
|
186 |
|
187 return pkg_info |
|
188 |
|
189 feed_parser = FeedParser() |
|
190 feed_parser.feed(metadata) |
|
191 parsed_metadata = feed_parser.close() |
|
192 |
|
193 for key in METADATA_KEYS: |
|
194 pkg_info[key] = parsed_metadata.get(key, LICENSE_UNKNOWN) |
|
195 |
|
196 if metadata is not None: |
|
197 message = message_from_string(metadata) |
|
198 pkg_info['license_classifier'] = \ |
|
199 find_license_from_classifier(message) |
|
200 |
|
201 if args.filter_strings: |
|
202 for k in pkg_info: |
|
203 if isinstance(pkg_info[k], list): |
|
204 for i, item in enumerate(pkg_info[k]): |
|
205 pkg_info[k][i] = item. \ |
|
206 encode(args.filter_code_page, errors="ignore"). \ |
|
207 decode(args.filter_code_page) |
|
208 else: |
|
209 pkg_info[k] = pkg_info[k]. \ |
|
210 encode(args.filter_code_page, errors="ignore"). \ |
|
211 decode(args.filter_code_page) |
|
212 |
|
213 return pkg_info |
|
214 |
|
215 pkgs = get_installed_distributions( |
|
216 local_only=args.local_only, |
|
217 user_only=args.user_only, |
|
218 ) |
|
219 ignore_pkgs_as_lower = [pkg.lower() for pkg in args.ignore_packages] |
|
220 pkgs_as_lower = [pkg.lower() for pkg in args.packages] |
|
221 |
|
222 fail_on_licenses = set() |
|
223 if args.fail_on: |
|
224 fail_on_licenses = set(map(str.strip, args.fail_on.split(";"))) |
|
225 |
|
226 allow_only_licenses = set() |
|
227 if args.allow_only: |
|
228 allow_only_licenses = set(map(str.strip, args.allow_only.split(";"))) |
|
229 |
|
230 for pkg in pkgs: |
|
231 pkg_name = pkg.project_name |
|
232 |
|
233 if pkg_name.lower() in ignore_pkgs_as_lower: |
|
234 continue |
|
235 |
|
236 if pkgs_as_lower and pkg_name.lower() not in pkgs_as_lower: |
|
237 continue |
|
238 |
|
239 if not args.with_system and pkg_name in SYSTEM_PACKAGES: |
|
240 continue |
|
241 |
|
242 pkg_info = get_pkg_info(pkg) |
|
243 |
|
244 license_names = select_license_by_source( |
|
245 args.from_, |
|
246 pkg_info['license_classifier'], |
|
247 pkg_info['license']) |
|
248 |
|
249 if fail_on_licenses: |
|
250 failed_licenses = license_names.intersection(fail_on_licenses) |
|
251 if failed_licenses: |
|
252 sys.stderr.write( |
|
253 "fail-on license {} was found for package " |
|
254 "{}:{}".format( |
|
255 '; '.join(sorted(failed_licenses)), |
|
256 pkg_info['name'], |
|
257 pkg_info['version']) |
|
258 ) |
|
259 sys.exit(1) |
|
260 |
|
261 if allow_only_licenses: |
|
262 uncommon_licenses = license_names.difference(allow_only_licenses) |
|
263 if len(uncommon_licenses) == len(license_names): |
|
264 sys.stderr.write( |
|
265 "license {} not in allow-only licenses was found" |
|
266 " for package {}:{}".format( |
|
267 '; '.join(sorted(uncommon_licenses)), |
|
268 pkg_info['name'], |
|
269 pkg_info['version']) |
|
270 ) |
|
271 sys.exit(1) |
|
272 |
|
273 yield pkg_info |
|
274 |
|
275 |
|
276 def create_licenses_list( |
|
277 args: "CustomNamespace", output_fields=DEFAULT_OUTPUT_FIELDS): |
|
278 |
|
279 licenses = [] |
|
280 for pkg in get_packages(args): |
|
281 row = {} |
|
282 for field in output_fields: |
|
283 if field == 'License': |
|
284 license_set = select_license_by_source( |
|
285 args.from_, pkg['license_classifier'], pkg['license']) |
|
286 license_str = '; '.join(sorted(license_set)) |
|
287 row[field] = license_str |
|
288 elif field == 'License-Classifier': |
|
289 row[field] = ('; '.join(sorted(pkg['license_classifier'])) |
|
290 or LICENSE_UNKNOWN) |
|
291 elif field.lower() in pkg: |
|
292 row[field] = pkg[field.lower()] |
|
293 else: |
|
294 row[field] = pkg[FIELDS_TO_METADATA_KEYS[field]] |
|
295 licenses.append(row) |
|
296 |
|
297 return licenses |
|
298 |
|
299 |
|
300 def create_summary_list(args: "CustomNamespace"): |
|
301 counts = Counter( |
|
302 '; '.join(sorted(select_license_by_source( |
|
303 args.from_, pkg['license_classifier'], pkg['license']))) |
|
304 for pkg in get_packages(args)) |
|
305 |
|
306 licenses = [] |
|
307 for license, count in counts.items(): |
|
308 licenses.append({ |
|
309 "Count": count, |
|
310 "License": license, |
|
311 }) |
|
312 |
|
313 return licenses |
|
314 |
|
315 |
|
316 def find_license_from_classifier(message): |
|
317 licenses = [] |
|
318 for k, v in message.items(): |
|
319 if k == 'Classifier' and v.startswith('License'): |
|
320 license = v.split(' :: ')[-1] |
|
321 |
|
322 # Through the declaration of 'Classifier: License :: OSI Approved' |
|
323 if license != 'OSI Approved': |
|
324 licenses.append(license) |
|
325 |
|
326 return licenses |
|
327 |
|
328 |
|
329 def select_license_by_source(from_source, license_classifier, license_meta): |
|
330 license_classifier_set = set(license_classifier) or {LICENSE_UNKNOWN} |
|
331 if (from_source == FromArg.CLASSIFIER or |
|
332 from_source == FromArg.MIXED and len(license_classifier) > 0): |
|
333 return license_classifier_set |
|
334 else: |
|
335 return {license_meta} |
|
336 |
|
337 |
|
338 def get_output_fields(args: "CustomNamespace"): |
|
339 if args.summary: |
|
340 return list(SUMMARY_OUTPUT_FIELDS) |
|
341 |
|
342 output_fields = list(DEFAULT_OUTPUT_FIELDS) |
|
343 |
|
344 if args.from_ == FromArg.ALL: |
|
345 output_fields.append('License-Metadata') |
|
346 output_fields.append('License-Classifier') |
|
347 else: |
|
348 output_fields.append('License') |
|
349 |
|
350 if args.with_authors: |
|
351 output_fields.append('Author') |
|
352 |
|
353 if args.with_urls: |
|
354 output_fields.append('URL') |
|
355 |
|
356 if args.with_description: |
|
357 output_fields.append('Description') |
|
358 |
|
359 if args.with_license_file: |
|
360 if not args.no_license_path: |
|
361 output_fields.append('LicenseFile') |
|
362 |
|
363 output_fields.append('LicenseText') |
|
364 |
|
365 if args.with_notice_file: |
|
366 output_fields.append('NoticeText') |
|
367 if not args.no_license_path: |
|
368 output_fields.append('NoticeFile') |
|
369 |
|
370 return output_fields |
|
371 |
|
372 |
|
373 def create_output_string(args: "CustomNamespace"): |
|
374 output_fields = get_output_fields(args) |
|
375 |
|
376 if args.summary: |
|
377 licenses = create_summary_list(args) |
|
378 else: |
|
379 licenses = create_licenses_list(args, output_fields) |
|
380 |
|
381 return json.dumps(licenses) |
|
382 |
|
383 |
|
384 class CustomHelpFormatter(argparse.HelpFormatter): # pragma: no cover |
|
385 def __init__( |
|
386 self, prog: Text, indent_increment: int = 2, |
|
387 max_help_position: int = 24, width: Optional[int] = None |
|
388 ) -> None: |
|
389 max_help_position = 30 |
|
390 super().__init__( |
|
391 prog, indent_increment=indent_increment, |
|
392 max_help_position=max_help_position, width=width) |
|
393 |
|
394 def _format_action(self, action: argparse.Action) -> str: |
|
395 flag_indent_argument: bool = False |
|
396 text = self._expand_help(action) |
|
397 separator_pos = text[:3].find('|') |
|
398 if separator_pos != -1 and 'I' in text[:separator_pos]: |
|
399 self._indent() |
|
400 flag_indent_argument = True |
|
401 help_str = super()._format_action(action) |
|
402 if flag_indent_argument: |
|
403 self._dedent() |
|
404 return help_str |
|
405 |
|
406 def _expand_help(self, action: argparse.Action) -> str: |
|
407 if isinstance(action.default, Enum): |
|
408 default_value = enum_key_to_value(action.default) |
|
409 return self._get_help_string(action) % {'default': default_value} |
|
410 return super()._expand_help(action) |
|
411 |
|
412 def _split_lines(self, text: Text, width: int) -> List[str]: |
|
413 separator_pos = text[:3].find('|') |
|
414 if separator_pos != -1: |
|
415 flag_splitlines: bool = 'R' in text[:separator_pos] |
|
416 text = text[separator_pos + 1:] |
|
417 if flag_splitlines: |
|
418 return text.splitlines() |
|
419 return super()._split_lines(text, width) |
|
420 |
|
421 |
|
422 class CustomNamespace(argparse.Namespace): |
|
423 from_: "FromArg" |
|
424 order: "OrderArg" |
|
425 summary: bool |
|
426 local_only: bool |
|
427 user_only:bool |
|
428 output_file: str |
|
429 ignore_packages: List[str] |
|
430 packages: List[str] |
|
431 with_system: bool |
|
432 with_authors: bool |
|
433 with_urls: bool |
|
434 with_description: bool |
|
435 with_license_file: bool |
|
436 no_license_path: bool |
|
437 with_notice_file: bool |
|
438 filter_strings: bool |
|
439 filter_code_page: str |
|
440 fail_on: Optional[str] |
|
441 allow_only: Optional[str] |
|
442 |
|
443 |
|
444 class CompatibleArgumentParser(argparse.ArgumentParser): |
|
445 def parse_args(self, args: Optional[Sequence[Text]] = None, |
|
446 namespace: CustomNamespace = None) -> CustomNamespace: |
|
447 args = super().parse_args(args, namespace) |
|
448 self._verify_args(args) |
|
449 return args |
|
450 |
|
451 def _verify_args(self, args: CustomNamespace): |
|
452 if args.with_license_file is False and ( |
|
453 args.no_license_path is True or |
|
454 args.with_notice_file is True): |
|
455 self.error( |
|
456 "'--no-license-path' and '--with-notice-file' require " |
|
457 "the '--with-license-file' option to be set") |
|
458 if args.filter_strings is False and \ |
|
459 args.filter_code_page != 'latin1': |
|
460 self.error( |
|
461 "'--filter-code-page' requires the '--filter-strings' " |
|
462 "option to be set") |
|
463 try: |
|
464 codecs.lookup(args.filter_code_page) |
|
465 except LookupError: |
|
466 self.error( |
|
467 "invalid code page '%s' given for '--filter-code-page, " |
|
468 "check https://docs.python.org/3/library/codecs.html" |
|
469 "#standard-encodings for valid code pages" |
|
470 % args.filter_code_page) |
|
471 |
|
472 |
|
473 class NoValueEnum(Enum): |
|
474 def __repr__(self): # pragma: no cover |
|
475 return '<%s.%s>' % (self.__class__.__name__, self.name) |
|
476 |
|
477 |
|
478 class FromArg(NoValueEnum): |
|
479 META = M = auto() |
|
480 CLASSIFIER = C = auto() |
|
481 MIXED = MIX = auto() |
|
482 ALL = auto() |
|
483 |
|
484 |
|
485 class OrderArg(NoValueEnum): |
|
486 COUNT = C = auto() |
|
487 LICENSE = L = auto() |
|
488 NAME = N = auto() |
|
489 AUTHOR = A = auto() |
|
490 URL = U = auto() |
|
491 |
|
492 |
|
493 def value_to_enum_key(value: str) -> str: |
|
494 return value.replace('-', '_').upper() |
|
495 |
|
496 |
|
497 def enum_key_to_value(enum_key: Enum) -> str: |
|
498 return enum_key.name.replace('_', '-').lower() |
|
499 |
|
500 |
|
501 def choices_from_enum(enum_cls: NoValueEnum) -> List[str]: |
|
502 return [key.replace('_', '-').lower() |
|
503 for key in enum_cls.__members__.keys()] |
|
504 |
|
505 |
|
506 MAP_DEST_TO_ENUM = { |
|
507 'from_': FromArg, |
|
508 'order': OrderArg, |
|
509 } |
|
510 |
|
511 |
|
512 class SelectAction(argparse.Action): |
|
513 def __call__( |
|
514 self, parser: argparse.ArgumentParser, |
|
515 namespace: argparse.Namespace, |
|
516 values: Text, |
|
517 option_string: Optional[Text] = None, |
|
518 ) -> None: |
|
519 enum_cls = MAP_DEST_TO_ENUM[self.dest] |
|
520 values = value_to_enum_key(values) |
|
521 setattr(namespace, self.dest, getattr(enum_cls, values)) |
|
522 |
|
523 |
|
524 def create_parser(): |
|
525 parser = CompatibleArgumentParser( |
|
526 description=__summary__, |
|
527 formatter_class=CustomHelpFormatter) |
|
528 |
|
529 common_options = parser.add_argument_group('Common options') |
|
530 format_options = parser.add_argument_group('Format options') |
|
531 verify_options = parser.add_argument_group('Verify options') |
|
532 |
|
533 parser.add_argument( |
|
534 '-v', '--version', |
|
535 action='version', |
|
536 version='%(prog)s ' + __version__) |
|
537 |
|
538 common_options.add_argument( |
|
539 '--from', |
|
540 dest='from_', |
|
541 action=SelectAction, type=str, |
|
542 default=FromArg.MIXED, metavar='SOURCE', |
|
543 choices=choices_from_enum(FromArg), |
|
544 help='R|where to find license information\n' |
|
545 '"meta", "classifier, "mixed", "all"\n' |
|
546 '(default: %(default)s)') |
|
547 common_options.add_argument( |
|
548 '-o', '--order', |
|
549 action=SelectAction, type=str, |
|
550 default=OrderArg.NAME, metavar='COL', |
|
551 choices=choices_from_enum(OrderArg), |
|
552 help='R|order by column\n' |
|
553 '"name", "license", "author", "url"\n' |
|
554 '(default: %(default)s)') |
|
555 common_options.add_argument( |
|
556 '--summary', |
|
557 action='store_true', |
|
558 default=False, |
|
559 help='dump summary of each license') |
|
560 common_options.add_argument( |
|
561 '--output-file', |
|
562 action='store', type=str, |
|
563 help='save license list to file') |
|
564 common_options.add_argument( |
|
565 '-i', '--ignore-packages', |
|
566 action='store', type=str, |
|
567 nargs='+', metavar='PKG', |
|
568 default=[], |
|
569 help='ignore package name in dumped list') |
|
570 common_options.add_argument( |
|
571 '-p', '--packages', |
|
572 action='store', type=str, |
|
573 nargs='+', metavar='PKG', |
|
574 default=[], |
|
575 help='only include selected packages in output') |
|
576 common_options.add_argument( |
|
577 '--local-only', |
|
578 action='store_true', |
|
579 default=False, |
|
580 help='include only local packages') |
|
581 common_options.add_argument( |
|
582 '--user-only', |
|
583 action='store_true', |
|
584 default=False, |
|
585 help='include only packages of the user site dir') |
|
586 |
|
587 format_options.add_argument( |
|
588 '-s', '--with-system', |
|
589 action='store_true', |
|
590 default=False, |
|
591 help='dump with system packages') |
|
592 format_options.add_argument( |
|
593 '-a', '--with-authors', |
|
594 action='store_true', |
|
595 default=False, |
|
596 help='dump with package authors') |
|
597 format_options.add_argument( |
|
598 '-u', '--with-urls', |
|
599 action='store_true', |
|
600 default=False, |
|
601 help='dump with package urls') |
|
602 format_options.add_argument( |
|
603 '-d', '--with-description', |
|
604 action='store_true', |
|
605 default=False, |
|
606 help='dump with short package description') |
|
607 format_options.add_argument( |
|
608 '-l', '--with-license-file', |
|
609 action='store_true', |
|
610 default=False, |
|
611 help='dump with location of license file and ' |
|
612 'contents, most useful with JSON output') |
|
613 format_options.add_argument( |
|
614 '--no-license-path', |
|
615 action='store_true', |
|
616 default=False, |
|
617 help='I|when specified together with option -l, ' |
|
618 'suppress location of license file output') |
|
619 format_options.add_argument( |
|
620 '--with-notice-file', |
|
621 action='store_true', |
|
622 default=False, |
|
623 help='I|when specified together with option -l, ' |
|
624 'dump with location of license file and contents') |
|
625 format_options.add_argument( |
|
626 '--filter-strings', |
|
627 action="store_true", |
|
628 default=False, |
|
629 help='filter input according to code page') |
|
630 format_options.add_argument( |
|
631 '--filter-code-page', |
|
632 action="store", type=str, |
|
633 default="latin1", |
|
634 metavar="CODE", |
|
635 help='I|specify code page for filtering ' |
|
636 '(default: %(default)s)') |
|
637 |
|
638 verify_options.add_argument( |
|
639 '--fail-on', |
|
640 action='store', type=str, |
|
641 default=None, |
|
642 help='fail (exit with code 1) on the first occurrence ' |
|
643 'of the licenses of the semicolon-separated list') |
|
644 verify_options.add_argument( |
|
645 '--allow-only', |
|
646 action='store', type=str, |
|
647 default=None, |
|
648 help='fail (exit with code 1) on the first occurrence ' |
|
649 'of the licenses not in the semicolon-separated list') |
|
650 |
|
651 return parser |
|
652 |
|
653 |
|
654 def main(): # pragma: no cover |
|
655 parser = create_parser() |
|
656 args = parser.parse_args() |
|
657 |
|
658 output_string = create_output_string(args) |
|
659 |
|
660 print(output_string) |
|
661 |
|
662 |
|
663 if __name__ == '__main__': # pragma: no cover |
|
664 main() |