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
« 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.
34.. seealso::
36 List of SPDX identifiers:
38 * https://spdx.org/licenses/
39 * https://github.com/spdx/license-list-XML
41 List of `Python classifiers <https://pypi.org/classifiers/>`__
43.. hint::
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
57__all__ = [
58 "PYTHON_LICENSE_NAMES",
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",
90 "SPDX_INDEX",
91 "LICENSES_BY_CLASSIFIER",
92 "OSI_LICENSE_URLS",
93 "LICENSE_URLS",
94 "LICENSE_TEXT_URLS"
95]
98@export
99class LicensingError(ToolingException):
100 """Base exception of all exceptions raised by :mod:`pyTooling.Licensing`."""
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 """
110@export
111@unique
112class LicenseAbsence(Enum):
113 """
114 SPDX's two ways of stating that no license is named, which mean different things.
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 """
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.
123 def __str__(self) -> str:
124 """
125 Return the value in SPDX's spelling.
127 :returns: ``NONE`` or ``NOASSERTION``.
128 """
129 return self.value
132@export
133@dataclass
134class PythonLicenseName:
135 """A *data class* to represent the license's short name and the package classifier for a license."""
137 ShortName: str #: License's short name
138 Classifier: str #: Package classifier for a license.
140 def __str__(self) -> str:
141 """
142 The string representation of this name tuple returns the short name of the license.
144 :returns: Short name of the license.
145 """
146 return self.ShortName
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}
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}
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}
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}
289@export
290class License(metaclass=ExtendedType, slots=True):
291 """Representation of a license."""
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
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.
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
312 @readonly
313 def Name(self) -> str:
314 """
315 Returns the license' name.
317 :returns: License name.
318 """
319 return self._name
321 @readonly
322 def SPDXIdentifier(self) -> str:
323 """
324 Returns the license' unique `SPDX identifier <https://spdx.org/licenses/>`__.
326 :returns: The unique SPDX identifier.
327 """
328 return self._spdxIdentifier
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/>`__.
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.
339 :returns: URL of the license's page at SPDX.
340 """
341 return f"https://spdx.org/licenses/{self._spdxIdentifier}.html"
343 @readonly
344 def URL(self) -> Nullable[str]:
345 """
346 Returns the page where the licensor publishes this license.
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.
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.
355 :returns: URL of the license's own page, or ``None`` if the licensor publishes none.
357 .. seealso::
359 :attr:`TextURLs`
360 |rarr| The same license as text, by format.
361 """
362 return LICENSE_URLS.get(self._spdxIdentifier, None)
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.
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.
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.
375 .. code-block:: python
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
380 A **copy** is returned, so a caller can't edit :data:`LICENSE_TEXT_URLS` through it.
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, {}))
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>`__.
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.
395 :returns: URL of the license's page at OSI, or ``None`` if OSI doesn't publish it.
397 .. seealso::
399 :attr:`OSIApproved`
400 |rarr| Whether OSI approved this license at all.
401 """
402 return OSI_LICENSE_URLS.get(self._spdxIdentifier, None)
404 @readonly
405 def OSIApproved(self) -> bool:
406 """
407 Returns true, if the license is approved by OSI (`Open Source Initiative <https://opensource.org/>`__).
409 :returns: ``True``, if the license is approved by the Open Source Initiative.
410 """
411 return self._osiApproved
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/>`__).
418 :returns: ``True``, if the license is approved by the Free Software Foundation.
419 """
420 return self._fsfApproved
422 @readonly
423 def PythonLicenseName(self) -> str:
424 """
425 Returns the Python license name for this license if it's defined.
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
435 return item.ShortName
437 @readonly
438 def PythonClassifier(self) -> str:
439 """
440 Returns the Python package classifier for this license if it's defined.
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`)
445 .. seealso::
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
454 osi = "OSI Approved :: " if self._osiApproved else ""
455 return f"License :: {osi}{item.Classifier}"
457 def __eq__(self, other: Any) -> bool:
458 """
459 Returns true, if both licenses are identical (comparison based on SPDX identifiers).
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
475 def __ne__(self, other: Any) -> bool:
476 """
477 Returns true, if both licenses are not identical (comparison based on SPDX identifiers).
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
493 def __hash__(self) -> int:
494 """
495 Compute a hash from the license's SPDX identifier.
497 A license compares equal to its identifier as a string, so the two hash equally as well.
499 :returns: Hash of the SPDX identifier.
500 """
501 return hash(self._spdxIdentifier)
503 def __le__(self, other: Any) -> bool:
504 """
505 Returns true, if both licenses are compatible.
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.")
513 def __ge__(self, other: Any) -> bool:
514 """
515 Returns true, if both licenses are compatible.
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.")
523 def __repr__(self) -> str:
524 """
525 Returns the internal unique representation (:attr:`spdxIdentifier`).
527 :returns: SPDX identifier of the license.
528 """
529 return self._spdxIdentifier
531 def __str__(self) -> str:
532 """
533 Returns the license' name.
535 :returns: Name of the license.
536 """
537 return self._name
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)
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)
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)
589#: Mapping of predefined licenses, indexed by their SPDX identifier.
590SPDX_INDEX: dict[str, License] = {spdxLicense.SPDXIdentifier: spdxLicense for spdxLicense in LICENSES}
593@export
594def buildClassifierIndex() -> dict[str, tuple[License, ...]]:
595 """
596 Index the predefined licenses by the Python classifier they are published as.
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.
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.
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
613 index.setdefault(classifier, []).append(spdxLicense)
615 return {classifier: tuple(licenses) for classifier, licenses in index.items()}
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()
625#: The :class:`License` class under a name no expression node shadows with a property of its own.
626_LicenseType = License
629@export
630@abstractclass
631class LicenseExpression(metaclass=ExtendedType, slots=True):
632 """
633 Base-class of every node in an `SPDX license expression`_ tree.
635 .. _SPDX license expression: https://spdx.github.io/spdx-spec/v2.3/SPDX-license-expressions/
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`:
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.
649 Its leaves are:
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``.
658 The SPDX grammar defines:
660 .. code-block:: text
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 ")" )
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.
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.
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:
679 .. code-block:: python
681 bottomUp = AndOperator(SPDXLicense(Apache_2_0_License), SPDXLicense(MIT_License))
683 assembled = AndOperator()
684 assembled.Left = SPDXLicense(Apache_2_0_License)
685 assembled.Right = SPDXLicense(MIT_License)
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 """
691 PRECEDENCE: ClassVar[int] = 0 #: Precedence of this node's operator; a lower value binds tighter.
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.
697 def __init__(self, parent: Nullable["Operator"] = None, originalText: str = "") -> None:
698 """
699 Initialize an expression node.
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
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
719 @property
720 def Parent(self) -> Nullable["Operator"]:
721 """
722 Property to access the operator this expression is an operand of (:attr:`_parent`).
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.
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
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
744 self._parent = parent
746 for expression in self.IterateExpression():
747 expression._root = parent._root
749 @readonly
750 def OriginalText(self) -> str:
751 """
752 Read-only property to access the text this expression stands for (:attr:`_originalText`).
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.
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.
763 :returns: The text this expression stands for, or an empty string if there is none.
764 """
765 return self._root._originalText
767 @readonly
768 def Root(self) -> "LicenseExpression":
769 """
770 Read-only property to access the outermost expression this node belongs to (:attr:`_root`).
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.
775 :returns: The root of the expression tree, which is the node itself if it has no parent.
776 """
777 return self._root
779 def IterateExpression(self) -> Generator["LicenseExpression", None, None]:
780 """
781 Iterate this expression depth-first, in the order its nodes are written.
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.
786 :returns: A generator of every node in this expression, this node included.
787 """
788 yield self
790 @classmethod
791 def Parse(cls, expression: str) -> "LicenseExpression":
792 """
793 Parse an SPDX license expression into a tree of expression nodes.
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.
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
807 return root
809 @abstractmethod
810 def __str__(self) -> str: # type: ignore[empty-body]
811 """
812 Return this expression in SPDX syntax.
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.
817 :returns: The expression in SPDX syntax.
818 """
821@export
822@abstractclass
823class Operator(LicenseExpression):
824 """
825 Base-class of every expression node that is applied to operands.
827 Operator kinds:
829 * :class:`UnaryOperator` takes one operand
830 * :class:`BinaryOperator` takes two operands.
832 Only an operator can be an operand's :attr:`~LicenseExpression.Parent`.
833 """
836@export
837@abstractclass
838class BaseLicense(LicenseExpression):
839 """
840 Base-class of every expression node that names a license the work is under.
842 Four nodes are one:
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.
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:
856 .. code-block:: python
858 [term.Identifier for term in expression.IterateExpression() if isinstance(term, BaseLicense)]
859 # ['MIT', 'LicenseRef-Proprietary'] for 'MIT AND LicenseRef-Proprietary'
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 """
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.
871 :returns: The SPDX identifier, or the ``LicenseRef-`` reference, in the spelling an expression writes it.
872 """
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.
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.
884 :returns: The license' name.
885 """
888@export
889class SPDXLicense(BaseLicense):
890 """
891 A single license in an expression, named by its SPDX identifier.
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 """
899 _license: _LicenseType #: The well-known license this node refers to.
901 def __init__(self, spdxLicense: _LicenseType, parent: Nullable[Operator] = None) -> None:
902 """
903 Initialize a reference to an SPDX license.
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)
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
920 self._license = spdxLicense
922 @readonly
923 def License(self) -> _LicenseType:
924 """
925 Read-only property to access the license this node stands for (:attr:`_license`).
927 :returns: The license.
928 """
929 return self._license
931 @readonly
932 def Identifier(self) -> str:
933 """
934 Read-only property to access the license' SPDX identifier (:pycode:`_license._spdxIdentifier`).
936 :returns: The license's SPDX identifier.
937 """
938 return self._license._spdxIdentifier
940 @readonly
941 def Name(self) -> str:
942 """
943 Read-only property to access the license' name (:pycode:`_license._name`).
945 :returns: The name SPDX publishes for this license, like ``Apache License 2.0``.
946 """
947 return self._license._name
949 def __str__(self) -> str:
950 """
951 Return the license's SPDX identifier.
953 :returns: The SPDX identifier.
954 """
955 return self._license.SPDXIdentifier
958@export
959class LicenseReference(BaseLicense):
960 """
961 A license that isn't on the SPDX License List, written as ``LicenseRef-<id>``.
963 It may name the document it is defined in, as ``DocumentRef-<id>:LicenseRef-<id>``.
965 There is no predefined :class:`License` object, because SPDX doesn't know the license - only the document declaring it
966 does.
967 """
969 _licenseIdentifier: str #: Identifier following ``LicenseRef-``.
970 _documentIdentifier: Nullable[str] #: Identifier following ``DocumentRef-``, if the reference names one.
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.
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)
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.")
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.")
1011 self._licenseIdentifier = licenseIdentifier
1012 self._documentIdentifier = documentIdentifier
1014 @readonly
1015 def LicenseIdentifier(self) -> str:
1016 """
1017 Read-only property to access the identifier following ``LicenseRef-`` (:attr:`_licenseIdentifier`).
1019 :returns: The license reference's identifier.
1020 """
1021 return self._licenseIdentifier
1023 @readonly
1024 def DocumentIdentifier(self) -> Nullable[str]:
1025 """
1026 Read-only property to access the identifier following ``DocumentRef-`` (:attr:`_documentIdentifier`).
1028 :returns: The document reference's identifier, or ``None`` if the reference names no document.
1029 """
1030 return self._documentIdentifier
1032 @readonly
1033 def Identifier(self) -> str:
1034 """
1035 Read-only property to return the reference in the spelling an expression writes it.
1037 This is the whole reference, ``DocumentRef-`` and all - :attr:`LicenseIdentifier` is the part following
1038 ``LicenseRef-`` on its own.
1040 :returns: The license reference.
1041 """
1042 return str(self)
1044 def __str__(self) -> str:
1045 """
1046 Return the reference in SPDX syntax.
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}"
1053 @readonly
1054 def Name(self) -> str:
1055 """
1056 Read-only property to access the identifier following ``LicenseRef-`` (:attr:`_licenseIdentifier`).
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.
1061 :returns: The license reference's identifier.
1062 """
1063 return self._licenseIdentifier
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.
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.
1075 .. code-block:: python
1077 str(ProprietaryLicense()) # 'LicenseRef-Proprietary'
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.
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 """
1088 #: The identifier a proprietary license is written with, following ``LicenseRef-``.
1089 IDENTIFIER: ClassVar[str] = "Proprietary"
1091 def __init__(self, parent: Nullable[Operator] = None, originalText: str = "") -> None:
1092 """
1093 Initialize a proprietary license.
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)
1101 self._originalText = originalText
1103 @readonly
1104 def Name(self) -> str:
1105 """
1106 Read-only property to return a readable name for a license that publishes none.
1108 :returns: ``Proprietary License``.
1109 """
1110 return "Proprietary License"
1113@export
1114class UnknownLicense(BaseLicense):
1115 """
1116 SPDX's statement that no license is named: ``NONE`` or ``NOASSERTION``.
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`.
1122 .. code-block:: python
1124 LicenseExpression.Parse("NOASSERTION") # UnknownLicense(LicenseAbsence.NoAssertion)
1125 LicenseExpression.Parse("NONE") # UnknownLicense(LicenseAbsence.NoLicense)
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`.
1131 .. seealso::
1133 :class:`ProprietaryLicense`
1134 |rarr| For a license that exists but isn't published - which is a different statement from this one.
1135 """
1137 _absence: LicenseAbsence #: Which of SPDX's two absences this node states.
1139 def __init__(self, absence: LicenseAbsence = LicenseAbsence.NoAssertion, originalText: str = "") -> None:
1140 """
1141 Initialize an absent license.
1143 No ``parent`` parameter, because neither value may be an operand.
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)
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
1157 self._absence = absence
1159 @readonly
1160 def Absence(self) -> LicenseAbsence:
1161 """
1162 Read-only property to access which absence this node states (:attr:`_absence`).
1164 :returns: The absence stated.
1165 """
1166 return self._absence
1168 @readonly
1169 def Identifier(self) -> str:
1170 """
1171 Read-only property to access the absence in SPDX's spelling (:pycode:`_absence.value`).
1173 :returns: ``NONE`` or ``NOASSERTION``.
1174 """
1175 return self._absence.value
1177 @readonly
1178 def Name(self) -> str:
1179 """
1180 Read-only property to return which absence this states, in words.
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"
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`).
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`.
1196 :returns: ``None``, always.
1197 """
1198 return self._parent
1200 def __str__(self) -> str:
1201 """
1202 Return the absence in SPDX's spelling.
1204 :returns: ``NONE`` or ``NOASSERTION``.
1205 """
1206 return self._absence.value
1209@export
1210class LicenseException(LicenseExpression):
1211 """
1212 The right operand of a :class:`WithOperator`, naming an exception from the SPDX exception list.
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 """
1218 _identifier: str #: The exception's SPDX identifier.
1220 def __init__(self, identifier: str, parent: Nullable[Operator] = None) -> None:
1221 """
1222 Initialize a license exception.
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)
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.")
1242 self._identifier = identifier
1244 @readonly
1245 def Identifier(self) -> str:
1246 """
1247 Read-only property to access the exception's SPDX identifier (:attr:`_identifier`).
1249 :returns: The exception's identifier.
1250 """
1251 return self._identifier
1253 def __str__(self) -> str:
1254 """
1255 Return the exception's SPDX identifier.
1257 :returns: The identifier.
1258 """
1259 return self._identifier
1262@export
1263@abstractclass
1264class UnaryOperator(Operator):
1265 """
1266 Base-class of the expression operators taking one operand.
1268 SPDX defines exactly one unary operator:
1270 * the ``+`` suffix of :class:`OrLaterOperator`.
1272 The operand is reachable as :attr:`Operand` and is assignable, so an operator can be filled after it was created.
1273 """
1275 _operand: Nullable[LicenseExpression] #: The expression this operator is applied to.
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.
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)
1292 if operand is not None:
1293 self._operand = operand
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
1300 operand.Parent = self
1301 else:
1302 self._operand = None
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.
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()
1313 yield self
1315 @property
1316 def Operand(self) -> Nullable[LicenseExpression]:
1317 """
1318 Property to access the expression this operator is applied to (:attr:`_operand`).
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.
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
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.")
1344 self._operand = operand
1345 operand.Parent = self
1348@export
1349class OrLaterOperator(UnaryOperator):
1350 """
1351 The ``+`` suffix, as in ``GPL-2.0+``: the named license *or any later version of it*.
1353 .. attention::
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 """
1362 PRECEDENCE: ClassVar[int] = 1 #: Binds tighter than every binary operator, looser than a bare license.
1364 def __str__(self) -> str:
1365 """
1366 Return the operand followed by ``+``.
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.")
1374 return f"{self._operand}+"
1377@export
1378@abstractclass
1379class BinaryOperator(Operator):
1380 """
1381 Base-class of the expression operators taking two operands.
1383 SPDX defines three binary operators:
1385 * :class:`WithOperator`,
1386 * :class:`AndOperator` and
1387 * :class:`OrOperator`.
1389 The operands are reachable as :attr:`Left` and :attr:`Right`. :attr:`KEYWORD` is the operator keyword between the
1390 operands.
1391 """
1393 KEYWORD: ClassVar[str] #: The operator's keyword, as it is written between the operands.
1395 _left: Nullable[LicenseExpression] #: The operator's left operand.
1396 _right: Nullable[LicenseExpression] #: The operator's right operand.
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.
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)
1416 if left is not None:
1417 self._left = left
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
1424 left.Parent = self
1425 else:
1426 self._left = None
1428 if right is not None:
1429 self._right = right
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
1436 right.Parent = self
1437 else:
1438 self._right = None
1440 def IterateExpression(self) -> Generator[LicenseExpression, None, None]:
1441 """
1442 Iterate this operator depth-first in infix order: left operand, this operator, right operand.
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()
1449 yield self
1451 if self._right is not None:
1452 yield from self._right.IterateExpression()
1454 @property
1455 def Left(self) -> Nullable[LicenseExpression]:
1456 """
1457 Property to access the operator's left operand (:attr:`_left`).
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.
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
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.")
1483 self._left = operand
1484 operand.Parent = self
1486 @property
1487 def Right(self) -> Nullable[LicenseExpression]:
1488 """
1489 Property to access the operator's right operand (:attr:`_right`).
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.
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
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.")
1515 self._right = operand
1516 operand.Parent = self
1518 def __str__(self) -> str:
1519 """
1520 Return both operands with the operator's keyword between them.
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.")
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}"
1533 return f"{left} {self.KEYWORD} {right}"
1536@export
1537class WithOperator(BinaryOperator):
1538 """
1539 ``WITH``, as in ``Apache-2.0 WITH LLVM-exception``: a license together with an exception to it.
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 """
1545 PRECEDENCE: ClassVar[int] = 2 #: Binds tighter than ``AND`` and ``OR``.
1546 KEYWORD: ClassVar[str] = "WITH" #: The operator's keyword.
1549@export
1550class AndOperator(BinaryOperator):
1551 """``AND``, as in ``Apache-2.0 AND MIT``: **both** licenses apply, and both have to be complied with."""
1553 PRECEDENCE: ClassVar[int] = 3 #: Binds tighter than ``OR``.
1554 KEYWORD: ClassVar[str] = "AND" #: The operator's keyword.
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.
1562 Which one they chose is not something the expression records.
1563 """
1565 PRECEDENCE: ClassVar[int] = 4 #: Binds loosest of all operators.
1566 KEYWORD: ClassVar[str] = "OR" #: The operator's keyword.
1569class _LicenseExpressionParser(metaclass=ExtendedType, slots=True):
1570 """
1571 Recursive-descent parser for SPDX license expressions.
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 """
1578 _TOKEN = re_compile(r"\(|\)|[^\s()]+") #: Splits an expression into parentheses and the words between them.
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.
1584 def __init__(self, expression: str) -> None:
1585 """
1586 Tokenize an expression.
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.")
1600 self._expression = expression
1601 self._tokens = self._TOKEN.findall(expression)
1602 self._position = 0
1604 if len(self._tokens) == 0:
1605 raise LicenseExpressionError(f"License expression '{expression}' is empty.")
1607 def Parse(self) -> LicenseExpression:
1608 """
1609 Parse the whole expression.
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.
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)
1624 result = self._ParseOr()
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 )
1631 return result
1633 def _ParseOr(self) -> LicenseExpression:
1634 """
1635 Parse a sequence of ``OR`` operands, the loosest-binding operator.
1637 :returns: The parsed expression.
1638 """
1639 left = self._ParseAnd()
1640 while self._Accept(OrOperator.KEYWORD):
1641 left = OrOperator(left, self._ParseAnd())
1643 return left
1645 def _ParseAnd(self) -> LicenseExpression:
1646 """
1647 Parse a sequence of ``AND`` operands.
1649 :returns: The parsed expression.
1650 """
1651 left = self._ParseWith()
1652 while self._Accept(AndOperator.KEYWORD):
1653 left = AndOperator(left, self._ParseWith())
1655 return left
1657 def _ParseWith(self) -> LicenseExpression:
1658 """
1659 Parse a ``WITH`` clause, whose right operand is an exception rather than a license.
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'.")
1669 left = WithOperator(left, LicenseException(identifier))
1671 return left
1673 def _ParseSimple(self) -> LicenseExpression:
1674 """
1675 Parse a parenthesized expression, a license reference, or a license identifier with an optional ``+``.
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.")
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.")
1688 return inner
1690 if token == ")":
1691 raise LicenseExpressionError(f"License expression '{self._expression}' has an unmatched closing parenthesis.")
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
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}'.")
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>'.")
1714 raise ex
1716 return OrLaterOperator(expression) if orLater else expression
1718 def _Next(self) -> Nullable[str]:
1719 """
1720 Consume and return the next token.
1722 :returns: The next token, or ``None`` if the expression is exhausted.
1723 """
1724 if self._position >= len(self._tokens):
1725 return None
1727 self._position += 1
1729 return self._tokens[self._position - 1]
1731 def _Accept(self, keyword: str) -> bool:
1732 """
1733 Consume the next token if it is the given keyword.
1735 Keywords are matched case-insensitively, because published metadata writes ``and`` as often as ``AND``.
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
1743 return True
1745 return False