Skip to content

Commit

Permalink
Merge pull request #23 from ontology-tools/documentation
Browse files Browse the repository at this point in the history
Added documentation
  • Loading branch information
b-gehrke authored Jun 19, 2024
2 parents 454a251 + 499ef57 commit ebb7309
Show file tree
Hide file tree
Showing 5 changed files with 262 additions and 25 deletions.
50 changes: 50 additions & 0 deletions docs/source/details.rst
Original file line number Diff line number Diff line change
@@ -0,0 +1,50 @@
Details
=======

Py-Horned-OWL provides Python bindings via PyO3. This bridge presents some challenges.

Mapping Rust ADT enums to Python classes
----------------------------------------
While Horned-OWL C like structs can be directly translated to Python classes bridging Rust ADTs to Python is not fully supported by PyO3 yet (`https://github.com/PyO3/pyo3/issues/417 <https://github.com/PyO3/pyo3/issues/417>`_). Hence Py-Horned-OWL uses a custom design.


For each tuple structs, a C like struct with fields ``first`` and ``second`` (depending on their airity) is generated. They are then directly translated to Python classes.


For each enum type ``enum E ( V1(...), V2{...}, ... )`` struct types ``E(E_Inner), V1(...), V2{...}, ...`` and an internal enum type ``enum E_Inner(V1(V1), V2(V2), ...)`` are generated. The structs ``V1, V2, ...`` are translated to Python classes.
The struct ``E`` manually implements the ``FromPyObject`` and ``IntoPy`` traits to hide the inner enum. Most notably, ``E`` is **not** exposed to Python. Instead, Py-Horned-OWL exposes ``E`` a ``typing.Union`` consisting of all variants ``E = typing.Union[V1, V2, ...]``. Ideally ``E`` would be a Python class as well and ``V1, V2, ...`` would be subclasses of ``E``. Unfortunately, class hierarchies are not supported by PyO3 to a level where this would be easily possible. The current approach, however, still allows for type hints and even runtime instance checking (as ``isinstance(V1(...), E) == True``).


Wrapper types
-------------

Exposing Rust datatypes to Python via PyO3 requires implementing certrain traits ``PyClass`` or ``FromPyObjectBound`` (or to use their macros like ``#[pyclass]``). Due to Rusts `orphan rules <https://doc.rust-lang.org/reference/items/implementations.html#orphan-rules>`_ the traits cannot be directly implemented for the datatypes in Horned-OWL. Therefore, each Horned-OWL type is wrapped (new type idiom). For each type ``T`` the procedure would be the same:

#. Define the wrapper type ``T_W`` depending on the Rust data type
#. Conversion from ``hornedowl::model::T`` to ``T_W`` and vice versa
#. Add python methods e.g. for creating, string conversion, equality, and hash.

As the tasks are very repetitive, macros are defined. The main macro is ``wrapped`` which takes Rust struct and enum definitions as defined in Horned-OWL and produces the wrapper types and implementations. It accepts custom arguments to control the wrapped datatypes:

``transparent pub enum ...``
Only valid on enums of the form ``pub enum E{ V1(V1), V2(V2), ... }``. It prevents the creation of intermediate types for ``V1, V2, ...``.

``#[transparent] V``
Only valid on variants of the form ``V1(V1)``. It also prevents the creation of an intermediate type.

``#[suffixed] pub enum ...``
For an enum ``pub enum E{ V1(...), V2{...}}`` the ``suffixed`` argument creates structs (and python classes) by concatenating enum and variant name (e.g. ``SimpleLiteral``). For some datatypes this makes their intention clearer and avoids name conflicts (e.g. ``Language`` vs. ``LanguageLiteral``).


To ensure the same interface in the macros, the ``FromCompatible`` trait is introduced as a wrapper around the ``From`` trait. This allows to redefine the conversion from data types from the Rust standard library e.g. Box or Vec for the wrapper types in Py-Horned-OWL.

Similarly, newtypes are defined for ``String``, ``Vec``, ``Box``, and ``BTreeSet``.


Python documentation
--------------------
Unfortunately, the documentation of rust datatypes vanishes at compile time. Therefore, we cannot simply copy the documentation of Horned-OWL data types to the wrapped data types. But a helper script extracts the doc strings from Horned-OWL and provides them in a form of a macro. Additionally, Py-Horned-OWL datatypes and functions follow the convention to include their signature as the first line of their documentation.

Python stubs / type hints
-------------------------
Currently, PyO3 does not output python type hints. So, all of the asserted type information in Rust is lost during the bridging to Python. To counter it, Py-Horned-OWL datatypes implement the trait ``ToPyi`` which contains a ``pyi(module: Option<String>) -> String`` function which returns a python stub. A helper script ``gen_pyi.py`` then iterates over all members and generates python stub files. If no such method is defined, the script searches the first line of the ``__doc__`` field for a signature and uses it instead.
3 changes: 3 additions & 0 deletions docs/source/index.rst
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,9 @@ Contents
.. toctree::
:maxdepth: 2

quickstart
installation
usage
details
api

41 changes: 41 additions & 0 deletions docs/source/installation.rst
Original file line number Diff line number Diff line change
@@ -0,0 +1,41 @@
Installation
============

With pip
---------
The simplest and quickest way to install Py-Horned-OWL is via pip.

.. code-block:: console
(.venv) $ pip install py-horned-owl
From source
-----------
Clone the repository from github

.. code-block:: console
$ git checkout https://github.com/ontology-tools/py-horned-owl.git
$ cd py-horned-owl
It is recommended to use a virtual environment

.. code-block:: console
$ python3 -m virtualenv .venv
$ source .venv/bin/activate
Install the build tool maturin and use ``maturin develop`` to build and install the project in the current virtual environment, or ``maturin build`` to create wheels. Consult the matuin documentation for further details.

.. code-block:: console
(.venv) $ pip install maturin
(.venv) $ maturin develop
(.venv) $ maturin build
42 changes: 42 additions & 0 deletions docs/source/quickstart.rst
Original file line number Diff line number Diff line change
@@ -0,0 +1,42 @@
Quickstart
==========

Installation
------------

To use py-horned-owl, first install it using pip:

.. code-block:: console
(.venv) $ pip install py-horned-owl
Work with ontologies
--------------------

.. code-block:: python
import pyhornedowl
ontology = pyhornedowl.open_ontology("<path/to/ontology>")
# Get all axioms
axioms = ontology.get_axioms()
# Add a prefix
ontology.add_prefix_mapping(":", "https://example.com/test#")
# Construct an axiom
from pyhornedowl.model import *
axiom = SubClassOf(
o.clazz(':Child'),
ObjectSomeValuesFrom(
o.object_property(':has_parent'),
o.clazz(':Human')
)
)
# Add the axiom
ontology.add_axiom(axiom)
# Save the ontology
o.save_to_file("output.owx")
151 changes: 126 additions & 25 deletions docs/source/usage.rst
Original file line number Diff line number Diff line change
@@ -1,36 +1,137 @@
Usage
=====

Installation
------------

To use py-horned-owl, first install it using pip:
Open an existing ontology
-------------------------

.. code-block:: console
To open an ontology use the :func:`~pyhornedowl.open_ontology` function. It guesses the serialization of the ontology by the file extension or tries all parsers. Alternatively, specify the serialization format explicitely with the ``serialization`` option.

(.venv) $ pip install py-horned-owl
.. code-block:: python
import pyhornedowl
rdf_ontology = pyhornedowl.open_ontology("path/to/ontology.owl")
owx_ontology = pyhornedowl.open_ontology("path/to/ontology.owx")
ofn_ontology = pyhornedowl.open_ontology("path/to/ontology", serialization='ofn')
Work with ontologies
--------------------
Save an ontology
----------------

Use the :func:`PyIndexedOntology.save_to_file <pyhornedowl.PyIndexedOntology.save_to_file>` function to write the ontology to a file. Again, the serialization is guessed by the file extension and defaults to OWL/XML. Alternatively, specify the serialization format explicitely with the ``serialization`` option.

.. code-block:: python
import pyhornedowl
ontology = pyhornedowl.open_ontology("<path/to/ontology>")
# Get all axioms
axioms = ontology.get_axioms()
# Construct an axiom
from pyhornedowl.model import *
axiom = SubClassOf(
Class(IRI.parse(':Child')),
ObjectSomeValuesFrom(
ObjectProperty(IRI.parse(':has_parent')),
Class(IRI.parse(':Human'))
)
)
# Add the axiom
ontology.add_axiom(axiom)
ontology = pyhornedowl.open_ontology("path/to/ontology.owl")
ontology.save_to_file("path/to/ontology.owl")
ontology.save_to_file("path/to/ontology.owx")
ontology.save_to_file("path/to/ontology", serialization='ofn')
IRIs and CURIEs
--------------------------
The preferred way to create IRIs is through an ontology instance as it enables Horned-OWLs caching mechanism. Alternatively, they can be created by hand using :func:`IRI.parse <pyhornedowl.model.IRI.parse>`.

.. code-block:: python
import pyhornedowl
from pyhornedowl.model import IRI
ontology = pyhornedowl.open_ontology("path/to/ontology.owl")
i1 = ontology.iri("https://example.com/test")
i2 = IRI.parse("https://example.com/test")
assert i1 == i2
The :func:`PyIndexedOntology.iri <pyhornedowl.PyIndexedOntology.iri>` function guesses if you passed it an absolute IRI or a CURIE based on the existence of ``://`` in the value. This is also true for all other convenience functions accepting IRIs as an argument. You can explicitely specify if the value is an absolute IRI or a CURIE by using the optional parameter ``absolute``.

An exception to this is the the function :func:`PyIndexedOntology.curie <pyhornedowl.PyIndexedOntology.curie>` which only accepts CURIEs.

.. note::
To create a curie the prefix must be defined.



.. note::
Similar to the OWL Manchester Syntax, for the empty prefix the colon can be omitted.

.. code-block:: python
import pyhornedowl
ontology = pyhornedowl.open_ontology("path/to/ontology.owl")
ontology.add_prefix_mapping("", "https://example.com/")
i1 = ontology.iri("https://example.com/test/A")
i2 = ontology.iri(":A")
i3 = ontology.iri("A")
assert i1 == i2 == i3
Prefixes
--------

By default, no prefixes are defined. The standard prefixes for ``rdf``, ``rdfs``, ``xsd``, and ``owl`` can be added via the :func:`PyIndexedOntology.add_default_prefix_names <pyhornedowl.PyIndexedOntology.add_default_prefix_names>`. Other prefixes can be added using the :func:`PyIndexedOntology.add_prefix_mapping <pyhornedowl.PyIndexedOntology.add_prefix_mapping>` method.

.. code-block:: python
import pyhornedowl
ontology = pyhornedowl.open_ontology("path/to/ontology.owl")
ontology.add_default_prefix_names()
ontology.add_prefix_mapping("ex", "https://example.com/")
Create entities
---------------
Classes, Individuals, Data- and Objectproperties can be created using convenience methods on an ontology.

.. code-block:: python
import pyhornedowl
o = pyhornedowl.open_ontology("path/to/ontology.owl")
o.add_prefix_mapping("", "https://example.com/")
c = o.clazz("A")
op = o.object_property("op")
dp = o.data_property("dp")
ap = o.annotation_property("ap")
i = o.named_individual("I")
n = o.anonymous_individual("n")
Write class expressions
-----------------------
.. warning::
Experimental feature! Only available on latest development branch!

Instead of writing class expressions as nested constructor calls, some expressions can be expressed using operators.

.. code-block:: python
import pyhornedowl
from pyhornedowl.model import *
o = pyhornedowl.PyIndexedOntology()
o.add_prefix_mapping("", "https://example.com/")
A = o.clazz("A")
B = o.clazz("B")
C = o.clazz("C")
r = o.object_property("r")
assert A & B == ObjectIntersectionOf([A, B])
assert A | B == ObjectUnionOf([A, B])
assert ~A == ObjectComplementOf(A)
assert ~r == InverseObjectProperty(r)
assert r.some(A) == ObjectSomeValuesFrom(r, A)
assert r.only(A) == ObjectAllValuesFrom(r, A)
assert r.some(A & B | (~r).only(C)) == ObjectSomeValuesFrom(r, ObjectUnionOf([ObjectIntersectionOf([A, B]), ObjectAllValuesFrom(InverseObjectProperty(r), C)]))

0 comments on commit ebb7309

Please sign in to comment.