Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Added documentation #23

Merged
merged 1 commit into from
Jun 19, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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)]))
Loading