Skip to content

Commit ec7ab3e

Browse files
authored
fix: declared license texts as such, not as license name (#694)
Signed-off-by: Jan Kowalleck <jan.kowalleck@gmail.com>
1 parent f27170e commit ec7ab3e

File tree

74 files changed

+1983
-186
lines changed

Some content is hidden

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

74 files changed

+1983
-186
lines changed

cyclonedx_py/_internal/environment.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -130,7 +130,7 @@ def __call__(self, *, # type:ignore[override]
130130
rc = None
131131
else:
132132
pyproject = pyproject_load(pyproject_file)
133-
root_c = pyproject2component(pyproject, type=mc_type)
133+
root_c = pyproject2component(pyproject, ctype=mc_type, fpath=pyproject_file)
134134
root_c.bom_ref.value = 'root-component'
135135
root_d = tuple(pyproject2dependencies(pyproject))
136136
rc = (root_c, root_d)

cyclonedx_py/_internal/pipenv.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -127,7 +127,7 @@ def __call__(self, *, # type:ignore[override]
127127
if pyproject_file is None:
128128
rc = None
129129
else:
130-
rc = pyproject_file2component(pyproject_file, type=mc_type)
130+
rc = pyproject_file2component(pyproject_file, ctype=mc_type)
131131
rc.bom_ref.value = 'root-component'
132132

133133
return self._make_bom(rc,

cyclonedx_py/_internal/poetry.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -230,7 +230,7 @@ def _make_bom(self, project: 'T_NameDict', locker: 'T_NameDict',
230230

231231
po_cfg = project['tool']['poetry']
232232

233-
bom.metadata.component = root_c = poetry2component(po_cfg, type=mc_type)
233+
bom.metadata.component = root_c = poetry2component(po_cfg, ctype=mc_type)
234234
root_c.bom_ref.value = root_c.name
235235
root_c.properties.update(
236236
Property(

cyclonedx_py/_internal/requirements.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -114,7 +114,7 @@ def __call__(self, *, # type:ignore[override]
114114
if pyproject_file is None:
115115
rc = None
116116
else:
117-
rc = pyproject_file2component(pyproject_file, type=mc_type)
117+
rc = pyproject_file2component(pyproject_file, ctype=mc_type)
118118
rc.bom_ref.value = 'root-component'
119119

120120
if requirements_file == '-':

cyclonedx_py/_internal/utils/packaging.py

Lines changed: 10 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -20,7 +20,8 @@
2020

2121
from cyclonedx.exception.model import InvalidUriException
2222
from cyclonedx.factory.license import LicenseFactory
23-
from cyclonedx.model import ExternalReference, ExternalReferenceType, XsUri
23+
from cyclonedx.model import AttachedText, ExternalReference, ExternalReferenceType, XsUri
24+
from cyclonedx.model.license import DisjunctiveLicense
2425

2526
from .cdx import url_label_to_ert
2627
from .pep621 import classifiers2licenses
@@ -42,9 +43,15 @@ def metadata2licenses(metadata: 'PackageMetadata') -> Generator['License', None,
4243
# see https://packaging.python.org/en/latest/specifications/core-metadata/#classifier-multiple-use
4344
classifiers: List[str] = metadata.get_all('Classifier') # type:ignore[assignment]
4445
yield from classifiers2licenses(classifiers, lfac)
45-
if 'License' in metadata:
46+
if 'License' in metadata and len(mlicense := metadata['License']) > 0:
4647
# see https://packaging.python.org/en/latest/specifications/core-metadata/#license
47-
yield lfac.make_from_string(metadata['License'])
48+
license = lfac.make_from_string(mlicense)
49+
if isinstance(license, DisjunctiveLicense) and license.id is None:
50+
# per spec, `License` is either a SPDX ID/Expression, or a license text(not name!)
51+
yield DisjunctiveLicense(name=f"declared license of '{metadata['Name']}'",
52+
text=AttachedText(content=mlicense))
53+
else:
54+
yield license
4855

4956

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

cyclonedx_py/_internal/utils/pep621.py

Lines changed: 35 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -15,21 +15,23 @@
1515
# SPDX-License-Identifier: Apache-2.0
1616
# Copyright (c) OWASP Foundation. All Rights Reserved.
1717

18-
1918
"""
2019
Functionality related to PEP 621.
2120
2221
See https://packaging.python.org/en/latest/specifications/declaring-project-metadata/
2322
See https://peps.python.org/pep-0621/
2423
"""
2524

25+
from base64 import b64encode
2626
from itertools import chain
27+
from os.path import dirname, join
2728
from typing import TYPE_CHECKING, Any, Dict, Generator, Iterable, Iterator
2829

2930
from cyclonedx.exception.model import InvalidUriException
3031
from cyclonedx.factory.license import LicenseFactory
31-
from cyclonedx.model import ExternalReference, XsUri
32+
from cyclonedx.model import AttachedText, Encoding, ExternalReference, XsUri
3233
from cyclonedx.model.component import Component
34+
from cyclonedx.model.license import DisjunctiveLicense
3335
from packaging.requirements import Requirement
3436

3537
from .cdx import licenses_fixup, url_label_to_ert
@@ -50,18 +52,37 @@ def classifiers2licenses(classifiers: Iterable[str], lfac: 'LicenseFactory') ->
5052
classifiers)))
5153

5254

53-
def project2licenses(project: Dict[str, Any], lfac: 'LicenseFactory') -> Generator['License', None, None]:
54-
if 'classifiers' in project:
55+
def project2licenses(project: Dict[str, Any], lfac: 'LicenseFactory', *,
56+
fpath: str) -> Generator['License', None, None]:
57+
if classifiers := project.get('classifiers'):
5558
# https://packaging.python.org/en/latest/specifications/pyproject-toml/#classifiers
5659
# https://peps.python.org/pep-0621/#classifiers
5760
# https://packaging.python.org/en/latest/specifications/core-metadata/#classifier-multiple-use
58-
yield from classifiers2licenses(project['classifiers'], lfac)
59-
license = project.get('license')
60-
# https://packaging.python.org/en/latest/specifications/pyproject-toml/#license
61-
# https://peps.python.org/pep-0621/#license
62-
# https://packaging.python.org/en/latest/specifications/core-metadata/#license
63-
if isinstance(license, dict) and 'text' in license:
64-
yield lfac.make_from_string(license['text'])
61+
yield from classifiers2licenses(classifiers, lfac)
62+
if plicense := project.get('license'):
63+
# https://packaging.python.org/en/latest/specifications/pyproject-toml/#license
64+
# https://peps.python.org/pep-0621/#license
65+
# https://packaging.python.org/en/latest/specifications/core-metadata/#license
66+
if 'file' in plicense and 'text' in plicense:
67+
# per spec:
68+
# > These keys are mutually exclusive, so a tool MUST raise an error if the metadata specifies both keys.
69+
raise ValueError('`license.file` and `license.text` are mutually exclusive,')
70+
if 'file' in plicense:
71+
# per spec:
72+
# > [...] a string value that is a relative file path [...].
73+
# > Tools MUST assume the file’s encoding is UTF-8.
74+
with open(join(dirname(fpath), plicense['file']), 'rb') as plicense_fileh:
75+
yield DisjunctiveLicense(name=f"declared license of '{project['name']}'",
76+
text=AttachedText(encoding=Encoding.BASE_64,
77+
content=b64encode(plicense_fileh.read()).decode()))
78+
elif len(plicense_text := plicense.get('text', '')) > 0:
79+
license = lfac.make_from_string(plicense_text)
80+
if isinstance(license, DisjunctiveLicense) and license.id is None:
81+
# per spec, `License` is either a SPDX ID/Expression, or a license text(not name!)
82+
yield DisjunctiveLicense(name=f"declared license of '{project['name']}'",
83+
text=AttachedText(content=plicense_text))
84+
else:
85+
yield license
6586

6687

6788
def project2extrefs(project: Dict[str, Any]) -> Generator['ExternalReference', None, None]:
@@ -77,14 +98,14 @@ def project2extrefs(project: Dict[str, Any]) -> Generator['ExternalReference', N
7798

7899

79100
def project2component(project: Dict[str, Any], *,
80-
type: 'ComponentType') -> 'Component':
101+
ctype: 'ComponentType', fpath: str) -> 'Component':
81102
dynamic = project.get('dynamic', ())
82103
return Component(
83-
type=type,
104+
type=ctype,
84105
name=project['name'],
85106
version=project.get('version', None) if 'version' not in dynamic else None,
86107
description=project.get('description', None) if 'description' not in dynamic else None,
87-
licenses=licenses_fixup(project2licenses(project, LicenseFactory())),
108+
licenses=licenses_fixup(project2licenses(project, LicenseFactory(), fpath=fpath)),
88109
external_references=project2extrefs(project),
89110
# TODO add more properties according to spec
90111
)

cyclonedx_py/_internal/utils/poetry.py

Lines changed: 4 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -61,17 +61,18 @@ def poetry2extrefs(poetry: Dict[str, Any]) -> Generator['ExternalReference', Non
6161
pass
6262

6363

64-
def poetry2component(poetry: Dict[str, Any], *, type: 'ComponentType') -> 'Component':
64+
def poetry2component(poetry: Dict[str, Any], *, ctype: 'ComponentType') -> 'Component':
6565
licenses: List['License'] = []
6666
lfac = LicenseFactory()
6767
if 'classifiers' in poetry:
6868
licenses.extend(classifiers2licenses(poetry['classifiers'], lfac))
6969
if 'license' in poetry:
70+
# per spec(https://python-poetry.org/docs/pyproject#license):
71+
# the `license` is intended to be the name of a license, not the license text itself.
7072
licenses.append(lfac.make_from_string(poetry['license']))
71-
del lfac
7273

7374
return Component(
74-
type=type,
75+
type=ctype,
7576
name=poetry['name'],
7677
version=poetry.get('version'),
7778
description=poetry.get('description'),

cyclonedx_py/_internal/utils/pyproject.py

Lines changed: 7 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -14,12 +14,12 @@
1414

1515

1616
def pyproject2component(data: Dict[str, Any], *,
17-
type: 'ComponentType') -> 'Component':
17+
ctype: 'ComponentType', fpath: str) -> 'Component':
1818
tool = data.get('tool', {})
19-
if 'poetry' in tool:
20-
return poetry2component(tool['poetry'], type=type)
21-
if 'project' in data:
22-
return project2component(data['project'], type=type)
19+
if poetry := tool.get('poetry'):
20+
return poetry2component(poetry, ctype=ctype)
21+
if project := data.get('project'):
22+
return project2component(project, ctype=ctype, fpath=fpath)
2323
raise ValueError('Unable to build component from pyproject')
2424

2525

@@ -33,10 +33,10 @@ def pyproject_load(pyproject_file: str) -> Dict[str, Any]:
3333

3434

3535
def pyproject_file2component(pyproject_file: str, *,
36-
type: 'ComponentType') -> 'Component':
36+
ctype: 'ComponentType') -> 'Component':
3737
return pyproject2component(
3838
pyproject_load(pyproject_file),
39-
type=type
39+
ctype=ctype, fpath=pyproject_file
4040
)
4141

4242

tests/_data/infiles/.gitattributes

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,2 @@
11
*.bin binary
22
*.txt.bin binary diff=text
3-
Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1 +1,4 @@
1-
build via `python -m build`
1+
build via :
2+
```shell
3+
python -m build
4+
```

0 commit comments

Comments
 (0)