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