Coverage for pyTooling/Licensing/__init__.py: 92%

74 statements  

« prev     ^ index     » next       coverage.py v7.8.0, created at 2025-04-25 22:22 +0000

1# ==================================================================================================================== # 

2# _____ _ _ _ _ _ # 

3# _ __ _ |_ _|__ ___ | (_)_ __ __ _ | | (_) ___ ___ _ __ ___(_)_ __ __ _ # 

4# | '_ \| | | || |/ _ \ / _ \| | | '_ \ / _` | | | | |/ __/ _ \ '_ \/ __| | '_ \ / _` | # 

5# | |_) | |_| || | (_) | (_) | | | | | | (_| |_| |___| | (_| __/ | | \__ \ | | | | (_| | # 

6# | .__/ \__, ||_|\___/ \___/|_|_|_| |_|\__, (_)_____|_|\___\___|_| |_|___/_|_| |_|\__, | # 

7# |_| |___/ |___/ |___/ # 

8# ==================================================================================================================== # 

9# Authors: # 

10# Patrick Lehmann # 

11# # 

12# License: # 

13# ==================================================================================================================== # 

14# Copyright 2017-2025 Patrick Lehmann - Bötzingen, Germany # 

15# # 

16# Licensed under the Apache License, Version 2.0 (the "License"); # 

17# you may not use this file except in compliance with the License. # 

18# You may obtain a copy of the License at # 

19# # 

20# http://www.apache.org/licenses/LICENSE-2.0 # 

21# # 

22# Unless required by applicable law or agreed to in writing, software # 

23# distributed under the License is distributed on an "AS IS" BASIS, # 

24# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. # 

25# See the License for the specific language governing permissions and # 

26# limitations under the License. # 

27# # 

28# SPDX-License-Identifier: Apache-2.0 # 

29# ==================================================================================================================== # 

30# 

31""" 

32The Licensing module implements mapping tables for various license names and identifiers. 

33 

34.. seealso:: 

35 

36 List of SPDX identifiers: 

37 

38 * https://spdx.org/licenses/ 

39 * https://github.com/spdx/license-list-XML 

40 

41 List of `Python classifiers <https://pypi.org/classifiers/>`__ 

42 

43.. hint:: See :ref:`high-level help <LICENSING>` for explanations and usage examples. 

44""" 

45from dataclasses import dataclass 

46from sys import version_info # needed for versions before Python 3.11 

47from typing import Any, Dict, Optional as Nullable 

48 

49 

50try: 

51 from pyTooling.Decorators import export, readonly 

52 from pyTooling.MetaClasses import ExtendedType 

53except (ImportError, ModuleNotFoundError): # pragma: no cover 

54 print("[pyTooling.Licensing] Could not import from 'pyTooling.*'!") 

55 

56 try: 

57 from Decorators import export, readonly 

58 from MetaClasses import ExtendedType 

59 except (ImportError, ModuleNotFoundError) as ex: # pragma: no cover 

60 print("[pyTooling.Licensing] Could not import directly!") 

61 raise ex 

62 

63 

64__all__ = [ 

65 "PYTHON_LICENSE_NAMES", 

66 

67 "Apache_2_0_License", 

68 "BSD_3_Clause_License", 

69 "GPL_2_0_or_later", 

70 "MIT_License", 

71 

72 "SPDX_INDEX" 

73] 

74 

75 

76@export 

77@dataclass 

78class PythonLicenseName: 

79 """A *data class* to represent the license's short name and the package classifier for a license.""" 

80 

81 ShortName: str #: License's short name 

82 Classifier: str #: Package classifier for a license. 

83 

84 def __str__(self) -> str: 

85 """ 

86 The string representation of this name tuple returns the short name of the license. 

87 

88 :returns: Short name of the license. 

89 """ 

90 return self.ShortName 

91 

92 

93#: Mapping of SPDX identifiers to Python license names 

94PYTHON_LICENSE_NAMES: Dict[str, PythonLicenseName] = { 

95 "Apache-2.0": PythonLicenseName("Apache 2.0", "Apache Software License"), 

96 "BSD-3-Clause": PythonLicenseName("BSD", "BSD License"), 

97 "MIT": PythonLicenseName("MIT", "MIT License"), 

98 "GPL-2.0-or-later": PythonLicenseName("GPL-2.0-or-later", "GNU General Public License v2 or later (GPLv2+)"), 

99} 

100 

101 

102@export 

103class License(metaclass=ExtendedType, slots=True): 

104 """Representation of a license.""" 

105 

106 _spdxIdentifier: str #: Unique SPDX identifier. 

107 _name: str #: Name of the license. 

108 _osiApproved: bool #: OSI approval status 

109 _fsfApproved: bool #: FSF approval status 

110 

111 def __init__(self, spdxIdentifier: str, name: str, osiApproved: bool = False, fsfApproved: bool = False): 

112 self._spdxIdentifier = spdxIdentifier 

113 self._name = name 

114 self._osiApproved = osiApproved 

115 self._fsfApproved = fsfApproved 

116 

117 @readonly 

118 def Name(self) -> str: 

119 """ 

120 Returns the license' name. 

121 

122 :returns: License name. 

123 """ 

124 return self._name 

125 

126 @readonly 

127 def SPDXIdentifier(self) -> str: 

128 """ 

129 Returns the license' unique `SPDX identifier <https://spdx.org/licenses/>`__. 

130 

131 :returns: The the unique SPDX identifier. 

132 """ 

133 return self._spdxIdentifier 

134 

135 @readonly 

136 def OSIApproved(self) -> bool: 

137 """ 

138 Returns true, if the license is approved by OSI (`Open Source Initiative <https://opensource.org/>`__). 

139 

140 :returns: ``True``, if the license is approved by the Open Source Initiative. 

141 """ 

142 return self._osiApproved 

143 

144 @readonly 

145 def FSFApproved(self) -> bool: 

146 """ 

147 Returns true, if the license is approved by FSF (`Free Software Foundation <https://www.fsf.org/>`__). 

148 

149 :returns: ``True``, if the license is approved by the Free Software Foundation. 

150 """ 

151 return self._fsfApproved 

152 

153 @readonly 

154 def PythonLicenseName(self) -> str: 

155 """ 

156 Returns the Python license name for this license if it's defined. 

157 

158 :returns: The Python license name. 

159 :raises ValueError: If there is no license name defined for the license. |br| (See and check :data:`~pyTooling.Licensing.PYTHON_LICENSE_NAMES`) 

160 """ 

161 try: 

162 item: PythonLicenseName = PYTHON_LICENSE_NAMES[self._spdxIdentifier] 

163 except KeyError as ex: 

164 raise ValueError("License has no Python specify information.") from ex 

165 

166 return item.ShortName 

167 

168 @readonly 

169 def PythonClassifier(self) -> str: 

170 """ 

171 Returns the Python package classifier for this license if it's defined. 

172 

173 :returns: The Python package classifier. 

174 :raises ValueError: If there is no classifier defined for the license. |br| (See and check :data:`~pyTooling.Licensing.PYTHON_LICENSE_NAMES`) 

175 

176 .. seealso:: 

177 

178 List of `Python classifiers <https://pypi.org/classifiers/>`__ 

179 """ 

180 try: 

181 item: PythonLicenseName = PYTHON_LICENSE_NAMES[self._spdxIdentifier] 

182 except KeyError as ex: 

183 raise ValueError(f"License has no Python specify information.") from ex 

184 

185 osi = "OSI Approved :: " if self._osiApproved else "" 

186 return f"License :: {osi}{item.Classifier}" 

187 

188 def __eq__(self, other: Any) -> bool: 

189 """ 

190 Returns true, if both licenses are identical (comparison based on SPDX identifiers). 

191 

192 :returns: ``True``, if both licenses are identical. 

193 :raises TypeError: If second operand is not of type :class:`License` or :class:`str`. 

194 """ 

195 if isinstance(other, License): 

196 return self._spdxIdentifier == other._spdxIdentifier 

197 else: 

198 ex = TypeError(f"Second operand of type '{other.__class__.__name__}' is not supported by equal operator.") 

199 if version_info >= (3, 11): # pragma: no cover 

200 ex.add_note(f"Supported types for second operand: License, str") 

201 raise ex 

202 

203 def __ne__(self, other: Any) -> bool: 

204 """ 

205 Returns true, if both licenses are not identical (comparison based on SPDX identifiers). 

206 

207 :returns: ``True``, if both licenses are not identical. 

208 :raises TypeError: If second operand is not of type :class:`License` or :class:`str`. 

209 """ 

210 if isinstance(other, License): 

211 return self._spdxIdentifier != other._spdxIdentifier 

212 else: 

213 ex = TypeError(f"Second operand of type '{other.__class__.__name__}' is not supported by unequal operator.") 

214 if version_info >= (3, 11): # pragma: no cover 

215 ex.add_note(f"Supported types for second operand: License, str") 

216 raise ex 

217 

218 def __le__(self, other: Any) -> bool: 

219 """Returns true, if both licenses are compatible.""" 

220 raise NotImplementedError("License compatibility check is not yet implemented.") 

221 

222 def __ge__(self, other: Any) -> bool: 

223 """Returns true, if both licenses are compatible.""" 

224 raise NotImplementedError("License compatibility check is not yet implemented.") 

225 

226 def __repr__(self) -> str: 

227 """ 

228 Returns the internal unique representation (a.k.a SPDX identifier). 

229 

230 :returns: SPDX identifier of the license. 

231 """ 

232 return self._spdxIdentifier 

233 

234 def __str__(self) -> str: 

235 """ 

236 Returns the license' name. 

237 

238 :returns: Name of the license. 

239 """ 

240 return self._name 

241 

242 

243Apache_2_0_License = License("Apache-2.0", "Apache License 2.0", True, True) 

244BSD_3_Clause_License = License("BSD-3-Clause", "BSD 3-Clause Revised License", True, True) 

245GPL_2_0_or_later = License("GPL-2.0-or-later", "GNU General Public License v2.0 or later", True, True) 

246MIT_License = License("MIT", "MIT License", True, True) 

247 

248 

249#: Mapping of predefined licenses 

250SPDX_INDEX: Dict[str, License] = { 

251 "Apache-2.0": Apache_2_0_License, 

252 "BSD-3-Clause": BSD_3_Clause_License, 

253 "GPL-2.0-or-later": GPL_2_0_or_later, 

254 "MIT": MIT_License 

255}