From fa1f13bd7be59c22a279c509e454eea790b683de Mon Sep 17 00:00:00 2001 From: Bryan Bartley Date: Sun, 4 Jun 2023 11:11:51 -0700 Subject: [PATCH 01/17] refobj property returns object not string --- sbol3/document.py | 6 +++ sbol3/object.py | 2 + sbol3/refobj_property.py | 46 ++++++++++------- test/test_referenced_object.py | 91 ++++++++++++++++++++++------------ 4 files changed, 97 insertions(+), 48 deletions(-) diff --git a/sbol3/document.py b/sbol3/document.py index 07b6c9fa..e8d9d3d4 100644 --- a/sbol3/document.py +++ b/sbol3/document.py @@ -221,6 +221,12 @@ def _parse_attributes(objects, graph) -> dict[str, Identified]: if o not in obj._properties[str_p]: obj._properties[str_p].append(o) else: + # check for referenced objects + if type(o) is rdflib.URIRef: + print(obj._properties[str_p]) + other_identity = str(o) + if other_identity in objects: + o = objects[other_identity] obj._properties[str_p].append(o) return child_objects diff --git a/sbol3/object.py b/sbol3/object.py index 1d009610..7b80cb6d 100644 --- a/sbol3/object.py +++ b/sbol3/object.py @@ -149,6 +149,8 @@ def copy(self, target_doc=None, target_namespace=None): return new_obj + def lookup(self): + return self def replace_namespace(old_uri, target_namespace, rdf_type): diff --git a/sbol3/refobj_property.py b/sbol3/refobj_property.py index 9aa7a206..44d1585e 100644 --- a/sbol3/refobj_property.py +++ b/sbol3/refobj_property.py @@ -23,25 +23,37 @@ def lookup(self): class ReferencedObjectMixin: def to_user(self, value: Any) -> str: - result = ReferencedURI(str(value)) if hasattr(self, 'property_owner'): parent = self.property_owner - result.parent = parent - return result - - @staticmethod - def from_user(value: Any) -> rdflib.URIRef: - if isinstance(value, SBOLObject): - # see https://github.com/SynBioDex/pySBOL3/issues/357 - if value.identity is None: - # The SBOLObject has an uninitialized identity - msg = f'Cannot set reference to {value}.' - msg += ' Object identity is uninitialized.' - raise ValueError(msg) - value = value.identity - if not isinstance(value, str): - raise TypeError(f'Expecting string, got {type(value)}') - return rdflib.URIRef(value) + value.parent = parent + # Should we check to see if value has a document as well? + return value + + #@staticmethod + def from_user(self, value: Any) -> rdflib.URIRef: + #if isinstance(value, SBOLObject): + # # see https://github.com/SynBioDex/pySBOL3/issues/357 + # if value.identity is None: + # # The SBOLObject has an uninitialized identity + # msg = f'Cannot set reference to {value}.' + # msg += ' Object identity is uninitialized.' + # raise ValueError(msg) + # value = value.identity + #if not isinstance(value, str): + # raise TypeError(f'Expecting string, got {type(value)}') + + # TODO: what is value is empty string? + if type(value) is str: + if self.property_owner.document: + value = self.property_owner.document.find(value) + # TODO: warn user referenced object is not in document + if value is not None: + return value + # If not found in Document + value = SBOLObject(value) + if not isinstance(value, SBOLObject): + raise TypeError('Cannot set property, the value must be str or instance of SBOLObect') + return value def maybe_add_to_document(self, value: Any) -> None: # if not isinstance(value, TopLevel): diff --git a/test/test_referenced_object.py b/test/test_referenced_object.py index fd1e1536..7baf6257 100644 --- a/test/test_referenced_object.py +++ b/test/test_referenced_object.py @@ -2,6 +2,8 @@ import unittest import sbol3 +import rdflib + MODULE_LOCATION = os.path.dirname(os.path.abspath(__file__)) SBOL3_LOCATION = os.path.join(MODULE_LOCATION, 'SBOLTestSuite', 'SBOL3') @@ -25,7 +27,7 @@ def setUp(self) -> None: def tearDown(self) -> None: sbol3.set_defaults() - def test_lookup(self): + def test_parse_refobj(self): test_path = os.path.join(SBOL3_LOCATION, 'entity', 'model', 'model.ttl') test_format = sbol3.TURTLE @@ -33,14 +35,17 @@ def test_lookup(self): doc.read(test_path, test_format) component = doc.find('toggle_switch') self.assertIsNotNone(component) - model_uri = component.models[0] - self.assertTrue(isinstance(model_uri, str)) - self.assertTrue(hasattr(model_uri, 'lookup')) - self.assertEqual('https://sbolstandard.org/examples/model1', model_uri) - model = model_uri.lookup() + model = component.models[0] + print('Model:', model) + print(type(model)) + self.assertFalse(type(model) is rdflib.URIRef) + self.assertTrue(type(model) is sbol3.Model) + self.assertTrue(hasattr(model, 'lookup'), f'{model}') + self.assertEqual('https://sbolstandard.org/examples/model1', model.identity) + model = model.lookup() self.assertIsNotNone(model) - def test_uri_assignment(self): + def test_uri_assignment_and_resolution(self): # Test assignment to a ReferencedObject attribute with a URI string sbol3.set_namespace('https://github.com/synbiodex/pysbol3') doc = sbol3.Document() @@ -49,11 +54,22 @@ def test_uri_assignment(self): doc.add(component) doc.add(sequence) component.sequences.append(sequence.identity) - seq2_uri = component.sequences[0] - self.assertEqual(sequence.identity, seq2_uri) - seq = seq2_uri.lookup() - self.assertIsNotNone(seq) - self.assertEqual(sequence, seq) + self.assertEqual(sequence, component.sequences[0]) + + def test_uri_assignment_not_resolved(self): + # Test assignment to a ReferencedObject attribute with a URI string + sbol3.set_namespace('https://github.com/synbiodex/pysbol3') + doc = sbol3.Document() + component = sbol3.Component('c1', sbol3.SBO_DNA) + sequence = sbol3.Sequence('seq1') + doc.add(component) + + # Because the Sequence is not contained in the Document, + # we can't resolve the reference + component.sequences.append(sequence.identity) + self.assertNotEqual(sequence, component.sequences[0]) + self.assertTrue(type(component.sequences[0]) is sbol3.SBOLObject) + def test_instance_append(self): # Test assignment to a ReferencedObject attribute with an @@ -65,11 +81,7 @@ def test_instance_append(self): doc.add(component) doc.add(sequence) component.sequences.append(sequence) - seq2_uri = component.sequences[0] - self.assertEqual(sequence.identity, seq2_uri) - seq = seq2_uri.lookup() - self.assertIsNotNone(seq) - self.assertEqual(sequence, seq) + self.assertEqual(sequence, component.sequences[0]) def test_instance_assignment(self): # Test assignment to a ReferencedObject attribute with an @@ -81,11 +93,10 @@ def test_instance_assignment(self): doc.add(component) doc.add(sequence) component.sequences = [sequence] - seq2_uri = component.sequences[0] - self.assertEqual(sequence.identity, seq2_uri) - seq = seq2_uri.lookup() - self.assertIsNotNone(seq) - self.assertEqual(sequence, seq) + self.assertEqual(component.sequences[0], sequence) + + def test_lookup_reverse_compatible(self): + pass def test_singleton_assignment(self): # Test assignment to a ReferencedObject attribute with an @@ -97,11 +108,7 @@ def test_singleton_assignment(self): doc.add(test_parent) doc.add(sequence) test_parent.sequence = sequence - seq2_uri = test_parent.sequence - self.assertEqual(sequence.identity, seq2_uri) - seq = seq2_uri.lookup() - self.assertIsNotNone(seq) - self.assertEqual(sequence, seq) + self.assertEqual(test_parent.sequence, sequence) def test_adding_referenced_objects(self): # Verify that sbol3 does not try to add objects @@ -120,10 +127,32 @@ def test_adding_referenced_objects(self): # Now explicitly add foo to the document and ensure # everything works as expected doc.add(foo) - self.assertEqual(execution.identity, foo.members[0]) - # Also verify that we can use lookup on the object - # to get back to the original instance via document lookup - self.assertEqual(execution.identity, foo.members[0].lookup().identity) + self.assertEqual(execution, foo.members[0]) + + def test_adding_referenced_objects(self): + # Verify that sbol3 does not try to add objects + # to the document when they are added to a referenced + # object property. + # + # See https://github.com/SynBioDex/pySBOL3/issues/184 + # Test assignment to a ReferencedObject attribute with a URI string + doc = sbol3.Document() + sbol3.set_namespace('https://example.org') + foo = sbol3.Collection('https://example.org/baz') + doc.add(foo) + + execution = sbol3.Activity('protocol_execution') + foo.members.append(execution) + # Verify that execution did not get document assigned + self.assertIsNone(execution.document) + self.assertNotIn(execution, doc.objects) + + # Now explicitly add foo to the document and ensure + # everything works as expected + doc.add(execution) + foo.members.append(execution) + self.assertEqual(execution, foo.members[0]) + def test_no_identity_exception(self): # See https://github.com/SynBioDex/pySBOL3/issues/357 From a73248a3a0519cd0fe0c171ee36a05d566bffd57 Mon Sep 17 00:00:00 2001 From: Bryan Bartley Date: Sun, 4 Jun 2023 21:55:21 -0700 Subject: [PATCH 02/17] Parsing, serialization, and copy works --- sbol3/document.py | 17 +++-- sbol3/identified.py | 6 ++ sbol3/object.py | 18 +++++ sbol3/property_base.py | 5 +- sbol3/refobj_property.py | 14 +++- test/test_referenced_object.py | 124 ++++++++++++++++++++++++++------- 6 files changed, 147 insertions(+), 37 deletions(-) diff --git a/sbol3/document.py b/sbol3/document.py index e8d9d3d4..4a8193b0 100644 --- a/sbol3/document.py +++ b/sbol3/document.py @@ -213,6 +213,17 @@ def _parse_attributes(objects, graph) -> dict[str, Identified]: obj._owned_objects[str_p].append(other) # Record the assigned object as a child child_objects[other_identity] = other + elif str_p in obj._referenced_objects: + reference = str(o) + # A reference may refer to another object + # in the current document, or it may be + # an external reference to another RDF entity + if reference in objects: + other = objects[reference] + obj._referenced_objects[str_p].append(other) + else: + # If an external reference, create a base SBOLObject to represent it + obj._referenced_objects[str_p].append(SBOLObject(reference)) elif str_p == RDF_TYPE: # Handle rdf:type specially because the main type(s) # will already be in the list from the build_object @@ -221,12 +232,6 @@ def _parse_attributes(objects, graph) -> dict[str, Identified]: if o not in obj._properties[str_p]: obj._properties[str_p].append(o) else: - # check for referenced objects - if type(o) is rdflib.URIRef: - print(obj._properties[str_p]) - other_identity = str(o) - if other_identity in objects: - o = objects[other_identity] obj._properties[str_p].append(o) return child_objects diff --git a/sbol3/identified.py b/sbol3/identified.py index 3edd9a46..d27cf66c 100644 --- a/sbol3/identified.py +++ b/sbol3/identified.py @@ -269,6 +269,12 @@ def serialize(self, graph: rdflib.Graph): rdf_prop = rdflib.URIRef(prop) for item in items: graph.add((identity, rdf_prop, item)) + for prop, items in self._referenced_objects.items(): + if not items: + continue + rdf_prop = rdflib.URIRef(prop) + for item in items: + graph.add((identity, rdf_prop, rdflib.URIRef(item.identity))) for prop, items in self._owned_objects.items(): if not items: continue diff --git a/sbol3/object.py b/sbol3/object.py index 7b80cb6d..0f3f57f6 100644 --- a/sbol3/object.py +++ b/sbol3/object.py @@ -14,6 +14,7 @@ class SBOLObject: def __init__(self, name: str) -> None: self._properties = defaultdict(list) self._owned_objects = defaultdict(list) + self._referenced_objects = defaultdict(list) # Does this need to be a property? It does not get serialized to the RDF file. # Could it be an attribute that gets composed on the fly? Keep it simple for # now, and change to a property in the future if needed. @@ -147,6 +148,23 @@ def copy(self, target_doc=None, target_namespace=None): new_obj._owned_objects[property_uri].append(o_copy) o_copy.parent = self + # After we have copied all the owned objects, copy the referenced objects + # and attempt to resolve the references + if target_doc: + for property_uri, object_store in self._referenced_objects.items(): + for o in object_store: + referenced_obj = target_doc.find(o.identity) + if referenced_obj: + new_obj._referenced_objects[property_uri].append(referenced_obj) + else: + new_obj._referenced_objects[property_uri].append(SBOLObject(o.identity)) + else: + # If the copy does not belong to a Document, then treat all references + # like external references + for property_uri, object_store in self._referenced_objects.items(): + for o in object_store: + new_obj._referenced_objects[property_uri].append(SBOLObject(o.identity)) + return new_obj def lookup(self): diff --git a/sbol3/property_base.py b/sbol3/property_base.py index 7ef6fbdb..3d8a78bf 100644 --- a/sbol3/property_base.py +++ b/sbol3/property_base.py @@ -175,7 +175,10 @@ def set(self, value: Any) -> None: msg = f'{name} requires one or more values' msg += ' packed in an iterable' raise TypeError(msg) - items = [self.from_user(v) for v in value] + try: + items = [self.from_user(v) for v in value] + except Exception as e: + print(e) self._storage()[self.property_uri] = items for val in value: self.item_added(val) diff --git a/sbol3/refobj_property.py b/sbol3/refobj_property.py index 44d1585e..bd3d211f 100644 --- a/sbol3/refobj_property.py +++ b/sbol3/refobj_property.py @@ -22,6 +22,9 @@ def lookup(self): class ReferencedObjectMixin: + def _storage(self) -> Dict[str, list]: + return self.property_owner._referenced_objects + def to_user(self, value: Any) -> str: if hasattr(self, 'property_owner'): parent = self.property_owner @@ -45,14 +48,19 @@ def from_user(self, value: Any) -> rdflib.URIRef: # TODO: what is value is empty string? if type(value) is str: if self.property_owner.document: - value = self.property_owner.document.find(value) + referenced_obj = self.property_owner.document.find(value) # TODO: warn user referenced object is not in document - if value is not None: - return value + if referenced_obj is not None: + return referenced_obj # If not found in Document value = SBOLObject(value) if not isinstance(value, SBOLObject): raise TypeError('Cannot set property, the value must be str or instance of SBOLObect') + if value.identity is None: + # The SBOLObject has an uninitialized identity + msg = f'Cannot set reference to {value}.' + msg += ' Object identity is uninitialized.' + raise ValueError(msg) return value def maybe_add_to_document(self, value: Any) -> None: diff --git a/test/test_referenced_object.py b/test/test_referenced_object.py index 7baf6257..1c2362a1 100644 --- a/test/test_referenced_object.py +++ b/test/test_referenced_object.py @@ -36,25 +36,110 @@ def test_parse_refobj(self): component = doc.find('toggle_switch') self.assertIsNotNone(component) model = component.models[0] - print('Model:', model) - print(type(model)) self.assertFalse(type(model) is rdflib.URIRef) self.assertTrue(type(model) is sbol3.Model) self.assertTrue(hasattr(model, 'lookup'), f'{model}') self.assertEqual('https://sbolstandard.org/examples/model1', model.identity) - model = model.lookup() - self.assertIsNotNone(model) + + # Test reverse compatibility with lookup + model_lookup = model.lookup() + self.assertTrue(model_lookup is model) + + def test_parse_external_reference(self): + # When parsing a document, if we encounter a reference to an object + # not in this document, create a stub object using SBOLObject + test_sbol=''' +@base . +@prefix : . +@prefix sbol: . +@prefix SBO: . + +:toggle_switch a sbol:Component ; + sbol:description "Toggle Switch genetic circuit" ; + sbol:displayId "toggle_switch" ; + sbol:hasModel :model1 ; + sbol:hasNamespace ; + sbol:name "Toggle Switch" ; + sbol:type SBO:0000241 . +''' + test_format = sbol3.TURTLE + + doc = sbol3.Document() + doc.read_string(test_sbol, file_format=test_format) + component = doc.find('toggle_switch') + model = component.models[0] + self.assertTrue(type(model) is sbol3.SBOLObject) + self.assertEqual(model.identity, 'https://sbolstandard.org/examples/model1') + + def test_serialize_external_reference(self): + # When serializing a document, if we encounter a reference to an object + # not in this document, serialize it as a URI + test_sbol=''' +@base . +@prefix : . +@prefix sbol: . +@prefix SBO: . + +:toggle_switch a sbol:Component ; + sbol:description "Toggle Switch genetic circuit" ; + sbol:displayId "toggle_switch" ; + sbol:hasModel :model1 ; + sbol:hasNamespace ; + sbol:name "Toggle Switch" ; + sbol:type SBO:0000241 . +''' + test_format = sbol3.TURTLE + + doc = sbol3.Document() + doc2 = sbol3.Document() + + doc.read_string(test_sbol, file_format=test_format) + component = doc.find('toggle_switch') + model = component.models[0] + self.assertTrue(type(model) is sbol3.SBOLObject) + self.assertEqual(model.identity, 'https://sbolstandard.org/examples/model1') + + doc2.read_string(doc.write_string(file_format=test_format), file_format=test_format) + component = doc2.find('toggle_switch') + model = component.models[0] + + + def test_copy(self): + test_path = os.path.join(SBOL3_LOCATION, 'entity', 'model', 'model.ttl') + test_format = sbol3.TURTLE + + doc = sbol3.Document() + doc2 = sbol3.Document() + + doc.read(test_path, test_format) + component = doc.find('toggle_switch') + model = component.models[0] + self.assertTrue(type(model) is sbol3.Model) + + # When the Component is copied to a new document, + # its reference to the Sequence should be treated as an external reference + component_copy = component.copy(target_doc=doc2) + model = component_copy.models[0] + self.assertTrue(type(model) is sbol3.SBOLObject) + def test_uri_assignment_and_resolution(self): # Test assignment to a ReferencedObject attribute with a URI string sbol3.set_namespace('https://github.com/synbiodex/pysbol3') doc = sbol3.Document() component = sbol3.Component('c1', sbol3.SBO_DNA) - sequence = sbol3.Sequence('seq1') + seq1 = sbol3.Sequence('seq1') + seq2 = sbol3.Sequence('seq2') doc.add(component) - doc.add(sequence) - component.sequences.append(sequence.identity) - self.assertEqual(sequence, component.sequences[0]) + doc.add(seq1) + doc.add(seq2) + component.sequences.append(seq1.identity) + self.assertEqual(seq1, component.sequences[0]) + component.sequences = [seq1.identity] + self.assertEqual(seq1, component.sequences[0]) + component.sequences = [seq1.identity, seq2.identity] + self.assertEqual(seq1, component.sequences[0]) + self.assertEqual(seq2, component.sequences[1]) def test_uri_assignment_not_resolved(self): # Test assignment to a ReferencedObject attribute with a URI string @@ -110,25 +195,6 @@ def test_singleton_assignment(self): test_parent.sequence = sequence self.assertEqual(test_parent.sequence, sequence) - def test_adding_referenced_objects(self): - # Verify that sbol3 does not try to add objects - # to the document when they are added to a referenced - # object property. - # - # See https://github.com/SynBioDex/pySBOL3/issues/184 - doc = sbol3.Document() - sbol3.set_namespace('https://example.org') - execution = sbol3.Activity('protocol_execution') - doc.add(execution) - foo = sbol3.Collection('https://example.org/baz') - foo.members.append(execution) - # Verify that foo did not get document assigned - self.assertIsNone(foo.document) - # Now explicitly add foo to the document and ensure - # everything works as expected - doc.add(foo) - self.assertEqual(execution, foo.members[0]) - def test_adding_referenced_objects(self): # Verify that sbol3 does not try to add objects # to the document when they are added to a referenced @@ -142,7 +208,11 @@ def test_adding_referenced_objects(self): doc.add(foo) execution = sbol3.Activity('protocol_execution') + self.assertFalse(execution in foo._owned_objects[sbol3.SBOL_MEMBER]) foo.members.append(execution) + self.assertTrue(execution in foo._referenced_objects[sbol3.SBOL_MEMBER]) + self.assertFalse(execution in foo._owned_objects[sbol3.SBOL_MEMBER]) + # Verify that execution did not get document assigned self.assertIsNone(execution.document) self.assertNotIn(execution, doc.objects) From 1983fe30d1a3b1c0d9e52b51a78ba2dd2e896146 Mon Sep 17 00:00:00 2001 From: Bryan Bartley Date: Thu, 8 Jun 2023 12:02:57 -0700 Subject: [PATCH 03/17] Add equality operator --- sbol3/object.py | 8 ++++++++ test/test_referenced_object.py | 11 +++++++++++ 2 files changed, 19 insertions(+) diff --git a/sbol3/object.py b/sbol3/object.py index 0f3f57f6..bf641c97 100644 --- a/sbol3/object.py +++ b/sbol3/object.py @@ -37,6 +37,14 @@ def __getattribute__(self, name): result = result.get() return result + def __eq__(self, other): + if type(other) is str: + return self.identity == other + return super().__eq__(other) + + def __hash__(self): + return hash(self.identity) + @staticmethod def _is_url(name: str) -> bool: parsed = urlparse(name) diff --git a/test/test_referenced_object.py b/test/test_referenced_object.py index 1c2362a1..83cfb494 100644 --- a/test/test_referenced_object.py +++ b/test/test_referenced_object.py @@ -233,6 +233,17 @@ def test_no_identity_exception(self): with self.assertRaisesRegex(ValueError, exc_regex): collection.members.append(subc) + def test_equality(self): + # Test comparison of a referenced object to a URI, in order + # to maintain reverse compatibility + foo = sbol3.SBOLObject('foo') + self.assertEqual(foo, foo.identity) + self.assertEqual(foo.identity, foo) + + def test_update(self): + # Update and resolve references to an external object when the object is + # added to the Document + pass if __name__ == '__main__': unittest.main() From 25695732901727b0e856211c29a7a455c783a7ca Mon Sep 17 00:00:00 2001 From: Bryan Bartley Date: Mon, 12 Jun 2023 13:28:54 -0700 Subject: [PATCH 04/17] Prototype implementation of reference counter --- sbol3/object.py | 1 + sbol3/refobj_property.py | 36 +++++++++++++++++++---------- test/test_referenced_object.py | 41 ++++++++++++++++++++++++++++++++++ 3 files changed, 66 insertions(+), 12 deletions(-) diff --git a/sbol3/object.py b/sbol3/object.py index bf641c97..671e9eda 100644 --- a/sbol3/object.py +++ b/sbol3/object.py @@ -19,6 +19,7 @@ def __init__(self, name: str) -> None: # Could it be an attribute that gets composed on the fly? Keep it simple for # now, and change to a property in the future if needed. self._identity = SBOLObject._make_identity(name) + self._references = [] def __setattr__(self, name, value): try: diff --git a/sbol3/refobj_property.py b/sbol3/refobj_property.py index bd3d211f..ac06e7f6 100644 --- a/sbol3/refobj_property.py +++ b/sbol3/refobj_property.py @@ -25,6 +25,10 @@ class ReferencedObjectMixin: def _storage(self) -> Dict[str, list]: return self.property_owner._referenced_objects + def _clear(self): + for obj in self._storage()[self.property_uri]: + obj._references = [] + def to_user(self, value: Any) -> str: if hasattr(self, 'property_owner'): parent = self.property_owner @@ -32,25 +36,15 @@ def to_user(self, value: Any) -> str: # Should we check to see if value has a document as well? return value - #@staticmethod def from_user(self, value: Any) -> rdflib.URIRef: - #if isinstance(value, SBOLObject): - # # see https://github.com/SynBioDex/pySBOL3/issues/357 - # if value.identity is None: - # # The SBOLObject has an uninitialized identity - # msg = f'Cannot set reference to {value}.' - # msg += ' Object identity is uninitialized.' - # raise ValueError(msg) - # value = value.identity - #if not isinstance(value, str): - # raise TypeError(f'Expecting string, got {type(value)}') - # TODO: what is value is empty string? if type(value) is str: if self.property_owner.document: referenced_obj = self.property_owner.document.find(value) # TODO: warn user referenced object is not in document if referenced_obj is not None: + if self not in referenced_obj._references: + referenced_obj._references += [self.property_owner] return referenced_obj # If not found in Document value = SBOLObject(value) @@ -61,6 +55,7 @@ def from_user(self, value: Any) -> rdflib.URIRef: msg = f'Cannot set reference to {value}.' msg += ' Object identity is uninitialized.' raise ValueError(msg) + value._references += [self.property_owner] return value def maybe_add_to_document(self, value: Any) -> None: @@ -87,6 +82,8 @@ def __init__(self, property_owner: Any, property_uri: str, self.set(initial_value) def set(self, value: Any) -> None: + for o in self._storage()[self.property_uri]: + o._references.remove(self.property_owner) super().set(value) # See bug 184 - don't add to document # self.maybe_add_to_document(value) @@ -107,6 +104,21 @@ def __init__(self, property_owner: Any, property_uri: str, initial_value = [initial_value] self.set(initial_value) + def __setitem__(self, key: Union[int, slice], value: Any) -> None: + replaced_obj = self._storage()[self.property_uri].__getitem__(key) + replaced_obj._references.remove(self.property_owner) + super().__setitem__(key, value) + + def __delitem__(self, key: Union[int, slice]) -> None: + replaced_item = self._storage()[self.property_uri][key] + replaced_item._references.remove(self.property_owner) + super().__delitem__(key) + + def set(self, value: Any) -> None: + for o in self._storage()[self.property_uri]: + o._references.remove(self.property_owner) + super().set(value) + # See bug 184 - don't add to document # def item_added(self, item: Any) -> None: # self.maybe_add_to_document(item) diff --git a/test/test_referenced_object.py b/test/test_referenced_object.py index 83cfb494..1dec6e76 100644 --- a/test/test_referenced_object.py +++ b/test/test_referenced_object.py @@ -133,6 +133,7 @@ def test_uri_assignment_and_resolution(self): doc.add(component) doc.add(seq1) doc.add(seq2) + component.sequences.append(seq1.identity) self.assertEqual(seq1, component.sequences[0]) component.sequences = [seq1.identity] @@ -240,6 +241,46 @@ def test_equality(self): self.assertEqual(foo, foo.identity) self.assertEqual(foo.identity, foo) + def test_singleton_property_reference_counter(self): + sbol3.set_namespace('https://github.com/synbiodex/pysbol3') + doc = sbol3.Document() + root = sbol3.Component('root', sbol3.SBO_DNA) + sub = sbol3.Component('sub', sbol3.SBO_DNA) + + doc.add(root) + doc.add(sub) + + feature = sbol3.SubComponent(instance_of=root) + root.features.append(feature) + self.assertEqual(root._references, [feature]) + + def test_list_property_reference_counter(self): + sbol3.set_namespace('https://github.com/synbiodex/pysbol3') + doc = sbol3.Document() + component = sbol3.Component('c1', sbol3.SBO_DNA) + + # Test that the reference counter is initialized + seq1 = sbol3.Sequence('seq1') + self.assertListEqual(seq1._references, []) + doc.add(component) + doc.add(seq1) + + # Test that the reference counter is working + component.sequences = [seq1.identity] + self.assertListEqual(seq1._references, [component]) + + # Test that the reference counter is cleared + component.sequences = [] + self.assertListEqual(seq1._references, []) + + # Test that the reference counter works with the append method + component.sequences.append(seq1.identity) + self.assertListEqual(seq1._references, [component]) + + # Test that the reference counter is cleared + component.sequences.remove(seq1) + self.assertListEqual(seq1._references, []) + def test_update(self): # Update and resolve references to an external object when the object is # added to the Document From 27ede0e7523e32ff9871c526c1c0ff338dc18909 Mon Sep 17 00:00:00 2001 From: Bryan Bartley Date: Thu, 12 Oct 2023 18:53:49 -0700 Subject: [PATCH 05/17] Implement assignment operations on ReferencedObjectList --- sbol3/refobj_property.py | 4 ++++ test/test_referenced_object.py | 33 ++++++++++++++++++++++++++++----- 2 files changed, 32 insertions(+), 5 deletions(-) diff --git a/sbol3/refobj_property.py b/sbol3/refobj_property.py index ac06e7f6..1dffec49 100644 --- a/sbol3/refobj_property.py +++ b/sbol3/refobj_property.py @@ -115,6 +115,10 @@ def __delitem__(self, key: Union[int, slice]) -> None: super().__delitem__(key) def set(self, value: Any) -> None: + # If the current value of the property + # is identical to the value being set, do nothing. + if value == self._storage()[self.property_uri]: + return for o in self._storage()[self.property_uri]: o._references.remove(self.property_owner) super().set(value) diff --git a/test/test_referenced_object.py b/test/test_referenced_object.py index 1dec6e76..c60d43af 100644 --- a/test/test_referenced_object.py +++ b/test/test_referenced_object.py @@ -123,6 +123,27 @@ def test_copy(self): self.assertTrue(type(model) is sbol3.SBOLObject) + def test_insert_into_list(self): + # Test assignment using list indices + sbol3.set_namespace('https://github.com/synbiodex/pysbol3') + doc = sbol3.Document() + component = sbol3.Component('c1', sbol3.SBO_DNA) + seq1 = sbol3.Sequence('seq1') + seq2 = sbol3.Sequence('seq2') + doc.add(component) + doc.add(seq1) + doc.add(seq2) + component.sequences = [seq1] + self.assertIn(component, seq1._references) + + component.sequences[0] = seq1 + self.assertIn(component, seq1._references) + self.assertEqual(len(seq1._references), 1) + + component.sequences[0] = seq2 + self.assertIn(component, seq2._references) + self.assertNotIn(component, seq1._references) + def test_uri_assignment_and_resolution(self): # Test assignment to a ReferencedObject attribute with a URI string sbol3.set_namespace('https://github.com/synbiodex/pysbol3') @@ -135,12 +156,15 @@ def test_uri_assignment_and_resolution(self): doc.add(seq2) component.sequences.append(seq1.identity) - self.assertEqual(seq1, component.sequences[0]) + self.assertEqual(list(component.sequences), [seq1]) + self.assertListEqual(seq1._references, [component]) + component.sequences = [seq1.identity] - self.assertEqual(seq1, component.sequences[0]) + self.assertListEqual(list(component.sequences), [seq1]) + self.assertListEqual(seq1._references, [component]) + component.sequences = [seq1.identity, seq2.identity] - self.assertEqual(seq1, component.sequences[0]) - self.assertEqual(seq2, component.sequences[1]) + self.assertListEqual(list(component.sequences), [seq1, seq2]) def test_uri_assignment_not_resolved(self): # Test assignment to a ReferencedObject attribute with a URI string @@ -223,7 +247,6 @@ def test_adding_referenced_objects(self): doc.add(execution) foo.members.append(execution) self.assertEqual(execution, foo.members[0]) - def test_no_identity_exception(self): # See https://github.com/SynBioDex/pySBOL3/issues/357 From 11c73a3ff23a8d9cfc5a670695015b5652231027 Mon Sep 17 00:00:00 2001 From: Bryan Bartley Date: Fri, 13 Oct 2023 08:43:22 -0700 Subject: [PATCH 06/17] Duplicate code is bad --- test/test_referenced_object.py | 46 ++++++++++++---------------------- 1 file changed, 16 insertions(+), 30 deletions(-) diff --git a/test/test_referenced_object.py b/test/test_referenced_object.py index c60d43af..d742f8ab 100644 --- a/test/test_referenced_object.py +++ b/test/test_referenced_object.py @@ -21,6 +21,20 @@ def __init__(self, identity: str, type_uri: str = SRO_URI): class TestReferencedObject(unittest.TestCase): + TEST_SBOL = ''' +@base . +@prefix : . +@prefix sbol: . +@prefix SBO: . + +:toggle_switch a sbol:Component ; + sbol:description "Toggle Switch genetic circuit" ; + sbol:displayId "toggle_switch" ; + sbol:hasModel :model1 ; + sbol:hasNamespace ; + sbol:name "Toggle Switch" ; + sbol:type SBO:0000241 .''' + def setUp(self) -> None: sbol3.set_defaults() @@ -48,24 +62,10 @@ def test_parse_refobj(self): def test_parse_external_reference(self): # When parsing a document, if we encounter a reference to an object # not in this document, create a stub object using SBOLObject - test_sbol=''' -@base . -@prefix : . -@prefix sbol: . -@prefix SBO: . - -:toggle_switch a sbol:Component ; - sbol:description "Toggle Switch genetic circuit" ; - sbol:displayId "toggle_switch" ; - sbol:hasModel :model1 ; - sbol:hasNamespace ; - sbol:name "Toggle Switch" ; - sbol:type SBO:0000241 . -''' test_format = sbol3.TURTLE doc = sbol3.Document() - doc.read_string(test_sbol, file_format=test_format) + doc.read_string(TestReferencedObject.TEST_SBOL, file_format=test_format) component = doc.find('toggle_switch') model = component.models[0] self.assertTrue(type(model) is sbol3.SBOLObject) @@ -74,26 +74,12 @@ def test_parse_external_reference(self): def test_serialize_external_reference(self): # When serializing a document, if we encounter a reference to an object # not in this document, serialize it as a URI - test_sbol=''' -@base . -@prefix : . -@prefix sbol: . -@prefix SBO: . - -:toggle_switch a sbol:Component ; - sbol:description "Toggle Switch genetic circuit" ; - sbol:displayId "toggle_switch" ; - sbol:hasModel :model1 ; - sbol:hasNamespace ; - sbol:name "Toggle Switch" ; - sbol:type SBO:0000241 . -''' test_format = sbol3.TURTLE doc = sbol3.Document() doc2 = sbol3.Document() - doc.read_string(test_sbol, file_format=test_format) + doc.read_string(TestReferencedObject.TEST_SBOL, file_format=test_format) component = doc.find('toggle_switch') model = component.models[0] self.assertTrue(type(model) is sbol3.SBOLObject) From 2a5764d3336e21bd27f181045cbc8d40bfe94706 Mon Sep 17 00:00:00 2001 From: Bryan Bartley Date: Sat, 14 Oct 2023 13:07:29 -0700 Subject: [PATCH 07/17] Replace stub objects when a reference is resolved --- sbol3/document.py | 18 ++++- sbol3/object.py | 14 ++++ sbol3/refobj_property.py | 8 ++- test/test_referenced_object.py | 123 ++++++++++++++++++++------------- 4 files changed, 111 insertions(+), 52 deletions(-) diff --git a/sbol3/document.py b/sbol3/document.py index 4a8193b0..ad2e01b1 100644 --- a/sbol3/document.py +++ b/sbol3/document.py @@ -223,7 +223,9 @@ def _parse_attributes(objects, graph) -> dict[str, Identified]: obj._referenced_objects[str_p].append(other) else: # If an external reference, create a base SBOLObject to represent it - obj._referenced_objects[str_p].append(SBOLObject(reference)) + stub = SBOLObject(reference) + obj._referenced_objects[str_p].append(stub) + stub._references.append(obj) # Add to reference counter elif str_p == RDF_TYPE: # Handle rdf:type specially because the main type(s) # will already be in the list from the build_object @@ -369,7 +371,12 @@ def _add(self, obj: TopLevel) -> TopLevel: # in the TopLevel being added def assign_document(x: Identified): x.document = self + obj.traverse(assign_document) + + # Update any external references to this object + # replacing stub SBOLObjects with this one + self._resolve_references(obj) return obj def _add_all(self, objects: pytyping.Sequence[TopLevel]) -> pytyping.Sequence[TopLevel]: @@ -433,6 +440,11 @@ def find(self, search_string: str) -> Optional[Identified]: return obj return self._find_in_objects(search_string) + def _resolve_references(self, new_obj): + """Update all unresolved references to this object, replacing stub SBOLObject with this one.""" + for updated in self.objects: + updated._resolve_references(new_obj) + def join_lines(self, lines: List[Union[bytes, str]]) -> Union[bytes, str]: """Join lines for either bytes or strings. Joins a list of lines together whether they are bytes or strings. Returns a bytes if the input was @@ -690,6 +702,10 @@ def remove(self, objects: Iterable[TopLevel]): # Now do the removal of each top level object and all of its children for obj in objects_to_remove: obj.remove_from_document() + # If the removed object is referenced anywhere, + # leave a stub + stub_obj = SBOLObject(obj.identity) + self._resolve_references(stub_obj) def remove_object(self, top_level: TopLevel): """Removes the given TopLevel from this document. No referential diff --git a/sbol3/object.py b/sbol3/object.py index 671e9eda..b8f73c64 100644 --- a/sbol3/object.py +++ b/sbol3/object.py @@ -106,6 +106,20 @@ def find(self, search_string: str) -> Optional['SBOLObject']: return result return None + def _resolve_references(self, new_obj): + NEW_OBJ = new_obj + def resolve_references(x): + for property_id, references in x._referenced_objects.items(): + needs_updating = False + for ref_obj in references: + if ref_obj.identity == NEW_OBJ.identity: + needs_updating = True + break + if needs_updating: + references.remove(ref_obj) + references.append(new_obj) + self.traverse(resolve_references) + def copy(self, target_doc=None, target_namespace=None): # Delete this method in v1.1 diff --git a/sbol3/refobj_property.py b/sbol3/refobj_property.py index 1dffec49..4bb68be1 100644 --- a/sbol3/refobj_property.py +++ b/sbol3/refobj_property.py @@ -41,13 +41,16 @@ def from_user(self, value: Any) -> rdflib.URIRef: if type(value) is str: if self.property_owner.document: referenced_obj = self.property_owner.document.find(value) - # TODO: warn user referenced object is not in document if referenced_obj is not None: + # Keep track of this reference to the object if self not in referenced_obj._references: referenced_obj._references += [self.property_owner] return referenced_obj - # If not found in Document + # The given URI refers to an object not currently in this + # Document, so create a stub + # TODO: warn user referenced object is not in document value = SBOLObject(value) + if not isinstance(value, SBOLObject): raise TypeError('Cannot set property, the value must be str or instance of SBOLObect') if value.identity is None: @@ -55,6 +58,7 @@ def from_user(self, value: Any) -> rdflib.URIRef: msg = f'Cannot set reference to {value}.' msg += ' Object identity is uninitialized.' raise ValueError(msg) + # Keep track of this reference to the object value._references += [self.property_owner] return value diff --git a/test/test_referenced_object.py b/test/test_referenced_object.py index d742f8ab..c371a8f6 100644 --- a/test/test_referenced_object.py +++ b/test/test_referenced_object.py @@ -21,20 +21,6 @@ def __init__(self, identity: str, type_uri: str = SRO_URI): class TestReferencedObject(unittest.TestCase): - TEST_SBOL = ''' -@base . -@prefix : . -@prefix sbol: . -@prefix SBO: . - -:toggle_switch a sbol:Component ; - sbol:description "Toggle Switch genetic circuit" ; - sbol:displayId "toggle_switch" ; - sbol:hasModel :model1 ; - sbol:hasNamespace ; - sbol:name "Toggle Switch" ; - sbol:type SBO:0000241 .''' - def setUp(self) -> None: sbol3.set_defaults() @@ -59,37 +45,6 @@ def test_parse_refobj(self): model_lookup = model.lookup() self.assertTrue(model_lookup is model) - def test_parse_external_reference(self): - # When parsing a document, if we encounter a reference to an object - # not in this document, create a stub object using SBOLObject - test_format = sbol3.TURTLE - - doc = sbol3.Document() - doc.read_string(TestReferencedObject.TEST_SBOL, file_format=test_format) - component = doc.find('toggle_switch') - model = component.models[0] - self.assertTrue(type(model) is sbol3.SBOLObject) - self.assertEqual(model.identity, 'https://sbolstandard.org/examples/model1') - - def test_serialize_external_reference(self): - # When serializing a document, if we encounter a reference to an object - # not in this document, serialize it as a URI - test_format = sbol3.TURTLE - - doc = sbol3.Document() - doc2 = sbol3.Document() - - doc.read_string(TestReferencedObject.TEST_SBOL, file_format=test_format) - component = doc.find('toggle_switch') - model = component.models[0] - self.assertTrue(type(model) is sbol3.SBOLObject) - self.assertEqual(model.identity, 'https://sbolstandard.org/examples/model1') - - doc2.read_string(doc.write_string(file_format=test_format), file_format=test_format) - component = doc2.find('toggle_switch') - model = component.models[0] - - def test_copy(self): test_path = os.path.join(SBOL3_LOCATION, 'entity', 'model', 'model.ttl') test_format = sbol3.TURTLE @@ -108,7 +63,6 @@ def test_copy(self): model = component_copy.models[0] self.assertTrue(type(model) is sbol3.SBOLObject) - def test_insert_into_list(self): # Test assignment using list indices sbol3.set_namespace('https://github.com/synbiodex/pysbol3') @@ -290,10 +244,81 @@ def test_list_property_reference_counter(self): component.sequences.remove(seq1) self.assertListEqual(seq1._references, []) + +class TestExternalReferences(unittest.TestCase): + + TEST_SBOL = ''' +@base . +@prefix : . +@prefix sbol: . +@prefix SBO: . + +:toggle_switch a sbol:Component ; + sbol:description "Toggle Switch genetic circuit" ; + sbol:displayId "toggle_switch" ; + sbol:hasModel :model1 ; + sbol:hasNamespace ; + sbol:name "Toggle Switch" ; + sbol:type SBO:0000241 .''' + TEST_FORMAT = sbol3.TURTLE + + def setUp(self) -> None: + sbol3.set_namespace('https://sbolstandard.org/examples') + self.doc = sbol3.Document() + self.doc.read_string(TestExternalReferences.TEST_SBOL, + file_format=TestExternalReferences.TEST_FORMAT) + + def test_parse_external_reference(self): + # When parsing a document, if we encounter a reference to an object + # not in this document, create a stub object using SBOLObject + component = self.doc.find('toggle_switch') + model = component.models[0] + self.assertTrue(type(model) is sbol3.SBOLObject) + self.assertEqual(model.identity, 'https://sbolstandard.org/examples/model1') + self.assertListEqual(model._references, [component]) + + def test_serialize_external_reference(self): + # When serializing a document, if we encounter a reference to an object + # not in this document, serialize it as a URI + + roundtrip_doc = sbol3.Document() + roundtrip_doc.read_string(self.doc.write_string(file_format=TestExternalReferences.TEST_FORMAT), file_format=TestExternalReferences.TEST_FORMAT) + component = roundtrip_doc.find('toggle_switch') + model = component.models[0] + def test_update(self): - # Update and resolve references to an external object when the object is - # added to the Document - pass + # Update and resolve references to an external object when the + # object is added to the Document. Upon resolving the reference, + # the SBOLObject that represents the unresolved reference will be + # downcasted to the specific SBOL type + component = self.doc.find('toggle_switch') + model = component.models[0] + self.assertTrue(type(model) is sbol3.SBOLObject) + + # now add an object which resolves the reference + model = sbol3.Model('model1', source='foo', language='foo', framework='foo') + self.assertEqual(model.identity, 'https://sbolstandard.org/examples/model1') + self.doc.add(model) + + # Check whether dereferencing now returns a Model + # instead of SBOLObject + model = component.models[0] + self.assertFalse(type(model) is sbol3.SBOLObject) + self.assertTrue(type(model) is sbol3.Model) + + def test_remove(self): + # The reverse of test_update, removing an object from a Document + # creates an unresolved, external reference. The object in the + # Document should be upcast to an SBOLObject + component = self.doc.find('toggle_switch') + model = sbol3.Model('model1', source='foo', language='foo', framework='foo') + self.doc.add(model) + self.doc.remove([model]) + + model = component.models[0] + self.assertFalse(type(model) is sbol3.Model) + self.assertTrue(type(model) is sbol3.SBOLObject) + if __name__ == '__main__': unittest.main() From 190c6c3a23a0767f78901ce128b268b46bb8c8ab Mon Sep 17 00:00:00 2001 From: Bryan Bartley Date: Sat, 14 Oct 2023 16:54:39 -0700 Subject: [PATCH 08/17] Remove debugging artifact --- sbol3/property_base.py | 5 +---- 1 file changed, 1 insertion(+), 4 deletions(-) diff --git a/sbol3/property_base.py b/sbol3/property_base.py index 3d8a78bf..7ef6fbdb 100644 --- a/sbol3/property_base.py +++ b/sbol3/property_base.py @@ -175,10 +175,7 @@ def set(self, value: Any) -> None: msg = f'{name} requires one or more values' msg += ' packed in an iterable' raise TypeError(msg) - try: - items = [self.from_user(v) for v in value] - except Exception as e: - print(e) + items = [self.from_user(v) for v in value] self._storage()[self.property_uri] = items for val in value: self.item_added(val) From 9bd4921a8b95ee119c34d6450bfc20df96de0ad2 Mon Sep 17 00:00:00 2001 From: Bryan Bartley Date: Sat, 14 Oct 2023 16:55:50 -0700 Subject: [PATCH 09/17] Update tests --- test/test_collection.py | 8 ++++---- test/test_component.py | 9 +++++---- test/test_implementation.py | 2 +- test/test_om_unit.py | 4 ++-- 4 files changed, 12 insertions(+), 11 deletions(-) diff --git a/test/test_collection.py b/test/test_collection.py index dcdd65d7..71fbdbe5 100644 --- a/test/test_collection.py +++ b/test/test_collection.py @@ -35,14 +35,14 @@ def test_member_property(self): sbol3.set_namespace('https://github.com/synbiodex/pysbol3') self.assertTrue(hasattr(sbol3, 'SBOL_MEMBER')) collection = sbol3.Collection('collection1') - self.assertIn(sbol3.SBOL_MEMBER, collection._properties) + self.assertIn(sbol3.SBOL_MEMBER, collection._referenced_objects) self.assertNotIn(sbol3.SBOL_ORIENTATION, collection._properties) uris = ['https://github.com/synbiodex/pysbol3/thing1', 'https://github.com/synbiodex/pysbol3/thing2'] collection.members = uris - self.assertIn(sbol3.SBOL_MEMBER, collection._properties) - self.assertNotIn(sbol3.SBOL_ORIENTATION, collection._properties) - self.assertEqual(uris, collection.members) + self.assertIn(sbol3.SBOL_MEMBER, collection._referenced_objects) + self.assertNotIn(sbol3.SBOL_ORIENTATION, collection._referenced_objects) + self.assertListEqual(uris, [m.identity for m in collection.members]) # Namespace testing def test_namespace_deduced(self): diff --git a/test/test_component.py b/test/test_component.py index 8486d926..e1a2dc45 100644 --- a/test/test_component.py +++ b/test/test_component.py @@ -94,7 +94,8 @@ def test_cloning_with_references(self): c2 = c1.clone(new_identity) self.assertEqual(posixpath.join(sbol3.get_namespace(), new_identity), c2.identity) - self.assertListEqual(list(c1.sequences), list(c2.sequences)) + self.assertListEqual([s.identity for s in c1.sequences], + [s.identity for s in c2.sequences]) def test_cloning_with_children(self): # This test does not use `sbol3.set_namespace` as the other @@ -129,7 +130,7 @@ def test_cloning_with_children(self): self.assertIsInstance(es2, sbol3.EntireSequence) self.assertNotEqual(es1.identity, es2.identity) self.assertTrue(es2.identity.startswith(c2.identity)) - self.assertEqual(es1.sequence, es2.sequence) + self.assertEqual(es1.sequence.identity, es2.sequence.identity) self.assertIsNone(es2.document) def test_cloning_references(self): @@ -159,7 +160,7 @@ def test_cloning_references(self): self.assertIsInstance(s_clone, sbol3.ComponentReference) self.assertTrue(s_clone.identity.startswith(toggle_clone.identity)) self.assertNotEqual(s.identity, s_clone.identity) - self.assertEqual(s.refers_to, s_clone.refers_to) + self.assertEqual(s.refers_to.identity, s_clone.refers_to.identity) o = c.object.lookup() self.assertIsInstance(o, sbol3.ComponentReference) self.assertTrue(o.identity.startswith(toggle.identity)) @@ -167,7 +168,7 @@ def test_cloning_references(self): self.assertIsInstance(o_clone, sbol3.ComponentReference) self.assertTrue(o_clone.identity.startswith(toggle_clone.identity)) self.assertNotEqual(o.identity, o_clone.identity) - self.assertEqual(o.refers_to, o_clone.refers_to) + self.assertEqual(o.refers_to.identity, o_clone.refers_to.identity) def test_measures_initial_value(self): # See https://github.com/SynBioDex/pySBOL3/issues/301 diff --git a/test/test_implementation.py b/test/test_implementation.py index b784dc25..bc7f5991 100644 --- a/test/test_implementation.py +++ b/test/test_implementation.py @@ -41,7 +41,7 @@ def test_read_from_file(self): self.assertIsInstance(implementation, sbol3.Implementation) tetr_uri = 'https://sbolstandard.org/examples/TetR_protein' built = tetr_uri - self.assertCountEqual(built, implementation.built) + self.assertCountEqual(built, implementation.built.identity) if __name__ == '__main__': diff --git a/test/test_om_unit.py b/test/test_om_unit.py index 39367065..e8d9607a 100644 --- a/test/test_om_unit.py +++ b/test/test_om_unit.py @@ -77,7 +77,7 @@ def test_create(self): punit = sbol3.PrefixedUnit(display_id, symbol, label, unit, prefix) self.assertIsNotNone(punit) self.assertIsInstance(punit, sbol3.PrefixedUnit) - self.assertCountEqual(prefix, punit.prefix) + self.assertCountEqual(prefix, punit.prefix.identity) self.assertEqual(unit, punit.unit) self.assertEqual(symbol, punit.symbol) self.assertEqual(label, punit.label) @@ -92,7 +92,7 @@ def test_read_from_file(self): punit = doc.find(uri) self.assertIsNotNone(punit) self.assertIsInstance(punit, sbol3.PrefixedUnit) - self.assertCountEqual('https://sbolstandard.org/examples/milli', punit.prefix) + self.assertCountEqual('https://sbolstandard.org/examples/milli', punit.prefix.identity) self.assertEqual('https://sbolstandard.org/examples/mole', punit.unit) self.assertEqual('millimole', punit.label) self.assertEqual('millimole', punit.name) From 6e2ea5e20320d96953ff4797fc5552447373cd22 Mon Sep 17 00:00:00 2001 From: Bryan Bartley Date: Sat, 14 Oct 2023 17:14:53 -0700 Subject: [PATCH 10/17] Fix style errors --- sbol3/document.py | 2 +- sbol3/object.py | 7 +++++-- sbol3/refobj_property.py | 2 +- test/test_referenced_object.py | 18 ++++++++++-------- 4 files changed, 17 insertions(+), 12 deletions(-) diff --git a/sbol3/document.py b/sbol3/document.py index ad2e01b1..60e6f184 100644 --- a/sbol3/document.py +++ b/sbol3/document.py @@ -376,7 +376,7 @@ def assign_document(x: Identified): # Update any external references to this object # replacing stub SBOLObjects with this one - self._resolve_references(obj) + self._resolve_references(obj) return obj def _add_all(self, objects: pytyping.Sequence[TopLevel]) -> pytyping.Sequence[TopLevel]: diff --git a/sbol3/object.py b/sbol3/object.py index b8f73c64..ec277e17 100644 --- a/sbol3/object.py +++ b/sbol3/object.py @@ -108,6 +108,7 @@ def find(self, search_string: str) -> Optional['SBOLObject']: def _resolve_references(self, new_obj): NEW_OBJ = new_obj + def resolve_references(x): for property_id, references in x._referenced_objects.items(): needs_updating = False @@ -118,6 +119,7 @@ def resolve_references(x): if needs_updating: references.remove(ref_obj) references.append(new_obj) + self.traverse(resolve_references) def copy(self, target_doc=None, target_namespace=None): @@ -180,19 +182,20 @@ def copy(self, target_doc=None, target_namespace=None): if referenced_obj: new_obj._referenced_objects[property_uri].append(referenced_obj) else: - new_obj._referenced_objects[property_uri].append(SBOLObject(o.identity)) + new_obj._referenced_objects[property_uri].append(SBOLObject(o.identity)) else: # If the copy does not belong to a Document, then treat all references # like external references for property_uri, object_store in self._referenced_objects.items(): for o in object_store: - new_obj._referenced_objects[property_uri].append(SBOLObject(o.identity)) + new_obj._referenced_objects[property_uri].append(SBOLObject(o.identity)) return new_obj def lookup(self): return self + def replace_namespace(old_uri, target_namespace, rdf_type): # Flag as not working to ensure nobody calls this function thinking diff --git a/sbol3/refobj_property.py b/sbol3/refobj_property.py index 4bb68be1..4027bbcf 100644 --- a/sbol3/refobj_property.py +++ b/sbol3/refobj_property.py @@ -110,7 +110,7 @@ def __init__(self, property_owner: Any, property_uri: str, def __setitem__(self, key: Union[int, slice], value: Any) -> None: replaced_obj = self._storage()[self.property_uri].__getitem__(key) - replaced_obj._references.remove(self.property_owner) + replaced_obj._references.remove(self.property_owner) super().__setitem__(key, value) def __delitem__(self, key: Union[int, slice]) -> None: diff --git a/test/test_referenced_object.py b/test/test_referenced_object.py index c371a8f6..0aacd0e8 100644 --- a/test/test_referenced_object.py +++ b/test/test_referenced_object.py @@ -119,7 +119,6 @@ def test_uri_assignment_not_resolved(self): component.sequences.append(sequence.identity) self.assertNotEqual(sequence, component.sequences[0]) self.assertTrue(type(component.sequences[0]) is sbol3.SBOLObject) - def test_instance_append(self): # Test assignment to a ReferencedObject attribute with an @@ -203,7 +202,7 @@ def test_equality(self): foo = sbol3.SBOLObject('foo') self.assertEqual(foo, foo.identity) self.assertEqual(foo.identity, foo) - + def test_singleton_property_reference_counter(self): sbol3.set_namespace('https://github.com/synbiodex/pysbol3') doc = sbol3.Document() @@ -227,7 +226,7 @@ def test_list_property_reference_counter(self): self.assertListEqual(seq1._references, []) doc.add(component) doc.add(seq1) - + # Test that the reference counter is working component.sequences = [seq1.identity] self.assertListEqual(seq1._references, [component]) @@ -269,7 +268,7 @@ def setUp(self) -> None: file_format=TestExternalReferences.TEST_FORMAT) def test_parse_external_reference(self): - # When parsing a document, if we encounter a reference to an object + # When parsing a document, if we encounter a reference to an object # not in this document, create a stub object using SBOLObject component = self.doc.find('toggle_switch') model = component.models[0] @@ -278,11 +277,14 @@ def test_parse_external_reference(self): self.assertListEqual(model._references, [component]) def test_serialize_external_reference(self): - # When serializing a document, if we encounter a reference to an object + # When serializing a document, if we encounter a reference to an object # not in this document, serialize it as a URI roundtrip_doc = sbol3.Document() - roundtrip_doc.read_string(self.doc.write_string(file_format=TestExternalReferences.TEST_FORMAT), file_format=TestExternalReferences.TEST_FORMAT) + roundtrip_doc.read_string( + self.doc.write_string(file_format=TestExternalReferences.TEST_FORMAT), + file_format=TestExternalReferences.TEST_FORMAT + ) component = roundtrip_doc.find('toggle_switch') model = component.models[0] @@ -300,7 +302,7 @@ def test_update(self): self.assertEqual(model.identity, 'https://sbolstandard.org/examples/model1') self.doc.add(model) - # Check whether dereferencing now returns a Model + # Check whether dereferencing now returns a Model # instead of SBOLObject model = component.models[0] self.assertFalse(type(model) is sbol3.SBOLObject) @@ -318,7 +320,7 @@ def test_remove(self): model = component.models[0] self.assertFalse(type(model) is sbol3.Model) self.assertTrue(type(model) is sbol3.SBOLObject) - + if __name__ == '__main__': unittest.main() From 783f1b560124fda6db8d37b7208de8485544de9c Mon Sep 17 00:00:00 2001 From: Bryan Bartley Date: Sun, 15 Oct 2023 20:10:23 -0700 Subject: [PATCH 11/17] Resolve references and replace stubs during copy operations --- sbol3/document.py | 17 +++++++++ sbol3/object.py | 32 ++++++++++------- sbol3/toplevel.py | 16 +++++++++ test/test_referenced_object.py | 63 +++++++++++++++++++++++++++++++--- 4 files changed, 111 insertions(+), 17 deletions(-) diff --git a/sbol3/document.py b/sbol3/document.py index 60e6f184..2ea4d6a0 100644 --- a/sbol3/document.py +++ b/sbol3/document.py @@ -221,6 +221,7 @@ def _parse_attributes(objects, graph) -> dict[str, Identified]: if reference in objects: other = objects[reference] obj._referenced_objects[str_p].append(other) + other._references.append(obj) else: # If an external reference, create a base SBOLObject to represent it stub = SBOLObject(reference) @@ -374,9 +375,24 @@ def assign_document(x: Identified): obj.traverse(assign_document) + # Update any external references from this object, + # which may be resolved upon adding it to the Document. + # Stub SBOLObjects will be replaced with the actual + # referenced object + for property_id, ref_objects in obj._referenced_objects.items(): + updated = [] + for ref_obj in ref_objects: + resolved_reference = self.find(ref_obj.identity) + if resolved_reference: + updated.append(resolved_reference) + else: + updated.append(ref_obj) + obj._referenced_objects[property_id] = updated + # Update any external references to this object # replacing stub SBOLObjects with this one self._resolve_references(obj) + return obj def _add_all(self, objects: pytyping.Sequence[TopLevel]) -> pytyping.Sequence[TopLevel]: @@ -705,6 +721,7 @@ def remove(self, objects: Iterable[TopLevel]): # If the removed object is referenced anywhere, # leave a stub stub_obj = SBOLObject(obj.identity) + assert stub_obj.identity == obj.identity self._resolve_references(stub_obj) def remove_object(self, top_level: TopLevel): diff --git a/sbol3/object.py b/sbol3/object.py index ec277e17..f9c52f2b 100644 --- a/sbol3/object.py +++ b/sbol3/object.py @@ -107,19 +107,7 @@ def find(self, search_string: str) -> Optional['SBOLObject']: return None def _resolve_references(self, new_obj): - NEW_OBJ = new_obj - - def resolve_references(x): - for property_id, references in x._referenced_objects.items(): - needs_updating = False - for ref_obj in references: - if ref_obj.identity == NEW_OBJ.identity: - needs_updating = True - break - if needs_updating: - references.remove(ref_obj) - references.append(new_obj) - + resolve_references = make_resolve_references_traverser(new_obj) self.traverse(resolve_references) def copy(self, target_doc=None, target_namespace=None): @@ -204,6 +192,24 @@ def replace_namespace(old_uri, target_namespace, rdf_type): raise NotImplementedError() +def make_resolve_references_traverser(new_obj) -> Callable: + # Return a callback for traversing documents and updating + # any references to new_obj + + def resolve_references(x): + for property_id, references in x._referenced_objects.items(): + needs_updating = False + for ref_obj in references: + if ref_obj.identity == new_obj.identity: + needs_updating = True + break + if needs_updating: + references.remove(ref_obj) + references.append(new_obj) + + return resolve_references + + # Global store for builder methods. Custom SBOL classes # register their builders in this store BUILDER_REGISTER: Dict[str, Callable[[str, str], SBOLObject]] = {} diff --git a/sbol3/toplevel.py b/sbol3/toplevel.py index 7cca28d5..46376dee 100644 --- a/sbol3/toplevel.py +++ b/sbol3/toplevel.py @@ -149,6 +149,22 @@ def clone(self, new_identity: str = None) -> 'TopLevel': obj.document = None obj.update_all_dependents(identity_map) + + # Replace references with stub objects, because + # the cloned object is not associated with a + # Document, resulting in external references + def reset_references(x): + for property_id, object_store in x._referenced_objects.items(): + upcast_objects = [] + for o in object_store: + stub = SBOLObject(o.identity) + assert stub.identity == o.identity + stub._references = o._references.copy() + upcast_objects.append(stub) + x._referenced_objects[property_id] = upcast_objects + + obj.traverse(reset_references) + obj._references = [SBOLObject(r.identity) for r in self._references] return obj def update_all_dependents(self, identity_map: dict[str, Identified]) -> Any: diff --git a/test/test_referenced_object.py b/test/test_referenced_object.py index 0aacd0e8..74bb72ce 100644 --- a/test/test_referenced_object.py +++ b/test/test_referenced_object.py @@ -40,6 +40,7 @@ def test_parse_refobj(self): self.assertTrue(type(model) is sbol3.Model) self.assertTrue(hasattr(model, 'lookup'), f'{model}') self.assertEqual('https://sbolstandard.org/examples/model1', model.identity) + self.assertListEqual(model._references, [component]) # Test reverse compatibility with lookup model_lookup = model.lookup() @@ -57,11 +58,65 @@ def test_copy(self): model = component.models[0] self.assertTrue(type(model) is sbol3.Model) + # When the Component is cloned, + # its reference to the Model should be upcast as + # since it becomes a reference to an external object + component_copy = component.clone() + model_copy = component_copy.models[0] + self.assertFalse(type(model_copy) is sbol3.Model) + self.assertTrue(type(model_copy) is sbol3.SBOLObject) + self.assertEqual(model_copy.identity, model.identity) + self.assertListEqual(model_copy._references, [component_copy]) + self.assertEqual(model_copy._references[0].identity, + component_copy.identity) + # When the Component is copied to a new document, - # its reference to the Sequence should be treated as an external reference - component_copy = component.copy(target_doc=doc2) - model = component_copy.models[0] - self.assertTrue(type(model) is sbol3.SBOLObject) + # its reference to the Model should be upcast as + # since it becomes a reference to an external object + #component_copy = component.copy(target_doc=doc2) + [component_copy] = sbol3.copy([component], into_document=doc2) + self.assertEqual(len(doc2.objects), 1) + model_copy = component_copy.models[0] + self.assertTrue(type(model_copy) is sbol3.SBOLObject) + + [model_copy] = sbol3.copy([model], into_document=doc2) + self.assertEqual(len(doc2.objects), 2) + model_copy = component_copy.models[0] + self.assertTrue(type(model_copy) is sbol3.Model) + + def test_copy_in_different_order(self): + # Test upcasting/downcasting to stub SBOLObjects by + # copying the referenced object first, followed by the + # parent object that makes the reference + test_path = os.path.join(SBOL3_LOCATION, 'entity', 'model', 'model.ttl') + test_format = sbol3.TURTLE + + doc = sbol3.Document() + doc2 = sbol3.Document() + + doc.read(test_path, test_format) + component = doc.find('toggle_switch') + model = component.models[0] + self.assertTrue(type(model) is sbol3.Model) + self.assertListEqual(model._references, [component]) + self.assertEqual(model._references[0].identity, + component.identity) + + model_copy = model.clone() + self.assertEqual(type(model_copy._references[0]), + sbol3.SBOLObject) + + [model_copy] = sbol3.copy([model], into_document=doc2) + self.assertEqual(len(doc2.objects), 1) + self.assertEqual(type(model_copy._references[0]), + sbol3.SBOLObject) + self.assertEqual(model_copy._references[0].identity, + component.identity) + + [component_copy] = sbol3.copy([component], into_document=doc2) + self.assertEqual(len(doc2.objects), 2) + model_copy = component_copy.models[0] + self.assertTrue(type(model_copy) is sbol3.Model) def test_insert_into_list(self): # Test assignment using list indices From dce93f03c5acc56866eba2ac963137b2a3a82246 Mon Sep 17 00:00:00 2001 From: Bryan Bartley Date: Sun, 15 Oct 2023 20:48:46 -0700 Subject: [PATCH 12/17] When cloning, maintain references internal to the object tree --- sbol3/toplevel.py | 34 ++++++++++++++++++---------------- 1 file changed, 18 insertions(+), 16 deletions(-) diff --git a/sbol3/toplevel.py b/sbol3/toplevel.py index 46376dee..4cd09c9a 100644 --- a/sbol3/toplevel.py +++ b/sbol3/toplevel.py @@ -140,32 +140,34 @@ def set_identity(self, new_identity: str) -> Any: self._display_id = self._extract_display_id(self._identity) def clone(self, new_identity: str = None) -> 'TopLevel': - obj = copy.deepcopy(self) - identity_map = {self.identity: obj} + clone = copy.deepcopy(self) + identity_map = {self.identity: clone} # Set identity of new object if new_identity is not None: - obj.set_identity(new_identity) + clone.set_identity(new_identity) # Drop the document pointer - obj.document = None + clone.document = None - obj.update_all_dependents(identity_map) + clone.update_all_dependents(identity_map) # Replace references with stub objects, because # the cloned object is not associated with a # Document, resulting in external references def reset_references(x): - for property_id, object_store in x._referenced_objects.items(): - upcast_objects = [] - for o in object_store: - stub = SBOLObject(o.identity) - assert stub.identity == o.identity - stub._references = o._references.copy() - upcast_objects.append(stub) - x._referenced_objects[property_id] = upcast_objects + for property_id, ref_objects in x._referenced_objects.items(): + updated_objects = [] + for o in ref_objects: + if clone.find(o.identity): + updated_objects.append(o) + else: + stub = SBOLObject(o.identity) + stub._references = o._references.copy() + updated_objects.append(stub) + x._referenced_objects[property_id] = updated_objects - obj.traverse(reset_references) - obj._references = [SBOLObject(r.identity) for r in self._references] - return obj + clone.traverse(reset_references) + clone._references = [SBOLObject(r.identity) for r in self._references] + return clone def update_all_dependents(self, identity_map: dict[str, Identified]) -> Any: """Update all dependent objects based on the provided identity map. From 1929965ebf60c25a9dc5f25c1dac5737577588ed Mon Sep 17 00:00:00 2001 From: Bryan Bartley Date: Sun, 15 Oct 2023 20:52:27 -0700 Subject: [PATCH 13/17] Fix style --- sbol3/object.py | 2 +- sbol3/toplevel.py | 2 +- test/test_referenced_object.py | 9 ++++----- 3 files changed, 6 insertions(+), 7 deletions(-) diff --git a/sbol3/object.py b/sbol3/object.py index f9c52f2b..5357d60c 100644 --- a/sbol3/object.py +++ b/sbol3/object.py @@ -208,7 +208,7 @@ def resolve_references(x): references.append(new_obj) return resolve_references - + # Global store for builder methods. Custom SBOL classes # register their builders in this store diff --git a/sbol3/toplevel.py b/sbol3/toplevel.py index 4cd09c9a..d89b574e 100644 --- a/sbol3/toplevel.py +++ b/sbol3/toplevel.py @@ -163,7 +163,7 @@ def reset_references(x): stub = SBOLObject(o.identity) stub._references = o._references.copy() updated_objects.append(stub) - x._referenced_objects[property_id] = updated_objects + x._referenced_objects[property_id] = updated_objects clone.traverse(reset_references) clone._references = [SBOLObject(r.identity) for r in self._references] diff --git a/test/test_referenced_object.py b/test/test_referenced_object.py index 74bb72ce..e9bc4d31 100644 --- a/test/test_referenced_object.py +++ b/test/test_referenced_object.py @@ -73,7 +73,6 @@ def test_copy(self): # When the Component is copied to a new document, # its reference to the Model should be upcast as # since it becomes a reference to an external object - #component_copy = component.copy(target_doc=doc2) [component_copy] = sbol3.copy([component], into_document=doc2) self.assertEqual(len(doc2.objects), 1) model_copy = component_copy.models[0] @@ -103,14 +102,14 @@ def test_copy_in_different_order(self): component.identity) model_copy = model.clone() - self.assertEqual(type(model_copy._references[0]), + self.assertEqual(type(model_copy._references[0]), sbol3.SBOLObject) [model_copy] = sbol3.copy([model], into_document=doc2) self.assertEqual(len(doc2.objects), 1) - self.assertEqual(type(model_copy._references[0]), + self.assertEqual(type(model_copy._references[0]), sbol3.SBOLObject) - self.assertEqual(model_copy._references[0].identity, + self.assertEqual(model_copy._references[0].identity, component.identity) [component_copy] = sbol3.copy([component], into_document=doc2) @@ -375,7 +374,7 @@ def test_remove(self): model = component.models[0] self.assertFalse(type(model) is sbol3.Model) self.assertTrue(type(model) is sbol3.SBOLObject) - + if __name__ == '__main__': unittest.main() From 99aaf637fd12a8008c05caa367045aa61b8a7460 Mon Sep 17 00:00:00 2001 From: Bryan Bartley Date: Sun, 15 Oct 2023 21:36:30 -0700 Subject: [PATCH 14/17] Set up tests by setting namespace --- test/test_referenced_object.py | 12 +----------- 1 file changed, 1 insertion(+), 11 deletions(-) diff --git a/test/test_referenced_object.py b/test/test_referenced_object.py index e9bc4d31..0041810f 100644 --- a/test/test_referenced_object.py +++ b/test/test_referenced_object.py @@ -22,7 +22,7 @@ def __init__(self, identity: str, type_uri: str = SRO_URI): class TestReferencedObject(unittest.TestCase): def setUp(self) -> None: - sbol3.set_defaults() + sbol3.set_namespace('https://github.com/synbiodex/pysbol3') def tearDown(self) -> None: sbol3.set_defaults() @@ -119,7 +119,6 @@ def test_copy_in_different_order(self): def test_insert_into_list(self): # Test assignment using list indices - sbol3.set_namespace('https://github.com/synbiodex/pysbol3') doc = sbol3.Document() component = sbol3.Component('c1', sbol3.SBO_DNA) seq1 = sbol3.Sequence('seq1') @@ -140,7 +139,6 @@ def test_insert_into_list(self): def test_uri_assignment_and_resolution(self): # Test assignment to a ReferencedObject attribute with a URI string - sbol3.set_namespace('https://github.com/synbiodex/pysbol3') doc = sbol3.Document() component = sbol3.Component('c1', sbol3.SBO_DNA) seq1 = sbol3.Sequence('seq1') @@ -162,7 +160,6 @@ def test_uri_assignment_and_resolution(self): def test_uri_assignment_not_resolved(self): # Test assignment to a ReferencedObject attribute with a URI string - sbol3.set_namespace('https://github.com/synbiodex/pysbol3') doc = sbol3.Document() component = sbol3.Component('c1', sbol3.SBO_DNA) sequence = sbol3.Sequence('seq1') @@ -177,7 +174,6 @@ def test_uri_assignment_not_resolved(self): def test_instance_append(self): # Test assignment to a ReferencedObject attribute with an # instance using append - sbol3.set_namespace('https://github.com/synbiodex/pysbol3') doc = sbol3.Document() component = sbol3.Component('c1', sbol3.SBO_DNA) sequence = sbol3.Sequence('seq1') @@ -189,7 +185,6 @@ def test_instance_append(self): def test_instance_assignment(self): # Test assignment to a ReferencedObject attribute with an # instance using assignment - sbol3.set_namespace('https://github.com/synbiodex/pysbol3') doc = sbol3.Document() component = sbol3.Component('c1', sbol3.SBO_DNA) sequence = sbol3.Sequence('seq1') @@ -204,7 +199,6 @@ def test_lookup_reverse_compatible(self): def test_singleton_assignment(self): # Test assignment to a ReferencedObject attribute with an # instance using assignment - sbol3.set_namespace('https://github.com/synbiodex/pysbol3') doc = sbol3.Document() test_parent = SingleRefObj('sro1') sequence = sbol3.Sequence('seq1') @@ -221,7 +215,6 @@ def test_adding_referenced_objects(self): # See https://github.com/SynBioDex/pySBOL3/issues/184 # Test assignment to a ReferencedObject attribute with a URI string doc = sbol3.Document() - sbol3.set_namespace('https://example.org') foo = sbol3.Collection('https://example.org/baz') doc.add(foo) @@ -243,7 +236,6 @@ def test_adding_referenced_objects(self): def test_no_identity_exception(self): # See https://github.com/SynBioDex/pySBOL3/issues/357 - sbol3.set_namespace('https://github.com/SynBioDex/pySBOL3') collection = sbol3.Collection('foo_collection') subc = sbol3.SubComponent(instance_of='https://github.com/SynBioDex/pySBOL3/c1') exc_regex = r'Object identity is uninitialized\.$' @@ -258,7 +250,6 @@ def test_equality(self): self.assertEqual(foo.identity, foo) def test_singleton_property_reference_counter(self): - sbol3.set_namespace('https://github.com/synbiodex/pysbol3') doc = sbol3.Document() root = sbol3.Component('root', sbol3.SBO_DNA) sub = sbol3.Component('sub', sbol3.SBO_DNA) @@ -271,7 +262,6 @@ def test_singleton_property_reference_counter(self): self.assertEqual(root._references, [feature]) def test_list_property_reference_counter(self): - sbol3.set_namespace('https://github.com/synbiodex/pysbol3') doc = sbol3.Document() component = sbol3.Component('c1', sbol3.SBO_DNA) From 8b35f2d2313fe17d51f10cae054d783ee9e7eead Mon Sep 17 00:00:00 2001 From: Bryan Bartley Date: Sun, 15 Oct 2023 21:50:45 -0700 Subject: [PATCH 15/17] Delete deprecated copy method --- sbol3/object.py | 71 +------------------------------------------------ 1 file changed, 1 insertion(+), 70 deletions(-) diff --git a/sbol3/object.py b/sbol3/object.py index 5357d60c..0586b52a 100644 --- a/sbol3/object.py +++ b/sbol3/object.py @@ -110,76 +110,6 @@ def _resolve_references(self, new_obj): resolve_references = make_resolve_references_traverser(new_obj) self.traverse(resolve_references) - def copy(self, target_doc=None, target_namespace=None): - - # Delete this method in v1.1 - warnings.warn('Use sbol3.copy() instead', DeprecationWarning) - - new_uri = self.identity - - # If caller specified a target_namespace argument, then copy the object into this - # new namespace. - if target_namespace: - - # Map the identity of self into the target namespace - if hasattr(self, 'identity'): - old_uri = self.identity - new_uri = replace_namespace(old_uri, target_namespace, self.getTypeURI()) - - try: - builder = BUILDER_REGISTER[self.type_uri] - except KeyError: - logging.warning(f'No builder found for {self.type_uri}; assuming {self.__class__.__name__}') - builder = self.__class__ - new_obj = builder(**dict(identity=new_uri, type_uri=self.type_uri)) - - # Copy properties - for property_uri, value_store in self._properties.items(): - new_obj._properties[property_uri] = value_store.copy() - - # TODO: Map into new namespace - - # Assign the new object to the target Document - if target_doc is not None: - try: - target_doc.add(new_obj) - except TypeError: - pass # object is not TopLevel - - # When an object is simply being cloned, the value of wasDerivedFrom should be - # copied exactly as is from self. However, when copy is being used to generate - # a new entity, the wasDerivedFrom should point back to self. - if self.identity == new_obj.identity: - new_obj.derived_from = self.derived_from - else: - new_obj.derived_from = self.identity - - # Copy child objects recursively - for property_uri, object_list in self._owned_objects.items(): - for o in object_list: - o_copy = o.copy(target_doc, target_namespace) - new_obj._owned_objects[property_uri].append(o_copy) - o_copy.parent = self - - # After we have copied all the owned objects, copy the referenced objects - # and attempt to resolve the references - if target_doc: - for property_uri, object_store in self._referenced_objects.items(): - for o in object_store: - referenced_obj = target_doc.find(o.identity) - if referenced_obj: - new_obj._referenced_objects[property_uri].append(referenced_obj) - else: - new_obj._referenced_objects[property_uri].append(SBOLObject(o.identity)) - else: - # If the copy does not belong to a Document, then treat all references - # like external references - for property_uri, object_store in self._referenced_objects.items(): - for o in object_store: - new_obj._referenced_objects[property_uri].append(SBOLObject(o.identity)) - - return new_obj - def lookup(self): return self @@ -199,6 +129,7 @@ def make_resolve_references_traverser(new_obj) -> Callable: def resolve_references(x): for property_id, references in x._referenced_objects.items(): needs_updating = False + ref_obj = None for ref_obj in references: if ref_obj.identity == new_obj.identity: needs_updating = True From 6b2417bd066b3214b08def80218a21fd26a923ef Mon Sep 17 00:00:00 2001 From: Bryan Bartley Date: Sun, 15 Oct 2023 21:53:27 -0700 Subject: [PATCH 16/17] Fix code checks --- test/test_collection.py | 2 +- test/test_referenced_object.py | 3 ++- 2 files changed, 3 insertions(+), 2 deletions(-) diff --git a/test/test_collection.py b/test/test_collection.py index 71fbdbe5..5f67b19c 100644 --- a/test/test_collection.py +++ b/test/test_collection.py @@ -42,7 +42,7 @@ def test_member_property(self): collection.members = uris self.assertIn(sbol3.SBOL_MEMBER, collection._referenced_objects) self.assertNotIn(sbol3.SBOL_ORIENTATION, collection._referenced_objects) - self.assertListEqual(uris, [m.identity for m in collection.members]) + self.assertListEqual(uris, [m.identity for m in collection.members]) # pylint: disable=E1101 # Namespace testing def test_namespace_deduced(self): diff --git a/test/test_referenced_object.py b/test/test_referenced_object.py index 0041810f..aba72c64 100644 --- a/test/test_referenced_object.py +++ b/test/test_referenced_object.py @@ -1,9 +1,10 @@ import os import unittest -import sbol3 import rdflib +import sbol3 + MODULE_LOCATION = os.path.dirname(os.path.abspath(__file__)) SBOL3_LOCATION = os.path.join(MODULE_LOCATION, 'SBOLTestSuite', 'SBOL3') From e2f4a5ad8798e1d329944676f3f496fdea91f7d9 Mon Sep 17 00:00:00 2001 From: Bryan Bartley Date: Sun, 15 Oct 2023 22:02:27 -0700 Subject: [PATCH 17/17] Restore deprecated copy because too many tests still depend on it --- sbol3/object.py | 70 +++++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 70 insertions(+) diff --git a/sbol3/object.py b/sbol3/object.py index 0586b52a..02b4df93 100644 --- a/sbol3/object.py +++ b/sbol3/object.py @@ -110,6 +110,76 @@ def _resolve_references(self, new_obj): resolve_references = make_resolve_references_traverser(new_obj) self.traverse(resolve_references) + def copy(self, target_doc=None, target_namespace=None): # pylint: disable=R0912 + + # Delete this method in v1.1 + warnings.warn('Use sbol3.copy() instead', DeprecationWarning) + + new_uri = self.identity + + # If caller specified a target_namespace argument, then copy the object into this + # new namespace. + if target_namespace: + + # Map the identity of self into the target namespace + if hasattr(self, 'identity'): + old_uri = self.identity + new_uri = replace_namespace(old_uri, target_namespace, self.getTypeURI()) + + try: + builder = BUILDER_REGISTER[self.type_uri] + except KeyError: + logging.warning(f'No builder found for {self.type_uri}; assuming {self.__class__.__name__}') + builder = self.__class__ + new_obj = builder(**dict(identity=new_uri, type_uri=self.type_uri)) + + # Copy properties + for property_uri, value_store in self._properties.items(): + new_obj._properties[property_uri] = value_store.copy() + + # TODO: Map into new namespace + + # Assign the new object to the target Document + if target_doc is not None: + try: + target_doc.add(new_obj) + except TypeError: + pass # object is not TopLevel + + # When an object is simply being cloned, the value of wasDerivedFrom should be + # copied exactly as is from self. However, when copy is being used to generate + # a new entity, the wasDerivedFrom should point back to self. + if self.identity == new_obj.identity: + new_obj.derived_from = self.derived_from + else: + new_obj.derived_from = self.identity + + # Copy child objects recursively + for property_uri, object_list in self._owned_objects.items(): + for o in object_list: + o_copy = o.copy(target_doc, target_namespace) + new_obj._owned_objects[property_uri].append(o_copy) + o_copy.parent = self + + # After we have copied all the owned objects, copy the referenced objects + # and attempt to resolve the references + if target_doc: + for property_uri, object_store in self._referenced_objects.items(): + for o in object_store: + referenced_obj = target_doc.find(o.identity) + if referenced_obj: + new_obj._referenced_objects[property_uri].append(referenced_obj) + else: + new_obj._referenced_objects[property_uri].append(SBOLObject(o.identity)) + else: + # If the copy does not belong to a Document, then treat all references + # like external references + for property_uri, object_store in self._referenced_objects.items(): + for o in object_store: + new_obj._referenced_objects[property_uri].append(SBOLObject(o.identity)) + + return new_obj + def lookup(self): return self