Skip to content

Commit

Permalink
fix: declared license texts as such, not as license name.
Browse files Browse the repository at this point in the history
Signed-off-by: Jan Kowalleck <jan.kowalleck@gmail.com>
  • Loading branch information
jkowalleck committed Mar 14, 2024
1 parent dc81c35 commit e0b7dae
Show file tree
Hide file tree
Showing 34 changed files with 6,161 additions and 113 deletions.
18 changes: 12 additions & 6 deletions cyclonedx_py/_internal/utils/packaging.py
Original file line number Diff line number Diff line change
Expand Up @@ -16,11 +16,12 @@
# Copyright (c) OWASP Foundation. All Rights Reserved.

from re import compile as re_compile
from typing import TYPE_CHECKING, Generator, List
from typing import TYPE_CHECKING, Generator

from cyclonedx.exception.model import InvalidUriException
from cyclonedx.factory.license import LicenseFactory
from cyclonedx.model import ExternalReference, ExternalReferenceType, XsUri
from cyclonedx.model import AttachedText, ExternalReference, ExternalReferenceType, XsUri
from cyclonedx.model.license import DisjunctiveLicense

from .cdx import url_label_to_ert
from .pep621 import classifiers2licenses
Expand All @@ -40,11 +41,16 @@ def metadata2licenses(metadata: 'PackageMetadata') -> Generator['License', None,
lfac = LicenseFactory()
if 'Classifier' in metadata:
# see https://packaging.python.org/en/latest/specifications/core-metadata/#classifier-multiple-use
classifiers: List[str] = metadata.get_all('Classifier') # type:ignore[assignment]
yield from classifiers2licenses(classifiers, lfac)
if 'License' in metadata:
yield from classifiers2licenses(metadata.get_all('Classifier'), lfac)
if len(mlicense := metadata.get('License', '')) > 0:
# see https://packaging.python.org/en/latest/specifications/core-metadata/#license
yield lfac.make_from_string(metadata['License'])
license = lfac.make_from_string(mlicense)
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 '{metadata['Name']}'",
text=AttachedText(content=mlicense))
else:
yield license


def metadata2extrefs(metadata: 'PackageMetadata') -> Generator['ExternalReference', None, None]:
Expand Down
27 changes: 20 additions & 7 deletions cyclonedx_py/_internal/utils/pep621.py
Original file line number Diff line number Diff line change
Expand Up @@ -28,8 +28,9 @@

from cyclonedx.exception.model import InvalidUriException
from cyclonedx.factory.license import LicenseFactory
from cyclonedx.model import ExternalReference, XsUri
from cyclonedx.model import AttachedText, ExternalReference, XsUri
from cyclonedx.model.component import Component
from cyclonedx.model.license import DisjunctiveLicense
from packaging.requirements import Requirement

from .cdx import licenses_fixup, url_label_to_ert
Expand All @@ -56,12 +57,24 @@ def project2licenses(project: Dict[str, Any], lfac: 'LicenseFactory') -> Generat
# https://peps.python.org/pep-0621/#classifiers
# https://packaging.python.org/en/latest/specifications/core-metadata/#classifier-multiple-use
yield from classifiers2licenses(project['classifiers'], lfac)
license = 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 isinstance(license, dict) and 'text' in license:
yield lfac.make_from_string(license['text'])
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:
# > 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:
pass # @TODO - https://github.com/CycloneDX/cyclonedx-python/issues/570
elif len(plicense_text := plicense.get('text', '')) > 0:
license = lfac.make_from_string(plicense_text)
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']}'",
text=AttachedText(content=plicense_text))
else:
yield license


def project2extrefs(project: Dict[str, Any]) -> Generator['ExternalReference', None, None]:
Expand Down
3 changes: 2 additions & 1 deletion cyclonedx_py/_internal/utils/poetry.py
Original file line number Diff line number Diff line change
Expand Up @@ -67,8 +67,9 @@ def poetry2component(poetry: Dict[str, Any], *, type: 'ComponentType') -> 'Compo
if 'classifiers' in poetry:
licenses.extend(classifiers2licenses(poetry['classifiers'], lfac))
if 'license' in poetry:
# per spec(https://python-poetry.org/docs/pyproject#license):
# the `license` is intended to be the name of a license, not the license text itself.
licenses.append(lfac.make_from_string(poetry['license']))
del lfac

return Component(
type=type,
Expand Down
58 changes: 58 additions & 0 deletions tests/_data/infiles/environment/with-license-text/init.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,58 @@
"""
initialize this testbed.
"""

from os import name as os_name
from os.path import dirname, join
from subprocess import PIPE, CompletedProcess, run # nosec:B404
from sys import argv, executable
from typing import Any
from venv import EnvBuilder

__all__ = ['main']

this_dir = dirname(__file__)
env_dir = join(this_dir, '.venv')
constraint_file = join(this_dir, 'pinning.txt')


def pip_run(*args: str, **kwargs: Any) -> CompletedProcess:
# pip is not API, but a CLI -- call it like that!
call = (
executable, '-m', 'pip',
'--python', env_dir,
*args
)
print('+ ', *call)
res = run(call, **kwargs, cwd=this_dir, shell=False) # nosec:B603
if res.returncode != 0:
raise RuntimeError('process failed')

Check failure on line 29 in tests/_data/infiles/environment/with-license-text/init.py

View workflow job for this annotation

GitHub Actions / Test (ubuntu-latest py3.8)

process failed

Check failure on line 29 in tests/_data/infiles/environment/with-license-text/init.py

View workflow job for this annotation

GitHub Actions / Test (macos-latest py3.8)

process failed

Check failure on line 29 in tests/_data/infiles/environment/with-license-text/init.py

View workflow job for this annotation

GitHub Actions / Test (windows-latest py3.8)

process failed
return res


def pip_install(*args: str) -> None:
pip_run(
'install', '--require-virtualenv', '--no-input', '--progress-bar=off', '--no-color',
'-c', constraint_file, # needed for reproducibility
*args
)


def main() -> None:
EnvBuilder(
system_site_packages=False,
symlinks=os_name != 'nt',
with_pip=False,
).create(env_dir)

pip_install(
'numpy==1.26.4',
)


if __name__ == '__main__':
main()
if '--pin' in argv:
res = pip_run('freeze', '--all', '--local', stdout=PIPE)
with open(constraint_file, 'wb') as cf:
cf.write(res.stdout)
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
numpy==1.26.4
12 changes: 12 additions & 0 deletions tests/_data/infiles/environment/with-license-text/pyproject.toml
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
[project]
# https://packaging.python.org/en/latest/specifications/declaring-project-metadata/#declaring-project-metadata
name = "with-license-text"
version = "0.1.0"
description = "with licenses as text, instead of SPDX ID/Expression"
# see https://packaging.python.org/en/latest/specifications/pyproject-toml/#license
license = {text="This is the license text of this component.\nIt is expected to be available in a SBOM."}

dependencies = [
# some packages that have a complete license text declared
"numpy==1.24.4"
]

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

30 changes: 20 additions & 10 deletions tests/_data/snapshots/environment/plain_with-extras_1.1.xml.bin

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

Loading

0 comments on commit e0b7dae

Please sign in to comment.