From e0489806473ed7074cb13655f946cd05e3369b12 Mon Sep 17 00:00:00 2001 From: Manav Gupta Date: Mon, 23 Jun 2025 12:53:30 -0400 Subject: [PATCH 01/11] Fix: update pep621 logic and add unit tests Signed-off-by: Manav Gupta --- cyclonedx_py/_internal/utils/pep621.py | 51 ++++++++++---------- tests/unit/test_pep621.py | 67 ++++++++++++++++++++++++++ 2 files changed, 92 insertions(+), 26 deletions(-) create mode 100644 tests/unit/test_pep621.py diff --git a/cyclonedx_py/_internal/utils/pep621.py b/cyclonedx_py/_internal/utils/pep621.py index a128dff9f..0be997d95 100644 --- a/cyclonedx_py/_internal/utils/pep621.py +++ b/cyclonedx_py/_internal/utils/pep621.py @@ -61,32 +61,31 @@ 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 - # https://peps.python.org/pep-0621/#license - # https://packaging.python.org/en/latest/specifications/core-metadata/#license - if 'file' in plicense and 'text' in plicense: - # per spec: - # > 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 + # Handle both PEP 621 (dict) and PEP 639 (str) license formats + if isinstance(plicense, dict): + 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 + elif isinstance(plicense, str): + # PEP 639: license is a string (SPDX expression or license reference) + license = lfac.make_from_string(plicense, license_acknowledgement=lack) + yield license + else: + raise TypeError(f"Unexpected type for 'license': {type(plicense)}") def project2extrefs(project: dict[str, Any]) -> Generator['ExternalReference', None, None]: diff --git a/tests/unit/test_pep621.py b/tests/unit/test_pep621.py new file mode 100644 index 000000000..e9aa95f43 --- /dev/null +++ b/tests/unit/test_pep621.py @@ -0,0 +1,67 @@ +# This file is part of CycloneDX Python +# SPDX-License-Identifier: Apache-2.0 +# Copyright (c) OWASP Foundation. All Rights Reserved. + +import os +import tempfile +from unittest import TestCase + +from cyclonedx.factory.license import LicenseFactory +from cyclonedx.model.license import DisjunctiveLicense, LicenseAcknowledgement + +from cyclonedx_py._internal.utils.pep621 import project2licenses + + +class TestProject2Licenses(TestCase): + def setUp(self): + self.lfac = LicenseFactory() + self.fpath = tempfile.mktemp() + + def test_license_string_pep639(self): + project = { + 'name': 'testpkg', + 'license': 'LicenseRef-Platform-Software-General-1.0', + } + licenses = list(project2licenses(project, self.lfac, fpath=self.fpath)) + self.assertEqual(len(licenses), 1) + lic = licenses[0] + self.assertIsInstance(lic, DisjunctiveLicense) + self.assertEqual(lic.acknowledgement, LicenseAcknowledgement.DECLARED) + if lic.id is not None: + self.assertEqual(lic.id, 'LicenseRef-Platform-Software-General-1.0') + elif lic.text is not None: + self.assertEqual(lic.text.content, 'LicenseRef-Platform-Software-General-1.0') + else: + # Acceptable fallback: both id and text are None for unknown license references + self.assertIsNone(lic.id) + self.assertIsNone(lic.text) + + 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) + lic = licenses[0] + self.assertIsInstance(lic, DisjunctiveLicense) + self.assertIsNotNone(lic.text.content) + self.assertEqual(lic.acknowledgement, LicenseAcknowledgement.DECLARED) + os.unlink(tf.name) From d5eb743f6fed22f571d63f7fea1552c8c7950da0 Mon Sep 17 00:00:00 2001 From: Manav Gupta Date: Tue, 24 Jun 2025 11:42:59 -0400 Subject: [PATCH 02/11] removed implementation of PEP639, removed raise, and updated test file Signed-off-by: Manav Gupta --- cyclonedx_py/_internal/utils/pep621.py | 9 ++------- tests/unit/test_pep621.py | 23 ++--------------------- 2 files changed, 4 insertions(+), 28 deletions(-) diff --git a/cyclonedx_py/_internal/utils/pep621.py b/cyclonedx_py/_internal/utils/pep621.py index 0be997d95..348bd0a46 100644 --- a/cyclonedx_py/_internal/utils/pep621.py +++ b/cyclonedx_py/_internal/utils/pep621.py @@ -61,7 +61,7 @@ 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'): - # Handle both PEP 621 (dict) and PEP 639 (str) license formats + # Only handle PEP 621 (dict) license formats if isinstance(plicense, dict): if 'file' in plicense and 'text' in plicense: raise ValueError('`license.file` and `license.text` are mutually exclusive,') @@ -80,12 +80,7 @@ def project2licenses(project: dict[str, Any], lfac: 'LicenseFactory', *, text=AttachedText(content=plicense_text)) else: yield license - elif isinstance(plicense, str): - # PEP 639: license is a string (SPDX expression or license reference) - license = lfac.make_from_string(plicense, license_acknowledgement=lack) - yield license - else: - raise TypeError(f"Unexpected type for 'license': {type(plicense)}") + # Silently skip any other types (including string/PEP 639) def project2extrefs(project: dict[str, Any]) -> Generator['ExternalReference', None, None]: diff --git a/tests/unit/test_pep621.py b/tests/unit/test_pep621.py index e9aa95f43..1d8e93eaa 100644 --- a/tests/unit/test_pep621.py +++ b/tests/unit/test_pep621.py @@ -4,7 +4,7 @@ import os import tempfile -from unittest import TestCase +import unittest from cyclonedx.factory.license import LicenseFactory from cyclonedx.model.license import DisjunctiveLicense, LicenseAcknowledgement @@ -12,30 +12,11 @@ from cyclonedx_py._internal.utils.pep621 import project2licenses -class TestProject2Licenses(TestCase): +class TestPEP621(unittest.TestCase): def setUp(self): self.lfac = LicenseFactory() self.fpath = tempfile.mktemp() - def test_license_string_pep639(self): - project = { - 'name': 'testpkg', - 'license': 'LicenseRef-Platform-Software-General-1.0', - } - licenses = list(project2licenses(project, self.lfac, fpath=self.fpath)) - self.assertEqual(len(licenses), 1) - lic = licenses[0] - self.assertIsInstance(lic, DisjunctiveLicense) - self.assertEqual(lic.acknowledgement, LicenseAcknowledgement.DECLARED) - if lic.id is not None: - self.assertEqual(lic.id, 'LicenseRef-Platform-Software-General-1.0') - elif lic.text is not None: - self.assertEqual(lic.text.content, 'LicenseRef-Platform-Software-General-1.0') - else: - # Acceptable fallback: both id and text are None for unknown license references - self.assertIsNone(lic.id) - self.assertIsNone(lic.text) - def test_license_dict_text_pep621(self): project = { 'name': 'testpkg', From 90b85af73d4c30a62a13db388ded61d005816c96 Mon Sep 17 00:00:00 2001 From: Manav Gupta Date: Tue, 24 Jun 2025 13:51:42 -0400 Subject: [PATCH 03/11] fix(pep621): Only handle dict license values, skip others silently - Simplified license handling to only process dict (PEP 621) values. - Silently skip string/other types. - Combined assignment and type check for clarity. Signed-off-by: Manav Gupta --- cyclonedx_py/_internal/utils/pep621.py | 40 ++++++++++++-------------- 1 file changed, 19 insertions(+), 21 deletions(-) diff --git a/cyclonedx_py/_internal/utils/pep621.py b/cyclonedx_py/_internal/utils/pep621.py index 348bd0a46..ad5fede09 100644 --- a/cyclonedx_py/_internal/utils/pep621.py +++ b/cyclonedx_py/_internal/utils/pep621.py @@ -60,27 +60,25 @@ 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'): - # Only handle PEP 621 (dict) license formats - if isinstance(plicense, dict): - 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) + if isinstance(plicense := project.get('license'), dict): + 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]: From 0faa6daabd614b66d3ee0ab1b7d8c3722cf67c44 Mon Sep 17 00:00:00 2001 From: Manav Gupta Date: Tue, 24 Jun 2025 19:47:36 -0400 Subject: [PATCH 04/11] improved documentation in pep621.py, moved test_pep621.py to integration/test_utils_pep621.py, and improved test coverage Signed-off-by: Manav Gupta --- cyclonedx_py/_internal/utils/pep621.py | 5 ++ tests/integration/test_utils_pep621.py | 76 ++++++++++++++++++++++++++ tests/unit/test_pep621.py | 48 ---------------- 3 files changed, 81 insertions(+), 48 deletions(-) create mode 100644 tests/integration/test_utils_pep621.py delete mode 100644 tests/unit/test_pep621.py diff --git a/cyclonedx_py/_internal/utils/pep621.py b/cyclonedx_py/_internal/utils/pep621.py index ad5fede09..418bb2f29 100644 --- a/cyclonedx_py/_internal/utils/pep621.py +++ b/cyclonedx_py/_internal/utils/pep621.py @@ -61,9 +61,13 @@ 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 isinstance(plicense := project.get('license'), dict): + # https://packaging.python.org/en/latest/specifications/pyproject-toml/#license if 'file' in plicense and 'text' in plicense: 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, @@ -73,6 +77,7 @@ def project2licenses(project: dict[str, Any], lfac: 'LicenseFactory', *, 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)) diff --git a/tests/integration/test_utils_pep621.py b/tests/integration/test_utils_pep621.py new file mode 100644 index 000000000..471c0794d --- /dev/null +++ b/tests/integration/test_utils_pep621.py @@ -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) diff --git a/tests/unit/test_pep621.py b/tests/unit/test_pep621.py deleted file mode 100644 index 1d8e93eaa..000000000 --- a/tests/unit/test_pep621.py +++ /dev/null @@ -1,48 +0,0 @@ -# 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): - def setUp(self): - self.lfac = LicenseFactory() - 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) - lic = licenses[0] - self.assertIsInstance(lic, DisjunctiveLicense) - self.assertIsNotNone(lic.text.content) - self.assertEqual(lic.acknowledgement, LicenseAcknowledgement.DECLARED) - os.unlink(tf.name) From 06a22c70ae50fb70ff53897ba5d1dc91e0dc9203 Mon Sep 17 00:00:00 2001 From: Manav Gupta Date: Wed, 25 Jun 2025 11:55:12 -0400 Subject: [PATCH 05/11] Added missing docs Signed-off-by: Manav Gupta --- cyclonedx_py/_internal/utils/pep621.py | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/cyclonedx_py/_internal/utils/pep621.py b/cyclonedx_py/_internal/utils/pep621.py index 418bb2f29..658b1a510 100644 --- a/cyclonedx_py/_internal/utils/pep621.py +++ b/cyclonedx_py/_internal/utils/pep621.py @@ -62,7 +62,12 @@ def project2licenses(project: dict[str, Any], lfac: 'LicenseFactory', *, yield from classifiers2licenses(classifiers, lfac, lack) if isinstance(plicense := project.get('license'), dict): # https://packaging.python.org/en/latest/specifications/pyproject-toml/#license + # https://peps.python.org/pep-0621/#license + # https://packaging.python.org/en/latest/specifications/core-metadata/#license if 'file' in plicense and 'text' in plicense: + # per spec: + # > If both a file and text are provided, tools MUST raise an error. + # > Tools MUST NOT assume that the file and text are the same. raise ValueError('`license.file` and `license.text` are mutually exclusive,') if 'file' in plicense: # per spec: From c2d0830179868a5b8500b938f172d8eebe03c635 Mon Sep 17 00:00:00 2001 From: Jan Kowalleck Date: Wed, 25 Jun 2025 18:00:00 +0200 Subject: [PATCH 06/11] Update pep621.py Signed-off-by: Jan Kowalleck --- cyclonedx_py/_internal/utils/pep621.py | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/cyclonedx_py/_internal/utils/pep621.py b/cyclonedx_py/_internal/utils/pep621.py index 658b1a510..cfb3ac979 100644 --- a/cyclonedx_py/_internal/utils/pep621.py +++ b/cyclonedx_py/_internal/utils/pep621.py @@ -66,8 +66,7 @@ def project2licenses(project: dict[str, Any], lfac: 'LicenseFactory', *, # https://packaging.python.org/en/latest/specifications/core-metadata/#license if 'file' in plicense and 'text' in plicense: # per spec: - # > If both a file and text are provided, tools MUST raise an error. - # > Tools MUST NOT assume that the file and text are the same. + # > 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: From acbffb41af29143415c1c2c3c8b7e33d2dfe88be Mon Sep 17 00:00:00 2001 From: Manav Gupta Date: Thu, 26 Jun 2025 09:17:14 -0400 Subject: [PATCH 07/11] fixed bandit and linting issues. Signed-off-by: Manav Gupta --- tests/integration/test_utils_pep621.py | 61 ++++++++++++++++---------- 1 file changed, 37 insertions(+), 24 deletions(-) diff --git a/tests/integration/test_utils_pep621.py b/tests/integration/test_utils_pep621.py index 471c0794d..30586204f 100644 --- a/tests/integration/test_utils_pep621.py +++ b/tests/integration/test_utils_pep621.py @@ -1,4 +1,17 @@ # This file is part of CycloneDX Python +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +# # SPDX-License-Identifier: Apache-2.0 # Copyright (c) OWASP Foundation. All Rights Reserved. @@ -16,36 +29,36 @@ 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) + with tempfile.TemporaryDirectory() as tmpdir: + fpath = tmpdir # Use the temp directory as the base for any temp files + 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() + with tempfile.TemporaryDirectory() as tmpdir: + file_path = os.path.join(tmpdir, 'license.txt') + with open(file_path, 'w') as tf: + tf.write('File license text') project = { 'name': 'testpkg', - 'license': {'file': os.path.basename(tf.name)}, + 'license': {'file': 'license.txt'}, } - # 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) + licenses = list(project2licenses(project, lfac, fpath=file_path)) + 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() From 107864e270983e3381e70663edeb65540e6fa7af Mon Sep 17 00:00:00 2001 From: Jan Kowalleck Date: Thu, 26 Jun 2025 15:45:43 +0200 Subject: [PATCH 08/11] tests Signed-off-by: Jan Kowalleck --- tests/integration/test_utils_pep621.py | 89 +++++++++++--------------- 1 file changed, 38 insertions(+), 51 deletions(-) diff --git a/tests/integration/test_utils_pep621.py b/tests/integration/test_utils_pep621.py index 30586204f..bb5d0b3e1 100644 --- a/tests/integration/test_utils_pep621.py +++ b/tests/integration/test_utils_pep621.py @@ -15,75 +15,62 @@ # SPDX-License-Identifier: Apache-2.0 # Copyright (c) OWASP Foundation. All Rights Reserved. -import os -import tempfile -import unittest +from os.path import join +from tempfile import TemporaryDirectory +from unittest import TestCase from cyclonedx.factory.license import LicenseFactory from cyclonedx.model.license import DisjunctiveLicense, LicenseAcknowledgement +from ddt import ddt, named_data from cyclonedx_py._internal.utils.pep621 import project2licenses -class TestUtilsPEP621(unittest.TestCase): +@ddt() +class TestUtilsPEP621(TestCase): def test_license_dict_text_pep621(self) -> None: - lfac = LicenseFactory() - with tempfile.TemporaryDirectory() as tmpdir: - fpath = tmpdir # Use the temp directory as the base for any temp files - 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.TemporaryDirectory() as tmpdir: - file_path = os.path.join(tmpdir, 'license.txt') - with open(file_path, 'w') as tf: - tf.write('File license text') - project = { - 'name': 'testpkg', - 'license': {'file': 'license.txt'}, - } - licenses = list(project2licenses(project, lfac, fpath=file_path)) - 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', + 'license': {'text': 'This is the license text.'}, } - licenses = list(project2licenses(project, lfac, fpath=fpath)) - self.assertEqual(len(licenses), 0) + lfac = LicenseFactory() + with TemporaryDirectory() as tmpdir: + licenses = list(project2licenses(project, lfac, fpath=join(tmpdir, 'pyproject.toml'))) + 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) - # Test with None license (should be silently skipped) + def test_license_dict_file_pep621(self) -> None: project = { 'name': 'testpkg', - 'license': None, + 'license': {'file': 'license.txt'}, } - licenses = list(project2licenses(project, lfac, fpath=fpath)) - self.assertEqual(len(licenses), 0) + lfac = LicenseFactory() + with TemporaryDirectory() as tmpdir: + with open(join(tmpdir, project['license']['file']), 'w') as tf: + tf.write('File license text') + licenses = list(project2licenses(project, lfac, fpath=join(tmpdir, 'pyproject.toml'))) + self.assertEqual(len(licenses), 1) + lic = licenses[0] + self.assertIsInstance(lic, DisjunctiveLicense) + self.assertIsNotNone(lic.text.content) + self.assertEqual(lic.acknowledgement, LicenseAcknowledgement.DECLARED) - # Test with list license (should be silently skipped) + @named_data( + ('none', None), + ('string', 'MIT'), + ('list', ['MIT', 'Apache-2.0']) + ) + def test_license_non_dict_pep621(self, license: any) -> None: project = { 'name': 'testpkg', - 'license': ['MIT', 'Apache-2.0'], + 'license': license, } - licenses = list(project2licenses(project, lfac, fpath=fpath)) + lfac = LicenseFactory() + with TemporaryDirectory() as tmpdir: + licenses = list(project2licenses(project, lfac, fpath=join(tmpdir, 'pyproject.toml'))) self.assertEqual(len(licenses), 0) From 5ec1314e3306948efb6e9e0fa648430bac685c98 Mon Sep 17 00:00:00 2001 From: Jan Kowalleck Date: Thu, 26 Jun 2025 15:47:14 +0200 Subject: [PATCH 09/11] tests Signed-off-by: Jan Kowalleck --- tests/integration/test_utils_pep621.py | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/tests/integration/test_utils_pep621.py b/tests/integration/test_utils_pep621.py index bb5d0b3e1..31e36d6ea 100644 --- a/tests/integration/test_utils_pep621.py +++ b/tests/integration/test_utils_pep621.py @@ -29,7 +29,7 @@ @ddt() class TestUtilsPEP621(TestCase): - def test_license_dict_text_pep621(self) -> None: + def test_license_dict_text(self) -> None: project = { 'name': 'testpkg', 'license': {'text': 'This is the license text.'}, @@ -44,7 +44,7 @@ def test_license_dict_text_pep621(self) -> None: self.assertEqual(lic.text.content, 'This is the license text.') self.assertEqual(lic.acknowledgement, LicenseAcknowledgement.DECLARED) - def test_license_dict_file_pep621(self) -> None: + def test_license_dict_file(self) -> None: project = { 'name': 'testpkg', 'license': {'file': 'license.txt'}, @@ -65,7 +65,7 @@ def test_license_dict_file_pep621(self) -> None: ('string', 'MIT'), ('list', ['MIT', 'Apache-2.0']) ) - def test_license_non_dict_pep621(self, license: any) -> None: + def test_license_non_dict(self, license: any) -> None: project = { 'name': 'testpkg', 'license': license, From dedf64d368de7407509f9b95556766112ee960e7 Mon Sep 17 00:00:00 2001 From: Jan Kowalleck Date: Thu, 26 Jun 2025 16:05:28 +0200 Subject: [PATCH 10/11] tests Signed-off-by: Jan Kowalleck --- tests/integration/test_utils_pep621.py | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/tests/integration/test_utils_pep621.py b/tests/integration/test_utils_pep621.py index 31e36d6ea..3cb1cd600 100644 --- a/tests/integration/test_utils_pep621.py +++ b/tests/integration/test_utils_pep621.py @@ -20,6 +20,7 @@ from unittest import TestCase from cyclonedx.factory.license import LicenseFactory +from cyclonedx.model import Encoding from cyclonedx.model.license import DisjunctiveLicense, LicenseAcknowledgement from ddt import ddt, named_data @@ -41,6 +42,7 @@ def test_license_dict_text(self) -> None: lic = licenses[0] self.assertIsInstance(lic, DisjunctiveLicense) self.assertIsNone(lic.id) + self.assertIsNone(lic.text.encoding) self.assertEqual(lic.text.content, 'This is the license text.') self.assertEqual(lic.acknowledgement, LicenseAcknowledgement.DECLARED) @@ -57,7 +59,8 @@ def test_license_dict_file(self) -> None: self.assertEqual(len(licenses), 1) lic = licenses[0] self.assertIsInstance(lic, DisjunctiveLicense) - self.assertIsNotNone(lic.text.content) + self.assertIs(lic.text.encoding, Encoding.BASE_64) + self.assertEqual(lic.text.content, 'RmlsZSBsaWNlbnNlIHRleHQ=') self.assertEqual(lic.acknowledgement, LicenseAcknowledgement.DECLARED) @named_data( From 0f4a2b2985c2123b3c4d18eb1c4e20bcfa4b3c99 Mon Sep 17 00:00:00 2001 From: Jan Kowalleck Date: Thu, 26 Jun 2025 16:09:38 +0200 Subject: [PATCH 11/11] tests Signed-off-by: Jan Kowalleck --- tests/integration/test_utils_pep621.py | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/tests/integration/test_utils_pep621.py b/tests/integration/test_utils_pep621.py index 3cb1cd600..463cddd48 100644 --- a/tests/integration/test_utils_pep621.py +++ b/tests/integration/test_utils_pep621.py @@ -30,7 +30,7 @@ @ddt() class TestUtilsPEP621(TestCase): - def test_license_dict_text(self) -> None: + def test_project2licenses_license_dict_text(self) -> None: project = { 'name': 'testpkg', 'license': {'text': 'This is the license text.'}, @@ -46,7 +46,7 @@ def test_license_dict_text(self) -> None: self.assertEqual(lic.text.content, 'This is the license text.') self.assertEqual(lic.acknowledgement, LicenseAcknowledgement.DECLARED) - def test_license_dict_file(self) -> None: + def test_project2licenses_license_dict_file(self) -> None: project = { 'name': 'testpkg', 'license': {'file': 'license.txt'}, @@ -68,7 +68,7 @@ def test_license_dict_file(self) -> None: ('string', 'MIT'), ('list', ['MIT', 'Apache-2.0']) ) - def test_license_non_dict(self, license: any) -> None: + def test_project2licenses_license_non_dict(self, license: any) -> None: project = { 'name': 'testpkg', 'license': license,