From f5877ae2b8e4d870880609c8f2325815e2affc03 Mon Sep 17 00:00:00 2001 From: Jake Beal Date: Fri, 6 Oct 2023 16:19:45 -0400 Subject: [PATCH 1/8] Create stub methods for Python-native conversion between SBOL3 and SBOL2 --- sbol_utilities/conversion.py | 14 +++++++++++-- sbol_utilities/sbol3_sbol2_conversion.py | 26 ++++++++++++++++++++++++ 2 files changed, 38 insertions(+), 2 deletions(-) create mode 100644 sbol_utilities/sbol3_sbol2_conversion.py diff --git a/sbol_utilities/conversion.py b/sbol_utilities/conversion.py index fc67cda1..2ce0468d 100644 --- a/sbol_utilities/conversion.py +++ b/sbol_utilities/conversion.py @@ -16,6 +16,7 @@ from sbol_utilities.helper_functions import strip_sbol2_version, GENETIC_DESIGN_FILE_TYPES, \ find_top_level from sbol_utilities.sbol3_genbank_conversion import GenBankSBOL3Converter +from sbol_utilities.sbol3_sbol2_conversion import SBOL2SBOL3Converter from sbol_utilities.workarounds import id_sort # sbol javascript executable based on https://github.com/sboltools/sbolgraph @@ -69,13 +70,18 @@ def convert_identities2to3(sbol3_data: str) -> str: return g.serialize(format="xml") -def convert2to3(sbol2_doc: Union[str, sbol2.Document], namespaces=None) -> sbol3.Document: +def convert2to3(sbol2_doc: Union[str, sbol2.Document], namespaces=None, use_native_converter: bool = True) \ + -> sbol3.Document: """Convert an SBOL2 document to an equivalent SBOL3 document :param sbol2_doc: Document to convert :param namespaces: list of URI prefixes to treat as namespaces + :param use_native_converter: if true, use experimental Python converter instead of JavaScript call-out :return: equivalent SBOL3 document """ + if use_native_converter: + return SBOL2SBOL3Converter.convert2to3(sbol2_doc) + # if we've started with a Document in memory, write it to a temp file if namespaces is None: namespaces = [] @@ -167,12 +173,16 @@ def change_orientation(o): return doc -def convert3to2(doc3: sbol3.Document) -> sbol2.Document: +def convert3to2(doc3: sbol3.Document, use_native_converter: bool = False) -> sbol2.Document: """Convert an SBOL3 document to an equivalent SBOL2 document :param doc3: Document to convert + :param use_native_converter: if true, use experimental Python converter instead of JavaScript call-out :return: equivalent SBOL2 document """ + if use_native_converter: + return SBOL2SBOL3Converter.convert3to2(doc3) + # TODO: remove workarounds after conversion errors fixed in https://github.com/sboltools/sbolgraph/issues/16 # remap sequence encodings: encoding_remapping = { diff --git a/sbol_utilities/sbol3_sbol2_conversion.py b/sbol_utilities/sbol3_sbol2_conversion.py new file mode 100644 index 00000000..7a77786f --- /dev/null +++ b/sbol_utilities/sbol3_sbol2_conversion.py @@ -0,0 +1,26 @@ +import sbol3 +import sbol2 + + +class SBOL2SBOL3Converter: + @staticmethod + def convert3to2(doc3: sbol3.Document) -> sbol2.Document: + """Convert an SBOL3 document to an SBOL2 document + + :param doc3: SBOL3 document to convert + :returns: SBOL2 document + """ + doc2 = sbol2.Document() + # TODO: build converter here + return doc2 + + @staticmethod + def convert2to3(doc2: sbol2.Document) -> sbol3.Document: + """Convert an SBOL2 document to an SBOL3 document + + :param doc2: SBOL2 document to convert + :returns: SBOL3 document + """ + doc3 = sbol3.Document() + # TODO: build converter here + return doc3 From 50467ef4afaac1eca6f99e5efa75ecace4d61676 Mon Sep 17 00:00:00 2001 From: Jake Beal Date: Fri, 6 Oct 2023 16:59:42 -0400 Subject: [PATCH 2/8] Set up visitor stubs for 3-to-2 conversion --- sbol_utilities/sbol3_sbol2_conversion.py | 124 ++++++++++++++++++++++- 1 file changed, 123 insertions(+), 1 deletion(-) diff --git a/sbol_utilities/sbol3_sbol2_conversion.py b/sbol_utilities/sbol3_sbol2_conversion.py index 7a77786f..042f39fa 100644 --- a/sbol_utilities/sbol3_sbol2_conversion.py +++ b/sbol_utilities/sbol3_sbol2_conversion.py @@ -2,6 +2,128 @@ import sbol2 +class SBOL3ConverterVisitor: + + doc2: sbol2.Document + + def __init__(self, doc2: sbol2.Document): + self.doc2 = doc2 + + def _convert_identified(self): + raise NotImplementedError('Conversion of identified objects from SBOL3 to SBOL2 not yet implemented') + + def _convert_toplevel(self): + raise NotImplementedError('Conversion of toplevel objects from SBOL3 to SBOL2 not yet implemented') + + def visit_activity(self, a: sbol3.Activity): + raise NotImplementedError('Conversion of Activity from SBOL3 to SBOL2 not yet implemented') + + def visit_agent(self, a: sbol3.Agent): + raise NotImplementedError('Conversion of Agent from SBOL3 to SBOL2 not yet implemented') + + 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): + raise NotImplementedError('Conversion of Attachment from SBOL3 to SBOL2 not yet implemented') + + def visit_binary_prefix(self, a: sbol3.BinaryPrefix): + raise NotImplementedError('Conversion of BinaryPrefix from SBOL3 to SBOL2 not yet implemented') + + def visit_collection(self, a: sbol3.Collection): + raise NotImplementedError('Conversion of Collection from SBOL3 to SBOL2 not yet implemented') + + def visit_combinatorial_derivation(self, a: sbol3.CombinatorialDerivation): + raise NotImplementedError('Conversion of CombinatorialDerivation from SBOL3 to SBOL2 not yet implemented') + + def visit_component(self, a: sbol3.Component): + raise NotImplementedError('Conversion of Component from SBOL3 to SBOL2 not yet implemented') + + def visit_component_reference(self, a: sbol3.ComponentReference): + raise NotImplementedError('Conversion of ComponentReference from SBOL3 to SBOL2 not yet implemented') + + def visit_constraint(self, a: sbol3.Constraint): + raise NotImplementedError('Conversion of Constraint from SBOL3 to SBOL2 not yet implemented') + + def visit_cut(self, a: sbol3.Cut): + raise NotImplementedError('Conversion of Cut from SBOL3 to SBOL2 not yet implemented') + + def visit_document(self, a: sbol3.Document): + raise NotImplementedError('Conversion of Document from SBOL3 to SBOL2 not yet implemented') + + def visit_entire_sequence(self, a: sbol3.EntireSequence): + raise NotImplementedError('Conversion of EntireSequence from SBOL3 to SBOL2 not yet implemented') + + def visit_experiment(self, a: sbol3.Experiment): + raise NotImplementedError('Conversion of Experiment from SBOL3 to SBOL2 not yet implemented') + + def visit_experimental_data(self, a: sbol3.ExperimentalData): + raise NotImplementedError('Conversion of ExperimentalData from SBOL3 to SBOL2 not yet implemented') + + def visit_externally_defined(self, a: sbol3.ExternallyDefined): + raise NotImplementedError('Conversion of ExternallyDefined from SBOL3 to SBOL2 not yet implemented') + + def visit_implementation(self, a: sbol3.Implementation): + raise NotImplementedError('Conversion of Implementation from SBOL3 to SBOL2 not yet implemented') + + def visit_interaction(self, a: sbol3.Interaction): + raise NotImplementedError('Conversion of Interaction from SBOL3 to SBOL2 not yet implemented') + + def visit_interface(self, a: sbol3.Interface): + raise NotImplementedError('Conversion of Interface from SBOL3 to SBOL2 not yet implemented') + + def visit_local_sub_component(self, a: sbol3.LocalSubComponent): + raise NotImplementedError('Conversion of LocalSubComponent from SBOL3 to SBOL2 not yet implemented') + + def visit_measure(self, a: sbol3.Measure): + raise NotImplementedError('Conversion of Measure from SBOL3 to SBOL2 not yet implemented') + + def visit_model(self, a: sbol3.Model): + raise NotImplementedError('Conversion of Model from SBOL3 to SBOL2 not yet implemented') + + def visit_participation(self, a: sbol3.Participation): + raise NotImplementedError('Conversion of Participation from SBOL3 to SBOL2 not yet implemented') + + def visit_plan(self, a: sbol3.Plan): + raise NotImplementedError('Conversion of Plan from SBOL3 to SBOL2 not yet implemented') + + def visit_prefixed_unit(self, a: sbol3.PrefixedUnit): + raise NotImplementedError('Conversion of PrefixedUnit from SBOL3 to SBOL2 not yet implemented') + + def visit_range(self, a: sbol3.Range): + raise NotImplementedError('Conversion of Range from SBOL3 to SBOL2 not yet implemented') + + def visit_si_prefix(self, a: sbol3.SIPrefix): + raise NotImplementedError('Conversion of SIPrefix from SBOL3 to SBOL2 not yet implemented') + + def visit_sequence(self, a: sbol3.Sequence): + raise NotImplementedError('Conversion of Sequence from SBOL3 to SBOL2 not yet implemented') + + def visit_sequence_feature(self, a: sbol3.SequenceFeature): + raise NotImplementedError('Conversion of SequenceFeature from SBOL3 to SBOL2 not yet implemented') + + def visit_singular_unit(self, a: sbol3.SingularUnit): + raise NotImplementedError('Conversion of SingularUnit from SBOL3 to SBOL2 not yet implemented') + + def visit_sub_component(self, a: sbol3.SubComponent): + raise NotImplementedError('Conversion of SubComponent from SBOL3 to SBOL2 not yet implemented') + + def visit_unit_division(self, a: sbol3.UnitDivision): + raise NotImplementedError('Conversion of UnitDivision from SBOL3 to SBOL2 not yet implemented') + + def visit_unit_exponentiation(self, a: sbol3.UnitExponentiation): + raise NotImplementedError('Conversion of UnitExponentiation from SBOL3 to SBOL2 not yet implemented') + + def visit_unit_multiplication(self, a: sbol3.UnitMultiplication): + raise NotImplementedError('Conversion of UnitMultiplication from SBOL3 to SBOL2 not yet implemented') + + def visit_usage(self, a: sbol3.Usage): + raise NotImplementedError('Conversion of Usage from SBOL3 to SBOL2 not yet implemented') + + def visit_variable_feature(self, a: sbol3.VariableFeature): + raise NotImplementedError('Conversion of VariableFeature from SBOL3 to SBOL2 not yet implemented') + + class SBOL2SBOL3Converter: @staticmethod def convert3to2(doc3: sbol3.Document) -> sbol2.Document: @@ -11,7 +133,7 @@ def convert3to2(doc3: sbol3.Document) -> sbol2.Document: :returns: SBOL2 document """ doc2 = sbol2.Document() - # TODO: build converter here + doc3.accept(SBOL3ConverterVisitor(doc2)) return doc2 @staticmethod From cf4cd95966f68dc4f32ebcc889f48eaedc0788d2 Mon Sep 17 00:00:00 2001 From: Jake Beal Date: Fri, 6 Oct 2023 17:28:12 -0400 Subject: [PATCH 3/8] Start work on Sequence --- sbol_utilities/sbol3_sbol2_conversion.py | 30 +++++++++++++++++++----- 1 file changed, 24 insertions(+), 6 deletions(-) diff --git a/sbol_utilities/sbol3_sbol2_conversion.py b/sbol_utilities/sbol3_sbol2_conversion.py index 042f39fa..6543bd04 100644 --- a/sbol_utilities/sbol3_sbol2_conversion.py +++ b/sbol_utilities/sbol3_sbol2_conversion.py @@ -1,6 +1,8 @@ import sbol3 import sbol2 +SBOL2_VERSION_PREDICATE = 'http://sboltools.org/backport#sbol2version' + class SBOL3ConverterVisitor: @@ -9,11 +11,23 @@ class SBOL3ConverterVisitor: def __init__(self, doc2: sbol2.Document): self.doc2 = doc2 - def _convert_identified(self): - raise NotImplementedError('Conversion of identified objects from SBOL3 to SBOL2 not yet implemented') + def _convert_extension_properties(self, obj: sbol2.Identified): + """Map over the other properties of an extension materials""" + pass + + def _convert_identified(self, obj: sbol2.Identified): + """Map over the other properties of an identified object""" + pass - def _convert_toplevel(self): - raise NotImplementedError('Conversion of toplevel objects from SBOL3 to SBOL2 not yet implemented') + def _convert_toplevel(self, obj: sbol2.TopLevel): + """Map over the other properties of a toplevel object""" + self._convert_identified(obj) + pass + + @staticmethod + def _sbol2_version(obj: sbol3.Identified): + obj.sbol2_version = sbol3.TextProperty(obj, SBOL2_VERSION_PREDICATE, 0, 1) + return obj.sbol2_version or '1' def visit_activity(self, a: sbol3.Activity): raise NotImplementedError('Conversion of Activity from SBOL3 to SBOL2 not yet implemented') @@ -96,8 +110,12 @@ def visit_range(self, a: sbol3.Range): def visit_si_prefix(self, a: sbol3.SIPrefix): raise NotImplementedError('Conversion of SIPrefix from SBOL3 to SBOL2 not yet implemented') - def visit_sequence(self, a: sbol3.Sequence): - raise NotImplementedError('Conversion of Sequence from SBOL3 to SBOL2 not yet implemented') + def visit_sequence(self, seq: sbol3.Sequence): + # Make the Sequence object and add it to the document + seq = sbol2.Sequence(seq.identity, seq.elements, encoding=seq.encoding, version=self._sbol2_version(seq)) + self.doc2.addSequence(seq) + # Add all of the other TopLevel properties and extensions not already covered + self._convert_toplevel(seq) def visit_sequence_feature(self, a: sbol3.SequenceFeature): raise NotImplementedError('Conversion of SequenceFeature from SBOL3 to SBOL2 not yet implemented') From e0d7d563be088a5b591500007274f3075b663c42 Mon Sep 17 00:00:00 2001 From: Jake Beal Date: Fri, 6 Oct 2023 21:10:40 -0400 Subject: [PATCH 4/8] SBOL3 to SBOL2 conversion of BBa_J23101 simple Component, Sequence, and Activity --- sbol_utilities/conversion.py | 6 +- sbol_utilities/sbol3_sbol2_conversion.py | 181 +++++++++++++++++------ test/test_files/BBa_J23101.xml | 62 ++++++++ test/test_sbol2_sbol3_direct.py | 30 ++++ 4 files changed, 231 insertions(+), 48 deletions(-) create mode 100644 test/test_files/BBa_J23101.xml create mode 100644 test/test_sbol2_sbol3_direct.py diff --git a/sbol_utilities/conversion.py b/sbol_utilities/conversion.py index 2ce0468d..30792318 100644 --- a/sbol_utilities/conversion.py +++ b/sbol_utilities/conversion.py @@ -16,7 +16,7 @@ from sbol_utilities.helper_functions import strip_sbol2_version, GENETIC_DESIGN_FILE_TYPES, \ find_top_level from sbol_utilities.sbol3_genbank_conversion import GenBankSBOL3Converter -from sbol_utilities.sbol3_sbol2_conversion import SBOL2SBOL3Converter +import sbol_utilities.sbol3_sbol2_conversion from sbol_utilities.workarounds import id_sort # sbol javascript executable based on https://github.com/sboltools/sbolgraph @@ -80,7 +80,7 @@ def convert2to3(sbol2_doc: Union[str, sbol2.Document], namespaces=None, use_nati :return: equivalent SBOL3 document """ if use_native_converter: - return SBOL2SBOL3Converter.convert2to3(sbol2_doc) + return sbol_utilities.sbol3_sbol2_conversion.convert2to3(sbol2_doc) # if we've started with a Document in memory, write it to a temp file if namespaces is None: @@ -181,7 +181,7 @@ def convert3to2(doc3: sbol3.Document, use_native_converter: bool = False) -> sbo :return: equivalent SBOL2 document """ if use_native_converter: - return SBOL2SBOL3Converter.convert3to2(doc3) + return sbol_utilities.sbol3_sbol2_conversion.convert3to2(doc3) # TODO: remove workarounds after conversion errors fixed in https://github.com/sboltools/sbolgraph/issues/16 # remap sequence encodings: diff --git a/sbol_utilities/sbol3_sbol2_conversion.py b/sbol_utilities/sbol3_sbol2_conversion.py index 6543bd04..0d07d3b4 100644 --- a/sbol_utilities/sbol3_sbol2_conversion.py +++ b/sbol_utilities/sbol3_sbol2_conversion.py @@ -1,36 +1,100 @@ import sbol3 import sbol2 -SBOL2_VERSION_PREDICATE = 'http://sboltools.org/backport#sbol2version' +# Namespaces +from rdflib import URIRef +BACKPORT_NAMESPACE = 'http://sboltools.org/backport#' +BACKPORT2_VERSION = f'{BACKPORT_NAMESPACE}sbol2version' +BACKPORT3_NAMESPACE = f'{BACKPORT_NAMESPACE}sbol3namespace' -class SBOL3ConverterVisitor: +NON_EXTENSION_PROPERTY_PREFIXES = {sbol3.SBOL3_NS, sbol3.SBOL2_NS, # SBOL 2 & 3 namespaces + sbol3.RDF_NS, sbol3.PROV_NS, sbol3.OM_NS, # Standard ontologies + BACKPORT_NAMESPACE} # Information added by this converter - doc2: sbol2.Document - def __init__(self, doc2: sbol2.Document): - self.doc2 = doc2 +class SBOL3To2ConversionVisitor: + """This class is used to map every object in an SBOL3 document into an empty SBOL2 document""" - def _convert_extension_properties(self, obj: sbol2.Identified): - """Map over the other properties of an extension materials""" - pass + doc2: sbol2.Document - def _convert_identified(self, obj: sbol2.Identified): + def __init__(self, doc3: sbol3.Document): + # Create the target document + self.doc2 = sbol2.Document() + # # Immediately run the conversion + self._convert(doc3) + + def _convert(self, doc3: sbol3.Document): + # Bind standard namespaces that aren't bound by default in pySBOL2 + self.doc2.addNamespace(BACKPORT_NAMESPACE, 'backport') + self.doc2.addNamespace(sbol3.PROV_NS, 'prov') + self.doc2.addNamespace(sbol3.OM_NS, 'om') + self.doc2.addNamespace('http://purl.org/dc/terms/', 'dcterms') + + # Override parameters that will otherwise interfere in conversion, saving old values + saved_compliance = sbol2.Config.getOption(sbol2.ConfigOptions.SBOL_COMPLIANT_URIS.value) + sbol2.Config.setOption(sbol2.ConfigOptions.SBOL_COMPLIANT_URIS.value, False) + saved_homespace = sbol2.getHomespace() + sbol2.setHomespace('') + + # Try conversion, resetting saved parameter values afterward + try: + doc3.accept(self) + finally: + sbol2.Config.setOption(sbol2.ConfigOptions.SBOL_COMPLIANT_URIS.value, saved_compliance) + sbol2.setHomespace(saved_homespace) + + @staticmethod + def _convert_extension_properties(obj3: sbol3.Identified, obj2: sbol2.Identified): + """Copy over extension properties""" + extension_properties = (p for p in obj3.properties + if not any(p.startswith(prefix) for prefix in NON_EXTENSION_PROPERTY_PREFIXES)) + for p in extension_properties: + obj2.properties[p] = obj3._properties[p] # Can't use setPropertyValue because it may not be a string + + def _convert_identified(self, obj3: sbol3.Identified, obj2: sbol2.Identified): """Map over the other properties of an identified object""" + self._convert_extension_properties(obj3, obj2) + # Map over equivalent properties + obj2.displayId = obj3.display_id + obj2.name = obj3.name + obj2.description = obj3.description + obj2.wasDerivedFrom = obj3.derived_from + obj2.wasGeneratedBy = obj3.generated_by + # Turn measures into extension properties + if obj3.measures: + raise NotImplementedError('Conversion of measures from SBOL3 to SBOL2 not yet implemented') pass - def _convert_toplevel(self, obj: sbol2.TopLevel): - """Map over the other properties of a toplevel object""" - self._convert_identified(obj) - pass + def _convert_toplevel(self, obj3: sbol3.TopLevel, obj2: sbol2.TopLevel): + """Map over the other properties of a TopLevel object""" + self._convert_identified(obj3, obj2) + obj2.attachments = [a.identity for a in obj3.attachments] + obj2.properties[BACKPORT3_NAMESPACE] = [URIRef(obj3.namespace)] @staticmethod def _sbol2_version(obj: sbol3.Identified): - obj.sbol2_version = sbol3.TextProperty(obj, SBOL2_VERSION_PREDICATE, 0, 1) + obj.sbol2_version = sbol3.TextProperty(obj, BACKPORT2_VERSION, 0, 1) return obj.sbol2_version or '1' - def visit_activity(self, a: sbol3.Activity): - raise NotImplementedError('Conversion of Activity from SBOL3 to SBOL2 not yet implemented') + def visit_activity(self, act3: sbol3.Activity): + # Make the Activity object and add it to the document + act2 = sbol2.Activity(act3.identity, version=self._sbol2_version(act3)) + if act3.types: + if len(act3.types) > 1: + raise NotImplementedError('Conversion of multi-type activities to SBOL2 not yet implemented:' + 'pySBOL2 currently supports a maximum of one type per activity' + 'Bug: https://github.com/SynBioDex/pySBOL2/issues/428') + act2.types = act3.types[0] # Take first type from list of length 1 + act2.startedAtTime = act3.start_time + act2.endedAtTime = act3.end_time + act2.usages = act3.usage + act2.associations = act3.association + # TODO: pySBOL3 is currently missing wasInformedBy (https://github.com/SynBioDex/pySBOL3/issues/436 + # act2.wasInformedBy = act3.informed_by + self.doc2.activities.add(act2) + # Map over all other TopLevel properties and extensions not covered by the constructor + self._convert_toplevel(act3, act2) def visit_agent(self, a: sbol3.Agent): raise NotImplementedError('Conversion of Agent from SBOL3 to SBOL2 not yet implemented') @@ -50,8 +114,32 @@ def visit_collection(self, a: sbol3.Collection): def visit_combinatorial_derivation(self, a: sbol3.CombinatorialDerivation): raise NotImplementedError('Conversion of CombinatorialDerivation from SBOL3 to SBOL2 not yet implemented') - def visit_component(self, a: sbol3.Component): - raise NotImplementedError('Conversion of Component from SBOL3 to SBOL2 not yet implemented') + def visit_component(self, cp3: sbol3.Component): + # Remap type if it's one of the ones that needs remapping; otherwise pass through unchanged + type_map = {sbol3.SBO_DNA: sbol2.BIOPAX_DNA, # TODO: distinguish biopax Dna from DnaRegion + sbol3.SBO_RNA: sbol2.BIOPAX_RNA, # TODO: distinguish biopax Rna from RnaRegion + sbol3.SBO_PROTEIN: sbol2.BIOPAX_PROTEIN, + sbol3.SBO_SIMPLE_CHEMICAL: sbol2.BIOPAX_SMALL_MOLECULE, + sbol3.SBO_NON_COVALENT_COMPLEX: sbol2.BIOPAX_COMPLEX} + types2 = [type_map.get(t, t) for t in cp3.types] + # Make the Component object and add it to the document + cp2 = sbol2.ComponentDefinition(cp3.identity, types2, version=self._sbol2_version(cp3)) + self.doc2.addComponentDefinition(cp2) + # Convert the Component properties not covered by the constructor + cp2.roles = cp3.roles + cp2.sequences = cp3.sequences + if cp3.features: + raise NotImplementedError('Conversion of Component features from SBOL3 to SBOL2 not yet implemented') + if cp3.interactions: + raise NotImplementedError('Conversion of Component interactions from SBOL3 to SBOL2 not yet implemented') + if cp3.constraints: + raise NotImplementedError('Conversion of Component constraints from SBOL3 to SBOL2 not yet implemented') + if cp3.interface: + raise NotImplementedError('Conversion of Component interface from SBOL3 to SBOL2 not yet implemented') + if cp3.models: + raise NotImplementedError('Conversion of Component models from SBOL3 to SBOL2 not yet implemented') + # Map over all other TopLevel properties and extensions not covered by the constructor + self._convert_toplevel(cp3, cp2) def visit_component_reference(self, a: sbol3.ComponentReference): raise NotImplementedError('Conversion of ComponentReference from SBOL3 to SBOL2 not yet implemented') @@ -62,8 +150,9 @@ def visit_constraint(self, a: sbol3.Constraint): def visit_cut(self, a: sbol3.Cut): raise NotImplementedError('Conversion of Cut from SBOL3 to SBOL2 not yet implemented') - def visit_document(self, a: sbol3.Document): - raise NotImplementedError('Conversion of Document from SBOL3 to SBOL2 not yet implemented') + def visit_document(self, doc3: sbol3.Document): + for obj in doc3.objects: + obj.accept(self) def visit_entire_sequence(self, a: sbol3.EntireSequence): raise NotImplementedError('Conversion of EntireSequence from SBOL3 to SBOL2 not yet implemented') @@ -110,12 +199,17 @@ def visit_range(self, a: sbol3.Range): def visit_si_prefix(self, a: sbol3.SIPrefix): raise NotImplementedError('Conversion of SIPrefix from SBOL3 to SBOL2 not yet implemented') - def visit_sequence(self, seq: sbol3.Sequence): + def visit_sequence(self, seq3: sbol3.Sequence): + # Remap encoding if it's one of the ones that needs remapping; otherwise pass through unchanged + encoding_map = {sbol3.IUPAC_DNA_ENCODING: sbol2.SBOL_ENCODING_IUPAC, + sbol3.IUPAC_PROTEIN_ENCODING: sbol2.SBOL_ENCODING_IUPAC_PROTEIN, + sbol3.SMILES_ENCODING: sbol2.SBOL_ENCODING_SMILES} + encoding2 = encoding_map.get(seq3.encoding, seq3.encoding) # Make the Sequence object and add it to the document - seq = sbol2.Sequence(seq.identity, seq.elements, encoding=seq.encoding, version=self._sbol2_version(seq)) - self.doc2.addSequence(seq) - # Add all of the other TopLevel properties and extensions not already covered - self._convert_toplevel(seq) + seq2 = sbol2.Sequence(seq3.identity, seq3.elements, encoding=encoding2, version=self._sbol2_version(seq3)) + self.doc2.addSequence(seq2) + # Map over all other TopLevel properties and extensions not covered by the constructor + self._convert_toplevel(seq3, seq2) def visit_sequence_feature(self, a: sbol3.SequenceFeature): raise NotImplementedError('Conversion of SequenceFeature from SBOL3 to SBOL2 not yet implemented') @@ -142,25 +236,22 @@ def visit_variable_feature(self, a: sbol3.VariableFeature): raise NotImplementedError('Conversion of VariableFeature from SBOL3 to SBOL2 not yet implemented') -class SBOL2SBOL3Converter: - @staticmethod - def convert3to2(doc3: sbol3.Document) -> sbol2.Document: - """Convert an SBOL3 document to an SBOL2 document +def convert3to2(doc3: sbol3.Document) -> sbol2.Document: + """Convert an SBOL3 document to an SBOL2 document - :param doc3: SBOL3 document to convert - :returns: SBOL2 document - """ - doc2 = sbol2.Document() - doc3.accept(SBOL3ConverterVisitor(doc2)) - return doc2 + :param doc3: SBOL3 document to convert + :returns: SBOL2 document + """ + converter = SBOL3To2ConversionVisitor(doc3) + return converter.doc2 - @staticmethod - def convert2to3(doc2: sbol2.Document) -> sbol3.Document: - """Convert an SBOL2 document to an SBOL3 document - - :param doc2: SBOL2 document to convert - :returns: SBOL3 document - """ - doc3 = sbol3.Document() - # TODO: build converter here - return doc3 + +def convert2to3(doc2: sbol2.Document) -> sbol3.Document: + """Convert an SBOL2 document to an SBOL3 document + + :param doc2: SBOL2 document to convert + :returns: SBOL3 document + """ + doc3 = sbol3.Document() + # TODO: build converter here + return doc3 diff --git a/test/test_files/BBa_J23101.xml b/test/test_files/BBa_J23101.xml new file mode 100644 index 00000000..9b12bea5 --- /dev/null +++ b/test/test_files/BBa_J23101.xml @@ -0,0 +1,62 @@ + + + later + + + 1 + N/A + In stock + BBa_J23101 + Released HQ 2013 + true + later + + true + + true + + + + false + 2015-08-31T04:08:40Z + + 2006-08-03T11:00:00Z + + BBa_J23101 + constitutive promoter family member + + + 483 + 95 + _52_ + + John Anderson + 0 + + + + BBa_J23101_sequence + + + + + + + + tttacagctagctcagtcctaggtattatgctagc + + 1 + + + Chris J. Myers + + 2017-03-06T15:00:00+00:00 + 1 + + + James Alastair McLaughlin + + + igem2sbol + + diff --git a/test/test_sbol2_sbol3_direct.py b/test/test_sbol2_sbol3_direct.py new file mode 100644 index 00000000..cb0b1688 --- /dev/null +++ b/test/test_sbol2_sbol3_direct.py @@ -0,0 +1,30 @@ +import tempfile +from pathlib import Path + +import unittest + +import sbol2 +import sbol3 + +from sbol_utilities.conversion import convert2to3, convert3to2 +from sbol_utilities.sbol_diff import file_diff + +TEST_FILES = Path(__file__).parent / 'test_files' + + +class TestDirectSBOL2SBOL3Conversion(unittest.TestCase): + + def test_3to2_conversion(self): + """Test ability to convert a simple part from SBOL3 to SBOL2""" + # Load an SBOL3 document and check its contents + doc3 = sbol3.Document() + doc3.read(TEST_FILES / 'BBa_J23101.nt') + # Convert to SBOL2 and check contents + doc2 = convert3to2(doc3, True) + with tempfile.NamedTemporaryFile(suffix='.xml') as tmp: + doc2.write(tmp.name) + self.assertFalse(file_diff(tmp.name, str(TEST_FILES / 'BBa_J23101.xml'))) + + +if __name__ == '__main__': + unittest.main() From 541cca0b8696181592be1613adf6676f375c9f80 Mon Sep 17 00:00:00 2001 From: Jake Beal Date: Fri, 6 Oct 2023 21:14:46 -0400 Subject: [PATCH 5/8] Add a NotImplementedError for 2 to 3 conversion --- sbol_utilities/sbol3_sbol2_conversion.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/sbol_utilities/sbol3_sbol2_conversion.py b/sbol_utilities/sbol3_sbol2_conversion.py index 0d07d3b4..d503a331 100644 --- a/sbol_utilities/sbol3_sbol2_conversion.py +++ b/sbol_utilities/sbol3_sbol2_conversion.py @@ -253,5 +253,5 @@ def convert2to3(doc2: sbol2.Document) -> sbol3.Document: :returns: SBOL3 document """ doc3 = sbol3.Document() - # TODO: build converter here + raise NotImplementedError('Conversion from SBOL2 to SBOL3 not yet implemented') return doc3 From ecfa115018e95674eb2cf97adc31af71592944e5 Mon Sep 17 00:00:00 2001 From: Jake Beal Date: Fri, 6 Oct 2023 21:36:49 -0400 Subject: [PATCH 6/8] Mark Activity usage and association conversion bug with NotImplementError and reference to pySBOL3 bug that needs to be fixed. --- sbol_utilities/sbol3_sbol2_conversion.py | 14 +++++++++----- 1 file changed, 9 insertions(+), 5 deletions(-) diff --git a/sbol_utilities/sbol3_sbol2_conversion.py b/sbol_utilities/sbol3_sbol2_conversion.py index d503a331..07825f85 100644 --- a/sbol_utilities/sbol3_sbol2_conversion.py +++ b/sbol_utilities/sbol3_sbol2_conversion.py @@ -82,14 +82,18 @@ def visit_activity(self, act3: sbol3.Activity): act2 = sbol2.Activity(act3.identity, version=self._sbol2_version(act3)) if act3.types: if len(act3.types) > 1: - raise NotImplementedError('Conversion of multi-type activities to SBOL2 not yet implemented:' + raise NotImplementedError('Conversion of multi-type Activities to SBOL2 not yet implemented:' 'pySBOL2 currently supports a maximum of one type per activity' 'Bug: https://github.com/SynBioDex/pySBOL2/issues/428') act2.types = act3.types[0] # Take first type from list of length 1 act2.startedAtTime = act3.start_time act2.endedAtTime = act3.end_time - act2.usages = act3.usage - act2.associations = act3.association + if act3.usage or act3.association: + raise NotImplementedError('Conversion of Activity usage and association properties to SBOL2 ' + 'not yet implemented, due to visitors failing to return values' + 'Bug: https://github.com/SynBioDex/pySBOL3/issues/437') + act2.usages = [usage.accept(self) for usage in act3.usage] + act2.associations = [assoc.accept(self) for assoc in act3.association] # TODO: pySBOL3 is currently missing wasInformedBy (https://github.com/SynBioDex/pySBOL3/issues/436 # act2.wasInformedBy = act3.informed_by self.doc2.activities.add(act2) @@ -116,8 +120,8 @@ def visit_combinatorial_derivation(self, a: sbol3.CombinatorialDerivation): def visit_component(self, cp3: sbol3.Component): # Remap type if it's one of the ones that needs remapping; otherwise pass through unchanged - type_map = {sbol3.SBO_DNA: sbol2.BIOPAX_DNA, # TODO: distinguish biopax Dna from DnaRegion - sbol3.SBO_RNA: sbol2.BIOPAX_RNA, # TODO: distinguish biopax Rna from RnaRegion + type_map = {sbol3.SBO_DNA: sbol2.BIOPAX_DNA, # TODO: distinguish BioPAX Dna from DnaRegion + sbol3.SBO_RNA: sbol2.BIOPAX_RNA, # TODO: distinguish BioPAX Rna from RnaRegion sbol3.SBO_PROTEIN: sbol2.BIOPAX_PROTEIN, sbol3.SBO_SIMPLE_CHEMICAL: sbol2.BIOPAX_SMALL_MOLECULE, sbol3.SBO_NON_COVALENT_COMPLEX: sbol2.BIOPAX_COMPLEX} From 79bc9166df1f005d2e46fdc82add211320e2ff91 Mon Sep 17 00:00:00 2001 From: Jake Beal Date: Fri, 6 Oct 2023 21:39:47 -0400 Subject: [PATCH 7/8] convert2to3 should not be defaulting to using the native converter --- sbol_utilities/conversion.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/sbol_utilities/conversion.py b/sbol_utilities/conversion.py index 30792318..6510bdb4 100644 --- a/sbol_utilities/conversion.py +++ b/sbol_utilities/conversion.py @@ -70,7 +70,7 @@ def convert_identities2to3(sbol3_data: str) -> str: return g.serialize(format="xml") -def convert2to3(sbol2_doc: Union[str, sbol2.Document], namespaces=None, use_native_converter: bool = True) \ +def convert2to3(sbol2_doc: Union[str, sbol2.Document], namespaces=None, use_native_converter: bool = False) \ -> sbol3.Document: """Convert an SBOL2 document to an equivalent SBOL3 document From 3c4765a45e41657549703263e9b786fa7139f71b Mon Sep 17 00:00:00 2001 From: Jake Beal Date: Sat, 7 Oct 2023 06:55:26 -0400 Subject: [PATCH 8/8] Add TODO notes about conversion edge cases that need to be checked / handled --- sbol_utilities/sbol3_sbol2_conversion.py | 3 +++ 1 file changed, 3 insertions(+) diff --git a/sbol_utilities/sbol3_sbol2_conversion.py b/sbol_utilities/sbol3_sbol2_conversion.py index 07825f85..55f76c8c 100644 --- a/sbol_utilities/sbol3_sbol2_conversion.py +++ b/sbol_utilities/sbol3_sbol2_conversion.py @@ -40,6 +40,9 @@ def _convert(self, doc3: sbol3.Document): # Try conversion, resetting saved parameter values afterward try: doc3.accept(self) + # TODO: make sure that complex extension objects (e.g., from SBOLFactory) are properly converted + # TODO: make sure that unhandled SBOL child objects / properties will throw errors + # TODO: check if we need to add post-creation fix-up of links, to ensure they point to objects finally: sbol2.Config.setOption(sbol2.ConfigOptions.SBOL_COMPLIANT_URIS.value, saved_compliance) sbol2.setHomespace(saved_homespace)