Skip to content

Commit cb1062c

Browse files
authored
Merge pull request #208 from dice-group/ontology-save-formats
Ontology save formats
2 parents ea7054c + 11585bf commit cb1062c

2 files changed

Lines changed: 292 additions & 5 deletions

File tree

owlapy/owl_ontology.py

Lines changed: 5 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -1379,8 +1379,8 @@ def save(self, path: str = None, document_iri: Optional[IRI] = None,
13791379
fmt_key = document_format.strip().lower() if document_format is not None else None
13801380

13811381
# ── rdflib-backed path ──────────────────────────────────────────────
1382-
if fmt_key is not None and fmt_key in self._RDFLIB_FORMATS:
1383-
self._save_via_rdflib(path, self._RDFLIB_FORMATS[fmt_key])
1382+
if fmt_key is not None and fmt_key in _RDFLIB_FORMATS:
1383+
self._save_via_rdflib(path, _RDFLIB_FORMATS[fmt_key])
13841384
return
13851385

13861386
# ── OWL API path ────────────────────────────────────────────────────
@@ -1390,10 +1390,10 @@ def save(self, path: str = None, document_iri: Optional[IRI] = None,
13901390
import org.semanticweb.owlapi.formats
13911391

13921392
if fmt_key is not None:
1393-
fmt_class_name = self._DOCUMENT_FORMATS.get(fmt_key)
1393+
fmt_class_name = _DOCUMENT_FORMATS.get(fmt_key)
13941394
if fmt_class_name is None:
13951395
all_supported = sorted(
1396-
set(self._DOCUMENT_FORMATS.keys()) | set(self._RDFLIB_FORMATS.keys())
1396+
set(_DOCUMENT_FORMATS.keys()) | set(_RDFLIB_FORMATS.keys())
13971397
)
13981398
raise ValueError(
13991399
f"Unsupported document format '{document_format}'. "
@@ -1444,7 +1444,7 @@ def _save_via_rdflib(self, path: str, rdflib_format: str) -> None:
14441444
self.owlapi_ontology, RDFXMLDocumentFormat(), fos
14451445
)
14461446
# Step 2 – parse into the appropriate graph type
1447-
if rdflib_format in self._RDFLIB_CONJUNCTIVE_FORMATS:
1447+
if rdflib_format in _RDFLIB_CONJUNCTIVE_FORMATS:
14481448
g = rdflib.ConjunctiveGraph()
14491449
else:
14501450
g = rdflib.Graph()
Lines changed: 287 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,287 @@
1+
"""
2+
Tests for SyncOntology.save() document_format parameter.
3+
4+
One test per supported format. Files are saved into the
5+
'tests/saved_formats/' directory and intentionally NOT deleted after
6+
testing so the user can inspect them.
7+
8+
Formats are split into two groups:
9+
10+
OWL API–backed
11+
Written directly by OWL API's built-in storers.
12+
13+
rdflib-backed
14+
Written by first dumping to a temporary RDF/XML file via OWL API,
15+
then re-serialising with rdflib. These are: turtle2, json-ld /
16+
jsonld, ntriples / nt, nt11, n3, trig, trix, nquads / nq.
17+
"""
18+
import glob
19+
import os
20+
import unittest
21+
22+
from owlapy.owl_ontology import SyncOntology
23+
24+
_HERE = os.path.dirname(os.path.abspath(__file__))
25+
26+
# All saved files land here – created automatically by _out().
27+
OUTPUT_DIR = os.path.join(_HERE, "saved_formats")
28+
29+
30+
def _out(filename: str) -> str:
31+
"""Return absolute path for an output file inside OUTPUT_DIR."""
32+
os.makedirs(OUTPUT_DIR, exist_ok=True)
33+
return os.path.join(OUTPUT_DIR, filename)
34+
35+
36+
def _axiom_counts(onto: SyncOntology):
37+
abox = len(list(onto.get_abox_axioms()))
38+
tbox = len(list(onto.get_tbox_axioms()))
39+
return abox, tbox
40+
41+
42+
class TestSyncOntologySaveFormats(unittest.TestCase):
43+
"""
44+
Each test:
45+
1. Loads the father ontology.
46+
2. Saves it using a specific document_format string.
47+
3. Asserts the output file exists and is non-empty.
48+
4. For formats that OWL API can reload (RDF/XML, OWL/XML, Turtle,
49+
Functional Syntax) also asserts ABox/TBox axiom counts match
50+
the original (roundtrip check).
51+
"""
52+
53+
@classmethod
54+
def setUpClass(cls):
55+
os.makedirs(OUTPUT_DIR, exist_ok=True)
56+
cls.source = SyncOntology("KGs/Family/father.owl")
57+
cls.expected_abox, cls.expected_tbox = _axiom_counts(cls.source)
58+
59+
# ------------------------------------------------------------------
60+
# Helpers
61+
# ------------------------------------------------------------------
62+
63+
def _assert_file_exists_and_nonempty(self, path: str):
64+
self.assertTrue(os.path.exists(path),
65+
f"Expected output file not found: {path}")
66+
self.assertGreater(os.path.getsize(path), 0,
67+
f"Output file is empty: {path}")
68+
69+
def _assert_roundtrip(self, path: str):
70+
"""Reload file with OWL API and verify axiom counts match source."""
71+
reloaded = SyncOntology(path)
72+
abox, tbox = _axiom_counts(reloaded)
73+
self.assertEqual(self.expected_abox, abox,
74+
f"ABox count mismatch after roundtrip ({path})")
75+
self.assertEqual(self.expected_tbox, tbox,
76+
f"TBox count mismatch after roundtrip ({path})")
77+
78+
# ==================================================================
79+
# OWL API–backed formats
80+
# ==================================================================
81+
82+
def test_save_rdfxml(self):
83+
path = _out("father_rdfxml.owl")
84+
self.source.save(path=path, document_format="rdfxml")
85+
self._assert_file_exists_and_nonempty(path)
86+
self._assert_roundtrip(path)
87+
88+
def test_save_rdf_xml_slash(self):
89+
"""Alias 'rdf/xml'."""
90+
path = _out("father_rdf_xml_slash.owl")
91+
self.source.save(path=path, document_format="rdf/xml")
92+
self._assert_file_exists_and_nonempty(path)
93+
self._assert_roundtrip(path)
94+
95+
def test_save_owlxml(self):
96+
path = _out("father_owlxml.owl")
97+
self.source.save(path=path, document_format="owlxml")
98+
self._assert_file_exists_and_nonempty(path)
99+
self._assert_roundtrip(path)
100+
101+
def test_save_owl_xml_slash(self):
102+
"""Alias 'owl/xml'."""
103+
path = _out("father_owl_xml_slash.owl")
104+
self.source.save(path=path, document_format="owl/xml")
105+
self._assert_file_exists_and_nonempty(path)
106+
self._assert_roundtrip(path)
107+
108+
def test_save_turtle_owlapi(self):
109+
"""Turtle via OWL API ('turtle' key)."""
110+
path = _out("father_turtle_owlapi.ttl")
111+
self.source.save(path=path, document_format="turtle")
112+
self._assert_file_exists_and_nonempty(path)
113+
self._assert_roundtrip(path)
114+
115+
def test_save_ttl_alias(self):
116+
"""Alias 'ttl' for OWL API Turtle."""
117+
path = _out("father_ttl.ttl")
118+
self.source.save(path=path, document_format="ttl")
119+
self._assert_file_exists_and_nonempty(path)
120+
self._assert_roundtrip(path)
121+
122+
def test_save_functional(self):
123+
path = _out("father_functional.ofn")
124+
self.source.save(path=path, document_format="functional")
125+
self._assert_file_exists_and_nonempty(path)
126+
self._assert_roundtrip(path)
127+
128+
def test_save_fss_alias(self):
129+
"""Alias 'fss' for Functional Syntax."""
130+
path = _out("father_fss.ofn")
131+
self.source.save(path=path, document_format="fss")
132+
self._assert_file_exists_and_nonempty(path)
133+
self._assert_roundtrip(path)
134+
135+
def test_save_manchester(self):
136+
"""Manchester Syntax – write-only check (OWL API cannot reload it)."""
137+
path = _out("father_manchester.omn")
138+
self.source.save(path=path, document_format="manchester")
139+
self._assert_file_exists_and_nonempty(path)
140+
141+
def test_save_ms_alias(self):
142+
"""Alias 'ms' for Manchester Syntax."""
143+
path = _out("father_ms.omn")
144+
self.source.save(path=path, document_format="ms")
145+
self._assert_file_exists_and_nonempty(path)
146+
147+
def test_save_latex(self):
148+
path = _out("father_latex.tex")
149+
self.source.save(path=path, document_format="latex")
150+
self._assert_file_exists_and_nonempty(path)
151+
152+
def test_save_dlsyntax(self):
153+
path = _out("father_dlsyntax.dl")
154+
self.source.save(path=path, document_format="dlsyntax")
155+
self._assert_file_exists_and_nonempty(path)
156+
157+
def test_save_dl_alias(self):
158+
"""Alias 'dl' for DL Syntax."""
159+
path = _out("father_dl.dl")
160+
self.source.save(path=path, document_format="dl")
161+
self._assert_file_exists_and_nonempty(path)
162+
163+
def test_save_krss2(self):
164+
path = _out("father_krss2.krss")
165+
self.source.save(path=path, document_format="krss2")
166+
self._assert_file_exists_and_nonempty(path)
167+
168+
169+
def test_save_obo(self):
170+
path = _out("father_obo.obo")
171+
self.source.save(path=path, document_format="obo")
172+
self._assert_file_exists_and_nonempty(path)
173+
174+
# ==================================================================
175+
# rdflib-backed formats
176+
# ==================================================================
177+
178+
def test_save_turtle2(self):
179+
"""Turtle via rdflib ('turtle2' key)."""
180+
path = _out("father_turtle2.ttl")
181+
self.source.save(path=path, document_format="turtle2")
182+
self._assert_file_exists_and_nonempty(path)
183+
184+
def test_save_ntriples(self):
185+
"""N-Triples via rdflib ('ntriples' key)."""
186+
path = _out("father_ntriples.nt")
187+
self.source.save(path=path, document_format="ntriples")
188+
self._assert_file_exists_and_nonempty(path)
189+
190+
def test_save_nt_alias(self):
191+
"""Alias 'nt' for N-Triples via rdflib."""
192+
path = _out("father_nt.nt")
193+
self.source.save(path=path, document_format="nt")
194+
self._assert_file_exists_and_nonempty(path)
195+
196+
def test_save_nt11(self):
197+
"""N-Triples 1.1 via rdflib ('nt11' key)."""
198+
path = _out("father_nt11.nt")
199+
self.source.save(path=path, document_format="nt11")
200+
self._assert_file_exists_and_nonempty(path)
201+
202+
def test_save_n3(self):
203+
"""Notation3 via rdflib ('n3' key)."""
204+
path = _out("father_n3.n3")
205+
self.source.save(path=path, document_format="n3")
206+
self._assert_file_exists_and_nonempty(path)
207+
208+
def test_save_trig(self):
209+
"""TriG via rdflib ('trig' key)."""
210+
path = _out("father_trig.trig")
211+
self.source.save(path=path, document_format="trig")
212+
self._assert_file_exists_and_nonempty(path)
213+
214+
def test_save_trix(self):
215+
"""TriX via rdflib ('trix' key)."""
216+
path = _out("father_trix.trix")
217+
self.source.save(path=path, document_format="trix")
218+
self._assert_file_exists_and_nonempty(path)
219+
220+
def test_save_nquads(self):
221+
"""N-Quads via rdflib ('nquads' key)."""
222+
path = _out("father_nquads.nq")
223+
self.source.save(path=path, document_format="nquads")
224+
self._assert_file_exists_and_nonempty(path)
225+
226+
def test_save_nq_alias(self):
227+
"""Alias 'nq' for N-Quads via rdflib."""
228+
path = _out("father_nq.nq")
229+
self.source.save(path=path, document_format="nq")
230+
self._assert_file_exists_and_nonempty(path)
231+
232+
def test_save_jsonld(self):
233+
"""JSON-LD via rdflib ('jsonld' key)."""
234+
path = _out("father_jsonld.jsonld")
235+
self.source.save(path=path, document_format="jsonld")
236+
self._assert_file_exists_and_nonempty(path)
237+
238+
def test_save_json_ld_alias(self):
239+
"""Alias 'json-ld' for JSON-LD via rdflib."""
240+
path = _out("father_json-ld.jsonld")
241+
self.source.save(path=path, document_format="json-ld")
242+
self._assert_file_exists_and_nonempty(path)
243+
244+
# ==================================================================
245+
# Edge-case / behavioural tests
246+
# ==================================================================
247+
248+
def test_save_default_format(self):
249+
"""document_format=None keeps the ontology's current format."""
250+
path = _out("father_default_format.owl")
251+
self.source.save(path=path, document_format=None)
252+
self._assert_file_exists_and_nonempty(path)
253+
self._assert_roundtrip(path)
254+
255+
def test_save_format_case_insensitive(self):
256+
"""OWL API format strings are matched case-insensitively."""
257+
path = _out("father_turtle_upper.ttl")
258+
self.source.save(path=path, document_format="TURTLE")
259+
self._assert_file_exists_and_nonempty(path)
260+
self._assert_roundtrip(path)
261+
262+
def test_save_rdflib_format_case_insensitive(self):
263+
"""rdflib format strings are also matched case-insensitively."""
264+
path = _out("father_ntriples_upper.nt")
265+
self.source.save(path=path, document_format="NTRIPLES")
266+
self._assert_file_exists_and_nonempty(path)
267+
268+
def test_save_invalid_format_raises(self):
269+
"""An unrecognised format string raises ValueError."""
270+
with self.assertRaises(ValueError):
271+
self.source.save(path=_out("should_not_exist.owl"),
272+
document_format="not_a_real_format")
273+
274+
def test_rdflib_temp_file_is_deleted(self):
275+
"""The intermediate RDF/XML temp file must not be left on disk."""
276+
before = set(glob.glob("/tmp/_owlapy_tmp_*.owl"))
277+
path = _out("father_trig_cleanup.trig")
278+
self.source.save(path=path, document_format="trig")
279+
after = set(glob.glob("/tmp/_owlapy_tmp_*.owl"))
280+
leftover = after - before
281+
self.assertEqual(set(), leftover,
282+
f"Temp file(s) not cleaned up: {leftover}")
283+
284+
285+
if __name__ == "__main__":
286+
unittest.main()
287+

0 commit comments

Comments
 (0)