From 1433b8de4c07eb1789794ae7d22fd0cc338a0fcc Mon Sep 17 00:00:00 2001 From: Thomas Graf Date: Sun, 24 Dec 2023 15:50:24 +0100 Subject: [PATCH] feat: add write functionality --- ChangeLog.md | 5 + cli_support/CLI.py | 165 ++++++++--- cli_support/cli_assessment_summary.py | 80 ++++++ cli_support/cli_copyright.py | 28 +- cli_support/cli_export_restriction.py | 32 ++- cli_support/cli_external_id.py | 51 ++++ cli_support/cli_file_item_base.py | 25 +- cli_support/cli_general_information.py | 131 +++++++++ cli_support/cli_irrelevant_files.py | 33 +++ cli_support/cli_license.py | 54 +++- cli_support/cli_obligation.py | 42 ++- cli_support/license_tools.py | 22 +- cli_support/xml_base.py | 34 +++ pyproject.toml | 26 +- show_licenses.py | 21 +- tests/fixtures/CLIXML_COMPLETE_simple.xml | 4 +- tests/fixtures/CLIXML_Full.xml | 85 ++++++ tests/fixtures/CLIXML_MIT_simple.xml | 4 +- tests/fixtures/CLIXML_Minimal.xml | 7 + tests/test_cli_clifile.py | 319 +++++++++++++++++++++- tests/test_cli_licensetools.py | 37 +-- 21 files changed, 1069 insertions(+), 136 deletions(-) create mode 100644 cli_support/cli_assessment_summary.py create mode 100644 cli_support/cli_external_id.py create mode 100644 cli_support/cli_general_information.py create mode 100644 cli_support/cli_irrelevant_files.py create mode 100644 cli_support/xml_base.py create mode 100644 tests/fixtures/CLIXML_Full.xml create mode 100644 tests/fixtures/CLIXML_Minimal.xml diff --git a/ChangeLog.md b/ChangeLog.md index 434e81b..ee990d9 100644 --- a/ChangeLog.md +++ b/ChangeLog.md @@ -1,5 +1,10 @@ # Python library to read Component License Information (CLI) files +## 2.0.0 + +* add CLI write functionality. +* add type information + ## V1.3 (2023-01-10) * relicensed to MIT. diff --git a/cli_support/CLI.py b/cli_support/CLI.py index 4bdf48d..5adc109 100644 --- a/cli_support/CLI.py +++ b/cli_support/CLI.py @@ -1,5 +1,5 @@ # ------------------------------------------------------------------------------- -# (c) 2019-2022 Siemens AG +# (c) 2019-2023 Siemens AG # All Rights Reserved. # Author: thomas.graf@siemens.com # @@ -8,14 +8,34 @@ # ------------------------------------------------------------------------------- import xml.etree.ElementTree as ET +# from typing import List +from typing import Any +from .cli_assessment_summary import CliAssessmentSummary from .cli_copyright import CliCopyright from .cli_export_restriction import CliExportRestriction +from .cli_external_id import CliExternalId +from .cli_general_information import CliGeneralInformation +from .cli_irrelevant_files import CliIrrelevantFiles from .cli_license import CliLicense from .cli_obligation import CliObligation +from .xml_base import XmlBase +# have our own serialization to handle CDATA +ET._original_serialize_xml = ET._serialize_xml # type: ignore -class CliFile: + +def _serialize_xml(write, elem, qnames, namespaces, short_empty_elements, **kwargs) -> Any: # type: ignore + if elem.tag == XmlBase.CDATA_ID: + write("<%s%s]]>" % (elem.tag, elem.text)) + return + return ET._original_serialize_xml(write, elem, qnames, namespaces, short_empty_elements, **kwargs) # type: ignore + + +ET._serialize_xml = ET._serialize["xml"] = _serialize_xml # type: ignore + + +class CliFile(XmlBase): """Encapsulates a CLI file, i.e. all licenses and copyrights found in a component""" @@ -24,27 +44,37 @@ class CliFile: EXPORTRESTRICTIONS_TAG = "ExportRestrictions" OBLIGATION_TAG = "Obligation" TAGS_TAG = "Tags" - - def __init__(self): - self.filename = "" - self.component = "" - self.creator = "" - self.date = "" - self.baseDoc = "" - self.toolUsed = "" - self.componentId = "" - self.includesAcknowledgements = False - self.componentSha1 = "" - self.version = "" - - self.licenses = [] - self.copyrights = [] - self.obligations = [] - self.tags = [] - self.irrelevant_files = [] - self.export_restrictions = [] - - def read_from_file(self, filename: str): + GENERAL_INFORMATION_TAG = "GeneralInformation" + ASSESSMENT_SUMMARY_TAG = "AssessmentSummary" + EXTERNAL_IDS_TAG = "ExternalIds" + IRRELEVANT_FILES_TAG = "IrrelevantFiles" + COMMENT_TAG = "Comment" + + def __init__(self) -> None: + self.filename: str = "" + self.component: str = "" + self.creator: str = "" + self.date: str = "" + self.baseDoc: str = "" + self.toolUsed: str = "" + self.componentId: str = "" + self.includesAcknowledgements: bool = False + self.componentSha1: str = "" + self.version: str = "" + + self.general_information = CliGeneralInformation() + self.assessment_summary = CliAssessmentSummary() + + self.licenses: list[CliLicense] = [] + self.copyrights: list[CliCopyright] = [] + self.obligations: list[CliObligation] = [] + self.tags: list[str] = [] + self.export_restrictions: list[CliExportRestriction] = [] + self.external_ids: list[CliExternalId] = [] + self.irrelevant_files = CliIrrelevantFiles() + self.comment: str = "" + + def read_from_file(self, filename: str) -> None: tree = ET.parse(filename) root = tree.getroot() @@ -61,7 +91,8 @@ def read_from_file(self, filename: str): self.componentId = root.attrib["componentID"] if "includesAcknowledgements" in root.attrib: - self.includesAcknowledgements = root.attrib["includesAcknowledgements"] + if root.attrib["includesAcknowledgements"].lower() == "true": + self.includesAcknowledgements = True if "componentSHA1" in root.attrib: self.componentSha1 = root.attrib["componentSHA1"] @@ -72,33 +103,103 @@ def read_from_file(self, filename: str): for elem in root: if elem.tag == self.LICENSE_TAG: lic = CliLicense() - lic.read_from_element(elem) + lic._read_from_element(elem) self.licenses.append(lic) continue if elem.tag == self.COPYRIGHT_TAG: copyr = CliCopyright() - copyr.read_from_element(elem) + copyr._read_from_element(elem) self.copyrights.append(copyr) continue if elem.tag == self.EXPORTRESTRICTIONS_TAG: restriction = CliExportRestriction() - restriction.read_from_element(elem) + restriction._read_from_element(elem) self.export_restrictions.append(restriction) continue if elem.tag == self.OBLIGATION_TAG: obligation = CliObligation() - obligation.read_from_element(elem) + obligation._read_from_element(elem) self.obligations.append(obligation) continue + if elem.tag == self.GENERAL_INFORMATION_TAG: + self.general_information._read_from_element(elem) + continue + + if elem.tag == self.ASSESSMENT_SUMMARY_TAG: + self.assessment_summary._read_from_element(elem) + continue + + if elem.tag == self.EXTERNAL_IDS_TAG: + for el in elem: + ext_id = CliExternalId() + ext_id._read_from_element(el) + self.external_ids.append(ext_id) + continue + + if elem.tag == self.IRRELEVANT_FILES_TAG: + self.irrelevant_files._read_from_element(elem) + continue + + if elem.tag == self.COMMENT_TAG: + self.comment = self.get_value(elem) + continue + if elem.tag == self.TAGS_TAG: if elem.text is not None: taglist = elem.text.strip() - if "," in taglist: - self.tags = taglist.split(",") - else: - self.tags = taglist.split(" ") + if taglist: + if "," in taglist: + self.tags = taglist.split(",") + else: + self.tags = taglist.split(" ") continue + + def write_to_file(self, filename: str) -> None: + """Write CLI data to a file with the given name.""" + root = ET.Element( + "ComponentLicenseInformation", + component=self.component, + creator=self.creator, + date=self.date, + baseDoc=self.baseDoc, + toolUsed=self.toolUsed, + componentID=self.componentId, + includesAcknowledgements=self.bool2str(self.includesAcknowledgements), + componentSHA1=self.componentSha1, + Version="1.6") + + self.general_information._append_to_xml(root) + self.assessment_summary._append_to_xml(root) + + for license in self.licenses: + license._append_to_xml(root) + + for copyright in self.copyrights: + copyright._append_to_xml(root) + + for obligation in self.obligations: + obligation._append_to_xml(root) + + for expr in self.export_restrictions: + expr._append_to_xml(root) + + self.irrelevant_files._append_to_xml(root) + + extids = ET.SubElement(root, "ExternalIds") + for extid in self.external_ids: + extid._append_to_xml(extids) + + tags = ET.SubElement(root, "Tags") + tags.text = ",".join(str(x) for x in self.tags) + + comment = ET.SubElement(root, "Comment") + cdata = self.CDATA(self.comment) + comment.append(cdata) + + tree = ET.ElementTree(root) + ET.indent(tree) + tree.write(filename, encoding="UTF-8") diff --git a/cli_support/cli_assessment_summary.py b/cli_support/cli_assessment_summary.py new file mode 100644 index 0000000..a7a4b68 --- /dev/null +++ b/cli_support/cli_assessment_summary.py @@ -0,0 +1,80 @@ +# ------------------------------------------------------------------------------- +# (c) 2023 Siemens AG +# All Rights Reserved. +# Author: thomas.graf@siemens.com +# +# Licensed under the MIT license. +# SPDX-License-Identifier: MIT +# ------------------------------------------------------------------------------- + +import xml.etree.ElementTree as ET + +from .xml_base import XmlBase + + +class CliAssessmentSummary(XmlBase): + """Encapsulates the assessment summary.""" + GENERAL_ASSESSMENT_TAG = "GeneralAssessment" + CRITICAL_FILES_TAG = "CriticalFilesFound" + DEPENDENCY_NOTES_TAG = "DependencyNotes" + EXPORT_RESTRICTIONS_TAG = "ExportRestrictionsFound" + USAGE_RESTRICTIONS_TAG = "UsageRestrictionsFound" + ADDITIONAL_NOTES_TAG = "AdditionalNotes" + + def __init__(self) -> None: + self.general_assessment: str = "" + self.critical_files_found: str = "None" + self.dependency_notes: str = "None" + self.export_restrictions_found: str = "None" + self.usage_restrictions_found: str = "None" + self.additional_notes: str = "" + + def _read_from_element(self, element: ET.Element) -> None: + """Read assessment summary from XML element.""" + for elem in element: + if elem.tag == self.GENERAL_ASSESSMENT_TAG: + self.general_assessment = self.get_value(elem) + continue + + if elem.tag == self.CRITICAL_FILES_TAG: + self.critical_files_found = self.get_value(elem) + continue + + if elem.tag == self.DEPENDENCY_NOTES_TAG: + self.dependency_notes = self.get_value(elem) + continue + + if elem.tag == self.EXPORT_RESTRICTIONS_TAG: + self.export_restrictions_found = self.get_value(elem) + continue + + if elem.tag == self.USAGE_RESTRICTIONS_TAG: + self.usage_restrictions_found = self.get_value(elem) + continue + + if elem.tag == self.ADDITIONAL_NOTES_TAG: + self.additional_notes = self.get_value(elem) + continue + + def _append_to_xml(self, parent: ET.Element) -> None: + """Write assessment summary to XML element.""" + gi = ET.SubElement(parent, "AssessmentSummary") + node = ET.SubElement(gi, "GeneralAssessment") + cdata = self.CDATA(self.general_assessment) + node.append(cdata) + + node = ET.SubElement(gi, "CriticalFilesFound") + node.text = self.critical_files_found + + node = ET.SubElement(gi, "DependencyNotes") + node.text = self.dependency_notes + + node = ET.SubElement(gi, "ExportRestrictionsFound") + node.text = self.export_restrictions_found + + node = ET.SubElement(gi, "UsageRestrictionsFound") + node.text = self.usage_restrictions_found + + node = ET.SubElement(gi, "AdditionalNotes") + cdata = self.CDATA(self.additional_notes) + node.append(cdata) diff --git a/cli_support/cli_copyright.py b/cli_support/cli_copyright.py index 5d5641d..02c8e25 100644 --- a/cli_support/cli_copyright.py +++ b/cli_support/cli_copyright.py @@ -1,5 +1,5 @@ # ------------------------------------------------------------------------------- -# (c) 2019-2022 Siemens AG +# (c) 2019-2023 Siemens AG # All Rights Reserved. # Author: thomas.graf@siemens.com # @@ -13,20 +13,30 @@ class CliCopyright(CliFileItemBase): - """Encapsulates a copyright statement""" + """Encapsulates a copyright statement.""" CONTENT_TAG = "Content" - def __init__(self): + def __init__(self) -> None: CliFileItemBase.__init__(self) - self.text = "" - self.files = [] - self.hashes = [] + self.text: str = "" + self.files: list[str] = [] + self.hashes: list[str] = [] - def read_from_element(self, element: ET.Element): - self.read_files_from_element(element) + def _read_from_element(self, element: ET.Element) -> None: + """Read copyright from XML element.""" + self._read_files_from_element(element) for elem in element: - if elem.tag == self.CONTENT_TAG: + if elem.tag == self.CONTENT_TAG and elem.text: if elem.text is not None: self.text = elem.text.strip() continue + + def _append_to_xml(self, parent: ET.Element) -> None: + """Write copyright to XML element.""" + cr = ET.SubElement(parent, "Copyright") + node = ET.SubElement(cr, "Content") + cdata = self.CDATA(self.text) + node.append(cdata) + + CliFileItemBase._append_to_xml(self, cr) diff --git a/cli_support/cli_export_restriction.py b/cli_support/cli_export_restriction.py index 6f1e700..15af493 100644 --- a/cli_support/cli_export_restriction.py +++ b/cli_support/cli_export_restriction.py @@ -16,16 +16,34 @@ class CliExportRestriction(CliFileItemBase): """Encapsulates an export restriction""" CONTENT_TAG = "Content" - CONTENT_TAG = "Content" + COMMENT_TAG = "Comment" - def __init__(self): + def __init__(self) -> None: CliFileItemBase.__init__(self) - self.export_restriction_text = "" - self.export_restriction_comment = "" + self.export_restriction_text: str = "" + self.export_restriction_comment: str = "" - def read_from_element(self, element: ET.Element): - self.read_files_from_element(element) + def _read_from_element(self, element: ET.Element) -> None: + """Read export restriction from XML element.""" + self._read_files_from_element(element) for elem in element: - if elem.tag == self.CONTENT_TAG: + if elem.tag == self.CONTENT_TAG and elem.text: self.export_restriction_text = elem.text.strip() continue + + if elem.tag == self.COMMENT_TAG and elem.text: + self.export_restriction_comment = elem.text.strip() + continue + + def _append_to_xml(self, parent: ET.Element) -> None: + """Write export restriction to XML element.""" + lic = ET.SubElement(parent, "ExportRestrictions") + node = ET.SubElement(lic, "Content") + cdata = self.CDATA(self.export_restriction_text) + node.append(cdata) + + CliFileItemBase._append_to_xml(self, lic) + + node = ET.SubElement(lic, "Comment") + cdata = self.CDATA(self.export_restriction_comment) + node.append(cdata) diff --git a/cli_support/cli_external_id.py b/cli_support/cli_external_id.py new file mode 100644 index 0000000..6b2f8b8 --- /dev/null +++ b/cli_support/cli_external_id.py @@ -0,0 +1,51 @@ +# ------------------------------------------------------------------------------- +# (c) 2023 Siemens AG +# All Rights Reserved. +# Author: thomas.graf@siemens.com +# +# Licensed under the MIT license. +# SPDX-License-Identifier: MIT +# ------------------------------------------------------------------------------- + +import xml.etree.ElementTree as ET + +from .xml_base import XmlBase + + +class CliExternalId(XmlBase): + """Encapsulates an external id""" + + KEY_TAG = "Key" + VALUE_TAG = "Value" + + def __init__(self) -> None: + self.key: str = "" + self.value: str = "" + + def _read_from_element(self, element: ET.Element) -> None: + """Read external id from XML element.""" + for elem in element: + if elem.tag == self.KEY_TAG: + if elem.text: + self.key = elem.text + if self.key: + self.key = self.key.strip() + continue + + if elem.tag == self.VALUE_TAG: + if elem.text: + self.value = elem.text + if self.value: + self.value = self.value.strip() + continue + + def _append_to_xml(self, parent: ET.Element) -> None: + """Write external id to XML element.""" + obl = ET.SubElement(parent, "ExternalId") + node = ET.SubElement(obl, "Key") + cdata = self.CDATA(self.key) + node.append(cdata) + + node = ET.SubElement(obl, "Value") + cdata = self.CDATA(self.value) + node.append(cdata) diff --git a/cli_support/cli_file_item_base.py b/cli_support/cli_file_item_base.py index 3ecbe59..a9a7615 100644 --- a/cli_support/cli_file_item_base.py +++ b/cli_support/cli_file_item_base.py @@ -1,5 +1,5 @@ # ------------------------------------------------------------------------------- -# (c) 2019-2022 Siemens AG +# (c) 2019-2023 Siemens AG # All Rights Reserved. # Author: thomas.graf@siemens.com # @@ -9,18 +9,21 @@ import xml.etree.ElementTree as ET +from .xml_base import XmlBase -class CliFileItemBase: + +class CliFileItemBase(XmlBase): """Common base class to handle files and file hashes""" FILES_TAG = "Files" FILEHASH_TAG = "FileHash" - def __init__(self): - self.files = [] - self.hashes = [] + def __init__(self) -> None: + self.files: list[str] = [] + self.hashes: list[str] = [] - def read_files_from_element(self, element: ET.Element): + def _read_files_from_element(self, element: ET.Element) -> None: + """Read files and hashes from XML element.""" for elem in element: if elem.tag == self.FILES_TAG: if elem.text is not None: @@ -33,3 +36,13 @@ def read_files_from_element(self, element: ET.Element): hashlist = elem.text.strip() self.hashes = hashlist.split() continue + + def _append_to_xml(self, parent: ET.Element) -> None: + """Write files and hashes to XML element.""" + file_data = ET.SubElement(parent, "Files") + cdata = self.CDATA("\n".join(str(x) for x in self.files)) + file_data.append(cdata) + + hash_data = ET.SubElement(parent, "FileHash") + cdata = self.CDATA("\n".join(str(x) for x in self.hashes)) + hash_data.append(cdata) diff --git a/cli_support/cli_general_information.py b/cli_support/cli_general_information.py new file mode 100644 index 0000000..70f6e2c --- /dev/null +++ b/cli_support/cli_general_information.py @@ -0,0 +1,131 @@ +# ------------------------------------------------------------------------------- +# (c) 2023 Siemens AG +# All Rights Reserved. +# Author: thomas.graf@siemens.com +# +# Licensed under the MIT license. +# SPDX-License-Identifier: MIT +# ------------------------------------------------------------------------------- + +import xml.etree.ElementTree as ET + +from .xml_base import XmlBase + + +class CliGeneralInformation(XmlBase): + """Encapsulates the general information""" + REPORTID_TAG = "ReportId" + REVIEWED_BY_TAG = "ReviewedBy" + COMPONENT_NAME_TAG = "ComponentName" + COMMUNITY_TAG = "Community" + COMPONENT_VERSION_TAG = "ComponentVersion" + COMPONENT_HASH_TAG = "ComponentHash" + COMPONENT_RELEAESE_DATE_TAG = "ComponentReleaseDate" + LINK_COMPONENT_MGNT_TAG = "LinkComponentManagement" + LINK_SCAN_TOOL_TAG = "LinkScanTool" + COMPONENT_ID_ELEMENT_TAG = "ComponentId" + COMPONENT_TYPE_TAG = "Type" + COMPONENT_ID_TAG = "Id" + + def __init__(self) -> None: + self.report_id: str = "" + self.reviewed_by: str = "" + self.component_name: str = "" + self.community: str = "" + self.component_version: str = "" + self.component_hash: str = "" + self.component_release_date: str = "" + self.link_component_management: str = "" + self.link_scan_tool: str = "" + self.component_id: str = "" + self.component_id_type: str = "" + + def _read_from_element(self, element: ET.Element) -> None: + """Read general information from XML element.""" + for elem in element: + if elem.tag == self.REPORTID_TAG: + self.report_id = self.get_value(elem) + continue + + if elem.tag == self.REVIEWED_BY_TAG: + self.reviewed_by = self.get_value(elem) + continue + + if elem.tag == self.COMPONENT_NAME_TAG: + self.component_name = self.get_value(elem) + continue + + if elem.tag == self.COMMUNITY_TAG: + self.community = self.get_value(elem) + continue + + if elem.tag == self.COMPONENT_VERSION_TAG: + self.component_version = self.get_value(elem) + continue + + if elem.tag == self.COMPONENT_HASH_TAG: + self.component_hash = self.get_value(elem) + continue + + if elem.tag == self.COMPONENT_RELEAESE_DATE_TAG: + self.component_release_date = self.get_value(elem) + continue + + if elem.tag == self.LINK_COMPONENT_MGNT_TAG: + self.link_component_management = self.get_value(elem) + continue + + if elem.tag == self.LINK_SCAN_TOOL_TAG: + self.link_scan_tool = self.get_value(elem) + continue + + if elem.tag == self.COMPONENT_ID_ELEMENT_TAG: + for el in elem: + if el.tag == self.COMPONENT_TYPE_TAG: + self.component_id_type = self.get_value(el) + continue + + if el.tag == self.COMPONENT_ID_TAG: + self.component_id = self.get_value(el) + continue + continue + + def _append_to_xml(self, parent: ET.Element) -> None: + """Write general information to XML element.""" + gi = ET.SubElement(parent, "GeneralInformation") + node = ET.SubElement(gi, "ReportId") + node.text = self.report_id + + node = ET.SubElement(gi, "ReviewedBy") + node.text = self.reviewed_by + + node = ET.SubElement(gi, "ComponentName") + node.text = self.component_name + + node = ET.SubElement(gi, "Community") + node.text = self.community + + node = ET.SubElement(gi, "ComponentVersion") + node.text = self.component_version + + node = ET.SubElement(gi, "ComponentHash") + node.text = self.component_hash + + node = ET.SubElement(gi, "ComponentReleaseDate") + node.text = self.component_release_date + + hash_data = ET.SubElement(gi, "LinkComponentManagement") + cdata = self.CDATA(self.link_component_management) + hash_data.append(cdata) + + hash_data = ET.SubElement(gi, "LinkScanTool") + cdata = self.CDATA(self.link_scan_tool) + hash_data.append(cdata) + + ci = ET.SubElement(gi, "ComponentId") + + node = ET.SubElement(ci, "Type") + node.text = self.component_id_type + + node = ET.SubElement(ci, "Id") + node.text = self.component_id diff --git a/cli_support/cli_irrelevant_files.py b/cli_support/cli_irrelevant_files.py new file mode 100644 index 0000000..eb90c77 --- /dev/null +++ b/cli_support/cli_irrelevant_files.py @@ -0,0 +1,33 @@ +# ------------------------------------------------------------------------------- +# (c) 2023 Siemens AG +# All Rights Reserved. +# Author: thomas.graf@siemens.com +# +# Licensed under the MIT license. +# SPDX-License-Identifier: MIT +# ------------------------------------------------------------------------------- + +import xml.etree.ElementTree as ET + +from .cli_file_item_base import CliFileItemBase + + +class CliIrrelevantFiles(CliFileItemBase): + """Encapsulates the irrelevant files.""" + + CONTENT_TAG = "Content" + + def __init__(self) -> None: + CliFileItemBase.__init__(self) + self.files: list[str] = [] + self.hashes: list[str] = [] + + def _read_from_element(self, element: ET.Element) -> None: + """Read irrelevant files from XML element.""" + self._read_files_from_element(element) + + def _append_to_xml(self, parent: ET.Element) -> None: + """Write irrelevant files to XML element.""" + cr = ET.SubElement(parent, "IrrelevantFiles") + + CliFileItemBase._append_to_xml(self, cr) diff --git a/cli_support/cli_license.py b/cli_support/cli_license.py index 443d254..b897c75 100644 --- a/cli_support/cli_license.py +++ b/cli_support/cli_license.py @@ -1,5 +1,5 @@ # ------------------------------------------------------------------------------- -# (c) 2019-2022 Siemens AG +# (c) 2019-2023 Siemens AG # All Rights Reserved. # Author: thomas.graf@siemens.com # @@ -13,31 +13,32 @@ class CliLicense(CliFileItemBase): - """Encapsulates a license""" + """Encapsulates a license.""" CONTENT_TAG = "Content" ACKNOWLEDGEMENTS_TAG = "Acknowledgements" TAGS_TAG = "Tags" - def __init__(self): + def __init__(self) -> None: CliFileItemBase.__init__(self) - self.license_text = "" - self.type = "" - self.name = "" - self.spdx_identifier = "" - self.acknowledgements = [] - self.tags = [] - self.files = [] - self.hashes = [] - - def read_from_element(self, element: ET.Element): + self.license_text: str = "" + self.type: str = "" + self.name: str = "" + self.spdx_identifier: str = "" + self.acknowledgements: list[str] = [] + self.tags: list[str] = [] + self.files: list[str] = [] + self.hashes: list[str] = [] + + def _read_from_element(self, element: ET.Element) -> None: + """Read license from XML element.""" self.type = element.attrib["type"] self.name = element.attrib["name"] if "spdxidentifier" in element.attrib: self.spdx_identifier = element.attrib["spdxidentifier"] - self.read_files_from_element(element) + self._read_files_from_element(element) for elem in element: if elem.tag == self.CONTENT_TAG: @@ -55,6 +56,29 @@ def read_from_element(self, element: ET.Element): taglist = elem.text.strip() if "," in taglist: self.tags = taglist.split(",") - else: + elif " " in taglist: self.tags = taglist.split(" ") + else: + self.tags.append(taglist) continue + + def _append_to_xml(self, parent: ET.Element) -> None: + """Write license to XML element.""" + lic = ET.SubElement( + parent, + "License", + type=self.type, + name=self.name, + spdxidentifier=self.spdx_identifier) + node = ET.SubElement(lic, "Content") + cdata = self.CDATA(self.license_text) + node.append(cdata) + + CliFileItemBase._append_to_xml(self, lic) + + ack = ET.SubElement(lic, "Acknowledgements") + cdata = self.CDATA("\n".join(str(x) for x in self.acknowledgements)) + ack.append(cdata) + + tags = ET.SubElement(lic, "Tags") + tags.text = ",".join(str(x) for x in self.tags) diff --git a/cli_support/cli_obligation.py b/cli_support/cli_obligation.py index 826d796..e4824aa 100644 --- a/cli_support/cli_obligation.py +++ b/cli_support/cli_obligation.py @@ -1,5 +1,5 @@ # ------------------------------------------------------------------------------- -# (c) 2019-2022 Siemens AG +# (c) 2019-2023 Siemens AG # All Rights Reserved. # Author: thomas.graf@siemens.com # @@ -9,8 +9,10 @@ import xml.etree.ElementTree as ET +from .xml_base import XmlBase -class CliObligation: + +class CliObligation(XmlBase): """Encapsulates an obligation""" TOPIC_TAG = "Topic" @@ -18,27 +20,47 @@ class CliObligation: LICENSES_TAG = "Licenses" LICENSE_TAG = "License" - def __init__(self): - self.text = "" - self.topic = "" - self.licenses = [] + def __init__(self) -> None: + self.text: str = "" + self.topic: str = "" + self.licenses: list[str] = [] - def read_from_element(self, element: ET.Element): + def _read_from_element(self, element: ET.Element) -> None: + """Read license from XML element.""" for elem in element: if elem.tag == self.TOPIC_TAG: - self.topic = elem.text + if elem.text: + self.topic = elem.text if self.topic: self.topic = self.topic.strip() continue if elem.tag == self.TEXT_TAG: - self.text = elem.text + if elem.text: + self.text = elem.text if self.text: self.text = self.text.strip() continue if elem.tag == self.LICENSES_TAG: for elem2 in elem: - if elem2.tag == self.LICENSE_TAG: + if elem2.tag == self.LICENSE_TAG and elem2.text: self.licenses.append(elem2.text.strip()) continue + + def _append_to_xml(self, parent: ET.Element) -> None: + """Write license to XML element.""" + obl = ET.SubElement(parent, "Obligation") + node = ET.SubElement(obl, "Topic") + cdata = self.CDATA(self.topic) + node.append(cdata) + + node = ET.SubElement(obl, "Text") + cdata = self.CDATA(self.text) + node.append(cdata) + + licenses = ET.SubElement(obl, "Licenses") + for license in self.licenses: + lic = ET.SubElement(licenses, "License") + cdata = self.CDATA(license) + lic.append(cdata) diff --git a/cli_support/license_tools.py b/cli_support/license_tools.py index 90d7c7b..e6343d0 100644 --- a/cli_support/license_tools.py +++ b/cli_support/license_tools.py @@ -7,6 +7,8 @@ # SPDX-License-Identifier: MIT # ------------------------------------------------------------------------------- +from typing import Optional + from .CLI import CliFile from .cli_license import CliLicense @@ -20,7 +22,7 @@ class LicenseTools: MANUAL_CHECK_NEEDED_TAG = "MANUAL_CHECK_NEEDED" @staticmethod - def get_global_license(clifile: CliFile) -> CliLicense: + def get_global_license(clifile: CliFile) -> Optional[CliLicense]: """Determines the global license.""" for lic in clifile.licenses: if lic.type.upper() == "GLOBAL": @@ -29,9 +31,9 @@ def get_global_license(clifile: CliFile) -> CliLicense: return None @staticmethod - def get_non_global_licenses(clifile: CliFile) -> list: + def get_non_global_licenses(clifile: CliFile) -> list[CliLicense]: """Gets the non global licenses.""" - result = [] + result: list[CliLicense] = [] for lic in clifile.licenses: if lic.type.upper() != "GLOBAL": result.append(lic) @@ -39,7 +41,7 @@ def get_non_global_licenses(clifile: CliFile) -> list: return result @staticmethod - def has_license(clifile: CliFile, spdx_identifier: str): + def has_license(clifile: CliFile, spdx_identifier: str) -> bool: """Determines whether the specified component has the specified license.""" for lic in clifile.licenses: @@ -91,7 +93,7 @@ def is_multi_license(spdx_identifier: str) -> bool: return False @staticmethod - def is_do_not_use_license(license: CliLicense): + def is_do_not_use_license(license: CliLicense) -> bool: """Determines whether this license is a 'do not use' license.""" typeUpper = license.type.upper() if typeUpper == "OTHER_RED": @@ -104,7 +106,7 @@ def is_do_not_use_license(license: CliLicense): return False @staticmethod - def has_multi_license(clifile: CliFile): + def has_multi_license(clifile: CliFile) -> bool: """Determines whether this component has at least one multi license.""" for lic in clifile.licenses: if LicenseTools.is_multi_license(lic.spdx_identifier): @@ -113,7 +115,7 @@ def has_multi_license(clifile: CliFile): return False @staticmethod - def has_do_not_use_files(clifile: CliFile): + def has_do_not_use_files(clifile: CliFile) -> bool: """Determines whether this component has at least one 'do not use' license/file.""" for lic in clifile.licenses: @@ -123,7 +125,7 @@ def has_do_not_use_files(clifile: CliFile): return False @staticmethod - def has_source_code_shipping_license(clifile: CliFile): + def has_source_code_shipping_license(clifile: CliFile) -> bool: """Determines whether this component has at least one license where the source code needs to get shipped.""" for lic in clifile.licenses: @@ -133,7 +135,7 @@ def has_source_code_shipping_license(clifile: CliFile): return False @staticmethod - def license_has_not_readme_tag(license: CliLicense): + def license_has_not_readme_tag(license: CliLicense) -> bool: """Determines whether the specified item has a 'not for Readme_OSS' tag.""" for tag in license.tags: @@ -143,7 +145,7 @@ def license_has_not_readme_tag(license: CliLicense): return False @staticmethod - def component_has_not_readme_tag(clifile: CliFile): + def component_has_not_readme_tag(clifile: CliFile) -> bool: """Determines whether the specified item has a 'not for Readme_OSS' tag.""" for tag in clifile.tags: diff --git a/cli_support/xml_base.py b/cli_support/xml_base.py new file mode 100644 index 0000000..336f409 --- /dev/null +++ b/cli_support/xml_base.py @@ -0,0 +1,34 @@ +# ------------------------------------------------------------------------------- +# (c) 2023 Siemens AG +# All Rights Reserved. +# Author: thomas.graf@siemens.com +# +# Licensed under the MIT license. +# SPDX-License-Identifier: MIT +# ------------------------------------------------------------------------------- + +import xml.etree.ElementTree as ET + + +class XmlBase: + """Common base class to XML serialization.""" + CDATA_ID = "![CDATA[" + + def CDATA(self, text: str = "") -> ET.Element: + """Helper method to allow serialization of CDATA.""" + element = ET.Element(self.CDATA_ID) + element.text = text + return element + + def bool2str(self, value: bool) -> str: + """Convert bool to string for XML export.""" + if value: + return "true" + + return "false" + + def get_value(self, elem: ET.Element) -> str: + if elem.text is not None: + return elem.text.strip() + + return "" diff --git a/pyproject.toml b/pyproject.toml index c9ea922..05d1693 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -1,6 +1,6 @@ [tool.poetry] name = "cli_support" -version = "1.3" +version = "2.0.pre1" description = "Support component license information (CLI) files" authors = ["Thomas Graf "] license = "MIT" @@ -28,3 +28,27 @@ coverage = ">=6.5.0" [build-system] requires = ["poetry>=0.12"] build-backend = "poetry.masonry.api" + +[tool.mypy] +exclude = [ + '/tests', +] +show_error_codes = true +pretty = true + +warn_unreachable = true +allow_redefinition = false + +### Strict mode ### +warn_unused_configs = true +disallow_subclassing_any = true +disallow_any_generics = true +disallow_untyped_calls = true +disallow_untyped_defs = true +disallow_incomplete_defs = true +check_untyped_defs = true +disallow_untyped_decorators = true +no_implicit_optional = true +warn_redundant_casts = true +warn_unused_ignores = true +no_implicit_reexport = true diff --git a/show_licenses.py b/show_licenses.py index c9bcf32..77e154c 100644 --- a/show_licenses.py +++ b/show_licenses.py @@ -16,7 +16,10 @@ """ import sys -from colorama import init, Fore +from typing import Any, List + +from colorama import Fore, init + import cli_support # initialize colorama @@ -25,12 +28,12 @@ class ShowLicenses(): """Application class""" - def __init__(self): - self.cli_filename = "" - self.global_license_list = [] + def __init__(self) -> None: + self.cli_filename: str = "" + self.global_license_list: List[str] = [] @classmethod - def print_license_list(cls, license_list): + def print_license_list(cls, license_list: List[str]) -> None: """Displays the licenses color-coded""" for lic in license_list: color = Fore.RESET @@ -54,7 +57,7 @@ def print_license_list(cls, license_list): print(Fore.RESET) - def process_cli_file(self, cli_filename): + def process_cli_file(self, cli_filename: str) -> None: """Processes a single CLI file""" clifile = cli_support.CLI.CliFile() @@ -67,13 +70,13 @@ def process_cli_file(self, cli_filename): print(Fore.RESET) return - license_list = [] + license_list: List[str] = [] for lic in clifile.licenses: license_list.append(lic.name) if lic.name not in self.global_license_list: self.global_license_list.append(lic.name) - def process_commandline(self, argv): + def process_commandline(self, argv: Any) -> Any: """Reads the command line arguments""" if len(argv) < 2: sys.exit( @@ -83,7 +86,7 @@ def process_commandline(self, argv): self.cli_filename = argv[1] - def main(self): + def main(self) -> None: """Main method()""" print("\nShow all licenses and copyrights of a component:\n") self.process_commandline(sys.argv) diff --git a/tests/fixtures/CLIXML_COMPLETE_simple.xml b/tests/fixtures/CLIXML_COMPLETE_simple.xml index c7bd908..8367129 100644 --- a/tests/fixtures/CLIXML_COMPLETE_simple.xml +++ b/tests/fixtures/CLIXML_COMPLETE_simple.xml @@ -1,5 +1,5 @@  - + 96819AA1-7466-42BF-9FA0-DCCE10272D09 mary.jane@cli.com @@ -158,4 +158,4 @@ Tests/UnitTests/Tethys.Test/Properties/AssemblyInfo.cs]]> SPDX,CREATED_FROM_SPDX TestComment - \ No newline at end of file + diff --git a/tests/fixtures/CLIXML_Full.xml b/tests/fixtures/CLIXML_Full.xml new file mode 100644 index 0000000..398e47e --- /dev/null +++ b/tests/fixtures/CLIXML_Full.xml @@ -0,0 +1,85 @@ + + + 112233 + TG + MyComp + Me + 1.2.3 + DEADBEEF + 2023-12-23 + + + + + + package-url + pkg:npm/%40angular/animation@12.3.1 + + + + + + None + yes + yes + yes + + + + + + + + + + + + + OSS + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + t1,OSS + + + diff --git a/tests/fixtures/CLIXML_MIT_simple.xml b/tests/fixtures/CLIXML_MIT_simple.xml index 72734bc..18fcfee 100644 --- a/tests/fixtures/CLIXML_MIT_simple.xml +++ b/tests/fixtures/CLIXML_MIT_simple.xml @@ -26,7 +26,7 @@ ngrx-store.zip/store/schematics-core/utility/change.ts]]> - SPDX,CREATED_FROM_SPDX + SPDX CREATED_FROM_SPDX TestComment - \ No newline at end of file + diff --git a/tests/fixtures/CLIXML_Minimal.xml b/tests/fixtures/CLIXML_Minimal.xml new file mode 100644 index 0000000..1a57d0f --- /dev/null +++ b/tests/fixtures/CLIXML_Minimal.xml @@ -0,0 +1,7 @@ + + + + + + + diff --git a/tests/test_cli_clifile.py b/tests/test_cli_clifile.py index 589000f..808791b 100644 --- a/tests/test_cli_clifile.py +++ b/tests/test_cli_clifile.py @@ -1,5 +1,5 @@ # ------------------------------------------------------------------------------- -# (c) 2022 Siemens AG +# (c) 2022-2023 Siemens AG # All Rights Reserved. # Author: thomas.graf@siemens.com # @@ -7,16 +7,44 @@ # SPDX-License-Identifier: MIT # ------------------------------------------------------------------------------- +import os import unittest +from typing import List -from cli_support import CliFile +from cli_support import (CliCopyright, CliExportRestriction, CliExternalId, + CliFile, CliIrrelevantFiles, CliLicense, + CliObligation) class CliFileTest(unittest.TestCase): - TESTFILE1 = "test/testfiles/CLIXML_MIT_simple.xml" - TESTFILE2 = "test/testfiles/CLIXML_COMPLETE_simple.xml" + TESTFILE1 = "tests/fixtures/CLIXML_MIT_simple.xml" + TESTFILE2 = "tests/fixtures/CLIXML_COMPLETE_simple.xml" + TESTFILE3 = "tests/fixtures/CLIXML_Minimal.xml" + TESTFILE4 = "dummy.xml" + TESTFILE5 = "tests/fixtures/CLIXML_Full.xml" - def test_constructor(self): + @staticmethod + def delete_file(filename: str) -> None: + """Delete the given file.""" + try: + if os.path.exists(filename): + os.remove(filename) + except Exception as ex: + print("Error removing file:", filename, repr(ex)) + + def compare_file_to_text(self, filename: str, expected: List[str]) -> None: + with open(filename) as file: + data = file.readlines() + + self.assertEqual(len(data), len(expected)) + index = 0 + for line in data: + t1 = line.strip() + t2 = expected[index].strip() + self.assertEqual(t1, t2) + index += 1 + + def test_constructor(self) -> None: lib = CliFile() self.assertEqual("", lib.filename) self.assertEqual("", lib.component) @@ -38,11 +66,10 @@ def test_constructor(self): self.assertIsNotNone(lib.tags) self.assertEqual(0, len(lib.tags)) self.assertIsNotNone(lib.irrelevant_files) - self.assertEqual(0, len(lib.irrelevant_files)) self.assertIsNotNone(lib.export_restrictions) self.assertEqual(0, len(lib.export_restrictions)) - def test_read_from_file_simple(self): + def test_read_from_file_simple(self) -> None: lib = CliFile() lib.read_from_file(self.TESTFILE1) self.assertEqual(self.TESTFILE1, lib.filename) @@ -52,7 +79,7 @@ def test_read_from_file_simple(self): self.assertEqual("/srv/fotransfer/repository/report", lib.baseDoc) self.assertEqual("SpdxToCliConverter", lib.toolUsed) self.assertEqual("12345", lib.componentId) - self.assertEqual("false", lib.includesAcknowledgements) + self.assertEqual(False, lib.includesAcknowledgements) self.assertEqual("5243c0eac8c674732802731670657c93855f4071", lib.componentSha1) self.assertEqual("1.4", lib.version) @@ -69,12 +96,11 @@ def test_read_from_file_simple(self): self.assertEqual(2, len(lib.tags)) self.assertIsNotNone(lib.irrelevant_files) - self.assertEqual(0, len(lib.irrelevant_files)) self.assertIsNotNone(lib.export_restrictions) self.assertEqual(0, len(lib.export_restrictions)) - def test_read_from_file_complete(self): + def test_read_from_file_complete(self) -> None: lib = CliFile() lib.read_from_file(self.TESTFILE2) self.assertEqual(self.TESTFILE2, lib.filename) @@ -84,16 +110,14 @@ def test_read_from_file_complete(self): self.assertEqual("/srv/fotransfer/repository/report", lib.baseDoc) self.assertEqual("SpdxToCliConverter", lib.toolUsed) self.assertEqual("12345", lib.componentId) - self.assertEqual("false", lib.includesAcknowledgements) + self.assertEqual(True, lib.includesAcknowledgements) self.assertEqual("5243c0eac8c674732802731670657c93855f4071", lib.componentSha1) self.assertEqual("1.5", lib.version) self.assertIsNotNone(lib.licenses) self.assertEqual(2, len(lib.licenses)) self.assertEqual("MIT License", lib.licenses[0].name) - self.assertEqual("MIT License", lib.licenses[0].name) - self.assertEqual("MIT License", lib.licenses[0].name) - self.assertEqual("MIT License", lib.licenses[0].name) + self.assertEqual("Apache-2.0", lib.licenses[1].name) self.assertIsNotNone(lib.copyrights) self.assertEqual(1, len(lib.copyrights)) @@ -105,7 +129,272 @@ def test_read_from_file_complete(self): self.assertEqual(2, len(lib.tags)) self.assertIsNotNone(lib.irrelevant_files) - self.assertEqual(0, len(lib.irrelevant_files)) self.assertIsNotNone(lib.export_restrictions) self.assertEqual(1, len(lib.export_restrictions)) + + def test_read_from_file_minimal(self) -> None: + lib = CliFile() + lib.read_from_file(self.TESTFILE3) + self.assertEqual(self.TESTFILE3, lib.filename) + self.assertEqual("TestFileX", lib.component) + self.assertEqual("john.doe@cli.com", lib.creator) + self.assertEqual("2022-03-08", lib.date) + self.assertEqual("/srv/fotransfer/repository/report", lib.baseDoc) + self.assertEqual("SpdxToCliConverter", lib.toolUsed) + self.assertEqual("12345", lib.componentId) + self.assertEqual(False, lib.includesAcknowledgements) + self.assertEqual("5243c0eac8c674732802731670657c93855f4071", lib.componentSha1) + self.assertEqual("1.4", lib.version) + + self.assertIsNotNone(lib.licenses) + self.assertEqual(0, len(lib.licenses)) + + self.assertIsNotNone(lib.copyrights) + self.assertEqual(0, len(lib.copyrights)) + + self.assertIsNotNone(lib.obligations) + self.assertEqual(0, len(lib.obligations)) + + self.assertIsNotNone(lib.tags) + self.assertEqual(0, len(lib.tags)) + + self.assertIsNotNone(lib.irrelevant_files) + + self.assertIsNotNone(lib.export_restrictions) + self.assertEqual(0, len(lib.export_restrictions)) + + self.assertIsNotNone(lib.general_information) + + self.assertIsNotNone(lib.assessment_summary) + + def test_write_file_minimal(self) -> None: + first = '\n' + expected = first + ''' + + + + + + + + + + + + + + + + + + + + None + None + None + None + + + + + + + + + + + + + +''' + + self.delete_file(self.TESTFILE4) + lib = CliFile() + lib.write_to_file(self.TESTFILE4) + + lines_expected = expected.splitlines() + self.compare_file_to_text(self.TESTFILE4, lines_expected) + + self.delete_file(self.TESTFILE4) + + def test_write_file(self) -> None: + self.delete_file(self.TESTFILE4) + lib = CliFile() + + lib.filename = "CLIXML_Full.xml" + lib.component = "MyComp, 1.2.3" + lib.creator = "Me" + lib.date = "2023-12-23" + lib.baseDoc = "Some doc" + lib.toolUsed = "clipython" + lib.componentId = "007" + lib.includesAcknowledgements = False + lib.componentSha1 = "dead" + lib.version = "1.6" + + lib.general_information.report_id = "112233" + lib.general_information.reviewed_by = "TG" + lib.general_information.component_name = "MyComp" + lib.general_information.community = "Me" + lib.general_information.component_version = "1.2.3" + lib.general_information.component_hash = "DEADBEEF" + lib.general_information.link_component_management = "https://anylink.com" + lib.general_information.link_scan_tool = "https://anothertools.com" + lib.general_information.component_id_type = "package-url" + lib.general_information.component_id = "pkg:npm/%40angular/animation@12.3.1" + lib.general_information.component_release_date = "2023-12-23" + + lib.assessment_summary.general_assessment = "abc" + lib.assessment_summary.critical_files_found = "None" + lib.assessment_summary.dependency_notes = "yes" + lib.assessment_summary.export_restrictions_found = "yes" + lib.assessment_summary.usage_restrictions_found = "yes" + lib.assessment_summary.additional_notes = "xyz" + + license = CliLicense() + license.license_text = "some text" + license.name = "Apache Software License, Version 2.0" + license.spdx_identifier = "Apache-2.0" + license.type = "global" + license.acknowledgements.append("Apache NOTICE file...") + license.tags.append("OSS") + license.files.append("license1.txt") + license.hashes.append("12AB") + lib.licenses.append(license) + + copyright = CliCopyright() + copyright.text = "Copyright (c) by me" + copyright.files.append("copyright1.txt") + copyright.hashes.append("12CD") + lib.copyrights.append(copyright) + + obligation = CliObligation() + obligation.text = "you have to" + obligation.topic = "obligation" + obligation.licenses.append("AGPL-9.0") + lib.obligations.append(obligation) + + lib.tags.append("t1") + lib.tags.append("OSS") + + export_restriction = CliExportRestriction() + export_restriction.export_restriction_text = "ECCN=N" + export_restriction.export_restriction_comment = "This is no fake!" + export_restriction.files.append("export_restriction1.txt") + export_restriction.hashes.append("12EF") + lib.export_restrictions.append(export_restriction) + + external_id = CliExternalId() + external_id.key = "category" + external_id.value = "fun" + lib.external_ids.append(external_id) + + lib.irrelevant_files = CliIrrelevantFiles() + lib.irrelevant_files.files.append("ir1.txt") + lib.irrelevant_files.hashes.append("12AB") + lib.comment = "some comment" + + lib.write_to_file(self.TESTFILE4) + + with open(self.TESTFILE5) as file: + expected = file.readlines() + self.compare_file_to_text(self.TESTFILE4, expected) + + self.delete_file(self.TESTFILE4) + + def test_read_from_full_file(self) -> None: + lib = CliFile() + lib.read_from_file(self.TESTFILE5) + self.assertEqual(self.TESTFILE5, lib.filename) + + self.assertEqual(lib.component, "MyComp, 1.2.3") + self.assertEqual(lib.creator, "Me") + self.assertEqual(lib.date, "2023-12-23") + self.assertEqual(lib.baseDoc, "Some doc") + self.assertEqual(lib.toolUsed, "clipython") + self.assertEqual(lib.componentId, "007") + self.assertEqual(lib.includesAcknowledgements, False) + self.assertEqual(lib.componentSha1, "dead") + self.assertEqual(lib.version, "1.6") + + self.assertEqual(lib.general_information.report_id, "112233") + self.assertEqual(lib.general_information.reviewed_by, "TG") + self.assertEqual(lib.general_information.component_name, "MyComp") + self.assertEqual(lib.general_information.community, "Me") + self.assertEqual(lib.general_information.component_version, "1.2.3") + self.assertEqual(lib.general_information.component_hash, "DEADBEEF") + self.assertEqual(lib.general_information.link_component_management, "https://anylink.com") + self.assertEqual(lib.general_information.link_scan_tool, "https://anothertools.com") + self.assertEqual(lib.general_information.component_id_type, "package-url") + self.assertEqual(lib.general_information.component_id, "pkg:npm/%40angular/animation@12.3.1") + self.assertEqual(lib.general_information.component_release_date, "2023-12-23") + + self.assertEqual(lib.assessment_summary.general_assessment, "abc") + self.assertEqual(lib.assessment_summary.critical_files_found, "None") + self.assertEqual(lib.assessment_summary.dependency_notes, "yes") + self.assertEqual(lib.assessment_summary.export_restrictions_found, "yes") + self.assertEqual(lib.assessment_summary.usage_restrictions_found, "yes") + self.assertEqual(lib.assessment_summary.additional_notes, "xyz") + + self.assertEqual(len(lib.licenses), 1) + license = lib.licenses[0] + self.assertEqual(license.license_text, "some text") + self.assertEqual(license.name, "Apache Software License, Version 2.0") + self.assertEqual(license.spdx_identifier, "Apache-2.0") + self.assertEqual(license.type, "global") + self.assertEqual(len(license.acknowledgements), 1) + self.assertEqual(license.acknowledgements[0], "Apache NOTICE file...") + self.assertEqual(len(license.tags), 1) + self.assertEqual(license.tags[0], "OSS") + self.assertEqual(len(license.files), 1) + self.assertEqual(license.files[0], "license1.txt") + self.assertEqual(len(license.hashes), 1) + self.assertEqual(license.hashes[0], "12AB") + + self.assertEqual(len(lib.copyrights), 1) + copyright = lib.copyrights[0] + self.assertEqual(copyright.text, "Copyright (c) by me") + self.assertEqual(len(copyright.files), 1) + self.assertEqual(copyright.files[0], "copyright1.txt") + self.assertEqual(len(copyright.hashes), 1) + self.assertEqual(copyright.hashes[0], "12CD") + + self.assertEqual(len(lib.obligations), 1) + obligation = lib.obligations[0] + self.assertEqual(obligation.text, "you have to") + self.assertEqual(obligation.topic, "obligation") + self.assertEqual(len(obligation.licenses), 1) + self.assertEqual(obligation.licenses[0], "AGPL-9.0") + + self.assertEqual(len(lib.tags), 2) + lib.tags.append("t1") + lib.tags.append("OSS") + + self.assertEqual(len(lib.export_restrictions), 1) + export_restriction = lib.export_restrictions[0] + self.assertEqual(export_restriction.export_restriction_text, "ECCN=N") + self.assertEqual(export_restriction.export_restriction_comment, "This is no fake!") + self.assertEqual(len(export_restriction.files), 1) + self.assertEqual(export_restriction.files[0], "export_restriction1.txt") + self.assertEqual(len(export_restriction.hashes), 1) + self.assertEqual(export_restriction.hashes[0], "12EF") + + self.assertEqual(len(lib.external_ids), 1) + external_id = lib.external_ids[0] + self.assertEqual(external_id.key, "category") + self.assertEqual(external_id.value, "fun") + + self.assertIsNotNone(lib.irrelevant_files) + self.assertEqual(len(lib.irrelevant_files.files), 1) + self.assertEqual(lib.irrelevant_files.files[0], "ir1.txt") + self.assertEqual(len(lib.irrelevant_files.hashes), 1) + self.assertEqual(lib.irrelevant_files.hashes[0], "12AB") + + self.assertEqual(lib.comment, "some comment") + + +if __name__ == "__main__": + APP = CliFileTest() + APP.test_write_file() diff --git a/tests/test_cli_licensetools.py b/tests/test_cli_licensetools.py index 5577f3c..1c12745 100644 --- a/tests/test_cli_licensetools.py +++ b/tests/test_cli_licensetools.py @@ -1,5 +1,5 @@ # ------------------------------------------------------------------------------- -# (c) 2022 Siemens AG +# (c) 2022-2023 Siemens AG # All Rights Reserved. # Author: thomas.graf@siemens.com # @@ -14,21 +14,22 @@ class LicenseToolTest(unittest.TestCase): - TESTFILE1 = "test/testfiles/CLIXML_MIT_simple.xml" - TESTFILE2 = "test/testfiles/CLIXML_COMPLETE_simple.xml" - TESTFILE3 = "test/testfiles/CLIXML_Licenses_source_shipping.xml" - TESTFILE4 = "test/testfiles/CLIXML_Licenses_multi.xml" + TESTFILE1 = "tests/fixtures/CLIXML_MIT_simple.xml" + TESTFILE2 = "tests/fixtures/CLIXML_COMPLETE_simple.xml" + TESTFILE3 = "tests/fixtures/CLIXML_Licenses_source_shipping.xml" + TESTFILE4 = "tests/fixtures/CLIXML_Licenses_multi.xml" - def test_get_global_license(self): + def test_get_global_license(self) -> None: cli = CliFile() cli.read_from_file(self.TESTFILE2) actual = LicenseTools.get_global_license(cli) self.assertIsNotNone(actual) - self.assertEqual("MIT", actual.spdx_identifier) - self.assertEqual("MIT License", actual.name) + if actual: # for mypy + self.assertEqual("MIT", actual.spdx_identifier) + self.assertEqual("MIT License", actual.name) - def test_get_non_global_licenses(self): + def test_get_non_global_licenses(self) -> None: cli = CliFile() cli.read_from_file(self.TESTFILE2) @@ -37,7 +38,7 @@ def test_get_non_global_licenses(self): self.assertEqual(1, len(actual)) self.assertEqual("Apache-2.0", actual[0].spdx_identifier) - def test_has_license(self): + def test_has_license(self) -> None: cli = CliFile() cli.read_from_file(self.TESTFILE2) @@ -53,7 +54,7 @@ def test_has_license(self): actual = LicenseTools.has_license(cli, "XYZ") self.assertFalse(actual) - def test_is_source_code_shipping_license(self): + def test_is_source_code_shipping_license(self) -> None: actual = LicenseTools.is_source_code_shipping_license("MIT") self.assertFalse(actual) @@ -87,7 +88,7 @@ def test_is_source_code_shipping_license(self): actual = LicenseTools.is_source_code_shipping_license("ECOS-1.1") self.assertTrue(actual) - def test_is_multi_license(self): + def test_is_multi_license(self) -> None: actual = LicenseTools.is_multi_license("MIT") self.assertFalse(actual) @@ -103,7 +104,7 @@ def test_is_multi_license(self): actual = LicenseTools.is_multi_license("DUAL: MIT, GPL-2.0") self.assertTrue(actual) - def test_is_do_not_use_license(self): + def test_is_do_not_use_license(self) -> None: license = CliLicense() license.type = "RED" actual = LicenseTools.is_do_not_use_license(license) @@ -121,7 +122,7 @@ def test_is_do_not_use_license(self): actual = LicenseTools.is_do_not_use_license(license) self.assertFalse(actual) - def test_has_multi_license(self): + def test_has_multi_license(self) -> None: cli = CliFile() cli.read_from_file(self.TESTFILE1) actual = LicenseTools.has_multi_license(cli) @@ -132,7 +133,7 @@ def test_has_multi_license(self): actual = LicenseTools.has_multi_license(cli) self.assertTrue(actual) - def test_has_do_not_use_files(self): + def test_has_do_not_use_files(self) -> None: cli = CliFile() cli.read_from_file(self.TESTFILE2) actual = LicenseTools.has_do_not_use_files(cli) @@ -143,7 +144,7 @@ def test_has_do_not_use_files(self): actual = LicenseTools.has_do_not_use_files(cli) self.assertTrue(actual) - def test_has_source_code_shipping_license(self): + def test_has_source_code_shipping_license(self) -> None: cli = CliFile() cli.read_from_file(self.TESTFILE3) actual = LicenseTools.has_source_code_shipping_license(cli) @@ -154,7 +155,7 @@ def test_has_source_code_shipping_license(self): actual = LicenseTools.has_source_code_shipping_license(cli) self.assertFalse(actual) - def test_license_has_not_readme_tag(self): + def test_license_has_not_readme_tag(self) -> None: cli = CliFile() cli.read_from_file(self.TESTFILE3) actual = LicenseTools.license_has_not_readme_tag(cli.licenses[2]) @@ -163,7 +164,7 @@ def test_license_has_not_readme_tag(self): actual = LicenseTools.license_has_not_readme_tag(cli.licenses[1]) self.assertFalse(actual) - def test_component_has_not_readme_tag(self): + def test_component_has_not_readme_tag(self) -> None: cli = CliFile() cli.read_from_file(self.TESTFILE1) actual = LicenseTools.component_has_not_readme_tag(cli)