Skip to content

Commit

Permalink
feat: depend on importlib_metadata rather than pip
Browse files Browse the repository at this point in the history
Especially, rather than functions that are internal to pip.

Solves raimon49#128.
  • Loading branch information
b-kamphorst committed Oct 25, 2022
1 parent 12d5d5f commit fe5910b
Show file tree
Hide file tree
Showing 3 changed files with 93 additions and 127 deletions.
130 changes: 44 additions & 86 deletions piplicenses.py
Original file line number Diff line number Diff line change
Expand Up @@ -28,41 +28,13 @@
"""
import argparse
import codecs
import glob
import os
import re
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
try:
from pip import get_installed_distributions
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

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

try:
Expand All @@ -78,6 +50,11 @@ def get_installed_distributions():
from prettytable import NONE as RULE_NONE
PTABLE = False

if sys.version_info < (3, 8):
import importlib_metadata
else:
from importlib import metadata as importlib_metadata

open = open # allow monkey patching

__pkgname__ = 'pip-licenses'
Expand Down Expand Up @@ -151,78 +128,57 @@ def get_installed_distributions():

def get_packages(args: "CustomNamespace"):

def get_pkg_included_file(pkg, file_names):
def get_pkg_included_file(pkg, file_names_rgx):
"""
Attempt to find the package's included file on disk and return the
tuple (included_file_path, included_file_contents).
"""
included_file = LICENSE_UNKNOWN
included_text = LICENSE_UNKNOWN
pkg_dirname = "{}-{}.dist-info".format(
pkg.project_name.replace("-", "_"), pkg.version)
patterns = []
[patterns.extend(sorted(glob.glob(os.path.join(pkg.location,
pkg_dirname,
f))))
for f in file_names]
# Search for path defined in PEP 639 https://peps.python.org/pep-0639/
[patterns.extend(sorted(glob.glob(os.path.join(pkg.location,
pkg_dirname,
"licenses",
f))))
for f in file_names]
for test_file in patterns:
if os.path.exists(test_file) and \
os.path.isdir(test_file) is not True:
included_file = test_file
with open(test_file, encoding='utf-8',
errors='backslashreplace') as included_file_handle:
included_text = included_file_handle.read()
break
return (included_file, included_text)

pkg_files = pkg.files or ()
pattern = re.compile(file_names_rgx)
matched_rel_paths = filter(lambda file: pattern.match(file.name), pkg_files)
for rel_path in matched_rel_paths:
abs_path = pkg.locate_file(rel_path)
if not abs_path.is_file():
continue
included_file = abs_path
with open(
abs_path,
encoding='utf-8',
errors='backslashreplace'
) as included_file_handle:
included_text = included_file_handle.read()
break
return (str(included_file), included_text)

def get_pkg_info(pkg):
(license_file, license_text) = get_pkg_included_file(
pkg,
('LICENSE*', 'LICENCE*', 'COPYING*')
"LICEN[CS]E.*|COPYING.*"
)
(notice_file, notice_text) = get_pkg_included_file(
pkg,
('NOTICE*',)
"NOTICE.*"
)
pkg_info = {
'name': pkg.project_name,
'name': pkg.name,
'version': pkg.version,
'namever': str(pkg),
'namever': "{} {}".format(pkg.name, pkg.version),
'licensefile': license_file,
'licensetext': license_text,
'noticefile': notice_file,
'noticetext': notice_text,
}
metadata = None
if pkg.has_metadata('METADATA'):
metadata = pkg.get_metadata('METADATA')

if pkg.has_metadata('PKG-INFO') and metadata is None:
metadata = pkg.get_metadata('PKG-INFO')

if metadata is None:
for key in METADATA_KEYS:
pkg_info[key] = LICENSE_UNKNOWN

return pkg_info

feed_parser = FeedParser()
feed_parser.feed(metadata)
parsed_metadata = feed_parser.close()

metadata = pkg.metadata
for key in METADATA_KEYS:
pkg_info[key] = parsed_metadata.get(key, LICENSE_UNKNOWN)
pkg_info[key] = metadata.get(key, LICENSE_UNKNOWN)

if metadata is not None:
message = message_from_string(metadata)
classifiers = metadata.get_all("classifier")
if classifiers:
pkg_info['license_classifier'] = \
find_license_from_classifier(message)
find_license_from_classifier(classifiers)

if args.filter_strings:
for k in pkg_info:
Expand All @@ -238,7 +194,10 @@ def get_pkg_info(pkg):

return pkg_info

pkgs = get_installed_distributions()
pkgs = filter(
lambda pkg: pkg.name != "pip-licenses",
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]

Expand All @@ -251,7 +210,7 @@ def get_pkg_info(pkg):
allow_only_licenses = set(map(str.strip, args.allow_only.split(";")))

for pkg in pkgs:
pkg_name = pkg.project_name
pkg_name = pkg.name

if pkg_name.lower() in ignore_pkgs_as_lower:
continue
Expand Down Expand Up @@ -477,15 +436,14 @@ def factory_styled_table_with_args(
return table


def find_license_from_classifier(message):
def find_license_from_classifier(classifiers):
licenses = []
for k, v in message.items():
if k == 'Classifier' and v.startswith('License'):
license = v.split(' :: ')[-1]
for classifier in filter(lambda c: c.startswith("License"), classifiers):
license = classifier.split(' :: ')[-1]

# Through the declaration of 'Classifier: License :: OSI Approved'
if license != 'OSI Approved':
licenses.append(license)
# Through the declaration of 'Classifier: License :: OSI Approved'
if license != 'OSI Approved':
licenses.append(license)

return licenses

Expand Down
1 change: 1 addition & 0 deletions setup.cfg
Original file line number Diff line number Diff line change
Expand Up @@ -28,6 +28,7 @@ setup_requires =
pytest-runner
install_requires =
PTable
importlib_metadata; python_version<"3.8"
tests_require =
docutils
pytest-cov
Expand Down
89 changes: 48 additions & 41 deletions test_piplicenses.py
Original file line number Diff line number Diff line change
Expand Up @@ -29,14 +29,26 @@
UNICODE_APPENDIX = f.readline().replace("\n", "")


def get_installed_distributions_mocked(*args, **kwargs):
packages = get_installed_distributions_orig(*args, **kwargs)
if not packages[-1].project_name.endswith(UNICODE_APPENDIX):
packages[-1].project_name += " "+UNICODE_APPENDIX
def importlib_metadata_distributions_mocked(*args, **kwargs):
class DistributionMocker(piplicenses.importlib_metadata.Distribution):
def __init__(self, orig_distribution):
self.__dist = orig_distribution

@property
def name(self):
return self.__dist.name + " " + UNICODE_APPENDIX

@property
def version(self):
return self.__dist.version

packages = list(importlib_metadata_distributions_orig(*args, **kwargs))
packages[-1] = DistributionMocker(packages[-1])
return packages


get_installed_distributions_orig = piplicenses.get_installed_distributions
importlib_metadata_distributions_orig = \
piplicenses.importlib_metadata.distributions


class CommandLineTestCase(unittest.TestCase):
Expand Down Expand Up @@ -171,28 +183,21 @@ def test_from_all(self):
self.assertIn(license, license_classifier)

def test_find_license_from_classifier(self):
metadata = ('Metadata-Version: 2.0\r\n'
'Name: pip-licenses\r\n'
'Version: 1.0.0\r\n'
'Classifier: License :: OSI Approved :: MIT License\r\n')
message = message_from_string(metadata)
classifiers = ['License :: OSI Approved :: MIT License']
self.assertEqual(['MIT License'],
find_license_from_classifier(message))
find_license_from_classifier(classifiers))

def test_display_multiple_license_from_classifier(self):
metadata = ('Metadata-Version: 2.0\r\n'
'Name: helga\r\n'
'Version: 1.7.6\r\n'
'Classifier: License :: OSI Approved\r\n'
'Classifier: License :: OSI Approved :: '
'GNU General Public License v3 (GPLv3)\r\n'
'Classifier: License :: OSI Approved :: MIT License\r\n'
'Classifier: License :: Public Domain\r\n')
message = message_from_string(metadata)
classifiers = [
'License :: OSI Approved',
'License :: OSI Approved :: GNU General Public License v3 (GPLv3)',
'License :: OSI Approved :: MIT License',
'License :: Public Domain'
]
self.assertEqual(['GNU General Public License v3 (GPLv3)',
'MIT License',
'Public Domain'],
find_license_from_classifier(message))
find_license_from_classifier(classifiers))

def test_not_found_license_from_classifier(self):
metadata_as_no_license = ('Metadata-Version: 2.0\r\n'
Expand Down Expand Up @@ -426,8 +431,8 @@ def test_format_markdown(self):
@unittest.skipIf(sys.version_info < (3, 6, 0),
"To unsupport Python 3.5 in the near future")
def test_format_rst_without_filter(self):
piplicenses.get_installed_distributions = \
get_installed_distributions_mocked
piplicenses.importlib_metadata.distributions = \
importlib_metadata_distributions_mocked
format_rst_args = ['--format=rst']
args = self.parser.parse_args(format_rst_args)
table = create_licenses_table(args)
Expand All @@ -439,12 +444,12 @@ def test_format_rst_without_filter(self):
self.assertEqual(RULE_ALL, table.hrules)
with self.assertRaises(docutils.utils.SystemMessage):
self.check_rst(str(table))
piplicenses.get_installed_distributions = \
get_installed_distributions_orig
piplicenses.importlib_metadata.distributions = \
importlib_metadata_distributions_orig

def test_format_rst_default_filter(self):
piplicenses.get_installed_distributions = \
get_installed_distributions_mocked
piplicenses.importlib_metadata.distributions = \
importlib_metadata_distributions_mocked
format_rst_args = ['--format=rst', '--filter-strings']
args = self.parser.parse_args(format_rst_args)
table = create_licenses_table(args)
Expand All @@ -455,8 +460,8 @@ def test_format_rst_default_filter(self):
self.assertEqual('+', table.junction_char)
self.assertEqual(RULE_ALL, table.hrules)
self.check_rst(str(table))
piplicenses.get_installed_distributions = \
get_installed_distributions_orig
piplicenses.importlib_metadata.distributions = \
importlib_metadata_distributions_orig

def test_format_confluence(self):
format_confluence_args = ['--format=confluence']
Expand Down Expand Up @@ -562,32 +567,34 @@ def test_output_colored_bold(self):
self.assertTrue(actual.endswith('\033[0m'))

def test_without_filter(self):
piplicenses.get_installed_distributions = \
get_installed_distributions_mocked
piplicenses.importlib_metadata.distributions = \
importlib_metadata_distributions_mocked
args = self.parser.parse_args([])
packages = list(piplicenses.get_packages(args))
for pkg in packages:
print(pkg["name"])
self.assertIn(UNICODE_APPENDIX, packages[-1]["name"])
piplicenses.get_installed_distributions = \
get_installed_distributions_orig
piplicenses.importlib_metadata.distributions = \
importlib_metadata_distributions_orig

def test_with_default_filter(self):
piplicenses.get_installed_distributions = \
get_installed_distributions_mocked
piplicenses.importlib_metadata.distributions = \
importlib_metadata_distributions_mocked
args = self.parser.parse_args(["--filter-strings"])
packages = list(piplicenses.get_packages(args))
piplicenses.get_installed_distributions = \
get_installed_distributions_orig
piplicenses.importlib_metadata.distributions = \
importlib_metadata_distributions_orig
self.assertNotIn(UNICODE_APPENDIX, packages[-1]["name"])

def test_with_specified_filter(self):
piplicenses.get_installed_distributions = \
get_installed_distributions_mocked
piplicenses.importlib_metadata.distributions = \
importlib_metadata_distributions_mocked
args = self.parser.parse_args(["--filter-strings",
"--filter-code-page=ascii"])
packages = list(piplicenses.get_packages(args))
self.assertNotIn(UNICODE_APPENDIX, packages[-1]["summary"])
piplicenses.get_installed_distributions = \
get_installed_distributions_orig
piplicenses.importlib_metadata.distributions = \
importlib_metadata_distributions_orig


class MockStdStream(object):
Expand Down

0 comments on commit fe5910b

Please sign in to comment.