Python implementation of the Eyeling Notation3 reasoner
Project description
pyling
A Python port of the Eyeling Notation3 reasoner.
RDF/Turtle/TriG compatibility through rdflib, RDF 1.2 surface-syntax checks, and RDF Message Log parsing/streaming.
The Eyeling repositories remains the main implementation.
Requirements
- Python 3.10 or newer
piprdflibis installed automatically frompyproject.tomlpytestis needed only for the test suite
Install in your project
After pyling is published to PyPI as pyling-n3, install it with Python's
standard package installer:
python -m pip install pyling-n3
With project-oriented package managers:
uv add pyling-n3
poetry add pyling-n3
pdm add pyling-n3
The distribution package is named pyling-n3; the Python import package and
CLI command remain pyling.
Until the package is published on PyPI, install directly from GitHub:
python -m pip install "pyling-n3 @ git+https://github.com/eyereasoner/pyling.git"
uv add "pyling-n3 @ git+https://github.com/eyereasoner/pyling.git"
Install from a checkout
cd pyling
python -m venv .venv
. .venv/bin/activate # Windows: .venv\Scripts\activate
python -m pip install --upgrade pip
python -m pip install -e ".[test]"
Verify the install:
pyling --help
python -m pytest -q
Command-line usage
Run a simple N3 rule program from standard input:
cat > example.n3 <<'EOF'
@prefix : <http://example.org/> .
:Socrates a :Man .
{ ?x a :Man } => { ?x a :Mortal } .
EOF
pyling example.n3
Expected derived output:
@prefix : <http://example.org/> .
:Socrates a :Mortal .
Include explicit input facts in the rendered closure:
pyling --include-input-facts example.n3
Read multiple sources, such as facts plus rules:
pyling facts.n3 rules.n3
Enable RDF/Turtle/TriG/RDF Message compatibility mode:
pyling --rdf data.trig rules.n3
Force a line format:
pyling --rdf --input-format nt data.nt
pyling --rdf --input-format nquads data.nq
Python API
from pyling import reason, reason_stream, run_async
program = """
@prefix : <http://example.org/> .
:a :p :b .
{ ?s :p ?o } => { ?s :q ?o } .
"""
print(reason(program))
result = reason_stream(program, include_input_facts_in_closure=True)
print(result.closure_n3)
print(result.derived)
Multi-source input mirrors Eyeling’s source-list style:
from pyling import reason
out = reason({
"sources": [
"@prefix : <http://example.org/> .\n:Socrates a :Man .\n",
"@prefix : <http://example.org/> .\n{ ?x a :Man } => { ?x a :Mortal } .\n",
]
})
The main exported term classes are Iri, Literal, Var, Blank, ListTerm, GraphTerm, Triple, Rule, and PrefixEnv.
Notebook examples
Publishable notebooks live in docs/notebooks/:
01-rdflib-reasoning.ipynbshows RDFLib graph input and RDFLib graph output.02-owl-style-rules.ipynbshows runtime-loaded OWL-style N3 rules.03-neuro-symbolic-validation.ipynbshows symbolic validation over facts extracted by a neural model.
Run them locally with:
python -m pip install -e ".[docs]"
jupyter lab docs/notebooks
GitHub Actions executes the notebooks and converts them to HTML in the
Notebook docs workflow. Pull requests upload the generated notebook-site
artifact; pushes to main also publish the same site/ output through GitHub
Pages when Pages is enabled for GitHub Actions.
RDF and RDF 1.2 compatibility
RDF mode is selected with --rdf on the CLI or {"rdf": True} in the API. It routes ordinary RDF syntax through rdflib instead of the N3 rule parser:
from pyling import reason
rdf = """
PREFIX : <http://example.org/>
:a :p :b .
"""
print(reason(rdf, rdf=True, include_input_facts_in_closure=True))
RDFLib graphs can also be passed directly:
from rdflib import Graph, Namespace
from pyling import reason_graph
ex = Namespace("http://example.org/")
g = Graph()
g.bind("", ex)
g.add((ex.a, ex.p, ex.b))
closure = reason_graph(g, include_input_facts_in_closure=True)
print(closure.serialize(format="turtle"))
Supported compatibility inputs include:
- Turtle /
.ttl - TriG /
.trig - N-Triples /
.nt - N-Quads /
.nq - uppercase
PREFIX,BASE, and RDF 1.2VERSIONsurface forms - simple RDF 1.2 annotation syntax, preserving the asserted triple
- RDF Message Logs using
VERSION "1.2-messages"andMESSAGE
The RDF 1.2 layer includes strict surface checks for common negative conformance cases such as surrogate numeric escapes, invalid RDF 1.2 language tags in line syntaxes, relative IRIREFs in N-Triples/N-Quads, and annotation syntax in line syntaxes.
Run the bundled offline RDF 1.2 smoke suite:
python tools/run_rdf12_w3c.py
Run the W3C RDF 1.2 syntax compliance manifests (requires Node.js and network access on the first run):
npm ci
npm run spec
The W3C runner caches downloaded manifests and fixtures in
.rdf-test-suite-cache/. All RDF 1.2 syntax cases in the configured N-Triples,
N-Quads, Turtle, and TriG manifests are enabled.
Performance comparisons
The comparison harness in tools/compare_reasoners.py benchmarks pyling and
optional FuXi installations over shared N3 fixtures. FuXi is not installed in
the main project environment. When the fuxi reasoner is selected for a
benchmark run, the harness lazily creates .cache/fuxi-venv with Python 3.13
and installs the fuxi package there. The default benchmark suite discovers
selected .n3 files from this repo's examples/ directory. --list never
installs dependencies.
npm run perf -- --list
npm run perf -- --case=socrates --reasoner=pyling --iterations=5 --warmup=2
npm run perf -- --case=socrates --reasoner=pyling,fuxi --json
npm run perf -- --csv > perf-results.csv
OWL 2 RL support can be benchmarked with the MobiBench OWL2RL archive. pyling
loads the RDFJS inference engine's OWL2RL N3 rules from
https://raw.githubusercontent.com/pietercolpaert/rdfjs-inference-engine/refs/heads/main/rules/owl2rl/owl2rl-eyeling.n3.
For the same MobiBench cases, FuXi uses its built-in OWL/DLP setup instead of
that Eyeling-specific N3 ruleset. This keeps each reasoner on its intended OWL
path: pyling exercises runtime-loaded N3 rules, while FuXi exercises the OWL
rule generation shipped with FuXi.
npm run perf -- --suite=owl-mobibench --mobibench-limit=5 --reasoner=pyling
npm run perf:mobibench
Latest local MobiBench OWL2RL run, measured with one iteration and no warmup on 2026-07-22:
| Reasoner | Cases | Failures | Median ms/case | Cases deriving facts | Total derived facts |
|---|---|---|---|---|---|
| pyling | 273 | 0 | 85.88 | 273 | 33,402 |
| FuXi | 273 | 0 | 95.93 | 43 | 221 |
Interpret these numbers as a profile of each configured reasoning path, not as
a strict semantic equivalence result. pyling materializes with the runtime-loaded
OWL2RL N3 rules, which include broad datatype and RDF-based entailment rules.
FuXi runs its built-in OWL/DLP translation, which is fast and useful for many
structural OWL cases but derives no facts for many datatype-heavy MobiBench
cases. For example, rdfbased-sem-rdfs-subclass-trans derives facts in both
engines, while the early rdfbased-dat-* cases mostly derive only in pyling.
The GitHub Actions performance workflow runs both pyling and fuxi for the
examples suite only. Full MobiBench is intentionally local-only through
npm run perf:mobibench, because it is larger and less suitable for normal pull
request feedback. Both paths write JSON, CSV, and Markdown reports to
performance-reports.
The slowest pyling cases in the latest completed full run were concentrated in a small set of RDF-based OWL patterns:
| Case | pyling median ms | Derived facts |
|---|---|---|
mobibench-rdfbased-xtr-reflection-subclasses |
133,292.61 | 290 |
mobibench-rdfbased-sem-bool-intersection-inst-comp |
31,885.76 | 105 |
mobibench-rdfbased-sem-bool-intersection-inst-expr |
31,301.35 | 106 |
mobibench-rdfbased-sem-bool-intersection-term |
30,417.15 | 104 |
mobibench-rdfbased-sem-key-def |
3,042.21 | 116 |
An interrupted full rerun stopped inside pyling.engine.Engine.solve, after
recursive goal solving had already entered a deep multi-premise OWL rule. That
matches the profile from the completed report: pyling is not uniformly slow,
but the current generic N3 fixpoint solver can spend disproportionate time on
rules with broad joins over RDF lists, subclass reflection, intersections, and
keys. The next performance work should focus on predicate indexing for rule
bodies, join ordering based on bound variables and candidate counts, and
deduplication of equivalent substitutions before recursive goal expansion.
The examples suite is useful for implementation regressions. After the
memoized Fibonacci fix, pyling completes examples/fibonacci.n3 and derives
the expected query formula containing F(0), F(1), F(10), F(100),
F(1000), and F(10000). FuXi's generic N3 rule loader does not currently run
that file because it rejects the literal-subject rule shape used by the example.
Disable lazy installation, or supply a dedicated Python executable or checkout path:
npm run perf -- --reasoner=fuxi --no-install-fuxi
FUXI_PYTHON=/path/to/venv/bin/python npm run perf -- --reasoner=fuxi
FUXI_PYTHONPATH=/path/to/FuXi-reincarnate/lib npm run perf -- --reasoner=fuxi
FuXi currently requires Python 3.13 or newer. If your system python3 is older,
install a 3.13 interpreter and let the benchmark harness create
.cache/fuxi-venv from it. Two practical options:
# uv-managed Python, no system Python upgrade needed
uv python install 3.13
FUXI_PYTHON="$(uv python find 3.13)" npm run perf -- --reasoner=fuxi --case=socrates
# pyenv-managed Python
pyenv install 3.13
FUXI_PYTHON="$(pyenv prefix 3.13)/bin/python" npm run perf -- --reasoner=fuxi --case=socrates
After Python 3.13 is available, npm run perf -- --reasoner=fuxi ... installs
the fuxi package lazily into .cache/fuxi-venv and reuses that environment on
later benchmark runs.
Additional fixtures can be added without editing the script:
npm run perf -- --fixture=my-case=../eyeling/examples/deep-taxonomy-100.n3
RDF Message Logs
Whole-log replay:
cat > messages.trig <<'EOF'
VERSION "1.2-messages"
PREFIX : <http://example.org/>
:a :value 21 .
MESSAGE
# Empty heartbeat message.
MESSAGE
:b :value 22 .
EOF
pyling --rdf --include-input-facts messages.trig
Streaming replay, one message envelope at a time:
pyling --rdf --stream-messages rules.n3 messages.trig
Python streaming API:
from pyling import reason_message_stream
for result in reason_message_stream({"sources": [rules_n3, messages_trig]}, {"rdf": True}):
print(result.closure_n3)
The replay vocabulary uses:
@prefix eymsg: <https://eyereasoner.github.io/eyeling/vocab/message#> .
It materializes stream/envelope metadata, eymsg:orderedEnvelopes, eymsg:messageCount, eymsg:payloadKind, and payload formula links through log:nameOf. Rules can inspect each payload graph with log:includes.
Built-ins
The package includes the built-in registry API:
from pyling import register_builtin, unregister_builtin, list_builtin_iris
Built-in coverage includes common predicates in these namespaces:
math:numeric comparison and arithmetic, plus common trig functionsstring:contains/matches/replace/format/length/comparison helperslist:first/rest/member/append/reverse/sort and related helperslog:includes/notIncludes/semantics/conjunction/skolem/uri and equality helpersdt:datatype inspection, validation, value comparison, canonicalizationcrypto:md5/sha/sha256/sha512time:year/month/day/hour/minute/second/timeZone/localTime
Registering a custom built-in:
from pyling import Literal, XSD_NS, register_builtin
def hello(ctx):
return [ctx.unify_term(ctx.goal.o, Literal("world", XSD_NS + "string"), ctx.subst)]
register_builtin("http://example.org/custom#hello", hello)
Stores
The synchronous API uses in-memory reasoning. run_async(..., {"store": ...}) can persist explicit and inferred triples through the included JSON-backed persistent store.
import asyncio
from pyling import run_async
async def main():
result = await run_async(program, {"store": {"name": "demo", "path": ".eyeling-store", "clear": True}})
await result.store.close()
asyncio.run(main())
Tests
Run the package tests:
python -m pytest -q
Run the offline RDF 1.2 compatibility smoke suite:
python tools/run_rdf12_w3c.py
Run the W3C RDF 1.2 compliance tests:
npm ci
npm run spec
Run the external phochste/notation3tests suite when you have network access or an existing checkout:
# Existing checkout
python tools/run_notation3tests.py /path/to/notation3tests
# Or let the runner clone and install the public suite at https://codeberg.org/phochste/notation3tests
python tools/run_notation3tests.py --clone
The runner defaults to NETWORKING=0, because this native port does not
dereference Web resources. It removes stale .out files for the 19 skipped
network fixtures so the reported score covers only tests actually executed.
You can also have pytest run the external suite by setting:
NOTATION3TESTS_DIR=/path/to/notation3tests python -m pytest -q
Packaging and release
The package metadata lives in pyproject.toml; releases are tracked in
CHANGELOG.md. PyPI is the Python package index, pip is the default installer
users will normally run, and build plus twine are the local maintainer tools
used to create and upload distributions. The Package build GitHub Actions
workflow builds the wheel and sdist, runs twine check, and uploads the
generated distributions as an artifact.
Build and inspect a release locally:
python -m pip install -e ".[build,test]"
python -m pytest -q
python -m build
twine check dist/pyling_n3-*
Publish first to TestPyPI:
twine upload --repository testpypi dist/pyling_n3-*
python -m pip install \
--index-url https://test.pypi.org/simple/ \
--extra-index-url https://pypi.org/simple/ \
pyling-n3
If that install works in a fresh environment, publish the same checked distributions to PyPI:
twine upload dist/pyling_n3-*
python -m pip install pyling-n3
For GitHub Actions releases, prefer PyPI Trusted Publishing over storing a
long-lived API token in repository secrets. Configure a PyPI trusted publisher
for the eyereasoner/pyling repository and the release workflow file, then use
pypa/gh-action-pypi-publish with id-token: write permissions.
Release checklist:
- Update
CHANGELOG.mdand move the relevant entries fromUnreleasedto the new version. - Update
versioninpyproject.toml. - Run tests, conformance checks, notebook build, and package build locally or in GitHub Actions.
- Confirm the published package name, ownership, license metadata, and long README rendering on TestPyPI.
- Create a signed git tag, for example
v0.1.0. - Upload to TestPyPI and install from TestPyPI in a fresh environment.
- Upload the same checked distributions to PyPI.
- Create a GitHub release from the tag and include the changelog notes.
Development notes
rdflibis used only for RDF/Turtle/TriG/N-Triples/N-Quads parsing. N3 rules and formulas are parsed by the local parser.- RDF Message Logs are split and replayed before reasoning so message boundaries, empty heartbeat messages, and per-message blank-node scope are preserved.
Project details
Download files
Download the file for your platform. If you're not sure which to choose, learn more about installing packages.
Source Distribution
Built Distribution
Filter files by name, interpreter, ABI, and platform.
If you're not sure about the file name format, learn more about wheel file names.
Copy a direct link to the current filters
File details
Details for the file pyling_n3-0.1.0.tar.gz.
File metadata
- Download URL: pyling_n3-0.1.0.tar.gz
- Upload date:
- Size: 64.1 kB
- Tags: Source
- Uploaded using Trusted Publishing? Yes
- Uploaded via: twine/6.1.0 CPython/3.13.14
File hashes
| Algorithm | Hash digest | |
|---|---|---|
| SHA256 |
76f574f827c59071e359eb59614c8f7b777986143ee1511c689e7fafce7250d2
|
|
| MD5 |
092a2f7ee90668259714f678609c90af
|
|
| BLAKE2b-256 |
704e58c5d4d1c7f954234fd25ec4a3c0c8a195f74b39d4544b874f0d28496621
|
Provenance
The following attestation bundles were made for pyling_n3-0.1.0.tar.gz:
Publisher:
package.yml on eyereasoner/pyling
-
Statement:
-
Statement type:
https://in-toto.io/Statement/v1 -
Predicate type:
https://docs.pypi.org/attestations/publish/v1 -
Subject name:
pyling_n3-0.1.0.tar.gz -
Subject digest:
76f574f827c59071e359eb59614c8f7b777986143ee1511c689e7fafce7250d2 - Sigstore transparency entry: 2224222145
- Sigstore integration time:
-
Permalink:
eyereasoner/pyling@e5affe707ee0773466a80d6313cffe0fb6b9f5e3 -
Branch / Tag:
refs/tags/0.1.0 - Owner: https://github.com/eyereasoner
-
Access:
public
-
Token Issuer:
https://token.actions.githubusercontent.com -
Runner Environment:
github-hosted -
Publication workflow:
package.yml@e5affe707ee0773466a80d6313cffe0fb6b9f5e3 -
Trigger Event:
release
-
Statement type:
File details
Details for the file pyling_n3-0.1.0-py3-none-any.whl.
File metadata
- Download URL: pyling_n3-0.1.0-py3-none-any.whl
- Upload date:
- Size: 55.2 kB
- Tags: Python 3
- Uploaded using Trusted Publishing? Yes
- Uploaded via: twine/6.1.0 CPython/3.13.14
File hashes
| Algorithm | Hash digest | |
|---|---|---|
| SHA256 |
a5b0a0f8be806e4d6c12f2bb12f251a2a4b75381a6a3a045af4fe9a9f082df57
|
|
| MD5 |
8631dd2eb0eb705b85c8c9d735a79d23
|
|
| BLAKE2b-256 |
efa23aae52b72fb7a31e32963da90243f25971152e9b9837da8b4328d95c6115
|
Provenance
The following attestation bundles were made for pyling_n3-0.1.0-py3-none-any.whl:
Publisher:
package.yml on eyereasoner/pyling
-
Statement:
-
Statement type:
https://in-toto.io/Statement/v1 -
Predicate type:
https://docs.pypi.org/attestations/publish/v1 -
Subject name:
pyling_n3-0.1.0-py3-none-any.whl -
Subject digest:
a5b0a0f8be806e4d6c12f2bb12f251a2a4b75381a6a3a045af4fe9a9f082df57 - Sigstore transparency entry: 2224222659
- Sigstore integration time:
-
Permalink:
eyereasoner/pyling@e5affe707ee0773466a80d6313cffe0fb6b9f5e3 -
Branch / Tag:
refs/tags/0.1.0 - Owner: https://github.com/eyereasoner
-
Access:
public
-
Token Issuer:
https://token.actions.githubusercontent.com -
Runner Environment:
github-hosted -
Publication workflow:
package.yml@e5affe707ee0773466a80d6313cffe0fb6b9f5e3 -
Trigger Event:
release
-
Statement type: