Mon, 28 Mar 2022 18:13:15 +0200
Finished implementing the basic functionality of a license lister for pip installed packages (based on pip-licenses).
--- a/eric7.epj Sun Mar 27 19:56:41 2022 +0200 +++ b/eric7.epj Mon Mar 28 18:13:15 2022 +0200 @@ -61,12 +61,17 @@ "CopyrightMinFileSize": 0, "DocstringType": "eric", "EnabledCheckerCategories": "C, D, E, M, N, Y, W", - "ExcludeFiles": "*/ThirdParty/*, */coverage/*, */Ui_*.py, */Examples/*, */*_rc.py,*/pycodestyle.py,*/pyflakes/checker.py,*/mccabe.py,*/eradicate.py,*/ast_unparse.py", + "ExcludeFiles": "*/ThirdParty/*, */coverage/*, */Ui_*.py, */Examples/*, */*_rc.py,*/pycodestyle.py,*/pyflakes/checker.py,*/mccabe.py,*/eradicate.py,*/ast_unparse.py,*/piplicenses.py", "ExcludeMessages": "C101,E265,E266,E305,E402,M201,M301,M302,M303,M304,M305,M306,M307,M308,M311,M312,M313,M314,M315,M321,M701,M702,M811,M834,N802,N803,N807,N808,N821,W293,W504,Y119,Y401,Y402", "FixCodes": "", "FixIssues": false, "FutureChecker": "", "HangClosing": false, + "ImportsChecker": { + "ApplicationPackageNames": [], + "BanRelativeImports": "", + "BannedModules": [] + }, "IncludeMessages": "", "LineComplexity": 25, "LineComplexityScore": 10,
--- a/eric7/PipInterface/Pip.py Sun Mar 27 19:56:41 2022 +0200 +++ b/eric7/PipInterface/Pip.py Mon Mar 28 18:13:15 2022 +0200 @@ -975,12 +975,19 @@ ## License handling methods below ####################################################################### - def getLicenses(self, envName, summary=False): + def getLicenses(self, envName, localPackages=True, usersite=False, + summary=False): """ Public method to get the licenses per package for a given environment. @param envName name of the environment to get the licenses for @type str + @param localPackages flag indicating to get the licenses for local + packages only + @type bool + @param usersite flag indicating to get the licenses for packages + installed in user-site directory only + @type bool @param summary flag indicating to get a summary listing (defaults to False) @type bool (optional) @@ -999,12 +1006,14 @@ args = [ "-c", content, - "--format", - "json", "--from", "mixed", "--with-system", ] + if localPackages: + args.append("--local-only") + if usersite: + args.append("--user-only") if summary: args.append("--summary") @@ -1019,15 +1028,22 @@ return licenses - def getLicensesSummary(self, envName): + def getLicensesSummary(self, envName, localPackages=True, usersite=False): """ Public method to get a summary of licenses found in a given environment. @param envName name of the environment to get the licenses summary for @type str + @param localPackages flag indicating to get the licenses summary for + local packages only + @type bool + @param usersite flag indicating to get the licenses summary for + packages installed in user-site directory only + @type bool @return list of dictionaries containing the license and the count of packages @rtype dict """ - return self.getLicenses(envName, summary=True) + return self.getLicenses(envName, localPackages=localPackages, + usersite=usersite, summary=True)
--- a/eric7/PipInterface/PipLicensesDialog.py Sun Mar 27 19:56:41 2022 +0200 +++ b/eric7/PipInterface/PipLicensesDialog.py Mon Mar 28 18:13:15 2022 +0200 @@ -7,15 +7,17 @@ Module implementing a dialog to show the licenses of an environment. """ -from PyQt6.QtCore import Qt +from PyQt6.QtCore import pyqtSlot, Qt from PyQt6.QtWidgets import QDialog, QTreeWidgetItem +from EricGui.EricOverrideCursor import EricOverrideCursor + from .Ui_PipLicensesDialog import Ui_PipLicensesDialog class PipLicensesDialog(QDialog, Ui_PipLicensesDialog): """ - Class documentation goes here. + Class implementing a dialog to show the licenses of an environment. """ LicensesPackageColumn = 0 LicensesVersionColumn = 1 @@ -24,14 +26,21 @@ SummaryCountColumn = 0 SummaryLicenseColumn = 1 - def __init__(self, pip, environment, parent=None): + def __init__(self, pip, environment, localPackages=True, usersite=False, + parent=None): """ Constructor @param pip reference to the pip interface object @type Pip - @param envName name of the environment to show the licenses for + @param environment name of the environment to show the licenses for @type str + @param localPackages flag indicating to show the licenses for local + packages only + @type bool + @param usersite flag indicating to show the licenses for packages + installed in user-site directory only + @type bool @param parent reference to the parent widget (defaults to None) @type QWidget (optional) """ @@ -39,20 +48,45 @@ self.setupUi(self) self.__pip = pip + self.__environment = environment + + self.localCheckBox.setChecked(localPackages) + self.userCheckBox.setChecked(usersite) + + self.localCheckBox.toggled.connect(self.__refreshLicenses) + self.userCheckBox.toggled.connect(self.__refreshLicenses) if environment: self.environmentLabel.setText("<b>{0}</b>".format( - self.tr("Licenses of {0}").format(environment) + self.tr('Licenses of "{0}"').format(environment) )) + else: + # That should never happen; play it safe. + self.environmentLabel.setText(self.tr("No environment specified.")) + + self.__refreshLicenses() + + @pyqtSlot() + def __refreshLicenses(self): + """ + Private slot to refresh the license lists. + """ + with EricOverrideCursor(): + self.licensesList.clear() + self.summaryList.clear() # step 1: show the licenses per package - self.licensesList.setUpdatesEnabled(True) - licenses = self.__pip.getLicenses(environment) - for license in licenses: + self.licensesList.setUpdatesEnabled(False) + licenses = self.__pip.getLicenses( + self.__environment, + localPackages=self.localCheckBox.isChecked(), + usersite=self.userCheckBox.isChecked(), + ) + for lic in licenses: QTreeWidgetItem(self.licensesList, [ - license["Name"], - license["Version"], - license["License"].replace("; ", "\n"), + lic["Name"], + lic["Version"], + lic["License"].replace("; ", "\n"), ]) self.licensesList.sortItems( @@ -63,12 +97,16 @@ self.licensesList.setUpdatesEnabled(True) # step 2: show the licenses summary - self.summaryList.setUpdatesEnabled(True) - licenses = self.__pip.getLicensesSummary(environment) - for license in licenses: + self.summaryList.setUpdatesEnabled(False) + licenses = self.__pip.getLicensesSummary( + self.__environment, + localPackages=self.localCheckBox.isChecked(), + usersite=self.userCheckBox.isChecked(), + ) + for lic in licenses: QTreeWidgetItem(self.summaryList, [ - "{0:4d}".format(license["Count"]), - license["License"].replace("; ", "\n"), + "{0:4d}".format(lic["Count"]), + lic["License"].replace("; ", "\n"), ]) self.summaryList.sortItems( @@ -77,7 +115,3 @@ for col in range(self.summaryList.columnCount()): self.summaryList.resizeColumnToContents(col) self.summaryList.setUpdatesEnabled(True) - - else: - # That should never happen; play it safe. - self.environmentLabel.setText(self.tr("No environment specified."))
--- a/eric7/PipInterface/PipLicensesDialog.ui Sun Mar 27 19:56:41 2022 +0200 +++ b/eric7/PipInterface/PipLicensesDialog.ui Mon Mar 28 18:13:15 2022 +0200 @@ -21,6 +21,33 @@ <widget class="QLabel" name="environmentLabel"/> </item> <item> + <layout class="QHBoxLayout" name="horizontalLayout_4"> + <item> + <widget class="QCheckBox" name="localCheckBox"> + <property name="toolTip"> + <string>Select to show only licenses of locally installed packages</string> + </property> + <property name="text"> + <string>Local packages only</string> + </property> + <property name="checked"> + <bool>true</bool> + </property> + </widget> + </item> + <item> + <widget class="QCheckBox" name="userCheckBox"> + <property name="toolTip"> + <string>Select to show only licenses of packages installed to the user-site</string> + </property> + <property name="text"> + <string>User-Site only</string> + </property> + </widget> + </item> + </layout> + </item> + <item> <widget class="QTreeWidget" name="licensesList"> <property name="sizePolicy"> <sizepolicy hsizetype="Expanding" vsizetype="Expanding"> @@ -107,6 +134,12 @@ </item> </layout> </widget> + <tabstops> + <tabstop>localCheckBox</tabstop> + <tabstop>userCheckBox</tabstop> + <tabstop>licensesList</tabstop> + <tabstop>summaryList</tabstop> + </tabstops> <resources/> <connections> <connection>
--- a/eric7/PipInterface/PipPackagesWidget.py Sun Mar 27 19:56:41 2022 +0200 +++ b/eric7/PipInterface/PipPackagesWidget.py Mon Mar 28 18:13:15 2022 +0200 @@ -1824,5 +1824,21 @@ from .PipLicensesDialog import PipLicensesDialog environment = self.environmentsComboBox.currentText() - dlg = PipLicensesDialog(self.__pip, environment, self) + localPackages = ( + self.localDepCheckBox.isChecked() + if self.viewToggleButton.isChecked() else + self.localCheckBox.isChecked() + ) + usersite = ( + self.userDepCheckBox.isChecked() + if self.viewToggleButton.isChecked() else + self.userCheckBox.isChecked() + ) + dlg = PipLicensesDialog( + self.__pip, + environment, + localPackages=localPackages, + usersite=usersite, + parent=self + ) dlg.exec()
--- a/eric7/PipInterface/piplicenses.py Sun Mar 27 19:56:41 2022 +0200 +++ b/eric7/PipInterface/piplicenses.py Mon Mar 28 18:13:15 2022 +0200 @@ -26,46 +26,47 @@ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. """ + +# +# Modified to be used within the eric-ide project. +# +# Copyright (c) 2022 Detlev Offenbach <detlev@die-offenbachs.de> +# + import argparse import codecs import glob +import json import os import sys from collections import Counter from email import message_from_string from email.parser import FeedParser from enum import Enum, auto -from functools import partial from typing import List, Optional, Sequence, Text -try: - from pip._internal.utils.misc import get_installed_distributions -except ImportError: # pragma: no cover + +def get_installed_distributions(local_only=True, user_only=False): try: - from pip import get_installed_distributions + from pip._internal.metadata import get_environment except ImportError: - def get_installed_distributions(): - from pip._internal.metadata import ( - get_default_environment, get_environment, - ) - from pip._internal.metadata.pkg_resources import ( - Distribution as _Dist, - ) - from pip._internal.utils.compat import stdlib_pkgs + # For backward compatibility with pip version 20.3.4 + from pip._internal.utils import misc + return misc.get_installed_distributions( + local_only=local_only, + user_only=user_only + ) + else: + from pip._internal.utils.compat import stdlib_pkgs + dists = get_environment(None).iter_installed_distributions( + local_only=local_only, + user_only=user_only, + skip=stdlib_pkgs, + include_editables=True, + editables_only=False, + ) + return [d._dist for d in dists] - env = get_default_environment() - dists = env.iter_installed_distributions( - local_only=True, - skip=stdlib_pkgs, - include_editables=True, - editables_only=False, - user_only=False, - ) - return [dist._dist for dist in dists] - -from prettytable import PrettyTable - -open = open # allow monkey patching __pkgname__ = 'pip-licenses' __version__ = '3.5.3' @@ -90,12 +91,6 @@ ) -SUMMARY_FIELD_NAMES = ( - 'Count', - 'License', -) - - DEFAULT_OUTPUT_FIELDS = ( 'Name', 'Version', @@ -217,7 +212,10 @@ return pkg_info - pkgs = get_installed_distributions() + pkgs = get_installed_distributions( + local_only=args.local_only, + user_only=args.user_only, + ) ignore_pkgs_as_lower = [pkg.lower() for pkg in args.ignore_packages] pkgs_as_lower = [pkg.lower() for pkg in args.packages] @@ -287,7 +285,6 @@ args.from_, pkg['license_classifier'], pkg['license']) license_str = '; '.join(sorted(license_set)) row[field] = license_str -## row.append(license_str) elif field == 'License-Classifier': row[field] = ('; '.join(sorted(pkg['license_classifier'])) or LICENSE_UNKNOWN) @@ -300,160 +297,20 @@ return licenses -def create_summary_table(args: "CustomNamespace"): +def create_summary_list(args: "CustomNamespace"): counts = Counter( '; '.join(sorted(select_license_by_source( args.from_, pkg['license_classifier'], pkg['license']))) for pkg in get_packages(args)) - table = factory_styled_table_with_args(args, SUMMARY_FIELD_NAMES) + licenses = [] for license, count in counts.items(): - table.add_row([count, license]) - return table - - -class JsonTable(): - - def _format_row(self, row, options): - resrow = {} - for (field, value) in zip(self._field_names, row): - if field not in options["fields"]: - continue - - resrow[field] = value - - return resrow - - def get_string(self, **kwargs): - # import included here in order to limit dependencies - # if not interested in JSON output, - # then the dependency is not required - import json - - options = self._get_options(kwargs) - rows = self._get_rows(options) - formatted_rows = self._format_rows(rows, options) - - lines = [] - for row in formatted_rows: - lines.append(row) - - return json.dumps(lines, indent=2, sort_keys=True) -## -## -##class JsonLicenseFinderTable(JsonPrettyTable): -## def _format_row(self, row, options): -## resrow = {} -## for (field, value) in zip(self._field_names, row): -## if field == 'Name': -## resrow['name'] = value -## -## if field == 'Version': -## resrow['version'] = value -## -## if field == 'License': -## resrow['licenses'] = [value] -## -## return resrow -## -## def get_string(self, **kwargs): -## # import included here in order to limit dependencies -## # if not interested in JSON output, -## # then the dependency is not required -## import json -## -## options = self._get_options(kwargs) -## rows = self._get_rows(options) -## formatted_rows = self._format_rows(rows, options) -## -## lines = [] -## for row in formatted_rows: -## lines.append(row) -## -## return json.dumps(lines, sort_keys=True) -## -## -##class CSVPrettyTable(PrettyTable): -## """PrettyTable-like class exporting to CSV""" -## -## def get_string(self, **kwargs): -## -## def esc_quotes(val): -## """ -## Meta-escaping double quotes -## https://tools.ietf.org/html/rfc4180 -## """ -## try: -## return val.replace('"', '""') -## except UnicodeDecodeError: # pragma: no cover -## return val.decode('utf-8').replace('"', '""') -## except UnicodeEncodeError: # pragma: no cover -## return val.encode('unicode_escape').replace('"', '""') -## -## options = self._get_options(kwargs) -## rows = self._get_rows(options) -## formatted_rows = self._format_rows(rows, options) -## -## lines = [] -## formatted_header = ','.join(['"%s"' % (esc_quotes(val), ) -## for val in self._field_names]) -## lines.append(formatted_header) -## for row in formatted_rows: -## formatted_row = ','.join(['"%s"' % (esc_quotes(val), ) -## for val in row]) -## lines.append(formatted_row) -## -## return '\n'.join(lines) -## -## -##class PlainVerticalTable(PrettyTable): -## """PrettyTable for outputting to a simple non-column based style. -## -## When used with --with-license-file, this style is similar to the default -## style generated from Angular CLI's --extractLicenses flag. -## """ -## -## def get_string(self, **kwargs): -## options = self._get_options(kwargs) -## rows = self._get_rows(options) -## -## output = '' -## for row in rows: -## for v in row: -## output += '{}\n'.format(v) -## output += '\n' -## -## return output -## -## -def factory_styled_table_with_args( - args: "CustomNamespace", output_fields=DEFAULT_OUTPUT_FIELDS): - table = PrettyTable() - table.field_names = output_fields - table.align = 'l' - table.border = args.format_ in (FormatArg.MARKDOWN, FormatArg.RST, - FormatArg.CONFLUENCE, FormatArg.JSON) - table.header = True - - if args.format_ == FormatArg.MARKDOWN: - table.junction_char = '|' - table.hrules = RULE_HEADER - elif args.format_ == FormatArg.RST: - table.junction_char = '+' - table.hrules = RULE_ALL - elif args.format_ == FormatArg.CONFLUENCE: - table.junction_char = '|' - table.hrules = RULE_NONE - elif args.format_ == FormatArg.JSON: - table = JsonPrettyTable(table.field_names) - elif args.format_ == FormatArg.JSON_LICENSE_FINDER: - table = JsonLicenseFinderTable(table.field_names) - elif args.format_ == FormatArg.CSV: - table = CSVPrettyTable(table.field_names) - elif args.format_ == FormatArg.PLAIN_VERTICAL: - table = PlainVerticalTable(table.field_names) - - return table + licenses.append({ + "Count": count, + "License": license, + }) + + return licenses def find_license_from_classifier(message): @@ -513,56 +370,15 @@ return output_fields -def get_sortby(args: "CustomNamespace"): - if args.summary and args.order == OrderArg.COUNT: - return 'Count' - elif args.summary or args.order == OrderArg.LICENSE: - return 'License' - elif args.order == OrderArg.NAME: - return 'Name' - elif args.order == OrderArg.AUTHOR and args.with_authors: - return 'Author' - elif args.order == OrderArg.URL and args.with_urls: - return 'URL' - - return 'Name' - - def create_output_string(args: "CustomNamespace"): output_fields = get_output_fields(args) if args.summary: - table = create_summary_table(args) + licenses = create_summary_list(args) else: - table = create_licenses_list(args, output_fields) + licenses = create_licenses_list(args, output_fields) - return json.dumps(table) -## -## sortby = get_sortby(args) -## -## if args.format_ == FormatArg.HTML: -## return table.get_html_string(fields=output_fields, sortby=sortby) -## else: -## return table.get_string(fields=output_fields, sortby=sortby) -## -## -##def create_warn_string(args: "CustomNamespace"): -## warn_messages = [] -## warn = partial(output_colored, '33') -## -## if args.with_license_file and not args.format_ == FormatArg.JSON: -## message = warn(('Due to the length of these fields, this option is ' -## 'best paired with --format=json.')) -## warn_messages.append(message) -## -## if args.summary and (args.with_authors or args.with_urls): -## message = warn(('When using this option, only --order=count or ' -## '--order=license has an effect for the --order ' -## 'option. And using --with-authors and --with-urls ' -## 'will be ignored.')) -## warn_messages.append(message) -## -## return '\n'.join(warn_messages) + return json.dumps(licenses) class CustomHelpFormatter(argparse.HelpFormatter): # pragma: no cover @@ -606,8 +422,9 @@ class CustomNamespace(argparse.Namespace): from_: "FromArg" order: "OrderArg" - format_: "FormatArg" summary: bool + local_only: bool + user_only:bool output_file: str ignore_packages: List[str] packages: List[str] @@ -673,18 +490,6 @@ URL = U = auto() -class FormatArg(NoValueEnum): - PLAIN = P = auto() - PLAIN_VERTICAL = auto() - MARKDOWN = MD = M = auto() - RST = REST = R = auto() - CONFLUENCE = C = auto() - HTML = H = auto() - JSON = J = auto() - JSON_LICENSE_FINDER = JLF = auto() - CSV = auto() - - def value_to_enum_key(value: str) -> str: return value.replace('-', '_').upper() @@ -701,7 +506,6 @@ MAP_DEST_TO_ENUM = { 'from_': FromArg, 'order': OrderArg, - 'format_': FormatArg, } @@ -749,17 +553,6 @@ '"name", "license", "author", "url"\n' '(default: %(default)s)') common_options.add_argument( - '-f', '--format', - dest='format_', - action=SelectAction, type=str, - default=FormatArg.PLAIN, metavar='STYLE', - choices=choices_from_enum(FormatArg), - help='R|dump as set format style\n' - '"plain", "plain-vertical" "markdown", "rst", \n' - '"confluence", "html", "json", \n' - '"json-license-finder", "csv"\n' - '(default: %(default)s)') - common_options.add_argument( '--summary', action='store_true', default=False, @@ -780,6 +573,17 @@ nargs='+', metavar='PKG', default=[], help='only include selected packages in output') + common_options.add_argument( + '--local-only', + action='store_true', + default=False, + help='include only local packages') + common_options.add_argument( + '--user-only', + action='store_true', + default=False, + help='include only packages of the user site dir') + format_options.add_argument( '-s', '--with-system', action='store_true', @@ -845,33 +649,6 @@ 'of the licenses not in the semicolon-separated list') return parser -## -## -##def output_colored(code, text, is_bold=False): -## """ -## Create function to output with color sequence -## """ -## if is_bold: -## code = '1;%s' % code -## -## return '\033[%sm%s\033[0m' % (code, text) -## -## -##def save_if_needs(output_file, output_string): -## """ -## Save to path given by args -## """ -## if output_file is None: -## return -## -## try: -## with open(output_file, 'w', encoding='utf-8') as f: -## f.write(output_string) -## sys.stdout.write('created path: ' + output_file + '\n') -## sys.exit(0) -## except IOError: -## sys.stderr.write('check path: --output-file\n') -## sys.exit(1) def main(): # pragma: no cover @@ -880,13 +657,7 @@ output_string = create_output_string(args) -## output_file = args.output_file -## save_if_needs(output_file, output_string) -## print(output_string) -## warn_string = create_warn_string(args) -## if warn_string: -## print(warn_string, file=sys.stderr) if __name__ == '__main__': # pragma: no cover
--- a/scripts/install.py Sun Mar 27 19:56:41 2022 +0200 +++ b/scripts/install.py Mon Mar 28 18:13:15 2022 +0200 @@ -1610,7 +1610,6 @@ "jedi": ("jedi", ""), "packaging": ("packaging", ""), "pipdeptree": ("pipdeptree", ""), - "pip-licenses": ("piplicenses", ""), } if not ignorePyqt6Tools: optionalModulesList["qt6-applications"] = ("qt6_applications", "")