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

523 statements  

« prev     ^ index     » next       coverage.py v7.17.0, created at 2026-10-25 12:33 +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 * :gh:`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 enum import Enum, unique 

49from re import compile as re_compile 

50from typing import Any, ClassVar, Generator, Optional as Nullable 

51from pyTooling.Common import getFullyQualifiedName 

52from pyTooling.Decorators import export, readonly 

53from pyTooling.Exceptions import ToolingException 

54from pyTooling.MetaClasses import ExtendedType, abstractclass, abstractmethod 

55 

56 

57__all__ = [ 

58 "PYTHON_LICENSE_NAMES", 

59 

60 "Apache_2_0_License", 

61 "BSD_2_Clause_License", 

62 "BSD_3_Clause_License", 

63 "MIT_License", 

64 "ISC_License", 

65 "MPL_2_0_License", 

66 "BSL_1_0_License", 

67 "Zlib_License", 

68 "PSF_2_0_License", 

69 "Unlicense", 

70 "CC0_1_0", 

71 "CC_BY_4_0", 

72 "CC_BY_SA_4_0", 

73 "CC_BY_NC_4_0", 

74 "CC_BY_ND_4_0", 

75 "CC_BY_NC_SA_4_0", 

76 "CC_BY_NC_ND_4_0", 

77 "EPL_1_0_License", 

78 "EPL_2_0_License", 

79 "LGPL_2_1_only", 

80 "LGPL_2_1_or_later", 

81 "LGPL_3_0_only", 

82 "LGPL_3_0_or_later", 

83 "GPL_2_0_only", 

84 "GPL_2_0_or_later", 

85 "GPL_3_0_only", 

86 "GPL_3_0_or_later", 

87 "AGPL_3_0_only", 

88 "AGPL_3_0_or_later", 

89 

90 "SPDX_INDEX", 

91 "LICENSES_BY_CLASSIFIER", 

92 "OSI_LICENSE_URLS", 

93 "LICENSE_URLS", 

94 "LICENSE_TEXT_URLS" 

95] 

96 

97 

98@export 

99class LicensingError(ToolingException): 

100 """Base exception of all exceptions raised by :mod:`pyTooling.Licensing`.""" 

101 

102 

103@export 

104class LicenseExpressionError(LicensingError): 

105 """ 

106 The exception is raised when an SPDX license expression is malformed or names a license SPDX doesn't define. 

107 """ 

108 

109 _expression: Nullable[str] #: License expression that can't be parsed. 

110 _identifier: Nullable[str] #: License identifier SPDX doesn't define. 

111 

112 def __init__(self, message: str, /, *, expression: Nullable[str] = None, identifier: Nullable[str] = None) -> None: 

113 """ 

114 Initializes the exception with the license expression and, for an unknown license, its identifier. 

115 

116 :param message: The exception's message. 

117 :param expression: Optional, the license expression that can't be parsed. 

118 :param identifier: Optional, the license identifier SPDX doesn't define, if that's the reason. 

119 """ 

120 super().__init__(message) 

121 self._expression = expression 

122 self._identifier = identifier 

123 

124 @readonly 

125 def Expression(self) -> Nullable[str]: 

126 """ 

127 Read-only property to access the license expression that can't be parsed (:attr:`_expression`). 

128 

129 :returns: The expression, or ``None`` if it wasn't recorded. 

130 """ 

131 return self._expression 

132 

133 @readonly 

134 def Identifier(self) -> Nullable[str]: 

135 """ 

136 Read-only property to access the license identifier SPDX doesn't define (:attr:`_identifier`). 

137 

138 :returns: The identifier, or ``None`` if it wasn't recorded. 

139 """ 

140 return self._identifier 

141 

142 

143@export 

144@unique 

145class LicenseAbsence(Enum): 

146 """ 

147 SPDX's two ways of stating that no license is named, which mean different things. 

148 

149 Both are values a license field may hold **on their own**; neither is part of the expression grammar, so neither 

150 can be an operand of ``AND``, ``OR`` or ``WITH``. 

151 """ 

152 

153 NoLicense = "NONE" #: The work states that no license applies to it. 

154 NoAssertion = "NOASSERTION" #: Someone looked and declined to state a license; nothing is claimed either way. 

155 

156 def __str__(self) -> str: 

157 """ 

158 Return the value in SPDX's spelling. 

159 

160 :returns: ``NONE`` or ``NOASSERTION``. 

161 """ 

162 return self.value 

163 

164 

165@export 

166@dataclass 

167class PythonLicenseName: 

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

169 

170 ShortName: str #: License's short name 

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

172 

173 def __str__(self) -> str: 

174 """ 

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

176 

177 :returns: Short name of the license. 

178 """ 

179 return self.ShortName 

180 

181 

182#: Mapping of SPDX identifiers to Python license names 

183PYTHON_LICENSE_NAMES: dict[str, PythonLicenseName] = { 

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

185 "0BSD": PythonLicenseName("0BSD", "Zero-Clause BSD (0BSD)"), 

186 "BSD-2-Clause": PythonLicenseName("BSD-2-Clause", "BSD License"), 

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

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

189 "MIT-0": PythonLicenseName("MIT-0", "MIT No Attribution License (MIT-0)"), 

190 "MIT-CMU": PythonLicenseName("MIT-CMU", "CMU License (MIT-CMU)"), 

191 "ISC": PythonLicenseName("ISC", "ISC License (ISCL)"), 

192 "MPL-2.0": PythonLicenseName("MPL-2.0", "Mozilla Public License 2.0 (MPL 2.0)"), 

193 "BSL-1.0": PythonLicenseName("BSL-1.0", "Boost Software License 1.0 (BSL-1.0)"), 

194 "Zlib": PythonLicenseName("Zlib", "zlib/libpng License"), 

195 "PSF-2.0": PythonLicenseName("PSF-2.0", "Python Software Foundation License"), 

196 "Unlicense": PythonLicenseName("Unlicense", "The Unlicense (Unlicense)"), 

197 "CC0-1.0": PythonLicenseName("CC0-1.0", "CC0 1.0 Universal (CC0 1.0) Public Domain Dedication"), 

198 "EPL-1.0": PythonLicenseName("EPL-1.0", "Eclipse Public License 1.0 (EPL-1.0)"), 

199 "EPL-2.0": PythonLicenseName("EPL-2.0", "Eclipse Public License 2.0 (EPL-2.0)"), 

200 "LGPL-2.1-only": PythonLicenseName("LGPL-2.1-only", "GNU Lesser General Public License v2 (LGPLv2)"), 

201 "LGPL-2.1-or-later": PythonLicenseName("LGPL-2.1-or-later", "GNU Lesser General Public License v2 or later (LGPLv2+)"), 

202 "LGPL-3.0-only": PythonLicenseName("LGPL-3.0-only", "GNU Lesser General Public License v3 (LGPLv3)"), 

203 "LGPL-3.0-or-later": PythonLicenseName("LGPL-3.0-or-later", "GNU Lesser General Public License v3 or later (LGPLv3+)"), 

204 "GPL-2.0-only": PythonLicenseName("GPL-2.0-only", "GNU General Public License v2 (GPLv2)"), 

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

206 "GPL-3.0-only": PythonLicenseName("GPL-3.0-only", "GNU General Public License v3 (GPLv3)"), 

207 "GPL-3.0-or-later": PythonLicenseName("GPL-3.0-or-later", "GNU General Public License v3 or later (GPLv3+)"), 

208 "AGPL-3.0-only": PythonLicenseName("AGPL-3.0-only", "GNU Affero General Public License v3"), 

209 "AGPL-3.0-or-later": PythonLicenseName("AGPL-3.0-or-later", "GNU Affero General Public License v3 or later (AGPLv3+)"), 

210} 

211 

212 

213#: Mapping of SPDX identifiers to the page where the licensor publishes the license. 

214#: 

215#: Sourced from the SPDX License List's own ``seeAlso`` field, or from the licensor's domain where SPDX names none. 

216#: A license whose only published home is its OSI page has **no entry** - that URL is 

217#: :attr:`License.OSIURL` and isn't repeated here, e.g. ``MIT`` or ``BSD-3-Clause``. 

218LICENSE_URLS: dict[str, str] = { 

219 "Apache-2.0": "https://www.apache.org/licenses/LICENSE-2.0", 

220 "ISC": "https://www.isc.org/licenses/", 

221 "MPL-2.0": "https://www.mozilla.org/MPL/2.0/", 

222 "BSL-1.0": "https://www.boost.org/doc/user-guide/bsl.html", 

223 "Zlib": "https://zlib.net/zlib_license.html", 

224 "PSF-2.0": "https://docs.python.org/3/license.html", 

225 "Unlicense": "https://unlicense.org/", 

226 "CC0-1.0": "https://creativecommons.org/publicdomain/zero/1.0/", 

227 "CC-BY-4.0": "https://creativecommons.org/licenses/by/4.0/", 

228 "CC-BY-SA-4.0": "https://creativecommons.org/licenses/by-sa/4.0/", 

229 "CC-BY-NC-4.0": "https://creativecommons.org/licenses/by-nc/4.0/", 

230 "CC-BY-ND-4.0": "https://creativecommons.org/licenses/by-nd/4.0/", 

231 "CC-BY-NC-SA-4.0": "https://creativecommons.org/licenses/by-nc-sa/4.0/", 

232 "CC-BY-NC-ND-4.0": "https://creativecommons.org/licenses/by-nc-nd/4.0/", 

233 "EPL-1.0": "https://www.eclipse.org/legal/epl/epl-v10.html", 

234 "EPL-2.0": "https://www.eclipse.org/legal/epl-2.0/", 

235 "GPL-2.0-only": "https://www.gnu.org/licenses/old-licenses/gpl-2.0-standalone.html", 

236 "GPL-2.0-or-later": "https://www.gnu.org/licenses/old-licenses/gpl-2.0-standalone.html", 

237 "LGPL-2.1-only": "https://www.gnu.org/licenses/old-licenses/lgpl-2.1-standalone.html", 

238 "LGPL-2.1-or-later": "https://www.gnu.org/licenses/old-licenses/lgpl-2.1-standalone.html", 

239 "GPL-3.0-only": "https://www.gnu.org/licenses/gpl-3.0-standalone.html", 

240 "GPL-3.0-or-later": "https://www.gnu.org/licenses/gpl-3.0-standalone.html", 

241 "LGPL-3.0-only": "https://www.gnu.org/licenses/lgpl-3.0-standalone.html", 

242 "LGPL-3.0-or-later": "https://www.gnu.org/licenses/lgpl-3.0-standalone.html", 

243 "AGPL-3.0-only": "https://www.gnu.org/licenses/agpl-3.0-standalone.html", 

244 "AGPL-3.0-or-later": "https://www.gnu.org/licenses/agpl-3.0-standalone.html", 

245} 

246 

247 

248#: Mapping of SPDX identifiers to the license text, by the file extension it is published as. 

249#: 

250#: Keys are the extension without its dot - ``txt``, ``md``, ``rst``, ``tex``. What a license offers is entirely up 

251#: to its licensor: the GNU licenses publish three formats - the GPL-3.0 a fourth, ``rst`` -, most publish one, and 

252#: several publish none at all beyond an HTML page, which is :data:`LICENSE_URLS`. 

253#: 

254#: Every URL here answered an HTTP request with the license, so the table is what is **known** rather than a claim 

255#: of completeness - a license absent from it may still publish a text nobody has looked up yet. 

256LICENSE_TEXT_URLS: dict[str, dict[str, str]] = { 

257 "Apache-2.0": {"txt": "https://www.apache.org/licenses/LICENSE-2.0.txt"}, 

258 "MPL-2.0": {"txt": "https://www.mozilla.org/media/MPL/2.0/index.txt"}, 

259 "BSL-1.0": {"txt": "https://www.boost.org/LICENSE_1_0.txt"}, 

260 "Unlicense": {"txt": "https://unlicense.org/UNLICENSE"}, 

261 "CC0-1.0": {"txt": "https://creativecommons.org/publicdomain/zero/1.0/legalcode.txt"}, 

262 "CC-BY-4.0": {"txt": "https://creativecommons.org/licenses/by/4.0/legalcode.txt"}, 

263 "CC-BY-SA-4.0": {"txt": "https://creativecommons.org/licenses/by-sa/4.0/legalcode.txt"}, 

264 "CC-BY-NC-4.0": {"txt": "https://creativecommons.org/licenses/by-nc/4.0/legalcode.txt"}, 

265 "CC-BY-ND-4.0": {"txt": "https://creativecommons.org/licenses/by-nd/4.0/legalcode.txt"}, 

266 "CC-BY-NC-SA-4.0": {"txt": "https://creativecommons.org/licenses/by-nc-sa/4.0/legalcode.txt"}, 

267 "CC-BY-NC-ND-4.0": {"txt": "https://creativecommons.org/licenses/by-nc-nd/4.0/legalcode.txt"}, 

268 "EPL-2.0": {"txt": "https://www.eclipse.org/org/documents/epl-2.0/EPL-2.0.txt"}, 

269 "GPL-2.0-only": { 

270 "txt": "https://www.gnu.org/licenses/old-licenses/gpl-2.0.txt", 

271 "md": "https://www.gnu.org/licenses/old-licenses/gpl-2.0.md", 

272 "tex": "https://www.gnu.org/licenses/old-licenses/gpl-2.0.tex", 

273 }, 

274 "GPL-2.0-or-later": { 

275 "txt": "https://www.gnu.org/licenses/old-licenses/gpl-2.0.txt", 

276 "md": "https://www.gnu.org/licenses/old-licenses/gpl-2.0.md", 

277 "tex": "https://www.gnu.org/licenses/old-licenses/gpl-2.0.tex", 

278 }, 

279 "LGPL-2.1-only": { 

280 "txt": "https://www.gnu.org/licenses/old-licenses/lgpl-2.1.txt", 

281 "md": "https://www.gnu.org/licenses/old-licenses/lgpl-2.1.md", 

282 "tex": "https://www.gnu.org/licenses/old-licenses/lgpl-2.1.tex", 

283 }, 

284 "LGPL-2.1-or-later": { 

285 "txt": "https://www.gnu.org/licenses/old-licenses/lgpl-2.1.txt", 

286 "md": "https://www.gnu.org/licenses/old-licenses/lgpl-2.1.md", 

287 "tex": "https://www.gnu.org/licenses/old-licenses/lgpl-2.1.tex", 

288 }, 

289 "GPL-3.0-only": { 

290 "txt": "https://www.gnu.org/licenses/gpl-3.0.txt", 

291 "md": "https://www.gnu.org/licenses/gpl-3.0.md", 

292 "rst": "https://www.gnu.org/licenses/gpl-3.0.rst", 

293 "tex": "https://www.gnu.org/licenses/gpl-3.0.tex", 

294 }, 

295 "GPL-3.0-or-later": { 

296 "txt": "https://www.gnu.org/licenses/gpl-3.0.txt", 

297 "md": "https://www.gnu.org/licenses/gpl-3.0.md", 

298 "rst": "https://www.gnu.org/licenses/gpl-3.0.rst", 

299 "tex": "https://www.gnu.org/licenses/gpl-3.0.tex", 

300 }, 

301 "LGPL-3.0-only": { 

302 "txt": "https://www.gnu.org/licenses/lgpl-3.0.txt", 

303 "md": "https://www.gnu.org/licenses/lgpl-3.0.md", 

304 "tex": "https://www.gnu.org/licenses/lgpl-3.0.tex", 

305 }, 

306 "LGPL-3.0-or-later": { 

307 "txt": "https://www.gnu.org/licenses/lgpl-3.0.txt", 

308 "md": "https://www.gnu.org/licenses/lgpl-3.0.md", 

309 "tex": "https://www.gnu.org/licenses/lgpl-3.0.tex", 

310 }, 

311 "AGPL-3.0-only": { 

312 "txt": "https://www.gnu.org/licenses/agpl-3.0.txt", 

313 "md": "https://www.gnu.org/licenses/agpl-3.0.md", 

314 "tex": "https://www.gnu.org/licenses/agpl-3.0.tex", 

315 }, 

316 "AGPL-3.0-or-later": { 

317 "txt": "https://www.gnu.org/licenses/agpl-3.0.txt", 

318 "md": "https://www.gnu.org/licenses/agpl-3.0.md", 

319 "tex": "https://www.gnu.org/licenses/agpl-3.0.tex", 

320 }, 

321} 

322 

323#: Mapping of SPDX identifiers to the license's page at the 

324#: `Open Source Initiative <https://opensource.org/licenses>`__. 

325#: 

326#: OSI's slugs don't follow the SPDX identifier and can't be derived from it: ``PSF-2.0`` is published as 

327#: ``Python-2.0``, and OSI has **one** page per license where SPDX has two identifiers - ``GPL-2.0-only`` and 

328#: ``GPL-2.0-or-later`` both point at it, because *only* versus *or later* is SPDX's distinction, not OSI's. 

329#: 

330#: A license OSI hasn't approved has no entry, which is why ``CC0-1.0`` and the Creative Commons licenses are absent. 

331OSI_LICENSE_URLS: dict[str, str] = { 

332 "0BSD": "https://opensource.org/license/0bsd", 

333 "Apache-2.0": "https://opensource.org/license/apache-2.0", 

334 "BSD-2-Clause": "https://opensource.org/license/bsd-2-clause", 

335 "BSD-3-Clause": "https://opensource.org/license/bsd-3-clause", 

336 "MIT-0": "https://opensource.org/license/mit-0", 

337 "MIT-CMU": "https://opensource.org/license/cmu-license", 

338 "MIT": "https://opensource.org/license/mit", 

339 "ISC": "https://opensource.org/license/isc", 

340 "MPL-2.0": "https://opensource.org/license/mpl-2.0", 

341 "BSL-1.0": "https://opensource.org/license/bsl-1.0", 

342 "Zlib": "https://opensource.org/license/zlib", 

343 "PSF-2.0": "https://opensource.org/license/Python-2.0", 

344 "Unlicense": "https://opensource.org/license/unlicense", 

345 "EPL-1.0": "https://opensource.org/license/epl-1.0", 

346 "EPL-2.0": "https://opensource.org/license/epl-2.0", 

347 "LGPL-2.1-only": "https://opensource.org/license/lgpl-2-1", 

348 "LGPL-2.1-or-later": "https://opensource.org/license/lgpl-2-1", 

349 "LGPL-3.0-only": "https://opensource.org/license/lgpl-3-0", 

350 "LGPL-3.0-or-later": "https://opensource.org/license/lgpl-3-0", 

351 "GPL-2.0-only": "https://opensource.org/license/gpl-2.0", 

352 "GPL-2.0-or-later": "https://opensource.org/license/gpl-2.0", 

353 "GPL-3.0-only": "https://opensource.org/license/gpl-3.0", 

354 "GPL-3.0-or-later": "https://opensource.org/license/gpl-3.0", 

355 "AGPL-3.0-only": "https://opensource.org/license/agpl-3-0", 

356 "AGPL-3.0-or-later": "https://opensource.org/license/agpl-3-0", 

357} 

358 

359 

360@export 

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

362 """Representation of a license.""" 

363 

364 _spdxIdentifier: str #: Unique SPDX identifier. 

365 _name: str #: Name of the license. 

366 _osiApproved: bool #: OSI approval status 

367 _fsfApproved: bool #: FSF approval status 

368 

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

370 """ 

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

372 

373 :param spdxIdentifier: SPDX identifier of the license. 

374 :param name: Name of the license. 

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

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

377 """ 

378 self._spdxIdentifier = spdxIdentifier 

379 self._name = name 

380 self._osiApproved = osiApproved 

381 self._fsfApproved = fsfApproved 

382 

383 @readonly 

384 def Name(self) -> str: 

385 """ 

386 Returns the license' name. 

387 

388 :returns: License name. 

389 """ 

390 return self._name 

391 

392 @readonly 

393 def SPDXIdentifier(self) -> str: 

394 """ 

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

396 

397 :returns: The unique SPDX identifier. 

398 """ 

399 return self._spdxIdentifier 

400 

401 @readonly 

402 def SPDXURL(self) -> str: 

403 """ 

404 Returns the URL of this license's page in the `SPDX License List <https://spdx.org/licenses/>`__. 

405 

406 SPDX publishes one page per identifier at a fixed address, so this is derived from 

407 :attr:`SPDXIdentifier` rather than stored. A license whose identifier isn't on that list has no page there, 

408 and the derived URL won't resolve. 

409 

410 :returns: URL of the license's page at SPDX. 

411 """ 

412 return f"https://spdx.org/licenses/{self._spdxIdentifier}.html" 

413 

414 @readonly 

415 def URL(self) -> Nullable[str]: 

416 """ 

417 Returns the page where the licensor publishes this license. 

418 

419 This is looked up in :data:`LICENSE_URLS`. It is the licensor's own page - ``https://www.apache.org/licenses/ 

420 LICENSE-2.0`` for the Apache License 2.0 - which is not the same thing as :attr:`SPDXURL` or :attr:`OSIURL`, 

421 the two catalogue entries describing it. 

422 

423 A license whose only published home *is* its OSI page has no entry here rather than a duplicate of 

424 :attr:`OSIURL`; ``MIT``, ``BSD-2-Clause`` and ``BSD-3-Clause`` are those. 

425 

426 :returns: URL of the license's own page, or ``None`` if the licensor publishes none. 

427 

428 .. seealso:: 

429 

430 :attr:`TextURLs` 

431 |rarr| The same license as text, by format. 

432 """ 

433 return LICENSE_URLS.get(self._spdxIdentifier, None) 

434 

435 @readonly 

436 def TextURLs(self) -> dict[str, str]: 

437 """ 

438 Returns the URLs of this license's text, keyed by the format it is published as. 

439 

440 Keys are the file extension without its dot: ``txt``, ``md``, ``rst``, ``tex``. Which formats exist is the 

441 licensor's choice - the GNU licenses publish four, most publish one, and several publish none. 

442 

443 The table is what is **known**, not a claim of completeness: an empty mapping means no URL is recorded, not 

444 that the licensor publishes no text. Every URL in it answered an HTTP request with the license. 

445 

446 .. code-block:: python 

447 

448 Apache_2_0_License.TextURLs["txt"] # https://www.apache.org/licenses/LICENSE-2.0.txt 

449 GPL_3_0_only.TextURLs["rst"] # https://www.gnu.org/licenses/gpl-3.0.rst 

450 

451 A **copy** is returned, so a caller can't edit :data:`LICENSE_TEXT_URLS` through it. 

452 

453 :returns: The license text's URLs by format, or an empty dictionary if none is published. 

454 """ 

455 return dict(LICENSE_TEXT_URLS.get(self._spdxIdentifier, {})) 

456 

457 @readonly 

458 def OSIURL(self) -> Nullable[str]: 

459 """ 

460 Returns the URL of this license's page at the `Open Source Initiative <https://opensource.org/licenses>`__. 

461 

462 This is looked up in :data:`OSI_LICENSE_URLS` rather than derived: OSI's slugs don't follow the SPDX 

463 identifier, and OSI has one page where SPDX has two identifiers - ``GPL-2.0-only`` and ``GPL-2.0-or-later`` 

464 share it. 

465 

466 :returns: URL of the license's page at OSI, or ``None`` if OSI doesn't publish it. 

467 

468 .. seealso:: 

469 

470 :attr:`OSIApproved` 

471 |rarr| Whether OSI approved this license at all. 

472 """ 

473 return OSI_LICENSE_URLS.get(self._spdxIdentifier, None) 

474 

475 @readonly 

476 def OSIApproved(self) -> bool: 

477 """ 

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

479 

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

481 """ 

482 return self._osiApproved 

483 

484 @readonly 

485 def FSFApproved(self) -> bool: 

486 """ 

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

488 

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

490 """ 

491 return self._fsfApproved 

492 

493 @readonly 

494 def PythonLicenseName(self) -> str: 

495 """ 

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

497 

498 :returns: The Python license name. 

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

500 """ 

501 try: 

502 item: PythonLicenseName = PYTHON_LICENSE_NAMES[self._spdxIdentifier] 

503 except KeyError as ex: 

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

505 

506 return item.ShortName 

507 

508 @readonly 

509 def PythonClassifier(self) -> str: 

510 """ 

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

512 

513 :returns: The Python package classifier. 

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

515 

516 .. seealso:: 

517 

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

519 """ 

520 try: 

521 item: PythonLicenseName = PYTHON_LICENSE_NAMES[self._spdxIdentifier] 

522 except KeyError as ex: 

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

524 

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

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

527 

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

529 """ 

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

531 

532 :param other: The second operand to compare with. A :class:`License` or its SPDX identifier as a string. 

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

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

535 """ 

536 if isinstance(other, License): 

537 return self._spdxIdentifier == other._spdxIdentifier 

538 elif isinstance(other, str): 

539 return self._spdxIdentifier == other 

540 else: 

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

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

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

544 raise ex 

545 

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

547 """ 

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

549 

550 :param other: The second operand to compare with. A :class:`License` or its SPDX identifier as a string. 

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

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

553 """ 

554 if isinstance(other, License): 

555 return self._spdxIdentifier != other._spdxIdentifier 

556 elif isinstance(other, str): 

557 return self._spdxIdentifier != other 

558 else: 

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

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

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

562 raise ex 

563 

564 def __hash__(self) -> int: 

565 """ 

566 Compute a hash from the license's SPDX identifier. 

567 

568 A license compares equal to its identifier as a string, so the two hash equally as well. 

569 

570 :returns: Hash of the SPDX identifier. 

571 """ 

572 return hash(self._spdxIdentifier) 

573 

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

575 """ 

576 Returns true, if both licenses are compatible. 

577 

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

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

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

581 """ 

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

583 

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

585 """ 

586 Returns true, if both licenses are compatible. 

587 

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

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

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

591 """ 

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

593 

594 def __repr__(self) -> str: 

595 """ 

596 Returns the internal unique representation (:attr:`spdxIdentifier`). 

597 

598 :returns: SPDX identifier of the license. 

599 """ 

600 return self._spdxIdentifier 

601 

602 def __str__(self) -> str: 

603 """ 

604 Returns the license' name. 

605 

606 :returns: Name of the license. 

607 """ 

608 return self._name 

609 

610 

611Zero_Clause_BSD = License("0BSD", "BSD Zero Clause License", True, False) 

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

613BSD_2_Clause_License = License("BSD-2-Clause", "BSD 2-Clause Simplified License", True, True) 

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

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

616MIT_0_License = License("MIT-0", "MIT No Attribution", True, False) 

617MIT_CMU_License = License("MIT-CMU", "CMU License", True, False) 

618ISC_License = License("ISC", "ISC License", True, True) 

619MPL_2_0_License = License("MPL-2.0", "Mozilla Public License 2.0", True, True) 

620BSL_1_0_License = License("BSL-1.0", "Boost Software License 1.0", True, True) 

621Zlib_License = License("Zlib", "zlib License", True, True) 

622PSF_2_0_License = License("PSF-2.0", "Python Software Foundation License 2.0", True, True) 

623Unlicense = License("Unlicense", "The Unlicense", True, True) 

624CC0_1_0 = License("CC0-1.0", "Creative Commons Zero v1.0 Universal", False, True) 

625EPL_1_0_License = License("EPL-1.0", "Eclipse Public License 1.0", True, True) 

626EPL_2_0_License = License("EPL-2.0", "Eclipse Public License 2.0", True, True) 

627LGPL_2_1_only = License("LGPL-2.1-only", "GNU Lesser General Public License v2.1 only", True, True) 

628LGPL_2_1_or_later = License("LGPL-2.1-or-later", "GNU Lesser General Public License v2.1 or later", True, True) 

629LGPL_3_0_only = License("LGPL-3.0-only", "GNU Lesser General Public License v3.0 only", True, True) 

630LGPL_3_0_or_later = License("LGPL-3.0-or-later", "GNU Lesser General Public License v3.0 or later", True, True) 

631GPL_2_0_only = License("GPL-2.0-only", "GNU General Public License v2.0 only", True, True) 

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

633GPL_3_0_only = License("GPL-3.0-only", "GNU General Public License v3.0 only", True, True) 

634GPL_3_0_or_later = License("GPL-3.0-or-later", "GNU General Public License v3.0 or later", True, True) 

635AGPL_3_0_only = License("AGPL-3.0-only", "GNU Affero General Public License v3.0 only", True, True) 

636AGPL_3_0_or_later = License("AGPL-3.0-or-later", "GNU Affero General Public License v3.0 or later", True, True) 

637 

638CC_BY_4_0 = License("CC-BY-4.0", "Creative Commons Attribution 4.0 International", False, True) 

639CC_BY_SA_4_0 = License("CC-BY-SA-4.0", "Creative Commons Attribution Share Alike 4.0 International", False, True) 

640CC_BY_NC_4_0 = License("CC-BY-NC-4.0", "Creative Commons Attribution Non Commercial 4.0 International", False, False) 

641CC_BY_ND_4_0 = License("CC-BY-ND-4.0", "Creative Commons Attribution No Derivatives 4.0 International", False, False) 

642CC_BY_NC_SA_4_0 = License( 

643 "CC-BY-NC-SA-4.0", "Creative Commons Attribution Non Commercial Share Alike 4.0 International", False, False 

644) 

645CC_BY_NC_ND_4_0 = License( 

646 "CC-BY-NC-ND-4.0", "Creative Commons Attribution Non Commercial No Derivatives 4.0 International", False, False 

647) 

648 

649 

650#: All predefined licenses, in the order they are defined above. 

651LICENSES: tuple[License, ...] = ( 

652 Zero_Clause_BSD, Apache_2_0_License, BSD_2_Clause_License, BSD_3_Clause_License, MIT_License, MIT_0_License, 

653 MIT_CMU_License, ISC_License, MPL_2_0_License, 

654 BSL_1_0_License, Zlib_License, PSF_2_0_License, Unlicense, CC0_1_0, EPL_1_0_License, EPL_2_0_License, 

655 LGPL_2_1_only, LGPL_2_1_or_later, LGPL_3_0_only, LGPL_3_0_or_later, 

656 GPL_2_0_only, GPL_2_0_or_later, GPL_3_0_only, GPL_3_0_or_later, 

657 AGPL_3_0_only, AGPL_3_0_or_later, 

658 CC_BY_4_0, CC_BY_SA_4_0, CC_BY_NC_4_0, CC_BY_ND_4_0, CC_BY_NC_SA_4_0, CC_BY_NC_ND_4_0, 

659) 

660 

661#: Mapping of predefined licenses, indexed by their SPDX identifier. 

662SPDX_INDEX: dict[str, License] = {spdxLicense.SPDXIdentifier: spdxLicense for spdxLicense in LICENSES} 

663 

664 

665@export 

666def buildClassifierIndex() -> dict[str, tuple[License, ...]]: 

667 """ 

668 Index the predefined licenses by the Python classifier they are published as. 

669 

670 A license without a classifier is skipped rather than reported: :attr:`License.PythonClassifier` raises for it, 

671 and a license the Python ecosystem has no classifier for simply can't be found that way. 

672 

673 The licenses indexed are :data:`LICENSES`, and :data:`LICENSES_BY_CLASSIFIER` holds what this returns, built 

674 once when the module is imported. Reading that constant is the usual way to ask; calling this rebuilds it. 

675 

676 :returns: Every classifier, mapped to the licenses it can mean. 

677 """ 

678 index: dict[str, list[License]] = {} 

679 for spdxLicense in LICENSES: 

680 try: 

681 classifier = spdxLicense.PythonClassifier 

682 except ValueError: 

683 continue 

684 

685 index.setdefault(classifier, []).append(spdxLicense) 

686 

687 return {classifier: tuple(licenses) for classifier, licenses in index.items()} 

688 

689 

690#: Mapping of a Python license classifier to the licenses it can mean. 

691#: 

692#: The mapping is one-to-one except for ``License :: OSI Approved :: BSD License``, which means either 

693#: :data:`BSD_2_Clause_License` or :data:`BSD_3_Clause_License` with nothing in the classifier to tell them apart. 

694LICENSES_BY_CLASSIFIER: dict[str, tuple[License, ...]] = buildClassifierIndex() 

695 

696 

697#: The :class:`License` class under a name no expression node shadows with a property of its own. 

698_LicenseType = License 

699 

700 

701@export 

702@abstractclass 

703class LicenseExpression(metaclass=ExtendedType, slots=True): 

704 """ 

705 Base-class of every node in an `SPDX license expression`_ tree. 

706 

707 .. _SPDX license expression: https://spdx.github.io/spdx-spec/v2.3/SPDX-license-expressions/ 

708 

709 An expression is a tree. Its operators - every node that is applied to operands, and the only kind of node that 

710 can be a :attr:`Parent` - derive from :class:`Operator`: 

711 

712 :class:`OrLaterOperator` 

713 |rarr| the ``+`` suffix: the named license or any later version of it. 

714 :class:`WithOperator` 

715 |rarr| ``WITH``: a license together with an exception to it. 

716 :class:`AndOperator` 

717 |rarr| ``AND``: both licenses apply. 

718 :class:`OrOperator` 

719 |rarr| ``OR``: either license applies. 

720 

721 Its leaves are: 

722 

723 :class:`SPDXLicense` 

724 |rarr| a license on the SPDX License List, named by its identifier. 

725 :class:`LicenseReference` 

726 |rarr| a license that is *not* on that list, written as ``LicenseRef-<id>``. 

727 :class:`LicenseException` 

728 |rarr| an exception from the SPDX exception list, the right operand of ``WITH``. 

729 

730 The SPDX grammar defines: 

731 

732 .. code-block:: text 

733 

734 simple-expression = license-id | license-id "+" | license-ref 

735 compound-expression = ( simple-expression 

736 | simple-expression "WITH" license-exception-id 

737 | compound-expression "AND" compound-expression 

738 | compound-expression "OR" compound-expression 

739 | "(" compound-expression ")" ) 

740 

741 There are three binary operators, one unary one, and parentheses. There is **no negation** - an expression 

742 says which licenses apply, never which don't. 

743 

744 Every node knows its :attr:`Parent` and its :attr:`Root`, which is what a :class:`License` can't carry: the 

745 predefined licenses are shared objects, so :data:`MIT_License` appears in many expressions at once and belongs to 

746 none of them. :class:`SPDXLicense` is the wrapper that gives a license object a placeholder in the expression tree. 

747 

748 A tree is built bottom-up by handing the operands to an operator, or by assigning them to the operator's operand 

749 slots afterwards. Both link the operand back to its operator: 

750 

751 .. code-block:: python 

752 

753 bottomUp = AndOperator(SPDXLicense(Apache_2_0_License), SPDXLicense(MIT_License)) 

754 

755 assembled = AndOperator() 

756 assembled.Left = SPDXLicense(Apache_2_0_License) 

757 assembled.Right = SPDXLicense(MIT_License) 

758 

759 The ``parent`` parameter only *records* a parent - it can't know which slot the operand belongs in, so it never 

760 fills one. 

761 """ 

762 

763 PRECEDENCE: ClassVar[int] = 0 #: Precedence of this node's operator; a lower value binds tighter. 

764 

765 _parent: Nullable["Operator"] #: The operator this expression is an operand of, or ``None`` at the root. 

766 _root: "LicenseExpression" #: The outermost expression this node belongs to; ``self`` at the root. 

767 _originalText: str #: The text this expression stands for, on the root; empty if there is none. 

768 

769 def __init__(self, parent: Nullable["Operator"] = None, originalText: str = "") -> None: 

770 """ 

771 Initialize an expression node. 

772 

773 :param parent: Optional, the operator this node becomes an operand of. 

774 :param originalText: Optional, the text this expression stands for. :meth:`Parse` fills it in; a node built 

775 in code carries it when the caller knows what was written. 

776 :raises TypeError: If parameter 'parent' is not of type :class:`Operator`. 

777 """ 

778 self._originalText = originalText 

779 

780 if parent is None: 

781 self._parent = None 

782 self._root = self 

783 elif not isinstance(parent, Operator): 

784 ex = TypeError("Parameter 'parent' is not an Operator.") 

785 ex.add_note(f"Got type '{getFullyQualifiedName(parent)}'.") 

786 raise ex 

787 else: 

788 self._parent = parent 

789 self._root = parent._root 

790 

791 @property 

792 def Parent(self) -> Nullable["Operator"]: 

793 """ 

794 Property to access the operator this expression is an operand of (:attr:`_parent`). 

795 

796 Assigning an operator records it as this node's parent and re-roots this node and everything below it to that 

797 operator's :attr:`Root`. The operator's operands are not changed by the assignment. 

798 

799 :returns: The parent operator, or ``None`` if this node is the root. 

800 :raises ValueError: If ``None`` is assigned. |br| 

801 A node that is an operand of an operator can't be detached from it. 

802 :raises TypeError: If an object that is not an :class:`Operator` is assigned. |br| 

803 A leaf is applied to nothing, so it can never be a parent. 

804 """ 

805 return self._parent 

806 

807 @Parent.setter 

808 def Parent(self, parent: Nullable["Operator"]) -> None: 

809 if parent is None: 

810 raise ValueError("Parameter 'parent' is None.") 

811 elif not isinstance(parent, Operator): 811 ↛ 812line 811 didn't jump to line 812 because the condition on line 811 was never true

812 ex = TypeError("Parameter 'parent' is not an Operator.") 

813 ex.add_note(f"Got type '{getFullyQualifiedName(parent)}'.") 

814 raise ex 

815 

816 self._parent = parent 

817 

818 for expression in self.IterateExpression(): 

819 expression._root = parent._root 

820 

821 @readonly 

822 def OriginalText(self) -> str: 

823 """ 

824 Read-only property to access the text this expression stands for (:attr:`_originalText`). 

825 

826 A tree is parsed from one string, so **every node answers with the root's** - reading it from a leaf gives the 

827 whole expression, not that leaf's fragment. :meth:`__str__` re-renders the tree canonically, which is not 

828 always what was written: ``Apache-2.0 or MIT`` parses and renders as ``Apache-2.0 OR MIT``. This is what was 

829 written. 

830 

831 :meth:`Parse` fills it in. A node **built** in code carries it when the caller knows what was stated and the 

832 expression can't be parsed from it - an :class:`UnknownLicense` standing for a license that didn't resolve 

833 keeps the text that didn't, and a :class:`ProprietaryLicense` keeps the classifier it was built from. 

834 

835 :returns: The text this expression stands for, or an empty string if there is none. 

836 """ 

837 return self._root._originalText 

838 

839 @readonly 

840 def Root(self) -> "LicenseExpression": 

841 """ 

842 Read-only property to access the outermost expression this node belongs to (:attr:`_root`). 

843 

844 The root is maintained by :attr:`Parent`: adopting a node re-roots it and everything below it, so the field 

845 can't fall behind the tree it describes. 

846 

847 :returns: The root of the expression tree, which is the node itself if it has no parent. 

848 """ 

849 return self._root 

850 

851 def IterateExpression(self) -> Generator["LicenseExpression", None, None]: 

852 """ 

853 Iterate this expression depth-first, in the order its nodes are written. 

854 

855 A leaf is the expression itself. The operators override this to yield their operands around themselves, so an 

856 infix operator comes between its two operands and the ``+`` suffix comes after the expression it applies to. 

857 

858 :returns: A generator of every node in this expression, this node included. 

859 """ 

860 yield self 

861 

862 @classmethod 

863 def Parse(cls, expression: str) -> "LicenseExpression": 

864 """ 

865 Parse an SPDX license expression into a tree of expression nodes. 

866 

867 Operator precedence is the one SPDX defines - ``+`` binds tighter than ``WITH``, which binds tighter than 

868 ``AND``, which binds tighter than ``OR`` - and parentheses override it. ``AND`` and ``OR`` associate to the 

869 left. 

870 

871 :param expression: The SPDX license expression to parse. 

872 :returns: The root of the parsed expression tree. 

873 :raises LicenseExpressionError: If the expression is empty, malformed, or names a license that isn't known. 

874 """ 

875 parser = _LicenseExpressionParser(expression) 

876 root = parser.Parse() 

877 root._originalText = expression 

878 

879 return root 

880 

881 @abstractmethod 

882 def __str__(self) -> str: # type: ignore[empty-body] 

883 """ 

884 Return this expression in SPDX syntax. 

885 

886 Parentheses are written only where the default precedence would otherwise read the expression differently, so 

887 a parsed expression renders back to its shortest correct form rather than a fully bracketed one. 

888 

889 :returns: The expression in SPDX syntax. 

890 """ 

891 

892 

893@export 

894@abstractclass 

895class Operator(LicenseExpression): 

896 """ 

897 Base-class of every expression node that is applied to operands. 

898 

899 Operator kinds: 

900 

901 * :class:`UnaryOperator` takes one operand 

902 * :class:`BinaryOperator` takes two operands. 

903 

904 Only an operator can be an operand's :attr:`~LicenseExpression.Parent`. 

905 """ 

906 

907 

908@export 

909@abstractclass 

910class BaseLicense(LicenseExpression): 

911 """ 

912 Base-class of every expression node that names a license the work is under. 

913 

914 Four nodes are one: 

915 

916 :class:`SPDXLicense` 

917 |rarr| a license on the SPDX License List, named by its identifier. 

918 :class:`LicenseReference` 

919 |rarr| a license that is not on that list, written as ``LicenseRef-<id>``. 

920 :class:`ProprietaryLicense` 

921 |rarr| a license that isn't published at all; a :class:`LicenseReference` with a fixed identifier. 

922 :class:`UnknownLicense` 

923 |rarr| SPDX's ``NONE`` or ``NOASSERTION`` - no license is named. 

924 

925 They have no common representation - SPDX knows the first as a :class:`License` object and the rest not at all - 

926 so this class is what lets them be collected and reported together: 

927 

928 .. code-block:: python 

929 

930 [term.Identifier for term in expression.IterateExpression() if isinstance(term, BaseLicense)] 

931 # ['MIT', 'LicenseRef-Proprietary'] for 'MIT AND LicenseRef-Proprietary' 

932 

933 :class:`LicenseException` is **not** one of these. The right operand of ``WITH`` is an exception granted from a 

934 license, not a license the work is under. 

935 """ 

936 

937 @readonly 

938 @abstractmethod 

939 def Identifier(self) -> str: # type: ignore[empty-body] 

940 """ 

941 Read-only property to return the identifier naming this license. 

942 

943 :returns: The SPDX identifier, or the ``LicenseRef-`` reference, in the spelling an expression writes it. 

944 """ 

945 

946 @readonly 

947 @abstractmethod 

948 def Name(self) -> str: # type: ignore[empty-body] 

949 """ 

950 Read-only property to return the license' name, as it is written for a reader. 

951 

952 :attr:`Identifier` is what an expression writes; this is what a document prints. For a license on the SPDX 

953 List the two differ - ``Apache-2.0`` against ``Apache License 2.0`` - and for the rest they are close or 

954 equal, because nothing else has a name SPDX publishes. 

955 

956 :returns: The license' name. 

957 """ 

958 

959 

960@export 

961class SPDXLicense(BaseLicense): 

962 """ 

963 A single license in an expression, named by its SPDX identifier. 

964 

965 The identifiers an expression may name are the `SPDX License List <https://spdx.org/licenses/>`__; the ones this 

966 package predefines are :data:`LICENSES`, and :attr:`License` reaches the :class:`License` object holding a 

967 license's name, approval flags and the URLs of its text. A license the list defines but this package doesn't 

968 predefine is a :class:`LicenseReference` when written as ``LicenseRef-``, and otherwise doesn't parse. 

969 """ 

970 

971 _license: _LicenseType #: The well-known license this node refers to. 

972 

973 def __init__(self, spdxLicense: _LicenseType, parent: Nullable[Operator] = None) -> None: 

974 """ 

975 Initialize a reference to an SPDX license. 

976 

977 :param spdxLicense: The license this node stands for. 

978 :param parent: Optional, the operator this node becomes an operand of. 

979 :raises TypeError: If parameter 'parent' is not of type :class:`Operator`. 

980 :raises ValueError: If parameter 'spdxLicense' is None. 

981 :raises TypeError: If parameter 'spdxLicense' is not of type :class:`License`. 

982 """ 

983 super().__init__(parent) 

984 

985 if spdxLicense is None: 985 ↛ 986line 985 didn't jump to line 986 because the condition on line 985 was never true

986 raise ValueError("Parameter 'spdxLicense' is None.") 

987 elif not isinstance(spdxLicense, _LicenseType): 

988 ex = TypeError("Parameter 'spdxLicense' is not a License.") 

989 ex.add_note(f"Got type '{getFullyQualifiedName(spdxLicense)}'.") 

990 raise ex 

991 

992 self._license = spdxLicense 

993 

994 @readonly 

995 def License(self) -> _LicenseType: 

996 """ 

997 Read-only property to access the license this node stands for (:attr:`_license`). 

998 

999 :returns: The license. 

1000 """ 

1001 return self._license 

1002 

1003 @readonly 

1004 def Identifier(self) -> str: 

1005 """ 

1006 Read-only property to access the license' SPDX identifier (:pycode:`_license._spdxIdentifier`). 

1007 

1008 :returns: The license's SPDX identifier. 

1009 """ 

1010 return self._license._spdxIdentifier 

1011 

1012 @readonly 

1013 def Name(self) -> str: 

1014 """ 

1015 Read-only property to access the license' name (:pycode:`_license._name`). 

1016 

1017 :returns: The name SPDX publishes for this license, like ``Apache License 2.0``. 

1018 """ 

1019 return self._license._name 

1020 

1021 def __str__(self) -> str: 

1022 """ 

1023 Return the license's SPDX identifier. 

1024 

1025 :returns: The SPDX identifier. 

1026 """ 

1027 return self._license.SPDXIdentifier 

1028 

1029 

1030@export 

1031class LicenseReference(BaseLicense): 

1032 """ 

1033 A license that isn't on the SPDX License List, written as ``LicenseRef-<id>``. 

1034 

1035 It may name the document it is defined in, as ``DocumentRef-<id>:LicenseRef-<id>``. 

1036 

1037 There is no predefined :class:`License` object, because SPDX doesn't know the license - only the document declaring it 

1038 does. 

1039 """ 

1040 

1041 _licenseIdentifier: str #: Identifier following ``LicenseRef-``. 

1042 _documentIdentifier: Nullable[str] #: Identifier following ``DocumentRef-``, if the reference names one. 

1043 

1044 def __init__( 

1045 self, 

1046 licenseIdentifier: str, 

1047 documentIdentifier: Nullable[str] = None, 

1048 parent: Nullable[Operator] = None 

1049 ) -> None: 

1050 """ 

1051 Initialize a license reference. 

1052 

1053 :param licenseIdentifier: Identifier following ``LicenseRef-``. 

1054 :param documentIdentifier: Optional, identifier following ``DocumentRef-``. 

1055 :param parent: Optional, the operator this node becomes an operand of. 

1056 :raises TypeError: If parameter 'parent' is not of type :class:`Operator`. 

1057 :raises ValueError: If parameter 'licenseIdentifier' is None. 

1058 :raises TypeError: If parameter 'licenseIdentifier' is not of type :class:`str`. 

1059 :raises ValueError: If parameter 'licenseIdentifier' is empty. 

1060 :raises TypeError: If parameter 'documentIdentifier' is not of type :class:`str`. 

1061 :raises ValueError: If parameter 'documentIdentifier' is empty. 

1062 """ 

1063 super().__init__(parent) 

1064 

1065 if licenseIdentifier is None: 1065 ↛ 1066line 1065 didn't jump to line 1066 because the condition on line 1065 was never true

1066 raise ValueError("Parameter 'licenseIdentifier' is None.") 

1067 elif not isinstance(licenseIdentifier, str): 

1068 ex = TypeError("Parameter 'licenseIdentifier' is not a string.") 

1069 ex.add_note(f"Got type '{getFullyQualifiedName(licenseIdentifier)}'.") 

1070 raise ex 

1071 elif len(licenseIdentifier) == 0: 

1072 raise ValueError("Parameter 'licenseIdentifier' is empty.") 

1073 

1074 if documentIdentifier is None: 

1075 pass 

1076 elif not isinstance(documentIdentifier, str): 

1077 ex = TypeError("Parameter 'documentIdentifier' is not a string.") 

1078 ex.add_note(f"Got type '{getFullyQualifiedName(documentIdentifier)}'.") 

1079 raise ex 

1080 elif len(documentIdentifier) == 0: 

1081 raise ValueError("Parameter 'documentIdentifier' is empty.") 

1082 

1083 self._licenseIdentifier = licenseIdentifier 

1084 self._documentIdentifier = documentIdentifier 

1085 

1086 @readonly 

1087 def LicenseIdentifier(self) -> str: 

1088 """ 

1089 Read-only property to access the identifier following ``LicenseRef-`` (:attr:`_licenseIdentifier`). 

1090 

1091 :returns: The license reference's identifier. 

1092 """ 

1093 return self._licenseIdentifier 

1094 

1095 @readonly 

1096 def DocumentIdentifier(self) -> Nullable[str]: 

1097 """ 

1098 Read-only property to access the identifier following ``DocumentRef-`` (:attr:`_documentIdentifier`). 

1099 

1100 :returns: The document reference's identifier, or ``None`` if the reference names no document. 

1101 """ 

1102 return self._documentIdentifier 

1103 

1104 @readonly 

1105 def Identifier(self) -> str: 

1106 """ 

1107 Read-only property to return the reference in the spelling an expression writes it. 

1108 

1109 This is the whole reference, ``DocumentRef-`` and all - :attr:`LicenseIdentifier` is the part following 

1110 ``LicenseRef-`` on its own. 

1111 

1112 :returns: The license reference. 

1113 """ 

1114 return str(self) 

1115 

1116 def __str__(self) -> str: 

1117 """ 

1118 Return the reference in SPDX syntax. 

1119 

1120 :returns: The license reference. 

1121 """ 

1122 document = "" if self._documentIdentifier is None else f"DocumentRef-{self._documentIdentifier}:" 

1123 return f"{document}LicenseRef-{self._licenseIdentifier}" 

1124 

1125 @readonly 

1126 def Name(self) -> str: 

1127 """ 

1128 Read-only property to access the identifier following ``LicenseRef-`` (:attr:`_licenseIdentifier`). 

1129 

1130 SPDX publishes no name for a license that isn't on its list, so the identifier the document chose is the 

1131 only name there is. 

1132 

1133 :returns: The license reference's identifier. 

1134 """ 

1135 return self._licenseIdentifier 

1136 

1137 

1138@export 

1139class ProprietaryLicense(LicenseReference): 

1140 """ 

1141 A license that is not open source and is not published under any identifier - an EULA, or a company's own terms. 

1142 

1143 SPDX has no way to say this. Its list is a list of *published* licenses, and a proprietary one is by definition 

1144 not on it, so the only thing SPDX offers is the generic escape hatch: this renders as ``LicenseRef-Proprietary`` 

1145 and **is** a :class:`LicenseReference`, because that is what it writes. 

1146 

1147 .. code-block:: python 

1148 

1149 str(ProprietaryLicense()) # 'LicenseRef-Proprietary' 

1150 

1151 The round-trip is deliberately one-way: parsing ``LicenseRef-Proprietary`` back gives a plain 

1152 :class:`LicenseReference`, because SPDX defines no convention that makes that identifier mean *proprietary* 

1153 rather than being one project's choice of words. This class is constructed where something else already knows - 

1154 :mod:`pyTooling.Dependency` builds one from PyPI's ``License :: Other/Proprietary License`` classifier. 

1155 

1156 A proprietary license that *does* have a name of its own is a :class:`LicenseReference` with that name, not this 

1157 class - ``LicenseReference("AcmeEULA-1.0")`` renders as ``LicenseRef-AcmeEULA-1.0``. 

1158 """ 

1159 

1160 #: The identifier a proprietary license is written with, following ``LicenseRef-``. 

1161 IDENTIFIER: ClassVar[str] = "Proprietary" 

1162 

1163 def __init__(self, parent: Nullable[Operator] = None, originalText: str = "") -> None: 

1164 """ 

1165 Initialize a proprietary license. 

1166 

1167 :param parent: Optional, the operator this node becomes an operand of. 

1168 :param originalText: Optional, what stated this - a classifier, say - since it can't have been parsed. 

1169 :raises TypeError: If parameter 'parent' is not of type :class:`Operator`. 

1170 """ 

1171 super().__init__(self.IDENTIFIER, None, parent) 

1172 

1173 self._originalText = originalText 

1174 

1175 @readonly 

1176 def Name(self) -> str: 

1177 """ 

1178 Read-only property to return a readable name for a license that publishes none. 

1179 

1180 :returns: ``Proprietary License``. 

1181 """ 

1182 return "Proprietary License" 

1183 

1184 

1185@export 

1186class UnknownLicense(BaseLicense): 

1187 """ 

1188 SPDX's statement that no license is named: ``NONE`` or ``NOASSERTION``. 

1189 

1190 The two are different claims - :attr:`LicenseAbsence.NoLicense` says the work states that no license applies, 

1191 :attr:`LicenseAbsence.NoAssertion` says someone looked and declined to say - so which one is asked with 

1192 :attr:`Absence`. 

1193 

1194 .. code-block:: python 

1195 

1196 LicenseExpression.Parse("NOASSERTION") # UnknownLicense(LicenseAbsence.NoAssertion) 

1197 LicenseExpression.Parse("NONE") # UnknownLicense(LicenseAbsence.NoLicense) 

1198 

1199 **Neither can be an operand.** SPDX's grammar is ``simple-expression | compound-expression``; ``NONE`` and 

1200 ``NOASSERTION`` are values a license *field* may hold instead of an expression, not terms inside one. So 

1201 ``MIT AND NOASSERTION`` doesn't parse, and this node refuses a :attr:`~LicenseExpression.Parent`. 

1202 

1203 .. seealso:: 

1204 

1205 :class:`ProprietaryLicense` 

1206 |rarr| For a license that exists but isn't published - which is a different statement from this one. 

1207 """ 

1208 

1209 _absence: LicenseAbsence #: Which of SPDX's two absences this node states. 

1210 

1211 def __init__(self, absence: LicenseAbsence = LicenseAbsence.NoAssertion, originalText: str = "") -> None: 

1212 """ 

1213 Initialize an absent license. 

1214 

1215 No ``parent`` parameter, because neither value may be an operand. 

1216 

1217 :param absence: Optional, which absence is stated. Defaults to :attr:`LicenseAbsence.NoAssertion`. 

1218 :param originalText: Optional, what was stated. A license nothing could be made of keeps the text here, which 

1219 is the only place left holding it. 

1220 :raises TypeError: If parameter 'absence' is not of type :class:`LicenseAbsence`. 

1221 """ 

1222 super().__init__(None, originalText) 

1223 

1224 if not isinstance(absence, LicenseAbsence): 

1225 ex = TypeError("Parameter 'absence' is not a LicenseAbsence.") 

1226 ex.add_note(f"Got type '{getFullyQualifiedName(absence)}'.") 

1227 raise ex 

1228 

1229 self._absence = absence 

1230 

1231 @readonly 

1232 def Absence(self) -> LicenseAbsence: 

1233 """ 

1234 Read-only property to access which absence this node states (:attr:`_absence`). 

1235 

1236 :returns: The absence stated. 

1237 """ 

1238 return self._absence 

1239 

1240 @readonly 

1241 def Identifier(self) -> str: 

1242 """ 

1243 Read-only property to access the absence in SPDX's spelling (:pycode:`_absence.value`). 

1244 

1245 :returns: ``NONE`` or ``NOASSERTION``. 

1246 """ 

1247 return self._absence.value 

1248 

1249 @readonly 

1250 def Name(self) -> str: 

1251 """ 

1252 Read-only property to return which absence this states, in words. 

1253 

1254 :returns: ``No license`` for :attr:`~LicenseAbsence.NoLicense`, ``No assertion`` otherwise. 

1255 """ 

1256 return "No license" if self._absence is LicenseAbsence.NoLicense else "No assertion" 

1257 

1258 @readonly 

1259 def Parent(self) -> Nullable[Operator]: 

1260 """ 

1261 Read-only property to access the operator this expression is an operand of (:attr:`_parent`). 

1262 

1263 Always ``None``, and **read-only** where every other node's is assignable: ``NONE`` and ``NOASSERTION`` are 

1264 values a license field may hold *instead of* an expression, and SPDX's grammar has no place for either 

1265 inside one. So this node is always the whole expression, and assigning a parent raises 

1266 :exc:`AttributeError`. 

1267 

1268 :returns: ``None``, always. 

1269 """ 

1270 return self._parent 

1271 

1272 def __str__(self) -> str: 

1273 """ 

1274 Return the absence in SPDX's spelling. 

1275 

1276 :returns: ``NONE`` or ``NOASSERTION``. 

1277 """ 

1278 return self._absence.value 

1279 

1280 

1281@export 

1282class LicenseException(LicenseExpression): 

1283 """ 

1284 The right operand of a :class:`WithOperator`, naming an exception from the SPDX exception list. 

1285 

1286 It is a leaf node in the expression tree like a license, but it is not one. An exception modifies a license and can't 

1287 stand on its own. 

1288 """ 

1289 

1290 _identifier: str #: The exception's SPDX identifier. 

1291 

1292 def __init__(self, identifier: str, parent: Nullable[Operator] = None) -> None: 

1293 """ 

1294 Initialize a license exception. 

1295 

1296 :param identifier: The exception's SPDX identifier. 

1297 :param parent: Optional, the operator this node becomes an operand of. 

1298 :raises TypeError: If parameter 'parent' is not of type :class:`Operator`. 

1299 :raises ValueError: If parameter 'identifier' is None. 

1300 :raises TypeError: If parameter 'identifier' is not of type :class:`str`. 

1301 :raises ValueError: If parameter 'identifier' is empty. 

1302 """ 

1303 super().__init__(parent) 

1304 

1305 if identifier is None: 1305 ↛ 1306line 1305 didn't jump to line 1306 because the condition on line 1305 was never true

1306 raise ValueError("Parameter 'identifier' is None.") 

1307 elif not isinstance(identifier, str): 

1308 ex = TypeError("Parameter 'identifier' is not a string.") 

1309 ex.add_note(f"Got type '{getFullyQualifiedName(identifier)}'.") 

1310 raise ex 

1311 elif len(identifier) == 0: 

1312 raise ValueError("Parameter 'identifier' is empty.") 

1313 

1314 self._identifier = identifier 

1315 

1316 @readonly 

1317 def Identifier(self) -> str: 

1318 """ 

1319 Read-only property to access the exception's SPDX identifier (:attr:`_identifier`). 

1320 

1321 :returns: The exception's identifier. 

1322 """ 

1323 return self._identifier 

1324 

1325 def __str__(self) -> str: 

1326 """ 

1327 Return the exception's SPDX identifier. 

1328 

1329 :returns: The identifier. 

1330 """ 

1331 return self._identifier 

1332 

1333 

1334@export 

1335@abstractclass 

1336class UnaryOperator(Operator): 

1337 """ 

1338 Base-class of the expression operators taking one operand. 

1339 

1340 SPDX defines exactly one unary operator: 

1341 

1342 * the ``+`` suffix of :class:`OrLaterOperator`. 

1343 

1344 The operand is reachable as :attr:`Operand` and is assignable, so an operator can be filled after it was created. 

1345 """ 

1346 

1347 _operand: Nullable[LicenseExpression] #: The expression this operator is applied to. 

1348 

1349 def __init__( 

1350 self, 

1351 operand: Nullable[LicenseExpression] = None, 

1352 parent: Nullable[Operator] = None 

1353 ) -> None: 

1354 """ 

1355 Initialize a unary operator with its operand. 

1356 

1357 :param operand: Optional, the expression this operator is applied to. 

1358 :param parent: Optional, the operator this node becomes an operand of. 

1359 :raises TypeError: If parameter 'parent' is not of type :class:`Operator`. 

1360 :raises TypeError: If parameter 'operand' is not of type :class:`LicenseExpression`. 

1361 """ 

1362 super().__init__(parent) 

1363 

1364 if operand is not None: 

1365 self._operand = operand 

1366 

1367 if not isinstance(operand, LicenseExpression): 

1368 ex = TypeError("Parameter 'operand' is not a LicenseExpression.") 

1369 ex.add_note(f"Got type '{getFullyQualifiedName(operand)}'.") 

1370 raise ex 

1371 

1372 operand.Parent = self 

1373 else: 

1374 self._operand = None 

1375 

1376 def IterateExpression(self) -> Generator[LicenseExpression, None, None]: 

1377 """ 

1378 Iterate this operator depth-first, its operand first, because ``+`` is written after the expression it suffixes. 

1379 

1380 :returns: A generator of every node in this expression, this operator included. 

1381 """ 

1382 if self._operand is not None: 1382 ↛ 1385line 1382 didn't jump to line 1385 because the condition on line 1382 was always true

1383 yield from self._operand.IterateExpression() 

1384 

1385 yield self 

1386 

1387 @property 

1388 def Operand(self) -> Nullable[LicenseExpression]: 

1389 """ 

1390 Property to access the expression this operator is applied to (:attr:`_operand`). 

1391 

1392 A slot is filled once. Assigning an expression makes this operator its :attr:`~LicenseExpression.Parent`, 

1393 which puts it - and everything below it - into this tree. 

1394 

1395 :returns: The operand, or ``None`` if the operator wasn't assigned yet. 

1396 :raises ValueError: If ``None`` is assigned. 

1397 :raises TypeError: If an object that is not a :class:`LicenseExpression` is assigned. 

1398 :raises LicensingError: If this operator has an operand already. 

1399 :raises LicensingError: If the assigned expression is already an operand of another operator. 

1400 """ 

1401 return self._operand 

1402 

1403 @Operand.setter 

1404 def Operand(self, operand: LicenseExpression) -> None: 

1405 if operand is None: 1405 ↛ 1406line 1405 didn't jump to line 1406 because the condition on line 1405 was never true

1406 raise ValueError("Parameter 'operand' is None.") 

1407 elif not isinstance(operand, LicenseExpression): 1407 ↛ 1408line 1407 didn't jump to line 1408 because the condition on line 1407 was never true

1408 ex = TypeError("Parameter 'operand' is not a LicenseExpression.") 

1409 ex.add_note(f"Got type '{getFullyQualifiedName(operand)}'.") 

1410 raise ex 

1411 elif self._operand is not None: 

1412 raise LicensingError(f"Operator '{getFullyQualifiedName(self)}' has an operand already.") 

1413 elif operand._parent is not None: 1413 ↛ 1414line 1413 didn't jump to line 1414 because the condition on line 1413 was never true

1414 raise LicensingError("Parameter 'operand' is already an operand of another operator.") 

1415 

1416 self._operand = operand 

1417 operand.Parent = self 

1418 

1419 

1420@export 

1421class OrLaterOperator(UnaryOperator): 

1422 """ 

1423 The ``+`` suffix, as in ``GPL-2.0+``: the named license *or any later version of it*. 

1424 

1425 .. attention:: 

1426 

1427 The operator itself is not deprecated - Annex D.3 of the SPDX specification defines it and uses ``CDDL-1.0+`` 

1428 as its example. Its combination with the GNU licenses is: `SPDX License List 3.0 

1429 <https://spdx.org/licenses/>`__ (2017-12-28) introduced ``GPL-2.0-only`` and ``GPL-2.0-or-later`` and 

1430 deprecated the ``GPL-2.0``, ``LGPL-2.1`` and ``AGPL-3.0`` identifiers that ``+`` was applied to. Published 

1431 metadata still contains ``GPL-2.0+``, so an expression using it has to parse. 

1432 """ 

1433 

1434 PRECEDENCE: ClassVar[int] = 1 #: Binds tighter than every binary operator, looser than a bare license. 

1435 

1436 def __str__(self) -> str: 

1437 """ 

1438 Return the operand followed by ``+``. 

1439 

1440 :returns: The expression in SPDX syntax. 

1441 :raises LicensingError: If the operator has no operand yet. 

1442 """ 

1443 if self._operand is None: 

1444 raise LicensingError(f"Operator '{getFullyQualifiedName(self)}' has no operand yet.") 

1445 

1446 return f"{self._operand}+" 

1447 

1448 

1449@export 

1450@abstractclass 

1451class BinaryOperator(Operator): 

1452 """ 

1453 Base-class of the expression operators taking two operands. 

1454 

1455 SPDX defines three binary operators: 

1456 

1457 * :class:`WithOperator`, 

1458 * :class:`AndOperator` and 

1459 * :class:`OrOperator`. 

1460 

1461 The operands are reachable as :attr:`Left` and :attr:`Right`. :attr:`KEYWORD` is the operator keyword between the 

1462 operands. 

1463 """ 

1464 

1465 KEYWORD: ClassVar[str] #: The operator's keyword, as it is written between the operands. 

1466 

1467 _left: Nullable[LicenseExpression] #: The operator's left operand. 

1468 _right: Nullable[LicenseExpression] #: The operator's right operand. 

1469 

1470 def __init__( 

1471 self, 

1472 left: Nullable[LicenseExpression] = None, 

1473 right: Nullable[LicenseExpression] = None, 

1474 parent: Nullable[Operator] = None 

1475 ) -> None: 

1476 """ 

1477 Initialize a binary operator with both operands. 

1478 

1479 :param left: Optional, the operator's left operand. 

1480 :param right: Optional, the operator's right operand. 

1481 :param parent: Optional, the operator this node becomes an operand of. 

1482 :raises TypeError: If parameter 'parent' is not of type :class:`Operator`. 

1483 :raises TypeError: If parameter 'left' is not of type :class:`LicenseExpression`. 

1484 :raises TypeError: If parameter 'right' is not of type :class:`LicenseExpression`. 

1485 """ 

1486 super().__init__(parent) 

1487 

1488 if left is not None: 

1489 self._left = left 

1490 

1491 if not isinstance(left, LicenseExpression): 

1492 ex = TypeError("Parameter 'left' is not a LicenseExpression.") 

1493 ex.add_note(f"Got type '{getFullyQualifiedName(left)}'.") 

1494 raise ex 

1495 

1496 left.Parent = self 

1497 else: 

1498 self._left = None 

1499 

1500 if right is not None: 

1501 self._right = right 

1502 

1503 if not isinstance(right, LicenseExpression): 

1504 ex = TypeError("Parameter 'right' is not a LicenseExpression.") 

1505 ex.add_note(f"Got type '{getFullyQualifiedName(right)}'.") 

1506 raise ex 

1507 

1508 right.Parent = self 

1509 else: 

1510 self._right = None 

1511 

1512 def IterateExpression(self) -> Generator[LicenseExpression, None, None]: 

1513 """ 

1514 Iterate this operator depth-first in infix order: left operand, this operator, right operand. 

1515 

1516 :returns: A generator of every node in this expression, this operator included. 

1517 """ 

1518 if self._left is not None: 1518 ↛ 1521line 1518 didn't jump to line 1521 because the condition on line 1518 was always true

1519 yield from self._left.IterateExpression() 

1520 

1521 yield self 

1522 

1523 if self._right is not None: 

1524 yield from self._right.IterateExpression() 

1525 

1526 @property 

1527 def Left(self) -> Nullable[LicenseExpression]: 

1528 """ 

1529 Property to access the operator's left operand (:attr:`_left`). 

1530 

1531 A slot is filled once. Assigning an expression makes this operator its :attr:`~LicenseExpression.Parent`, 

1532 which puts it - and everything below it - into this tree. 

1533 

1534 :returns: The left operand, or ``None`` if it wasn't assigned yet. 

1535 :raises ValueError: If ``None`` is assigned. 

1536 :raises TypeError: If an object that is not a :class:`LicenseExpression` is assigned. 

1537 :raises LicensingError: If this operator has a left operand already. 

1538 :raises LicensingError: If the assigned expression is already an operand of another operator. 

1539 """ 

1540 return self._left 

1541 

1542 @Left.setter 

1543 def Left(self, operand: LicenseExpression) -> None: 

1544 if operand is None: 1544 ↛ 1545line 1544 didn't jump to line 1545 because the condition on line 1544 was never true

1545 raise ValueError("Parameter 'operand' is None.") 

1546 elif not isinstance(operand, LicenseExpression): 1546 ↛ 1547line 1546 didn't jump to line 1547 because the condition on line 1546 was never true

1547 ex = TypeError("Parameter 'operand' is not a LicenseExpression.") 

1548 ex.add_note(f"Got type '{getFullyQualifiedName(operand)}'.") 

1549 raise ex 

1550 elif self._left is not None: 

1551 raise LicensingError(f"Operator '{getFullyQualifiedName(self)}' has a left operand already.") 

1552 elif operand._parent is not None: 

1553 raise LicensingError("Parameter 'operand' is already an operand of another operator.") 

1554 

1555 self._left = operand 

1556 operand.Parent = self 

1557 

1558 @property 

1559 def Right(self) -> Nullable[LicenseExpression]: 

1560 """ 

1561 Property to access the operator's right operand (:attr:`_right`). 

1562 

1563 A slot is filled once. Assigning an expression makes this operator its :attr:`~LicenseExpression.Parent`, 

1564 which puts it - and everything below it - into this tree. 

1565 

1566 :returns: The right operand, or ``None`` if it wasn't assigned yet. 

1567 :raises ValueError: If ``None`` is assigned. 

1568 :raises TypeError: If an object that is not a :class:`LicenseExpression` is assigned. 

1569 :raises LicensingError: If this operator has a right operand already. 

1570 :raises LicensingError: If the assigned expression is already an operand of another operator. 

1571 """ 

1572 return self._right 

1573 

1574 @Right.setter 

1575 def Right(self, operand: LicenseExpression) -> None: 

1576 if operand is None: 1576 ↛ 1577line 1576 didn't jump to line 1577 because the condition on line 1576 was never true

1577 raise ValueError("Parameter 'operand' is None.") 

1578 elif not isinstance(operand, LicenseExpression): 1578 ↛ 1579line 1578 didn't jump to line 1579 because the condition on line 1578 was never true

1579 ex = TypeError("Parameter 'operand' is not a LicenseExpression.") 

1580 ex.add_note(f"Got type '{getFullyQualifiedName(operand)}'.") 

1581 raise ex 

1582 elif self._right is not None: 1582 ↛ 1583line 1582 didn't jump to line 1583 because the condition on line 1582 was never true

1583 raise LicensingError(f"Operator '{getFullyQualifiedName(self)}' has a right operand already.") 

1584 elif operand._parent is not None: 1584 ↛ 1585line 1584 didn't jump to line 1585 because the condition on line 1584 was never true

1585 raise LicensingError("Parameter 'operand' is already an operand of another operator.") 

1586 

1587 self._right = operand 

1588 operand.Parent = self 

1589 

1590 def __str__(self) -> str: 

1591 """ 

1592 Return both operands with the operator's keyword between them. 

1593 

1594 :returns: The expression in SPDX syntax. 

1595 :raises LicensingError: If one of the operator's operands wasn't assigned yet. 

1596 """ 

1597 if self._left is None: 

1598 raise LicensingError(f"Operator '{getFullyQualifiedName(self)}' has no left operand yet.") 

1599 elif self._right is None: 

1600 raise LicensingError(f"Operator '{getFullyQualifiedName(self)}' has no right operand yet.") 

1601 

1602 left = f"({self._left})" if self._left.PRECEDENCE > self.PRECEDENCE else f"{self._left}" 

1603 right = f"({self._right})" if self._right.PRECEDENCE > self.PRECEDENCE else f"{self._right}" 

1604 

1605 return f"{left} {self.KEYWORD} {right}" 

1606 

1607 

1608@export 

1609class WithOperator(BinaryOperator): 

1610 """ 

1611 ``WITH``, as in ``Apache-2.0 WITH LLVM-exception``: a license together with an exception to it. 

1612 

1613 It is the only operator whose operands differ in kind - the right one is a :class:`LicenseException`, never a 

1614 license - and it binds tighter than ``AND`` and ``OR``. 

1615 """ 

1616 

1617 PRECEDENCE: ClassVar[int] = 2 #: Binds tighter than ``AND`` and ``OR``. 

1618 KEYWORD: ClassVar[str] = "WITH" #: The operator's keyword. 

1619 

1620 

1621@export 

1622class AndOperator(BinaryOperator): 

1623 """``AND``, as in ``Apache-2.0 AND MIT``: **both** licenses apply, and both have to be complied with.""" 

1624 

1625 PRECEDENCE: ClassVar[int] = 3 #: Binds tighter than ``OR``. 

1626 KEYWORD: ClassVar[str] = "AND" #: The operator's keyword. 

1627 

1628 

1629@export 

1630class OrOperator(BinaryOperator): 

1631 """ 

1632 ``OR``, as in ``Apache-2.0 OR BSD-2-Clause``: **either** license applies, and the recipient chooses which. 

1633 

1634 Which one they chose is not something the expression records. 

1635 """ 

1636 

1637 PRECEDENCE: ClassVar[int] = 4 #: Binds loosest of all operators. 

1638 KEYWORD: ClassVar[str] = "OR" #: The operator's keyword. 

1639 

1640 

1641class _LicenseExpressionParser(metaclass=ExtendedType, slots=True): 

1642 """ 

1643 Recursive-descent parser for SPDX license expressions. 

1644 

1645 One level of the descent per precedence level, lowest-binding first, which is what makes ``A OR B AND C`` parse as 

1646 ``A OR (B AND C)`` without the grammar having to say so twice. The descent is a class rather than a function 

1647 because every level reads and advances the same token position, and that position is state the levels share. 

1648 """ 

1649 

1650 _TOKEN = re_compile(r"\(|\)|[^\s()]+") #: Splits an expression into parentheses and the words between them. 

1651 

1652 _expression: str #: The expression being parsed, kept for the error messages. 

1653 _tokens: list[str] #: The expression's tokens, in order. 

1654 _position: int #: Index of the token to read next. 

1655 

1656 def __init__(self, expression: str) -> None: 

1657 """ 

1658 Tokenize an expression. 

1659 

1660 :param expression: The SPDX license expression to parse. 

1661 :raises TypeError: If parameter 'expression' is not of type :class:`str`. 

1662 :raises ValueError: If parameter 'expression' is empty. 

1663 :raises LicenseExpressionError: If the expression holds nothing but whitespace. 

1664 """ 

1665 if not isinstance(expression, str): 

1666 ex = TypeError("Parameter 'expression' is not a string.") 

1667 ex.add_note(f"Got type '{getFullyQualifiedName(expression)}'.") 

1668 raise ex 

1669 elif expression == "": 

1670 raise ValueError("Parameter 'expression' is empty.") 

1671 

1672 self._expression = expression 

1673 self._tokens = self._TOKEN.findall(expression) 

1674 self._position = 0 

1675 

1676 if len(self._tokens) == 0: 

1677 raise LicenseExpressionError(f"License expression '{expression}' is empty.", expression=expression) 

1678 

1679 def Parse(self) -> LicenseExpression: 

1680 """ 

1681 Parse the whole expression. 

1682 

1683 ``NONE`` and ``NOASSERTION`` are read here rather than in the descent, because SPDX allows them **only** as 

1684 the entire expression - they are values a license field may hold instead of one, not terms inside one. Seeing 

1685 either anywhere else leaves it to the descent, where it is an unknown license identifier. 

1686 

1687 :returns: The root of the expression tree. 

1688 :raises LicenseExpressionError: If the expression is malformed or names an unknown license. 

1689 """ 

1690 if len(self._tokens) == 1: 

1691 for absence in LicenseAbsence: 

1692 if self._tokens[0] == absence.value: 

1693 self._position = 1 

1694 return UnknownLicense(absence) 

1695 

1696 result = self._ParseOr() 

1697 

1698 if self._position < len(self._tokens): 

1699 raise LicenseExpressionError( 

1700 f"License expression '{self._expression}' has trailing input at '{self._tokens[self._position]}'.", 

1701 expression=self._expression 

1702 ) 

1703 

1704 return result 

1705 

1706 def _ParseOr(self) -> LicenseExpression: 

1707 """ 

1708 Parse a sequence of ``OR`` operands, the loosest-binding operator. 

1709 

1710 :returns: The parsed expression. 

1711 """ 

1712 left = self._ParseAnd() 

1713 while self._Accept(OrOperator.KEYWORD): 

1714 left = OrOperator(left, self._ParseAnd()) 

1715 

1716 return left 

1717 

1718 def _ParseAnd(self) -> LicenseExpression: 

1719 """ 

1720 Parse a sequence of ``AND`` operands. 

1721 

1722 :returns: The parsed expression. 

1723 """ 

1724 left = self._ParseWith() 

1725 while self._Accept(AndOperator.KEYWORD): 

1726 left = AndOperator(left, self._ParseWith()) 

1727 

1728 return left 

1729 

1730 def _ParseWith(self) -> LicenseExpression: 

1731 """ 

1732 Parse a ``WITH`` clause, whose right operand is an exception rather than a license. 

1733 

1734 :returns: The parsed expression. 

1735 :raises LicenseExpressionError: If ``WITH`` isn't followed by an exception identifier. 

1736 """ 

1737 left = self._ParseSimple() 

1738 if self._Accept(WithOperator.KEYWORD): 

1739 if (identifier := self._Next()) is None: 

1740 raise LicenseExpressionError( 

1741 f"License expression '{self._expression}' ends after 'WITH'.", 

1742 expression=self._expression 

1743 ) 

1744 

1745 left = WithOperator(left, LicenseException(identifier)) 

1746 

1747 return left 

1748 

1749 def _ParseSimple(self) -> LicenseExpression: 

1750 """ 

1751 Parse a parenthesized expression, a license reference, or a license identifier with an optional ``+``. 

1752 

1753 :returns: The parsed expression. 

1754 :raises LicenseExpressionError: If the expression ends early, a parenthesis is unbalanced, or a license is unknown. 

1755 """ 

1756 if (token := self._Next()) is None: 

1757 raise LicenseExpressionError( 

1758 f"License expression '{self._expression}' ends unexpectedly.", 

1759 expression=self._expression 

1760 ) 

1761 

1762 if token == "(": 

1763 inner = self._ParseOr() 

1764 if not self._Accept(")"): 

1765 raise LicenseExpressionError( 

1766 f"License expression '{self._expression}' is missing a closing parenthesis.", 

1767 expression=self._expression 

1768 ) 

1769 

1770 return inner 

1771 

1772 if token == ")": 

1773 raise LicenseExpressionError( 

1774 f"License expression '{self._expression}' has an unmatched closing parenthesis.", 

1775 expression=self._expression 

1776 ) 

1777 

1778 # 'GPL-2.0+' is the deprecated spelling of 'GPL-2.0-or-later' and is still legal grammar 

1779 orLater = token.endswith("+") 

1780 identifier = token[:-1] if orLater else token 

1781 

1782 if identifier.startswith("LicenseRef-"): 

1783 expression: LicenseExpression = LicenseReference(identifier[len("LicenseRef-"):]) 

1784 elif identifier.startswith("DocumentRef-") and ":LicenseRef-" in identifier: 

1785 documentIdentifier, _, licenseIdentifier = identifier.partition(":LicenseRef-") 

1786 expression = LicenseReference(licenseIdentifier, documentIdentifier[len("DocumentRef-"):]) 

1787 elif (spdxLicense := SPDX_INDEX.get(identifier, None)) is not None: 

1788 expression = SPDXLicense(spdxLicense) 

1789 else: 

1790 ex = LicenseExpressionError( 

1791 f"License expression '{self._expression}' names unknown license '{identifier}'.", 

1792 expression=self._expression, 

1793 identifier=identifier 

1794 ) 

1795 

1796 if any(identifier == absence.value for absence in LicenseAbsence): 

1797 ex.add_note(f"'{identifier}' is a value a license field may hold instead of an expression.") 

1798 ex.add_note("SPDX's grammar has no place for it inside one, so it may only stand alone.") 

1799 else: 

1800 ex.add_note("Known licenses are the SPDX identifiers in 'pyTooling.Licensing.SPDX_INDEX'.") 

1801 ex.add_note("A license that isn't on that list is written 'LicenseRef-<id>'.") 

1802 

1803 raise ex 

1804 

1805 return OrLaterOperator(expression) if orLater else expression 

1806 

1807 def _Next(self) -> Nullable[str]: 

1808 """ 

1809 Consume and return the next token. 

1810 

1811 :returns: The next token, or ``None`` if the expression is exhausted. 

1812 """ 

1813 if self._position >= len(self._tokens): 

1814 return None 

1815 

1816 self._position += 1 

1817 

1818 return self._tokens[self._position - 1] 

1819 

1820 def _Accept(self, keyword: str) -> bool: 

1821 """ 

1822 Consume the next token if it is the given keyword. 

1823 

1824 Keywords are matched case-insensitively, because published metadata writes ``and`` as often as ``AND``. 

1825 

1826 :param keyword: The keyword to look for. 

1827 :returns: ``True``, if the keyword was there and was consumed. 

1828 """ 

1829 if self._position < len(self._tokens) and self._tokens[self._position].upper() == keyword: 

1830 self._position += 1 

1831 

1832 return True 

1833 

1834 return False