A DLe parser and writer for the Owlready2. This is a library for working with the Web Ontology Language (OWL).
DLe (Description Logic, Extended) is a compact formal syntax for ontologies that is readable by both people and language models. It is standard Description Logic plus a small set of additions: annotations, comments, multi-role predicate restrictions, and comments.
addle reads a DLe document into an Owlready2 ontology, and writes an Owlready2
ontology back out as DLe.
import addle
onto = addle.load("model.dle")
print(onto.Animal.is_a)
print(addle.dumps(onto))Everything in Owlready2 still applies: SPARQL queries, reasoning with HermiT or Pellet, and saving to RDF/XML or N-Triples.
pip install addleaddle model.dle # print as DLe (canonically formatted)
addle model.dle model.owl # convert to RDF/XML
addle model.owl # convert RDF/XML back to DLe
addle model.dle -f nt # N-Triples to stdout
addle model.dle --no-header # omit the explanatory file header| Function | Purpose |
|---|---|
addle.loads(text, ...) |
parse DLe text into an ontology |
addle.load(path_or_file, ...) |
parse a DLe document from a path or file object |
addle.dumps(onto, ...) |
render an ontology as DLe text |
addle.dump(onto, path_or_file, ...) |
write an ontology as DLe |
All four accept warnings=[], which collects anything that could not be
represented faithfully rather than failing or discarding it silently:
warnings = []
onto = addle.loads(text, warnings=warnings)
for warning in warnings:
print(warning)loads and load take world= to build in a specific Owlready2 World,
onto= to add to an existing ontology, and base_iri= to override the base
IRI. A malformed document raises addle.DleSyntaxError, which reports every
diagnostic the parse produced with line and column.
The DLe wiki provides all the documentation for standard Description Logic (DL) and for the extensions provided by DLe.
The wiki includes:
- Syntax for standard DL
- Syntax for DL extensions
- Mappings between OWL and DL
- Standard representations of OWL in DL
- Theoretic Semantics for DL
| DLe | Owlready2 |
|---|---|
C ⊑ D, C ≡ D |
C.is_a, C.equivalent_to |
C ⊓ D, C ⊔ D, ¬C |
And, Or, Not |
∃r.C, ∀r.C |
Restriction(r, SOME/ONLY, …) |
≥n r.C, ≤n r.C, =n r.C |
Restriction(r, MIN/MAX/EXACTLY, n, C) |
∃r.Self |
Restriction(r, HAS_SELF, …) |
r⁻ |
Inverse(r) |
∃r.⊤ ⊑ C |
r.domain |
⊤ ⊑ ∀r.C |
r.range |
≤1 r.⊤ |
FunctionalProperty |
Trans(r), Sym(r), … |
TransitiveProperty, SymmetricProperty, … |
q ∘ r ⊑ s |
s.property_chain |
r ⊓ s ⊑ ⊥, Disj(r,s) |
AllDisjoint |
{a,b}, {"x","y"} |
OneOf |
xsd:integer[≥0 ⊓ <10] |
ConstrainedDatatype |
@label, @doc, @storage, @db |
rdfs:label, rdfs:comment, rdfs:seeAlso, rdfs:isDefinedBy |
@ann X p "v" |
an annotation assertion with property p |
Three DLe constructs have no OWL vocabulary, and are encoded exactly as the Java reference implementation encodes them so that documents can move between the two:
- Predicate definitions (
greaterThan(x,y) ≝ x > y) become anrdf:valueannotation on the predicate IRI, holding"greaterThan(x,y) → x > y". - Predicate restrictions (
∃a,b.greaterThan) become a named class in thedle:namespace whoserdfs:labelis the original expression. The IRI embeds ajava.lang.String.hashCodeof that expression, computed identically here, so both implementations mint the same IRI for the same expression. - Comments become
dle:commentannotations on the entity that follows them.
Because all three are ordinary annotations, they survive a trip through any OWL serialiser. Writing the reference document to RDF/XML with Owlready2 and reading it back reproduces the DLe byte for byte; that is a test, not an aspiration.
Reading and writing preserves content, not layout. Specifically:
- Output is canonically ordered — properties before classes, each group
sorted by name — so writing is deterministic and idempotent:
write(read(write(read(x)))) == write(read(x)), byte for byte. Owlready2 iterates entities in creation order, which for a parsed document is the order names are first mentioned, so preserving source order is not possible; sorting makes output reviewable in a diff instead. - Some axioms are rewritten to an equivalent spelling.
≤1 r.⊤comes back asFunc(r); a general concept equivalence is stored as two subsumptions and re-folded into≡on the way out. Both say the same thing. - Comments are preserved and reattached to their entity, except the leading comment block of a file, which is treated as a header describing the format rather than the ontology. This matches the Java implementation.
- OWL punning is not representable. If an IRI is used as both a property and
an individual —
∃statementRelationship.{studies}wherestudiesis also a property — Owlready2 maps one IRI to one entity and cannot hold both. addle reports this throughwarningsand reuses the existing entity. owl:hasKeyis written as raw RDF. Owlready2 has no model for key axioms, soC ⊑ key(a,b)is written directly to the quadstore. It round-trips through addle and through RDF, but no Owlready2 accessor exposes it.⊤ ⊑ Xis written as raw RDF onowl:Thing, for the same reason: Owlready2'sGeneralClassAxiomrequires a blank node on its left.- Datatype IRIs outside Owlready2's set are approximated.
xsd:doubleandxsd:floatboth map to Pythonfloat, which Owlready2 writes back asxsd:decimal. Anything unrecognised falls back toxsd:stringwith a warning. - The top data range has no Owlready2 spelling.
∃p.⊤on a data property is stored as the equivalent≥1 p, and written back as∃p.⊤.
The reference implementation is
io.github.quoll.owlapi:dlextended-parsers,
built on the OWL API. adDLe is a separate implementation, not a port: the OWL API
discovers parsers and storers through ServiceLoader, and Owlready2 has no
comparable extension point, so addle is a top-level library rather than a plugin.
The two projects share the language. Two files are copies of their counterparts in the Java repository, and must never diverge:
| File | Why |
|---|---|
grammar/DLESyntax.g4 |
defines the language both implementations accept |
tests/data/wildlife-reserve-test.dle |
the conformance corpus to test agreement |
They are copies rather than a submodule deliberately: a submodule is paid for on every clone, and this grammar changes about once a year. The cost of a copy is that it can drift silently, so drift is checked instead:
tools/grammar.py check # verify the grammar's recorded hash
tools/grammar.py check --against ../dle # diff both files against a DLe checkout
tools/grammar.py generate # regenerate the parser after a change
tools/grammar.py update # re-record the hashThe hash check runs as part of the test suite, so it fires on every local run and in CI. The comparison against the reference implementation runs weekly on GitHub Actions and opens an issue if the copies diverge — the drift that matters happens upstream, while nobody is working on addle, so it needs a trigger that isn't someone's attention.
The generated ANTLR parser is checked into src/addle/_antlr, so installing
addle does not need Java nor the ANTLR tool. Regeneration is only required when
the grammar itself changes.
python -m venv .venv && .venv/bin/pip install -e '.[dev]'
.venv/bin/python -m pytestApache 2.0, matching the DLe reference implementation. Owlready2 is LGPL-3.0 and is used as a separately-installed library, which the LGPL permits.