Files
semantica/tests/export/test_rdf_literal_escaping.py
T
Kevin Zhang 551b94c524 fix(export): escape Turtle/N-Triples string literals (closes #1098) (#1148)
* fix(export): escape Turtle/N-Triples string literals (fixes #1098)

Add RDFSerializer._escape_turtle_literal and apply it to the semantica:text
literal in serialize_to_turtle and the N-Triples text triple. Backslash,
double quote, newline, CR, and tab are escaped per the RDF 1.1 Turtle
STRING_LITERAL_QUOTE grammar, so entity text containing quotes or control
characters no longer emits invalid Turtle/N-Triples.

N-Triples previously escaped only quotes and newlines; now it also handles
backslashes and tabs via the shared escaper.

* fix(export): escape OWL-Time timestamp literals in Turtle output

Addresses Qodo finding on #1148: the OWL-Time branch of
serialize_to_turtle interpolated from_val/until_val directly into quoted
literals. Apply _escape_turtle_literal there too so timestamps containing
quotes, backslashes, or control characters cannot produce invalid Turtle.

* chore: remove stray local files (AGENTS.md, evals superpowers docs) from PR branch

---------
2026-08-25 20:57:57 +05:00

107 lines
3.6 KiB
Python

"""Regression tests for #1098: Turtle/N-Triples literal escaping."""
import pytest
from semantica.export.rdf_exporter import RDFExporter, RDFSerializer
@pytest.fixture
def serializer():
return RDFSerializer()
class TestTurtleLiteralEscaping:
def test_quote_in_text_is_escaped(self, serializer):
kg = {
"entities": [{"id": "e1", "text": 'He said "hello"', "type": "ORG"}],
"relationships": [],
}
turtle = serializer.serialize_to_turtle(kg)
assert '"He said \\"hello\\""' in turtle
def test_backslash_in_text_is_escaped(self, serializer):
kg = {
"entities": [{"id": "e1", "text": r"path\to\file", "type": "ORG"}],
"relationships": [],
}
turtle = serializer.serialize_to_turtle(kg)
assert r"path\\to\\file" in turtle
def test_newline_in_text_is_escaped(self, serializer):
kg = {
"entities": [{"id": "e1", "text": "line1\nline2", "type": "ORG"}],
"relationships": [],
}
turtle = serializer.serialize_to_turtle(kg)
assert "line1\\nline2" in turtle
def test_tab_in_text_is_escaped(self, serializer):
kg = {
"entities": [{"id": "e1", "text": "a\tb", "type": "ORG"}],
"relationships": [],
}
turtle = serializer.serialize_to_turtle(kg)
assert "a\\tb" in turtle
def test_plain_text_unchanged(self, serializer):
kg = {
"entities": [{"id": "e1", "text": "Apple Inc.", "type": "ORG"}],
"relationships": [],
}
turtle = serializer.serialize_to_turtle(kg)
assert 'semantica:text "Apple Inc."' in turtle
class TestNTriplesLiteralEscaping:
def test_quote_in_text_is_escaped(self, serializer):
kg = {
"entities": [{"id": "e1", "text": 'He said "hello"', "type": "ORG"}],
"relationships": [],
}
ntriples = serializer.serialize_to_ntriples(kg)
assert '\\"hello\\"' in ntriples
def test_backslash_in_text_is_escaped(self, serializer):
kg = {
"entities": [{"id": "e1", "text": r"path\to\file", "type": "ORG"}],
"relationships": [],
}
ntriples = serializer.serialize_to_ntriples(kg)
assert r"path\\to\\file" in ntriples
def test_newline_in_text_is_escaped(self, serializer):
kg = {
"entities": [{"id": "e1", "text": "line1\nline2", "type": "ORG"}],
"relationships": [],
}
ntriples = serializer.serialize_to_ntriples(kg)
assert "line1\\nline2" in ntriples
def test_tab_in_text_is_escaped(self, serializer):
kg = {
"entities": [{"id": "e1", "text": "a\tb", "type": "ORG"}],
"relationships": [],
}
ntriples = serializer.serialize_to_ntriples(kg)
assert "a\\tb" in ntriples
class TestOWLTimeLiteralEscaping:
"""Timestamp literals in OWL-Time turtle output must also be escaped."""
def test_owl_time_timestamps_are_escaped(self):
exporter = RDFExporter()
kg = {
"entities": [],
"relationships": [
{
"id": "r1",
"source_id": "a",
"target_id": "b",
"type": "works_for",
"valid_from": "2020-01-01T00:00:00Z",
"valid_until": None,
}
],
}
turtle = exporter.export_to_rdf(kg, format="turtle", include_temporal=True)
assert 'time:inXSDDateTimeStamp "2020-01-01T00:00:00Z"' in turtle