src/eric7/PipInterface/piplicenses.py

branch
eric7
changeset 10027
63728909f3ff
parent 9851
ec12090e9cd9
child 10179
c2125fcab4b8
--- a/src/eric7/PipInterface/piplicenses.py	Sat May 13 11:35:51 2023 +0200
+++ b/src/eric7/PipInterface/piplicenses.py	Sat May 13 12:15:43 2023 +0200
@@ -45,7 +45,9 @@
 import argparse
 import codecs
 import json
+import os
 import re
+import subprocess
 import sys
 from collections import Counter
 from enum import Enum, auto
@@ -59,11 +61,12 @@
     importlib_metadata = None
 
 if TYPE_CHECKING:
-    from typing import Iterator, Optional, Sequence
+    from email.message import Message
+    from typing import Callable, Dict, Iterator, Optional, Sequence
 
 
 __pkgname__ = "pip-licenses"
-__version__ = "4.1.0"
+__version__ = "4.3.1"
 __author__ = "raimon"
 __license__ = "MIT"
 __summary__ = (
@@ -81,6 +84,7 @@
     'NoticeFile',
     'NoticeText',
     'Author',
+    "Maintainer",
     'Description',
     'URL',
 )
@@ -104,13 +108,68 @@
 )
 
 
-METADATA_KEYS = (
-    'home-page',
-    'author',
-    'license',
-    'summary',
-    'license_classifier',
-)
+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
+
+
+PATTERN_DELIMITER = re.compile(r"[-_.]+")
+
+
+def normalize_pkg_name(pkg_name: str) -> str:
+    """Return normalized name according to PEP specification
+
+    See here: https://peps.python.org/pep-0503/#normalized-names
+
+    Args:
+        pkg_name: Package name it is extracted from the package metadata
+                  or specified in the CLI
+
+    Returns:
+        normalized packege name
+    """
+    return PATTERN_DELIMITER.sub("-", pkg_name).lower()
+
+
+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 = {
@@ -176,8 +235,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(
@@ -199,13 +265,28 @@
 
         return pkg_info
 
+    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()
+
     # modified for 'eric-ide' for Python < 3.8
     if importlib_metadata is None:
         return []
     else:
-        pkgs = importlib_metadata.distributions()
-    ignore_pkgs_as_lower = [pkg.lower() for pkg in args.ignore_packages]
-    pkgs_as_lower = [pkg.lower() for pkg in args.packages]
+        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_normalize = [
+        normalize_pkg_name(pkg) for pkg in args.ignore_packages
+    ]
+    pkgs_as_normalize = [normalize_pkg_name(pkg) for pkg in args.packages]
 
     fail_on_licenses = set()
     if args.fail_on:
@@ -216,12 +297,16 @@
         allow_only_licenses = set(map(str.strip, args.allow_only.split(";")))
 
     for pkg in pkgs:
-        pkg_name = pkg.metadata["name"]
+        pkg_name = normalize_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_normalize
+            or pkg_name_and_version.lower() in ignore_pkgs_as_normalize
+        ):
             continue
 
-        if pkgs_as_lower and pkg_name.lower() not in pkgs_as_lower:
+        if pkgs_as_normalize and pkg_name.lower() not in pkgs_as_normalize:
             continue
 
         if not args.with_system and pkg_name in SYSTEM_PACKAGES:
@@ -270,8 +355,8 @@
 
 # modified for 'eric-ide'
 def create_licenses_list(
-        args: "CustomNamespace", output_fields=DEFAULT_OUTPUT_FIELDS):
-    
+    args: "CustomNamespace", output_fields=DEFAULT_OUTPUT_FIELDS):
+
     licenses = []
     for pkg in get_packages(args):
         row = {}
@@ -384,12 +469,18 @@
     if args.with_authors:
         output_fields.append("Author")
 
+    if args.with_maintainers:
+        output_fields.append("Maintainer")
+
     if args.with_urls:
         output_fields.append("URL")
 
     if args.with_description:
         output_fields.append("Description")
 
+    if args.no_version:
+        output_fields.remove("Version")
+
     if args.with_license_file:
         if not args.no_license_path:
             output_fields.append("LicenseFile")
@@ -533,6 +624,7 @@
     LICENSE = L = auto()
     NAME = N = auto()
     AUTHOR = A = auto()
+    MAINTAINER = M = auto()
     URL = U = auto()
 
 
@@ -583,6 +675,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,
@@ -651,6 +754,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",
@@ -665,6 +774,13 @@
         help="dump with short package description",
     )
     format_options.add_argument(
+        "-nv",
+        "--no-version",
+        action="store_true",
+        default=False,
+        help="dump without package version",
+    )
+    format_options.add_argument(
         "-l",
         "--with-license-file",
         action="store_true",

eric ide

mercurial