Coverage for python/lsst/daf/butler/registry/queries/expressions/parser/exprTree.py: 38%
140 statements
« prev ^ index » next coverage.py v6.5.0, created at 2023-01-04 02:04 -0800
« prev ^ index » next coverage.py v6.5.0, created at 2023-01-04 02:04 -0800
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/>.
22"""Module which defines classes for intermediate representation of the
23expression tree produced by parser.
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"""
32from __future__ import annotations
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]
50# -------------------------------
51# Imports of standard modules --
52# -------------------------------
53from abc import ABC, abstractmethod
54from typing import TYPE_CHECKING, Any, List, Optional, Tuple
56# -----------------------------
57# Imports for other modules --
58# -----------------------------
60# ----------------------------------
61# Local non-exported definitions --
62# ----------------------------------
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
67 from .treeVisitor import TreeVisitor
69# ------------------------
70# Exported definitions --
71# ------------------------
74class Node(ABC):
75 """Base class of IR node in expression tree.
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.
82 Attributes
83 ----------
84 children : tuple of :py:class:`Node`
85 Possibly empty list of sub-nodes.
86 """
88 def __init__(self, children: Tuple[Node, ...] | None = None):
89 self.children = tuple(children or ())
91 @abstractmethod
92 def visit(self, visitor: TreeVisitor) -> Any:
93 """Implement Visitor pattern for parsed tree.
95 Parameters
96 ----------
97 visitor : `TreeVisitor`
98 Instance of visitor type.
99 """
102class BinaryOp(Node):
103 """Node representing binary operator.
105 This class is used for representing all binary operators including
106 arithmetic and boolean operations.
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 """
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
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)
130 def __str__(self) -> str:
131 return "{lhs} {op} {rhs}".format(**vars(self))
134class UnaryOp(Node):
135 """Node representing unary operator.
137 This class is used for representing all unary operators including
138 arithmetic and boolean operations.
140 Attributes
141 ----------
142 op : str
143 Operator name, e.g. '+', 'NOT'
144 operand : Node
145 Operand.
146 """
148 def __init__(self, op: str, operand: Node):
149 Node.__init__(self, (operand,))
150 self.op = op
151 self.operand = operand
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)
158 def __str__(self) -> str:
159 return "{op} {operand}".format(**vars(self))
162class StringLiteral(Node):
163 """Node representing string literal.
165 Attributes
166 ----------
167 value : str
168 Literal value.
169 """
171 def __init__(self, value: str):
172 Node.__init__(self)
173 self.value = value
175 def visit(self, visitor: TreeVisitor) -> Any:
176 # Docstring inherited from Node.visit
177 return visitor.visitStringLiteral(self.value, self)
179 def __str__(self) -> str:
180 return "'{value}'".format(**vars(self))
183class TimeLiteral(Node):
184 """Node representing time literal.
186 Attributes
187 ----------
188 value : `astropy.time.Time`
189 Literal string value.
190 """
192 def __init__(self, value: astropy.time.Time):
193 Node.__init__(self)
194 self.value = value
196 def visit(self, visitor: TreeVisitor) -> Any:
197 # Docstring inherited from Node.visit
198 return visitor.visitTimeLiteral(self.value, self)
200 def __str__(self) -> str:
201 return "'{value}'".format(**vars(self))
204class NumericLiteral(Node):
205 """Node representing string literal.
207 We do not convert literals to numbers, their text representation
208 is stored literally.
210 Attributes
211 ----------
212 value : str
213 Literal value.
214 """
216 def __init__(self, value: str):
217 Node.__init__(self)
218 self.value = value
220 def visit(self, visitor: TreeVisitor) -> Any:
221 # Docstring inherited from Node.visit
222 return visitor.visitNumericLiteral(self.value, self)
224 def __str__(self) -> str:
225 return "{value}".format(**vars(self))
228class Identifier(Node):
229 """Node representing identifier.
231 Value of the identifier is its name, it may contain zero or one dot
232 character.
234 Attributes
235 ----------
236 name : str
237 Identifier name.
238 """
240 def __init__(self, name: str):
241 Node.__init__(self)
242 self.name = name
244 def visit(self, visitor: TreeVisitor) -> Any:
245 # Docstring inherited from Node.visit
246 return visitor.visitIdentifier(self.name, self)
248 def __str__(self) -> str:
249 return "{name}".format(**vars(self))
252class RangeLiteral(Node):
253 """Node representing range literal appearing in `IN` list.
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).
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 """
272 def __init__(self, start: int, stop: int, stride: Optional[int] = None):
273 self.start = start
274 self.stop = stop
275 self.stride = stride
277 def visit(self, visitor: TreeVisitor) -> Any:
278 # Docstring inherited from Node.visit
279 return visitor.visitRangeLiteral(self.start, self.stop, self.stride, self)
281 def __str__(self) -> str:
282 res = f"{self.start}..{self.stop}" + (f":{self.stride}" if self.stride else "")
283 return res
286class IsIn(Node):
287 """Node representing IN or NOT IN expression.
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 """
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
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)
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)
319class Parens(Node):
320 """Node representing parenthesized expression.
322 Attributes
323 ----------
324 expr : Node
325 Expression inside parentheses.
326 """
328 def __init__(self, expr: Node):
329 Node.__init__(self, (expr,))
330 self.expr = expr
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)
337 def __str__(self) -> str:
338 return "({expr})".format(**vars(self))
341class TupleNode(Node):
342 """Node representing a tuple, sequence of parenthesized expressions.
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.
348 Attributes
349 ----------
350 items : tuple of Node
351 Expressions inside parentheses.
352 """
354 def __init__(self, items: Tuple[Node, ...]):
355 Node.__init__(self, items)
356 self.items = items
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)
363 def __str__(self) -> str:
364 items = ", ".join(str(item) for item in self.items)
365 return f"({items})"
368class FunctionCall(Node):
369 """Node representing a function call.
371 Attributes
372 ----------
373 function : `str`
374 Name of the function.
375 args : `list` [ `Node` ]
376 Arguments passed to function.
377 """
379 def __init__(self, function: str, args: List[Node]):
380 Node.__init__(self, tuple(args))
381 self.name = function
382 self.args = args[:]
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)
389 def __str__(self) -> str:
390 args = ", ".join(str(arg) for arg in self.args)
391 return f"{self.name}({args})"
394class PointNode(Node):
395 """Node representing a point, (ra, dec) pair.
397 Attributes
398 ----------
399 ra : `Node`
400 Node representing ra value.
401 dec : `Node`
402 Node representing dec value.
403 """
405 def __init__(self, ra: Node, dec: Node):
406 Node.__init__(self, (ra, dec))
407 self.ra = ra
408 self.dec = dec
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)
416 def __str__(self) -> str:
417 return f"POINT({self.ra}, {self.dec})"
420def function_call(function: str, args: List[Node]) -> Node:
421 """Factory method for nodes representing function calls.
423 Attributes
424 ----------
425 function : `str`
426 Name of the function.
427 args : `list` [ `Node` ]
428 Arguments passed to function.
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)