turtle.py 18 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508
  1. """
  2. Turtle RDF graph serializer for RDFLib.
  3. See <http://www.w3.org/TeamSubmission/turtle/> for syntax specification.
  4. """
  5. from __future__ import annotations
  6. import re
  7. import warnings
  8. from collections import defaultdict
  9. from typing import (
  10. IO,
  11. TYPE_CHECKING,
  12. Any,
  13. DefaultDict,
  14. Dict,
  15. List,
  16. Mapping,
  17. Optional,
  18. Sequence,
  19. Tuple,
  20. TypeVar,
  21. Union,
  22. )
  23. from rdflib.exceptions import Error
  24. from rdflib.graph import Graph
  25. from rdflib.namespace import RDF, RDFS
  26. from rdflib.serializer import Serializer
  27. from rdflib.term import BNode, Literal, Node, URIRef
  28. _StrT = TypeVar("_StrT", bound=str)
  29. if TYPE_CHECKING:
  30. from rdflib.graph import _PredicateType, _SubjectType, _TripleType
  31. __all__ = ["RecursiveSerializer", "TurtleSerializer"]
  32. class RecursiveSerializer(Serializer):
  33. """Base class for recursive serializers."""
  34. topClasses = [RDFS.Class]
  35. predicateOrder = [RDF.type, RDFS.label]
  36. maxDepth = 10
  37. indentString = " "
  38. roundtrip_prefixes: Tuple[Any, ...] = ()
  39. LOCALNAME_PECRENT_CHARACTER_REQUIRING_ESCAPE_REGEX = re.compile(
  40. r"%(?![0-9A-Fa-f]{2})"
  41. )
  42. def __init__(self, store: Graph):
  43. super(RecursiveSerializer, self).__init__(store)
  44. self.stream: Optional[IO[bytes]] = None
  45. self.reset()
  46. def addNamespace(self, prefix: str, uri: URIRef) -> None:
  47. if prefix in self.namespaces and self.namespaces[prefix] != uri:
  48. raise Exception(
  49. "Trying to override namespace prefix %s => %s, but it's already bound to %s"
  50. % (prefix, uri, self.namespaces[prefix])
  51. )
  52. self.namespaces[prefix] = uri
  53. def checkSubject(self, subject: _SubjectType) -> bool:
  54. """Check to see if the subject should be serialized yet"""
  55. if (
  56. (self.isDone(subject))
  57. or (subject not in self._subjects)
  58. or ((subject in self._topLevels) and (self.depth > 1))
  59. or (isinstance(subject, URIRef) and (self.depth >= self.maxDepth))
  60. ):
  61. return False
  62. return True
  63. def isDone(self, subject: _SubjectType) -> bool:
  64. """Return true if subject is serialized"""
  65. return subject in self._serialized
  66. def orderSubjects(self) -> List[_SubjectType]:
  67. seen: Dict[_SubjectType, bool] = {}
  68. subjects: List[_SubjectType] = []
  69. for classURI in self.topClasses:
  70. members = list(self.store.subjects(RDF.type, classURI))
  71. # type error: All overload variants of "sort" of "list" require at least one argument
  72. members.sort() # type: ignore[call-overload]
  73. subjects.extend(members)
  74. for member in members:
  75. self._topLevels[member] = True
  76. seen[member] = True
  77. recursable = [
  78. (isinstance(subject, BNode), self._references[subject], subject)
  79. for subject in self._subjects
  80. if subject not in seen
  81. ]
  82. recursable.sort()
  83. subjects.extend([subject for (isbnode, refs, subject) in recursable])
  84. return subjects
  85. def preprocess(self) -> None:
  86. for triple in self.store.triples((None, None, None)):
  87. self.preprocessTriple(triple)
  88. def preprocessTriple(self, spo: _TripleType) -> None:
  89. s, p, o = spo
  90. self._references[o] += 1
  91. self._subjects[s] = True
  92. def reset(self) -> None:
  93. self.depth = 0
  94. # Typed none because nothing is using it ...
  95. self.lists: Dict[None, None] = {}
  96. self.namespaces: Dict[str, URIRef] = {}
  97. self._references: DefaultDict[Node, int] = defaultdict(int)
  98. self._serialized: Dict[_SubjectType, bool] = {}
  99. self._subjects: Dict[_SubjectType, bool] = {}
  100. self._topLevels: Dict[_SubjectType, bool] = {}
  101. if self.roundtrip_prefixes:
  102. if hasattr(self.roundtrip_prefixes, "__iter__"):
  103. for prefix, ns in self.store.namespaces():
  104. if prefix in self.roundtrip_prefixes:
  105. self.addNamespace(prefix, ns)
  106. else:
  107. for prefix, ns in self.store.namespaces():
  108. self.addNamespace(prefix, ns)
  109. def buildPredicateHash(
  110. self, subject: _SubjectType
  111. ) -> Mapping[_PredicateType, List[Node]]:
  112. """
  113. Build a hash key by predicate to a list of objects for the given
  114. subject
  115. """
  116. properties: Dict[_PredicateType, List[Node]] = {}
  117. for s, p, o in self.store.triples((subject, None, None)):
  118. oList = properties.get(p, [])
  119. oList.append(o)
  120. properties[p] = oList
  121. return properties
  122. def sortProperties(
  123. self, properties: Mapping[_PredicateType, List[Node]]
  124. ) -> List[_PredicateType]:
  125. """Take a hash from predicate uris to lists of values.
  126. Sort the lists of values. Return a sorted list of properties."""
  127. # Sort object lists
  128. for prop, objects in properties.items():
  129. # type error: All overload variants of "sort" of "list" require at least one argument
  130. objects.sort() # type: ignore[call-overload]
  131. # Make sorted list of properties
  132. propList: List[_PredicateType] = []
  133. seen: Dict[_PredicateType, bool] = {}
  134. for prop in self.predicateOrder:
  135. if (prop in properties) and (prop not in seen):
  136. propList.append(prop)
  137. seen[prop] = True
  138. props = list(properties.keys())
  139. # type error: All overload variants of "sort" of "list" require at least one argument
  140. props.sort() # type: ignore[call-overload]
  141. for prop in props:
  142. if prop not in seen:
  143. propList.append(prop)
  144. seen[prop] = True
  145. return propList
  146. def subjectDone(self, subject: _SubjectType) -> None:
  147. """Mark a subject as done."""
  148. self._serialized[subject] = True
  149. def indent(self, modifier: int = 0) -> str:
  150. """Returns indent string multiplied by the depth"""
  151. return (self.depth + modifier) * self.indentString
  152. def write(self, text: str) -> None:
  153. """Write text in given encoding."""
  154. # type error: Item "None" of "Optional[IO[bytes]]" has no attribute "write"
  155. self.stream.write(text.encode(self.encoding, "replace")) # type: ignore[union-attr]
  156. def relativize(self, uri: _StrT) -> Union[_StrT, URIRef]:
  157. base = self.base
  158. if (
  159. base is not None
  160. and uri.startswith(base)
  161. and "#" not in uri.replace(base, "")
  162. and "/" not in uri.replace(base, "")
  163. ):
  164. # type error: Incompatible types in assignment (expression has type "str", variable has type "Node")
  165. uri = URIRef(uri.replace(base, "", 1)) # type: ignore[assignment]
  166. return uri
  167. SUBJECT = 0
  168. VERB = 1
  169. OBJECT = 2
  170. _GEN_QNAME_FOR_DT = False
  171. _SPACIOUS_OUTPUT = False
  172. class TurtleSerializer(RecursiveSerializer):
  173. """Turtle RDF graph serializer."""
  174. short_name = "turtle"
  175. indentString = " "
  176. LOCALNAME_PECRENT_CHARACTER_REQUIRING_ESCAPE_REGEX = re.compile(
  177. r"%(?![0-9A-Fa-f]{2})"
  178. )
  179. def __init__(self, store: Graph):
  180. self._ns_rewrite: Dict[str, str] = {}
  181. super(TurtleSerializer, self).__init__(store)
  182. self.keywords: Dict[Node, str] = {RDF.type: "a"}
  183. self.reset()
  184. self.stream = None
  185. self._spacious = _SPACIOUS_OUTPUT
  186. # type error: Return type "str" of "addNamespace" incompatible with return type "None" in supertype "RecursiveSerializer"
  187. def addNamespace(self, prefix: str, namespace: URIRef) -> str: # type: ignore[override]
  188. # Turtle does not support prefix that start with _
  189. # if they occur in the graph, rewrite to p_blah
  190. # this is more complicated since we need to make sure p_blah
  191. # does not already exist. And we register namespaces as we go, i.e.
  192. # we may first see a triple with prefix _9 - rewrite it to p_9
  193. # and then later find a triple with a "real" p_9 prefix
  194. # so we need to keep track of ns rewrites we made so far.
  195. if (prefix > "" and prefix[0] == "_") or self.namespaces.get(
  196. prefix, namespace
  197. ) != namespace:
  198. if prefix not in self._ns_rewrite:
  199. p = "p" + prefix
  200. while p in self.namespaces:
  201. p = "p" + p
  202. self._ns_rewrite[prefix] = p
  203. prefix = self._ns_rewrite.get(prefix, prefix)
  204. super(TurtleSerializer, self).addNamespace(prefix, namespace)
  205. return prefix
  206. def reset(self) -> None:
  207. super(TurtleSerializer, self).reset()
  208. # typing as Dict[None, None] because nothing seems to be using it
  209. self._shortNames: Dict[None, None] = {}
  210. self._started = False
  211. self._ns_rewrite = {}
  212. def serialize(
  213. self,
  214. stream: IO[bytes],
  215. base: Optional[str] = None,
  216. encoding: Optional[str] = None,
  217. spacious: Optional[bool] = None,
  218. **kwargs: Any,
  219. ) -> None:
  220. self.reset()
  221. self.stream = stream
  222. # if base is given here, use that, if not and a base is set for the graph use that
  223. if base is not None:
  224. self.base = base
  225. elif self.store.base is not None:
  226. self.base = self.store.base
  227. if spacious is not None:
  228. self._spacious = spacious
  229. self.preprocess()
  230. subjects_list = self.orderSubjects()
  231. self.startDocument()
  232. firstTime = True
  233. for subject in subjects_list:
  234. if self.isDone(subject):
  235. continue
  236. if firstTime:
  237. firstTime = False
  238. if self.statement(subject) and not firstTime:
  239. self.write("\n")
  240. self.endDocument()
  241. stream.write("\n".encode("latin-1"))
  242. self.base = None
  243. def preprocessTriple(self, triple: _TripleType) -> None:
  244. super(TurtleSerializer, self).preprocessTriple(triple)
  245. for i, node in enumerate(triple):
  246. if i == VERB:
  247. if node in self.keywords:
  248. # predicate is a keyword
  249. continue
  250. if (
  251. self.base is not None
  252. and isinstance(node, URIRef)
  253. and node.startswith(self.base)
  254. and "#" not in node.replace(self.base, "")
  255. and "/" not in node.replace(self.base, "")
  256. ):
  257. # predicate corresponds to base namespace
  258. continue
  259. # Don't use generated prefixes for subjects and objects
  260. self.get_pname(node, gen_prefix=(i == VERB))
  261. if isinstance(node, Literal) and node.datatype:
  262. self.get_pname(node.datatype, gen_prefix=_GEN_QNAME_FOR_DT)
  263. p = triple[1]
  264. if isinstance(p, BNode): # hmm - when is P ever a bnode?
  265. self._references[p] += 1
  266. # Refer to Productions for terminals PNAME_NS and PNAME_LN https://www.w3.org/TR/turtle/#sec-grammar-grammar
  267. def get_pname(self, uri: Node, gen_prefix: bool = True) -> Optional[str]:
  268. if not isinstance(uri, URIRef):
  269. return None
  270. parts = None
  271. try:
  272. parts = self.store.compute_qname(uri, generate=gen_prefix)
  273. except Exception:
  274. # is the uri a namespace in itself?
  275. pfx = self.store.store.prefix(uri)
  276. if pfx is not None:
  277. parts = (pfx, uri, "")
  278. else:
  279. # nothing worked
  280. return None
  281. prefix, namespace, local = parts
  282. # To understand treatment of % character refer to Productions for terminal PLX at
  283. # https://www.w3.org/TR/turtle/#grammar-production-PLX
  284. # Only % NOT followed by two hex chars requires manual backslash escaping
  285. local = local.replace(r"(", r"\(").replace(r")", r"\)")
  286. local = self.LOCALNAME_PECRENT_CHARACTER_REQUIRING_ESCAPE_REGEX.sub(
  287. "\\%", local
  288. )
  289. # PName cannot end with .
  290. if local.endswith("."):
  291. return None
  292. prefix = self.addNamespace(prefix, namespace)
  293. return "%s:%s" % (prefix, local)
  294. def getQName(self, uri: Node, gen_prefix: bool = True) -> Optional[str]:
  295. warnings.warn(
  296. "TurtleSerializer.getQName is deprecated, use TurtleSerializer.get_pname instead.",
  297. DeprecationWarning,
  298. stacklevel=2,
  299. )
  300. return self.get_pname(uri, gen_prefix)
  301. def startDocument(self) -> None:
  302. self._started = True
  303. ns_list = sorted(self.namespaces.items())
  304. if self.base:
  305. self.write(self.indent() + "@base <%s> .\n" % self.base)
  306. for prefix, uri in ns_list:
  307. self.write(self.indent() + "@prefix %s: <%s> .\n" % (prefix, uri))
  308. if ns_list and self._spacious:
  309. self.write("\n")
  310. def endDocument(self) -> None:
  311. if self._spacious:
  312. self.write("\n")
  313. def statement(self, subject: _SubjectType) -> bool:
  314. self.subjectDone(subject)
  315. return self.s_squared(subject) or self.s_default(subject)
  316. def s_default(self, subject: _SubjectType) -> bool:
  317. self.write("\n" + self.indent())
  318. self.path(subject, SUBJECT)
  319. self.predicateList(subject)
  320. self.write(" .")
  321. return True
  322. def s_squared(self, subject: _SubjectType) -> bool:
  323. if (self._references[subject] > 0) or not isinstance(subject, BNode):
  324. return False
  325. self.write("\n" + self.indent() + "[]")
  326. self.predicateList(subject)
  327. self.write(" .")
  328. return True
  329. def path(self, node: Node, position: int, newline: bool = False) -> None:
  330. if not (
  331. self.p_squared(node, position, newline)
  332. or self.p_default(node, position, newline)
  333. ):
  334. raise Error("Cannot serialize node '%s'" % (node,))
  335. def p_default(self, node: Node, position: int, newline: bool = False) -> bool:
  336. if position != SUBJECT and not newline:
  337. self.write(" ")
  338. self.write(self.label(node, position))
  339. return True
  340. def label(self, node: Node, position: int) -> str:
  341. if node == RDF.nil:
  342. return "()"
  343. if position is VERB and node in self.keywords:
  344. return self.keywords[node]
  345. if isinstance(node, Literal):
  346. return node._literal_n3(
  347. use_plain=True,
  348. qname_callback=lambda dt: self.get_pname(dt, _GEN_QNAME_FOR_DT),
  349. )
  350. else:
  351. node = self.relativize(node) # type: ignore[type-var]
  352. return self.get_pname(node, position == VERB) or node.n3()
  353. def p_squared(self, node: Node, position: int, newline: bool = False) -> bool:
  354. if (
  355. not isinstance(node, BNode)
  356. or node in self._serialized
  357. or self._references[node] > 1
  358. or position == SUBJECT
  359. ):
  360. return False
  361. if not newline:
  362. self.write(" ")
  363. if self.isValidList(node):
  364. # this is a list
  365. self.write("(")
  366. self.depth += 1 # 2
  367. self.doList(node)
  368. self.depth -= 1 # 2
  369. self.write(" )")
  370. else:
  371. self.subjectDone(node)
  372. self.depth += 2
  373. # self.write('[\n' + self.indent())
  374. self.write("[")
  375. self.depth -= 1
  376. # self.predicateList(node, newline=True)
  377. self.predicateList(node, newline=False)
  378. # self.write('\n' + self.indent() + ']')
  379. self.write(" ]")
  380. self.depth -= 1
  381. return True
  382. def isValidList(self, l_: Node) -> bool:
  383. """
  384. Checks if l is a valid RDF list, i.e. no nodes have other properties.
  385. """
  386. try:
  387. if self.store.value(l_, RDF.first) is None:
  388. return False
  389. except Exception:
  390. return False
  391. while l_:
  392. if l_ != RDF.nil and len(list(self.store.predicate_objects(l_))) != 2:
  393. return False
  394. # type error: Incompatible types in assignment (expression has type "Optional[Node]", variable has type "Node")
  395. l_ = self.store.value(l_, RDF.rest) # type: ignore[assignment]
  396. return True
  397. def doList(self, l_: Node) -> None:
  398. while l_:
  399. item = self.store.value(l_, RDF.first)
  400. if item is not None:
  401. self.path(item, OBJECT)
  402. self.subjectDone(l_)
  403. # type error: Incompatible types in assignment (expression has type "Optional[Node]", variable has type "Node")
  404. l_ = self.store.value(l_, RDF.rest) # type: ignore[assignment]
  405. def predicateList(self, subject: Node, newline: bool = False) -> None:
  406. properties = self.buildPredicateHash(subject)
  407. propList = self.sortProperties(properties)
  408. if len(propList) == 0:
  409. return
  410. self.verb(propList[0], newline=newline)
  411. self.objectList(properties[propList[0]])
  412. for predicate in propList[1:]:
  413. self.write(" ;\n" + self.indent(1))
  414. self.verb(predicate, newline=True)
  415. self.objectList(properties[predicate])
  416. def verb(self, node: Node, newline: bool = False) -> None:
  417. self.path(node, VERB, newline)
  418. def objectList(self, objects: Sequence[Node]) -> None:
  419. count = len(objects)
  420. if count == 0:
  421. return
  422. depthmod = (count == 1) and 0 or 1
  423. self.depth += depthmod
  424. self.path(objects[0], OBJECT)
  425. for obj in objects[1:]:
  426. self.write(",\n" + self.indent(1))
  427. self.path(obj, OBJECT, newline=True)
  428. self.depth -= depthmod