diff --git a/spdx/parsers/rdf.py b/spdx/parsers/rdf.py index 774966f16..8e4b14d9b 100644 --- a/spdx/parsers/rdf.py +++ b/spdx/parsers/rdf.py @@ -1220,7 +1220,7 @@ def get_relationship(self, subject_term, relation_term): for sub, pre, rel_ele in self.graph.triples( (relation_term, self.spdx_namespace["relatedSpdxElement"], None) ): - related_element = str(rel_ele.split("#")[1]) + related_element = str(rel_ele.split("#")[1]) if '#' in rel_ele else rel_ele except: related_element = None diff --git a/spdx/relationship.py b/spdx/relationship.py index 980990ddc..742315b7a 100644 --- a/spdx/relationship.py +++ b/spdx/relationship.py @@ -10,18 +10,9 @@ # See the License for the specific language governing permissions and # limitations under the License. -from enum import Enum -from spdx.parsers.loggers import ErrorMessages - -# Implement the auto feature that becomes available in 3.6 -autoinc = 0 - +from enum import auto, Enum -def auto(): - global autoinc - autoval = autoinc - autoinc += 1 - return autoval +from spdx.parsers.loggers import ErrorMessages class RelationshipType(Enum): @@ -82,33 +73,37 @@ def __init__(self, relationship=None, relationship_comment=None): self.relationship = relationship self.relationship_comment = relationship_comment + def __eq__(self, other): + return ( + isinstance(other, Relationship) + and self.relationship == other.relationship + ) + @property def has_comment(self): return self.relationship_comment is not None @property - def spdxelementid(self): + def spdx_element_id(self): return self.relationship.split(" ")[0] @property - def relationshiptype(self): + def relationship_type(self): return self.relationship.split(" ")[1] @property - def relatedspdxelement(self): + def related_spdx_element(self): return self.relationship.split(" ")[2] - def validate(self, messages): + def validate(self, messages: ErrorMessages) -> ErrorMessages: """ Check that all the fields are valid. Appends any error messages to messages parameter shall be a ErrorMessages. """ - self.validate_relationship(messages) - - def validate_relationship(self, messages): - r_type = self.relationship.split(" ")[1] + r_type = self.relationship_type if r_type not in [name for name, _ in RelationshipType.__members__.items()]: messages.append( "Relationship type must be one of the constants defined in " "class spdx.relationship.Relationship" ) + return messages diff --git a/spdx/utils.py b/spdx/utils.py index d40113ce2..fc8b9d13e 100644 --- a/spdx/utils.py +++ b/spdx/utils.py @@ -73,6 +73,9 @@ def to_value(self): def __str__(self): return self.to_value() + def __repr__(self): + return self.to_value() + class UnKnown(object): """ @@ -91,6 +94,7 @@ def __repr__(self): def __eq__(self, other): return self.to_value() == other.to_value() + class SPDXNone(object): """ Represent SPDX None value. diff --git a/spdx/writers/json.py b/spdx/writers/json.py index 0177cd104..a2b226fdf 100644 --- a/spdx/writers/json.py +++ b/spdx/writers/json.py @@ -14,6 +14,14 @@ from spdx.writers.tagvalue import InvalidDocumentError from spdx.writers.jsonyamlxml import Writer from spdx.parsers.loggers import ErrorMessages +import datetime + + +def json_converter(obj): + if isinstance(obj, datetime.datetime): + return str(obj) + else: + raise TypeError("No implementation available to serialize objects of type " + type(obj).__name__) def write_document(document, out, validate=True): @@ -26,4 +34,4 @@ def write_document(document, out, validate=True): writer = Writer(document) document_object = writer.create_document() - json.dump(document_object, out, indent=4) + json.dump(document_object, out, indent=4, default=json_converter) diff --git a/spdx/writers/jsonyamlxml.py b/spdx/writers/jsonyamlxml.py index 2abf31a93..9bb78c731 100644 --- a/spdx/writers/jsonyamlxml.py +++ b/spdx/writers/jsonyamlxml.py @@ -331,16 +331,16 @@ def create_relationship_info(self): relationship_objects = [] for relationship_term in self.document.relationships: - if relationship_term.relationshiptype == "DESCRIBES": + if relationship_term.relationship_type == "DESCRIBES": continue - if relationship_term.relationshiptype == "CONTAINS": + if relationship_term.relationship_type == "CONTAINS": continue relationship_object = dict() - relationship_object["spdxElementId"] = relationship_term.spdxelementid + relationship_object["spdxElementId"] = relationship_term.spdx_element_id relationship_object[ "relatedSpdxElement" - ] = relationship_term.relatedspdxelement - relationship_object["relationshipType"] = relationship_term.relationshiptype + ] = relationship_term.related_spdx_element + relationship_object["relationshipType"] = relationship_term.relationship_type if relationship_term.has_comment: relationship_object["comment"] = relationship_term.relationship_comment @@ -398,9 +398,12 @@ def __init__(self, document): def create_extracted_license(self): extracted_license_objects = [] - extracted_licenses = self.document.extracted_licenses + unique_extracted_licenses = {} + for lic in self.document.extracted_licenses: + if lic.identifier not in unique_extracted_licenses.keys(): + unique_extracted_licenses[lic.identifier] = lic - for extracted_license in extracted_licenses: + for extracted_license in unique_extracted_licenses.values(): extracted_license_object = dict() if isinstance(extracted_license.identifier, Literal): @@ -495,8 +498,8 @@ def create_document_describes(self): document_describes = [] remove_rel = [] for relationship in self.document.relationships: - if relationship.relationshiptype == "DESCRIBES": - document_describes.append(relationship.relatedspdxelement) + if relationship.relationship_type == "DESCRIBES": + document_describes.append(relationship.related_spdx_element) if not relationship.has_comment: remove_rel.append(relationship) for relationship in remove_rel: @@ -516,9 +519,14 @@ def create_document(self): document_describes = self.create_document_describes() self.document_object["documentDescribes"] = document_describes + unique_doc_packages = {} + for doc_package in self.document.packages: + if doc_package.spdx_id not in unique_doc_packages.keys(): + unique_doc_packages[doc_package.spdx_id] = doc_package + package_objects = [] file_objects = [] - for package in self.document.packages: + for package in unique_doc_packages.values(): package_info_object, files_in_package = self.create_package_info(package) package_objects.append(package_info_object) file_objects.extend(file for file in files_in_package if file not in file_objects) diff --git a/spdx/writers/rdf.py b/spdx/writers/rdf.py index 004e030dd..e700fadc7 100644 --- a/spdx/writers/rdf.py +++ b/spdx/writers/rdf.py @@ -24,6 +24,7 @@ from spdx import utils from spdx.package import Package from spdx.parsers.loggers import ErrorMessages +from spdx.relationship import Relationship from spdx.writers.tagvalue import InvalidDocumentError import warnings @@ -536,15 +537,31 @@ class RelationshipInfoWriter(BaseWriter): def __init__(self, document, out): super(RelationshipInfoWriter, self).__init__(document, out) - def create_relationship_node(self, relationship): + def transform_relationship_type_to_rdf_model(self, relationship_type: str) -> str: """ - Return an relationship node. + Transform relationship type from upper snake case to camel case to match rdf-specific output e.g. + COPY_OF -> relationshipType_copyOf. """ - relationship_node = URIRef(str(relationship.spdxelementid)) + return "relationshipType_" + relationship_type[0].lower() + relationship_type.title().replace('_', '')[1:] + + def create_relationship_node(self, relationship: Relationship) -> BNode: + """ + Return a relationship node. + """ + relationship_node = BNode() type_triple = (relationship_node, RDF.type, self.spdx_namespace.Relationship) self.graph.add(type_triple) - relationship_type_node = Literal(relationship.relationshiptype) + relationship_spdx_element_id = Literal(relationship.spdx_element_id) + self.graph.add( + ( + relationship_node, + self.spdx_namespace.spdxElementId, + relationship_spdx_element_id, + ) + ) + relationship_type = self.transform_relationship_type_to_rdf_model(relationship.relationship_type) + relationship_type_node = self.spdx_namespace[relationship_type] self.graph.add( ( relationship_node, @@ -552,7 +569,7 @@ def create_relationship_node(self, relationship): relationship_type_node, ) ) - related_spdx_node = Literal(relationship.relatedspdxelement) + related_spdx_node = Literal(relationship.related_spdx_element) related_spdx_triple = ( relationship_node, self.spdx_namespace.relatedSpdxElement, @@ -1019,6 +1036,7 @@ def write(self, doc_node=None): for package_node in self.packages(): package_triple = (doc_node, self.spdx_namespace.describesPackage, package_node) self.graph.add(package_triple) + # Add external package reference for pkg_ext_ref_node in self.pkg_ext_refs(): pkg_ext_ref_triple = ( @@ -1027,10 +1045,12 @@ def write(self, doc_node=None): pkg_ext_ref_node, ) self.graph.add(pkg_ext_ref_triple) - """# Add relationship - relate_node = self.relationships() - relate_triple = (doc_node, self.spdx_namespace.relationship, relate_node) - self.graph.add(relate_triple)""" + + # Add relationship + for relate_node in self.relationships(): + relate_triple = (doc_node, self.spdx_namespace.relationship, relate_node) + self.graph.add(relate_triple) + # Add snippet snippet_nodes = self.snippets() for snippet in snippet_nodes: diff --git a/spdx/writers/yaml.py b/spdx/writers/yaml.py index 24600d8a7..2ccc6884d 100644 --- a/spdx/writers/yaml.py +++ b/spdx/writers/yaml.py @@ -27,4 +27,4 @@ def write_document(document, out, validate=True): writer = Writer(document) document_object = writer.create_document() - yaml.safe_dump(document_object, out, indent=2, explicit_start=True) + yaml.safe_dump(document_object, out, indent=2, explicit_start=True, encoding='utf-8') diff --git a/tests/data/doc_write/rdf-simple-plus.json b/tests/data/doc_write/rdf-simple-plus.json index 5f84b380f..f7711a568 100644 --- a/tests/data/doc_write/rdf-simple-plus.json +++ b/tests/data/doc_write/rdf-simple-plus.json @@ -61,8 +61,16 @@ "@rdf:resource": "http://spdx.org/licenses/LGPL-2.1-or-later" } } - }, + }, + "ns1:relationship": { + "ns1:Relationship": { + "ns1:spdxElementId": "SPDXRef-DOCUMENT", + "ns1:relatedSpdxElement": "SPDXRef-Package", + "ns1:relationshipType": { + "@rdf:resource": "http://spdx.org/rdf/terms#relationshipType_describes" + } + }}, "@rdf:about": "http://www.spdx.org/tools#SPDXRef-DOCUMENT" } } -} \ No newline at end of file +} diff --git a/tests/data/doc_write/rdf-simple.json b/tests/data/doc_write/rdf-simple.json index 7327c703f..a2717510f 100644 --- a/tests/data/doc_write/rdf-simple.json +++ b/tests/data/doc_write/rdf-simple.json @@ -61,8 +61,17 @@ }, "ns1:fileName": "./some/path/tofile" } - }, + }, + "ns1:relationship": { + "ns1:Relationship": { + "ns1:spdxElementId": "SPDXRef-DOCUMENT", + "ns1:relatedSpdxElement": "SPDXRef-Package", + "ns1:relationshipType": { + "@rdf:resource": "http://spdx.org/rdf/terms#relationshipType_describes" + } + } + }, "@rdf:about": "http://www.spdx.org/tools#SPDXRef-DOCUMENT" } } -} \ No newline at end of file +} diff --git a/tests/data/doc_write/xml-simple-plus.xml b/tests/data/doc_write/xml-simple-plus.xml index f3b2bec73..6de34d2a8 100644 --- a/tests/data/doc_write/xml-simple-plus.xml +++ b/tests/data/doc_write/xml-simple-plus.xml @@ -1,43 +1,37 @@ - - - SPDX-2.1 - CC0-1.0 - Sample_Document-V2.1 - SPDXRef-DOCUMENT - https://spdx.org/spdxdocs/spdx-example-444504E0-4F89-41D3-9A0C-0305E82C3301 - SPDXRef-Package - - SPDXRef-Package - some/path - NOASSERTION - Some copyrught - - SOME code - - - SOME-SHA1 - SHA1 - - NOASSERTION - NOASSERTION - LGPL-2.1-or-later - - SPDXRef-File - - - - ./some/path/tofile - SPDXRef-File - - SOME-SHA1 - SHA1 - - NOASSERTION - NOASSERTION - LGPL-2.1-or-later - - - - - \ No newline at end of file + + SPDX-2.1 + CC0-1.0 + Sample_Document-V2.1 + SPDXRef-DOCUMENT + https://spdx.org/spdxdocs/spdx-example-444504E0-4F89-41D3-9A0C-0305E82C3301 + SPDXRef-Package + + SPDXRef-Package + some/path + NOASSERTION + Some copyrught + + SOME code + + + SOME-SHA1 + SHA1 + + NOASSERTION + NOASSERTION + LGPL-2.1-or-later + SPDXRef-File + + + ./some/path/tofile + SPDXRef-File + + SOME-SHA1 + SHA1 + + NOASSERTION + NOASSERTION + LGPL-2.1-or-later + + diff --git a/tests/data/doc_write/xml-simple.xml b/tests/data/doc_write/xml-simple.xml index 9eaca485b..f45ca9707 100644 --- a/tests/data/doc_write/xml-simple.xml +++ b/tests/data/doc_write/xml-simple.xml @@ -20,7 +20,6 @@ NOASSERTION NOASSERTION - LGPL-2.1-only SPDXRef-File @@ -35,4 +34,4 @@ NOASSERTION LGPL-2.1-only - \ No newline at end of file + diff --git a/tests/utils_test.py b/tests/utils_test.py index 3d2c9630a..c1482aee5 100644 --- a/tests/utils_test.py +++ b/tests/utils_test.py @@ -138,7 +138,7 @@ def check_rdf_scan(expected_file, result_file, regen=False): else: with io.open(expected_file, 'r', encoding='utf-8') as i: expected = sort_nested(json.load(i)) - assert expected == result + assert result == expected def load_and_clean_tv(location): @@ -165,7 +165,7 @@ def check_tv_scan(expected_file, result_file, regen=False): o.write(result) expected = load_and_clean_tv(expected_file) - assert expected == result + assert result == expected def load_and_clean_json(location): @@ -195,7 +195,7 @@ def check_json_scan(expected_file, result_file, regen=False): o.write(result) expected = load_and_clean_json(expected_file) - assert expected == result + assert result == expected def load_and_clean_yaml(location): @@ -225,7 +225,7 @@ def check_yaml_scan(expected_file, result_file, regen=False): o.write(result) expected = load_and_clean_yaml(expected_file) - assert expected == result + assert result == expected def load_and_clean_xml(location): @@ -255,7 +255,7 @@ def check_xml_scan(expected_file, result_file, regen=False): o.write(result) expected = load_and_clean_xml(expected_file) - assert expected == result + assert result == expected class TestParserUtils(object):