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""" 5pip-licenses 6 7MIT License 8 9Copyright (c) 2018 raimon 10 11Permission is hereby granted, free of charge, to any person obtaining a copy 12of this software and associated documentation files (the "Software"), to deal 13in the Software without restriction, including without limitation the rights 14to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 15copies of the Software, and to permit persons to whom the Software is 16furnished to do so, subject to the following conditions: 17 18The above copyright notice and this permission notice shall be included in all 19copies or substantial portions of the Software. 20 21THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 22IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 23FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 24AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 25LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 26OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 27SOFTWARE. 28""" 29import argparse 30import codecs 31import glob 32import os 33import sys 34from collections import Counter 35from email import message_from_string 36from email.parser import FeedParser 37from enum import Enum, auto 38from functools import partial 39from typing import List, Optional, Sequence, Text 40 41try: 42 from pip._internal.utils.misc import get_installed_distributions 43except ImportError: # pragma: no cover 44 try: 45 from pip import get_installed_distributions 46 except ImportError: 47 def get_installed_distributions(): 48 from pip._internal.metadata import ( 49 get_default_environment, get_environment, 50 ) 51 from pip._internal.metadata.pkg_resources import ( 52 Distribution as _Dist, 53 ) 54 from pip._internal.utils.compat import stdlib_pkgs 55 56 env = get_default_environment() 57 dists = env.iter_installed_distributions( 58 local_only=True, 59 skip=stdlib_pkgs, 60 include_editables=True, 61 editables_only=False, 62 user_only=False, 63 ) 64 return [dist._dist for dist in dists] 65 66from prettytable import PrettyTable 67 68try: 69 from prettytable.prettytable import ALL as RULE_ALL 70 from prettytable.prettytable import FRAME as RULE_FRAME 71 from prettytable.prettytable import HEADER as RULE_HEADER 72 from prettytable.prettytable import NONE as RULE_NONE 73 PTABLE = True 74except ImportError: # pragma: no cover 75 from prettytable import ALL as RULE_ALL 76 from prettytable import FRAME as RULE_FRAME 77 from prettytable import HEADER as RULE_HEADER 78 from prettytable import NONE as RULE_NONE 79 PTABLE = False 80 81open = open # allow monkey patching 82 83__pkgname__ = 'pip-licenses' 84__version__ = '3.5.3' 85__author__ = 'raimon' 86__license__ = 'MIT' 87__summary__ = ('Dump the software license list of ' 88 'Python packages installed with pip.') 89__url__ = 'https://github.com/raimon49/pip-licenses' 90 91 92FIELD_NAMES = ( 93 'Name', 94 'Version', 95 'License', 96 'LicenseFile', 97 'LicenseText', 98 'NoticeFile', 99 'NoticeText', 100 'Author', 101 'Description', 102 'URL', 103) 104 105 106SUMMARY_FIELD_NAMES = ( 107 'Count', 108 'License', 109) 110 111 112DEFAULT_OUTPUT_FIELDS = ( 113 'Name', 114 'Version', 115) 116 117 118SUMMARY_OUTPUT_FIELDS = ( 119 'Count', 120 'License', 121) 122 123 124METADATA_KEYS = ( 125 'home-page', 126 'author', 127 'license', 128 'summary', 129 'license_classifier', 130) 131 132# Mapping of FIELD_NAMES to METADATA_KEYS where they differ by more than case 133FIELDS_TO_METADATA_KEYS = { 134 'URL': 'home-page', 135 'Description': 'summary', 136 'License-Metadata': 'license', 137 'License-Classifier': 'license_classifier', 138} 139 140 141SYSTEM_PACKAGES = ( 142 __pkgname__, 143 'pip', 144 'PTable' if PTABLE else 'prettytable', 145 'setuptools', 146 'wheel', 147) 148 149LICENSE_UNKNOWN = 'UNKNOWN' 150 151 152def get_packages(args: "CustomNamespace"): 153 154 def get_pkg_included_file(pkg, file_names): 155 """ 156 Attempt to find the package's included file on disk and return the 157 tuple (included_file_path, included_file_contents). 158 """ 159 included_file = LICENSE_UNKNOWN 160 included_text = LICENSE_UNKNOWN 161 pkg_dirname = "{}-{}.dist-info".format( 162 pkg.project_name.replace("-", "_"), pkg.version) 163 patterns = [] 164 [patterns.extend(sorted(glob.glob(os.path.join(pkg.location, 165 pkg_dirname, 166 f)))) 167 for f in file_names] 168 for test_file in patterns: 169 if os.path.exists(test_file): 170 included_file = test_file 171 with open(test_file, encoding='utf-8', 172 errors='backslashreplace') as included_file_handle: 173 included_text = included_file_handle.read() 174 break 175 return (included_file, included_text) 176 177 def get_pkg_info(pkg): 178 (license_file, license_text) = get_pkg_included_file( 179 pkg, 180 ('LICENSE*', 'LICENCE*', 'COPYING*') 181 ) 182 (notice_file, notice_text) = get_pkg_included_file( 183 pkg, 184 ('NOTICE*',) 185 ) 186 pkg_info = { 187 'name': pkg.project_name, 188 'version': pkg.version, 189 'namever': str(pkg), 190 'licensefile': license_file, 191 'licensetext': license_text, 192 'noticefile': notice_file, 193 'noticetext': notice_text, 194 } 195 metadata = None 196 if pkg.has_metadata('METADATA'): 197 metadata = pkg.get_metadata('METADATA') 198 199 if pkg.has_metadata('PKG-INFO') and metadata is None: 200 metadata = pkg.get_metadata('PKG-INFO') 201 202 if metadata is None: 203 for key in METADATA_KEYS: 204 pkg_info[key] = LICENSE_UNKNOWN 205 206 return pkg_info 207 208 feed_parser = FeedParser() 209 feed_parser.feed(metadata) 210 parsed_metadata = feed_parser.close() 211 212 for key in METADATA_KEYS: 213 pkg_info[key] = parsed_metadata.get(key, LICENSE_UNKNOWN) 214 215 if metadata is not None: 216 message = message_from_string(metadata) 217 pkg_info['license_classifier'] = \ 218 find_license_from_classifier(message) 219 220 if args.filter_strings: 221 for k in pkg_info: 222 if isinstance(pkg_info[k], list): 223 for i, item in enumerate(pkg_info[k]): 224 pkg_info[k][i] = item. \ 225 encode(args.filter_code_page, errors="ignore"). \ 226 decode(args.filter_code_page) 227 else: 228 pkg_info[k] = pkg_info[k]. \ 229 encode(args.filter_code_page, errors="ignore"). \ 230 decode(args.filter_code_page) 231 232 return pkg_info 233 234 pkgs = get_installed_distributions() 235 ignore_pkgs_as_lower = [pkg.lower() for pkg in args.ignore_packages] 236 pkgs_as_lower = [pkg.lower() for pkg in args.packages] 237 238 fail_on_licenses = set() 239 if args.fail_on: 240 fail_on_licenses = set(map(str.strip, args.fail_on.split(";"))) 241 242 allow_only_licenses = set() 243 if args.allow_only: 244 allow_only_licenses = set(map(str.strip, args.allow_only.split(";"))) 245 246 for pkg in pkgs: 247 pkg_name = pkg.project_name 248 249 if pkg_name.lower() in ignore_pkgs_as_lower: 250 continue 251 252 if pkgs_as_lower and pkg_name.lower() not in pkgs_as_lower: 253 continue 254 255 if not args.with_system and pkg_name in SYSTEM_PACKAGES: 256 continue 257 258 pkg_info = get_pkg_info(pkg) 259 260 license_names = select_license_by_source( 261 args.from_, 262 pkg_info['license_classifier'], 263 pkg_info['license']) 264 265 if fail_on_licenses: 266 failed_licenses = license_names.intersection(fail_on_licenses) 267 if failed_licenses: 268 sys.stderr.write( 269 "fail-on license {} was found for package " 270 "{}:{}".format( 271 '; '.join(sorted(failed_licenses)), 272 pkg_info['name'], 273 pkg_info['version']) 274 ) 275 sys.exit(1) 276 277 if allow_only_licenses: 278 uncommon_licenses = license_names.difference(allow_only_licenses) 279 if len(uncommon_licenses) == len(license_names): 280 sys.stderr.write( 281 "license {} not in allow-only licenses was found" 282 " for package {}:{}".format( 283 '; '.join(sorted(uncommon_licenses)), 284 pkg_info['name'], 285 pkg_info['version']) 286 ) 287 sys.exit(1) 288 289 yield pkg_info 290 291 292def create_licenses_table( 293 args: "CustomNamespace", output_fields=DEFAULT_OUTPUT_FIELDS): 294 table = factory_styled_table_with_args(args, output_fields) 295 296 for pkg in get_packages(args): 297 row = [] 298 for field in output_fields: 299 if field == 'License': 300 license_set = select_license_by_source( 301 args.from_, pkg['license_classifier'], pkg['license']) 302 license_str = '; '.join(sorted(license_set)) 303 row.append(license_str) 304 elif field == 'License-Classifier': 305 row.append('; '.join(sorted(pkg['license_classifier'])) 306 or LICENSE_UNKNOWN) 307 elif field.lower() in pkg: 308 row.append(pkg[field.lower()]) 309 else: 310 row.append(pkg[FIELDS_TO_METADATA_KEYS[field]]) 311 table.add_row(row) 312 313 return table 314 315 316def create_summary_table(args: "CustomNamespace"): 317 counts = Counter( 318 '; '.join(sorted(select_license_by_source( 319 args.from_, pkg['license_classifier'], pkg['license']))) 320 for pkg in get_packages(args)) 321 322 table = factory_styled_table_with_args(args, SUMMARY_FIELD_NAMES) 323 for license, count in counts.items(): 324 table.add_row([count, license]) 325 return table 326 327 328class JsonPrettyTable(PrettyTable): 329 """PrettyTable-like class exporting to JSON""" 330 331 def _format_row(self, row, options): 332 resrow = {} 333 for (field, value) in zip(self._field_names, row): 334 if field not in options["fields"]: 335 continue 336 337 resrow[field] = value 338 339 return resrow 340 341 def get_string(self, **kwargs): 342 # import included here in order to limit dependencies 343 # if not interested in JSON output, 344 # then the dependency is not required 345 import json 346 347 options = self._get_options(kwargs) 348 rows = self._get_rows(options) 349 formatted_rows = self._format_rows(rows, options) 350 351 lines = [] 352 for row in formatted_rows: 353 lines.append(row) 354 355 return json.dumps(lines, indent=2, sort_keys=True) 356 357 358class JsonLicenseFinderTable(JsonPrettyTable): 359 def _format_row(self, row, options): 360 resrow = {} 361 for (field, value) in zip(self._field_names, row): 362 if field == 'Name': 363 resrow['name'] = value 364 365 if field == 'Version': 366 resrow['version'] = value 367 368 if field == 'License': 369 resrow['licenses'] = [value] 370 371 return resrow 372 373 def get_string(self, **kwargs): 374 # import included here in order to limit dependencies 375 # if not interested in JSON output, 376 # then the dependency is not required 377 import json 378 379 options = self._get_options(kwargs) 380 rows = self._get_rows(options) 381 formatted_rows = self._format_rows(rows, options) 382 383 lines = [] 384 for row in formatted_rows: 385 lines.append(row) 386 387 return json.dumps(lines, sort_keys=True) 388 389 390class CSVPrettyTable(PrettyTable): 391 """PrettyTable-like class exporting to CSV""" 392 393 def get_string(self, **kwargs): 394 395 def esc_quotes(val): 396 """ 397 Meta-escaping double quotes 398 https://tools.ietf.org/html/rfc4180 399 """ 400 try: 401 return val.replace('"', '""') 402 except UnicodeDecodeError: # pragma: no cover 403 return val.decode('utf-8').replace('"', '""') 404 except UnicodeEncodeError: # pragma: no cover 405 return val.encode('unicode_escape').replace('"', '""') 406 407 options = self._get_options(kwargs) 408 rows = self._get_rows(options) 409 formatted_rows = self._format_rows(rows, options) 410 411 lines = [] 412 formatted_header = ','.join(['"%s"' % (esc_quotes(val), ) 413 for val in self._field_names]) 414 lines.append(formatted_header) 415 for row in formatted_rows: 416 formatted_row = ','.join(['"%s"' % (esc_quotes(val), ) 417 for val in row]) 418 lines.append(formatted_row) 419 420 return '\n'.join(lines) 421 422 423class PlainVerticalTable(PrettyTable): 424 """PrettyTable for outputting to a simple non-column based style. 425 426 When used with --with-license-file, this style is similar to the default 427 style generated from Angular CLI's --extractLicenses flag. 428 """ 429 430 def get_string(self, **kwargs): 431 options = self._get_options(kwargs) 432 rows = self._get_rows(options) 433 434 output = '' 435 for row in rows: 436 for v in row: 437 output += '{}\n'.format(v) 438 output += '\n' 439 440 return output 441 442 443def factory_styled_table_with_args( 444 args: "CustomNamespace", output_fields=DEFAULT_OUTPUT_FIELDS): 445 table = PrettyTable() 446 table.field_names = output_fields 447 table.align = 'l' 448 table.border = args.format_ in (FormatArg.MARKDOWN, FormatArg.RST, 449 FormatArg.CONFLUENCE, FormatArg.JSON) 450 table.header = True 451 452 if args.format_ == FormatArg.MARKDOWN: 453 table.junction_char = '|' 454 table.hrules = RULE_HEADER 455 elif args.format_ == FormatArg.RST: 456 table.junction_char = '+' 457 table.hrules = RULE_ALL 458 elif args.format_ == FormatArg.CONFLUENCE: 459 table.junction_char = '|' 460 table.hrules = RULE_NONE 461 elif args.format_ == FormatArg.JSON: 462 table = JsonPrettyTable(table.field_names) 463 elif args.format_ == FormatArg.JSON_LICENSE_FINDER: 464 table = JsonLicenseFinderTable(table.field_names) 465 elif args.format_ == FormatArg.CSV: 466 table = CSVPrettyTable(table.field_names) 467 elif args.format_ == FormatArg.PLAIN_VERTICAL: 468 table = PlainVerticalTable(table.field_names) 469 470 return table 471 472 473def find_license_from_classifier(message): 474 licenses = [] 475 for k, v in message.items(): 476 if k == 'Classifier' and v.startswith('License'): 477 license = v.split(' :: ')[-1] 478 479 # Through the declaration of 'Classifier: License :: OSI Approved' 480 if license != 'OSI Approved': 481 licenses.append(license) 482 483 return licenses 484 485 486def select_license_by_source(from_source, license_classifier, license_meta): 487 license_classifier_set = set(license_classifier) or {LICENSE_UNKNOWN} 488 if (from_source == FromArg.CLASSIFIER or 489 from_source == FromArg.MIXED and len(license_classifier) > 0): 490 return license_classifier_set 491 else: 492 return {license_meta} 493 494 495def get_output_fields(args: "CustomNamespace"): 496 if args.summary: 497 return list(SUMMARY_OUTPUT_FIELDS) 498 499 output_fields = list(DEFAULT_OUTPUT_FIELDS) 500 501 if args.from_ == FromArg.ALL: 502 output_fields.append('License-Metadata') 503 output_fields.append('License-Classifier') 504 else: 505 output_fields.append('License') 506 507 if args.with_authors: 508 output_fields.append('Author') 509 510 if args.with_urls: 511 output_fields.append('URL') 512 513 if args.with_description: 514 output_fields.append('Description') 515 516 if args.with_license_file: 517 if not args.no_license_path: 518 output_fields.append('LicenseFile') 519 520 output_fields.append('LicenseText') 521 522 if args.with_notice_file: 523 output_fields.append('NoticeText') 524 if not args.no_license_path: 525 output_fields.append('NoticeFile') 526 527 return output_fields 528 529 530def get_sortby(args: "CustomNamespace"): 531 if args.summary and args.order == OrderArg.COUNT: 532 return 'Count' 533 elif args.summary or args.order == OrderArg.LICENSE: 534 return 'License' 535 elif args.order == OrderArg.NAME: 536 return 'Name' 537 elif args.order == OrderArg.AUTHOR and args.with_authors: 538 return 'Author' 539 elif args.order == OrderArg.URL and args.with_urls: 540 return 'URL' 541 542 return 'Name' 543 544 545def create_output_string(args: "CustomNamespace"): 546 output_fields = get_output_fields(args) 547 548 if args.summary: 549 table = create_summary_table(args) 550 else: 551 table = create_licenses_table(args, output_fields) 552 553 sortby = get_sortby(args) 554 555 if args.format_ == FormatArg.HTML: 556 return table.get_html_string(fields=output_fields, sortby=sortby) 557 else: 558 return table.get_string(fields=output_fields, sortby=sortby) 559 560 561def create_warn_string(args: "CustomNamespace"): 562 warn_messages = [] 563 warn = partial(output_colored, '33') 564 565 if args.with_license_file and not args.format_ == FormatArg.JSON: 566 message = warn(('Due to the length of these fields, this option is ' 567 'best paired with --format=json.')) 568 warn_messages.append(message) 569 570 if args.summary and (args.with_authors or args.with_urls): 571 message = warn(('When using this option, only --order=count or ' 572 '--order=license has an effect for the --order ' 573 'option. And using --with-authors and --with-urls ' 574 'will be ignored.')) 575 warn_messages.append(message) 576 577 return '\n'.join(warn_messages) 578 579 580class CustomHelpFormatter(argparse.HelpFormatter): # pragma: no cover 581 def __init__( 582 self, prog: Text, indent_increment: int = 2, 583 max_help_position: int = 24, width: Optional[int] = None 584 ) -> None: 585 max_help_position = 30 586 super().__init__( 587 prog, indent_increment=indent_increment, 588 max_help_position=max_help_position, width=width) 589 590 def _format_action(self, action: argparse.Action) -> str: 591 flag_indent_argument: bool = False 592 text = self._expand_help(action) 593 separator_pos = text[:3].find('|') 594 if separator_pos != -1 and 'I' in text[:separator_pos]: 595 self._indent() 596 flag_indent_argument = True 597 help_str = super()._format_action(action) 598 if flag_indent_argument: 599 self._dedent() 600 return help_str 601 602 def _expand_help(self, action: argparse.Action) -> str: 603 if isinstance(action.default, Enum): 604 default_value = enum_key_to_value(action.default) 605 return self._get_help_string(action) % {'default': default_value} 606 return super()._expand_help(action) 607 608 def _split_lines(self, text: Text, width: int) -> List[str]: 609 separator_pos = text[:3].find('|') 610 if separator_pos != -1: 611 flag_splitlines: bool = 'R' in text[:separator_pos] 612 text = text[separator_pos + 1:] 613 if flag_splitlines: 614 return text.splitlines() 615 return super()._split_lines(text, width) 616 617 618class CustomNamespace(argparse.Namespace): 619 from_: "FromArg" 620 order: "OrderArg" 621 format_: "FormatArg" 622 summary: bool 623 output_file: str 624 ignore_packages: List[str] 625 packages: List[str] 626 with_system: bool 627 with_authors: bool 628 with_urls: bool 629 with_description: bool 630 with_license_file: bool 631 no_license_path: bool 632 with_notice_file: bool 633 filter_strings: bool 634 filter_code_page: str 635 fail_on: Optional[str] 636 allow_only: Optional[str] 637 638 639class CompatibleArgumentParser(argparse.ArgumentParser): 640 def parse_args(self, args: Optional[Sequence[Text]] = None, 641 namespace: CustomNamespace = None) -> CustomNamespace: 642 args = super().parse_args(args, namespace) 643 self._verify_args(args) 644 return args 645 646 def _verify_args(self, args: CustomNamespace): 647 if args.with_license_file is False and ( 648 args.no_license_path is True or 649 args.with_notice_file is True): 650 self.error( 651 "'--no-license-path' and '--with-notice-file' require " 652 "the '--with-license-file' option to be set") 653 if args.filter_strings is False and \ 654 args.filter_code_page != 'latin1': 655 self.error( 656 "'--filter-code-page' requires the '--filter-strings' " 657 "option to be set") 658 try: 659 codecs.lookup(args.filter_code_page) 660 except LookupError: 661 self.error( 662 "invalid code page '%s' given for '--filter-code-page, " 663 "check https://docs.python.org/3/library/codecs.html" 664 "#standard-encodings for valid code pages" 665 % args.filter_code_page) 666 667 668class NoValueEnum(Enum): 669 def __repr__(self): # pragma: no cover 670 return '<%s.%s>' % (self.__class__.__name__, self.name) 671 672 673class FromArg(NoValueEnum): 674 META = M = auto() 675 CLASSIFIER = C = auto() 676 MIXED = MIX = auto() 677 ALL = auto() 678 679 680class OrderArg(NoValueEnum): 681 COUNT = C = auto() 682 LICENSE = L = auto() 683 NAME = N = auto() 684 AUTHOR = A = auto() 685 URL = U = auto() 686 687 688class FormatArg(NoValueEnum): 689 PLAIN = P = auto() 690 PLAIN_VERTICAL = auto() 691 MARKDOWN = MD = M = auto() 692 RST = REST = R = auto() 693 CONFLUENCE = C = auto() 694 HTML = H = auto() 695 JSON = J = auto() 696 JSON_LICENSE_FINDER = JLF = auto() 697 CSV = auto() 698 699 700def value_to_enum_key(value: str) -> str: 701 return value.replace('-', '_').upper() 702 703 704def enum_key_to_value(enum_key: Enum) -> str: 705 return enum_key.name.replace('_', '-').lower() 706 707 708def choices_from_enum(enum_cls: NoValueEnum) -> List[str]: 709 return [key.replace('_', '-').lower() 710 for key in enum_cls.__members__.keys()] 711 712 713MAP_DEST_TO_ENUM = { 714 'from_': FromArg, 715 'order': OrderArg, 716 'format_': FormatArg, 717} 718 719 720class SelectAction(argparse.Action): 721 def __call__( 722 self, parser: argparse.ArgumentParser, 723 namespace: argparse.Namespace, 724 values: Text, 725 option_string: Optional[Text] = None, 726 ) -> None: 727 enum_cls = MAP_DEST_TO_ENUM[self.dest] 728 values = value_to_enum_key(values) 729 setattr(namespace, self.dest, getattr(enum_cls, values)) 730 731 732def create_parser(): 733 parser = CompatibleArgumentParser( 734 description=__summary__, 735 formatter_class=CustomHelpFormatter) 736 737 common_options = parser.add_argument_group('Common options') 738 format_options = parser.add_argument_group('Format options') 739 verify_options = parser.add_argument_group('Verify options') 740 741 parser.add_argument( 742 '-v', '--version', 743 action='version', 744 version='%(prog)s ' + __version__) 745 746 common_options.add_argument( 747 '--from', 748 dest='from_', 749 action=SelectAction, type=str, 750 default=FromArg.MIXED, metavar='SOURCE', 751 choices=choices_from_enum(FromArg), 752 help='R|where to find license information\n' 753 '"meta", "classifier, "mixed", "all"\n' 754 '(default: %(default)s)') 755 common_options.add_argument( 756 '-o', '--order', 757 action=SelectAction, type=str, 758 default=OrderArg.NAME, metavar='COL', 759 choices=choices_from_enum(OrderArg), 760 help='R|order by column\n' 761 '"name", "license", "author", "url"\n' 762 '(default: %(default)s)') 763 common_options.add_argument( 764 '-f', '--format', 765 dest='format_', 766 action=SelectAction, type=str, 767 default=FormatArg.PLAIN, metavar='STYLE', 768 choices=choices_from_enum(FormatArg), 769 help='R|dump as set format style\n' 770 '"plain", "plain-vertical" "markdown", "rst", \n' 771 '"confluence", "html", "json", \n' 772 '"json-license-finder", "csv"\n' 773 '(default: %(default)s)') 774 common_options.add_argument( 775 '--summary', 776 action='store_true', 777 default=False, 778 help='dump summary of each license') 779 common_options.add_argument( 780 '--output-file', 781 action='store', type=str, 782 help='save license list to file') 783 common_options.add_argument( 784 '-i', '--ignore-packages', 785 action='store', type=str, 786 nargs='+', metavar='PKG', 787 default=[], 788 help='ignore package name in dumped list') 789 common_options.add_argument( 790 '-p', '--packages', 791 action='store', type=str, 792 nargs='+', metavar='PKG', 793 default=[], 794 help='only include selected packages in output') 795 format_options.add_argument( 796 '-s', '--with-system', 797 action='store_true', 798 default=False, 799 help='dump with system packages') 800 format_options.add_argument( 801 '-a', '--with-authors', 802 action='store_true', 803 default=False, 804 help='dump with package authors') 805 format_options.add_argument( 806 '-u', '--with-urls', 807 action='store_true', 808 default=False, 809 help='dump with package urls') 810 format_options.add_argument( 811 '-d', '--with-description', 812 action='store_true', 813 default=False, 814 help='dump with short package description') 815 format_options.add_argument( 816 '-l', '--with-license-file', 817 action='store_true', 818 default=False, 819 help='dump with location of license file and ' 820 'contents, most useful with JSON output') 821 format_options.add_argument( 822 '--no-license-path', 823 action='store_true', 824 default=False, 825 help='I|when specified together with option -l, ' 826 'suppress location of license file output') 827 format_options.add_argument( 828 '--with-notice-file', 829 action='store_true', 830 default=False, 831 help='I|when specified together with option -l, ' 832 'dump with location of license file and contents') 833 format_options.add_argument( 834 '--filter-strings', 835 action="store_true", 836 default=False, 837 help='filter input according to code page') 838 format_options.add_argument( 839 '--filter-code-page', 840 action="store", type=str, 841 default="latin1", 842 metavar="CODE", 843 help='I|specify code page for filtering ' 844 '(default: %(default)s)') 845 846 verify_options.add_argument( 847 '--fail-on', 848 action='store', type=str, 849 default=None, 850 help='fail (exit with code 1) on the first occurrence ' 851 'of the licenses of the semicolon-separated list') 852 verify_options.add_argument( 853 '--allow-only', 854 action='store', type=str, 855 default=None, 856 help='fail (exit with code 1) on the first occurrence ' 857 'of the licenses not in the semicolon-separated list') 858 859 return parser 860 861 862def output_colored(code, text, is_bold=False): 863 """ 864 Create function to output with color sequence 865 """ 866 if is_bold: 867 code = '1;%s' % code 868 869 return '\033[%sm%s\033[0m' % (code, text) 870 871 872def save_if_needs(output_file, output_string): 873 """ 874 Save to path given by args 875 """ 876 if output_file is None: 877 return 878 879 try: 880 with open(output_file, 'w', encoding='utf-8') as f: 881 f.write(output_string) 882 sys.stdout.write('created path: ' + output_file + '\n') 883 sys.exit(0) 884 except IOError: 885 sys.stderr.write('check path: --output-file\n') 886 sys.exit(1) 887 888 889def main(): # pragma: no cover 890 parser = create_parser() 891 args = parser.parse_args() 892 893 output_string = create_output_string(args) 894 895 output_file = args.output_file 896 save_if_needs(output_file, output_string) 897 898 print(output_string) 899 warn_string = create_warn_string(args) 900 if warn_string: 901 print(warn_string, file=sys.stderr) 902 903 904if __name__ == '__main__': # pragma: no cover 905 main() 906