Skip to content
46 changes: 20 additions & 26 deletions cyclonedx_py/_internal/utils/pep621.py
Original file line number Diff line number Diff line change
Expand Up @@ -61,32 +61,26 @@ def project2licenses(project: dict[str, Any], lfac: 'LicenseFactory', *,
# https://packaging.python.org/en/latest/specifications/core-metadata/#classifier-multiple-use
yield from classifiers2licenses(classifiers, lfac, lack)
if plicense := project.get('license'):
# https://packaging.python.org/en/latest/specifications/pyproject-toml/#license
Comment thread
jkowalleck marked this conversation as resolved.
# https://peps.python.org/pep-0621/#license
Comment thread
jkowalleck marked this conversation as resolved.
# https://packaging.python.org/en/latest/specifications/core-metadata/#license
if 'file' in plicense and 'text' in plicense:
# per spec:
Comment thread
jkowalleck marked this conversation as resolved.
# > These keys are mutually exclusive, so a tool MUST raise an error if the metadata specifies both keys.
raise ValueError('`license.file` and `license.text` are mutually exclusive,')
if 'file' in plicense:
# per spec:
# > [...] a string value that is a relative file path [...].
# > Tools MUST assume the file’s encoding is UTF-8.
with open(join(dirname(fpath), plicense['file']), 'rb') as plicense_fileh:
yield DisjunctiveLicense(name=f"declared license of '{project['name']}'",
acknowledgement=lack,
text=AttachedText(encoding=Encoding.BASE_64,
content=b64encode(plicense_fileh.read()).decode()))
elif len(plicense_text := plicense.get('text', '')) > 0:
license = lfac.make_from_string(plicense_text,
license_acknowledgement=lack)
if isinstance(license, DisjunctiveLicense) and license.id is None:
# per spec, `License` is either a SPDX ID/Expression, or a license text(not name!)
yield DisjunctiveLicense(name=f"declared license of '{project['name']}'",
acknowledgement=lack,
text=AttachedText(content=plicense_text))
else:
yield license
# Only handle PEP 621 (dict) license formats
if isinstance(plicense, dict):
Comment thread
jkowalleck marked this conversation as resolved.
Outdated
if 'file' in plicense and 'text' in plicense:
raise ValueError('`license.file` and `license.text` are mutually exclusive,')
if 'file' in plicense:
with open(join(dirname(fpath), plicense['file']), 'rb') as plicense_fileh:
yield DisjunctiveLicense(name=f"declared license of '{project['name']}'",
acknowledgement=lack,
text=AttachedText(encoding=Encoding.BASE_64,
content=b64encode(plicense_fileh.read()).decode()))
elif len(plicense_text := plicense.get('text', '')) > 0:
license = lfac.make_from_string(plicense_text,
license_acknowledgement=lack)
if isinstance(license, DisjunctiveLicense) and license.id is None:
yield DisjunctiveLicense(name=f"declared license of '{project['name']}'",
acknowledgement=lack,
text=AttachedText(content=plicense_text))
else:
yield license
# Silently skip any other types (including string/PEP 639)


def project2extrefs(project: dict[str, Any]) -> Generator['ExternalReference', None, None]:
Expand Down
48 changes: 48 additions & 0 deletions tests/unit/test_pep621.py
Comment thread
manavgup marked this conversation as resolved.
Outdated
Original file line number Diff line number Diff line change
@@ -0,0 +1,48 @@
# This file is part of CycloneDX Python
# SPDX-License-Identifier: Apache-2.0
# Copyright (c) OWASP Foundation. All Rights Reserved.

import os
import tempfile
import unittest

from cyclonedx.factory.license import LicenseFactory
from cyclonedx.model.license import DisjunctiveLicense, LicenseAcknowledgement

from cyclonedx_py._internal.utils.pep621 import project2licenses


class TestPEP621(unittest.TestCase):
Comment thread
jkowalleck marked this conversation as resolved.
Outdated
Comment thread
jkowalleck marked this conversation as resolved.
Outdated
def setUp(self):
Comment thread
jkowalleck marked this conversation as resolved.
Outdated
self.lfac = LicenseFactory()
Comment thread
jkowalleck marked this conversation as resolved.
Outdated
self.fpath = tempfile.mktemp()

def test_license_dict_text_pep621(self):
project = {
'name': 'testpkg',
'license': {'text': 'This is the license text.'},
}
licenses = list(project2licenses(project, self.lfac, fpath=self.fpath))
self.assertEqual(len(licenses), 1)
lic = licenses[0]
self.assertIsInstance(lic, DisjunctiveLicense)
self.assertIsNone(lic.id)
self.assertEqual(lic.text.content, 'This is the license text.')
self.assertEqual(lic.acknowledgement, LicenseAcknowledgement.DECLARED)

def test_license_dict_file_pep621(self):
with tempfile.NamedTemporaryFile('w+', delete=False) as tf:
tf.write('File license text')
tf.flush()
project = {
'name': 'testpkg',
'license': {'file': os.path.basename(tf.name)},
}
# fpath should be the file path so dirname(fpath) resolves to the correct directory
licenses = list(project2licenses(project, self.lfac, fpath=tf.name))
self.assertEqual(len(licenses), 1)
Comment thread
jkowalleck marked this conversation as resolved.
Outdated
lic = licenses[0]
self.assertIsInstance(lic, DisjunctiveLicense)
self.assertIsNotNone(lic.text.content)
self.assertEqual(lic.acknowledgement, LicenseAcknowledgement.DECLARED)
os.unlink(tf.name)
Comment thread
jkowalleck marked this conversation as resolved.
Outdated