parserutils.py 9.0 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313
  1. """
  2. NOTE: PyParsing setResultName/__call__ provides a very similar solution to this
  3. I didn't realise at the time of writing and I will remove a
  4. lot of this code at some point
  5. Utility classes for creating an abstract-syntax tree out with pyparsing actions
  6. Lets you label and group parts of parser production rules
  7. For example:
  8. # [5] BaseDecl ::= 'BASE' IRIREF
  9. BaseDecl = Comp('Base', Keyword('BASE') + Param('iri',IRIREF))
  10. After parsing, this gives you back an CompValue object,
  11. which is a dict/object with the parameters specified.
  12. So you can access the parameters are attributes or as keys:
  13. baseDecl.iri
  14. Comp lets you set an evalFn that is bound to the eval method of
  15. the resulting CompValue
  16. """
  17. from __future__ import annotations
  18. from collections import OrderedDict
  19. from types import MethodType
  20. from typing import (
  21. TYPE_CHECKING,
  22. Any,
  23. Callable,
  24. List,
  25. Mapping,
  26. Optional,
  27. Tuple,
  28. TypeVar,
  29. Union,
  30. )
  31. from pyparsing import ParserElement, ParseResults, TokenConverter
  32. from rdflib.term import BNode, Identifier, Variable
  33. from .pyparsing_compat import original_text_for
  34. if TYPE_CHECKING:
  35. from rdflib.plugins.sparql.sparql import FrozenBindings
  36. # This is an alternative
  37. # Comp('Sum')( Param('x')(Number) + '+' + Param('y')(Number) )
  38. def value(
  39. ctx: FrozenBindings,
  40. val: Any,
  41. variables: bool = False,
  42. errors: bool = False,
  43. ) -> Any:
  44. """Utility function for evaluating something...
  45. Variables will be looked up in the context
  46. Normally, non-bound vars is an error,
  47. set variables=True to return unbound vars
  48. Normally, an error raises the error,
  49. set errors=True to return error
  50. """
  51. if isinstance(val, Expr):
  52. return val.eval(ctx) # recurse?
  53. elif isinstance(val, CompValue):
  54. raise Exception("What do I do with this CompValue? %s" % val)
  55. elif isinstance(val, list):
  56. return [value(ctx, x, variables, errors) for x in val]
  57. elif isinstance(val, (BNode, Variable)):
  58. r = ctx.get(val)
  59. if isinstance(r, SPARQLError) and not errors:
  60. raise r
  61. if r is not None:
  62. return r
  63. # not bound
  64. if variables:
  65. return val
  66. else:
  67. raise NotBoundError
  68. elif isinstance(val, ParseResults) and len(val) == 1:
  69. return value(ctx, val[0], variables, errors)
  70. else:
  71. return val
  72. class ParamValue:
  73. """
  74. The result of parsing a Param
  75. This just keeps the name/value
  76. All cleverness is in the CompValue
  77. """
  78. def __init__(
  79. self, name: str, tokenList: Union[List[Any], ParseResults], isList: bool
  80. ):
  81. self.isList = isList
  82. self.name = name
  83. if isinstance(tokenList, (list, ParseResults)) and len(tokenList) == 1:
  84. tokenList = tokenList[0]
  85. self.tokenList = tokenList
  86. def __str__(self) -> str:
  87. return "Param(%s, %s)" % (self.name, self.tokenList)
  88. class Param(TokenConverter):
  89. """
  90. A pyparsing token for labelling a part of the parse-tree
  91. if isList is true repeat occurrences of ParamList have
  92. their values merged in a list
  93. """
  94. def __init__(self, name: str, expr, isList: bool = False):
  95. self.isList = isList
  96. TokenConverter.__init__(self, expr)
  97. self.set_name(name)
  98. self.add_parse_action(self.postParse2)
  99. def postParse2(self, tokenList: Union[List[Any], ParseResults]) -> ParamValue:
  100. return ParamValue(self.name, tokenList, self.isList)
  101. class ParamList(Param):
  102. """
  103. A shortcut for a Param with isList=True
  104. """
  105. def __init__(self, name: str, expr):
  106. Param.__init__(self, name, expr, True)
  107. _ValT = TypeVar("_ValT")
  108. class CompValue(OrderedDict):
  109. """
  110. The result of parsing a Comp
  111. Any included Params are available as Dict keys
  112. or as attributes
  113. """
  114. def __init__(self, name: str, **values):
  115. OrderedDict.__init__(self)
  116. self.name = name
  117. self.update(values)
  118. def clone(self) -> CompValue:
  119. return CompValue(self.name, **self)
  120. def __str__(self) -> str:
  121. return self.name + "_" + OrderedDict.__str__(self)
  122. def __repr__(self) -> str:
  123. return self.name + "_" + dict.__repr__(self)
  124. def _value(
  125. self, val: _ValT, variables: bool = False, errors: bool = False
  126. ) -> Union[_ValT, Any]:
  127. if self.ctx is not None:
  128. return value(self.ctx, val, variables)
  129. else:
  130. return val
  131. def __getitem__(self, a):
  132. return self._value(OrderedDict.__getitem__(self, a))
  133. # type error: Signature of "get" incompatible with supertype "dict"
  134. # type error: Signature of "get" incompatible with supertype "Mapping" [override]
  135. def get(self, a, variables: bool = False, errors: bool = False): # type: ignore[override]
  136. return self._value(OrderedDict.get(self, a, a), variables, errors)
  137. def __getattr__(self, a: str) -> Any:
  138. # Hack hack: OrderedDict relies on this
  139. if a in ("_OrderedDict__root", "_OrderedDict__end"):
  140. raise AttributeError()
  141. try:
  142. return self[a]
  143. except KeyError:
  144. # raise AttributeError('no such attribute '+a)
  145. return None
  146. if TYPE_CHECKING:
  147. # this is here because properties are dynamically set on CompValue
  148. def __setattr__(self, __name: str, __value: Any) -> None: ...
  149. class Expr(CompValue):
  150. """
  151. A CompValue that is evaluatable
  152. """
  153. def __init__(
  154. self,
  155. name: str,
  156. evalfn: Optional[Callable[[Any, Any], Any]] = None,
  157. **values,
  158. ):
  159. super(Expr, self).__init__(name, **values)
  160. self._evalfn = None
  161. if evalfn:
  162. self._evalfn = MethodType(evalfn, self)
  163. def eval(self, ctx: Any = {}) -> Union[SPARQLError, Any]:
  164. try:
  165. self.ctx: Optional[Union[Mapping, FrozenBindings]] = ctx
  166. # type error: "None" not callable
  167. return self._evalfn(ctx) # type: ignore[misc]
  168. except SPARQLError as e:
  169. return e
  170. finally:
  171. self.ctx = None
  172. class Comp(TokenConverter):
  173. """
  174. A pyparsing token for grouping together things with a label
  175. Any sub-tokens that are not Params will be ignored.
  176. Returns CompValue / Expr objects - depending on whether evalFn is set.
  177. """
  178. def __init__(self, name: str, expr: ParserElement):
  179. self.expr = expr
  180. TokenConverter.__init__(self, expr)
  181. self.set_name(name)
  182. self.evalfn: Optional[Callable[[Any, Any], Any]] = None
  183. def postParse(
  184. self, instring: str, loc: int, tokenList: ParseResults
  185. ) -> Union[Expr, CompValue]:
  186. res: Union[Expr, CompValue]
  187. if self.evalfn:
  188. res = Expr(self.name)
  189. res._evalfn = MethodType(self.evalfn, res)
  190. else:
  191. res = CompValue(self.name)
  192. if self.name == "ServiceGraphPattern":
  193. # Then this must be a service graph pattern and have
  194. # already matched.
  195. # lets assume there is one, for now, then test for two later.
  196. sgp = original_text_for(self.expr)
  197. service_string = sgp.search_string(instring)[0][0]
  198. res["service_string"] = service_string
  199. for t in tokenList:
  200. if isinstance(t, ParamValue):
  201. if t.isList:
  202. if t.name not in res:
  203. res[t.name] = []
  204. res[t.name].append(t.tokenList)
  205. else:
  206. res[t.name] = t.tokenList
  207. # res.append(t.tokenList)
  208. # if isinstance(t,CompValue):
  209. # res.update(t)
  210. return res
  211. def setEvalFn(self, evalfn: Callable[[Any, Any], Any]) -> Comp:
  212. self.evalfn = evalfn
  213. return self
  214. def prettify_parsetree(t: ParseResults, indent: str = "", depth: int = 0) -> str:
  215. out: List[str] = []
  216. for e in t.as_list():
  217. out.append(_prettify_sub_parsetree(e, indent, depth + 1))
  218. for k, v in sorted(t.items()):
  219. out.append("%s%s- %s:\n" % (indent, " " * depth, k))
  220. out.append(_prettify_sub_parsetree(v, indent, depth + 1))
  221. return "".join(out)
  222. def _prettify_sub_parsetree(
  223. t: Union[Identifier, CompValue, set, list, dict, Tuple, bool, None],
  224. indent: str = "",
  225. depth: int = 0,
  226. ) -> str:
  227. out: List[str] = []
  228. if isinstance(t, CompValue):
  229. out.append("%s%s> %s:\n" % (indent, " " * depth, t.name))
  230. for k, v in t.items():
  231. out.append("%s%s- %s:\n" % (indent, " " * (depth + 1), k))
  232. out.append(_prettify_sub_parsetree(v, indent, depth + 2))
  233. elif isinstance(t, dict):
  234. for k, v in t.items():
  235. out.append("%s%s- %s:\n" % (indent, " " * (depth + 1), k))
  236. out.append(_prettify_sub_parsetree(v, indent, depth + 2))
  237. elif isinstance(t, list):
  238. for e in t:
  239. out.append(_prettify_sub_parsetree(e, indent, depth + 1))
  240. else:
  241. out.append("%s%s- %r\n" % (indent, " " * depth, t))
  242. return "".join(out)
  243. # hurrah for circular imports
  244. from rdflib.plugins.sparql.sparql import NotBoundError, SPARQLError # noqa: E402