39 # - changed 'create_summary_table' to 'create_summary_list' |
39 # - changed 'create_summary_table' to 'create_summary_list' |
40 # - added 'create_summary_by_license_list' together with the |
40 # - added 'create_summary_by_license_list' together with the |
41 # '--summary-by-license' switch to count each individual |
41 # '--summary-by-license' switch to count each individual |
42 # license used |
42 # license used |
43 # - changed 'create_output_string' to return a JSON string |
43 # - changed 'create_output_string' to return a JSON string |
|
44 # - added 'get_installed_distributions' to get the distributions |
|
45 # via pip in order to filter for 'local only' and/or 'user only' |
44 # |
46 # |
|
47 |
|
48 from __future__ import annotations |
45 |
49 |
46 import argparse |
50 import argparse |
47 import codecs |
51 import codecs |
48 import glob |
|
49 import json |
52 import json |
50 import os |
53 import re |
51 import sys |
54 import sys |
52 from collections import Counter |
55 from collections import Counter |
53 from email import message_from_string |
|
54 from email.parser import FeedParser |
|
55 from enum import Enum, auto |
56 from enum import Enum, auto |
56 from typing import List, Optional, Sequence, Text |
57 from pathlib import Path |
|
58 from typing import TYPE_CHECKING, List, Type, cast |
|
59 |
|
60 if TYPE_CHECKING: |
|
61 from typing import Iterator, Optional, Sequence |
57 |
62 |
58 |
63 |
59 def get_installed_distributions(local_only=True, user_only=False): |
64 def get_installed_distributions(local_only=True, user_only=False): |
|
65 """ |
|
66 Function to get the installed packages via pip. |
|
67 |
|
68 Note: importlib_metadata.distributions() does not respect |
|
69 'local_only' and 'user_only' keyword parameters. |
|
70 |
|
71 @param local_only DESCRIPTION (defaults to True) |
|
72 @type TYPE (optional) |
|
73 @param user_only DESCRIPTION (defaults to False) |
|
74 @type TYPE (optional) |
|
75 @return DESCRIPTION |
|
76 @rtype TYPE |
|
77 """ |
60 try: |
78 try: |
61 from pip._internal.metadata import get_environment |
79 from pip._internal.metadata import get_environment |
62 except ImportError: |
80 except ImportError: |
63 # For backward compatibility with pip version 20.3.4 |
81 # For backward compatibility with pip version 20.3.4 |
64 from pip._internal.utils import misc |
82 from pip._internal.utils import misc |
138 ) |
163 ) |
139 |
164 |
140 LICENSE_UNKNOWN = 'UNKNOWN' |
165 LICENSE_UNKNOWN = 'UNKNOWN' |
141 |
166 |
142 |
167 |
143 def get_packages(args: "CustomNamespace"): |
168 def get_packages( |
144 |
169 args: CustomNamespace, |
145 def get_pkg_included_file(pkg, file_names): |
170 ) -> Iterator[dict[str, str | list[str]]]: |
|
171 def get_pkg_included_file( |
|
172 pkg, file_names_rgx: str |
|
173 ) -> tuple[str, str]: |
146 """ |
174 """ |
147 Attempt to find the package's included file on disk and return the |
175 Attempt to find the package's included file on disk and return the |
148 tuple (included_file_path, included_file_contents). |
176 tuple (included_file_path, included_file_contents). |
149 """ |
177 """ |
150 included_file = LICENSE_UNKNOWN |
178 included_file = LICENSE_UNKNOWN |
151 included_text = LICENSE_UNKNOWN |
179 included_text = LICENSE_UNKNOWN |
152 pkg_dirname = "{}-{}.dist-info".format( |
180 |
153 pkg.project_name.replace("-", "_"), pkg.version) |
181 pkg_files = pkg.files or () |
154 patterns = [] |
182 pattern = re.compile(file_names_rgx) |
155 [patterns.extend(sorted(glob.glob(os.path.join(pkg.location, |
183 matched_rel_paths = filter( |
156 pkg_dirname, |
184 lambda file: pattern.match(file.name), pkg_files |
157 f)))) |
185 ) |
158 for f in file_names] |
186 for rel_path in matched_rel_paths: |
159 for test_file in patterns: |
187 abs_path = Path(pkg.locate_file(rel_path)) |
160 if os.path.exists(test_file) and not os.path.isdir(test_file): |
188 if not abs_path.is_file(): |
161 included_file = test_file |
189 continue |
162 with open(test_file, encoding='utf-8', |
190 included_file = str(abs_path) |
163 errors='backslashreplace') as included_file_handle: |
191 with open( |
164 included_text = included_file_handle.read() |
192 abs_path, encoding="utf-8", errors="backslashreplace" |
165 break |
193 ) as included_file_handle: |
|
194 included_text = included_file_handle.read() |
|
195 break |
166 return (included_file, included_text) |
196 return (included_file, included_text) |
167 |
197 |
168 def get_pkg_info(pkg): |
198 def get_pkg_info(pkg) -> dict[str, str | list[str]]: |
169 (license_file, license_text) = get_pkg_included_file( |
199 (license_file, license_text) = get_pkg_included_file( |
170 pkg, |
200 pkg, "LICEN[CS]E.*|COPYING.*" |
171 ('LICENSE*', 'LICENCE*', 'COPYING*') |
|
172 ) |
201 ) |
173 (notice_file, notice_text) = get_pkg_included_file( |
202 (notice_file, notice_text) = get_pkg_included_file(pkg, "NOTICE.*") |
174 pkg, |
203 pkg_info: dict[str, str | list[str]] = { |
175 ('NOTICE*',) |
204 "name": pkg.metadata["name"], |
|
205 "version": pkg.version, |
|
206 "namever": "{} {}".format(pkg.metadata["name"], pkg.version), |
|
207 "licensefile": license_file, |
|
208 "licensetext": license_text, |
|
209 "noticefile": notice_file, |
|
210 "noticetext": notice_text, |
|
211 } |
|
212 metadata = pkg.metadata |
|
213 for key in METADATA_KEYS: |
|
214 pkg_info[key] = metadata.get(key, LICENSE_UNKNOWN) # type: ignore[attr-defined] # noqa: E501 |
|
215 |
|
216 classifiers: list[str] = metadata.get_all("classifier", []) |
|
217 pkg_info["license_classifier"] = find_license_from_classifier( |
|
218 classifiers |
176 ) |
219 ) |
177 pkg_info = { |
|
178 'name': pkg.project_name, |
|
179 'version': pkg.version, |
|
180 'namever': str(pkg), |
|
181 'licensefile': license_file, |
|
182 'licensetext': license_text, |
|
183 'noticefile': notice_file, |
|
184 'noticetext': notice_text, |
|
185 } |
|
186 metadata = None |
|
187 if pkg.has_metadata('METADATA'): |
|
188 metadata = pkg.get_metadata('METADATA') |
|
189 |
|
190 if pkg.has_metadata('PKG-INFO') and metadata is None: |
|
191 metadata = pkg.get_metadata('PKG-INFO') |
|
192 |
|
193 if metadata is None: |
|
194 for key in METADATA_KEYS: |
|
195 pkg_info[key] = LICENSE_UNKNOWN |
|
196 |
|
197 return pkg_info |
|
198 |
|
199 feed_parser = FeedParser() |
|
200 feed_parser.feed(metadata) |
|
201 parsed_metadata = feed_parser.close() |
|
202 |
|
203 for key in METADATA_KEYS: |
|
204 pkg_info[key] = parsed_metadata.get(key, LICENSE_UNKNOWN) |
|
205 |
|
206 if metadata is not None: |
|
207 message = message_from_string(metadata) |
|
208 pkg_info['license_classifier'] = \ |
|
209 find_license_from_classifier(message) |
|
210 |
220 |
211 if args.filter_strings: |
221 if args.filter_strings: |
|
222 |
|
223 def filter_string(item: str) -> str: |
|
224 return item.encode( |
|
225 args.filter_code_page, errors="ignore" |
|
226 ).decode(args.filter_code_page) |
|
227 |
212 for k in pkg_info: |
228 for k in pkg_info: |
213 if isinstance(pkg_info[k], list): |
229 if isinstance(pkg_info[k], list): |
214 for i, item in enumerate(pkg_info[k]): |
230 pkg_info[k] = list(map(filter_string, pkg_info[k])) |
215 pkg_info[k][i] = item. \ |
|
216 encode(args.filter_code_page, errors="ignore"). \ |
|
217 decode(args.filter_code_page) |
|
218 else: |
231 else: |
219 pkg_info[k] = pkg_info[k]. \ |
232 pkg_info[k] = filter_string(cast(str, pkg_info[k])) |
220 encode(args.filter_code_page, errors="ignore"). \ |
|
221 decode(args.filter_code_page) |
|
222 |
233 |
223 return pkg_info |
234 return pkg_info |
224 |
235 |
225 pkgs = get_installed_distributions( |
236 pkgs = get_installed_distributions( |
226 local_only=args.local_only, |
237 local_only=args.local_only, |
251 |
262 |
252 pkg_info = get_pkg_info(pkg) |
263 pkg_info = get_pkg_info(pkg) |
253 |
264 |
254 license_names = select_license_by_source( |
265 license_names = select_license_by_source( |
255 args.from_, |
266 args.from_, |
256 pkg_info['license_classifier'], |
267 cast(List[str], pkg_info["license_classifier"]), |
257 pkg_info['license']) |
268 cast(str, pkg_info["license"]), |
|
269 ) |
258 |
270 |
259 if fail_on_licenses: |
271 if fail_on_licenses: |
260 failed_licenses = license_names.intersection(fail_on_licenses) |
272 failed_licenses = license_names.intersection(fail_on_licenses) |
261 if failed_licenses: |
273 if failed_licenses: |
262 sys.stderr.write( |
274 sys.stderr.write( |
263 "fail-on license {} was found for package " |
275 "fail-on license {} was found for package " |
264 "{}:{}".format( |
276 "{}:{}".format( |
265 '; '.join(sorted(failed_licenses)), |
277 "; ".join(sorted(failed_licenses)), |
266 pkg_info['name'], |
278 pkg_info["name"], |
267 pkg_info['version']) |
279 pkg_info["version"], |
|
280 ) |
268 ) |
281 ) |
269 sys.exit(1) |
282 sys.exit(1) |
270 |
283 |
271 if allow_only_licenses: |
284 if allow_only_licenses: |
272 uncommon_licenses = license_names.difference(allow_only_licenses) |
285 uncommon_licenses = license_names.difference(allow_only_licenses) |
273 if len(uncommon_licenses) == len(license_names): |
286 if len(uncommon_licenses) == len(license_names): |
274 sys.stderr.write( |
287 sys.stderr.write( |
275 "license {} not in allow-only licenses was found" |
288 "license {} not in allow-only licenses was found" |
276 " for package {}:{}".format( |
289 " for package {}:{}".format( |
277 '; '.join(sorted(uncommon_licenses)), |
290 "; ".join(sorted(uncommon_licenses)), |
278 pkg_info['name'], |
291 pkg_info["name"], |
279 pkg_info['version']) |
292 pkg_info["version"], |
|
293 ) |
280 ) |
294 ) |
281 sys.exit(1) |
295 sys.exit(1) |
282 |
296 |
283 yield pkg_info |
297 yield pkg_info |
284 |
298 |
290 for pkg in get_packages(args): |
304 for pkg in get_packages(args): |
291 row = {} |
305 row = {} |
292 for field in output_fields: |
306 for field in output_fields: |
293 if field == 'License': |
307 if field == 'License': |
294 license_set = select_license_by_source( |
308 license_set = select_license_by_source( |
295 args.from_, pkg['license_classifier'], pkg['license']) |
309 args.from_, |
296 license_str = '; '.join(sorted(license_set)) |
310 cast(List[str], pkg["license_classifier"]), |
|
311 cast(str, pkg["license"]), |
|
312 ) |
|
313 license_str = "; ".join(sorted(license_set)) |
297 row[field] = license_str |
314 row[field] = license_str |
298 elif field == 'License-Classifier': |
315 elif field == 'License-Classifier': |
299 row[field] = ('; '.join(sorted(pkg['license_classifier'])) |
316 row[field] = ( |
300 or LICENSE_UNKNOWN) |
317 "; ".join(sorted(pkg["license_classifier"])) |
|
318 or LICENSE_UNKNOWN |
|
319 ) |
301 elif field.lower() in pkg: |
320 elif field.lower() in pkg: |
302 row[field] = pkg[field.lower()] |
321 row[field] = cast(str, pkg[field.lower()]) |
303 else: |
322 else: |
304 row[field] = pkg[FIELDS_TO_METADATA_KEYS[field]] |
323 row[field] = cast(str, pkg[FIELDS_TO_METADATA_KEYS[field]]) |
305 licenses.append(row) |
324 licenses.append(row) |
306 |
325 |
307 return licenses |
326 return licenses |
308 |
327 |
309 |
328 |
310 def create_summary_list(args: "CustomNamespace"): |
329 def create_summary_list(args: "CustomNamespace"): |
311 counts = Counter( |
330 counts = Counter( |
312 '; '.join(sorted(select_license_by_source( |
331 "; ".join( |
313 args.from_, pkg['license_classifier'], pkg['license']))) |
332 sorted( |
314 for pkg in get_packages(args)) |
333 select_license_by_source( |
|
334 args.from_, |
|
335 cast(List[str], pkg["license_classifier"]), |
|
336 cast(str, pkg["license"]), |
|
337 ) |
|
338 ) |
|
339 ) |
|
340 for pkg in get_packages(args) |
|
341 ) |
315 |
342 |
316 licenses = [] |
343 licenses = [] |
317 for license, count in counts.items(): |
344 for license, count in counts.items(): |
318 licenses.append({ |
345 licenses.append({ |
319 "Count": count, |
346 "Count": count, |
338 }) |
365 }) |
339 |
366 |
340 return licenses |
367 return licenses |
341 |
368 |
342 |
369 |
343 def find_license_from_classifier(message): |
370 def find_license_from_classifier(classifiers: list[str]) -> list[str]: |
344 licenses = [] |
371 licenses = [] |
345 for k, v in message.items(): |
372 for classifier in filter(lambda c: c.startswith("License"), classifiers): |
346 if k == 'Classifier' and v.startswith('License'): |
373 license = classifier.split(" :: ")[-1] |
347 license = v.split(' :: ')[-1] |
374 |
348 |
375 # Through the declaration of 'Classifier: License :: OSI Approved' |
349 # Through the declaration of 'Classifier: License :: OSI Approved' |
376 if license != "OSI Approved": |
350 if license != 'OSI Approved': |
377 licenses.append(license) |
351 licenses.append(license) |
|
352 |
378 |
353 return licenses |
379 return licenses |
354 |
380 |
355 |
381 |
356 def select_license_by_source(from_source, license_classifier, license_meta): |
382 def select_license_by_source( |
|
383 from_source: FromArg, license_classifier: list[str], license_meta: str |
|
384 ) -> set[str]: |
357 license_classifier_set = set(license_classifier) or {LICENSE_UNKNOWN} |
385 license_classifier_set = set(license_classifier) or {LICENSE_UNKNOWN} |
358 if (from_source == FromArg.CLASSIFIER or |
386 if ( |
359 from_source == FromArg.MIXED and len(license_classifier) > 0): |
387 from_source == FromArg.CLASSIFIER |
|
388 or from_source == FromArg.MIXED |
|
389 and len(license_classifier) > 0 |
|
390 ): |
360 return license_classifier_set |
391 return license_classifier_set |
361 else: |
392 else: |
362 return {license_meta} |
393 return {license_meta} |
363 |
394 |
364 |
395 |
365 def get_output_fields(args: "CustomNamespace"): |
396 def get_output_fields(args: CustomNamespace) -> list[str]: |
366 if args.summary: |
397 if args.summary: |
367 return list(SUMMARY_OUTPUT_FIELDS) |
398 return list(SUMMARY_OUTPUT_FIELDS) |
368 |
399 |
369 output_fields = list(DEFAULT_OUTPUT_FIELDS) |
400 output_fields = list(DEFAULT_OUTPUT_FIELDS) |
370 |
401 |
371 if args.from_ == FromArg.ALL: |
402 if args.from_ == FromArg.ALL: |
372 output_fields.append('License-Metadata') |
403 output_fields.append("License-Metadata") |
373 output_fields.append('License-Classifier') |
404 output_fields.append("License-Classifier") |
374 else: |
405 else: |
375 output_fields.append('License') |
406 output_fields.append("License") |
376 |
407 |
377 if args.with_authors: |
408 if args.with_authors: |
378 output_fields.append('Author') |
409 output_fields.append("Author") |
379 |
410 |
380 if args.with_urls: |
411 if args.with_urls: |
381 output_fields.append('URL') |
412 output_fields.append("URL") |
382 |
413 |
383 if args.with_description: |
414 if args.with_description: |
384 output_fields.append('Description') |
415 output_fields.append("Description") |
385 |
416 |
386 if args.with_license_file: |
417 if args.with_license_file: |
387 if not args.no_license_path: |
418 if not args.no_license_path: |
388 output_fields.append('LicenseFile') |
419 output_fields.append("LicenseFile") |
389 |
420 |
390 output_fields.append('LicenseText') |
421 output_fields.append("LicenseText") |
391 |
422 |
392 if args.with_notice_file: |
423 if args.with_notice_file: |
393 output_fields.append('NoticeText') |
424 output_fields.append("NoticeText") |
394 if not args.no_license_path: |
425 if not args.no_license_path: |
395 output_fields.append('NoticeFile') |
426 output_fields.append("NoticeFile") |
396 |
427 |
397 return output_fields |
428 return output_fields |
398 |
429 |
399 |
430 |
400 def create_output_string(args: "CustomNamespace"): |
431 def create_output_string(args: "CustomNamespace"): |
410 return json.dumps(licenses) |
441 return json.dumps(licenses) |
411 |
442 |
412 |
443 |
413 class CustomHelpFormatter(argparse.HelpFormatter): # pragma: no cover |
444 class CustomHelpFormatter(argparse.HelpFormatter): # pragma: no cover |
414 def __init__( |
445 def __init__( |
415 self, prog: Text, indent_increment: int = 2, |
446 self, |
416 max_help_position: int = 24, width: Optional[int] = None |
447 prog: str, |
|
448 indent_increment: int = 2, |
|
449 max_help_position: int = 24, |
|
450 width: Optional[int] = None, |
417 ) -> None: |
451 ) -> None: |
418 max_help_position = 30 |
452 max_help_position = 30 |
419 super().__init__( |
453 super().__init__( |
420 prog, indent_increment=indent_increment, |
454 prog, |
421 max_help_position=max_help_position, width=width) |
455 indent_increment=indent_increment, |
|
456 max_help_position=max_help_position, |
|
457 width=width, |
|
458 ) |
422 |
459 |
423 def _format_action(self, action: argparse.Action) -> str: |
460 def _format_action(self, action: argparse.Action) -> str: |
424 flag_indent_argument: bool = False |
461 flag_indent_argument: bool = False |
425 text = self._expand_help(action) |
462 text = self._expand_help(action) |
426 separator_pos = text[:3].find('|') |
463 separator_pos = text[:3].find("|") |
427 if separator_pos != -1 and 'I' in text[:separator_pos]: |
464 if separator_pos != -1 and "I" in text[:separator_pos]: |
428 self._indent() |
465 self._indent() |
429 flag_indent_argument = True |
466 flag_indent_argument = True |
430 help_str = super()._format_action(action) |
467 help_str = super()._format_action(action) |
431 if flag_indent_argument: |
468 if flag_indent_argument: |
432 self._dedent() |
469 self._dedent() |
433 return help_str |
470 return help_str |
434 |
471 |
435 def _expand_help(self, action: argparse.Action) -> str: |
472 def _expand_help(self, action: argparse.Action) -> str: |
436 if isinstance(action.default, Enum): |
473 if isinstance(action.default, Enum): |
437 default_value = enum_key_to_value(action.default) |
474 default_value = enum_key_to_value(action.default) |
438 return self._get_help_string(action) % {'default': default_value} |
475 return cast(str, self._get_help_string(action)) % { |
|
476 "default": default_value |
|
477 } |
439 return super()._expand_help(action) |
478 return super()._expand_help(action) |
440 |
479 |
441 def _split_lines(self, text: Text, width: int) -> List[str]: |
480 def _split_lines(self, text: str, width: int) -> List[str]: |
442 separator_pos = text[:3].find('|') |
481 separator_pos = text[:3].find("|") |
443 if separator_pos != -1: |
482 if separator_pos != -1: |
444 flag_splitlines: bool = 'R' in text[:separator_pos] |
483 flag_splitlines: bool = "R" in text[:separator_pos] |
445 text = text[separator_pos + 1:] |
484 text = text[separator_pos + 1:] # fmt: skip |
446 if flag_splitlines: |
485 if flag_splitlines: |
447 return text.splitlines() |
486 return text.splitlines() |
448 return super()._split_lines(text, width) |
487 return super()._split_lines(text, width) |
449 |
488 |
450 |
489 |
470 fail_on: Optional[str] |
509 fail_on: Optional[str] |
471 allow_only: Optional[str] |
510 allow_only: Optional[str] |
472 |
511 |
473 |
512 |
474 class CompatibleArgumentParser(argparse.ArgumentParser): |
513 class CompatibleArgumentParser(argparse.ArgumentParser): |
475 def parse_args(self, args: Optional[Sequence[Text]] = None, |
514 def parse_args( # type: ignore[override] |
476 namespace: CustomNamespace = None) -> CustomNamespace: |
515 self, |
477 args = super().parse_args(args, namespace) |
516 args: None | Sequence[str] = None, |
478 self._verify_args(args) |
517 namespace: None | CustomNamespace = None, |
479 return args |
518 ) -> CustomNamespace: |
480 |
519 args_ = cast(CustomNamespace, super().parse_args(args, namespace)) |
481 def _verify_args(self, args: CustomNamespace): |
520 self._verify_args(args_) |
|
521 return args_ |
|
522 |
|
523 def _verify_args(self, args: CustomNamespace) -> None: |
482 if args.with_license_file is False and ( |
524 if args.with_license_file is False and ( |
483 args.no_license_path is True or |
525 args.no_license_path is True or args.with_notice_file is True |
484 args.with_notice_file is True): |
526 ): |
485 self.error( |
527 self.error( |
486 "'--no-license-path' and '--with-notice-file' require " |
528 "'--no-license-path' and '--with-notice-file' require " |
487 "the '--with-license-file' option to be set") |
529 "the '--with-license-file' option to be set" |
488 if args.filter_strings is False and \ |
530 ) |
489 args.filter_code_page != 'latin1': |
531 if args.filter_strings is False and args.filter_code_page != "latin1": |
490 self.error( |
532 self.error( |
491 "'--filter-code-page' requires the '--filter-strings' " |
533 "'--filter-code-page' requires the '--filter-strings' " |
492 "option to be set") |
534 "option to be set" |
|
535 ) |
493 try: |
536 try: |
494 codecs.lookup(args.filter_code_page) |
537 codecs.lookup(args.filter_code_page) |
495 except LookupError: |
538 except LookupError: |
496 self.error( |
539 self.error( |
497 "invalid code page '%s' given for '--filter-code-page, " |
540 "invalid code page '%s' given for '--filter-code-page, " |
498 "check https://docs.python.org/3/library/codecs.html" |
541 "check https://docs.python.org/3/library/codecs.html" |
499 "#standard-encodings for valid code pages" |
542 "#standard-encodings for valid code pages" |
500 % args.filter_code_page) |
543 % args.filter_code_page |
|
544 ) |
501 |
545 |
502 |
546 |
503 class NoValueEnum(Enum): |
547 class NoValueEnum(Enum): |
504 def __repr__(self): # pragma: no cover |
548 def __repr__(self): # pragma: no cover |
505 return '<%s.%s>' % (self.__class__.__name__, self.name) |
549 return '<%s.%s>' % (self.__class__.__name__, self.name) |
526 |
570 |
527 def enum_key_to_value(enum_key: Enum) -> str: |
571 def enum_key_to_value(enum_key: Enum) -> str: |
528 return enum_key.name.replace('_', '-').lower() |
572 return enum_key.name.replace('_', '-').lower() |
529 |
573 |
530 |
574 |
531 def choices_from_enum(enum_cls: NoValueEnum) -> List[str]: |
575 def choices_from_enum(enum_cls: Type[NoValueEnum]) -> List[str]: |
532 return [key.replace('_', '-').lower() |
576 return [ |
533 for key in enum_cls.__members__.keys()] |
577 key.replace("_", "-").lower() for key in enum_cls.__members__.keys() |
|
578 ] |
534 |
579 |
535 |
580 |
536 MAP_DEST_TO_ENUM = { |
581 MAP_DEST_TO_ENUM = { |
537 'from_': FromArg, |
582 'from_': FromArg, |
538 'order': OrderArg, |
583 'order': OrderArg, |
539 } |
584 } |
540 |
585 |
541 |
586 |
542 class SelectAction(argparse.Action): |
587 class SelectAction(argparse.Action): |
543 def __call__( |
588 def __call__( # type: ignore[override] |
544 self, parser: argparse.ArgumentParser, |
589 self, |
|
590 parser: argparse.ArgumentParser, |
545 namespace: argparse.Namespace, |
591 namespace: argparse.Namespace, |
546 values: Text, |
592 values: str, |
547 option_string: Optional[Text] = None, |
593 option_string: Optional[str] = None, |
548 ) -> None: |
594 ) -> None: |
549 enum_cls = MAP_DEST_TO_ENUM[self.dest] |
595 enum_cls = MAP_DEST_TO_ENUM[self.dest] |
550 values = value_to_enum_key(values) |
596 values = value_to_enum_key(values) |
551 setattr(namespace, self.dest, getattr(enum_cls, values)) |
597 setattr(namespace, self.dest, getattr(enum_cls, values)) |
552 |
598 |
553 |
599 |
554 def create_parser(): |
600 def create_parser(): |
555 parser = CompatibleArgumentParser( |
601 parser = CompatibleArgumentParser( |
556 description=__summary__, |
602 description=__summary__, formatter_class=CustomHelpFormatter |
557 formatter_class=CustomHelpFormatter) |
603 ) |
558 |
604 |
559 common_options = parser.add_argument_group('Common options') |
605 common_options = parser.add_argument_group('Common options') |
560 format_options = parser.add_argument_group('Format options') |
606 format_options = parser.add_argument_group('Format options') |
561 verify_options = parser.add_argument_group('Verify options') |
607 verify_options = parser.add_argument_group('Verify options') |
562 |
608 |
563 parser.add_argument( |
609 parser.add_argument( |
564 '-v', '--version', |
610 "-v", "--version", action="version", version="%(prog)s " + __version__ |
565 action='version', |
611 ) |
566 version='%(prog)s ' + __version__) |
612 |
567 |
613 common_options.add_argument( |
568 common_options.add_argument( |
614 "--from", |
569 '--from', |
615 dest="from_", |
570 dest='from_', |
616 action=SelectAction, |
571 action=SelectAction, type=str, |
617 type=str, |
572 default=FromArg.MIXED, metavar='SOURCE', |
618 default=FromArg.MIXED, |
|
619 metavar="SOURCE", |
573 choices=choices_from_enum(FromArg), |
620 choices=choices_from_enum(FromArg), |
574 help='R|where to find license information\n' |
621 help="R|where to find license information\n" |
575 '"meta", "classifier, "mixed", "all"\n' |
622 '"meta", "classifier, "mixed", "all"\n' |
576 '(default: %(default)s)') |
623 "(default: %(default)s)", |
577 common_options.add_argument( |
624 ) |
578 '-o', '--order', |
625 common_options.add_argument( |
579 action=SelectAction, type=str, |
626 "-o", |
580 default=OrderArg.NAME, metavar='COL', |
627 "--order", |
|
628 action=SelectAction, |
|
629 type=str, |
|
630 default=OrderArg.NAME, |
|
631 metavar="COL", |
581 choices=choices_from_enum(OrderArg), |
632 choices=choices_from_enum(OrderArg), |
582 help='R|order by column\n' |
633 help="R|order by column\n" |
583 '"name", "license", "author", "url"\n' |
634 '"name", "license", "author", "url"\n' |
584 '(default: %(default)s)') |
635 "(default: %(default)s)", |
|
636 ) |
585 common_options.add_argument( |
637 common_options.add_argument( |
586 '--summary', |
638 '--summary', |
587 action='store_true', |
639 action='store_true', |
588 default=False, |
640 default=False, |
589 help='dump summary of each license') |
641 help='dump summary of each license') |
595 common_options.add_argument( |
647 common_options.add_argument( |
596 '--output-file', |
648 '--output-file', |
597 action='store', type=str, |
649 action='store', type=str, |
598 help='save license list to file') |
650 help='save license list to file') |
599 common_options.add_argument( |
651 common_options.add_argument( |
600 '-i', '--ignore-packages', |
652 "-i", |
601 action='store', type=str, |
653 "--ignore-packages", |
602 nargs='+', metavar='PKG', |
654 action="store", |
|
655 type=str, |
|
656 nargs="+", |
|
657 metavar="PKG", |
603 default=[], |
658 default=[], |
604 help='ignore package name in dumped list') |
659 help="ignore package name in dumped list", |
605 common_options.add_argument( |
660 ) |
606 '-p', '--packages', |
661 common_options.add_argument( |
607 action='store', type=str, |
662 "-p", |
608 nargs='+', metavar='PKG', |
663 "--packages", |
|
664 action="store", |
|
665 type=str, |
|
666 nargs="+", |
|
667 metavar="PKG", |
609 default=[], |
668 default=[], |
610 help='only include selected packages in output') |
669 help="only include selected packages in output", |
|
670 ) |
611 common_options.add_argument( |
671 common_options.add_argument( |
612 '--local-only', |
672 '--local-only', |
613 action='store_true', |
673 action='store_true', |
614 default=False, |
674 default=False, |
615 help='include only local packages') |
675 help='include only local packages') |
618 action='store_true', |
678 action='store_true', |
619 default=False, |
679 default=False, |
620 help='include only packages of the user site dir') |
680 help='include only packages of the user site dir') |
621 |
681 |
622 format_options.add_argument( |
682 format_options.add_argument( |
623 '-s', '--with-system', |
683 "-s", |
624 action='store_true', |
684 "--with-system", |
625 default=False, |
|
626 help='dump with system packages') |
|
627 format_options.add_argument( |
|
628 '-a', '--with-authors', |
|
629 action='store_true', |
|
630 default=False, |
|
631 help='dump with package authors') |
|
632 format_options.add_argument( |
|
633 '-u', '--with-urls', |
|
634 action='store_true', |
|
635 default=False, |
|
636 help='dump with package urls') |
|
637 format_options.add_argument( |
|
638 '-d', '--with-description', |
|
639 action='store_true', |
|
640 default=False, |
|
641 help='dump with short package description') |
|
642 format_options.add_argument( |
|
643 '-l', '--with-license-file', |
|
644 action='store_true', |
|
645 default=False, |
|
646 help='dump with location of license file and ' |
|
647 'contents, most useful with JSON output') |
|
648 format_options.add_argument( |
|
649 '--no-license-path', |
|
650 action='store_true', |
|
651 default=False, |
|
652 help='I|when specified together with option -l, ' |
|
653 'suppress location of license file output') |
|
654 format_options.add_argument( |
|
655 '--with-notice-file', |
|
656 action='store_true', |
|
657 default=False, |
|
658 help='I|when specified together with option -l, ' |
|
659 'dump with location of license file and contents') |
|
660 format_options.add_argument( |
|
661 '--filter-strings', |
|
662 action="store_true", |
685 action="store_true", |
663 default=False, |
686 default=False, |
664 help='filter input according to code page') |
687 help="dump with system packages", |
665 format_options.add_argument( |
688 ) |
666 '--filter-code-page', |
689 format_options.add_argument( |
667 action="store", type=str, |
690 "-a", |
|
691 "--with-authors", |
|
692 action="store_true", |
|
693 default=False, |
|
694 help="dump with package authors", |
|
695 ) |
|
696 format_options.add_argument( |
|
697 "-u", |
|
698 "--with-urls", |
|
699 action="store_true", |
|
700 default=False, |
|
701 help="dump with package urls", |
|
702 ) |
|
703 format_options.add_argument( |
|
704 "-d", |
|
705 "--with-description", |
|
706 action="store_true", |
|
707 default=False, |
|
708 help="dump with short package description", |
|
709 ) |
|
710 format_options.add_argument( |
|
711 "-l", |
|
712 "--with-license-file", |
|
713 action="store_true", |
|
714 default=False, |
|
715 help="dump with location of license file and " |
|
716 "contents, most useful with JSON output", |
|
717 ) |
|
718 format_options.add_argument( |
|
719 "--no-license-path", |
|
720 action="store_true", |
|
721 default=False, |
|
722 help="I|when specified together with option -l, " |
|
723 "suppress location of license file output", |
|
724 ) |
|
725 format_options.add_argument( |
|
726 "--with-notice-file", |
|
727 action="store_true", |
|
728 default=False, |
|
729 help="I|when specified together with option -l, " |
|
730 "dump with location of license file and contents", |
|
731 ) |
|
732 format_options.add_argument( |
|
733 "--filter-strings", |
|
734 action="store_true", |
|
735 default=False, |
|
736 help="filter input according to code page", |
|
737 ) |
|
738 format_options.add_argument( |
|
739 "--filter-code-page", |
|
740 action="store", |
|
741 type=str, |
668 default="latin1", |
742 default="latin1", |
669 metavar="CODE", |
743 metavar="CODE", |
670 help='I|specify code page for filtering ' |
744 help="I|specify code page for filtering " "(default: %(default)s)", |
671 '(default: %(default)s)') |
745 ) |
672 |
746 |
673 verify_options.add_argument( |
747 verify_options.add_argument( |
674 '--fail-on', |
748 "--fail-on", |
675 action='store', type=str, |
749 action="store", |
|
750 type=str, |
676 default=None, |
751 default=None, |
677 help='fail (exit with code 1) on the first occurrence ' |
752 help="fail (exit with code 1) on the first occurrence " |
678 'of the licenses of the semicolon-separated list') |
753 "of the licenses of the semicolon-separated list", |
|
754 ) |
679 verify_options.add_argument( |
755 verify_options.add_argument( |
680 '--allow-only', |
756 "--allow-only", |
681 action='store', type=str, |
757 action="store", |
|
758 type=str, |
682 default=None, |
759 default=None, |
683 help='fail (exit with code 1) on the first occurrence ' |
760 help="fail (exit with code 1) on the first occurrence " |
684 'of the licenses not in the semicolon-separated list') |
761 "of the licenses not in the semicolon-separated list", |
|
762 ) |
685 |
763 |
686 return parser |
764 return parser |
687 |
765 |
688 |
766 |
689 def main(): # pragma: no cover |
767 def main(): # pragma: no cover |
690 os.environ["_PIP_USE_IMPORTLIB_METADATA"] = "False" |
|
691 # patched for 3.11+ compatibility |
|
692 |
|
693 parser = create_parser() |
768 parser = create_parser() |
694 args = parser.parse_args() |
769 args = parser.parse_args() |
695 |
770 |
696 output_string = create_output_string(args) |
771 output_string = create_output_string(args) |
697 |
772 |