Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion .github/workflows/build.yml
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,7 @@ jobs:
runs-on: ubuntu-24.04
strategy:
matrix:
python-version: [ '3.8', '3.9', '3.10', '3.11' ]
python-version: [ '3.8', '3.9', '3.10', '3.11', '3.12', '3.13' ]
steps:
- uses: actions/checkout@v4
- name: set up Python ${{ matrix.python-version }}
Expand Down
2 changes: 1 addition & 1 deletion .github/workflows/release.yml
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,7 @@ jobs:
runs-on: ubuntu-24.04
strategy:
matrix:
python-version: [ '3.8', '3.9', '3.10', '3.11' ]
python-version: [ '3.8', '3.9', '3.10', '3.11', '3.12', '3.13' ]
steps:
- uses: actions/checkout@v4
- name: set up Python ${{ matrix.python-version }}
Expand Down
1 change: 1 addition & 0 deletions liccheck.ini
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@ authorized_licenses:
simplified BSD
Apache
Apache 2.0
Apache-2.0
Apache software license
gnu LGPL
LGPL with exceptions or zpl
Expand Down
21 changes: 13 additions & 8 deletions liccheck/command_line.py
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@
import os.path

from liccheck.requirements import parse_requirements, resolve, resolve_without_deps
from packaging.requirements import Requirement

from configparser import ConfigParser, NoOptionError
import enum
Expand Down Expand Up @@ -153,16 +154,21 @@ def transform(dist):
licenses = list(set([strip_license(l) for l in licenses]))

return {
"name": dist.project_name,
"name": dist.metadata["Name"],
"version": dist.version,
"location": dist.location,
"dependencies": [dependency.project_name for dependency in dist.requires()],
"location": str(dist.locate_file("")),
"dependencies": [
Requirement(dependency).name for dependency in dist.requires or []
],
"licenses": licenses,
}

def get_metadata_text(dist):
return dist.read_text("METADATA") or dist.read_text("PKG-INFO")

def get_license(dist):
if dist.has_metadata(dist.PKG_INFO):
metadata = dist.get_metadata(dist.PKG_INFO)
metadata = get_metadata_text(dist)
if metadata:
match = regex_license.search(metadata)
if match:
license = match.group("license")
Expand All @@ -172,9 +178,8 @@ def get_license(dist):
return []

def get_licenses_from_classifiers(dist):
if dist.has_metadata(dist.PKG_INFO):
metadata = dist.get_metadata(dist.PKG_INFO)

metadata = get_metadata_text(dist)
if metadata:
# match might be found, but None if using the classifier:
# License :: OSI Approved
return [m for m in regex_classifier.findall(metadata) if m]
Expand Down
69 changes: 38 additions & 31 deletions liccheck/requirements.py
Original file line number Diff line number Diff line change
@@ -1,52 +1,59 @@
import pkg_resources
from importlib.metadata import PackageNotFoundError, distribution

try:
from pip._internal.network.session import PipSession
except ImportError:
try:
from pip._internal.download import PipSession
except ImportError:
from pip.download import PipSession

try:
from pip._internal.req import parse_requirements as pip_parse_requirements
except ImportError:
from pip.req import parse_requirements as pip_parse_requirements
from packaging.requirements import Requirement
from packaging.utils import canonicalize_name

try:
from pip._internal.req.constructors import install_req_from_parsed_requirement
except ImportError:
def install_req_from_parsed_requirement(r):
return r
from pip._internal.network.session import PipSession
from pip._internal.req import parse_requirements as pip_parse_requirements
from pip._internal.req.constructors import install_req_from_parsed_requirement


def parse_requirements(requirement_file):
requirements = []
for req in pip_parse_requirements(requirement_file, session=PipSession()):
install_req = install_req_from_parsed_requirement(req)
if install_req.markers and not pkg_resources.evaluate_marker(str(install_req.markers)):
if install_req.markers and not install_req.markers.evaluate():
# req should not installed due to env markers
continue
elif install_req.editable:
# skip editable req as they are failing in the resolve phase
continue
requirements.append(pkg_resources.Requirement.parse(str(install_req.req)))
requirements.append(Requirement(str(install_req.req)))
return requirements


def _find_distribution(req):
try:
dist = distribution(req.name)
except PackageNotFoundError:
return None
if req.specifier and not req.specifier.contains(dist.version, prereleases=True):
return None
return dist


def resolve_without_deps(requirements):
working_set = pkg_resources.working_set
for req in requirements:
env = pkg_resources.Environment(working_set.entries)
dist = env.best_match(
req=req,
working_set=working_set,
installer=None,
replace_conflicting=False,
)
yield dist
yield _find_distribution(req)


def _resolve_one(req, seen):
dist = _find_distribution(req)
if dist is None:
return
key = canonicalize_name(dist.metadata["Name"])
if key in seen:
return
seen.add(key)
yield dist
for dep_str in dist.requires or []:
dep_req = Requirement(dep_str)
if dep_req.marker and not dep_req.marker.evaluate():
continue
yield from _resolve_one(dep_req, seen)


def resolve(requirements):
for dist in pkg_resources.working_set.resolve(requirements):
yield dist
seen = set()
for req in requirements:
yield from _resolve_one(req, seen)
4 changes: 2 additions & 2 deletions requirements.txt
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
pip>=9.0.1
enum34;python_version<"3.4"
pip>=20.1
semantic_version
toml
packaging
14 changes: 8 additions & 6 deletions setup.py
Original file line number Diff line number Diff line change
Expand Up @@ -57,10 +57,12 @@
# Specify the Python versions you support here. In particular, ensure
# that you indicate whether you support Python 2, Python 3 or both.
'Programming Language :: Python :: 3',
'Programming Language :: Python :: 3.5',
'Programming Language :: Python :: 3.6',
'Programming Language :: Python :: 3.7',
'Programming Language :: Python :: 3.8'
'Programming Language :: Python :: 3.8',
'Programming Language :: Python :: 3.9',
'Programming Language :: Python :: 3.10',
'Programming Language :: Python :: 3.11',
'Programming Language :: Python :: 3.12',
'Programming Language :: Python :: 3.13'
],

# What does your project relate to?
Expand All @@ -74,9 +76,9 @@
# this:
# py_modules=["my_module"],

python_requires='>=3.5',
python_requires='>=3.8',

install_requires=['semantic_version>=2.7.0', 'toml'],
install_requires=['semantic_version>=2.7.0', 'toml', 'packaging'],

# If there are data files included in your packages that need to be
# installed, specify them here. If using Python 2.6 or less, then these
Expand Down
8 changes: 4 additions & 4 deletions tests/test_cli.py
Original file line number Diff line number Diff line change
Expand Up @@ -51,9 +51,9 @@ def test_run(capsys):
expected = textwrap.dedent(
'''\
gathering licenses...
3 packages and dependencies.
4 packages and dependencies.
check authorized packages...
3 packages.
4 packages.
'''
)
assert captured == expected
Expand All @@ -67,9 +67,9 @@ def test_run_without_deps(capsys):
expected = textwrap.dedent(
'''\
gathering licenses...
3 packages.
4 packages.
check authorized packages...
3 packages.
4 packages.
'''
)
assert captured == expected
11 changes: 5 additions & 6 deletions tests/test_get_packages_info.py
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
import importlib.metadata
import pathlib
import sys

import pkg_resources
import pytest

from liccheck.command_line import get_packages_info
Expand All @@ -24,9 +25,8 @@ def test_license_strip_with_return_carriage(tmp_path, mocker):
tmpfh.write(b"Name: pip\r\n")
tmpfh.write(b"Version: 23.3.1\r\n")
tmpfh.write(b"Classifier: License :: OSI Approved :: MIT License\r\n")
metadata = pkg_resources.PathMetadata(tmp_path, tmp_path)
resolve.return_value = [
pkg_resources.Distribution(project_name="pip", metadata=metadata)
importlib.metadata.PathDistribution(pathlib.Path(tmp_path))
]
assert get_packages_info(req_path)[0]["licenses"] == ["MIT"]

Expand Down Expand Up @@ -60,7 +60,7 @@ def test_editable_requirements_get_ignored(tmpfile):
('no_deps', 'expected_packages'), (
pytest.param(
False,
('liccheck', 'semantic-version', 'toml'),
('liccheck', 'packaging', 'semantic-version', 'toml'),
id='with deps'
),
pytest.param(True, ('liccheck',), id='without deps'),
Expand All @@ -86,8 +86,7 @@ def test_license_expression(tmp_path, mocker):
tmpfh.write("Name: Twisted\n")
tmpfh.write("Version: 23.8.0\n")
tmpfh.write("License-Expression: MIT\n")
metadata = pkg_resources.FileMetadata(pkg_info_path)
resolve.return_value = [
pkg_resources.Distribution(project_name="Twisted", metadata=metadata)
importlib.metadata.PathDistribution(pathlib.Path(tmp_path))
]
assert get_packages_info(req_path)[0]["licenses"] == ["MIT"]
7 changes: 3 additions & 4 deletions tox.ini
Original file line number Diff line number Diff line change
@@ -1,16 +1,15 @@
[tox]
envlist = py35, py36, py37, py38, py39, py310, py311
envlist = py38, py39, py310, py311, py312, py313
skip_missing_interpreters = True

[gh-actions]
python =
3.5: py35
3.6: py36
3.7: py37
3.8: py38
3.9: py39
3.10: py310
3.11: py311
3.12: py312
3.13: py313

[testenv]
deps =
Expand Down