diff --git a/src/oaklib/implementations/funowl/funowl_implementation.py b/src/oaklib/implementations/funowl/funowl_implementation.py index 4072db4f7..499c4b9db 100644 --- a/src/oaklib/implementations/funowl/funowl_implementation.py +++ b/src/oaklib/implementations/funowl/funowl_implementation.py @@ -38,6 +38,7 @@ ) from oaklib.datamodels import obograph +from oaklib.datamodels.search import SearchConfiguration, SearchProperty, SearchTermSyntax from oaklib.datamodels.vocabulary import ( DEPRECATED_PREDICATE, EQUIVALENT_CLASS, @@ -413,6 +414,57 @@ def entity_alias_map(self, curie: CURIE) -> Dict[PRED_CURIE, List[str]]: alias_map[predicate].extend(metadata.get(predicate, [])) return dict(alias_map) + def basic_search( + self, search_term: str, config: Optional[SearchConfiguration] = None + ) -> Iterable[CURIE]: + if config is None: + config = SearchConfiguration() + properties = list(config.properties or []) + if config.syntax == SearchTermSyntax(SearchTermSyntax.STARTS_WITH): + matcher = lambda text: str(text).startswith(search_term) + elif config.syntax == SearchTermSyntax(SearchTermSyntax.REGULAR_EXPRESSION): + prog = re.compile(search_term) + matcher = lambda text: prog.search(str(text)) + elif config.is_partial: + matcher = lambda text: search_term in str(text) + else: + matcher = lambda text: str(text) == search_term + search_all = SearchProperty(SearchProperty.ANYTHING) in properties + search_label = search_all or not properties or SearchProperty(SearchProperty.LABEL) in properties + search_identifier = search_all or SearchProperty(SearchProperty.IDENTIFIER) in properties + search_alias = search_all or SearchProperty(SearchProperty.ALIAS) in properties + search_mapped = search_all or SearchProperty(SearchProperty.MAPPED_IDENTIFIER) in properties + seen: Set[CURIE] = set() + for curie in self.entities(filter_obsoletes=False): + if search_label: + label = self.label(curie) + if label and matcher(label): + if curie not in seen: + seen.add(curie) + yield curie + continue + if search_identifier and matcher(curie): + if curie not in seen: + seen.add(curie) + yield curie + continue + if search_alias: + for alias in self.entity_aliases(curie): + if alias and matcher(alias): + if curie not in seen: + seen.add(curie) + yield curie + break + if curie in seen: + continue + if search_mapped: + for xref in self.entity_metadata_map(curie).get(HAS_DBXREF, []): + if matcher(xref): + if curie not in seen: + seen.add(curie) + yield curie + break + def terms_subsets(self, curies: Iterable[CURIE]) -> Iterable[tuple[CURIE, CURIE]]: for curie in curies: for subset in self.entity_metadata_map(curie).get(IN_SUBSET, []): @@ -434,10 +486,11 @@ def node( ) -> Optional[obograph.Node]: entity_types = set(self.owl_type(curie)) label = self.label(curie) + node_id = cast(CURIE, self.curie_to_uri(curie)) if expand_curies else curie if not entity_types and label is None: if strict: raise ValueError(f"Unknown entity: {curie}") - return None + return obograph.Node(id=node_id, type="CLASS") if any( owl_type in entity_types for owl_type in [OWL_OBJECT_PROPERTY, OWL_ANNOTATION_PROPERTY, OWL_DATATYPE_PROPERTY] @@ -447,7 +500,6 @@ def node( node_type = "INDIVIDUAL" else: node_type = "CLASS" - node_id = cast(CURIE, self.curie_to_uri(curie)) if expand_curies else curie meta = None if include_metadata: meta = obograph.Meta() diff --git a/tests/test_cli.py b/tests/test_cli.py index 0f5593b3c..0fba5567e 100644 --- a/tests/test_cli.py +++ b/tests/test_cli.py @@ -66,6 +66,18 @@ TEST_SYNONYMIZER_OBO = "simpleobo:" + str(INPUT_DIR / "synonym-test.obo") RULES_FILE = INPUT_DIR / "matcher_rules.yaml" SYNONYMIZER_RULES_FILE = INPUT_DIR / "cli-synonymizer-rules.yaml" +EXTERNAL_REFERENCE_OFN = """\ +Prefix(rdfs:=) +Prefix(CL:=) +Prefix(BFO:=) +Prefix(GO:=) +Ontology( +Declaration(Class(CL:0000540)) +AnnotationAssertion(rdfs:label CL:0000540 "neuron") +SubClassOf(CL:0000540 GO:0008150) +SubClassOf(CL:0000540 ObjectSomeValuesFrom(BFO:0000050 GO:0008150)) +) +""" def _outpath(test: str, fmt: str = "tmp") -> str: @@ -115,6 +127,35 @@ def test_input_type_and_sniff_for_functional_owl_suffix(self): self.assertEqual(0, result.exit_code, result.output) self.assertIn("nucleus", result.stdout) + def test_funowl_info_search_and_graph_outputs(self): + with tempfile.TemporaryDirectory() as tmpdir: + ontology_path = Path(tmpdir) / "external-ref.ofn" + ontology_path.write_text(EXTERNAL_REFERENCE_OFN) + viz_out = Path(tmpdir) / "viz.json" + for args, expected in ( + (["-I", "ofn", "-i", str(ontology_path), "info", "neuron"], "CL:0000540 ! neuron"), + ( + ["-I", "ofn", "-i", str(ontology_path), "info", "l~neur"], + "CL:0000540 ! neuron", + ), + ( + ["-I", "ofn", "-i", str(ontology_path), "info", "CL:0000540", "-O", "obo"], + "id: CL:0000540", + ), + ): + result = self.runner.invoke(main, args) + self.assertEqual(0, result.exit_code, result.output) + self.assertIn(expected, result.stdout) + result = self.runner.invoke( + main, + ["-I", "ofn", "-i", str(ontology_path), "viz", "CL:0000540", "-O", "json", "-o", str(viz_out)], + ) + self.assertEqual(0, result.exit_code, result.output) + obj = json.load(open(viz_out)) + node_ids = {node["id"] for node in obj["nodes"]} + self.assertIn("CL:0000540", node_ids) + self.assertIn("GO:0008150", node_ids) + def test_multilingual(self): for input_arg in [INPUT_DIR / "hp-international-test.db"]: results = self.runner.invoke(main, ["-i", str(input_arg), "languages"]) diff --git a/tests/test_implementations/test_funowl.py b/tests/test_implementations/test_funowl.py index acfdf764f..4b4b6912b 100644 --- a/tests/test_implementations/test_funowl.py +++ b/tests/test_implementations/test_funowl.py @@ -1,9 +1,12 @@ import logging +import tempfile import unittest +from pathlib import Path from kgcl_schema.datamodel import kgcl from pyhornedowl.model import EquivalentClasses, SubClassOf +from oaklib.datamodels.search import SearchConfiguration from oaklib.implementations.funowl.funowl_implementation import FunOwlImplementation from oaklib.interfaces.obograph_interface import OboGraphInterface from oaklib.interfaces.owl_interface import AxiomFilter @@ -16,6 +19,18 @@ TEST_GRAPH_PROJECTION_ONT = INPUT_DIR / "graph_projection.owl" TEST_INST_ONT = INPUT_DIR / "inst.ofn" NEW_NAME = "new name" +EXTERNAL_REFERENCE_OFN = """\ +Prefix(rdfs:=) +Prefix(CL:=) +Prefix(BFO:=) +Prefix(GO:=) +Ontology( +Declaration(Class(CL:0000540)) +AnnotationAssertion(rdfs:label CL:0000540 "neuron") +SubClassOf(CL:0000540 GO:0008150) +SubClassOf(CL:0000540 ObjectSomeValuesFrom(BFO:0000050 GO:0008150)) +) +""" class TestFunOwlImplementation(unittest.TestCase): @@ -92,6 +107,21 @@ def test_logical_definitions(self): def test_ancestors_descendants(self): self.compliance_tester.test_ancestors_descendants(self.oi) + def test_basic_search(self): + self.assertIn(NUCLEUS, list(self.oi.basic_search("nucleus"))) + cfg = SearchConfiguration(is_partial=True) + self.assertIn(NUCLEUS, list(self.oi.basic_search("nucl", config=cfg))) + + def test_stub_nodes_for_unresolved_external_references(self): + with tempfile.TemporaryDirectory() as tmpdir: + path = Path(tmpdir) / "external-ref.ofn" + path.write_text(EXTERNAL_REFERENCE_OFN) + oi = FunOwlImplementation(OntologyResource(str(path))) + graph = oi.direct_graph("CL:0000540") + node_ids = {node.id for node in graph.nodes} + self.assertIn("CL:0000540", node_ids) + self.assertIn("GO:0008150", node_ids) + def test_patcher(self): oi = self.oi anns = list(oi.annotation_assertion_axioms(NUCLEUS))