Files
semantica/tests/export/test_rdf_exporter_turtle_iris.py
T
KaifAhmad1 cf6c9b7b9c fix(export): stop double-encoding valid % escapes and fix built-in prefix shadowing
_as_turtle_iri() re-encoded absolute IRIs wholesale, turning already-valid
percent-escapes like %20 into %2520. Only spans outside existing valid
%XX escapes are quoted now, so malformed escapes (%zz) still get repaired
while valid ones pass through unchanged.

serialize_to_ntriples()/serialize_to_rdfxml() also passed only the
@context-derived namespaces into _as_turtle_iri(), which shadowed the
built-in semantica:/rdf:/rdfs:/owl: prefixes entirely whenever any
@context was present. _as_turtle_iri() now always merges the built-ins
with whatever namespaces the caller passes.
2026-08-23 21:52:51 +05:30

234 lines
7.3 KiB
Python

"""Regression tests for valid Turtle IRI generation (issue #1099)."""
from rdflib import RDF, Graph, URIRef
from semantica.export import RDFExporter
from semantica.kg.graph_builder import GraphBuilder
def test_turtle_normalizes_graph_builder_default_identifiers():
"""Default GraphBuilder labels with spaces become stable absolute IRIs."""
source = {
"entities": [
{
"id": "Kochi, Kerala",
"name": "Kochi, Kerala",
"type": "LOCATION",
},
{"id": "Jane Doe", "name": "Jane Doe", "type": "PERSON"},
],
"relationships": [
{
"source": "Jane Doe",
"target": "Kochi, Kerala",
"type": "located_in",
},
],
}
graph_data = GraphBuilder(resolve_conflicts=False).build(sources=[source])
turtle = RDFExporter().export_to_rdf(graph_data, format="turtle")
parsed = Graph().parse(data=turtle, format="turtle")
assert "<Jane Doe>" not in turtle
assert "<Kochi, Kerala>" not in turtle
kochi = URIRef("https://semantica.dev/ns#Kochi%2C%20Kerala")
jane = URIRef("https://semantica.dev/ns#Jane%20Doe")
assert (
kochi,
RDF.type,
URIRef("https://semantica.dev/ns#LOCATION"),
) in parsed
jane_type = URIRef("https://semantica.dev/ns#PERSON")
assert (jane, RDF.type, jane_type) in parsed
assert (
jane,
URIRef("https://semantica.dev/ns#located_in"),
kochi,
) in parsed
def test_turtle_preserves_absolute_iris():
"""Already-valid absolute resource IRIs remain unchanged."""
turtle = RDFExporter().export_to_rdf(
{
"entities": [
{
"id": "https://example.org/entities/jane",
"text": "Jane",
"type": "urn:example:Person",
}
],
"relationships": [],
},
format="turtle",
)
parsed = Graph().parse(data=turtle, format="turtle")
assert (
URIRef("https://example.org/entities/jane"),
RDF.type,
URIRef("urn:example:Person"),
) in parsed
def test_turtle_preserves_opaque_absolute_iris_and_encodes_bad_percent_escapes():
"""Opaque schemes remain absolute and malformed percent escapes are encoded."""
turtle = RDFExporter().export_to_rdf(
{
"entities": [
{"id": "mailto:foo", "type": "isbn:0451450523"},
{"id": "http://example.org/bad%zz", "type": "PERSON"},
],
"relationships": [],
},
format="turtle",
)
parsed = Graph().parse(data=turtle, format="turtle")
assert (
URIRef("mailto:foo"),
RDF.type,
URIRef("isbn:0451450523"),
) in parsed
assert URIRef("http://example.org/bad%25zz") in parsed.all_nodes()
def test_turtle_normalizes_temporal_relationship_endpoints():
"""Temporal relationship metadata uses the same normalized resource IRIs."""
turtle = RDFExporter().export_to_rdf(
{
"entities": [
{"id": "Jane Doe", "type": "PERSON"},
{"id": "Kochi, Kerala", "type": "LOCATION"},
],
"relationships": [
{
"source": "Jane Doe",
"target": "Kochi, Kerala",
"type": "located_in",
"valid_from": "2024-01-01T00:00:00+00:00",
"valid_until": "2024-02-01T00:00:00+00:00",
}
],
},
format="turtle",
include_temporal=True,
)
parsed = Graph().parse(data=turtle, format="turtle")
assert (
None,
URIRef("https://semantica.dev/ns#source"),
URIRef("https://semantica.dev/ns#Jane%20Doe"),
) in parsed
assert (
None,
URIRef("https://semantica.dev/ns#target"),
URIRef("https://semantica.dev/ns#Kochi%2C%20Kerala"),
) in parsed
def test_turtle_expands_context_prefixes_and_mints_relative_values():
"""Context prefixes expand while bare values use the fallback namespace."""
turtle = RDFExporter().export_to_rdf(
{
"@context": {"ex": "https://example.org/"},
"entities": [{"id": "ORG", "type": "ex:Person"}],
"relationships": [],
},
format="turtle",
)
parsed = Graph().parse(data=turtle, format="turtle")
assert (
URIRef("https://semantica.dev/ns#ORG"),
RDF.type,
URIRef("https://example.org/Person"),
) in parsed
def test_rdfxml_normalizes_resource_iris():
"""RDF/XML resource attributes use the same safe absolute IRIs."""
data = {
"entities": [
{"id": "Acme Corp", "type": "Person"},
{"id": "mailto:foo", "type": "isbn:0451450523"},
],
"relationships": [
{"source": "Jane Doe", "target": "Acme Corp", "type": "knows"}
],
}
rdfxml = RDFExporter().export_to_rdf(data, format="rdfxml")
parsed = Graph().parse(data=rdfxml, format="xml")
assert URIRef("https://semantica.dev/ns#Acme%20Corp") in parsed.all_nodes()
assert URIRef("mailto:foo") in parsed.all_nodes()
def test_ntriples_normalizes_resource_iris():
"""N-Triples resource IRIs reject neither spaces nor opaque schemes."""
data = {
"entities": [
{"id": "Acme Corp", "type": "Person"},
{"id": "mailto:foo", "type": "isbn:0451450523"},
],
"relationships": [
{"source": "Jane Doe", "target": "Acme Corp", "type": "knows"}
],
}
ntriples = RDFExporter().export_to_rdf(data, format="ntriples")
parsed = Graph().parse(data=ntriples, format="nt")
assert URIRef("https://semantica.dev/ns#Acme%20Corp") in parsed.all_nodes()
assert URIRef("mailto:foo") in parsed.all_nodes()
def test_turtle_preserves_existing_valid_percent_escapes():
"""A pre-encoded absolute IRI keeps its escape, instead of %20 -> %2520."""
turtle = RDFExporter().export_to_rdf(
{
"entities": [
{
"id": "https://example.org/entities/path%20name",
"type": "PERSON",
}
],
"relationships": [],
},
format="turtle",
)
parsed = Graph().parse(data=turtle, format="turtle")
assert (
URIRef("https://example.org/entities/path%20name"),
RDF.type,
URIRef("https://semantica.dev/ns#PERSON"),
) in parsed
assert "%2520" not in turtle
def test_ntriples_and_rdfxml_expand_builtin_prefixes_alongside_context():
"""A user @context must not shadow built-in prefixes like semantica:."""
data = {
"@context": {"ex": "https://example.org/"},
"entities": [{"id": "ORG", "type": "semantica:Entity"}],
"relationships": [],
}
ntriples = RDFExporter().export_to_rdf(data, format="ntriples")
nt_parsed = Graph().parse(data=ntriples, format="nt")
assert (
URIRef("https://semantica.dev/ns#ORG"),
RDF.type,
URIRef("https://semantica.dev/ns#Entity"),
) in nt_parsed
rdfxml = RDFExporter().export_to_rdf(data, format="rdfxml")
xml_parsed = Graph().parse(data=rdfxml, format="xml")
assert (
URIRef("https://semantica.dev/ns#ORG"),
RDF.type,
URIRef("https://semantica.dev/ns#Entity"),
) in xml_parsed