From 41698b8e59a032818082339b345b531772020907 Mon Sep 17 00:00:00 2001 From: Nicola Coretti Date: Thu, 6 Jul 2023 10:53:43 +0200 Subject: [PATCH] Add license module --- exasol/toolbox/license.py | 86 ++++++++++++++++++++++++++++++++++++++ test/unit/license_test.py | 88 +++++++++++++++++++++++++++++++++++++++ 2 files changed, 174 insertions(+) create mode 100644 exasol/toolbox/license.py create mode 100644 test/unit/license_test.py diff --git a/exasol/toolbox/license.py b/exasol/toolbox/license.py new file mode 100644 index 000000000..321118db3 --- /dev/null +++ b/exasol/toolbox/license.py @@ -0,0 +1,86 @@ +from collections import defaultdict +from dataclasses import dataclass +from typing import ( + Dict, + Iterable, + List, + Tuple, +) + + +@dataclass(frozen=True) +class Package: + name: str + license: str + version: str + + +def _packages(package_info): + for p in package_info: + kwargs = {key.lower(): value for key, value in p.items()} + yield Package(**kwargs) + + +def _normalize(license): + def is_mulit_license(l): + return ";" in l + + def select_most_permissive(l): + licenses = [_normalize(l.strip()) for l in l.split(";")] + priority = defaultdict( + lambda: 9999, + {"Unlicense": 0, "BSD": 1, "MIT": 2, "MPLv2": 3, "LGPLv2": 4, "GPLv2": 5}, + ) + priority_to_license = defaultdict( + lambda: "Unknown", {v: k for k, v in priority.items()} + ) + selected = min(*[priority[lic] for lic in licenses]) + return priority_to_license[selected] + + mapping = { + "BSD License": "BSD", + "MIT License": "MIT", + "The Unlicense (Unlicense)": "Unlicense", + "Mozilla Public License 2.0 (MPL 2.0)": "MPLv2", + "GNU Lesser General Public License v2 (LGPLv2)": "LGPLv2", + "GNU General Public License v2 (GPLv2)": "GPLv2", + } + if is_mulit_license(license): + return select_most_permissive(license) + + if license not in mapping: + return license + + return mapping[license] + + +def audit( + licenses: List[Dict[str, str]], acceptable: List[str], exceptions: Dict[str, str] +) -> Tuple[List[Package], List[Package]]: + """ + Audit package licenses. + + Args: + licenses: a list of dictionaries containing license information for packages. + This information e.g. can be obtained by running `pip-licenses --format=json`. + + example: [{"License": "BSD License", "Name": "Babel", "Version": "2.12.1"}, ...] + + acceptable: A list of licenses which shall be accepted. + example: ["BSD License", "MIT License", ...] + + exceptions: A dictionary containing package names and justifications for packages to ignore/skip. + example: {'packagename': 'justification why this is/can be an exception'} + + Returns: + Two lists containing found violations and ignored packages. + """ + packages = list(_packages(licenses)) + acceptable = [_normalize(a) for a in acceptable] + ignored = [p for p in packages if p.name in exceptions and exceptions[p.name]] + violations = [ + p + for p in packages + if _normalize(p.license) not in acceptable and p not in ignored + ] + return violations, ignored diff --git a/test/unit/license_test.py b/test/unit/license_test.py new file mode 100644 index 000000000..c056de6b4 --- /dev/null +++ b/test/unit/license_test.py @@ -0,0 +1,88 @@ +import pytest + +from exasol.toolbox.license import audit + + +@pytest.fixture +def licenses(): + yield [ + {"License": "BSD License", "Name": "Babel", "Version": "2.12.1"}, + {"License": "MIT License", "Name": "PyYAML", "Version": "6.0"}, + { + "License": "Apache Software License", + "Name": "argcomplete", + "Version": "2.1.2", + }, + { + "License": "GNU Lesser General Public License v2 (LGPLv2)", + "Name": "astroid", + "Version": "2.15.5", + }, + { + "License": "Mozilla Public License 2.0 (MPL 2.0)", + "Name": "certifi", + "Version": "2023.5.7", + }, + { + "License": "Python Software Foundation License", + "Name": "distlib", + "Version": "0.3.6", + }, + { + "License": "BSD License; GNU General Public License (GPL); Public Domain; Python Software Foundation License", + "Name": "docutils", + "Version": "0.19", + }, + { + "License": "The Unlicense (Unlicense)", + "Name": "filelock", + "Version": "3.12.2", + }, + { + "License": "Mozilla Public License 2.0 (MPL 2.0)", + "Name": "pathspec", + "Version": "0.11.1", + }, + { + "License": "GNU General Public License (GPL); GNU General Public License v2 or later (GPLv2+); Other/Proprietary License", + "Name": "prysk", + "Version": "0.15.1", + }, + { + "License": "GNU General Public License v2 (GPLv2)", + "Name": "pylint", + "Version": "2.17.4", + }, + ] + + +def test_nothing_to_validate(): + licenses = [] + acceptable = [] + exceptions = [] + violations, exceptions = audit(licenses, acceptable, exceptions) + assert set(violations) == set() + assert set(exceptions) == set() + + +@pytest.mark.parametrize( + "acceptable,exceptions,expected_violations,expected_exceptions", + [ + ([], [], 11, 0), + (["BSD License", "MIT License"], {}, 8, 0), + ( + ["BSD License", "MIT License"], + {"prysk": "Prysk is only a development dependency"}, + 7, + 1, + ), + ], +) +def test_audit( + licenses, acceptable, exceptions, expected_violations, expected_exceptions +): + violations, exceptions = audit( + licenses, acceptable=acceptable, exceptions=exceptions + ) + assert len(violations) == expected_violations + assert len(exceptions) == expected_exceptions