Skip to content

Commit

Permalink
Replace stub objects when a reference is resolved
Browse files Browse the repository at this point in the history
  • Loading branch information
Bryan Bartley committed Oct 14, 2023
1 parent 11c73a3 commit 2a5764d
Show file tree
Hide file tree
Showing 4 changed files with 111 additions and 52 deletions.
18 changes: 17 additions & 1 deletion sbol3/document.py
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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]:
Expand Down Expand Up @@ -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
Expand Down Expand Up @@ -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
Expand Down
14 changes: 14 additions & 0 deletions sbol3/object.py
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
8 changes: 6 additions & 2 deletions sbol3/refobj_property.py
Original file line number Diff line number Diff line change
Expand Up @@ -41,20 +41,24 @@ 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:
# The SBOLObject has an uninitialized identity
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

Expand Down
123 changes: 74 additions & 49 deletions test/test_referenced_object.py
Original file line number Diff line number Diff line change
Expand Up @@ -21,20 +21,6 @@ def __init__(self, identity: str, type_uri: str = SRO_URI):

class TestReferencedObject(unittest.TestCase):

TEST_SBOL = '''
@base <https://sbolstandard.org/examples/> .
@prefix : <https://sbolstandard.org/examples/> .
@prefix sbol: <http://sbols.org/v3#> .
@prefix SBO: <https://identifiers.org/SBO:> .
:toggle_switch a sbol:Component ;
sbol:description "Toggle Switch genetic circuit" ;
sbol:displayId "toggle_switch" ;
sbol:hasModel :model1 ;
sbol:hasNamespace <https://sbolstandard.org/examples> ;
sbol:name "Toggle Switch" ;
sbol:type SBO:0000241 .'''

def setUp(self) -> None:
sbol3.set_defaults()

Expand All @@ -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
Expand All @@ -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')
Expand Down Expand Up @@ -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 <https://sbolstandard.org/examples/> .
@prefix : <https://sbolstandard.org/examples/> .
@prefix sbol: <http://sbols.org/v3#> .
@prefix SBO: <https://identifiers.org/SBO:> .
:toggle_switch a sbol:Component ;
sbol:description "Toggle Switch genetic circuit" ;
sbol:displayId "toggle_switch" ;
sbol:hasModel :model1 ;
sbol:hasNamespace <https://sbolstandard.org/examples> ;
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()

0 comments on commit 2a5764d

Please sign in to comment.