diff --git a/sbol3/document.py b/sbol3/document.py
index f65a636..8a255ad 100644
--- a/sbol3/document.py
+++ b/sbol3/document.py
@@ -213,6 +213,20 @@ 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)
+ other._references.append(obj)
+ else:
+ # If an external reference, create a base SBOLObject to represent it
+ 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
@@ -358,7 +372,27 @@ 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 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]:
@@ -422,6 +456,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
@@ -679,6 +718,11 @@ 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)
+ assert stub_obj.identity == 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/identified.py b/sbol3/identified.py
index 3edd9a4..d27cf66 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 1d00961..02b4df9 100644
--- a/sbol3/object.py
+++ b/sbol3/object.py
@@ -14,10 +14,12 @@ 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.
self._identity = SBOLObject._make_identity(name)
+ self._references = []
def __setattr__(self, name, value):
try:
@@ -36,6 +38,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)
@@ -96,7 +106,11 @@ def find(self, search_string: str) -> Optional['SBOLObject']:
return result
return None
- def copy(self, target_doc=None, target_namespace=None):
+ 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)
@@ -147,8 +161,28 @@ 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):
+ return self
+
def replace_namespace(old_uri, target_namespace, rdf_type):
@@ -158,6 +192,25 @@ 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
+ ref_obj = None
+ 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/refobj_property.py b/sbol3/refobj_property.py
index 9aa7a20..4027bbc 100644
--- a/sbol3/refobj_property.py
+++ b/sbol3/refobj_property.py
@@ -22,26 +22,45 @@ def lookup(self):
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:
- 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
+
+ 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:
+ referenced_obj = self.property_owner.document.find(value)
+ 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
+ # 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
def maybe_add_to_document(self, value: Any) -> None:
# if not isinstance(value, TopLevel):
@@ -67,6 +86,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)
@@ -87,6 +108,25 @@ 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:
+ # 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)
+
# See bug 184 - don't add to document
# def item_added(self, item: Any) -> None:
# self.maybe_add_to_document(item)
diff --git a/sbol3/toplevel.py b/sbol3/toplevel.py
index 7cca28d..d89b574 100644
--- a/sbol3/toplevel.py
+++ b/sbol3/toplevel.py
@@ -140,16 +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)
- return obj
+ 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, 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
+
+ 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.
diff --git a/test/test_collection.py b/test/test_collection.py
index dcdd65d..5f67b19 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]) # pylint: disable=E1101
# Namespace testing
def test_namespace_deduced(self):
diff --git a/test/test_component.py b/test/test_component.py
index 8486d92..e1a2dc4 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 b784dc2..bc7f599 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 3936706..e8d9607 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)
diff --git a/test/test_referenced_object.py b/test/test_referenced_object.py
index fd1e153..aba72c6 100644
--- a/test/test_referenced_object.py
+++ b/test/test_referenced_object.py
@@ -1,8 +1,11 @@
import os
import unittest
+import rdflib
+
import sbol3
+
MODULE_LOCATION = os.path.dirname(os.path.abspath(__file__))
SBOL3_LOCATION = os.path.join(MODULE_LOCATION, 'SBOLTestSuite', 'SBOL3')
@@ -20,12 +23,12 @@ 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()
- 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,75 +36,177 @@ 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()
- self.assertIsNotNone(model)
-
- def test_uri_assignment(self):
+ model = component.models[0]
+ 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)
+ self.assertListEqual(model._references, [component])
+
+ # Test reverse compatibility with lookup
+ model_lookup = model.lookup()
+ self.assertTrue(model_lookup is model)
+
+ 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 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 Model should be upcast as
+ # since it becomes a reference to an external object
+ [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
+ 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
+ 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.append(seq1.identity)
+ self.assertEqual(list(component.sequences), [seq1])
+ self.assertListEqual(seq1._references, [component])
+
+ component.sequences = [seq1.identity]
+ self.assertListEqual(list(component.sequences), [seq1])
+ self.assertListEqual(seq1._references, [component])
+
+ component.sequences = [seq1.identity, seq2.identity]
+ self.assertListEqual(list(component.sequences), [seq1, seq2])
+
+ 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)
- doc.add(sequence)
+
+ # Because the Sequence is not contained in the Document,
+ # we can't resolve the reference
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.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
# 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')
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
# 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')
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
# instance using assignment
- sbol3.set_namespace('https://github.com/synbiodex/pysbol3')
doc = sbol3.Document()
test_parent = SingleRefObj('sro1')
sequence = sbol3.Sequence('seq1')
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
@@ -109,31 +214,158 @@ def test_adding_referenced_objects(self):
# 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')
- execution = sbol3.Activity('protocol_execution')
- doc.add(execution)
foo = sbol3.Collection('https://example.org/baz')
+ doc.add(foo)
+
+ execution = sbol3.Activity('protocol_execution')
+ self.assertFalse(execution in foo._owned_objects[sbol3.SBOL_MEMBER])
foo.members.append(execution)
- # Verify that foo did not get document assigned
- self.assertIsNone(foo.document)
+ 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)
+
# 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)
+ 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
- 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\.$'
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_singleton_property_reference_counter(self):
+ 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):
+ 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, [])
+
+
+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. 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()