mirror of
https://github.com/semantica-agi/semantica.git
synced 2026-08-29 04:26:20 +00:00
Merge pull request #1156 from 13g4d0/fix/ontology-ingestor-named-graph
Read JSON-LD named graphs in OntologyIngestor (#1129)
This commit is contained in:
@@ -40,7 +40,7 @@ from pathlib import Path
|
||||
from typing import Any, Dict, List, Optional, Union
|
||||
|
||||
import rdflib
|
||||
from rdflib import RDF, RDFS, OWL, Graph
|
||||
from rdflib import RDF, RDFS, OWL, Dataset, Graph
|
||||
|
||||
from ..utils.exceptions import ProcessingError, ValidationError
|
||||
from ..utils.logging import get_logger
|
||||
@@ -106,7 +106,16 @@ class OntologyIngestor:
|
||||
raise ValidationError(f"File not found: {file_path}")
|
||||
|
||||
self.progress.update_tracking(tracking_id, message="Parsing RDF graph...")
|
||||
g = Graph()
|
||||
# `Dataset`, not `Graph`: a JSON-LD document with a top-level `@id` *and*
|
||||
# `@graph` places its terms in a NAMED graph. `Graph.parse()` loads only the
|
||||
# default graph and discards the rest without an error, so every class and
|
||||
# property in such a document was dropped while the load reported success.
|
||||
# Same migration #757 made for JenaStore; the ingest path was not covered by it.
|
||||
# `default_union=True` makes the Dataset itself present triples from every
|
||||
# graph as one merged view (it is an rdflib.Graph subclass, so it satisfies
|
||||
# _convert_to_dict()'s Graph-typed contract directly) instead of copying every
|
||||
# quad into a second in-memory Graph.
|
||||
ds = Dataset(default_union=True)
|
||||
|
||||
# Use provided format or let rdflib guess based on extension
|
||||
parse_kwargs = kwargs.copy()
|
||||
@@ -114,7 +123,7 @@ class OntologyIngestor:
|
||||
parse_kwargs['format'] = format
|
||||
|
||||
try:
|
||||
g.parse(file_path, **parse_kwargs)
|
||||
ds.parse(file_path, **parse_kwargs)
|
||||
except Exception as e:
|
||||
# Fallback: try to guess format from extension if not provided and initial parse failed
|
||||
if not format:
|
||||
@@ -130,12 +139,14 @@ class OntologyIngestor:
|
||||
guessed_fmt = fmt_map.get(ext)
|
||||
if guessed_fmt:
|
||||
self.logger.info(f"Retrying with guessed format: {guessed_fmt}")
|
||||
g.parse(file_path, format=guessed_fmt, **kwargs)
|
||||
ds.parse(file_path, format=guessed_fmt, **kwargs)
|
||||
else:
|
||||
raise e
|
||||
else:
|
||||
raise e
|
||||
|
||||
g = ds
|
||||
|
||||
self.progress.update_tracking(tracking_id, message="Converting to internal format...")
|
||||
|
||||
# Determine format for metadata
|
||||
|
||||
@@ -0,0 +1,92 @@
|
||||
"""A JSON-LD ontology whose terms live in a named graph must not be silently dropped.
|
||||
|
||||
A JSON-LD document with a top-level ``@id`` *and* ``@graph`` places its terms in a NAMED
|
||||
graph. ``rdflib.Graph.parse()`` loads only the default graph and discards the rest without
|
||||
raising, so every class and property in such a document disappeared while the load reported
|
||||
success — see issue #1129 for the reproduction through the public API.
|
||||
|
||||
This is the same ``Graph`` -> ``Dataset`` migration #757 made for ``JenaStore`` (#756); the
|
||||
ingest path was not covered by it.
|
||||
"""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import json
|
||||
|
||||
import pytest
|
||||
|
||||
from semantica.ingest.ontology_ingestor import OntologyIngestor
|
||||
|
||||
NAMED_GRAPH_ONTOLOGY = {
|
||||
"@context": {
|
||||
"ex": "https://example.org/ns#",
|
||||
"owl": "http://www.w3.org/2002/07/owl#",
|
||||
"rdfs": "http://www.w3.org/2000/01/rdf-schema#",
|
||||
},
|
||||
"@id": "https://example.org/ns",
|
||||
"@type": "owl:Ontology",
|
||||
"@graph": [
|
||||
{"@id": "ex:Thing", "@type": "owl:Class", "rdfs:label": "Thing"},
|
||||
{"@id": "ex:Other", "@type": "owl:Class", "rdfs:label": "Other"},
|
||||
{
|
||||
"@id": "ex:relatesTo",
|
||||
"@type": "owl:ObjectProperty",
|
||||
"rdfs:domain": {"@id": "ex:Thing"},
|
||||
"rdfs:range": {"@id": "ex:Other"},
|
||||
},
|
||||
],
|
||||
}
|
||||
|
||||
DEFAULT_GRAPH_ONTOLOGY = {
|
||||
"@context": NAMED_GRAPH_ONTOLOGY["@context"],
|
||||
"@graph": [
|
||||
{"@id": "ex:Thing", "@type": "owl:Class", "rdfs:label": "Thing"},
|
||||
{"@id": "ex:Other", "@type": "owl:Class", "rdfs:label": "Other"},
|
||||
],
|
||||
}
|
||||
|
||||
|
||||
def _write(tmp_path, name, document):
|
||||
path = tmp_path / name
|
||||
path.write_text(json.dumps(document), encoding="utf-8")
|
||||
return path
|
||||
|
||||
|
||||
def test_terms_in_a_named_graph_are_ingested(tmp_path):
|
||||
"""The regression: two classes and one object property, all inside the named graph."""
|
||||
path = _write(tmp_path, "named.jsonld", NAMED_GRAPH_ONTOLOGY)
|
||||
|
||||
data = OntologyIngestor().ingest_ontology(path).data
|
||||
|
||||
assert len(data["classes"]) == 2, (
|
||||
"classes inside a JSON-LD named graph were dropped; the ingestor is reading only "
|
||||
"the default graph"
|
||||
)
|
||||
assert len(data["properties"]) == 1
|
||||
assert {c["uri"] for c in data["classes"]} == {
|
||||
"https://example.org/ns#Thing",
|
||||
"https://example.org/ns#Other",
|
||||
}
|
||||
|
||||
|
||||
def test_terms_in_the_default_graph_still_work(tmp_path):
|
||||
"""Canary for the test above: a document *without* a top-level ``@id`` keeps its terms
|
||||
in the default graph and always parsed correctly. If this stopped passing, the fix would
|
||||
have traded one blind spot for another."""
|
||||
path = _write(tmp_path, "default.jsonld", DEFAULT_GRAPH_ONTOLOGY)
|
||||
|
||||
data = OntologyIngestor().ingest_ontology(path).data
|
||||
|
||||
assert len(data["classes"]) == 2
|
||||
|
||||
|
||||
@pytest.mark.parametrize("document", [NAMED_GRAPH_ONTOLOGY, DEFAULT_GRAPH_ONTOLOGY])
|
||||
def test_metadata_reports_what_was_actually_read(tmp_path, document):
|
||||
"""Whatever the shape of the document, the counts reported have to match the terms
|
||||
returned — a load that says it succeeded while returning nothing is what made #1129
|
||||
cost an afternoon to find."""
|
||||
path = _write(tmp_path, "any.jsonld", document)
|
||||
|
||||
result = OntologyIngestor().ingest_ontology(path)
|
||||
|
||||
assert result.data["classes"], "reported success with zero classes"
|
||||
Reference in New Issue
Block a user