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

71 statements  

« prev     ^ index     » next       coverage.py v7.15.4, created at 2026-08-22 21:29 +0000

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

2# _____ _ _ _ _ _ # 

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

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

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

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

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

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

9# Authors: # 

10# Patrick Lehmann # 

11# # 

12# License: # 

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

14# Copyright 2017-2026 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:: 

44 

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

46""" 

47from dataclasses import dataclass 

48from typing import Any 

49from pyTooling.Common import getFullyQualifiedName 

50from pyTooling.Decorators import export, readonly 

51from pyTooling.MetaClasses import ExtendedType 

52 

53 

54__all__ = [ 

55 "PYTHON_LICENSE_NAMES", 

56 

57 "Apache_2_0_License", 

58 "BSD_3_Clause_License", 

59 "GPL_2_0_or_later", 

60 "MIT_License", 

61 

62 "SPDX_INDEX" 

63] 

64 

65 

66@export 

67@dataclass 

68class PythonLicenseName: 

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

70 

71 ShortName: str #: License's short name 

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

73 

74 def __str__(self) -> str: 

75 """ 

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

77 

78 :returns: Short name of the license. 

79 """ 

80 return self.ShortName 

81 

82 

83#: Mapping of SPDX identifiers to Python license names 

84PYTHON_LICENSE_NAMES: dict[str, PythonLicenseName] = { 

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

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

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

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

89} 

90 

91 

92@export 

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

94 """Representation of a license.""" 

95 

96 _spdxIdentifier: str #: Unique SPDX identifier. 

97 _name: str #: Name of the license. 

98 _osiApproved: bool #: OSI approval status 

99 _fsfApproved: bool #: FSF approval status 

100 

101 def __init__(self, spdxIdentifier: str, name: str, osiApproved: bool = False, fsfApproved: bool = False) -> None: 

102 """ 

103 Initialize a license with its SPDX identifier, its name and its approval flags. 

104 

105 :param spdxIdentifier: SPDX identifier of the license. 

106 :param name: Name of the license. 

107 :param osiApproved: Optional, ``True``, if the license is approved by the Open Source Initiative. 

108 :param fsfApproved: Optional, ``True``, if the license is approved by the Free Software Foundation. 

109 """ 

110 self._spdxIdentifier = spdxIdentifier 

111 self._name = name 

112 self._osiApproved = osiApproved 

113 self._fsfApproved = fsfApproved 

114 

115 @readonly 

116 def Name(self) -> str: 

117 """ 

118 Returns the license' name. 

119 

120 :returns: License name. 

121 """ 

122 return self._name 

123 

124 @readonly 

125 def SPDXIdentifier(self) -> str: 

126 """ 

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

128 

129 :returns: The the unique SPDX identifier. 

130 """ 

131 return self._spdxIdentifier 

132 

133 @readonly 

134 def OSIApproved(self) -> bool: 

135 """ 

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

137 

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

139 """ 

140 return self._osiApproved 

141 

142 @readonly 

143 def FSFApproved(self) -> bool: 

144 """ 

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

146 

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

148 """ 

149 return self._fsfApproved 

150 

151 @readonly 

152 def PythonLicenseName(self) -> str: 

153 """ 

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

155 

156 :returns: The Python license name. 

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

158 """ 

159 try: 

160 item: PythonLicenseName = PYTHON_LICENSE_NAMES[self._spdxIdentifier] 

161 except KeyError as ex: 

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

163 

164 return item.ShortName 

165 

166 @readonly 

167 def PythonClassifier(self) -> str: 

168 """ 

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

170 

171 :returns: The Python package classifier. 

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

173 

174 .. seealso:: 

175 

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

177 """ 

178 try: 

179 item: PythonLicenseName = PYTHON_LICENSE_NAMES[self._spdxIdentifier] 

180 except KeyError as ex: 

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

182 

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

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

185 

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

187 """ 

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

189 

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

191 :raises TypeError: If second operand is not of type :class:`License` or string. 

192 """ 

193 if isinstance(other, License): 

194 return self._spdxIdentifier == other._spdxIdentifier 

195 else: 

196 ex = TypeError(f"Second operand is not supported by equal operator.") 

197 ex.add_note(f"Got type '{getFullyQualifiedName(other)}'.") 

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

199 raise ex 

200 

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

202 """ 

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

204 

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

206 :raises TypeError: If second operand is not of type :class:`License` or string. 

207 """ 

208 if isinstance(other, License): 

209 return self._spdxIdentifier != other._spdxIdentifier 

210 else: 

211 ex = TypeError(f"Second operand is not supported by unequal operator.") 

212 ex.add_note(f"Got type '{getFullyQualifiedName(other)}'.") 

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

214 raise ex 

215 

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

217 """ 

218 Returns true, if both licenses are compatible. 

219 

220 :param other: Second operand, the license to compare with. 

221 :returns: ``True``, if both licenses are compatible. 

222 :raises NotImplementedError: License compatibility is not implemented yet. 

223 """ 

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

225 

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

227 """ 

228 Returns true, if both licenses are compatible. 

229 

230 :param other: Second operand, the license to compare with. 

231 :returns: ``True``, if both licenses are compatible. 

232 :raises NotImplementedError: License compatibility is not implemented yet. 

233 """ 

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

235 

236 def __repr__(self) -> str: 

237 """ 

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

239 

240 :returns: SPDX identifier of the license. 

241 """ 

242 return self._spdxIdentifier 

243 

244 def __str__(self) -> str: 

245 """ 

246 Returns the license' name. 

247 

248 :returns: Name of the license. 

249 """ 

250 return self._name 

251 

252 

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

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

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

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

257 

258 

259#: Mapping of predefined licenses 

260SPDX_INDEX: dict[str, License] = { 

261 "Apache-2.0": Apache_2_0_License, 

262 "BSD-3-Clause": BSD_3_Clause_License, 

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

264 "MIT": MIT_License 

265}