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
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
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__ = ['Node', 'BinaryOp', 'FunctionCall', 'Identifier', 'IsIn', 'NumericLiteral',
35 'Parens', 'RangeLiteral', 'StringLiteral', 'TimeLiteral', 'TupleNode',
36 'UnaryOp', 'function_call']
38# -------------------------------
39# Imports of standard modules --
40# -------------------------------
41from abc import ABC, abstractmethod
42from typing import Any, List, Optional, Tuple, TYPE_CHECKING
44# -----------------------------
45# Imports for other modules --
46# -----------------------------
48# ----------------------------------
49# Local non-exported definitions --
50# ----------------------------------
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
56# ------------------------
57# Exported definitions --
58# ------------------------
61class Node(ABC):
62 """Base class of IR node in expression tree.
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.
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 ())
77 @abstractmethod
78 def visit(self, visitor: TreeVisitor) -> Any:
79 """Implement Visitor pattern for parsed tree.
81 Parameters
82 ----------
83 visitor : `TreeVisitor`
84 Instance of visitor type.
85 """
88class BinaryOp(Node):
89 """Node representing binary operator.
91 This class is used for representing all binary operators including
92 arithmetic and boolean operations.
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
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)
115 def __str__(self) -> str:
116 return "{lhs} {op} {rhs}".format(**vars(self))
119class UnaryOp(Node):
120 """Node representing unary operator.
122 This class is used for representing all unary operators including
123 arithmetic and boolean operations.
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
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)
142 def __str__(self) -> str:
143 return "{op} {operand}".format(**vars(self))
146class StringLiteral(Node):
147 """Node representing string literal.
149 Attributes
150 ----------
151 value : str
152 Literal value.
153 """
154 def __init__(self, value: str):
155 Node.__init__(self)
156 self.value = value
158 def visit(self, visitor: TreeVisitor) -> Any:
159 # Docstring inherited from Node.visit
160 return visitor.visitStringLiteral(self.value, self)
162 def __str__(self) -> str:
163 return "'{value}'".format(**vars(self))
166class TimeLiteral(Node):
167 """Node representing time literal.
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
178 def visit(self, visitor: TreeVisitor) -> Any:
179 # Docstring inherited from Node.visit
180 return visitor.visitTimeLiteral(self.value, self)
182 def __str__(self) -> str:
183 return "'{value}'".format(**vars(self))
186class NumericLiteral(Node):
187 """Node representing string literal.
189 We do not convert literals to numbers, their text representation
190 is stored literally.
192 Attributes
193 ----------
194 value : str
195 Literal value.
196 """
197 def __init__(self, value: str):
198 Node.__init__(self)
199 self.value = value
201 def visit(self, visitor: TreeVisitor) -> Any:
202 # Docstring inherited from Node.visit
203 return visitor.visitNumericLiteral(self.value, self)
205 def __str__(self) -> str:
206 return "{value}".format(**vars(self))
209class Identifier(Node):
210 """Node representing identifier.
212 Value of the identifier is its name, it may contain zero or one dot
213 character.
215 Attributes
216 ----------
217 name : str
218 Identifier name.
219 """
220 def __init__(self, name: str):
221 Node.__init__(self)
222 self.name = name
224 def visit(self, visitor: TreeVisitor) -> Any:
225 # Docstring inherited from Node.visit
226 return visitor.visitIdentifier(self.name, self)
228 def __str__(self) -> str:
229 return "{name}".format(**vars(self))
232class RangeLiteral(Node):
233 """Node representing range literal appearing in `IN` list.
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).
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
256 def visit(self, visitor: TreeVisitor) -> Any:
257 # Docstring inherited from Node.visit
258 return visitor.visitRangeLiteral(self.start, self.stop, self.stride, self)
260 def __str__(self) -> str:
261 res = f"{self.start}..{self.stop}" + (f":{self.stride}" if self.stride else "")
262 return res
265class IsIn(Node):
266 """Node representing IN or NOT IN expression.
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
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)
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)
299class Parens(Node):
300 """Node representing parenthesized expression.
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
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)
316 def __str__(self) -> str:
317 return "({expr})".format(**vars(self))
320class TupleNode(Node):
321 """Node representing a tuple, sequence of parenthesized expressions.
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.
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
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)
341 def __str__(self) -> str:
342 items = ", ".join(str(item) for item in self.items)
343 return f"({items})"
346class FunctionCall(Node):
347 """Node representing a function call.
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[:]
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)
366 def __str__(self) -> str:
367 args = ", ".join(str(arg) for arg in self.args)
368 return f"{self.name}({args})"
371class PointNode(Node):
372 """Node representing a point, (ra, dec) pair.
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
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)
392 def __str__(self) -> str:
393 return f"POINT({self.ra}, {self.dec})"
396def function_call(function: str, args: List[Node]) -> Node:
397 """Factory method for nodes representing function calls.
399 Attributes
400 ----------
401 function : `str`
402 Name of the function.
403 args : `list` [ `Node` ]
404 Arguments passed to function.
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)