From 7085bd642c524f5f28a73e37050d046d26ff4f35 Mon Sep 17 00:00:00 2001 From: Henrik Sandklef Date: Mon, 12 Jan 2026 09:16:29 +0100 Subject: [PATCH 01/41] first attempt to apply policy in report --- licomp_toolkit/license_policy.py | 130 +++++++++++++++++++++++++++++++ 1 file changed, 130 insertions(+) create mode 100644 licomp_toolkit/license_policy.py diff --git a/licomp_toolkit/license_policy.py b/licomp_toolkit/license_policy.py new file mode 100644 index 0000000..51ed0f5 --- /dev/null +++ b/licomp_toolkit/license_policy.py @@ -0,0 +1,130 @@ +# SPDX-FileCopyrightText: 2024 Henrik Sandklef +# +# SPDX-License-Identifier: GPL-3.0-or-later + + +import json + +from licomp_toolkit.toolkit import LicompToolkit +from licomp.interface import UseCase +from licomp.interface import Provisioning +from licomp.interface import Modification + + +class LicensePolicy: + + def __init__(self, policy_file): + data = self._read_json_file(policy_file) + self.policy_meta = data['meta'] + self.policy = data['policy'] + + def _read_json_data(self, data): + return json.load(data) + + def _read_json_file(self, file_name): + with open(file_name) as fp: + return self._read_json_data(fp) + + def allowed(self): + return self.policy['allowed'] + + def avoid(self): + return self.policy['avoid'] + + def denied(self): + return self.policy['denied'] + + def meta(self): + return self.policy_meta + + +class DefaultLicensePolicy(LicensePolicy): + + def __init__(self, resources, usecase, provisioning): + self.lt = LicompToolkit() + self.__licenses(resources, usecase, provisioning) + self.__order(resources, usecase, provisioning) + + def __order(self, resources, usecase, provisioning): + self.scores = {} + print("order...") + for out_license in self.licenses: + print(" order...") + for in_license in self.licenses: + print(" order...") + for resource in self.resources: + print(" order...") + compat = resource.outbound_inbound_compatibility(out_license, + in_license, + UseCase.string_to_usecase(usecase), + Provisioning.string_to_provisioning(provisioning), + Modification.UNMODIFIED) + print(str(compat['compatibility_status'])) + if compat['compatibility_status'] == 'yes': + if in_license not in self.scores: + self.scores[in_license] = 0 + self.scores[in_license] += 1 + + print("score: " + str(self.scores)) + + + def __licenses(self, resources, usecase, provisioning): + self.licenses = [] + self.resources = [] + print(str(self.lt.licomp_resources())) + for resource in resources: + for licomp_resource in self.lt.licomp_resources(): + print("lr:" + str(licomp_resource)) + if resource == licomp_resource: + print("Found: " + str(self.lt.licomp_resources()[licomp_resource])) + print("Found: " + str(self.lt.licomp_resources()[licomp_resource].supported_licenses())) + self.licenses += self.lt.licomp_resources()[licomp_resource].supported_licenses() + self.resources.append(self.lt.licomp_resources()[licomp_resource]) + +class LicensePolicyHandler: + + def __init__(self, policy_file): + print("PolicyHandler()") + self.policy = LicensePolicy(policy_file) + + def __apply_to_compat_object(self, compat_object, indent=0): + print("---------------------------") + print("apply type: " + str(compat_object['compatibility_check'])) + print("apply out: " + str(compat_object['outbound_license'])) + print("apply in: " + str(compat_object['inbound_license'])) + + if 'outbound-expression' in compat_object['compatibility_check']: + operator = compat_object['operator'] + print(operator) + #print("* out: "+ str(compat_object['outbound_license']) + " not supported right now") + for operand in compat_object['operands']: + print(" " * indent + "* operand: "+ str(operand['compatibility_object']['outbound_license'])) + self.__apply_to_compat_object(operand['compatibility_object'], indent=4) + elif 'outbound-license' in compat_object['compatibility_check']: + if 'inbound-expression' in compat_object['compatibility_check']: + inner_compat_object = compat_object['compatibility_object'] + #print("ELSE " + str(inner_compat_object)) + for operand in inner_compat_object['operands']: + self.__apply_to_compat_object(operand['compatibility_object'], indent=4) + if 'inbound-expression' in compat_object['compatibility_check']: + pass + else: + raise Exception("We should not be here") + return None + + def apply_policy(self, compat_report): + top_object = compat_report['compatibility_report'] + print("Top object:") + print(" * out: " + str(top_object['outbound_license'])) + print(" * in: " + str(top_object['inbound_license'])) + print(" * type: " + str(top_object['compatibility_check'])) + + + #compat_object = top_object['compatibility_object'] + self.__apply_to_compat_object(top_object) + #print("compt check: " + str(compat_object['compatibility_check'])) + + import sys + sys.exit(1) + return None + From c8ec28f8a726fae86c69b18f5e2cba54908e56c8 Mon Sep 17 00:00:00 2001 From: Henrik Sandklef Date: Mon, 12 Jan 2026 09:22:40 +0100 Subject: [PATCH 02/41] unit tests for policy --- tests/python/test_policy.py | 70 +++++++++++++++++++++++++++++++++++++ 1 file changed, 70 insertions(+) create mode 100644 tests/python/test_policy.py diff --git a/tests/python/test_policy.py b/tests/python/test_policy.py new file mode 100644 index 0000000..1f1a472 --- /dev/null +++ b/tests/python/test_policy.py @@ -0,0 +1,70 @@ +# SPDX-FileCopyrightText: 2025 Henrik Sandklef +# +# SPDX-License-Identifier: GPL-3.0-or-later + +import json +import pytest +import logging +import sys + +from licomp_toolkit.license_policy import LicensePolicy +from licomp_toolkit.license_policy import DefaultLicensePolicy +from licomp_toolkit.license_policy import LicensePolicyHandler +from licomp_toolkit.toolkit import ExpressionExpressionChecker +from licomp.interface import UseCase +from licomp.interface import Provisioning + +TEST_POLICY_FILE = 'tests/policy/license-policy.json' + +policy = LicensePolicy(TEST_POLICY_FILE) +policy_handler = LicensePolicyHandler(TEST_POLICY_FILE) +expr_checker = ExpressionExpressionChecker() + +def test_policy_allowed(): + assert "MIT" in policy.allowed() + assert "BSD-3-Clause" in policy.allowed() + + assert "BSD-4-Clause" not in policy.allowed() + assert "BSD-2-Clause-Patent" not in policy.allowed() + +def test_policy_avoid(): + assert "MIT" not in policy.avoid() + assert "BSD-3-Clause" not in policy.avoid() + + assert "BSD-4-Clause" in policy.avoid() + + assert "BSD-2-Clause-Patent" not in policy.avoid() + +def test_policy_denied(): + assert "MIT" not in policy.denied() + assert "BSD-3-Clause" not in policy.denied() + + assert "BSD-4-Clause" not in policy.denied() + + assert "BSD-2-Clause-Patent" in policy.denied() + +def test_meta(): + assert policy.meta() != None + +def _test_expr_expr_library_bin(outbound, inbound, policy_file=None): + compats = expr_checker.check_compatibility(outbound, + inbound, + 'library', + 'binary-distribution') + return compats + + +# BRING BACK - RENAME +def _test_expr_expr_1(): + compat_report = _test_expr_expr_library_bin('MIT OR 0BSD', 'MIT OR (BSD-3-Clause AND MIT)') + #compat_report = _test_expr_expr_library_bin('MIT', 'MIT') + #print("cr: " + str(compat_report)) + policy_report = policy_handler.apply(compat_report) + print("pr: " + str(policy_report)) + assert False + + +def test_default_policy(): + dpol = DefaultLicensePolicy(['licomp_reclicense'], 'library', 'binary-distribution') + + assert False From a46fc9b0709cb1f7b69f072a11ec34d2c905506b Mon Sep 17 00:00:00 2001 From: Henrik Sandklef Date: Mon, 12 Jan 2026 21:02:45 +0100 Subject: [PATCH 03/41] add more policy tests --- tests/python/test_policy.py | 77 ++++++++++++++++++++++++++++++++----- 1 file changed, 68 insertions(+), 9 deletions(-) diff --git a/tests/python/test_policy.py b/tests/python/test_policy.py index 1f1a472..731d8ef 100644 --- a/tests/python/test_policy.py +++ b/tests/python/test_policy.py @@ -1,4 +1,4 @@ -# SPDX-FileCopyrightText: 2025 Henrik Sandklef +# SPDX-FileCopyrightText: 2026 Henrik Sandklef # # SPDX-License-Identifier: GPL-3.0-or-later @@ -9,6 +9,7 @@ from licomp_toolkit.license_policy import LicensePolicy from licomp_toolkit.license_policy import DefaultLicensePolicy +from licomp_toolkit.license_policy import LicensePolicyException from licomp_toolkit.license_policy import LicensePolicyHandler from licomp_toolkit.toolkit import ExpressionExpressionChecker from licomp.interface import UseCase @@ -20,6 +21,8 @@ policy_handler = LicensePolicyHandler(TEST_POLICY_FILE) expr_checker = ExpressionExpressionChecker() +default_policy = DefaultLicensePolicy(['licomp_reclicense'], 'library', 'binary-distribution') + def test_policy_allowed(): assert "MIT" in policy.allowed() assert "BSD-3-Clause" in policy.allowed() @@ -27,13 +30,13 @@ def test_policy_allowed(): assert "BSD-4-Clause" not in policy.allowed() assert "BSD-2-Clause-Patent" not in policy.allowed() -def test_policy_avoid(): - assert "MIT" not in policy.avoid() - assert "BSD-3-Clause" not in policy.avoid() +def test_policy_avoided(): + assert "MIT" not in policy.avoided() + assert "BSD-3-Clause" not in policy.avoided() - assert "BSD-4-Clause" in policy.avoid() + assert "BSD-4-Clause" in policy.avoided() - assert "BSD-2-Clause-Patent" not in policy.avoid() + assert "BSD-2-Clause-Patent" not in policy.avoided() def test_policy_denied(): assert "MIT" not in policy.denied() @@ -54,6 +57,42 @@ def _test_expr_expr_library_bin(outbound, inbound, policy_file=None): return compats +def test_policy_preferences_allowed(): + assert policy.compare_preferences('MIT', 'BSD-3-Clause') < 0 + assert policy.compare_preferences('BSD-3-Clause', 'MIT') > 0 + assert policy.compare_preferences('BSD-3-Clause', 'BSD-3-Clause') == 0 + +def test_policy_preferences_allowed_avoided(): + assert policy.compare_preferences('MIT', 'BSD-4-Clause') < 0 + assert policy.compare_preferences('BSD-4-Clause', 'MIT') > 0 + assert policy.compare_preferences('BSD-4-Clause', 'BSD-4-Clause') == 0 + +def test_policy_preferences_allowed_denied(): + assert policy.compare_preferences('MIT', 'BSD-2-Clause-Patent') < 0 + assert policy.compare_preferences('BSD-2-Clause-Patent', 'MIT') > 0 + assert policy.compare_preferences('BSD-2-Clause-Patent', 'BSD-2-Clause-Patent') == None + +def test_policy_preferences_allowed_denied_ignore(): + assert policy.compare_preferences('MIT', 'BSD-2-Clause-Patent', ignore_missing=True) < 0 + assert policy.compare_preferences('BSD-2-Clause-Patent', 'BSD-2-Clause-Patent', ignore_missing=True) == None + +def test_policy_preferences_allowed_denied_names(): + assert policy.most_preferred('MIT', 'BSD-3-Clause') == 'MIT' + assert policy.most_preferred('MIT', 'BSD-2-Clause-Patent') == 'MIT' + assert policy.most_preferred('BSD-2-Clause-Patent', 'BSD-3-Clause') == 'BSD-3-Clause' + assert policy.most_preferred('BSD-2-Clause-Patent', 'BSD-2-Clause-Patent') == None + +def test_policy_preferences_allowed_denied_names_ignore(): + assert policy.most_preferred('BSD-2-Clause-Patent', 'BSD-3-Clause', ignore_missing=True) == 'BSD-3-Clause' + assert policy.most_preferred('BSD-2-Clause-Patent', 'BSD-2-Clause-Patent', ignore_missing=True) == None + +def test_policy_preferences_raises(): + with pytest.raises(LicensePolicyException) as e_info: + policy.most_preferred('MIT2', 'GPL-2.0-or-later2') + +def test_policy_preferences_raises_ignore(): + policy.most_preferred('MIT2', 'GPL-2.0-or-later2', ignore_missing=True) == None + # BRING BACK - RENAME def _test_expr_expr_1(): compat_report = _test_expr_expr_library_bin('MIT OR 0BSD', 'MIT OR (BSD-3-Clause AND MIT)') @@ -64,7 +103,27 @@ def _test_expr_expr_1(): assert False -def test_default_policy(): - dpol = DefaultLicensePolicy(['licomp_reclicense'], 'library', 'binary-distribution') +def test_default_policy_listed(): + assert "MIT" in default_policy.allowed() + assert "BSD-3-Clause" in default_policy.allowed() - assert False + assert "BSD-4-Clause" in default_policy.allowed() + assert "BSD-2-Clause-Patent" in default_policy.allowed() + + assert len(default_policy.avoided()) == 0 + assert len(default_policy.denied()) == 0 + +def test_default_policy_preference(): + assert default_policy.compare_preferences('MIT', 'GPL-2.0-or-later') < 0 + assert default_policy.compare_preferences('GPL-2.0-or-later', 'MIT') > 0 + assert default_policy.compare_preferences('MIT', 'MIT') == 0 + +def test_default_policy_preferences_allowed_denied_names(): + assert default_policy.most_preferred('MIT', 'BSD-3-Clause') == 'MIT' + assert default_policy.most_preferred('MIT', 'GPL-2.0-or-later') == 'MIT' + + +def test_default_policy_preferences_raises(): + with pytest.raises(LicensePolicyException) as e_info: + default_policy.most_preferred('MIT2', 'GPL-2.0-or-later2') + From e90786a56e693be661924ace8d0d8a30f2c99f28 Mon Sep 17 00:00:00 2001 From: Henrik Sandklef Date: Tue, 13 Jan 2026 22:21:44 +0100 Subject: [PATCH 04/41] new strategy to add policy calcuations to report --- licomp_toolkit/license_policy.py | 266 +++++++++++++++++++++++++++---- 1 file changed, 234 insertions(+), 32 deletions(-) diff --git a/licomp_toolkit/license_policy.py b/licomp_toolkit/license_policy.py index 51ed0f5..055e1ca 100644 --- a/licomp_toolkit/license_policy.py +++ b/licomp_toolkit/license_policy.py @@ -2,14 +2,23 @@ # # SPDX-License-Identifier: GPL-3.0-or-later +from collections import OrderedDict +from functools import cmp_to_key import json +import logging from licomp_toolkit.toolkit import LicompToolkit from licomp.interface import UseCase from licomp.interface import Provisioning from licomp.interface import Modification +class LicensePolicyException(Exception): + + def __init__(self, message): + self.message = message + super().__init__(self.message) + class LicensePolicy: @@ -28,8 +37,8 @@ def _read_json_file(self, file_name): def allowed(self): return self.policy['allowed'] - def avoid(self): - return self.policy['avoid'] + def avoided(self): + return self.policy['avoided'] def denied(self): return self.policy['denied'] @@ -37,76 +46,267 @@ def denied(self): def meta(self): return self.policy_meta + def list_presence(self, lic, ignore_missing=False): + if lic in self.allowed(): + return 1, self.allowed().index(lic) + if lic in self.avoided(): + return 2, self.avoided().index(lic) + if lic in self.denied(): + return 3, self.denied().index(lic) + + if ignore_missing: + return None, None + raise LicensePolicyException(f'License "{lic}" is not present in any of the policy\'s lists.') + + def list_nr_to_name(self, nr): + return { + 1: 'allowed', + 2: 'avoided', + 3: 'denied'}.get(nr, -1) + + def compare_preferences(self, lic1, lic2, ignore_missing=False): + """ + + returns + * negative if lic1 is more preferred than lic2 + * None if both licenses are denied + raises + * LicensePolicyException if at least one license is not listed (if ignore_missing, then both licenses need to be not listed to raise exception) + """ + lic1_list, lic1_index = self.list_presence(lic1, ignore_missing) + lic2_list, lic2_index = self.list_presence(lic2, ignore_missing) + if not (lic1_list and lic2_list): + if ignore_missing: + print("---------------------------------------------|||1 " + str(lic1)) + print("---------------------------------------------|||1 " + str(lic2)) + print("---------------------------------------------|||1 " + str(lic1_list) + " " + str(lic1_index)) + print("---------------------------------------------|||1 " + str(lic2_list) + " " + str(lic2_index)) + print("---------------------------------------------|||1 " + str(self.allowed())) + print("---------------------------------------------|||1 " + str(self.avoided())) + print("---------------------------------------------|||1 " + str(self.denied())) + return None + + raise LicensePolicyException(f'License "{lic}" is not present in any of the policy\'s lists.') + + if lic1_list == lic2_list: + # if both are denied, return None to indicate "error" + if lic1_list == 3: + return None + return lic1_index - lic2_index + if not lic1_list: + return 1 + if not lic2_list: + return -1 + return lic1_list - lic2_list + + def most_preferred(self, lic1, lic2, ignore_missing=False): + pref = self.compare_preferences(lic1, lic2, ignore_missing) + if pref == None: + return None + if pref < 0: + return lic1 + return lic2 + + def preferred_score_ignore_missing(self, lic1, lic2): + pref = self.compare_preferences(lic1, lic2, ignore_missing=True) + if pref == None: + return 10000 + if pref < 0: + return -1 + if lic1 == lic2: + return 0 + return 1 + + def preferred_score_inbounds(self, lic1, lic2): + print(f'preferred_score_inbounds({lic1}, {lic2})') + pref = self.compare_preferences(lic1['inbound_license'], lic2['inbound_license']) + if pref < 0: + return -1 + if lic1 == lic2: + return 0 + return 1 + + def least_preferred(self, lic1, lic2, ignore_missing=False): + most_preferred = self.most_preferred(lic1, lic2, ignore_missing) + if most_preferred == lic1: + return lic2 + return lic1 + class DefaultLicensePolicy(LicensePolicy): def __init__(self, resources, usecase, provisioning): self.lt = LicompToolkit() self.__licenses(resources, usecase, provisioning) - self.__order(resources, usecase, provisioning) + license_order = self.__order(resources, usecase, provisioning) + self.policy_meta = {} + self.policy = { + 'allowed': license_order, + 'avoided': [], + 'denied': [] + } + #print("Liverpool: " + str(self.policy)) def __order(self, resources, usecase, provisioning): - self.scores = {} - print("order...") + scores = {} + for lic in self.licenses: + scores[lic] = 0 for out_license in self.licenses: - print(" order...") for in_license in self.licenses: - print(" order...") for resource in self.resources: - print(" order...") compat = resource.outbound_inbound_compatibility(out_license, in_license, UseCase.string_to_usecase(usecase), Provisioning.string_to_provisioning(provisioning), Modification.UNMODIFIED) - print(str(compat['compatibility_status'])) if compat['compatibility_status'] == 'yes': - if in_license not in self.scores: - self.scores[in_license] = 0 - self.scores[in_license] += 1 - - print("score: " + str(self.scores)) - + scores[in_license] += 1 + scores_dict = OrderedDict(sorted(scores.items(), key=lambda x: x[1], reverse=True)) + return [x for (x,y) in scores_dict.items()] + def __licenses(self, resources, usecase, provisioning): - self.licenses = [] self.resources = [] - print(str(self.lt.licomp_resources())) + self.licenses = [] + print("resources: " + str(self.lt.licomp_resources())) + print("resources: " + str(resources)) for resource in resources: for licomp_resource in self.lt.licomp_resources(): - print("lr:" + str(licomp_resource)) + #print("lr:" + str(licomp_resource)) if resource == licomp_resource: - print("Found: " + str(self.lt.licomp_resources()[licomp_resource])) - print("Found: " + str(self.lt.licomp_resources()[licomp_resource].supported_licenses())) + #print("Found: " + str(self.lt.licomp_resources()[licomp_resource])) + #print("Found: " + str(self.lt.licomp_resources()[licomp_resource].supported_licenses())) self.licenses += self.lt.licomp_resources()[licomp_resource].supported_licenses() self.resources.append(self.lt.licomp_resources()[licomp_resource]) + print("licenses: " + str(self.licenses)) class LicensePolicyHandler: - def __init__(self, policy_file): + def __init__(self, policy_file=None, resources=None, usecase=None, provisioning=None): print("PolicyHandler()") - self.policy = LicensePolicy(policy_file) + if policy_file: + self.policy = LicensePolicy(policy_file) + else: + self.policy = DefaultLicensePolicy(resources, usecase, provisioning) + + def _usable_license(self, lic): + print() + print() + print("_usable_license " + str(lic)) + print(str(lic)) + print(str(lic.keys())) + license_name = lic['inbound_license'] + policy_ok = license_name in (self.policy.allowed() + self.policy.avoided()) + compat_ok = lic['compatibility'] == 'yes' + + ret = policy_ok and compat_ok + print(f'_usable_license({lic}) | policy_ok: {policy_ok}, compat_ok: {compat_ok} ===> {ret}') + + return ret + + def __scored_inbounds(self, inbounds, operator): + if operator == "OR": + print(f'preferred_inbounds({inbounds}, {operator})') + filtered_inbound_licenses = [x for x in inbounds if self._usable_license(x)] + print(f'filtered_preferred_inbounds: {filtered_inbound_licenses}') + sorted_inbounds = sorted(filtered_inbound_licenses, key=cmp_to_key(self.policy.preferred_score_inbounds)) + print(f'sorted_inbounds: {[x["inbound_license"] for x in sorted_inbounds]})') + + return sorted_inbounds + elif operator == "AND": + sorted_inbounds = sorted(inbounds, key=cmp_to_key(self.policy.preferred_score_inbounds)) + return sorted_inbounds + else: + raise Exception("TODO fix this") + + def OBSOLETE__summarize_inbounds(self, inbounds, operator): + inbound_licenses = [x['inbound_license'] for x in inbounds] + most_license = inbound_licenses[0] + least_license = inbound_licenses[0] + if operator == "OR": + for inbound_license in inbound_licenses: + most_license = self.policy.most_preferred(most_license, inbound_license, ignore_missing=True) + least_license = self.policy.least_preferred(least_license, inbound_license, ignore_missing=True) + elif operator == "AND": + for inbound_license in inbound_licenses: + most_license = self.policy.most_preferred(most_license, inbound_license, ignore_missing=True) + least_license = most_license + return { + 'most_preferred_license': most_license, + 'least_preferred_license': least_license, + } def __apply_to_compat_object(self, compat_object, indent=0): - print("---------------------------") - print("apply type: " + str(compat_object['compatibility_check'])) - print("apply out: " + str(compat_object['outbound_license'])) - print("apply in: " + str(compat_object['inbound_license'])) + print(" " * indent + "---------------------------") + print(" " * indent + "apply type: " + str(compat_object['compatibility_check'])) + print(" " * indent + "apply out: " + str(compat_object['outbound_license'])) + print(" " * indent + "apply in: " + str(compat_object['inbound_license'])) if 'outbound-expression' in compat_object['compatibility_check']: operator = compat_object['operator'] - print(operator) + print(" " * indent + "* operator: " + operator) #print("* out: "+ str(compat_object['outbound_license']) + " not supported right now") for operand in compat_object['operands']: print(" " * indent + "* operand: "+ str(operand['compatibility_object']['outbound_license'])) - self.__apply_to_compat_object(operand['compatibility_object'], indent=4) + self.__apply_to_compat_object(operand['compatibility_object'], indent+4) elif 'outbound-license' in compat_object['compatibility_check']: if 'inbound-expression' in compat_object['compatibility_check']: - inner_compat_object = compat_object['compatibility_object'] - #print("ELSE " + str(inner_compat_object)) + print("keys: " + str(compat_object.keys())) + print("il: " + str(compat_object['inbound_license'])) + print("ol: " + str(compat_object['outbound_license'])) + print("type: " + str(compat_object.get('compatibility_type', None))) + #print("co: " + str(compat_object)) + print("co: " + str(compat_object.get('compatibility_object', None))) + print("keys: " + str(compat_object.keys())) + print("il: " + str(compat_object['inbound_license'])) + print("ol: " + str(compat_object['outbound_license'])) + print("type: " + str(compat_object.get('compatibility_type', None))) + print("op: " + str(compat_object.get('operator', 'noop'))) + print("cc: " + str(compat_object.get('compatibility_check', "nocheck"))) + # inner_compat_object = compat_object['compatibility_object'] + inner_compat_object = compat_object + operator = inner_compat_object['operator'] + print(" " * indent + "* operator: " + operator) + # BASED ON OPERATOR ... sum up operands + inbounds = [] for operand in inner_compat_object['operands']: - self.__apply_to_compat_object(operand['compatibility_object'], indent=4) - if 'inbound-expression' in compat_object['compatibility_check']: + self.__apply_to_compat_object(operand['compatibility_object'], indent+4) + print(" " * indent + " * operand: "+ str(operand['compatibility_object']['inbound_license']) + " prefs: " + str(operand['compatibility_object']['policy_check'])) + inbounds.append(operand['compatibility_object']['policy_check']) + scored_inbounds = self.__scored_inbounds(inbounds, operator) + print("0 preferred: " + str(len(scored_inbounds))) + print("1 preferred: " + str(scored_inbounds)) + + inner_compat_object['policy_check'] = { + 'check_type': 'inbound', + 'inbound_license': inner_compat_object['inbound_license'], + 'outbound_license': inner_compat_object['outbound_license'], + 'inbound_license_type': 'license-expression', + 'outbound_license_type': 'license', + 'compatibility': compat_object['compatibility'], + 'inbound_licenses': scored_inbounds, + 'inbound_list': scored_inbounds[0]['inbound_list'], + 'inbound_list_index': scored_inbounds[0]['inbound_list_index'], + } + print("policy_check: HIGH: " + str(inner_compat_object['policy_check'])) + if 'inbound-license' in compat_object['compatibility_check']: + pref = self.policy.most_preferred(compat_object['outbound_license'], compat_object['inbound_license'], ignore_missing=True) + print(" " * indent + f'FIX ME: {compat_object["outbound_license"]} --> {compat_object["inbound_license"]} : {compat_object["compatibility"]} ---> {pref}') + + lic = compat_object["inbound_license"] + list_nr, index = self.policy.list_presence(lic) + list_name = self.policy.list_nr_to_name(list_nr) + compat_object['policy_check'] = { + 'check_type': 'inbound', + 'inbound_license': compat_object['inbound_license'], + 'outbound_license': compat_object['outbound_license'], + 'inbound_license_type': 'license', + 'outbound_license_type': 'license', + 'compatibility': compat_object['compatibility'], + 'inbound_list': list_name, + 'inbound_list_index': index, + } + #print(" " * indent + str(compat_object['policy_check'])) pass else: raise Exception("We should not be here") @@ -124,6 +324,8 @@ def apply_policy(self, compat_report): self.__apply_to_compat_object(top_object) #print("compt check: " + str(compat_object['compatibility_check'])) + print(json.dumps(top_object, indent=4)) + print("done....") import sys sys.exit(1) return None From 0182f89c2ed955fcf902b77e72f62e4bfb3a873a Mon Sep 17 00:00:00 2001 From: Henrik Sandklef Date: Sat, 17 Jan 2026 16:11:39 +0100 Subject: [PATCH 05/41] add command: apply-license-policy --- licomp_toolkit/__main__.py | 21 +++++++++++++++++++++ 1 file changed, 21 insertions(+) diff --git a/licomp_toolkit/__main__.py b/licomp_toolkit/__main__.py index d96bbb1..168070a 100755 --- a/licomp_toolkit/__main__.py +++ b/licomp_toolkit/__main__.py @@ -19,6 +19,7 @@ from licomp_toolkit.suggester import OutboundSuggester from licomp_toolkit.display_compatibility import DisplayCompatibility from licomp_toolkit.utils import resources_to_use +from licomp_toolkit.license_policy import LicensePolicyHandler from licomp.main_base import LicompParser from licomp.interface import UseCase @@ -43,6 +44,22 @@ def validate(self, args): LicompToolkitSchemaChecker().validate_file(args.file_name, deep=True) return None, ReturnCodes.LICOMP_OK.value, None + def apply_license_policy(self, args): + expr_checker = ExpressionExpressionChecker() + compatibilities = expr_checker.check_compatibility("MIT", + "GPL-2.0-only OR (ISC AND 0BSD)", + 'library', + 'binary-distribution') +# lph = LicensePolicyHandler('tests/policy/license-policy.json') + lph = LicensePolicyHandler(resources=['licomp_reclicense'], + usecase='library', + provisioning='binary-distribution') + report = lph.apply_policy(compatibilities) + ret_code = compatibility_status_to_returncode(compatibilities['compatibility']) + formatter = LicompToolkitFormatter.formatter(self.args.output_format) + formatted_report = formatter.format_policy_report(report) + return formatted_report, ret_code, False + def verify(self, args): formatter = LicompToolkitFormatter.formatter(self.args.output_format) try: @@ -226,6 +243,10 @@ def main(): parser_sr = subparsers.add_parser('versions', help='Output version of licomp-toolkit and all the licomp resources') parser_sr.set_defaults(which="versions", func=lct_parser.versions) + # Command: apply policy + parser_sr = subparsers.add_parser('apply-license-policy', help='') + parser_sr.set_defaults(which="apply_license_policy", func=lct_parser.apply_license_policy) + res, code, err, func = lct_parser.run_noexit() if _working_return_code(code): if res: From 5d9e79876dc3112deb785fd45f87dcde7c2f18e9 Mon Sep 17 00:00:00 2001 From: Henrik Sandklef Date: Sun, 18 Jan 2026 19:31:33 +0100 Subject: [PATCH 06/41] remove compatibility_type, add check_class, fix typo --- licomp_toolkit/data/reply_schema.json | 24 ++++++++++++------------ 1 file changed, 12 insertions(+), 12 deletions(-) diff --git a/licomp_toolkit/data/reply_schema.json b/licomp_toolkit/data/reply_schema.json index 916f275..57eed9e 100644 --- a/licomp_toolkit/data/reply_schema.json +++ b/licomp_toolkit/data/reply_schema.json @@ -78,13 +78,13 @@ "$ref": "#/$defs/compatibility", "description" : "The inbound license expression." }, - "compatibility_type": { - "enum": [ "license" ], - "description": "Describing what is being checked. Can be either expression or license. In this case the value is \"license\"." - }, "compatibility_check": { "$ref": "#/$defs/compatibility_check" }, + "check_class": { + "type": "string", + "description": "Names the class/function used to determine. For development purposes" + }, "inbound_license": { "$ref": "#/$defs/license", "description" : "The inbound license expression." @@ -123,7 +123,7 @@ ] } }, - "required": [ "compatibility", "compatibility_type", "compatibility_check", "inbound_license" , "outbound_license", "compatibility_object", "compatibility_details"], + "required": [ "compatibility", "compatibility_check", "inbound_license" , "outbound_license", "compatibility_object", "compatibility_details"], "additionalProperties" : false }, { @@ -132,13 +132,13 @@ "$ref": "#/$defs/compatibility", "description" : "The inbound license expression." }, - "compatibility_type": { - "enum": [ "expression" ], - "description": "Describing what is being checked. Can be either expression or license. In this case the value is \"expression\"." - }, "compatibility_check": { "$ref": "#/$defs/compatibility_check" }, + "check_class": { + "type": "string", + "description": "Names the class/function used to determine. For development purposes" + }, "inbound_license": { "$ref": "#/$defs/license", "description" : "The inbound license expression." @@ -166,7 +166,7 @@ "description": "The operands for the operator. The operands can be either a license or an operator." } }, - "required": [ "compatibility", "compatibility_type", "compatibility_check", "inbound_license" , "outbound_license" , "operator", "operands" ], + "required": [ "compatibility", "compatibility_check", "inbound_license" , "outbound_license" , "operator", "operands" ], "additionalProperties" : false } ] @@ -255,7 +255,7 @@ "pattern": "^[0-9].[0-9](.[0-9]){0,1}$", "description" : "The api version of the program providing the reply" }, - "resource_name1" : { + "resource_name" : { "type" : "string", "minLength": 1, "description" : "The name of the program providing the reply, e.g. licomp-osadl" @@ -281,7 +281,7 @@ "description" : "A URL pointing to the project page (or similar) for the program providing the resulting compatibility." } }, - "required" : [ "status", "status_details", "outbound", "inbound", "usecase", "provisioning", "modification", "compatibility_status", "explanation", "api_version", "resource_name1", "resource_version", "resource_disclaimer", "data_url", "resource_url"], + "required" : [ "status", "status_details", "outbound", "inbound", "usecase", "provisioning", "modification", "compatibility_status", "explanation", "api_version", "resource_name", "resource_version", "resource_disclaimer", "data_url", "resource_url"], "additionalProperties" : false } } From 673c75fd5c251437483f5a1238650becdac4685c Mon Sep 17 00:00:00 2001 From: Henrik Sandklef Date: Sun, 18 Jan 2026 19:33:01 +0100 Subject: [PATCH 07/41] add policy format --- licomp_toolkit/format.py | 93 +++++++++++++++++++++++++++++++--------- 1 file changed, 73 insertions(+), 20 deletions(-) diff --git a/licomp_toolkit/format.py b/licomp_toolkit/format.py index c25e1f1..3105483 100644 --- a/licomp_toolkit/format.py +++ b/licomp_toolkit/format.py @@ -73,6 +73,9 @@ def format_display_compatibilities(self, compats, settings={}): display_compats = self._pre_format_display_compatibilities(compats) return json.dumps(display_compats, indent=4) + def format_policy_report(self, report): + return json.dumps(report, indent=4) + class YamlLicompToolkitFormatter(LicompToolkitFormatter): def format_compatibilities(self, compat): @@ -139,50 +142,100 @@ def __statuses(self, statuses, indent=''): return output - def _format_compat(self, compat): + def _format_compat_pref(self, compat, pref_lic=None): PAREN_OPEN = '(' - PAREN_START = ')' - return f'{PAREN_OPEN}{compat}{PAREN_START}' + PAREN_CLOSE = ')' + if compat == 'yes': + compat_string = 'compatible' + else: + compat_string = 'incompatible' + + if pref_lic: + return f'{PAREN_OPEN}{compat_string}, {pref_lic}{PAREN_CLOSE}' + else: + return f'{PAREN_OPEN}{compat_string}{PAREN_CLOSE}' - def format_compatibilities_object(self, compat_object, indent=''): + def format_compatibilities_general(self, compat_object, indent='', policy_report=False, preferred_license=False): compatibility_check = compat_object["compatibility_check"] output = [] if compatibility_check == "outbound-license -> inbound-license": - if not compat_object["compatibility_object"]: - pass - else: - compat_object = compat_object["compatibility_object"] + compat_object = compat_object details = compat_object["compatibility_details"] summary = details["summary"] + preferred_info = 'no' + if policy_report and preferred_license: + preferred_info = 'yes' - output.append(f'{indent}{compat_object["outbound_license"]} -> {compat_object["inbound_license"]} {self._format_compat(compat_object["compatibility"])}') + output.append(f'{indent}{compat_object["outbound_license"]} -> {compat_object["inbound_license"]} {self._format_compat_pref(compat_object["compatibility"])}') output.append(f'{indent} compatibility: {compat_object["compatibility"]}') + output.append(f'{indent} preferred license: {preferred_info}') output.append(f'{indent} compatibility details:') output += self.__compatibility_statuses(summary['compatibility_statuses'], f'{indent} ') - if compatibility_check == "outbound-license -> inbound-expression": - operator = compat_object["compatibility_object"]["operator"] - output.append(f'{indent}{operator} {self._format_compat(compat_object["compatibility"])}') - for operand in compat_object["compatibility_object"]["operands"]: - res = self.format_compatibilities_object(operand['compatibility_object'], indent=f'{indent} ') - output.append(res) + if compatibility_check == "outbound-license -> inbound-expression": + operator = compat_object["operator"] + inner_output = [] + if policy_report: + if len(compat_object['policy_check']['inbound_licenses']) > 0: + preferred_license = compat_object['policy_check']['inbound_licenses'][0]['inbound_license'] + for operand in compat_object["operands"]: + preferred = False + if policy_report: + operand_license = operand['compatibility_object']['inbound_license'] + if operand_license == preferred_license: + preferred = True + + res = self.format_compatibilities_general(operand['compatibility_object'], indent=f'{indent} ', policy_report=policy_report, preferred_license=preferred) + inner_output.append(res) + if len(compat_object['policy_check']['inbound_list']) > 0: + if operator == 'OR': + pref_lic = compat_object['policy_check']['inbound_licenses'][0]['inbound_license'] + elif operator == 'AND': + pref_lic = ' AND '.join([x['inbound_license'] for x in compat_object['policy_check']['inbound_licenses']]) + else: + pref_lic = '' + output.append(f'{indent}{operator} {self._format_compat_pref(compat_object["compatibility"], pref_lic)}') + output += inner_output if compatibility_check == "outbound-expression -> inbound-license": operator = compat_object["operator"] - output.append(f'{indent}{operator} {self._format_compat(compat_object["compatibility"])}') + output.append(f'{indent}{operator} {self._format_compat_pref(compat_object["compatibility"])}') for operand in compat_object["operands"]: - res = self.format_compatibilities_object(operand['compatibility_object'], indent=f'{indent} ') + res = self.format_compatibilities_general(operand['compatibility_object'], indent=f'{indent} ', policy_report=policy_report) output.append(res) if compatibility_check == "outbound-expression -> inbound-expression": operator = compat_object["operator"] compat = compat_object["compatibility"] - output.append(f'{indent}{operator} {self._format_compat(compat)}') + output.append(f'{indent}{operator} {self._format_compat_pref(compat)}') for operand in compat_object['operands']: - res = self.format_compatibilities_object(operand['compatibility_object'], indent=f'{indent} ') + res = self.format_compatibilities_general(operand['compatibility_object'], indent=f'{indent} ', policy_report=policy_report) output.append(f'{res}') return "\n".join(output) + def format_compatibilities_object(self, compat_object): + return self.format_compatibilities_general(compat_object, indent='') + + def format_policy_report(self, report): + output = [] + preferred_inbound = '' + if report["compatibility_report"]["policy_check"]['inbound_license_type'] == 'license': + preferred_inbound = report["compatibility_report"]["policy_check"]["inbound_license"] + elif report["compatibility_report"]["policy_check"]['inbound_license_type'] == 'license-expression': + if len(report["compatibility_report"]["policy_check"]["inbound_licenses"]) > 0: + preferred_inbound = report["compatibility_report"]["policy_check"]["inbound_licenses"][0]['inbound_license'] + + output.append(f'outbound: {report["outbound"]}') + output.append(f'inbound: {report["inbound"]}') + output.append(f'resources: {", ".join(report["resources"])}') + output.append(f'provisioning: {report["provisioning"]}') + output.append(f'usecase: {report["usecase"]}') + output.append(f'compatibility: {report["compatibility"]}') + output.append(f'preferred inbound: {preferred_inbound}') + output.append('report:') + output.append(self.format_compatibilities_general(report["compatibility_report"], indent=' ', policy_report=True)) + return "\n".join(output) + def format_compatibilities(self, compat): output = [] output.append(f'outbound: {compat["outbound"]}') @@ -192,7 +245,7 @@ def format_compatibilities(self, compat): output.append(f'usecase: {compat["usecase"]}') output.append(f'compatibility: {compat["compatibility"]}') output.append('report:') - output.append(self.format_compatibilities_object(compat["compatibility_report"], ' ')) + output.append(self.format_compatibilities_general(compat["compatibility_report"], ' ')) return "\n".join(output) From 2b949e5637c2162999ccf198f427964a8a130fd6 Mon Sep 17 00:00:00 2001 From: Henrik Sandklef Date: Sun, 18 Jan 2026 19:46:06 +0100 Subject: [PATCH 08/41] only check license if needed, remove printoutd --- licomp_toolkit/license_policy.py | 114 ++++++++++--------------------- 1 file changed, 35 insertions(+), 79 deletions(-) diff --git a/licomp_toolkit/license_policy.py b/licomp_toolkit/license_policy.py index 055e1ca..9f38abf 100644 --- a/licomp_toolkit/license_policy.py +++ b/licomp_toolkit/license_policy.py @@ -75,15 +75,9 @@ def compare_preferences(self, lic1, lic2, ignore_missing=False): """ lic1_list, lic1_index = self.list_presence(lic1, ignore_missing) lic2_list, lic2_index = self.list_presence(lic2, ignore_missing) - if not (lic1_list and lic2_list): + if (not lic1_list) and (not lic2_list): if ignore_missing: - print("---------------------------------------------|||1 " + str(lic1)) - print("---------------------------------------------|||1 " + str(lic2)) - print("---------------------------------------------|||1 " + str(lic1_list) + " " + str(lic1_index)) - print("---------------------------------------------|||1 " + str(lic2_list) + " " + str(lic2_index)) - print("---------------------------------------------|||1 " + str(self.allowed())) - print("---------------------------------------------|||1 " + str(self.avoided())) - print("---------------------------------------------|||1 " + str(self.denied())) + logging.debug(f'compare_preferences({lic1}, {lic2}, {ignore_missing}): ignore since both None') return None raise LicensePolicyException(f'License "{lic}" is not present in any of the policy\'s lists.') @@ -119,7 +113,7 @@ def preferred_score_ignore_missing(self, lic1, lic2): return 1 def preferred_score_inbounds(self, lic1, lic2): - print(f'preferred_score_inbounds({lic1}, {lic2})') + logging.debug(f'preferred_score_inbounds({lic1}, {lic2})') pref = self.compare_preferences(lic1['inbound_license'], lic2['inbound_license']) if pref < 0: return -1 @@ -145,7 +139,6 @@ def __init__(self, resources, usecase, provisioning): 'avoided': [], 'denied': [] } - #print("Liverpool: " + str(self.policy)) def __order(self, resources, usecase, provisioning): scores = {} @@ -168,50 +161,50 @@ def __order(self, resources, usecase, provisioning): def __licenses(self, resources, usecase, provisioning): self.resources = [] self.licenses = [] - print("resources: " + str(self.lt.licomp_resources())) - print("resources: " + str(resources)) + + logging.debug(f'__licenses({resources}, {usecase}, {provisioning})') for resource in resources: for licomp_resource in self.lt.licomp_resources(): - #print("lr:" + str(licomp_resource)) if resource == licomp_resource: - #print("Found: " + str(self.lt.licomp_resources()[licomp_resource])) - #print("Found: " + str(self.lt.licomp_resources()[licomp_resource].supported_licenses())) self.licenses += self.lt.licomp_resources()[licomp_resource].supported_licenses() self.resources.append(self.lt.licomp_resources()[licomp_resource]) - print("licenses: " + str(self.licenses)) + logging.debug(f'__licenses({resources}, {usecase}, {provisioning}) ==> {self.licenses}') class LicensePolicyHandler: def __init__(self, policy_file=None, resources=None, usecase=None, provisioning=None): - print("PolicyHandler()") + logging.debug("LicensePolicyHandler()") if policy_file: self.policy = LicensePolicy(policy_file) else: self.policy = DefaultLicensePolicy(resources, usecase, provisioning) - def _usable_license(self, lic): - print() - print() - print("_usable_license " + str(lic)) - print(str(lic)) - print(str(lic.keys())) + def __is_license_expression(self, lic): + CONTAINS_AND = 'AND' in lic + CONTAINS_OR = 'OR' in lic + return CONTAINS_AND or CONTAINS_OR + + + def usable_license(self, lic): license_name = lic['inbound_license'] - policy_ok = license_name in (self.policy.allowed() + self.policy.avoided()) + if self.__is_license_expression(license_name): + # should have been checked already, so skip + policy_ok = True + else: + policy_ok = license_name in (self.policy.allowed() + self.policy.avoided()) compat_ok = lic['compatibility'] == 'yes' ret = policy_ok and compat_ok - print(f'_usable_license({lic}) | policy_ok: {policy_ok}, compat_ok: {compat_ok} ===> {ret}') + logging.debug(f'usable_license({lic}) | policy_ok: {policy_ok}, compat_ok: {compat_ok} ===> {ret}') return ret - def __scored_inbounds(self, inbounds, operator): + def scored_inbounds(self, inbounds, operator): if operator == "OR": - print(f'preferred_inbounds({inbounds}, {operator})') - filtered_inbound_licenses = [x for x in inbounds if self._usable_license(x)] - print(f'filtered_preferred_inbounds: {filtered_inbound_licenses}') + logging.debug(f'preferred_inbounds({inbounds}, {operator})') + filtered_inbound_licenses = [x for x in inbounds if self.usable_license(x)] sorted_inbounds = sorted(filtered_inbound_licenses, key=cmp_to_key(self.policy.preferred_score_inbounds)) - print(f'sorted_inbounds: {[x["inbound_license"] for x in sorted_inbounds]})') - + logging.debug(f'sorted_inbounds: {[x["inbound_license"] for x in sorted_inbounds]})') return sorted_inbounds elif operator == "AND": sorted_inbounds = sorted(inbounds, key=cmp_to_key(self.policy.preferred_score_inbounds)) @@ -237,46 +230,26 @@ def OBSOLETE__summarize_inbounds(self, inbounds, operator): } def __apply_to_compat_object(self, compat_object, indent=0): - print(" " * indent + "---------------------------") - print(" " * indent + "apply type: " + str(compat_object['compatibility_check'])) - print(" " * indent + "apply out: " + str(compat_object['outbound_license'])) - print(" " * indent + "apply in: " + str(compat_object['inbound_license'])) - if 'outbound-expression' in compat_object['compatibility_check']: operator = compat_object['operator'] - print(" " * indent + "* operator: " + operator) - #print("* out: "+ str(compat_object['outbound_license']) + " not supported right now") for operand in compat_object['operands']: - print(" " * indent + "* operand: "+ str(operand['compatibility_object']['outbound_license'])) self.__apply_to_compat_object(operand['compatibility_object'], indent+4) elif 'outbound-license' in compat_object['compatibility_check']: if 'inbound-expression' in compat_object['compatibility_check']: - print("keys: " + str(compat_object.keys())) - print("il: " + str(compat_object['inbound_license'])) - print("ol: " + str(compat_object['outbound_license'])) - print("type: " + str(compat_object.get('compatibility_type', None))) - #print("co: " + str(compat_object)) - print("co: " + str(compat_object.get('compatibility_object', None))) - print("keys: " + str(compat_object.keys())) - print("il: " + str(compat_object['inbound_license'])) - print("ol: " + str(compat_object['outbound_license'])) - print("type: " + str(compat_object.get('compatibility_type', None))) - print("op: " + str(compat_object.get('operator', 'noop'))) - print("cc: " + str(compat_object.get('compatibility_check', "nocheck"))) - # inner_compat_object = compat_object['compatibility_object'] inner_compat_object = compat_object operator = inner_compat_object['operator'] - print(" " * indent + "* operator: " + operator) # BASED ON OPERATOR ... sum up operands inbounds = [] for operand in inner_compat_object['operands']: self.__apply_to_compat_object(operand['compatibility_object'], indent+4) - print(" " * indent + " * operand: "+ str(operand['compatibility_object']['inbound_license']) + " prefs: " + str(operand['compatibility_object']['policy_check'])) inbounds.append(operand['compatibility_object']['policy_check']) - scored_inbounds = self.__scored_inbounds(inbounds, operator) - print("0 preferred: " + str(len(scored_inbounds))) - print("1 preferred: " + str(scored_inbounds)) - + scored_inbounds = self.scored_inbounds(inbounds, operator) + inbound_list = [] + inbound_list_index = -1 + if len(scored_inbounds) > 0: + inbound_list = scored_inbounds[0]['inbound_list'] + inbound_list_index = scored_inbounds[0]['inbound_list_index'] + inner_compat_object['policy_check'] = { 'check_type': 'inbound', 'inbound_license': inner_compat_object['inbound_license'], @@ -285,13 +258,11 @@ def __apply_to_compat_object(self, compat_object, indent=0): 'outbound_license_type': 'license', 'compatibility': compat_object['compatibility'], 'inbound_licenses': scored_inbounds, - 'inbound_list': scored_inbounds[0]['inbound_list'], - 'inbound_list_index': scored_inbounds[0]['inbound_list_index'], + 'inbound_list': inbound_list, + 'inbound_list_index': inbound_list_index, } - print("policy_check: HIGH: " + str(inner_compat_object['policy_check'])) if 'inbound-license' in compat_object['compatibility_check']: pref = self.policy.most_preferred(compat_object['outbound_license'], compat_object['inbound_license'], ignore_missing=True) - print(" " * indent + f'FIX ME: {compat_object["outbound_license"]} --> {compat_object["inbound_license"]} : {compat_object["compatibility"]} ---> {pref}') lic = compat_object["inbound_license"] list_nr, index = self.policy.list_presence(lic) @@ -306,27 +277,12 @@ def __apply_to_compat_object(self, compat_object, indent=0): 'inbound_list': list_name, 'inbound_list_index': index, } - #print(" " * indent + str(compat_object['policy_check'])) - pass else: raise Exception("We should not be here") return None def apply_policy(self, compat_report): top_object = compat_report['compatibility_report'] - print("Top object:") - print(" * out: " + str(top_object['outbound_license'])) - print(" * in: " + str(top_object['inbound_license'])) - print(" * type: " + str(top_object['compatibility_check'])) - - - #compat_object = top_object['compatibility_object'] + logging.debug("apply_policy") self.__apply_to_compat_object(top_object) - #print("compt check: " + str(compat_object['compatibility_check'])) - - print(json.dumps(top_object, indent=4)) - print("done....") - import sys - sys.exit(1) - return None - + return compat_report From e0538ec374f3b7a0dca76e8cd8c9624c04ce40fe Mon Sep 17 00:00:00 2001 From: Henrik Sandklef Date: Mon, 19 Jan 2026 23:06:39 +0100 Subject: [PATCH 09/41] remove unused defs --- licomp_toolkit/expr_parser.py | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) diff --git a/licomp_toolkit/expr_parser.py b/licomp_toolkit/expr_parser.py index a5750d8..b72d021 100644 --- a/licomp_toolkit/expr_parser.py +++ b/licomp_toolkit/expr_parser.py @@ -12,8 +12,6 @@ OR = "OR" COMPATIBILITY_TYPE = 'compatibility_type' -COMPATIBILITY_OUTBOUND_LICENSE = 'outbound_license' -COMPATIBILITY_INBOUND_LICENSE = 'inbound_license' class LicenseExpressionParser(): @@ -127,7 +125,7 @@ def __parse_expression(self, expression): elif self.__is_close(expression): return "" - raise Exception("Bottom reached") + raise Exception("Bottom reached in __parse_expression (expr_parser.py)") def to_string(self, parsed_license): license_type = parsed_license['compatibility_type'] From d53ec345eff4a5b1e6aa94d5cacc0746d21b98fe Mon Sep 17 00:00:00 2001 From: Henrik Sandklef Date: Tue, 20 Jan 2026 10:01:12 +0100 Subject: [PATCH 10/41] add (c) and license for test policy --- .reuse/dep5 | 3 +++ 1 file changed, 3 insertions(+) diff --git a/.reuse/dep5 b/.reuse/dep5 index ec76bbb..6f74588 100644 --- a/.reuse/dep5 +++ b/.reuse/dep5 @@ -27,3 +27,6 @@ Files: README.md docs/*.md Copyright: Henrik Sandklef License: GPL-3.0-or-later +Files: tests/policy/license-policy.json +Copyright: Henrik Sandklef +License: GPL-3.0-or-later From 7cfb4acf9addb2814fee07e5ca77d21908852345 Mon Sep 17 00:00:00 2001 From: Henrik Sandklef Date: Tue, 20 Jan 2026 18:24:09 +0100 Subject: [PATCH 11/41] format text for outbound-license works --- licomp_toolkit/format.py | 51 ++++++++++++++++++++++++++-------------- 1 file changed, 33 insertions(+), 18 deletions(-) diff --git a/licomp_toolkit/format.py b/licomp_toolkit/format.py index 3105483..3c0d189 100644 --- a/licomp_toolkit/format.py +++ b/licomp_toolkit/format.py @@ -155,7 +155,7 @@ def _format_compat_pref(self, compat, pref_lic=None): else: return f'{PAREN_OPEN}{compat_string}{PAREN_CLOSE}' - def format_compatibilities_general(self, compat_object, indent='', policy_report=False, preferred_license=False): + def format_compatibilities_general(self, compat_object, indent='', policy_report=False, preferred_license=False, least_preferred_license=False): compatibility_check = compat_object["compatibility_check"] output = [] @@ -164,14 +164,18 @@ def format_compatibilities_general(self, compat_object, indent='', policy_report details = compat_object["compatibility_details"] summary = details["summary"] preferred_info = 'no' + least_preferred_info = 'no' if policy_report and preferred_license: preferred_info = 'yes' - + if policy_report and least_preferred_license: + least_preferred_info = 'yes' output.append(f'{indent}{compat_object["outbound_license"]} -> {compat_object["inbound_license"]} {self._format_compat_pref(compat_object["compatibility"])}') - output.append(f'{indent} compatibility: {compat_object["compatibility"]}') - output.append(f'{indent} preferred license: {preferred_info}') - output.append(f'{indent} compatibility details:') - output += self.__compatibility_statuses(summary['compatibility_statuses'], f'{indent} ') + if policy_report: + output.append(f'{indent} preferred license: {preferred_info}') + output.append(f'{indent} least preferred license: {least_preferred_info}') + output.append(f'{indent} compatibility: {compat_object["compatibility"]}') + output.append(f'{indent} compatibility details: ') + output += self.__compatibility_statuses(summary['compatibility_statuses'], f'{indent} ') if compatibility_check == "outbound-license -> inbound-expression": operator = compat_object["operator"] @@ -179,24 +183,29 @@ def format_compatibilities_general(self, compat_object, indent='', policy_report if policy_report: if len(compat_object['policy_check']['inbound_licenses']) > 0: preferred_license = compat_object['policy_check']['inbound_licenses'][0]['inbound_license'] + least_preferred_license = compat_object['policy_check']['inbound_licenses'][-1]['inbound_license'] for operand in compat_object["operands"]: preferred = False + least_preferred = False if policy_report: operand_license = operand['compatibility_object']['inbound_license'] if operand_license == preferred_license: preferred = True - - res = self.format_compatibilities_general(operand['compatibility_object'], indent=f'{indent} ', policy_report=policy_report, preferred_license=preferred) + if operand_license == least_preferred_license: + least_preferred = True + res = self.format_compatibilities_general(operand['compatibility_object'], indent=f'{indent} ', policy_report=policy_report, preferred_license=preferred, least_preferred_license=least_preferred) + #inner_output.append(f'{indent} allowed licenses: {", ".join([x["inbound_license"] for x in compat_object["policy_check"]["inbound_licenses"]])}') inner_output.append(res) - if len(compat_object['policy_check']['inbound_list']) > 0: - if operator == 'OR': - pref_lic = compat_object['policy_check']['inbound_licenses'][0]['inbound_license'] - elif operator == 'AND': - pref_lic = ' AND '.join([x['inbound_license'] for x in compat_object['policy_check']['inbound_licenses']]) - else: - pref_lic = '' - output.append(f'{indent}{operator} {self._format_compat_pref(compat_object["compatibility"], pref_lic)}') - output += inner_output + if policy_report: + if len(compat_object['policy_check']['inbound_list']) > 0: + if operator == 'OR': + pref_lic = compat_object['policy_check']['inbound_licenses'][0]['inbound_license'] + elif operator == 'AND': + pref_lic = ' AND '.join([x['inbound_license'] for x in compat_object['policy_check']['inbound_licenses']]) + else: + pref_lic = '' + output.append(f'{indent}{operator} {self._format_compat_pref(compat_object["compatibility"], pref_lic)}') + output += inner_output if compatibility_check == "outbound-expression -> inbound-license": operator = compat_object["operator"] output.append(f'{indent}{operator} {self._format_compat_pref(compat_object["compatibility"])}') @@ -219,6 +228,7 @@ def format_compatibilities_object(self, compat_object): def format_policy_report(self, report): output = [] preferred_inbound = '' + print("keys: " + str(report["compatibility_report"].keys())) if report["compatibility_report"]["policy_check"]['inbound_license_type'] == 'license': preferred_inbound = report["compatibility_report"]["policy_check"]["inbound_license"] elif report["compatibility_report"]["policy_check"]['inbound_license_type'] == 'license-expression': @@ -232,6 +242,11 @@ def format_policy_report(self, report): output.append(f'usecase: {report["usecase"]}') output.append(f'compatibility: {report["compatibility"]}') output.append(f'preferred inbound: {preferred_inbound}') + if report["meta"]["policy_type"] == 'default': + policy_string = 'default' + else: + policy_string = report["meta"]["policy_file"] + output.append(f'policy: {policy_string}') output.append('report:') output.append(self.format_compatibilities_general(report["compatibility_report"], indent=' ', policy_report=True)) return "\n".join(output) @@ -245,7 +260,7 @@ def format_compatibilities(self, compat): output.append(f'usecase: {compat["usecase"]}') output.append(f'compatibility: {compat["compatibility"]}') output.append('report:') - output.append(self.format_compatibilities_general(compat["compatibility_report"], ' ')) + output.append(self.format_compatibilities_general(compat["compatibility_report"], ' ', policy_report=False)) return "\n".join(output) From bbca60830ac0f59eeee3772d95aa93853fb349b0 Mon Sep 17 00:00:00 2001 From: Henrik Sandklef Date: Tue, 20 Jan 2026 18:24:40 +0100 Subject: [PATCH 12/41] add command and option for applying license policy --- licomp_toolkit/__main__.py | 71 ++++++++++++++++++++++++++++++-------- 1 file changed, 57 insertions(+), 14 deletions(-) diff --git a/licomp_toolkit/__main__.py b/licomp_toolkit/__main__.py index 168070a..057edd0 100755 --- a/licomp_toolkit/__main__.py +++ b/licomp_toolkit/__main__.py @@ -4,16 +4,22 @@ # # SPDX-License-Identifier: GPL-3.0-or-later +import json +from json.decoder import JSONDecodeError import logging import sys from licomp.interface import LicompException +from licomp_toolkit.exception import LicompToolkitException +from licomp_toolkit.return_codes import LicompToolkitReturnCodes from licomp_toolkit.toolkit import LicompToolkit from licomp_toolkit.toolkit import ExpressionExpressionChecker from licomp_toolkit.format import LicompToolkitFormatter from licomp_toolkit.config import cli_name from licomp_toolkit.config import description +from licomp_toolkit.config import module_name +from licomp_toolkit.config import licomp_toolkit_file_version from licomp_toolkit.config import epilog from licomp_toolkit.schema_checker import LicompToolkitSchemaChecker from licomp_toolkit.suggester import OutboundSuggester @@ -44,18 +50,35 @@ def validate(self, args): LicompToolkitSchemaChecker().validate_file(args.file_name, deep=True) return None, ReturnCodes.LICOMP_OK.value, None + def _read_report_file(self, report_file): + try: + with open(report_file) as fp: + report = json.load(fp) + meta = report['meta'] + meta_OK = meta['tool'] == module_name + meta_OK = meta['file_version'] == licomp_toolkit_file_version + file_OK = meta['file'] == 'verification' + if not (meta_OK and file_OK): + err_msg = f'File "{report_file}" not in Licomp Toolkit\'s license policy format.' + err_code = LicompToolkitReturnCodes.LICOMP_TOOLKIT_INVALID_FILE.value + return None, err_code, err_msg + return report, ReturnCodes.LICOMP_OK.value, None + except (FileNotFoundError, JSONDecodeError) as e: + err_msg = f'File "{args.report_file}" not found or not in JSON format' + err_code = LicompToolkitReturnCodes.LICOMP_TOOLKIT_INVALID_FILE.value + return None, err_code, err_msg + except (KeyError) as e: + err_msg = f'File "{report_file}" not in Licomp Toolkit\'s license policy format.' + err_code = LicompToolkitReturnCodes.LICOMP_TOOLKIT_INVALID_FILE.value + return None, err_code, err_msg + def apply_license_policy(self, args): - expr_checker = ExpressionExpressionChecker() - compatibilities = expr_checker.check_compatibility("MIT", - "GPL-2.0-only OR (ISC AND 0BSD)", - 'library', - 'binary-distribution') -# lph = LicensePolicyHandler('tests/policy/license-policy.json') - lph = LicensePolicyHandler(resources=['licomp_reclicense'], - usecase='library', - provisioning='binary-distribution') - report = lph.apply_policy(compatibilities) - ret_code = compatibility_status_to_returncode(compatibilities['compatibility']) + report, err_code, err_msg = self._read_report_file(args.report_file) + if err_code != ReturnCodes.LICOMP_OK.value: + return None, err_code, err_msg + lph = LicensePolicyHandler(policy_file=args.license_policy_file) + policy_report = lph.apply_policy(report, ignore_missing=True) + ret_code = compatibility_status_to_returncode(report['compatibility']) formatter = LicompToolkitFormatter.formatter(self.args.output_format) formatted_report = formatter.format_policy_report(report) return formatted_report, ret_code, False @@ -80,7 +103,20 @@ def verify(self, args): detailed_report=detailed_report) ret_code = compatibility_status_to_returncode(compatibilities['compatibility']) - return formatter.format_compatibilities(compatibilities), ret_code, False + if args.apply_license_policy: + if args.license_policy_file: + lph = LicensePolicyHandler(policy_file=args.license_policy_file, + resources=args.resources, + usecase=args.usecase, + provisioning=args.provisioning) + else: + lph = LicensePolicyHandler(resources=args.resources, + usecase=args.usecase, + provisioning=args.provisioning) + policy_report = lph.apply_policy(compatibilities) + return formatter.format_policy_report(policy_report), ret_code, False + else: + return formatter.format_compatibilities(compatibilities), ret_code, False except LicompException as e: return e, e.return_code.value, True except FlameException as e: @@ -171,7 +207,7 @@ def versions(self, args): def _working_return_code(return_code): - return return_code >= 0 and return_code < ReturnCodes.LICOMP_LAST_SUCCESSFUL_CODE.value + return return_code >= 0 and return_code < LicompToolkitReturnCodes.LICOMP_TOOLKIT_LAST_ERROR_CODE.value def main(): logging.debug("Licomp Toolkit") @@ -189,13 +225,18 @@ def main(): type=str, action='append', help='use specified licomp resource. For a list use the commands \'supported-resources\'. Use \'all\' to use all', - default=[]) + default=['licomp_reclicense', 'licomp_osadl']) parser.add_argument('-nv', '--no-verbose', action='store_true', help='keep compatibility report as short as possible', default=[]) + parser_v = subparsers.choices['verify'] + parser_v.add_argument("--apply-license-policy", action='store_true', help='Apply license policy', default=False) + parser_v.add_argument("--license-policy-file", type=str, help='License policy file. Defaults to use default license policy.', default=None) + + # Commands parser_si = subparsers.add_parser('simplify', help='Normalize and simplify a license expression') parser_si.set_defaults(which="simplify", func=lct_parser.simplify) @@ -246,6 +287,8 @@ def main(): # Command: apply policy parser_sr = subparsers.add_parser('apply-license-policy', help='') parser_sr.set_defaults(which="apply_license_policy", func=lct_parser.apply_license_policy) + parser_sr.add_argument('--license-policy-file', '-lpf', type=str, help='License policy file', default=None) + parser_sr.add_argument("report_file", type=str) res, code, err, func = lct_parser.run_noexit() if _working_return_code(code): From 76605d9d3635e891748a44aa445c83c262603ad9 Mon Sep 17 00:00:00 2001 From: Henrik Sandklef Date: Tue, 20 Jan 2026 18:24:55 +0100 Subject: [PATCH 13/41] format text for outbound-license works --- licomp_toolkit/license_policy.py | 33 ++++++++++++++++++++++++++------ 1 file changed, 27 insertions(+), 6 deletions(-) diff --git a/licomp_toolkit/license_policy.py b/licomp_toolkit/license_policy.py index 9f38abf..90599b2 100644 --- a/licomp_toolkit/license_policy.py +++ b/licomp_toolkit/license_policy.py @@ -75,6 +75,9 @@ def compare_preferences(self, lic1, lic2, ignore_missing=False): """ lic1_list, lic1_index = self.list_presence(lic1, ignore_missing) lic2_list, lic2_index = self.list_presence(lic2, ignore_missing) + logging.debug(f'compare_preferences({lic1}, {lic2}, {ignore_missing})') + logging.debug(f'compare_preferences: "{lic1_list}", "{lic1_index}" "{lic2_list}", "{lic2_index}"') + if (not lic1_list) and (not lic2_list): if ignore_missing: logging.debug(f'compare_preferences({lic1}, {lic2}, {ignore_missing}): ignore since both None') @@ -172,12 +175,16 @@ def __licenses(self, resources, usecase, provisioning): class LicensePolicyHandler: - def __init__(self, policy_file=None, resources=None, usecase=None, provisioning=None): + def __init__(self, policy_file=None): logging.debug("LicensePolicyHandler()") if policy_file: self.policy = LicensePolicy(policy_file) + self.policy_type = 'policy_file' + self.policy_file = policy_file else: - self.policy = DefaultLicensePolicy(resources, usecase, provisioning) + self.policy = None + self.policy_type = None + self.policy_file = None def __is_license_expression(self, lic): CONTAINS_AND = 'AND' in lic @@ -229,11 +236,12 @@ def OBSOLETE__summarize_inbounds(self, inbounds, operator): 'least_preferred_license': least_license, } - def __apply_to_compat_object(self, compat_object, indent=0): + def __apply_to_compat_object(self, compat_object, ignore_missing=False, indent=0): if 'outbound-expression' in compat_object['compatibility_check']: operator = compat_object['operator'] for operand in compat_object['operands']: self.__apply_to_compat_object(operand['compatibility_object'], indent+4) + compat_object['policy_check'] = 'apa' elif 'outbound-license' in compat_object['compatibility_check']: if 'inbound-expression' in compat_object['compatibility_check']: inner_compat_object = compat_object @@ -265,7 +273,7 @@ def __apply_to_compat_object(self, compat_object, indent=0): pref = self.policy.most_preferred(compat_object['outbound_license'], compat_object['inbound_license'], ignore_missing=True) lic = compat_object["inbound_license"] - list_nr, index = self.policy.list_presence(lic) + list_nr, index = self.policy.list_presence(lic, ignore_missing=ignore_missing) list_name = self.policy.list_nr_to_name(list_nr) compat_object['policy_check'] = { 'check_type': 'inbound', @@ -281,8 +289,21 @@ def __apply_to_compat_object(self, compat_object, indent=0): raise Exception("We should not be here") return None - def apply_policy(self, compat_report): + def apply_policy(self, compat_report, ignore_missing=False): + if not self.policy: + logging.debug(f'apply_policy, no policy. Creating default one') + resources = compat_report['resources'] + usecase = compat_report['usecase'] + provisioning = compat_report['provisioning'] + self.policy = DefaultLicensePolicy(resources, usecase, provisioning) + self.policy_type = 'default' + self.policy_file = None + top_object = compat_report['compatibility_report'] logging.debug("apply_policy") - self.__apply_to_compat_object(top_object) + self.__apply_to_compat_object(top_object, + ignore_missing=ignore_missing) + meta = compat_report['meta'] + meta['policy_type'] = self.policy_type + meta['policy_file'] = self.policy_file return compat_report From 9901e7f52ae881ab53cc69ec8b90da933823aab5 Mon Sep 17 00:00:00 2001 From: Henrik Sandklef Date: Wed, 21 Jan 2026 00:18:09 +0100 Subject: [PATCH 14/41] add support for applying license policy on outbound --- licomp_toolkit/license_policy.py | 95 +++++++++++++++++++++----------- 1 file changed, 62 insertions(+), 33 deletions(-) diff --git a/licomp_toolkit/license_policy.py b/licomp_toolkit/license_policy.py index 90599b2..3d3ec6a 100644 --- a/licomp_toolkit/license_policy.py +++ b/licomp_toolkit/license_policy.py @@ -115,14 +115,22 @@ def preferred_score_ignore_missing(self, lic1, lic2): return 0 return 1 - def preferred_score_inbounds(self, lic1, lic2): - logging.debug(f'preferred_score_inbounds({lic1}, {lic2})') - pref = self.compare_preferences(lic1['inbound_license'], lic2['inbound_license']) + def preferred_score_licenses(self, lic1, lic2, key): + logging.debug(f'preferred_score_licenses({lic1}, {lic2}, {key})') + pref = self.compare_preferences(lic1[key], lic2[key]) if pref < 0: return -1 if lic1 == lic2: return 0 return 1 + + def preferred_score_inbounds(self, lic1, lic2): + logging.debug(f'preferred_score_inbounds({lic1}, {lic2})') + return self.preferred_score_licenses(lic1, lic2, 'inbound_license') + + def preferred_score_outbounds(self, lic1, lic2): + logging.debug(f'preferred_score_inbounds({lic1}, {lic2})') + return self.preferred_score_licenses(lic1, lic2, 'inbound_license') def least_preferred(self, lic1, lic2, ignore_missing=False): most_preferred = self.most_preferred(lic1, lic2, ignore_missing) @@ -206,47 +214,67 @@ def usable_license(self, lic): return ret - def scored_inbounds(self, inbounds, operator): + def scored_general(self, licenses, operator, key): if operator == "OR": - logging.debug(f'preferred_inbounds({inbounds}, {operator})') - filtered_inbound_licenses = [x for x in inbounds if self.usable_license(x)] - sorted_inbounds = sorted(filtered_inbound_licenses, key=cmp_to_key(self.policy.preferred_score_inbounds)) - logging.debug(f'sorted_inbounds: {[x["inbound_license"] for x in sorted_inbounds]})') - return sorted_inbounds + logging.debug(f'preferred_licenses({licenses}, {operator})') + filtered_licenses = [x for x in licenses if self.usable_license(x)] + if key == 'inbound_license': + sorted_licenses = sorted(filtered_licenses, key=cmp_to_key(self.policy.preferred_score_inbounds)) + elif key == 'outbound_license': + sorted_licenses = sorted(filtered_licenses, key=cmp_to_key(self.policy.preferred_score_outbounds)) + logging.debug(f'sorted_licenses: {[x["inbound_license"] for x in sorted_licenses]})') + return sorted_licenses elif operator == "AND": - sorted_inbounds = sorted(inbounds, key=cmp_to_key(self.policy.preferred_score_inbounds)) - return sorted_inbounds + if key == 'inbound_license': + sorted_licenses = sorted(licenses, key=cmp_to_key(self.policy.preferred_score_inbounds)) + elif key == 'outbound_license': + sorted_licenses = sorted(licenses, key=cmp_to_key(self.policy.preferred_score_outbounds)) + return sorted_licenses else: raise Exception("TODO fix this") - def OBSOLETE__summarize_inbounds(self, inbounds, operator): - inbound_licenses = [x['inbound_license'] for x in inbounds] - most_license = inbound_licenses[0] - least_license = inbound_licenses[0] - if operator == "OR": - for inbound_license in inbound_licenses: - most_license = self.policy.most_preferred(most_license, inbound_license, ignore_missing=True) - least_license = self.policy.least_preferred(least_license, inbound_license, ignore_missing=True) - elif operator == "AND": - for inbound_license in inbound_licenses: - most_license = self.policy.most_preferred(most_license, inbound_license, ignore_missing=True) - least_license = most_license - return { - 'most_preferred_license': most_license, - 'least_preferred_license': least_license, - } - + def scored_inbounds(self, inbounds, operator): + return self.scored_general(inbounds, operator, 'inbound_license') + + def scored_outbounds(self, outbounds, operator): + return self.scored_general(outbounds, operator, 'outbound_license') + def __apply_to_compat_object(self, compat_object, ignore_missing=False, indent=0): if 'outbound-expression' in compat_object['compatibility_check']: operator = compat_object['operator'] + outbounds = [] for operand in compat_object['operands']: self.__apply_to_compat_object(operand['compatibility_object'], indent+4) - compat_object['policy_check'] = 'apa' + outbounds.append(operand['compatibility_object']['policy_check']) + + scored_outbounds = self.scored_outbounds(outbounds, operator) + outbound_list = [] + outbound_list_index = -1 + if len(scored_outbounds) > 0: + outbound_list = scored_outbounds[0]['outbound_list'] + outbound_list_index = scored_outbounds[0]['outbound_list_index'] + compat_object['policy_check'] = { + 'outbound_license': compat_object['outbound_license'], + 'outbound_license_type': 'license-expression', + 'outbound_licenses': scored_outbounds, + 'outbound_list': outbound_list, + 'outbound_list_index': outbound_list_index, + } elif 'outbound-license' in compat_object['compatibility_check']: + # Get outbound license (not expression) data + out_lic = compat_object["outbound_license"] + out_list_nr, out_index = self.policy.list_presence(out_lic, ignore_missing=ignore_missing) + out_list_name = self.policy.list_nr_to_name(out_list_nr) + compat_object['policy_check'] = { + 'outbound_license': compat_object['outbound_license'], + 'outbound_license_type': 'license', + 'outbound_list': out_list_name, + 'outbound_list_index': out_index, + } + if 'inbound-expression' in compat_object['compatibility_check']: inner_compat_object = compat_object operator = inner_compat_object['operator'] - # BASED ON OPERATOR ... sum up operands inbounds = [] for operand in inner_compat_object['operands']: self.__apply_to_compat_object(operand['compatibility_object'], indent+4) @@ -255,7 +283,7 @@ def __apply_to_compat_object(self, compat_object, ignore_missing=False, indent=0 inbound_list = [] inbound_list_index = -1 if len(scored_inbounds) > 0: - inbound_list = scored_inbounds[0]['inbound_list'] + inbound_list = scored_inbounds[0]['inbound_list'] inbound_list_index = scored_inbounds[0]['inbound_list_index'] inner_compat_object['policy_check'] = { @@ -275,7 +303,7 @@ def __apply_to_compat_object(self, compat_object, ignore_missing=False, indent=0 lic = compat_object["inbound_license"] list_nr, index = self.policy.list_presence(lic, ignore_missing=ignore_missing) list_name = self.policy.list_nr_to_name(list_nr) - compat_object['policy_check'] = { + compat_object['policy_check'].update({ 'check_type': 'inbound', 'inbound_license': compat_object['inbound_license'], 'outbound_license': compat_object['outbound_license'], @@ -284,7 +312,8 @@ def __apply_to_compat_object(self, compat_object, ignore_missing=False, indent=0 'compatibility': compat_object['compatibility'], 'inbound_list': list_name, 'inbound_list_index': index, - } + }) + else: raise Exception("We should not be here") return None From 2ba5c8975db4962f73a0aeae7499be0f7336d95b Mon Sep 17 00:00:00 2001 From: Henrik Sandklef Date: Thu, 22 Jan 2026 12:24:14 +0100 Subject: [PATCH 15/41] resources, usecase, provisioning handled in verify --- licomp_toolkit/__main__.py | 9 ++------- 1 file changed, 2 insertions(+), 7 deletions(-) diff --git a/licomp_toolkit/__main__.py b/licomp_toolkit/__main__.py index 057edd0..b4dd98a 100755 --- a/licomp_toolkit/__main__.py +++ b/licomp_toolkit/__main__.py @@ -105,14 +105,9 @@ def verify(self, args): ret_code = compatibility_status_to_returncode(compatibilities['compatibility']) if args.apply_license_policy: if args.license_policy_file: - lph = LicensePolicyHandler(policy_file=args.license_policy_file, - resources=args.resources, - usecase=args.usecase, - provisioning=args.provisioning) + lph = LicensePolicyHandler(policy_file=args.license_policy_file) else: - lph = LicensePolicyHandler(resources=args.resources, - usecase=args.usecase, - provisioning=args.provisioning) + lph = LicensePolicyHandler() policy_report = lph.apply_policy(compatibilities) return formatter.format_policy_report(policy_report), ret_code, False else: From 7709198f0e40a2109f36e170ea4a20b1f368e079 Mon Sep 17 00:00:00 2001 From: Henrik Sandklef Date: Thu, 22 Jan 2026 12:24:42 +0100 Subject: [PATCH 16/41] add simple exception class --- licomp_toolkit/exception.py | 11 +++++++++++ 1 file changed, 11 insertions(+) create mode 100644 licomp_toolkit/exception.py diff --git a/licomp_toolkit/exception.py b/licomp_toolkit/exception.py new file mode 100644 index 0000000..7eae0e1 --- /dev/null +++ b/licomp_toolkit/exception.py @@ -0,0 +1,11 @@ +# SPDX-FileCopyrightText: 2026 Henrik Sandklef +# +# SPDX-License-Identifier: GPL-3.0-or-later + +class LicompToolkitException(Exception): + + def __init__(self, message, error_code, orig_exception=None): + self.message = message + super().__init__(self.message) + self.error_code = error_code + self.original_exception = orig_exception From 1976c135cef0421de8a71c6007ddb3e10bb57493 Mon Sep 17 00:00:00 2001 From: Henrik Sandklef Date: Thu, 5 Feb 2026 18:53:53 +0100 Subject: [PATCH 17/41] expr-expr seems to work --- licomp_toolkit/config.py | 1 + licomp_toolkit/data/reply_schema.json | 17 + licomp_toolkit/format.py | 34 +- licomp_toolkit/license_policy.py | 509 +++++++++++++++++++++++--- licomp_toolkit/schema_checker.py | 3 +- licomp_toolkit/toolkit.py | 66 +++- 6 files changed, 550 insertions(+), 80 deletions(-) diff --git a/licomp_toolkit/config.py b/licomp_toolkit/config.py index 71ebc7a..ec73a9e 100644 --- a/licomp_toolkit/config.py +++ b/licomp_toolkit/config.py @@ -3,6 +3,7 @@ # SPDX-License-Identifier: GPL-3.0-or-later licomp_toolkit_version = '0.5.16' +licomp_toolkit_file_version = '0.5' my_supported_api_version = '0.5' cli_name = 'licomp-toolkit' diff --git a/licomp_toolkit/data/reply_schema.json b/licomp_toolkit/data/reply_schema.json index 57eed9e..34b98ad 100644 --- a/licomp_toolkit/data/reply_schema.json +++ b/licomp_toolkit/data/reply_schema.json @@ -4,6 +4,23 @@ "title" : "Licomp Toolkit Reply", "type" : "object", "properties" : { + "meta": { + "type" : "object", + "properties" : { + "status" : { + "type": "string", + "description" : "" + }, + "tool" : { + "type": "string", + "description" : "" + }, + "file_version" : { + "type": "string", + "description" : "" + } + } + }, "compatibility_report": { "type" : "object", "$ref": "#/$defs/compatibility_object" diff --git a/licomp_toolkit/format.py b/licomp_toolkit/format.py index 3c0d189..1988896 100644 --- a/licomp_toolkit/format.py +++ b/licomp_toolkit/format.py @@ -142,14 +142,14 @@ def __statuses(self, statuses, indent=''): return output + def _format_compat_value(self, compat): + return {'yes': 'compatible'}.get(compat, 'incompatible') + + def _format_compat_pref(self, compat, pref_lic=None): PAREN_OPEN = '(' PAREN_CLOSE = ')' - if compat == 'yes': - compat_string = 'compatible' - else: - compat_string = 'incompatible' - + compat_string = self._format_compat_value(compat) if pref_lic: return f'{PAREN_OPEN}{compat_string}, {pref_lic}{PAREN_CLOSE}' else: @@ -169,7 +169,9 @@ def format_compatibilities_general(self, compat_object, indent='', policy_report preferred_info = 'yes' if policy_report and least_preferred_license: least_preferred_info = 'yes' - output.append(f'{indent}{compat_object["outbound_license"]} -> {compat_object["inbound_license"]} {self._format_compat_pref(compat_object["compatibility"])}') + #output.append(f'{indent}{compat_object["outbound_license"]} -> {compat_object["inbound_license"]} {self._format_compat_pref(compat_object["compatibility"])}') + output.append(f'{indent}{compat_object["outbound_license"]} -> {compat_object["inbound_license"]}') + #output.append(f'{indent} check: {compatibility_check}') if policy_report: output.append(f'{indent} preferred license: {preferred_info}') output.append(f'{indent} least preferred license: {least_preferred_info}') @@ -193,7 +195,7 @@ def format_compatibilities_general(self, compat_object, indent='', policy_report preferred = True if operand_license == least_preferred_license: least_preferred = True - res = self.format_compatibilities_general(operand['compatibility_object'], indent=f'{indent} ', policy_report=policy_report, preferred_license=preferred, least_preferred_license=least_preferred) + res = self.format_compatibilities_general(operand['compatibility_object'], indent=f'{indent} ', policy_report=policy_report, preferred_license=preferred, least_preferred_license=least_preferred) #inner_output.append(f'{indent} allowed licenses: {", ".join([x["inbound_license"] for x in compat_object["policy_check"]["inbound_licenses"]])}') inner_output.append(res) if policy_report: @@ -205,7 +207,14 @@ def format_compatibilities_general(self, compat_object, indent='', policy_report else: pref_lic = '' output.append(f'{indent}{operator} {self._format_compat_pref(compat_object["compatibility"], pref_lic)}') + output.append(f'{indent} check: {compatibility_check}') + output.append(f'{indent} outbound: {compat_object["outbound_license"]}') + output.append(f'{indent} inbound: {compat_object["inbound_license"]}') + output.append(f'{indent} comaptibility: {compat_object["compatibility"]}') + output.append(f'{indent} preferred: {pref_lic}') + output.append(f'{indent} details:') output += inner_output + if compatibility_check == "outbound-expression -> inbound-license": operator = compat_object["operator"] output.append(f'{indent}{operator} {self._format_compat_pref(compat_object["compatibility"])}') @@ -216,6 +225,12 @@ def format_compatibilities_general(self, compat_object, indent='', policy_report operator = compat_object["operator"] compat = compat_object["compatibility"] output.append(f'{indent}{operator} {self._format_compat_pref(compat)}') + output.append(f'{indent} check: {compatibility_check}') + output.append(f'{indent} outbound: {compat_object["outbound_license"]}') + output.append(f'{indent} inbound: {compat_object["inbound_license"]}') + output.append(f'{indent} comaptibility: {compat_object["compatibility"]}') + output.append(f'{indent} preferred: TODO') + output.append(f'{indent} details:') for operand in compat_object['operands']: res = self.format_compatibilities_general(operand['compatibility_object'], indent=f'{indent} ', policy_report=policy_report) output.append(f'{res}') @@ -229,6 +244,11 @@ def format_policy_report(self, report): output = [] preferred_inbound = '' print("keys: " + str(report["compatibility_report"].keys())) + + import sys, json + print(json.dumps(report["compatibility_report"]["policy_check"], indent=4)) + print("done in format") + sys.exit(1) if report["compatibility_report"]["policy_check"]['inbound_license_type'] == 'license': preferred_inbound = report["compatibility_report"]["policy_check"]["inbound_license"] elif report["compatibility_report"]["policy_check"]['inbound_license_type'] == 'license-expression': diff --git a/licomp_toolkit/license_policy.py b/licomp_toolkit/license_policy.py index 3d3ec6a..642ed35 100644 --- a/licomp_toolkit/license_policy.py +++ b/licomp_toolkit/license_policy.py @@ -9,6 +9,7 @@ import logging from licomp_toolkit.toolkit import LicompToolkit +from licomp_toolkit.exception import LicompToolkitException from licomp.interface import UseCase from licomp.interface import Provisioning from licomp.interface import Modification @@ -47,6 +48,9 @@ def meta(self): return self.policy_meta def list_presence(self, lic, ignore_missing=False): + print(f'list_presence({lic}, {ignore_missing}') + print(f'list_presence({type(lic)}, {ignore_missing}') + if lic in self.allowed(): return 1, self.allowed().index(lic) if lic in self.avoided(): @@ -64,7 +68,19 @@ def list_nr_to_name(self, nr): 2: 'avoided', 3: 'denied'}.get(nr, -1) + def list_name_to_nr(self, nr): + return { + 'allowed': 1, + 'avoided': 2, + 'denied': 3}.get(nr, None) + def compare_preferences(self, lic1, lic2, ignore_missing=False): + return self.compare_preferences_general(lic1, lic2, ignore_missing=ignore_missing) + + def compare_preferences_expressions(self, lic1, lic2, key, ignore_missing=False): + return self.compare_preferences_general(lic1, lic2, key, ignore_missing=ignore_missing) + + def compare_preferences_general(self, lic1, lic2, key=None, ignore_missing=False): """ returns @@ -73,17 +89,65 @@ def compare_preferences(self, lic1, lic2, ignore_missing=False): raises * LicensePolicyException if at least one license is not listed (if ignore_missing, then both licenses need to be not listed to raise exception) """ - lic1_list, lic1_index = self.list_presence(lic1, ignore_missing) - lic2_list, lic2_index = self.list_presence(lic2, ignore_missing) - logging.debug(f'compare_preferences({lic1}, {lic2}, {ignore_missing})') + print(f'compare_preferences_general({lic1}, {lic2}, {key}, {ignore_missing})') + logging.debug(f'compare_preferences({lic1}, {lic2}, {key}, {ignore_missing})') + print("SCHNEEBLY1 1 " + str(lic1)) + print("SCHNEEBLY1 2 " + str(lic2)) + if key: + list_key = key.replace('_license', '') + print("SCHNEEBLY 1.0") + print("SCHNEEBLY KEY: " + key) + print("SCHNEEBLY KEY key: " + list_key) + print("SCHNEEBLY KEY lic: " + str(lic1)) + print("SCHNEEBLY KEY keys: " + str(lic1.keys())) + print("SCHNEEBLY TYPE : " + str(lic1[list_key]['type'])) + print("SCHNEEBLY KEY sub: " + str(lic1[list_key])) + print("SCHNEEBLY KEY sub: " + str(lic1[list_key].keys())) + if lic1[list_key]['type'] == 'license': + print("SCHNEEBLY 1.1") + lic1_list, lic1_index = self.list_presence(lic1[list_key]['license'], ignore_missing) + print("SCHNEEBLY 1.1 " + str(lic1_list)) + print("SCHNEEBLY 1.1 " + str(lic1_index)) + else: + print("SCHNEEBLY 1.2 type " + str(lic1[list_key]['type'])) + print("SCHNEEBLY 1.2 " + str(list_key)) + print("SCHNEEBLY 1.2 " + str(lic1[list_key])) + print("SCHNEEBLY 1.2 keys " + str(lic1[list_key].keys())) + print("SCHNEEBLY 1.2 pref " + str(lic1[list_key]['preferences'])) + print("SCHNEEBLY 1.2 lic1 " + str(lic1)) + + lic1_list = self.list_name_to_nr(lic1[list_key]['license']) + print("lic1: " + str(lic1)) + print("lic1: " + str(list_key)) + print("lic1: " + str(lic1[list_key])) + lic1_index = lic1[list_key]['preferences']['license_index'] + + if lic2[list_key]['type'] == 'license': + print("SCHNEEBLY 2.1") + lic2_list, lic2_index = self.list_presence(lic2[list_key]['license'], ignore_missing) + else: # license-expression + print("SCHNEEBLY 2.1 lic: " + str(lic2)) + print("SCHNEEBLY 2.1 lic: " + str(list_key)) + print("SCHNEEBLY 2.1 lic_key: " + str(lic2[list_key])) + print("SCHNEEBLY 2.1 pref: " + str(lic2[list_key]['preferences'])) + print("SCHNEEBLY 2.1 lic: " + str(lic2[list_key]['license'])) + + lic2_list = lic2[list_key]['preferences']['license_list'] + lic2_index = lic2[list_key]['preferences']['license_index'] + else: + print("SCHNEEBLY 2.2 .... should list_key be selected as above???? " + str(lic1)) + lic1_list, lic1_index = self.list_presence(lic1, ignore_missing) + lic2_list, lic2_index = self.list_presence(lic2, ignore_missing) + + print(f'compare_preferences: "{lic1_list}", "{lic1_index}" "{lic2_list}", "{lic2_index}"') logging.debug(f'compare_preferences: "{lic1_list}", "{lic1_index}" "{lic2_list}", "{lic2_index}"') if (not lic1_list) and (not lic2_list): if ignore_missing: - logging.debug(f'compare_preferences({lic1}, {lic2}, {ignore_missing}): ignore since both None') + logging.debug(f'compare_preferences({lic1[list_key]["license"]}, {lic2[list_key]["license"]}, {ignore_missing}): ignore since both None') return None - raise LicensePolicyException(f'License "{lic}" is not present in any of the policy\'s lists.') + raise LicensePolicyException(f'License "{lic1[list_key]["license"]}" and/or {lic2[list_key]["license"]} is not present in any of the policy\'s lists.') if lic1_list == lic2_list: # if both are denied, return None to indicate "error" @@ -105,7 +169,7 @@ def most_preferred(self, lic1, lic2, ignore_missing=False): return lic1 return lic2 - def preferred_score_ignore_missing(self, lic1, lic2): + def OBSOLETE_preferred_score_ignore_missing(self, lic1, lic2): pref = self.compare_preferences(lic1, lic2, ignore_missing=True) if pref == None: return 10000 @@ -117,7 +181,8 @@ def preferred_score_ignore_missing(self, lic1, lic2): def preferred_score_licenses(self, lic1, lic2, key): logging.debug(f'preferred_score_licenses({lic1}, {lic2}, {key})') - pref = self.compare_preferences(lic1[key], lic2[key]) + print(f'preferred_score_licenses({lic1}, {lic2}, {key})') + pref = self.compare_preferences_expressions(lic1, lic2, key) if pref < 0: return -1 if lic1 == lic2: @@ -126,11 +191,12 @@ def preferred_score_licenses(self, lic1, lic2, key): def preferred_score_inbounds(self, lic1, lic2): logging.debug(f'preferred_score_inbounds({lic1}, {lic2})') + print(f'preferred_score_inbounds({lic1}, {lic2})') return self.preferred_score_licenses(lic1, lic2, 'inbound_license') def preferred_score_outbounds(self, lic1, lic2): - logging.debug(f'preferred_score_inbounds({lic1}, {lic2})') - return self.preferred_score_licenses(lic1, lic2, 'inbound_license') + logging.debug(f'preferred_score_outbounds({lic1}, {lic2})') + return self.preferred_score_licenses(lic1, lic2, 'outbound_license') def least_preferred(self, lic1, lic2, ignore_missing=False): most_preferred = self.most_preferred(lic1, lic2, ignore_missing) @@ -198,121 +264,449 @@ def __is_license_expression(self, lic): CONTAINS_AND = 'AND' in lic CONTAINS_OR = 'OR' in lic return CONTAINS_AND or CONTAINS_OR - + def usable_license(self, lic): - license_name = lic['inbound_license'] + print("FALCAO1 usable_license keys: " + str(lic)) + print("FALCAO usable_license keys: " + str(lic['inbound']) + " " + str(lic.keys())) + # TODO: fix inbound to key + license_name = lic['inbound']['license'] if self.__is_license_expression(license_name): # should have been checked already, so skip policy_ok = True else: + + print(f'FALCAO +++ -2 usable_license {self.policy}') + print(f'FALCAO +++ -1 usable_license {license_name}') + print(f'FALCAO +++ 0 usable_license {self.policy.allowed()}') + print(f'FALCAO +++ 0 usable_license {self.policy.avoided()}') + print(f'FALCAO +++ 1 usable_license {license_name in self.policy.allowed()}') + print(f'FALCAO +++ 2 usable_license {license_name in self.policy.avoided()}') + print(f'FALCAO +++ 3 usable_license {license_name in (self.policy.allowed() + self.policy.avoided())}') policy_ok = license_name in (self.policy.allowed() + self.policy.avoided()) compat_ok = lic['compatibility'] == 'yes' ret = policy_ok and compat_ok logging.debug(f'usable_license({lic}) | policy_ok: {policy_ok}, compat_ok: {compat_ok} ===> {ret}') + print(f'FALCAO +++ usable_license({lic}) | policy_ok: {policy_ok}, compat_ok: {compat_ok} ===> {ret}') return ret + def unusable_licenses(self, licenses, key): + print("UG " + str(licenses)) + problematic_licenses = [] + for lic in licenses: + print("lic: " + str(lic.keys())) + license_name = lic[key]['license'] + if self.__is_license_expression(license_name): + # should have been checked already, so skip + policy_ok = True + else: + policy_ok = license_name in (self.policy.allowed() + self.policy.avoided()) + compat_ok = lic['compatibility'] == 'yes' + + ret = policy_ok and compat_ok + logging.debug(f'usable_license({lic}) | policy_ok: {policy_ok}, compat_ok: {compat_ok} ===> {ret}') + if not ret: + problematic_licenses.append(lic) + return len(problematic_licenses) > 0 + def scored_general(self, licenses, operator, key): + print(f'scored_general({licenses}, {operator}, {key})') + compare_function = { + 'inbound': self.policy.preferred_score_inbounds, + 'outbound': self.policy.preferred_score_outbounds, + }[key] + print("sgo: func= " + str(compare_function)) if operator == "OR": + print("FALCAO sg: 9: " + str(licenses)) logging.debug(f'preferred_licenses({licenses}, {operator})') filtered_licenses = [x for x in licenses if self.usable_license(x)] - if key == 'inbound_license': - sorted_licenses = sorted(filtered_licenses, key=cmp_to_key(self.policy.preferred_score_inbounds)) - elif key == 'outbound_license': - sorted_licenses = sorted(filtered_licenses, key=cmp_to_key(self.policy.preferred_score_outbounds)) - logging.debug(f'sorted_licenses: {[x["inbound_license"] for x in sorted_licenses]})') + print("FALCAO sg: 9.1: " + str(filtered_licenses)) + sorted_licenses = sorted(filtered_licenses, key=cmp_to_key(compare_function)) + print("FALCAO sg: 9.2: " + str(sorted_licenses)) + logging.debug(f'sorted_licenses: {[x[key]['license'] for x in sorted_licenses]})') return sorted_licenses elif operator == "AND": - if key == 'inbound_license': - sorted_licenses = sorted(licenses, key=cmp_to_key(self.policy.preferred_score_inbounds)) - elif key == 'outbound_license': - sorted_licenses = sorted(licenses, key=cmp_to_key(self.policy.preferred_score_outbounds)) + print("FALCAO sg: 10") + if self.unusable_licenses(licenses, 'inbound'): + print("FALCAO sg: 10.1") + sorted_licenses = [] + else: + sorted_licenses = sorted(licenses, key=cmp_to_key(compare_function)) + return sorted_licenses else: raise Exception("TODO fix this") def scored_inbounds(self, inbounds, operator): - return self.scored_general(inbounds, operator, 'inbound_license') + print(f'scored_inbounds({inbounds}, {operator})') + return self.scored_general(inbounds, operator, 'inbound') def scored_outbounds(self, outbounds, operator): - return self.scored_general(outbounds, operator, 'outbound_license') - + return self.scored_general(outbounds, operator, 'outbound') + + def __check_policy_check(self, reply): + keys = [ + 'license', + 'type', + 'preferences:license', + 'preferences:license_list', + 'preferences:license_index' + ] + print("reply: " + json.dumps(reply, indent=4)) + print("reply keys: " + str(reply.keys())) + for key in keys: + in_value = reply['inbound'] + out_value = reply['outbound'] + for sub_key in key.split(':'): + assert sub_key in in_value + print(str(" sub_key " + sub_key)) + assert sub_key in out_value + in_value = in_value[sub_key] + out_value = out_value[sub_key] + + assert 'compatibility' in reply + assert 'unusable' in reply + + def __pack_policy_check(self, outbound_pref, inbound_pref, compat, unusable): + return { + 'outbound': outbound_pref, + 'inbound': inbound_pref, + 'compatibility': compat, + 'unusable': unusable, + } + + def __pack_policy_unusable(self, unusables): + return { + 'unusable': unusables + } + + def __pack_policy_preferences(self, license_string, license_list, license_index): + return { + 'license': license_tring, + 'license_list': license_list, + 'license_index': license_index + } + + def __pack_policy_check_license(self, license_name, license_type, pref_lic, pref_lic_list, pref_lic_idx): + return { + 'license': license_name, + 'type': license_type, + 'preferences': { + 'license': pref_lic, + 'license_list': pref_lic_list, + 'license_index': pref_lic_idx + } + } + def __apply_to_compat_object(self, compat_object, ignore_missing=False, indent=0): + """ + Specification of the policy_check format + + define license_data: + + license (str) + license_type (str: license | license-expression) + preferrences { + license + license_list + license_index + } + + unusable [ + { license (str), + reason (str) + } + ] + + inbound (license_data) + outbound (license_data) + compatibility (str) + + """ + print("ATC") if 'outbound-expression' in compat_object['compatibility_check']: operator = compat_object['operator'] outbounds = [] + print("LFC: " + str(compat_object.keys())) for operand in compat_object['operands']: + operand['compatibility_object']['TEST'] = "Liverpool" + print("sgo: operand: BEGIN ") + print("sgo: operand: BEGIN " + str(operand['compatibility_object']['outbound_license']) + " " + str(operand['compatibility_object'].keys())) self.__apply_to_compat_object(operand['compatibility_object'], indent+4) - outbounds.append(operand['compatibility_object']['policy_check']) + self.__check_policy_check(operand['compatibility_object']['policy_check']) + outbounds.append(operand['compatibility_object']['policy_check']) + + POL=operand['compatibility_object']['policy_check'] + print("sgo: operand: WATCH " + str(operand['compatibility_object']['policy_check']['outbound']) + " " + str(operand['compatibility_object']['policy_check'].keys())) + print("sgo: operand: KEEP " + str(POL.keys())) + #print("sgo: operand: KEEP " + str(POL['inbound_list']) + " " + str(POL['inbound_list_index']) + " " + str(POL['preferred_inbound_license'])) + print("sgo: operand: END " + str(operand['compatibility_object']['policy_check']['outbound']) + " " + str(operand['compatibility_object']['policy_check'].keys())) scored_outbounds = self.scored_outbounds(outbounds, operator) + print("sgo: operands: HERE: " + str(outbounds[0])) + print("sgo: operands: HERE: " + str(outbounds[0].keys())) + + outbound_list = [] outbound_list_index = -1 + preferred_inbound_license = None + compatibility = 'no' + + print("EDER: " + operator) + print("EDER: " + str(len(scored_outbounds))) if len(scored_outbounds) > 0: - outbound_list = scored_outbounds[0]['outbound_list'] - outbound_list_index = scored_outbounds[0]['outbound_list_index'] - compat_object['policy_check'] = { + print("EDER: OUTBONDS") + if operator == 'AND': + outbound_name = ' AND '.join([x['outbound']['preferences']['license'] for x in scored_outbounds]) + outbound_pref = scored_outbounds[-1] + else: + outbound_name = scored_outbounds[0]['outbound']['preferences']['license'] + outbound_pref = scored_outbounds[0] + + print("0 --------------- " + str(len(scored_outbounds))) + print("2 --------------- " + str(scored_outbounds[0])) + print("2 --------------- " + str(scored_outbounds[1])) + outbound_list = outbound_pref['outbound']['preferences']['license_list'] + outbound_list_index = outbound_pref['outbound']['preferences']['license_index'] + inbound_prefs = outbound_pref['inbound']['preferences'] + print("inbound_prefs: " + str(inbound_prefs)) + preferred_inbound_license = inbound_prefs['license'] + preferred_inbound_license_list = inbound_prefs['license_list'] + preferred_inbound_license_index = inbound_prefs['license_index'] + + compatibility = 'yes' + + else: + print("EDER: NO outbonds") + outbound_name = None + outbound_list = None + outbound_list_index = None + + preferred_inbound_license = None + preferred_inbound_license_list = None + preferred_inbound_license_index = None + + compatibility = 'no' + + if 'inbound-expression' in compat_object['compatibility_check']: + inbound_license_type = 'license-expression' + else: + inbound_license_type = 'license' + + policy_check_inbound_license = self.__pack_policy_check_license( + compat_object['inbound_license'], + inbound_license_type, + preferred_inbound_license, + preferred_inbound_license_list, + preferred_inbound_license_index + ) + policy_check_outbound_license = self.__pack_policy_check_license( + compat_object['outbound_license'], + 'license-expression', + outbound_name, + outbound_list, + outbound_list_index + ) + # TODO: add identify and pass on unusable + unusable = self.__pack_policy_unusable([]) + policy_license_object = self.__pack_policy_check( + policy_check_outbound_license, + policy_check_inbound_license, + compatibility, + unusable) + import json,sys + print("OP" + json.dumps(policy_license_object, indent=4)) + + compat_object['policy_check'] = policy_license_object + + + + #compat_object['policy_check_OLD'] = { + dummy = { 'outbound_license': compat_object['outbound_license'], 'outbound_license_type': 'license-expression', 'outbound_licenses': scored_outbounds, 'outbound_list': outbound_list, 'outbound_list_index': outbound_list_index, + + 'inbound_license': compat_object['inbound_license'], + 'inbound_license_type': inbound_license_type, + 'compatibility': compatibility, +# 'inbound_licenses': None, + 'inbound_list': None, + 'inbound_list_index': None, + 'preferred_outbound_license': outbound_name, + 'preferred_outbound_license_list': outbound_list, + 'preferred_outbound_license_index': outbound_list_index, + 'preferred_inbound_license': preferred_inbound_license, + 'preferred_inbound_license_list': preferred_inbound_license_list, + 'preferred_inbound_license_index': preferred_inbound_license_index, + 'HENRIK_OP': operator, } + if operator == 'AND': + compat_object['policy_check']['HENRIK_DIX'] = 'AND' + else: + compat_object['policy_check']['HENRIK_DIX'] = 'NOT NOT AND : ' + operator + elif 'outbound-license' in compat_object['compatibility_check']: - # Get outbound license (not expression) data - out_lic = compat_object["outbound_license"] - out_list_nr, out_index = self.policy.list_presence(out_lic, ignore_missing=ignore_missing) - out_list_name = self.policy.list_nr_to_name(out_list_nr) - compat_object['policy_check'] = { - 'outbound_license': compat_object['outbound_license'], - 'outbound_license_type': 'license', - 'outbound_list': out_list_name, - 'outbound_list_index': out_index, - } if 'inbound-expression' in compat_object['compatibility_check']: inner_compat_object = compat_object operator = inner_compat_object['operator'] inbounds = [] + print("ZICO inbound: " + compat_object['inbound_license']) + print("ZICO operator: " + operator) for operand in inner_compat_object['operands']: self.__apply_to_compat_object(operand['compatibility_object'], indent+4) + import json, sys + print(json.dumps(operand['compatibility_object']['policy_check'], indent=4)) + print(" FALCAO operand " + str(operand['compatibility_object']['policy_check'])) + #assert False + #sys.exit(1) + self.__check_policy_check(operand['compatibility_object']['policy_check']) inbounds.append(operand['compatibility_object']['policy_check']) + print("ZICO operand: " + operand['compatibility_object']['inbound_license']) + + import json, sys + print("FALCAO operands " + str(inbounds)) scored_inbounds = self.scored_inbounds(inbounds, operator) + print("FALCAO scored inbounds " + str(scored_inbounds)) inbound_list = [] inbound_list_index = -1 + preferred_inbound_license = None + + print("FALCAO inner: " + str(inbounds)) + print("FALCAO scored: " + str(scored_inbounds)) + print("FALCAO oper: " + str(operator)) +# sys.exit(1) if len(scored_inbounds) > 0: - inbound_list = scored_inbounds[0]['inbound_list'] - inbound_list_index = scored_inbounds[0]['inbound_list_index'] + print("REICH: " + str(scored_inbounds[0])) + if operator == 'AND': + # use last (least preferred) license + inbound_list = scored_inbounds[-1]['inbound']['preferences']['license_list'] + inbound_list_index = scored_inbounds[-1]['inbound']['preferences']['license_index'] + preferred_inbound_license = ' AND '.join([x['inbound']['preferences']['license'] for x in scored_inbounds]) + else: + # use first (most preferred) license + inbound_list = scored_inbounds[0]['inbound']['preferences']['license_list'] + inbound_list_index = scored_inbounds[0]['inbound']['preferences']['license_index'] + preferred_inbound_license = scored_inbounds[0]['inbound']['preferences']['license'] + + print("ZICO: " + str(compat_object['compatibility_check']) + ": " + str(len(scored_inbounds))) + print("ZICO: " + str(compat_object['compatibility_check']) + ": " + str(preferred_inbound_license)) + print("ZICO: " + str(compat_object['compatibility_check']) + ": " + json.dumps(scored_inbounds, indent=4)) + print("ZICO: " + str(compat_object['compatibility_check']) + ": " + operator) + print("ZICO: " + str(compat_object['compatibility_check']) + ": " + str(preferred_inbound_license)) +# sys.exit(1) + if len(scored_inbounds) == 1: + #sys.exit(1) + pass + out_lic = inner_compat_object["outbound_license"] + out_list_nr, out_index = self.policy.list_presence(out_lic, ignore_missing=ignore_missing) + out_list_name = self.policy.list_nr_to_name(out_list_nr) - inner_compat_object['policy_check'] = { - 'check_type': 'inbound', + + print("FALCAO inner: " + json.dumps(preferred_inbound_license, indent=4)) + policy_check_inbound_license = self.__pack_policy_check_license( + inner_compat_object['inbound_license'], + 'license-expression', + preferred_inbound_license, + inbound_list, + inbound_list_index + ) + policy_check_outbound_license = self.__pack_policy_check_license( + out_lic, + 'license', + out_lic, + out_list_name, + out_index + ) + # TODO: add identify and pass on unusable + unusable = self.__pack_policy_unusable([]) + policy_license_object = self.__pack_policy_check( + policy_check_outbound_license, + policy_check_inbound_license, + compat_object['compatibility'], + unusable) + inner_compat_object['policy_check'] = policy_license_object + + print("FALCAO: " + json.dumps(inner_compat_object['policy_check'], indent=4)) + print("FALCAO done") +# sys.exit(1) + + + +# inner_compat_object['policy_check_TMP'] = { + dummy = { 'inbound_license': inner_compat_object['inbound_license'], 'outbound_license': inner_compat_object['outbound_license'], 'inbound_license_type': 'license-expression', 'outbound_license_type': 'license', 'compatibility': compat_object['compatibility'], - 'inbound_licenses': scored_inbounds, +# 'inbound_licenses': scored_inbounds, 'inbound_list': inbound_list, 'inbound_list_index': inbound_list_index, + 'outbound_list': out_list_name, + 'outbound_list_index': out_index, + 'preferred_inbound_license': preferred_inbound_license, + 'preferred_outbound_license': inner_compat_object['outbound_license'], } if 'inbound-license' in compat_object['compatibility_check']: - pref = self.policy.most_preferred(compat_object['outbound_license'], compat_object['inbound_license'], ignore_missing=True) - - lic = compat_object["inbound_license"] - list_nr, index = self.policy.list_presence(lic, ignore_missing=ignore_missing) - list_name = self.policy.list_nr_to_name(list_nr) - compat_object['policy_check'].update({ - 'check_type': 'inbound', - 'inbound_license': compat_object['inbound_license'], - 'outbound_license': compat_object['outbound_license'], - 'inbound_license_type': 'license', - 'outbound_license_type': 'license', - 'compatibility': compat_object['compatibility'], - 'inbound_list': list_name, - 'inbound_list_index': index, - }) + #pref = self.policy.most_preferred(compat_object['outbound_license'], compat_object['inbound_license'], ignore_missing=True) + + out_lic = compat_object["outbound_license"] + out_list_nr, out_index = self.policy.list_presence(out_lic, ignore_missing=ignore_missing) + out_list_name = self.policy.list_nr_to_name(out_list_nr) + + in_lic = compat_object['inbound_license'] + in_list_nr, in_list_index = self.policy.list_presence(in_lic, ignore_missing=ignore_missing) + in_list_name = self.policy.list_nr_to_name(in_list_nr) + out_lic = compat_object["outbound_license"] + out_list_nr, out_index = self.policy.list_presence(out_lic, ignore_missing=ignore_missing) + out_list_name = self.policy.list_nr_to_name(out_list_nr) + # TODO: fill in the below + + policy_check_inbound_license = self.__pack_policy_check_license( + in_lic, + 'license', + in_lic, + in_list_name, + in_list_index + ) + policy_check_outbound_license = self.__pack_policy_check_license( + out_lic, + 'license', + out_lic, + out_list_name, + out_index, + ) + # TODO: add identify and pass on unusable + unusable = self.__pack_policy_unusable([]) + policy_license_object = self.__pack_policy_check( + policy_check_outbound_license, + policy_check_inbound_license, + compat_object['compatibility'], + unusable) + compat_object['policy_check'] = policy_license_object + # TODO: REMOVE + if False: + compat_object['policy_check_TMP'] = { + 'inbound_license': in_lic, + 'outbound_license': out_lic, + 'inbound_license_type': 'license', + 'outbound_license_type': 'license', + 'compatibility': compat_object['compatibility'], + 'inbound_list': list_name, + 'inbound_list_index': index, + 'outbound_list': out_list_name, + 'outbound_list_index': out_index, + 'preferred_inbound_license': lic, + "HESA": "LIVEROPOPOL", + } + print("sgo: operand outbound license: " + str(compat_object.keys())) + #print("sgo: operand: ---> " + str(compat_object['policy_check']['outbound_license']) + " <--- " + str(compat_object['policy_check']['outbound_list'] + " keys: " + str(compat_object['policy_check'].keys()))) else: raise Exception("We should not be here") @@ -332,6 +726,7 @@ def apply_policy(self, compat_report, ignore_missing=False): logging.debug("apply_policy") self.__apply_to_compat_object(top_object, ignore_missing=ignore_missing) + self.__check_policy_check(top_object['policy_check']) meta = compat_report['meta'] meta['policy_type'] = self.policy_type meta['policy_file'] = self.policy_file diff --git a/licomp_toolkit/schema_checker.py b/licomp_toolkit/schema_checker.py index 142c0b5..45f0edd 100644 --- a/licomp_toolkit/schema_checker.py +++ b/licomp_toolkit/schema_checker.py @@ -43,7 +43,8 @@ def __validate_deeply(self, compat): if compat_check == 'outbound-expression -> inbound-license' or compat_check == 'outbound-expression -> inbound-expression': compat_object = compat elif compat['compatibility_check'] == 'outbound-license -> inbound-expression': - compat_object = compat['compatibility_object'] + #compat_object = compat['compatibility_object'] + compat_object = compat else: raise LicompException("Validation failed. Invalid state: " + compat_check) diff --git a/licomp_toolkit/toolkit.py b/licomp_toolkit/toolkit.py index ae3f7ab..e38e674 100644 --- a/licomp_toolkit/toolkit.py +++ b/licomp_toolkit/toolkit.py @@ -23,7 +23,9 @@ from licomp_toolkit.config import disclaimer from licomp_toolkit.config import licomp_toolkit_version +from licomp_toolkit.config import licomp_toolkit_file_version from licomp_toolkit.config import cli_name +from licomp_toolkit.config import module_name from licomp_toolkit.expr_parser import LicenseExpressionParser from licomp_toolkit.expr_parser import COMPATIBILITY_TYPE @@ -92,6 +94,12 @@ def licomp_resources_long(self): _resources.append(self.licomp_resource_long(resource)) return _resources + def licomp_resources_short(self): + _resources = [] + for resource in self.licomp_resources().values(): + _resources.append(f'{resource.name()}:{resource.version()}') + return _resources + def _resource_type(self, resource): if self._resource_is_standard(resource): return 'standard' @@ -114,7 +122,7 @@ def __summarize_compatibility(self, compatibilities, outbound, inbound, usecase, logging.debug(f': {compat["resource_name"]}') self.__add_to_list(statuses, compat['status'], compat) self.__add_to_list(compats, compat['compatibility_status'], compat) - compatibilities["summary"]["resources"] = self.licomp_resources_long() + compatibilities["summary"]["resources"] = self.licomp_resources_short() compatibilities["summary"]["outbound"] = outbound compatibilities["summary"]["inbound"] = inbound compatibilities["summary"]["usecase"] = UseCase.usecase_to_string(usecase) @@ -245,8 +253,9 @@ def check_compatibility(self, detailed_report=True): compat_object = { - COMPATIBILITY_TYPE: parsed_expression[COMPATIBILITY_TYPE], - 'compatibility_check': 'outbound-expression -> inbound-license', +# INBOUND_COMPATIBILITY_TYPE: parsed_expression[COMPATIBILITY_TYPE], + 'compatibility_check': f'outbound-{self.le_parser.parse_license_expression(outbound)["compatibility_type"]} -> inbound-{parsed_expression["compatibility_type"]}', + 'check_class': __class__.__name__, } if parsed_expression[COMPATIBILITY_TYPE] == 'license': @@ -265,7 +274,7 @@ def check_compatibility(self, compat_object['inbound_license'] = lic compat_object['outbound_license'] = outbound compat_object['compatibility_object'] = {} - + else: operator = parsed_expression['operator'] operands = parsed_expression['operands'] @@ -279,7 +288,7 @@ def check_compatibility(self, operand_compat = self.check_compatibility(outbound, operand, usecase, provisioning, resources, detailed_report=detailed_report) operand_object = { 'compatibility_object': operand_compat, - 'compatibility': operand_compat['compatibility'], + 'compatibility': operand_compat['compatibility'] } operands_object.append(operand_object) @@ -344,6 +353,13 @@ def __init__(self): def __parsed_expression_to_name(self, parsed_expression): return parsed_expression[parsed_expression[COMPATIBILITY_TYPE]] + def meta_information(self): + return { + 'tool': module_name, + 'file': 'verification', + 'file_version': licomp_toolkit_file_version + } + def check_compatibility(self, outbound, inbound, usecase, provisioning, resources=None, detailed_report=True): # Check usecase @@ -396,6 +412,7 @@ def check_compatibility(self, outbound, inbound, usecase, provisioning, resource resources, detailed_report) return { + 'meta': self.meta_information(), 'inbound': str(inbound), 'outbound': str(outbound), 'usecase': usecase, @@ -404,7 +421,7 @@ def check_compatibility(self, outbound, inbound, usecase, provisioning, resource 'compatibility': compatibility_object['compatibility'], 'compatibility_report': compatibility_object, 'unavailable_resources': unavailable_resources, - 'available_resources': available_resources, + 'available_resources': available_resources } def __check_compatibility(self, @@ -417,27 +434,46 @@ def __check_compatibility(self, outbound_type = outbound_parsed[COMPATIBILITY_TYPE] compat_object = { - COMPATIBILITY_TYPE: outbound_type, +# OUTBOUND_COMPATIBILITY_TYPE: outbound_type, 'inbound_license': self.le_parser.to_string(inbound_parsed), 'outbound_license': self.le_parser.to_string(outbound_parsed), + 'check_class': __class__.__name__, } if outbound_type == 'license': - compat_object['compatibility_check'] = f'outbound-license -> inbound-{inbound_parsed["compatibility_type"]}' + if False: + # TODO: REMOVE HERE? + compat_object['compatibility_check'] = f'outbound-license -> inbound-{inbound_parsed["compatibility_type"]} HERE?' + outbound_parsed_license = outbound_parsed['license'] + compat_object['HENRIK_TESTS'] = 'MONKEY BALLS' + + # TODO: SHOULD operator/operands be added???? SEEMS LIKE NO + #compat_object['operator'] = inbound_parsed['operator'] + #compat_object['operands'] = inbound_parsed['operands'] + # Check if: + # outbound license + # is compatible with + # inbound license + compat = self.le_checker.check_compatibility(outbound_parsed_license, + inbound_parsed, + usecase, + provisioning, + resources, + detailed_report) + compat_object['compatibility'] = compat['compatibility'] + compat_object['compatibility_object'] = compat + compat_object['compatibility_details'] = None + + # TODO: CHECK IF ABOVE CAN BE REMOVED outbound_parsed_license = outbound_parsed['license'] - # Check if: - # outbound license - # is compatible with - # inbound license compat = self.le_checker.check_compatibility(outbound_parsed_license, inbound_parsed, usecase, provisioning, resources, detailed_report) - compat_object['compatibility'] = compat['compatibility'] - compat_object['compatibility_object'] = compat - compat_object['compatibility_details'] = None + compat_object = compat + elif outbound_type == 'expression': compat_object['compatibility_details'] = None From ffd1dfc04c100c04942c83587f55392b3fa2beba Mon Sep 17 00:00:00 2001 From: Henrik Sandklef Date: Fri, 6 Feb 2026 22:08:26 +0100 Subject: [PATCH 18/41] using the precalculated values --- licomp_toolkit/format.py | 49 ++++++++++++---------------------------- 1 file changed, 14 insertions(+), 35 deletions(-) diff --git a/licomp_toolkit/format.py b/licomp_toolkit/format.py index 1988896..fb82ce3 100644 --- a/licomp_toolkit/format.py +++ b/licomp_toolkit/format.py @@ -155,7 +155,7 @@ def _format_compat_pref(self, compat, pref_lic=None): else: return f'{PAREN_OPEN}{compat_string}{PAREN_CLOSE}' - def format_compatibilities_general(self, compat_object, indent='', policy_report=False, preferred_license=False, least_preferred_license=False): + def format_compatibilities_general(self, compat_object, indent='', policy_report=False, preferred_license=False): compatibility_check = compat_object["compatibility_check"] output = [] @@ -164,17 +164,16 @@ def format_compatibilities_general(self, compat_object, indent='', policy_report details = compat_object["compatibility_details"] summary = details["summary"] preferred_info = 'no' - least_preferred_info = 'no' + import sys + print ("policy_report and preferred_license:" + str(policy_report) + " " + str(preferred_license)) +# sys.exit(1) if policy_report and preferred_license: preferred_info = 'yes' - if policy_report and least_preferred_license: - least_preferred_info = 'yes' #output.append(f'{indent}{compat_object["outbound_license"]} -> {compat_object["inbound_license"]} {self._format_compat_pref(compat_object["compatibility"])}') output.append(f'{indent}{compat_object["outbound_license"]} -> {compat_object["inbound_license"]}') #output.append(f'{indent} check: {compatibility_check}') if policy_report: - output.append(f'{indent} preferred license: {preferred_info}') - output.append(f'{indent} least preferred license: {least_preferred_info}') + output.append(f'{indent} preferred: {preferred_info}') output.append(f'{indent} compatibility: {compat_object["compatibility"]}') output.append(f'{indent} compatibility details: ') output += self.__compatibility_statuses(summary['compatibility_statuses'], f'{indent} ') @@ -183,35 +182,24 @@ def format_compatibilities_general(self, compat_object, indent='', policy_report operator = compat_object["operator"] inner_output = [] if policy_report: - if len(compat_object['policy_check']['inbound_licenses']) > 0: - preferred_license = compat_object['policy_check']['inbound_licenses'][0]['inbound_license'] - least_preferred_license = compat_object['policy_check']['inbound_licenses'][-1]['inbound_license'] + preferred_license_ = compat_object['policy_check']['inbound']['preferences']['license'] for operand in compat_object["operands"]: preferred = False - least_preferred = False if policy_report: operand_license = operand['compatibility_object']['inbound_license'] if operand_license == preferred_license: preferred = True - if operand_license == least_preferred_license: - least_preferred = True - res = self.format_compatibilities_general(operand['compatibility_object'], indent=f'{indent} ', policy_report=policy_report, preferred_license=preferred, least_preferred_license=least_preferred) + res = self.format_compatibilities_general(operand['compatibility_object'], indent=f'{indent} ', policy_report=policy_report, preferred_license=preferred_license) #inner_output.append(f'{indent} allowed licenses: {", ".join([x["inbound_license"] for x in compat_object["policy_check"]["inbound_licenses"]])}') inner_output.append(res) if policy_report: - if len(compat_object['policy_check']['inbound_list']) > 0: - if operator == 'OR': - pref_lic = compat_object['policy_check']['inbound_licenses'][0]['inbound_license'] - elif operator == 'AND': - pref_lic = ' AND '.join([x['inbound_license'] for x in compat_object['policy_check']['inbound_licenses']]) - else: - pref_lic = '' - output.append(f'{indent}{operator} {self._format_compat_pref(compat_object["compatibility"], pref_lic)}') + preferred_license = compat_object['policy_check']['inbound']['preferences']['license'] + output.append(f'{indent}{operator} {self._format_compat_pref(compat_object["compatibility"], preferred_license)}') output.append(f'{indent} check: {compatibility_check}') output.append(f'{indent} outbound: {compat_object["outbound_license"]}') output.append(f'{indent} inbound: {compat_object["inbound_license"]}') output.append(f'{indent} comaptibility: {compat_object["compatibility"]}') - output.append(f'{indent} preferred: {pref_lic}') + output.append(f'{indent} preferred: {preferred_license}') output.append(f'{indent} details:') output += inner_output @@ -219,9 +207,10 @@ def format_compatibilities_general(self, compat_object, indent='', policy_report operator = compat_object["operator"] output.append(f'{indent}{operator} {self._format_compat_pref(compat_object["compatibility"])}') for operand in compat_object["operands"]: - res = self.format_compatibilities_general(operand['compatibility_object'], indent=f'{indent} ', policy_report=policy_report) + res = self.format_compatibilities_general(operand['compatibility_object'], indent=f'{indent} ', policy_report=policy_report, preferred_license=preferred_license) output.append(res) if compatibility_check == "outbound-expression -> inbound-expression": + print("APA") operator = compat_object["operator"] compat = compat_object["compatibility"] output.append(f'{indent}{operator} {self._format_compat_pref(compat)}') @@ -232,7 +221,7 @@ def format_compatibilities_general(self, compat_object, indent='', policy_report output.append(f'{indent} preferred: TODO') output.append(f'{indent} details:') for operand in compat_object['operands']: - res = self.format_compatibilities_general(operand['compatibility_object'], indent=f'{indent} ', policy_report=policy_report) + res = self.format_compatibilities_general(operand['compatibility_object'], indent=f'{indent} ', policy_report=policy_report, preferred_license=preferred_license) output.append(f'{res}') return "\n".join(output) @@ -245,16 +234,6 @@ def format_policy_report(self, report): preferred_inbound = '' print("keys: " + str(report["compatibility_report"].keys())) - import sys, json - print(json.dumps(report["compatibility_report"]["policy_check"], indent=4)) - print("done in format") - sys.exit(1) - if report["compatibility_report"]["policy_check"]['inbound_license_type'] == 'license': - preferred_inbound = report["compatibility_report"]["policy_check"]["inbound_license"] - elif report["compatibility_report"]["policy_check"]['inbound_license_type'] == 'license-expression': - if len(report["compatibility_report"]["policy_check"]["inbound_licenses"]) > 0: - preferred_inbound = report["compatibility_report"]["policy_check"]["inbound_licenses"][0]['inbound_license'] - output.append(f'outbound: {report["outbound"]}') output.append(f'inbound: {report["inbound"]}') output.append(f'resources: {", ".join(report["resources"])}') @@ -268,7 +247,7 @@ def format_policy_report(self, report): policy_string = report["meta"]["policy_file"] output.append(f'policy: {policy_string}') output.append('report:') - output.append(self.format_compatibilities_general(report["compatibility_report"], indent=' ', policy_report=True)) + output.append(self.format_compatibilities_general(report["compatibility_report"], indent=' ', policy_report=True, preferred_license=True)) return "\n".join(output) def format_compatibilities(self, compat): From b60940c9fa7975ee8a37803412b9d4e3d225df8e Mon Sep 17 00:00:00 2001 From: Henrik Sandklef Date: Fri, 6 Feb 2026 22:08:56 +0100 Subject: [PATCH 19/41] remove test variables --- licomp_toolkit/license_policy.py | 5 ----- 1 file changed, 5 deletions(-) diff --git a/licomp_toolkit/license_policy.py b/licomp_toolkit/license_policy.py index 642ed35..f7850d0 100644 --- a/licomp_toolkit/license_policy.py +++ b/licomp_toolkit/license_policy.py @@ -543,12 +543,7 @@ def __apply_to_compat_object(self, compat_object, ignore_missing=False, indent=0 'preferred_inbound_license': preferred_inbound_license, 'preferred_inbound_license_list': preferred_inbound_license_list, 'preferred_inbound_license_index': preferred_inbound_license_index, - 'HENRIK_OP': operator, } - if operator == 'AND': - compat_object['policy_check']['HENRIK_DIX'] = 'AND' - else: - compat_object['policy_check']['HENRIK_DIX'] = 'NOT NOT AND : ' + operator elif 'outbound-license' in compat_object['compatibility_check']: From d8276c7edb22e3c91af2a3a85ca31ec0dea3da86 Mon Sep 17 00:00:00 2001 From: Henrik Sandklef Date: Sat, 7 Feb 2026 00:19:02 +0100 Subject: [PATCH 20/41] add " around arguments --- devel/licomp-toolkit | 9 ++++++++- 1 file changed, 8 insertions(+), 1 deletion(-) diff --git a/devel/licomp-toolkit b/devel/licomp-toolkit index f061b27..3c28ce8 100755 --- a/devel/licomp-toolkit +++ b/devel/licomp-toolkit @@ -14,6 +14,13 @@ fi if [ "$1" = "" ] then ARGS="verify -il MIT -ol \"MIT OR X11\"" +else + ARGS="" + for arg in "$@" + do + ARGS="$ARGS \"$arg\"" + done fi -echo PYTHONPATH=${EXTRA_PYTHONPATH}:${PYTHONPATH} ${SCRIPT_DIR}/licomp_toolkit/__main__.py $* $ARGS | bash + +echo PYTHONPATH=${EXTRA_PYTHONPATH}:${PYTHONPATH} ${SCRIPT_DIR}/licomp_toolkit/__main__.py $ARGS | bash From 9a0e41882572ee2b905af30cc67c5df7a83368d8 Mon Sep 17 00:00:00 2001 From: Henrik Sandklef Date: Sat, 7 Feb 2026 00:55:07 +0100 Subject: [PATCH 21/41] update to new data struct --- tests/python/test_expr_expr.py | 41 ++++++++++++++++++++++------------ 1 file changed, 27 insertions(+), 14 deletions(-) diff --git a/tests/python/test_expr_expr.py b/tests/python/test_expr_expr.py index d52c0a8..f1e4378 100644 --- a/tests/python/test_expr_expr.py +++ b/tests/python/test_expr_expr.py @@ -29,8 +29,8 @@ def _compat_status(report): return report['compatibility_report']['compatibility'] -def _compat_type(report): - return report['compatibility_report']['compatibility_type'] +def _compat_check(report): + return report['compatibility_report']['compatibility_check'] # # license compat with license @@ -42,7 +42,8 @@ def test_lic_lic_compat(): usecase=UseCase.usecase_to_string(UseCase.LIBRARY), provisioning=Provisioning.provisioning_to_string(Provisioning.BIN_DIST)) assert _compat_status(compat_report) == 'yes' - assert _compat_type(compat_report) == 'license' + assert 'inbound-license' in _compat_check(compat_report) + assert 'outbound-license' in _compat_check(compat_report) # MIT -> GPL-2.0-only -> are NOT compatible def test_lic_lic_incompat(): @@ -50,7 +51,8 @@ def test_lic_lic_incompat(): usecase=UseCase.usecase_to_string(UseCase.LIBRARY), provisioning=Provisioning.provisioning_to_string(Provisioning.BIN_DIST)) assert _compat_status(compat_report) == 'no' - assert _compat_type(compat_report) == 'license' + assert 'inbound-license' in _compat_check(compat_report) + assert 'outbound-license' in _compat_check(compat_report) # # license compat with expression @@ -62,7 +64,8 @@ def test_lic_expr_compat(): usecase=UseCase.usecase_to_string(UseCase.LIBRARY), provisioning=Provisioning.provisioning_to_string(Provisioning.BIN_DIST)) assert _compat_status(compat_report) == 'yes' - assert _compat_type(compat_report) == 'license' + assert 'inbound-expression' in _compat_check(compat_report) + assert 'outbound-license' in _compat_check(compat_report) # GPL-2.0-only -> MIT AND Apache-2.0 are NOT compatible def test_lic_expr_incompat(): @@ -70,7 +73,9 @@ def test_lic_expr_incompat(): usecase=UseCase.usecase_to_string(UseCase.LIBRARY), provisioning=Provisioning.provisioning_to_string(Provisioning.BIN_DIST)) assert _compat_status(compat_report) == 'no' - assert _compat_type(compat_report) == 'license' + assert 'inbound-expression' in _compat_check(compat_report) + assert 'inbound-expression' in _compat_check(compat_report) + assert 'outbound-license' in _compat_check(compat_report) # # expression compat with license @@ -81,7 +86,8 @@ def test_expr_lic_compat(): usecase=UseCase.usecase_to_string(UseCase.LIBRARY), provisioning=Provisioning.provisioning_to_string(Provisioning.BIN_DIST)) assert _compat_status(compat_report) == 'yes' - assert _compat_type(compat_report) == 'expression' + assert 'inbound-license' in _compat_check(compat_report) + assert 'outbound-expression' in _compat_check(compat_report) # GPL-2.0-only AND BSD-3-Clause -> Apache-2.0 are NOT compatible def test_expr_lic_incompat(): @@ -89,7 +95,8 @@ def test_expr_lic_incompat(): usecase=UseCase.usecase_to_string(UseCase.LIBRARY), provisioning=Provisioning.provisioning_to_string(Provisioning.BIN_DIST)) assert _compat_status(compat_report) == 'no' - assert _compat_type(compat_report) == 'expression' + assert 'inbound-license' in _compat_check(compat_report) + assert 'outbound-expression' in _compat_check(compat_report) # # expression compat with expression @@ -101,7 +108,8 @@ def test_expr_expr_compat(): usecase=UseCase.usecase_to_string(UseCase.LIBRARY), provisioning=Provisioning.provisioning_to_string(Provisioning.BIN_DIST)) assert _compat_status(compat_report) == 'yes' - assert _compat_type(compat_report) == 'expression' + assert 'inbound-expression' in _compat_check(compat_report) + assert 'outbound-expression' in _compat_check(compat_report) # GPL-2.0-only AND BSD-3-Clause -> Apache-2.0 AND ISC are NOT compatible def test_expr_expr_incompat(): @@ -109,7 +117,8 @@ def test_expr_expr_incompat(): usecase=UseCase.usecase_to_string(UseCase.LIBRARY), provisioning=Provisioning.provisioning_to_string(Provisioning.BIN_DIST)) assert _compat_status(compat_report) == 'no' - assert _compat_type(compat_report) == 'expression' + assert 'inbound-expression' in _compat_check(compat_report) + assert 'outbound-expression' in _compat_check(compat_report) @@ -125,7 +134,8 @@ def test_expr_expr_large_compat(): usecase=UseCase.usecase_to_string(UseCase.LIBRARY), provisioning=Provisioning.provisioning_to_string(Provisioning.BIN_DIST)) assert _compat_status(compat_report) == 'yes' - assert _compat_type(compat_report) == 'expression' + assert 'inbound-expression' in _compat_check(compat_report) + assert 'outbound-expression' in _compat_check(compat_report) def test_expr_expr_large_incompat(): @@ -135,7 +145,8 @@ def test_expr_expr_large_incompat(): usecase=UseCase.usecase_to_string(UseCase.LIBRARY), provisioning=Provisioning.provisioning_to_string(Provisioning.BIN_DIST)) assert _compat_status(compat_report) == 'no' - assert _compat_type(compat_report) == 'expression' + assert 'inbound-expression' in _compat_check(compat_report) + assert 'outbound-expression' in _compat_check(compat_report) @@ -149,7 +160,8 @@ def test_expr_expr_with_1(): provisioning=Provisioning.provisioning_to_string(Provisioning.BIN_DIST)) assert _compat_status(compat_report) == 'no' - assert _compat_type(compat_report) == 'expression' + assert 'inbound-expression' in _compat_check(compat_report) + assert 'outbound-expression' in _compat_check(compat_report) def test_expr_expr_with_2(): @@ -159,7 +171,8 @@ def test_expr_expr_with_2(): usecase=UseCase.usecase_to_string(UseCase.LIBRARY), provisioning=Provisioning.provisioning_to_string(Provisioning.BIN_DIST)) assert _compat_status(compat_report) == 'no' - assert _compat_type(compat_report) == 'expression' + assert 'inbound-expression' in _compat_check(compat_report) + assert 'outbound-expression' in _compat_check(compat_report) From d42c24d3a33f68430a2429d4c648e2602bbdbf0e Mon Sep 17 00:00:00 2001 From: Henrik Sandklef Date: Sat, 7 Feb 2026 10:35:18 +0100 Subject: [PATCH 22/41] add verbose to formatting --- licomp_toolkit/__main__.py | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/licomp_toolkit/__main__.py b/licomp_toolkit/__main__.py index b4dd98a..d15b62f 100755 --- a/licomp_toolkit/__main__.py +++ b/licomp_toolkit/__main__.py @@ -80,7 +80,7 @@ def apply_license_policy(self, args): policy_report = lph.apply_policy(report, ignore_missing=True) ret_code = compatibility_status_to_returncode(report['compatibility']) formatter = LicompToolkitFormatter.formatter(self.args.output_format) - formatted_report = formatter.format_policy_report(report) + formatted_report = formatter.format_policy_report(report, verbose=args.verbose) return formatted_report, ret_code, False def verify(self, args): @@ -109,9 +109,9 @@ def verify(self, args): else: lph = LicensePolicyHandler() policy_report = lph.apply_policy(compatibilities) - return formatter.format_policy_report(policy_report), ret_code, False + return formatter.format_policy_report(policy_report, verbose=args.verbose), ret_code, False else: - return formatter.format_compatibilities(compatibilities), ret_code, False + return formatter.format_compatibilities(compatibilities, verbose=args.verbose), ret_code, False except LicompException as e: return e, e.return_code.value, True except FlameException as e: From 48aaf7cc3298ee68005e27884fdaccb52ed9afc1 Mon Sep 17 00:00:00 2001 From: Henrik Sandklef Date: Tue, 10 Feb 2026 22:37:55 +0100 Subject: [PATCH 23/41] simplify formatting --- licomp_toolkit/format.py | 55 ++++++++++++++++++++++++---------------- 1 file changed, 33 insertions(+), 22 deletions(-) diff --git a/licomp_toolkit/format.py b/licomp_toolkit/format.py index fb82ce3..96c85f2 100644 --- a/licomp_toolkit/format.py +++ b/licomp_toolkit/format.py @@ -18,7 +18,7 @@ def formatter(fmt): if fmt.lower() == 'dot': return DotLicompToolkitFormatter() - def format_compatibilities(self, compat): + def format_compatibilities(self, compat, verbose=False): raise Exception(f'{self.__class__.__name__} cannot format compatibilities.') def _pre_format_display_compatibilities(self, compats): @@ -55,7 +55,7 @@ def format_licomp_versions(self, licomp_versions): class JsonLicompToolkitFormatter(LicompToolkitFormatter): - def format_compatibilities(self, compat): + def format_compatibilities(self, compat, verbose=False): return json.dumps(compat, indent=4) def format_licomp_resources(self, licomp_resources): @@ -73,12 +73,15 @@ def format_display_compatibilities(self, compats, settings={}): display_compats = self._pre_format_display_compatibilities(compats) return json.dumps(display_compats, indent=4) - def format_policy_report(self, report): + def format_policy_report(self, report, verbose=False): return json.dumps(report, indent=4) class YamlLicompToolkitFormatter(LicompToolkitFormatter): - def format_compatibilities(self, compat): + def format_compatibilities(self, compat, verbose=False): + return yaml.safe_dump(compat, indent=4) + + def format_policy_report(self, report, verbose=False): return yaml.safe_dump(compat, indent=4) def format_licomp_resources(self, licomp_resources): @@ -155,7 +158,7 @@ def _format_compat_pref(self, compat, pref_lic=None): else: return f'{PAREN_OPEN}{compat_string}{PAREN_CLOSE}' - def format_compatibilities_general(self, compat_object, indent='', policy_report=False, preferred_license=False): + def format_compatibilities_general(self, compat_object, indent='', policy_report=False, preferred_license=None): compatibility_check = compat_object["compatibility_check"] output = [] @@ -165,15 +168,17 @@ def format_compatibilities_general(self, compat_object, indent='', policy_report summary = details["summary"] preferred_info = 'no' import sys - print ("policy_report and preferred_license:" + str(policy_report) + " " + str(preferred_license)) -# sys.exit(1) - if policy_report and preferred_license: + # TODO: + print ("policy_report and preferred_license DIEGO2:" + str(policy_report) + " " + str(preferred_license == compat_object["policy_check"]["inbound"]["preferences"]["license"])) + print ("policy_report and preferred_license DIEGO:" + str(compat_object["policy_check"]["inbound"]["preferences"]["license"]) + " <---> " + str(compat_object["inbound_license"]) + " SAME:" + str(compat_object["policy_check"]["inbound"]["preferences"]["license"]== compat_object["inbound_license"])) + #sys.exit(1) + if policy_report and preferred_license == compat_object["policy_check"]["inbound"]["preferences"]["license"]: preferred_info = 'yes' #output.append(f'{indent}{compat_object["outbound_license"]} -> {compat_object["inbound_license"]} {self._format_compat_pref(compat_object["compatibility"])}') output.append(f'{indent}{compat_object["outbound_license"]} -> {compat_object["inbound_license"]}') #output.append(f'{indent} check: {compatibility_check}') if policy_report: - output.append(f'{indent} preferred: {preferred_info}') + output.append(f'{indent} preferred: {preferred_info}') output.append(f'{indent} compatibility: {compat_object["compatibility"]}') output.append(f'{indent} compatibility details: ') output += self.__compatibility_statuses(summary['compatibility_statuses'], f'{indent} ') @@ -181,8 +186,9 @@ def format_compatibilities_general(self, compat_object, indent='', policy_report if compatibility_check == "outbound-license -> inbound-expression": operator = compat_object["operator"] inner_output = [] - if policy_report: - preferred_license_ = compat_object['policy_check']['inbound']['preferences']['license'] + preferred_info = 'no' + if policy_report and preferred_license == compat_object['policy_check']['inbound']['preferences']['license']: + preferred_info = 'yes' for operand in compat_object["operands"]: preferred = False if policy_report: @@ -193,13 +199,12 @@ def format_compatibilities_general(self, compat_object, indent='', policy_report #inner_output.append(f'{indent} allowed licenses: {", ".join([x["inbound_license"] for x in compat_object["policy_check"]["inbound_licenses"]])}') inner_output.append(res) if policy_report: - preferred_license = compat_object['policy_check']['inbound']['preferences']['license'] output.append(f'{indent}{operator} {self._format_compat_pref(compat_object["compatibility"], preferred_license)}') output.append(f'{indent} check: {compatibility_check}') output.append(f'{indent} outbound: {compat_object["outbound_license"]}') output.append(f'{indent} inbound: {compat_object["inbound_license"]}') output.append(f'{indent} comaptibility: {compat_object["compatibility"]}') - output.append(f'{indent} preferred: {preferred_license}') + output.append(f'{indent} preferred: {preferred_info}') output.append(f'{indent} details:') output += inner_output @@ -210,15 +215,18 @@ def format_compatibilities_general(self, compat_object, indent='', policy_report res = self.format_compatibilities_general(operand['compatibility_object'], indent=f'{indent} ', policy_report=policy_report, preferred_license=preferred_license) output.append(res) if compatibility_check == "outbound-expression -> inbound-expression": - print("APA") + #print("APA") operator = compat_object["operator"] compat = compat_object["compatibility"] + preferred_info = 'no' + if policy_report and preferred_license == compat_object['policy_check']['inbound']['preferences']['license']: + preferred_info = 'yes' output.append(f'{indent}{operator} {self._format_compat_pref(compat)}') output.append(f'{indent} check: {compatibility_check}') output.append(f'{indent} outbound: {compat_object["outbound_license"]}') output.append(f'{indent} inbound: {compat_object["inbound_license"]}') output.append(f'{indent} comaptibility: {compat_object["compatibility"]}') - output.append(f'{indent} preferred: TODO') + output.append(f'{indent} preferred: {preferred_info}') output.append(f'{indent} details:') for operand in compat_object['operands']: res = self.format_compatibilities_general(operand['compatibility_object'], indent=f'{indent} ', policy_report=policy_report, preferred_license=preferred_license) @@ -229,9 +237,9 @@ def format_compatibilities_general(self, compat_object, indent='', policy_report def format_compatibilities_object(self, compat_object): return self.format_compatibilities_general(compat_object, indent='') - def format_policy_report(self, report): + def format_policy_report(self, report, verbose=False): output = [] - preferred_inbound = '' + preferred_inbound = report['compatibility_report']['policy_check']['inbound']['preferences']['license'] print("keys: " + str(report["compatibility_report"].keys())) output.append(f'outbound: {report["outbound"]}') @@ -241,16 +249,18 @@ def format_policy_report(self, report): output.append(f'usecase: {report["usecase"]}') output.append(f'compatibility: {report["compatibility"]}') output.append(f'preferred inbound: {preferred_inbound}') + output.append(f'HESA: {report["compatibility_report"]["policy_check"]}') if report["meta"]["policy_type"] == 'default': policy_string = 'default' else: policy_string = report["meta"]["policy_file"] output.append(f'policy: {policy_string}') - output.append('report:') - output.append(self.format_compatibilities_general(report["compatibility_report"], indent=' ', policy_report=True, preferred_license=True)) + if verbose: + output.append('report:') + output.append(self.format_compatibilities_general(report["compatibility_report"], indent=' ', policy_report=True, preferred_license=preferred_inbound)) return "\n".join(output) - def format_compatibilities(self, compat): + def format_compatibilities(self, compat, verbose=False): output = [] output.append(f'outbound: {compat["outbound"]}') output.append(f'inbound: {compat["inbound"]}') @@ -258,8 +268,9 @@ def format_compatibilities(self, compat): output.append(f'provisioning: {compat["provisioning"]}') output.append(f'usecase: {compat["usecase"]}') output.append(f'compatibility: {compat["compatibility"]}') - output.append('report:') - output.append(self.format_compatibilities_general(compat["compatibility_report"], ' ', policy_report=False)) + if verbose: + output.append('report:') + output.append(self.format_compatibilities_general(compat["compatibility_report"], ' ', policy_report=False)) return "\n".join(output) From 1d6f179f927aa5cb8af65eccbcced0a78d5fc4fb Mon Sep 17 00:00:00 2001 From: Henrik Sandklef Date: Thu, 12 Feb 2026 10:36:18 +0100 Subject: [PATCH 24/41] test license policy --- tests/shell/test_policy.sh | 69 ++++++++++++++++++++++++++++++++++++++ 1 file changed, 69 insertions(+) create mode 100755 tests/shell/test_policy.sh diff --git a/tests/shell/test_policy.sh b/tests/shell/test_policy.sh new file mode 100755 index 0000000..70b97a6 --- /dev/null +++ b/tests/shell/test_policy.sh @@ -0,0 +1,69 @@ +#!/bin/bash + +# SPDX-FileCopyrightText: 2025 Henrik Sandklef +# +# SPDX-License-Identifier: GPL-3.0-or-later + +if [ "$1" == "--local" ] +then + export IMPLEMENTATIONS=../licomp:../licomp-dwheeler:../licomp-hermione:../licomp-osadl:../licomp-reclicense:../licomp-proprietary::../licomp-gnuguide:. + shift +fi +comment_file_presence() +{ + EXPECTED="$1" + REPORT=$2 + PRESENT=$(grep -c -e "$EXPECTED" $REPORT) + ACTUAL=$(grep -e "$EXPECTED" $REPORT) + MSG="$3" + if [ $PRESENT -ne 1 ] + then + echo "ERROR" + echo "Values differ" + echo " Expected: $EXPECTED" + #echo " Actual: $ACTUAL" + echo " Message: $MSG" + echo " Reproduce: grep -e \"$EXPECTED\" $REPORT" + exit 1 + fi + +} + + +licomp-toolkit-verify() +{ + INBOUND="$1" + OUTBOUND="$2" + + PYTHONPATH=$IMPLEMENTATIONS::. python3 licomp_toolkit/__main__.py $RESOURCE_ARGS verify -il "$INBOUND" -ol "$OUTBOUND" +} + +licomp-toolkit-apply() +{ + OUTPUT_ARGS="$1" + REPORT="$2" + + PYTHONPATH=$IMPLEMENTATIONS::. python3 licomp_toolkit/__main__.py $RESOURCE_ARGS $OUTPUT_ARGS apply-license-policy $REPORT +} + +# +# Text output +# +licomp-toolkit-verify MIT MIT > report.json +licomp-toolkit-apply " -of text" report.json > policy-report.txt +comment_file_presence "preferred inbound:[ ]*MIT" policy-report.txt "test 1.1" +comment_file_presence "preferred inbound: MIT" policy-report.txt "test 1.2" +comment_file_presence "preferred license: no" policy-report.txt "test 1.3" +comment_file_presence "least preferred license: no" policy-report.txt "test 1.4" +comment_file_presence "compatibility: yes" policy-report.txt "test 1.5" +comment_file_presence "compatibility details:" policy-report.txt "test 1.6" + +licomp-toolkit-verify MIT "MIT OR LGPL-2.1-only" > report.json +licomp-toolkit-apply " -of text" report.json > policy-report.txt +comment_file_presence "preferred inbound: MIT" policy-report.txt "test 2.1" +comment_file_presence "preferred inbound: MIT" policy-report.txt "test 2.2" +comment_file_presence "preferred license: no" policy-report.txt "test 2.3" +comment_file_presence "least preferred license: no" policy-report.txt "test 2.4" +comment_file_presence "compatibility: yes" policy-report.txt "test 2.5" +comment_file_presence "compatibility details:" policy-report.txt "test 2.6" + From aa7b74115de7ae77f9f0f13caea2a203380694c3 Mon Sep 17 00:00:00 2001 From: Henrik Sandklef Date: Mon, 16 Feb 2026 17:55:55 +0100 Subject: [PATCH 25/41] remove debug printouts, fix typos --- licomp_toolkit/license_policy.py | 414 ++++++++++--------------------- 1 file changed, 125 insertions(+), 289 deletions(-) diff --git a/licomp_toolkit/license_policy.py b/licomp_toolkit/license_policy.py index f7850d0..ede43c7 100644 --- a/licomp_toolkit/license_policy.py +++ b/licomp_toolkit/license_policy.py @@ -9,7 +9,6 @@ import logging from licomp_toolkit.toolkit import LicompToolkit -from licomp_toolkit.exception import LicompToolkitException from licomp.interface import UseCase from licomp.interface import Provisioning from licomp.interface import Modification @@ -40,7 +39,7 @@ def allowed(self): def avoided(self): return self.policy['avoided'] - + def denied(self): return self.policy['denied'] @@ -48,9 +47,7 @@ def meta(self): return self.policy_meta def list_presence(self, lic, ignore_missing=False): - print(f'list_presence({lic}, {ignore_missing}') - print(f'list_presence({type(lic)}, {ignore_missing}') - + if lic in self.allowed(): return 1, self.allowed().index(lic) if lic in self.avoided(): @@ -60,13 +57,13 @@ def list_presence(self, lic, ignore_missing=False): if ignore_missing: return None, None - raise LicensePolicyException(f'License "{lic}" is not present in any of the policy\'s lists.') + raise LicensePolicyException(f'License "{lic}" is not present in any of the policy\'s lists. Licenses: {self.allowed()}, {self.avoided()} ') def list_nr_to_name(self, nr): return { 1: 'allowed', 2: 'avoided', - 3: 'denied'}.get(nr, -1) + 3: 'denied'}.get(nr, '') def list_name_to_nr(self, nr): return { @@ -82,73 +79,46 @@ def compare_preferences_expressions(self, lic1, lic2, key, ignore_missing=False) def compare_preferences_general(self, lic1, lic2, key=None, ignore_missing=False): """ - + returns * negative if lic1 is more preferred than lic2 * None if both licenses are denied raises * LicensePolicyException if at least one license is not listed (if ignore_missing, then both licenses need to be not listed to raise exception) """ - print(f'compare_preferences_general({lic1}, {lic2}, {key}, {ignore_missing})') - logging.debug(f'compare_preferences({lic1}, {lic2}, {key}, {ignore_missing})') - print("SCHNEEBLY1 1 " + str(lic1)) - print("SCHNEEBLY1 2 " + str(lic2)) + logging.debug(f'compare_preferences {lic1}, {lic2}, {key}, {ignore_missing}') if key: list_key = key.replace('_license', '') - print("SCHNEEBLY 1.0") - print("SCHNEEBLY KEY: " + key) - print("SCHNEEBLY KEY key: " + list_key) - print("SCHNEEBLY KEY lic: " + str(lic1)) - print("SCHNEEBLY KEY keys: " + str(lic1.keys())) - print("SCHNEEBLY TYPE : " + str(lic1[list_key]['type'])) - print("SCHNEEBLY KEY sub: " + str(lic1[list_key])) - print("SCHNEEBLY KEY sub: " + str(lic1[list_key].keys())) + lic1_name = lic1[list_key]['license'] + lic2_name = lic2[list_key]['license'] if lic1[list_key]['type'] == 'license': - print("SCHNEEBLY 1.1") - lic1_list, lic1_index = self.list_presence(lic1[list_key]['license'], ignore_missing) - print("SCHNEEBLY 1.1 " + str(lic1_list)) - print("SCHNEEBLY 1.1 " + str(lic1_index)) + lic1_list, lic1_index = self.list_presence(lic1[list_key]['license'], ignore_missing=False) else: - print("SCHNEEBLY 1.2 type " + str(lic1[list_key]['type'])) - print("SCHNEEBLY 1.2 " + str(list_key)) - print("SCHNEEBLY 1.2 " + str(lic1[list_key])) - print("SCHNEEBLY 1.2 keys " + str(lic1[list_key].keys())) - print("SCHNEEBLY 1.2 pref " + str(lic1[list_key]['preferences'])) - print("SCHNEEBLY 1.2 lic1 " + str(lic1)) - - lic1_list = self.list_name_to_nr(lic1[list_key]['license']) - print("lic1: " + str(lic1)) - print("lic1: " + str(list_key)) - print("lic1: " + str(lic1[list_key])) + lic1_list_name = lic1[list_key]['preferences']['license_list'] + lic1_list = self.list_name_to_nr(lic1_list_name) lic1_index = lic1[list_key]['preferences']['license_index'] - + if lic2[list_key]['type'] == 'license': - print("SCHNEEBLY 2.1") - lic2_list, lic2_index = self.list_presence(lic2[list_key]['license'], ignore_missing) + lic2_list, lic2_index = self.list_presence(lic2[list_key]['license'], ignore_missing=False) else: # license-expression - print("SCHNEEBLY 2.1 lic: " + str(lic2)) - print("SCHNEEBLY 2.1 lic: " + str(list_key)) - print("SCHNEEBLY 2.1 lic_key: " + str(lic2[list_key])) - print("SCHNEEBLY 2.1 pref: " + str(lic2[list_key]['preferences'])) - print("SCHNEEBLY 2.1 lic: " + str(lic2[list_key]['license'])) - - lic2_list = lic2[list_key]['preferences']['license_list'] + lic2_list_name = lic2[list_key]['preferences']['license_list'] + lic2_list = self.list_name_to_nr(lic2_list_name) lic2_index = lic2[list_key]['preferences']['license_index'] else: - print("SCHNEEBLY 2.2 .... should list_key be selected as above???? " + str(lic1)) - lic1_list, lic1_index = self.list_presence(lic1, ignore_missing) - lic2_list, lic2_index = self.list_presence(lic2, ignore_missing) - - print(f'compare_preferences: "{lic1_list}", "{lic1_index}" "{lic2_list}", "{lic2_index}"') + lic1_name = lic1 + lic2_name = lic2 + lic1_list, lic1_index = self.list_presence(lic1, ignore_missing=ignore_missing) + lic2_list, lic2_index = self.list_presence(lic2, ignore_missing=ignore_missing) + logging.debug(f'compare_preferences: "{lic1_list}", "{lic1_index}" "{lic2_list}", "{lic2_index}"') if (not lic1_list) and (not lic2_list): if ignore_missing: - logging.debug(f'compare_preferences({lic1[list_key]["license"]}, {lic2[list_key]["license"]}, {ignore_missing}): ignore since both None') + logging.debug(f'compare_preferences({lic1_name}, {lic2_name}, {ignore_missing}): ignore since both None') return None - - raise LicensePolicyException(f'License "{lic1[list_key]["license"]}" and/or {lic2[list_key]["license"]} is not present in any of the policy\'s lists.') - + + raise LicensePolicyException(f'License "{lic1_name}" and/or "{lic2_name}" is not present in any of the policy\'s lists.') + if lic1_list == lic2_list: # if both are denied, return None to indicate "error" if lic1_list == 3: @@ -159,29 +129,29 @@ def compare_preferences_general(self, lic1, lic2, key=None, ignore_missing=False return 1 if not lic2_list: return -1 + return lic1_list - lic2_list def most_preferred(self, lic1, lic2, ignore_missing=False): pref = self.compare_preferences(lic1, lic2, ignore_missing) - if pref == None: + if pref is None: return None if pref < 0: return lic1 return lic2 - + def OBSOLETE_preferred_score_ignore_missing(self, lic1, lic2): pref = self.compare_preferences(lic1, lic2, ignore_missing=True) - if pref == None: + if pref is None: return 10000 if pref < 0: return -1 if lic1 == lic2: return 0 return 1 - + def preferred_score_licenses(self, lic1, lic2, key): - logging.debug(f'preferred_score_licenses({lic1}, {lic2}, {key})') - print(f'preferred_score_licenses({lic1}, {lic2}, {key})') + logging.debug(f'preferred_score_licenses {lic1}, {lic2}, {key}') pref = self.compare_preferences_expressions(lic1, lic2, key) if pref < 0: return -1 @@ -190,20 +160,19 @@ def preferred_score_licenses(self, lic1, lic2, key): return 1 def preferred_score_inbounds(self, lic1, lic2): - logging.debug(f'preferred_score_inbounds({lic1}, {lic2})') - print(f'preferred_score_inbounds({lic1}, {lic2})') + logging.debug(f'preferred_score_inbounds {lic1}, {lic2}') return self.preferred_score_licenses(lic1, lic2, 'inbound_license') - + def preferred_score_outbounds(self, lic1, lic2): - logging.debug(f'preferred_score_outbounds({lic1}, {lic2})') + logging.debug(f'preferred_score_outbounds {lic1}, {lic2}') return self.preferred_score_licenses(lic1, lic2, 'outbound_license') - + def least_preferred(self, lic1, lic2, ignore_missing=False): most_preferred = self.most_preferred(lic1, lic2, ignore_missing) if most_preferred == lic1: return lic2 return lic1 - + class DefaultLicensePolicy(LicensePolicy): def __init__(self, resources, usecase, provisioning): @@ -214,7 +183,7 @@ def __init__(self, resources, usecase, provisioning): self.policy = { 'allowed': license_order, 'avoided': [], - 'denied': [] + 'denied': [], } def __order(self, resources, usecase, provisioning): @@ -231,33 +200,34 @@ def __order(self, resources, usecase, provisioning): Modification.UNMODIFIED) if compat['compatibility_status'] == 'yes': scores[in_license] += 1 - + scores_dict = OrderedDict(sorted(scores.items(), key=lambda x: x[1], reverse=True)) - return [x for (x,y) in scores_dict.items()] + return [x for (x, y) in scores_dict.items()] def __licenses(self, resources, usecase, provisioning): self.resources = [] - self.licenses = [] + self.licenses = [] - logging.debug(f'__licenses({resources}, {usecase}, {provisioning})') + logging.debug(f'__licenses {resources}, {usecase}, {provisioning}') for resource in resources: for licomp_resource in self.lt.licomp_resources(): if resource == licomp_resource: self.licenses += self.lt.licomp_resources()[licomp_resource].supported_licenses() self.resources.append(self.lt.licomp_resources()[licomp_resource]) - logging.debug(f'__licenses({resources}, {usecase}, {provisioning}) ==> {self.licenses}') + logging.debug(f'__licenses {resources}, {usecase}, {provisioning} ==> {self.licenses}') + class LicensePolicyHandler: - def __init__(self, policy_file=None): + def __init__(self, policy_file=None, resources=None, usecase=None, provisioning=None): logging.debug("LicensePolicyHandler()") if policy_file: self.policy = LicensePolicy(policy_file) self.policy_type = 'policy_file' self.policy_file = policy_file else: - self.policy = None - self.policy_type = None + self.policy = DefaultLicensePolicy(resources, usecase, provisioning) + self.policy_type = 'default' self.policy_file = None def __is_license_expression(self, lic): @@ -265,39 +235,27 @@ def __is_license_expression(self, lic): CONTAINS_OR = 'OR' in lic return CONTAINS_AND or CONTAINS_OR - - def usable_license(self, lic): - print("FALCAO1 usable_license keys: " + str(lic)) - print("FALCAO usable_license keys: " + str(lic['inbound']) + " " + str(lic.keys())) - # TODO: fix inbound to key - license_name = lic['inbound']['license'] + def usable_license(self, lic, key): + if key in lic: + license_name = lic[key]['license'] + else: + license_name = lic if self.__is_license_expression(license_name): # should have been checked already, so skip policy_ok = True else: - - print(f'FALCAO +++ -2 usable_license {self.policy}') - print(f'FALCAO +++ -1 usable_license {license_name}') - print(f'FALCAO +++ 0 usable_license {self.policy.allowed()}') - print(f'FALCAO +++ 0 usable_license {self.policy.avoided()}') - print(f'FALCAO +++ 1 usable_license {license_name in self.policy.allowed()}') - print(f'FALCAO +++ 2 usable_license {license_name in self.policy.avoided()}') - print(f'FALCAO +++ 3 usable_license {license_name in (self.policy.allowed() + self.policy.avoided())}') policy_ok = license_name in (self.policy.allowed() + self.policy.avoided()) compat_ok = lic['compatibility'] == 'yes' ret = policy_ok and compat_ok - logging.debug(f'usable_license({lic}) | policy_ok: {policy_ok}, compat_ok: {compat_ok} ===> {ret}') - print(f'FALCAO +++ usable_license({lic}) | policy_ok: {policy_ok}, compat_ok: {compat_ok} ===> {ret}') + logging.debug(f'usable_license {lic} | policy_ok: {policy_ok}, compat_ok: {compat_ok} ===> {ret}') return ret - + def unusable_licenses(self, licenses, key): - print("UG " + str(licenses)) problematic_licenses = [] for lic in licenses: - print("lic: " + str(lic.keys())) - license_name = lic[key]['license'] + license_name = lic[key]['license'] if self.__is_license_expression(license_name): # should have been checked already, so skip policy_ok = True @@ -306,43 +264,44 @@ def unusable_licenses(self, licenses, key): compat_ok = lic['compatibility'] == 'yes' ret = policy_ok and compat_ok - logging.debug(f'usable_license({lic}) | policy_ok: {policy_ok}, compat_ok: {compat_ok} ===> {ret}') + logging.debug(f'unusable_license {lic} | policy_ok: {policy_ok}, compat_ok: {compat_ok} ===> {ret}') if not ret: problematic_licenses.append(lic) return len(problematic_licenses) > 0 - + def scored_general(self, licenses, operator, key): - print(f'scored_general({licenses}, {operator}, {key})') compare_function = { 'inbound': self.policy.preferred_score_inbounds, 'outbound': self.policy.preferred_score_outbounds, }[key] - print("sgo: func= " + str(compare_function)) if operator == "OR": - print("FALCAO sg: 9: " + str(licenses)) - logging.debug(f'preferred_licenses({licenses}, {operator})') - filtered_licenses = [x for x in licenses if self.usable_license(x)] - print("FALCAO sg: 9.1: " + str(filtered_licenses)) + logging.debug(f'preferred_licenses {licenses}, {operator}') + filtered_licenses = [] + unusable_licenses = [] + for lic in licenses: + if self.usable_license(lic, key): + filtered_licenses.append(lic) + else: + print("UNUSABLE: " + str(key)) + print("UNUSABLE: " + str(lic)) + unusable_licenses.append(lic[key]) sorted_licenses = sorted(filtered_licenses, key=cmp_to_key(compare_function)) - print("FALCAO sg: 9.2: " + str(sorted_licenses)) - logging.debug(f'sorted_licenses: {[x[key]['license'] for x in sorted_licenses]})') - return sorted_licenses + return sorted_licenses, unusable_licenses elif operator == "AND": - print("FALCAO sg: 10") - if self.unusable_licenses(licenses, 'inbound'): - print("FALCAO sg: 10.1") + if self.unusable_licenses(licenses, key): sorted_licenses = [] + unusable_licenses = [x[key] for x in licenses] else: sorted_licenses = sorted(licenses, key=cmp_to_key(compare_function)) - - return sorted_licenses + unusable_licenses = [] + + return sorted_licenses, unusable_licenses else: - raise Exception("TODO fix this") - + raise Exception(f'scored_general got a bad operator "{operator}"') + def scored_inbounds(self, inbounds, operator): - print(f'scored_inbounds({inbounds}, {operator})') return self.scored_general(inbounds, operator, 'inbound') - + def scored_outbounds(self, outbounds, operator): return self.scored_general(outbounds, operator, 'outbound') @@ -352,22 +311,19 @@ def __check_policy_check(self, reply): 'type', 'preferences:license', 'preferences:license_list', - 'preferences:license_index' + 'preferences:license_index', ] - print("reply: " + json.dumps(reply, indent=4)) - print("reply keys: " + str(reply.keys())) for key in keys: in_value = reply['inbound'] out_value = reply['outbound'] for sub_key in key.split(':'): - assert sub_key in in_value - print(str(" sub_key " + sub_key)) - assert sub_key in out_value + assert sub_key in in_value # noqa: S101 + assert sub_key in out_value # noqa: S101 in_value = in_value[sub_key] out_value = out_value[sub_key] - assert 'compatibility' in reply - assert 'unusable' in reply + assert 'compatibility' in reply # noqa: S101 + assert 'unusable' in reply # noqa: S101 def __pack_policy_check(self, outbound_pref, inbound_pref, compat, unusable): return { @@ -378,15 +334,13 @@ def __pack_policy_check(self, outbound_pref, inbound_pref, compat, unusable): } def __pack_policy_unusable(self, unusables): - return { - 'unusable': unusables - } + return unusables def __pack_policy_preferences(self, license_string, license_list, license_index): return { - 'license': license_tring, + 'license': license_string, 'license_list': license_list, - 'license_index': license_index + 'license_index': license_index, } def __pack_policy_check_license(self, license_name, license_type, pref_lic, pref_lic_list, pref_lic_idx): @@ -396,10 +350,10 @@ def __pack_policy_check_license(self, license_name, license_type, pref_lic, pref 'preferences': { 'license': pref_lic, 'license_list': pref_lic_list, - 'license_index': pref_lic_idx - } + 'license_index': pref_lic_idx, + }, } - + def __apply_to_compat_object(self, compat_object, ignore_missing=False, indent=0): """ Specification of the policy_check format @@ -413,7 +367,7 @@ def __apply_to_compat_object(self, compat_object, ignore_missing=False, indent=0 license_list license_index } - + unusable [ { license (str), reason (str) @@ -423,41 +377,27 @@ def __apply_to_compat_object(self, compat_object, ignore_missing=False, indent=0 inbound (license_data) outbound (license_data) compatibility (str) - + """ - print("ATC") + unusable = [] if 'outbound-expression' in compat_object['compatibility_check']: operator = compat_object['operator'] outbounds = [] - print("LFC: " + str(compat_object.keys())) for operand in compat_object['operands']: operand['compatibility_object']['TEST'] = "Liverpool" - print("sgo: operand: BEGIN ") - print("sgo: operand: BEGIN " + str(operand['compatibility_object']['outbound_license']) + " " + str(operand['compatibility_object'].keys())) - self.__apply_to_compat_object(operand['compatibility_object'], indent+4) + self.__apply_to_compat_object(operand['compatibility_object'], indent + 4) self.__check_policy_check(operand['compatibility_object']['policy_check']) - - outbounds.append(operand['compatibility_object']['policy_check']) - POL=operand['compatibility_object']['policy_check'] - print("sgo: operand: WATCH " + str(operand['compatibility_object']['policy_check']['outbound']) + " " + str(operand['compatibility_object']['policy_check'].keys())) - print("sgo: operand: KEEP " + str(POL.keys())) - #print("sgo: operand: KEEP " + str(POL['inbound_list']) + " " + str(POL['inbound_list_index']) + " " + str(POL['preferred_inbound_license'])) - print("sgo: operand: END " + str(operand['compatibility_object']['policy_check']['outbound']) + " " + str(operand['compatibility_object']['policy_check'].keys())) - scored_outbounds = self.scored_outbounds(outbounds, operator) - print("sgo: operands: HERE: " + str(outbounds[0])) - print("sgo: operands: HERE: " + str(outbounds[0].keys())) + outbounds.append(operand['compatibility_object']['policy_check']) + scored_outbounds, unusable = self.scored_outbounds(outbounds, operator) outbound_list = [] outbound_list_index = -1 preferred_inbound_license = None compatibility = 'no' - print("EDER: " + operator) - print("EDER: " + str(len(scored_outbounds))) if len(scored_outbounds) > 0: - print("EDER: OUTBONDS") if operator == 'AND': outbound_name = ' AND '.join([x['outbound']['preferences']['license'] for x in scored_outbounds]) outbound_pref = scored_outbounds[-1] @@ -465,31 +405,24 @@ def __apply_to_compat_object(self, compat_object, ignore_missing=False, indent=0 outbound_name = scored_outbounds[0]['outbound']['preferences']['license'] outbound_pref = scored_outbounds[0] - print("0 --------------- " + str(len(scored_outbounds))) - print("2 --------------- " + str(scored_outbounds[0])) - print("2 --------------- " + str(scored_outbounds[1])) outbound_list = outbound_pref['outbound']['preferences']['license_list'] outbound_list_index = outbound_pref['outbound']['preferences']['license_index'] inbound_prefs = outbound_pref['inbound']['preferences'] - print("inbound_prefs: " + str(inbound_prefs)) preferred_inbound_license = inbound_prefs['license'] preferred_inbound_license_list = inbound_prefs['license_list'] preferred_inbound_license_index = inbound_prefs['license_index'] compatibility = 'yes' - else: - print("EDER: NO outbonds") outbound_name = None outbound_list = None outbound_list_index = None - + preferred_inbound_license = None preferred_inbound_license_list = None preferred_inbound_license_index = None compatibility = 'no' - if 'inbound-expression' in compat_object['compatibility_check']: inbound_license_type = 'license-expression' else: @@ -497,53 +430,27 @@ def __apply_to_compat_object(self, compat_object, ignore_missing=False, indent=0 policy_check_inbound_license = self.__pack_policy_check_license( compat_object['inbound_license'], - inbound_license_type, + inbound_license_type, preferred_inbound_license, preferred_inbound_license_list, - preferred_inbound_license_index + preferred_inbound_license_index, ) policy_check_outbound_license = self.__pack_policy_check_license( compat_object['outbound_license'], - 'license-expression', + 'license-expression', outbound_name, outbound_list, - outbound_list_index + outbound_list_index, ) - # TODO: add identify and pass on unusable - unusable = self.__pack_policy_unusable([]) + unusable = self.__pack_policy_unusable(unusable) policy_license_object = self.__pack_policy_check( policy_check_outbound_license, policy_check_inbound_license, compatibility, - unusable) - import json,sys - print("OP" + json.dumps(policy_license_object, indent=4)) - - compat_object['policy_check'] = policy_license_object - + unusable, + ) - - #compat_object['policy_check_OLD'] = { - dummy = { - 'outbound_license': compat_object['outbound_license'], - 'outbound_license_type': 'license-expression', - 'outbound_licenses': scored_outbounds, - 'outbound_list': outbound_list, - 'outbound_list_index': outbound_list_index, - - 'inbound_license': compat_object['inbound_license'], - 'inbound_license_type': inbound_license_type, - 'compatibility': compatibility, -# 'inbound_licenses': None, - 'inbound_list': None, - 'inbound_list_index': None, - 'preferred_outbound_license': outbound_name, - 'preferred_outbound_license_list': outbound_list, - 'preferred_outbound_license_index': outbound_list_index, - 'preferred_inbound_license': preferred_inbound_license, - 'preferred_inbound_license_list': preferred_inbound_license_list, - 'preferred_inbound_license_index': preferred_inbound_license_index, - } + compat_object['policy_check'] = policy_license_object elif 'outbound-license' in compat_object['compatibility_check']: @@ -551,172 +458,101 @@ def __apply_to_compat_object(self, compat_object, ignore_missing=False, indent=0 inner_compat_object = compat_object operator = inner_compat_object['operator'] inbounds = [] - print("ZICO inbound: " + compat_object['inbound_license']) - print("ZICO operator: " + operator) for operand in inner_compat_object['operands']: - self.__apply_to_compat_object(operand['compatibility_object'], indent+4) - import json, sys - print(json.dumps(operand['compatibility_object']['policy_check'], indent=4)) - print(" FALCAO operand " + str(operand['compatibility_object']['policy_check'])) - #assert False - #sys.exit(1) + self.__apply_to_compat_object(operand['compatibility_object'], indent + 4) self.__check_policy_check(operand['compatibility_object']['policy_check']) inbounds.append(operand['compatibility_object']['policy_check']) - print("ZICO operand: " + operand['compatibility_object']['inbound_license']) - import json, sys - print("FALCAO operands " + str(inbounds)) - scored_inbounds = self.scored_inbounds(inbounds, operator) - print("FALCAO scored inbounds " + str(scored_inbounds)) - inbound_list = [] + scored_inbounds, unusable = self.scored_inbounds(inbounds, operator) + inbound_list = '' inbound_list_index = -1 preferred_inbound_license = None - print("FALCAO inner: " + str(inbounds)) - print("FALCAO scored: " + str(scored_inbounds)) - print("FALCAO oper: " + str(operator)) -# sys.exit(1) if len(scored_inbounds) > 0: - print("REICH: " + str(scored_inbounds[0])) if operator == 'AND': - # use last (least preferred) license + # use last (least preferred) license inbound_list = scored_inbounds[-1]['inbound']['preferences']['license_list'] inbound_list_index = scored_inbounds[-1]['inbound']['preferences']['license_index'] preferred_inbound_license = ' AND '.join([x['inbound']['preferences']['license'] for x in scored_inbounds]) else: - # use first (most preferred) license + # use first (most preferred) license inbound_list = scored_inbounds[0]['inbound']['preferences']['license_list'] inbound_list_index = scored_inbounds[0]['inbound']['preferences']['license_index'] preferred_inbound_license = scored_inbounds[0]['inbound']['preferences']['license'] - print("ZICO: " + str(compat_object['compatibility_check']) + ": " + str(len(scored_inbounds))) - print("ZICO: " + str(compat_object['compatibility_check']) + ": " + str(preferred_inbound_license)) - print("ZICO: " + str(compat_object['compatibility_check']) + ": " + json.dumps(scored_inbounds, indent=4)) - print("ZICO: " + str(compat_object['compatibility_check']) + ": " + operator) - print("ZICO: " + str(compat_object['compatibility_check']) + ": " + str(preferred_inbound_license)) -# sys.exit(1) - if len(scored_inbounds) == 1: - #sys.exit(1) - pass out_lic = inner_compat_object["outbound_license"] - out_list_nr, out_index = self.policy.list_presence(out_lic, ignore_missing=ignore_missing) + out_list_nr, out_index = self.policy.list_presence(out_lic, ignore_missing=True) out_list_name = self.policy.list_nr_to_name(out_list_nr) - - print("FALCAO inner: " + json.dumps(preferred_inbound_license, indent=4)) policy_check_inbound_license = self.__pack_policy_check_license( inner_compat_object['inbound_license'], - 'license-expression', + 'license-expression', preferred_inbound_license, inbound_list, - inbound_list_index + inbound_list_index, ) policy_check_outbound_license = self.__pack_policy_check_license( out_lic, - 'license', + 'license', out_lic, out_list_name, - out_index + out_index, ) - # TODO: add identify and pass on unusable - unusable = self.__pack_policy_unusable([]) + packed_unusable = self.__pack_policy_unusable(unusable) policy_license_object = self.__pack_policy_check( policy_check_outbound_license, policy_check_inbound_license, compat_object['compatibility'], - unusable) + packed_unusable) inner_compat_object['policy_check'] = policy_license_object - - print("FALCAO: " + json.dumps(inner_compat_object['policy_check'], indent=4)) - print("FALCAO done") -# sys.exit(1) - - - -# inner_compat_object['policy_check_TMP'] = { - dummy = { - 'inbound_license': inner_compat_object['inbound_license'], - 'outbound_license': inner_compat_object['outbound_license'], - 'inbound_license_type': 'license-expression', - 'outbound_license_type': 'license', - 'compatibility': compat_object['compatibility'], -# 'inbound_licenses': scored_inbounds, - 'inbound_list': inbound_list, - 'inbound_list_index': inbound_list_index, - 'outbound_list': out_list_name, - 'outbound_list_index': out_index, - 'preferred_inbound_license': preferred_inbound_license, - 'preferred_outbound_license': inner_compat_object['outbound_license'], - } + if 'inbound-license' in compat_object['compatibility_check']: - #pref = self.policy.most_preferred(compat_object['outbound_license'], compat_object['inbound_license'], ignore_missing=True) - out_lic = compat_object["outbound_license"] - out_list_nr, out_index = self.policy.list_presence(out_lic, ignore_missing=ignore_missing) - out_list_name = self.policy.list_nr_to_name(out_list_nr) - in_lic = compat_object['inbound_license'] - in_list_nr, in_list_index = self.policy.list_presence(in_lic, ignore_missing=ignore_missing) + in_list_nr, in_list_index = self.policy.list_presence(in_lic, ignore_missing=True) in_list_name = self.policy.list_nr_to_name(in_list_nr) + out_lic = compat_object["outbound_license"] - out_list_nr, out_index = self.policy.list_presence(out_lic, ignore_missing=ignore_missing) + out_list_nr, out_index = self.policy.list_presence(out_lic, ignore_missing=True) out_list_name = self.policy.list_nr_to_name(out_list_nr) - # TODO: fill in the below policy_check_inbound_license = self.__pack_policy_check_license( in_lic, - 'license', + 'license', in_lic, in_list_name, - in_list_index + in_list_index, ) policy_check_outbound_license = self.__pack_policy_check_license( out_lic, - 'license', + 'license', out_lic, out_list_name, out_index, ) - # TODO: add identify and pass on unusable - unusable = self.__pack_policy_unusable([]) + packed_unusable = self.__pack_policy_unusable(unusable) policy_license_object = self.__pack_policy_check( policy_check_outbound_license, policy_check_inbound_license, compat_object['compatibility'], - unusable) + packed_unusable, + ) compat_object['policy_check'] = policy_license_object - # TODO: REMOVE - if False: - compat_object['policy_check_TMP'] = { - 'inbound_license': in_lic, - 'outbound_license': out_lic, - 'inbound_license_type': 'license', - 'outbound_license_type': 'license', - 'compatibility': compat_object['compatibility'], - 'inbound_list': list_name, - 'inbound_list_index': index, - 'outbound_list': out_list_name, - 'outbound_list_index': out_index, - 'preferred_inbound_license': lic, - "HESA": "LIVEROPOPOL", - } - print("sgo: operand outbound license: " + str(compat_object.keys())) - #print("sgo: operand: ---> " + str(compat_object['policy_check']['outbound_license']) + " <--- " + str(compat_object['policy_check']['outbound_list'] + " keys: " + str(compat_object['policy_check'].keys()))) - + else: raise Exception("We should not be here") return None def apply_policy(self, compat_report, ignore_missing=False): if not self.policy: - logging.debug(f'apply_policy, no policy. Creating default one') + logging.debug('apply_policy, no policy. Creating default one') resources = compat_report['resources'] usecase = compat_report['usecase'] provisioning = compat_report['provisioning'] self.policy = DefaultLicensePolicy(resources, usecase, provisioning) self.policy_type = 'default' self.policy_file = None - + top_object = compat_report['compatibility_report'] logging.debug("apply_policy") self.__apply_to_compat_object(top_object, From 756a1051ee094cb143f3bb3656ab01ae2dade529 Mon Sep 17 00:00:00 2001 From: Henrik Sandklef Date: Mon, 16 Feb 2026 17:56:39 +0100 Subject: [PATCH 26/41] add --debug option, use verbose for verbose oputput --- licomp_toolkit/__main__.py | 48 ++++++++++++++++++++++++++------------ 1 file changed, 33 insertions(+), 15 deletions(-) diff --git a/licomp_toolkit/__main__.py b/licomp_toolkit/__main__.py index d15b62f..68317e8 100755 --- a/licomp_toolkit/__main__.py +++ b/licomp_toolkit/__main__.py @@ -11,7 +11,6 @@ from licomp.interface import LicompException -from licomp_toolkit.exception import LicompToolkitException from licomp_toolkit.return_codes import LicompToolkitReturnCodes from licomp_toolkit.toolkit import LicompToolkit from licomp_toolkit.toolkit import ExpressionExpressionChecker @@ -25,6 +24,7 @@ from licomp_toolkit.suggester import OutboundSuggester from licomp_toolkit.display_compatibility import DisplayCompatibility from licomp_toolkit.utils import resources_to_use +from licomp_toolkit.utils import default_resources from licomp_toolkit.license_policy import LicensePolicyHandler from licomp.main_base import LicompParser @@ -63,26 +63,34 @@ def _read_report_file(self, report_file): err_code = LicompToolkitReturnCodes.LICOMP_TOOLKIT_INVALID_FILE.value return None, err_code, err_msg return report, ReturnCodes.LICOMP_OK.value, None - except (FileNotFoundError, JSONDecodeError) as e: - err_msg = f'File "{args.report_file}" not found or not in JSON format' + except (FileNotFoundError, JSONDecodeError): + err_msg = f'File "{report_file}" not found or not in JSON format' err_code = LicompToolkitReturnCodes.LICOMP_TOOLKIT_INVALID_FILE.value return None, err_code, err_msg - except (KeyError) as e: + except (KeyError): err_msg = f'File "{report_file}" not in Licomp Toolkit\'s license policy format.' err_code = LicompToolkitReturnCodes.LICOMP_TOOLKIT_INVALID_FILE.value return None, err_code, err_msg - + def apply_license_policy(self, args): report, err_code, err_msg = self._read_report_file(args.report_file) if err_code != ReturnCodes.LICOMP_OK.value: return None, err_code, err_msg - lph = LicensePolicyHandler(policy_file=args.license_policy_file) + if args.resources: + logging.warning(f'User specified resources are ignored. Using the resources as specified in the report file ("{args.report_file}").') + resources = report['resources'] + usecase = report['usecase'] + provisioning = report['provisioning'] + lph = LicensePolicyHandler(policy_file=args.license_policy_file, + resources=resources, + usecase=usecase, + provisioning=provisioning) policy_report = lph.apply_policy(report, ignore_missing=True) - ret_code = compatibility_status_to_returncode(report['compatibility']) + ret_code = compatibility_status_to_returncode(policy_report['compatibility']) formatter = LicompToolkitFormatter.formatter(self.args.output_format) formatted_report = formatter.format_policy_report(report, verbose=args.verbose) return formatted_report, ret_code, False - + def verify(self, args): formatter = LicompToolkitFormatter.formatter(self.args.output_format) try: @@ -107,7 +115,12 @@ def verify(self, args): if args.license_policy_file: lph = LicensePolicyHandler(policy_file=args.license_policy_file) else: - lph = LicensePolicyHandler() + resources, unsupported = resources_to_use(args) + if unsupported: + return f'Resource(s) {", ".join(unsupported)} is/are not supported', ReturnCodes.LICOMP_UNSUPPORTED_RESOURCE.value, True + lph = LicensePolicyHandler(resources=resources, + usecase=args.usecase, + provisioning=args.provisioning) policy_report = lph.apply_policy(compatibilities) return formatter.format_policy_report(policy_report, verbose=args.verbose), ret_code, False else: @@ -173,11 +186,14 @@ def outbound_candidate(self, args): if args.all_licenses: licenses_to_check = self.licomp_toolkit.supported_licenses() + resources, unsupported = resources_to_use(args) + if unsupported: + return f'Resource(s) {", ".join(unsupported)} is/are not supported', ReturnCodes.LICOMP_UNSUPPORTED_RESOURCE.value, True candidates = suggester.compat_licenses(args.license_expression, args.usecase, args.provisioning, licenses_to_check, - args.resources) + resources) if args.least_compatible: candidates.reverse() @@ -187,10 +203,13 @@ def outbound_candidate(self, args): def display_compatibility(self, args): display_compat = DisplayCompatibility(self.licomp_toolkit) + resources, unsupported = resources_to_use(args) + if unsupported: + return f'Resource(s) {", ".join(unsupported)} is/are not supported', ReturnCodes.LICOMP_UNSUPPORTED_RESOURCE.value, True compats = display_compat.display_compatibility(args.licenses, UseCase.string_to_usecase(args.usecase), Provisioning.string_to_provisioning(args.provisioning), - args.resources) + resources) formatter = LicompToolkitFormatter.formatter(args.output_format) formatted = formatter.format_display_compatibilities(compats, {'discard_unsupported': args.discard_unsupported_licenses}) @@ -219,8 +238,8 @@ def main(): parser.add_argument('-r', '--resources', type=str, action='append', - help='use specified licomp resource. For a list use the commands \'supported-resources\'. Use \'all\' to use all', - default=['licomp_reclicense', 'licomp_osadl']) + help=f'use specified licomp resource. For a list use the commands \'supported-resources\'. Use \'all\' to use all. Default: {", ".join(default_resources())}', + default=[]) parser.add_argument('-nv', '--no-verbose', action='store_true', @@ -230,8 +249,7 @@ def main(): parser_v = subparsers.choices['verify'] parser_v.add_argument("--apply-license-policy", action='store_true', help='Apply license policy', default=False) parser_v.add_argument("--license-policy-file", type=str, help='License policy file. Defaults to use default license policy.', default=None) - - + # Commands parser_si = subparsers.add_parser('simplify', help='Normalize and simplify a license expression') parser_si.set_defaults(which="simplify", func=lct_parser.simplify) From 5c32a540e46b8354d0ab28b64caa6230fdd7d5a2 Mon Sep 17 00:00:00 2001 From: Henrik Sandklef Date: Mon, 16 Feb 2026 17:56:58 +0100 Subject: [PATCH 27/41] clean up --- licomp_toolkit/format.py | 87 ++++++++++++++-------------------------- 1 file changed, 31 insertions(+), 56 deletions(-) diff --git a/licomp_toolkit/format.py b/licomp_toolkit/format.py index 96c85f2..b58b5b8 100644 --- a/licomp_toolkit/format.py +++ b/licomp_toolkit/format.py @@ -67,7 +67,7 @@ def format_licomp_licenses(self, licomp_licenses): def format_licomp_versions(self, licomp_versions): return json.dumps(licomp_versions, indent=4) - def format_display_compatibilities(self, compats, settings={}): + def format_display_compatibilities(self, compats, settings={}): # noqa: B006 # settings # discard_unsupported: True - will remove unsupported licenses from the output display_compats = self._pre_format_display_compatibilities(compats) @@ -75,14 +75,14 @@ def format_display_compatibilities(self, compats, settings={}): def format_policy_report(self, report, verbose=False): return json.dumps(report, indent=4) - + class YamlLicompToolkitFormatter(LicompToolkitFormatter): def format_compatibilities(self, compat, verbose=False): return yaml.safe_dump(compat, indent=4) def format_policy_report(self, report, verbose=False): - return yaml.safe_dump(compat, indent=4) + return yaml.safe_dump(report, indent=4) def format_licomp_resources(self, licomp_resources): return yaml.safe_dump(licomp_resources, indent=4) @@ -93,7 +93,7 @@ def format_licomp_licenses(self, licomp_licenses): def format_licomp_versions(self, licomp_versions): return yaml.safe_dump(licomp_versions, indent=4) - def format_display_compatibilities(self, compats, settings={}): + def format_display_compatibilities(self, compats, settings={}): # noqa: B006 display_compats = self._pre_format_display_compatibilities(compats) return yaml.safe_dump(display_compats, indent=4) @@ -147,8 +147,7 @@ def __statuses(self, statuses, indent=''): def _format_compat_value(self, compat): return {'yes': 'compatible'}.get(compat, 'incompatible') - - + def _format_compat_pref(self, compat, pref_lic=None): PAREN_OPEN = '(' PAREN_CLOSE = ')' @@ -167,16 +166,9 @@ def format_compatibilities_general(self, compat_object, indent='', policy_report details = compat_object["compatibility_details"] summary = details["summary"] preferred_info = 'no' - import sys - # TODO: - print ("policy_report and preferred_license DIEGO2:" + str(policy_report) + " " + str(preferred_license == compat_object["policy_check"]["inbound"]["preferences"]["license"])) - print ("policy_report and preferred_license DIEGO:" + str(compat_object["policy_check"]["inbound"]["preferences"]["license"]) + " <---> " + str(compat_object["inbound_license"]) + " SAME:" + str(compat_object["policy_check"]["inbound"]["preferences"]["license"]== compat_object["inbound_license"])) - #sys.exit(1) if policy_report and preferred_license == compat_object["policy_check"]["inbound"]["preferences"]["license"]: preferred_info = 'yes' - #output.append(f'{indent}{compat_object["outbound_license"]} -> {compat_object["inbound_license"]} {self._format_compat_pref(compat_object["compatibility"])}') output.append(f'{indent}{compat_object["outbound_license"]} -> {compat_object["inbound_license"]}') - #output.append(f'{indent} check: {compatibility_check}') if policy_report: output.append(f'{indent} preferred: {preferred_info}') output.append(f'{indent} compatibility: {compat_object["compatibility"]}') @@ -190,13 +182,7 @@ def format_compatibilities_general(self, compat_object, indent='', policy_report if policy_report and preferred_license == compat_object['policy_check']['inbound']['preferences']['license']: preferred_info = 'yes' for operand in compat_object["operands"]: - preferred = False - if policy_report: - operand_license = operand['compatibility_object']['inbound_license'] - if operand_license == preferred_license: - preferred = True res = self.format_compatibilities_general(operand['compatibility_object'], indent=f'{indent} ', policy_report=policy_report, preferred_license=preferred_license) - #inner_output.append(f'{indent} allowed licenses: {", ".join([x["inbound_license"] for x in compat_object["policy_check"]["inbound_licenses"]])}') inner_output.append(res) if policy_report: output.append(f'{indent}{operator} {self._format_compat_pref(compat_object["compatibility"], preferred_license)}') @@ -215,7 +201,6 @@ def format_compatibilities_general(self, compat_object, indent='', policy_report res = self.format_compatibilities_general(operand['compatibility_object'], indent=f'{indent} ', policy_report=policy_report, preferred_license=preferred_license) output.append(res) if compatibility_check == "outbound-expression -> inbound-expression": - #print("APA") operator = compat_object["operator"] compat = compat_object["compatibility"] preferred_info = 'no' @@ -240,26 +225,28 @@ def format_compatibilities_object(self, compat_object): def format_policy_report(self, report, verbose=False): output = [] preferred_inbound = report['compatibility_report']['policy_check']['inbound']['preferences']['license'] - print("keys: " + str(report["compatibility_report"].keys())) - - output.append(f'outbound: {report["outbound"]}') - output.append(f'inbound: {report["inbound"]}') - output.append(f'resources: {", ".join(report["resources"])}') - output.append(f'provisioning: {report["provisioning"]}') - output.append(f'usecase: {report["usecase"]}') - output.append(f'compatibility: {report["compatibility"]}') - output.append(f'preferred inbound: {preferred_inbound}') - output.append(f'HESA: {report["compatibility_report"]["policy_check"]}') - if report["meta"]["policy_type"] == 'default': - policy_string = 'default' - else: - policy_string = report["meta"]["policy_file"] - output.append(f'policy: {policy_string}') + preferred_outbound = report['compatibility_report']['policy_check']['outbound']['preferences']['license'] + + policy_info = report['meta']['policy_type'] + if policy_info == 'policy_file': + policy_info = f'{policy_info} {report["meta"]["policy_file"]}' + + output.append(f'outbound: {report["outbound"]}') + output.append(f'inbound: {report["inbound"]}') + res = ', '.join(report['resources']) + output.append(f'resources: {res}') + output.append(f'provisioning: {report["provisioning"]}') + output.append(f'usecase: {report["usecase"]}') + output.append(f'policy: {policy_info}') + output.append(f'compatibility: {report["compatibility"]}') + output.append(f'preferred inbound: {preferred_inbound}') + output.append(f'preferred outbound: {preferred_outbound}') + if verbose: output.append('report:') output.append(self.format_compatibilities_general(report["compatibility_report"], indent=' ', policy_report=True, preferred_license=preferred_inbound)) return "\n".join(output) - + def format_compatibilities(self, compat, verbose=False): output = [] output.append(f'outbound: {compat["outbound"]}') @@ -282,13 +269,6 @@ def format_licomp_versions(self, licomp_versions): return '\n'.join(res) def format_display_compatibilities(self, compats): - # possible compats are: - # no (red) - # yes (green) - # depends (yellow) - # unsupported (yellow) - # unknown (yellow) - # mixed (yellow) display_compats = self._pre_format_display_compatibilities(compats) licenses = list(display_compats.keys()) @@ -309,7 +289,7 @@ def _compat_line_color(self, compats): } _color_map = { 'yes': 'darkgreen', - 'no': 'darkred' + 'no': 'darkred', } same = True value = None @@ -334,20 +314,15 @@ def _compat_line_color(self, compats): def _license_license_compat(self, outbound, inbound, outbound_compat, inbound_compat): out_line, out_color = self._compat_line_color(outbound_compat) in_line, in_color = self._compat_line_color(inbound_compat) + BR_START = '[' + BR_END = ']' if out_line == in_line and out_color == in_color: - return (f' "{outbound}" -> "{inbound}" [dir="both" color="{out_color}" {out_line}]') + return (f' "{outbound}" -> "{inbound}" {BR_START}dir="both" color="{out_color}" {out_line}{BR_END}') else: - return '\n'.join([f' "{outbound}" -> "{inbound}" [color="{out_color}" {out_line}]', - f' "{inbound}" -> "{outbound}" [color="{in_color}" {in_line}]']) - - def format_display_compatibilities(self, compats, settings={}): - # possible compats are: - # no (red) - # yes (green) - # depends (yellow) - # unsupported (yellow) - # unknown (yellow) - # mixed (yellow) + return '\n'.join([f' "{outbound}" -> "{inbound}" {BR_START}color="{out_color}" {out_line}{BR_END}', + f' "{inbound}" -> "{outbound}" {BR_START}color="{in_color}" {in_line}{BR_END}']) + + def format_display_compatibilities(self, compats, settings={}): # noqa: B006 display_compats = self._pre_format_display_compatibilities(compats) licenses = list(display_compats.keys()) From 30a2ce36a971e053e59ed5265045a81606251af9 Mon Sep 17 00:00:00 2001 From: Henrik Sandklef Date: Mon, 16 Feb 2026 17:57:21 +0100 Subject: [PATCH 28/41] remove unused code --- licomp_toolkit/schema_checker.py | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/licomp_toolkit/schema_checker.py b/licomp_toolkit/schema_checker.py index 45f0edd..42a5855 100644 --- a/licomp_toolkit/schema_checker.py +++ b/licomp_toolkit/schema_checker.py @@ -43,8 +43,7 @@ def __validate_deeply(self, compat): if compat_check == 'outbound-expression -> inbound-license' or compat_check == 'outbound-expression -> inbound-expression': compat_object = compat elif compat['compatibility_check'] == 'outbound-license -> inbound-expression': - #compat_object = compat['compatibility_object'] - compat_object = compat + compat_object = compat else: raise LicompException("Validation failed. Invalid state: " + compat_check) From defe5e0e724c928d3c83ca1817195fe6432801cb Mon Sep 17 00:00:00 2001 From: Henrik Sandklef Date: Mon, 16 Feb 2026 17:58:13 +0100 Subject: [PATCH 29/41] fix supported resources failure --- licomp_toolkit/utils.py | 18 ++++++++++++------ 1 file changed, 12 insertions(+), 6 deletions(-) diff --git a/licomp_toolkit/utils.py b/licomp_toolkit/utils.py index 8306539..348f372 100644 --- a/licomp_toolkit/utils.py +++ b/licomp_toolkit/utils.py @@ -54,14 +54,20 @@ def _inc_map(_map, _name): def resource_avilable(resource, licomp_toolkit): return resource in licomp_toolkit.licomp_resources().keys() +def default_resources(): + return ['licomp_osadl', 'licomp_reclicense'] + + def resources_to_use(args): lt = LicompToolkit() resources = args.resources - new_resources = [] - unsupported = [] - if args.resources == ['all']: + new_resources = set() + unsupported = set() + if 'all' in args.resources: new_resources = list(lt.licomp_resources().keys()) return new_resources, [] + if not args.resources: + return default_resources(), [] for resource in resources: if 'licomp' not in resource: resource = f'licomp_{resource}' @@ -69,7 +75,7 @@ def resources_to_use(args): resource = resource.replace('-', '_') if not resource_avilable(resource, lt): - unsupported.append(resource) + unsupported.add(resource) else: - new_resources.append(resource) - return new_resources, unsupported + new_resources.add(resource) + return list(new_resources), list(unsupported) From a39288c607611df75059041f48a398330c1a4267 Mon Sep 17 00:00:00 2001 From: Henrik Sandklef Date: Mon, 16 Feb 2026 17:58:40 +0100 Subject: [PATCH 30/41] fix flake issues --- licomp_toolkit/toolkit.py | 23 +++++------------------ 1 file changed, 5 insertions(+), 18 deletions(-) diff --git a/licomp_toolkit/toolkit.py b/licomp_toolkit/toolkit.py index e38e674..37c77fe 100644 --- a/licomp_toolkit/toolkit.py +++ b/licomp_toolkit/toolkit.py @@ -253,7 +253,6 @@ def check_compatibility(self, detailed_report=True): compat_object = { -# INBOUND_COMPATIBILITY_TYPE: parsed_expression[COMPATIBILITY_TYPE], 'compatibility_check': f'outbound-{self.le_parser.parse_license_expression(outbound)["compatibility_type"]} -> inbound-{parsed_expression["compatibility_type"]}', 'check_class': __class__.__name__, } @@ -274,7 +273,7 @@ def check_compatibility(self, compat_object['inbound_license'] = lic compat_object['outbound_license'] = outbound compat_object['compatibility_object'] = {} - + else: operator = parsed_expression['operator'] operands = parsed_expression['operands'] @@ -288,7 +287,7 @@ def check_compatibility(self, operand_compat = self.check_compatibility(outbound, operand, usecase, provisioning, resources, detailed_report=detailed_report) operand_object = { 'compatibility_object': operand_compat, - 'compatibility': operand_compat['compatibility'] + 'compatibility': operand_compat['compatibility'], } operands_object.append(operand_object) @@ -357,9 +356,9 @@ def meta_information(self): return { 'tool': module_name, 'file': 'verification', - 'file_version': licomp_toolkit_file_version + 'file_version': licomp_toolkit_file_version, } - + def check_compatibility(self, outbound, inbound, usecase, provisioning, resources=None, detailed_report=True): # Check usecase @@ -421,7 +420,7 @@ def check_compatibility(self, outbound, inbound, usecase, provisioning, resource 'compatibility': compatibility_object['compatibility'], 'compatibility_report': compatibility_object, 'unavailable_resources': unavailable_resources, - 'available_resources': available_resources + 'available_resources': available_resources, } def __check_compatibility(self, @@ -434,7 +433,6 @@ def __check_compatibility(self, outbound_type = outbound_parsed[COMPATIBILITY_TYPE] compat_object = { -# OUTBOUND_COMPATIBILITY_TYPE: outbound_type, 'inbound_license': self.le_parser.to_string(inbound_parsed), 'outbound_license': self.le_parser.to_string(outbound_parsed), 'check_class': __class__.__name__, @@ -442,18 +440,9 @@ def __check_compatibility(self, if outbound_type == 'license': if False: - # TODO: REMOVE HERE? compat_object['compatibility_check'] = f'outbound-license -> inbound-{inbound_parsed["compatibility_type"]} HERE?' outbound_parsed_license = outbound_parsed['license'] - compat_object['HENRIK_TESTS'] = 'MONKEY BALLS' - # TODO: SHOULD operator/operands be added???? SEEMS LIKE NO - #compat_object['operator'] = inbound_parsed['operator'] - #compat_object['operands'] = inbound_parsed['operands'] - # Check if: - # outbound license - # is compatible with - # inbound license compat = self.le_checker.check_compatibility(outbound_parsed_license, inbound_parsed, usecase, @@ -464,7 +453,6 @@ def __check_compatibility(self, compat_object['compatibility_object'] = compat compat_object['compatibility_details'] = None - # TODO: CHECK IF ABOVE CAN BE REMOVED outbound_parsed_license = outbound_parsed['license'] compat = self.le_checker.check_compatibility(outbound_parsed_license, inbound_parsed, @@ -473,7 +461,6 @@ def __check_compatibility(self, resources, detailed_report) compat_object = compat - elif outbound_type == 'expression': compat_object['compatibility_details'] = None From b134ccdfa484958c6ee72763a258e033ab649897 Mon Sep 17 00:00:00 2001 From: Henrik Sandklef Date: Mon, 16 Feb 2026 18:07:16 +0100 Subject: [PATCH 31/41] adjust to new format --- tests/shell/test_policy.sh | 19 +++++++------------ 1 file changed, 7 insertions(+), 12 deletions(-) diff --git a/tests/shell/test_policy.sh b/tests/shell/test_policy.sh index 70b97a6..1b1aa4b 100755 --- a/tests/shell/test_policy.sh +++ b/tests/shell/test_policy.sh @@ -35,7 +35,7 @@ licomp-toolkit-verify() INBOUND="$1" OUTBOUND="$2" - PYTHONPATH=$IMPLEMENTATIONS::. python3 licomp_toolkit/__main__.py $RESOURCE_ARGS verify -il "$INBOUND" -ol "$OUTBOUND" + PYTHONPATH=$IMPLEMENTATIONS::. python3 licomp_toolkit/__main__.py --verbose $RESOURCE_ARGS verify -il "$INBOUND" -ol "$OUTBOUND" } licomp-toolkit-apply() @@ -43,7 +43,7 @@ licomp-toolkit-apply() OUTPUT_ARGS="$1" REPORT="$2" - PYTHONPATH=$IMPLEMENTATIONS::. python3 licomp_toolkit/__main__.py $RESOURCE_ARGS $OUTPUT_ARGS apply-license-policy $REPORT + PYTHONPATH=$IMPLEMENTATIONS::. python3 licomp_toolkit/__main__.py --verbose $RESOURCE_ARGS $OUTPUT_ARGS apply-license-policy $REPORT } # @@ -52,18 +52,13 @@ licomp-toolkit-apply() licomp-toolkit-verify MIT MIT > report.json licomp-toolkit-apply " -of text" report.json > policy-report.txt comment_file_presence "preferred inbound:[ ]*MIT" policy-report.txt "test 1.1" -comment_file_presence "preferred inbound: MIT" policy-report.txt "test 1.2" -comment_file_presence "preferred license: no" policy-report.txt "test 1.3" -comment_file_presence "least preferred license: no" policy-report.txt "test 1.4" -comment_file_presence "compatibility: yes" policy-report.txt "test 1.5" +comment_file_presence "preferred outbound:[ ]*MIT" policy-report.txt "test 1.2" +comment_file_presence "^compatibility:[ ]*yes" policy-report.txt "test 1.5" comment_file_presence "compatibility details:" policy-report.txt "test 1.6" licomp-toolkit-verify MIT "MIT OR LGPL-2.1-only" > report.json licomp-toolkit-apply " -of text" report.json > policy-report.txt -comment_file_presence "preferred inbound: MIT" policy-report.txt "test 2.1" -comment_file_presence "preferred inbound: MIT" policy-report.txt "test 2.2" -comment_file_presence "preferred license: no" policy-report.txt "test 2.3" -comment_file_presence "least preferred license: no" policy-report.txt "test 2.4" -comment_file_presence "compatibility: yes" policy-report.txt "test 2.5" -comment_file_presence "compatibility details:" policy-report.txt "test 2.6" +comment_file_presence "preferred inbound:[ ]*MIT" policy-report.txt "test 2.1" +comment_file_presence "preferred outbound:[ ]*MIT" policy-report.txt "test 2.2" +comment_file_presence "^compatibility:[ ]*yes" policy-report.txt "test 2.5" From a3f6c61d3ce021b25a7fa155549e16cb22dd4747 Mon Sep 17 00:00:00 2001 From: Henrik Sandklef Date: Mon, 16 Feb 2026 18:08:51 +0100 Subject: [PATCH 32/41] extended license expression tests --- tests/python/test_policy_expr.py | 311 +++++++++++++++++++++++++++++++ 1 file changed, 311 insertions(+) create mode 100644 tests/python/test_policy_expr.py diff --git a/tests/python/test_policy_expr.py b/tests/python/test_policy_expr.py new file mode 100644 index 0000000..4701a4d --- /dev/null +++ b/tests/python/test_policy_expr.py @@ -0,0 +1,311 @@ +# SPDX-FileCopyrightText: 2026 Henrik Sandklef +# +# SPDX-License-Identifier: GPL-3.0-or-later + +import json +import pytest +import logging +import sys + +from licomp_toolkit.license_policy import LicensePolicy +from licomp_toolkit.license_policy import DefaultLicensePolicy +from licomp_toolkit.license_policy import LicensePolicyException +from licomp_toolkit.license_policy import LicensePolicyHandler +from licomp_toolkit.toolkit import ExpressionExpressionChecker +from licomp.interface import UseCase +from licomp.interface import Provisioning + +TEST_POLICY_FILE = 'tests/policy/license-policy.json' + +policy = LicensePolicy(TEST_POLICY_FILE) +policy_handler = LicensePolicyHandler(TEST_POLICY_FILE) +default_policy_handler = LicensePolicyHandler( + resources = ['licomp_reclicense'], + usecase = 'library', + provisioning = 'binary-distribution') +expr_checker = ExpressionExpressionChecker() + +default_policy = DefaultLicensePolicy( + ['licomp_reclicense'], + 'library', + 'binary-distribution') + +def _test_expr_expr_library_bin(outbound, inbound): + report = expr_checker.check_compatibility(outbound, + inbound, + 'library', + 'binary-distribution') + policy_report = policy_handler.apply_policy(report) + policy_report_default = default_policy_handler.apply_policy(report) + + return report, policy_report, policy_report_default + + + +def test_lic_lic(): + + report, policy_report, policy_report_default = _test_expr_expr_library_bin( + 'MIT', + 'BSD-3-Clause' + ) + + assert report['compatibility_report']['compatibility'] == 'yes' + + assert policy_report['compatibility_report']['policy_check']['outbound']['license'] == 'MIT' + assert policy_report['compatibility_report']['policy_check']['inbound']['license'] == 'BSD-3-Clause' + + assert policy_report['compatibility_report']['policy_check']['outbound']['preferences']['license'] == 'MIT' + assert policy_report['compatibility_report']['policy_check']['inbound']['preferences']['license'] == 'BSD-3-Clause' + + assert policy_report_default['compatibility_report']['policy_check']['outbound']['license'] == 'MIT' + assert policy_report_default['compatibility_report']['policy_check']['inbound']['license'] == 'BSD-3-Clause' + + assert policy_report_default['compatibility_report']['policy_check']['outbound']['preferences']['license'] == 'MIT' + assert policy_report_default['compatibility_report']['policy_check']['inbound']['preferences']['license'] == 'BSD-3-Clause' + + + +def test_lic_expr_and(): + + report, policy_report, policy_report_default = _test_expr_expr_library_bin( + 'MIT', + 'BSD-3-Clause AND 0BSD' + ) + + assert report['compatibility_report']['compatibility'] == 'yes' + + assert policy_report['compatibility_report']['policy_check']['outbound']['license'] == 'MIT' + assert policy_report['compatibility_report']['policy_check']['inbound']['license'] == 'BSD-3-Clause AND 0BSD' + + assert policy_report['compatibility_report']['policy_check']['outbound']['preferences']['license'] == 'MIT' + assert policy_report['compatibility_report']['policy_check']['inbound']['preferences']['license'] == '0BSD AND BSD-3-Clause' + + assert policy_report_default['compatibility_report']['policy_check']['outbound']['license'] == 'MIT' + assert policy_report_default['compatibility_report']['policy_check']['inbound']['license'] == 'BSD-3-Clause AND 0BSD' + + assert policy_report_default['compatibility_report']['policy_check']['outbound']['preferences']['license'] == 'MIT' + assert policy_report_default['compatibility_report']['policy_check']['inbound']['preferences']['license'] == '0BSD AND BSD-3-Clause' + + +def test_lic_expr_or(): + + report, policy_report, policy_report_default = _test_expr_expr_library_bin( + 'MIT', + 'BSD-3-Clause OR 0BSD' + ) + + assert report['compatibility_report']['compatibility'] == 'yes' + + assert policy_report['compatibility_report']['policy_check']['outbound']['license'] == 'MIT' + assert policy_report['compatibility_report']['policy_check']['inbound']['license'] == 'BSD-3-Clause OR 0BSD' + + assert policy_report['compatibility_report']['policy_check']['outbound']['preferences']['license'] == 'MIT' + assert policy_report['compatibility_report']['policy_check']['inbound']['preferences']['license'] == '0BSD' + + assert policy_report_default['compatibility_report']['policy_check']['outbound']['license'] == 'MIT' + assert policy_report_default['compatibility_report']['policy_check']['inbound']['license'] == 'BSD-3-Clause OR 0BSD' + + assert policy_report_default['compatibility_report']['policy_check']['outbound']['preferences']['license'] == 'MIT' + assert policy_report_default['compatibility_report']['policy_check']['inbound']['preferences']['license'] == '0BSD' + + +def test_expr_lic_or(): + + report, policy_report, policy_report_default = _test_expr_expr_library_bin( + 'MIT OR 0BSD', + 'BSD-3-Clause' + ) + + assert report['compatibility_report']['compatibility'] == 'yes' + + assert policy_report['compatibility_report']['policy_check']['outbound']['license'] == 'MIT OR 0BSD' + assert policy_report['compatibility_report']['policy_check']['inbound']['license'] == 'BSD-3-Clause' + + assert policy_report['compatibility_report']['policy_check']['outbound']['preferences']['license'] == '0BSD' + assert policy_report['compatibility_report']['policy_check']['inbound']['preferences']['license'] == 'BSD-3-Clause' + + assert policy_report_default['compatibility_report']['policy_check']['outbound']['license'] == 'MIT OR 0BSD' + assert policy_report_default['compatibility_report']['policy_check']['inbound']['license'] == 'BSD-3-Clause' + + assert policy_report_default['compatibility_report']['policy_check']['outbound']['preferences']['license'] == '0BSD' + assert policy_report_default['compatibility_report']['policy_check']['inbound']['preferences']['license'] == 'BSD-3-Clause' + + +def test_expr_lic_and(): + + report, policy_report, policy_report_default = _test_expr_expr_library_bin( + 'MIT AND 0BSD', + 'BSD-3-Clause' + ) + + assert report['compatibility_report']['compatibility'] == 'yes' + + assert policy_report['compatibility_report']['policy_check']['outbound']['license'] == 'MIT AND 0BSD' + assert policy_report['compatibility_report']['policy_check']['inbound']['license'] == 'BSD-3-Clause' + + assert policy_report['compatibility_report']['policy_check']['outbound']['preferences']['license'] == '0BSD AND MIT' + assert policy_report['compatibility_report']['policy_check']['inbound']['preferences']['license'] == 'BSD-3-Clause' + + assert policy_report_default['compatibility_report']['policy_check']['outbound']['license'] == 'MIT AND 0BSD' + assert policy_report_default['compatibility_report']['policy_check']['inbound']['license'] == 'BSD-3-Clause' + + assert policy_report_default['compatibility_report']['policy_check']['outbound']['preferences']['license'] == '0BSD AND MIT' + assert policy_report_default['compatibility_report']['policy_check']['inbound']['preferences']['license'] == 'BSD-3-Clause' + + +def test_expr_expr_or(): + + report, policy_report, policy_report_default = _test_expr_expr_library_bin( + 'MIT OR 0BSD', + 'BSD-2-Clause OR BSD-3-Clause' + ) + + assert report['compatibility_report']['compatibility'] == 'yes' + + assert policy_report['compatibility_report']['policy_check']['outbound']['license'] == 'MIT OR 0BSD' + assert policy_report['compatibility_report']['policy_check']['inbound']['license'] == 'BSD-2-Clause OR BSD-3-Clause' + + assert policy_report['compatibility_report']['policy_check']['outbound']['preferences']['license'] == '0BSD' + assert policy_report['compatibility_report']['policy_check']['inbound']['preferences']['license'] == 'BSD-2-Clause' + + assert policy_report_default['compatibility_report']['policy_check']['outbound']['license'] == 'MIT OR 0BSD' + assert policy_report_default['compatibility_report']['policy_check']['inbound']['license'] == 'BSD-2-Clause OR BSD-3-Clause' + + assert policy_report_default['compatibility_report']['policy_check']['outbound']['preferences']['license'] == '0BSD' + assert policy_report_default['compatibility_report']['policy_check']['inbound']['preferences']['license'] == 'BSD-2-Clause' + + +def test_expr_expr_and(): + + report, policy_report, policy_report_default = _test_expr_expr_library_bin( + 'MIT AND 0BSD', + 'BSD-2-Clause AND BSD-3-Clause' + ) + + assert report['compatibility_report']['compatibility'] == 'yes' + + assert policy_report['compatibility_report']['policy_check']['outbound']['license'] == 'MIT AND 0BSD' + assert policy_report['compatibility_report']['policy_check']['inbound']['license'] == 'BSD-2-Clause AND BSD-3-Clause' + + assert policy_report['compatibility_report']['policy_check']['outbound']['preferences']['license'] == '0BSD AND MIT' + assert policy_report['compatibility_report']['policy_check']['inbound']['preferences']['license'] == 'BSD-2-Clause AND BSD-3-Clause' + + assert policy_report_default['compatibility_report']['policy_check']['outbound']['license'] == 'MIT AND 0BSD' + assert policy_report_default['compatibility_report']['policy_check']['inbound']['license'] == 'BSD-2-Clause AND BSD-3-Clause' + + assert policy_report_default['compatibility_report']['policy_check']['outbound']['preferences']['license'] == '0BSD AND MIT' + assert policy_report_default['compatibility_report']['policy_check']['inbound']['preferences']['license'] == 'BSD-2-Clause AND BSD-3-Clause' + +def test_expr_expr_incompat(): + + report, policy_report, policy_report_default = _test_expr_expr_library_bin( + 'MIT AND 0BSD', + 'BSD-2-Clause AND GPL-2.0-or-later' + ) + + assert report['compatibility_report']['compatibility'] == 'no' + + assert policy_report['compatibility_report']['policy_check']['outbound']['license'] == 'MIT AND 0BSD' + assert policy_report['compatibility_report']['policy_check']['inbound']['license'] == 'BSD-2-Clause AND GPL-2.0-or-later' + + assert policy_report['compatibility_report']['policy_check']['outbound']['preferences']['license'] == None + assert policy_report['compatibility_report']['policy_check']['inbound']['preferences']['license'] == None + + assert policy_report_default['compatibility_report']['policy_check']['outbound']['license'] == 'MIT AND 0BSD' + assert policy_report_default['compatibility_report']['policy_check']['inbound']['license'] == 'BSD-2-Clause AND GPL-2.0-or-later' + + assert policy_report_default['compatibility_report']['policy_check']['outbound']['preferences']['license'] == None + assert policy_report_default['compatibility_report']['policy_check']['inbound']['preferences']['license'] == None + +def test_expr_expr_bad_license_or(): + # X11 not supported by licomp_reclicense, but we have and OR so should be compatible + report, policy_report, policy_report_default = _test_expr_expr_library_bin( + 'MIT AND 0BSD', + 'BSD-2-Clause OR X11' + ) + + assert report['compatibility_report']['compatibility'] == 'yes' + + assert policy_report['compatibility_report']['policy_check']['outbound']['license'] == 'MIT AND 0BSD' + assert policy_report['compatibility_report']['policy_check']['inbound']['license'] == 'BSD-2-Clause OR X11' + + assert policy_report['compatibility_report']['policy_check']['outbound']['preferences']['license'] == '0BSD AND MIT' + assert policy_report['compatibility_report']['policy_check']['inbound']['preferences']['license'] == 'BSD-2-Clause' + + assert policy_report_default['compatibility_report']['policy_check']['outbound']['license'] == 'MIT AND 0BSD' + assert policy_report_default['compatibility_report']['policy_check']['inbound']['license'] == 'BSD-2-Clause OR X11' + + assert policy_report_default['compatibility_report']['policy_check']['outbound']['preferences']['license'] == '0BSD AND MIT' + assert policy_report_default['compatibility_report']['policy_check']['inbound']['preferences']['license'] == 'BSD-2-Clause' + +def test_expr_expr_bad_license_and(): + # X11 not supported by licomp_reclicense + report, policy_report, policy_report_default = _test_expr_expr_library_bin( + 'MIT AND 0BSD', + 'BSD-2-Clause AND X11' + ) + + assert report['compatibility_report']['compatibility'] == 'no' + + assert policy_report['compatibility_report']['policy_check']['outbound']['license'] == 'MIT AND 0BSD' + assert policy_report['compatibility_report']['policy_check']['inbound']['license'] == 'BSD-2-Clause AND X11' + + assert policy_report['compatibility_report']['policy_check']['outbound']['preferences']['license'] == None + assert policy_report['compatibility_report']['policy_check']['inbound']['preferences']['license'] == None + + assert policy_report_default['compatibility_report']['policy_check']['outbound']['license'] == 'MIT AND 0BSD' + assert policy_report_default['compatibility_report']['policy_check']['inbound']['license'] == 'BSD-2-Clause AND X11' + + assert policy_report_default['compatibility_report']['policy_check']['outbound']['preferences']['license'] == None + assert policy_report_default['compatibility_report']['policy_check']['inbound']['preferences']['license'] == None + +def test_expr_expr_no_license_or(): + # MONKEYWRENCH is not a license + # ... and nor supported by licomp_reclicense + # ... and not in a policy list + + report, policy_report, policy_report_default = _test_expr_expr_library_bin( + 'MIT AND 0BSD', + 'BSD-2-Clause OR MONKEYWRENCH' + ) + + assert report['compatibility_report']['compatibility'] == 'yes' + + assert policy_report['compatibility_report']['policy_check']['outbound']['license'] == 'MIT AND 0BSD' + assert policy_report['compatibility_report']['policy_check']['inbound']['license'] == 'BSD-2-Clause OR MONKEYWRENCH' + + assert policy_report['compatibility_report']['policy_check']['outbound']['preferences']['license'] == '0BSD AND MIT' + assert policy_report['compatibility_report']['policy_check']['inbound']['preferences']['license'] == 'BSD-2-Clause' + + assert policy_report_default['compatibility_report']['policy_check']['outbound']['license'] == 'MIT AND 0BSD' + assert policy_report_default['compatibility_report']['policy_check']['inbound']['license'] == 'BSD-2-Clause OR MONKEYWRENCH' + + assert policy_report_default['compatibility_report']['policy_check']['outbound']['preferences']['license'] == '0BSD AND MIT' + assert policy_report_default['compatibility_report']['policy_check']['inbound']['preferences']['license'] == 'BSD-2-Clause' + + +def test_expr_expr_no_license_and(): + # MONKEYWRENCH is not a license + # ... and nor supported by licomp_reclicense + # ... and not in a policy list + + report, policy_report, policy_report_default = _test_expr_expr_library_bin( + 'MIT AND 0BSD', + 'BSD-2-Clause AND MONKEYWRENCH' + ) + + assert report['compatibility_report']['compatibility'] == 'no' + + assert policy_report['compatibility_report']['policy_check']['outbound']['license'] == 'MIT AND 0BSD' + assert policy_report['compatibility_report']['policy_check']['inbound']['license'] == 'BSD-2-Clause AND MONKEYWRENCH' + + assert policy_report['compatibility_report']['policy_check']['outbound']['preferences']['license'] == None + assert policy_report['compatibility_report']['policy_check']['inbound']['preferences']['license'] == None + + assert policy_report_default['compatibility_report']['policy_check']['outbound']['license'] == 'MIT AND 0BSD' + assert policy_report_default['compatibility_report']['policy_check']['inbound']['license'] == 'BSD-2-Clause AND MONKEYWRENCH' + + assert policy_report_default['compatibility_report']['policy_check']['outbound']['preferences']['license'] == None + assert policy_report_default['compatibility_report']['policy_check']['inbound']['preferences']['license'] == None + +# with pytest.raises(LicensePolicyException): From bf80c19385353b6e7fa8d55c2f5a144a2a380169 Mon Sep 17 00:00:00 2001 From: Henrik Sandklef Date: Mon, 16 Feb 2026 18:09:10 +0100 Subject: [PATCH 33/41] adjust to new format --- tests/python/test_policy.py | 163 +++++++++++++++++++++++++++++++++++- 1 file changed, 162 insertions(+), 1 deletion(-) diff --git a/tests/python/test_policy.py b/tests/python/test_policy.py index 731d8ef..896c007 100644 --- a/tests/python/test_policy.py +++ b/tests/python/test_policy.py @@ -19,9 +19,17 @@ policy = LicensePolicy(TEST_POLICY_FILE) policy_handler = LicensePolicyHandler(TEST_POLICY_FILE) +default_policy_handler = LicensePolicyHandler( + resources = ['licomp_reclicense'], + usecase = 'library', + provisioning = 'binary-distribution') expr_checker = ExpressionExpressionChecker() -default_policy = DefaultLicensePolicy(['licomp_reclicense'], 'library', 'binary-distribution') +default_policy = DefaultLicensePolicy( + ['licomp_reclicense'], + 'library', + 'binary-distribution') + def test_policy_allowed(): assert "MIT" in policy.allowed() @@ -68,6 +76,7 @@ def test_policy_preferences_allowed_avoided(): assert policy.compare_preferences('BSD-4-Clause', 'BSD-4-Clause') == 0 def test_policy_preferences_allowed_denied(): + assert policy.compare_preferences('BSD-3-Clause', 'BSD-3-Clause') == 0 assert policy.compare_preferences('MIT', 'BSD-2-Clause-Patent') < 0 assert policy.compare_preferences('BSD-2-Clause-Patent', 'MIT') > 0 assert policy.compare_preferences('BSD-2-Clause-Patent', 'BSD-2-Clause-Patent') == None @@ -86,9 +95,29 @@ def test_policy_preferences_allowed_denied_names_ignore(): assert policy.most_preferred('BSD-2-Clause-Patent', 'BSD-3-Clause', ignore_missing=True) == 'BSD-3-Clause' assert policy.most_preferred('BSD-2-Clause-Patent', 'BSD-2-Clause-Patent', ignore_missing=True) == None +def OBSOLETE_test_policy_preferred_score_ignore_missing(): + assert policy.preferred_score_ignore_missing('MIT', 'BSD-3-Clause') == -1 + assert policy.preferred_score_ignore_missing('BSD-3-Clause', 'BSD-3-Clause') == 0 + assert policy.preferred_score_ignore_missing('BSD-3-Clause', 'MIT') == 1 + +def OBSOLETE_test_policy_sorted(): + assert policy.preferred_score_ignore_missing('MIT', 'BSD-3-Clause') == -1 + assert policy.preferred_score_ignore_missing('BSD-3-Clause', 'BSD-3-Clause') == 0 + assert policy.preferred_score_ignore_missing('BSD-3-Clause', 'MIT') == 1 + assert policy.preferred_score_ignore_missing('BSD-3-Clause', 'GPL-2.0-or-later') == -1 + from functools import cmp_to_key + inbounds = ['MIT', 'GPL-2.0-or-later', 'BSD-3-Clause'] + sorted_inbounds = sorted(inbounds, key=cmp_to_key(policy.preferred_score_ignore_missing)) + print("TEST: " + str(inbounds)) + print("TEST: " + str(sorted_inbounds)) + assert sorted_inbounds == ['MIT', 'BSD-3-Clause', 'GPL-2.0-or-later'] + + def test_policy_preferences_raises(): with pytest.raises(LicensePolicyException) as e_info: policy.most_preferred('MIT2', 'GPL-2.0-or-later2') + with pytest.raises(LicensePolicyException) as e_info: + policy.most_preferred('MIT2', 'GPL-2.0-or-later2', ignore_missing=False) def test_policy_preferences_raises_ignore(): policy.most_preferred('MIT2', 'GPL-2.0-or-later2', ignore_missing=True) == None @@ -119,6 +148,7 @@ def test_default_policy_preference(): assert default_policy.compare_preferences('MIT', 'MIT') == 0 def test_default_policy_preferences_allowed_denied_names(): + assert default_policy.most_preferred('BSD-3-Clause', 'BSD-3-Clause') == 'BSD-3-Clause' assert default_policy.most_preferred('MIT', 'BSD-3-Clause') == 'MIT' assert default_policy.most_preferred('MIT', 'GPL-2.0-or-later') == 'MIT' @@ -127,3 +157,134 @@ def test_default_policy_preferences_raises(): with pytest.raises(LicensePolicyException) as e_info: default_policy.most_preferred('MIT2', 'GPL-2.0-or-later2') +def test_preferred_score_inbounds(): + OBSD_MIT = {'outbound': {'license': 'MIT', 'type': 'license', 'preferences': {'license': 'MIT', 'license_list': 'allowed', 'license_index': 3}}, 'inbound': {'license': '0BSD', 'type': 'license', 'preferences': {'license': '0BSD', 'license_list': 'allowed', 'license_index': 2}}, 'compatibility': 'yes', 'unusable': {'unusable': []}} + #{'check_type': 'inbound', 'inbound_license': '0BSD', 'outbound_license': 'MIT', 'inbound_license_type': 'license', 'outbound_license_type': 'license', 'compatibility': 'yes', 'inbound_list': 'allowed', 'inbound_list_index': 3} + + ISC_MIT = {'outbound': {'license': 'MIT', 'type': 'license', 'preferences': {'license': 'MIT', 'license_list': 'allowed', 'license_index': 3}}, 'inbound': {'license': 'ISC', 'type': 'license', 'preferences': {'license': 'ISC', 'license_list': 'allowed', 'license_index': 4}}, 'compatibility': 'yes', 'unusable': {'unusable': []}} + #{'check_type': 'inbound', 'inbound_license': 'ISC', 'outbound_license': 'MIT', 'inbound_license_type': 'license', 'outbound_license_type': 'license', 'compatibility': 'yes', 'inbound_list': 'allowed', 'inbound_list_index': 7} + + assert default_policy.preferred_score_inbounds(OBSD_MIT, ISC_MIT) < 0 + assert default_policy.preferred_score_inbounds(ISC_MIT, OBSD_MIT) > 0 + assert default_policy.preferred_score_inbounds(ISC_MIT, ISC_MIT) == 0 + +def test_scored_inbounds_zerobsd_isc(): + inbounds = [ + { + "outbound": { + "license": "MIT", + "type": "license", + "preferences": { + "license": "MIT", + "license_list": "allowed", + "license_index": 0 + } + }, + "inbound": { + "license": "0BSD", + "type": "license", + "preferences": { + "license": "0BSD", + "license_list": "allowed", + "license_index": 4 + } + }, + "compatibility": "yes", + "unusable": [] + }, + { + "outbound": { + "license": "MIT", + "type": "license", + "preferences": { + "license": "MIT", + "license_list": "allowed", + "license_index": 0 + } + }, + "inbound": { + "license": "ISC", + "type": "license", + "preferences": { + "license": "ISC", + "license_list": "", + "license_index": None + } + }, + "compatibility": "yes", + "unusable": [] + } + ] + + scored_inbounds, unusable = default_policy_handler.scored_inbounds(inbounds, "OR") + assert len(scored_inbounds) == 2 + assert len(scored_inbounds[0]['inbound']) == 3 + assert scored_inbounds[0]['inbound']['license'] == '0BSD' + assert scored_inbounds[1]['inbound']['license'] == 'ISC' + + +def test_scored_inbounds_isc_zerobsd(): + inbounds = [ + { + "outbound": { + "license": "MIT", + "type": "license", + "preferences": { + "license": "MIT", + "license_list": "allowed", + "license_index": 0 + } + }, + "inbound": { + "license": "ISC", + "type": "license", + "preferences": { + "license": "ISC", + "license_list": "", + "license_index": None + } + }, + "compatibility": "yes", + "unusable": [] + }, + { + "outbound": { + "license": "MIT", + "type": "license", + "preferences": { + "license": "MIT", + "license_list": "allowed", + "license_index": 0 + } + }, + "inbound": { + "license": "0BSD", + "type": "license", + "preferences": { + "license": "0BSD", + "license_list": "allowed", + "license_index": 4 + } + }, + "compatibility": "yes", + "unusable": [] + } + ] + + scored_inbounds, unusable = default_policy_handler.scored_inbounds(inbounds, "OR") + assert len(scored_inbounds) == 2 + assert len(scored_inbounds[0]['inbound']) == 3 + assert scored_inbounds[0]['inbound']['license'] == '0BSD' + assert scored_inbounds[1]['inbound']['license'] == 'ISC' + + +def test_usable_license(): + LIC = {'outbound': {'license': 'MIT', 'type': 'license', 'preferences': {'license': 'MIT', 'license_list': 'allowed', 'license_index': 3}}, 'inbound': {'license': 'BSD-3-Clause', 'type': 'license', 'preferences': {'license': 'BSD-3-Clause', 'license_list': 'allowed', 'license_index': 2}}, 'compatibility': 'no', 'unusable': {'unusable': []}} + + assert not policy_handler.usable_license(LIC, 'inbound') + assert not policy_handler.usable_license(LIC, 'outbound') + + LIC['compatibility'] = 'yes' + assert policy_handler.usable_license(LIC, 'inbound') + assert policy_handler.usable_license(LIC, 'outbound') + From 83b3eeb8e3d4001a5dbf3d42c698385a083650ce Mon Sep 17 00:00:00 2001 From: Henrik Sandklef Date: Mon, 16 Feb 2026 18:09:24 +0100 Subject: [PATCH 34/41] adjust to new format --- tests/shell/test-cli.sh | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tests/shell/test-cli.sh b/tests/shell/test-cli.sh index c0c5f69..b14786f 100755 --- a/tests/shell/test-cli.sh +++ b/tests/shell/test-cli.sh @@ -6,7 +6,7 @@ LT_VERSION=$(grep licomp_toolkit_version licomp_toolkit/config.py | cut -d = -f 2 | sed "s,[' ]*,,g") -EXTRACT_COMPAT=".compatibility_report.compatibility_object.compatibility_details" +EXTRACT_COMPAT=".compatibility_report.compatibility_details" if [ "$1" == "--local" ] then From df48fc7d5b0f31a5bc12547f32aa562bc61fa91d Mon Sep 17 00:00:00 2001 From: Henrik Sandklef Date: Mon, 16 Feb 2026 18:10:20 +0100 Subject: [PATCH 35/41] add test file --- .reuse/dep5 | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.reuse/dep5 b/.reuse/dep5 index 6f74588..38c2d78 100644 --- a/.reuse/dep5 +++ b/.reuse/dep5 @@ -27,6 +27,6 @@ Files: README.md docs/*.md Copyright: Henrik Sandklef License: GPL-3.0-or-later -Files: tests/policy/license-policy.json +Files: tests/policy/license-policy.json tests/policy/bad-license-policy.json Copyright: Henrik Sandklef License: GPL-3.0-or-later From 1ccc0645bf628ec80fc35a16b70c25b36d5cfce1 Mon Sep 17 00:00:00 2001 From: Henrik Sandklef Date: Mon, 16 Feb 2026 18:11:32 +0100 Subject: [PATCH 36/41] remove tmp file --- licomp_toolkit_test.tmp | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/licomp_toolkit_test.tmp b/licomp_toolkit_test.tmp index e101afb..28f1d68 100644 --- a/licomp_toolkit_test.tmp +++ b/licomp_toolkit_test.tmp @@ -1,6 +1,5 @@ digraph depends { graph [label="License Compatibility Graph (library)" labelloc=t] node [shape=plaintext] - "MIT" -> "BSD-3-Clause" [color="darkblue" style="dotted"] - "BSD-3-Clause" -> "MIT" [color="darkgreen" ] + "MIT" -> "BSD-3-Clause" [dir="both" color="darkgreen" ] } From e9a663b78e5b0369977ba65ba7a636f7bf5fdd27 Mon Sep 17 00:00:00 2001 From: Henrik Sandklef Date: Mon, 16 Feb 2026 18:30:33 +0100 Subject: [PATCH 37/41] remove file --- licomp_toolkit_test.tmp | 5 ----- 1 file changed, 5 deletions(-) delete mode 100644 licomp_toolkit_test.tmp diff --git a/licomp_toolkit_test.tmp b/licomp_toolkit_test.tmp deleted file mode 100644 index 28f1d68..0000000 --- a/licomp_toolkit_test.tmp +++ /dev/null @@ -1,5 +0,0 @@ -digraph depends { - graph [label="License Compatibility Graph (library)" labelloc=t] - node [shape=plaintext] - "MIT" -> "BSD-3-Clause" [dir="both" color="darkgreen" ] -} From c21aa31b2bb6d3fb77087c1a3248f93b20fd5eb8 Mon Sep 17 00:00:00 2001 From: Henrik Sandklef Date: Mon, 16 Feb 2026 18:34:21 +0100 Subject: [PATCH 38/41] add licomp_toolkit_file_version --- licomp_toolkit/config.py | 1 + 1 file changed, 1 insertion(+) diff --git a/licomp_toolkit/config.py b/licomp_toolkit/config.py index 51a87f4..73c3968 100644 --- a/licomp_toolkit/config.py +++ b/licomp_toolkit/config.py @@ -4,6 +4,7 @@ licomp_toolkit_version = '0.5.18' my_supported_api_version = '0.5' +licomp_toolkit_file_version = '0.5' cli_name = 'licomp-toolkit' module_name = 'licomp_toolkit' From b61694a2db12a5f0037ebfd8359b39c30d5aa71a Mon Sep 17 00:00:00 2001 From: Henrik Sandklef Date: Mon, 16 Feb 2026 18:35:28 +0100 Subject: [PATCH 39/41] add license policy --- tests/policy/license-policy.json | 22 ++++++++++++++++++++++ 1 file changed, 22 insertions(+) create mode 100644 tests/policy/license-policy.json diff --git a/tests/policy/license-policy.json b/tests/policy/license-policy.json new file mode 100644 index 0000000..4a757d0 --- /dev/null +++ b/tests/policy/license-policy.json @@ -0,0 +1,22 @@ +{ + "meta": { + "tool": "licomp-toolkit", + "file": "license-policy-file" + }, + "policy": { + "allowed": [ + "MIT", + "BSD-3-Clause", + "GPL-2.0-only", + "LGPL-2.1-only", + "0BSD" + ], + "avoided": [ + "BSD-4-Clause" + ], + "denied": [ + "BSD-2-Clause-Patent" + ] + } +} + From b9a29112a19e4ce9d750c86345ebdeb74ca11728 Mon Sep 17 00:00:00 2001 From: Henrik Sandklef Date: Mon, 16 Feb 2026 18:37:31 +0100 Subject: [PATCH 40/41] add return codes --- licomp_toolkit/return_codes.py | 12 ++++++++++++ 1 file changed, 12 insertions(+) create mode 100644 licomp_toolkit/return_codes.py diff --git a/licomp_toolkit/return_codes.py b/licomp_toolkit/return_codes.py new file mode 100644 index 0000000..b39a7dd --- /dev/null +++ b/licomp_toolkit/return_codes.py @@ -0,0 +1,12 @@ +# SPDX-FileCopyrightText: 2026 Henrik Sandklef +# +# SPDX-License-Identifier: GPL-3.0-or-later + +from enum import Enum + +from licomp.return_codes import ReturnCodes + +class LicompToolkitReturnCodes(Enum): + LICOMP_TOOLKIT_INVALID_FILE = ReturnCodes.LICOMP_LAST_ERROR_CODE.value + 1 + + LICOMP_TOOLKIT_LAST_ERROR_CODE = ReturnCodes.LICOMP_LAST_ERROR_CODE.value + 100 From d6b64a45b6378f017fd628f3c1607ab4eff2ba75 Mon Sep 17 00:00:00 2001 From: update-generated-files-action <41898282+github-actions[bot]@users.noreply.github.com> Date: Mon, 16 Feb 2026 17:38:41 +0000 Subject: [PATCH 41/41] Autofix (Push test pipeline / generate) Auto-generated-by: update-generated-files-action; https://github.com/hesa/licomp-toolkit/actions/runs/22072467888 --- licomp_toolkit_test.tmp | 5 +++++ 1 file changed, 5 insertions(+) create mode 100644 licomp_toolkit_test.tmp diff --git a/licomp_toolkit_test.tmp b/licomp_toolkit_test.tmp new file mode 100644 index 0000000..28f1d68 --- /dev/null +++ b/licomp_toolkit_test.tmp @@ -0,0 +1,5 @@ +digraph depends { + graph [label="License Compatibility Graph (library)" labelloc=t] + node [shape=plaintext] + "MIT" -> "BSD-3-Clause" [dir="both" color="darkgreen" ] +}