From 499ef576aa597206e1b90dad80d65ff78aebf435 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bj=C3=B6rn=20Gehrke?= <5106696+b-gehrke@users.noreply.github.com> Date: Wed, 19 Jun 2024 19:48:52 +0200 Subject: [PATCH] Added docs --- docs/source/details.rst | 50 ++++++++++++ docs/source/index.rst | 3 + docs/source/installation.rst | 41 ++++++++++ docs/source/quickstart.rst | 42 ++++++++++ docs/source/usage.rst | 151 +++++++++++++++++++++++++++++------ 5 files changed, 262 insertions(+), 25 deletions(-) create mode 100644 docs/source/details.rst create mode 100644 docs/source/installation.rst create mode 100644 docs/source/quickstart.rst diff --git a/docs/source/details.rst b/docs/source/details.rst new file mode 100644 index 0000000..fb5911d --- /dev/null +++ b/docs/source/details.rst @@ -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 `_). 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 `_ 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`` 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. \ No newline at end of file diff --git a/docs/source/index.rst b/docs/source/index.rst index d9cf939..26a19f3 100644 --- a/docs/source/index.rst +++ b/docs/source/index.rst @@ -14,6 +14,9 @@ Contents .. toctree:: :maxdepth: 2 + quickstart + installation usage + details api diff --git a/docs/source/installation.rst b/docs/source/installation.rst new file mode 100644 index 0000000..d695cd0 --- /dev/null +++ b/docs/source/installation.rst @@ -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 + diff --git a/docs/source/quickstart.rst b/docs/source/quickstart.rst new file mode 100644 index 0000000..e9b17cd --- /dev/null +++ b/docs/source/quickstart.rst @@ -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("") + + # 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") \ No newline at end of file diff --git a/docs/source/usage.rst b/docs/source/usage.rst index ea68410..198e86b 100644 --- a/docs/source/usage.rst +++ b/docs/source/usage.rst @@ -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 ` 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("") - - # 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) \ No newline at end of file + 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 `. + +.. 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 ` 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 ` 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 `. Other prefixes can be added using the :func:`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)]))