Coverage for pyTooling/Licensing/__init__.py: 93%
523 statements
« prev ^ index » next coverage.py v7.17.0, created at 2026-10-25 12:33 +0000
« prev ^ index » next coverage.py v7.17.0, created at 2026-10-25 12:33 +0000
1# ==================================================================================================================== #
2# _____ _ _ _ _ _ #
3# _ __ _ |_ _|__ ___ | (_)_ __ __ _ | | (_) ___ ___ _ __ ___(_)_ __ __ _ #
4# | '_ \| | | || |/ _ \ / _ \| | | '_ \ / _` | | | | |/ __/ _ \ '_ \/ __| | '_ \ / _` | #
5# | |_) | |_| || | (_) | (_) | | | | | | (_| |_| |___| | (_| __/ | | \__ \ | | | | (_| | #
6# | .__/ \__, ||_|\___/ \___/|_|_|_| |_|\__, (_)_____|_|\___\___|_| |_|___/_|_| |_|\__, | #
7# |_| |___/ |___/ |___/ #
8# ==================================================================================================================== #
9# Authors: #
10# Patrick Lehmann #
11# #
12# License: #
13# ==================================================================================================================== #
14# Copyright 2017-2026 Patrick Lehmann - Bötzingen, Germany #
15# #
16# Licensed under the Apache License, Version 2.0 (the "License"); #
17# you may not use this file except in compliance with the License. #
18# You may obtain a copy of the License at #
19# #
20# http://www.apache.org/licenses/LICENSE-2.0 #
21# #
22# Unless required by applicable law or agreed to in writing, software #
23# distributed under the License is distributed on an "AS IS" BASIS, #
24# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. #
25# See the License for the specific language governing permissions and #
26# limitations under the License. #
27# #
28# SPDX-License-Identifier: Apache-2.0 #
29# ==================================================================================================================== #
30#
31"""
32The Licensing module implements mapping tables for various license names and identifiers.
34.. seealso::
36 List of SPDX identifiers:
38 * https://spdx.org/licenses/
39 * :gh:`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 """
109 _expression: Nullable[str] #: License expression that can't be parsed.
110 _identifier: Nullable[str] #: License identifier SPDX doesn't define.
112 def __init__(self, message: str, /, *, expression: Nullable[str] = None, identifier: Nullable[str] = None) -> None:
113 """
114 Initializes the exception with the license expression and, for an unknown license, its identifier.
116 :param message: The exception's message.
117 :param expression: Optional, the license expression that can't be parsed.
118 :param identifier: Optional, the license identifier SPDX doesn't define, if that's the reason.
119 """
120 super().__init__(message)
121 self._expression = expression
122 self._identifier = identifier
124 @readonly
125 def Expression(self) -> Nullable[str]:
126 """
127 Read-only property to access the license expression that can't be parsed (:attr:`_expression`).
129 :returns: The expression, or ``None`` if it wasn't recorded.
130 """
131 return self._expression
133 @readonly
134 def Identifier(self) -> Nullable[str]:
135 """
136 Read-only property to access the license identifier SPDX doesn't define (:attr:`_identifier`).
138 :returns: The identifier, or ``None`` if it wasn't recorded.
139 """
140 return self._identifier
143@export
144@unique
145class LicenseAbsence(Enum):
146 """
147 SPDX's two ways of stating that no license is named, which mean different things.
149 Both are values a license field may hold **on their own**; neither is part of the expression grammar, so neither
150 can be an operand of ``AND``, ``OR`` or ``WITH``.
151 """
153 NoLicense = "NONE" #: The work states that no license applies to it.
154 NoAssertion = "NOASSERTION" #: Someone looked and declined to state a license; nothing is claimed either way.
156 def __str__(self) -> str:
157 """
158 Return the value in SPDX's spelling.
160 :returns: ``NONE`` or ``NOASSERTION``.
161 """
162 return self.value
165@export
166@dataclass
167class PythonLicenseName:
168 """A *data class* to represent the license's short name and the package classifier for a license."""
170 ShortName: str #: License's short name
171 Classifier: str #: Package classifier for a license.
173 def __str__(self) -> str:
174 """
175 The string representation of this name tuple returns the short name of the license.
177 :returns: Short name of the license.
178 """
179 return self.ShortName
182#: Mapping of SPDX identifiers to Python license names
183PYTHON_LICENSE_NAMES: dict[str, PythonLicenseName] = {
184 "Apache-2.0": PythonLicenseName("Apache 2.0", "Apache Software License"),
185 "0BSD": PythonLicenseName("0BSD", "Zero-Clause BSD (0BSD)"),
186 "BSD-2-Clause": PythonLicenseName("BSD-2-Clause", "BSD License"),
187 "BSD-3-Clause": PythonLicenseName("BSD", "BSD License"),
188 "MIT": PythonLicenseName("MIT", "MIT License"),
189 "MIT-0": PythonLicenseName("MIT-0", "MIT No Attribution License (MIT-0)"),
190 "MIT-CMU": PythonLicenseName("MIT-CMU", "CMU License (MIT-CMU)"),
191 "ISC": PythonLicenseName("ISC", "ISC License (ISCL)"),
192 "MPL-2.0": PythonLicenseName("MPL-2.0", "Mozilla Public License 2.0 (MPL 2.0)"),
193 "BSL-1.0": PythonLicenseName("BSL-1.0", "Boost Software License 1.0 (BSL-1.0)"),
194 "Zlib": PythonLicenseName("Zlib", "zlib/libpng License"),
195 "PSF-2.0": PythonLicenseName("PSF-2.0", "Python Software Foundation License"),
196 "Unlicense": PythonLicenseName("Unlicense", "The Unlicense (Unlicense)"),
197 "CC0-1.0": PythonLicenseName("CC0-1.0", "CC0 1.0 Universal (CC0 1.0) Public Domain Dedication"),
198 "EPL-1.0": PythonLicenseName("EPL-1.0", "Eclipse Public License 1.0 (EPL-1.0)"),
199 "EPL-2.0": PythonLicenseName("EPL-2.0", "Eclipse Public License 2.0 (EPL-2.0)"),
200 "LGPL-2.1-only": PythonLicenseName("LGPL-2.1-only", "GNU Lesser General Public License v2 (LGPLv2)"),
201 "LGPL-2.1-or-later": PythonLicenseName("LGPL-2.1-or-later", "GNU Lesser General Public License v2 or later (LGPLv2+)"),
202 "LGPL-3.0-only": PythonLicenseName("LGPL-3.0-only", "GNU Lesser General Public License v3 (LGPLv3)"),
203 "LGPL-3.0-or-later": PythonLicenseName("LGPL-3.0-or-later", "GNU Lesser General Public License v3 or later (LGPLv3+)"),
204 "GPL-2.0-only": PythonLicenseName("GPL-2.0-only", "GNU General Public License v2 (GPLv2)"),
205 "GPL-2.0-or-later": PythonLicenseName("GPL-2.0-or-later", "GNU General Public License v2 or later (GPLv2+)"),
206 "GPL-3.0-only": PythonLicenseName("GPL-3.0-only", "GNU General Public License v3 (GPLv3)"),
207 "GPL-3.0-or-later": PythonLicenseName("GPL-3.0-or-later", "GNU General Public License v3 or later (GPLv3+)"),
208 "AGPL-3.0-only": PythonLicenseName("AGPL-3.0-only", "GNU Affero General Public License v3"),
209 "AGPL-3.0-or-later": PythonLicenseName("AGPL-3.0-or-later", "GNU Affero General Public License v3 or later (AGPLv3+)"),
210}
213#: Mapping of SPDX identifiers to the page where the licensor publishes the license.
214#:
215#: Sourced from the SPDX License List's own ``seeAlso`` field, or from the licensor's domain where SPDX names none.
216#: A license whose only published home is its OSI page has **no entry** - that URL is
217#: :attr:`License.OSIURL` and isn't repeated here, e.g. ``MIT`` or ``BSD-3-Clause``.
218LICENSE_URLS: dict[str, str] = {
219 "Apache-2.0": "https://www.apache.org/licenses/LICENSE-2.0",
220 "ISC": "https://www.isc.org/licenses/",
221 "MPL-2.0": "https://www.mozilla.org/MPL/2.0/",
222 "BSL-1.0": "https://www.boost.org/doc/user-guide/bsl.html",
223 "Zlib": "https://zlib.net/zlib_license.html",
224 "PSF-2.0": "https://docs.python.org/3/license.html",
225 "Unlicense": "https://unlicense.org/",
226 "CC0-1.0": "https://creativecommons.org/publicdomain/zero/1.0/",
227 "CC-BY-4.0": "https://creativecommons.org/licenses/by/4.0/",
228 "CC-BY-SA-4.0": "https://creativecommons.org/licenses/by-sa/4.0/",
229 "CC-BY-NC-4.0": "https://creativecommons.org/licenses/by-nc/4.0/",
230 "CC-BY-ND-4.0": "https://creativecommons.org/licenses/by-nd/4.0/",
231 "CC-BY-NC-SA-4.0": "https://creativecommons.org/licenses/by-nc-sa/4.0/",
232 "CC-BY-NC-ND-4.0": "https://creativecommons.org/licenses/by-nc-nd/4.0/",
233 "EPL-1.0": "https://www.eclipse.org/legal/epl/epl-v10.html",
234 "EPL-2.0": "https://www.eclipse.org/legal/epl-2.0/",
235 "GPL-2.0-only": "https://www.gnu.org/licenses/old-licenses/gpl-2.0-standalone.html",
236 "GPL-2.0-or-later": "https://www.gnu.org/licenses/old-licenses/gpl-2.0-standalone.html",
237 "LGPL-2.1-only": "https://www.gnu.org/licenses/old-licenses/lgpl-2.1-standalone.html",
238 "LGPL-2.1-or-later": "https://www.gnu.org/licenses/old-licenses/lgpl-2.1-standalone.html",
239 "GPL-3.0-only": "https://www.gnu.org/licenses/gpl-3.0-standalone.html",
240 "GPL-3.0-or-later": "https://www.gnu.org/licenses/gpl-3.0-standalone.html",
241 "LGPL-3.0-only": "https://www.gnu.org/licenses/lgpl-3.0-standalone.html",
242 "LGPL-3.0-or-later": "https://www.gnu.org/licenses/lgpl-3.0-standalone.html",
243 "AGPL-3.0-only": "https://www.gnu.org/licenses/agpl-3.0-standalone.html",
244 "AGPL-3.0-or-later": "https://www.gnu.org/licenses/agpl-3.0-standalone.html",
245}
248#: Mapping of SPDX identifiers to the license text, by the file extension it is published as.
249#:
250#: Keys are the extension without its dot - ``txt``, ``md``, ``rst``, ``tex``. What a license offers is entirely up
251#: to its licensor: the GNU licenses publish three formats - the GPL-3.0 a fourth, ``rst`` -, most publish one, and
252#: several publish none at all beyond an HTML page, which is :data:`LICENSE_URLS`.
253#:
254#: Every URL here answered an HTTP request with the license, so the table is what is **known** rather than a claim
255#: of completeness - a license absent from it may still publish a text nobody has looked up yet.
256LICENSE_TEXT_URLS: dict[str, dict[str, str]] = {
257 "Apache-2.0": {"txt": "https://www.apache.org/licenses/LICENSE-2.0.txt"},
258 "MPL-2.0": {"txt": "https://www.mozilla.org/media/MPL/2.0/index.txt"},
259 "BSL-1.0": {"txt": "https://www.boost.org/LICENSE_1_0.txt"},
260 "Unlicense": {"txt": "https://unlicense.org/UNLICENSE"},
261 "CC0-1.0": {"txt": "https://creativecommons.org/publicdomain/zero/1.0/legalcode.txt"},
262 "CC-BY-4.0": {"txt": "https://creativecommons.org/licenses/by/4.0/legalcode.txt"},
263 "CC-BY-SA-4.0": {"txt": "https://creativecommons.org/licenses/by-sa/4.0/legalcode.txt"},
264 "CC-BY-NC-4.0": {"txt": "https://creativecommons.org/licenses/by-nc/4.0/legalcode.txt"},
265 "CC-BY-ND-4.0": {"txt": "https://creativecommons.org/licenses/by-nd/4.0/legalcode.txt"},
266 "CC-BY-NC-SA-4.0": {"txt": "https://creativecommons.org/licenses/by-nc-sa/4.0/legalcode.txt"},
267 "CC-BY-NC-ND-4.0": {"txt": "https://creativecommons.org/licenses/by-nc-nd/4.0/legalcode.txt"},
268 "EPL-2.0": {"txt": "https://www.eclipse.org/org/documents/epl-2.0/EPL-2.0.txt"},
269 "GPL-2.0-only": {
270 "txt": "https://www.gnu.org/licenses/old-licenses/gpl-2.0.txt",
271 "md": "https://www.gnu.org/licenses/old-licenses/gpl-2.0.md",
272 "tex": "https://www.gnu.org/licenses/old-licenses/gpl-2.0.tex",
273 },
274 "GPL-2.0-or-later": {
275 "txt": "https://www.gnu.org/licenses/old-licenses/gpl-2.0.txt",
276 "md": "https://www.gnu.org/licenses/old-licenses/gpl-2.0.md",
277 "tex": "https://www.gnu.org/licenses/old-licenses/gpl-2.0.tex",
278 },
279 "LGPL-2.1-only": {
280 "txt": "https://www.gnu.org/licenses/old-licenses/lgpl-2.1.txt",
281 "md": "https://www.gnu.org/licenses/old-licenses/lgpl-2.1.md",
282 "tex": "https://www.gnu.org/licenses/old-licenses/lgpl-2.1.tex",
283 },
284 "LGPL-2.1-or-later": {
285 "txt": "https://www.gnu.org/licenses/old-licenses/lgpl-2.1.txt",
286 "md": "https://www.gnu.org/licenses/old-licenses/lgpl-2.1.md",
287 "tex": "https://www.gnu.org/licenses/old-licenses/lgpl-2.1.tex",
288 },
289 "GPL-3.0-only": {
290 "txt": "https://www.gnu.org/licenses/gpl-3.0.txt",
291 "md": "https://www.gnu.org/licenses/gpl-3.0.md",
292 "rst": "https://www.gnu.org/licenses/gpl-3.0.rst",
293 "tex": "https://www.gnu.org/licenses/gpl-3.0.tex",
294 },
295 "GPL-3.0-or-later": {
296 "txt": "https://www.gnu.org/licenses/gpl-3.0.txt",
297 "md": "https://www.gnu.org/licenses/gpl-3.0.md",
298 "rst": "https://www.gnu.org/licenses/gpl-3.0.rst",
299 "tex": "https://www.gnu.org/licenses/gpl-3.0.tex",
300 },
301 "LGPL-3.0-only": {
302 "txt": "https://www.gnu.org/licenses/lgpl-3.0.txt",
303 "md": "https://www.gnu.org/licenses/lgpl-3.0.md",
304 "tex": "https://www.gnu.org/licenses/lgpl-3.0.tex",
305 },
306 "LGPL-3.0-or-later": {
307 "txt": "https://www.gnu.org/licenses/lgpl-3.0.txt",
308 "md": "https://www.gnu.org/licenses/lgpl-3.0.md",
309 "tex": "https://www.gnu.org/licenses/lgpl-3.0.tex",
310 },
311 "AGPL-3.0-only": {
312 "txt": "https://www.gnu.org/licenses/agpl-3.0.txt",
313 "md": "https://www.gnu.org/licenses/agpl-3.0.md",
314 "tex": "https://www.gnu.org/licenses/agpl-3.0.tex",
315 },
316 "AGPL-3.0-or-later": {
317 "txt": "https://www.gnu.org/licenses/agpl-3.0.txt",
318 "md": "https://www.gnu.org/licenses/agpl-3.0.md",
319 "tex": "https://www.gnu.org/licenses/agpl-3.0.tex",
320 },
321}
323#: Mapping of SPDX identifiers to the license's page at the
324#: `Open Source Initiative <https://opensource.org/licenses>`__.
325#:
326#: OSI's slugs don't follow the SPDX identifier and can't be derived from it: ``PSF-2.0`` is published as
327#: ``Python-2.0``, and OSI has **one** page per license where SPDX has two identifiers - ``GPL-2.0-only`` and
328#: ``GPL-2.0-or-later`` both point at it, because *only* versus *or later* is SPDX's distinction, not OSI's.
329#:
330#: A license OSI hasn't approved has no entry, which is why ``CC0-1.0`` and the Creative Commons licenses are absent.
331OSI_LICENSE_URLS: dict[str, str] = {
332 "0BSD": "https://opensource.org/license/0bsd",
333 "Apache-2.0": "https://opensource.org/license/apache-2.0",
334 "BSD-2-Clause": "https://opensource.org/license/bsd-2-clause",
335 "BSD-3-Clause": "https://opensource.org/license/bsd-3-clause",
336 "MIT-0": "https://opensource.org/license/mit-0",
337 "MIT-CMU": "https://opensource.org/license/cmu-license",
338 "MIT": "https://opensource.org/license/mit",
339 "ISC": "https://opensource.org/license/isc",
340 "MPL-2.0": "https://opensource.org/license/mpl-2.0",
341 "BSL-1.0": "https://opensource.org/license/bsl-1.0",
342 "Zlib": "https://opensource.org/license/zlib",
343 "PSF-2.0": "https://opensource.org/license/Python-2.0",
344 "Unlicense": "https://opensource.org/license/unlicense",
345 "EPL-1.0": "https://opensource.org/license/epl-1.0",
346 "EPL-2.0": "https://opensource.org/license/epl-2.0",
347 "LGPL-2.1-only": "https://opensource.org/license/lgpl-2-1",
348 "LGPL-2.1-or-later": "https://opensource.org/license/lgpl-2-1",
349 "LGPL-3.0-only": "https://opensource.org/license/lgpl-3-0",
350 "LGPL-3.0-or-later": "https://opensource.org/license/lgpl-3-0",
351 "GPL-2.0-only": "https://opensource.org/license/gpl-2.0",
352 "GPL-2.0-or-later": "https://opensource.org/license/gpl-2.0",
353 "GPL-3.0-only": "https://opensource.org/license/gpl-3.0",
354 "GPL-3.0-or-later": "https://opensource.org/license/gpl-3.0",
355 "AGPL-3.0-only": "https://opensource.org/license/agpl-3-0",
356 "AGPL-3.0-or-later": "https://opensource.org/license/agpl-3-0",
357}
360@export
361class License(metaclass=ExtendedType, slots=True):
362 """Representation of a license."""
364 _spdxIdentifier: str #: Unique SPDX identifier.
365 _name: str #: Name of the license.
366 _osiApproved: bool #: OSI approval status
367 _fsfApproved: bool #: FSF approval status
369 def __init__(self, spdxIdentifier: str, name: str, osiApproved: bool = False, fsfApproved: bool = False) -> None:
370 """
371 Initialize a license with its SPDX identifier, its name and its approval flags.
373 :param spdxIdentifier: SPDX identifier of the license.
374 :param name: Name of the license.
375 :param osiApproved: Optional, ``True``, if the license is approved by the Open Source Initiative.
376 :param fsfApproved: Optional, ``True``, if the license is approved by the Free Software Foundation.
377 """
378 self._spdxIdentifier = spdxIdentifier
379 self._name = name
380 self._osiApproved = osiApproved
381 self._fsfApproved = fsfApproved
383 @readonly
384 def Name(self) -> str:
385 """
386 Returns the license' name.
388 :returns: License name.
389 """
390 return self._name
392 @readonly
393 def SPDXIdentifier(self) -> str:
394 """
395 Returns the license' unique `SPDX identifier <https://spdx.org/licenses/>`__.
397 :returns: The unique SPDX identifier.
398 """
399 return self._spdxIdentifier
401 @readonly
402 def SPDXURL(self) -> str:
403 """
404 Returns the URL of this license's page in the `SPDX License List <https://spdx.org/licenses/>`__.
406 SPDX publishes one page per identifier at a fixed address, so this is derived from
407 :attr:`SPDXIdentifier` rather than stored. A license whose identifier isn't on that list has no page there,
408 and the derived URL won't resolve.
410 :returns: URL of the license's page at SPDX.
411 """
412 return f"https://spdx.org/licenses/{self._spdxIdentifier}.html"
414 @readonly
415 def URL(self) -> Nullable[str]:
416 """
417 Returns the page where the licensor publishes this license.
419 This is looked up in :data:`LICENSE_URLS`. It is the licensor's own page - ``https://www.apache.org/licenses/
420 LICENSE-2.0`` for the Apache License 2.0 - which is not the same thing as :attr:`SPDXURL` or :attr:`OSIURL`,
421 the two catalogue entries describing it.
423 A license whose only published home *is* its OSI page has no entry here rather than a duplicate of
424 :attr:`OSIURL`; ``MIT``, ``BSD-2-Clause`` and ``BSD-3-Clause`` are those.
426 :returns: URL of the license's own page, or ``None`` if the licensor publishes none.
428 .. seealso::
430 :attr:`TextURLs`
431 |rarr| The same license as text, by format.
432 """
433 return LICENSE_URLS.get(self._spdxIdentifier, None)
435 @readonly
436 def TextURLs(self) -> dict[str, str]:
437 """
438 Returns the URLs of this license's text, keyed by the format it is published as.
440 Keys are the file extension without its dot: ``txt``, ``md``, ``rst``, ``tex``. Which formats exist is the
441 licensor's choice - the GNU licenses publish four, most publish one, and several publish none.
443 The table is what is **known**, not a claim of completeness: an empty mapping means no URL is recorded, not
444 that the licensor publishes no text. Every URL in it answered an HTTP request with the license.
446 .. code-block:: python
448 Apache_2_0_License.TextURLs["txt"] # https://www.apache.org/licenses/LICENSE-2.0.txt
449 GPL_3_0_only.TextURLs["rst"] # https://www.gnu.org/licenses/gpl-3.0.rst
451 A **copy** is returned, so a caller can't edit :data:`LICENSE_TEXT_URLS` through it.
453 :returns: The license text's URLs by format, or an empty dictionary if none is published.
454 """
455 return dict(LICENSE_TEXT_URLS.get(self._spdxIdentifier, {}))
457 @readonly
458 def OSIURL(self) -> Nullable[str]:
459 """
460 Returns the URL of this license's page at the `Open Source Initiative <https://opensource.org/licenses>`__.
462 This is looked up in :data:`OSI_LICENSE_URLS` rather than derived: OSI's slugs don't follow the SPDX
463 identifier, and OSI has one page where SPDX has two identifiers - ``GPL-2.0-only`` and ``GPL-2.0-or-later``
464 share it.
466 :returns: URL of the license's page at OSI, or ``None`` if OSI doesn't publish it.
468 .. seealso::
470 :attr:`OSIApproved`
471 |rarr| Whether OSI approved this license at all.
472 """
473 return OSI_LICENSE_URLS.get(self._spdxIdentifier, None)
475 @readonly
476 def OSIApproved(self) -> bool:
477 """
478 Returns true, if the license is approved by OSI (`Open Source Initiative <https://opensource.org/>`__).
480 :returns: ``True``, if the license is approved by the Open Source Initiative.
481 """
482 return self._osiApproved
484 @readonly
485 def FSFApproved(self) -> bool:
486 """
487 Returns true, if the license is approved by FSF (`Free Software Foundation <https://www.fsf.org/>`__).
489 :returns: ``True``, if the license is approved by the Free Software Foundation.
490 """
491 return self._fsfApproved
493 @readonly
494 def PythonLicenseName(self) -> str:
495 """
496 Returns the Python license name for this license if it's defined.
498 :returns: The Python license name.
499 :raises ValueError: If there is no license name defined for the license. |br| (See and check :data:`~pyTooling.Licensing.PYTHON_LICENSE_NAMES`)
500 """
501 try:
502 item: PythonLicenseName = PYTHON_LICENSE_NAMES[self._spdxIdentifier]
503 except KeyError as ex:
504 raise ValueError("License has no Python specify information.") from ex
506 return item.ShortName
508 @readonly
509 def PythonClassifier(self) -> str:
510 """
511 Returns the Python package classifier for this license if it's defined.
513 :returns: The Python package classifier.
514 :raises ValueError: If there is no classifier defined for the license. |br| (See and check :data:`~pyTooling.Licensing.PYTHON_LICENSE_NAMES`)
516 .. seealso::
518 List of `Python classifiers <https://pypi.org/classifiers/>`__
519 """
520 try:
521 item: PythonLicenseName = PYTHON_LICENSE_NAMES[self._spdxIdentifier]
522 except KeyError as ex:
523 raise ValueError("License has no Python specify information.") from ex
525 osi = "OSI Approved :: " if self._osiApproved else ""
526 return f"License :: {osi}{item.Classifier}"
528 def __eq__(self, other: Any) -> bool:
529 """
530 Returns true, if both licenses are identical (comparison based on SPDX identifiers).
532 :param other: The second operand to compare with. A :class:`License` or its SPDX identifier as a string.
533 :returns: ``True``, if both licenses are identical.
534 :raises TypeError: If second operand is not of type :class:`License` or string.
535 """
536 if isinstance(other, License):
537 return self._spdxIdentifier == other._spdxIdentifier
538 elif isinstance(other, str):
539 return self._spdxIdentifier == other
540 else:
541 ex = TypeError("Second operand is not supported by equal operator.")
542 ex.add_note(f"Got type '{getFullyQualifiedName(other)}'.")
543 ex.add_note("Supported types for second operand: License, str")
544 raise ex
546 def __ne__(self, other: Any) -> bool:
547 """
548 Returns true, if both licenses are not identical (comparison based on SPDX identifiers).
550 :param other: The second operand to compare with. A :class:`License` or its SPDX identifier as a string.
551 :returns: ``True``, if both licenses are not identical.
552 :raises TypeError: If second operand is not of type :class:`License` or string.
553 """
554 if isinstance(other, License):
555 return self._spdxIdentifier != other._spdxIdentifier
556 elif isinstance(other, str):
557 return self._spdxIdentifier != other
558 else:
559 ex = TypeError("Second operand is not supported by unequal operator.")
560 ex.add_note(f"Got type '{getFullyQualifiedName(other)}'.")
561 ex.add_note("Supported types for second operand: License, str")
562 raise ex
564 def __hash__(self) -> int:
565 """
566 Compute a hash from the license's SPDX identifier.
568 A license compares equal to its identifier as a string, so the two hash equally as well.
570 :returns: Hash of the SPDX identifier.
571 """
572 return hash(self._spdxIdentifier)
574 def __le__(self, other: Any) -> bool:
575 """
576 Returns true, if both licenses are compatible.
578 :param other: Second operand, the license to compare with.
579 :returns: ``True``, if both licenses are compatible.
580 :raises NotImplementedError: License compatibility is not implemented yet.
581 """
582 raise NotImplementedError("License compatibility check is not yet implemented.")
584 def __ge__(self, other: Any) -> bool:
585 """
586 Returns true, if both licenses are compatible.
588 :param other: Second operand, the license to compare with.
589 :returns: ``True``, if both licenses are compatible.
590 :raises NotImplementedError: License compatibility is not implemented yet.
591 """
592 raise NotImplementedError("License compatibility check is not yet implemented.")
594 def __repr__(self) -> str:
595 """
596 Returns the internal unique representation (:attr:`spdxIdentifier`).
598 :returns: SPDX identifier of the license.
599 """
600 return self._spdxIdentifier
602 def __str__(self) -> str:
603 """
604 Returns the license' name.
606 :returns: Name of the license.
607 """
608 return self._name
611Zero_Clause_BSD = License("0BSD", "BSD Zero Clause License", True, False)
612Apache_2_0_License = License("Apache-2.0", "Apache License 2.0", True, True)
613BSD_2_Clause_License = License("BSD-2-Clause", "BSD 2-Clause Simplified License", True, True)
614BSD_3_Clause_License = License("BSD-3-Clause", "BSD 3-Clause Revised License", True, True)
615MIT_License = License("MIT", "MIT License", True, True)
616MIT_0_License = License("MIT-0", "MIT No Attribution", True, False)
617MIT_CMU_License = License("MIT-CMU", "CMU License", True, False)
618ISC_License = License("ISC", "ISC License", True, True)
619MPL_2_0_License = License("MPL-2.0", "Mozilla Public License 2.0", True, True)
620BSL_1_0_License = License("BSL-1.0", "Boost Software License 1.0", True, True)
621Zlib_License = License("Zlib", "zlib License", True, True)
622PSF_2_0_License = License("PSF-2.0", "Python Software Foundation License 2.0", True, True)
623Unlicense = License("Unlicense", "The Unlicense", True, True)
624CC0_1_0 = License("CC0-1.0", "Creative Commons Zero v1.0 Universal", False, True)
625EPL_1_0_License = License("EPL-1.0", "Eclipse Public License 1.0", True, True)
626EPL_2_0_License = License("EPL-2.0", "Eclipse Public License 2.0", True, True)
627LGPL_2_1_only = License("LGPL-2.1-only", "GNU Lesser General Public License v2.1 only", True, True)
628LGPL_2_1_or_later = License("LGPL-2.1-or-later", "GNU Lesser General Public License v2.1 or later", True, True)
629LGPL_3_0_only = License("LGPL-3.0-only", "GNU Lesser General Public License v3.0 only", True, True)
630LGPL_3_0_or_later = License("LGPL-3.0-or-later", "GNU Lesser General Public License v3.0 or later", True, True)
631GPL_2_0_only = License("GPL-2.0-only", "GNU General Public License v2.0 only", True, True)
632GPL_2_0_or_later = License("GPL-2.0-or-later", "GNU General Public License v2.0 or later", True, True)
633GPL_3_0_only = License("GPL-3.0-only", "GNU General Public License v3.0 only", True, True)
634GPL_3_0_or_later = License("GPL-3.0-or-later", "GNU General Public License v3.0 or later", True, True)
635AGPL_3_0_only = License("AGPL-3.0-only", "GNU Affero General Public License v3.0 only", True, True)
636AGPL_3_0_or_later = License("AGPL-3.0-or-later", "GNU Affero General Public License v3.0 or later", True, True)
638CC_BY_4_0 = License("CC-BY-4.0", "Creative Commons Attribution 4.0 International", False, True)
639CC_BY_SA_4_0 = License("CC-BY-SA-4.0", "Creative Commons Attribution Share Alike 4.0 International", False, True)
640CC_BY_NC_4_0 = License("CC-BY-NC-4.0", "Creative Commons Attribution Non Commercial 4.0 International", False, False)
641CC_BY_ND_4_0 = License("CC-BY-ND-4.0", "Creative Commons Attribution No Derivatives 4.0 International", False, False)
642CC_BY_NC_SA_4_0 = License(
643 "CC-BY-NC-SA-4.0", "Creative Commons Attribution Non Commercial Share Alike 4.0 International", False, False
644)
645CC_BY_NC_ND_4_0 = License(
646 "CC-BY-NC-ND-4.0", "Creative Commons Attribution Non Commercial No Derivatives 4.0 International", False, False
647)
650#: All predefined licenses, in the order they are defined above.
651LICENSES: tuple[License, ...] = (
652 Zero_Clause_BSD, Apache_2_0_License, BSD_2_Clause_License, BSD_3_Clause_License, MIT_License, MIT_0_License,
653 MIT_CMU_License, ISC_License, MPL_2_0_License,
654 BSL_1_0_License, Zlib_License, PSF_2_0_License, Unlicense, CC0_1_0, EPL_1_0_License, EPL_2_0_License,
655 LGPL_2_1_only, LGPL_2_1_or_later, LGPL_3_0_only, LGPL_3_0_or_later,
656 GPL_2_0_only, GPL_2_0_or_later, GPL_3_0_only, GPL_3_0_or_later,
657 AGPL_3_0_only, AGPL_3_0_or_later,
658 CC_BY_4_0, CC_BY_SA_4_0, CC_BY_NC_4_0, CC_BY_ND_4_0, CC_BY_NC_SA_4_0, CC_BY_NC_ND_4_0,
659)
661#: Mapping of predefined licenses, indexed by their SPDX identifier.
662SPDX_INDEX: dict[str, License] = {spdxLicense.SPDXIdentifier: spdxLicense for spdxLicense in LICENSES}
665@export
666def buildClassifierIndex() -> dict[str, tuple[License, ...]]:
667 """
668 Index the predefined licenses by the Python classifier they are published as.
670 A license without a classifier is skipped rather than reported: :attr:`License.PythonClassifier` raises for it,
671 and a license the Python ecosystem has no classifier for simply can't be found that way.
673 The licenses indexed are :data:`LICENSES`, and :data:`LICENSES_BY_CLASSIFIER` holds what this returns, built
674 once when the module is imported. Reading that constant is the usual way to ask; calling this rebuilds it.
676 :returns: Every classifier, mapped to the licenses it can mean.
677 """
678 index: dict[str, list[License]] = {}
679 for spdxLicense in LICENSES:
680 try:
681 classifier = spdxLicense.PythonClassifier
682 except ValueError:
683 continue
685 index.setdefault(classifier, []).append(spdxLicense)
687 return {classifier: tuple(licenses) for classifier, licenses in index.items()}
690#: Mapping of a Python license classifier to the licenses it can mean.
691#:
692#: The mapping is one-to-one except for ``License :: OSI Approved :: BSD License``, which means either
693#: :data:`BSD_2_Clause_License` or :data:`BSD_3_Clause_License` with nothing in the classifier to tell them apart.
694LICENSES_BY_CLASSIFIER: dict[str, tuple[License, ...]] = buildClassifierIndex()
697#: The :class:`License` class under a name no expression node shadows with a property of its own.
698_LicenseType = License
701@export
702@abstractclass
703class LicenseExpression(metaclass=ExtendedType, slots=True):
704 """
705 Base-class of every node in an `SPDX license expression`_ tree.
707 .. _SPDX license expression: https://spdx.github.io/spdx-spec/v2.3/SPDX-license-expressions/
709 An expression is a tree. Its operators - every node that is applied to operands, and the only kind of node that
710 can be a :attr:`Parent` - derive from :class:`Operator`:
712 :class:`OrLaterOperator`
713 |rarr| the ``+`` suffix: the named license or any later version of it.
714 :class:`WithOperator`
715 |rarr| ``WITH``: a license together with an exception to it.
716 :class:`AndOperator`
717 |rarr| ``AND``: both licenses apply.
718 :class:`OrOperator`
719 |rarr| ``OR``: either license applies.
721 Its leaves are:
723 :class:`SPDXLicense`
724 |rarr| a license on the SPDX License List, named by its identifier.
725 :class:`LicenseReference`
726 |rarr| a license that is *not* on that list, written as ``LicenseRef-<id>``.
727 :class:`LicenseException`
728 |rarr| an exception from the SPDX exception list, the right operand of ``WITH``.
730 The SPDX grammar defines:
732 .. code-block:: text
734 simple-expression = license-id | license-id "+" | license-ref
735 compound-expression = ( simple-expression
736 | simple-expression "WITH" license-exception-id
737 | compound-expression "AND" compound-expression
738 | compound-expression "OR" compound-expression
739 | "(" compound-expression ")" )
741 There are three binary operators, one unary one, and parentheses. There is **no negation** - an expression
742 says which licenses apply, never which don't.
744 Every node knows its :attr:`Parent` and its :attr:`Root`, which is what a :class:`License` can't carry: the
745 predefined licenses are shared objects, so :data:`MIT_License` appears in many expressions at once and belongs to
746 none of them. :class:`SPDXLicense` is the wrapper that gives a license object a placeholder in the expression tree.
748 A tree is built bottom-up by handing the operands to an operator, or by assigning them to the operator's operand
749 slots afterwards. Both link the operand back to its operator:
751 .. code-block:: python
753 bottomUp = AndOperator(SPDXLicense(Apache_2_0_License), SPDXLicense(MIT_License))
755 assembled = AndOperator()
756 assembled.Left = SPDXLicense(Apache_2_0_License)
757 assembled.Right = SPDXLicense(MIT_License)
759 The ``parent`` parameter only *records* a parent - it can't know which slot the operand belongs in, so it never
760 fills one.
761 """
763 PRECEDENCE: ClassVar[int] = 0 #: Precedence of this node's operator; a lower value binds tighter.
765 _parent: Nullable["Operator"] #: The operator this expression is an operand of, or ``None`` at the root.
766 _root: "LicenseExpression" #: The outermost expression this node belongs to; ``self`` at the root.
767 _originalText: str #: The text this expression stands for, on the root; empty if there is none.
769 def __init__(self, parent: Nullable["Operator"] = None, originalText: str = "") -> None:
770 """
771 Initialize an expression node.
773 :param parent: Optional, the operator this node becomes an operand of.
774 :param originalText: Optional, the text this expression stands for. :meth:`Parse` fills it in; a node built
775 in code carries it when the caller knows what was written.
776 :raises TypeError: If parameter 'parent' is not of type :class:`Operator`.
777 """
778 self._originalText = originalText
780 if parent is None:
781 self._parent = None
782 self._root = self
783 elif not isinstance(parent, Operator):
784 ex = TypeError("Parameter 'parent' is not an Operator.")
785 ex.add_note(f"Got type '{getFullyQualifiedName(parent)}'.")
786 raise ex
787 else:
788 self._parent = parent
789 self._root = parent._root
791 @property
792 def Parent(self) -> Nullable["Operator"]:
793 """
794 Property to access the operator this expression is an operand of (:attr:`_parent`).
796 Assigning an operator records it as this node's parent and re-roots this node and everything below it to that
797 operator's :attr:`Root`. The operator's operands are not changed by the assignment.
799 :returns: The parent operator, or ``None`` if this node is the root.
800 :raises ValueError: If ``None`` is assigned. |br|
801 A node that is an operand of an operator can't be detached from it.
802 :raises TypeError: If an object that is not an :class:`Operator` is assigned. |br|
803 A leaf is applied to nothing, so it can never be a parent.
804 """
805 return self._parent
807 @Parent.setter
808 def Parent(self, parent: Nullable["Operator"]) -> None:
809 if parent is None:
810 raise ValueError("Parameter 'parent' is None.")
811 elif not isinstance(parent, Operator): 811 ↛ 812line 811 didn't jump to line 812 because the condition on line 811 was never true
812 ex = TypeError("Parameter 'parent' is not an Operator.")
813 ex.add_note(f"Got type '{getFullyQualifiedName(parent)}'.")
814 raise ex
816 self._parent = parent
818 for expression in self.IterateExpression():
819 expression._root = parent._root
821 @readonly
822 def OriginalText(self) -> str:
823 """
824 Read-only property to access the text this expression stands for (:attr:`_originalText`).
826 A tree is parsed from one string, so **every node answers with the root's** - reading it from a leaf gives the
827 whole expression, not that leaf's fragment. :meth:`__str__` re-renders the tree canonically, which is not
828 always what was written: ``Apache-2.0 or MIT`` parses and renders as ``Apache-2.0 OR MIT``. This is what was
829 written.
831 :meth:`Parse` fills it in. A node **built** in code carries it when the caller knows what was stated and the
832 expression can't be parsed from it - an :class:`UnknownLicense` standing for a license that didn't resolve
833 keeps the text that didn't, and a :class:`ProprietaryLicense` keeps the classifier it was built from.
835 :returns: The text this expression stands for, or an empty string if there is none.
836 """
837 return self._root._originalText
839 @readonly
840 def Root(self) -> "LicenseExpression":
841 """
842 Read-only property to access the outermost expression this node belongs to (:attr:`_root`).
844 The root is maintained by :attr:`Parent`: adopting a node re-roots it and everything below it, so the field
845 can't fall behind the tree it describes.
847 :returns: The root of the expression tree, which is the node itself if it has no parent.
848 """
849 return self._root
851 def IterateExpression(self) -> Generator["LicenseExpression", None, None]:
852 """
853 Iterate this expression depth-first, in the order its nodes are written.
855 A leaf is the expression itself. The operators override this to yield their operands around themselves, so an
856 infix operator comes between its two operands and the ``+`` suffix comes after the expression it applies to.
858 :returns: A generator of every node in this expression, this node included.
859 """
860 yield self
862 @classmethod
863 def Parse(cls, expression: str) -> "LicenseExpression":
864 """
865 Parse an SPDX license expression into a tree of expression nodes.
867 Operator precedence is the one SPDX defines - ``+`` binds tighter than ``WITH``, which binds tighter than
868 ``AND``, which binds tighter than ``OR`` - and parentheses override it. ``AND`` and ``OR`` associate to the
869 left.
871 :param expression: The SPDX license expression to parse.
872 :returns: The root of the parsed expression tree.
873 :raises LicenseExpressionError: If the expression is empty, malformed, or names a license that isn't known.
874 """
875 parser = _LicenseExpressionParser(expression)
876 root = parser.Parse()
877 root._originalText = expression
879 return root
881 @abstractmethod
882 def __str__(self) -> str: # type: ignore[empty-body]
883 """
884 Return this expression in SPDX syntax.
886 Parentheses are written only where the default precedence would otherwise read the expression differently, so
887 a parsed expression renders back to its shortest correct form rather than a fully bracketed one.
889 :returns: The expression in SPDX syntax.
890 """
893@export
894@abstractclass
895class Operator(LicenseExpression):
896 """
897 Base-class of every expression node that is applied to operands.
899 Operator kinds:
901 * :class:`UnaryOperator` takes one operand
902 * :class:`BinaryOperator` takes two operands.
904 Only an operator can be an operand's :attr:`~LicenseExpression.Parent`.
905 """
908@export
909@abstractclass
910class BaseLicense(LicenseExpression):
911 """
912 Base-class of every expression node that names a license the work is under.
914 Four nodes are one:
916 :class:`SPDXLicense`
917 |rarr| a license on the SPDX License List, named by its identifier.
918 :class:`LicenseReference`
919 |rarr| a license that is not on that list, written as ``LicenseRef-<id>``.
920 :class:`ProprietaryLicense`
921 |rarr| a license that isn't published at all; a :class:`LicenseReference` with a fixed identifier.
922 :class:`UnknownLicense`
923 |rarr| SPDX's ``NONE`` or ``NOASSERTION`` - no license is named.
925 They have no common representation - SPDX knows the first as a :class:`License` object and the rest not at all -
926 so this class is what lets them be collected and reported together:
928 .. code-block:: python
930 [term.Identifier for term in expression.IterateExpression() if isinstance(term, BaseLicense)]
931 # ['MIT', 'LicenseRef-Proprietary'] for 'MIT AND LicenseRef-Proprietary'
933 :class:`LicenseException` is **not** one of these. The right operand of ``WITH`` is an exception granted from a
934 license, not a license the work is under.
935 """
937 @readonly
938 @abstractmethod
939 def Identifier(self) -> str: # type: ignore[empty-body]
940 """
941 Read-only property to return the identifier naming this license.
943 :returns: The SPDX identifier, or the ``LicenseRef-`` reference, in the spelling an expression writes it.
944 """
946 @readonly
947 @abstractmethod
948 def Name(self) -> str: # type: ignore[empty-body]
949 """
950 Read-only property to return the license' name, as it is written for a reader.
952 :attr:`Identifier` is what an expression writes; this is what a document prints. For a license on the SPDX
953 List the two differ - ``Apache-2.0`` against ``Apache License 2.0`` - and for the rest they are close or
954 equal, because nothing else has a name SPDX publishes.
956 :returns: The license' name.
957 """
960@export
961class SPDXLicense(BaseLicense):
962 """
963 A single license in an expression, named by its SPDX identifier.
965 The identifiers an expression may name are the `SPDX License List <https://spdx.org/licenses/>`__; the ones this
966 package predefines are :data:`LICENSES`, and :attr:`License` reaches the :class:`License` object holding a
967 license's name, approval flags and the URLs of its text. A license the list defines but this package doesn't
968 predefine is a :class:`LicenseReference` when written as ``LicenseRef-``, and otherwise doesn't parse.
969 """
971 _license: _LicenseType #: The well-known license this node refers to.
973 def __init__(self, spdxLicense: _LicenseType, parent: Nullable[Operator] = None) -> None:
974 """
975 Initialize a reference to an SPDX license.
977 :param spdxLicense: The license this node stands for.
978 :param parent: Optional, the operator this node becomes an operand of.
979 :raises TypeError: If parameter 'parent' is not of type :class:`Operator`.
980 :raises ValueError: If parameter 'spdxLicense' is None.
981 :raises TypeError: If parameter 'spdxLicense' is not of type :class:`License`.
982 """
983 super().__init__(parent)
985 if spdxLicense is None: 985 ↛ 986line 985 didn't jump to line 986 because the condition on line 985 was never true
986 raise ValueError("Parameter 'spdxLicense' is None.")
987 elif not isinstance(spdxLicense, _LicenseType):
988 ex = TypeError("Parameter 'spdxLicense' is not a License.")
989 ex.add_note(f"Got type '{getFullyQualifiedName(spdxLicense)}'.")
990 raise ex
992 self._license = spdxLicense
994 @readonly
995 def License(self) -> _LicenseType:
996 """
997 Read-only property to access the license this node stands for (:attr:`_license`).
999 :returns: The license.
1000 """
1001 return self._license
1003 @readonly
1004 def Identifier(self) -> str:
1005 """
1006 Read-only property to access the license' SPDX identifier (:pycode:`_license._spdxIdentifier`).
1008 :returns: The license's SPDX identifier.
1009 """
1010 return self._license._spdxIdentifier
1012 @readonly
1013 def Name(self) -> str:
1014 """
1015 Read-only property to access the license' name (:pycode:`_license._name`).
1017 :returns: The name SPDX publishes for this license, like ``Apache License 2.0``.
1018 """
1019 return self._license._name
1021 def __str__(self) -> str:
1022 """
1023 Return the license's SPDX identifier.
1025 :returns: The SPDX identifier.
1026 """
1027 return self._license.SPDXIdentifier
1030@export
1031class LicenseReference(BaseLicense):
1032 """
1033 A license that isn't on the SPDX License List, written as ``LicenseRef-<id>``.
1035 It may name the document it is defined in, as ``DocumentRef-<id>:LicenseRef-<id>``.
1037 There is no predefined :class:`License` object, because SPDX doesn't know the license - only the document declaring it
1038 does.
1039 """
1041 _licenseIdentifier: str #: Identifier following ``LicenseRef-``.
1042 _documentIdentifier: Nullable[str] #: Identifier following ``DocumentRef-``, if the reference names one.
1044 def __init__(
1045 self,
1046 licenseIdentifier: str,
1047 documentIdentifier: Nullable[str] = None,
1048 parent: Nullable[Operator] = None
1049 ) -> None:
1050 """
1051 Initialize a license reference.
1053 :param licenseIdentifier: Identifier following ``LicenseRef-``.
1054 :param documentIdentifier: Optional, identifier following ``DocumentRef-``.
1055 :param parent: Optional, the operator this node becomes an operand of.
1056 :raises TypeError: If parameter 'parent' is not of type :class:`Operator`.
1057 :raises ValueError: If parameter 'licenseIdentifier' is None.
1058 :raises TypeError: If parameter 'licenseIdentifier' is not of type :class:`str`.
1059 :raises ValueError: If parameter 'licenseIdentifier' is empty.
1060 :raises TypeError: If parameter 'documentIdentifier' is not of type :class:`str`.
1061 :raises ValueError: If parameter 'documentIdentifier' is empty.
1062 """
1063 super().__init__(parent)
1065 if licenseIdentifier is None: 1065 ↛ 1066line 1065 didn't jump to line 1066 because the condition on line 1065 was never true
1066 raise ValueError("Parameter 'licenseIdentifier' is None.")
1067 elif not isinstance(licenseIdentifier, str):
1068 ex = TypeError("Parameter 'licenseIdentifier' is not a string.")
1069 ex.add_note(f"Got type '{getFullyQualifiedName(licenseIdentifier)}'.")
1070 raise ex
1071 elif len(licenseIdentifier) == 0:
1072 raise ValueError("Parameter 'licenseIdentifier' is empty.")
1074 if documentIdentifier is None:
1075 pass
1076 elif not isinstance(documentIdentifier, str):
1077 ex = TypeError("Parameter 'documentIdentifier' is not a string.")
1078 ex.add_note(f"Got type '{getFullyQualifiedName(documentIdentifier)}'.")
1079 raise ex
1080 elif len(documentIdentifier) == 0:
1081 raise ValueError("Parameter 'documentIdentifier' is empty.")
1083 self._licenseIdentifier = licenseIdentifier
1084 self._documentIdentifier = documentIdentifier
1086 @readonly
1087 def LicenseIdentifier(self) -> str:
1088 """
1089 Read-only property to access the identifier following ``LicenseRef-`` (:attr:`_licenseIdentifier`).
1091 :returns: The license reference's identifier.
1092 """
1093 return self._licenseIdentifier
1095 @readonly
1096 def DocumentIdentifier(self) -> Nullable[str]:
1097 """
1098 Read-only property to access the identifier following ``DocumentRef-`` (:attr:`_documentIdentifier`).
1100 :returns: The document reference's identifier, or ``None`` if the reference names no document.
1101 """
1102 return self._documentIdentifier
1104 @readonly
1105 def Identifier(self) -> str:
1106 """
1107 Read-only property to return the reference in the spelling an expression writes it.
1109 This is the whole reference, ``DocumentRef-`` and all - :attr:`LicenseIdentifier` is the part following
1110 ``LicenseRef-`` on its own.
1112 :returns: The license reference.
1113 """
1114 return str(self)
1116 def __str__(self) -> str:
1117 """
1118 Return the reference in SPDX syntax.
1120 :returns: The license reference.
1121 """
1122 document = "" if self._documentIdentifier is None else f"DocumentRef-{self._documentIdentifier}:"
1123 return f"{document}LicenseRef-{self._licenseIdentifier}"
1125 @readonly
1126 def Name(self) -> str:
1127 """
1128 Read-only property to access the identifier following ``LicenseRef-`` (:attr:`_licenseIdentifier`).
1130 SPDX publishes no name for a license that isn't on its list, so the identifier the document chose is the
1131 only name there is.
1133 :returns: The license reference's identifier.
1134 """
1135 return self._licenseIdentifier
1138@export
1139class ProprietaryLicense(LicenseReference):
1140 """
1141 A license that is not open source and is not published under any identifier - an EULA, or a company's own terms.
1143 SPDX has no way to say this. Its list is a list of *published* licenses, and a proprietary one is by definition
1144 not on it, so the only thing SPDX offers is the generic escape hatch: this renders as ``LicenseRef-Proprietary``
1145 and **is** a :class:`LicenseReference`, because that is what it writes.
1147 .. code-block:: python
1149 str(ProprietaryLicense()) # 'LicenseRef-Proprietary'
1151 The round-trip is deliberately one-way: parsing ``LicenseRef-Proprietary`` back gives a plain
1152 :class:`LicenseReference`, because SPDX defines no convention that makes that identifier mean *proprietary*
1153 rather than being one project's choice of words. This class is constructed where something else already knows -
1154 :mod:`pyTooling.Dependency` builds one from PyPI's ``License :: Other/Proprietary License`` classifier.
1156 A proprietary license that *does* have a name of its own is a :class:`LicenseReference` with that name, not this
1157 class - ``LicenseReference("AcmeEULA-1.0")`` renders as ``LicenseRef-AcmeEULA-1.0``.
1158 """
1160 #: The identifier a proprietary license is written with, following ``LicenseRef-``.
1161 IDENTIFIER: ClassVar[str] = "Proprietary"
1163 def __init__(self, parent: Nullable[Operator] = None, originalText: str = "") -> None:
1164 """
1165 Initialize a proprietary license.
1167 :param parent: Optional, the operator this node becomes an operand of.
1168 :param originalText: Optional, what stated this - a classifier, say - since it can't have been parsed.
1169 :raises TypeError: If parameter 'parent' is not of type :class:`Operator`.
1170 """
1171 super().__init__(self.IDENTIFIER, None, parent)
1173 self._originalText = originalText
1175 @readonly
1176 def Name(self) -> str:
1177 """
1178 Read-only property to return a readable name for a license that publishes none.
1180 :returns: ``Proprietary License``.
1181 """
1182 return "Proprietary License"
1185@export
1186class UnknownLicense(BaseLicense):
1187 """
1188 SPDX's statement that no license is named: ``NONE`` or ``NOASSERTION``.
1190 The two are different claims - :attr:`LicenseAbsence.NoLicense` says the work states that no license applies,
1191 :attr:`LicenseAbsence.NoAssertion` says someone looked and declined to say - so which one is asked with
1192 :attr:`Absence`.
1194 .. code-block:: python
1196 LicenseExpression.Parse("NOASSERTION") # UnknownLicense(LicenseAbsence.NoAssertion)
1197 LicenseExpression.Parse("NONE") # UnknownLicense(LicenseAbsence.NoLicense)
1199 **Neither can be an operand.** SPDX's grammar is ``simple-expression | compound-expression``; ``NONE`` and
1200 ``NOASSERTION`` are values a license *field* may hold instead of an expression, not terms inside one. So
1201 ``MIT AND NOASSERTION`` doesn't parse, and this node refuses a :attr:`~LicenseExpression.Parent`.
1203 .. seealso::
1205 :class:`ProprietaryLicense`
1206 |rarr| For a license that exists but isn't published - which is a different statement from this one.
1207 """
1209 _absence: LicenseAbsence #: Which of SPDX's two absences this node states.
1211 def __init__(self, absence: LicenseAbsence = LicenseAbsence.NoAssertion, originalText: str = "") -> None:
1212 """
1213 Initialize an absent license.
1215 No ``parent`` parameter, because neither value may be an operand.
1217 :param absence: Optional, which absence is stated. Defaults to :attr:`LicenseAbsence.NoAssertion`.
1218 :param originalText: Optional, what was stated. A license nothing could be made of keeps the text here, which
1219 is the only place left holding it.
1220 :raises TypeError: If parameter 'absence' is not of type :class:`LicenseAbsence`.
1221 """
1222 super().__init__(None, originalText)
1224 if not isinstance(absence, LicenseAbsence):
1225 ex = TypeError("Parameter 'absence' is not a LicenseAbsence.")
1226 ex.add_note(f"Got type '{getFullyQualifiedName(absence)}'.")
1227 raise ex
1229 self._absence = absence
1231 @readonly
1232 def Absence(self) -> LicenseAbsence:
1233 """
1234 Read-only property to access which absence this node states (:attr:`_absence`).
1236 :returns: The absence stated.
1237 """
1238 return self._absence
1240 @readonly
1241 def Identifier(self) -> str:
1242 """
1243 Read-only property to access the absence in SPDX's spelling (:pycode:`_absence.value`).
1245 :returns: ``NONE`` or ``NOASSERTION``.
1246 """
1247 return self._absence.value
1249 @readonly
1250 def Name(self) -> str:
1251 """
1252 Read-only property to return which absence this states, in words.
1254 :returns: ``No license`` for :attr:`~LicenseAbsence.NoLicense`, ``No assertion`` otherwise.
1255 """
1256 return "No license" if self._absence is LicenseAbsence.NoLicense else "No assertion"
1258 @readonly
1259 def Parent(self) -> Nullable[Operator]:
1260 """
1261 Read-only property to access the operator this expression is an operand of (:attr:`_parent`).
1263 Always ``None``, and **read-only** where every other node's is assignable: ``NONE`` and ``NOASSERTION`` are
1264 values a license field may hold *instead of* an expression, and SPDX's grammar has no place for either
1265 inside one. So this node is always the whole expression, and assigning a parent raises
1266 :exc:`AttributeError`.
1268 :returns: ``None``, always.
1269 """
1270 return self._parent
1272 def __str__(self) -> str:
1273 """
1274 Return the absence in SPDX's spelling.
1276 :returns: ``NONE`` or ``NOASSERTION``.
1277 """
1278 return self._absence.value
1281@export
1282class LicenseException(LicenseExpression):
1283 """
1284 The right operand of a :class:`WithOperator`, naming an exception from the SPDX exception list.
1286 It is a leaf node in the expression tree like a license, but it is not one. An exception modifies a license and can't
1287 stand on its own.
1288 """
1290 _identifier: str #: The exception's SPDX identifier.
1292 def __init__(self, identifier: str, parent: Nullable[Operator] = None) -> None:
1293 """
1294 Initialize a license exception.
1296 :param identifier: The exception's SPDX identifier.
1297 :param parent: Optional, the operator this node becomes an operand of.
1298 :raises TypeError: If parameter 'parent' is not of type :class:`Operator`.
1299 :raises ValueError: If parameter 'identifier' is None.
1300 :raises TypeError: If parameter 'identifier' is not of type :class:`str`.
1301 :raises ValueError: If parameter 'identifier' is empty.
1302 """
1303 super().__init__(parent)
1305 if identifier is None: 1305 ↛ 1306line 1305 didn't jump to line 1306 because the condition on line 1305 was never true
1306 raise ValueError("Parameter 'identifier' is None.")
1307 elif not isinstance(identifier, str):
1308 ex = TypeError("Parameter 'identifier' is not a string.")
1309 ex.add_note(f"Got type '{getFullyQualifiedName(identifier)}'.")
1310 raise ex
1311 elif len(identifier) == 0:
1312 raise ValueError("Parameter 'identifier' is empty.")
1314 self._identifier = identifier
1316 @readonly
1317 def Identifier(self) -> str:
1318 """
1319 Read-only property to access the exception's SPDX identifier (:attr:`_identifier`).
1321 :returns: The exception's identifier.
1322 """
1323 return self._identifier
1325 def __str__(self) -> str:
1326 """
1327 Return the exception's SPDX identifier.
1329 :returns: The identifier.
1330 """
1331 return self._identifier
1334@export
1335@abstractclass
1336class UnaryOperator(Operator):
1337 """
1338 Base-class of the expression operators taking one operand.
1340 SPDX defines exactly one unary operator:
1342 * the ``+`` suffix of :class:`OrLaterOperator`.
1344 The operand is reachable as :attr:`Operand` and is assignable, so an operator can be filled after it was created.
1345 """
1347 _operand: Nullable[LicenseExpression] #: The expression this operator is applied to.
1349 def __init__(
1350 self,
1351 operand: Nullable[LicenseExpression] = None,
1352 parent: Nullable[Operator] = None
1353 ) -> None:
1354 """
1355 Initialize a unary operator with its operand.
1357 :param operand: Optional, the expression this operator is applied to.
1358 :param parent: Optional, the operator this node becomes an operand of.
1359 :raises TypeError: If parameter 'parent' is not of type :class:`Operator`.
1360 :raises TypeError: If parameter 'operand' is not of type :class:`LicenseExpression`.
1361 """
1362 super().__init__(parent)
1364 if operand is not None:
1365 self._operand = operand
1367 if not isinstance(operand, LicenseExpression):
1368 ex = TypeError("Parameter 'operand' is not a LicenseExpression.")
1369 ex.add_note(f"Got type '{getFullyQualifiedName(operand)}'.")
1370 raise ex
1372 operand.Parent = self
1373 else:
1374 self._operand = None
1376 def IterateExpression(self) -> Generator[LicenseExpression, None, None]:
1377 """
1378 Iterate this operator depth-first, its operand first, because ``+`` is written after the expression it suffixes.
1380 :returns: A generator of every node in this expression, this operator included.
1381 """
1382 if self._operand is not None: 1382 ↛ 1385line 1382 didn't jump to line 1385 because the condition on line 1382 was always true
1383 yield from self._operand.IterateExpression()
1385 yield self
1387 @property
1388 def Operand(self) -> Nullable[LicenseExpression]:
1389 """
1390 Property to access the expression this operator is applied to (:attr:`_operand`).
1392 A slot is filled once. Assigning an expression makes this operator its :attr:`~LicenseExpression.Parent`,
1393 which puts it - and everything below it - into this tree.
1395 :returns: The operand, or ``None`` if the operator wasn't assigned yet.
1396 :raises ValueError: If ``None`` is assigned.
1397 :raises TypeError: If an object that is not a :class:`LicenseExpression` is assigned.
1398 :raises LicensingError: If this operator has an operand already.
1399 :raises LicensingError: If the assigned expression is already an operand of another operator.
1400 """
1401 return self._operand
1403 @Operand.setter
1404 def Operand(self, operand: LicenseExpression) -> None:
1405 if operand is None: 1405 ↛ 1406line 1405 didn't jump to line 1406 because the condition on line 1405 was never true
1406 raise ValueError("Parameter 'operand' is None.")
1407 elif not isinstance(operand, LicenseExpression): 1407 ↛ 1408line 1407 didn't jump to line 1408 because the condition on line 1407 was never true
1408 ex = TypeError("Parameter 'operand' is not a LicenseExpression.")
1409 ex.add_note(f"Got type '{getFullyQualifiedName(operand)}'.")
1410 raise ex
1411 elif self._operand is not None:
1412 raise LicensingError(f"Operator '{getFullyQualifiedName(self)}' has an operand already.")
1413 elif operand._parent is not None: 1413 ↛ 1414line 1413 didn't jump to line 1414 because the condition on line 1413 was never true
1414 raise LicensingError("Parameter 'operand' is already an operand of another operator.")
1416 self._operand = operand
1417 operand.Parent = self
1420@export
1421class OrLaterOperator(UnaryOperator):
1422 """
1423 The ``+`` suffix, as in ``GPL-2.0+``: the named license *or any later version of it*.
1425 .. attention::
1427 The operator itself is not deprecated - Annex D.3 of the SPDX specification defines it and uses ``CDDL-1.0+``
1428 as its example. Its combination with the GNU licenses is: `SPDX License List 3.0
1429 <https://spdx.org/licenses/>`__ (2017-12-28) introduced ``GPL-2.0-only`` and ``GPL-2.0-or-later`` and
1430 deprecated the ``GPL-2.0``, ``LGPL-2.1`` and ``AGPL-3.0`` identifiers that ``+`` was applied to. Published
1431 metadata still contains ``GPL-2.0+``, so an expression using it has to parse.
1432 """
1434 PRECEDENCE: ClassVar[int] = 1 #: Binds tighter than every binary operator, looser than a bare license.
1436 def __str__(self) -> str:
1437 """
1438 Return the operand followed by ``+``.
1440 :returns: The expression in SPDX syntax.
1441 :raises LicensingError: If the operator has no operand yet.
1442 """
1443 if self._operand is None:
1444 raise LicensingError(f"Operator '{getFullyQualifiedName(self)}' has no operand yet.")
1446 return f"{self._operand}+"
1449@export
1450@abstractclass
1451class BinaryOperator(Operator):
1452 """
1453 Base-class of the expression operators taking two operands.
1455 SPDX defines three binary operators:
1457 * :class:`WithOperator`,
1458 * :class:`AndOperator` and
1459 * :class:`OrOperator`.
1461 The operands are reachable as :attr:`Left` and :attr:`Right`. :attr:`KEYWORD` is the operator keyword between the
1462 operands.
1463 """
1465 KEYWORD: ClassVar[str] #: The operator's keyword, as it is written between the operands.
1467 _left: Nullable[LicenseExpression] #: The operator's left operand.
1468 _right: Nullable[LicenseExpression] #: The operator's right operand.
1470 def __init__(
1471 self,
1472 left: Nullable[LicenseExpression] = None,
1473 right: Nullable[LicenseExpression] = None,
1474 parent: Nullable[Operator] = None
1475 ) -> None:
1476 """
1477 Initialize a binary operator with both operands.
1479 :param left: Optional, the operator's left operand.
1480 :param right: Optional, the operator's right operand.
1481 :param parent: Optional, the operator this node becomes an operand of.
1482 :raises TypeError: If parameter 'parent' is not of type :class:`Operator`.
1483 :raises TypeError: If parameter 'left' is not of type :class:`LicenseExpression`.
1484 :raises TypeError: If parameter 'right' is not of type :class:`LicenseExpression`.
1485 """
1486 super().__init__(parent)
1488 if left is not None:
1489 self._left = left
1491 if not isinstance(left, LicenseExpression):
1492 ex = TypeError("Parameter 'left' is not a LicenseExpression.")
1493 ex.add_note(f"Got type '{getFullyQualifiedName(left)}'.")
1494 raise ex
1496 left.Parent = self
1497 else:
1498 self._left = None
1500 if right is not None:
1501 self._right = right
1503 if not isinstance(right, LicenseExpression):
1504 ex = TypeError("Parameter 'right' is not a LicenseExpression.")
1505 ex.add_note(f"Got type '{getFullyQualifiedName(right)}'.")
1506 raise ex
1508 right.Parent = self
1509 else:
1510 self._right = None
1512 def IterateExpression(self) -> Generator[LicenseExpression, None, None]:
1513 """
1514 Iterate this operator depth-first in infix order: left operand, this operator, right operand.
1516 :returns: A generator of every node in this expression, this operator included.
1517 """
1518 if self._left is not None: 1518 ↛ 1521line 1518 didn't jump to line 1521 because the condition on line 1518 was always true
1519 yield from self._left.IterateExpression()
1521 yield self
1523 if self._right is not None:
1524 yield from self._right.IterateExpression()
1526 @property
1527 def Left(self) -> Nullable[LicenseExpression]:
1528 """
1529 Property to access the operator's left operand (:attr:`_left`).
1531 A slot is filled once. Assigning an expression makes this operator its :attr:`~LicenseExpression.Parent`,
1532 which puts it - and everything below it - into this tree.
1534 :returns: The left operand, or ``None`` if it wasn't assigned yet.
1535 :raises ValueError: If ``None`` is assigned.
1536 :raises TypeError: If an object that is not a :class:`LicenseExpression` is assigned.
1537 :raises LicensingError: If this operator has a left operand already.
1538 :raises LicensingError: If the assigned expression is already an operand of another operator.
1539 """
1540 return self._left
1542 @Left.setter
1543 def Left(self, operand: LicenseExpression) -> None:
1544 if operand is None: 1544 ↛ 1545line 1544 didn't jump to line 1545 because the condition on line 1544 was never true
1545 raise ValueError("Parameter 'operand' is None.")
1546 elif not isinstance(operand, LicenseExpression): 1546 ↛ 1547line 1546 didn't jump to line 1547 because the condition on line 1546 was never true
1547 ex = TypeError("Parameter 'operand' is not a LicenseExpression.")
1548 ex.add_note(f"Got type '{getFullyQualifiedName(operand)}'.")
1549 raise ex
1550 elif self._left is not None:
1551 raise LicensingError(f"Operator '{getFullyQualifiedName(self)}' has a left operand already.")
1552 elif operand._parent is not None:
1553 raise LicensingError("Parameter 'operand' is already an operand of another operator.")
1555 self._left = operand
1556 operand.Parent = self
1558 @property
1559 def Right(self) -> Nullable[LicenseExpression]:
1560 """
1561 Property to access the operator's right operand (:attr:`_right`).
1563 A slot is filled once. Assigning an expression makes this operator its :attr:`~LicenseExpression.Parent`,
1564 which puts it - and everything below it - into this tree.
1566 :returns: The right operand, or ``None`` if it wasn't assigned yet.
1567 :raises ValueError: If ``None`` is assigned.
1568 :raises TypeError: If an object that is not a :class:`LicenseExpression` is assigned.
1569 :raises LicensingError: If this operator has a right operand already.
1570 :raises LicensingError: If the assigned expression is already an operand of another operator.
1571 """
1572 return self._right
1574 @Right.setter
1575 def Right(self, operand: LicenseExpression) -> None:
1576 if operand is None: 1576 ↛ 1577line 1576 didn't jump to line 1577 because the condition on line 1576 was never true
1577 raise ValueError("Parameter 'operand' is None.")
1578 elif not isinstance(operand, LicenseExpression): 1578 ↛ 1579line 1578 didn't jump to line 1579 because the condition on line 1578 was never true
1579 ex = TypeError("Parameter 'operand' is not a LicenseExpression.")
1580 ex.add_note(f"Got type '{getFullyQualifiedName(operand)}'.")
1581 raise ex
1582 elif self._right is not None: 1582 ↛ 1583line 1582 didn't jump to line 1583 because the condition on line 1582 was never true
1583 raise LicensingError(f"Operator '{getFullyQualifiedName(self)}' has a right operand already.")
1584 elif operand._parent is not None: 1584 ↛ 1585line 1584 didn't jump to line 1585 because the condition on line 1584 was never true
1585 raise LicensingError("Parameter 'operand' is already an operand of another operator.")
1587 self._right = operand
1588 operand.Parent = self
1590 def __str__(self) -> str:
1591 """
1592 Return both operands with the operator's keyword between them.
1594 :returns: The expression in SPDX syntax.
1595 :raises LicensingError: If one of the operator's operands wasn't assigned yet.
1596 """
1597 if self._left is None:
1598 raise LicensingError(f"Operator '{getFullyQualifiedName(self)}' has no left operand yet.")
1599 elif self._right is None:
1600 raise LicensingError(f"Operator '{getFullyQualifiedName(self)}' has no right operand yet.")
1602 left = f"({self._left})" if self._left.PRECEDENCE > self.PRECEDENCE else f"{self._left}"
1603 right = f"({self._right})" if self._right.PRECEDENCE > self.PRECEDENCE else f"{self._right}"
1605 return f"{left} {self.KEYWORD} {right}"
1608@export
1609class WithOperator(BinaryOperator):
1610 """
1611 ``WITH``, as in ``Apache-2.0 WITH LLVM-exception``: a license together with an exception to it.
1613 It is the only operator whose operands differ in kind - the right one is a :class:`LicenseException`, never a
1614 license - and it binds tighter than ``AND`` and ``OR``.
1615 """
1617 PRECEDENCE: ClassVar[int] = 2 #: Binds tighter than ``AND`` and ``OR``.
1618 KEYWORD: ClassVar[str] = "WITH" #: The operator's keyword.
1621@export
1622class AndOperator(BinaryOperator):
1623 """``AND``, as in ``Apache-2.0 AND MIT``: **both** licenses apply, and both have to be complied with."""
1625 PRECEDENCE: ClassVar[int] = 3 #: Binds tighter than ``OR``.
1626 KEYWORD: ClassVar[str] = "AND" #: The operator's keyword.
1629@export
1630class OrOperator(BinaryOperator):
1631 """
1632 ``OR``, as in ``Apache-2.0 OR BSD-2-Clause``: **either** license applies, and the recipient chooses which.
1634 Which one they chose is not something the expression records.
1635 """
1637 PRECEDENCE: ClassVar[int] = 4 #: Binds loosest of all operators.
1638 KEYWORD: ClassVar[str] = "OR" #: The operator's keyword.
1641class _LicenseExpressionParser(metaclass=ExtendedType, slots=True):
1642 """
1643 Recursive-descent parser for SPDX license expressions.
1645 One level of the descent per precedence level, lowest-binding first, which is what makes ``A OR B AND C`` parse as
1646 ``A OR (B AND C)`` without the grammar having to say so twice. The descent is a class rather than a function
1647 because every level reads and advances the same token position, and that position is state the levels share.
1648 """
1650 _TOKEN = re_compile(r"\(|\)|[^\s()]+") #: Splits an expression into parentheses and the words between them.
1652 _expression: str #: The expression being parsed, kept for the error messages.
1653 _tokens: list[str] #: The expression's tokens, in order.
1654 _position: int #: Index of the token to read next.
1656 def __init__(self, expression: str) -> None:
1657 """
1658 Tokenize an expression.
1660 :param expression: The SPDX license expression to parse.
1661 :raises TypeError: If parameter 'expression' is not of type :class:`str`.
1662 :raises ValueError: If parameter 'expression' is empty.
1663 :raises LicenseExpressionError: If the expression holds nothing but whitespace.
1664 """
1665 if not isinstance(expression, str):
1666 ex = TypeError("Parameter 'expression' is not a string.")
1667 ex.add_note(f"Got type '{getFullyQualifiedName(expression)}'.")
1668 raise ex
1669 elif expression == "":
1670 raise ValueError("Parameter 'expression' is empty.")
1672 self._expression = expression
1673 self._tokens = self._TOKEN.findall(expression)
1674 self._position = 0
1676 if len(self._tokens) == 0:
1677 raise LicenseExpressionError(f"License expression '{expression}' is empty.", expression=expression)
1679 def Parse(self) -> LicenseExpression:
1680 """
1681 Parse the whole expression.
1683 ``NONE`` and ``NOASSERTION`` are read here rather than in the descent, because SPDX allows them **only** as
1684 the entire expression - they are values a license field may hold instead of one, not terms inside one. Seeing
1685 either anywhere else leaves it to the descent, where it is an unknown license identifier.
1687 :returns: The root of the expression tree.
1688 :raises LicenseExpressionError: If the expression is malformed or names an unknown license.
1689 """
1690 if len(self._tokens) == 1:
1691 for absence in LicenseAbsence:
1692 if self._tokens[0] == absence.value:
1693 self._position = 1
1694 return UnknownLicense(absence)
1696 result = self._ParseOr()
1698 if self._position < len(self._tokens):
1699 raise LicenseExpressionError(
1700 f"License expression '{self._expression}' has trailing input at '{self._tokens[self._position]}'.",
1701 expression=self._expression
1702 )
1704 return result
1706 def _ParseOr(self) -> LicenseExpression:
1707 """
1708 Parse a sequence of ``OR`` operands, the loosest-binding operator.
1710 :returns: The parsed expression.
1711 """
1712 left = self._ParseAnd()
1713 while self._Accept(OrOperator.KEYWORD):
1714 left = OrOperator(left, self._ParseAnd())
1716 return left
1718 def _ParseAnd(self) -> LicenseExpression:
1719 """
1720 Parse a sequence of ``AND`` operands.
1722 :returns: The parsed expression.
1723 """
1724 left = self._ParseWith()
1725 while self._Accept(AndOperator.KEYWORD):
1726 left = AndOperator(left, self._ParseWith())
1728 return left
1730 def _ParseWith(self) -> LicenseExpression:
1731 """
1732 Parse a ``WITH`` clause, whose right operand is an exception rather than a license.
1734 :returns: The parsed expression.
1735 :raises LicenseExpressionError: If ``WITH`` isn't followed by an exception identifier.
1736 """
1737 left = self._ParseSimple()
1738 if self._Accept(WithOperator.KEYWORD):
1739 if (identifier := self._Next()) is None:
1740 raise LicenseExpressionError(
1741 f"License expression '{self._expression}' ends after 'WITH'.",
1742 expression=self._expression
1743 )
1745 left = WithOperator(left, LicenseException(identifier))
1747 return left
1749 def _ParseSimple(self) -> LicenseExpression:
1750 """
1751 Parse a parenthesized expression, a license reference, or a license identifier with an optional ``+``.
1753 :returns: The parsed expression.
1754 :raises LicenseExpressionError: If the expression ends early, a parenthesis is unbalanced, or a license is unknown.
1755 """
1756 if (token := self._Next()) is None:
1757 raise LicenseExpressionError(
1758 f"License expression '{self._expression}' ends unexpectedly.",
1759 expression=self._expression
1760 )
1762 if token == "(":
1763 inner = self._ParseOr()
1764 if not self._Accept(")"):
1765 raise LicenseExpressionError(
1766 f"License expression '{self._expression}' is missing a closing parenthesis.",
1767 expression=self._expression
1768 )
1770 return inner
1772 if token == ")":
1773 raise LicenseExpressionError(
1774 f"License expression '{self._expression}' has an unmatched closing parenthesis.",
1775 expression=self._expression
1776 )
1778 # 'GPL-2.0+' is the deprecated spelling of 'GPL-2.0-or-later' and is still legal grammar
1779 orLater = token.endswith("+")
1780 identifier = token[:-1] if orLater else token
1782 if identifier.startswith("LicenseRef-"):
1783 expression: LicenseExpression = LicenseReference(identifier[len("LicenseRef-"):])
1784 elif identifier.startswith("DocumentRef-") and ":LicenseRef-" in identifier:
1785 documentIdentifier, _, licenseIdentifier = identifier.partition(":LicenseRef-")
1786 expression = LicenseReference(licenseIdentifier, documentIdentifier[len("DocumentRef-"):])
1787 elif (spdxLicense := SPDX_INDEX.get(identifier, None)) is not None:
1788 expression = SPDXLicense(spdxLicense)
1789 else:
1790 ex = LicenseExpressionError(
1791 f"License expression '{self._expression}' names unknown license '{identifier}'.",
1792 expression=self._expression,
1793 identifier=identifier
1794 )
1796 if any(identifier == absence.value for absence in LicenseAbsence):
1797 ex.add_note(f"'{identifier}' is a value a license field may hold instead of an expression.")
1798 ex.add_note("SPDX's grammar has no place for it inside one, so it may only stand alone.")
1799 else:
1800 ex.add_note("Known licenses are the SPDX identifiers in 'pyTooling.Licensing.SPDX_INDEX'.")
1801 ex.add_note("A license that isn't on that list is written 'LicenseRef-<id>'.")
1803 raise ex
1805 return OrLaterOperator(expression) if orLater else expression
1807 def _Next(self) -> Nullable[str]:
1808 """
1809 Consume and return the next token.
1811 :returns: The next token, or ``None`` if the expression is exhausted.
1812 """
1813 if self._position >= len(self._tokens):
1814 return None
1816 self._position += 1
1818 return self._tokens[self._position - 1]
1820 def _Accept(self, keyword: str) -> bool:
1821 """
1822 Consume the next token if it is the given keyword.
1824 Keywords are matched case-insensitively, because published metadata writes ``and`` as often as ``AND``.
1826 :param keyword: The keyword to look for.
1827 :returns: ``True``, if the keyword was there and was consumed.
1828 """
1829 if self._position < len(self._tokens) and self._tokens[self._position].upper() == keyword:
1830 self._position += 1
1832 return True
1834 return False