Skip to content

Commit ef9a255

Browse files
committed
Read PEP 639 License-Expression in PyPI #4431
Wheels and PKG-INFO that only set License-Expression were reduced to license classifiers, so the declared SPDX expression was ignored. Prefer License-Expression over the legacy License field when both are present. Signed-off-by: Brian Wing <bwing@anaconda.com>
1 parent 058f439 commit ef9a255

5 files changed

Lines changed: 67 additions & 3 deletions

File tree

AUTHORS.rst

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -18,6 +18,7 @@ The following organizations or individuals have contributed to ScanCode:
1818
- Ayush @macayu17
1919
- Ayan Sinha Mahapatra @AyanSinhaMahapatra
2020
- Ayush Jain @aj4ayushjain
21+
- Brian Wing @bwingconda
2122
- Bruno Oliveira @nicoddemus
2223
- Carmen Bianca Bakker @carmenbianca
2324
- Chaitya Shah @Chaitya62

CHANGELOG.rst

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,12 @@ Changelog
44
Next release
55
--------------
66

7+
- Collect PEP 639 ``License-Expression`` from PyPI METADATA, PKG-INFO and
8+
pyproject.toml in ``packagedcode.pypi.get_declared_license``. Wheels that
9+
only declare ``License-Expression: MIT`` (with no legacy ``License`` field)
10+
were previously reduced to license classifiers, so the expression was
11+
ignored. https://github.com/aboutcode-org/scancode-toolkit/issues/4431
12+
713
- Fix the optional ``licenses`` extra dependency typo to install
814
``licensedcode-data``.
915
https://github.com/aboutcode-org/scancode-toolkit/pull/5056

src/packagedcode/pypi.py

Lines changed: 10 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1942,17 +1942,26 @@ def get_declared_license(metainfo):
19421942
"""
19431943
Return a mapping of declared license information and license file name
19441944
found in a ``metainfo`` package data mapping.
1945+
1946+
Collects PEP 639 ``License-Expression`` (and pyproject.toml
1947+
``license-expression``) in addition to the legacy ``License`` field and
1948+
license classifiers. ``License-Expression`` is preferred when both it and
1949+
``License`` are present: they are mutually exclusive in core metadata, and
1950+
the expression is the machine-readable declared license.
19451951
"""
19461952
declared_license = {}
19471953
# TODO: We should make the declared license as it is, this should be
19481954
# updated in scancode to parse a pure string
19491955
lic = get_attribute(metainfo, 'License')
1956+
license_expression = get_attribute(metainfo, 'License-Expression')
19501957
license_file = get_attribute(metainfo, 'License-File')
19511958
if not license_file and lic:
19521959
if isinstance(lic, dict) and 'file' in lic.keys():
19531960
license_file = lic.pop('file')
19541961

1955-
if lic and not lic == 'UNKNOWN':
1962+
if license_expression and not license_expression == 'UNKNOWN':
1963+
declared_license['license'] = license_expression
1964+
elif lic and not lic == 'UNKNOWN':
19561965
if 'text' in lic:
19571966
declared_license['license'] = lic.get('text')
19581967
else:

tests/packagedcode/data/pypi/metadata/v23/PKG-INFO-expected.json

Lines changed: 23 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -51,6 +51,28 @@
5151
"declared_license_expression": "mit",
5252
"declared_license_expression_spdx": "MIT",
5353
"license_detections": [
54+
{
55+
"license_expression": "mit",
56+
"license_expression_spdx": "MIT",
57+
"matches": [
58+
{
59+
"license_expression": "mit",
60+
"license_expression_spdx": "MIT",
61+
"from_file": null,
62+
"start_line": 1,
63+
"end_line": 1,
64+
"matcher": "1-spdx-id",
65+
"score": 100.0,
66+
"matched_length": 1,
67+
"match_coverage": 100.0,
68+
"rule_relevance": 100,
69+
"rule_identifier": "spdx-license-identifier-mit-5da48780aba670b0860c46d899ed42a0f243ff06",
70+
"rule_url": null,
71+
"matched_text": "MIT"
72+
}
73+
],
74+
"identifier": "mit-a822f434-d61f-f2b1-c792-8b8cb9e7b9bf"
75+
},
5476
{
5577
"license_expression": "mit",
5678
"license_expression_spdx": "MIT",
@@ -77,7 +99,7 @@
7799
"other_license_expression": null,
78100
"other_license_expression_spdx": null,
79101
"other_license_detections": [],
80-
"extracted_license_statement": "classifiers:\n - 'License :: OSI Approved :: MIT License'\n",
102+
"extracted_license_statement": "license: MIT\nclassifiers:\n - 'License :: OSI Approved :: MIT License'\n",
81103
"notice_text": null,
82104
"source_packages": [],
83105
"file_references": [],

tests/packagedcode/test_pypi.py

Lines changed: 27 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -260,7 +260,33 @@ def test_parse_with_unpacked_wheel_meta_v24_1(self):
260260
test_file = self.get_test_loc('pypi/unpacked_wheel/metadata-2.4/narwhals-1.29.0.dist-info/METADATA')
261261
package = pypi.PythonInstalledWheelMetadataFile.parse(test_file)
262262
expected_loc = self.get_test_loc('pypi/unpacked_wheel/metadata-2.4/narwhals-1.29.0.dist-info-expected.json')
263-
self.check_packages_data(package, expected_loc, regen=REGEN_TEST_FIXTURES)
263+
self.check_packages_data(package, expected_loc, regen=REGEN_TEST_FIXTURES)
264+
265+
266+
class TestPypiGetDeclaredLicense(PackageTester):
267+
test_data_dir = os.path.join(os.path.dirname(__file__), 'data')
268+
269+
def test_get_declared_license_from_license_expression(self):
270+
declared, license_file = pypi.get_declared_license({
271+
'License-Expression': 'MIT',
272+
})
273+
assert declared == {'license': 'MIT'}
274+
assert license_file is None
275+
276+
def test_get_declared_license_from_pyproject_license_expression(self):
277+
declared, license_file = pypi.get_declared_license({
278+
'license-expression': 'Apache-2.0 AND MIT',
279+
})
280+
assert declared == {'license': 'Apache-2.0 AND MIT'}
281+
assert license_file is None
282+
283+
def test_get_declared_license_prefers_license_expression_over_license(self):
284+
declared, license_file = pypi.get_declared_license({
285+
'License': 'MIT',
286+
'License-Expression': 'BSD-3-Clause',
287+
})
288+
assert declared == {'license': 'BSD-3-Clause'}
289+
assert license_file is None
264290

265291

266292
class TestPypiUnpackedSdist(PackageTester):

0 commit comments

Comments
 (0)