|
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 import argparse |
|
30 import codecs |
|
31 import glob |
|
32 import os |
|
33 import sys |
|
34 from collections import Counter |
|
35 from email import message_from_string |
|
36 from email.parser import FeedParser |
|
37 from enum import Enum, auto |
|
38 from functools import partial |
|
39 from typing import List, Optional, Sequence, Text |
|
40 |
|
41 try: |
|
42 from pip._internal.utils.misc import get_installed_distributions |
|
43 except 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 |
|
66 from prettytable import PrettyTable |
|
67 |
|
68 open = open # allow monkey patching |
|
69 |
|
70 __pkgname__ = 'pip-licenses' |
|
71 __version__ = '3.5.3' |
|
72 __author__ = 'raimon' |
|
73 __license__ = 'MIT' |
|
74 __summary__ = ('Dump the software license list of ' |
|
75 'Python packages installed with pip.') |
|
76 __url__ = 'https://github.com/raimon49/pip-licenses' |
|
77 |
|
78 |
|
79 FIELD_NAMES = ( |
|
80 'Name', |
|
81 'Version', |
|
82 'License', |
|
83 'LicenseFile', |
|
84 'LicenseText', |
|
85 'NoticeFile', |
|
86 'NoticeText', |
|
87 'Author', |
|
88 'Description', |
|
89 'URL', |
|
90 ) |
|
91 |
|
92 |
|
93 SUMMARY_FIELD_NAMES = ( |
|
94 'Count', |
|
95 'License', |
|
96 ) |
|
97 |
|
98 |
|
99 DEFAULT_OUTPUT_FIELDS = ( |
|
100 'Name', |
|
101 'Version', |
|
102 ) |
|
103 |
|
104 |
|
105 SUMMARY_OUTPUT_FIELDS = ( |
|
106 'Count', |
|
107 'License', |
|
108 ) |
|
109 |
|
110 |
|
111 METADATA_KEYS = ( |
|
112 'home-page', |
|
113 'author', |
|
114 'license', |
|
115 'summary', |
|
116 'license_classifier', |
|
117 ) |
|
118 |
|
119 # Mapping of FIELD_NAMES to METADATA_KEYS where they differ by more than case |
|
120 FIELDS_TO_METADATA_KEYS = { |
|
121 'URL': 'home-page', |
|
122 'Description': 'summary', |
|
123 'License-Metadata': 'license', |
|
124 'License-Classifier': 'license_classifier', |
|
125 } |
|
126 |
|
127 |
|
128 SYSTEM_PACKAGES = ( |
|
129 __pkgname__, |
|
130 'pip', |
|
131 'setuptools', |
|
132 'wheel', |
|
133 ) |
|
134 |
|
135 LICENSE_UNKNOWN = 'UNKNOWN' |
|
136 |
|
137 |
|
138 def get_packages(args: "CustomNamespace"): |
|
139 |
|
140 def get_pkg_included_file(pkg, file_names): |
|
141 """ |
|
142 Attempt to find the package's included file on disk and return the |
|
143 tuple (included_file_path, included_file_contents). |
|
144 """ |
|
145 included_file = LICENSE_UNKNOWN |
|
146 included_text = LICENSE_UNKNOWN |
|
147 pkg_dirname = "{}-{}.dist-info".format( |
|
148 pkg.project_name.replace("-", "_"), pkg.version) |
|
149 patterns = [] |
|
150 [patterns.extend(sorted(glob.glob(os.path.join(pkg.location, |
|
151 pkg_dirname, |
|
152 f)))) |
|
153 for f in file_names] |
|
154 for test_file in patterns: |
|
155 if os.path.exists(test_file): |
|
156 included_file = test_file |
|
157 with open(test_file, encoding='utf-8', |
|
158 errors='backslashreplace') as included_file_handle: |
|
159 included_text = included_file_handle.read() |
|
160 break |
|
161 return (included_file, included_text) |
|
162 |
|
163 def get_pkg_info(pkg): |
|
164 (license_file, license_text) = get_pkg_included_file( |
|
165 pkg, |
|
166 ('LICENSE*', 'LICENCE*', 'COPYING*') |
|
167 ) |
|
168 (notice_file, notice_text) = get_pkg_included_file( |
|
169 pkg, |
|
170 ('NOTICE*',) |
|
171 ) |
|
172 pkg_info = { |
|
173 'name': pkg.project_name, |
|
174 'version': pkg.version, |
|
175 'namever': str(pkg), |
|
176 'licensefile': license_file, |
|
177 'licensetext': license_text, |
|
178 'noticefile': notice_file, |
|
179 'noticetext': notice_text, |
|
180 } |
|
181 metadata = None |
|
182 if pkg.has_metadata('METADATA'): |
|
183 metadata = pkg.get_metadata('METADATA') |
|
184 |
|
185 if pkg.has_metadata('PKG-INFO') and metadata is None: |
|
186 metadata = pkg.get_metadata('PKG-INFO') |
|
187 |
|
188 if metadata is None: |
|
189 for key in METADATA_KEYS: |
|
190 pkg_info[key] = LICENSE_UNKNOWN |
|
191 |
|
192 return pkg_info |
|
193 |
|
194 feed_parser = FeedParser() |
|
195 feed_parser.feed(metadata) |
|
196 parsed_metadata = feed_parser.close() |
|
197 |
|
198 for key in METADATA_KEYS: |
|
199 pkg_info[key] = parsed_metadata.get(key, LICENSE_UNKNOWN) |
|
200 |
|
201 if metadata is not None: |
|
202 message = message_from_string(metadata) |
|
203 pkg_info['license_classifier'] = \ |
|
204 find_license_from_classifier(message) |
|
205 |
|
206 if args.filter_strings: |
|
207 for k in pkg_info: |
|
208 if isinstance(pkg_info[k], list): |
|
209 for i, item in enumerate(pkg_info[k]): |
|
210 pkg_info[k][i] = item. \ |
|
211 encode(args.filter_code_page, errors="ignore"). \ |
|
212 decode(args.filter_code_page) |
|
213 else: |
|
214 pkg_info[k] = pkg_info[k]. \ |
|
215 encode(args.filter_code_page, errors="ignore"). \ |
|
216 decode(args.filter_code_page) |
|
217 |
|
218 return pkg_info |
|
219 |
|
220 pkgs = get_installed_distributions() |
|
221 ignore_pkgs_as_lower = [pkg.lower() for pkg in args.ignore_packages] |
|
222 pkgs_as_lower = [pkg.lower() for pkg in args.packages] |
|
223 |
|
224 fail_on_licenses = set() |
|
225 if args.fail_on: |
|
226 fail_on_licenses = set(map(str.strip, args.fail_on.split(";"))) |
|
227 |
|
228 allow_only_licenses = set() |
|
229 if args.allow_only: |
|
230 allow_only_licenses = set(map(str.strip, args.allow_only.split(";"))) |
|
231 |
|
232 for pkg in pkgs: |
|
233 pkg_name = pkg.project_name |
|
234 |
|
235 if pkg_name.lower() in ignore_pkgs_as_lower: |
|
236 continue |
|
237 |
|
238 if pkgs_as_lower and pkg_name.lower() not in pkgs_as_lower: |
|
239 continue |
|
240 |
|
241 if not args.with_system and pkg_name in SYSTEM_PACKAGES: |
|
242 continue |
|
243 |
|
244 pkg_info = get_pkg_info(pkg) |
|
245 |
|
246 license_names = select_license_by_source( |
|
247 args.from_, |
|
248 pkg_info['license_classifier'], |
|
249 pkg_info['license']) |
|
250 |
|
251 if fail_on_licenses: |
|
252 failed_licenses = license_names.intersection(fail_on_licenses) |
|
253 if failed_licenses: |
|
254 sys.stderr.write( |
|
255 "fail-on license {} was found for package " |
|
256 "{}:{}".format( |
|
257 '; '.join(sorted(failed_licenses)), |
|
258 pkg_info['name'], |
|
259 pkg_info['version']) |
|
260 ) |
|
261 sys.exit(1) |
|
262 |
|
263 if allow_only_licenses: |
|
264 uncommon_licenses = license_names.difference(allow_only_licenses) |
|
265 if len(uncommon_licenses) == len(license_names): |
|
266 sys.stderr.write( |
|
267 "license {} not in allow-only licenses was found" |
|
268 " for package {}:{}".format( |
|
269 '; '.join(sorted(uncommon_licenses)), |
|
270 pkg_info['name'], |
|
271 pkg_info['version']) |
|
272 ) |
|
273 sys.exit(1) |
|
274 |
|
275 yield pkg_info |
|
276 |
|
277 |
|
278 def create_licenses_list( |
|
279 args: "CustomNamespace", output_fields=DEFAULT_OUTPUT_FIELDS): |
|
280 |
|
281 licenses = [] |
|
282 for pkg in get_packages(args): |
|
283 row = {} |
|
284 for field in output_fields: |
|
285 if field == 'License': |
|
286 license_set = select_license_by_source( |
|
287 args.from_, pkg['license_classifier'], pkg['license']) |
|
288 license_str = '; '.join(sorted(license_set)) |
|
289 row[field] = license_str |
|
290 ## row.append(license_str) |
|
291 elif field == 'License-Classifier': |
|
292 row[field] = ('; '.join(sorted(pkg['license_classifier'])) |
|
293 or LICENSE_UNKNOWN) |
|
294 elif field.lower() in pkg: |
|
295 row[field] = pkg[field.lower()] |
|
296 else: |
|
297 row[field] = pkg[FIELDS_TO_METADATA_KEYS[field]] |
|
298 licenses.append(row) |
|
299 |
|
300 return licenses |
|
301 |
|
302 |
|
303 def create_summary_table(args: "CustomNamespace"): |
|
304 counts = Counter( |
|
305 '; '.join(sorted(select_license_by_source( |
|
306 args.from_, pkg['license_classifier'], pkg['license']))) |
|
307 for pkg in get_packages(args)) |
|
308 |
|
309 table = factory_styled_table_with_args(args, SUMMARY_FIELD_NAMES) |
|
310 for license, count in counts.items(): |
|
311 table.add_row([count, license]) |
|
312 return table |
|
313 |
|
314 |
|
315 class JsonTable(): |
|
316 |
|
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 |
|
458 |
|
459 def find_license_from_classifier(message): |
|
460 licenses = [] |
|
461 for k, v in message.items(): |
|
462 if k == 'Classifier' and v.startswith('License'): |
|
463 license = v.split(' :: ')[-1] |
|
464 |
|
465 # Through the declaration of 'Classifier: License :: OSI Approved' |
|
466 if license != 'OSI Approved': |
|
467 licenses.append(license) |
|
468 |
|
469 return licenses |
|
470 |
|
471 |
|
472 def select_license_by_source(from_source, license_classifier, license_meta): |
|
473 license_classifier_set = set(license_classifier) or {LICENSE_UNKNOWN} |
|
474 if (from_source == FromArg.CLASSIFIER or |
|
475 from_source == FromArg.MIXED and len(license_classifier) > 0): |
|
476 return license_classifier_set |
|
477 else: |
|
478 return {license_meta} |
|
479 |
|
480 |
|
481 def get_output_fields(args: "CustomNamespace"): |
|
482 if args.summary: |
|
483 return list(SUMMARY_OUTPUT_FIELDS) |
|
484 |
|
485 output_fields = list(DEFAULT_OUTPUT_FIELDS) |
|
486 |
|
487 if args.from_ == FromArg.ALL: |
|
488 output_fields.append('License-Metadata') |
|
489 output_fields.append('License-Classifier') |
|
490 else: |
|
491 output_fields.append('License') |
|
492 |
|
493 if args.with_authors: |
|
494 output_fields.append('Author') |
|
495 |
|
496 if args.with_urls: |
|
497 output_fields.append('URL') |
|
498 |
|
499 if args.with_description: |
|
500 output_fields.append('Description') |
|
501 |
|
502 if args.with_license_file: |
|
503 if not args.no_license_path: |
|
504 output_fields.append('LicenseFile') |
|
505 |
|
506 output_fields.append('LicenseText') |
|
507 |
|
508 if args.with_notice_file: |
|
509 output_fields.append('NoticeText') |
|
510 if not args.no_license_path: |
|
511 output_fields.append('NoticeFile') |
|
512 |
|
513 return output_fields |
|
514 |
|
515 |
|
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"): |
|
532 output_fields = get_output_fields(args) |
|
533 |
|
534 if args.summary: |
|
535 table = create_summary_table(args) |
|
536 else: |
|
537 table = create_licenses_list(args, output_fields) |
|
538 |
|
539 return json.dumps(table) |
|
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 |
|
567 |
|
568 class CustomHelpFormatter(argparse.HelpFormatter): # pragma: no cover |
|
569 def __init__( |
|
570 self, prog: Text, indent_increment: int = 2, |
|
571 max_help_position: int = 24, width: Optional[int] = None |
|
572 ) -> None: |
|
573 max_help_position = 30 |
|
574 super().__init__( |
|
575 prog, indent_increment=indent_increment, |
|
576 max_help_position=max_help_position, width=width) |
|
577 |
|
578 def _format_action(self, action: argparse.Action) -> str: |
|
579 flag_indent_argument: bool = False |
|
580 text = self._expand_help(action) |
|
581 separator_pos = text[:3].find('|') |
|
582 if separator_pos != -1 and 'I' in text[:separator_pos]: |
|
583 self._indent() |
|
584 flag_indent_argument = True |
|
585 help_str = super()._format_action(action) |
|
586 if flag_indent_argument: |
|
587 self._dedent() |
|
588 return help_str |
|
589 |
|
590 def _expand_help(self, action: argparse.Action) -> str: |
|
591 if isinstance(action.default, Enum): |
|
592 default_value = enum_key_to_value(action.default) |
|
593 return self._get_help_string(action) % {'default': default_value} |
|
594 return super()._expand_help(action) |
|
595 |
|
596 def _split_lines(self, text: Text, width: int) -> List[str]: |
|
597 separator_pos = text[:3].find('|') |
|
598 if separator_pos != -1: |
|
599 flag_splitlines: bool = 'R' in text[:separator_pos] |
|
600 text = text[separator_pos + 1:] |
|
601 if flag_splitlines: |
|
602 return text.splitlines() |
|
603 return super()._split_lines(text, width) |
|
604 |
|
605 |
|
606 class CustomNamespace(argparse.Namespace): |
|
607 from_: "FromArg" |
|
608 order: "OrderArg" |
|
609 format_: "FormatArg" |
|
610 summary: bool |
|
611 output_file: str |
|
612 ignore_packages: List[str] |
|
613 packages: List[str] |
|
614 with_system: bool |
|
615 with_authors: bool |
|
616 with_urls: bool |
|
617 with_description: bool |
|
618 with_license_file: bool |
|
619 no_license_path: bool |
|
620 with_notice_file: bool |
|
621 filter_strings: bool |
|
622 filter_code_page: str |
|
623 fail_on: Optional[str] |
|
624 allow_only: Optional[str] |
|
625 |
|
626 |
|
627 class CompatibleArgumentParser(argparse.ArgumentParser): |
|
628 def parse_args(self, args: Optional[Sequence[Text]] = None, |
|
629 namespace: CustomNamespace = None) -> CustomNamespace: |
|
630 args = super().parse_args(args, namespace) |
|
631 self._verify_args(args) |
|
632 return args |
|
633 |
|
634 def _verify_args(self, args: CustomNamespace): |
|
635 if args.with_license_file is False and ( |
|
636 args.no_license_path is True or |
|
637 args.with_notice_file is True): |
|
638 self.error( |
|
639 "'--no-license-path' and '--with-notice-file' require " |
|
640 "the '--with-license-file' option to be set") |
|
641 if args.filter_strings is False and \ |
|
642 args.filter_code_page != 'latin1': |
|
643 self.error( |
|
644 "'--filter-code-page' requires the '--filter-strings' " |
|
645 "option to be set") |
|
646 try: |
|
647 codecs.lookup(args.filter_code_page) |
|
648 except LookupError: |
|
649 self.error( |
|
650 "invalid code page '%s' given for '--filter-code-page, " |
|
651 "check https://docs.python.org/3/library/codecs.html" |
|
652 "#standard-encodings for valid code pages" |
|
653 % args.filter_code_page) |
|
654 |
|
655 |
|
656 class NoValueEnum(Enum): |
|
657 def __repr__(self): # pragma: no cover |
|
658 return '<%s.%s>' % (self.__class__.__name__, self.name) |
|
659 |
|
660 |
|
661 class FromArg(NoValueEnum): |
|
662 META = M = auto() |
|
663 CLASSIFIER = C = auto() |
|
664 MIXED = MIX = auto() |
|
665 ALL = auto() |
|
666 |
|
667 |
|
668 class OrderArg(NoValueEnum): |
|
669 COUNT = C = auto() |
|
670 LICENSE = L = auto() |
|
671 NAME = N = auto() |
|
672 AUTHOR = A = auto() |
|
673 URL = U = auto() |
|
674 |
|
675 |
|
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: |
|
689 return value.replace('-', '_').upper() |
|
690 |
|
691 |
|
692 def enum_key_to_value(enum_key: Enum) -> str: |
|
693 return enum_key.name.replace('_', '-').lower() |
|
694 |
|
695 |
|
696 def choices_from_enum(enum_cls: NoValueEnum) -> List[str]: |
|
697 return [key.replace('_', '-').lower() |
|
698 for key in enum_cls.__members__.keys()] |
|
699 |
|
700 |
|
701 MAP_DEST_TO_ENUM = { |
|
702 'from_': FromArg, |
|
703 'order': OrderArg, |
|
704 'format_': FormatArg, |
|
705 } |
|
706 |
|
707 |
|
708 class SelectAction(argparse.Action): |
|
709 def __call__( |
|
710 self, parser: argparse.ArgumentParser, |
|
711 namespace: argparse.Namespace, |
|
712 values: Text, |
|
713 option_string: Optional[Text] = None, |
|
714 ) -> None: |
|
715 enum_cls = MAP_DEST_TO_ENUM[self.dest] |
|
716 values = value_to_enum_key(values) |
|
717 setattr(namespace, self.dest, getattr(enum_cls, values)) |
|
718 |
|
719 |
|
720 def create_parser(): |
|
721 parser = CompatibleArgumentParser( |
|
722 description=__summary__, |
|
723 formatter_class=CustomHelpFormatter) |
|
724 |
|
725 common_options = parser.add_argument_group('Common options') |
|
726 format_options = parser.add_argument_group('Format options') |
|
727 verify_options = parser.add_argument_group('Verify options') |
|
728 |
|
729 parser.add_argument( |
|
730 '-v', '--version', |
|
731 action='version', |
|
732 version='%(prog)s ' + __version__) |
|
733 |
|
734 common_options.add_argument( |
|
735 '--from', |
|
736 dest='from_', |
|
737 action=SelectAction, type=str, |
|
738 default=FromArg.MIXED, metavar='SOURCE', |
|
739 choices=choices_from_enum(FromArg), |
|
740 help='R|where to find license information\n' |
|
741 '"meta", "classifier, "mixed", "all"\n' |
|
742 '(default: %(default)s)') |
|
743 common_options.add_argument( |
|
744 '-o', '--order', |
|
745 action=SelectAction, type=str, |
|
746 default=OrderArg.NAME, metavar='COL', |
|
747 choices=choices_from_enum(OrderArg), |
|
748 help='R|order by column\n' |
|
749 '"name", "license", "author", "url"\n' |
|
750 '(default: %(default)s)') |
|
751 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', |
|
764 action='store_true', |
|
765 default=False, |
|
766 help='dump summary of each license') |
|
767 common_options.add_argument( |
|
768 '--output-file', |
|
769 action='store', type=str, |
|
770 help='save license list to file') |
|
771 common_options.add_argument( |
|
772 '-i', '--ignore-packages', |
|
773 action='store', type=str, |
|
774 nargs='+', metavar='PKG', |
|
775 default=[], |
|
776 help='ignore package name in dumped list') |
|
777 common_options.add_argument( |
|
778 '-p', '--packages', |
|
779 action='store', type=str, |
|
780 nargs='+', metavar='PKG', |
|
781 default=[], |
|
782 help='only include selected packages in output') |
|
783 format_options.add_argument( |
|
784 '-s', '--with-system', |
|
785 action='store_true', |
|
786 default=False, |
|
787 help='dump with system packages') |
|
788 format_options.add_argument( |
|
789 '-a', '--with-authors', |
|
790 action='store_true', |
|
791 default=False, |
|
792 help='dump with package authors') |
|
793 format_options.add_argument( |
|
794 '-u', '--with-urls', |
|
795 action='store_true', |
|
796 default=False, |
|
797 help='dump with package urls') |
|
798 format_options.add_argument( |
|
799 '-d', '--with-description', |
|
800 action='store_true', |
|
801 default=False, |
|
802 help='dump with short package description') |
|
803 format_options.add_argument( |
|
804 '-l', '--with-license-file', |
|
805 action='store_true', |
|
806 default=False, |
|
807 help='dump with location of license file and ' |
|
808 'contents, most useful with JSON output') |
|
809 format_options.add_argument( |
|
810 '--no-license-path', |
|
811 action='store_true', |
|
812 default=False, |
|
813 help='I|when specified together with option -l, ' |
|
814 'suppress location of license file output') |
|
815 format_options.add_argument( |
|
816 '--with-notice-file', |
|
817 action='store_true', |
|
818 default=False, |
|
819 help='I|when specified together with option -l, ' |
|
820 'dump with location of license file and contents') |
|
821 format_options.add_argument( |
|
822 '--filter-strings', |
|
823 action="store_true", |
|
824 default=False, |
|
825 help='filter input according to code page') |
|
826 format_options.add_argument( |
|
827 '--filter-code-page', |
|
828 action="store", type=str, |
|
829 default="latin1", |
|
830 metavar="CODE", |
|
831 help='I|specify code page for filtering ' |
|
832 '(default: %(default)s)') |
|
833 |
|
834 verify_options.add_argument( |
|
835 '--fail-on', |
|
836 action='store', type=str, |
|
837 default=None, |
|
838 help='fail (exit with code 1) on the first occurrence ' |
|
839 'of the licenses of the semicolon-separated list') |
|
840 verify_options.add_argument( |
|
841 '--allow-only', |
|
842 action='store', type=str, |
|
843 default=None, |
|
844 help='fail (exit with code 1) on the first occurrence ' |
|
845 'of the licenses not in the semicolon-separated list') |
|
846 |
|
847 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 |
|
876 |
|
877 def main(): # pragma: no cover |
|
878 parser = create_parser() |
|
879 args = parser.parse_args() |
|
880 |
|
881 output_string = create_output_string(args) |
|
882 |
|
883 ## output_file = args.output_file |
|
884 ## save_if_needs(output_file, output_string) |
|
885 ## |
|
886 print(output_string) |
|
887 ## warn_string = create_warn_string(args) |
|
888 ## if warn_string: |
|
889 ## print(warn_string, file=sys.stderr) |
|
890 |
|
891 |
|
892 if __name__ == '__main__': # pragma: no cover |
|
893 main() |