Coverage for python/lsst/pipe/base/tests/mocks/_data_id_match.py: 35%

59 statements  

« prev     ^ index     » next       coverage.py v7.2.7, created at 2023-06-14 02:10 -0700

1# This file is part of pipe_base. 

2# 

3# Developed for the LSST Data Management System. 

4# This product includes software developed by the LSST Project 

5# (http://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 <http://www.gnu.org/licenses/>. 

21 

22from __future__ import annotations 

23 

24__all__ = ["DataIdMatch"] 

25 

26import operator 

27from collections.abc import Callable 

28from typing import Any 

29 

30import astropy.time 

31from lsst.daf.butler import DataId 

32from lsst.daf.butler.registry.queries.expressions.parser import Node, ParserYacc, TreeVisitor # type: ignore 

33 

34 

35class _DataIdMatchTreeVisitor(TreeVisitor): 

36 """Expression tree visitor which evaluates expression using values from 

37 `~lsst.daf.butler.DataId`. 

38 """ 

39 

40 def __init__(self, dataId: DataId): 

41 self.dataId = dataId 

42 

43 def visitNumericLiteral(self, value: str, node: Node) -> Any: 

44 # docstring is inherited from base class 

45 try: 

46 return int(value) 

47 except ValueError: 

48 return float(value) 

49 

50 def visitStringLiteral(self, value: str, node: Node) -> Any: 

51 # docstring is inherited from base class 

52 return value 

53 

54 def visitTimeLiteral(self, value: astropy.time.Time, node: Node) -> Any: 

55 # docstring is inherited from base class 

56 return value 

57 

58 def visitRangeLiteral(self, start: int, stop: int, stride: int | None, node: Node) -> Any: 

59 # docstring is inherited from base class 

60 if stride is None: 

61 return range(start, stop + 1) 

62 else: 

63 return range(start, stop + 1, stride) 

64 

65 def visitIdentifier(self, name: str, node: Node) -> Any: 

66 # docstring is inherited from base class 

67 return self.dataId[name] 

68 

69 def visitUnaryOp(self, operator_name: str, operand: Any, node: Node) -> Any: 

70 # docstring is inherited from base class 

71 operators: dict[str, Callable[[Any], Any]] = { 

72 "NOT": operator.not_, 

73 "+": operator.pos, 

74 "-": operator.neg, 

75 } 

76 return operators[operator_name](operand) 

77 

78 def visitBinaryOp(self, operator_name: str, lhs: Any, rhs: Any, node: Node) -> Any: 

79 # docstring is inherited from base class 

80 operators = { 

81 "OR": operator.or_, 

82 "AND": operator.and_, 

83 "+": operator.add, 

84 "-": operator.sub, 

85 "*": operator.mul, 

86 "/": operator.truediv, 

87 "%": operator.mod, 

88 "=": operator.eq, 

89 "!=": operator.ne, 

90 "<": operator.lt, 

91 ">": operator.gt, 

92 "<=": operator.le, 

93 ">=": operator.ge, 

94 } 

95 return operators[operator_name](lhs, rhs) 

96 

97 def visitIsIn(self, lhs: Any, values: list[Any], not_in: bool, node: Node) -> Any: 

98 # docstring is inherited from base class 

99 is_in = True 

100 for value in values: 

101 if not isinstance(value, range): 

102 value = [value] 

103 if lhs in value: 

104 break 

105 else: 

106 is_in = False 

107 if not_in: 

108 is_in = not is_in 

109 return is_in 

110 

111 def visitParens(self, expression: Any, node: Node) -> Any: 

112 # docstring is inherited from base class 

113 return expression 

114 

115 def visitTupleNode(self, items: tuple[Any, ...], node: Node) -> Any: 

116 # docstring is inherited from base class 

117 raise NotImplementedError() 

118 

119 def visitFunctionCall(self, name: str, args: list[Any], node: Node) -> Any: 

120 # docstring is inherited from base class 

121 raise NotImplementedError() 

122 

123 def visitPointNode(self, ra: Any, dec: Any, node: Node) -> Any: 

124 # docstring is inherited from base class 

125 raise NotImplementedError() 

126 

127 

128class DataIdMatch: 

129 """Class that can match DataId against the user-defined string expression. 

130 

131 Parameters 

132 ---------- 

133 expression : `str` 

134 User-defined expression, supports syntax defined by daf_butler 

135 expression parser. Maps identifiers in the expression to the values of 

136 DataId. 

137 """ 

138 

139 def __init__(self, expression: str): 

140 parser = ParserYacc() 

141 self.expression = expression 

142 self.tree = parser.parse(expression) 

143 

144 def match(self, dataId: DataId) -> bool: 

145 """Match DataId contents against the expression. 

146 

147 Parameters 

148 ---------- 

149 dataId : `~lsst.daf.butler.DataId` 

150 DataId that is matched against an expression. 

151 

152 Returns 

153 ------- 

154 match : `bool` 

155 Result of expression evaluation. 

156 

157 Raises 

158 ------ 

159 KeyError 

160 Raised when identifier in expression is not defined for given 

161 `~lsst.daf.butler.DataId`. 

162 TypeError 

163 Raised when expression evaluates to a non-boolean type or when 

164 operation in expression cannot be performed on operand types. 

165 NotImplementedError 

166 Raised when expression includes valid but unsupported syntax, e.g. 

167 function call. 

168 """ 

169 visitor = _DataIdMatchTreeVisitor(dataId) 

170 result = self.tree.visit(visitor) 

171 if not isinstance(result, bool): 

172 raise TypeError(f"Expression '{self.expression}' returned non-boolean object {type(result)}") 

173 return result