diff --git a/.bumpversion.cfg b/.bumpversion.cfg
index b670036..7052cd7 100644
--- a/.bumpversion.cfg
+++ b/.bumpversion.cfg
@@ -3,6 +3,8 @@ current_version = 0.3.6
commit = True
tag = False
-[bumpversion:file:setup.py]
+[bumpversion:file:pyproject.toml]
+search = version = "{current_version}"
+replace = version = "{new_version}"
[bumpversion:file:CITATION.cff]
diff --git a/.github/workflows/deploy.yml b/.github/workflows/deploy.yml
index 9b6b839..9e7621e 100644
--- a/.github/workflows/deploy.yml
+++ b/.github/workflows/deploy.yml
@@ -16,15 +16,12 @@ jobs:
with:
python-version: "3.10"
- - name: Install dependencies
+ - name: Install build dependencies
run: >-
- python -m pip install --user --upgrade setuptools wheel
- - name: Convert dependencies
+ python -m pip install --upgrade build
+ - name: Build distribution
run: >-
- sed -i 's/==/>=/g' setup.py; cat setup.py
- - name: Build
- run: >-
- python setup.py sdist bdist_wheel
+ python -m build
- name: Publish distribution 📦 to PyPI
if: startsWith(github.event.ref, 'refs/tags') || github.event_name == 'release'
uses: pypa/gh-action-pypi-publish@master
diff --git a/.github/workflows/testing.yml b/.github/workflows/testing.yml
index 365e566..4006fab 100644
--- a/.github/workflows/testing.yml
+++ b/.github/workflows/testing.yml
@@ -29,8 +29,7 @@ jobs:
pip install -e .
pip install pytest
pip install pytest-cov
- #pytest tests/
- pytest --cov-report=xml --cov=atomrdf tests/
+ pytest --cov-report=xml --cov=tools4rdf tests/
- name: Upload coverage reports to Codecov
uses: codecov/codecov-action@v4.0.1
diff --git a/CITATION.cff b/CITATION.cff
index a7dbc64..75c02f5 100644
--- a/CITATION.cff
+++ b/CITATION.cff
@@ -13,10 +13,22 @@ authors:
family-names: Azocar Guzman
affiliation: Institute for Advanced Simulations – Materials Data Science and Informatics (IAS-9), Forschungszentrum Jülich GmbH
orcid: 'https://orcid.org/0000-0001-7564-7990'
-#date-released: '2024-02-15'
-#doi: 10.5281/zenodo.8146527
-#url: 'https://atomrdf.pyscal.org'
+- given-names: Osamu
+ family-names: Waseda
+ affiliation: Max Planck Institute for Sustainable Materials, Düsseldorf, Germany
+ orcid: 'https://orcid.org/0000-0002-1677-4057'
+- given-names: Stefan
+ family-names: Sandfeld
+ affiliation: Institute for Advanced Simulations – Materials Data Science and Informatics (IAS-9), Forschungszentrum Jülich GmbH, Jülich, Germany
+ orcid: 'https://orcid.org/0000-0001-9560-4728'
+- given-names: Tilmann
+ family-names: Hickel
+ affiliation: Federal Institute for Materials Research and Testing (BAM), Berlin, Germany
+ orcid: 'https://orcid.org/0000-0003-0698-4891'
+#date-released:
+#doi:
+url: 'https://tools4rdf.readthedocs.io/en/latest/'
license: "MIT"
-repository-code: https://github.com/codo/tools4RDF
+repository-code: https://github.com/OCDO/tools4RDF
type: software
version: 0.3.6
diff --git a/MANIFEST.in b/MANIFEST.in
deleted file mode 100644
index 779a2e5..0000000
--- a/MANIFEST.in
+++ /dev/null
@@ -1 +0,0 @@
-include tools4rdf/data/*.??l
diff --git a/README.md b/README.md
index 5e7130b..bca5e53 100644
--- a/README.md
+++ b/README.md
@@ -1,4 +1,5 @@
-## 🧰 tools4RDF
+
+
**tools4RDF** is a Python toolkit for working with RDF data, SPARQL queries, and semantic networks.
diff --git a/_config.yml b/_config.yml
index 6e1a28b..ed539b3 100644
--- a/_config.yml
+++ b/_config.yml
@@ -1,7 +1,7 @@
# Book settings
# Learn more at https://jupyterbook.org/customize/config.html
-title: "atomrdf"
+title: "tools4RDF"
#author: The Jupyter Book Community
logo: docs/source/_static/logo.png
@@ -19,7 +19,7 @@ latex:
# Information about where the book exists on the web
repository:
- url: https://github.com/pyscal/atomrdf
+ url: https://github.com/OCDO/tools4RDF/
path_to_book: book
branch: main
diff --git a/docs/source/_static/logo.png b/docs/source/_static/logo.png
index 7fc319e..983e27b 100644
Binary files a/docs/source/_static/logo.png and b/docs/source/_static/logo.png differ
diff --git a/environment-docs.yml b/environment-docs.yml
index 2055cec..2f70058 100644
--- a/environment-docs.yml
+++ b/environment-docs.yml
@@ -10,4 +10,4 @@ dependencies:
- python-graphviz
- networkx
- pandas
- - jupyter-book
+ - jupyter-book =1.0.4
diff --git a/index.ipynb b/index.ipynb
index 3f7ad33..1b5824b 100644
--- a/index.ipynb
+++ b/index.ipynb
@@ -13,7 +13,7 @@
"id": "a123db1c",
"metadata": {},
"source": [
- "`tools4RDF` is a Python toolkit designed to simplify working with RDF data, ontologies, and knowledge graphs. It provides user-friendly utilities for creating, manipulating, querying, and visualizing RDF data, making it easier to integrate semantic web technologies into your research and applications.\n",
+ "`tools4RDF` is a Python toolkit designed to simplify working with RDF data, ontologies, and knowledge graphs. It provides user-friendly utilities for creating, manipulating, querying, and visualizing RDF data, making it easier to integrate semantic web technologies into research and applications, particularly for domain scientists and developers without deep RDF expertise.\n",
"\n",
"It is built on top of rdflib, and a primary function is the automated creation of SPARQL queries through an autocompleted programmatic interface.\n",
"\n",
@@ -53,7 +53,11 @@
"metadata": {},
"outputs": [],
"source": [
- "onto = OntologyNetwork('http://xmlns.com/foaf/0.1/')"
+ "# via XMLNS (recommended)\n",
+ "onto = OntologyNetwork('http://xmlns.com/foaf/0.1/')\n",
+ "\n",
+ "# if XMLNS fails\n",
+ "# onto = OntologyNetwork(\"https://raw.githubusercontent.com/SPAROntologies/foaf/master/docs/current/foaf.xml\")"
]
},
{
@@ -347,7 +351,7 @@
"- Read and parse ontologies\n",
"- Combine ontologies programatically and connect them\n",
"\n",
- "For more examples, please check [here](add link)\n"
+ "For more examples, please check [here](https://tools4rdf.readthedocs.io/en/latest/docs/examples.html)"
]
}
],
diff --git a/pyproject.toml b/pyproject.toml
new file mode 100644
index 0000000..55908d3
--- /dev/null
+++ b/pyproject.toml
@@ -0,0 +1,86 @@
+[build-system]
+requires = ["setuptools>=61.0", "wheel"]
+build-backend = "setuptools.build_meta"
+
+[project]
+name = "tools4rdf"
+version = "0.3.6"
+description = "python tool for working with ontologies and data models"
+readme = "README.md"
+requires-python = ">=3.8"
+license = {text = "MIT"}
+authors = [
+ {name = "Abril Azocar Guzman", email = "sarath.menon@pyscal.org"},
+ {name = "Sarath Menon", email = "sarath.menon@pyscal.org"}
+]
+maintainers = [
+ {name = "Sarath Menon", email = "sarath.menon@pyscal.org"}
+]
+keywords = ["ontology", "rdf", "sparql", "semantic-web", "knowledge-graph"]
+classifiers = [
+ "Programming Language :: Python :: 3",
+ "Programming Language :: Python :: 3.8",
+ "Programming Language :: Python :: 3.9",
+ "Programming Language :: Python :: 3.10",
+ "Programming Language :: Python :: 3.11",
+ "Programming Language :: Python :: 3.12",
+ "License :: OSI Approved :: MIT License",
+ "Operating System :: OS Independent",
+ "Intended Audience :: Science/Research",
+ "Topic :: Scientific/Engineering",
+]
+dependencies = [
+ "numpy",
+ "rdflib",
+ "pyyaml",
+ "graphviz",
+ "networkx",
+ "pandas",
+]
+
+[project.optional-dependencies]
+dev = [
+ "pytest>=7.0",
+ "pytest-cov",
+]
+docs = [
+ "sphinx",
+ "sphinx-rtd-theme",
+]
+
+[project.urls]
+Homepage = "https://pyscal.org"
+Documentation = "https://pyscal.org"
+Repository = "https://github.com/ocdo/tools4rdf"
+Download = "https://github.com/ocdo/tools4rdf"
+
+[tool.setuptools]
+zip-safe = false
+include-package-data = true
+
+[tool.setuptools.packages.find]
+include = ["tools4rdf*"]
+
+[tool.setuptools.package-data]
+tools4rdf = ["data/*.owl", "data/*.rdf", "data/*.ttl"]
+
+[tool.pytest.ini_options]
+testpaths = ["tests"]
+python_files = ["test_*.py"]
+python_classes = ["Test*"]
+python_functions = ["test_*"]
+addopts = "-v --strict-markers"
+
+[tool.coverage.run]
+source = ["tools4rdf"]
+omit = ["*/tests/*", "*/test_*.py"]
+
+[tool.coverage.report]
+exclude_lines = [
+ "pragma: no cover",
+ "def __repr__",
+ "raise AssertionError",
+ "raise NotImplementedError",
+ "if __name__ == .__main__.:",
+ "if TYPE_CHECKING:",
+]
diff --git a/setup.py b/setup.py
deleted file mode 100644
index 0c89831..0000000
--- a/setup.py
+++ /dev/null
@@ -1,26 +0,0 @@
-from setuptools import setup, find_packages
-
-
-with open('README.md') as readme_file:
- readme = readme_file.read()
-
-setup(
- name='tools4rdf',
- version='0.3.6',
- author='Abril Azocar Guzman, Sarath Menon',
- author_email='sarath.menon@pyscal.org',
- description='python tool for working with ontologies and data models',
- long_description=readme,
- long_description_content_type='text/markdown',
- packages=find_packages(include=['tools4rdf', 'tools4rdf.*']),
- zip_safe=False,
- download_url = 'https://github.com/ocdo/tools4rdf',
- url = 'https://pyscal.org',
- install_requires=['numpy', 'rdflib',
- 'pyyaml', 'graphviz', 'networkx',
- 'pandas'],
- classifiers=[
- 'Programming Language :: Python :: 3'
- ],
- include_package_data=True,
-)
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"
diff --git a/tools4rdf/network/network.py b/tools4rdf/network/network.py
index 31cc76f..adb9829 100644
--- a/tools4rdf/network/network.py
+++ b/tools4rdf/network/network.py
@@ -236,18 +236,21 @@ def get_shortest_path(self, source, target, triples=False, num_paths=1):
complete_list = [source, *target._parents, target]
# get path for first two terms
path = self._get_shortest_path(complete_list[0], complete_list[1])
+ # this is always of shape 1, x so we can directly take first element
+ path = path[0]
+
for x in range(2, len(complete_list)):
temp_source = complete_list[x - 1]
temp_dest = complete_list[x]
temp_path = self._get_shortest_path(temp_source, temp_dest)
- if len(temp_path) == 1:
- if len(temp_path[0]) == 2:
- # this means that they are next to each other, so we cannot form a full path
- # so we need to add the last item of the previous path
- path[-1][-1] = temp_path[0][-1]
+ # this will also always be of shape 1, x so we can directly take first element
+ temp_path = temp_path[0]
+ # ok now we need to merge temp_path into path
+ if len(temp_path) == 2:
+ path[-1] = temp_path[-1]
else:
path.extend(temp_path[1:])
- paths.extend(path)
+ paths.append(path)
else:
paths = self._get_shortest_path(source, target, num_paths=num_paths)
if triples:
@@ -298,6 +301,9 @@ def _modify_destinations(destinations):
modified_destinations.append(last_destination)
else:
modified_destinations.append(destination)
+ # now update conditions
+ for modified_destination in modified_destinations:
+ modified_destination._update_condition_string()
return modified_destinations
def _is_already_in_destinations(self, object_property, destinations):
@@ -635,10 +641,17 @@ def _add_types_for_source(self, source):
namespaces_used = []
if source._add_subclass and source.node_type == "class":
# we have to make a type query connection by union
- query.append(
- " { ?%s rdf:type %s . }"
- % (_strip_name(source.variable_name), source.query_name)
- )
+ # check if has any subclasses
+ if len(source.subclasses) == 0:
+ query.append(
+ " ?%s rdf:type %s . "
+ % (_strip_name(source.variable_name), source.query_name)
+ )
+ else:
+ query.append(
+ " { ?%s rdf:type %s . }"
+ % (_strip_name(source.variable_name), source.query_name)
+ )
if source.name.split(":")[0] not in namespaces_used:
namespaces_used.append(source.name.split(":")[0])
for cls_name in source.subclasses:
@@ -689,10 +702,23 @@ def _add_types_for_destination(self, destinations):
for destination in destinations:
if destination._add_subclass and destination.node_type == "class":
# we have to make a type query connection by union
- query.append(
- " { ?%s rdf:type %s . }"
- % (_strip_name(destination.variable_name), destination.query_name)
- )
+ # check if has any subclasses
+ if len(destination.subclasses) == 0:
+ query.append(
+ " ?%s rdf:type %s . "
+ % (
+ _strip_name(destination.variable_name),
+ destination.query_name,
+ )
+ )
+ else:
+ query.append(
+ " { ?%s rdf:type %s . }"
+ % (
+ _strip_name(destination.variable_name),
+ destination.query_name,
+ )
+ )
if destination.name.split(":")[0] not in namespaces_used:
namespaces_used.append(destination.name.split(":")[0])
for cls_name in destination.subclasses:
@@ -716,6 +742,21 @@ def _add_types_for_destination(self, destinations):
)
if destination.name.split(":")[0] not in namespaces_used:
namespaces_used.append(destination.name.split(":")[0])
+
+ # should do the same for parents of destination, if exists
+ # stepped guys, which are parents SHOULD NOT HAVE CLASS FLEXIBILITY!
+ for parent in destination._parents:
+ if parent._enforce_type and parent.node_type == "class":
+ query.append(
+ " ?%s rdf:type %s ."
+ % (
+ parent.variable_name,
+ parent.query_name,
+ )
+ )
+ if parent.name.split(":")[0] not in namespaces_used:
+ namespaces_used.append(parent.name.split(":")[0])
+
return query, namespaces_used
def _add_filters(self, destinations, remote_source=None):
diff --git a/tools4rdf/network/parser.py b/tools4rdf/network/parser.py
index 78baf39..2f649bb 100644
--- a/tools4rdf/network/parser.py
+++ b/tools4rdf/network/parser.py
@@ -530,9 +530,47 @@ class (`cls`) and predicate (`predicate`). It then resolves these objects
"""
domain = []
for obj in self.graph.objects(cls, predicate):
+ # Check if this BNode is a union/intersection
+ if isinstance(obj, BNode):
+ union_term = self.extract_values(obj, OWL.unionOf)
+ intersection_term = self.extract_values(obj, OWL.intersectionOf)
+
+ if union_term is not None or intersection_term is not None:
+ collection_term = (
+ union_term if union_term is not None else intersection_term
+ )
+ unravel_list = self.unravel_relation(collection_term, [])
+ items = [
+ strip_name(
+ item.toPython(),
+ namespace=self._lookup_namespace(item.toPython()),
+ )
+ for item in unravel_list
+ ]
+ domain.extend(items)
+
+ additional_terms = []
+ for term in items:
+ if term in self.attributes["class"]:
+ additional_terms += self.attributes["class"][
+ term
+ ].subclasses
+ additional_terms += self.attributes["class"][
+ term
+ ].equivalent_classes
+ additional_terms += self.attributes["class"][
+ term
+ ].named_individuals
+ domain.extend(additional_terms)
+ continue
+
domain_term = self.lookup_node(obj)
for term in domain_term:
domain.append(term)
+ if term in self.attributes["class"]:
+ domain.extend(self.attributes["class"][term].subclasses)
+ domain.extend(self.attributes["class"][term].equivalent_classes)
+ domain.extend(self.attributes["class"][term].named_individuals)
return domain
def create_term(self, cls):
diff --git a/tools4rdf/network/term.py b/tools4rdf/network/term.py
index 5df5a7f..9eab032 100644
--- a/tools4rdf/network/term.py
+++ b/tools4rdf/network/term.py
@@ -185,6 +185,7 @@ def __init__(
self.target = target
self._enforce_type = True
self._add_subclass = True
+ self._old_variable_name = None
@property
def URIRef(self):
@@ -437,7 +438,18 @@ def _is_data_node(self):
"This operation can only be performed with a data property!"
)
+ def _update_condition_string(
+ self,
+ ):
+ condition_string = self._condition
+ if self._old_variable_name is not None:
+ condition_string = condition_string.replace(
+ self._old_variable_name, self.variable_name
+ )
+ self._condition = condition_string
+
def _create_condition_string(self, condition, val):
+ self._old_variable_name = self.variable_name
return f'(?{self.variable_name}{condition}"{val}"^^xsd:{self._clean_datatype(self.range[0])})'
# overloading operators
@@ -487,8 +499,9 @@ def __gt__(self, val):
# print(f'lhs {self} rhs {val}')
self._is_number(val)
self._is_data_node()
- self._condition = self._create_condition_string(">", val)
- return self
+ item = copy.deepcopy(self)
+ item._condition = item._create_condition_string(">", val)
+ return item
def __and__(self, term):
self._is_term(term)
@@ -503,6 +516,7 @@ def __and__(self, term):
# and clean up the inbound term
if item.name != term.name:
term.refresh_condition()
+ self.refresh_condition()
return item
def and_(self, term):
@@ -521,6 +535,7 @@ def __or__(self, term):
# and clean up the inbound term
if item.name != term.name:
term.refresh_condition()
+ self.refresh_condition()
return item
def or_(self, term):