Skip to content
7 changes: 2 additions & 5 deletions cyclonedx_py/_internal/utils/pep621.py
Original file line number Diff line number Diff line change
Expand Up @@ -60,13 +60,9 @@ def project2licenses(project: dict[str, Any], lfac: 'LicenseFactory', *,
# https://peps.python.org/pep-0621/#classifiers
# https://packaging.python.org/en/latest/specifications/core-metadata/#classifier-multiple-use
yield from classifiers2licenses(classifiers, lfac, lack)
if plicense := project.get('license'):
if isinstance(plicense := project.get('license'), dict):
# 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:
Expand All @@ -87,6 +83,7 @@ def project2licenses(project: dict[str, Any], lfac: 'LicenseFactory', *,
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
76 changes: 76 additions & 0 deletions tests/integration/test_utils_pep621.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,76 @@
# 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 TestUtilsPEP621(unittest.TestCase):

def test_license_dict_text_pep621(self) -> None:
lfac = LicenseFactory()
fpath = tempfile.mktemp()
project = {
'name': 'testpkg',
'license': {'text': 'This is the license text.'},
}
licenses = list(project2licenses(project, lfac, fpath=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) -> None:
lfac = LicenseFactory()
with tempfile.NamedTemporaryFile('w+', delete=True) 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, lfac, fpath=tf.name))

self.assertEqual(len(licenses), 1)
lic = licenses[0]
self.assertIsInstance(lic, DisjunctiveLicense)
self.assertIsNotNone(lic.text.content)
self.assertEqual(lic.acknowledgement, LicenseAcknowledgement.DECLARED)

def test_license_non_dict_pep621(self) -> None:
lfac = LicenseFactory()
fpath = tempfile.mktemp()

# Test with string license (should be silently skipped)
project = {
'name': 'testpkg',
'license': 'MIT',
}
licenses = list(project2licenses(project, lfac, fpath=fpath))
self.assertEqual(len(licenses), 0)

# Test with None license (should be silently skipped)
project = {
'name': 'testpkg',
'license': None,
}
licenses = list(project2licenses(project, lfac, fpath=fpath))
self.assertEqual(len(licenses), 0)

# Test with list license (should be silently skipped)
project = {
'name': 'testpkg',
'license': ['MIT', 'Apache-2.0'],
}
licenses = list(project2licenses(project, lfac, fpath=fpath))
self.assertEqual(len(licenses), 0)
Loading