Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 3 additions & 0 deletions .github/workflows/push.yml
Original file line number Diff line number Diff line change
Expand Up @@ -44,6 +44,9 @@ jobs:
- name: CLI check
run: |
tests/shell/test-cli.sh
tests/shell/test_display_compat.sh
tests/shell/test_returns.sh
tests/shell/test_validate.sh

generate:
runs-on: ubuntu-latest
Expand Down
6 changes: 5 additions & 1 deletion Makefile
Original file line number Diff line number Diff line change
Expand Up @@ -10,13 +10,14 @@ clean:
rm -fr licomp_toolkit/__pycache__
rm -fr tests/python/__pycache__
rm -fr .pytest_cache
rm -f licomp_toolkit_test.tmp tmp.pdf

.PHONY: build
build:
rm -fr build && python3 setup.py sdist

lint:
PYTHONPATH=. flake8 licomp_toolkit
PYTHONPATH=. python3.11 -m flake8 licomp_toolkit

check_version:
@echo -n "Checking api versions: "
Expand All @@ -34,10 +35,13 @@ unit-test-local-verbose:
cli-test:
tests/shell/test-cli.sh
tests/shell/test_returns.sh
tests/shell/test_display_compat.sh
tests/shell/test_validate.sh

cli-test-local:
tests/shell/test-cli.sh --local
tests/shell/test_returns.sh --local
tests/shell/test_display_compat.sh --local

test: unit-test cli-test

Expand Down
26 changes: 24 additions & 2 deletions licomp_toolkit/__main__.py
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,7 @@
from licomp_toolkit.config import epilog
from licomp_toolkit.schema_checker import LicompToolkitSchemaChecker
from licomp_toolkit.suggester import OutboundSuggester
from licomp_toolkit.display_compatibility import DisplayCompatibility

from licomp.main_base import LicompParser
from licomp.interface import UseCase
Expand Down Expand Up @@ -64,7 +65,6 @@ def verify(self, args):
new_resources.append(resource)
if unsupported:
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),
Expand Down Expand Up @@ -141,6 +141,17 @@ def outbound_candidate(self, args):

return formatter.format_licomp_licenses(candidates), ReturnCodes.LICOMP_OK.value, None

def display_compatibility(self, args):
display_compat = DisplayCompatibility(self.licomp_toolkit)
compats = display_compat.display_compatibility(args.licenses,
UseCase.string_to_usecase(args.usecase),
Provisioning.string_to_provisioning(args.provisioning),
args.resources)
formatter = LicompToolkitFormatter.formatter(args.output_format)
formatted = formatter.format_display_compatibilities(compats,
{'discard_unsupported': args.discard_unsupported_licenses})
return formatted, ReturnCodes.LICOMP_OK.value, None

def supports_provisioning(self, args):
try:
provisioning = Provisioning.string_to_provisioning(args.provisioning)
Expand Down Expand Up @@ -183,7 +194,7 @@ def main():
help='keep compatibility report as short as possible',
default=[])

# Command: list supported
# Commands
parser_si = subparsers.add_parser('simplify', help='Normalize and simplify a license expression')
parser_si.set_defaults(which="simplify", func=lct_parser.simplify)
parser_si.add_argument("license", type=str, nargs="+", help='License expression to simplify')
Expand All @@ -203,6 +214,17 @@ def main():
parser_sp.set_defaults(which="supports_provisioning", func=lct_parser.supports_provisioning)
parser_sp.add_argument("provisioning")

parser_dc = subparsers.add_parser('display-compatibility', help='Display the compatibility between the supplied licenses')
parser_dc.set_defaults(which="display_compatibility", func=lct_parser.display_compatibility)
parser_dc.add_argument("licenses",
type=str,
nargs='+',
default=[])
parser_dc.add_argument('-dul', '--discard-unsupported-licenses',
action='store_true',
help='Discard unsupported licenses from the output',
default=False)

parser_ob = subparsers.add_parser('outbound-candidate', help='Identify outbound candidates to a license expression')
parser_ob.set_defaults(which='outbound_candidate', func=lct_parser.outbound_candidate)
parser_ob.add_argument("license_expression")
Expand Down
21 changes: 21 additions & 0 deletions licomp_toolkit/display_compatibility.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
# SPDX-FileCopyrightText: 2024 Henrik Sandklef
#
# SPDX-License-Identifier: GPL-3.0-or-later

class DisplayCompatibility:

def __init__(self, licomp_toolkit):
self.licomp_toolkit = licomp_toolkit

def display_compatibility(self, licenses, usecase, provisioning, resources):
compats = {}
for outbound in licenses:
compats[outbound] = {}
for inbound in licenses:
ret = self.licomp_toolkit.outbound_inbound_compatibility(outbound,
inbound,
usecase,
provisioning,
resources)
compats[outbound][inbound] = ret
return compats
158 changes: 148 additions & 10 deletions licomp_toolkit/format.py
Original file line number Diff line number Diff line change
Expand Up @@ -13,24 +13,47 @@ def formatter(fmt):
return JsonLicompToolkitFormatter()
if fmt.lower() == 'text':
return TextLicompToolkitFormatter()
if fmt.lower() == 'yaml':
return YamlLicompToolkitFormatter()
if fmt.lower() == 'yml':
if fmt.lower() == 'yaml' or fmt.lower() == 'yml':
return YamlLicompToolkitFormatter()
if fmt.lower() == 'dot':
return DotLicompToolkitFormatter()

def format_compatibilities(self, compat):
return None
raise Exception(f'{self.__class__.__name__} cannot format compatibilities.')

def _pre_format_display_compatibilities(self, compats):
licenses = list(compats.keys())
finished = {}
for outbound in licenses:
finished[outbound] = {}
for inbound in licenses:
finished[outbound][inbound] = False

for outbound in licenses:
for inbound in licenses:
if finished[outbound][inbound] and finished[inbound][outbound]:
continue
if outbound == inbound:
finished[outbound][inbound] = ['yes']
finished[inbound][outbound] = ['yes']
else:
outbound_compat = compats[outbound][inbound]['summary']['compatibility_statuses']
inbound_compat = compats[inbound][outbound]['summary']['compatibility_statuses']
finished[outbound][inbound] = list(outbound_compat.keys())
finished[inbound][outbound] = list(inbound_compat.keys())

return finished

def format_licomp_resources(self, licomp_resources):
return None
raise Exception(f'{self.__class__.__name__} cannot format licomp resources.')

def format_licomp_licenses(self, licomp_licenses):
return None
raise Exception(f'{self.__class__.__name__} cannot format licomp licenses.')

def format_licomp_versions(self, licomp_versions):
return None
raise Exception(f'{self.__class__.__name__} cannot format licomp versions.')

class JsonLicompToolkitFormatter():
class JsonLicompToolkitFormatter(LicompToolkitFormatter):

def format_compatibilities(self, compat):
return json.dumps(compat, indent=4)
Expand All @@ -44,7 +67,13 @@ def format_licomp_licenses(self, licomp_licenses):
def format_licomp_versions(self, licomp_versions):
return json.dumps(licomp_versions, indent=4)

class YamlLicompToolkitFormatter():
def format_display_compatibilities(self, compats, settings={}):
# settings
# discard_unsupported: True - will remove unsupported licenses from the output
display_compats = self._pre_format_display_compatibilities(compats)
return json.dumps(display_compats, indent=4)

class YamlLicompToolkitFormatter(LicompToolkitFormatter):

def format_compatibilities(self, compat):
return yaml.safe_dump(compat, indent=4)
Expand All @@ -58,7 +87,11 @@ def format_licomp_licenses(self, licomp_licenses):
def format_licomp_versions(self, licomp_versions):
return yaml.safe_dump(licomp_versions, indent=4)

class TextLicompToolkitFormatter():
def format_display_compatibilities(self, compats, settings={}):
display_compats = self._pre_format_display_compatibilities(compats)
return yaml.safe_dump(display_compats, indent=4)

class TextLicompToolkitFormatter(LicompToolkitFormatter):

def format_licomp_resources(self, licomp_resources):
return "\n".join(licomp_resources)
Expand Down Expand Up @@ -161,3 +194,108 @@ def format_licomp_versions(self, licomp_versions):
for k, v in licomp_versions['licomp-resources'].items():
res.append(f'{k}: {v}')
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())

lines = []
for outbound in licenses:
for inbound in licenses:
lines.append(f'{outbound:30s} {"---->":10s} {inbound:30s}: {", ".join(display_compats[outbound][inbound])}')
return '\n'.join(lines)

class DotLicompToolkitFormatter(LicompToolkitFormatter):

def _compat_line_color(self, compats):
_line_map = {
'unknown': 'style="dotted"',
'depends': 'style="dotted"',
'unsupported': 'style="dotted"',
'mixed': 'style="dotted"',
}
_color_map = {
'yes': 'darkgreen',
'no': 'darkred'
}
same = True
value = None
for compat in compats:
if compat == 'unsupported':
continue
if not value:
value = compat
else:
if compat != value:
same = False

if same:
line = _line_map.get(value, '')
color = _color_map.get(value, 'yellow')
else:
line = _line_map['mixed']
color = 'darkblue'

return line, color

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)
if out_line == in_line and out_color == in_color:
return (f' "{outbound}" -> "{inbound}" [dir="both" color="{out_color}" {out_line}]')
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)
display_compats = self._pre_format_display_compatibilities(compats)
licenses = list(display_compats.keys())

discard_unsupported = settings.get('discard_unsupported')

lines = []
finished = {}
usecase = compats[licenses[0]][licenses[0]]['compatibilities'][0]['usecase']
lines.append('digraph depends {')
lines.append(f' graph [label="License Compatibility Graph ({usecase})" labelloc=t]')
lines.append(' node [shape=plaintext]')
for outbound in licenses:
finished[outbound] = {}
for inbound in licenses:
if inbound not in finished:
finished[inbound] = {}
if inbound == outbound:
continue

if display_compats[outbound][inbound] == []:
if discard_unsupported:
continue
if display_compats[inbound][outbound] == []:
if discard_unsupported:
continue

if finished[outbound].get(inbound, False):
continue
elif finished[inbound].get(outbound, False):
continue

lines.append(self._license_license_compat(outbound, inbound, display_compats[outbound][inbound], display_compats[inbound][outbound]))
finished[outbound][inbound] = True
finished[inbound][outbound] = True

lines.append('}')
return '\n'.join(lines)
5 changes: 2 additions & 3 deletions licomp_toolkit/toolkit.py
Original file line number Diff line number Diff line change
Expand Up @@ -368,16 +368,15 @@ def check_compatibility(self, outbound, inbound, usecase, provisioning, resource

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_obj,
provisioning_obj,
resources,
detailed_report)
return {
'inbound': inbound,
'outbound': outbound,
'inbound': str(inbound),
'outbound': str(outbound),
'usecase': usecase,
'resources': resources,
'provisioning': provisioning,
Expand Down
Loading
Loading