Skip to content

Commit 30e1705

Browse files
committed
🐛 fix issues
1 parent 99f232c commit 30e1705

1 file changed

Lines changed: 137 additions & 11 deletions

File tree

‎continuous_delivery_scripts/utils/third_party_licences.py‎

Lines changed: 137 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -9,9 +9,9 @@
99
import json
1010
from dataclasses import dataclass
1111
from importlib.util import find_spec
12-
from license_expression import Licensing, LicenseExpression, OR
12+
from license_expression import Licensing, LicenseExpression, OR, get_spdx_licensing
1313
from pathlib import Path
14-
from typing import Iterable, cast, Optional, Iterator, List, Pattern, Any
14+
from typing import Dict, Iterable, cast, Optional, Iterator, List, Pattern, Any, Tuple
1515

1616
from continuous_delivery_scripts.utils.configuration import (
1717
ConfigurationVariable,
@@ -57,10 +57,101 @@ class Licence:
5757
def _get_spdx_licenses_path() -> Path:
5858
spec = find_spec("spdx")
5959
if not spec or not spec.origin:
60-
raise ModuleNotFoundError("No module named 'spdx'")
60+
raise FileNotFoundError("Could not find SPDX licenses.json")
6161
return Path(spec.origin).resolve().parent.joinpath("licenses.json")
6262

6363

64+
FALLBACK_LICENCE_DATA = {
65+
"0BSD": {
66+
"reference_number": "319",
67+
"name": "BSD Zero Clause License",
68+
},
69+
"Apache-2.0": {
70+
"reference_number": "26",
71+
"name": "Apache License 2.0",
72+
},
73+
"GPL-3.0-only": {
74+
"name": "GNU General Public License v3.0 only",
75+
},
76+
"MPL-2.0": {
77+
"name": "Mozilla Public License 2.0",
78+
},
79+
"MIT": {
80+
"name": "MIT License",
81+
},
82+
"PSF-2.0": {
83+
"name": "Python Software Foundation License 2.0",
84+
},
85+
"Python-2.0": {
86+
"name": "Python License 2.0",
87+
},
88+
}
89+
90+
91+
FALLBACK_LICENCE_ALIASES = {
92+
"0BSD": ["BSD", "BSD License", "BSD Zero Clause License"],
93+
"Apache-2.0": [
94+
"Apache",
95+
"Apache 2",
96+
"Apache 2.0",
97+
"Apache License 2",
98+
"Apache License 2.0",
99+
"Apache License Version 2",
100+
"Apache License Version 2.0",
101+
"Apache License, Version 2",
102+
"Apache License, Version 2.0",
103+
"Apache Licence 2",
104+
"Apache Licence 2.0",
105+
"Apache Licence Version 2",
106+
"Apache Licence Version 2.0",
107+
"Apache Licence, Version 2",
108+
"Apache Licence, Version 2.0",
109+
"Apache Software License",
110+
],
111+
"GPL-3.0-only": ["GPL 3", "GPL 3.0", "GPL-3.0", "GPL-3", "GNU GPL 3"],
112+
"MIT": ["MIT License"],
113+
"PSF-2.0": ["Python Software Foundation License 2.0"],
114+
"Python-2.0": ["Python Software Foundation License"],
115+
}
116+
117+
118+
def _build_fallback_licence(identifier: str, is_deprecated: bool = False) -> Licence:
119+
metadata = FALLBACK_LICENCE_DATA.get(identifier, {})
120+
return Licence(
121+
reference_number=str(metadata.get("reference_number", "")),
122+
identifier=identifier,
123+
name=str(metadata.get("name", identifier)),
124+
is_deprecated=is_deprecated,
125+
is_osi_approved=bool(metadata.get("is_osi_approved", True)),
126+
url=f"http://spdx.org/licenses/{identifier}.json",
127+
reference=f"./{identifier}.html",
128+
)
129+
130+
131+
def _iter_fallback_licences() -> Iterable[Tuple[Licence, Iterable[str]]]:
132+
spdx_licensing = get_spdx_licensing()
133+
seen = set()
134+
for symbol in spdx_licensing.known_symbols.values():
135+
identifier = getattr(symbol, "key", None)
136+
if not identifier or identifier in seen:
137+
continue
138+
if identifier.startswith("LicenseRef-") or getattr(symbol, "is_exception", False):
139+
continue
140+
seen.add(identifier)
141+
aliases = list(getattr(symbol, "aliases", ())) + FALLBACK_LICENCE_ALIASES.get(identifier, [])
142+
yield _build_fallback_licence(identifier, is_deprecated=bool(getattr(symbol, "is_deprecated", False))), aliases
143+
144+
145+
def _normalise_licence_text(text: str) -> str:
146+
normalised_text = text.strip().lower()
147+
normalised_text = re.sub(r"osi\s?approved[:]*", "", normalised_text)
148+
normalised_text = re.sub(r"licen[cs]e", " ", normalised_text)
149+
normalised_text = re.sub(r"version", " ", normalised_text)
150+
normalised_text = re.sub(r"[^\w\s]", " ", normalised_text)
151+
normalised_text = re.sub(r"\s+", " ", normalised_text)
152+
return normalised_text.strip()
153+
154+
64155
def _parse_licence_expression(licensing: Licensing, licence_expression: str) -> LicenseExpression:
65156
# Removing any unwanted characters so that the expression follows the laws:
66157
# > 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]:
92183
def _handle_special_licence_entries(cleansed_descriptor: str) -> str:
93184
if cleansed_descriptor in ["Python Software Foundation License"]:
94185
return "Python"
186+
if re.fullmatch(r"Python(?:[\w\s\-\.]*)", cleansed_descriptor, re.IGNORECASE):
187+
return "Python-2.0"
95188
if cleansed_descriptor in ["Apache Software License", "Apache", "apache"]:
96189
return "Apache-2.0"
97190
if cleansed_descriptor in ["LGPL", "UNKNOWN", "Dual License"]:
98191
# It is not possible to find which is the actual licence to consider.
99192
return UNKNOWN_LICENCE.identifier
193+
if re.fullmatch(r"Apache(?:\s+(?:Software\s+)?)?(?:Licen[cs]e)?(?:,?\s+Version)?\s*2(?:\.0)?", cleansed_descriptor):
194+
return "Apache-2.0"
195+
if re.fullmatch(r"GPL\s*3(?:\.0)?", cleansed_descriptor, re.IGNORECASE):
196+
return "GPL-3.0-only"
100197
return cleansed_descriptor
101198

102199

@@ -122,18 +219,32 @@ def __init__(self) -> None:
122219
self._licence_store: Optional[dict] = None
123220
self._licence_list: Optional[list] = None
124221

222+
def _store_licence(self, licence: Licence, aliases: Iterable[str] = ()) -> None:
223+
if not self._licence_store or self._licence_list is None:
224+
return
225+
entries = [licence.identifier, licence.name, *aliases]
226+
for entry in entries:
227+
if not entry:
228+
continue
229+
self._licence_store[entry] = licence
230+
self._licence_list.append(entry)
231+
125232
def load(self) -> None:
126233
"""Loads licence data from internal Json file."""
127234
if self._licence_list and self._licence_store:
128235
return
129236
self._licence_store = {UNKNOWN_LICENCE.identifier: UNKNOWN_LICENCE}
130237
self._licence_list = [UNKNOWN_LICENCE.identifier]
131-
with open(_get_spdx_licenses_path(), "r", encoding="utf8") as f:
132-
for licence in iter_licenses(json.load(f)):
133-
self._licence_store[licence.identifier] = licence
134-
self._licence_list.append(licence.identifier)
135-
self._licence_store[licence.name] = licence
136-
self._licence_list.append(licence.name)
238+
try:
239+
with open(_get_spdx_licenses_path(), "r", encoding="utf8") as f:
240+
for licence in iter_licenses(json.load(f)):
241+
self._store_licence(licence)
242+
return
243+
except (FileNotFoundError, ModuleNotFoundError):
244+
pass
245+
246+
for licence, aliases in _iter_fallback_licences():
247+
self._store_licence(licence, aliases)
137248

138249
def get_licences_from_pattern(self, licence_descriptor_pattern: Pattern) -> Optional[List[Licence]]:
139250
"""Determines all the licences following a certain pattern."""
@@ -153,8 +264,23 @@ def get_licence(self, licence_descriptor: Optional[str]) -> Optional[Licence]:
153264
if not self._licence_store or not self._licence_list or not licence_descriptor:
154265
return None
155266
cleansed_descriptor = cleanse_licence_descriptor(licence_descriptor)
156-
likelihood, licence = determine_similar_string_from_list(cleansed_descriptor, self._licence_list)
157-
return self._licence_store.get(licence) if likelihood > LICENCE_LIKELIHOOD_THRESHOLD else None
267+
exact_match = self._licence_store.get(cleansed_descriptor)
268+
if exact_match:
269+
return cast(Licence, exact_match)
270+
271+
normalised_map: Dict[str, Licence] = {}
272+
for name in self._licence_list:
273+
licence = cast(Licence, self._licence_store.get(name))
274+
if licence:
275+
normalised_map[_normalise_licence_text(name)] = licence
276+
277+
normalised_descriptor = _normalise_licence_text(cleansed_descriptor)
278+
normalised_exact_match = normalised_map.get(normalised_descriptor)
279+
if normalised_exact_match:
280+
return normalised_exact_match
281+
282+
likelihood, matched_key = determine_similar_string_from_list(normalised_descriptor, normalised_map.keys())
283+
return normalised_map.get(matched_key) if likelihood > LICENCE_LIKELIHOOD_THRESHOLD else None
158284

159285

160286
OPENSOURCE_LICENCES = OpenSourceLicences()

0 commit comments

Comments
 (0)