diff --git a/src/oaklib/cli.py b/src/oaklib/cli.py index be9aa5615..2d1798ce0 100644 --- a/src/oaklib/cli.py +++ b/src/oaklib/cli.py @@ -560,11 +560,23 @@ def chain_it(v): "--autosave/--no-autosave", help="For commands that mutate the ontology, this determines if these are automatically saved in place", ) +@click.option( + "--import-depth", + type=click.INT, + help="Maximum depth in the import tree to traverse", +) @input_option @input_type_option @add_option def main( - verbose: int, quiet: bool, input: str, input_type: str, add: List, save_as: str, autosave: bool + verbose: int, + quiet: bool, + input: str, + input_type: str, + add: List, + save_as: str, + autosave: bool, + import_depth: Optional[int], ): """Run the oaklib Command Line. @@ -593,7 +605,7 @@ def main( if input: impl_class: Type[OntologyInterface] - resource = get_resource_from_shorthand(input, format=input_type) + resource = get_resource_from_shorthand(input, format=input_type, import_depth=import_depth) impl_class = resource.implementation_class logging.info(f"RESOURCE={resource}") settings.impl = impl_class(resource) diff --git a/src/oaklib/implementations/pronto/pronto_implementation.py b/src/oaklib/implementations/pronto/pronto_implementation.py index a17e04fdd..d782932cd 100644 --- a/src/oaklib/implementations/pronto/pronto_implementation.py +++ b/src/oaklib/implementations/pronto/pronto_implementation.py @@ -108,12 +108,15 @@ def __post_init__(self): if self.wrapped_ontology is None: resource = self.resource logging.info(f"Pronto using resource: {resource}") + kwargs = {} + if resource and resource.import_depth is not None: + kwargs["import_depth"] = resource.import_depth if resource is None: ontology = Ontology() elif resource.local: - ontology = Ontology(str(resource.local_path)) + ontology = Ontology(str(resource.local_path), **kwargs) else: - ontology = Ontology.from_obo_library(resource.slug) + ontology = Ontology.from_obo_library(resource.slug, **kwargs) self.wrapped_ontology = ontology @classmethod diff --git a/src/oaklib/interfaces/search_interface.py b/src/oaklib/interfaces/search_interface.py index 0ca9ca178..e01b39dbb 100644 --- a/src/oaklib/interfaces/search_interface.py +++ b/src/oaklib/interfaces/search_interface.py @@ -5,9 +5,7 @@ from oaklib.interfaces.basic_ontology_interface import BasicOntologyInterface from oaklib.types import CURIE -__all__ = [ - "SearchConfiguration", -] +__all__ = ["SearchConfiguration", "SearchInterface"] class SearchInterface(BasicOntologyInterface, ABC): diff --git a/src/oaklib/resource.py b/src/oaklib/resource.py index 8488ac5f5..026b5eede 100644 --- a/src/oaklib/resource.py +++ b/src/oaklib/resource.py @@ -1,6 +1,6 @@ from dataclasses import dataclass from pathlib import Path -from typing import TYPE_CHECKING, Type, Union +from typing import TYPE_CHECKING, Optional, Type, Union from class_resolver import HintOrType @@ -21,16 +21,36 @@ class OntologyResource: """ slug: str = None - directory: str = None - scheme: str = None - format: str = None - url: str = None - readonly: bool = False - provider: str = None - local: bool = False - in_memory: bool = False - data: str = None - implementation_class: Union[Type] = None + """Name or path of ontology resource""" + + directory: Optional[str] = None + """For local resources, the directory where the serialization is found""" + + scheme: Optional[str] = None + """The scheme of the resource, e.g pronto, sqlite""" + + format: Optional[str] = None + """For serialized resources, the serialization format""" + + url: Optional[str] = None + """For remote resources, the URL from which it can be obtained""" + + readonly: Optional[bool] = False + """Typically true for remote resources""" + + provider: Optional[str] = None + + local: Optional[bool] = True + """Is the resource locally on disk or remote?""" + + in_memory: Optional[bool] = False + + data: Optional[str] = None + + implementation_class: Optional[Union[Type]] = None + + import_depth: Optional[int] = None + """If set, this determines the maximum depth in the import tree to follow""" @property def local_path(self) -> Path: diff --git a/src/oaklib/selector.py b/src/oaklib/selector.py index 471483ebd..490b1dc6d 100644 --- a/src/oaklib/selector.py +++ b/src/oaklib/selector.py @@ -122,12 +122,15 @@ def get_resource_imp_class_from_suffix_descriptor( return impl_class, resource -def get_resource_from_shorthand(descriptor: str, format: str = None) -> OntologyResource: +def get_resource_from_shorthand( + descriptor: str, format: str = None, import_depth: Optional[int] = None +) -> OntologyResource: """ Maps from a shorthand descriptor to an OntologyResource. :param descriptor: - :param format: + :param format: file format/syntax, e.g obo, turtle + :param import_depth: maximum import depth to traverse :return: """ from oaklib.implementations import ( @@ -137,6 +140,7 @@ def get_resource_from_shorthand(descriptor: str, format: str = None) -> Ontology ) resource = OntologyResource(format=format) + resource.import_depth = import_depth resource.slug = descriptor impl_class: Optional[Type[OntologyInterface]] = None if descriptor: diff --git a/tests/input/catalog-v001.xml b/tests/input/catalog-v001.xml new file mode 100644 index 000000000..28309b74c --- /dev/null +++ b/tests/input/catalog-v001.xml @@ -0,0 +1,6 @@ + + + + + + diff --git a/tests/input/test_import_root.obo b/tests/input/test_import_root.obo new file mode 100644 index 000000000..44ab15c7e --- /dev/null +++ b/tests/input/test_import_root.obo @@ -0,0 +1,7 @@ +format-version: 1.2 +ontology: test_import_root +import: http://purl.obolibrary.org/obo/test_imported_ontology.owl + +[Term] +id: PATO:0000001 +name: term in root diff --git a/tests/input/test_import_root.owl b/tests/input/test_import_root.owl new file mode 100644 index 000000000..4e23d45a8 --- /dev/null +++ b/tests/input/test_import_root.owl @@ -0,0 +1,61 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + PATO:1 + term in root + + + + + + + diff --git a/tests/input/test_imported_ontology.obo b/tests/input/test_imported_ontology.obo new file mode 100644 index 000000000..ded0c3252 --- /dev/null +++ b/tests/input/test_imported_ontology.obo @@ -0,0 +1,6 @@ +format-version: 1.2 +ontology: test_imported_ontology + +[Term] +id: PATO:2 +name: term in imported diff --git a/tests/input/test_imported_ontology.owl b/tests/input/test_imported_ontology.owl new file mode 100644 index 000000000..cac061856 --- /dev/null +++ b/tests/input/test_imported_ontology.owl @@ -0,0 +1,69 @@ + + + + 1.2 + + + + + + + + + + + + + has_obo_format_version + + + + + + + + + + + + + + + + + + + + + + + + + PATO:2 + term in imported + + + + + + + diff --git a/tests/test_implementations/test_pronto.py b/tests/test_implementations/test_pronto.py index 0f7f590fa..7a39d30ba 100644 --- a/tests/test_implementations/test_pronto.py +++ b/tests/test_implementations/test_pronto.py @@ -1,6 +1,7 @@ import logging import unittest +import pronto from kgcl_schema.datamodel import kgcl from oaklib.datamodels import obograph @@ -103,7 +104,6 @@ def test_metadata(self): def test_labels(self): """ Tests labels can be retrieved, and no label is retrieved when a term does not exist - :return: """ oi = self.oi label = oi.label("GO:0005773") @@ -143,8 +143,6 @@ def test_synonyms(self): def test_mappings(self): oi = self.oi mappings = list(oi.get_sssom_mappings_by_curie(NUCLEUS)) - # for m in mappings: - # logging.info(yaml_dumper.dumps(m)) assert any(m for m in mappings if m.object_id == "Wikipedia:Cell_nucleus") self.assertEqual(len(mappings), 2) for m in mappings: @@ -177,6 +175,38 @@ def test_from_obo_library(self): curies = oi.curies_by_label("shape") self.assertEqual(["PATO:0000052"], curies) + @unittest.skip("https://github.com/althonos/pronto/issues/186") + def test_import_behavior(self): + """ + Tests behavior of owl:imports + + by default, imports should be followed + + See: https://github.com/INCATools/ontology-access-kit/issues/248 + """ + for slug in ["test_import_root.obo", "test_import_root.obo"]: + resource = OntologyResource(slug=slug, directory=INPUT_DIR, local=True) + # print(resource.local_path) + # currently throws exception + pronto.Ontology(resource.local_path) + oi = ProntoImplementation.create(resource) + terms = list(oi.entities(owl_type="owl:Class")) + self.assertEqual(2, len(terms)) + + def test_no_import_depth(self): + """ + Tests behavior of owl:imports + + do not follow imports if depth is set to zero + + See: https://github.com/INCATools/ontology-access-kit/issues/248 + """ + for slug in ["test_import_root.obo", "test_import_root.obo"]: + resource = OntologyResource(slug=slug, directory=INPUT_DIR, local=True, import_depth=0) + oi = ProntoImplementation(resource) + terms = list(oi.entities(owl_type="owl:Class")) + self.assertEqual(1, len(terms)) + @unittest.skip("Hide warnings") def test_from_owl(self): r = OntologyResource(local=True, slug="go-nucleus.owl", directory=INPUT_DIR) diff --git a/tests/test_implementations/test_sparql.py b/tests/test_implementations/test_sparql.py index 2dd85379b..916220845 100644 --- a/tests/test_implementations/test_sparql.py +++ b/tests/test_implementations/test_sparql.py @@ -34,6 +34,7 @@ TEST_RDF = INPUT_DIR / "go-nucleus.owl.ttl" TEST_INST_RDF = INPUT_DIR / "inst.owl.ttl" TEST_MUTABLE_RDF = OUTPUT_DIR / "go-nucleus.owl.ttl" +TEST_IMPORTER = INPUT_DIR / "test_import_root.owl" class TestSparqlImplementation(unittest.TestCase): @@ -249,3 +250,17 @@ def test_mutable(self): ) self.assertCountEqual([], list(oi.descendants(NUCLEUS, predicates=preds, reflexive=False))) oi.save() + + @unittest.skip("not yet implemented") + def test_import_behavior(self): + """ + Tests behavior of owl:imports + + by default, imports should be followed + + See: https://github.com/INCATools/ontology-access-kit/issues/248 + """ + resource = OntologyResource(slug=str(TEST_IMPORTER)) + oi = SparqlImplementation(resource) + terms = list(oi.entities(owl_type="owl:Class")) + self.assertCountEqual(["PATO:1", "PATO:2"], terms)