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 License, Version 2.0 |
|
|
|
The 3-Clause 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 |
|---|---|
The page where the licensor publishes it. Looked up in
|
|
SPDX’s catalogue entry. Derived from the identifier. |
|
OSI’s catalogue entry, if OSI approved it. Looked up in
|
|
The license text, by the format it is published as. Looked up in
|
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,
# ...
)