From 1123dc80928b417e52dd3cd9f28b0b23d35bbc13 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Wed, 8 Apr 2026 14:54:01 +0000 Subject: [PATCH 1/3] Bump spdx-tools from 0.6.1 to 0.8.3 Bumps [spdx-tools](https://github.com/spdx/tools-python) from 0.6.1 to 0.8.3. - [Release notes](https://github.com/spdx/tools-python/releases) - [Changelog](https://github.com/spdx/tools-python/blob/main/CHANGELOG.md) - [Commits](https://github.com/spdx/tools-python/compare/v0.6.1...v0.8.3) --- updated-dependencies: - dependency-name: spdx-tools dependency-type: direct:production update-type: version-update:semver-minor ... Signed-off-by: dependabot[bot] --- setup.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/setup.py b/setup.py index d0495f4..0dcf3b3 100644 --- a/setup.py +++ b/setup.py @@ -70,7 +70,7 @@ "detect-secrets[gibberish]==1.5.0", "packaging", "licenseheaders<0.8.9", - "spdx-tools==0.6.1", + "spdx-tools==0.8.3", "license-expression", "wcmatch", "jellyfish", From 4aadd1beb9421002e58e8bdede3c2684a7ce2fcd Mon Sep 17 00:00:00 2001 From: Monty Bot Date: Wed, 8 Apr 2026 14:54:29 +0000 Subject: [PATCH 2/3] =?UTF-8?q?=F0=9F=93=B0=20Automatic=20changes=20?= =?UTF-8?q?=E2=9A=99=20Adding=20news=20file?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- news/20260408145429.bugfix | 1 + 1 file changed, 1 insertion(+) create mode 100644 news/20260408145429.bugfix diff --git a/news/20260408145429.bugfix b/news/20260408145429.bugfix new file mode 100644 index 0000000..ec8fcff --- /dev/null +++ b/news/20260408145429.bugfix @@ -0,0 +1 @@ +Dependency upgrade: spdx-tools-0.8.3 From 30e1705ec329d42b33afb6b83ab63ae706f87b5b Mon Sep 17 00:00:00 2001 From: Adrien CABARBAYE Date: Thu, 20 Aug 2026 00:08:55 +0100 Subject: [PATCH 3/3] :bug: fix issues --- .../utils/third_party_licences.py | 148 ++++++++++++++++-- 1 file changed, 137 insertions(+), 11 deletions(-) diff --git a/continuous_delivery_scripts/utils/third_party_licences.py b/continuous_delivery_scripts/utils/third_party_licences.py index 14eff44..4a1618b 100644 --- a/continuous_delivery_scripts/utils/third_party_licences.py +++ b/continuous_delivery_scripts/utils/third_party_licences.py @@ -9,9 +9,9 @@ import json from dataclasses import dataclass from importlib.util import find_spec -from license_expression import Licensing, LicenseExpression, OR +from license_expression import Licensing, LicenseExpression, OR, get_spdx_licensing from pathlib import Path -from typing import Iterable, cast, Optional, Iterator, List, Pattern, Any +from typing import Dict, Iterable, cast, Optional, Iterator, List, Pattern, Any, Tuple from continuous_delivery_scripts.utils.configuration import ( ConfigurationVariable, @@ -57,10 +57,101 @@ class Licence: def _get_spdx_licenses_path() -> Path: spec = find_spec("spdx") if not spec or not spec.origin: - raise ModuleNotFoundError("No module named 'spdx'") + raise FileNotFoundError("Could not find SPDX licenses.json") return Path(spec.origin).resolve().parent.joinpath("licenses.json") +FALLBACK_LICENCE_DATA = { + "0BSD": { + "reference_number": "319", + "name": "BSD Zero Clause License", + }, + "Apache-2.0": { + "reference_number": "26", + "name": "Apache License 2.0", + }, + "GPL-3.0-only": { + "name": "GNU General Public License v3.0 only", + }, + "MPL-2.0": { + "name": "Mozilla Public License 2.0", + }, + "MIT": { + "name": "MIT License", + }, + "PSF-2.0": { + "name": "Python Software Foundation License 2.0", + }, + "Python-2.0": { + "name": "Python License 2.0", + }, +} + + +FALLBACK_LICENCE_ALIASES = { + "0BSD": ["BSD", "BSD License", "BSD Zero Clause License"], + "Apache-2.0": [ + "Apache", + "Apache 2", + "Apache 2.0", + "Apache License 2", + "Apache License 2.0", + "Apache License Version 2", + "Apache License Version 2.0", + "Apache License, Version 2", + "Apache License, Version 2.0", + "Apache Licence 2", + "Apache Licence 2.0", + "Apache Licence Version 2", + "Apache Licence Version 2.0", + "Apache Licence, Version 2", + "Apache Licence, Version 2.0", + "Apache Software License", + ], + "GPL-3.0-only": ["GPL 3", "GPL 3.0", "GPL-3.0", "GPL-3", "GNU GPL 3"], + "MIT": ["MIT License"], + "PSF-2.0": ["Python Software Foundation License 2.0"], + "Python-2.0": ["Python Software Foundation License"], +} + + +def _build_fallback_licence(identifier: str, is_deprecated: bool = False) -> Licence: + metadata = FALLBACK_LICENCE_DATA.get(identifier, {}) + return Licence( + reference_number=str(metadata.get("reference_number", "")), + identifier=identifier, + name=str(metadata.get("name", identifier)), + is_deprecated=is_deprecated, + is_osi_approved=bool(metadata.get("is_osi_approved", True)), + url=f"http://spdx.org/licenses/{identifier}.json", + reference=f"./{identifier}.html", + ) + + +def _iter_fallback_licences() -> Iterable[Tuple[Licence, Iterable[str]]]: + spdx_licensing = get_spdx_licensing() + seen = set() + for symbol in spdx_licensing.known_symbols.values(): + identifier = getattr(symbol, "key", None) + if not identifier or identifier in seen: + continue + if identifier.startswith("LicenseRef-") or getattr(symbol, "is_exception", False): + continue + seen.add(identifier) + aliases = list(getattr(symbol, "aliases", ())) + FALLBACK_LICENCE_ALIASES.get(identifier, []) + yield _build_fallback_licence(identifier, is_deprecated=bool(getattr(symbol, "is_deprecated", False))), aliases + + +def _normalise_licence_text(text: str) -> str: + normalised_text = text.strip().lower() + normalised_text = re.sub(r"osi\s?approved[:]*", "", normalised_text) + normalised_text = re.sub(r"licen[cs]e", " ", normalised_text) + normalised_text = re.sub(r"version", " ", normalised_text) + normalised_text = re.sub(r"[^\w\s]", " ", normalised_text) + normalised_text = re.sub(r"\s+", " ", normalised_text) + return normalised_text.strip() + + def _parse_licence_expression(licensing: Licensing, licence_expression: str) -> LicenseExpression: # Removing any unwanted characters so that the expression follows the laws: # > the valid characters are: letters and numbers, underscore, dot, colon or hyphen signs and spaces @@ -92,11 +183,17 @@ def iter_licenses(licence_info: dict) -> Iterable[Licence]: def _handle_special_licence_entries(cleansed_descriptor: str) -> str: if cleansed_descriptor in ["Python Software Foundation License"]: return "Python" + if re.fullmatch(r"Python(?:[\w\s\-\.]*)", cleansed_descriptor, re.IGNORECASE): + return "Python-2.0" if cleansed_descriptor in ["Apache Software License", "Apache", "apache"]: return "Apache-2.0" if cleansed_descriptor in ["LGPL", "UNKNOWN", "Dual License"]: # It is not possible to find which is the actual licence to consider. return UNKNOWN_LICENCE.identifier + if re.fullmatch(r"Apache(?:\s+(?:Software\s+)?)?(?:Licen[cs]e)?(?:,?\s+Version)?\s*2(?:\.0)?", cleansed_descriptor): + return "Apache-2.0" + if re.fullmatch(r"GPL\s*3(?:\.0)?", cleansed_descriptor, re.IGNORECASE): + return "GPL-3.0-only" return cleansed_descriptor @@ -122,18 +219,32 @@ def __init__(self) -> None: self._licence_store: Optional[dict] = None self._licence_list: Optional[list] = None + def _store_licence(self, licence: Licence, aliases: Iterable[str] = ()) -> None: + if not self._licence_store or self._licence_list is None: + return + entries = [licence.identifier, licence.name, *aliases] + for entry in entries: + if not entry: + continue + self._licence_store[entry] = licence + self._licence_list.append(entry) + def load(self) -> None: """Loads licence data from internal Json file.""" if self._licence_list and self._licence_store: return self._licence_store = {UNKNOWN_LICENCE.identifier: UNKNOWN_LICENCE} self._licence_list = [UNKNOWN_LICENCE.identifier] - with open(_get_spdx_licenses_path(), "r", encoding="utf8") as f: - for licence in iter_licenses(json.load(f)): - self._licence_store[licence.identifier] = licence - self._licence_list.append(licence.identifier) - self._licence_store[licence.name] = licence - self._licence_list.append(licence.name) + try: + with open(_get_spdx_licenses_path(), "r", encoding="utf8") as f: + for licence in iter_licenses(json.load(f)): + self._store_licence(licence) + return + except (FileNotFoundError, ModuleNotFoundError): + pass + + for licence, aliases in _iter_fallback_licences(): + self._store_licence(licence, aliases) def get_licences_from_pattern(self, licence_descriptor_pattern: Pattern) -> Optional[List[Licence]]: """Determines all the licences following a certain pattern.""" @@ -153,8 +264,23 @@ def get_licence(self, licence_descriptor: Optional[str]) -> Optional[Licence]: if not self._licence_store or not self._licence_list or not licence_descriptor: return None cleansed_descriptor = cleanse_licence_descriptor(licence_descriptor) - likelihood, licence = determine_similar_string_from_list(cleansed_descriptor, self._licence_list) - return self._licence_store.get(licence) if likelihood > LICENCE_LIKELIHOOD_THRESHOLD else None + exact_match = self._licence_store.get(cleansed_descriptor) + if exact_match: + return cast(Licence, exact_match) + + normalised_map: Dict[str, Licence] = {} + for name in self._licence_list: + licence = cast(Licence, self._licence_store.get(name)) + if licence: + normalised_map[_normalise_licence_text(name)] = licence + + normalised_descriptor = _normalise_licence_text(cleansed_descriptor) + normalised_exact_match = normalised_map.get(normalised_descriptor) + if normalised_exact_match: + return normalised_exact_match + + likelihood, matched_key = determine_similar_string_from_list(normalised_descriptor, normalised_map.keys()) + return normalised_map.get(matched_key) if likelihood > LICENCE_LIKELIHOOD_THRESHOLD else None OPENSOURCE_LICENCES = OpenSourceLicences()