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

137 statements  

« prev     ^ index     » next       coverage.py v7.3.2, created at 2023-12-05 11:07 +0000

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 software is dual licensed under the GNU General Public License and also 

10# under a 3-clause BSD license. Recipients may choose which of these licenses 

11# to use; please see the files gpl-3.0.txt and/or bsd_license.txt, 

12# respectively. If you choose the GPL option then the following text applies 

13# (but note that there is still no warranty even if you opt for BSD instead): 

14# 

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

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

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

18# (at your option) any later version. 

19# 

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

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

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

23# GNU General Public License for more details. 

24# 

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

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

27 

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

29expression tree produced by parser. 

30 

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

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

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

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

35to database directly. 

36""" 

37 

38from __future__ import annotations 

39 

40__all__ = [ 

41 "Node", 

42 "BinaryOp", 

43 "FunctionCall", 

44 "Identifier", 

45 "IsIn", 

46 "NumericLiteral", 

47 "Parens", 

48 "RangeLiteral", 

49 "StringLiteral", 

50 "TimeLiteral", 

51 "TupleNode", 

52 "UnaryOp", 

53 "function_call", 

54] 

55 

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

57# Imports of standard modules -- 

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

59from abc import ABC, abstractmethod 

60from typing import TYPE_CHECKING, Any 

61 

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

63# Imports for other modules -- 

64# ----------------------------- 

65 

66# ---------------------------------- 

67# Local non-exported definitions -- 

68# ---------------------------------- 

69 

70if TYPE_CHECKING: 

71 import astropy.time 

72 

73 from .treeVisitor import TreeVisitor 

74 

75# ------------------------ 

76# Exported definitions -- 

77# ------------------------ 

78 

79 

80class Node(ABC): 

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

82 

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

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

85 node so that visiting code can navigate whole tree without 

86 knowing exact types of each node. 

87 

88 Attributes 

89 ---------- 

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

91 Possibly empty list of sub-nodes. 

92 """ 

93 

94 def __init__(self, children: tuple[Node, ...] | None = None): 

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

96 

97 @abstractmethod 

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

99 """Implement Visitor pattern for parsed tree. 

100 

101 Parameters 

102 ---------- 

103 visitor : `TreeVisitor` 

104 Instance of visitor type. 

105 """ 

106 

107 

108class BinaryOp(Node): 

109 """Node representing binary operator. 

110 

111 This class is used for representing all binary operators including 

112 arithmetic and boolean operations. 

113 

114 Attributes 

115 ---------- 

116 lhs : Node 

117 Left-hand side of the operation 

118 rhs : Node 

119 Right-hand side of the operation 

120 op : str 

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

122 """ 

123 

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

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

126 self.lhs = lhs 

127 self.op = op 

128 self.rhs = rhs 

129 

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

131 # Docstring inherited from Node.visit 

132 lhs = self.lhs.visit(visitor) 

133 rhs = self.rhs.visit(visitor) 

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

135 

136 def __str__(self) -> str: 

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

138 

139 

140class UnaryOp(Node): 

141 """Node representing unary operator. 

142 

143 This class is used for representing all unary operators including 

144 arithmetic and boolean operations. 

145 

146 Attributes 

147 ---------- 

148 op : str 

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

150 operand : Node 

151 Operand. 

152 """ 

153 

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

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

156 self.op = op 

157 self.operand = operand 

158 

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

160 # Docstring inherited from Node.visit 

161 operand = self.operand.visit(visitor) 

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

163 

164 def __str__(self) -> str: 

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

166 

167 

168class StringLiteral(Node): 

169 """Node representing string literal. 

170 

171 Attributes 

172 ---------- 

173 value : str 

174 Literal value. 

175 """ 

176 

177 def __init__(self, value: str): 

178 Node.__init__(self) 

179 self.value = value 

180 

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

182 # Docstring inherited from Node.visit 

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

184 

185 def __str__(self) -> str: 

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

187 

188 

189class TimeLiteral(Node): 

190 """Node representing time literal. 

191 

192 Attributes 

193 ---------- 

194 value : `astropy.time.Time` 

195 Literal string value. 

196 """ 

197 

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

199 Node.__init__(self) 

200 self.value = value 

201 

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

203 # Docstring inherited from Node.visit 

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

205 

206 def __str__(self) -> str: 

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

208 

209 

210class NumericLiteral(Node): 

211 """Node representing string literal. 

212 

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

214 is stored literally. 

215 

216 Attributes 

217 ---------- 

218 value : str 

219 Literal value. 

220 """ 

221 

222 def __init__(self, value: str): 

223 Node.__init__(self) 

224 self.value = value 

225 

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

227 # Docstring inherited from Node.visit 

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

229 

230 def __str__(self) -> str: 

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

232 

233 

234class Identifier(Node): 

235 """Node representing identifier. 

236 

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

238 character. 

239 

240 Attributes 

241 ---------- 

242 name : str 

243 Identifier name. 

244 """ 

245 

246 def __init__(self, name: str): 

247 Node.__init__(self) 

248 self.name = name 

249 

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

251 # Docstring inherited from Node.visit 

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

253 

254 def __str__(self) -> str: 

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

256 

257 

258class RangeLiteral(Node): 

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

260 

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

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

263 (default is 1). 

264 

265 Attributes 

266 ---------- 

267 start : `int` 

268 Start value of a range. 

269 stop : `int` 

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

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

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

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

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

275 stride was missing from literal. 

276 """ 

277 

278 def __init__(self, start: int, stop: int, stride: int | None = None): 

279 self.start = start 

280 self.stop = stop 

281 self.stride = stride 

282 

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

284 # Docstring inherited from Node.visit 

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

286 

287 def __str__(self) -> str: 

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

289 return res 

290 

291 

292class IsIn(Node): 

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

294 

295 Attributes 

296 ---------- 

297 lhs : Node 

298 Left-hand side of the operation 

299 values : list of Node 

300 List of values on the right side. 

301 not_in : bool 

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

303 """ 

304 

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

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

307 self.lhs = lhs 

308 self.values = values 

309 self.not_in = not_in 

310 

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

312 # Docstring inherited from Node.visit 

313 lhs = self.lhs.visit(visitor) 

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

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

316 

317 def __str__(self) -> str: 

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

319 not_in = "" 

320 if self.not_in: 

321 not_in = "NOT " 

322 return f"{self.lhs} {not_in}IN ({values})" 

323 

324 

325class Parens(Node): 

326 """Node representing parenthesized expression. 

327 

328 Attributes 

329 ---------- 

330 expr : Node 

331 Expression inside parentheses. 

332 """ 

333 

334 def __init__(self, expr: Node): 

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

336 self.expr = expr 

337 

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

339 # Docstring inherited from Node.visit 

340 expr = self.expr.visit(visitor) 

341 return visitor.visitParens(expr, self) 

342 

343 def __str__(self) -> str: 

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

345 

346 

347class TupleNode(Node): 

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

349 

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

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

352 number of items in sequence. 

353 

354 Attributes 

355 ---------- 

356 items : tuple of Node 

357 Expressions inside parentheses. 

358 """ 

359 

360 def __init__(self, items: tuple[Node, ...]): 

361 Node.__init__(self, items) 

362 self.items = items 

363 

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

365 # Docstring inherited from Node.visit 

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

367 return visitor.visitTupleNode(items, self) 

368 

369 def __str__(self) -> str: 

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

371 return f"({items})" 

372 

373 

374class FunctionCall(Node): 

375 """Node representing a function call. 

376 

377 Attributes 

378 ---------- 

379 function : `str` 

380 Name of the function. 

381 args : `list` [ `Node` ] 

382 Arguments passed to function. 

383 """ 

384 

385 def __init__(self, function: str, args: list[Node]): 

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

387 self.name = function 

388 self.args = args[:] 

389 

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

391 # Docstring inherited from Node.visit 

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

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

394 

395 def __str__(self) -> str: 

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

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

398 

399 

400class PointNode(Node): 

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

402 

403 Attributes 

404 ---------- 

405 ra : `Node` 

406 Node representing ra value. 

407 dec : `Node` 

408 Node representing dec value. 

409 """ 

410 

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

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

413 self.ra = ra 

414 self.dec = dec 

415 

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

417 # Docstring inherited from Node.visit 

418 ra = self.ra.visit(visitor) 

419 dec = self.dec.visit(visitor) 

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

421 

422 def __str__(self) -> str: 

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

424 

425 

426def function_call(function: str, args: list[Node]) -> Node: 

427 """Return node representing function calls. 

428 

429 Attributes 

430 ---------- 

431 function : `str` 

432 Name of the function. 

433 args : `list` [ `Node` ] 

434 Arguments passed to function. 

435 

436 Notes 

437 ----- 

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

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

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

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

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

443 functions. 

444 """ 

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

446 if len(args) != 2: 

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

448 return PointNode(*args) 

449 else: 

450 # generic function call 

451 return FunctionCall(function, args)