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

512 statements  

« prev     ^ index     » next       coverage.py v7.16.2, created at 2026-10-03 23:02 +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 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 

110@export 

111@unique 

112class LicenseAbsence(Enum): 

113 """ 

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

115 

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

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

118 """ 

119 

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

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

122 

123 def __str__(self) -> str: 

124 """ 

125 Return the value in SPDX's spelling. 

126 

127 :returns: ``NONE`` or ``NOASSERTION``. 

128 """ 

129 return self.value 

130 

131 

132@export 

133@dataclass 

134class PythonLicenseName: 

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

136 

137 ShortName: str #: License's short name 

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

139 

140 def __str__(self) -> str: 

141 """ 

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

143 

144 :returns: Short name of the license. 

145 """ 

146 return self.ShortName 

147 

148 

149#: Mapping of SPDX identifiers to Python license names 

150PYTHON_LICENSE_NAMES: dict[str, PythonLicenseName] = { 

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

176} 

177 

178 

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

180#: 

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

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

183#: :attr:`License.OSIURL` and isn't repeated here. ``MIT``, ``BSD-2-Clause`` and ``BSD-3-Clause`` are the three. 

184LICENSE_URLS: dict[str, str] = { 

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

209} 

210 

211 

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

213#: 

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

215#: to its licensor: the GNU licenses publish four formats, most publish one, and several publish none at all beyond 

216#: an HTML page, which is :data:`LICENSE_URLS`. 

217#: 

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

233 "GPL-2.0-only": {"txt": "https://www.gnu.org/licenses/old-licenses/gpl-2.0.txt"}, 

234 "GPL-2.0-or-later": {"txt": "https://www.gnu.org/licenses/old-licenses/gpl-2.0.txt"}, 

235 "LGPL-2.1-only": {"tex": "https://www.gnu.org/licenses/old-licenses/lgpl-2.1.tex"}, 

236 "LGPL-2.1-or-later": {"tex": "https://www.gnu.org/licenses/old-licenses/lgpl-2.1.tex"}, 

237 "GPL-3.0-only": { 

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

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

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

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

242 }, 

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

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

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

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

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

248 }, 

249 "AGPL-3.0-only": {"txt": "https://www.gnu.org/licenses/agpl-3.0.txt"}, 

250 "AGPL-3.0-or-later": {"txt": "https://www.gnu.org/licenses/agpl-3.0.txt"}, 

251} 

252 

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

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

255#: 

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

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

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

259#: 

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

261OSI_LICENSE_URLS: dict[str, str] = { 

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

286} 

287 

288 

289@export 

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

291 """Representation of a license.""" 

292 

293 _spdxIdentifier: str #: Unique SPDX identifier. 

294 _name: str #: Name of the license. 

295 _osiApproved: bool #: OSI approval status 

296 _fsfApproved: bool #: FSF approval status 

297 

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

299 """ 

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

301 

302 :param spdxIdentifier: SPDX identifier of the license. 

303 :param name: Name of the license. 

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

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

306 """ 

307 self._spdxIdentifier = spdxIdentifier 

308 self._name = name 

309 self._osiApproved = osiApproved 

310 self._fsfApproved = fsfApproved 

311 

312 @readonly 

313 def Name(self) -> str: 

314 """ 

315 Returns the license' name. 

316 

317 :returns: License name. 

318 """ 

319 return self._name 

320 

321 @readonly 

322 def SPDXIdentifier(self) -> str: 

323 """ 

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

325 

326 :returns: The unique SPDX identifier. 

327 """ 

328 return self._spdxIdentifier 

329 

330 @readonly 

331 def SPDXURL(self) -> str: 

332 """ 

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

334 

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

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

337 and the derived URL won't resolve. 

338 

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

340 """ 

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

342 

343 @readonly 

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

345 """ 

346 Returns the page where the licensor publishes this license. 

347 

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

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

350 the two catalogue entries describing it. 

351 

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

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

354 

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

356 

357 .. seealso:: 

358 

359 :attr:`TextURLs` 

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

361 """ 

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

363 

364 @readonly 

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

366 """ 

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

368 

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

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

371 

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

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

374 

375 .. code-block:: python 

376 

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

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

379 

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

381 

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

383 """ 

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

385 

386 @readonly 

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

388 """ 

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

390 

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

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

393 share it. 

394 

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

396 

397 .. seealso:: 

398 

399 :attr:`OSIApproved` 

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

401 """ 

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

403 

404 @readonly 

405 def OSIApproved(self) -> bool: 

406 """ 

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

408 

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

410 """ 

411 return self._osiApproved 

412 

413 @readonly 

414 def FSFApproved(self) -> bool: 

415 """ 

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

417 

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

419 """ 

420 return self._fsfApproved 

421 

422 @readonly 

423 def PythonLicenseName(self) -> str: 

424 """ 

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

426 

427 :returns: The Python license name. 

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

429 """ 

430 try: 

431 item: PythonLicenseName = PYTHON_LICENSE_NAMES[self._spdxIdentifier] 

432 except KeyError as ex: 

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

434 

435 return item.ShortName 

436 

437 @readonly 

438 def PythonClassifier(self) -> str: 

439 """ 

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

441 

442 :returns: The Python package classifier. 

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

444 

445 .. seealso:: 

446 

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

448 """ 

449 try: 

450 item: PythonLicenseName = PYTHON_LICENSE_NAMES[self._spdxIdentifier] 

451 except KeyError as ex: 

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

453 

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

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

456 

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

458 """ 

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

460 

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

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

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

464 """ 

465 if isinstance(other, License): 

466 return self._spdxIdentifier == other._spdxIdentifier 

467 elif isinstance(other, str): 

468 return self._spdxIdentifier == other 

469 else: 

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

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

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

473 raise ex 

474 

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

476 """ 

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

478 

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

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

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

482 """ 

483 if isinstance(other, License): 

484 return self._spdxIdentifier != other._spdxIdentifier 

485 elif isinstance(other, str): 

486 return self._spdxIdentifier != other 

487 else: 

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

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

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

491 raise ex 

492 

493 def __hash__(self) -> int: 

494 """ 

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

496 

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

498 

499 :returns: Hash of the SPDX identifier. 

500 """ 

501 return hash(self._spdxIdentifier) 

502 

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

504 """ 

505 Returns true, if both licenses are compatible. 

506 

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

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

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

510 """ 

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

512 

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

514 """ 

515 Returns true, if both licenses are compatible. 

516 

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

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

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

520 """ 

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

522 

523 def __repr__(self) -> str: 

524 """ 

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

526 

527 :returns: SPDX identifier of the license. 

528 """ 

529 return self._spdxIdentifier 

530 

531 def __str__(self) -> str: 

532 """ 

533 Returns the license' name. 

534 

535 :returns: Name of the license. 

536 """ 

537 return self._name 

538 

539 

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

565 

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

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

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

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

570CC_BY_NC_SA_4_0 = License( 

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

572) 

573CC_BY_NC_ND_4_0 = License( 

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

575) 

576 

577 

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

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

580 Zero_Clause_BSD, Apache_2_0_License, BSD_2_Clause_License, BSD_3_Clause_License, MIT_License, MIT_0_License, 

581 ISC_License, MPL_2_0_License, 

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

583 LGPL_2_1_only, LGPL_2_1_or_later, LGPL_3_0_only, LGPL_3_0_or_later, 

584 GPL_2_0_only, GPL_2_0_or_later, GPL_3_0_only, GPL_3_0_or_later, 

585 AGPL_3_0_only, AGPL_3_0_or_later, 

586 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, 

587) 

588 

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

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

591 

592 

593@export 

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

595 """ 

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

597 

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

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

600 

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

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

603 

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

605 """ 

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

607 for spdxLicense in LICENSES: 

608 try: 

609 classifier = spdxLicense.PythonClassifier 

610 except ValueError: 

611 continue 

612 

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

614 

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

616 

617 

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

619#: 

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

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

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

623 

624 

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

626_LicenseType = License 

627 

628 

629@export 

630@abstractclass 

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

632 """ 

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

634 

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

636 

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

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

639 

640 :class:`OrLaterOperator` 

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

642 :class:`WithOperator` 

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

644 :class:`AndOperator` 

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

646 :class:`OrOperator` 

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

648 

649 Its leaves are: 

650 

651 :class:`SPDXLicense` 

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

653 :class:`LicenseReference` 

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

655 :class:`LicenseException` 

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

657 

658 The SPDX grammar defines: 

659 

660 .. code-block:: text 

661 

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

663 compound-expression = ( simple-expression 

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

665 | compound-expression "AND" compound-expression 

666 | compound-expression "OR" compound-expression 

667 | "(" compound-expression ")" ) 

668 

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

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

671 

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

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

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

675 

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

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

678 

679 .. code-block:: python 

680 

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

682 

683 assembled = AndOperator() 

684 assembled.Left = SPDXLicense(Apache_2_0_License) 

685 assembled.Right = SPDXLicense(MIT_License) 

686 

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

688 fills one. 

689 """ 

690 

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

692 

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

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

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

696 

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

698 """ 

699 Initialize an expression node. 

700 

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

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

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

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

705 """ 

706 self._originalText = originalText 

707 

708 if parent is None: 

709 self._parent = None 

710 self._root = self 

711 elif not isinstance(parent, Operator): 

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

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

714 raise ex 

715 else: 

716 self._parent = parent 

717 self._root = parent._root 

718 

719 @property 

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

721 """ 

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

723 

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

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

726 

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

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

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

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

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

732 """ 

733 return self._parent 

734 

735 @Parent.setter 

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

737 if parent is None: 

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

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

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

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

742 raise ex 

743 

744 self._parent = parent 

745 

746 for expression in self.IterateExpression(): 

747 expression._root = parent._root 

748 

749 @readonly 

750 def OriginalText(self) -> str: 

751 """ 

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

753 

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

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

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

757 written. 

758 

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

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

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

762 

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

764 """ 

765 return self._root._originalText 

766 

767 @readonly 

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

769 """ 

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

771 

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

773 can't fall behind the tree it describes. 

774 

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

776 """ 

777 return self._root 

778 

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

780 """ 

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

782 

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

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

785 

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

787 """ 

788 yield self 

789 

790 @classmethod 

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

792 """ 

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

794 

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

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

797 left. 

798 

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

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

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

802 """ 

803 parser = _LicenseExpressionParser(expression) 

804 root = parser.Parse() 

805 root._originalText = expression 

806 

807 return root 

808 

809 @abstractmethod 

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

811 """ 

812 Return this expression in SPDX syntax. 

813 

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

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

816 

817 :returns: The expression in SPDX syntax. 

818 """ 

819 

820 

821@export 

822@abstractclass 

823class Operator(LicenseExpression): 

824 """ 

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

826 

827 Operator kinds: 

828 

829 * :class:`UnaryOperator` takes one operand 

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

831 

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

833 """ 

834 

835 

836@export 

837@abstractclass 

838class BaseLicense(LicenseExpression): 

839 """ 

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

841 

842 Four nodes are one: 

843 

844 :class:`SPDXLicense` 

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

846 :class:`LicenseReference` 

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

848 :class:`ProprietaryLicense` 

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

850 :class:`UnknownLicense` 

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

852 

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

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

855 

856 .. code-block:: python 

857 

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

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

860 

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

862 license, not a license the work is under. 

863 """ 

864 

865 @readonly 

866 @abstractmethod 

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

868 """ 

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

870 

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

872 """ 

873 

874 @readonly 

875 @abstractmethod 

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

877 """ 

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

879 

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

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

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

883 

884 :returns: The license' name. 

885 """ 

886 

887 

888@export 

889class SPDXLicense(BaseLicense): 

890 """ 

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

892 

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

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

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

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

897 """ 

898 

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

900 

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

902 """ 

903 Initialize a reference to an SPDX license. 

904 

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

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

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

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

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

910 """ 

911 super().__init__(parent) 

912 

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

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

915 elif not isinstance(spdxLicense, _LicenseType): 

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

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

918 raise ex 

919 

920 self._license = spdxLicense 

921 

922 @readonly 

923 def License(self) -> _LicenseType: 

924 """ 

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

926 

927 :returns: The license. 

928 """ 

929 return self._license 

930 

931 @readonly 

932 def Identifier(self) -> str: 

933 """ 

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

935 

936 :returns: The license's SPDX identifier. 

937 """ 

938 return self._license._spdxIdentifier 

939 

940 @readonly 

941 def Name(self) -> str: 

942 """ 

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

944 

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

946 """ 

947 return self._license._name 

948 

949 def __str__(self) -> str: 

950 """ 

951 Return the license's SPDX identifier. 

952 

953 :returns: The SPDX identifier. 

954 """ 

955 return self._license.SPDXIdentifier 

956 

957 

958@export 

959class LicenseReference(BaseLicense): 

960 """ 

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

962 

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

964 

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

966 does. 

967 """ 

968 

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

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

971 

972 def __init__( 

973 self, 

974 licenseIdentifier: str, 

975 documentIdentifier: Nullable[str] = None, 

976 parent: Nullable[Operator] = None 

977 ) -> None: 

978 """ 

979 Initialize a license reference. 

980 

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

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

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

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

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

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

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

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

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

990 """ 

991 super().__init__(parent) 

992 

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

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

995 elif not isinstance(licenseIdentifier, str): 

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

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

998 raise ex 

999 elif len(licenseIdentifier) == 0: 

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

1001 

1002 if documentIdentifier is None: 

1003 pass 

1004 elif not isinstance(documentIdentifier, str): 

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

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

1007 raise ex 

1008 elif len(documentIdentifier) == 0: 

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

1010 

1011 self._licenseIdentifier = licenseIdentifier 

1012 self._documentIdentifier = documentIdentifier 

1013 

1014 @readonly 

1015 def LicenseIdentifier(self) -> str: 

1016 """ 

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

1018 

1019 :returns: The license reference's identifier. 

1020 """ 

1021 return self._licenseIdentifier 

1022 

1023 @readonly 

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

1025 """ 

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

1027 

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

1029 """ 

1030 return self._documentIdentifier 

1031 

1032 @readonly 

1033 def Identifier(self) -> str: 

1034 """ 

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

1036 

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

1038 ``LicenseRef-`` on its own. 

1039 

1040 :returns: The license reference. 

1041 """ 

1042 return str(self) 

1043 

1044 def __str__(self) -> str: 

1045 """ 

1046 Return the reference in SPDX syntax. 

1047 

1048 :returns: The license reference. 

1049 """ 

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

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

1052 

1053 @readonly 

1054 def Name(self) -> str: 

1055 """ 

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

1057 

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

1059 only name there is. 

1060 

1061 :returns: The license reference's identifier. 

1062 """ 

1063 return self._licenseIdentifier 

1064 

1065 

1066@export 

1067class ProprietaryLicense(LicenseReference): 

1068 """ 

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

1070 

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

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

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

1074 

1075 .. code-block:: python 

1076 

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

1078 

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

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

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

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

1083 

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

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

1086 """ 

1087 

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

1089 IDENTIFIER: ClassVar[str] = "Proprietary" 

1090 

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

1092 """ 

1093 Initialize a proprietary license. 

1094 

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

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

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

1098 """ 

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

1100 

1101 self._originalText = originalText 

1102 

1103 @readonly 

1104 def Name(self) -> str: 

1105 """ 

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

1107 

1108 :returns: ``Proprietary License``. 

1109 """ 

1110 return "Proprietary License" 

1111 

1112 

1113@export 

1114class UnknownLicense(BaseLicense): 

1115 """ 

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

1117 

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

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

1120 :attr:`Absence`. 

1121 

1122 .. code-block:: python 

1123 

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

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

1126 

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

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

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

1130 

1131 .. seealso:: 

1132 

1133 :class:`ProprietaryLicense` 

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

1135 """ 

1136 

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

1138 

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

1140 """ 

1141 Initialize an absent license. 

1142 

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

1144 

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

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

1147 is the only place left holding it. 

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

1149 """ 

1150 super().__init__(None, originalText) 

1151 

1152 if not isinstance(absence, LicenseAbsence): 

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

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

1155 raise ex 

1156 

1157 self._absence = absence 

1158 

1159 @readonly 

1160 def Absence(self) -> LicenseAbsence: 

1161 """ 

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

1163 

1164 :returns: The absence stated. 

1165 """ 

1166 return self._absence 

1167 

1168 @readonly 

1169 def Identifier(self) -> str: 

1170 """ 

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

1172 

1173 :returns: ``NONE`` or ``NOASSERTION``. 

1174 """ 

1175 return self._absence.value 

1176 

1177 @readonly 

1178 def Name(self) -> str: 

1179 """ 

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

1181 

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

1183 """ 

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

1185 

1186 @readonly 

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

1188 """ 

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

1190 

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

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

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

1194 :exc:`AttributeError`. 

1195 

1196 :returns: ``None``, always. 

1197 """ 

1198 return self._parent 

1199 

1200 def __str__(self) -> str: 

1201 """ 

1202 Return the absence in SPDX's spelling. 

1203 

1204 :returns: ``NONE`` or ``NOASSERTION``. 

1205 """ 

1206 return self._absence.value 

1207 

1208 

1209@export 

1210class LicenseException(LicenseExpression): 

1211 """ 

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

1213 

1214 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 

1215 stand on its own. 

1216 """ 

1217 

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

1219 

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

1221 """ 

1222 Initialize a license exception. 

1223 

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

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

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

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

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

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

1230 """ 

1231 super().__init__(parent) 

1232 

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

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

1235 elif not isinstance(identifier, str): 

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

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

1238 raise ex 

1239 elif len(identifier) == 0: 

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

1241 

1242 self._identifier = identifier 

1243 

1244 @readonly 

1245 def Identifier(self) -> str: 

1246 """ 

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

1248 

1249 :returns: The exception's identifier. 

1250 """ 

1251 return self._identifier 

1252 

1253 def __str__(self) -> str: 

1254 """ 

1255 Return the exception's SPDX identifier. 

1256 

1257 :returns: The identifier. 

1258 """ 

1259 return self._identifier 

1260 

1261 

1262@export 

1263@abstractclass 

1264class UnaryOperator(Operator): 

1265 """ 

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

1267 

1268 SPDX defines exactly one unary operator: 

1269 

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

1271 

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

1273 """ 

1274 

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

1276 

1277 def __init__( 

1278 self, 

1279 operand: Nullable[LicenseExpression] = None, 

1280 parent: Nullable[Operator] = None 

1281 ) -> None: 

1282 """ 

1283 Initialize a unary operator with its operand. 

1284 

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

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

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

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

1289 """ 

1290 super().__init__(parent) 

1291 

1292 if operand is not None: 

1293 self._operand = operand 

1294 

1295 if not isinstance(operand, LicenseExpression): 

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

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

1298 raise ex 

1299 

1300 operand.Parent = self 

1301 else: 

1302 self._operand = None 

1303 

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

1305 """ 

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

1307 

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

1309 """ 

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

1311 yield from self._operand.IterateExpression() 

1312 

1313 yield self 

1314 

1315 @property 

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

1317 """ 

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

1319 

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

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

1322 

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

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

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

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

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

1328 """ 

1329 return self._operand 

1330 

1331 @Operand.setter 

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

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

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

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

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

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

1338 raise ex 

1339 elif self._operand is not None: 

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

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

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

1343 

1344 self._operand = operand 

1345 operand.Parent = self 

1346 

1347 

1348@export 

1349class OrLaterOperator(UnaryOperator): 

1350 """ 

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

1352 

1353 .. attention:: 

1354 

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

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

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

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

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

1360 """ 

1361 

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

1363 

1364 def __str__(self) -> str: 

1365 """ 

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

1367 

1368 :returns: The expression in SPDX syntax. 

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

1370 """ 

1371 if self._operand is None: 

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

1373 

1374 return f"{self._operand}+" 

1375 

1376 

1377@export 

1378@abstractclass 

1379class BinaryOperator(Operator): 

1380 """ 

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

1382 

1383 SPDX defines three binary operators: 

1384 

1385 * :class:`WithOperator`, 

1386 * :class:`AndOperator` and 

1387 * :class:`OrOperator`. 

1388 

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

1390 operands. 

1391 """ 

1392 

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

1394 

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

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

1397 

1398 def __init__( 

1399 self, 

1400 left: Nullable[LicenseExpression] = None, 

1401 right: Nullable[LicenseExpression] = None, 

1402 parent: Nullable[Operator] = None 

1403 ) -> None: 

1404 """ 

1405 Initialize a binary operator with both operands. 

1406 

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

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

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

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

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

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

1413 """ 

1414 super().__init__(parent) 

1415 

1416 if left is not None: 

1417 self._left = left 

1418 

1419 if not isinstance(left, LicenseExpression): 

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

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

1422 raise ex 

1423 

1424 left.Parent = self 

1425 else: 

1426 self._left = None 

1427 

1428 if right is not None: 

1429 self._right = right 

1430 

1431 if not isinstance(right, LicenseExpression): 

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

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

1434 raise ex 

1435 

1436 right.Parent = self 

1437 else: 

1438 self._right = None 

1439 

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

1441 """ 

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

1443 

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

1445 """ 

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

1447 yield from self._left.IterateExpression() 

1448 

1449 yield self 

1450 

1451 if self._right is not None: 

1452 yield from self._right.IterateExpression() 

1453 

1454 @property 

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

1456 """ 

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

1458 

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

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

1461 

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

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

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

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

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

1467 """ 

1468 return self._left 

1469 

1470 @Left.setter 

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

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

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

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

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

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

1477 raise ex 

1478 elif self._left is not None: 

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

1480 elif operand._parent is not None: 

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

1482 

1483 self._left = operand 

1484 operand.Parent = self 

1485 

1486 @property 

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

1488 """ 

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

1490 

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

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

1493 

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

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

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

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

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

1499 """ 

1500 return self._right 

1501 

1502 @Right.setter 

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

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

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

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

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

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

1509 raise ex 

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

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

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

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

1514 

1515 self._right = operand 

1516 operand.Parent = self 

1517 

1518 def __str__(self) -> str: 

1519 """ 

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

1521 

1522 :returns: The expression in SPDX syntax. 

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

1524 """ 

1525 if self._left is None: 

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

1527 elif self._right is None: 

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

1529 

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

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

1532 

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

1534 

1535 

1536@export 

1537class WithOperator(BinaryOperator): 

1538 """ 

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

1540 

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

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

1543 """ 

1544 

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

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

1547 

1548 

1549@export 

1550class AndOperator(BinaryOperator): 

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

1552 

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

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

1555 

1556 

1557@export 

1558class OrOperator(BinaryOperator): 

1559 """ 

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

1561 

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

1563 """ 

1564 

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

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

1567 

1568 

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

1570 """ 

1571 Recursive-descent parser for SPDX license expressions. 

1572 

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

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

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

1576 """ 

1577 

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

1579 

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

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

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

1583 

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

1585 """ 

1586 Tokenize an expression. 

1587 

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

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

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

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

1592 """ 

1593 if not isinstance(expression, str): 

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

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

1596 raise ex 

1597 elif expression == "": 

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

1599 

1600 self._expression = expression 

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

1602 self._position = 0 

1603 

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

1605 raise LicenseExpressionError(f"License expression '{expression}' is empty.") 

1606 

1607 def Parse(self) -> LicenseExpression: 

1608 """ 

1609 Parse the whole expression. 

1610 

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

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

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

1614 

1615 :returns: The root of the expression tree. 

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

1617 """ 

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

1619 for absence in LicenseAbsence: 

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

1621 self._position = 1 

1622 return UnknownLicense(absence) 

1623 

1624 result = self._ParseOr() 

1625 

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

1627 raise LicenseExpressionError( 

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

1629 ) 

1630 

1631 return result 

1632 

1633 def _ParseOr(self) -> LicenseExpression: 

1634 """ 

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

1636 

1637 :returns: The parsed expression. 

1638 """ 

1639 left = self._ParseAnd() 

1640 while self._Accept(OrOperator.KEYWORD): 

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

1642 

1643 return left 

1644 

1645 def _ParseAnd(self) -> LicenseExpression: 

1646 """ 

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

1648 

1649 :returns: The parsed expression. 

1650 """ 

1651 left = self._ParseWith() 

1652 while self._Accept(AndOperator.KEYWORD): 

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

1654 

1655 return left 

1656 

1657 def _ParseWith(self) -> LicenseExpression: 

1658 """ 

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

1660 

1661 :returns: The parsed expression. 

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

1663 """ 

1664 left = self._ParseSimple() 

1665 if self._Accept(WithOperator.KEYWORD): 

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

1667 raise LicenseExpressionError(f"License expression '{self._expression}' ends after 'WITH'.") 

1668 

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

1670 

1671 return left 

1672 

1673 def _ParseSimple(self) -> LicenseExpression: 

1674 """ 

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

1676 

1677 :returns: The parsed expression. 

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

1679 """ 

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

1681 raise LicenseExpressionError(f"License expression '{self._expression}' ends unexpectedly.") 

1682 

1683 if token == "(": 

1684 inner = self._ParseOr() 

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

1686 raise LicenseExpressionError(f"License expression '{self._expression}' is missing a closing parenthesis.") 

1687 

1688 return inner 

1689 

1690 if token == ")": 

1691 raise LicenseExpressionError(f"License expression '{self._expression}' has an unmatched closing parenthesis.") 

1692 

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

1694 orLater = token.endswith("+") 

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

1696 

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

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

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

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

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

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

1703 expression = SPDXLicense(spdxLicense) 

1704 else: 

1705 ex = LicenseExpressionError(f"License expression '{self._expression}' names unknown license '{identifier}'.") 

1706 

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

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

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

1710 else: 

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

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

1713 

1714 raise ex 

1715 

1716 return OrLaterOperator(expression) if orLater else expression 

1717 

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

1719 """ 

1720 Consume and return the next token. 

1721 

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

1723 """ 

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

1725 return None 

1726 

1727 self._position += 1 

1728 

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

1730 

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

1732 """ 

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

1734 

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

1736 

1737 :param keyword: The keyword to look for. 

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

1739 """ 

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

1741 self._position += 1 

1742 

1743 return True 

1744 

1745 return False