Coverage for python/lsst/daf/butler/registry/queries/expressions/parser/exprTree.py: 46%

Shortcuts on this page

r m x p   toggle line displays

j k   next/prev highlighted chunk

0   (zero) top of page

1   (one) first highlighted chunk

140 statements  

1# This file is part of daf_butler. 

2# 

3# Developed for the LSST Data Management System. 

4# This product includes software developed by the LSST Project 

5# (https://www.lsst.org). 

6# See the COPYRIGHT file at the top-level directory of this distribution 

7# for details of code ownership. 

8# 

9# This program is free software: you can redistribute it and/or modify 

10# it under the terms of the GNU General Public License as published by 

11# the Free Software Foundation, either version 3 of the License, or 

12# (at your option) any later version. 

13# 

14# This program is distributed in the hope that it will be useful, 

15# but WITHOUT ANY WARRANTY; without even the implied warranty of 

16# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 

17# GNU General Public License for more details. 

18# 

19# You should have received a copy of the GNU General Public License 

20# along with this program. If not, see <https://www.gnu.org/licenses/>. 

21 

22"""Module which defines classes for intermediate representation of the 

23expression tree produced by parser. 

24 

25The purpose of the intermediate representation is to be able to generate 

26same expression as a part of SQL statement with the minimal changes. We 

27will need to be able to replace identifiers in original expression with 

28database-specific identifiers but everything else will probably be sent 

29to database directly. 

30""" 

31 

32from __future__ import annotations 

33 

34__all__ = ['Node', 'BinaryOp', 'FunctionCall', 'Identifier', 'IsIn', 'NumericLiteral', 

35 'Parens', 'RangeLiteral', 'StringLiteral', 'TimeLiteral', 'TupleNode', 

36 'UnaryOp', 'function_call'] 

37 

38# ------------------------------- 

39# Imports of standard modules -- 

40# ------------------------------- 

41from abc import ABC, abstractmethod 

42from typing import Any, List, Optional, Tuple, TYPE_CHECKING 

43 

44# ----------------------------- 

45# Imports for other modules -- 

46# ----------------------------- 

47 

48# ---------------------------------- 

49# Local non-exported definitions -- 

50# ---------------------------------- 

51 

52if TYPE_CHECKING: 52 ↛ 53line 52 didn't jump to line 53, because the condition on line 52 was never true

53 import astropy.time 

54 from .treeVisitor import TreeVisitor 

55 

56# ------------------------ 

57# Exported definitions -- 

58# ------------------------ 

59 

60 

61class Node(ABC): 

62 """Base class of IR node in expression tree. 

63 

64 The purpose of this class is to simplify visiting of the 

65 all nodes in a tree. It has a list of sub-nodes of this 

66 node so that visiting code can navigate whole tree without 

67 knowing exact types of each node. 

68 

69 Attributes 

70 ---------- 

71 children : tuple of :py:class:`Node` 

72 Possibly empty list of sub-nodes. 

73 """ 

74 def __init__(self, children: Tuple[Node, ...] = None): 

75 self.children = tuple(children or ()) 

76 

77 @abstractmethod 

78 def visit(self, visitor: TreeVisitor) -> Any: 

79 """Implement Visitor pattern for parsed tree. 

80 

81 Parameters 

82 ---------- 

83 visitor : `TreeVisitor` 

84 Instance of visitor type. 

85 """ 

86 

87 

88class BinaryOp(Node): 

89 """Node representing binary operator. 

90 

91 This class is used for representing all binary operators including 

92 arithmetic and boolean operations. 

93 

94 Attributes 

95 ---------- 

96 lhs : Node 

97 Left-hand side of the operation 

98 rhs : Node 

99 Right-hand side of the operation 

100 op : str 

101 Operator name, e.g. '+', 'OR' 

102 """ 

103 def __init__(self, lhs: Node, op: str, rhs: Node): 

104 Node.__init__(self, (lhs, rhs)) 

105 self.lhs = lhs 

106 self.op = op 

107 self.rhs = rhs 

108 

109 def visit(self, visitor: TreeVisitor) -> Any: 

110 # Docstring inherited from Node.visit 

111 lhs = self.lhs.visit(visitor) 

112 rhs = self.rhs.visit(visitor) 

113 return visitor.visitBinaryOp(self.op, lhs, rhs, self) 

114 

115 def __str__(self) -> str: 

116 return "{lhs} {op} {rhs}".format(**vars(self)) 

117 

118 

119class UnaryOp(Node): 

120 """Node representing unary operator. 

121 

122 This class is used for representing all unary operators including 

123 arithmetic and boolean operations. 

124 

125 Attributes 

126 ---------- 

127 op : str 

128 Operator name, e.g. '+', 'NOT' 

129 operand : Node 

130 Operand. 

131 """ 

132 def __init__(self, op: str, operand: Node): 

133 Node.__init__(self, (operand,)) 

134 self.op = op 

135 self.operand = operand 

136 

137 def visit(self, visitor: TreeVisitor) -> Any: 

138 # Docstring inherited from Node.visit 

139 operand = self.operand.visit(visitor) 

140 return visitor.visitUnaryOp(self.op, operand, self) 

141 

142 def __str__(self) -> str: 

143 return "{op} {operand}".format(**vars(self)) 

144 

145 

146class StringLiteral(Node): 

147 """Node representing string literal. 

148 

149 Attributes 

150 ---------- 

151 value : str 

152 Literal value. 

153 """ 

154 def __init__(self, value: str): 

155 Node.__init__(self) 

156 self.value = value 

157 

158 def visit(self, visitor: TreeVisitor) -> Any: 

159 # Docstring inherited from Node.visit 

160 return visitor.visitStringLiteral(self.value, self) 

161 

162 def __str__(self) -> str: 

163 return "'{value}'".format(**vars(self)) 

164 

165 

166class TimeLiteral(Node): 

167 """Node representing time literal. 

168 

169 Attributes 

170 ---------- 

171 value : `astropy.time.Time` 

172 Literal string value. 

173 """ 

174 def __init__(self, value: astropy.time.Time): 

175 Node.__init__(self) 

176 self.value = value 

177 

178 def visit(self, visitor: TreeVisitor) -> Any: 

179 # Docstring inherited from Node.visit 

180 return visitor.visitTimeLiteral(self.value, self) 

181 

182 def __str__(self) -> str: 

183 return "'{value}'".format(**vars(self)) 

184 

185 

186class NumericLiteral(Node): 

187 """Node representing string literal. 

188 

189 We do not convert literals to numbers, their text representation 

190 is stored literally. 

191 

192 Attributes 

193 ---------- 

194 value : str 

195 Literal value. 

196 """ 

197 def __init__(self, value: str): 

198 Node.__init__(self) 

199 self.value = value 

200 

201 def visit(self, visitor: TreeVisitor) -> Any: 

202 # Docstring inherited from Node.visit 

203 return visitor.visitNumericLiteral(self.value, self) 

204 

205 def __str__(self) -> str: 

206 return "{value}".format(**vars(self)) 

207 

208 

209class Identifier(Node): 

210 """Node representing identifier. 

211 

212 Value of the identifier is its name, it may contain zero or one dot 

213 character. 

214 

215 Attributes 

216 ---------- 

217 name : str 

218 Identifier name. 

219 """ 

220 def __init__(self, name: str): 

221 Node.__init__(self) 

222 self.name = name 

223 

224 def visit(self, visitor: TreeVisitor) -> Any: 

225 # Docstring inherited from Node.visit 

226 return visitor.visitIdentifier(self.name, self) 

227 

228 def __str__(self) -> str: 

229 return "{name}".format(**vars(self)) 

230 

231 

232class RangeLiteral(Node): 

233 """Node representing range literal appearing in `IN` list. 

234 

235 Range literal defines a range of integer numbers with start and 

236 end of the range (with inclusive end) and optional stride value 

237 (default is 1). 

238 

239 Attributes 

240 ---------- 

241 start : `int` 

242 Start value of a range. 

243 stop : `int` 

244 End value of a range, inclusive, same or higher than ``start``. 

245 stride : `int` or `None`, optional 

246 Stride value, must be positive, can be `None` which means that stride 

247 was not specified. Consumers are supposed to treat `None` the same way 

248 as stride=1 but for some consumers it may be useful to know that 

249 stride was missing from literal. 

250 """ 

251 def __init__(self, start: int, stop: int, stride: Optional[int] = None): 

252 self.start = start 

253 self.stop = stop 

254 self.stride = stride 

255 

256 def visit(self, visitor: TreeVisitor) -> Any: 

257 # Docstring inherited from Node.visit 

258 return visitor.visitRangeLiteral(self.start, self.stop, self.stride, self) 

259 

260 def __str__(self) -> str: 

261 res = f"{self.start}..{self.stop}" + (f":{self.stride}" if self.stride else "") 

262 return res 

263 

264 

265class IsIn(Node): 

266 """Node representing IN or NOT IN expression. 

267 

268 Attributes 

269 ---------- 

270 lhs : Node 

271 Left-hand side of the operation 

272 values : list of Node 

273 List of values on the right side. 

274 not_in : bool 

275 If `True` then it is NOT IN expression, otherwise it is IN expression. 

276 """ 

277 def __init__(self, lhs: Node, values: List[Node], not_in: bool = False): 

278 Node.__init__(self, (lhs,) + tuple(values)) 

279 self.lhs = lhs 

280 self.values = values 

281 self.not_in = not_in 

282 

283 def visit(self, visitor: TreeVisitor) -> Any: 

284 # Docstring inherited from Node.visit 

285 lhs = self.lhs.visit(visitor) 

286 values = [value.visit(visitor) for value in self.values] 

287 return visitor.visitIsIn(lhs, values, self.not_in, self) 

288 

289 def __str__(self) -> str: 

290 values = ", ".join(str(x) for x in self.values) 

291 not_in = "" 

292 if self.not_in: 

293 not_in = "NOT " 

294 return "{lhs} {not_in}IN ({values})".format(lhs=self.lhs, 

295 not_in=not_in, 

296 values=values) 

297 

298 

299class Parens(Node): 

300 """Node representing parenthesized expression. 

301 

302 Attributes 

303 ---------- 

304 expr : Node 

305 Expression inside parentheses. 

306 """ 

307 def __init__(self, expr: Node): 

308 Node.__init__(self, (expr,)) 

309 self.expr = expr 

310 

311 def visit(self, visitor: TreeVisitor) -> Any: 

312 # Docstring inherited from Node.visit 

313 expr = self.expr.visit(visitor) 

314 return visitor.visitParens(expr, self) 

315 

316 def __str__(self) -> str: 

317 return "({expr})".format(**vars(self)) 

318 

319 

320class TupleNode(Node): 

321 """Node representing a tuple, sequence of parenthesized expressions. 

322 

323 Tuple is used to represent time ranges, for now parser supports tuples 

324 with two items, though this class can be used to represent different 

325 number of items in sequence. 

326 

327 Attributes 

328 ---------- 

329 items : tuple of Node 

330 Expressions inside parentheses. 

331 """ 

332 def __init__(self, items: Tuple[Node, ...]): 

333 Node.__init__(self, items) 

334 self.items = items 

335 

336 def visit(self, visitor: TreeVisitor) -> Any: 

337 # Docstring inherited from Node.visit 

338 items = tuple(item.visit(visitor) for item in self.items) 

339 return visitor.visitTupleNode(items, self) 

340 

341 def __str__(self) -> str: 

342 items = ", ".join(str(item) for item in self.items) 

343 return f"({items})" 

344 

345 

346class FunctionCall(Node): 

347 """Node representing a function call. 

348 

349 Attributes 

350 ---------- 

351 function : `str` 

352 Name of the function. 

353 args : `list` [ `Node` ] 

354 Arguments passed to function. 

355 """ 

356 def __init__(self, function: str, args: List[Node]): 

357 Node.__init__(self, tuple(args)) 

358 self.name = function 

359 self.args = args[:] 

360 

361 def visit(self, visitor: TreeVisitor) -> Any: 

362 # Docstring inherited from Node.visit 

363 args = [arg.visit(visitor) for arg in self.args] 

364 return visitor.visitFunctionCall(self.name, args, self) 

365 

366 def __str__(self) -> str: 

367 args = ", ".join(str(arg) for arg in self.args) 

368 return f"{self.name}({args})" 

369 

370 

371class PointNode(Node): 

372 """Node representing a point, (ra, dec) pair. 

373 

374 Attributes 

375 ---------- 

376 ra : `Node` 

377 Node representing ra value. 

378 dec : `Node` 

379 Node representing dec value. 

380 """ 

381 def __init__(self, ra: Node, dec: Node): 

382 Node.__init__(self, (ra, dec)) 

383 self.ra = ra 

384 self.dec = dec 

385 

386 def visit(self, visitor: TreeVisitor) -> Any: 

387 # Docstring inherited from Node.visit 

388 ra = self.ra.visit(visitor) 

389 dec = self.dec.visit(visitor) 

390 return visitor.visitPointNode(ra, dec, self) 

391 

392 def __str__(self) -> str: 

393 return f"POINT({self.ra}, {self.dec})" 

394 

395 

396def function_call(function: str, args: List[Node]) -> Node: 

397 """Factory method for nodes representing function calls. 

398 

399 Attributes 

400 ---------- 

401 function : `str` 

402 Name of the function. 

403 args : `list` [ `Node` ] 

404 Arguments passed to function. 

405 

406 Notes 

407 ----- 

408 Our parser supports arbitrary functions with arbitrary list of parameters. 

409 For now the main purpose of the syntax is to support POINT(ra, dec) 

410 construct, and to simplify implementation of visitors we define special 

411 type of node for that. This method makes `PointNode` instance for that 

412 special function call and generic `FunctionCall` instance for all other 

413 functions. 

414 """ 

415 if function.upper() == "POINT": 

416 if len(args) != 2: 

417 raise ValueError("POINT requires two arguments (ra, dec)") 

418 return PointNode(*args) 

419 else: 

420 # generic function call 

421 return FunctionCall(function, args)