diff --git a/sbol3/document.py b/sbol3/document.py
index 4a8193b..ad2e01b 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 671e9ed..b8f73c6 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 1dffec4..4bb68be 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 d742f8a..c371a8f 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()