-
-
Notifications
You must be signed in to change notification settings - Fork 67
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
feat: environment - gather declared license information according to …
…PEP639 (#755) From python environments, gather additional declared license information according to [PEP 639](https://peps.python.org/pep-0639) (improving license clarity with better package metadata). New CLI switches for `cyclonedx environment`: * `--PEP-639`: Enable license gathering according to PEP 639 (improving license clarity with better package metadata). The behavior may change during the draft development of the PEP. * `--gather-license-texts`: Enable license text gathering. In current state of implementation, `--gather-license-texts` has effect only if `--PEP-639` is also given. --------- Signed-off-by: Jan Kowalleck <[email protected]>
- Loading branch information
1 parent
cba521e
commit e9cc805
Showing
57 changed files
with
14,333 additions
and
4 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,38 @@ | ||
# This file is part of CycloneDX Python Lib | ||
# | ||
# 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. | ||
|
||
from mimetypes import guess_type as _stdlib_guess_type | ||
from os.path import splitext | ||
from typing import Optional | ||
|
||
_ext_mime_map = { | ||
# https://www.iana.org/assignments/media-types/media-types.xhtml | ||
'md': 'text/markdown', | ||
'txt': 'text/plain', | ||
'rst': 'text/prs.fallenstein.rst', | ||
# add more mime types. pull-requests welcome! | ||
} | ||
|
||
|
||
def guess_type(file_name: str) -> Optional[str]: | ||
""" | ||
The stdlib `mimetypes.guess_type()` is inconsistent, as it depends heavily on type registry in the env/os. | ||
Therefore, this polyfill exists. | ||
""" | ||
ext = splitext(file_name)[1][1:].lower() | ||
return _ext_mime_map.get(ext) \ | ||
or _stdlib_guess_type(file_name)[0] |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,80 @@ | ||
# This file is part of CycloneDX Python Lib | ||
# | ||
# 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. | ||
|
||
""" | ||
Functionality related to PEP 639. | ||
See https://peps.python.org/pep-0639/ | ||
""" | ||
|
||
from base64 import b64encode | ||
from os.path import join | ||
from typing import TYPE_CHECKING, Generator | ||
|
||
from cyclonedx.factory.license import LicenseFactory | ||
from cyclonedx.model import AttachedText, Encoding | ||
from cyclonedx.model.license import DisjunctiveLicense, LicenseAcknowledgement | ||
|
||
from .mimetypes import guess_type | ||
|
||
if TYPE_CHECKING: # pragma: no cover | ||
from importlib.metadata import Distribution | ||
from logging import Logger | ||
|
||
from cyclonedx.model.license import License | ||
|
||
|
||
def dist2licenses( | ||
dist: 'Distribution', | ||
gather_text: bool, | ||
logger: 'Logger' | ||
) -> Generator['License', None, None]: | ||
lfac = LicenseFactory() | ||
lack = LicenseAcknowledgement.DECLARED | ||
metadata = dist.metadata # see https://packaging.python.org/en/latest/specifications/core-metadata/ | ||
if (lexp := metadata['License-Expression']) is not None: | ||
# see spec: https://peps.python.org/pep-0639/#add-license-expression-field | ||
yield lfac.make_from_string(lexp, | ||
license_acknowledgement=lack) | ||
if gather_text: | ||
for mlfile in set(metadata.get_all('License-File', ())): | ||
# see spec: https://peps.python.org/pep-0639/#add-license-file-field | ||
# latest spec rev: https://discuss.python.org/t/pep-639-round-3-improving-license-clarity-with-better-package-metadata/53020 # noqa: E501 | ||
|
||
# per spec > license files are stored in the `.dist-info/licenses/` subdirectory of the produced wheel. | ||
# but in practice, other locations are used, too. | ||
content = dist.read_text(join('licenses', mlfile)) \ | ||
or dist.read_text(join('license_files', mlfile)) \ | ||
or dist.read_text(mlfile) | ||
if content is None: # pragma: no cover | ||
logger.debug('Error: failed to read license file %r for dist %r', | ||
mlfile, metadata['Name']) | ||
continue | ||
encoding = None | ||
content_type = guess_type(mlfile) or AttachedText.DEFAULT_CONTENT_TYPE | ||
# per default, license files are human-readable texts. | ||
if not content_type.startswith('text/'): | ||
encoding = Encoding.BASE_64 | ||
content = b64encode(content.encode('utf-8')).decode('ascii') | ||
yield DisjunctiveLicense( | ||
name=f'declared license file: {mlfile}', | ||
acknowledgement=lack, | ||
text=AttachedText( | ||
content=content, | ||
encoding=encoding, | ||
content_type=content_type | ||
)) |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
64 changes: 64 additions & 0 deletions
64
tests/_data/infiles/environment/with-license-pep639/init.py
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,64 @@ | ||
""" | ||
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') | ||
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( | ||
# with License-Expression | ||
'attrs', | ||
# with License-File | ||
'boolean.py', | ||
'jsonpointer', | ||
'license_expression', | ||
'lxml', | ||
) | ||
|
||
|
||
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) |
5 changes: 5 additions & 0 deletions
5
tests/_data/infiles/environment/with-license-pep639/pinning.txt
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,5 @@ | ||
attrs==23.2.0 | ||
boolean.py==4.0 | ||
jsonpointer==2.4 | ||
license-expression==30.3.0 | ||
lxml==5.2.2 |
15 changes: 15 additions & 0 deletions
15
tests/_data/infiles/environment/with-license-pep639/pyproject.toml
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,15 @@ | ||
[project] | ||
# https://packaging.python.org/en/latest/specifications/declaring-project-metadata/#declaring-project-metadata | ||
name = "with-extras" | ||
version = "0.1.0" | ||
description = "depenndencies with license declaration accoring to PEP 639" | ||
|
||
dependencies = [ | ||
# with License-Expression | ||
"attrs", | ||
# with License-File | ||
"boolean.py", | ||
"jsonpointer", | ||
"license_expression", | ||
"lxml", | ||
] |
40 changes: 40 additions & 0 deletions
40
tests/_data/snapshots/environment/pep639-texts_with-license-pep639_1.0.xml.bin
Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.
Oops, something went wrong.
Oops, something went wrong.