eric7/PipInterface/piplicenses.py

branch
eric7
changeset 9003
6bc210cd5726
parent 9002
31a7decd3393
child 9098
fb9351497cea
equal deleted inserted replaced
9002:31a7decd3393 9003:6bc210cd5726
24 AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 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, 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 26 OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
27 SOFTWARE. 27 SOFTWARE.
28 """ 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
29 import argparse 36 import argparse
30 import codecs 37 import codecs
31 import glob 38 import glob
39 import json
32 import os 40 import os
33 import sys 41 import sys
34 from collections import Counter 42 from collections import Counter
35 from email import message_from_string 43 from email import message_from_string
36 from email.parser import FeedParser 44 from email.parser import FeedParser
37 from enum import Enum, auto 45 from enum import Enum, auto
38 from functools import partial
39 from typing import List, Optional, Sequence, Text 46 from typing import List, Optional, Sequence, Text
40 47
41 try: 48
42 from pip._internal.utils.misc import get_installed_distributions 49 def get_installed_distributions(local_only=True, user_only=False):
43 except ImportError: # pragma: no cover
44 try: 50 try:
45 from pip import get_installed_distributions 51 from pip._internal.metadata import get_environment
46 except ImportError: 52 except ImportError:
47 def get_installed_distributions(): 53 # For backward compatibility with pip version 20.3.4
48 from pip._internal.metadata import ( 54 from pip._internal.utils import misc
49 get_default_environment, get_environment, 55 return misc.get_installed_distributions(
50 ) 56 local_only=local_only,
51 from pip._internal.metadata.pkg_resources import ( 57 user_only=user_only
52 Distribution as _Dist, 58 )
53 ) 59 else:
54 from pip._internal.utils.compat import stdlib_pkgs 60 from pip._internal.utils.compat import stdlib_pkgs
55 61 dists = get_environment(None).iter_installed_distributions(
56 env = get_default_environment() 62 local_only=local_only,
57 dists = env.iter_installed_distributions( 63 user_only=user_only,
58 local_only=True, 64 skip=stdlib_pkgs,
59 skip=stdlib_pkgs, 65 include_editables=True,
60 include_editables=True, 66 editables_only=False,
61 editables_only=False, 67 )
62 user_only=False, 68 return [d._dist for d in dists]
63 ) 69
64 return [dist._dist for dist in dists]
65
66 from prettytable import PrettyTable
67
68 open = open # allow monkey patching
69 70
70 __pkgname__ = 'pip-licenses' 71 __pkgname__ = 'pip-licenses'
71 __version__ = '3.5.3' 72 __version__ = '3.5.3'
72 __author__ = 'raimon' 73 __author__ = 'raimon'
73 __license__ = 'MIT' 74 __license__ = 'MIT'
85 'NoticeFile', 86 'NoticeFile',
86 'NoticeText', 87 'NoticeText',
87 'Author', 88 'Author',
88 'Description', 89 'Description',
89 'URL', 90 'URL',
90 )
91
92
93 SUMMARY_FIELD_NAMES = (
94 'Count',
95 'License',
96 ) 91 )
97 92
98 93
99 DEFAULT_OUTPUT_FIELDS = ( 94 DEFAULT_OUTPUT_FIELDS = (
100 'Name', 95 'Name',
215 encode(args.filter_code_page, errors="ignore"). \ 210 encode(args.filter_code_page, errors="ignore"). \
216 decode(args.filter_code_page) 211 decode(args.filter_code_page)
217 212
218 return pkg_info 213 return pkg_info
219 214
220 pkgs = get_installed_distributions() 215 pkgs = get_installed_distributions(
216 local_only=args.local_only,
217 user_only=args.user_only,
218 )
221 ignore_pkgs_as_lower = [pkg.lower() for pkg in args.ignore_packages] 219 ignore_pkgs_as_lower = [pkg.lower() for pkg in args.ignore_packages]
222 pkgs_as_lower = [pkg.lower() for pkg in args.packages] 220 pkgs_as_lower = [pkg.lower() for pkg in args.packages]
223 221
224 fail_on_licenses = set() 222 fail_on_licenses = set()
225 if args.fail_on: 223 if args.fail_on:
285 if field == 'License': 283 if field == 'License':
286 license_set = select_license_by_source( 284 license_set = select_license_by_source(
287 args.from_, pkg['license_classifier'], pkg['license']) 285 args.from_, pkg['license_classifier'], pkg['license'])
288 license_str = '; '.join(sorted(license_set)) 286 license_str = '; '.join(sorted(license_set))
289 row[field] = license_str 287 row[field] = license_str
290 ## row.append(license_str)
291 elif field == 'License-Classifier': 288 elif field == 'License-Classifier':
292 row[field] = ('; '.join(sorted(pkg['license_classifier'])) 289 row[field] = ('; '.join(sorted(pkg['license_classifier']))
293 or LICENSE_UNKNOWN) 290 or LICENSE_UNKNOWN)
294 elif field.lower() in pkg: 291 elif field.lower() in pkg:
295 row[field] = pkg[field.lower()] 292 row[field] = pkg[field.lower()]
298 licenses.append(row) 295 licenses.append(row)
299 296
300 return licenses 297 return licenses
301 298
302 299
303 def create_summary_table(args: "CustomNamespace"): 300 def create_summary_list(args: "CustomNamespace"):
304 counts = Counter( 301 counts = Counter(
305 '; '.join(sorted(select_license_by_source( 302 '; '.join(sorted(select_license_by_source(
306 args.from_, pkg['license_classifier'], pkg['license']))) 303 args.from_, pkg['license_classifier'], pkg['license'])))
307 for pkg in get_packages(args)) 304 for pkg in get_packages(args))
308 305
309 table = factory_styled_table_with_args(args, SUMMARY_FIELD_NAMES) 306 licenses = []
310 for license, count in counts.items(): 307 for license, count in counts.items():
311 table.add_row([count, license]) 308 licenses.append({
312 return table 309 "Count": count,
313 310 "License": license,
314 311 })
315 class JsonTable(): 312
316 313 return licenses
317 def _format_row(self, row, options):
318 resrow = {}
319 for (field, value) in zip(self._field_names, row):
320 if field not in options["fields"]:
321 continue
322
323 resrow[field] = value
324
325 return resrow
326
327 def get_string(self, **kwargs):
328 # import included here in order to limit dependencies
329 # if not interested in JSON output,
330 # then the dependency is not required
331 import json
332
333 options = self._get_options(kwargs)
334 rows = self._get_rows(options)
335 formatted_rows = self._format_rows(rows, options)
336
337 lines = []
338 for row in formatted_rows:
339 lines.append(row)
340
341 return json.dumps(lines, indent=2, sort_keys=True)
342 ##
343 ##
344 ##class JsonLicenseFinderTable(JsonPrettyTable):
345 ## def _format_row(self, row, options):
346 ## resrow = {}
347 ## for (field, value) in zip(self._field_names, row):
348 ## if field == 'Name':
349 ## resrow['name'] = value
350 ##
351 ## if field == 'Version':
352 ## resrow['version'] = value
353 ##
354 ## if field == 'License':
355 ## resrow['licenses'] = [value]
356 ##
357 ## return resrow
358 ##
359 ## def get_string(self, **kwargs):
360 ## # import included here in order to limit dependencies
361 ## # if not interested in JSON output,
362 ## # then the dependency is not required
363 ## import json
364 ##
365 ## options = self._get_options(kwargs)
366 ## rows = self._get_rows(options)
367 ## formatted_rows = self._format_rows(rows, options)
368 ##
369 ## lines = []
370 ## for row in formatted_rows:
371 ## lines.append(row)
372 ##
373 ## return json.dumps(lines, sort_keys=True)
374 ##
375 ##
376 ##class CSVPrettyTable(PrettyTable):
377 ## """PrettyTable-like class exporting to CSV"""
378 ##
379 ## def get_string(self, **kwargs):
380 ##
381 ## def esc_quotes(val):
382 ## """
383 ## Meta-escaping double quotes
384 ## https://tools.ietf.org/html/rfc4180
385 ## """
386 ## try:
387 ## return val.replace('"', '""')
388 ## except UnicodeDecodeError: # pragma: no cover
389 ## return val.decode('utf-8').replace('"', '""')
390 ## except UnicodeEncodeError: # pragma: no cover
391 ## return val.encode('unicode_escape').replace('"', '""')
392 ##
393 ## options = self._get_options(kwargs)
394 ## rows = self._get_rows(options)
395 ## formatted_rows = self._format_rows(rows, options)
396 ##
397 ## lines = []
398 ## formatted_header = ','.join(['"%s"' % (esc_quotes(val), )
399 ## for val in self._field_names])
400 ## lines.append(formatted_header)
401 ## for row in formatted_rows:
402 ## formatted_row = ','.join(['"%s"' % (esc_quotes(val), )
403 ## for val in row])
404 ## lines.append(formatted_row)
405 ##
406 ## return '\n'.join(lines)
407 ##
408 ##
409 ##class PlainVerticalTable(PrettyTable):
410 ## """PrettyTable for outputting to a simple non-column based style.
411 ##
412 ## When used with --with-license-file, this style is similar to the default
413 ## style generated from Angular CLI's --extractLicenses flag.
414 ## """
415 ##
416 ## def get_string(self, **kwargs):
417 ## options = self._get_options(kwargs)
418 ## rows = self._get_rows(options)
419 ##
420 ## output = ''
421 ## for row in rows:
422 ## for v in row:
423 ## output += '{}\n'.format(v)
424 ## output += '\n'
425 ##
426 ## return output
427 ##
428 ##
429 def factory_styled_table_with_args(
430 args: "CustomNamespace", output_fields=DEFAULT_OUTPUT_FIELDS):
431 table = PrettyTable()
432 table.field_names = output_fields
433 table.align = 'l'
434 table.border = args.format_ in (FormatArg.MARKDOWN, FormatArg.RST,
435 FormatArg.CONFLUENCE, FormatArg.JSON)
436 table.header = True
437
438 if args.format_ == FormatArg.MARKDOWN:
439 table.junction_char = '|'
440 table.hrules = RULE_HEADER
441 elif args.format_ == FormatArg.RST:
442 table.junction_char = '+'
443 table.hrules = RULE_ALL
444 elif args.format_ == FormatArg.CONFLUENCE:
445 table.junction_char = '|'
446 table.hrules = RULE_NONE
447 elif args.format_ == FormatArg.JSON:
448 table = JsonPrettyTable(table.field_names)
449 elif args.format_ == FormatArg.JSON_LICENSE_FINDER:
450 table = JsonLicenseFinderTable(table.field_names)
451 elif args.format_ == FormatArg.CSV:
452 table = CSVPrettyTable(table.field_names)
453 elif args.format_ == FormatArg.PLAIN_VERTICAL:
454 table = PlainVerticalTable(table.field_names)
455
456 return table
457 314
458 315
459 def find_license_from_classifier(message): 316 def find_license_from_classifier(message):
460 licenses = [] 317 licenses = []
461 for k, v in message.items(): 318 for k, v in message.items():
511 output_fields.append('NoticeFile') 368 output_fields.append('NoticeFile')
512 369
513 return output_fields 370 return output_fields
514 371
515 372
516 def get_sortby(args: "CustomNamespace"):
517 if args.summary and args.order == OrderArg.COUNT:
518 return 'Count'
519 elif args.summary or args.order == OrderArg.LICENSE:
520 return 'License'
521 elif args.order == OrderArg.NAME:
522 return 'Name'
523 elif args.order == OrderArg.AUTHOR and args.with_authors:
524 return 'Author'
525 elif args.order == OrderArg.URL and args.with_urls:
526 return 'URL'
527
528 return 'Name'
529
530
531 def create_output_string(args: "CustomNamespace"): 373 def create_output_string(args: "CustomNamespace"):
532 output_fields = get_output_fields(args) 374 output_fields = get_output_fields(args)
533 375
534 if args.summary: 376 if args.summary:
535 table = create_summary_table(args) 377 licenses = create_summary_list(args)
536 else: 378 else:
537 table = create_licenses_list(args, output_fields) 379 licenses = create_licenses_list(args, output_fields)
538 380
539 return json.dumps(table) 381 return json.dumps(licenses)
540 ##
541 ## sortby = get_sortby(args)
542 ##
543 ## if args.format_ == FormatArg.HTML:
544 ## return table.get_html_string(fields=output_fields, sortby=sortby)
545 ## else:
546 ## return table.get_string(fields=output_fields, sortby=sortby)
547 ##
548 ##
549 ##def create_warn_string(args: "CustomNamespace"):
550 ## warn_messages = []
551 ## warn = partial(output_colored, '33')
552 ##
553 ## if args.with_license_file and not args.format_ == FormatArg.JSON:
554 ## message = warn(('Due to the length of these fields, this option is '
555 ## 'best paired with --format=json.'))
556 ## warn_messages.append(message)
557 ##
558 ## if args.summary and (args.with_authors or args.with_urls):
559 ## message = warn(('When using this option, only --order=count or '
560 ## '--order=license has an effect for the --order '
561 ## 'option. And using --with-authors and --with-urls '
562 ## 'will be ignored.'))
563 ## warn_messages.append(message)
564 ##
565 ## return '\n'.join(warn_messages)
566 382
567 383
568 class CustomHelpFormatter(argparse.HelpFormatter): # pragma: no cover 384 class CustomHelpFormatter(argparse.HelpFormatter): # pragma: no cover
569 def __init__( 385 def __init__(
570 self, prog: Text, indent_increment: int = 2, 386 self, prog: Text, indent_increment: int = 2,
604 420
605 421
606 class CustomNamespace(argparse.Namespace): 422 class CustomNamespace(argparse.Namespace):
607 from_: "FromArg" 423 from_: "FromArg"
608 order: "OrderArg" 424 order: "OrderArg"
609 format_: "FormatArg"
610 summary: bool 425 summary: bool
426 local_only: bool
427 user_only:bool
611 output_file: str 428 output_file: str
612 ignore_packages: List[str] 429 ignore_packages: List[str]
613 packages: List[str] 430 packages: List[str]
614 with_system: bool 431 with_system: bool
615 with_authors: bool 432 with_authors: bool
671 NAME = N = auto() 488 NAME = N = auto()
672 AUTHOR = A = auto() 489 AUTHOR = A = auto()
673 URL = U = auto() 490 URL = U = auto()
674 491
675 492
676 class FormatArg(NoValueEnum):
677 PLAIN = P = auto()
678 PLAIN_VERTICAL = auto()
679 MARKDOWN = MD = M = auto()
680 RST = REST = R = auto()
681 CONFLUENCE = C = auto()
682 HTML = H = auto()
683 JSON = J = auto()
684 JSON_LICENSE_FINDER = JLF = auto()
685 CSV = auto()
686
687
688 def value_to_enum_key(value: str) -> str: 493 def value_to_enum_key(value: str) -> str:
689 return value.replace('-', '_').upper() 494 return value.replace('-', '_').upper()
690 495
691 496
692 def enum_key_to_value(enum_key: Enum) -> str: 497 def enum_key_to_value(enum_key: Enum) -> str:
699 504
700 505
701 MAP_DEST_TO_ENUM = { 506 MAP_DEST_TO_ENUM = {
702 'from_': FromArg, 507 'from_': FromArg,
703 'order': OrderArg, 508 'order': OrderArg,
704 'format_': FormatArg,
705 } 509 }
706 510
707 511
708 class SelectAction(argparse.Action): 512 class SelectAction(argparse.Action):
709 def __call__( 513 def __call__(
747 choices=choices_from_enum(OrderArg), 551 choices=choices_from_enum(OrderArg),
748 help='R|order by column\n' 552 help='R|order by column\n'
749 '"name", "license", "author", "url"\n' 553 '"name", "license", "author", "url"\n'
750 '(default: %(default)s)') 554 '(default: %(default)s)')
751 common_options.add_argument( 555 common_options.add_argument(
752 '-f', '--format',
753 dest='format_',
754 action=SelectAction, type=str,
755 default=FormatArg.PLAIN, metavar='STYLE',
756 choices=choices_from_enum(FormatArg),
757 help='R|dump as set format style\n'
758 '"plain", "plain-vertical" "markdown", "rst", \n'
759 '"confluence", "html", "json", \n'
760 '"json-license-finder", "csv"\n'
761 '(default: %(default)s)')
762 common_options.add_argument(
763 '--summary', 556 '--summary',
764 action='store_true', 557 action='store_true',
765 default=False, 558 default=False,
766 help='dump summary of each license') 559 help='dump summary of each license')
767 common_options.add_argument( 560 common_options.add_argument(
778 '-p', '--packages', 571 '-p', '--packages',
779 action='store', type=str, 572 action='store', type=str,
780 nargs='+', metavar='PKG', 573 nargs='+', metavar='PKG',
781 default=[], 574 default=[],
782 help='only include selected packages in output') 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
783 format_options.add_argument( 587 format_options.add_argument(
784 '-s', '--with-system', 588 '-s', '--with-system',
785 action='store_true', 589 action='store_true',
786 default=False, 590 default=False,
787 help='dump with system packages') 591 help='dump with system packages')
843 default=None, 647 default=None,
844 help='fail (exit with code 1) on the first occurrence ' 648 help='fail (exit with code 1) on the first occurrence '
845 'of the licenses not in the semicolon-separated list') 649 'of the licenses not in the semicolon-separated list')
846 650
847 return parser 651 return parser
848 ##
849 ##
850 ##def output_colored(code, text, is_bold=False):
851 ## """
852 ## Create function to output with color sequence
853 ## """
854 ## if is_bold:
855 ## code = '1;%s' % code
856 ##
857 ## return '\033[%sm%s\033[0m' % (code, text)
858 ##
859 ##
860 ##def save_if_needs(output_file, output_string):
861 ## """
862 ## Save to path given by args
863 ## """
864 ## if output_file is None:
865 ## return
866 ##
867 ## try:
868 ## with open(output_file, 'w', encoding='utf-8') as f:
869 ## f.write(output_string)
870 ## sys.stdout.write('created path: ' + output_file + '\n')
871 ## sys.exit(0)
872 ## except IOError:
873 ## sys.stderr.write('check path: --output-file\n')
874 ## sys.exit(1)
875 652
876 653
877 def main(): # pragma: no cover 654 def main(): # pragma: no cover
878 parser = create_parser() 655 parser = create_parser()
879 args = parser.parse_args() 656 args = parser.parse_args()
880 657
881 output_string = create_output_string(args) 658 output_string = create_output_string(args)
882 659
883 ## output_file = args.output_file
884 ## save_if_needs(output_file, output_string)
885 ##
886 print(output_string) 660 print(output_string)
887 ## warn_string = create_warn_string(args)
888 ## if warn_string:
889 ## print(warn_string, file=sys.stderr)
890 661
891 662
892 if __name__ == '__main__': # pragma: no cover 663 if __name__ == '__main__': # pragma: no cover
893 main() 664 main()

eric ide

mercurial