From b7344fd7579e53569e1058caf3680f9b0e6683f2 Mon Sep 17 00:00:00 2001 From: srmnitc Date: Fri, 21 Nov 2025 10:25:05 +0100 Subject: [PATCH] update tests --- tests/test_network.py | 468 +++++++++++++++++++++++++++++- tests/test_parser.py | 660 ++++++++++++++++++++++++++++++++++++++++++ tests/test_term.py | 384 ++++++++++++++++++++++++ 3 files changed, 1508 insertions(+), 4 deletions(-) create mode 100644 tests/test_parser.py create mode 100644 tests/test_term.py diff --git a/tests/test_network.py b/tests/test_network.py index 7c39c35..aed3244 100644 --- a/tests/test_network.py +++ b/tests/test_network.py @@ -1,15 +1,475 @@ import pytest from tools4rdf.network.ontology import read_ontology -from rdflib import Graph +from tools4rdf.network.network import ( + Network, + OntologyNetwork, + OntologyNetworkBase, + _replace_name, + _strip_name, +) +from tools4rdf.network.parser import OntoParser +from tools4rdf.network.term import OntoTerm +from rdflib import Graph, Namespace, RDF, RDFS, OWL, URIRef + def test_network(): onto = read_ontology() kg = Graph() kg.parse("tests/triples", format="turtle") - df = onto.query(kg, onto.terms.cmso.AtomicScaleSample, [onto.terms.cmso.hasSpaceGroupSymbol, onto.terms.cmso.hasNumberOfAtoms==4]) + df = onto.query( + kg, + onto.terms.cmso.AtomicScaleSample, + [onto.terms.cmso.hasSpaceGroupSymbol, onto.terms.cmso.hasNumberOfAtoms == 4], + ) assert len(df) == 14 + def test_owlThing(): onto = read_ontology() - query = (onto.create_query(onto.terms.cmso.AtomicScaleSample, [[onto.terms.cmso.CrystalStructure, onto.terms.cmso.hasAltName]])) - assert "CrystalStructure_hasAltNamevalue" in query \ No newline at end of file + query = onto.create_query( + onto.terms.cmso.AtomicScaleSample, + [[onto.terms.cmso.CrystalStructure, onto.terms.cmso.hasAltName]], + ) + assert "CrystalStructure_hasAltNamevalue" in query + + +@pytest.fixture +def simple_onto(): + g = Graph() + ex = Namespace("http://example.org/ontology#") + g.bind("ex", ex) + g.add((ex.Pizza, RDF.type, OWL.Class)) + g.add((ex.Food, RDF.type, OWL.Class)) + g.add((ex.Person, RDF.type, OWL.Class)) + g.add((ex.Pizza, RDFS.subClassOf, ex.Food)) + g.add((ex.hasTopping, RDF.type, OWL.ObjectProperty)) + g.add((ex.hasTopping, RDFS.domain, ex.Pizza)) + g.add((ex.hasTopping, RDFS.range, ex.Food)) + g.add((ex.hasPrice, RDF.type, OWL.DatatypeProperty)) + g.add((ex.hasPrice, RDFS.domain, ex.Pizza)) + return OntoParser(g) + + +@pytest.fixture +def network_fixture(simple_onto): + return Network(simple_onto) + + +def test_replace_name(): + assert _replace_name("ex:Pizza") == "ex.Pizza" + assert _replace_name("simple") == "simple" + + +def test_strip_name(): + assert _strip_name("ex:Pizza") == "Pizza" + assert _strip_name("Pizza") == "Pizza" + + +def test_network_init(simple_onto): + network = Network(simple_onto) + assert network.terms is not None + assert network.g is not None + assert network.namespaces is not None + assert network.extra_namespaces is not None + + +def test_network_draw(network_fixture): + dot = network_fixture.draw() + assert dot is not None + + +def test_network_draw_custom_style(network_fixture): + style = { + "class": {"shape": "circle"}, + "object_property": {"shape": "diamond"}, + "data_property": {"shape": "box"}, + "literal": {"shape": "parallelogram"}, + } + dot = network_fixture.draw(styledict=style) + assert dot is not None + + +def test_get_shortest_path_simple(network_fixture): + source = network_fixture.terms.ex.Pizza + target = network_fixture.terms.ex.hasTopping + paths = network_fixture._get_shortest_path(source, target, num_paths=1) + assert len(paths) > 0 + assert paths[0][0] == source.variable_name + + +def test_get_shortest_path_multiple_paths(network_fixture): + source = network_fixture.terms.ex.Pizza + target = network_fixture.terms.ex.hasTopping + paths = network_fixture._get_shortest_path(source, target, num_paths=2) + assert len(paths) <= 2 + + +def test_get_shortest_path_with_triples(network_fixture): + source = network_fixture.terms.ex.Pizza + target = network_fixture.terms.ex.hasTopping + result = network_fixture.get_shortest_path( + source, target, triples=True, num_paths=1 + ) + assert isinstance(result, list) + assert isinstance(result[0], list) + + +def test_get_shortest_path_without_triples(network_fixture): + source = network_fixture.terms.ex.Pizza + target = network_fixture.terms.ex.hasTopping + result = network_fixture.get_shortest_path( + source, target, triples=False, num_paths=1 + ) + assert isinstance(result, list) + + +def test_get_shortest_path_stepped_query(network_fixture): + source = network_fixture.terms.ex.Pizza + target = network_fixture.terms.ex.Food + parent = network_fixture.terms.ex.hasTopping + target._parents.append(parent) + result = network_fixture.get_shortest_path(source, target, triples=False) + assert isinstance(result, list) + target._parents = [] + + +def test_get_shortest_path_stepped_multiple_paths_error(network_fixture): + source = network_fixture.terms.ex.Pizza + target = network_fixture.terms.ex.hasTopping + parent = network_fixture.terms.ex.Food + target._parents.append(parent) + with pytest.raises(ValueError): + network_fixture.get_shortest_path(source, target, num_paths=2) + target._parents = [] + + +def test_insert_namespaces(network_fixture): + namespaces = {"ex": "http://example.org/ontology#"} + result = network_fixture._insert_namespaces(namespaces) + assert len(result) > 0 + assert "PREFIX ex:" in result[0] + + +def test_modify_destinations(network_fixture): + dest1 = network_fixture.terms.ex.Pizza + dest2 = network_fixture.terms.ex.Food + destinations = [[dest1, dest2]] + modified = network_fixture._modify_destinations(destinations) + assert len(modified) == 1 + assert dest1 in modified[0]._parents + + +def test_is_already_in_destinations(network_fixture): + obj_prop = network_fixture.terms.ex.hasTopping + destinations = [obj_prop] + assert network_fixture._is_already_in_destinations(obj_prop, destinations) + + +def test_is_not_in_destinations(network_fixture): + obj_prop = network_fixture.terms.ex.hasTopping + other_prop = network_fixture.terms.ex.hasPrice + destinations = [obj_prop] + assert not network_fixture._is_already_in_destinations(other_prop, destinations) + + +def test_create_query_single_source(network_fixture): + source = network_fixture.terms.ex.Pizza + query = network_fixture.create_query(source) + assert isinstance(query, str) + assert "SELECT DISTINCT" in query + + +def test_create_query_with_destination(network_fixture): + source = network_fixture.terms.ex.Pizza + dest = network_fixture.terms.ex.hasTopping + query = network_fixture.create_query(source, dest) + assert isinstance(query, str) + assert "SELECT DISTINCT" in query + + +def test_create_query_multiple_destinations(network_fixture): + source = network_fixture.terms.ex.Pizza + dest1 = network_fixture.terms.ex.hasTopping + dest2 = network_fixture.terms.ex.hasPrice + query = network_fixture.create_query(source, [dest1, dest2], num_paths=1) + assert isinstance(query, str) + + +def test_create_query_return_list(network_fixture): + source = network_fixture.terms.ex.Pizza + query = network_fixture.create_query(source, return_list=True) + assert isinstance(query, list) + + +def test_create_query_data_property_source_error(network_fixture): + source = network_fixture.terms.ex.hasPrice + with pytest.raises(ValueError): + network_fixture.create_query(source) + + +def test_create_query_multiple_destinations_multiple_paths_error(network_fixture): + source = network_fixture.terms.ex.Pizza + dest1 = network_fixture.terms.ex.hasTopping + dest2 = network_fixture.terms.ex.hasPrice + with pytest.raises(TypeError): + network_fixture.create_query(source, [dest1, dest2], num_paths=2) + + +def test_prepare_destinations(network_fixture): + source = network_fixture.terms.ex.Pizza + dest = network_fixture.terms.ex.hasTopping + destinations = [dest] + result = network_fixture._prepare_destinations(destinations, source) + assert len(result) > 0 + + +def test_prepare_destinations_source_any_error(network_fixture): + source = network_fixture.terms.ex.Pizza.any + with pytest.raises(ValueError): + network_fixture._prepare_destinations(None, source) + + +def test_prepare_destinations_multiple_conditions_error(network_fixture): + source = network_fixture.terms.ex.Pizza + dest1 = network_fixture.terms.ex.Pizza + dest2 = network_fixture.terms.ex.Pizza + dest1._condition = "condition1" + dest2._condition = "condition2" + with pytest.raises(ValueError): + network_fixture._prepare_destinations([dest1, dest2], source) + dest1._condition = None + dest2._condition = None + + +def test_create_query_prefix(network_fixture): + source = network_fixture.terms.ex.Pizza + dest = network_fixture.terms.ex.hasTopping + result = network_fixture._create_query_prefix(source, [dest]) + assert "SELECT DISTINCT" in result[0] + assert "WHERE" in result[1] + + +def test_create_query_prefix_with_remote_source(network_fixture): + source = network_fixture.terms.ex.Pizza + dest = network_fixture.terms.ex.hasTopping + result = network_fixture._create_query_prefix( + source, [dest], remote_source="https://example.org/sparql" + ) + assert "SERVICE" in result[2] + + +def test_create_query_prefix_invalid_remote_url(network_fixture): + source = network_fixture.terms.ex.Pizza + dest = network_fixture.terms.ex.hasTopping + with pytest.raises(ValueError): + network_fixture._create_query_prefix(source, [dest], remote_source="not-a-url") + + +def test_get_triples(network_fixture): + source = network_fixture.terms.ex.Pizza + dest = network_fixture.terms.ex.hasTopping + queries, namespaces = network_fixture._get_triples(source, [dest], num_paths=1) + assert len(queries) > 0 + assert len(namespaces) > 0 + + +def test_add_types_for_source(network_fixture): + source = network_fixture.terms.ex.Pizza + query, namespaces = network_fixture._add_types_for_source(source) + assert isinstance(query, list) + assert isinstance(namespaces, list) + + +def test_add_types_for_destination(network_fixture): + dest = network_fixture.terms.ex.Pizza.only + query, namespaces = network_fixture._add_types_for_destination([dest]) + assert isinstance(query, list) + assert isinstance(namespaces, list) + + +def test_add_filters_no_condition(network_fixture): + dest = network_fixture.terms.ex.hasTopping + result = network_fixture._add_filters([dest]) + assert "}" in result + + +def test_add_filters_with_condition(network_fixture): + dest = network_fixture.terms.ex.Pizza + dest._condition = "(?Pizza > 10)" + result = network_fixture._add_filters([dest]) + assert "FILTER" in result[0] + dest._condition = None + + +def test_add_limit_none(network_fixture): + result = network_fixture._add_limit(None) + assert result == [] + + +def test_add_limit_with_value(network_fixture): + result = network_fixture._add_limit(10) + assert "LIMIT 10" in result[0] + + +def test_create_query_full(network_fixture): + source = network_fixture.terms.ex.Pizza + dest = network_fixture.terms.ex.hasTopping + queries = network_fixture._create_query(source, [dest], num_paths=1) + assert len(queries) > 0 + assert "SELECT DISTINCT" in queries[0] + + +def test_create_query_with_limit(network_fixture): + source = network_fixture.terms.ex.Pizza + dest = network_fixture.terms.ex.hasTopping + queries = network_fixture._create_query(source, [dest], limit=10) + assert "LIMIT 10" in queries[0] + + +def test_query_execution(network_fixture): + kg = Graph() + ex = Namespace("http://example.org/ontology#") + kg.bind("ex", ex) + kg.add((ex.MyPizza, RDF.type, ex.Pizza)) + kg.add((ex.MyPizza, ex.hasTopping, ex.Cheese)) + + source = network_fixture.terms.ex.Pizza + dest = network_fixture.terms.ex.hasTopping + result = network_fixture.query(kg, source, [dest]) + assert result is not None + + +def test_query_execution_no_results(network_fixture): + kg = Graph() + source = network_fixture.terms.ex.Pizza + dest = network_fixture.terms.ex.hasTopping + result = network_fixture.query(kg, source, [dest]) + assert result is not None + assert len(result) == 0 + + +def test_query_execution_with_limit(network_fixture): + kg = Graph() + ex = Namespace("http://example.org/ontology#") + kg.bind("ex", ex) + kg.add((ex.MyPizza, RDF.type, ex.Pizza)) + kg.add((ex.MyPizza, ex.hasTopping, ex.Cheese)) + + source = network_fixture.terms.ex.Pizza + dest = network_fixture.terms.ex.hasTopping + result = network_fixture.query(kg, source, [dest], limit=1) + assert result is not None + + +def test_query_with_remote_endpoint(network_fixture): + source = network_fixture.terms.ex.Pizza + dest = network_fixture.terms.ex.hasTopping + with pytest.raises(Exception): + network_fixture.query("https://example.org/sparql", source, [dest]) + + +def test_query_internal(network_fixture): + kg = Graph() + query_string = "SELECT DISTINCT ?s WHERE { ?s ?p ?o }" + result = network_fixture._query(kg, query_string, return_df=True) + assert result is not None + assert len(result) == 0 + + +def test_ontology_network_base_init(simple_onto): + network = OntologyNetworkBase(simple_onto) + assert network.onto == simple_onto + assert network._terms is None + assert network._g is None + + +def test_ontology_network_base_terms_lazy(simple_onto): + network = OntologyNetworkBase(simple_onto) + assert network._terms is None + terms = network.terms + assert network._terms is not None + + +def test_ontology_network_base_g_lazy(simple_onto): + network = OntologyNetworkBase(simple_onto) + assert network._g is None + g = network.g + assert network._g is not None + + +def test_ontology_network_base_add(simple_onto): + network1 = OntologyNetworkBase(simple_onto) + network2 = OntologyNetworkBase(simple_onto) + combined = network1 + network2 + assert isinstance(combined, OntologyNetworkBase) + + +def test_ontology_network_base_radd(simple_onto): + network1 = OntologyNetworkBase(simple_onto) + network2 = OntologyNetworkBase(simple_onto) + combined = network2.__radd__(network1) + assert isinstance(combined, OntologyNetworkBase) + + +def test_ontology_network_base_attributes(simple_onto): + network = OntologyNetworkBase(simple_onto) + attrs = network.attributes + assert attrs is not None + + +def test_ontology_network_base_namespaces(simple_onto): + network = OntologyNetworkBase(simple_onto) + ns = network.namespaces + assert ns is not None + + +def test_ontology_network_base_extra_namespaces(simple_onto): + network = OntologyNetworkBase(simple_onto) + extra_ns = network.extra_namespaces + assert extra_ns is not None + + +def test_ontology_network_base_add_namespace(simple_onto): + network = OntologyNetworkBase(simple_onto) + network.add_namespace("test", "http://test.org/") + assert network._terms is None + + +def test_ontology_network_base_add_term(simple_onto): + network = OntologyNetworkBase(simple_onto) + network.add_term("http://example.org/ontology#NewClass", "class", namespace="ex") + assert network._terms is None + assert network._g is None + + +def test_ontology_network_base_add_path(simple_onto): + network = OntologyNetworkBase(simple_onto) + network.add_path(("ex:Pizza", "ex:hasTopping", "ex:Food")) + assert network._terms is None + assert network._g is None + + +def test_ontology_network_base_add_path_invalid_subject(simple_onto): + network = OntologyNetworkBase(simple_onto) + with pytest.raises(ValueError): + network.add_path(("ex:Invalid", "ex:hasTopping", "ex:Food")) + + +def test_ontology_network_base_add_path_invalid_object(simple_onto): + network = OntologyNetworkBase(simple_onto) + with pytest.raises(ValueError): + network.add_path(("ex:Pizza", "ex:hasTopping", "ex:Invalid")) + + +def test_ontology_network_init(tmp_path): + owl_file = tmp_path / "test.owl" + owl_content = """ + + + + """ + owl_file.write_text(owl_content) + network = OntologyNetwork(str(owl_file), format="xml") + assert network.onto is not None diff --git a/tests/test_parser.py b/tests/test_parser.py new file mode 100644 index 0000000..4a2b88e --- /dev/null +++ b/tests/test_parser.py @@ -0,0 +1,660 @@ +import pytest +from rdflib import Graph, Namespace, RDF, RDFS, OWL, URIRef, Literal, BNode +from tools4rdf.network.parser import OntoParser, parse_ontology +from tools4rdf.network.term import OntoTerm + + +@pytest.fixture +def simple_graph(): + g = Graph() + ex = Namespace("http://example.org/ontology#") + g.bind("ex", ex) + g.add((ex.Pizza, RDF.type, OWL.Class)) + g.add((ex.Food, RDF.type, OWL.Class)) + g.add((ex.Pizza, RDFS.subClassOf, ex.Food)) + return g + + +@pytest.fixture +def parser_with_properties(): + g = Graph() + ex = Namespace("http://example.org/ontology#") + g.bind("ex", ex) + + g.add((ex.Pizza, RDF.type, OWL.Class)) + g.add((ex.Person, RDF.type, OWL.Class)) + + g.add((ex.hasTopping, RDF.type, OWL.ObjectProperty)) + g.add((ex.hasTopping, RDFS.domain, ex.Pizza)) + g.add((ex.hasTopping, RDFS.range, ex.Food)) + + g.add((ex.hasPrice, RDF.type, OWL.DatatypeProperty)) + g.add((ex.hasPrice, RDFS.domain, ex.Pizza)) + + return OntoParser(g) + + +@pytest.fixture +def parser_with_union(): + g = Graph() + ex = Namespace("http://example.org/ontology#") + g.bind("ex", ex) + + g.add((ex.Pizza, RDF.type, OWL.Class)) + g.add((ex.Pasta, RDF.type, OWL.Class)) + g.add((ex.Person, RDF.type, OWL.Class)) + + union_node = BNode() + g.add((union_node, RDF.type, OWL.Class)) + collection = BNode() + g.add((union_node, OWL.unionOf, collection)) + g.add((collection, RDF.first, ex.Pizza)) + rest_node = BNode() + g.add((collection, RDF.rest, rest_node)) + g.add((rest_node, RDF.first, ex.Pasta)) + g.add((rest_node, RDF.rest, RDF.nil)) + + g.add((ex.hasFood, RDF.type, OWL.ObjectProperty)) + g.add((ex.hasFood, RDFS.domain, union_node)) + + return OntoParser(g) + + +def test_parse_ontology_xml(tmp_path): + owl_file = tmp_path / "test.owl" + owl_content = """ + + + + """ + owl_file.write_text(owl_content) + parser = parse_ontology(str(owl_file), format="xml") + assert isinstance(parser, OntoParser) + + +def test_ontoparser_init(): + g = Graph() + parser = OntoParser(g) + assert parser.graph == g + assert parser._data_dict is None + + +def test_ontoparser_lazy_initialization(): + g = Graph() + parser = OntoParser(g) + assert parser._data_dict is None + classes = parser.classes + assert parser._data_dict is not None + + +def test_ontoparser_add(): + g1 = Graph() + ex = Namespace("http://example.org/ontology#") + g1.add((ex.Pizza, RDF.type, OWL.Class)) + + g2 = Graph() + g2.add((ex.Pasta, RDF.type, OWL.Class)) + + parser1 = OntoParser(g1) + parser2 = OntoParser(g2) + combined = parser1 + parser2 + + assert isinstance(combined, OntoParser) + assert (ex.Pizza, RDF.type, OWL.Class) in combined.graph + assert (ex.Pasta, RDF.type, OWL.Class) in combined.graph + + +def test_ontoparser_radd(): + g1 = Graph() + ex = Namespace("http://example.org/ontology#") + g1.add((ex.Pizza, RDF.type, OWL.Class)) + + g2 = Graph() + g2.add((ex.Pasta, RDF.type, OWL.Class)) + + parser1 = OntoParser(g1) + parser2 = OntoParser(g2) + combined = parser2.__radd__(parser1) + + assert isinstance(combined, OntoParser) + + +def test_base_iri(): + g = Graph() + base = URIRef("http://example.org/ontology") + g.add((base, RDF.type, OWL.Ontology)) + parser = OntoParser(g) + assert parser.base_iri == "http://example.org/ontology" + + +def test_base_iri_none(): + g = Graph() + parser = OntoParser(g) + assert parser.base_iri is None + + +def test_extract_default_namespaces(): + g = Graph() + ex = Namespace("http://example.org/ontology#") + g.bind("ex", ex) + parser = OntoParser(g) + parser._extract_default_namespaces() + assert "ex" in parser.namespaces + assert parser.namespaces["ex"] == "http://example.org/ontology#" + + +def test_extract_classes(simple_graph): + parser = OntoParser(simple_graph) + parser._initialize() + assert len(parser.classes) > 0 + assert OWL.Thing in parser.classes + + +def test_extract_object_properties(): + g = Graph() + ex = Namespace("http://example.org/ontology#") + g.bind("ex", ex) + g.add((ex.hasTopping, RDF.type, OWL.ObjectProperty)) + g.add((ex.Pizza, RDF.type, OWL.Class)) + g.add((ex.hasTopping, RDFS.domain, ex.Pizza)) + + parser = OntoParser(g) + parser._initialize() + + assert "ex:hasTopping" in parser.attributes["object_property"] + assert ( + parser.attributes["object_property"]["ex:hasTopping"].node_type + == "object_property" + ) + + +def test_extract_data_properties(): + g = Graph() + ex = Namespace("http://example.org/ontology#") + g.bind("ex", ex) + g.add((ex.hasPrice, RDF.type, OWL.DatatypeProperty)) + g.add((ex.Pizza, RDF.type, OWL.Class)) + g.add((ex.hasPrice, RDFS.domain, ex.Pizza)) + + parser = OntoParser(g) + parser._initialize() + + assert "ex:hasPrice" in parser.attributes["data_property"] + assert ( + parser.attributes["data_property"]["ex:hasPrice"].node_type == "data_property" + ) + assert "ex:hasPricevalue" in parser.attributes["data_nodes"] + + +def test_extract_subproperties(): + g = Graph() + ex = Namespace("http://example.org/ontology#") + g.bind("ex", ex) + g.add((ex.hasTopping, RDF.type, OWL.ObjectProperty)) + g.add((ex.hasMozzarella, RDF.type, OWL.ObjectProperty)) + g.add((ex.hasMozzarella, RDFS.subPropertyOf, ex.hasTopping)) + + parser = OntoParser(g) + parser._initialize() + + assert ( + "ex:hasMozzarella" + in parser.attributes["object_property"]["ex:hasTopping"].subclasses + ) + + +def test_create_term(): + g = Graph() + ex = Namespace("http://example.org/ontology#") + g.bind("ex", ex) + g.add((ex.Pizza, RDF.type, OWL.Class)) + g.add((ex.Pizza, RDFS.comment, Literal("A delicious food"))) + + parser = OntoParser(g) + parser._extract_default_namespaces() + term = parser.create_term(ex.Pizza) + + assert isinstance(term, OntoTerm) + assert term.uri == "http://example.org/ontology#Pizza" + assert term.description.toPython() == "A delicious food" + + +def test_get_description_with_comment(): + g = Graph() + ex = Namespace("http://example.org/ontology#") + g.add((ex.Pizza, RDFS.comment, Literal("Pizza comment"))) + + parser = OntoParser(g) + desc = parser.get_description(ex.Pizza) + assert desc.toPython() == "Pizza comment" + + +def test_get_description_empty(): + g = Graph() + ex = Namespace("http://example.org/ontology#") + + parser = OntoParser(g) + desc = parser.get_description(ex.Pizza) + assert desc == "" + + +def test_lookup_namespace(): + g = Graph() + ex = Namespace("http://example.org/ontology#") + g.bind("ex", ex) + parser = OntoParser(g) + parser._extract_default_namespaces() + + result = parser._lookup_namespace("http://example.org/ontology#Pizza") + assert result == "ex" + + +def test_lookup_namespace_not_found(): + g = Graph() + parser = OntoParser(g) + result = parser._lookup_namespace("http://unknown.org/Pizza") + assert result is None + + +def test_parse_subclasses(): + g = Graph() + ex = Namespace("http://example.org/ontology#") + g.bind("ex", ex) + g.add((ex.Margherita, RDF.type, OWL.Class)) + g.add((ex.Pizza, RDF.type, OWL.Class)) + g.add((ex.Margherita, RDFS.subClassOf, ex.Pizza)) + + parser = OntoParser(g) + parser._initialize() + + assert "ex:Margherita" in parser.attributes["class"]["ex:Pizza"].subclasses + + +def test_add_subclasses_to_owlthing(): + g = Graph() + ex = Namespace("http://example.org/ontology#") + g.bind("ex", ex) + g.add((ex.Pizza, RDF.type, OWL.Class)) + + parser = OntoParser(g) + parser._initialize() + + assert "ex:Pizza" in parser.attributes["class"]["owl:Thing"].subclasses + + +def test_parse_equivalents(): + g = Graph() + ex = Namespace("http://example.org/ontology#") + g.bind("ex", ex) + g.add((ex.Pizza, RDF.type, OWL.Class)) + g.add((ex.Pie, RDF.type, OWL.Class)) + g.add((ex.Pizza, OWL.equivalentClass, ex.Pie)) + + parser = OntoParser(g) + parser._initialize() + + assert "ex:Pie" in parser.attributes["class"]["ex:Pizza"].equivalent_classes + assert "ex:Pizza" in parser.attributes["class"]["ex:Pie"].equivalent_classes + + +def test_parse_named_individuals(): + g = Graph() + ex = Namespace("http://example.org/ontology#") + g.bind("ex", ex) + g.add((ex.Pizza, RDF.type, OWL.Class)) + g.add((ex.MyPizza, RDF.type, OWL.NamedIndividual)) + g.add((ex.MyPizza, RDF.type, ex.Pizza)) + + parser = OntoParser(g) + parser._initialize() + + assert "ex:MyPizza" in parser.attributes["class"]["ex:Pizza"].named_individuals + + +def test_get_domain(parser_with_properties): + domain = parser_with_properties.attributes["object_property"][ + "ex:hasTopping" + ].domain + assert "ex:Pizza" in domain + + +def test_get_range(parser_with_properties): + rng = parser_with_properties.attributes["object_property"]["ex:hasTopping"].range + assert "ex:Food" in rng + + +def test_unravel_relation(): + g = Graph() + ex = Namespace("http://example.org/ontology#") + + collection = BNode() + g.add((collection, RDF.first, ex.Pizza)) + rest_node = BNode() + g.add((collection, RDF.rest, rest_node)) + g.add((rest_node, RDF.first, ex.Pasta)) + g.add((rest_node, RDF.rest, RDF.nil)) + + parser = OntoParser(g) + result = parser.unravel_relation(collection, []) + + assert len(result) == 2 + assert ex.Pizza in result + assert ex.Pasta in result + + +def test_lookup_node_simple(): + g = Graph() + ex = Namespace("http://example.org/ontology#") + g.bind("ex", ex) + g.add((ex.Pizza, RDF.type, OWL.Class)) + + parser = OntoParser(g) + parser._extract_default_namespaces() + parser.extract_classes() + parser.add_classes_to_attributes() + + result = parser.lookup_node(ex.Pizza) + assert "ex:Pizza" in result + + +def test_lookup_node_blank(): + g = Graph() + ex = Namespace("http://example.org/ontology#") + g.bind("ex", ex) + + union_node = BNode() + g.add((union_node, RDF.type, OWL.Class)) + collection = BNode() + g.add((union_node, OWL.unionOf, collection)) + g.add((collection, RDF.first, ex.Pizza)) + g.add((collection, RDF.rest, RDF.nil)) + + parser = OntoParser(g) + parser._extract_default_namespaces() + parser.extract_classes() + + node_str = union_node.toPython() + assert node_str in parser.mappings + + +def test_lookup_class(): + g = Graph() + ex = Namespace("http://example.org/ontology#") + g.bind("ex", ex) + g.add((ex.Pizza, RDF.type, OWL.Class)) + + parser = OntoParser(g) + parser._initialize() + + result = parser.lookup_class(ex.Pizza) + assert "ex:Pizza" in result + + +def test_lookup_class_blank_node(): + g = Graph() + ex = Namespace("http://example.org/ontology#") + g.bind("ex", ex) + g.add((ex.Pizza, RDF.type, OWL.Class)) + + union_node = BNode() + + parser = OntoParser(g) + parser._initialize() + + result = parser.lookup_class(union_node) + assert isinstance(result, list) + + +def test_get_attributes(): + g = Graph() + ex = Namespace("http://example.org/ontology#") + g.bind("ex", ex) + g.add((ex.Pizza, RDF.type, OWL.Class)) + g.add((ex.hasTopping, RDF.type, OWL.ObjectProperty)) + + parser = OntoParser(g) + parser._initialize() + + attrs = parser.get_attributes() + assert "ex" in attrs + assert "Pizza" in attrs["ex"] + + +def test_get_networkx_graph(): + g = Graph() + ex = Namespace("http://example.org/ontology#") + g.bind("ex", ex) + g.add((ex.Pizza, RDF.type, OWL.Class)) + g.add((ex.hasTopping, RDF.type, OWL.ObjectProperty)) + g.add((ex.hasTopping, RDFS.domain, ex.Pizza)) + + parser = OntoParser(g) + parser._initialize() + + nx_graph = parser.get_networkx_graph() + assert "ex:Pizza" in nx_graph.nodes() + assert "ex:hasTopping" in nx_graph.nodes() + + +def test_add_term_class(): + g = Graph() + ex = Namespace("http://example.org/ontology#") + g.bind("ex", ex) + + parser = OntoParser(g) + parser.add_term("http://example.org/ontology#NewPizza", "class", namespace="ex") + + assert ( + URIRef("http://example.org/ontology#NewPizza"), + RDF.type, + OWL.Class, + ) in parser.graph + + +def test_add_term_object_property(): + g = Graph() + ex = Namespace("http://example.org/ontology#") + g.bind("ex", ex) + + parser = OntoParser(g) + parser.add_term( + "http://example.org/ontology#newProperty", "object_property", namespace="ex" + ) + + assert ( + URIRef("http://example.org/ontology#newProperty"), + RDF.type, + OWL.ObjectProperty, + ) in parser.graph + + +def test_add_term_data_property(): + g = Graph() + ex = Namespace("http://example.org/ontology#") + g.bind("ex", ex) + + parser = OntoParser(g) + parser.add_term( + "http://example.org/ontology#newDataProp", "data_property", namespace="ex" + ) + + assert ( + URIRef("http://example.org/ontology#newDataProp"), + RDF.type, + OWL.DatatypeProperty, + ) in parser.graph + + +def test_add_term_invalid_type(): + g = Graph() + parser = OntoParser(g) + + with pytest.raises(ValueError): + parser.add_term("http://example.org/ontology#Invalid", "invalid_type") + + +def test_add_term_with_domain_range(): + g = Graph() + ex = Namespace("http://example.org/ontology#") + g.bind("ex", ex) + + parser = OntoParser(g) + parser.add_term( + "http://example.org/ontology#hasProperty", + "object_property", + namespace="ex", + dm=["http://example.org/ontology#Pizza"], + rn=["http://example.org/ontology#Topping"], + ) + + assert ( + URIRef("http://example.org/ontology#hasProperty"), + RDFS.domain, + URIRef("http://example.org/ontology#Pizza"), + ) in parser.graph + assert ( + URIRef("http://example.org/ontology#hasProperty"), + RDFS.range, + URIRef("http://example.org/ontology#Topping"), + ) in parser.graph + + +def test_add_namespace(): + g = Graph() + parser = OntoParser(g) + parser._initialize() + + parser.add_namespace("custom", "http://custom.org/") + + assert parser._data_dict is None + + +def test_extract_values(): + g = Graph() + ex = Namespace("http://example.org/ontology#") + g.add((ex.Pizza, RDFS.comment, Literal("comment"))) + + parser = OntoParser(g) + result = parser.extract_values(ex.Pizza, RDFS.comment) + + assert result == Literal("comment") + + +def test_extract_values_none(): + g = Graph() + ex = Namespace("http://example.org/ontology#") + + parser = OntoParser(g) + result = parser.extract_values(ex.Pizza, RDFS.comment) + + assert result is None + + +def test_union_class_extraction(): + g = Graph() + ex = Namespace("http://example.org/ontology#") + g.bind("ex", ex) + + g.add((ex.Pizza, RDF.type, OWL.Class)) + g.add((ex.Pasta, RDF.type, OWL.Class)) + + union_node = BNode() + g.add((union_node, RDF.type, OWL.Class)) + collection = BNode() + g.add((union_node, OWL.unionOf, collection)) + g.add((collection, RDF.first, ex.Pizza)) + rest_node = BNode() + g.add((collection, RDF.rest, rest_node)) + g.add((rest_node, RDF.first, ex.Pasta)) + g.add((rest_node, RDF.rest, RDF.nil)) + + parser = OntoParser(g) + parser._initialize() + + union_key = union_node.toPython() + assert union_key in parser.mappings + assert parser.mappings[union_key]["type"] == "union" + assert "ex:Pizza" in parser.mappings[union_key]["items"] + assert "ex:Pasta" in parser.mappings[union_key]["items"] + + +def test_intersection_class_extraction(): + g = Graph() + ex = Namespace("http://example.org/ontology#") + g.bind("ex", ex) + + g.add((ex.Pizza, RDF.type, OWL.Class)) + g.add((ex.Italian, RDF.type, OWL.Class)) + + intersection_node = BNode() + g.add((intersection_node, RDF.type, OWL.Class)) + collection = BNode() + g.add((intersection_node, OWL.intersectionOf, collection)) + g.add((collection, RDF.first, ex.Pizza)) + rest_node = BNode() + g.add((collection, RDF.rest, rest_node)) + g.add((rest_node, RDF.first, ex.Italian)) + g.add((rest_node, RDF.rest, RDF.nil)) + + parser = OntoParser(g) + parser._initialize() + + intersection_key = intersection_node.toPython() + assert intersection_key in parser.mappings + assert parser.mappings[intersection_key]["type"] == "intersection" + + +def test_domain_with_union(parser_with_union): + domain = parser_with_union.attributes["object_property"]["ex:hasFood"].domain + assert "ex:Pizza" in domain + assert "ex:Pasta" in domain + + +def test_recursively_add_subclasses(): + g = Graph() + ex = Namespace("http://example.org/ontology#") + g.bind("ex", ex) + + g.add((ex.Food, RDF.type, OWL.Class)) + g.add((ex.Pizza, RDF.type, OWL.Class)) + g.add((ex.Margherita, RDF.type, OWL.Class)) + + g.add((ex.Pizza, RDFS.subClassOf, ex.Food)) + g.add((ex.Margherita, RDFS.subClassOf, ex.Pizza)) + + parser = OntoParser(g) + parser._initialize() + + assert "ex:Margherita" in parser.attributes["class"]["ex:Food"].subclasses + + +def test_recursively_add_equivalents(): + g = Graph() + ex = Namespace("http://example.org/ontology#") + g.bind("ex", ex) + + g.add((ex.Pizza, RDF.type, OWL.Class)) + g.add((ex.Pie, RDF.type, OWL.Class)) + g.add((ex.FlatBread, RDF.type, OWL.Class)) + + g.add((ex.Pizza, OWL.equivalentClass, ex.Pie)) + g.add((ex.Pie, OWL.equivalentClass, ex.FlatBread)) + + parser = OntoParser(g) + parser._initialize() + + assert "ex:FlatBread" in parser.attributes["class"]["ex:Pizza"].equivalent_classes + + +def test_recheck_namespaces(): + g = Graph() + ex = Namespace("http://example.org/ontology#") + g.bind("ex", ex) + g.add((ex.Pizza, RDF.type, OWL.Class)) + + parser = OntoParser(g) + parser._initialize() + + assert "ex" in parser.namespaces diff --git a/tests/test_term.py b/tests/test_term.py new file mode 100644 index 0000000..4895b11 --- /dev/null +++ b/tests/test_term.py @@ -0,0 +1,384 @@ +import pytest +from tools4rdf.network.term import ( + OntoTerm, + is_url, + _get_namespace_and_name, + _get_namespace_with_prefix, + strip_name, +) + + +def test_is_url_valid(): + assert is_url("https://example.org/ontology") + assert is_url("http://example.org") + assert is_url("ftp://files.example.com/data") + + +def test_is_url_invalid(): + assert not is_url("not a url") + assert not is_url("example.org") + assert not is_url("/local/path") + + +def test_get_namespace_and_name_with_hash(): + uri = "http://example.org/ontology#Pizza" + namespace, name = _get_namespace_and_name(uri) + assert namespace == "ontology" + assert name == "Pizza" + + +def test_get_namespace_and_name_with_slash(): + uri = "http://example.org/ontology/Pizza" + namespace, name = _get_namespace_and_name(uri) + assert namespace == "ontology" + assert name == "Pizza" + + +def test_get_namespace_and_name_simple(): + uri = "Pizza" + namespace, name = _get_namespace_and_name(uri) + assert namespace == "" + assert name == "Pizza" + + +def test_get_namespace_with_prefix_hash(): + uri = "http://example.org/ontology#Pizza" + result = _get_namespace_with_prefix(uri) + assert result == "http://example.org/ontology" + + +def test_get_namespace_with_prefix_slash(): + uri = "http://example.org/ontology/Pizza" + result = _get_namespace_with_prefix(uri) + assert result == "http://example.org/ontology/" + + +def test_strip_name_returns_name(): + uri = "http://example.org/ontology#Pizza" + result = strip_name(uri, get_what="name") + assert result == "ontology:Pizza" + + +def test_strip_name_returns_namespace(): + uri = "http://example.org/ontology#Pizza" + result = strip_name(uri, get_what="namespace") + assert result == "ontology" + + +def test_strip_name_with_provided_namespace(): + uri = "http://example.org/ontology#Pizza" + result = strip_name(uri, get_what="name", namespace="custom") + assert result == "custom:Pizza" + + +def test_strip_name_invalid_option(): + uri = "http://example.org/ontology#Pizza" + with pytest.raises(ValueError): + strip_name(uri, get_what="invalid") + + +def test_ontoterm_init_with_uri(): + term = OntoTerm(uri="http://example.org/ontology#Pizza") + assert term.uri == "http://example.org/ontology#Pizza" + assert term.namespace == "ontology" + + +def test_ontoterm_init_with_name(): + term = OntoTerm(name="pizza:Pizza", uri="http://example.org#Pizza") + assert term.name == "pizza:Pizza" + + +def test_ontoterm_init_without_uri_and_name(): + with pytest.raises(ValueError): + OntoTerm() + + +def test_ontoterm_uri_property(): + term = OntoTerm(uri="http://example.org#Pizza") + assert term.uri == "http://example.org#Pizza" + term.uri = "http://new.org#Food" + assert term.uri == "http://new.org#Food" + + +def test_ontoterm_description_single_string(): + term = OntoTerm(uri="http://example.org#Pizza", description="A pizza") + assert term.description == "A pizza" + + +def test_ontoterm_description_list(): + term = OntoTerm(uri="http://example.org#Pizza", description=["First", "Second"]) + assert term.description == "First. Second" + + +def test_ontoterm_description_empty_list(): + term = OntoTerm(uri="http://example.org#Pizza", description=[]) + assert term.description == "" + + +def test_ontoterm_name_without_prefix(): + term = OntoTerm(uri="http://example.org#Pizza", name="pizza:Margherita-Special") + assert term.name_without_prefix == "MargheritaSpecial" + + +def test_ontoterm_query_name_data_property(): + term = OntoTerm( + uri="http://example.org#hasPrice", + name="pizza:hasPrice", + node_type="data_property", + ) + assert term.query_name == "pizza:hasPricevalue" + + +def test_ontoterm_query_name_other_type(): + term = OntoTerm( + uri="http://example.org#Pizza", name="pizza:Pizza", node_type="class" + ) + assert term.query_name == "pizza:Pizza" + + +def test_ontoterm_variable_name_no_parents(): + term = OntoTerm(uri="http://example.org#Pizza", name="pizza:Pizza") + assert term.variable_name == "Pizza" + + +def test_ontoterm_variable_name_with_parents(): + parent = OntoTerm(uri="http://example.org#Food", name="pizza:Food") + term = OntoTerm(uri="http://example.org#Pizza", name="pizza:Pizza") + term._parents.append(parent) + assert term.variable_name == "Food_Pizza" + + +def test_ontoterm_variable_name_data_property(): + term = OntoTerm( + uri="http://example.org#hasPrice", + name="pizza:hasPrice", + node_type="data_property", + ) + assert term.variable_name == "hasPricevalue" + + +def test_ontoterm_any_property(): + term = OntoTerm(uri="http://example.org#Pizza", name="pizza:Pizza") + any_term = term.any + assert any_term._enforce_type is False + assert any_term._add_subclass is False + + +def test_ontoterm_all_subtypes_property(): + term = OntoTerm(uri="http://example.org#Pizza", name="pizza:Pizza") + all_term = term.all_subtypes + assert all_term._enforce_type is False + assert all_term._add_subclass is True + + +def test_ontoterm_only_property(): + term = OntoTerm(uri="http://example.org#Pizza", name="pizza:Pizza") + only_term = term.only + assert only_term._enforce_type is True + assert only_term._add_subclass is False + + +def test_ontoterm_repr(): + term = OntoTerm(uri="http://example.org#Pizza", name="pizza:Pizza") + assert str(term) == "pizza:Pizza" + + +def test_ontoterm_clean_datatype(): + term = OntoTerm(uri="http://example.org#Pizza") + assert term._clean_datatype("str") == "string" + assert term._clean_datatype("int") == "int" + + +def test_ontoterm_eq_operator_data_property(): + term = OntoTerm( + uri="http://example.org#hasPrice", + name="pizza:hasPrice", + node_type="data_property", + rn=["float"], + ) + result = term == 10.5 + assert result._condition == '(?hasPricevalue="10.5"^^xsd:float)' + + +def test_ontoterm_eq_operator_non_data_property(): + term1 = OntoTerm( + uri="http://example.org#Pizza", name="pizza:Pizza", node_type="class" + ) + term2 = OntoTerm( + uri="http://example.org#Pizza", name="pizza:Pizza", node_type="class" + ) + assert (term1 == term2) is True + + +def test_ontoterm_lt_operator(): + term = OntoTerm( + uri="http://example.org#hasPrice", + name="pizza:hasPrice", + node_type="data_property", + rn=["float"], + ) + result = term < 10.5 + assert result._condition == '(?hasPricevalue<"10.5"^^xsd:float)' + + +def test_ontoterm_le_operator(): + term = OntoTerm( + uri="http://example.org#hasPrice", + name="pizza:hasPrice", + node_type="data_property", + rn=["float"], + ) + result = term <= 10.5 + assert result._condition == '(?hasPricevalue<="10.5"^^xsd:float)' + + +def test_ontoterm_gt_operator(): + term = OntoTerm( + uri="http://example.org#hasPrice", + name="pizza:hasPrice", + node_type="data_property", + rn=["float"], + ) + result = term > 10.5 + assert result._condition == '(?hasPricevalue>"10.5"^^xsd:float)' + + +def test_ontoterm_ge_operator(): + term = OntoTerm( + uri="http://example.org#hasPrice", + name="pizza:hasPrice", + node_type="data_property", + rn=["float"], + ) + result = term >= 10.5 + assert result._condition == '(?hasPricevalue>="10.5"^^xsd:float)' + + +def test_ontoterm_ne_operator(): + term = OntoTerm( + uri="http://example.org#hasPrice", + name="pizza:hasPrice", + node_type="data_property", + rn=["float"], + ) + result = term != 10.5 + assert result._condition == '(?hasPricevalue!="10.5"^^xsd:float)' + + +def test_ontoterm_and_operator(): + term1 = OntoTerm( + uri="http://example.org#hasPrice", + name="pizza:hasPrice", + node_type="data_property", + rn=["float"], + ) + term2 = OntoTerm( + uri="http://example.org#hasWeight", + name="pizza:hasWeight", + node_type="data_property", + rn=["float"], + ) + cond1 = term1 > 10 + cond2 = term2 < 5 + result = cond1 & cond2 + assert "&&" in result._condition + assert "hasPricevalue" in result._condition + assert "hasWeightvalue" in result._condition + + +def test_ontoterm_or_operator(): + term1 = OntoTerm( + uri="http://example.org#hasPrice", + name="pizza:hasPrice", + node_type="data_property", + rn=["float"], + ) + term2 = OntoTerm( + uri="http://example.org#hasWeight", + name="pizza:hasWeight", + node_type="data_property", + rn=["float"], + ) + cond1 = term1 > 10 + cond2 = term2 < 5 + result = cond1 | cond2 + assert "||" in result._condition + assert "hasPricevalue" in result._condition + assert "hasWeightvalue" in result._condition + + +def test_ontoterm_matmul_operator_deprecated(): + term1 = OntoTerm(uri="http://example.org#Pizza", name="pizza:Pizza") + term2 = OntoTerm(uri="http://example.org#Food", name="pizza:Food") + with pytest.warns(UserWarning): + result = term1 @ term2 + assert len(result._parents) == 1 + + +def test_ontoterm_refresh_condition(): + term = OntoTerm( + uri="http://example.org#hasPrice", + name="pizza:hasPrice", + node_type="data_property", + rn=["float"], + ) + cond = term > 10 + cond.refresh_condition() + assert cond._condition is None + assert cond._condition_parents == [] + + +def test_ontoterm_refresh(): + term = OntoTerm(uri="http://example.org#Pizza", name="pizza:Pizza") + parent = OntoTerm(uri="http://example.org#Food", name="pizza:Food") + term._parents.append(parent) + term._condition = "test" + term.refresh() + assert term._condition is None + assert term._parents == [] + assert term._condition_parents == [] + + +def test_ontoterm_operator_type_checking(): + term = OntoTerm( + uri="http://example.org#Pizza", name="pizza:Pizza", node_type="class" + ) + with pytest.raises(TypeError): + term < 10 + + +def test_ontoterm_operator_requires_data_property(): + term = OntoTerm( + uri="http://example.org#Pizza", name="pizza:Pizza", node_type="class" + ) + with pytest.raises(TypeError): + term > 5 + + +def test_ontoterm_and_requires_condition(): + term1 = OntoTerm( + uri="http://example.org#hasPrice", + name="pizza:hasPrice", + node_type="data_property", + rn=["float"], + ) + term2 = OntoTerm( + uri="http://example.org#hasWeight", + name="pizza:hasWeight", + node_type="data_property", + rn=["float"], + ) + with pytest.raises(ValueError): + term1 & term2 + + +def test_ontoterm_and_requires_ontoterm(): + term = OntoTerm( + uri="http://example.org#hasPrice", + name="pizza:hasPrice", + node_type="data_property", + rn=["float"], + ) + cond = term > 10 + with pytest.raises(TypeError): + cond & "not a term"