From 063922bf8ea9dde12d784e7b4dc44db4ab16185b Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bj=C3=B6rn=20Gehrke?= <5106696+b-gehrke@users.noreply.github.com> Date: Thu, 27 Jun 2024 08:08:56 +0200 Subject: [PATCH 1/2] Allowed access to prefix mapping --- docs/source/usage.rst | 36 ++----- gen_pyi.py | 13 ++- pyhornedowl/__init__.py | 4 +- pyhornedowl/__init__.pyi | 51 +++++++--- src/lib.rs | 81 +++++++++------ src/ontology.rs | 209 +++++++++++++++++++++++---------------- src/prefix_mapping.rs | 114 +++++++++++++++++++++ test/test_base.py | 4 +- test/test_id.py | 18 ++-- test/test_io.py | 4 + test/test_iri.py | 15 ++- test/test_label.py | 12 +-- test/test_modify.py | 16 +-- 13 files changed, 386 insertions(+), 191 deletions(-) create mode 100644 src/prefix_mapping.rs diff --git a/docs/source/usage.rst b/docs/source/usage.rst index 198e86b..ca37f73 100644 --- a/docs/source/usage.rst +++ b/docs/source/usage.rst @@ -57,24 +57,6 @@ An exception to this is the the function :func:`PyIndexedOntology.curie None: - """ - Adds the prefix for rdf, rdfs, xsd, and owl - """ - ... - def get_id_for_iri(self, iri: str, iri_is_absolute: Optional[bool] = None) -> Optional[str]: """ Gets the ID of term by it IRI. @@ -30,12 +24,6 @@ class PyIndexedOntology: """ ... - def add_prefix_mapping(self, iriprefix: str, mappedid: str) -> None: - """ - Adds the prefix `iriprefix`. - """ - ... - def set_label(self, iri: str, label: str, *, absolute: Optional[bool] = None) -> None: """ Sets the label of a term by iri. @@ -286,6 +274,11 @@ class PyIndexedOntology: """ ... + prefix_mapping: PrefixMapping + """ + The prefix mapping + """ + class IndexCreationStrategy: """ @@ -305,6 +298,38 @@ class IndexCreationStrategy: Only create the additional indexes when explicity requested """ +class PrefixMapping: + def add_default_prefix_names(self) -> None: + """ + Adds the prefix for rdf, rdfs, xsd, and owl + """ + ... + + def add_prefix(self, iriprefix: str, mappedid: str) -> None: + """ + Adds the prefix `iriprefix`. + """ + ... + + def remove_prefix(self, iriprefix: str) -> None: + """ + Remove a prefix from the mapping. + """ + ... + + def expand_curie(self, curie: str) -> str: + """ + Expands a curie. Throws a ValueError if the prefix is invalid or unknown + """ + ... + + def shring_iri(self, iri: str) -> str: + """ + Shrinks an absolute IRI to a CURIE. Throws a ValueError on failure + """ + ... + + def open_ontology(ontology: str, serialization: Optional[typing.Literal['owl', 'rdf','ofn', 'owx']]=None, index_strategy = IndexCreationStrategy.OnQuery) -> PyIndexedOntology: """ Opens an ontology from a path or plain text. @@ -343,7 +368,7 @@ def get_descendants(onto: PyIndexedOntology, parent: str) -> Set[str]: ... -@deprecated(please use `PyIndexedOntology.get_ancestors` instead) +@deprecated("please use `PyIndexedOntology.get_ancestors` instead") def get_ancestors(onto: PyIndexedOntology, child: str) -> Set[str]: """ Gets all direct and indirect super classes of a class. diff --git a/src/lib.rs b/src/lib.rs index 479d1d1..c260729 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -12,45 +12,47 @@ use pyo3::exceptions::PyValueError; use pyo3::prelude::*; use pyo3::wrap_pyfunction; -use crate::ontology::{get_ancestors, get_descendants, PyIndexedOntology, IndexCreationStrategy}; +use crate::ontology::{get_ancestors, get_descendants, IndexCreationStrategy, PyIndexedOntology}; #[macro_use] mod doc; mod model; mod ontology; - - +mod prefix_mapping; #[macro_export] macro_rules! to_py_err { ($message:literal) => { - | error | PyValueError::new_err(format!("{}: {:?}", $message, error)) + |error| PyValueError::new_err(format!("{}: {:?}", $message, error)) }; } - fn parse_serialization(serialization: Option<&str>) -> Option { match serialization.map(|s| s.to_lowercase()).as_deref() { Some("owx") => Some(ResourceType::OWX), Some("ofn") => Some(ResourceType::OFN), Some("rdf") => Some(ResourceType::RDF), Some("owl") => Some(ResourceType::RDF), - _ => None + _ => None, } } fn guess_serialization(path: &String, serialization: Option<&str>) -> PyResult { parse_serialization(serialization).map(Ok).unwrap_or( match serialization.map(|s| s.to_lowercase()).as_deref() { - Some(f) => Err(PyValueError::new_err(format!("Unsupported serialization '{}'", f))), - None => Ok(path_type(path.as_ref()).unwrap_or(ResourceType::OWX)) - }) + Some(f) => Err(PyValueError::new_err(format!( + "Unsupported serialization '{}'", + f + ))), + None => Ok(path_type(path.as_ref()).unwrap_or(ResourceType::OWX)), + }, + ) } fn open_ontology_owx( content: &mut R, b: &Build>, - index_strategy: IndexCreationStrategy + index_strategy: IndexCreationStrategy, ) -> Result<(PyIndexedOntology, PrefixMapping), HornedError> { let (o, m) = horned_owl::io::owx::reader::read_with_build(content, &b)?; Ok((PyIndexedOntology::from_set_ontology(o, index_strategy), m)) @@ -59,7 +61,7 @@ fn open_ontology_owx( fn open_ontology_ofn( content: &mut R, b: &Build>, - index_strategy: IndexCreationStrategy + index_strategy: IndexCreationStrategy, ) -> Result<(PyIndexedOntology, PrefixMapping), HornedError> { let (o, m) = horned_owl::io::ofn::reader::read_with_build(content, &b)?; Ok((PyIndexedOntology::from_set_ontology(o, index_strategy), m)) @@ -68,16 +70,22 @@ fn open_ontology_ofn( fn open_ontology_rdf( content: &mut R, b: &Build, - index_strategy: IndexCreationStrategy + index_strategy: IndexCreationStrategy, ) -> Result<(PyIndexedOntology, PrefixMapping), HornedError> { horned_owl::io::rdf::reader::read_with_build::( content, - &b, + &b, ParserConfiguration { rdf: RDFParserConfiguration { lax: true }, ..Default::default() }, - ).map(|(o, _)| (PyIndexedOntology::from_rdf_ontology(o, index_strategy), PrefixMapping::default())) + ) + .map(|(o, _)| { + ( + PyIndexedOntology::from_rdf_ontology(o, index_strategy), + PrefixMapping::default(), + ) + }) } /// open_ontology_from_file(path: str, serialization: Optional[typing.Literal['owl', 'rdf','ofn', 'owx']]=None, index_strategy = IndexCreationStrategy.OnQuery) -> PyIndexedOntology @@ -86,7 +94,12 @@ fn open_ontology_rdf( /// /// If the serialization is not specified it is guessed from the file extension. Defaults to OWL/XML. #[pyfunction(signature = (path, serialization = None, index_strategy = IndexCreationStrategy::OnQuery))] -fn open_ontology_from_file(path: String, serialization: Option<&str>, index_strategy: IndexCreationStrategy) -> PyResult { +fn open_ontology_from_file( + py: Python<'_>, + path: String, + serialization: Option<&str>, + index_strategy: IndexCreationStrategy, +) -> PyResult { let serialization = guess_serialization(&path, serialization)?; let file = File::open(path)?; @@ -97,10 +110,11 @@ fn open_ontology_from_file(path: String, serialization: Option<&str>, index_stra let (mut pio, mapping) = match serialization { ResourceType::OFN => open_ontology_ofn(&mut f, &b, index_strategy), ResourceType::OWX => open_ontology_owx(&mut f, &b, index_strategy), - ResourceType::RDF => open_ontology_rdf(&mut f, &b, index_strategy) - }.map_err(to_py_err!("Failed to open ontology"))?; + ResourceType::RDF => open_ontology_rdf(&mut f, &b, index_strategy), + } + .map_err(to_py_err!("Failed to open ontology"))?; - pio.mapping = mapping; + pio.mapping = Py::new(py, prefix_mapping::PrefixMapping::from(mapping))?; Ok(pio) } @@ -110,24 +124,29 @@ fn open_ontology_from_file(path: String, serialization: Option<&str>, index_stra /// /// If no serialization is specified, all parsers are tried until one succeeds #[pyfunction(signature = (ontology, serialization = None, index_strategy = IndexCreationStrategy::OnQuery))] -fn open_ontology_from_string(ontology: String, serialization: Option<&str>, index_strategy: IndexCreationStrategy) -> PyResult { +fn open_ontology_from_string( + py: Python<'_>, + ontology: String, + serialization: Option<&str>, + index_strategy: IndexCreationStrategy, +) -> PyResult { let serialization = parse_serialization(serialization); let mut f = BufReader::new(ontology.as_bytes()); let b = Build::new_arc(); - let (imo, mapping) = match serialization { + let (mut pio, mapping) = match serialization { Some(ResourceType::OFN) => open_ontology_ofn(&mut f, &b, index_strategy), Some(ResourceType::OWX) => open_ontology_owx(&mut f, &b, index_strategy), Some(ResourceType::RDF) => open_ontology_rdf(&mut f, &b, index_strategy), None => open_ontology_ofn(&mut f, &b, index_strategy) .or_else(|_| open_ontology_rdf(&mut f, &b, index_strategy)) - .or_else(|_| open_ontology_owx(&mut f, &b, index_strategy)) - }.map_err(to_py_err!("Failed to open ontology"))?; + .or_else(|_| open_ontology_owx(&mut f, &b, index_strategy)), + } + .map_err(to_py_err!("Failed to open ontology"))?; - let mut lo = PyIndexedOntology::from(imo); - lo.mapping = mapping; //Needed when saving - Ok(lo) + pio.mapping = Py::new(py, prefix_mapping::PrefixMapping::from(mapping))?; + Ok(pio) } /// open_ontology(ontology: str, serialization: Optional[typing.Literal['owl', 'rdf','ofn', 'owx']]=None, index_strategy = IndexCreationStrategy.OnQuery) -> PyIndexedOntology @@ -139,11 +158,16 @@ fn open_ontology_from_string(ontology: String, serialization: Option<&str>, inde /// If no serialization is specified the serialization is guessed by the file extension or all parsers are tried /// until one succeeds. #[pyfunction(signature = (ontology, serialization = None, index_strategy = IndexCreationStrategy::OnQuery))] -fn open_ontology(ontology: String, serialization: Option<&str>, index_strategy: IndexCreationStrategy) -> PyResult { +fn open_ontology( + py: Python<'_>, + ontology: String, + serialization: Option<&str>, + index_strategy: IndexCreationStrategy, +) -> PyResult { if Path::exists(ontology.as_ref()) { - open_ontology_from_file(ontology, serialization, index_strategy) + open_ontology_from_file(py, ontology, serialization, index_strategy) } else { - open_ontology_from_string(ontology, serialization, index_strategy) + open_ontology_from_string(py, ontology, serialization, index_strategy) } } @@ -151,6 +175,7 @@ fn open_ontology(ontology: String, serialization: Option<&str>, index_strategy: fn pyhornedowl(py: Python, m: &Bound<'_, PyModule>) -> PyResult<()> { m.add_class::()?; m.add_class::()?; + m.add_class::()?; m.add_function(wrap_pyfunction!(open_ontology, m)?)?; m.add_function(wrap_pyfunction!(open_ontology_from_file, m)?)?; diff --git a/src/ontology.rs b/src/ontology.rs index cf6ebcc..3ba85b8 100644 --- a/src/ontology.rs +++ b/src/ontology.rs @@ -2,7 +2,7 @@ use std::collections::{BTreeSet, HashMap, HashSet}; use std::fs::File; use std::sync::Arc; -use curie::{Curie, PrefixMapping}; +use curie::Curie; use horned_owl::io::rdf::reader::RDFOntology; use horned_owl::io::ResourceType; use horned_owl::model::{ @@ -18,14 +18,15 @@ use horned_owl::ontology::iri_mapped::IRIMappedIndex; use horned_owl::ontology::set::{SetIndex, SetOntology}; use horned_owl::vocab::AnnotationBuiltIn; use pyo3::exceptions::PyValueError; -use pyo3::{pyclass, pyfunction, pymethods, PyObject, PyResult, Python, ToPyObject}; +use pyo3::{pyclass, pyfunction, pymethods, Bound, Py, PyObject, PyResult, Python, ToPyObject}; +use crate::prefix_mapping::PrefixMapping; use crate::{guess_serialization, model, to_py_err}; #[pyclass] #[derive(Eq, PartialEq, Ord, PartialOrd, Hash, Debug, Clone, Copy)] /// Values to indicate when to build the additional indexes. -/// +/// /// OnLoad: Create the additional indexes when the ontology is loaded /// OnQuery: Create the additional indexes only when they are needed /// Explicit: Only create the additional indexes when explicity requested @@ -61,7 +62,7 @@ pub struct PyIndexedOntology { pub component_index: Option>, pub set_index: SetIndex, //Need this for converting IRIs to IDs and for saving again afterwards - pub mapping: PrefixMapping, + pub mapping: Py, pub build: Build, pub index_strategy: IndexCreationStrategy, @@ -69,17 +70,18 @@ pub struct PyIndexedOntology { impl Default for PyIndexedOntology { fn default() -> Self { - PyIndexedOntology { + Python::with_gil(|py| PyIndexedOntology { labels_to_iris: Default::default(), classes_to_subclasses: Default::default(), classes_to_superclasses: Default::default(), iri_index: None, component_index: None, set_index: Default::default(), - mapping: Default::default(), + mapping: Py::new(py, PrefixMapping::default()) + .expect("Unable to create default prefix mapping"), build: Build::new_arc(), index_strategy: IndexCreationStrategy::OnQuery, - } + }) } } @@ -132,26 +134,6 @@ impl PyIndexedOntology { s } - /// add_default_prefix_names(self) -> None - /// - /// Adds the prefix for rdf, rdfs, xsd, and owl - pub fn add_default_prefix_names(&mut self) -> PyResult<()> { - self.mapping - .add_prefix("rdf", "http://www.w3.org/1999/02/22-rdf-syntax-ns#") - .map_err(to_py_err!("Error while adding predefined prefix 'rdf'"))?; - self.mapping - .add_prefix("rdfs", "http://www.w3.org/2000/01/rdf-schema#") - .map_err(to_py_err!("Error while adding predefined prefix 'rdfs'"))?; - self.mapping - .add_prefix("xsd", "http://www.w3.org/2001/XMLSchema#") - .map_err(to_py_err!("Error while adding predefined prefix 'xsd'"))?; - self.mapping - .add_prefix("owl", "http://www.w3.org/2002/07/owl#") - .map_err(to_py_err!("Error while adding predefined prefix 'owl'"))?; - - Ok(()) - } - /// get_id_for_iri(self, iri: str, iri_is_absolute: Optional[bool] = None) -> Optional[str] /// /// Gets the ID of term by it IRI. @@ -160,11 +142,13 @@ impl PyIndexedOntology { #[pyo3[signature = (iri, *, iri_is_absolute = None)]] pub fn get_id_for_iri( &mut self, + py: Python<'_>, iri: String, iri_is_absolute: Option, ) -> PyResult> { - let iri: String = self.iri(iri, iri_is_absolute)?.into(); - let res = self.mapping.shrink_iri(iri.as_str()); + let iri: String = self.iri(py, iri, iri_is_absolute)?.into(); + let mapping = self.mapping.borrow_mut(py); + let res = mapping.0.shrink_iri(iri.as_str()); if let Ok(curie) = res { Ok(Some(curie.to_string())) @@ -179,13 +163,14 @@ impl PyIndexedOntology { /// Gets the IRI of a term by its ID. /// /// If the term does not have an IRI, `None` is returned. - pub fn get_iri_for_id(&mut self, py: Python, id: String) -> PyResult { + pub fn get_iri_for_id(&mut self, py: Python<'_>, id: String) -> PyResult { let idparts: Vec<&str> = id.split(":").collect(); if idparts.len() == 2 { let curie = Curie::new(Some(idparts[0]), idparts[1]); - let res = self.mapping.expand_curie(&curie); + let mapping = self.mapping.borrow_mut(py); + let res = mapping.0.expand_curie(&curie); if let Ok(iri) = res { Ok(iri.to_string().to_object(py)) @@ -199,18 +184,13 @@ impl PyIndexedOntology { } } - /// add_prefix_mapping(self, iriprefix: str, mappedid: str) -> None - /// - /// Adds the prefix `iriprefix`. - pub fn add_prefix_mapping(&mut self, iriprefix: String, mappedid: String) -> PyResult<()> { - let result = self.mapping.add_prefix(&iriprefix, &mappedid); - let _ = result.map_err(to_py_err!("Error - prefix is invalid."))?; - - if iriprefix.is_empty() { - self.mapping.set_default(mappedid.as_str()); - } - - Ok(()) + /// prefix_mapping: PrefixMapping + /// + /// The prefix mapping + #[getter] + pub fn get_prefix_mapping<'py>(&self, py: Python<'py>) -> PyResult<&Bound<'py, PrefixMapping>> { + let mapping = self.mapping.bind(py); + Ok(mapping) } /// set_label(self, iri: str, label: str, *, absolute: Optional[bool] = None) -> None @@ -221,11 +201,12 @@ impl PyIndexedOntology { #[pyo3[signature = (iri, label, *, absolute = None)]] pub fn set_label( &mut self, + py: Python<'_>, iri: String, label: String, absolute: Option, ) -> PyResult<()> { - let iri: IRI = self.iri(iri, absolute)?.into(); + let iri: IRI = self.iri(py, iri, absolute)?.into(); let ax1: AnnotatedComponent = Component::AnnotationAssertion(AnnotationAssertion { subject: iri.clone().into(), @@ -287,8 +268,11 @@ impl PyIndexedOntology { as Box>> } else { Box::new((&self.set_index).into_iter().filter_map(|c| match c { - AnnotatedComponent {component: Component::AnnotationAssertion(a), ..} => Some(a), - _ => None + AnnotatedComponent { + component: Component::AnnotationAssertion(a), + .. + } => Some(a), + _ => None, })) }; @@ -300,7 +284,11 @@ impl PyIndexedOntology { ap, av: AnnotationValue::Literal(Literal::Simple { literal }), }, - } if *literal == label && AnnotationBuiltIn::Label.underlying().eq(&ap.0.to_string()) => Some(subj), + } if *literal == label + && AnnotationBuiltIn::Label.underlying().eq(&ap.0.to_string()) => + { + Some(subj) + } _ => None, }); @@ -333,10 +321,11 @@ impl PyIndexedOntology { #[pyo3[signature = (iri, iri_is_absolute = None)]] pub fn get_subclasses( &mut self, + py: Python<'_>, iri: String, iri_is_absolute: Option, ) -> PyResult> { - let iri: IRI = self.iri(iri, iri_is_absolute)?.into(); + let iri: IRI = self.iri(py, iri, iri_is_absolute)?.into(); let subclasses = self.classes_to_subclasses.get(&iri); if let Some(subclss) = subclasses { @@ -353,10 +342,11 @@ impl PyIndexedOntology { #[pyo3[signature = (iri, iri_is_absolute = None)]] pub fn get_superclasses( &mut self, + py: Python<'_>, iri: String, iri_is_absolute: Option, ) -> PyResult> { - let iri: IRI = self.iri(iri, iri_is_absolute)?.into(); + let iri: IRI = self.iri(py, iri, iri_is_absolute)?.into(); let superclasses = self.classes_to_superclasses.get(&iri); if let Some(superclss) = superclasses { @@ -420,12 +410,14 @@ impl PyIndexedOntology { #[pyo3[signature = (class_iri, ann_iri, *, class_iri_is_absolute = None, ann_iri_is_absolute = None)]] pub fn get_annotation( &mut self, + py: Python<'_>, class_iri: String, ann_iri: String, class_iri_is_absolute: Option, ann_iri_is_absolute: Option, ) -> PyResult> { self.get_annotations( + py, class_iri, ann_iri, class_iri_is_absolute, @@ -443,13 +435,14 @@ impl PyIndexedOntology { #[pyo3[signature = (class_iri, ann_iri, *, class_iri_is_absolute = None, ann_iri_is_absolute = None)]] pub fn get_annotations( &mut self, + py: Python<'_>, class_iri: String, ann_iri: String, class_iri_is_absolute: Option, ann_iri_is_absolute: Option, ) -> PyResult> { - let class_iri: IRI = self.iri(class_iri, class_iri_is_absolute)?.into(); - let ann_iri: IRI = self.iri(ann_iri, ann_iri_is_absolute)?.into(); + let class_iri: IRI = self.iri(py, class_iri, class_iri_is_absolute)?.into(); + let ann_iri: IRI = self.iri(py, ann_iri, ann_iri_is_absolute)?.into(); let components = if let Some(iri_index) = &self.iri_index { Box::new(iri_index.component_for_iri(&class_iri)) @@ -489,7 +482,12 @@ impl PyIndexedOntology { /// Saves the ontology to disk. If no serialization is given it is guessed by the file extension. /// Defaults to OWL/XML #[pyo3(signature = (file_name, serialization = None))] - pub fn save_to_file(&mut self, file_name: String, serialization: Option<&str>) -> PyResult<()> { + pub fn save_to_file( + &mut self, + py: Python<'_>, + file_name: String, + serialization: Option<&str>, + ) -> PyResult<()> { let serialization = guess_serialization(&file_name, serialization)?; let mut file = File::create(file_name)?; @@ -500,12 +498,14 @@ impl PyIndexedOntology { amo.insert(component.clone()); } + let mapping = self.mapping.borrow_mut(py); + let result = match serialization { ResourceType::OFN => { - horned_owl::io::ofn::writer::write(&mut file, &amo, Some(&self.mapping)) + horned_owl::io::ofn::writer::write(&mut file, &amo, Some(&mapping.0)) } ResourceType::OWX => { - horned_owl::io::owx::writer::write(&mut file, &amo, Some(&self.mapping)) + horned_owl::io::owx::writer::write(&mut file, &amo, Some(&mapping.0)) } ResourceType::RDF => horned_owl::io::rdf::writer::write(&mut file, &amo), }; @@ -519,10 +519,11 @@ impl PyIndexedOntology { #[pyo3[signature = (iri, *, iri_is_absolute = None)]] pub fn get_axioms_for_iri( &mut self, + py: Python<'_>, iri: String, iri_is_absolute: Option, ) -> PyResult> { - let iri: IRI = self.iri(iri, iri_is_absolute)?.into(); + let iri: IRI = self.iri(py, iri, iri_is_absolute)?.into(); let iri_index = if let Some(index) = &self.iri_index { Some(index) @@ -561,10 +562,11 @@ impl PyIndexedOntology { #[pyo3[signature = (iri, *, iri_is_absolute = None)]] pub fn get_components_for_iri( &mut self, + py: Python<'_>, iri: String, iri_is_absolute: Option, ) -> PyResult> { - let iri: IRI = self.iri(iri, iri_is_absolute)?.into(); + let iri: IRI = self.iri(py, iri, iri_is_absolute)?.into(); let iri_index = if let Some(index) = &self.iri_index { Some(index) @@ -681,12 +683,12 @@ impl PyIndexedOntology { /// If `absolute` is None it is guessed by the occurrence of `"://"` in the IRI whether the iri /// is absolute or not. #[pyo3[signature = (iri, *, absolute = true)]] - pub fn iri(&self, iri: String, absolute: Option) -> PyResult { + pub fn iri(&self, py: Python<'_>, iri: String, absolute: Option) -> PyResult { let absolute = absolute.unwrap_or_else(|| iri.contains("://")); let r = if absolute { model::IRI::new(iri, &self.build) } else { - self.curie(iri)? + self.curie(py, iri)? }; Ok(r) } @@ -696,9 +698,10 @@ impl PyIndexedOntology { /// Creates a new IRI from CURIE string. /// /// Use this method instead of `model.IRI.parse` if possible as it is more optimized using caches. - pub fn curie(&self, curie: String) -> PyResult { - let iri = self - .mapping + pub fn curie(&self, py: Python<'_>, curie: String) -> PyResult { + let mapping = self.mapping.borrow_mut(py); + let iri = mapping + .0 .expand_curie_string(&curie) .map_err(to_py_err!("Invalid curie"))?; Ok(model::IRI::new(iri, &self.build)) @@ -710,16 +713,26 @@ impl PyIndexedOntology { /// /// Uses the `iri` method to cache native IRI instances. #[pyo3[signature = (iri, *, absolute = None)]] - pub fn clazz(&self, iri: String, absolute: Option) -> PyResult { - Ok(model::Class(self.iri(iri, absolute)?)) + pub fn clazz( + &self, + py: Python<'_>, + iri: String, + absolute: Option, + ) -> PyResult { + Ok(model::Class(self.iri(py, iri, absolute)?)) } /// declare_class(self, iri: str, *, absolute: Optional[bool]=None) -> bool /// /// Convenience method to add a Declare(Class(iri)) axiom. #[pyo3[signature = (iri, *, absolute = None)]] - pub fn declare_class(&mut self, iri: String, absolute: Option) -> PyResult { - Ok(self.declare::>(self.clazz(iri, absolute)?.into())) + pub fn declare_class( + &mut self, + py: Python<'_>, + iri: String, + absolute: Option, + ) -> PyResult { + Ok(self.declare::>(self.clazz(py, iri, absolute)?.into())) } /// object_property(self, iri: str, *, absolute: Optional[bool]=None) -> model.ObjectProperty @@ -730,10 +743,11 @@ impl PyIndexedOntology { #[pyo3[signature = (iri, *, absolute = None)]] pub fn object_property( &self, + py: Python<'_>, iri: String, absolute: Option, ) -> PyResult { - Ok(model::ObjectProperty(self.iri(iri, absolute)?)) + Ok(model::ObjectProperty(self.iri(py, iri, absolute)?)) } /// declare_object_property(self, iri: str, *, absolute: Optional[bool]=None) -> bool @@ -742,11 +756,12 @@ impl PyIndexedOntology { #[pyo3[signature = (iri, *, absolute = None)]] pub fn declare_object_property( &mut self, + py: Python<'_>, iri: String, absolute: Option, ) -> PyResult { Ok(self.declare::>( - self.object_property(iri, absolute)?.into(), + self.object_property(py, iri, absolute)?.into(), )) } @@ -758,19 +773,25 @@ impl PyIndexedOntology { #[pyo3[signature = (iri, *, absolute = None)]] pub fn data_property( &self, + py: Python<'_>, iri: String, absolute: Option, ) -> PyResult { - Ok(model::DataProperty(self.iri(iri, absolute)?)) + Ok(model::DataProperty(self.iri(py, iri, absolute)?)) } /// declare_data_property(self, iri: str, *, absolute: Optional[bool]=None) -> bool /// /// Convenience method to add a Declare(DataProperty(iri)) axiom. #[pyo3[signature = (iri, *, absolute = None)]] - pub fn declare_data_property(&mut self, iri: String, absolute: Option) -> PyResult { + pub fn declare_data_property( + &mut self, + py: Python<'_>, + iri: String, + absolute: Option, + ) -> PyResult { Ok(self.declare::>( - self.data_property(iri, absolute)?.into(), + self.data_property(py, iri, absolute)?.into(), )) } @@ -782,10 +803,11 @@ impl PyIndexedOntology { #[pyo3[signature = (iri, *, absolute = None)]] pub fn annotation_property( &self, + py: Python<'_>, iri: String, absolute: Option, ) -> PyResult { - Ok(model::AnnotationProperty(self.iri(iri, absolute)?)) + Ok(model::AnnotationProperty(self.iri(py, iri, absolute)?)) } /// declare_annotation_property(self, iri: str, *, absolute: Optional[bool]=None) -> bool @@ -794,12 +816,13 @@ impl PyIndexedOntology { #[pyo3[signature = (iri, *, absolute = None)]] pub fn declare_annotation_property( &mut self, + py: Python<'_>, iri: String, absolute: Option, ) -> PyResult { Ok( self.declare::>( - self.annotation_property(iri, absolute)?.into(), + self.annotation_property(py, iri, absolute)?.into(), ), ) } @@ -812,19 +835,25 @@ impl PyIndexedOntology { #[pyo3[signature = (iri, *, absolute = None)]] pub fn named_individual( &self, + py: Python<'_>, iri: String, absolute: Option, ) -> PyResult { - Ok(model::NamedIndividual(self.iri(iri, absolute)?)) + Ok(model::NamedIndividual(self.iri(py, iri, absolute)?)) } /// declare_individual(self, iri: str, *, absolute: Optional[bool]=None) -> bool /// /// Convenience method to add a Declare(NamedIndividual(iri)) axiom. #[pyo3[signature = (iri, *, absolute = None)]] - pub fn declare_individual(&mut self, iri: String, absolute: Option) -> PyResult { + pub fn declare_individual( + &mut self, + py: Python<'_>, + iri: String, + absolute: Option, + ) -> PyResult { Ok(self.declare::>( - self.named_individual(iri, absolute)?.into(), + self.named_individual(py, iri, absolute)?.into(), )) } @@ -841,11 +870,12 @@ impl PyIndexedOntology { #[pyo3[signature = (parent_iri, *, iri_is_absolute = None)]] pub fn get_descendants( &self, + py: Python<'_>, parent_iri: String, iri_is_absolute: Option, ) -> PyResult> { let mut descendants = HashSet::new(); - let parent_iri: IRI = self.iri(parent_iri, iri_is_absolute)?.into(); + let parent_iri: IRI = self.iri(py, parent_iri, iri_is_absolute)?.into(); self.recurse_descendants(&parent_iri, &mut descendants); @@ -858,12 +888,13 @@ impl PyIndexedOntology { #[pyo3[signature = (child_iri, *, iri_is_absolute = None)]] pub fn get_ancestors( &self, + py: Python<'_>, child_iri: String, iri_is_absolute: Option, ) -> PyResult> { let mut ancestors = HashSet::new(); - let child_iri: IRI = self.iri(child_iri, iri_is_absolute)?.into(); + let child_iri: IRI = self.iri(py, child_iri, iri_is_absolute)?.into(); self.recurse_ancestors(&child_iri, &mut ancestors); @@ -871,7 +902,7 @@ impl PyIndexedOntology { } /// build_iri_index(self) -> None - /// + /// /// Builds an index by iri (IRIMappedIndex). pub fn build_iri_index(&mut self) -> () { if self.iri_index.is_some() { @@ -888,7 +919,7 @@ impl PyIndexedOntology { } /// component_index(self) -> None - /// + /// /// Builds an index by component kind (ComponentMappedIndex). pub fn build_component_index(&mut self) { if self.component_index.is_some() { @@ -905,7 +936,7 @@ impl PyIndexedOntology { } /// build_indexes(self) -> None - /// + /// /// Builds indexes to allow (a quicker) access to axioms and entities. pub fn build_indexes(&mut self) { match (&self.iri_index, &self.component_index) { @@ -1007,15 +1038,23 @@ impl PyIndexedOntology { /// /// Gets all direct and indirect subclasses of a class. #[pyfunction] -pub fn get_descendants(onto: &PyIndexedOntology, parent: String) -> PyResult> { - onto.get_descendants(parent, Some(true)) +pub fn get_descendants( + py: Python<'_>, + onto: &PyIndexedOntology, + parent: String, +) -> PyResult> { + onto.get_descendants(py, parent, Some(true)) } -/// @deprecated(please use `PyIndexedOntology.get_ancestors` instead) +/// @deprecated("please use `PyIndexedOntology.get_ancestors` instead") /// get_ancestors(onto: PyIndexedOntology, child: str) -> Set[str] /// /// Gets all direct and indirect super classes of a class. #[pyfunction] -pub fn get_ancestors(onto: &PyIndexedOntology, child: String) -> PyResult> { - onto.get_ancestors(child, Some(true)) +pub fn get_ancestors( + py: Python<'_>, + onto: &PyIndexedOntology, + child: String, +) -> PyResult> { + onto.get_ancestors(py, child, Some(true)) } diff --git a/src/prefix_mapping.rs b/src/prefix_mapping.rs new file mode 100644 index 0000000..d56e8fb --- /dev/null +++ b/src/prefix_mapping.rs @@ -0,0 +1,114 @@ +use pyo3::{ + exceptions::{PyKeyError, PyValueError}, + prelude::*, +}; + +use crate::to_py_err; + +#[pyclass(mapping)] +pub struct PrefixMapping(pub(crate) curie::PrefixMapping); + +impl Default for PrefixMapping { + fn default() -> Self { + Self(Default::default()) + } +} + +impl From for PrefixMapping { + fn from(value: curie::PrefixMapping) -> Self { + PrefixMapping(value) + } +} + +impl From for curie::PrefixMapping { + fn from(value: PrefixMapping) -> Self { + value.0 + } +} + +#[pymethods] +impl PrefixMapping { + pub fn __getitem__(&self, key: &str) -> PyResult { + self.0 + .expand_curie(&curie::Curie::new(Some(key), "")) + .map_err(|_| PyKeyError::new_err(format!("No such prefix '{}'", key))) + } + + pub fn __contains__(&self, key: &str) -> PyResult { + Ok(self.__getitem__(key).is_ok()) + } + + pub fn __setitem__(&mut self, key: &str, value: &str) -> PyResult<()> { + self.0 + .add_prefix(key, value) + .map_err(|e| PyKeyError::new_err(format!("Invalid prefix '{}': {:?}", key, e))) + } + + pub fn __delitem__(&mut self, key: &str) -> PyResult<()> { + Ok(self.0.remove_prefix(key)) + } + + pub fn __len__(&self) -> usize { + self.0.mappings().len() + } + + /// add_default_prefix_names(self) -> None + /// + /// Adds the prefix for rdf, rdfs, xsd, and owl + pub fn add_default_prefix_names(&mut self) -> PyResult<()> { + self.0 + .add_prefix("rdf", "http://www.w3.org/1999/02/22-rdf-syntax-ns#") + .map_err(to_py_err!("Error while adding predefined prefix 'rdf'"))?; + self.0 + .add_prefix("rdfs", "http://www.w3.org/2000/01/rdf-schema#") + .map_err(to_py_err!("Error while adding predefined prefix 'rdfs'"))?; + self.0 + .add_prefix("xsd", "http://www.w3.org/2001/XMLSchema#") + .map_err(to_py_err!("Error while adding predefined prefix 'xsd'"))?; + self.0 + .add_prefix("owl", "http://www.w3.org/2002/07/owl#") + .map_err(to_py_err!("Error while adding predefined prefix 'owl'"))?; + + Ok(()) + } + + /// add_prefix(self, iriprefix: str, mappedid: str) -> None + /// + /// Adds the prefix `iriprefix`. + pub fn add_prefix(&mut self, iriprefix: String, mappedid: String) -> PyResult<()> { + let result = self.0.add_prefix(&iriprefix, &mappedid); + result.map_err(to_py_err!("Error - prefix is invalid."))?; + + // if iriprefix.is_empty() { + // self.0.set_default(mappedid.as_str()); + // } + + Ok(()) + } + + /// remove_prefix(self, iriprefix: str) -> None + /// + /// Remove a prefix from the mapping. + pub fn remove_prefix(&mut self, prefix: &str) { + self.0.remove_prefix(prefix); + } + + /// expand_curie(self, curie: str) -> str + /// + /// Expands a curie. Throws a ValueError if the prefix is invalid or unknown + pub fn expand_curie(&self, curie: &str) -> PyResult { + self.0 + .expand_curie_string(curie) + .map_err(to_py_err!("Invalid or unknown prefix")) + } + + /// shring_iri(self, iri: str) -> str + /// + /// Shrinks an absolute IRI to a CURIE. Throws a ValueError on failure + pub fn shrink_iri(&self, iri: &str) -> PyResult { + self.0 + .shrink_iri(iri) + .map(|c| c.to_string()) + .map_err(|e| PyValueError::new_err(e)) + } +} diff --git a/test/test_base.py b/test/test_base.py index 3a67aa2..e2372a8 100644 --- a/test/test_base.py +++ b/test/test_base.py @@ -40,8 +40,8 @@ def simple_ontology_comps_annotated() -> list[AnnotatedComponent]: def simple_ontology() -> pyhornedowl.PyIndexedOntology: onto = pyhornedowl.PyIndexedOntology() - onto.add_default_prefix_names() - onto.add_prefix_mapping("", "https://example.com/") + onto.prefix_mapping.add_default_prefix_names() + onto.prefix_mapping.add_prefix("", "https://example.com/") for c in simple_ontology_comps(): onto.add_component(c) diff --git a/test/test_id.py b/test/test_id.py index ca6d2c5..b23af4d 100644 --- a/test/test_id.py +++ b/test/test_id.py @@ -6,7 +6,7 @@ class IdTestCase(unittest.TestCase): def test_id_from_iri_empty(self): o = simple_ontology() - o.add_prefix_mapping("ex", "https://example.com/") + o.prefix_mapping.add_prefix("ex", "https://example.com/") expected = "ex:A" actual = o.get_id_for_iri("https://example.com/A") @@ -16,34 +16,34 @@ def test_id_from_iri_empty(self): def test_id_from_absolute(self): o = simple_ontology() - o.add_prefix_mapping("ex", "https://example.com/") + o.prefix_mapping.add_prefix("ex", "https://example.com/") - expected = "A" + expected = ":A" actual = o.get_id_for_iri("https://example.com/A") self.assertEqual(expected, actual) def test_id_from_curie_empty_prefix(self): o = simple_ontology() - o.add_prefix_mapping("ex", "https://example.com/") + o.prefix_mapping.add_prefix("ex", "https://example.com/") - expected = "A" - actual = o.get_id_for_iri("A") + expected = ":A" + actual = o.get_id_for_iri(":A") self.assertEqual(expected, actual) def test_id_from_curie_defined_prefix(self): o = simple_ontology() - o.add_prefix_mapping("ex", "https://example.com/") + o.prefix_mapping.add_prefix("ex", "https://example.com/") - expected = "A" + expected = ":A" actual = o.get_id_for_iri("ex:A") self.assertEqual(expected, actual) def test_iri_from_id(self): o = simple_ontology() - o.add_prefix_mapping("ex", "https://example.com/") + o.prefix_mapping.add_prefix("ex", "https://example.com/") expected = "https://example.com/A" actual = o.get_iri_for_id("ex:A") diff --git a/test/test_io.py b/test/test_io.py index 397542e..d6384bf 100644 --- a/test/test_io.py +++ b/test/test_io.py @@ -93,6 +93,7 @@ def test_write_simple_guess_ext(self): with self.subTest(serialization=s): with NamedTemporaryFile(suffix=f".{s}") as f: original = simple_ontology() + original.prefix_mapping.remove_prefix("") original.save_to_file(f.name) actual = pyhornedowl.open_ontology_from_file(f.name, s) @@ -105,6 +106,9 @@ def test_write_simple_explicit(self): with NamedTemporaryFile(suffix=f".{s}.raw") as f: original = simple_ontology() original.save_to_file(f.name, s) + + if s == 'owx': + self.skipTest('Empty prefix is currently rendered weird in xml (xmlns:="https://example.com/")') actual = pyhornedowl.open_ontology_from_file(f.name, s) diff --git a/test/test_iri.py b/test/test_iri.py index 62072fa..4fe2cb8 100644 --- a/test/test_iri.py +++ b/test/test_iri.py @@ -9,7 +9,7 @@ class IRITestCase(unittest.TestCase): def test_absolute_iri(self): o = PyIndexedOntology() - o.add_prefix_mapping("example", "https://example.com/test#") + o.prefix_mapping.add_prefix("example", "https://example.com/test#") expected = "https://example.com/test#A" self.assertEqual(expected, str(o.iri(expected))) @@ -17,17 +17,16 @@ def test_absolute_iri(self): def test_expand_curie(self): o = PyIndexedOntology() - o.add_prefix_mapping("example", "https://example.com/test#") + o.prefix_mapping.add_prefix("example", "https://example.com/test#") self.assertEqual("https://example.com/test#A", str(o.curie("example:A"))) self.assertEqual("https://example.com/test#A", str(o.iri("example:A", absolute=False))) def test_expand_curie_empty_prefix(self): o = PyIndexedOntology() - o.add_prefix_mapping("", "https://example.com/test#") + o.prefix_mapping.add_prefix("", "https://example.com/test#") self.assertEqual("https://example.com/test#A", str(o.curie(":A"))) - self.assertEqual("https://example.com/test#A", str(o.curie("A"))) def test_expand_curie_unknown_prefix(self): o = PyIndexedOntology() @@ -37,7 +36,7 @@ def test_expand_curie_unknown_prefix(self): def test_iri_guess_curie(self): o = PyIndexedOntology() - o.add_prefix_mapping("ex", "https://example.com/test#") + o.prefix_mapping.add_prefix("ex", "https://example.com/test#") expected = "https://example.com/test#A" @@ -46,7 +45,7 @@ def test_iri_guess_curie(self): def test_iri_guess_absolute(self): o = PyIndexedOntology() - o.add_prefix_mapping("ex", "https://example.com/test#") + o.prefix_mapping.add_prefix("ex", "https://example.com/test#") expected = "https://example.com/test#A" @@ -79,7 +78,7 @@ def test_find_by_curie(self): AnnotationAssertion(o.iri("https://example.com/A"), Annotation(o.annotation_property(RDFS_LABEL), SimpleLiteral("ClassA"))) }} - actual = o.get_axioms_for_iri("A", iri_is_absolute=False) + actual = o.get_axioms_for_iri(":A", iri_is_absolute=False) self.assertSetEqual(expected, set(actual)) @@ -109,7 +108,7 @@ def test_find_by_curie_guess(self): AnnotationAssertion(o.iri("https://example.com/A"), Annotation(o.annotation_property(RDFS_LABEL), SimpleLiteral("ClassA"))) }} - actual = o.get_axioms_for_iri("A") + actual = o.get_axioms_for_iri(":A") self.assertSetEqual(expected, set(actual)) diff --git a/test/test_label.py b/test/test_label.py index 6ce04fd..f2f76c0 100644 --- a/test/test_label.py +++ b/test/test_label.py @@ -16,7 +16,7 @@ def test_label_for_iri_not_existing(self): o = simple_ontology() expected = None - actual = o.get_iri_for_label("C") + actual = o.get_iri_for_label("ClassZ") self.assertEqual(expected, actual) @@ -24,22 +24,22 @@ def test_update_label(self): o = simple_ontology() expected = "NewClassA" - o.set_label("A", expected) + o.set_label(":A", expected) - actual = o.get_annotation("A", "rdfs:label") + actual = o.get_annotation(":A", "rdfs:label") self.assertEqual(expected, actual) - all_labels = o.get_annotations("A", "rdfs:label") + all_labels = o.get_annotations(":A", "rdfs:label") self.assertEqual([expected], all_labels) def test_add_label(self): o = simple_ontology() expected = "ClassC" - o.set_label("C", expected) + o.set_label(":C", expected) - actual = o.get_annotation("C", "rdfs:label") + actual = o.get_annotation(":C", "rdfs:label") self.assertEqual(expected, actual) diff --git a/test/test_modify.py b/test/test_modify.py index a11e8e1..cc3cbd8 100644 --- a/test/test_modify.py +++ b/test/test_modify.py @@ -9,7 +9,7 @@ def test_add_collect_all_axioms(self): o = simple_ontology() axioms = set(o.get_axioms()) - axiom = EquivalentClasses([o.clazz("A"), o.clazz("B")]) + axiom = EquivalentClasses([o.clazz(":A"), o.clazz(":B")]) o.add_axiom(axiom) @@ -21,13 +21,13 @@ def test_add_collect_all_axioms(self): def test_add_collect_by_iri(self): o = simple_ontology() - axioms = set(o.get_axioms_for_iri("A")) - axiom = EquivalentClasses([o.clazz("A"), o.clazz("B")]) + axioms = set(o.get_axioms_for_iri(":A")) + axiom = EquivalentClasses([o.clazz(":A"), o.clazz(":B")]) o.add_axiom(axiom) expected = {*axioms, AnnotatedComponent(axiom, set())} - actual = set(o.get_axioms_for_iri("A")) + actual = set(o.get_axioms_for_iri(":A")) self.assertSetEqual(expected, actual) @@ -35,7 +35,7 @@ def test_remove_collect_all_axioms(self): o = simple_ontology() axioms = set(o.get_axioms()) - axiom = SubClassOf(o.clazz("A"), o.clazz("B")) + axiom = SubClassOf(o.clazz(":A"), o.clazz(":B")) o.remove_axiom(axiom) @@ -47,13 +47,13 @@ def test_remove_collect_all_axioms(self): def test_remove_collect_by_iri(self): o = simple_ontology() - axioms = set(o.get_axioms_for_iri("A")) - axiom = SubClassOf(o.clazz("A"), o.clazz("B")) + axioms = set(o.get_axioms_for_iri(":A")) + axiom = SubClassOf(o.clazz(":A"), o.clazz(":B")) o.remove_axiom(axiom) expected = axioms - {AnnotatedComponent(axiom, set())} - actual = set(o.get_axioms_for_iri("A")) + actual = set(o.get_axioms_for_iri(":A")) self.assertSetEqual(expected, actual) From 47946bebaf2b270bc8976732f9d590013342bc2a Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bj=C3=B6rn=20Gehrke?= <5106696+b-gehrke@users.noreply.github.com> Date: Fri, 28 Jun 2024 08:30:41 +0200 Subject: [PATCH 2/2] Added write_to_string method --- pyhornedowl/__init__.pyi | 12 +++++++ src/ontology.rs | 75 ++++++++++++++++++++++++++++------------ test/test_io.py | 15 ++++++-- 3 files changed, 77 insertions(+), 25 deletions(-) diff --git a/pyhornedowl/__init__.pyi b/pyhornedowl/__init__.pyi index ff42982..5ef9359 100644 --- a/pyhornedowl/__init__.pyi +++ b/pyhornedowl/__init__.pyi @@ -95,6 +95,18 @@ class PyIndexedOntology: """ ... + def save_to_bytes(self, serialization: typing.Literal['owl', 'rdf','ofn', 'owx']) -> bytes: + """ + Saves the ontology to a byte buffer. + """ + ... + + def save_to_string(self, serialization: typing.Literal['owl', 'rdf','ofn', 'owx']) -> str: + """ + Saves the ontology to a UTF8 string. + """ + ... + def save_to_file(self, file_name: str, serialization: Optional[typing.Literal['owl', 'rdf','ofn', 'owx']]=None) -> None: """ Saves the ontology to disk. If no serialization is given it is guessed by the file extension. diff --git a/src/ontology.rs b/src/ontology.rs index 3ba85b8..8e6c78c 100644 --- a/src/ontology.rs +++ b/src/ontology.rs @@ -1,5 +1,6 @@ use std::collections::{BTreeSet, HashMap, HashSet}; use std::fs::File; +use std::io::Write; use std::sync::Arc; use curie::Curie; @@ -18,10 +19,11 @@ use horned_owl::ontology::iri_mapped::IRIMappedIndex; use horned_owl::ontology::set::{SetIndex, SetOntology}; use horned_owl::vocab::AnnotationBuiltIn; use pyo3::exceptions::PyValueError; +use pyo3::types::PyBytes; use pyo3::{pyclass, pyfunction, pymethods, Bound, Py, PyObject, PyResult, Python, ToPyObject}; use crate::prefix_mapping::PrefixMapping; -use crate::{guess_serialization, model, to_py_err}; +use crate::{guess_serialization, model, parse_serialization, to_py_err}; #[pyclass] #[derive(Eq, PartialEq, Ord, PartialOrd, Hash, Debug, Clone, Copy)] @@ -185,7 +187,7 @@ impl PyIndexedOntology { } /// prefix_mapping: PrefixMapping - /// + /// /// The prefix mapping #[getter] pub fn get_prefix_mapping<'py>(&self, py: Python<'py>) -> PyResult<&Bound<'py, PrefixMapping>> { @@ -477,6 +479,25 @@ impl PyIndexedOntology { Ok(literal_values) } + /// save_to_string(self, serialization: typing.Literal['owl', 'rdf','ofn', 'owx']) -> str + /// + /// Saves the ontology to a UTF8 string. + pub fn save_to_string( + &mut self, + py: Python<'_>, + serialization: &str, + ) -> PyResult { + let serialization = parse_serialization(Some(serialization)).ok_or( + PyValueError::new_err(format!("Unknown serialization {}", serialization)), + )?; + + let mut writer = Vec::::new(); + + self.save_to_buf(py, &mut writer, serialization)?; + + String::from_utf8(writer).map_err(to_py_err!("Failed to save ontology to UTF-8")) + } + /// save_to_file(self, file_name: str, serialization: Optional[typing.Literal['owl', 'rdf','ofn', 'owx']]=None) -> None /// /// Saves the ontology to disk. If no serialization is given it is guessed by the file extension. @@ -489,28 +510,9 @@ impl PyIndexedOntology { serialization: Option<&str>, ) -> PyResult<()> { let serialization = guess_serialization(&file_name, serialization)?; - let mut file = File::create(file_name)?; - let mut amo: ArcComponentMappedOntology = ComponentMappedOntology::new_arc(); - - //Copy the components into an ComponentMappedOntology as that is what horned owl writes - for component in (&self.set_index).into_iter() { - amo.insert(component.clone()); - } - - let mapping = self.mapping.borrow_mut(py); - - let result = match serialization { - ResourceType::OFN => { - horned_owl::io::ofn::writer::write(&mut file, &amo, Some(&mapping.0)) - } - ResourceType::OWX => { - horned_owl::io::owx::writer::write(&mut file, &amo, Some(&mapping.0)) - } - ResourceType::RDF => horned_owl::io::rdf::writer::write(&mut file, &amo), - }; - result.map_err(to_py_err!("Problem saving the ontology to a file")) + self.save_to_buf(py, &mut file, serialization) } /// get_axioms_for_iri(self, iri: str, iri_is_absolute: Optional[bool] = None) -> List[model.AnnotatedComponent] @@ -1031,6 +1033,35 @@ impl PyIndexedOntology { pio } + + fn save_to_buf( + &mut self, + py: Python<'_>, + w: &mut W, + serialization: ResourceType, + ) -> PyResult<()> { + let mut file = w; + let mut amo: ArcComponentMappedOntology = ComponentMappedOntology::new_arc(); + + //Copy the components into an ComponentMappedOntology as that is what horned owl writes + for component in (&self.set_index).into_iter() { + amo.insert(component.clone()); + } + + let mapping = self.mapping.borrow_mut(py); + + let result = match serialization { + ResourceType::OFN => { + horned_owl::io::ofn::writer::write(&mut file, &amo, Some(&mapping.0)) + } + ResourceType::OWX => { + horned_owl::io::owx::writer::write(&mut file, &amo, Some(&mapping.0)) + } + ResourceType::RDF => horned_owl::io::rdf::writer::write(&mut file, &amo), + }; + + result.map_err(to_py_err!("Problem saving the ontology to a file")) + } } /// @deprecated("please use `PyIndexedOntology.get_descendants` instead") diff --git a/test/test_io.py b/test/test_io.py index d6384bf..a0b7d0c 100644 --- a/test/test_io.py +++ b/test/test_io.py @@ -105,14 +105,23 @@ def test_write_simple_explicit(self): with self.subTest(serialization=s): with NamedTemporaryFile(suffix=f".{s}.raw") as f: original = simple_ontology() + original.prefix_mapping.remove_prefix("") original.save_to_file(f.name, s) - - if s == 'owx': - self.skipTest('Empty prefix is currently rendered weird in xml (xmlns:="https://example.com/")') actual = pyhornedowl.open_ontology_from_file(f.name, s) self.assertOntologiesEqual(original, actual) + + def test_write_simple_to_string(self): + for s in SERIALIZATIONS: + with self.subTest(serialization=s): + original = simple_ontology() + original.prefix_mapping.remove_prefix("") + o = original.save_to_string(s) + + actual = pyhornedowl.open_ontology_from_string(o, s) + + self.assertOntologiesEqual(original, actual) if __name__ == '__main__':