patch.py 6.4 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179
  1. from __future__ import annotations
  2. from codecs import getreader
  3. from enum import Enum
  4. from typing import TYPE_CHECKING, Any, MutableMapping, Optional, Union
  5. from rdflib.exceptions import ParserError as ParseError
  6. from rdflib.graph import Dataset
  7. from rdflib.parser import InputSource
  8. from rdflib.plugins.parsers.nquads import NQuadsParser
  9. # Build up from the NTriples parser:
  10. from rdflib.plugins.parsers.ntriples import r_nodeid, r_tail, r_uriref, r_wspace
  11. from rdflib.term import BNode, URIRef
  12. if TYPE_CHECKING:
  13. import typing_extensions as te
  14. __all__ = ["RDFPatchParser", "Operation"]
  15. _BNodeContextType = MutableMapping[str, BNode]
  16. class Operation(Enum):
  17. """Enum of RDF Patch operations.
  18. Operations:
  19. - `AddTripleOrQuad` (A): Adds a triple or quad.
  20. - `DeleteTripleOrQuad` (D): Deletes a triple or quad.
  21. - `AddPrefix` (PA): Adds a prefix.
  22. - `DeletePrefix` (PD): Deletes a prefix.
  23. - `TransactionStart` (TX): Starts a transaction.
  24. - `TransactionCommit` (TC): Commits a transaction.
  25. - `TransactionAbort` (TA): Aborts a transaction.
  26. - `Header` (H): Specifies a header.
  27. """
  28. AddTripleOrQuad = "A"
  29. DeleteTripleOrQuad = "D"
  30. AddPrefix = "PA"
  31. DeletePrefix = "PD"
  32. TransactionStart = "TX"
  33. TransactionCommit = "TC"
  34. TransactionAbort = "TA"
  35. Header = "H"
  36. class RDFPatchParser(NQuadsParser):
  37. def parse( # type: ignore[override]
  38. self,
  39. inputsource: InputSource,
  40. sink: Dataset,
  41. bnode_context: Optional[_BNodeContextType] = None,
  42. skolemize: bool = False,
  43. **kwargs: Any,
  44. ) -> Dataset:
  45. """Parse inputsource as an RDF Patch file.
  46. Args:
  47. inputsource: the source of RDF Patch formatted data
  48. sink: where to send parsed data
  49. bnode_context: a dict mapping blank node identifiers to [`BNode`][rdflib.term.BNode]
  50. instances. See `.W3CNTriplesParser.parse`
  51. """
  52. assert sink.store.context_aware, (
  53. "RDFPatchParser must be given" " a context aware store."
  54. )
  55. # type error: Incompatible types in assignment (expression has type "ConjunctiveGraph", base class "W3CNTriplesParser" defined the type as "Union[DummySink, NTGraphSink]")
  56. self.sink: Dataset = Dataset(store=sink.store)
  57. self.skolemize = skolemize
  58. source = inputsource.getCharacterStream()
  59. if not source:
  60. source = inputsource.getByteStream()
  61. source = getreader("utf-8")(source)
  62. if not hasattr(source, "read"):
  63. raise ParseError("Item to parse must be a file-like object.")
  64. self.file = source
  65. self.buffer = ""
  66. while True:
  67. self.line = __line = self.readline()
  68. if self.line is None:
  69. break
  70. try:
  71. self.parsepatch(bnode_context)
  72. except ParseError as msg:
  73. raise ParseError("Invalid line (%s):\n%r" % (msg, __line))
  74. return self.sink
  75. def parsepatch(self, bnode_context: Optional[_BNodeContextType] = None) -> None:
  76. self.eat(r_wspace)
  77. # From spec: "No comments should be included (comments start # and run to end
  78. # of line)."
  79. if (not self.line) or self.line.startswith("#"):
  80. return # The line is empty or a comment
  81. # if header, transaction, skip
  82. operation = self.operation()
  83. self.eat(r_wspace)
  84. if operation in [Operation.AddTripleOrQuad, Operation.DeleteTripleOrQuad]:
  85. self.add_or_remove_triple_or_quad(operation, bnode_context)
  86. elif operation == Operation.AddPrefix:
  87. self.add_prefix()
  88. elif operation == Operation.DeletePrefix:
  89. self.delete_prefix()
  90. def add_or_remove_triple_or_quad(
  91. self, operation, bnode_context: Optional[_BNodeContextType] = None
  92. ) -> None:
  93. self.eat(r_wspace)
  94. if (not self.line) or self.line.startswith("#"):
  95. return # The line is empty or a comment
  96. subject = self.labeled_bnode() or self.subject(bnode_context)
  97. self.eat(r_wspace)
  98. predicate = self.predicate()
  99. self.eat(r_wspace)
  100. obj = self.labeled_bnode() or self.object(bnode_context)
  101. self.eat(r_wspace)
  102. context = self.labeled_bnode() or self.uriref() or self.nodeid(bnode_context)
  103. self.eat(r_tail)
  104. if self.line:
  105. raise ParseError("Trailing garbage")
  106. # Must have a context aware store - add on a normal Graph
  107. # discards anything where the ctx != graph.identifier
  108. if operation == Operation.AddTripleOrQuad:
  109. if context:
  110. self.sink.get_context(context).add((subject, predicate, obj))
  111. else:
  112. self.sink.default_context.add((subject, predicate, obj))
  113. elif operation == Operation.DeleteTripleOrQuad:
  114. if context:
  115. self.sink.get_context(context).remove((subject, predicate, obj))
  116. else:
  117. self.sink.default_context.remove((subject, predicate, obj))
  118. def add_prefix(self):
  119. # Extract prefix and URI from the line
  120. prefix, ns, _ = self.line.replace('"', "").replace("'", "").split(" ") # type: ignore[union-attr]
  121. ns_stripped = ns.strip("<>")
  122. self.sink.bind(prefix, ns_stripped)
  123. def delete_prefix(self):
  124. prefix, _, _ = self.line.replace('"', "").replace("'", "").split(" ") # type: ignore[union-attr]
  125. self.sink.namespace_manager.bind(prefix, None, replace=True)
  126. def operation(self) -> Operation:
  127. for op in Operation:
  128. if self.line.startswith(op.value): # type: ignore[union-attr]
  129. self.eat_op(op.value)
  130. return op
  131. raise ValueError(
  132. f'Invalid or no Operation found in line: "{self.line}". Valid Operations '
  133. f"codes are {', '.join([op.value for op in Operation])}"
  134. )
  135. def eat_op(self, op: str) -> None:
  136. self.line = self.line.lstrip(op) # type: ignore[union-attr]
  137. def nodeid(
  138. self, bnode_context: Optional[_BNodeContextType] = None
  139. ) -> Union[te.Literal[False], BNode, URIRef]:
  140. if self.peek("_"):
  141. return BNode(self.eat(r_nodeid).group(1))
  142. return False
  143. def labeled_bnode(self):
  144. if self.peek("<_"):
  145. plain_uri = self.eat(r_uriref).group(1)
  146. bnode_id = r_nodeid.match(plain_uri).group(1) # type: ignore[union-attr]
  147. return BNode(bnode_id)
  148. return False