Files
semantica/tests/ontology/test_vocabulary.py
T
KaifAhmad1andfabio-rovai 2d75952476 fix: close remaining review gaps in vocabulary/deterministic-IRI PR
serialize_to_rdfxml still defaulted entity_type to the bare string
"semantica:Entity" written into an rdf:resource attribute, which isn't
namespace-expanded the way a Turtle angle-bracket or XML element name is -
the same #1101 failure mode, just on the path the original tests didn't
cover. Now uses the full-IRI DEFAULT_ENTITY_TYPE like the Turtle path.

json_exporter.py emits semantica:format and @type: "semantica:KnowledgeGraph",
neither of which was declared in the vocabulary or included in
EMITTED_TERMS, so the "undeclared terms fail the build" guarantee didn't
actually cover them. Both are now declared with rdfs:label/comment and
added to the guard set.

MANIFEST.in didn't mirror the pyproject.toml package-data addition, so a
source-distribution install could ship without the vocabulary file.

The cross-process minting-stability test replaced the subprocess's entire
environment with a POSIX-only PATH, breaking it on Windows and any host
needing other inherited env vars; now overrides only PYTHONHASHSEED on top
of the inherited environment.

Also folds mint_entity_iri/mint_relationship_iri's hand-rolled
hashlib.sha256(...).hexdigest() into the existing hash_data() helper this
file already imports alongside.

229 export and ontology tests pass, including a new regression test for
the RDF/XML default-type fix.

Co-Authored-By: fabio-rovai <fabio@thetesseractacademy.com>
2026-08-19 19:09:02 +05:30

117 lines
3.7 KiB
Python

"""The vocabulary must stay true to what the exporters emit (issue #1107).
A vocabulary document that drifts from the code is worse than none, because it
states that terms mean something while the exporters emit different ones. These
tests tie the two together: every term the serializers can write must be
declared here, so adding a term to an exporter without declaring it fails the
build rather than shipping an undeclared IRI.
"""
import pytest
rdflib = pytest.importorskip("rdflib")
from semantica.export.rdf_exporter import ( # noqa: E402
DEFAULT_ENTITY_TYPE,
DEFAULT_RELATION_TYPE,
SEMANTICA_NS,
)
from semantica.ontology.vocabulary import ( # noqa: E402
NAMESPACE,
vocabulary_path,
vocabulary_turtle,
)
#: Every term the exporters emit in the Semantica namespace, by local name.
#: RDF and OWL-Time paths in export/rdf_exporter.py, document and relationship
#: terms in export/json_exporter.py, roles in provenance/manager.py.
EMITTED_TERMS = {
"Entity",
"Relationship",
"KnowledgeGraph",
"text",
"confidence",
"metadata",
"related_to",
"source",
"target",
"type",
"entities",
"relationships",
"exportedAt",
"format",
"openEndedInterval",
"role_generator",
}
@pytest.fixture(scope="module")
def graph():
g = rdflib.Graph()
g.parse(data=vocabulary_turtle(), format="turtle")
return g
def test_vocabulary_ships_with_the_package():
assert vocabulary_path().is_file()
def test_vocabulary_parses(graph):
assert len(graph) > 0
def test_namespace_matches_the_one_the_exporters_use():
assert NAMESPACE == SEMANTICA_NS
def test_every_emitted_term_is_declared(graph):
declared = {
str(s)[len(NAMESPACE) :]
for s in set(graph.subjects())
if isinstance(s, rdflib.URIRef) and str(s).startswith(NAMESPACE)
}
missing = EMITTED_TERMS - declared
assert not missing, f"emitted but not declared in the vocabulary: {sorted(missing)}"
def test_the_defaults_the_exporters_fall_back_to_are_declared(graph):
for iri in (DEFAULT_ENTITY_TYPE, DEFAULT_RELATION_TYPE):
assert (rdflib.URIRef(iri), None, None) in graph, f"{iri} is not declared"
def test_every_declared_term_carries_a_label_and_a_comment(graph):
for subject in set(graph.subjects()):
if not (isinstance(subject, rdflib.URIRef) and str(subject).startswith(NAMESPACE)):
continue
assert graph.value(subject, rdflib.RDFS.label), f"{subject} has no rdfs:label"
assert graph.value(subject, rdflib.RDFS.comment), f"{subject} has no rdfs:comment"
def test_declared_ranges_do_not_contradict_what_the_exporters_emit(graph):
"""A declared range must match the datatype the serializers actually write.
Caught by review on #1109: sem:confidence was declared xsd:decimal while the
N-Triples serializer types the same value xsd:float. A vocabulary that
contradicts the code is worse than no vocabulary, so any range declared here
has to be one the exporters really emit.
"""
import re
from semantica.export.rdf_exporter import RDFExporter
sample = {
"entities": [{"id": "https://example.org/e1", "text": "A",
"type": "https://example.org/T", "confidence": 0.5}],
"relationships": [],
}
emitted = RDFExporter().export_to_rdf(sample, format="ntriples")
for subject, _, range_ in graph.triples((None, rdflib.RDFS.range, None)):
if not str(subject).startswith(NAMESPACE):
continue
local = str(subject)[len(NAMESPACE):]
for match in re.finditer(rf'<{NAMESPACE}{local}> "[^"]*"\^\^<([^>]+)>', emitted):
assert match.group(1) == str(range_), (
f"{local}: vocabulary declares {range_}, N-Triples emits {match.group(1)}"
)