From fadfe26ba3e684bdf96ff4af20e1da44510b272d Mon Sep 17 00:00:00 2001 From: Henrik Sandklef Date: Mon, 23 Jun 2025 22:48:58 +0200 Subject: [PATCH 01/16] add -r -nv, make use of -v --- licomp_toolkit/__main__.py | 22 +++++++++++++++++++++- 1 file changed, 21 insertions(+), 1 deletion(-) diff --git a/licomp_toolkit/__main__.py b/licomp_toolkit/__main__.py index be16595..0bddd1d 100755 --- a/licomp_toolkit/__main__.py +++ b/licomp_toolkit/__main__.py @@ -43,12 +43,19 @@ def validate(self, args): def verify(self, args): formatter = LicompToolkitFormatter.formatter(self.args.output_format) try: + if args.no_verbose: + detailed_report = False + else: + detailed_report = True + + expr_checker = ExpressionExpressionChecker() compatibilities = expr_checker.check_compatibility(self.__normalize_license(args.out_license), self.__normalize_license(args.in_license), args.usecase, args.provisioning, - detailed_report=True) + resources=args.resources, + detailed_report=detailed_report) ret_code = compatibility_status_to_returncode(compatibilities['compatibility']) return formatter.format_compatibilities(compatibilities), ret_code, False @@ -118,8 +125,21 @@ def main(): UseCase.LIBRARY, Provisioning.BIN_DIST) + parser = lct_parser.parser subparsers = lct_parser.sub_parsers() + parser.add_argument('-r', '--resources', + type=str, + action='append', + help='use only specified licomp resource', + default=[]) + # TODO: check if resource exists + + parser.add_argument('-nv', '--no-verbose', + action='store_true', + help='keep compatibility report as short as possible', + default=[]) + # Command: list supported parser_sr = subparsers.add_parser('supported-resources', help='List all supported Licomp resources') parser_sr.set_defaults(which="supported_resources", func=lct_parser.supported_resources) From bd0b88ff07b88cce1b05e3628efd501cfdba9e26 Mon Sep 17 00:00:00 2001 From: Henrik Sandklef Date: Mon, 23 Jun 2025 22:49:26 +0200 Subject: [PATCH 02/16] basic impl of text output --- licomp_toolkit/format.py | 115 ++++++++++++++++++++++++++++++++++++--- 1 file changed, 107 insertions(+), 8 deletions(-) diff --git a/licomp_toolkit/format.py b/licomp_toolkit/format.py index 7b7b0f6..7e93790 100644 --- a/licomp_toolkit/format.py +++ b/licomp_toolkit/format.py @@ -12,6 +12,8 @@ def formatter(fmt): return JsonLicompToolkitFormatter() if fmt.lower() == 'text': return TextLicompToolkitFormatter() + if fmt.lower() == 'markdown': + return MarkdownLicompToolkitFormatter() def format_compatibilities(self, compat): return None @@ -47,16 +49,113 @@ def format_licomp_resources(self, licomp_resources): def format_licomp_licenses(self, licomp_licenses): return "\n".join(licomp_licenses) + def __get_responses(self, results, indent=""): + output = [] + for res in ['yes', 'no', 'schneben']: + result = results.get(res) + if not result: + count = 0 + else: + count = result["count"] + output.append(f'{indent}{res}: {count}') + + return output + + def __compatibility_statuses(self, statuses, indent=""): + output = [] + for status, values in statuses.items(): + resources = [] + for object in values: + resources.append(object['resource_name']) + output.append(f'{indent}{status}: {", ".join(resources)}') + + return output + + def __statuses(self, statuses, indent=""): + output = [] + for status, values in statuses.items(): + resources = [] + for object in values: + resources.append(object['resource_name']) + output.append(f'{indent}{status}: {", ".join(resources)}') + + return output + + def format_compatibilities_object(self, compat_object, indent=""): + compatibility_check = compat_object["compatibility_check"] + output = [] + + if compatibility_check == "outbound-license -> inbound-license": + print(f' ---=== {compat_object["outbound_license"]} -> {compat_object["inbound_license"]} ===---') + if not compat_object["compatibility_object"]: + #print("APA " + str(json.dumps(compat_object, indent=4))) + #print("APA " + str(compat_object)) + import sys + #sys.exit(1) + pass + else: + compat_object = compat_object["compatibility_object"] + details = compat_object["compatibility_details"] + summary = details["summary"] + results = summary["results"] + #output.append(f'{indent} validity:') + #output.append(f'{indent} total: {details["nr_licomp"]}') + #output.append(f'{indent} valid: {results["nr_valid"]}') + #output += self.__statuses(summary['statuses'], f'{indent} ') + + output.append(f'{indent}{compat_object["outbound_license"]} -> {compat_object["inbound_license"]}') + output.append(f'{indent} compatibility: {compat_object["compatibility"]}') + output.append(f'{indent} compatibility details:') + output += self.__compatibility_statuses(summary['compatibility_statuses'], f'{indent} ') + + #return "l -> l" + if compatibility_check == "outbound-license -> inbound-expression": + #output.append(' l -> e - remove me later') + outbound = compat_object["compatibility_object"]["outbound_license"] + operator = compat_object["compatibility_object"]["operator"] + #output.append(f'{indent}{outbound}') + output.append(f'{indent}{operator}') + for operand in compat_object["compatibility_object"]["operands"]: + res = self.format_compatibilities_object(operand['compatibility_object'], indent = indent + ' ') + output.append(res) + + + if compatibility_check == "outbound-expression -> inbound-license": + output.append(f'{indent} e -> l === TODO') + print("000000 " + str(compat_object.get("compatibility_object"))) + #outbound = compat_object["compatibility_object"]["outbound_license"] + operator = compat_object["operator"] + #output.append(f'{indent}{outbound}') + output.append(f'{indent}{operator}') + for operand in compat_object["operands"]: + res = self.format_compatibilities_object(operand['compatibility_object'], indent = indent + ' ') + output.append(res) + if compatibility_check == "outbound-expression -> inbound-expression": + output.append(f'{indent}{compat_object["operator"]}') + for operand in compat_object['operands']: + #print(" ---- " + str(operand)) + res = self.format_compatibilities_object(operand['compatibility_object'], indent = indent + ' ') + output.append(f'{indent} {res}') + + + + return "\n".join(output) + + def format_compatibilities(self, compat): - summary = compat['summary'] output = [] - nr_valid = summary['results']['nr_valid'] - output.append(f'{nr_valid} succesfull response(s)') - if int(nr_valid) > 0: - output.append('Results:') - statuses = summary['compatibility_statuses'] - for status in statuses.keys(): - output.append(f' {status}: {", ".join(statuses[status])}') + output.append(f'outbound: {compat["outbound"]}') + output.append(f'inbound: {compat["inbound"]}') + output.append(f'resources: {", ".join(compat["resources"])}') + output.append(f'provisioning: {compat["provisioning"]}') + output.append(f'usecase: {compat["usecase"]}') + output.append(f'compatibility: {compat["compatibility"]}') + output.append(f'report:') + output.append(self.format_compatibilities_object(compat["compatibility_report"])) + + print(str("\n".join(output))) + import sys + sys.exit(1) return "\n".join(output) def format_licomp_versions(self, licomp_versions): From 0e99c36c67d9197eaeb2a107b6adb89493da32bb Mon Sep 17 00:00:00 2001 From: Henrik Sandklef Date: Mon, 23 Jun 2025 23:05:37 +0200 Subject: [PATCH 03/16] clean up, fix indent --- licomp_toolkit/format.py | 43 ++++++++++------------------------------ 1 file changed, 10 insertions(+), 33 deletions(-) diff --git a/licomp_toolkit/format.py b/licomp_toolkit/format.py index 7e93790..86351a0 100644 --- a/licomp_toolkit/format.py +++ b/licomp_toolkit/format.py @@ -81,64 +81,44 @@ def __statuses(self, statuses, indent=""): return output - def format_compatibilities_object(self, compat_object, indent=""): + def format_compatibilities_object(self, compat_object, indent=''): compatibility_check = compat_object["compatibility_check"] output = [] if compatibility_check == "outbound-license -> inbound-license": - print(f' ---=== {compat_object["outbound_license"]} -> {compat_object["inbound_license"]} ===---') if not compat_object["compatibility_object"]: - #print("APA " + str(json.dumps(compat_object, indent=4))) - #print("APA " + str(compat_object)) - import sys - #sys.exit(1) pass else: compat_object = compat_object["compatibility_object"] details = compat_object["compatibility_details"] summary = details["summary"] results = summary["results"] - #output.append(f'{indent} validity:') - #output.append(f'{indent} total: {details["nr_licomp"]}') - #output.append(f'{indent} valid: {results["nr_valid"]}') - #output += self.__statuses(summary['statuses'], f'{indent} ') output.append(f'{indent}{compat_object["outbound_license"]} -> {compat_object["inbound_license"]}') output.append(f'{indent} compatibility: {compat_object["compatibility"]}') output.append(f'{indent} compatibility details:') - output += self.__compatibility_statuses(summary['compatibility_statuses'], f'{indent} ') - - #return "l -> l" + output += self.__compatibility_statuses(summary['compatibility_statuses'], f'{indent} ') if compatibility_check == "outbound-license -> inbound-expression": - #output.append(' l -> e - remove me later') outbound = compat_object["compatibility_object"]["outbound_license"] operator = compat_object["compatibility_object"]["operator"] - #output.append(f'{indent}{outbound}') - output.append(f'{indent}{operator}') + output.append(f'{indent}{operator} (compatibility: {compat_object["compatibility"]})') for operand in compat_object["compatibility_object"]["operands"]: - res = self.format_compatibilities_object(operand['compatibility_object'], indent = indent + ' ') + res = self.format_compatibilities_object(operand['compatibility_object'], indent = f'{indent} ') output.append(res) if compatibility_check == "outbound-expression -> inbound-license": - output.append(f'{indent} e -> l === TODO') - print("000000 " + str(compat_object.get("compatibility_object"))) - #outbound = compat_object["compatibility_object"]["outbound_license"] operator = compat_object["operator"] - #output.append(f'{indent}{outbound}') - output.append(f'{indent}{operator}') + output.append(f'{indent}{operator} (compatibility: {compat_object["compatibility"]})') for operand in compat_object["operands"]: - res = self.format_compatibilities_object(operand['compatibility_object'], indent = indent + ' ') + res = self.format_compatibilities_object(operand['compatibility_object'], indent = f'{indent} ') output.append(res) if compatibility_check == "outbound-expression -> inbound-expression": - output.append(f'{indent}{compat_object["operator"]}') + output.append(f'{indent}{compat_object["operator"]} (compatibility: {compat_object["compatibility"]})') for operand in compat_object['operands']: - #print(" ---- " + str(operand)) - res = self.format_compatibilities_object(operand['compatibility_object'], indent = indent + ' ') - output.append(f'{indent} {res}') + res = self.format_compatibilities_object(operand['compatibility_object'], indent = f'{indent} ') + output.append(f'{res}') - - return "\n".join(output) @@ -151,11 +131,8 @@ def format_compatibilities(self, compat): output.append(f'usecase: {compat["usecase"]}') output.append(f'compatibility: {compat["compatibility"]}') output.append(f'report:') - output.append(self.format_compatibilities_object(compat["compatibility_report"])) + output.append(self.format_compatibilities_object(compat["compatibility_report"], ' ')) - print(str("\n".join(output))) - import sys - sys.exit(1) return "\n".join(output) def format_licomp_versions(self, licomp_versions): From f4c89589750b0277545ca4d821936acfc66ae706 Mon Sep 17 00:00:00 2001 From: Henrik Sandklef Date: Mon, 23 Jun 2025 23:16:57 +0200 Subject: [PATCH 04/16] fix flake8 failures --- licomp_toolkit/format.py | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/licomp_toolkit/format.py b/licomp_toolkit/format.py index 86351a0..03bc05a 100644 --- a/licomp_toolkit/format.py +++ b/licomp_toolkit/format.py @@ -3,6 +3,7 @@ # SPDX-License-Identifier: GPL-3.0-or-later import json +import yaml class LicompToolkitFormatter(): @@ -12,8 +13,6 @@ def formatter(fmt): return JsonLicompToolkitFormatter() if fmt.lower() == 'text': return TextLicompToolkitFormatter() - if fmt.lower() == 'markdown': - return MarkdownLicompToolkitFormatter() def format_compatibilities(self, compat): return None @@ -101,7 +100,7 @@ def format_compatibilities_object(self, compat_object, indent=''): if compatibility_check == "outbound-license -> inbound-expression": outbound = compat_object["compatibility_object"]["outbound_license"] operator = compat_object["compatibility_object"]["operator"] - output.append(f'{indent}{operator} (compatibility: {compat_object["compatibility"]})') + output.append(f'{indent}{operator} -- compatibility: {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) @@ -109,18 +108,19 @@ def format_compatibilities_object(self, compat_object, indent=''): if compatibility_check == "outbound-expression -> inbound-license": operator = compat_object["operator"] - output.append(f'{indent}{operator} (compatibility: {compat_object["compatibility"]})') + output.append(f'{indent}{operator} -- compatibility: {compat_object["compatibility"]}') for operand in compat_object["operands"]: res = self.format_compatibilities_object(operand['compatibility_object'], indent = f'{indent} ') output.append(res) if compatibility_check == "outbound-expression -> inbound-expression": - output.append(f'{indent}{compat_object["operator"]} (compatibility: {compat_object["compatibility"]})') + operator = compat_object["operator"] + compat = compat_object["compatibility"] + output.append(f'{indent}{operator} -- compatibility: {compat}') for operand in compat_object['operands']: res = self.format_compatibilities_object(operand['compatibility_object'], indent = f'{indent} ') output.append(f'{res}') return "\n".join(output) - def format_compatibilities(self, compat): output = [] From 0175794d99fc943df6152137e5cc5d7572456922 Mon Sep 17 00:00:00 2001 From: Henrik Sandklef Date: Wed, 25 Jun 2025 18:21:29 +0200 Subject: [PATCH 05/16] add support for passing resources --- licomp_toolkit/toolkit.py | 82 ++++++++++++++++++++++++++++++--------- 1 file changed, 63 insertions(+), 19 deletions(-) diff --git a/licomp_toolkit/toolkit.py b/licomp_toolkit/toolkit.py index 4852182..4061bd2 100644 --- a/licomp_toolkit/toolkit.py +++ b/licomp_toolkit/toolkit.py @@ -31,6 +31,9 @@ class LicompToolkit(Licomp): + #TODO: document that this class check + # lic -> lic + def __init__(self): Licomp.__init__(self) self.LICOMP_RESOURCES = {} @@ -74,11 +77,11 @@ def licomp_resources(self): self.LICOMP_RESOURCES[licomp_instance.name()] = licomp_instance return self.LICOMP_RESOURCES - def __summarize_compatibility(self, compatibilities, outbound, inbound, usecase, provisioning): + def __summarize_compatibility(self, compatibilities, outbound, inbound, usecase, provisioning, resources): compatibilities["summary"] = {} statuses = {} compats = {} - compatibilities['nr_licomp'] = len(self.licomp_resources()) + compatibilities['nr_licomp'] = len(resources) # for resource_name in self.licomp_resources(): for compat in compatibilities["compatibilities"]: logging.debug(f': {compat}') @@ -114,20 +117,20 @@ def __summarize_compatibility(self, compatibilities, outbound, inbound, usecase, compatibilities['summary']['results'] = results # override top class - def outbound_inbound_compatibility(self, outbound, inbound, usecase, provisioning): + def outbound_inbound_compatibility(self, outbound, inbound, usecase, provisioning, resources): logging.debug(f'{inbound} {outbound} ') compatibilities = {} compatibilities['compatibilities'] = [] - for resource_name in self.licomp_resources(): + for resource_name in resources: resource = self.licomp_resources()[resource_name] logging.debug(f'-- resource: {resource.name()}') compat = resource.outbound_inbound_compatibility(outbound, inbound, usecase, provisioning=provisioning) compatibilities['compatibilities'].append(compat) - self.__summarize_compatibility(compatibilities, outbound, inbound, usecase, provisioning) + self.__summarize_compatibility(compatibilities, outbound, inbound, usecase, provisioning, resources) self.__add_meta(compatibilities) return compatibilities @@ -172,15 +175,12 @@ def name(self): class LicenseExpressionChecker(): + #TODO: document that this class check + # lic -> expr + def __init__(self): self.le_parser = LicenseExpressionParser() - self.licomp = LicompToolkit() - - def outbound_inbound_compatibility(self, outbound, lic, usecase, provisioning): - return self.licomp.outbound_inbound_compatibility(outbound, - lic, - usecase, - provisioning) + self.licomp_toolkit = LicompToolkit() def __compatibility_status(self, compatibility): status = compatibility['summary']['results'] @@ -208,8 +208,11 @@ def check_compatibility(self, parsed_expression, usecase, provisioning, + resources, detailed_report=True): + + compat_object = { COMPATIBILITY_TYPE: parsed_expression[COMPATIBILITY_TYPE], 'compatibility_check': 'outbound-expression -> inbound-license', @@ -218,10 +221,11 @@ def check_compatibility(self, if parsed_expression[COMPATIBILITY_TYPE] == 'license': compat_object['compatibility_check'] = 'outbound-license -> inbound-license' lic = parsed_expression['license'] - compat = self.outbound_inbound_compatibility(outbound, - lic, - usecase, - provisioning) + compat = self.licomp_toolkit.outbound_inbound_compatibility(outbound, + lic, + usecase, + provisioning, + resources) compat_object['compatibility'] = self.__compatibility_status(compat) if detailed_report: compat_object['compatibility_details'] = compat @@ -241,7 +245,7 @@ def check_compatibility(self, compat_object['compatibility_details'] = None operands_object = [] for operand in operands: - operand_compat = self.check_compatibility(outbound, operand, usecase, provisioning, detailed_report=detailed_report) + operand_compat = self.check_compatibility(outbound, operand, usecase, provisioning, resources, detailed_report=detailed_report) operand_object = { 'compatibility_object': operand_compat, 'compatibility': operand_compat['compatibility'], @@ -296,14 +300,47 @@ def summarise_compatibilities(self, operator, operands): class ExpressionExpressionChecker(): + #TODO: document that this class check + # expr -> expr + def __init__(self): self.le_checker = LicenseExpressionChecker() self.le_parser = LicenseExpressionParser() + self.licomp_toolkit = LicompToolkit() def __parsed_expression_to_name(self, parsed_expression): return parsed_expression[parsed_expression[COMPATIBILITY_TYPE]] - def check_compatibility(self, outbound, inbound, usecase, provisioning, detailed_report=True): + def check_compatibility(self, outbound, inbound, usecase, provisioning, resources=[], detailed_report=True): + + licomp_resources = list(self.licomp_toolkit.licomp_resources().keys()) + if not resources: + resources = licomp_resources + else: + resources = args.resources + + unavailable_resources = {} + + for resource in resources: + resource_object = self.licomp_toolkit.licomp_resources()[resource] + unavailable_reasons = [] + + # is usecase supported by resource + if not resource_object.usecase_supported(UseCase.string_to_usecase(usecase)): + unavailable_reasons.append(f'Usecase "{usecase}" not supported') + + # is prov case supported by resource + if not resource_object.provisioning_supported(Provisioning.string_to_provisioning(provisioning)): + unavailable_reasons.append(f'Provisioning case "{provisioning}" not supported') + + if unavailable_reasons: + unavailable_resources[resource] = { + 'reasons': ", ".join(unavailable_reasons) + } + + + available_resources = [resource for resource in resources if resource not in list(unavailable_resources.keys())] + # Check usecase try: usecase = UseCase.string_to_usecase(usecase) @@ -317,20 +354,24 @@ def check_compatibility(self, outbound, inbound, usecase, provisioning, detailed raise LicompException(f'Provisioning {provisioning} not supported.', ReturnCodes.LICOMP_UNSUPPORTED_PROVISIONING) inbound_parsed = self.le_parser.parse_license_expression(inbound) - outbound_parsed = self.le_parser.parse_license_expression(outbound) + compatibility_object = self.__check_compatibility(outbound_parsed, inbound_parsed, usecase, provisioning, + resources, detailed_report) return { 'inbound': inbound, 'outbound': outbound, 'usecase': UseCase.usecase_to_string(usecase), + 'resources': resources, 'provisioning': Provisioning.provisioning_to_string(provisioning), 'compatibility': compatibility_object['compatibility'], 'compatibility_report': compatibility_object, + 'unavailable_resources': unavailable_resources, + 'available_resources': available_resources, } def __check_compatibility(self, @@ -338,6 +379,7 @@ def __check_compatibility(self, inbound_parsed, usecase, provisioning, + resources, detailed_report=True): outbound_type = outbound_parsed[COMPATIBILITY_TYPE] @@ -358,6 +400,7 @@ def __check_compatibility(self, inbound_parsed, usecase, provisioning, + resources, detailed_report) compat_object['compatibility'] = compat['compatibility'] compat_object['compatibility_object'] = compat @@ -381,6 +424,7 @@ def __check_compatibility(self, inbound_parsed, usecase, provisioning, + resources, detailed_report) operand_object = { 'compatibility_object': operand_compat, From eae4fa625817a2fc69b6a6de767812f9cdb45e5b Mon Sep 17 00:00:00 2001 From: Henrik Sandklef Date: Fri, 27 Jun 2025 23:49:02 +0200 Subject: [PATCH 06/16] fix flake8 problems --- licomp_toolkit/format.py | 39 ++++++++++++++++++++------------------- 1 file changed, 20 insertions(+), 19 deletions(-) diff --git a/licomp_toolkit/format.py b/licomp_toolkit/format.py index 03bc05a..263ef75 100644 --- a/licomp_toolkit/format.py +++ b/licomp_toolkit/format.py @@ -3,7 +3,6 @@ # SPDX-License-Identifier: GPL-3.0-or-later import json -import yaml class LicompToolkitFormatter(): @@ -64,8 +63,8 @@ def __compatibility_statuses(self, statuses, indent=""): output = [] for status, values in statuses.items(): resources = [] - for object in values: - resources.append(object['resource_name']) + for value_object in values: + resources.append(value_object['resource_name']) output.append(f'{indent}{status}: {", ".join(resources)}') return output @@ -74,12 +73,17 @@ def __statuses(self, statuses, indent=""): output = [] for status, values in statuses.items(): resources = [] - for object in values: - resources.append(object['resource_name']) + for value_object in values: + resources.append(value_object['resource_name']) output.append(f'{indent}{status}: {", ".join(resources)}') return output + def _format_compat(self, compat): + PAREN_OPEN = '(' + PAREN_START = ')' + return f'{PAREN_OPEN}{compat}{PAREN_START}' + def format_compatibilities_object(self, compat_object, indent=''): compatibility_check = compat_object["compatibility_check"] output = [] @@ -91,37 +95,34 @@ def format_compatibilities_object(self, compat_object, indent=''): compat_object = compat_object["compatibility_object"] details = compat_object["compatibility_details"] summary = details["summary"] - results = summary["results"] - output.append(f'{indent}{compat_object["outbound_license"]} -> {compat_object["inbound_license"]}') + output.append(f'{indent}{compat_object["outbound_license"]} -> {compat_object["inbound_license"]} {self._format_compat(compat_object["compatibility"])}') 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": - outbound = compat_object["compatibility_object"]["outbound_license"] operator = compat_object["compatibility_object"]["operator"] - output.append(f'{indent}{operator} -- compatibility: {compat_object["compatibility"]}') + 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} ') + res = self.format_compatibilities_object(operand['compatibility_object'], indent=f'{indent} ') output.append(res) - - + if compatibility_check == "outbound-expression -> inbound-license": operator = compat_object["operator"] - output.append(f'{indent}{operator} -- compatibility: {compat_object["compatibility"]}') + output.append(f'{indent}{operator} {self._format_compat(compat_object["compatibility"])}') for operand in compat_object["operands"]: - res = self.format_compatibilities_object(operand['compatibility_object'], indent = f'{indent} ') + res = self.format_compatibilities_object(operand['compatibility_object'], indent=f'{indent} ') output.append(res) if compatibility_check == "outbound-expression -> inbound-expression": operator = compat_object["operator"] compat = compat_object["compatibility"] - output.append(f'{indent}{operator} -- compatibility: {compat}') + output.append(f'{indent}{operator} {self._format_compat(compat)}') for operand in compat_object['operands']: - res = self.format_compatibilities_object(operand['compatibility_object'], indent = f'{indent} ') + res = self.format_compatibilities_object(operand['compatibility_object'], indent=f'{indent} ') output.append(f'{res}') - + return "\n".join(output) - + def format_compatibilities(self, compat): output = [] output.append(f'outbound: {compat["outbound"]}') @@ -130,7 +131,7 @@ 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(f'report:') + output.append('report:') output.append(self.format_compatibilities_object(compat["compatibility_report"], ' ')) return "\n".join(output) From e07b7e95ebbbfe5aae9627bc5eafa2d75e383779 Mon Sep 17 00:00:00 2001 From: Henrik Sandklef Date: Sat, 28 Jun 2025 00:26:52 +0200 Subject: [PATCH 07/16] exit with new return code if resource not supported --- licomp_toolkit/__main__.py | 15 ++++++++++++--- 1 file changed, 12 insertions(+), 3 deletions(-) diff --git a/licomp_toolkit/__main__.py b/licomp_toolkit/__main__.py index 0bddd1d..4f86bbd 100755 --- a/licomp_toolkit/__main__.py +++ b/licomp_toolkit/__main__.py @@ -48,13 +48,20 @@ def verify(self, args): else: detailed_report = True - + resources = args.resources + unsupported = [] + for resource in resources: + if not self.resource_avilable(resource): + unsupported.append(resource) + if unsupported: + return f'Resources {", ".join(unsupported)} are not supported', ReturnCodes.LICOMP_UNSUPPORTED_RESOURCE.value, True + expr_checker = ExpressionExpressionChecker() compatibilities = expr_checker.check_compatibility(self.__normalize_license(args.out_license), self.__normalize_license(args.in_license), args.usecase, args.provisioning, - resources=args.resources, + resources=resources, detailed_report=detailed_report) ret_code = compatibility_status_to_returncode(compatibilities['compatibility']) @@ -113,6 +120,9 @@ def versions(self, args): formatter = LicompToolkitFormatter.formatter(args.output_format) return formatter.format_licomp_versions(self.licomp_toolkit.versions()), ReturnCodes.LICOMP_OK.value, False + def resource_avilable(self, resource): + return resource in self.licomp_toolkit.licomp_resources().keys() + def _working_return_code(return_code): return return_code < ReturnCodes.LICOMP_LAST_SUCCESSFUL_CODE.value @@ -133,7 +143,6 @@ def main(): action='append', help='use only specified licomp resource', default=[]) - # TODO: check if resource exists parser.add_argument('-nv', '--no-verbose', action='store_true', From 49ef2abd171aaabf4a90813ce8d2413f9e850864 Mon Sep 17 00:00:00 2001 From: Henrik Sandklef Date: Sat, 28 Jun 2025 00:28:03 +0200 Subject: [PATCH 08/16] fix flake8 complaints --- licomp_toolkit/toolkit.py | 13 +++++-------- 1 file changed, 5 insertions(+), 8 deletions(-) diff --git a/licomp_toolkit/toolkit.py b/licomp_toolkit/toolkit.py index 4061bd2..f335a81 100644 --- a/licomp_toolkit/toolkit.py +++ b/licomp_toolkit/toolkit.py @@ -211,8 +211,6 @@ def check_compatibility(self, resources, detailed_report=True): - - compat_object = { COMPATIBILITY_TYPE: parsed_expression[COMPATIBILITY_TYPE], 'compatibility_check': 'outbound-expression -> inbound-license', @@ -311,20 +309,20 @@ def __init__(self): def __parsed_expression_to_name(self, parsed_expression): return parsed_expression[parsed_expression[COMPATIBILITY_TYPE]] - def check_compatibility(self, outbound, inbound, usecase, provisioning, resources=[], detailed_report=True): + def check_compatibility(self, outbound, inbound, usecase, provisioning, resources=None, detailed_report=True): licomp_resources = list(self.licomp_toolkit.licomp_resources().keys()) if not resources: resources = licomp_resources else: - resources = args.resources + resources = resources unavailable_resources = {} for resource in resources: resource_object = self.licomp_toolkit.licomp_resources()[resource] unavailable_reasons = [] - + # is usecase supported by resource if not resource_object.usecase_supported(UseCase.string_to_usecase(usecase)): unavailable_reasons.append(f'Usecase "{usecase}" not supported') @@ -335,12 +333,11 @@ def check_compatibility(self, outbound, inbound, usecase, provisioning, resource if unavailable_reasons: unavailable_resources[resource] = { - 'reasons': ", ".join(unavailable_reasons) + 'reasons': ", ".join(unavailable_reasons), } - available_resources = [resource for resource in resources if resource not in list(unavailable_resources.keys())] - + # Check usecase try: usecase = UseCase.string_to_usecase(usecase) From f4f7c9b6c1c5908354f19dbbfdce4a1b320973cf Mon Sep 17 00:00:00 2001 From: Henrik Sandklef Date: Sat, 5 Jul 2025 01:32:58 +0200 Subject: [PATCH 09/16] add LicompToolkit description --- licomp_toolkit/toolkit.py | 8 ++++++-- 1 file changed, 6 insertions(+), 2 deletions(-) diff --git a/licomp_toolkit/toolkit.py b/licomp_toolkit/toolkit.py index f335a81..de29868 100644 --- a/licomp_toolkit/toolkit.py +++ b/licomp_toolkit/toolkit.py @@ -30,9 +30,13 @@ from licomp_toolkit.config import my_supported_api_version class LicompToolkit(Licomp): + """A class implementing Licomp, but for a misc Licomp resources + and packaging the responses into a new reply + (licomp_toolkit/reply_schema.json). - #TODO: document that this class check - # lic -> lic + LicompToolkit can check a single license agaisnt another for + compatibility, but not license expressions. + """ def __init__(self): Licomp.__init__(self) From cbdc77476b6d329d035577669c10aacdb9833401 Mon Sep 17 00:00:00 2001 From: Henrik Sandklef Date: Sat, 5 Jul 2025 01:34:49 +0200 Subject: [PATCH 10/16] add description for lic->expression --- licomp_toolkit/toolkit.py | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) diff --git a/licomp_toolkit/toolkit.py b/licomp_toolkit/toolkit.py index de29868..02d7a4f 100644 --- a/licomp_toolkit/toolkit.py +++ b/licomp_toolkit/toolkit.py @@ -178,9 +178,10 @@ def name(self): return cli_name class LicenseExpressionChecker(): - - #TODO: document that this class check - # lic -> expr + """This class can check compatibility between a single outbound + license (e.g GPL-2.0-only) against an inbound license expression + (e.g. MIT OR X11) + """ def __init__(self): self.le_parser = LicenseExpressionParser() From 8ec55d91edd09afd9e47f91c160f2059a2d02c06 Mon Sep 17 00:00:00 2001 From: Henrik Sandklef Date: Sat, 5 Jul 2025 01:37:01 +0200 Subject: [PATCH 11/16] document ExpressionExpressionChecker --- licomp_toolkit/toolkit.py | 8 +++++--- 1 file changed, 5 insertions(+), 3 deletions(-) diff --git a/licomp_toolkit/toolkit.py b/licomp_toolkit/toolkit.py index 02d7a4f..c5ff183 100644 --- a/licomp_toolkit/toolkit.py +++ b/licomp_toolkit/toolkit.py @@ -302,9 +302,11 @@ def summarise_compatibilities(self, operator, operands): class ExpressionExpressionChecker(): - - #TODO: document that this class check - # expr -> expr + """ + This class can check, for compatibility; + * inbound license expression (e.g. MIT OR Apache-2.0) + * against outbound license expression (e.g. GPL-2.0-only OR BSD-2-Clause) + """ def __init__(self): self.le_checker = LicenseExpressionChecker() From e207798c21895ba0904281ccb49f085d0c64e655 Mon Sep 17 00:00:00 2001 From: Henrik Sandklef Date: Tue, 22 Jul 2025 17:26:23 +0200 Subject: [PATCH 12/16] rewrite exec of licomp --- devel/licomp-toolkit | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/devel/licomp-toolkit b/devel/licomp-toolkit index c9ad5d1..f061b27 100755 --- a/devel/licomp-toolkit +++ b/devel/licomp-toolkit @@ -16,4 +16,4 @@ then ARGS="verify -il MIT -ol \"MIT OR X11\"" fi -PYTHONPATH=${EXTRA_PYTHONPATH}:${PYTHONPATH} ${SCRIPT_DIR}/licomp_toolkit/__main__.py $* $ARGS +echo PYTHONPATH=${EXTRA_PYTHONPATH}:${PYTHONPATH} ${SCRIPT_DIR}/licomp_toolkit/__main__.py $* $ARGS | bash From 5f00b09204e8b6af331b921fadd72d0f2fa34e6a Mon Sep 17 00:00:00 2001 From: Henrik Sandklef Date: Tue, 22 Jul 2025 17:27:02 +0200 Subject: [PATCH 13/16] allow -/_ as pypi and without licomp_ --- licomp_toolkit/__main__.py | 11 ++++++++++- 1 file changed, 10 insertions(+), 1 deletion(-) diff --git a/licomp_toolkit/__main__.py b/licomp_toolkit/__main__.py index 4f86bbd..9c128d6 100755 --- a/licomp_toolkit/__main__.py +++ b/licomp_toolkit/__main__.py @@ -49,13 +49,22 @@ def verify(self, args): detailed_report = True resources = args.resources + new_resources = [] unsupported = [] for resource in resources: + if 'licomp' not in resource: + resource = f'licomp_{resource}' + else: + resource = resource.replace('-', '_') + if not self.resource_avilable(resource): unsupported.append(resource) + else: + new_resources.append(resource) if unsupported: - return f'Resources {", ".join(unsupported)} are not supported', ReturnCodes.LICOMP_UNSUPPORTED_RESOURCE.value, True + return f'Resource(s) {", ".join(unsupported)} is/are not supported', ReturnCodes.LICOMP_UNSUPPORTED_RESOURCE.value, True + resources = new_resources expr_checker = ExpressionExpressionChecker() compatibilities = expr_checker.check_compatibility(self.__normalize_license(args.out_license), self.__normalize_license(args.in_license), From 331c1f8ac705706cf77e50a21d085f5eece1a608 Mon Sep 17 00:00:00 2001 From: Henrik Sandklef Date: Tue, 22 Jul 2025 17:27:26 +0200 Subject: [PATCH 14/16] update to reflect new reply format --- licomp_toolkit/data/reply_schema.json | 32 ++++++++++++++++++++++++++- 1 file changed, 31 insertions(+), 1 deletion(-) diff --git a/licomp_toolkit/data/reply_schema.json b/licomp_toolkit/data/reply_schema.json index c64bc04..d02713a 100644 --- a/licomp_toolkit/data/reply_schema.json +++ b/licomp_toolkit/data/reply_schema.json @@ -31,12 +31,42 @@ "description" : "Has the component been modified. Currently not used/implemented.", "enum": [ "unmodified", "modified"] }, + "resources": { + "description": "Which resources are available, regardless of context", + "type": "array", + "items": { + "type" : "string" + } + }, + "available_resources": { + "description": "Which resources are available given the context", + "type": "array", + "items": { + "type" : "string" + } + }, + "unavailable_resources": { + "description": "Which resources are not available given the context", + "type": "array", + "items": { + "type": "object", + "properties": { + "resource": { + "type": "string", + "description": "The resource name" +}, + "reasons": { + "type": "string", + "description": "The reason the resource is not available" } + } + } + }, "compatibility" : { "$ref": "#/$defs/compatibility", "description" : "The inbound license expression." } }, - "required" : [ "compatibility_report", "compatibility", "outbound", "inbound" , "usecase" ], + "required" : [ "compatibility_report", "compatibility", "outbound", "inbound" , "usecase", "resources", "unavailable_resources", "available_resources" ], "additionalProperties" : false, "$defs": { "compatibility_object": { From 57d00cced7ba565c4f8cace1cd75010c1a413a70 Mon Sep 17 00:00:00 2001 From: Henrik Sandklef Date: Tue, 22 Jul 2025 17:27:58 +0200 Subject: [PATCH 15/16] add tests with expr -> expr --- tests/shell/test_validate.sh | 56 +++++++++++++++++++++++++++--------- 1 file changed, 43 insertions(+), 13 deletions(-) diff --git a/tests/shell/test_validate.sh b/tests/shell/test_validate.sh index b0ba71f..12fbc11 100755 --- a/tests/shell/test_validate.sh +++ b/tests/shell/test_validate.sh @@ -13,8 +13,8 @@ fi check_return_value() { - EXPEXcTED=$1 - ACTUAL=$2 + ACTUAL=$1 + EXPECTED=$2 COMMAND="$3" if [ $EXPECTED -ne $ACTUAL ] @@ -36,20 +36,50 @@ validate_reply() { INBOUND="$1" OUTBOUND="$2" - EXPECTED=$3 - PYTHONPATH=$IMPLEMENTATIONS:${PYTHONPATH}:. python3 licomp_toolkit/__main__.py verify -il "$INBOUND" -ol "$OUTBOUND" > $REPLY_FILE + VERIFY_EXPECTED=$3 + VALIDATE_EXPECTED=$4 + PYTHONPATH=$IMPLEMENTAIONS:${PYTHONPATH}:. python3 licomp_toolkit/__main__.py verify -il "$INBOUND" -ol "$OUTBOUND" > $REPLY_FILE + RET=$? +# echo " ---------------------------||||| RET: $RET == $VERIFY_EXPECTED" + printf "%-80s" "verify -il \"$INBOUND\" -ol \"$OUTBOUND\"" + check_return_value $RET $VERIFY_EXPECTED "verify -il \"$INBOUND\" -ol \"$OUTBOUND\"" + echo " OK" + PYTHONPATH=$IMPLEMENTATIONS:${PYTHONPATH}:. python3 licomp_toolkit/__main__.py validate $REPLY_FILE RET=$? - printf "%-75s" "reply from verify -il \"$INBOUND\" -ol \"$OUTBOUND\"" - check_return_value $RET $EXPECTED "validate $REPLY_FILE (verify -il \"$INBOUND\" -ol \"$OUTBOUND\")" - echo OK + printf "\\ %-78s" "validate -il \"$INBOUND\" -ol \"$OUTBOUND\"" + check_return_value $RET $VALIDATE_EXPECTED "validate $REPLY_FILE (verify -il \"$INBOUND\" -ol \"$OUTBOUND\")" + echo " OK" } -validate_reply MIT MIT 0 -validate_reply MIT BSD-3-Clause 0 -validate_reply MIT "BSD-3-Clause OR MIT" 0 -validate_reply "BSD-3-Clause OR MIT" MIT 0 -validate_reply "BSD-3-Clause OR MIT" "X11 AND ISC" 0 -exit +compatibles() +{ + validate_reply MIT MIT 0 0 + validate_reply MIT BSD-3-Clause 0 0 + validate_reply MIT "BSD-3-Clause OR MIT" 0 0 + validate_reply "BSD-3-Clause OR MIT" MIT 0 0 + validate_reply "BSD-3-Clause OR MIT" "BSD-2-Clause AND ISC" 0 0 + validate_reply "BSD-3-Clause OR GPL-2.0-only" "BSD-2-Clause AND ISC" 0 0 + validate_reply "BSD-3-Clause OR GPL-2.0-only" "BSD-2-Clause AND Apache-2.0" 0 0 + validate_reply "BSD-2-Clause OR Apache-2.0" "GPL-2.0-only" 0 0 + validate_reply "Apache-2.0" "GPL-3.0-only" 0 0 +# validate_reply "GPL-3.0-only" "Apache-2.0" 0 0 +} + +incompatibles() +{ + validate_reply GPL-2.0-only MIT 2 0 + validate_reply "BSD-3-Clause AND GPL-2.0-only" "BSD-2-Clause AND ISC" 2 0 + validate_reply "GPL-2.0-only" "BSD-2-Clause AND Apache-2.0" 2 0 + validate_reply "BSD-3-Clause AND GPL-2.0-only" "BSD-2-Clause AND Apache-2.0" 2 0 + validate_reply "GPL-2.0-only" "BSD-2-Clause AND Apache-2.0" 2 0 + validate_reply "BSD-2-Clause AND Apache-2.0" "GPL-2.0-only" 2 0 + validate_reply "Apache-2.0" "GPL-2.0-only" 2 0 +} + +echo "Compatibles" +compatibles +echo "Incompatibles" +incompatibles rm $REPLY_FILE From 3cb3c60a61c61dbe1b6a519cbc37ddff65e555b7 Mon Sep 17 00:00:00 2001 From: Henrik Sandklef Date: Tue, 22 Jul 2025 17:28:25 +0200 Subject: [PATCH 16/16] add support to chose resources --- licomp_toolkit/toolkit.py | 51 +++++++++++++++++++++------------------ 1 file changed, 28 insertions(+), 23 deletions(-) diff --git a/licomp_toolkit/toolkit.py b/licomp_toolkit/toolkit.py index c5ff183..b3466ee 100644 --- a/licomp_toolkit/toolkit.py +++ b/licomp_toolkit/toolkit.py @@ -37,7 +37,7 @@ class LicompToolkit(Licomp): LicompToolkit can check a single license agaisnt another for compatibility, but not license expressions. """ - + def __init__(self): Licomp.__init__(self) self.LICOMP_RESOURCES = {} @@ -121,12 +121,15 @@ def __summarize_compatibility(self, compatibilities, outbound, inbound, usecase, compatibilities['summary']['results'] = results # override top class - def outbound_inbound_compatibility(self, outbound, inbound, usecase, provisioning, resources): + def outbound_inbound_compatibility(self, outbound, inbound, usecase, provisioning, resources=None): logging.debug(f'{inbound} {outbound} ') compatibilities = {} compatibilities['compatibilities'] = [] + if not resources: + resources = self.licomp_resources().keys() + for resource_name in resources: resource = self.licomp_resources()[resource_name] logging.debug(f'-- resource: {resource.name()}') @@ -318,13 +321,25 @@ def __parsed_expression_to_name(self, parsed_expression): def check_compatibility(self, outbound, inbound, usecase, provisioning, resources=None, detailed_report=True): + # Check usecase + try: + usecase_obj = UseCase.string_to_usecase(usecase) + except KeyError: + raise LicompException(f'Usecase {usecase} not supported.', ReturnCodes.LICOMP_UNSUPPORTED_USECASE) + + # Check provisioning + try: + provisioning_obj = Provisioning.string_to_provisioning(provisioning) + except KeyError: + raise LicompException(f'Provisioning {provisioning} not supported.', ReturnCodes.LICOMP_UNSUPPORTED_PROVISIONING) + licomp_resources = list(self.licomp_toolkit.licomp_resources().keys()) if not resources: resources = licomp_resources else: resources = resources - unavailable_resources = {} + unavailable_resources = [] for resource in resources: resource_object = self.licomp_toolkit.licomp_resources()[resource] @@ -339,39 +354,29 @@ def check_compatibility(self, outbound, inbound, usecase, provisioning, resource unavailable_reasons.append(f'Provisioning case "{provisioning}" not supported') if unavailable_reasons: - unavailable_resources[resource] = { + unavailable_resources.append({ + "resource": resource, 'reasons': ", ".join(unavailable_reasons), - } - - available_resources = [resource for resource in resources if resource not in list(unavailable_resources.keys())] - - # Check usecase - try: - usecase = UseCase.string_to_usecase(usecase) - except KeyError: - raise LicompException(f'Usecase {usecase} not supported.', ReturnCodes.LICOMP_UNSUPPORTED_USECASE) + }) - # Check provisioning - try: - provisioning = Provisioning.string_to_provisioning(provisioning) - except KeyError: - raise LicompException(f'Provisioning {provisioning} not supported.', ReturnCodes.LICOMP_UNSUPPORTED_PROVISIONING) + unavailable_resource_keys = [resource['resource'] for resource in unavailable_resources] + available_resources = [resource for resource in resources if resource not in unavailable_resource_keys] inbound_parsed = self.le_parser.parse_license_expression(inbound) outbound_parsed = self.le_parser.parse_license_expression(outbound) - + compatibility_object = self.__check_compatibility(outbound_parsed, inbound_parsed, - usecase, - provisioning, + usecase_obj, + provisioning_obj, resources, detailed_report) return { 'inbound': inbound, 'outbound': outbound, - 'usecase': UseCase.usecase_to_string(usecase), + 'usecase': usecase, 'resources': resources, - 'provisioning': Provisioning.provisioning_to_string(provisioning), + 'provisioning': provisioning, 'compatibility': compatibility_object['compatibility'], 'compatibility_report': compatibility_object, 'unavailable_resources': unavailable_resources,