Skip to content

Commit e048980

Browse files
committed
Fix: update pep621 logic and add unit tests
Signed-off-by: Manav Gupta <[email protected]>
1 parent 168f81d commit e048980

File tree

2 files changed

+92
-26
lines changed

2 files changed

+92
-26
lines changed

cyclonedx_py/_internal/utils/pep621.py

Lines changed: 25 additions & 26 deletions
Original file line numberDiff line numberDiff line change
@@ -61,32 +61,31 @@ def project2licenses(project: dict[str, Any], lfac: 'LicenseFactory', *,
6161
# https://packaging.python.org/en/latest/specifications/core-metadata/#classifier-multiple-use
6262
yield from classifiers2licenses(classifiers, lfac, lack)
6363
if plicense := project.get('license'):
64-
# https://packaging.python.org/en/latest/specifications/pyproject-toml/#license
65-
# https://peps.python.org/pep-0621/#license
66-
# https://packaging.python.org/en/latest/specifications/core-metadata/#license
67-
if 'file' in plicense and 'text' in plicense:
68-
# per spec:
69-
# > These keys are mutually exclusive, so a tool MUST raise an error if the metadata specifies both keys.
70-
raise ValueError('`license.file` and `license.text` are mutually exclusive,')
71-
if 'file' in plicense:
72-
# per spec:
73-
# > [...] a string value that is a relative file path [...].
74-
# > Tools MUST assume the file’s encoding is UTF-8.
75-
with open(join(dirname(fpath), plicense['file']), 'rb') as plicense_fileh:
76-
yield DisjunctiveLicense(name=f"declared license of '{project['name']}'",
77-
acknowledgement=lack,
78-
text=AttachedText(encoding=Encoding.BASE_64,
79-
content=b64encode(plicense_fileh.read()).decode()))
80-
elif len(plicense_text := plicense.get('text', '')) > 0:
81-
license = lfac.make_from_string(plicense_text,
82-
license_acknowledgement=lack)
83-
if isinstance(license, DisjunctiveLicense) and license.id is None:
84-
# per spec, `License` is either a SPDX ID/Expression, or a license text(not name!)
85-
yield DisjunctiveLicense(name=f"declared license of '{project['name']}'",
86-
acknowledgement=lack,
87-
text=AttachedText(content=plicense_text))
88-
else:
89-
yield license
64+
# Handle both PEP 621 (dict) and PEP 639 (str) license formats
65+
if isinstance(plicense, dict):
66+
if 'file' in plicense and 'text' in plicense:
67+
raise ValueError('`license.file` and `license.text` are mutually exclusive,')
68+
if 'file' in plicense:
69+
with open(join(dirname(fpath), plicense['file']), 'rb') as plicense_fileh:
70+
yield DisjunctiveLicense(name=f"declared license of '{project['name']}'",
71+
acknowledgement=lack,
72+
text=AttachedText(encoding=Encoding.BASE_64,
73+
content=b64encode(plicense_fileh.read()).decode()))
74+
elif len(plicense_text := plicense.get('text', '')) > 0:
75+
license = lfac.make_from_string(plicense_text,
76+
license_acknowledgement=lack)
77+
if isinstance(license, DisjunctiveLicense) and license.id is None:
78+
yield DisjunctiveLicense(name=f"declared license of '{project['name']}'",
79+
acknowledgement=lack,
80+
text=AttachedText(content=plicense_text))
81+
else:
82+
yield license
83+
elif isinstance(plicense, str):
84+
# PEP 639: license is a string (SPDX expression or license reference)
85+
license = lfac.make_from_string(plicense, license_acknowledgement=lack)
86+
yield license
87+
else:
88+
raise TypeError(f"Unexpected type for 'license': {type(plicense)}")
9089

9190

9291
def project2extrefs(project: dict[str, Any]) -> Generator['ExternalReference', None, None]:

tests/unit/test_pep621.py

Lines changed: 67 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,67 @@
1+
# This file is part of CycloneDX Python
2+
# SPDX-License-Identifier: Apache-2.0
3+
# Copyright (c) OWASP Foundation. All Rights Reserved.
4+
5+
import os
6+
import tempfile
7+
from unittest import TestCase
8+
9+
from cyclonedx.factory.license import LicenseFactory
10+
from cyclonedx.model.license import DisjunctiveLicense, LicenseAcknowledgement
11+
12+
from cyclonedx_py._internal.utils.pep621 import project2licenses
13+
14+
15+
class TestProject2Licenses(TestCase):
16+
def setUp(self):
17+
self.lfac = LicenseFactory()
18+
self.fpath = tempfile.mktemp()
19+
20+
def test_license_string_pep639(self):
21+
project = {
22+
'name': 'testpkg',
23+
'license': 'LicenseRef-Platform-Software-General-1.0',
24+
}
25+
licenses = list(project2licenses(project, self.lfac, fpath=self.fpath))
26+
self.assertEqual(len(licenses), 1)
27+
lic = licenses[0]
28+
self.assertIsInstance(lic, DisjunctiveLicense)
29+
self.assertEqual(lic.acknowledgement, LicenseAcknowledgement.DECLARED)
30+
if lic.id is not None:
31+
self.assertEqual(lic.id, 'LicenseRef-Platform-Software-General-1.0')
32+
elif lic.text is not None:
33+
self.assertEqual(lic.text.content, 'LicenseRef-Platform-Software-General-1.0')
34+
else:
35+
# Acceptable fallback: both id and text are None for unknown license references
36+
self.assertIsNone(lic.id)
37+
self.assertIsNone(lic.text)
38+
39+
def test_license_dict_text_pep621(self):
40+
project = {
41+
'name': 'testpkg',
42+
'license': {'text': 'This is the license text.'},
43+
}
44+
licenses = list(project2licenses(project, self.lfac, fpath=self.fpath))
45+
self.assertEqual(len(licenses), 1)
46+
lic = licenses[0]
47+
self.assertIsInstance(lic, DisjunctiveLicense)
48+
self.assertIsNone(lic.id)
49+
self.assertEqual(lic.text.content, 'This is the license text.')
50+
self.assertEqual(lic.acknowledgement, LicenseAcknowledgement.DECLARED)
51+
52+
def test_license_dict_file_pep621(self):
53+
with tempfile.NamedTemporaryFile('w+', delete=False) as tf:
54+
tf.write('File license text')
55+
tf.flush()
56+
project = {
57+
'name': 'testpkg',
58+
'license': {'file': os.path.basename(tf.name)},
59+
}
60+
# fpath should be the file path so dirname(fpath) resolves to the correct directory
61+
licenses = list(project2licenses(project, self.lfac, fpath=tf.name))
62+
self.assertEqual(len(licenses), 1)
63+
lic = licenses[0]
64+
self.assertIsInstance(lic, DisjunctiveLicense)
65+
self.assertIsNotNone(lic.text.content)
66+
self.assertEqual(lic.acknowledgement, LicenseAcknowledgement.DECLARED)
67+
os.unlink(tf.name)

0 commit comments

Comments
 (0)