Licensing

The pyTooling.Licensing package provides auxiliary classes to represent commonly known licenses and mappings of their names, because some tools use differing names for the same license.

Background Information

There are several names, identifiers and (Python package) classifiers referring to the same license. E.g. package classifiers used by setuptools and displayed by PIP/PyPI are different from SPDX identifiers and sometimes they are not even identical to the official license names. Also some allegedly similar licenses got different SPDX identifiers.

The package pyTooling.Licensing provides license name and identifiers mappings to unify all these names and classifiers to and from SPDX identifiers.

Examples:

SDPX Identifier

Official License Name

License (short) Name

Python package classifier

Apache-2.0

Apache License, Version 2.0

Apache 2.0

License :: OSI Approved :: Apache Software License

BSD-3-Clause

The 3-Clause BSD License

BSD

License :: OSI Approved :: BSD License

Licenses

The License class represents of a license like Apache License, Version 2.0 (SPDX: Apache-2.0). It offers several information about a license as properties. Licenses can be compared for equality (==, !=) based on there SPDX identifier.

Condensed definition of class License:

@export
class License(metaclass=ExtendedType, slots=True):

  def __init__(
    self,
    spdxIdentifier: str,
    name: str,
    osiApproved: bool = False,
    fsfApproved: bool = False,
  ) -> None:
    ...

  @readonly
  def Name(self) -> str:
    ...

  @readonly
  def SPDXIdentifier(self) -> str:
    ...

  @readonly
  def SPDXURL(self) -> str:
    ...

  @readonly
  def URL(self) -> Nullable[str]:
    ...

  @readonly
  def TextURLs(self) -> dict[str, str]:
    ...

  @readonly
  def OSIURL(self) -> Nullable[str]:
    ...

  @readonly
  def OSIApproved(self) -> bool:
    ...

  @readonly
  def FSFApproved(self) -> bool:
    ...

  @readonly
  def PythonLicenseName(self) -> str:
    ...

  @readonly
  def PythonClassifier(self) -> str:
    ...

  def __eq__(self, other: Any) -> bool:
    ...

  def __ne__(self, other: Any) -> bool:
    ...

  def __hash__(self) -> int:
    ...

  def __le__(self, other: Any) -> bool:
    ...

  def __ge__(self, other: Any) -> bool:
    ...

  def __repr__(self) -> str:
    ...

  def __str__(self) -> str:
    ...

The licenses supported by this package are available as individual package variables.

Package variables of predefined licenses, grouped by family:

Permissive

Weak copyleft

Strong copyleft

Public domain dedications and waivers: Unlicense and CC0_1_0.

Note

CC0_1_0 is the one predefined license that is not OSI-approved, so its classifier is License :: CC0 1.0 Universal (CC0 1.0) Public Domain Dedication without the OSI Approved :: prefix. OSIApproved says so.

Hint

The -only and -or-later pairs are SPDX’s replacement for the old + suffix, and PyPI has a separate classifier for each - GNU General Public License v3 (GPLv3) versus ... v3 or later (GPLv3+). Picking the wrong one of a pair states a different license, so they are separate variables rather than one with a flag.

SPDX_INDEX maps every SPDX identifier above to its license.

from pyTooling.Licensing import Apache_2_0_License

license = Apache_2_0_License
print(f"Python classifier: {license.PythonClassifier}")
print(f"SPDX:              {license.SPDXIdentifier}")
# Python classifier: License :: OSI Approved :: Apache Software License
# SPDX:              Apache-2.0

In addition a dictionary (SPDX_INDEX) maps from SPDX identified to License instances.

from pyTooling.License import SPDX_INDEX

licenseName = "MIT"
license = SPDX_INDEX[licenseName]
print(f"Python classifier: {license.PythonClassifier}")
print(f"SPDX:              {license.SPDXIdentifier}")
# Python classifier: License :: OSI Approved :: MIT License
# SPDX:              MIT

When no license is named

Two situations SPDX’s list can’t answer, which have different nodes because they are different statements.

Nothing is named. SPDX defines two values a license field may hold instead of an expression:

LicenseExpression.Parse("NONE")          # UnknownLicense, Absence == LicenseAbsence.NoLicense
LicenseExpression.Parse("NOASSERTION")   # UnknownLicense, Absence == LicenseAbsence.NoAssertion

NONE says the work states that no license applies. NOASSERTION says someone looked and declined to say - which is not the same claim, so Absence keeps them apart.

Attention

Neither may be an operand. SPDX’s grammar is simple-expression | compound-expression, and these are field values rather than terms inside one. MIT AND NOASSERTION raises LicenseExpressionError, and assigning a Parent to an UnknownLicense raises ValueError.

A license that exists but isn’t published - an EULA, or a company’s own terms - has no SPDX identifier either, because the list is a list of published licenses. The only thing SPDX offers is its generic escape hatch:

str(ProprietaryLicense())   # 'LicenseRef-Proprietary'

So ProprietaryLicense is a LicenseReference, and it can be an operand like any other license. It is constructed where something already knows - pyTooling.Dependency builds one from PyPI’s License :: Other/Proprietary License classifier.

Note

Parsing LicenseRef-Proprietary back gives a plain LicenseReference, not a ProprietaryLicense. SPDX defines no convention that makes that identifier mean proprietary rather than being one project’s choice of words, so reading it as such would be a guess.

A proprietary license that has a name of its own is a reference with that name - LicenseReference("AcmeEULA-1.0") renders as LicenseRef-AcmeEULA-1.0.

Where a license is published

Every license carries two links to its text, so a report can point at the wording rather than only naming it.

SPDXURL is derived from the SPDX identifier, because SPDX publishes one page per identifier at a fixed address. OSIURL is looked up in OSI_LICENSE_URLS, because OSI’s addresses don’t follow the identifier.

from pyTooling.Licensing import MIT_License, GPL_2_0_only, CC0_1_0

print(MIT_License.SPDXURL)     # https://spdx.org/licenses/MIT.html
print(MIT_License.OSIURL)      # https://opensource.org/license/mit
print(GPL_2_0_only.OSIURL)     # https://opensource.org/license/gpl-2.0
print(CC0_1_0.OSIURL)          # None - not OSI-approved

Note

Two identifiers can share one OSI page. GPL-2.0-only and GPL-2.0-or-later both point at gpl-2.0, because only versus or later is SPDX’s distinction and not OSI’s. PSF-2.0 is published by OSI as Python-2.0, which is why the addresses are a table and not a rule.

OSIURL is None exactly when OSIApproved is False.

A license has four URLs, and they answer four different questions:

Property

What it points at

URL

The page where the licensor publishes it. Looked up in LICENSE_URLS.

SPDXURL

SPDX’s catalogue entry. Derived from the identifier.

OSIURL

OSI’s catalogue entry, if OSI approved it. Looked up in OSI_LICENSE_URLS.

TextURLs

The license text, by the format it is published as. Looked up in LICENSE_TEXT_URLS.

from pyTooling.Licensing import Apache_2_0_License

Apache_2_0_License.URL               # https://www.apache.org/licenses/LICENSE-2.0
Apache_2_0_License.SPDXURL           # https://spdx.org/licenses/Apache-2.0.html
Apache_2_0_License.OSIURL            # https://opensource.org/license/apache-2.0
Apache_2_0_License.TextURLs["txt"]   # https://www.apache.org/licenses/LICENSE-2.0.txt

TextURLs is keyed by the file extension without its dot - txt, md, rst, tex. Which formats exist is entirely the licensor’s choice: the GNU licenses publish four, most publish one, and several publish none beyond an HTML page. It returns a copy, so editing it can’t reach the table.

Note

MIT, BSD-2-Clause and BSD-3-Clause have no URL. Nobody but OSI publishes them, and that URL is already OSIURL - repeating it as a second answer would suggest a second source that doesn’t exist.

See also

SPDX License List

→ Every SPDX identifier, with its full name and license text.

OSI License List

→ Every license the Open Source Initiative has approved.

Mappings

PYTHON_LICENSE_NAMES offers a Python specific mapping from SPDX identifier to license names used by Python (setuptools). Each dictionary item contains a PythonLicenseNames instance which contains the license name and package classifier used by setuptools.

Every predefined license is listed in that mapping - the same 23 SPDX identifiers LICENSES holds. LICENSES_BY_CLASSIFIER is the inverse, from a Python classifier back to the licenses it can mean; it is one-to-one except for License :: OSI Approved :: BSD License, which names either BSD_2_Clause_License or BSD_3_Clause_License.

Usage with Setuptools

The following examples demonstrates the usage with setuptools in a setup.py.

Usage Example

from setuptools import setup
from pyTooling.Licensing import Apache_2_0_License

classifiers = [
  "Operating System :: OS Independent",
  "Programming Language :: Python :: 3 :: Only"
]

license = Apache_2_0_License
classifiers.append(license.PythonClassifier)

# Assemble other parameters
# ...

# Handover to setuptools
setup(
  # ...
  license=license.SPDXIdentifier,
  # ...
  classifiers=classifiers,
  # ...
)