diff --git a/sbol_utilities/sbol3_sbol2_conversion.py b/sbol_utilities/sbol3_sbol2_conversion.py index 19485ecb..a125fa32 100644 --- a/sbol_utilities/sbol3_sbol2_conversion.py +++ b/sbol_utilities/sbol3_sbol2_conversion.py @@ -5,7 +5,7 @@ # Namespaces -from rdflib import URIRef +from rdflib import URIRef, Literal BACKPORT_NAMESPACE = 'http://sboltools.org/backport#' BACKPORT2_VERSION = f'{BACKPORT_NAMESPACE}sbol2version' @@ -150,8 +150,26 @@ def visit_association(self, a: sbol3.Association): raise NotImplementedError('Conversion of Association from SBOL3 to SBOL2 not yet implemented') def visit_attachment(self, a: sbol3.Attachment): - # Priority: 2 - raise NotImplementedError('Conversion of Attachment from SBOL3 to SBOL2 not yet implemented') + att2 = sbol2.Attachment(self._sbol2_identity(a), source=a.source, version=self._sbol2_version(a)) + self.doc2.addAttachment(att2) + + if a.hash: + # Check if it's SHA1 (SBOL2 only supports SHA1) + hash_algorithm = a.hash_algorithm + + if hash_algorithm.replace("-", "").replace(" ", "").lower() == 'sha1': + # It's SHA1, so we can set it directly in SBOL2 + att2.hash = a.hash + else: + # It's not SHA1, add as backport extension properties + att2.properties[BACKPORT_NAMESPACE + 'hash'] = [Literal(a.hash)] + att2.properties[BACKPORT_NAMESPACE + 'hashAlgorithm'] = [Literal(hash_algorithm)] + + att2.format = str(a.format) + att2.size = a.size + + self._convert_toplevel(a, att2) + def visit_binary_prefix(self, a: sbol3.BinaryPrefix): # Priority: 4 @@ -541,8 +559,21 @@ def visit_association(self, a: sbol2.Association): raise NotImplementedError('Conversion of Association from SBOL2 to SBOL3 not yet implemented') def visit_attachment(self, a: sbol2.Attachment): - # Priority: 2 - raise NotImplementedError('Conversion of Attachment from SBOL2 to SBOL3 not yet implemented') + att3 = sbol3.Attachment(self._sbol3_identity(a), namespace=self._sbol3_namespace(a), source=a.source) + self.doc3.add(att3) + + # Check for backported hash properties first (higher priority) + if BACKPORT_NAMESPACE + 'hash' in a.properties: + att3.hash = a.properties[BACKPORT_NAMESPACE + 'hash'][0] + att3.hash_algorithm = a.properties[BACKPORT_NAMESPACE + 'hashAlgorithm'][0] + elif a.hash: + att3.hash = a.hash + att3.hash_algorithm = 'sha1' + + att3.format = str(a.format) + att3.size = a.size + + self._convert_toplevel(a, att3) def visit_collection(self, coll2: sbol2.Collection): # Make the Collection object and add it to the document @@ -752,7 +783,7 @@ def visit_range(self, r2: sbol2.Range): cdef = r2.parent.parent ns = self._sbol3_namespace(cdef) seq_stub = sbol3.Sequence(f'{ns}/{cdef.displayId}Seq/', namespace=ns) - cdef.sequence = seq_stup + cdef.sequence = seq_stub cdef.doc.add(seq_stub) r3 = sbol3.Range(seq_ref, r2.start, r2.end) self._convert_identified(r2, r3) diff --git a/test/test_files/test_attachment_sbol2_converted.xml b/test/test_files/test_attachment_sbol2_converted.xml new file mode 100644 index 00000000..7e158c1a --- /dev/null +++ b/test/test_files/test_attachment_sbol2_converted.xml @@ -0,0 +1,18 @@ + + + + exp1_growth_data + + + + + 147 + 8d297ddafd1955b6095356582c13a58f23a1a133 + sha1 + 1 + + diff --git a/test/test_files/test_attachment_sbol2_converted_loop.xml b/test/test_files/test_attachment_sbol2_converted_loop.xml index 0ecbc98c..37f5cfcf 100644 --- a/test/test_files/test_attachment_sbol2_converted_loop.xml +++ b/test/test_files/test_attachment_sbol2_converted_loop.xml @@ -1,12 +1,12 @@ - - exp1_growth_data - 1 - 8d297ddafd1955b6095356582c13a58f23a1a133 147 + 1 + + exp1_growth_data + 8d297ddafd1955b6095356582c13a58f23a1a133 diff --git a/test/test_files/test_attachment_sbol3.xml b/test/test_files/test_attachment_sbol3.xml index 2356f804..d4fb9736 100644 --- a/test/test_files/test_attachment_sbol3.xml +++ b/test/test_files/test_attachment_sbol3.xml @@ -3,10 +3,10 @@ xmlns:rdf="http://www.w3.org/1999/02/22-rdf-syntax-ns#" xmlns:sbol="http://sbols.org/v3#" > - - exp1_growth_data + + attachment1 - + 147 diff --git a/test/test_files/test_attachment_sbol3_converted.xml b/test/test_files/test_attachment_sbol3_converted.xml new file mode 100644 index 00000000..73d087d8 --- /dev/null +++ b/test/test_files/test_attachment_sbol3_converted.xml @@ -0,0 +1,12 @@ + + + + 147 + + sha256 + + + c531131f1bfc4c56b1d49a8caf389ac744263582163df2a6aab45916f2eab045 + attachment1 + + diff --git a/test/test_files/test_attachment_sbol3_converted_loop.xml b/test/test_files/test_attachment_sbol3_converted_loop.xml new file mode 100644 index 00000000..75b5408c --- /dev/null +++ b/test/test_files/test_attachment_sbol3_converted_loop.xml @@ -0,0 +1,24 @@ + + + + attachment1 + Dummy Attachment + A dummy attachment object + + + + + 1024 + + + promoter1 + Dummy Promoter + A dummy promoter component for demonstration purposes + + + + + diff --git a/test/test_helpers.py b/test/test_helpers.py index 5960e747..f7b7b0a4 100644 --- a/test/test_helpers.py +++ b/test/test_helpers.py @@ -124,7 +124,7 @@ def test_generate_hash(self): test_file = os.path.join(test_dir, 'test_files', 'test_attachment_sbol3.xml') doc = sbol3.Document() doc.read(test_file) - attachment: sbol3.Attachment = doc.find('exp1_growth_data') + attachment: sbol3.Attachment = doc.find('attachment1') # 1. Test with a valid file and default algorithm (sha3_256) hash = generate_hash(attachment) diff --git a/test/test_sbol2_sbol3_direct.py b/test/test_sbol2_sbol3_direct.py index 74ba6acf..edc8aea8 100644 --- a/test/test_sbol2_sbol3_direct.py +++ b/test/test_sbol2_sbol3_direct.py @@ -283,7 +283,7 @@ def handle_2to3_conversion(self, test_filename: str, comparison_filename: str): with tempfile.TemporaryDirectory() as tmpdir: tmp3 = Path(tmpdir) / 'doc3.nt' doc3.write(tmp3) - if file_diff(str(tmp3), str(TEST_FILES / comparison_filename)): + if file_diff(str(tmp3), str(TEST_FILES / comparison_filename), strip_backport_properties=True): raise SBOL2to3ConversionError() # Round-trip back to SBOL2 and check contents @@ -294,7 +294,7 @@ def handle_2to3_conversion(self, test_filename: str, comparison_filename: str): tmp2 = Path(tmpdir) / 'doc2_loop.xml' doc2_loop.write(tmp2) - if file_diff(str(tmp2), str(TEST_FILES / test_filename)): + if file_diff(str(tmp2), str(TEST_FILES / test_filename), strip_backport_properties=True): raise SBOL3to2ConversionError() def test_implementation_conversion(self): @@ -309,6 +309,9 @@ def test_functionalcomponent_conversion(self): def test_interaction_conversion(self): self.handle_2to3_conversion('sbol_3to2_interaction.xml', 'sbol_3to2_interaction.nt') + def test_attachment_conversion(self): + """Test ability to convert SBOL2 attachment objects to SBOL3""" + self.handle_2to3_conversion('test_attachment_sbol2.xml', 'test_attachment_sbol2_converted.xml') class TestDirectSBOL3SBOL2Conversion(unittest.TestCase): @@ -330,7 +333,7 @@ def handle_3to2_conversion(self, test_filename: str, comparison_filename: str): with tempfile.TemporaryDirectory() as tmpdir: tmp2 = Path(tmpdir) / 'doc2.xml' doc2.write(tmp2) - if file_diff(str(tmp2), str(TEST_FILES / comparison_filename)): + if file_diff(str(tmp2), str(TEST_FILES / comparison_filename), strip_backport_properties=True): raise SBOL3to2ConversionError() # Round-trip back to SBOL3 and check contents @@ -341,7 +344,7 @@ def handle_3to2_conversion(self, test_filename: str, comparison_filename: str): tmp3 = Path(tmpdir) / 'doc3_loop.nt' doc3_loop.write(tmp3) - if file_diff(str(tmp3), str(TEST_FILES / test_filename)): + if file_diff(str(tmp3), str(TEST_FILES / test_filename), strip_backport_properties=True): raise SBOL2to3ConversionError() def test_implementation_conversion(self): @@ -373,6 +376,9 @@ def test_identity_conversion(self): def test_interaction_conversion(self): self.handle_3to2_conversion('sbol_3to2_interaction.nt', 'sbol_3to2_interaction.xml') + def test_attachment_conversion(self): + """Test ability to convert SBOL3 attachment objects to SBOL2""" + self.handle_3to2_conversion('test_attachment_sbol3.xml', 'test_attachment_sbol3_converted.xml') if __name__ == '__main__': unittest.main()