Script 'mail_helper' called by obssrc Hello community, here is the log from the commit of package python-pip-licenses for openSUSE:Factory checked in at 2023-04-12 15:48:22 ++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++ Comparing /work/SRC/openSUSE:Factory/python-pip-licenses (Old) and /work/SRC/openSUSE:Factory/.python-pip-licenses.new.19717 (New) ++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Package is "python-pip-licenses" Wed Apr 12 15:48:22 2023 rev:10 rq:1078727 version:4.2.0 Changes: -------- --- /work/SRC/openSUSE:Factory/python-pip-licenses/python-pip-licenses.changes 2023-02-06 14:15:35.924625021 +0100 +++ /work/SRC/openSUSE:Factory/.python-pip-licenses.new.19717/python-pip-licenses.changes 2023-04-12 15:48:23.034752858 +0200 @@ -1,0 +2,15 @@ +Wed Apr 12 13:26:16 UTC 2023 - Matej Cepl <mc...@suse.com> + +- Update to 4.2.0: + - Implement new option --with-maintainers + - Implement new option --python + - Allow version spec in --ignore-packages parameters + - When the Author field is UNKNOWN, the output is automatically + completed from Author-email + - When the home-page field is UNKNOWN, the output is + automatically completed from Project-URL +- Update to 4.1.0: + - Support case-insensitive license name matching around + --fail-on and --allow-only parameters + +------------------------------------------------------------------- Old: ---- pip-licenses-4.0.3.tar.gz New: ---- pip-licenses-4.2.0.tar.gz ++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++ Other differences: ------------------ ++++++ python-pip-licenses.spec ++++++ --- /var/tmp/diff_new_pack.TDq5gS/_old 2023-04-12 15:48:23.522755719 +0200 +++ /var/tmp/diff_new_pack.TDq5gS/_new 2023-04-12 15:48:23.526755742 +0200 @@ -18,7 +18,7 @@ %define skip_python2 1 Name: python-pip-licenses -Version: 4.0.3 +Version: 4.2.0 Release: 0 Summary: Python packages license list License: MIT @@ -65,7 +65,8 @@ %check export LANG=en_US.UTF-8 # gh#raimon49/pip-licenses#120 for test_from_all -%pytest -k 'not test_from_all' +# gh#raimon49/pip-licenses#156 for test_different_python +%pytest -k 'not (test_from_all or test_different_python)' %python_expand PYTHONPATH=%{buildroot}%{$python_sitelib} %{buildroot}%{_bindir}/pip-licenses-%{$python_bin_suffix} -s %post ++++++ pip-licenses-4.0.3.tar.gz -> pip-licenses-4.2.0.tar.gz ++++++ diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' '--exclude=.svnignore' old/pip-licenses-4.0.3/CHANGELOG.md new/pip-licenses-4.2.0/CHANGELOG.md --- old/pip-licenses-4.0.3/CHANGELOG.md 2022-12-08 12:58:08.000000000 +0100 +++ new/pip-licenses-4.2.0/CHANGELOG.md 2023-04-12 11:58:29.000000000 +0200 @@ -1,5 +1,17 @@ ## CHANGELOG +### 4.2.0 + +* Implement new option `--with-maintainers` +* Implement new option `--python` +* Allow version spec in `--ignore-packages` parameters +* When the `Author` field is `UNKNOWN`, the output is automatically completed from `Author-email` +* When the `home-page` field is `UNKNOWN`, the output is automatically completed from `Project-URL` + +### 4.1.0 + +* Support case-insensitive license name matching around `--fail-on` and `--allow-only` parameters + ### 4.0.3 * Escape unicode output (to e.g. `{`) in the html output diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' '--exclude=.svnignore' old/pip-licenses-4.0.3/PKG-INFO new/pip-licenses-4.2.0/PKG-INFO --- old/pip-licenses-4.0.3/PKG-INFO 2022-12-08 13:02:14.390000000 +0100 +++ new/pip-licenses-4.2.0/PKG-INFO 2023-04-12 13:24:35.490549600 +0200 @@ -1,6 +1,6 @@ Metadata-Version: 2.1 Name: pip-licenses -Version: 4.0.3 +Version: 4.2.0 Summary: Dump the software license list of Python packages installed with pip. Home-page: https://github.com/raimon49/pip-licenses Author: raimon @@ -44,6 +44,7 @@ - `Common options <#common-options>`__ + - `Option: python <#option-python>`__ - `Option: from <#option-from>`__ - `Option: order <#option-order>`__ - `Option: format <#option-format>`__ @@ -66,6 +67,7 @@ - `Option: with-system <#option-with-system>`__ - `Option: with-authors <#option-with-authors>`__ + - `Option: with-maintainers <#option-with-maintainers>`__ - `Option: with-urls <#option-with-urls>`__ - `Option: with-description <#option-with-description>`__ - `Option: with-license-file <#option-with-license-file>`__ @@ -150,6 +152,28 @@ Common options ~~~~~~~~~~~~~~ +Option: python +^^^^^^^^^^^^^^ + +By default, this tools finds the packages from the environment +pip-licenses is launched from, by searching in current python's +``sys.path`` folders. In the case you want to search for packages in an +other environment (e.g. if you want to run pip-licenses from its own +isolated environment), you can specify a path to a python executable. +The packages will be searched for in the given python's ``sys.path``, +free of pip-licenses dependencies. + +.. code:: bash + + (venv) $ pip-licenses --with-system | grep pip + pip 22.3.1 MIT License + pip-licenses 4.1.0 MIT License + +.. code:: bash + + (venv) $ pip-licenses --python=</path/to/other/env>/bin/python --with-system | grep pip + pip 23.0.1 MIT License + Option: from ^^^^^^^^^^^^ @@ -458,6 +482,17 @@ setuptools 38.5.0 UNKNOWN wcwidth 0.2.5 MIT License +Packages can also be specified with a version, only ignoring that +specific version. + +.. code:: bash + + (venv) $ pip-licenses --with-system --ignore-packages django pytz:2017.3 + Name Version License + prettytable 3.5.0 BSD License + setuptools 38.5.0 UNKNOWN + wcwidth 0.2.5 MIT License + Option: packages ^^^^^^^^^^^^^^^^ @@ -519,6 +554,16 @@ Django 2.0.2 BSD Django Software Foundation pytz 2017.3 MIT Stuart Bishop +Option: with-maintainers +^^^^^^^^^^^^^^^^^^^^^^^^ + +When executed with the ``--with-maintainers`` option, output with +maintainer of the package. + +**Note:** This option is available for users who want information about +the maintainer as well as the author. See +`#144 <https://github.com/raimon49/pip-licenses/issues/144>`__ + Option: with-urls ^^^^^^^^^^^^^^^^^ @@ -583,7 +628,7 @@ ^^^^^^^^^^^^^^^ Fail (exit with code 1) on the first occurrence of the licenses of the -semicolon-separated list +semicolon-separated list. The license name matching is case-insensitive. If ``--from=all``, the option will apply to the metadata license field. @@ -597,19 +642,23 @@ :: # keyring library has 2 licenses - $ pip-licenses | grep keyring - keyring 21.4.0 Python Software Foundation License, MIT License + $ pip-licenses --package keyring + Name Version License + keyring 23.0.1 MIT License; Python Software Foundation License # If just "Python Software Foundation License" is specified, it will fail. - $ pip-licenses --fail-on="Python Software Foundation License;" + $ pip-licenses --package keyring --fail-on="Python Software Foundation License;" $ echo $? 1 + # Matching is case-insensitive. Following check will fail: + $ pip-licenses --fail-on="mit license" + Option: allow-only ^^^^^^^^^^^^^^^^^^ Fail (exit with code 1) if none of the package licenses are in the -semicolon-separated list +semicolon-separated list. The license name matching is case-insensitive. If ``--from=all``, the option will apply to the metadata license field. @@ -624,11 +673,12 @@ # keyring library has 2 licenses $ pip-licenses --package keyring - Name Version License + Name Version License keyring 23.0.1 MIT License; Python Software Foundation License - # One or both licenses must be specified (order does not matter). Following checks will pass: + # One or both licenses must be specified (order and case does not matter). Following checks will pass: $ pip-licenses --package keyring --allow-only="MIT License" + $ pip-licenses --package keyring --allow-only="mit License" $ pip-licenses --package keyring --allow-only="BSD License;MIT License" $ pip-licenses --package keyring --allow-only="Python Software Foundation License" $ pip-licenses --package keyring --allow-only="Python Software Foundation License;MIT License" @@ -765,6 +815,27 @@ CHANGELOG --------- +.. _420: + +4.2.0 +~~~~~ + +- Implement new option ``--with-maintainers`` +- Implement new option ``--python`` +- Allow version spec in ``--ignore-packages`` parameters +- When the ``Author`` field is ``UNKNOWN``, the output is automatically + completed from ``Author-email`` +- When the ``home-page`` field is ``UNKNOWN``, the output is + automatically completed from ``Project-URL`` + +.. _410: + +4.1.0 +~~~~~ + +- Support case-insensitive license name matching around ``--fail-on`` + and ``--allow-only`` parameters + .. _403: 4.0.3 diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' '--exclude=.svnignore' old/pip-licenses-4.0.3/README.md new/pip-licenses-4.2.0/README.md --- old/pip-licenses-4.0.3/README.md 2022-11-06 04:18:47.000000000 +0100 +++ new/pip-licenses-4.2.0/README.md 2023-04-12 11:58:26.000000000 +0200 @@ -11,6 +11,7 @@ * [Usage](#usage) * [Command\-Line Options](#command-line-options) * [Common options](#common-options) + * [Option: python](#option-python) * [Option: from](#option-from) * [Option: order](#option-order) * [Option: format](#option-format) @@ -29,6 +30,7 @@ * [Format options](#format-options) * [Option: with\-system](#option-with-system) * [Option: with\-authors](#option-with-authors) + * [Option: with\-maintainers](#option-with-maintainers) * [Option: with\-urls](#option-with-urls) * [Option: with\-description](#option-with-description) * [Option: with\-license\-file](#option-with-license-file) @@ -97,6 +99,21 @@ ### Common options +#### Option: python + +By default, this tools finds the packages from the environment pip-licenses is launched from, by searching in current python's `sys.path` folders. In the case you want to search for packages in an other environment (e.g. if you want to run pip-licenses from its own isolated environment), you can specify a path to a python executable. The packages will be searched for in the given python's `sys.path`, free of pip-licenses dependencies. + +```bash +(venv) $ pip-licenses --with-system | grep pip + pip 22.3.1 MIT License + pip-licenses 4.1.0 MIT License +``` + +```bash +(venv) $ pip-licenses --python=</path/to/other/env>/bin/python --with-system | grep pip + pip 23.0.1 MIT License +``` + #### Option: from By default, this tool finds the license from [Trove Classifiers](https://pypi.org/classifiers/) or package Metadata. Some Python packages declare their license only in Trove Classifiers. @@ -351,6 +368,16 @@ wcwidth 0.2.5 MIT License ``` +Packages can also be specified with a version, only ignoring that specific version. + +```bash +(venv) $ pip-licenses --with-system --ignore-packages django pytz:2017.3 + Name Version License + prettytable 3.5.0 BSD License + setuptools 38.5.0 UNKNOWN + wcwidth 0.2.5 MIT License +``` + #### Option: packages When executed with the `packages` option, look at the package specified by argument from list output. @@ -403,6 +430,12 @@ pytz 2017.3 MIT Stuart Bishop ``` +#### Option: with-maintainers + +When executed with the `--with-maintainers` option, output with maintainer of the package. + +**Note:** This option is available for users who want information about the maintainer as well as the author. See [#144](https://github.com/raimon49/pip-licenses/issues/144) + #### Option: with-urls For packages without Metadata, the license is output as `UNKNOWN`. To get more package information, use the `--with-urls` option. @@ -446,7 +479,8 @@ #### Option: fail\-on -Fail (exit with code 1) on the first occurrence of the licenses of the semicolon-separated list +Fail (exit with code 1) on the first occurrence of the licenses of the semicolon-separated list. The license name +matching is case-insensitive. If `--from=all`, the option will apply to the metadata license field. @@ -456,18 +490,23 @@ **Note:** Packages with multiple licenses will fail if at least one license is included in the fail-on list. For example: ``` # keyring library has 2 licenses -$ pip-licenses | grep keyring - keyring 21.4.0 Python Software Foundation License, MIT License +$ pip-licenses --package keyring + Name Version License + keyring 23.0.1 MIT License; Python Software Foundation License # If just "Python Software Foundation License" is specified, it will fail. -$ pip-licenses --fail-on="Python Software Foundation License;" +$ pip-licenses --package keyring --fail-on="Python Software Foundation License;" $ echo $? 1 + +# Matching is case-insensitive. Following check will fail: +$ pip-licenses --fail-on="mit license" ``` #### Option: allow\-only -Fail (exit with code 1) if none of the package licenses are in the semicolon-separated list +Fail (exit with code 1) if none of the package licenses are in the semicolon-separated list. The license name +matching is case-insensitive. If `--from=all`, the option will apply to the metadata license field. @@ -478,11 +517,12 @@ ``` # keyring library has 2 licenses $ pip-licenses --package keyring - Name Version License + Name Version License keyring 23.0.1 MIT License; Python Software Foundation License -# One or both licenses must be specified (order does not matter). Following checks will pass: +# One or both licenses must be specified (order and case does not matter). Following checks will pass: $ pip-licenses --package keyring --allow-only="MIT License" +$ pip-licenses --package keyring --allow-only="mit License" $ pip-licenses --package keyring --allow-only="BSD License;MIT License" $ pip-licenses --package keyring --allow-only="Python Software Foundation License" $ pip-licenses --package keyring --allow-only="Python Software Foundation License;MIT License" diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' '--exclude=.svnignore' old/pip-licenses-4.0.3/pip_licenses.egg-info/PKG-INFO new/pip-licenses-4.2.0/pip_licenses.egg-info/PKG-INFO --- old/pip-licenses-4.0.3/pip_licenses.egg-info/PKG-INFO 2022-12-08 13:02:14.000000000 +0100 +++ new/pip-licenses-4.2.0/pip_licenses.egg-info/PKG-INFO 2023-04-12 13:24:35.000000000 +0200 @@ -1,6 +1,6 @@ Metadata-Version: 2.1 Name: pip-licenses -Version: 4.0.3 +Version: 4.2.0 Summary: Dump the software license list of Python packages installed with pip. Home-page: https://github.com/raimon49/pip-licenses Author: raimon @@ -44,6 +44,7 @@ - `Common options <#common-options>`__ + - `Option: python <#option-python>`__ - `Option: from <#option-from>`__ - `Option: order <#option-order>`__ - `Option: format <#option-format>`__ @@ -66,6 +67,7 @@ - `Option: with-system <#option-with-system>`__ - `Option: with-authors <#option-with-authors>`__ + - `Option: with-maintainers <#option-with-maintainers>`__ - `Option: with-urls <#option-with-urls>`__ - `Option: with-description <#option-with-description>`__ - `Option: with-license-file <#option-with-license-file>`__ @@ -150,6 +152,28 @@ Common options ~~~~~~~~~~~~~~ +Option: python +^^^^^^^^^^^^^^ + +By default, this tools finds the packages from the environment +pip-licenses is launched from, by searching in current python's +``sys.path`` folders. In the case you want to search for packages in an +other environment (e.g. if you want to run pip-licenses from its own +isolated environment), you can specify a path to a python executable. +The packages will be searched for in the given python's ``sys.path``, +free of pip-licenses dependencies. + +.. code:: bash + + (venv) $ pip-licenses --with-system | grep pip + pip 22.3.1 MIT License + pip-licenses 4.1.0 MIT License + +.. code:: bash + + (venv) $ pip-licenses --python=</path/to/other/env>/bin/python --with-system | grep pip + pip 23.0.1 MIT License + Option: from ^^^^^^^^^^^^ @@ -458,6 +482,17 @@ setuptools 38.5.0 UNKNOWN wcwidth 0.2.5 MIT License +Packages can also be specified with a version, only ignoring that +specific version. + +.. code:: bash + + (venv) $ pip-licenses --with-system --ignore-packages django pytz:2017.3 + Name Version License + prettytable 3.5.0 BSD License + setuptools 38.5.0 UNKNOWN + wcwidth 0.2.5 MIT License + Option: packages ^^^^^^^^^^^^^^^^ @@ -519,6 +554,16 @@ Django 2.0.2 BSD Django Software Foundation pytz 2017.3 MIT Stuart Bishop +Option: with-maintainers +^^^^^^^^^^^^^^^^^^^^^^^^ + +When executed with the ``--with-maintainers`` option, output with +maintainer of the package. + +**Note:** This option is available for users who want information about +the maintainer as well as the author. See +`#144 <https://github.com/raimon49/pip-licenses/issues/144>`__ + Option: with-urls ^^^^^^^^^^^^^^^^^ @@ -583,7 +628,7 @@ ^^^^^^^^^^^^^^^ Fail (exit with code 1) on the first occurrence of the licenses of the -semicolon-separated list +semicolon-separated list. The license name matching is case-insensitive. If ``--from=all``, the option will apply to the metadata license field. @@ -597,19 +642,23 @@ :: # keyring library has 2 licenses - $ pip-licenses | grep keyring - keyring 21.4.0 Python Software Foundation License, MIT License + $ pip-licenses --package keyring + Name Version License + keyring 23.0.1 MIT License; Python Software Foundation License # If just "Python Software Foundation License" is specified, it will fail. - $ pip-licenses --fail-on="Python Software Foundation License;" + $ pip-licenses --package keyring --fail-on="Python Software Foundation License;" $ echo $? 1 + # Matching is case-insensitive. Following check will fail: + $ pip-licenses --fail-on="mit license" + Option: allow-only ^^^^^^^^^^^^^^^^^^ Fail (exit with code 1) if none of the package licenses are in the -semicolon-separated list +semicolon-separated list. The license name matching is case-insensitive. If ``--from=all``, the option will apply to the metadata license field. @@ -624,11 +673,12 @@ # keyring library has 2 licenses $ pip-licenses --package keyring - Name Version License + Name Version License keyring 23.0.1 MIT License; Python Software Foundation License - # One or both licenses must be specified (order does not matter). Following checks will pass: + # One or both licenses must be specified (order and case does not matter). Following checks will pass: $ pip-licenses --package keyring --allow-only="MIT License" + $ pip-licenses --package keyring --allow-only="mit License" $ pip-licenses --package keyring --allow-only="BSD License;MIT License" $ pip-licenses --package keyring --allow-only="Python Software Foundation License" $ pip-licenses --package keyring --allow-only="Python Software Foundation License;MIT License" @@ -765,6 +815,27 @@ CHANGELOG --------- +.. _420: + +4.2.0 +~~~~~ + +- Implement new option ``--with-maintainers`` +- Implement new option ``--python`` +- Allow version spec in ``--ignore-packages`` parameters +- When the ``Author`` field is ``UNKNOWN``, the output is automatically + completed from ``Author-email`` +- When the ``home-page`` field is ``UNKNOWN``, the output is + automatically completed from ``Project-URL`` + +.. _410: + +4.1.0 +~~~~~ + +- Support case-insensitive license name matching around ``--fail-on`` + and ``--allow-only`` parameters + .. _403: 4.0.3 diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' '--exclude=.svnignore' old/pip-licenses-4.0.3/piplicenses.py new/pip-licenses-4.2.0/piplicenses.py --- old/pip-licenses-4.0.3/piplicenses.py 2022-12-08 12:57:32.000000000 +0100 +++ new/pip-licenses-4.2.0/piplicenses.py 2023-04-12 13:23:06.000000000 +0200 @@ -30,7 +30,9 @@ import argparse import codecs +import os import re +import subprocess import sys from collections import Counter from enum import Enum, auto @@ -47,13 +49,14 @@ from prettytable import PrettyTable if TYPE_CHECKING: - from typing import Iterator, Optional, Sequence + from email.message import Message + from typing import Callable, Dict, Iterator, Optional, Sequence open = open # allow monkey patching __pkgname__ = "pip-licenses" -__version__ = "4.0.3" +__version__ = "4.2.0" __author__ = "raimon" __license__ = "MIT" __summary__ = ( @@ -71,6 +74,7 @@ "NoticeFile", "NoticeText", "Author", + "Maintainer", "Description", "URL", ) @@ -94,12 +98,50 @@ ) -METADATA_KEYS = ( - "home-page", - "author", - "license", - "summary", -) +def extract_homepage(metadata: Message) -> Optional[str]: + """Extracts the homepage attribute from the package metadata. + + Not all python packages have defined a home-page attribute. + As a fallback, the `Project-URL` metadata can be used. + The python core metadata supports multiple (free text) values for + the `Project-URL` field that are comma separated. + + Args: + metadata: The package metadata to extract the homepage from. + + Returns: + The home page if applicable, None otherwise. + """ + homepage = metadata.get("home-page", None) + if homepage is not None: + return homepage + + candidates: Dict[str, str] = {} + + for entry in metadata.get_all("Project-URL", []): + key, value = entry.split(",", 1) + candidates[key.strip()] = value.strip() + + for priority_key in ["Homepage", "Source", "Changelog", "Bug Tracker"]: + if priority_key in candidates: + return candidates[priority_key] + + return None + + +METADATA_KEYS: Dict[str, List[Callable[[Message], Optional[str]]]] = { + "home-page": [extract_homepage], + "author": [ + lambda metadata: metadata.get("author"), + lambda metadata: metadata.get("author-email"), + ], + "maintainer": [ + lambda metadata: metadata.get("maintainer"), + lambda metadata: metadata.get("maintainer-email"), + ], + "license": [lambda metadata: metadata.get("license")], + "summary": [lambda metadata: metadata.get("summary")], +} # Mapping of FIELD_NAMES to METADATA_KEYS where they differ by more than case FIELDS_TO_METADATA_KEYS = { @@ -167,8 +209,15 @@ "noticetext": notice_text, } metadata = pkg.metadata - for key in METADATA_KEYS: - pkg_info[key] = metadata.get(key, LICENSE_UNKNOWN) # type: ignore[attr-defined] # noqa: E501 + for field_name, field_selector_fns in METADATA_KEYS.items(): + value = None + for field_selector_fn in field_selector_fns: + # Type hint of `Distribution.metadata` states `PackageMetadata` + # but it's actually of type `email.Message` + value = field_selector_fn(metadata) # type: ignore + if value: + break + pkg_info[field_name] = value or LICENSE_UNKNOWN classifiers: list[str] = metadata.get_all("classifier", []) pkg_info["license_classifier"] = find_license_from_classifier( @@ -190,7 +239,21 @@ return pkg_info - pkgs = importlib_metadata.distributions() + def get_python_sys_path(executable: str) -> list[str]: + script = "import sys; print(' '.join(filter(bool, sys.path)))" + output = subprocess.run( + [executable, "-c", script], + capture_output=True, + env={**os.environ, "PYTHONPATH": "", "VIRTUAL_ENV": ""}, + ) + return output.stdout.decode().strip().split() + + if args.python == sys.executable: + search_paths = sys.path + else: + search_paths = get_python_sys_path(args.python) + + pkgs = importlib_metadata.distributions(path=search_paths) ignore_pkgs_as_lower = [pkg.lower() for pkg in args.ignore_packages] pkgs_as_lower = [pkg.lower() for pkg in args.packages] @@ -204,8 +267,12 @@ for pkg in pkgs: pkg_name = pkg.metadata["name"] + pkg_name_and_version = pkg_name + ":" + pkg.metadata["version"] - if pkg_name.lower() in ignore_pkgs_as_lower: + if ( + pkg_name.lower() in ignore_pkgs_as_lower + or pkg_name_and_version.lower() in ignore_pkgs_as_lower + ): continue if pkgs_as_lower and pkg_name.lower() not in pkgs_as_lower: @@ -223,7 +290,9 @@ ) if fail_on_licenses: - failed_licenses = license_names.intersection(fail_on_licenses) + failed_licenses = case_insensitive_set_intersect( + license_names, fail_on_licenses + ) if failed_licenses: sys.stderr.write( "fail-on license {} was found for package " @@ -236,7 +305,9 @@ sys.exit(1) if allow_only_licenses: - uncommon_licenses = license_names.difference(allow_only_licenses) + uncommon_licenses = case_insensitive_set_diff( + license_names, allow_only_licenses + ) if len(uncommon_licenses) == len(license_names): sys.stderr.write( "license {} not in allow-only licenses was found" @@ -302,12 +373,32 @@ return table +def case_insensitive_set_intersect(set_a, set_b): + """Same as set.intersection() but case-insensitive""" + common_items = set() + set_b_lower = {item.lower() for item in set_b} + for elem in set_a: + if elem.lower() in set_b_lower: + common_items.add(elem) + return common_items + + +def case_insensitive_set_diff(set_a, set_b): + """Same as set.difference() but case-insensitive""" + uncommon_items = set() + set_b_lower = {item.lower() for item in set_b} + for elem in set_a: + if not elem.lower() in set_b_lower: + uncommon_items.add(elem) + return uncommon_items + + class JsonPrettyTable(PrettyTable): """PrettyTable-like class exporting to JSON""" def _format_row(self, row: Iterable[str]) -> dict[str, str | list[str]]: resrow: dict[str, str | List[str]] = {} - for (field, value) in zip(self._field_names, row): + for field, value in zip(self._field_names, row): resrow[field] = value return resrow @@ -332,7 +423,7 @@ class JsonLicenseFinderTable(JsonPrettyTable): def _format_row(self, row: Iterable[str]) -> dict[str, str | list[str]]: resrow: dict[str, str | List[str]] = {} - for (field, value) in zip(self._field_names, row): + for field, value in zip(self._field_names, row): if field == "Name": resrow["name"] = value @@ -494,6 +585,9 @@ if args.with_authors: output_fields.append("Author") + if args.with_maintainers: + output_fields.append("Maintainer") + if args.with_urls: output_fields.append("URL") @@ -523,6 +617,8 @@ return "Name" elif args.order == OrderArg.AUTHOR and args.with_authors: return "Author" + elif args.order == OrderArg.MAINTAINER and args.with_maintainers: + return "Maintainer" elif args.order == OrderArg.URL and args.with_urls: return "URL" @@ -691,6 +787,7 @@ LICENSE = L = auto() NAME = N = auto() AUTHOR = A = auto() + MAINTAINER = M = auto() URL = U = auto() @@ -754,6 +851,17 @@ ) common_options.add_argument( + "--python", + type=str, + default=sys.executable, + metavar="PYTHON_EXEC", + help="R| path to python executable to search distributions from\n" + "Package will be searched in the selected python's sys.path\n" + "By default, will search packages for current env executable\n" + "(default: sys.executable)", + ) + + common_options.add_argument( "--from", dest="from_", action=SelectAction, @@ -839,6 +947,12 @@ help="dump with package authors", ) format_options.add_argument( + "--with-maintainers", + action="store_true", + default=False, + help="dump with package maintainers", + ) + format_options.add_argument( "-u", "--with-urls", action="store_true", diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' '--exclude=.svnignore' old/pip-licenses-4.0.3/test_piplicenses.py new/pip-licenses-4.2.0/test_piplicenses.py --- old/pip-licenses-4.0.3/test_piplicenses.py 2022-12-08 12:56:51.000000000 +0100 +++ new/pip-licenses-4.2.0/test_piplicenses.py 2023-04-12 11:58:29.000000000 +0200 @@ -4,12 +4,16 @@ import copy import email +import json import re import sys import unittest +import venv from enum import Enum, auto from importlib.metadata import Distribution +from types import SimpleNamespace from typing import TYPE_CHECKING, Any, List +from unittest.mock import MagicMock import docutils.frontend import docutils.parsers.rst @@ -29,14 +33,18 @@ CompatibleArgumentParser, FromArg, __pkgname__, + case_insensitive_set_diff, + case_insensitive_set_intersect, create_licenses_table, create_output_string, create_parser, create_warn_string, enum_key_to_value, + extract_homepage, factory_styled_table_with_args, find_license_from_classifier, get_output_fields, + get_packages, get_sortby, output_colored, save_if_needs, @@ -218,14 +226,14 @@ for row in table.rows: license_classifier.append(row[index_license_classifier]) - for license in ("BSD", "MIT", "Apache 2.0"): - self.assertIn(license, license_meta) - for license in ( + for license_name in ("BSD", "MIT", "Apache 2.0"): + self.assertIn(license_name, license_meta) + for license_name in ( "BSD License", "MIT License", "Apache Software License", ): - self.assertIn(license, license_classifier) + self.assertIn(license_name, license_classifier) def test_find_license_from_classifier(self) -> None: classifiers = ["License :: OSI Approved :: MIT License"] @@ -303,6 +311,17 @@ output_string = create_output_string(args) self.assertIn("Author", output_string) + def test_with_maintainers(self) -> None: + with_maintainers_args = ["--with-maintainers"] + args = self.parser.parse_args(with_maintainers_args) + + output_fields = get_output_fields(args) + self.assertNotEqual(output_fields, list(DEFAULT_OUTPUT_FIELDS)) + self.assertIn("Maintainer", output_fields) + + output_string = create_output_string(args) + self.assertIn("Maintainer", output_string) + def test_with_urls(self) -> None: with_urls_args = ["--with-urls"] args = self.parser.parse_args(with_urls_args) @@ -388,17 +407,32 @@ self.assertIn("best paired with --format=json", warn_string) def test_ignore_packages(self) -> None: - if "PTable" in SYSTEM_PACKAGES: - ignore_pkg_name = "PTable" - else: - ignore_pkg_name = "prettytable" - ignore_packages_args = ["--ignore-package=" + ignore_pkg_name] + ignore_pkg_name = "prettytable" + ignore_packages_args = [ + "--ignore-package=" + ignore_pkg_name, + "--with-system", + ] args = self.parser.parse_args(ignore_packages_args) table = create_licenses_table(args) pkg_name_columns = self._create_pkg_name_columns(table) self.assertNotIn(ignore_pkg_name, pkg_name_columns) + def test_ignore_packages_and_version(self) -> None: + # Fictitious version that does not exist + ignore_pkg_name = "prettytable" + ignore_pkg_spec = ignore_pkg_name + ":1.99.99" + ignore_packages_args = [ + "--ignore-package=" + ignore_pkg_spec, + "--with-system", + ] + args = self.parser.parse_args(ignore_packages_args) + table = create_licenses_table(args) + + pkg_name_columns = self._create_pkg_name_columns(table) + # It is expected that prettytable will include + self.assertIn(ignore_pkg_name, pkg_name_columns) + def test_with_packages(self) -> None: pkg_name = "py" only_packages_args = ["--packages=" + pkg_name] @@ -409,10 +443,7 @@ self.assertListEqual([pkg_name], pkg_name_columns) def test_with_packages_with_system(self) -> None: - if "PTable" in SYSTEM_PACKAGES: - pkg_name = "PTable" - else: - pkg_name = "prettytable" + pkg_name = "prettytable" only_packages_args = ["--packages=" + pkg_name, "--with-system"] args = self.parser.parse_args(only_packages_args) table = create_licenses_table(args) @@ -441,6 +472,13 @@ sortby = get_sortby(args) self.assertEqual("Author", sortby) + def test_order_maintainer(self) -> None: + order_maintainer_args = ["--order=maintainer", "--with-maintainers"] + args = self.parser.parse_args(order_maintainer_args) + + sortby = get_sortby(args) + self.assertEqual("Maintainer", sortby) + def test_order_url(self) -> None: order_url_args = ["--order=url", "--with-urls"] args = self.parser.parse_args(order_url_args) @@ -669,6 +707,34 @@ importlib_metadata_distributions_orig ) + def test_case_insensitive_set_diff(self) -> None: + set_a = {"MIT License"} + set_b = {"Mit License", "BSD License"} + set_c = {"mit license"} + a_diff_b = case_insensitive_set_diff(set_a, set_b) + a_diff_c = case_insensitive_set_diff(set_a, set_c) + b_diff_c = case_insensitive_set_diff(set_b, set_c) + a_diff_empty = case_insensitive_set_diff(set_a, set()) + + self.assertTrue(len(a_diff_b) == 0) + self.assertTrue(len(a_diff_c) == 0) + self.assertIn("BSD License", b_diff_c) + self.assertIn("MIT License", a_diff_empty) + + def test_case_insensitive_set_intersect(self) -> None: + set_a = {"Revised BSD"} + set_b = {"Apache License", "revised BSD"} + set_c = {"revised bsd"} + a_intersect_b = case_insensitive_set_intersect(set_a, set_b) + a_intersect_c = case_insensitive_set_intersect(set_a, set_c) + b_intersect_c = case_insensitive_set_intersect(set_b, set_c) + a_intersect_empty = case_insensitive_set_intersect(set_a, set()) + + self.assertTrue(set_a == a_intersect_b) + self.assertTrue(set_a == a_intersect_c) + self.assertTrue({"revised BSD"} == b_intersect_c) + self.assertTrue(len(a_intersect_empty) == 0) + class MockStdStream(object): def __init__(self) -> None: @@ -726,7 +792,7 @@ def test_allow_only(monkeypatch) -> None: licenses = ( - "BSD License", + "Bsd License", "Apache Software License", "Mozilla Public License 2.0 (MPL 2.0)", "Python Software Foundation License", @@ -750,8 +816,28 @@ ) +def test_different_python() -> None: + import tempfile + + class TempEnvBuild(venv.EnvBuilder): + def post_setup(self, context: SimpleNamespace) -> None: + self.context = context + + with tempfile.TemporaryDirectory() as target_dir_path: + venv_builder = TempEnvBuild(with_pip=True) + venv_builder.create(str(target_dir_path)) + python_exec = venv_builder.context.env_exe + python_arg = f"--python={python_exec}" + args = create_parser().parse_args([python_arg, "-s", "-f=json"]) + pkgs = get_packages(args) + package_names = sorted(p["name"] for p in pkgs) + print(package_names) + + assert package_names == ["pip", "setuptools"] + + def test_fail_on(monkeypatch) -> None: - licenses = ("MIT License",) + licenses = ("MIT license",) allow_only_args = ["--fail-on={}".format(";".join(licenses))] mocked_stdout = MockStdStream() mocked_stderr = MockStdStream() @@ -821,3 +907,56 @@ capture = capsys.readouterr().err for arg in ("invalid code", "--filter-code-page"): assert arg in capture + + +def test_extract_homepage_home_page_set() -> None: + metadata = MagicMock() + metadata.get.return_value = "Foobar" + + assert "Foobar" == extract_homepage(metadata=metadata) # type: ignore + + metadata.get.assert_called_once_with("home-page", None) + + +def test_extract_homepage_project_url_fallback() -> None: + metadata = MagicMock() + metadata.get.return_value = None + + # `Homepage` is prioritized higher than `Source` + metadata.get_all.return_value = [ + "Source, source", + "Homepage, homepage", + ] + + assert "homepage" == extract_homepage(metadata=metadata) # type: ignore + + metadata.get_all.assert_called_once_with("Project-URL", []) + + +def test_extract_homepage_project_url_fallback_multiple_parts() -> None: + metadata = MagicMock() + metadata.get.return_value = None + + # `Homepage` is prioritized higher than `Source` + metadata.get_all.return_value = [ + "Source, source", + "Homepage, homepage, foo, bar", + ] + + assert "homepage, foo, bar" == extract_homepage( + metadata=metadata # type: ignore + ) + + metadata.get_all.assert_called_once_with("Project-URL", []) + + +def test_extract_homepage_empty() -> None: + metadata = MagicMock() + + metadata.get.return_value = None + metadata.get_all.return_value = [] + + assert None is extract_homepage(metadata=metadata) # type: ignore + + metadata.get.assert_called_once_with("home-page", None) + metadata.get_all.assert_called_once_with("Project-URL", [])