99import json
1010from dataclasses import dataclass
1111from importlib .util import find_spec
12- from license_expression import Licensing , LicenseExpression , OR
12+ from license_expression import Licensing , LicenseExpression , OR , get_spdx_licensing
1313from 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
1616from continuous_delivery_scripts .utils .configuration import (
1717 ConfigurationVariable ,
@@ -57,10 +57,101 @@ class Licence:
5757def _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+
64155def _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]:
92183def _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
160286OPENSOURCE_LICENCES = OpenSourceLicences ()
0 commit comments