Coverage for python/lsst/daf/butler/registry/queries/expressions/parser/exprTree.py: 38%
137 statements
« prev ^ index » next coverage.py v7.4.4, created at 2024-03-30 02:51 -0700
« prev ^ index » next coverage.py v7.4.4, created at 2024-03-30 02:51 -0700
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/>.
28"""Module which defines classes for intermediate representation of the
29expression tree produced by parser.
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"""
38from __future__ import annotations
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]
56# -------------------------------
57# Imports of standard modules --
58# -------------------------------
59from abc import ABC, abstractmethod
60from typing import TYPE_CHECKING, Any
62# -----------------------------
63# Imports for other modules --
64# -----------------------------
66# ----------------------------------
67# Local non-exported definitions --
68# ----------------------------------
70if TYPE_CHECKING:
71 import astropy.time
73 from .treeVisitor import TreeVisitor
75# ------------------------
76# Exported definitions --
77# ------------------------
80class Node(ABC):
81 """Base class of IR node in expression tree.
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.
88 Parameters
89 ----------
90 children : tuple of :py:class:`Node`
91 Possibly empty list of sub-nodes.
92 """
94 def __init__(self, children: tuple[Node, ...] | None = None):
95 self.children = tuple(children or ())
97 @abstractmethod
98 def visit(self, visitor: TreeVisitor) -> Any:
99 """Implement Visitor pattern for parsed tree.
101 Parameters
102 ----------
103 visitor : `TreeVisitor`
104 Instance of visitor type.
105 """
108class BinaryOp(Node):
109 """Node representing binary operator.
111 This class is used for representing all binary operators including
112 arithmetic and boolean operations.
114 Parameters
115 ----------
116 lhs : `Node`
117 Left-hand side of the operation.
118 op : `str`
119 Operator name, e.g. '+', 'OR'.
120 rhs : `Node`
121 Right-hand side of the operation.
122 """
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
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)
136 def __str__(self) -> str:
137 return "{lhs} {op} {rhs}".format(**vars(self))
140class UnaryOp(Node):
141 """Node representing unary operator.
143 This class is used for representing all unary operators including
144 arithmetic and boolean operations.
146 Parameters
147 ----------
148 op : `str`
149 Operator name, e.g. '+', 'NOT'.
150 operand : `Node`
151 Operand.
152 """
154 def __init__(self, op: str, operand: Node):
155 Node.__init__(self, (operand,))
156 self.op = op
157 self.operand = operand
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)
164 def __str__(self) -> str:
165 return "{op} {operand}".format(**vars(self))
168class StringLiteral(Node):
169 """Node representing string literal.
171 Parameters
172 ----------
173 value : `str`
174 Literal value.
175 """
177 def __init__(self, value: str):
178 Node.__init__(self)
179 self.value = value
181 def visit(self, visitor: TreeVisitor) -> Any:
182 # Docstring inherited from Node.visit
183 return visitor.visitStringLiteral(self.value, self)
185 def __str__(self) -> str:
186 return "'{value}'".format(**vars(self))
189class TimeLiteral(Node):
190 """Node representing time literal.
192 Parameters
193 ----------
194 value : `astropy.time.Time`
195 Literal string value.
196 """
198 def __init__(self, value: astropy.time.Time):
199 Node.__init__(self)
200 self.value = value
202 def visit(self, visitor: TreeVisitor) -> Any:
203 # Docstring inherited from Node.visit
204 return visitor.visitTimeLiteral(self.value, self)
206 def __str__(self) -> str:
207 return "'{value}'".format(**vars(self))
210class NumericLiteral(Node):
211 """Node representing string literal.
213 We do not convert literals to numbers, their text representation
214 is stored literally.
216 Parameters
217 ----------
218 value : str
219 Literal value.
220 """
222 def __init__(self, value: str):
223 Node.__init__(self)
224 self.value = value
226 def visit(self, visitor: TreeVisitor) -> Any:
227 # Docstring inherited from Node.visit
228 return visitor.visitNumericLiteral(self.value, self)
230 def __str__(self) -> str:
231 return "{value}".format(**vars(self))
234class Identifier(Node):
235 """Node representing identifier.
237 Value of the identifier is its name, it may contain zero or one dot
238 character.
240 Parameters
241 ----------
242 name : str
243 Identifier name.
244 """
246 def __init__(self, name: str):
247 Node.__init__(self)
248 self.name = name
250 def visit(self, visitor: TreeVisitor) -> Any:
251 # Docstring inherited from Node.visit
252 return visitor.visitIdentifier(self.name, self)
254 def __str__(self) -> str:
255 return "{name}".format(**vars(self))
258class RangeLiteral(Node):
259 """Node representing range literal appearing in `IN` list.
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).
265 Parameters
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 """
278 def __init__(self, start: int, stop: int, stride: int | None = None):
279 self.start = start
280 self.stop = stop
281 self.stride = stride
283 def visit(self, visitor: TreeVisitor) -> Any:
284 # Docstring inherited from Node.visit
285 return visitor.visitRangeLiteral(self.start, self.stop, self.stride, self)
287 def __str__(self) -> str:
288 res = f"{self.start}..{self.stop}" + (f":{self.stride}" if self.stride else "")
289 return res
292class IsIn(Node):
293 """Node representing IN or NOT IN expression.
295 Parameters
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 """
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
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)
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})"
325class Parens(Node):
326 """Node representing parenthesized expression.
328 Parameters
329 ----------
330 expr : `Node`
331 Expression inside parentheses.
332 """
334 def __init__(self, expr: Node):
335 Node.__init__(self, (expr,))
336 self.expr = expr
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)
343 def __str__(self) -> str:
344 return "({expr})".format(**vars(self))
347class TupleNode(Node):
348 """Node representing a tuple, sequence of parenthesized expressions.
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.
354 Parameters
355 ----------
356 items : `tuple` of `Node`
357 Expressions inside parentheses.
358 """
360 def __init__(self, items: tuple[Node, ...]):
361 Node.__init__(self, items)
362 self.items = items
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)
369 def __str__(self) -> str:
370 items = ", ".join(str(item) for item in self.items)
371 return f"({items})"
374class FunctionCall(Node):
375 """Node representing a function call.
377 Parameters
378 ----------
379 function : `str`
380 Name of the function.
381 args : `list` [ `Node` ]
382 Arguments passed to function.
383 """
385 def __init__(self, function: str, args: list[Node]):
386 Node.__init__(self, tuple(args))
387 self.name = function
388 self.args = args[:]
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)
395 def __str__(self) -> str:
396 args = ", ".join(str(arg) for arg in self.args)
397 return f"{self.name}({args})"
400class PointNode(Node):
401 """Node representing a point, (ra, dec) pair.
403 Parameters
404 ----------
405 ra : `Node`
406 Node representing ra value.
407 dec : `Node`
408 Node representing dec value.
409 """
411 def __init__(self, ra: Node, dec: Node):
412 Node.__init__(self, (ra, dec))
413 self.ra = ra
414 self.dec = dec
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)
422 def __str__(self) -> str:
423 return f"POINT({self.ra}, {self.dec})"
426def function_call(function: str, args: list[Node]) -> Node:
427 """Return node representing function calls.
429 Parameters
430 ----------
431 function : `str`
432 Name of the function.
433 args : `list` [ `Node` ]
434 Arguments passed to function.
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)