Source code for pyTooling.Licensing

# ==================================================================================================================== #
#             _____           _ _               _     _                    _                                           #
#  _ __  _   |_   _|__   ___ | (_)_ __   __ _  | |   (_) ___ ___ _ __  ___(_)_ __   __ _                               #
# | '_ \| | | || |/ _ \ / _ \| | | '_ \ / _` | | |   | |/ __/ _ \ '_ \/ __| | '_ \ / _` |                              #
# | |_) | |_| || | (_) | (_) | | | | | | (_| |_| |___| | (_|  __/ | | \__ \ | | | | (_| |                              #
# | .__/ \__, ||_|\___/ \___/|_|_|_| |_|\__, (_)_____|_|\___\___|_| |_|___/_|_| |_|\__, |                              #
# |_|    |___/                          |___/                                      |___/                               #
# ==================================================================================================================== #
# Authors:                                                                                                             #
#   Patrick Lehmann                                                                                                    #
#                                                                                                                      #
# License:                                                                                                             #
# ==================================================================================================================== #
# Copyright 2017-2026 Patrick Lehmann - Bötzingen, Germany                                                             #
#                                                                                                                      #
# Licensed under the Apache License, Version 2.0 (the "License");                                                      #
# you may not use this file except in compliance with the License.                                                     #
# You may obtain a copy of the License at                                                                              #
#                                                                                                                      #
#   http://www.apache.org/licenses/LICENSE-2.0                                                                         #
#                                                                                                                      #
# Unless required by applicable law or agreed to in writing, software                                                  #
# distributed under the License is distributed on an "AS IS" BASIS,                                                    #
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.                                             #
# See the License for the specific language governing permissions and                                                  #
# limitations under the License.                                                                                       #
#                                                                                                                      #
# SPDX-License-Identifier: Apache-2.0                                                                                  #
# ==================================================================================================================== #
#
"""
The Licensing module implements mapping tables for various license names and identifiers.

.. seealso::

   List of SPDX identifiers:

   * https://spdx.org/licenses/
   * :gh:`spdx/license-list-XML`

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

.. hint::

   See :ref:`high-level help <LICENSING>` for explanations and usage examples.
"""
from dataclasses           import dataclass
from enum                  import Enum, unique
from re                    import compile as re_compile
from typing                import Any, ClassVar, Generator, Optional as Nullable
from pyTooling.Common      import getFullyQualifiedName
from pyTooling.Decorators  import export, readonly
from pyTooling.Exceptions  import ToolingException
from pyTooling.MetaClasses import ExtendedType, abstractclass, abstractmethod


__all__ = [
	"PYTHON_LICENSE_NAMES",

	"Apache_2_0_License",
	"BSD_2_Clause_License",
	"BSD_3_Clause_License",
	"MIT_License",
	"ISC_License",
	"MPL_2_0_License",
	"BSL_1_0_License",
	"Zlib_License",
	"PSF_2_0_License",
	"Unlicense",
	"CC0_1_0",
	"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",
	"EPL_1_0_License",
	"EPL_2_0_License",
	"LGPL_2_1_only",
	"LGPL_2_1_or_later",
	"LGPL_3_0_only",
	"LGPL_3_0_or_later",
	"GPL_2_0_only",
	"GPL_2_0_or_later",
	"GPL_3_0_only",
	"GPL_3_0_or_later",
	"AGPL_3_0_only",
	"AGPL_3_0_or_later",

	"SPDX_INDEX",
	"LICENSES_BY_CLASSIFIER",
	"OSI_LICENSE_URLS",
	"LICENSE_URLS",
	"LICENSE_TEXT_URLS"
]


[docs] @export class LicensingError(ToolingException): """Base exception of all exceptions raised by :mod:`pyTooling.Licensing`."""
[docs] @export class LicenseExpressionError(LicensingError): """ The exception is raised when an SPDX license expression is malformed or names a license SPDX doesn't define. """
[docs] @export @unique class LicenseAbsence(Enum): """ SPDX's two ways of stating that no license is named, which mean different things. Both are values a license field may hold **on their own**; neither is part of the expression grammar, so neither can be an operand of ``AND``, ``OR`` or ``WITH``. """ NoLicense = "NONE" #: The work states that no license applies to it. NoAssertion = "NOASSERTION" #: Someone looked and declined to state a license; nothing is claimed either way.
[docs] def __str__(self) -> str: """ Return the value in SPDX's spelling. :returns: ``NONE`` or ``NOASSERTION``. """ return self.value
[docs] @export @dataclass class PythonLicenseName: """A *data class* to represent the license's short name and the package classifier for a license.""" ShortName: str #: License's short name Classifier: str #: Package classifier for a license.
[docs] def __str__(self) -> str: """ The string representation of this name tuple returns the short name of the license. :returns: Short name of the license. """ return self.ShortName
#: Mapping of SPDX identifiers to Python license names PYTHON_LICENSE_NAMES: dict[str, PythonLicenseName] = { "Apache-2.0": PythonLicenseName("Apache 2.0", "Apache Software License"), "0BSD": PythonLicenseName("0BSD", "Zero-Clause BSD (0BSD)"), "BSD-2-Clause": PythonLicenseName("BSD-2-Clause", "BSD License"), "BSD-3-Clause": PythonLicenseName("BSD", "BSD License"), "MIT": PythonLicenseName("MIT", "MIT License"), "MIT-0": PythonLicenseName("MIT-0", "MIT No Attribution License (MIT-0)"), "MIT-CMU": PythonLicenseName("MIT-CMU", "CMU License (MIT-CMU)"), "ISC": PythonLicenseName("ISC", "ISC License (ISCL)"), "MPL-2.0": PythonLicenseName("MPL-2.0", "Mozilla Public License 2.0 (MPL 2.0)"), "BSL-1.0": PythonLicenseName("BSL-1.0", "Boost Software License 1.0 (BSL-1.0)"), "Zlib": PythonLicenseName("Zlib", "zlib/libpng License"), "PSF-2.0": PythonLicenseName("PSF-2.0", "Python Software Foundation License"), "Unlicense": PythonLicenseName("Unlicense", "The Unlicense (Unlicense)"), "CC0-1.0": PythonLicenseName("CC0-1.0", "CC0 1.0 Universal (CC0 1.0) Public Domain Dedication"), "EPL-1.0": PythonLicenseName("EPL-1.0", "Eclipse Public License 1.0 (EPL-1.0)"), "EPL-2.0": PythonLicenseName("EPL-2.0", "Eclipse Public License 2.0 (EPL-2.0)"), "LGPL-2.1-only": PythonLicenseName("LGPL-2.1-only", "GNU Lesser General Public License v2 (LGPLv2)"), "LGPL-2.1-or-later": PythonLicenseName("LGPL-2.1-or-later", "GNU Lesser General Public License v2 or later (LGPLv2+)"), "LGPL-3.0-only": PythonLicenseName("LGPL-3.0-only", "GNU Lesser General Public License v3 (LGPLv3)"), "LGPL-3.0-or-later": PythonLicenseName("LGPL-3.0-or-later", "GNU Lesser General Public License v3 or later (LGPLv3+)"), "GPL-2.0-only": PythonLicenseName("GPL-2.0-only", "GNU General Public License v2 (GPLv2)"), "GPL-2.0-or-later": PythonLicenseName("GPL-2.0-or-later", "GNU General Public License v2 or later (GPLv2+)"), "GPL-3.0-only": PythonLicenseName("GPL-3.0-only", "GNU General Public License v3 (GPLv3)"), "GPL-3.0-or-later": PythonLicenseName("GPL-3.0-or-later", "GNU General Public License v3 or later (GPLv3+)"), "AGPL-3.0-only": PythonLicenseName("AGPL-3.0-only", "GNU Affero General Public License v3"), "AGPL-3.0-or-later": PythonLicenseName("AGPL-3.0-or-later", "GNU Affero General Public License v3 or later (AGPLv3+)"), } #: Mapping of SPDX identifiers to the page where the licensor publishes the license. #: #: Sourced from the SPDX License List's own ``seeAlso`` field, or from the licensor's domain where SPDX names none. #: A license whose only published home is its OSI page has **no entry** - that URL is #: :attr:`License.OSIURL` and isn't repeated here, e.g. ``MIT`` or ``BSD-3-Clause``. LICENSE_URLS: dict[str, str] = { "Apache-2.0": "https://www.apache.org/licenses/LICENSE-2.0", "ISC": "https://www.isc.org/licenses/", "MPL-2.0": "https://www.mozilla.org/MPL/2.0/", "BSL-1.0": "https://www.boost.org/doc/user-guide/bsl.html", "Zlib": "https://zlib.net/zlib_license.html", "PSF-2.0": "https://docs.python.org/3/license.html", "Unlicense": "https://unlicense.org/", "CC0-1.0": "https://creativecommons.org/publicdomain/zero/1.0/", "CC-BY-4.0": "https://creativecommons.org/licenses/by/4.0/", "CC-BY-SA-4.0": "https://creativecommons.org/licenses/by-sa/4.0/", "CC-BY-NC-4.0": "https://creativecommons.org/licenses/by-nc/4.0/", "CC-BY-ND-4.0": "https://creativecommons.org/licenses/by-nd/4.0/", "CC-BY-NC-SA-4.0": "https://creativecommons.org/licenses/by-nc-sa/4.0/", "CC-BY-NC-ND-4.0": "https://creativecommons.org/licenses/by-nc-nd/4.0/", "EPL-1.0": "https://www.eclipse.org/legal/epl/epl-v10.html", "EPL-2.0": "https://www.eclipse.org/legal/epl-2.0/", "GPL-2.0-only": "https://www.gnu.org/licenses/old-licenses/gpl-2.0-standalone.html", "GPL-2.0-or-later": "https://www.gnu.org/licenses/old-licenses/gpl-2.0-standalone.html", "LGPL-2.1-only": "https://www.gnu.org/licenses/old-licenses/lgpl-2.1-standalone.html", "LGPL-2.1-or-later": "https://www.gnu.org/licenses/old-licenses/lgpl-2.1-standalone.html", "GPL-3.0-only": "https://www.gnu.org/licenses/gpl-3.0-standalone.html", "GPL-3.0-or-later": "https://www.gnu.org/licenses/gpl-3.0-standalone.html", "LGPL-3.0-only": "https://www.gnu.org/licenses/lgpl-3.0-standalone.html", "LGPL-3.0-or-later": "https://www.gnu.org/licenses/lgpl-3.0-standalone.html", "AGPL-3.0-only": "https://www.gnu.org/licenses/agpl-3.0-standalone.html", "AGPL-3.0-or-later": "https://www.gnu.org/licenses/agpl-3.0-standalone.html", } #: Mapping of SPDX identifiers to the license text, by the file extension it is published as. #: #: Keys are the extension without its dot - ``txt``, ``md``, ``rst``, ``tex``. What a license offers is entirely up #: to its licensor: the GNU licenses publish three formats - the GPL-3.0 a fourth, ``rst`` -, most publish one, and #: several publish none at all beyond an HTML page, which is :data:`LICENSE_URLS`. #: #: Every URL here answered an HTTP request with the license, so the table is what is **known** rather than a claim #: of completeness - a license absent from it may still publish a text nobody has looked up yet. LICENSE_TEXT_URLS: dict[str, dict[str, str]] = { "Apache-2.0": {"txt": "https://www.apache.org/licenses/LICENSE-2.0.txt"}, "MPL-2.0": {"txt": "https://www.mozilla.org/media/MPL/2.0/index.txt"}, "BSL-1.0": {"txt": "https://www.boost.org/LICENSE_1_0.txt"}, "Unlicense": {"txt": "https://unlicense.org/UNLICENSE"}, "CC0-1.0": {"txt": "https://creativecommons.org/publicdomain/zero/1.0/legalcode.txt"}, "CC-BY-4.0": {"txt": "https://creativecommons.org/licenses/by/4.0/legalcode.txt"}, "CC-BY-SA-4.0": {"txt": "https://creativecommons.org/licenses/by-sa/4.0/legalcode.txt"}, "CC-BY-NC-4.0": {"txt": "https://creativecommons.org/licenses/by-nc/4.0/legalcode.txt"}, "CC-BY-ND-4.0": {"txt": "https://creativecommons.org/licenses/by-nd/4.0/legalcode.txt"}, "CC-BY-NC-SA-4.0": {"txt": "https://creativecommons.org/licenses/by-nc-sa/4.0/legalcode.txt"}, "CC-BY-NC-ND-4.0": {"txt": "https://creativecommons.org/licenses/by-nc-nd/4.0/legalcode.txt"}, "EPL-2.0": {"txt": "https://www.eclipse.org/org/documents/epl-2.0/EPL-2.0.txt"}, "GPL-2.0-only": { "txt": "https://www.gnu.org/licenses/old-licenses/gpl-2.0.txt", "md": "https://www.gnu.org/licenses/old-licenses/gpl-2.0.md", "tex": "https://www.gnu.org/licenses/old-licenses/gpl-2.0.tex", }, "GPL-2.0-or-later": { "txt": "https://www.gnu.org/licenses/old-licenses/gpl-2.0.txt", "md": "https://www.gnu.org/licenses/old-licenses/gpl-2.0.md", "tex": "https://www.gnu.org/licenses/old-licenses/gpl-2.0.tex", }, "LGPL-2.1-only": { "txt": "https://www.gnu.org/licenses/old-licenses/lgpl-2.1.txt", "md": "https://www.gnu.org/licenses/old-licenses/lgpl-2.1.md", "tex": "https://www.gnu.org/licenses/old-licenses/lgpl-2.1.tex", }, "LGPL-2.1-or-later": { "txt": "https://www.gnu.org/licenses/old-licenses/lgpl-2.1.txt", "md": "https://www.gnu.org/licenses/old-licenses/lgpl-2.1.md", "tex": "https://www.gnu.org/licenses/old-licenses/lgpl-2.1.tex", }, "GPL-3.0-only": { "txt": "https://www.gnu.org/licenses/gpl-3.0.txt", "md": "https://www.gnu.org/licenses/gpl-3.0.md", "rst": "https://www.gnu.org/licenses/gpl-3.0.rst", "tex": "https://www.gnu.org/licenses/gpl-3.0.tex", }, "GPL-3.0-or-later": { "txt": "https://www.gnu.org/licenses/gpl-3.0.txt", "md": "https://www.gnu.org/licenses/gpl-3.0.md", "rst": "https://www.gnu.org/licenses/gpl-3.0.rst", "tex": "https://www.gnu.org/licenses/gpl-3.0.tex", }, "LGPL-3.0-only": { "txt": "https://www.gnu.org/licenses/lgpl-3.0.txt", "md": "https://www.gnu.org/licenses/lgpl-3.0.md", "tex": "https://www.gnu.org/licenses/lgpl-3.0.tex", }, "LGPL-3.0-or-later": { "txt": "https://www.gnu.org/licenses/lgpl-3.0.txt", "md": "https://www.gnu.org/licenses/lgpl-3.0.md", "tex": "https://www.gnu.org/licenses/lgpl-3.0.tex", }, "AGPL-3.0-only": { "txt": "https://www.gnu.org/licenses/agpl-3.0.txt", "md": "https://www.gnu.org/licenses/agpl-3.0.md", "tex": "https://www.gnu.org/licenses/agpl-3.0.tex", }, "AGPL-3.0-or-later": { "txt": "https://www.gnu.org/licenses/agpl-3.0.txt", "md": "https://www.gnu.org/licenses/agpl-3.0.md", "tex": "https://www.gnu.org/licenses/agpl-3.0.tex", }, } #: Mapping of SPDX identifiers to the license's page at the #: `Open Source Initiative <https://opensource.org/licenses>`__. #: #: OSI's slugs don't follow the SPDX identifier and can't be derived from it: ``PSF-2.0`` is published as #: ``Python-2.0``, and OSI has **one** page per license where SPDX has two identifiers - ``GPL-2.0-only`` and #: ``GPL-2.0-or-later`` both point at it, because *only* versus *or later* is SPDX's distinction, not OSI's. #: #: A license OSI hasn't approved has no entry, which is why ``CC0-1.0`` and the Creative Commons licenses are absent. OSI_LICENSE_URLS: dict[str, str] = { "0BSD": "https://opensource.org/license/0bsd", "Apache-2.0": "https://opensource.org/license/apache-2.0", "BSD-2-Clause": "https://opensource.org/license/bsd-2-clause", "BSD-3-Clause": "https://opensource.org/license/bsd-3-clause", "MIT-0": "https://opensource.org/license/mit-0", "MIT-CMU": "https://opensource.org/license/cmu-license", "MIT": "https://opensource.org/license/mit", "ISC": "https://opensource.org/license/isc", "MPL-2.0": "https://opensource.org/license/mpl-2.0", "BSL-1.0": "https://opensource.org/license/bsl-1.0", "Zlib": "https://opensource.org/license/zlib", "PSF-2.0": "https://opensource.org/license/Python-2.0", "Unlicense": "https://opensource.org/license/unlicense", "EPL-1.0": "https://opensource.org/license/epl-1.0", "EPL-2.0": "https://opensource.org/license/epl-2.0", "LGPL-2.1-only": "https://opensource.org/license/lgpl-2-1", "LGPL-2.1-or-later": "https://opensource.org/license/lgpl-2-1", "LGPL-3.0-only": "https://opensource.org/license/lgpl-3-0", "LGPL-3.0-or-later": "https://opensource.org/license/lgpl-3-0", "GPL-2.0-only": "https://opensource.org/license/gpl-2.0", "GPL-2.0-or-later": "https://opensource.org/license/gpl-2.0", "GPL-3.0-only": "https://opensource.org/license/gpl-3.0", "GPL-3.0-or-later": "https://opensource.org/license/gpl-3.0", "AGPL-3.0-only": "https://opensource.org/license/agpl-3-0", "AGPL-3.0-or-later": "https://opensource.org/license/agpl-3-0", }
[docs] @export class License(metaclass=ExtendedType, slots=True): """Representation of a license.""" _spdxIdentifier: str #: Unique SPDX identifier. _name: str #: Name of the license. _osiApproved: bool #: OSI approval status _fsfApproved: bool #: FSF approval status
[docs] def __init__(self, spdxIdentifier: str, name: str, osiApproved: bool = False, fsfApproved: bool = False) -> None: """ Initialize a license with its SPDX identifier, its name and its approval flags. :param spdxIdentifier: SPDX identifier of the license. :param name: Name of the license. :param osiApproved: Optional, ``True``, if the license is approved by the Open Source Initiative. :param fsfApproved: Optional, ``True``, if the license is approved by the Free Software Foundation. """ self._spdxIdentifier = spdxIdentifier self._name = name self._osiApproved = osiApproved self._fsfApproved = fsfApproved
@readonly def Name(self) -> str: """ Returns the license' name. :returns: License name. """ return self._name @readonly def SPDXIdentifier(self) -> str: """ Returns the license' unique `SPDX identifier <https://spdx.org/licenses/>`__. :returns: The unique SPDX identifier. """ return self._spdxIdentifier @readonly def SPDXURL(self) -> str: """ Returns the URL of this license's page in the `SPDX License List <https://spdx.org/licenses/>`__. SPDX publishes one page per identifier at a fixed address, so this is derived from :attr:`SPDXIdentifier` rather than stored. A license whose identifier isn't on that list has no page there, and the derived URL won't resolve. :returns: URL of the license's page at SPDX. """ return f"https://spdx.org/licenses/{self._spdxIdentifier}.html" @readonly def URL(self) -> Nullable[str]: """ Returns the page where the licensor publishes this license. This is looked up in :data:`LICENSE_URLS`. It is the licensor's own page - ``https://www.apache.org/licenses/ LICENSE-2.0`` for the Apache License 2.0 - which is not the same thing as :attr:`SPDXURL` or :attr:`OSIURL`, the two catalogue entries describing it. A license whose only published home *is* its OSI page has no entry here rather than a duplicate of :attr:`OSIURL`; ``MIT``, ``BSD-2-Clause`` and ``BSD-3-Clause`` are those. :returns: URL of the license's own page, or ``None`` if the licensor publishes none. .. seealso:: :attr:`TextURLs` |rarr| The same license as text, by format. """ return LICENSE_URLS.get(self._spdxIdentifier, None) @readonly def TextURLs(self) -> dict[str, str]: """ Returns the URLs of this license's text, keyed by the format it is published as. Keys are the file extension without its dot: ``txt``, ``md``, ``rst``, ``tex``. Which formats exist is the licensor's choice - the GNU licenses publish four, most publish one, and several publish none. The table is what is **known**, not a claim of completeness: an empty mapping means no URL is recorded, not that the licensor publishes no text. Every URL in it answered an HTTP request with the license. .. code-block:: python Apache_2_0_License.TextURLs["txt"] # https://www.apache.org/licenses/LICENSE-2.0.txt GPL_3_0_only.TextURLs["rst"] # https://www.gnu.org/licenses/gpl-3.0.rst A **copy** is returned, so a caller can't edit :data:`LICENSE_TEXT_URLS` through it. :returns: The license text's URLs by format, or an empty dictionary if none is published. """ return dict(LICENSE_TEXT_URLS.get(self._spdxIdentifier, {})) @readonly def OSIURL(self) -> Nullable[str]: """ Returns the URL of this license's page at the `Open Source Initiative <https://opensource.org/licenses>`__. This is looked up in :data:`OSI_LICENSE_URLS` rather than derived: OSI's slugs don't follow the SPDX identifier, and OSI has one page where SPDX has two identifiers - ``GPL-2.0-only`` and ``GPL-2.0-or-later`` share it. :returns: URL of the license's page at OSI, or ``None`` if OSI doesn't publish it. .. seealso:: :attr:`OSIApproved` |rarr| Whether OSI approved this license at all. """ return OSI_LICENSE_URLS.get(self._spdxIdentifier, None) @readonly def OSIApproved(self) -> bool: """ Returns true, if the license is approved by OSI (`Open Source Initiative <https://opensource.org/>`__). :returns: ``True``, if the license is approved by the Open Source Initiative. """ return self._osiApproved @readonly def FSFApproved(self) -> bool: """ Returns true, if the license is approved by FSF (`Free Software Foundation <https://www.fsf.org/>`__). :returns: ``True``, if the license is approved by the Free Software Foundation. """ return self._fsfApproved @readonly def PythonLicenseName(self) -> str: """ Returns the Python license name for this license if it's defined. :returns: The Python license name. :raises ValueError: If there is no license name defined for the license. |br| (See and check :data:`~pyTooling.Licensing.PYTHON_LICENSE_NAMES`) """ try: item: PythonLicenseName = PYTHON_LICENSE_NAMES[self._spdxIdentifier] except KeyError as ex: raise ValueError("License has no Python specify information.") from ex return item.ShortName @readonly def PythonClassifier(self) -> str: """ Returns the Python package classifier for this license if it's defined. :returns: The Python package classifier. :raises ValueError: If there is no classifier defined for the license. |br| (See and check :data:`~pyTooling.Licensing.PYTHON_LICENSE_NAMES`) .. seealso:: List of `Python classifiers <https://pypi.org/classifiers/>`__ """ try: item: PythonLicenseName = PYTHON_LICENSE_NAMES[self._spdxIdentifier] except KeyError as ex: raise ValueError("License has no Python specify information.") from ex osi = "OSI Approved :: " if self._osiApproved else "" return f"License :: {osi}{item.Classifier}"
[docs] def __eq__(self, other: Any) -> bool: """ Returns true, if both licenses are identical (comparison based on SPDX identifiers). :param other: The second operand to compare with. A :class:`License` or its SPDX identifier as a string. :returns: ``True``, if both licenses are identical. :raises TypeError: If second operand is not of type :class:`License` or string. """ if isinstance(other, License): return self._spdxIdentifier == other._spdxIdentifier elif isinstance(other, str): return self._spdxIdentifier == other else: ex = TypeError("Second operand is not supported by equal operator.") ex.add_note(f"Got type '{getFullyQualifiedName(other)}'.") ex.add_note("Supported types for second operand: License, str") raise ex
[docs] def __ne__(self, other: Any) -> bool: """ Returns true, if both licenses are not identical (comparison based on SPDX identifiers). :param other: The second operand to compare with. A :class:`License` or its SPDX identifier as a string. :returns: ``True``, if both licenses are not identical. :raises TypeError: If second operand is not of type :class:`License` or string. """ if isinstance(other, License): return self._spdxIdentifier != other._spdxIdentifier elif isinstance(other, str): return self._spdxIdentifier != other else: ex = TypeError("Second operand is not supported by unequal operator.") ex.add_note(f"Got type '{getFullyQualifiedName(other)}'.") ex.add_note("Supported types for second operand: License, str") raise ex
[docs] def __hash__(self) -> int: """ Compute a hash from the license's SPDX identifier. A license compares equal to its identifier as a string, so the two hash equally as well. :returns: Hash of the SPDX identifier. """ return hash(self._spdxIdentifier)
[docs] def __le__(self, other: Any) -> bool: """ Returns true, if both licenses are compatible. :param other: Second operand, the license to compare with. :returns: ``True``, if both licenses are compatible. :raises NotImplementedError: License compatibility is not implemented yet. """ raise NotImplementedError("License compatibility check is not yet implemented.")
[docs] def __ge__(self, other: Any) -> bool: """ Returns true, if both licenses are compatible. :param other: Second operand, the license to compare with. :returns: ``True``, if both licenses are compatible. :raises NotImplementedError: License compatibility is not implemented yet. """ raise NotImplementedError("License compatibility check is not yet implemented.")
[docs] def __repr__(self) -> str: """ Returns the internal unique representation (:attr:`spdxIdentifier`). :returns: SPDX identifier of the license. """ return self._spdxIdentifier
[docs] def __str__(self) -> str: """ Returns the license' name. :returns: Name of the license. """ return self._name
Zero_Clause_BSD = License("0BSD", "BSD Zero Clause License", True, False) Apache_2_0_License = License("Apache-2.0", "Apache License 2.0", True, True) BSD_2_Clause_License = License("BSD-2-Clause", "BSD 2-Clause Simplified License", True, True) BSD_3_Clause_License = License("BSD-3-Clause", "BSD 3-Clause Revised License", True, True) MIT_License = License("MIT", "MIT License", True, True) MIT_0_License = License("MIT-0", "MIT No Attribution", True, False) MIT_CMU_License = License("MIT-CMU", "CMU License", True, False) ISC_License = License("ISC", "ISC License", True, True) MPL_2_0_License = License("MPL-2.0", "Mozilla Public License 2.0", True, True) BSL_1_0_License = License("BSL-1.0", "Boost Software License 1.0", True, True) Zlib_License = License("Zlib", "zlib License", True, True) PSF_2_0_License = License("PSF-2.0", "Python Software Foundation License 2.0", True, True) Unlicense = License("Unlicense", "The Unlicense", True, True) CC0_1_0 = License("CC0-1.0", "Creative Commons Zero v1.0 Universal", False, True) EPL_1_0_License = License("EPL-1.0", "Eclipse Public License 1.0", True, True) EPL_2_0_License = License("EPL-2.0", "Eclipse Public License 2.0", True, True) LGPL_2_1_only = License("LGPL-2.1-only", "GNU Lesser General Public License v2.1 only", True, True) LGPL_2_1_or_later = License("LGPL-2.1-or-later", "GNU Lesser General Public License v2.1 or later", True, True) LGPL_3_0_only = License("LGPL-3.0-only", "GNU Lesser General Public License v3.0 only", True, True) LGPL_3_0_or_later = License("LGPL-3.0-or-later", "GNU Lesser General Public License v3.0 or later", True, True) GPL_2_0_only = License("GPL-2.0-only", "GNU General Public License v2.0 only", True, True) GPL_2_0_or_later = License("GPL-2.0-or-later", "GNU General Public License v2.0 or later", True, True) GPL_3_0_only = License("GPL-3.0-only", "GNU General Public License v3.0 only", True, True) GPL_3_0_or_later = License("GPL-3.0-or-later", "GNU General Public License v3.0 or later", True, True) AGPL_3_0_only = License("AGPL-3.0-only", "GNU Affero General Public License v3.0 only", True, True) AGPL_3_0_or_later = License("AGPL-3.0-or-later", "GNU Affero General Public License v3.0 or later", True, True) CC_BY_4_0 = License("CC-BY-4.0", "Creative Commons Attribution 4.0 International", False, True) CC_BY_SA_4_0 = License("CC-BY-SA-4.0", "Creative Commons Attribution Share Alike 4.0 International", False, True) CC_BY_NC_4_0 = License("CC-BY-NC-4.0", "Creative Commons Attribution Non Commercial 4.0 International", False, False) CC_BY_ND_4_0 = License("CC-BY-ND-4.0", "Creative Commons Attribution No Derivatives 4.0 International", False, False) CC_BY_NC_SA_4_0 = License( "CC-BY-NC-SA-4.0", "Creative Commons Attribution Non Commercial Share Alike 4.0 International", False, False ) CC_BY_NC_ND_4_0 = License( "CC-BY-NC-ND-4.0", "Creative Commons Attribution Non Commercial No Derivatives 4.0 International", False, False ) #: All predefined licenses, in the order they are defined above. LICENSES: tuple[License, ...] = ( Zero_Clause_BSD, Apache_2_0_License, BSD_2_Clause_License, BSD_3_Clause_License, MIT_License, MIT_0_License, MIT_CMU_License, ISC_License, MPL_2_0_License, BSL_1_0_License, Zlib_License, PSF_2_0_License, Unlicense, CC0_1_0, EPL_1_0_License, EPL_2_0_License, LGPL_2_1_only, LGPL_2_1_or_later, LGPL_3_0_only, LGPL_3_0_or_later, GPL_2_0_only, GPL_2_0_or_later, GPL_3_0_only, GPL_3_0_or_later, AGPL_3_0_only, AGPL_3_0_or_later, 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, ) #: Mapping of predefined licenses, indexed by their SPDX identifier. SPDX_INDEX: dict[str, License] = {spdxLicense.SPDXIdentifier: spdxLicense for spdxLicense in LICENSES}
[docs] @export def buildClassifierIndex() -> dict[str, tuple[License, ...]]: """ Index the predefined licenses by the Python classifier they are published as. A license without a classifier is skipped rather than reported: :attr:`License.PythonClassifier` raises for it, and a license the Python ecosystem has no classifier for simply can't be found that way. The licenses indexed are :data:`LICENSES`, and :data:`LICENSES_BY_CLASSIFIER` holds what this returns, built once when the module is imported. Reading that constant is the usual way to ask; calling this rebuilds it. :returns: Every classifier, mapped to the licenses it can mean. """ index: dict[str, list[License]] = {} for spdxLicense in LICENSES: try: classifier = spdxLicense.PythonClassifier except ValueError: continue index.setdefault(classifier, []).append(spdxLicense) return {classifier: tuple(licenses) for classifier, licenses in index.items()}
#: Mapping of a Python license classifier to the licenses it can mean. #: #: The mapping is one-to-one except for ``License :: OSI Approved :: BSD License``, which means either #: :data:`BSD_2_Clause_License` or :data:`BSD_3_Clause_License` with nothing in the classifier to tell them apart. LICENSES_BY_CLASSIFIER: dict[str, tuple[License, ...]] = buildClassifierIndex() #: The :class:`License` class under a name no expression node shadows with a property of its own. _LicenseType = License
[docs] @export @abstractclass class LicenseExpression(metaclass=ExtendedType, slots=True): """ Base-class of every node in an `SPDX license expression`_ tree. .. _SPDX license expression: https://spdx.github.io/spdx-spec/v2.3/SPDX-license-expressions/ An expression is a tree. Its operators - every node that is applied to operands, and the only kind of node that can be a :attr:`Parent` - derive from :class:`Operator`: :class:`OrLaterOperator` |rarr| the ``+`` suffix: the named license or any later version of it. :class:`WithOperator` |rarr| ``WITH``: a license together with an exception to it. :class:`AndOperator` |rarr| ``AND``: both licenses apply. :class:`OrOperator` |rarr| ``OR``: either license applies. Its leaves are: :class:`SPDXLicense` |rarr| a license on the SPDX License List, named by its identifier. :class:`LicenseReference` |rarr| a license that is *not* on that list, written as ``LicenseRef-<id>``. :class:`LicenseException` |rarr| an exception from the SPDX exception list, the right operand of ``WITH``. The SPDX grammar defines: .. code-block:: text simple-expression = license-id | license-id "+" | license-ref compound-expression = ( simple-expression | simple-expression "WITH" license-exception-id | compound-expression "AND" compound-expression | compound-expression "OR" compound-expression | "(" compound-expression ")" ) There are three binary operators, one unary one, and parentheses. There is **no negation** - an expression says which licenses apply, never which don't. Every node knows its :attr:`Parent` and its :attr:`Root`, which is what a :class:`License` can't carry: the predefined licenses are shared objects, so :data:`MIT_License` appears in many expressions at once and belongs to none of them. :class:`SPDXLicense` is the wrapper that gives a license object a placeholder in the expression tree. A tree is built bottom-up by handing the operands to an operator, or by assigning them to the operator's operand slots afterwards. Both link the operand back to its operator: .. code-block:: python bottomUp = AndOperator(SPDXLicense(Apache_2_0_License), SPDXLicense(MIT_License)) assembled = AndOperator() assembled.Left = SPDXLicense(Apache_2_0_License) assembled.Right = SPDXLicense(MIT_License) The ``parent`` parameter only *records* a parent - it can't know which slot the operand belongs in, so it never fills one. """ PRECEDENCE: ClassVar[int] = 0 #: Precedence of this node's operator; a lower value binds tighter. _parent: Nullable["Operator"] #: The operator this expression is an operand of, or ``None`` at the root. _root: "LicenseExpression" #: The outermost expression this node belongs to; ``self`` at the root. _originalText: str #: The text this expression stands for, on the root; empty if there is none.
[docs] def __init__(self, parent: Nullable["Operator"] = None, originalText: str = "") -> None: """ Initialize an expression node. :param parent: Optional, the operator this node becomes an operand of. :param originalText: Optional, the text this expression stands for. :meth:`Parse` fills it in; a node built in code carries it when the caller knows what was written. :raises TypeError: If parameter 'parent' is not of type :class:`Operator`. """ self._originalText = originalText if parent is None: self._parent = None self._root = self elif not isinstance(parent, Operator): ex = TypeError("Parameter 'parent' is not an Operator.") ex.add_note(f"Got type '{getFullyQualifiedName(parent)}'.") raise ex else: self._parent = parent self._root = parent._root
@property def Parent(self) -> Nullable["Operator"]: """ Property to access the operator this expression is an operand of (:attr:`_parent`). Assigning an operator records it as this node's parent and re-roots this node and everything below it to that operator's :attr:`Root`. The operator's operands are not changed by the assignment. :returns: The parent operator, or ``None`` if this node is the root. :raises ValueError: If ``None`` is assigned. |br| A node that is an operand of an operator can't be detached from it. :raises TypeError: If an object that is not an :class:`Operator` is assigned. |br| A leaf is applied to nothing, so it can never be a parent. """ return self._parent @Parent.setter def Parent(self, parent: Nullable["Operator"]) -> None: if parent is None: raise ValueError("Parameter 'parent' is None.") elif not isinstance(parent, Operator): ex = TypeError("Parameter 'parent' is not an Operator.") ex.add_note(f"Got type '{getFullyQualifiedName(parent)}'.") raise ex self._parent = parent for expression in self.IterateExpression(): expression._root = parent._root @readonly def OriginalText(self) -> str: """ Read-only property to access the text this expression stands for (:attr:`_originalText`). A tree is parsed from one string, so **every node answers with the root's** - reading it from a leaf gives the whole expression, not that leaf's fragment. :meth:`__str__` re-renders the tree canonically, which is not always what was written: ``Apache-2.0 or MIT`` parses and renders as ``Apache-2.0 OR MIT``. This is what was written. :meth:`Parse` fills it in. A node **built** in code carries it when the caller knows what was stated and the expression can't be parsed from it - an :class:`UnknownLicense` standing for a license that didn't resolve keeps the text that didn't, and a :class:`ProprietaryLicense` keeps the classifier it was built from. :returns: The text this expression stands for, or an empty string if there is none. """ return self._root._originalText @readonly def Root(self) -> "LicenseExpression": """ Read-only property to access the outermost expression this node belongs to (:attr:`_root`). The root is maintained by :attr:`Parent`: adopting a node re-roots it and everything below it, so the field can't fall behind the tree it describes. :returns: The root of the expression tree, which is the node itself if it has no parent. """ return self._root
[docs] def IterateExpression(self) -> Generator["LicenseExpression", None, None]: """ Iterate this expression depth-first, in the order its nodes are written. A leaf is the expression itself. The operators override this to yield their operands around themselves, so an infix operator comes between its two operands and the ``+`` suffix comes after the expression it applies to. :returns: A generator of every node in this expression, this node included. """ yield self
[docs] @classmethod def Parse(cls, expression: str) -> "LicenseExpression": """ Parse an SPDX license expression into a tree of expression nodes. Operator precedence is the one SPDX defines - ``+`` binds tighter than ``WITH``, which binds tighter than ``AND``, which binds tighter than ``OR`` - and parentheses override it. ``AND`` and ``OR`` associate to the left. :param expression: The SPDX license expression to parse. :returns: The root of the parsed expression tree. :raises LicenseExpressionError: If the expression is empty, malformed, or names a license that isn't known. """ parser = _LicenseExpressionParser(expression) root = parser.Parse() root._originalText = expression return root
[docs] @abstractmethod def __str__(self) -> str: # type: ignore[empty-body] """ Return this expression in SPDX syntax. Parentheses are written only where the default precedence would otherwise read the expression differently, so a parsed expression renders back to its shortest correct form rather than a fully bracketed one. :returns: The expression in SPDX syntax. """
[docs] @export @abstractclass class Operator(LicenseExpression): """ Base-class of every expression node that is applied to operands. Operator kinds: * :class:`UnaryOperator` takes one operand * :class:`BinaryOperator` takes two operands. Only an operator can be an operand's :attr:`~LicenseExpression.Parent`. """
[docs] @export @abstractclass class BaseLicense(LicenseExpression): """ Base-class of every expression node that names a license the work is under. Four nodes are one: :class:`SPDXLicense` |rarr| a license on the SPDX License List, named by its identifier. :class:`LicenseReference` |rarr| a license that is not on that list, written as ``LicenseRef-<id>``. :class:`ProprietaryLicense` |rarr| a license that isn't published at all; a :class:`LicenseReference` with a fixed identifier. :class:`UnknownLicense` |rarr| SPDX's ``NONE`` or ``NOASSERTION`` - no license is named. They have no common representation - SPDX knows the first as a :class:`License` object and the rest not at all - so this class is what lets them be collected and reported together: .. code-block:: python [term.Identifier for term in expression.IterateExpression() if isinstance(term, BaseLicense)] # ['MIT', 'LicenseRef-Proprietary'] for 'MIT AND LicenseRef-Proprietary' :class:`LicenseException` is **not** one of these. The right operand of ``WITH`` is an exception granted from a license, not a license the work is under. """ @readonly @abstractmethod def Identifier(self) -> str: # type: ignore[empty-body] """ Read-only property to return the identifier naming this license. :returns: The SPDX identifier, or the ``LicenseRef-`` reference, in the spelling an expression writes it. """ @readonly @abstractmethod def Name(self) -> str: # type: ignore[empty-body] """ Read-only property to return the license' name, as it is written for a reader. :attr:`Identifier` is what an expression writes; this is what a document prints. For a license on the SPDX List the two differ - ``Apache-2.0`` against ``Apache License 2.0`` - and for the rest they are close or equal, because nothing else has a name SPDX publishes. :returns: The license' name. """
[docs] @export class SPDXLicense(BaseLicense): """ A single license in an expression, named by its SPDX identifier. The identifiers an expression may name are the `SPDX License List <https://spdx.org/licenses/>`__; the ones this package predefines are :data:`LICENSES`, and :attr:`License` reaches the :class:`License` object holding a license's name, approval flags and the URLs of its text. A license the list defines but this package doesn't predefine is a :class:`LicenseReference` when written as ``LicenseRef-``, and otherwise doesn't parse. """ _license: _LicenseType #: The well-known license this node refers to.
[docs] def __init__(self, spdxLicense: _LicenseType, parent: Nullable[Operator] = None) -> None: """ Initialize a reference to an SPDX license. :param spdxLicense: The license this node stands for. :param parent: Optional, the operator this node becomes an operand of. :raises TypeError: If parameter 'parent' is not of type :class:`Operator`. :raises ValueError: If parameter 'spdxLicense' is None. :raises TypeError: If parameter 'spdxLicense' is not of type :class:`License`. """ super().__init__(parent) if spdxLicense is None: raise ValueError("Parameter 'spdxLicense' is None.") elif not isinstance(spdxLicense, _LicenseType): ex = TypeError("Parameter 'spdxLicense' is not a License.") ex.add_note(f"Got type '{getFullyQualifiedName(spdxLicense)}'.") raise ex self._license = spdxLicense
@readonly def License(self) -> _LicenseType: """ Read-only property to access the license this node stands for (:attr:`_license`). :returns: The license. """ return self._license @readonly def Identifier(self) -> str: """ Read-only property to access the license' SPDX identifier (:pycode:`_license._spdxIdentifier`). :returns: The license's SPDX identifier. """ return self._license._spdxIdentifier @readonly def Name(self) -> str: """ Read-only property to access the license' name (:pycode:`_license._name`). :returns: The name SPDX publishes for this license, like ``Apache License 2.0``. """ return self._license._name
[docs] def __str__(self) -> str: """ Return the license's SPDX identifier. :returns: The SPDX identifier. """ return self._license.SPDXIdentifier
[docs] @export class LicenseReference(BaseLicense): """ A license that isn't on the SPDX License List, written as ``LicenseRef-<id>``. It may name the document it is defined in, as ``DocumentRef-<id>:LicenseRef-<id>``. There is no predefined :class:`License` object, because SPDX doesn't know the license - only the document declaring it does. """ _licenseIdentifier: str #: Identifier following ``LicenseRef-``. _documentIdentifier: Nullable[str] #: Identifier following ``DocumentRef-``, if the reference names one.
[docs] def __init__( self, licenseIdentifier: str, documentIdentifier: Nullable[str] = None, parent: Nullable[Operator] = None ) -> None: """ Initialize a license reference. :param licenseIdentifier: Identifier following ``LicenseRef-``. :param documentIdentifier: Optional, identifier following ``DocumentRef-``. :param parent: Optional, the operator this node becomes an operand of. :raises TypeError: If parameter 'parent' is not of type :class:`Operator`. :raises ValueError: If parameter 'licenseIdentifier' is None. :raises TypeError: If parameter 'licenseIdentifier' is not of type :class:`str`. :raises ValueError: If parameter 'licenseIdentifier' is empty. :raises TypeError: If parameter 'documentIdentifier' is not of type :class:`str`. :raises ValueError: If parameter 'documentIdentifier' is empty. """ super().__init__(parent) if licenseIdentifier is None: raise ValueError("Parameter 'licenseIdentifier' is None.") elif not isinstance(licenseIdentifier, str): ex = TypeError("Parameter 'licenseIdentifier' is not a string.") ex.add_note(f"Got type '{getFullyQualifiedName(licenseIdentifier)}'.") raise ex elif len(licenseIdentifier) == 0: raise ValueError("Parameter 'licenseIdentifier' is empty.") if documentIdentifier is None: pass elif not isinstance(documentIdentifier, str): ex = TypeError("Parameter 'documentIdentifier' is not a string.") ex.add_note(f"Got type '{getFullyQualifiedName(documentIdentifier)}'.") raise ex elif len(documentIdentifier) == 0: raise ValueError("Parameter 'documentIdentifier' is empty.") self._licenseIdentifier = licenseIdentifier self._documentIdentifier = documentIdentifier
@readonly def LicenseIdentifier(self) -> str: """ Read-only property to access the identifier following ``LicenseRef-`` (:attr:`_licenseIdentifier`). :returns: The license reference's identifier. """ return self._licenseIdentifier @readonly def DocumentIdentifier(self) -> Nullable[str]: """ Read-only property to access the identifier following ``DocumentRef-`` (:attr:`_documentIdentifier`). :returns: The document reference's identifier, or ``None`` if the reference names no document. """ return self._documentIdentifier @readonly def Identifier(self) -> str: """ Read-only property to return the reference in the spelling an expression writes it. This is the whole reference, ``DocumentRef-`` and all - :attr:`LicenseIdentifier` is the part following ``LicenseRef-`` on its own. :returns: The license reference. """ return str(self)
[docs] def __str__(self) -> str: """ Return the reference in SPDX syntax. :returns: The license reference. """ document = "" if self._documentIdentifier is None else f"DocumentRef-{self._documentIdentifier}:" return f"{document}LicenseRef-{self._licenseIdentifier}"
@readonly def Name(self) -> str: """ Read-only property to access the identifier following ``LicenseRef-`` (:attr:`_licenseIdentifier`). SPDX publishes no name for a license that isn't on its list, so the identifier the document chose is the only name there is. :returns: The license reference's identifier. """ return self._licenseIdentifier
[docs] @export class ProprietaryLicense(LicenseReference): """ A license that is not open source and is not published under any identifier - an EULA, or a company's own terms. SPDX has no way to say this. Its list is a list of *published* licenses, and a proprietary one is by definition not on it, so the only thing SPDX offers is the generic escape hatch: this renders as ``LicenseRef-Proprietary`` and **is** a :class:`LicenseReference`, because that is what it writes. .. code-block:: python str(ProprietaryLicense()) # 'LicenseRef-Proprietary' The round-trip is deliberately one-way: parsing ``LicenseRef-Proprietary`` back gives a plain :class:`LicenseReference`, because SPDX defines no convention that makes that identifier mean *proprietary* rather than being one project's choice of words. This class is constructed where something else already knows - :mod:`pyTooling.Dependency` builds one from PyPI's ``License :: Other/Proprietary License`` classifier. A proprietary license that *does* have a name of its own is a :class:`LicenseReference` with that name, not this class - ``LicenseReference("AcmeEULA-1.0")`` renders as ``LicenseRef-AcmeEULA-1.0``. """ #: The identifier a proprietary license is written with, following ``LicenseRef-``. IDENTIFIER: ClassVar[str] = "Proprietary"
[docs] def __init__(self, parent: Nullable[Operator] = None, originalText: str = "") -> None: """ Initialize a proprietary license. :param parent: Optional, the operator this node becomes an operand of. :param originalText: Optional, what stated this - a classifier, say - since it can't have been parsed. :raises TypeError: If parameter 'parent' is not of type :class:`Operator`. """ super().__init__(self.IDENTIFIER, None, parent) self._originalText = originalText
@readonly def Name(self) -> str: """ Read-only property to return a readable name for a license that publishes none. :returns: ``Proprietary License``. """ return "Proprietary License"
[docs] @export class UnknownLicense(BaseLicense): """ SPDX's statement that no license is named: ``NONE`` or ``NOASSERTION``. The two are different claims - :attr:`LicenseAbsence.NoLicense` says the work states that no license applies, :attr:`LicenseAbsence.NoAssertion` says someone looked and declined to say - so which one is asked with :attr:`Absence`. .. code-block:: python LicenseExpression.Parse("NOASSERTION") # UnknownLicense(LicenseAbsence.NoAssertion) LicenseExpression.Parse("NONE") # UnknownLicense(LicenseAbsence.NoLicense) **Neither can be an operand.** SPDX's grammar is ``simple-expression | compound-expression``; ``NONE`` and ``NOASSERTION`` are values a license *field* may hold instead of an expression, not terms inside one. So ``MIT AND NOASSERTION`` doesn't parse, and this node refuses a :attr:`~LicenseExpression.Parent`. .. seealso:: :class:`ProprietaryLicense` |rarr| For a license that exists but isn't published - which is a different statement from this one. """ _absence: LicenseAbsence #: Which of SPDX's two absences this node states.
[docs] def __init__(self, absence: LicenseAbsence = LicenseAbsence.NoAssertion, originalText: str = "") -> None: """ Initialize an absent license. No ``parent`` parameter, because neither value may be an operand. :param absence: Optional, which absence is stated. Defaults to :attr:`LicenseAbsence.NoAssertion`. :param originalText: Optional, what was stated. A license nothing could be made of keeps the text here, which is the only place left holding it. :raises TypeError: If parameter 'absence' is not of type :class:`LicenseAbsence`. """ super().__init__(None, originalText) if not isinstance(absence, LicenseAbsence): ex = TypeError("Parameter 'absence' is not a LicenseAbsence.") ex.add_note(f"Got type '{getFullyQualifiedName(absence)}'.") raise ex self._absence = absence
@readonly def Absence(self) -> LicenseAbsence: """ Read-only property to access which absence this node states (:attr:`_absence`). :returns: The absence stated. """ return self._absence @readonly def Identifier(self) -> str: """ Read-only property to access the absence in SPDX's spelling (:pycode:`_absence.value`). :returns: ``NONE`` or ``NOASSERTION``. """ return self._absence.value @readonly def Name(self) -> str: """ Read-only property to return which absence this states, in words. :returns: ``No license`` for :attr:`~LicenseAbsence.NoLicense`, ``No assertion`` otherwise. """ return "No license" if self._absence is LicenseAbsence.NoLicense else "No assertion" @readonly def Parent(self) -> Nullable[Operator]: """ Read-only property to access the operator this expression is an operand of (:attr:`_parent`). Always ``None``, and **read-only** where every other node's is assignable: ``NONE`` and ``NOASSERTION`` are values a license field may hold *instead of* an expression, and SPDX's grammar has no place for either inside one. So this node is always the whole expression, and assigning a parent raises :exc:`AttributeError`. :returns: ``None``, always. """ return self._parent
[docs] def __str__(self) -> str: """ Return the absence in SPDX's spelling. :returns: ``NONE`` or ``NOASSERTION``. """ return self._absence.value
[docs] @export class LicenseException(LicenseExpression): """ The right operand of a :class:`WithOperator`, naming an exception from the SPDX exception list. 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 stand on its own. """ _identifier: str #: The exception's SPDX identifier.
[docs] def __init__(self, identifier: str, parent: Nullable[Operator] = None) -> None: """ Initialize a license exception. :param identifier: The exception's SPDX identifier. :param parent: Optional, the operator this node becomes an operand of. :raises TypeError: If parameter 'parent' is not of type :class:`Operator`. :raises ValueError: If parameter 'identifier' is None. :raises TypeError: If parameter 'identifier' is not of type :class:`str`. :raises ValueError: If parameter 'identifier' is empty. """ super().__init__(parent) if identifier is None: raise ValueError("Parameter 'identifier' is None.") elif not isinstance(identifier, str): ex = TypeError("Parameter 'identifier' is not a string.") ex.add_note(f"Got type '{getFullyQualifiedName(identifier)}'.") raise ex elif len(identifier) == 0: raise ValueError("Parameter 'identifier' is empty.") self._identifier = identifier
@readonly def Identifier(self) -> str: """ Read-only property to access the exception's SPDX identifier (:attr:`_identifier`). :returns: The exception's identifier. """ return self._identifier
[docs] def __str__(self) -> str: """ Return the exception's SPDX identifier. :returns: The identifier. """ return self._identifier
[docs] @export @abstractclass class UnaryOperator(Operator): """ Base-class of the expression operators taking one operand. SPDX defines exactly one unary operator: * the ``+`` suffix of :class:`OrLaterOperator`. The operand is reachable as :attr:`Operand` and is assignable, so an operator can be filled after it was created. """ _operand: Nullable[LicenseExpression] #: The expression this operator is applied to.
[docs] def __init__( self, operand: Nullable[LicenseExpression] = None, parent: Nullable[Operator] = None ) -> None: """ Initialize a unary operator with its operand. :param operand: Optional, the expression this operator is applied to. :param parent: Optional, the operator this node becomes an operand of. :raises TypeError: If parameter 'parent' is not of type :class:`Operator`. :raises TypeError: If parameter 'operand' is not of type :class:`LicenseExpression`. """ super().__init__(parent) if operand is not None: self._operand = operand if not isinstance(operand, LicenseExpression): ex = TypeError("Parameter 'operand' is not a LicenseExpression.") ex.add_note(f"Got type '{getFullyQualifiedName(operand)}'.") raise ex operand.Parent = self else: self._operand = None
[docs] def IterateExpression(self) -> Generator[LicenseExpression, None, None]: """ Iterate this operator depth-first, its operand first, because ``+`` is written after the expression it suffixes. :returns: A generator of every node in this expression, this operator included. """ if self._operand is not None: yield from self._operand.IterateExpression() yield self
@property def Operand(self) -> Nullable[LicenseExpression]: """ Property to access the expression this operator is applied to (:attr:`_operand`). A slot is filled once. Assigning an expression makes this operator its :attr:`~LicenseExpression.Parent`, which puts it - and everything below it - into this tree. :returns: The operand, or ``None`` if the operator wasn't assigned yet. :raises ValueError: If ``None`` is assigned. :raises TypeError: If an object that is not a :class:`LicenseExpression` is assigned. :raises LicensingError: If this operator has an operand already. :raises LicensingError: If the assigned expression is already an operand of another operator. """ return self._operand @Operand.setter def Operand(self, operand: LicenseExpression) -> None: if operand is None: raise ValueError("Parameter 'operand' is None.") elif not isinstance(operand, LicenseExpression): ex = TypeError("Parameter 'operand' is not a LicenseExpression.") ex.add_note(f"Got type '{getFullyQualifiedName(operand)}'.") raise ex elif self._operand is not None: raise LicensingError(f"Operator '{getFullyQualifiedName(self)}' has an operand already.") elif operand._parent is not None: raise LicensingError("Parameter 'operand' is already an operand of another operator.") self._operand = operand operand.Parent = self
[docs] @export class OrLaterOperator(UnaryOperator): """ The ``+`` suffix, as in ``GPL-2.0+``: the named license *or any later version of it*. .. attention:: The operator itself is not deprecated - Annex D.3 of the SPDX specification defines it and uses ``CDDL-1.0+`` as its example. Its combination with the GNU licenses is: `SPDX License List 3.0 <https://spdx.org/licenses/>`__ (2017-12-28) introduced ``GPL-2.0-only`` and ``GPL-2.0-or-later`` and deprecated the ``GPL-2.0``, ``LGPL-2.1`` and ``AGPL-3.0`` identifiers that ``+`` was applied to. Published metadata still contains ``GPL-2.0+``, so an expression using it has to parse. """ PRECEDENCE: ClassVar[int] = 1 #: Binds tighter than every binary operator, looser than a bare license.
[docs] def __str__(self) -> str: """ Return the operand followed by ``+``. :returns: The expression in SPDX syntax. :raises LicensingError: If the operator has no operand yet. """ if self._operand is None: raise LicensingError(f"Operator '{getFullyQualifiedName(self)}' has no operand yet.") return f"{self._operand}+"
[docs] @export @abstractclass class BinaryOperator(Operator): """ Base-class of the expression operators taking two operands. SPDX defines three binary operators: * :class:`WithOperator`, * :class:`AndOperator` and * :class:`OrOperator`. The operands are reachable as :attr:`Left` and :attr:`Right`. :attr:`KEYWORD` is the operator keyword between the operands. """ KEYWORD: ClassVar[str] #: The operator's keyword, as it is written between the operands. _left: Nullable[LicenseExpression] #: The operator's left operand. _right: Nullable[LicenseExpression] #: The operator's right operand.
[docs] def __init__( self, left: Nullable[LicenseExpression] = None, right: Nullable[LicenseExpression] = None, parent: Nullable[Operator] = None ) -> None: """ Initialize a binary operator with both operands. :param left: Optional, the operator's left operand. :param right: Optional, the operator's right operand. :param parent: Optional, the operator this node becomes an operand of. :raises TypeError: If parameter 'parent' is not of type :class:`Operator`. :raises TypeError: If parameter 'left' is not of type :class:`LicenseExpression`. :raises TypeError: If parameter 'right' is not of type :class:`LicenseExpression`. """ super().__init__(parent) if left is not None: self._left = left if not isinstance(left, LicenseExpression): ex = TypeError("Parameter 'left' is not a LicenseExpression.") ex.add_note(f"Got type '{getFullyQualifiedName(left)}'.") raise ex left.Parent = self else: self._left = None if right is not None: self._right = right if not isinstance(right, LicenseExpression): ex = TypeError("Parameter 'right' is not a LicenseExpression.") ex.add_note(f"Got type '{getFullyQualifiedName(right)}'.") raise ex right.Parent = self else: self._right = None
[docs] def IterateExpression(self) -> Generator[LicenseExpression, None, None]: """ Iterate this operator depth-first in infix order: left operand, this operator, right operand. :returns: A generator of every node in this expression, this operator included. """ if self._left is not None: yield from self._left.IterateExpression() yield self if self._right is not None: yield from self._right.IterateExpression()
@property def Left(self) -> Nullable[LicenseExpression]: """ Property to access the operator's left operand (:attr:`_left`). A slot is filled once. Assigning an expression makes this operator its :attr:`~LicenseExpression.Parent`, which puts it - and everything below it - into this tree. :returns: The left operand, or ``None`` if it wasn't assigned yet. :raises ValueError: If ``None`` is assigned. :raises TypeError: If an object that is not a :class:`LicenseExpression` is assigned. :raises LicensingError: If this operator has a left operand already. :raises LicensingError: If the assigned expression is already an operand of another operator. """ return self._left @Left.setter def Left(self, operand: LicenseExpression) -> None: if operand is None: raise ValueError("Parameter 'operand' is None.") elif not isinstance(operand, LicenseExpression): ex = TypeError("Parameter 'operand' is not a LicenseExpression.") ex.add_note(f"Got type '{getFullyQualifiedName(operand)}'.") raise ex elif self._left is not None: raise LicensingError(f"Operator '{getFullyQualifiedName(self)}' has a left operand already.") elif operand._parent is not None: raise LicensingError("Parameter 'operand' is already an operand of another operator.") self._left = operand operand.Parent = self @property def Right(self) -> Nullable[LicenseExpression]: """ Property to access the operator's right operand (:attr:`_right`). A slot is filled once. Assigning an expression makes this operator its :attr:`~LicenseExpression.Parent`, which puts it - and everything below it - into this tree. :returns: The right operand, or ``None`` if it wasn't assigned yet. :raises ValueError: If ``None`` is assigned. :raises TypeError: If an object that is not a :class:`LicenseExpression` is assigned. :raises LicensingError: If this operator has a right operand already. :raises LicensingError: If the assigned expression is already an operand of another operator. """ return self._right @Right.setter def Right(self, operand: LicenseExpression) -> None: if operand is None: raise ValueError("Parameter 'operand' is None.") elif not isinstance(operand, LicenseExpression): ex = TypeError("Parameter 'operand' is not a LicenseExpression.") ex.add_note(f"Got type '{getFullyQualifiedName(operand)}'.") raise ex elif self._right is not None: raise LicensingError(f"Operator '{getFullyQualifiedName(self)}' has a right operand already.") elif operand._parent is not None: raise LicensingError("Parameter 'operand' is already an operand of another operator.") self._right = operand operand.Parent = self
[docs] def __str__(self) -> str: """ Return both operands with the operator's keyword between them. :returns: The expression in SPDX syntax. :raises LicensingError: If one of the operator's operands wasn't assigned yet. """ if self._left is None: raise LicensingError(f"Operator '{getFullyQualifiedName(self)}' has no left operand yet.") elif self._right is None: raise LicensingError(f"Operator '{getFullyQualifiedName(self)}' has no right operand yet.") left = f"({self._left})" if self._left.PRECEDENCE > self.PRECEDENCE else f"{self._left}" right = f"({self._right})" if self._right.PRECEDENCE > self.PRECEDENCE else f"{self._right}" return f"{left} {self.KEYWORD} {right}"
[docs] @export class WithOperator(BinaryOperator): """ ``WITH``, as in ``Apache-2.0 WITH LLVM-exception``: a license together with an exception to it. It is the only operator whose operands differ in kind - the right one is a :class:`LicenseException`, never a license - and it binds tighter than ``AND`` and ``OR``. """ PRECEDENCE: ClassVar[int] = 2 #: Binds tighter than ``AND`` and ``OR``. KEYWORD: ClassVar[str] = "WITH" #: The operator's keyword.
[docs] @export class AndOperator(BinaryOperator): """``AND``, as in ``Apache-2.0 AND MIT``: **both** licenses apply, and both have to be complied with.""" PRECEDENCE: ClassVar[int] = 3 #: Binds tighter than ``OR``. KEYWORD: ClassVar[str] = "AND" #: The operator's keyword.
[docs] @export class OrOperator(BinaryOperator): """ ``OR``, as in ``Apache-2.0 OR BSD-2-Clause``: **either** license applies, and the recipient chooses which. Which one they chose is not something the expression records. """ PRECEDENCE: ClassVar[int] = 4 #: Binds loosest of all operators. KEYWORD: ClassVar[str] = "OR" #: The operator's keyword.
class _LicenseExpressionParser(metaclass=ExtendedType, slots=True): """ Recursive-descent parser for SPDX license expressions. One level of the descent per precedence level, lowest-binding first, which is what makes ``A OR B AND C`` parse as ``A OR (B AND C)`` without the grammar having to say so twice. The descent is a class rather than a function because every level reads and advances the same token position, and that position is state the levels share. """ _TOKEN = re_compile(r"\(|\)|[^\s()]+") #: Splits an expression into parentheses and the words between them. _expression: str #: The expression being parsed, kept for the error messages. _tokens: list[str] #: The expression's tokens, in order. _position: int #: Index of the token to read next. def __init__(self, expression: str) -> None: """ Tokenize an expression. :param expression: The SPDX license expression to parse. :raises TypeError: If parameter 'expression' is not of type :class:`str`. :raises ValueError: If parameter 'expression' is empty. :raises LicenseExpressionError: If the expression holds nothing but whitespace. """ if not isinstance(expression, str): ex = TypeError("Parameter 'expression' is not a string.") ex.add_note(f"Got type '{getFullyQualifiedName(expression)}'.") raise ex elif expression == "": raise ValueError("Parameter 'expression' is empty.") self._expression = expression self._tokens = self._TOKEN.findall(expression) self._position = 0 if len(self._tokens) == 0: raise LicenseExpressionError(f"License expression '{expression}' is empty.") def Parse(self) -> LicenseExpression: """ Parse the whole expression. ``NONE`` and ``NOASSERTION`` are read here rather than in the descent, because SPDX allows them **only** as the entire expression - they are values a license field may hold instead of one, not terms inside one. Seeing either anywhere else leaves it to the descent, where it is an unknown license identifier. :returns: The root of the expression tree. :raises LicenseExpressionError: If the expression is malformed or names an unknown license. """ if len(self._tokens) == 1: for absence in LicenseAbsence: if self._tokens[0] == absence.value: self._position = 1 return UnknownLicense(absence) result = self._ParseOr() if self._position < len(self._tokens): raise LicenseExpressionError( f"License expression '{self._expression}' has trailing input at '{self._tokens[self._position]}'." ) return result def _ParseOr(self) -> LicenseExpression: """ Parse a sequence of ``OR`` operands, the loosest-binding operator. :returns: The parsed expression. """ left = self._ParseAnd() while self._Accept(OrOperator.KEYWORD): left = OrOperator(left, self._ParseAnd()) return left def _ParseAnd(self) -> LicenseExpression: """ Parse a sequence of ``AND`` operands. :returns: The parsed expression. """ left = self._ParseWith() while self._Accept(AndOperator.KEYWORD): left = AndOperator(left, self._ParseWith()) return left def _ParseWith(self) -> LicenseExpression: """ Parse a ``WITH`` clause, whose right operand is an exception rather than a license. :returns: The parsed expression. :raises LicenseExpressionError: If ``WITH`` isn't followed by an exception identifier. """ left = self._ParseSimple() if self._Accept(WithOperator.KEYWORD): if (identifier := self._Next()) is None: raise LicenseExpressionError(f"License expression '{self._expression}' ends after 'WITH'.") left = WithOperator(left, LicenseException(identifier)) return left def _ParseSimple(self) -> LicenseExpression: """ Parse a parenthesized expression, a license reference, or a license identifier with an optional ``+``. :returns: The parsed expression. :raises LicenseExpressionError: If the expression ends early, a parenthesis is unbalanced, or a license is unknown. """ if (token := self._Next()) is None: raise LicenseExpressionError(f"License expression '{self._expression}' ends unexpectedly.") if token == "(": inner = self._ParseOr() if not self._Accept(")"): raise LicenseExpressionError(f"License expression '{self._expression}' is missing a closing parenthesis.") return inner if token == ")": raise LicenseExpressionError(f"License expression '{self._expression}' has an unmatched closing parenthesis.") # 'GPL-2.0+' is the deprecated spelling of 'GPL-2.0-or-later' and is still legal grammar orLater = token.endswith("+") identifier = token[:-1] if orLater else token if identifier.startswith("LicenseRef-"): expression: LicenseExpression = LicenseReference(identifier[len("LicenseRef-"):]) elif identifier.startswith("DocumentRef-") and ":LicenseRef-" in identifier: documentIdentifier, _, licenseIdentifier = identifier.partition(":LicenseRef-") expression = LicenseReference(licenseIdentifier, documentIdentifier[len("DocumentRef-"):]) elif (spdxLicense := SPDX_INDEX.get(identifier, None)) is not None: expression = SPDXLicense(spdxLicense) else: ex = LicenseExpressionError(f"License expression '{self._expression}' names unknown license '{identifier}'.") if any(identifier == absence.value for absence in LicenseAbsence): ex.add_note(f"'{identifier}' is a value a license field may hold instead of an expression.") ex.add_note("SPDX's grammar has no place for it inside one, so it may only stand alone.") else: ex.add_note("Known licenses are the SPDX identifiers in 'pyTooling.Licensing.SPDX_INDEX'.") ex.add_note("A license that isn't on that list is written 'LicenseRef-<id>'.") raise ex return OrLaterOperator(expression) if orLater else expression def _Next(self) -> Nullable[str]: """ Consume and return the next token. :returns: The next token, or ``None`` if the expression is exhausted. """ if self._position >= len(self._tokens): return None self._position += 1 return self._tokens[self._position - 1] def _Accept(self, keyword: str) -> bool: """ Consume the next token if it is the given keyword. Keywords are matched case-insensitively, because published metadata writes ``and`` as often as ``AND``. :param keyword: The keyword to look for. :returns: ``True``, if the keyword was there and was consumed. """ if self._position < len(self._tokens) and self._tokens[self._position].upper() == keyword: self._position += 1 return True return False