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__ = [ 

35 "Node", 

36 "BinaryOp", 

37 "FunctionCall", 

38 "Identifier", 

39 "IsIn", 

40 "NumericLiteral", 

41 "Parens", 

42 "RangeLiteral", 

43 "StringLiteral", 

44 "TimeLiteral", 

45 "TupleNode", 

46 "UnaryOp", 

47 "function_call", 

48] 

49 

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

51# Imports of standard modules -- 

52# ------------------------------- 

53from abc import ABC, abstractmethod 

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

55 

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

57# Imports for other modules -- 

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

59 

60# ---------------------------------- 

61# Local non-exported definitions -- 

62# ---------------------------------- 

63 

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

65 import astropy.time 

66 

67 from .treeVisitor import TreeVisitor 

68 

69# ------------------------ 

70# Exported definitions -- 

71# ------------------------ 

72 

73 

74class Node(ABC): 

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

76 

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

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

79 node so that visiting code can navigate whole tree without 

80 knowing exact types of each node. 

81 

82 Attributes 

83 ---------- 

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

85 Possibly empty list of sub-nodes. 

86 """ 

87 

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

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

90 

91 @abstractmethod 

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

93 """Implement Visitor pattern for parsed tree. 

94 

95 Parameters 

96 ---------- 

97 visitor : `TreeVisitor` 

98 Instance of visitor type. 

99 """ 

100 

101 

102class BinaryOp(Node): 

103 """Node representing binary operator. 

104 

105 This class is used for representing all binary operators including 

106 arithmetic and boolean operations. 

107 

108 Attributes 

109 ---------- 

110 lhs : Node 

111 Left-hand side of the operation 

112 rhs : Node 

113 Right-hand side of the operation 

114 op : str 

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

116 """ 

117 

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

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

120 self.lhs = lhs 

121 self.op = op 

122 self.rhs = rhs 

123 

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

125 # Docstring inherited from Node.visit 

126 lhs = self.lhs.visit(visitor) 

127 rhs = self.rhs.visit(visitor) 

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

129 

130 def __str__(self) -> str: 

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

132 

133 

134class UnaryOp(Node): 

135 """Node representing unary operator. 

136 

137 This class is used for representing all unary operators including 

138 arithmetic and boolean operations. 

139 

140 Attributes 

141 ---------- 

142 op : str 

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

144 operand : Node 

145 Operand. 

146 """ 

147 

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

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

150 self.op = op 

151 self.operand = operand 

152 

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

154 # Docstring inherited from Node.visit 

155 operand = self.operand.visit(visitor) 

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

157 

158 def __str__(self) -> str: 

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

160 

161 

162class StringLiteral(Node): 

163 """Node representing string literal. 

164 

165 Attributes 

166 ---------- 

167 value : str 

168 Literal value. 

169 """ 

170 

171 def __init__(self, value: str): 

172 Node.__init__(self) 

173 self.value = value 

174 

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

176 # Docstring inherited from Node.visit 

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

178 

179 def __str__(self) -> str: 

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

181 

182 

183class TimeLiteral(Node): 

184 """Node representing time literal. 

185 

186 Attributes 

187 ---------- 

188 value : `astropy.time.Time` 

189 Literal string value. 

190 """ 

191 

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

193 Node.__init__(self) 

194 self.value = value 

195 

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

197 # Docstring inherited from Node.visit 

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

199 

200 def __str__(self) -> str: 

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

202 

203 

204class NumericLiteral(Node): 

205 """Node representing string literal. 

206 

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

208 is stored literally. 

209 

210 Attributes 

211 ---------- 

212 value : str 

213 Literal value. 

214 """ 

215 

216 def __init__(self, value: str): 

217 Node.__init__(self) 

218 self.value = value 

219 

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

221 # Docstring inherited from Node.visit 

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

223 

224 def __str__(self) -> str: 

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

226 

227 

228class Identifier(Node): 

229 """Node representing identifier. 

230 

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

232 character. 

233 

234 Attributes 

235 ---------- 

236 name : str 

237 Identifier name. 

238 """ 

239 

240 def __init__(self, name: str): 

241 Node.__init__(self) 

242 self.name = name 

243 

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

245 # Docstring inherited from Node.visit 

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

247 

248 def __str__(self) -> str: 

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

250 

251 

252class RangeLiteral(Node): 

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

254 

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

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

257 (default is 1). 

258 

259 Attributes 

260 ---------- 

261 start : `int` 

262 Start value of a range. 

263 stop : `int` 

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

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

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

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

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

269 stride was missing from literal. 

270 """ 

271 

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

273 self.start = start 

274 self.stop = stop 

275 self.stride = stride 

276 

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

278 # Docstring inherited from Node.visit 

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

280 

281 def __str__(self) -> str: 

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

283 return res 

284 

285 

286class IsIn(Node): 

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

288 

289 Attributes 

290 ---------- 

291 lhs : Node 

292 Left-hand side of the operation 

293 values : list of Node 

294 List of values on the right side. 

295 not_in : bool 

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

297 """ 

298 

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

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

301 self.lhs = lhs 

302 self.values = values 

303 self.not_in = not_in 

304 

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

306 # Docstring inherited from Node.visit 

307 lhs = self.lhs.visit(visitor) 

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

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

310 

311 def __str__(self) -> str: 

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

313 not_in = "" 

314 if self.not_in: 

315 not_in = "NOT " 

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

317 

318 

319class Parens(Node): 

320 """Node representing parenthesized expression. 

321 

322 Attributes 

323 ---------- 

324 expr : Node 

325 Expression inside parentheses. 

326 """ 

327 

328 def __init__(self, expr: Node): 

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

330 self.expr = expr 

331 

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

333 # Docstring inherited from Node.visit 

334 expr = self.expr.visit(visitor) 

335 return visitor.visitParens(expr, self) 

336 

337 def __str__(self) -> str: 

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

339 

340 

341class TupleNode(Node): 

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

343 

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

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

346 number of items in sequence. 

347 

348 Attributes 

349 ---------- 

350 items : tuple of Node 

351 Expressions inside parentheses. 

352 """ 

353 

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

355 Node.__init__(self, items) 

356 self.items = items 

357 

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

359 # Docstring inherited from Node.visit 

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

361 return visitor.visitTupleNode(items, self) 

362 

363 def __str__(self) -> str: 

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

365 return f"({items})" 

366 

367 

368class FunctionCall(Node): 

369 """Node representing a function call. 

370 

371 Attributes 

372 ---------- 

373 function : `str` 

374 Name of the function. 

375 args : `list` [ `Node` ] 

376 Arguments passed to function. 

377 """ 

378 

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

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

381 self.name = function 

382 self.args = args[:] 

383 

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

385 # Docstring inherited from Node.visit 

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

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

388 

389 def __str__(self) -> str: 

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

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

392 

393 

394class PointNode(Node): 

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

396 

397 Attributes 

398 ---------- 

399 ra : `Node` 

400 Node representing ra value. 

401 dec : `Node` 

402 Node representing dec value. 

403 """ 

404 

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

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

407 self.ra = ra 

408 self.dec = dec 

409 

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

411 # Docstring inherited from Node.visit 

412 ra = self.ra.visit(visitor) 

413 dec = self.dec.visit(visitor) 

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

415 

416 def __str__(self) -> str: 

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

418 

419 

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

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

422 

423 Attributes 

424 ---------- 

425 function : `str` 

426 Name of the function. 

427 args : `list` [ `Node` ] 

428 Arguments passed to function. 

429 

430 Notes 

431 ----- 

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

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

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

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

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

437 functions. 

438 """ 

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

440 if len(args) != 2: 

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

442 return PointNode(*args) 

443 else: 

444 # generic function call 

445 return FunctionCall(function, args)