diff --git a/sbol3/toplevel.py b/sbol3/toplevel.py index 7d5b70a..848a674 100644 --- a/sbol3/toplevel.py +++ b/sbol3/toplevel.py @@ -1,6 +1,7 @@ import copy import math import posixpath +import uuid from typing import List, Dict, Callable, Union from urllib.parse import urlparse @@ -49,25 +50,25 @@ def default_namespace(namespace: Union[None, str], identity: str) -> str: # short circuit if a namespace is set if namespace is not None: return namespace - # Try using the default namespace if set - namespace = get_namespace() - if namespace is None: - # No default namespace was defined - # Try parsing the identity as a URL - parsed = urlparse(identity) - if parsed.scheme and parsed.netloc and parsed.path: - # This is a URL - # Reverse index to drop the displayId and use the rest - # for the namespace - delim = posixpath.sep - if '#' in identity: - delim = '#' - namespace = identity[:identity.rindex(delim)] - if namespace is None: - # TODO: what should we do here? We haven't been able to determine - # a namespace. But in the case where we're loading a file - # that's probably ok. We don't want that loading to fail. + default_namespace = get_namespace() + # If identity is a uuid, don't bother with namespaces + try: + # If it is a UUID, accept it as the identity + uuid.UUID(identity) + return default_namespace or PYSBOL3_DEFAULT_NAMESPACE + except ValueError: pass + # If default namespace is a prefix of identity, use it for the namespace + if default_namespace and identity.startswith(default_namespace): + return default_namespace + # Identity does not start with the default namespace then + # heuristically determine the namespace. We use a greedy + # algorithm by assuming there is no local path, and that + # everything other than the display_id is the namespace. + delim = posixpath.sep + if '#' in identity: + delim = '#' + namespace = identity[:identity.rindex(delim)] return namespace def validate_identity(self, report: ValidationReport) -> None: diff --git a/setup.py b/setup.py index 8974a6b..d3a4d75 100644 --- a/setup.py +++ b/setup.py @@ -38,11 +38,11 @@ install_requires=[ # Require at least rdflib 6.0.1, and allow newer versions # of rdflib 6.x - 'rdflib>=6.0.1,==6.*', - 'python-dateutil~=2.8', - 'pyshacl~=0.17.0', + 'rdflib>=6.0.2,==6.*', + 'python-dateutil~=2.8.2', + 'pyshacl~=0.17.1', ], test_suite='test', tests_require=[ - 'pycodestyle~=2.7.0' + 'pycodestyle~=2.8.0' ]) diff --git a/test/test_collection.py b/test/test_collection.py index bcbc4d6..dcdd65d 100644 --- a/test/test_collection.py +++ b/test/test_collection.py @@ -67,11 +67,12 @@ def test_namespace_none(self): # Test the exception case when a namespace cannot be deduced identity = uuid.uuid4().urn collection = sbol3.Collection(identity) - # The namespace should be None in this case. - # We can't determine a namespace from a UUID and we don't - # want to error and break file loading if we can't - # determine a namespace. - self.assertIsNone(collection.namespace) + # The namespace should always be set per SBOL 3.0.1 + # "A TopLevel object MUST have precisely one hasNamespace property" + self.assertIsNotNone(collection.namespace) + # There should be no validation errors on this object + report = collection.validate() + self.assertEqual(0, len(report)) class TestExperiment(unittest.TestCase): diff --git a/test/test_document.py b/test/test_document.py index c80b80f..dda4920 100644 --- a/test/test_document.py +++ b/test/test_document.py @@ -404,6 +404,32 @@ def test_json_ld_parser_bug(self): self.assertEqual(m_turtle.source, m_json_ld.source) self.assertEqual(m_turtle.namespace, m_json_ld.namespace) + def test_read_with_default_namespace(self): + # Test reading a file when the default namespace is set + # See https://github.com/SynBioDex/pySBOL3/issues/337 + sbol3.set_namespace('http://example.com') + test_path = os.path.join(SBOL3_LOCATION, 'toggle_switch', + 'toggle_switch.ttl') + doc = sbol3.Document() + doc.read(test_path) + + def test_read_default_namespace(self): + # This is a modified version of the initial bug report for + # https://github.com/SynBioDex/pySBOL3/issues/337 + doc = sbol3.Document() + sbol3.set_namespace('http://foo.org') + doc.add(sbol3.Sequence('bar')) + self.assertEqual(0, len(doc.validate())) + file_format = sbol3.SORTED_NTRIPLES + data = doc.write_string(file_format=file_format) + + doc2 = sbol3.Document() + doc2.read_string(data, file_format=file_format) # Successful read + + sbol3.set_namespace('http://baz.com/') + doc3 = sbol3.Document() + doc3.read_string(data, file_format=file_format) + if __name__ == '__main__': unittest.main() diff --git a/test/test_toplevel.py b/test/test_toplevel.py index 37097fc..1dab739 100644 --- a/test/test_toplevel.py +++ b/test/test_toplevel.py @@ -100,6 +100,29 @@ def test_namespace_mismatch(self): # Expecting at least one error self.assertEqual(len(report), 0) + def test_namespace_mismatch_uuid(self): + # Now check a UUID with no default namespace set + # sbol3.set_namespace(None) + self.assertIsNone(sbol3.get_namespace()) + c = sbol3.Component(uuid.uuid4().urn, types=[sbol3.SBO_DNA]) + report = c.validate() + self.assertIsNotNone(report) + # Expecting at least one error + self.assertEqual(0, len(report)) + + def test_default_namespace_with_local_path(self): + # Make sure default namespace is honored when the identity has + # a local path included + test_namespace = 'https://github.com/synbiodex' + sbol3.set_namespace(test_namespace) + self.assertEqual(test_namespace, sbol3.get_namespace()) + identity = posixpath.join(sbol3.get_namespace(), 'pysbol3', 'foo') + c = sbol3.Component(identity, types=[sbol3.SBO_DNA]) + self.assertEqual(sbol3.get_namespace(), c.namespace) + self.assertEqual('foo', c.display_id) + self.assertEqual(identity, c.identity) + self.assertEqual(0, len(c.validate())) + def test_creation_namespace_mismatch(self): # Prevent an identity/namespace mismatch on object creation # See https://github.com/SynBioDex/pySBOL3/issues/277