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 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.
Licenses for documentation and media, the six Creative Commons 4.0 licenses:
Free |
Non-commercial |
No derivatives |
|---|---|---|
Note
None of them is OSI-approved, and only the two free ones are FSF-approved. PyPI has no classifier for any of
them, so PythonClassifier and
PythonLicenseName raise a ValueError.
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 PyPI has a classifier for is listed in that mapping - all of
LICENSES except the Creative Commons licenses.
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,
# ...
)
Competing Solutions
pyTooling.Licensing knows a selection of licenses - SPDX_INDEX holds 32 - with
their names, URLs, approvals and Python classifiers, and parses SPDX license expressions into a tree. It doesn’t
simplify or compare expressions, and checking whether licenses are compatible is not implemented: the comparison
operators of License raise NotImplementedError.
license-expression
Source: license-expression, on PyPI as license-expression.
Disadvantages
It knows a license by its key only: there is no Python classifier, no OSI or FSF approval and no URL of a license’s text.
It depends on
boolean.py.
Standoff
Both parse an SPDX license expression into a tree of operators and licenses. pyTooling rejects an unknown license always, license-expression when asked to validate.
Advantages
It bundles the whole SPDX license list and the ScanCode license database, so every published license is known.
It simplifies and normalizes an expression, and tests two expressions for equality, containment and equivalence.
spdx-tools
Source: tools-python, on PyPI as spdx-tools.
Disadvantages
It has nine direct dependencies, among them
rdflibandlicense-expression.
Standoff
A different purpose: it reads, writes, converts and validates SPDX documents - a software bill of materials in Tag/Value, RDF, JSON, YAML or XML - and handles their license expressions with
license-expression.
pip-licenses and licensecheck
Source: pip-licenses, licensecheck, on PyPI as pip-licenses and licensecheck.
Disadvantages
pip-licenses is a command line tool; it lists the licenses of the installed packages, taken from their classifiers or metadata.
licensecheck has eleven direct dependencies, among them
pydantic,richandrequests.
Standoff
Both report the licenses of a project’s dependencies, as
pyTooling.Dependencyreads a package version’s license into aLicenseExpressionfrom PyPI.
Advantages
pip-licenses writes its list in many formats and fails a build on a forbidden license (
--fail-on,--allow-only).licensecheck checks whether the dependencies’ licenses are compatible with the project’s license.