Convert Python license classifiers to SPDX expressions

Alongside with the license-related code the tests were significantly
refactored to run offline. The changes are intertwined and so shipped in
a single commit.
The licensing changes:
- updated Trove-Fedora mappings (removed Callaway expressions, added
  SPDX)
- delegated the compliance check of Fedora licenses to a source
  maintained by the Fedora Legal team
- the logic of license processing was moved to a dedicated module:
  license_processor

The testing changes:
- moved betamax configuration to conftest.py to achieve a reliable global setting
- mocked fedora licensing data and PyPI package data to stop making
  unnecessary requests - most of the betamax cassettes were deleted
- several tests rewritten, many added
- regenerated remaining betamax cassettes

This is a best-effort solution which can't (nor aims to) cover all edge-cases.
Unless Python adopts SPDX standard, this will always be an approximation.
This commit is contained in:
Karolina Surma
2023-04-25 10:17:47 +02:00
committed by Karolina Surma
parent 737036a322
commit dce4344006
40 changed files with 835 additions and 282 deletions
+8
View File
@@ -1,5 +1,13 @@
# Changelog
# Unreleased
### Changed
- Convert the Python classifiers to Fedora SPDX expressions (where possible, based on [PEP 639](https://peps.python.org/pep-0639/))
- When `--fedora-compliant` option is set, a check of the converted license expressions is performed
to determine whether the license is allowed in Fedora. Data is provided by the [Fedora Legal team](https://gitlab.com/fedora/legal/fedora-license-data/)
- tomllib library is used to read toml files from Python 3.11 on
# [0.6.1] - 2022-08-01
### Fixed
- Include LICENSE-MIT-0 in the sdist and fix license metadata
+82 -83
View File
@@ -1,85 +1,84 @@
{
"License :: Aladdin Free Public License (AFPL)": ["AFPL", "BAD"],
"License :: CC0 1.0 Universal (CC0 1.0) Public Domain Dedication": ["CC0", "GOOD"],
"License :: CeCILL-B Free Software License Agreement (CECILL-B)": ["CECILL-B", "GOOD"],
"License :: CeCILL-C Free Software License Agreement (CECILL-C)": ["CECILL-C", "GOOD"],
"License :: DFSG approved": ["DFSG approved", "UNKNOWN"],
"License :: Eiffel Forum License (EFL)": ["EFL", "BAD"],
"License :: Free For Educational Use": ["Free For Educational Use", "BAD"],
"License :: Free For Home Use": ["Free For Home Use", "BAD"],
"License :: Free To Use But Restricted": ["Free To Use But Restricted", "BAD"],
"License :: Free for non-commercial use": ["Free for non-commercial use", "BAD"],
"License :: Freely Distributable": ["Freely Distributable", "BAD"],
"License :: Freeware": ["Freeware", "BAD"],
"License :: GUST Font License 1.0": ["LPPL", "GOOD"],
"License :: GUST Font License 2006-09-30": ["GUST Font License 2006-09-30","UNKNOWN"],
"License :: Netscape Public License (NPL)": ["Netscape", "GOOD"],
"License :: Nokia Open Source License (NOKOS)": ["Nokia", "GOOD"],
"License :: OSI Approved": ["OSI Approved", "IGNORED"],
"License :: OSI Approved :: Academic Free License (AFL)": ["AFL", "GOOD"],
"License :: OSI Approved :: Apache Software License": ["ASL", "GOOD"],
"License :: OSI Approved :: Apple Public Source License": ["APSL", "???"],
"License :: OSI Approved :: Artistic License": ["Artistic", "???"],
"License :: OSI Approved :: Attribution Assurance License": ["AAL", "GOOD"],
"License :: OSI Approved :: BSD License": ["BSD", "GOOD"],
"License :: OSI Approved :: Boost Software License 1.0 (BSL-1.0)": ["Boost", "GOOD"],
"License :: OSI Approved :: CEA CNRS Inria Logiciel Libre License, version 2.1 (CeCILL-2.1)": ["CeCILL", "GOOD"],
"License :: OSI Approved :: Common Development and Distribution License 1.0 (CDDL-1.0)": ["CDDL-1.0", "GOOD"],
"License :: OSI Approved :: Common Public License": ["CPL", "GOOD"],
"License :: OSI Approved :: Eclipse Public License 1.0 (EPL-1.0)": ["EPL-1.0", "GOOD"],
"License :: OSI Approved :: Eclipse Public License 2.0 (EPL-2.0)": ["EPL-2.0", "GOOD"],
"License :: OSI Approved :: Eiffel Forum License": ["Eiffel Forum License", "???"],
"License :: OSI Approved :: European Union Public Licence 1.0 (EUPL 1.0)": ["EUPL 1.0", "BAD"],
"License :: OSI Approved :: European Union Public Licence 1.1 (EUPL 1.1)": ["EUPL 1.1", "GOOD"],
"License :: OSI Approved :: European Union Public Licence 1.2 (EUPL 1.2)": ["EUPL 1.2", "GOOD"],
"License :: OSI Approved :: GNU Affero General Public License v3": ["AGPLv3", "GOOD"],
"License :: OSI Approved :: GNU Affero General Public License v3 or later (AGPLv3+)": ["AGPLv3+", "GOOD"],
"License :: OSI Approved :: GNU Free Documentation License (FDL)": ["GFDL", "GOOD"],
"License :: OSI Approved :: GNU General Public License (GPL)": ["GPL+", "GOOD"],
"License :: OSI Approved :: GNU General Public License v2 (GPLv2)": ["GPLv2", "GOOD"],
"License :: OSI Approved :: GNU General Public License v2 or later (GPLv2+)": ["GPLv2+", "GOOD"],
"License :: OSI Approved :: GNU General Public License v3 (GPLv3)": ["GPLv3", "GOOD"],
"License :: OSI Approved :: GNU General Public License v3 or later (GPLv3+)": ["GPLv3+", "GOOD"],
"License :: OSI Approved :: GNU Lesser General Public License v2 (LGPLv2)": ["LGPLv2", "GOOD"],
"License :: OSI Approved :: GNU Lesser General Public License v2 or later (LGPLv2+)": ["LGPLv2+", "GOOD"],
"License :: OSI Approved :: GNU Lesser General Public License v3 (LGPLv3)": ["LGPLv3", "GOOD"],
"License :: OSI Approved :: GNU Lesser General Public License v3 or later (LGPLv3+)": ["LGPLv3+", "GOOD"],
"License :: OSI Approved :: GNU Library or Lesser General Public License (LGPL)": ["LGPLv2+", "GOOD"],
"License :: OSI Approved :: Historical Permission Notice and Disclaimer (HPND)": ["MIT", "GOOD"],
"License :: OSI Approved :: IBM Public License": ["IBM", "GOOD"],
"License :: OSI Approved :: ISC License (ISCL)": ["ISC", "GOOD"],
"License :: OSI Approved :: Intel Open Source License": ["Intel Open Source License", "BAD"],
"License :: OSI Approved :: Jabber Open Source License": ["Jabber", "GOOD"],
"License :: OSI Approved :: MIT License": ["MIT", "GOOD"],
"License :: OSI Approved :: MIT No Attribution License (MIT-0)": ["MIT-0", "GOOD"],
"License :: OSI Approved :: MITRE Collaborative Virtual Workspace License (CVW)": ["MITRE Collaborative Virtual Workspace License (CVW)", "BAD"],
"License :: OSI Approved :: MirOS License (MirOS)": ["MirOS", "GOOD"],
"License :: OSI Approved :: Motosoto License": ["Motosoto", "GOOD"],
"License :: OSI Approved :: Mozilla Public License 1.0 (MPL)": ["MPLv1.0", "GOOD"],
"License :: OSI Approved :: Mozilla Public License 1.1 (MPL 1.1)": ["MPLv1.1", "GOOD"],
"License :: OSI Approved :: Mozilla Public License 2.0 (MPL 2.0)": ["MPLv2.0", "GOOD"],
"License :: OSI Approved :: Nethack General Public License": ["NGPL", "GOOD"],
"License :: OSI Approved :: Nokia Open Source License": ["Nokia", "GOOD"],
"License :: OSI Approved :: Open Group Test Suite License": ["Open Group Test Suite License", "BAD"],
"License :: OSI Approved :: Open Software License 3.0 (OSL-3.0)": ["OSL 3.0", "GOOD"],
"License :: OSI Approved :: PostgreSQL License": ["PostgreSQL", "GOOD"],
"License :: OSI Approved :: Python License (CNRI Python License)": ["CNRI", "GOOD"],
"License :: OSI Approved :: Python Software Foundation License": ["Python", "GOOD"],
"License :: OSI Approved :: Qt Public License (QPL)": ["QPL", "GOOD"],
"License :: OSI Approved :: Ricoh Source Code Public License": ["Ricoh Source Code Public License", "BAD"],
"License :: OSI Approved :: SIL Open Font License 1.1 (OFL-1.1)": ["OFL", "GOOD"],
"License :: OSI Approved :: Sleepycat License": ["Sleepycat", "GOOD"],
"License :: OSI Approved :: Sun Industry Standards Source License (SISSL)": ["SISSL", "GOOD"],
"License :: OSI Approved :: Sun Public License": ["SPL", "GOOD"],
"License :: OSI Approved :: The Unlicense (Unlicense)": ["Unlicense", "GOOD"],
"License :: OSI Approved :: Universal Permissive License (UPL)": ["UPL", "GOOD"],
"License :: OSI Approved :: University of Illinois/NCSA Open Source License": ["NCSA", "GOOD"],
"License :: OSI Approved :: Vovida Software License 1.0": ["VSL", "GOOD"],
"License :: OSI Approved :: W3C License": ["W3C", "GOOD"],
"License :: OSI Approved :: X.Net License": ["X.Net License", "BAD"],
"License :: OSI Approved :: Zope Public License": ["ZPL", "GOOD"],
"License :: OSI Approved :: zlib/libpng License": ["zlib", "GOOD"],
"License :: Other/Proprietary License": ["Proprietary License", "BAD"],
"License :: Public Domain": ["Public Domain", "GOOD"],
"License :: Repoze Public License": ["Repoze Public License", "UNKNOWN"]
"License :: Aladdin Free Public License (AFPL)": "Aladdin",
"License :: CC0 1.0 Universal (CC0 1.0) Public Domain Dedication": "CC0-1.0",
"License :: CeCILL-B Free Software License Agreement (CECILL-B)": "CECILL-B",
"License :: CeCILL-C Free Software License Agreement (CECILL-C)": "CECILL-C",
"License :: Eiffel Forum License (EFL)": null,
"License :: Free For Educational Use": null,
"License :: Free For Home Use": null,
"License :: Free To Use But Restricted": null,
"License :: Free for non-commercial use": null,
"License :: Freely Distributable": null,
"License :: Freeware": null,
"License :: GUST Font License 1.0": "LicenseRef-LPPL",
"License :: GUST Font License 2006-09-30": null,
"License :: Netscape Public License (NPL)": "NPL-1.0",
"License :: Nokia Open Source License (NOKOS)": "Nokia",
"License :: OSI Approved :: Academic Free License (AFL)": null,
"License :: OSI Approved :: Apache Software License": null,
"License :: OSI Approved :: Apple Public Source License": null,
"License :: OSI Approved :: Artistic License": null,
"License :: OSI Approved :: Attribution Assurance License": "AAL",
"License :: OSI Approved :: BSD License": null,
"License :: OSI Approved :: Boost Software License 1.0 (BSL-1.0)": "BSL-1.0",
"License :: OSI Approved :: CEA CNRS Inria Logiciel Libre License, version 2.1 (CeCILL-2.1)": "CECILL-2.1",
"License :: OSI Approved :: Common Development and Distribution License 1.0 (CDDL-1.0)": "CDDL-1.0",
"License :: OSI Approved :: Common Public License": "CPL-1.0",
"License :: OSI Approved :: Eclipse Public License 1.0 (EPL-1.0)": "EPL-1.0",
"License :: OSI Approved :: Eclipse Public License 2.0 (EPL-2.0)": "EPL-2.0",
"License :: OSI Approved :: Eiffel Forum License": null,
"License :: OSI Approved :: European Union Public Licence 1.0 (EUPL 1.0)": "EUPL-1.0",
"License :: OSI Approved :: European Union Public Licence 1.1 (EUPL 1.1)": "EUPL-1.1",
"License :: OSI Approved :: European Union Public Licence 1.2 (EUPL 1.2)": "EUPL-1.2",
"License :: OSI Approved :: GNU Affero General Public License v3": null,
"License :: OSI Approved :: GNU Affero General Public License v3 or later (AGPLv3+)": "AGPL-3.0-or-later",
"License :: OSI Approved :: GNU Free Documentation License (FDL)": null,
"License :: OSI Approved :: GNU General Public License (GPL)": null,
"License :: OSI Approved :: GNU General Public License v2 (GPLv2)": null,
"License :: OSI Approved :: GNU General Public License v2 or later (GPLv2+)": "GPL-2.0-or-later",
"License :: OSI Approved :: GNU General Public License v3 (GPLv3)": null,
"License :: OSI Approved :: GNU General Public License v3 or later (GPLv3+)": "GPL-3.0-or-later",
"License :: OSI Approved :: GNU Lesser General Public License v2 (LGPLv2)": null,
"License :: OSI Approved :: GNU Lesser General Public License v2 or later (LGPLv2+)": null,
"License :: OSI Approved :: GNU Lesser General Public License v3 (LGPLv3)": null,
"License :: OSI Approved :: GNU Lesser General Public License v3 or later (LGPLv3+)": "LGPL-3.0-or-later",
"License :: OSI Approved :: GNU Library or Lesser General Public License (LGPL)": null,
"License :: OSI Approved :: Historical Permission Notice and Disclaimer (HPND)": "HPND",
"License :: OSI Approved :: IBM Public License": "IPL-1.0",
"License :: OSI Approved :: ISC License (ISCL)": "ISC",
"License :: OSI Approved :: Intel Open Source License": "Intel",
"License :: OSI Approved :: Jabber Open Source License": "LicenseRef-Jabber",
"License :: OSI Approved :: MIT License": "MIT",
"License :: OSI Approved :: MIT No Attribution License (MIT-0)": "MIT-0",
"License :: OSI Approved :: MITRE Collaborative Virtual Workspace License (CVW)": null,
"License :: OSI Approved :: MirOS License (MirOS)": "MirOS",
"License :: OSI Approved :: Motosoto License": "Motosoto",
"License :: OSI Approved :: Mozilla Public License 1.0 (MPL)": "MPL-1.0",
"License :: OSI Approved :: Mozilla Public License 1.1 (MPL 1.1)": "MPL-1.1",
"License :: OSI Approved :: Mozilla Public License 2.0 (MPL 2.0)": "MPL-2.0",
"License :: OSI Approved :: Mulan Permissive Software License v2 (MulanPSL-2.0)": "MulanPSL-2.0",
"License :: OSI Approved :: Nethack General Public License": "NGPL",
"License :: OSI Approved :: Nokia Open Source License": "Nokia",
"License :: OSI Approved :: Open Group Test Suite License": "OGTSL",
"License :: OSI Approved :: Open Software License 3.0 (OSL-3.0)": "OSL-3.0",
"License :: OSI Approved :: PostgreSQL License": "PostgreSQL",
"License :: OSI Approved :: Python License (CNRI Python License)": "CNRI-Python",
"License :: OSI Approved :: Python Software Foundation License": "PSF-2.0",
"License :: OSI Approved :: Qt Public License (QPL)": "QPL-1.0",
"License :: OSI Approved :: Ricoh Source Code Public License": "RSCPL",
"License :: OSI Approved :: SIL Open Font License 1.1 (OFL-1.1)": "OFL-1.1",
"License :: OSI Approved :: Sleepycat License": "Sleepycat",
"License :: OSI Approved :: Sun Industry Standards Source License (SISSL)": "SISSL",
"License :: OSI Approved :: Sun Public License": "SPL-1.0",
"License :: OSI Approved :: The Unlicense (Unlicense)": "Unlicense",
"License :: OSI Approved :: Universal Permissive License (UPL)": "UPL-1.0",
"License :: OSI Approved :: University of Illinois/NCSA Open Source License": "NCSA",
"License :: OSI Approved :: Vovida Software License 1.0": "VSL-1.0",
"License :: OSI Approved :: W3C License": "W3C",
"License :: OSI Approved :: X.Net License": "Xnet",
"License :: OSI Approved :: Zope Public License": null,
"License :: OSI Approved :: zlib/libpng License": "Zlib",
"License :: Other/Proprietary License": null,
"License :: Public Domain": "LicenseRef-Fedora-Public-Domain",
"License :: Repoze Public License": null
}
+163
View File
@@ -0,0 +1,163 @@
from importlib.resources import open_text
import json
import requests
from license_expression import get_spdx_licensing, ExpressionError
TROVE2FEDORA_MAP = {}
FEDORA_LICENSES = {}
def _load_package_resource(filename):
with open_text("pyp2spec", filename) as f:
return json.load(f)
def _load_from_drive(source_path):
with open(source_path, "r", encoding="utf-8") as f:
return json.load(f)
def _load_from_url(url, session=None):
s = session or requests.Session()
response = s.get(url)
response.raise_for_status()
return response.json()
def classifiers_to_spdx_identifiers(classifiers):
"""Return the list of SPDX identifiers converted from the license classifiers.
If the conversion of any of the classifiers is not possible, return None.
Raise KeyError if the source data doesn't contain the found classifier.
"""
if not TROVE2FEDORA_MAP:
# Load the file with Python trove classifiers mapped to SPDX identifiers
TROVE2FEDORA_MAP.update(_load_package_resource("classifiers_to_fedora.json"))
spdx_identifiers = []
for classifier in classifiers:
try:
fedora_identifier = TROVE2FEDORA_MAP[classifier]
except KeyError as err:
err_string = f"{classifier}: Such classifier doesn't exist. " \
"If you believe that's pyp2spec's error, open an issue at the project's GitHub repo: " \
"https://github.com/befeleme/pyp2spec"
raise KeyError(err_string) from err
# Classifiers that don't map unambiguously to a single Fedora SPDX expression are nulls
# in the json
# If the conversion is not possible, return None - meaning that another method of determining the license will be tried
if fedora_identifier is None:
return None
spdx_identifiers.append(fedora_identifier)
return spdx_identifiers
def license_keyword_to_spdx_identifiers(license_keyword):
"""Return a sorted list of SPDX identifiers extracted from the license_keyword
If no identifiers were parsed out of the license_keyword, return None.
Raise ValueError if the license_keyword isn't a valid SPDX expression.
"""
# `Artistic-1.0-Perl` alone is forbidden in Fedora, but the combination is allowed
# These expressions may be a part of even longer license strings, which is impossible to cover
# In an unlikely event of such license in the upstream data, raise an explicit exception
special_cases = [
"GPL-1.0-or-later OR Artistic-1.0-Perl",
"GPL-2.0-or-later OR Artistic-1.0-Perl"
]
for case in special_cases:
if case in license_keyword:
err_str = f"The detected license contains `{case}` which is handled in a special way in Fedora. " \
"If you encountered this error, open an issue at https://github.com/befeleme/pyp2spec " \
"and include the details about the package that causes the issue."
raise NotImplementedError(err_str)
licensing = get_spdx_licensing()
try:
parsed_license = licensing.parse(license_keyword, validate=True)
if parsed_license is None: # license keyword is probably empty
return None
# The objects are stored in sets, sort and return as a list
return sorted(parsed_license.objects)
except ExpressionError as err:
raise ValueError("Invalid SPDX expression") from err
def _load_fedora_licenses(source_path=None, session=None):
"""Load the dictionary of licenses evaluated for Fedora by the Fedora Legal team.
Try to get them from the hard drive (installed by `fedora-license-data`).
Fall back to the resources published by Fedora Legal online when the file isn't found.
"""
fedora_licenses_path = source_path or "/usr/share/fedora-license-data/licenses/fedora-licenses.json"
try:
return _load_from_drive(fedora_licenses_path)
except FileNotFoundError:
url = "https://gitlab.com/fedora/legal/fedora-license-data/-/jobs/artifacts/main/raw/fedora-licenses.json?job=json"
return _load_from_url(url, session=session)
def _is_compliant_with_fedora(identifier, fedora_licenses):
"""Return True if the given identifier is "allowed" for Fedora and False if not.
Fedora allows different types of licenses: "allowed for content", "allowed fonts",
"allowed firmware", "allowed for documentation" and "allowed" for any use.
For our purposes, only the generally "allowed" licenses are considered good.
"""
# fedora_licenses doesn't have useful keys, hence iterate only through values
# {"0": {
# "license": {
# "expression": "Rdisc",
# "status": ["allowed"],
# "url": "https://fedoraproject.org/wiki/Licensing/Rdisc_License"
# },
# "fedora": {
# "legacy-name": ["Rdisc License"],
# "legacy-abbreviation": ["Rdisc"]
# },
# "approved": "yes",
# "fedora_abbrev": "Rdisc",
# "fedora_name": "Rdisc License",
# "spdx_abbrev": "Rdisc"}...}
for entry in fedora_licenses.values():
license_info = entry.get("license")
if license_info is not None:
if identifier == entry["license"]["expression"]:
return "allowed" in entry["license"]["status"]
# No such identifier was found, we assume it's not good for Fedora
return False
def good_for_fedora(spdx_identifiers, *, licenses_dict=None, session=None):
"""Determine whether all of the given SPDX identifiers are good for Fedora.
If no `spdx_identifiers` are given, return tuple: (False, []).
If all of the given identifiers are good, return (True, []),
otherwise return (False, [<all bad identifiers>]).
bad_identifiers are returned in the same order as the given spdx_identifiers.
"""
# populate FEDORA_LICENSES only if they're still empty and
# no other dictionary with licenses was given to be used here
if licenses_dict is None and not FEDORA_LICENSES:
FEDORA_LICENSES.update(_load_fedora_licenses(session=session))
fedora_licenses = licenses_dict or FEDORA_LICENSES
if not spdx_identifiers:
return (False, [])
bad_identifiers = []
for spdx_identifier in spdx_identifiers:
if not _is_compliant_with_fedora(spdx_identifier, fedora_licenses):
bad_identifiers.append(spdx_identifier)
if bad_identifiers:
return (False, bad_identifiers)
return (True, [])
+75 -68
View File
@@ -1,6 +1,5 @@
from datetime import date
from functools import wraps
from importlib.resources import open_text
from subprocess import check_output
import json
import re
@@ -11,14 +10,23 @@ import requests
import tomli_w
from pyp2spec.rpmversion import RpmVersion
from pyp2spec.license_processor import classifiers_to_spdx_identifiers
from pyp2spec.license_processor import license_keyword_to_spdx_identifiers, good_for_fedora
class NoLicenseDetectedError(ValueError):
"""Raised when there's no valid license detected"""
class PypiPackage:
"""Get and save package data from PyPI."""
"""Store and process the package data obtained from PyPI."""
def __init__(self, package, session=None):
self.package = package
self.package_data = self.get_package_metadata(session)
def __init__(self, package_name, *, package_metadata=None, session=None):
self.package_name = package_name
# package_metadata - custom dictionary with package metadata - used for testing
# in the typical app run it's not set,
# meaning we jump to the other sources package metadata data (eg. PyPI)
self.package_data = package_metadata or self.get_package_metadata(session=session)
self.sdist_filename = None
@property
@@ -34,13 +42,13 @@ class PypiPackage:
return re.sub(r"[-_.]+", "-", package_name).lower()
def get_package_metadata(self, session):
pkg_index = f"https://pypi.org/pypi/{self.package}/json"
def get_package_metadata(self, *,session=None):
pkg_index = f"https://pypi.org/pypi/{self.package_name}/json"
s = session or requests.Session()
try:
response = s.get(pkg_index).json()
except json.decoder.JSONDecodeError:
click.secho(f"'{self.package}' was not found on PyPI, did you spell it correctly?", fg="red")
click.secho(f"'{self.package_name}' was not found on PyPI, did you spell it correctly?", fg="red")
sys.exit(1)
else:
return response
@@ -92,71 +100,70 @@ class PypiPackage:
return "%{version}"
return version
def license(self, compliant=False):
def filter_license_classifiers(self):
"""Return the list of license classifiers defined for the package.
Filter out the parent categories `OSI-/DFSG Approved` which don't have any meaning.
"""
return [
c for c in self.package_data["info"]["classifiers"]
if (
c.startswith("License")
and c not in ("License :: OSI Approved", "License :: DFSG approved")
)
]
def transform_to_spdx(self):
"""Return SPDX identifiers and expression based on the found
package license metadata (classifiers or license keyword).
If multiple identifiers are found, create an expression that's the safest option (with AND as joining operator).
Raise ValueError if no valid SPDX identifiers are found.
"""
if (license_classifiers := self.filter_license_classifiers()):
identifiers = classifiers_to_spdx_identifiers(license_classifiers)
if identifiers:
expression = " AND ".join(identifiers)
return (identifiers, expression)
license_keyword = self.package_data["info"]["license"]
identifiers = license_keyword_to_spdx_identifiers(license_keyword)
# No more options to detect licenses left, hence explicit fail
if not identifiers:
raise NoLicenseDetectedError()
return (identifiers, license_keyword)
def license(self, *, check_compliance=False, session=None, licenses_dict=None):
"""Return the license string from package metadata.
First, check trove classifiers which are the current Python standard:
https://www.python.org/dev/peps/pep-0301/#distutils-trove-classification
If argument `compliant` is set to True, perform the approximate compliance
check of the parsed classifiers with Fedora Good Licenses.
If `check_compliance` is set to True, check each of the found
SPDX identifiers against the Fedora allowed licenses.
This isn't a 100% reliable solution and in case of ambiguous results,
the license is discarded as invalid.
Only if a package doesn't define the license in the classifiers, the "license"
keyword is checked. There isn't any check whether that value is compliant
with Fedora, it is passed to the config file for the user to validate.
Some packages fill in "license" with the whole license text.
This is indicated by the multiline value and such is discarded as invalid.
If the license can't be determined from both classifiers and "license"
keyword, this fact is logged and the script ended.
If the license can't be determined, this fact is printed to stdout and the script ends.
"""
click.secho("Attempting to get license from Classifiers", fg="cyan")
self.classifiers = self.read_license_classifiers()
# Process classifiers further if there are some
if self.classifiers:
return self.get_license_from_classifiers(compliant)
else:
click.secho("License in Classifiers not found, looking for the 'License' keyword", fg="cyan")
pkg_license = self.package_data["info"]["license"]
# Check if license isn't empty and is only one line
if pkg_license and len(pkg_license.split("\n")) == 1:
return pkg_license
else:
click.secho("Invalid license field value length, cannot reliably determine the license", fg="red")
click.secho("License not found. Specify --license explicitly. Quitting", fg="red")
sys.exit(1)
try:
identifiers, expression = self.transform_to_spdx()
except NoLicenseDetectedError:
click.secho("No valid license detected, Quitting", fg="red")
sys.exit(1)
except Exception as err:
click.secho(err, fg="red")
sys.exit(1)
if check_compliance:
is_compliant, bad_identifiers = good_for_fedora(identifiers, session=session, licenses_dict=licenses_dict)
if not is_compliant:
if bad_identifiers:
err_string = "The detected licenses: `{0}` aren't allowed in Fedora."
click.secho(err_string.format(", ".join(bad_identifiers), fg="red"))
click.secho("Could not create a compliant license field, Quitting", fg="red")
sys.exit(1)
def get_license_from_classifiers(self, compliant):
license_map = self.read_license_map()
licenses = []
for classifier in self.classifiers:
short_license, fedora_status = license_map[classifier]
# "OSI Approved" is a top-level category which doesn't bring
# any valuable information, let's just ignore it when encountered
if short_license == "OSI Approved":
continue
if compliant:
# "???" are APSL, Artistic, Eiffel - some versions are Fedora OK,
# some not. On PyPI there are <100 packages with them, rather than
# adding another layer of decision matrix, just skip them all.
if fedora_status in ["BAD", "UNKNOWN", "???"]:
click.secho(f"License '{short_license}' is or may not be allowed in Fedora, quitting", fg="red")
sys.exit(1)
else:
licenses.append(short_license)
else:
licenses.append(short_license)
return " and ".join(licenses)
def read_license_map(self):
with open_text("pyp2spec", "classifiers_to_fedora.json") as f:
license_map = json.load(f)
return license_map
def read_license_classifiers(self):
classifiers = self.package_data["info"]["classifiers"]
return [c for c in classifiers if c.startswith("License")]
return expression
def summary(self):
summary = self.package_data["info"]["summary"]
@@ -301,7 +308,7 @@ def create_config_contents(
# a package name was given as the `package`, look for it on PyPI
if is_package_name(package):
pkg = PypiPackage(package, session)
pkg = PypiPackage(package, session=session)
click.secho(f"Querying PyPI for package '{package}'", fg="cyan")
# a URL was given as the `package`
else:
@@ -326,7 +333,7 @@ def create_config_contents(
click.secho(f"Assuming PyPI --version={version}", fg="yellow")
if license is None:
license = pkg.license(compliant)
license = pkg.license(check_compliance=compliant, session=session)
click.secho(f"Assuming --license={license}", fg="yellow")
if release is None:
+1
View File
@@ -1,5 +1,6 @@
click
jinja2
license-expression
requests
tomli;python_version<"3.11"
tomli-w
+1
View File
@@ -25,6 +25,7 @@ python_requires = >=3.8
install_requires =
click
jinja2
license-expression
packaging
requests
tomli;python_version<"3.11"
+19
View File
@@ -0,0 +1,19 @@
import json
import betamax
import pytest
config = betamax.Betamax.configure()
config.cassette_library_dir = "tests/fixtures/cassettes"
# only replay recorded cassettes -
# error if an actual HTTP request would be necessary
# this is to prevent packaging issues in the offline environment (like rpm build)
# change to 'once' to enable recording new cassettes when writing new tests
config.default_cassette_options["record_mode"] = "none"
@pytest.fixture(scope="session")
def fake_fedora_licenses():
with open("tests/fedora_license_data.json", "r", encoding="utf-8") as f:
return json.load(f)
+1 -1
View File
@@ -5,7 +5,7 @@ Summary: Composable command line interface toolkit
# Check if the automatically generated License and its spelling is correct for Fedora
# https://docs.fedoraproject.org/en-US/packaging-guidelines/LicensingGuidelines/
License: BSD
License: BSD-3-Clause
URL: https://palletsprojects.com/p/click/
Source: %{pypi_source click}
+240
View File
@@ -0,0 +1,240 @@
{
"39": {
"license": {
"expression": "GPL-3.0-or-later WITH Classpath-exception-2.0",
"status": [
"allowed"
],
"url": "https://fedoraproject.org/wiki/Licensing/GPL_Classpath_Exception"
},
"fedora": {
"legacy-name": [
"GNU General Public License v3.0 or later, with Classpath exception"
],
"legacy-abbreviation": [
"GPLv3+ with exceptions"
]
},
"approved": "yes",
"fedora_abbrev": "GPLv3+ with exceptions",
"fedora_name": "GNU General Public License v3.0 or later, with Classpath exception",
"spdx_abbrev": "GPL-3.0-or-later WITH Classpath-exception-2.0"
},
"346": {
"approved": "yes",
"spdx_abbrev": "GPL-3.0-or-later WITH GCC-exception-3.1",
"license": {
"expression": "GPL-3.0-or-later WITH GCC-exception-3.1",
"status": [
"allowed"
]
},
"fedora": {}
},
"0": {
"license": {
"expression": "CC0-1.0",
"status": [
"allowed-content"
],
"url": "http://creativecommons.org/publicdomain/zero/1.0/legalcode",
"usage": "Existing uses of CC0-1.0 on code files in Fedora packages prior to\n2022-08-01, and subsequent upstream versions of those files in those\npackages, continue to be allowed. We encourage Fedora package\nmaintainers to ask upstreams to relicense such files.\n\nUpstream application of CC0-1.0 to trivial, noncreative, unoriginal,\nand nonexpressive material as part of an effort to achieve conformance\nto the REUSE specification (https://reuse.software/) (for example,\nCI/CD configuration files) is permitted regardless of whether such\nmaterial would normally be classified as \"content\"."
},
"fedora": {
"legacy-name": [
"Creative Commons Zero 1.0 Universal"
],
"legacy-abbreviation": [
"CC0"
]
},
"approved": "yes",
"fedora_abbrev": "CC0",
"fedora_name": "Creative Commons Zero 1.0 Universal",
"spdx_abbrev": "CC0-1.0"
},
"32": {
"license": {
"expression": "LicenseRef-LPPL",
"status": [
"allowed-fonts"
]
},
"fedora": {
"legacy-name": [
"GUST Font License"
],
"legacy-abbreviation": [
"LPPL"
]
},
"approved": "yes",
"fedora_abbrev": "LPPL",
"fedora_name": "GUST Font License",
"spdx_abbrev": "LicenseRef-LPPL"
},
"237": {
"approved": "no",
"spdx_abbrev": "EUPL-1.0",
"license": {
"expression": "EUPL-1.0",
"status": [
"not-allowed"
],
"url": "http://ec.europa.eu/idabc/eupl"
},
"fedora": {}
},
"339": {
"license": {
"expression": "AAL",
"status": [
"allowed"
],
"url": "http://opensource.org/licenses/attribution.php"
},
"fedora": {
"legacy-name": [
"Attribution Assurance License"
],
"legacy-abbreviation": [
"AAL"
]
},
"approved": "yes",
"fedora_abbrev": "AAL",
"fedora_name": "Attribution Assurance License",
"spdx_abbrev": "AAL"
},
"328": {
"license": {
"expression": "MIT",
"status": [
"allowed"
],
"url": "https://fedoraproject.org/wiki/Licensing:MIT?rd=Licensing/MIT#Modern_Style_with_sublicense"
},
"fedora": {
"legacy-name": [
"MIT:Modern Style with sublicense"
],
"legacy-abbreviation": [
"MIT"
]
},
"approved": "yes",
"fedora_abbrev": "MIT",
"fedora_name": "MIT:Modern Style with sublicense",
"spdx_abbrev": "MIT"
},
"121": {
"license": {
"expression": "CECILL-B",
"status": [
"allowed"
],
"url": "http://www.cecill.info/licences.en.html"
},
"fedora": {
"legacy-name": [
"CeCILL-B License"
],
"legacy-abbreviation": [
"CeCILL-B"
]
},
"approved": "yes",
"fedora_abbrev": "CeCILL-B",
"fedora_name": "CeCILL-B License",
"spdx_abbrev": "CECILL-B"
},
"142": {
"license": {
"expression": "CECILL-C",
"status": [
"allowed"
],
"url": "http://www.cecill.info/licences.en.html"
},
"fedora": {
"legacy-name": [
"CeCILL-C License"
],
"legacy-abbreviation": [
"CeCILL-C"
]
},
"approved": "yes",
"fedora_abbrev": "CeCILL-C",
"fedora_name": "CeCILL-C License",
"spdx_abbrev": "CECILL-C"
},
"40": {
"approved": "no",
"spdx_abbrev": "Aladdin",
"license": {
"expression": "Aladdin",
"status": [
"not-allowed"
],
"url": "http://www.artifex.com/downloads/doc/Public.htm"
},
"fedora": {}
},
"48": {
"approved": "no",
"spdx_abbrev": "LicenseRef-OpenFlow",
"license": {
"expression": "LicenseRef-OpenFlow",
"status": [
"not-allowed"
],
"text": "We are making <project or content> available for public use and benefit with the\nexpectation that others will use, modify and enhance the Software and\ncontribute those enhancements back to the community. However, since we\nwould like to make the Software available for broadest use, with as few\nrestrictions as possible permission is hereby granted, free of charge, to\nany person obtaining a copy of this Software to deal in the Software\nunder the copyrights without restriction, including without limitation\nthe rights to use, copy, modify, merge, publish, distribute, sublicense,\nand/or sell copies of the Software, and to permit persons to whom the\nSoftware is furnished to do so, subject to the following conditions:\n\nThe above copyright notice and this permission notice shall be included\nin all copies or substantial portions of the Software.\n\nTHE SOFTWARE IS PROVIDED \"AS IS\", WITHOUT WARRANTY OF ANY KIND, EXPRESS\nOR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF\nMERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT.\nIN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY\nCLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT,\nTORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE\nSOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.\n\nThe name and trademarks of copyright holder(s) may NOT be used in\nadvertising or publicity pertaining to the Software or any derivatives\nwithout specific, written prior permission."
},
"fedora": {}
},
"379": {
"approved": "no",
"spdx_abbrev": "APL-1.0",
"license": {
"expression": "APL-1.0",
"status": [
"not-allowed"
],
"url": "http://opensource.org/licenses/apl1.0.php"
},
"fedora": {}
},
"406": {
"license": {
"expression": "MIT-0",
"status": [
"allowed"
],
"url": "https://fedoraproject.org/wiki/Licensing/MIT-0"
},
"fedora": {
"legacy-name": [
"MIT No Attribution (MIT-0)"
],
"legacy-abbreviation": [
"MIT-0"
]
},
"approved": "yes",
"fedora_abbrev": "MIT-0",
"fedora_name": "MIT No Attribution (MIT-0)",
"spdx_abbrev": "MIT-0"
}
}
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
+1 -1
View File
@@ -3,7 +3,7 @@ changelog_head = "Wed Nov 03 2021 Packager <packager@maint.com>"
description = "This is package 'click' generated automatically by pyp2spec."
summary = "Composable command line interface toolkit"
version = "7.1.2"
license = "BSD"
license = "BSD-3-Clause"
release = "1"
pypi_name = "click"
pypi_version = "%{version}"
+96
View File
@@ -0,0 +1,96 @@
import pytest
from pyp2spec.license_processor import classifiers_to_spdx_identifiers, license_keyword_to_spdx_identifiers
from pyp2spec.license_processor import _is_compliant_with_fedora, good_for_fedora
@pytest.mark.parametrize(
("classifiers", "spdx_identifiers"),
(
(["License :: OSI Approved :: X.Net License"], ["Xnet"]), # good
(["License :: OSI Approved :: X.Net License", "License :: OSI Approved :: Artistic License"], None), # good & bad
(["License :: OSI Approved :: Artistic License"], None), # bad
(["License :: OSI Approved :: Eiffel Forum License", "License :: OSI Approved :: GNU General Public License (GPL)"], None), # bad & bad
(["License :: OSI Approved :: GNU Lesser General Public License v3 or later (LGPLv3+)", "License :: OSI Approved :: ISC License (ISCL)"], ["LGPL-3.0-or-later", "ISC"]), # good & good
),
)
def test_classifiers_to_spdx_identifiers(classifiers, spdx_identifiers):
assert classifiers_to_spdx_identifiers(classifiers) == spdx_identifiers
@pytest.mark.parametrize(
("classifiers"), (
["Non-existing-classifier"],
[""]
),
)
def test_conversion_to_spdx_fails_for_invalid_inputs(classifiers):
with pytest.raises(KeyError):
classifiers_to_spdx_identifiers(classifiers)
@pytest.mark.parametrize(
("expression", "expected"),
(
("LGPL-2.1-only AND MIT AND BSD-2-Clause", ['BSD-2-Clause', 'LGPL-2.1-only', 'MIT']),
("MIT AND (LGPL-2.1-or-later OR BSD-3-Clause)", ['BSD-3-Clause', 'LGPL-2.1-or-later', 'MIT']),
("(MIT AND (LGPL-2.1+ AND BSD-3-Clause))", ['BSD-3-Clause', 'LGPL-2.1-or-later', 'MIT']),
("GPL-3.0-or-later WITH GCC-exception-3.1", ["GPL-3.0-or-later WITH GCC-exception-3.1"]),
("GPL-3.0-or-later WITH GCC-exception-3.1 OR Apache-2.0 OR MPL-2.0", ["Apache-2.0", "GPL-3.0-or-later WITH GCC-exception-3.1", "MPL-2.0"]),
("", None),
),
)
def test_split_expressions_to_identifiers(expression, expected):
assert license_keyword_to_spdx_identifiers(expression) == expected
def test_perl_license_raises_exception():
with pytest.raises(NotImplementedError):
license_keyword_to_spdx_identifiers("MPL-2.0 OR GPL-2.0-or-later OR Artistic-1.0-Perl AND BSD-3-Clause")
@pytest.mark.parametrize(
("expression"),
(
("FOO AND bar OR baz WITH EXCEPTION"),
("an arbitrary string"),
("multiline\nstring"),
("BSD BSD AND BSD"),
("string"),
),
)
def test_invalid_expressions_raise_errors(expression):
with pytest.raises(ValueError):
license_keyword_to_spdx_identifiers(expression)
@pytest.mark.parametrize(
("identifier", "expected"),
(
("CECILL-C", True),
("GPL-3.0-or-later WITH Classpath-exception-2.0", True),
("GPL-3.0-or-later WITH GCC-exception-3.1", True),
("CC0-1.0", False), # allowed content license
("LicenseRef-LPPL", False), # allowed font license
("EUPL-1.0", False), # not allowed at all
("Bitstream-Vera", False), # existing SPDX identifiers, but missing in fake_fedora_licenses
)
)
def test_compliance_check_with_fedora(identifier, expected, fake_fedora_licenses):
assert _is_compliant_with_fedora(identifier, fake_fedora_licenses) is expected
@pytest.mark.parametrize(
("identifiers", "expected"),
(
(["AAL", "MIT", "CECILL-B"], (True, [])),
([], (False, [])),
(["LicenseRef-LPPL", "MIT"], (False, ["LicenseRef-LPPL"])), # mixed not allowed/allowed
(["LicenseRef-LPPL", "Aladdin", "LicenseRef-OpenFlow", "APL-1.0"], (False, ["LicenseRef-LPPL", "Aladdin", "LicenseRef-OpenFlow", "APL-1.0"])),
),
)
def test_is_allowed_in_fedora(identifiers, expected, fake_fedora_licenses):
"""Test the function logic, bear in mind that Fedora status may change in time"""
assert good_for_fedora(identifiers, licenses_dict=fake_fedora_licenses) == expected
+144 -101
View File
@@ -2,8 +2,6 @@
The data is downloaded from the PyPI and stored in betamax casettes
to prevent loading from the internet on each request.
"""
import betamax
import pytest
try:
@@ -12,15 +10,7 @@ except ImportError:
import tomli as tomllib
from pyp2spec.pyp2conf import PypiPackage, create_config_contents
with betamax.Betamax.configure() as config:
config.cassette_library_dir = "tests/fixtures/cassettes"
# only replay recorded cassettes -
# error if an actual HTTP request would be necessary
# this is to prevent further packaging issues
# change to 'once' to enable recording new cassettes when writing new tests
config.default_cassette_options['record_mode'] = 'none'
from pyp2spec.pyp2conf import convert_version_to_rpm_scheme
@pytest.fixture
@@ -53,6 +43,8 @@ def test_automatically_generated_config_is_valid(betamax_parametrized_session, c
def test_config_with_customization_is_valid(betamax_session):
"""Get the upstream metadata and modify some fields to get the custom config file.
This also tests the compliance with Fedora Legal data by making
a request to the remote resource.
"""
package = "aionotion"
config = create_config_contents(
@@ -66,6 +58,7 @@ def test_config_with_customization_is_valid(betamax_session):
summary="Python library for Notion Home Monitoring",
date="Fri Jun 04 2021",
top_level=True,
compliant=True,
session=betamax_session,
)
@@ -84,6 +77,7 @@ def test_archful_package(betamax_session, changelog):
name=changelog[1],
email=changelog[2],
top_level=True,
license="Apache 2.0",
archful=True,
session=betamax_session,
)
@@ -94,130 +88,179 @@ def test_archful_package(betamax_session, changelog):
assert config["archful"] == loaded_contents["archful"]
def test_license_classifier_read_correctly(betamax_session):
pkg = PypiPackage("tomli", session=betamax_session)
assert pkg.read_license_classifiers() == ["License :: OSI Approved :: MIT License"]
def test_license_classifier_read_correctly():
fake_pkg_data = {"info":{"classifiers": [
"Programming Language :: Python :: 3",
"Programming Language :: Python :: 3.8",
"Operating System :: POSIX :: Linux",
"License :: OSI Approved :: MIT License",
"License :: OSI Approved :: MIT No Attribution License (MIT-0)",
"Development Status :: 3 - Alpha",
]}}
pkg = PypiPackage("_", package_metadata=fake_pkg_data)
assert pkg.filter_license_classifiers() == [
"License :: OSI Approved :: MIT License",
"License :: OSI Approved :: MIT No Attribution License (MIT-0)"
]
def test_no_license_classifiers_and_no_license_keyword(betamax_session):
pkg = PypiPackage("tomli", session=betamax_session)
pkg.package_data["info"]["classifiers"] = []
assert pkg.read_license_classifiers() == []
def test_no_license_classifiers_and_no_license_keyword():
pkg = PypiPackage("_", package_metadata={"info":{"classifiers": []}})
assert pkg.filter_license_classifiers() == []
with pytest.raises(SystemExit):
pkg.license()
def test_compliant_license_is_returned(betamax_session):
pkg = PypiPackage("tomli", session=betamax_session)
pkg.classifiers = pkg.read_license_classifiers()
assert pkg.get_license_from_classifiers(compliant=True) == "MIT"
def test_bad_license_if_compliant_is_not_returned(betamax_session):
pkg = PypiPackage("tomli", session=betamax_session)
pkg.classifiers = ["License :: Eiffel Forum License (EFL)"]
with pytest.raises(SystemExit):
pkg.get_license_from_classifiers(compliant=True)
def test_bad_license_if_not_compliant_is_returned(betamax_session):
pkg = PypiPackage("tomli", session=betamax_session)
pkg.classifiers = ["License :: Eiffel Forum License (EFL)"]
assert pkg.get_license_from_classifiers(compliant=False) == "EFL"
def test_mix_non_compliant_licenses_wont_work_if_strict(betamax_session):
pkg = PypiPackage("tomli", session=betamax_session)
pkg.classifiers = [
"License :: Eiffel Forum License (EFL)",
def test_compliant_license_is_returned(fake_fedora_licenses):
fake_pkg_data = {"info": {"classifiers" : [
"Programming Language :: Python :: 3",
"Programming Language :: Python :: 3.8",
"Operating System :: POSIX :: Linux",
"License :: OSI Approved :: MIT License",
]
"License :: OSI Approved :: MIT No Attribution License (MIT-0)",
"Development Status :: 3 - Alpha",
]}}
pkg = PypiPackage("_", package_metadata=fake_pkg_data)
assert pkg.license(check_compliance=True, licenses_dict=fake_fedora_licenses) == "MIT AND MIT-0"
def test_bad_license_fails_compliance_check(fake_fedora_licenses):
fake_pkg_data = {"info": {"classifiers" : [
"License :: OSI Approved :: European Union Public Licence 1.0 (EUPL 1.0)"
]}}
pkg = PypiPackage("_", package_metadata=fake_pkg_data)
with pytest.raises(SystemExit):
pkg.get_license_from_classifiers(compliant=True)
pkg.license(check_compliance=True, licenses_dict=fake_fedora_licenses)
def test_mix_non_compliant_licenses_work_if_not_strict(betamax_session):
pkg = PypiPackage("tomli", session=betamax_session)
pkg.classifiers = [
"License :: Eiffel Forum License (EFL)",
def test_bad_license_without_compliance_check_is_returned():
fake_pkg_data = {"info": {"classifiers" : [
"License :: OSI Approved :: European Union Public Licence 1.0 (EUPL 1.0)"
]}}
pkg = PypiPackage("_", package_metadata=fake_pkg_data)
assert pkg.license(check_compliance=False) == "EUPL-1.0"
def test_mix_good_bad_licenses_fail_compliance_check(fake_fedora_licenses):
fake_pkg_data = {"info": {"classifiers" : [
"License :: OSI Approved :: European Union Public Licence 1.0 (EUPL 1.0)",
"License :: OSI Approved :: MIT License",
]
assert pkg.get_license_from_classifiers(compliant=False) == "EFL and MIT"
]}}
pkg = PypiPackage("_", package_metadata=fake_pkg_data)
with pytest.raises(SystemExit):
pkg.license(check_compliance=True, licenses_dict=fake_fedora_licenses)
def test_mix_good_bad_licenses_without_compliance_check_are_returned():
fake_pkg_data = {"info": {"classifiers" : [
"License :: OSI Approved :: European Union Public Licence 1.0 (EUPL 1.0)",
"License :: OSI Approved :: MIT License",
]}}
pkg = PypiPackage("_", package_metadata=fake_pkg_data)
assert pkg.license(check_compliance=False) == "EUPL-1.0 AND MIT"
def test_license_keyword_without_compliance_check():
fake_pkg_data = {"info": {"license": "BSD-2-Clause", "classifiers": []}}
pkg = PypiPackage("_", package_metadata=fake_pkg_data)
assert pkg.license(check_compliance=False) == "BSD-2-Clause"
def test_license_keyword_with_compliance_check(fake_fedora_licenses):
fake_pkg_data = {"info": {"license": "RSCPL", "classifiers": []}}
pkg = PypiPackage("_", package_metadata=fake_pkg_data)
with pytest.raises(SystemExit):
pkg.license(check_compliance=True, licenses_dict=fake_fedora_licenses)
@pytest.mark.parametrize("compliant", (True, False))
def test_OSI_Approved_is_ignored(betamax_session, compliant):
pkg = PypiPackage("tomli", session=betamax_session)
pkg.classifiers = [
"License :: OSI Approved :: MIT License",
"License :: OSI Approved",
]
assert pkg.get_license_from_classifiers(compliant) == "MIT"
def test_multiline_license_keyword_fails(compliant, fake_fedora_licenses):
fake_pkg_data = {"info": {"license": "This is absolutely\nhorrible\nidea\nto\ndo..."}}
pkg = PypiPackage("_", package_metadata=fake_pkg_data)
with pytest.raises(SystemExit):
pkg.license(check_compliance=compliant, licenses_dict=fake_fedora_licenses)
def test_zip_sdist_is_added_to_source_macro(betamax_session):
pkg = PypiPackage("azure-common", session=betamax_session)
@pytest.mark.parametrize("compliant", (True, False))
def test_empty_license_keyword_fails(compliant, fake_fedora_licenses):
fake_pkg_data = {"info": {"license": "", "classifiers": []}}
pkg = PypiPackage("_", package_metadata=fake_pkg_data)
with pytest.raises(SystemExit):
pkg.license(check_compliance=compliant, licenses_dict=fake_fedora_licenses)
def test_nonexisting_classifiers():
fake_pkg_data = {"info": {"classifiers": ["License :: OSI Approved :: XXXXXX"]}}
pkg = PypiPackage("_", package_metadata=fake_pkg_data)
with pytest.raises(SystemExit):
pkg.license()
def test_zip_sdist_is_added_to_source_macro():
fake_pkg_data = {
"releases": {
"1.2.3": [{
"filename": "Awesome_TestPkg-1.2.3.zip",
"packagetype": "sdist"}]
},
"info": {"version": "1.2.3"}
}
pkg = PypiPackage("_", package_metadata=fake_pkg_data)
version = pkg.version()
assert pkg.source(version) == "%{pypi_source azure-common %{version} zip}"
assert pkg.source(version) == "%{pypi_source Awesome_TestPkg %{version} zip}"
def test_no_homepage_in_metadata(betamax_session):
pkg = PypiPackage("pycountry", session=betamax_session)
assert pkg.project_url() == "https://pypi.org/project/pycountry/"
def test_no_homepage_in_metadata():
fake_pkg_data = {"info": {"package_url": "https://foo"}}
pkg = PypiPackage("_", package_metadata=fake_pkg_data)
assert pkg.project_url() == "https://foo"
def test_summary_is_generated_if_not_in_upstream(betamax_session):
pkg = PypiPackage("google-cloud-appengine-logging", session=betamax_session)
def test_summary_is_generated_if_not_in_upstream():
fake_pkg_data = {"info": {"summary": ""}}
pkg = PypiPackage("_", package_metadata=fake_pkg_data)
assert pkg.summary() == "..."
def test_summary_is_generated_if_upstream_data_is_multiline(betamax_session):
pkg = PypiPackage("wifi", session=betamax_session)
def test_summary_is_generated_if_upstream_data_is_multiline():
fake_pkg_data = {"info": {"summary": "I\nforgot\nthat summary\nmust\nbe short"}}
pkg = PypiPackage("_", package_metadata=fake_pkg_data)
assert pkg.summary() == "..."
def test_pypi_name_with_underscore_is_normalized(betamax_session):
pkg = PypiPackage("sphinx_design", session=betamax_session)
assert pkg.pypi_name == "sphinx-design"
assert pkg.python_name() == "python-sphinx-design"
def test_pypi_name_with_capital_letter_is_normalized(betamax_session):
pkg = PypiPackage("Pello", session=betamax_session)
assert pkg.pypi_name == "pello"
assert pkg.python_name() == "python-pello"
def test_capitalized_underscored_pypi_name_is_normalized():
fake_pkg_data = {"info": {"name": "Awesome_TestPkg"}}
pkg = PypiPackage("Awesome_TestPkg", package_metadata=fake_pkg_data)
assert pkg.pypi_name == "awesome-testpkg"
assert pkg.python_name() == "python-awesome-testpkg"
@pytest.mark.parametrize(
("package", "pypi_version", "rpm_version"), [
("markdown-it-py", "1.1.0", "1.1.0"),
("javascript", "1!0.2.13", "1:0.2.13"),
("python3-wget", "0.0.2-beta1", "0.0.2~b1"),
("django-apistar", "0.5.40-0", "0.5.40^post0"),
("pypi_version", "rpm_version"), [
("1.1.0", "1.1.0"),
("1!0.2.13", "1:0.2.13"),
("0.0.2-beta1", "0.0.2~b1"),
("0.5.40-0", "0.5.40^post0"),
]
)
def test_pypi_version_is_converted_to_rpm(betamax_parametrized_session, package, pypi_version, rpm_version):
config = create_config_contents(
package,
version=pypi_version,
session=betamax_parametrized_session,
)
assert config["version"] == rpm_version
def test_pypi_version_is_converted_to_rpm(pypi_version, rpm_version):
assert convert_version_to_rpm_scheme(pypi_version) == rpm_version
@pytest.mark.parametrize(
("package", "pypi_version", "pypi_version_macro"), [
("tomli", None, "%{version}"),
("markdown-it-py", "1.1.0", "%{version}"),
("javascript", "1!0.2.13", "1!0.2.13"),
("python3-wget", "0.0.2-beta1", "0.0.2-beta1"),
("django-apistar", "0.5.40-0", "0.5.40-0"),
("pypi_version", "pypi_version_macro"), [
("1.1.0", "%{version}"), # conversion to RPM doesn't change the string
("1!0.2.13", "1!0.2.13"), # Version string is normalized, see previous test
("0.0.2-beta1", "0.0.2-beta1"),
("0.5.40-0", "0.5.40-0"),
]
)
def test_pypi_version_is_converted_to_rpm(betamax_parametrized_session, package, pypi_version, pypi_version_macro):
config = create_config_contents(
package,
version=pypi_version,
session=betamax_parametrized_session,
)
assert config["pypi_version"] == pypi_version_macro
def test_pypi_version_or_macro(pypi_version, pypi_version_macro):
fake_pkg_data = {"info": {"version": pypi_version}}
pkg = PypiPackage("_", package_metadata=fake_pkg_data)
assert pkg.pypi_version_or_macro(pypi_version) == pypi_version_macro