__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:

Licensing

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