Skip to content

Commit b0ae453

Browse files
authored
feat: improve declared licenses detection (#722)
- Add declared licenses from License Troves if not mapped to SPDX license ID - CycloneDX 1.6 mark licenses as "declared" fixes #718 --------- Signed-off-by: Jan Kowalleck <jan.kowalleck@gmail.com>
1 parent 0181c3d commit b0ae453

File tree

91 files changed

+1017
-90
lines changed

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

91 files changed

+1017
-90
lines changed

cyclonedx_py/_internal/utils/license_trove_classifier.py

Lines changed: 11 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -21,9 +21,15 @@
2121
All in here may have breaking change without notice.
2222
"""
2323

24-
2524
from typing import Optional
2625

26+
__LICENSE_TROVE_PREFIX = 'License :: '
27+
28+
29+
def is_license_trove(classifier: str) -> bool:
30+
return classifier.startswith(__LICENSE_TROVE_PREFIX)
31+
32+
2733
"""
2834
Map of trove classifiers to SPDX license ID or SPDX license expression.
2935
@@ -73,6 +79,7 @@
7379
# !! see the ideas and cases of https://peps.python.org/pep-0639/#mapping-license-classifiers-to-spdx-identifiers
7480
# 'License :: OSI Approved :: Academic Free License (AFL)': which one?
7581
# - AFL-1.1
82+
# - AFL-...
7683
# - AFL-3.0
7784
# 'License :: OSI Approved :: Apache Software License': which one?
7885
# - Apache-1.1
@@ -81,6 +88,9 @@
8188
# - APSL-1.0
8289
# - APSL-2.0
8390
# 'License :: OSI Approved :: Artistic License': which version?
91+
# - Artistic-1.0
92+
# - Artistic-...
93+
# - Artistic-3.0
8494
'License :: OSI Approved :: Attribution Assurance License': 'AAL',
8595
# 'License :: OSI Approved :: BSD License': which exactly?
8696
'License :: OSI Approved :: Boost Software License 1.0 (BSL-1.0)': 'BSL-1.0',

cyclonedx_py/_internal/utils/packaging.py

Lines changed: 13 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -21,7 +21,7 @@
2121
from cyclonedx.exception.model import InvalidUriException
2222
from cyclonedx.factory.license import LicenseFactory
2323
from cyclonedx.model import AttachedText, ExternalReference, ExternalReferenceType, XsUri
24-
from cyclonedx.model.license import DisjunctiveLicense
24+
from cyclonedx.model.license import DisjunctiveLicense, LicenseAcknowledgement
2525

2626
from .cdx import url_label_to_ert
2727
from .pep621 import classifiers2licenses
@@ -39,19 +39,26 @@
3939

4040
def metadata2licenses(metadata: 'PackageMetadata') -> Generator['License', None, None]:
4141
lfac = LicenseFactory()
42+
lack = LicenseAcknowledgement.DECLARED
4243
if 'Classifier' in metadata:
43-
# see https://packaging.python.org/en/latest/specifications/core-metadata/#classifier-multiple-use
44+
# see spec: https://packaging.python.org/en/latest/specifications/core-metadata/#classifier-multiple-use
4445
classifiers: List[str] = metadata.get_all('Classifier') # type:ignore[assignment]
45-
yield from classifiers2licenses(classifiers, lfac)
46-
if 'License' in metadata and len(mlicense := metadata['License']) > 0:
47-
# see https://packaging.python.org/en/latest/specifications/core-metadata/#license
48-
license = lfac.make_from_string(mlicense)
46+
yield from classifiers2licenses(classifiers, lfac, lack)
47+
for mlicense in metadata.get_all('License', ()):
48+
# see spec: https://packaging.python.org/en/latest/specifications/core-metadata/#license
49+
if len(mlicense) <= 0:
50+
continue
51+
license = lfac.make_from_string(mlicense,
52+
license_acknowledgement=lack)
4953
if isinstance(license, DisjunctiveLicense) and license.id is None:
5054
# per spec, `License` is either a SPDX ID/Expression, or a license text(not name!)
5155
yield DisjunctiveLicense(name=f"declared license of '{metadata['Name']}'",
56+
acknowledgement=lack,
5257
text=AttachedText(content=mlicense))
5358
else:
5459
yield license
60+
# TODO: iterate over "License-File" declarations and read them
61+
# for mlfile in metadata.get_all('License-File'): ...
5562

5663

5764
def metadata2extrefs(metadata: 'PackageMetadata') -> Generator['ExternalReference', None, None]:

cyclonedx_py/_internal/utils/pep621.py

Lines changed: 15 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -31,34 +31,34 @@
3131
from cyclonedx.factory.license import LicenseFactory
3232
from cyclonedx.model import AttachedText, Encoding, ExternalReference, XsUri
3333
from cyclonedx.model.component import Component
34-
from cyclonedx.model.license import DisjunctiveLicense
34+
from cyclonedx.model.license import DisjunctiveLicense, LicenseAcknowledgement
3535
from packaging.requirements import Requirement
3636

3737
from .cdx import licenses_fixup, url_label_to_ert
38-
from .license_trove_classifier import license_trove2spdx
38+
from .license_trove_classifier import is_license_trove, license_trove2spdx
3939

4040
if TYPE_CHECKING:
4141
from cyclonedx.model.component import ComponentType
4242
from cyclonedx.model.license import License
4343

4444

45-
def classifiers2licenses(classifiers: Iterable[str], lfac: 'LicenseFactory') -> Generator['License', None, None]:
46-
yield from map(lfac.make_from_string,
47-
# `lfac.make_with_id` could be a shortcut,
48-
# but some SPDX ID might not (yet) be known to CDX.
49-
# So better go with `lfac.make_from_string` and be safe.
50-
filter(None,
51-
map(license_trove2spdx,
52-
classifiers)))
45+
def classifiers2licenses(classifiers: Iterable[str], lfac: 'LicenseFactory',
46+
lack: 'LicenseAcknowledgement'
47+
) -> Generator['License', None, None]:
48+
for c in classifiers:
49+
if is_license_trove(c):
50+
yield lfac.make_from_string(license_trove2spdx(c) or c,
51+
license_acknowledgement=lack)
5352

5453

5554
def project2licenses(project: Dict[str, Any], lfac: 'LicenseFactory', *,
5655
fpath: str) -> Generator['License', None, None]:
56+
lack = LicenseAcknowledgement.DECLARED
5757
if classifiers := project.get('classifiers'):
5858
# https://packaging.python.org/en/latest/specifications/pyproject-toml/#classifiers
5959
# https://peps.python.org/pep-0621/#classifiers
6060
# https://packaging.python.org/en/latest/specifications/core-metadata/#classifier-multiple-use
61-
yield from classifiers2licenses(classifiers, lfac)
61+
yield from classifiers2licenses(classifiers, lfac, lack)
6262
if plicense := project.get('license'):
6363
# https://packaging.python.org/en/latest/specifications/pyproject-toml/#license
6464
# https://peps.python.org/pep-0621/#license
@@ -73,13 +73,16 @@ def project2licenses(project: Dict[str, Any], lfac: 'LicenseFactory', *,
7373
# > Tools MUST assume the file’s encoding is UTF-8.
7474
with open(join(dirname(fpath), plicense['file']), 'rb') as plicense_fileh:
7575
yield DisjunctiveLicense(name=f"declared license of '{project['name']}'",
76+
acknowledgement=lack,
7677
text=AttachedText(encoding=Encoding.BASE_64,
7778
content=b64encode(plicense_fileh.read()).decode()))
7879
elif len(plicense_text := plicense.get('text', '')) > 0:
79-
license = lfac.make_from_string(plicense_text)
80+
license = lfac.make_from_string(plicense_text,
81+
license_acknowledgement=lack)
8082
if isinstance(license, DisjunctiveLicense) and license.id is None:
8183
# per spec, `License` is either a SPDX ID/Expression, or a license text(not name!)
8284
yield DisjunctiveLicense(name=f"declared license of '{project['name']}'",
85+
acknowledgement=lack,
8386
text=AttachedText(content=plicense_text))
8487
else:
8588
yield license

cyclonedx_py/_internal/utils/poetry.py

Lines changed: 5 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -28,6 +28,7 @@
2828
from cyclonedx.factory.license import LicenseFactory
2929
from cyclonedx.model import ExternalReference, ExternalReferenceType, XsUri
3030
from cyclonedx.model.component import Component
31+
from cyclonedx.model.license import LicenseAcknowledgement
3132
from packaging.requirements import Requirement
3233

3334
from .cdx import licenses_fixup, url_label_to_ert
@@ -64,12 +65,14 @@ def poetry2extrefs(poetry: Dict[str, Any]) -> Generator['ExternalReference', Non
6465
def poetry2component(poetry: Dict[str, Any], *, ctype: 'ComponentType') -> 'Component':
6566
licenses: List['License'] = []
6667
lfac = LicenseFactory()
68+
lack = LicenseAcknowledgement.DECLARED
6769
if 'classifiers' in poetry:
68-
licenses.extend(classifiers2licenses(poetry['classifiers'], lfac))
70+
licenses.extend(classifiers2licenses(poetry['classifiers'], lfac, lack))
6971
if 'license' in poetry:
7072
# per spec(https://python-poetry.org/docs/pyproject#license):
7173
# the `license` is intended to be the name of a license, not the license text itself.
72-
licenses.append(lfac.make_from_string(poetry['license']))
74+
licenses.append(lfac.make_from_string(poetry['license'],
75+
license_acknowledgement=lack))
7376

7477
return Component(
7578
type=ctype,

pyproject.toml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -69,7 +69,7 @@ cyclonedx-py = "cyclonedx_py._internal.cli:run"
6969

7070
[tool.poetry.dependencies]
7171
python = "^3.8"
72-
cyclonedx-python-lib = { version = "^7.0.0", extras = ["validation"] }
72+
cyclonedx-python-lib = { version = "^7.3.0", extras = ["validation"] }
7373
packageurl-python = ">=0.11, <2" # keep in sync with same dep in `cyclonedx-python-lib`
7474
pip-requirements-parser = "^32.0"
7575
packaging = "^22 || ^23 || ^24"

tests/_data/snapshots/environment/plain_editable-self_1.6.json.bin

Lines changed: 1 addition & 0 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

tests/_data/snapshots/environment/plain_editable-self_1.6.xml.bin

Lines changed: 1 addition & 1 deletion
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

tests/_data/snapshots/environment/plain_local_1.1.xml.bin

Lines changed: 3 additions & 0 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

tests/_data/snapshots/environment/plain_local_1.2.json.bin

Lines changed: 5 additions & 0 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

tests/_data/snapshots/environment/plain_local_1.2.xml.bin

Lines changed: 3 additions & 0 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

0 commit comments

Comments
 (0)