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()