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

69 statements  

« prev     ^ index     » next       coverage.py v7.13.5, created at 2026-04-26 08:59 +0000

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

27 

28from __future__ import annotations 

29 

30__all__ = ["DataIdMatch"] 

31 

32import operator 

33from collections.abc import Callable 

34from typing import Any 

35from uuid import UUID 

36 

37import astropy.time 

38 

39from lsst.daf.butler import DataId 

40from lsst.daf.butler.queries.expressions.parser import Node, TreeVisitor, parse_expression 

41 

42 

43class _DataIdMatchTreeVisitor(TreeVisitor): 

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

45 `~lsst.daf.butler.DataId`. 

46 """ 

47 

48 def __init__(self, dataId: DataId): 

49 self.dataId = dataId 

50 

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

52 # docstring is inherited from base class 

53 try: 

54 return int(value) 

55 except ValueError: 

56 return float(value) 

57 

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

59 # docstring is inherited from base class 

60 return value 

61 

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

63 # docstring is inherited from base class 

64 return value 

65 

66 def visitUuidLiteral(self, value: UUID, node: Node) -> Any: 

67 # docstring is inherited from base class 

68 return value 

69 

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

71 # docstring is inherited from base class 

72 if stride is None: 

73 return range(start, stop + 1) 

74 else: 

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

76 

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

78 # docstring is inherited from base class 

79 return self.dataId[name] 

80 

81 def visitBind(self, name: str, node: Node) -> Any: 

82 # docstring is inherited from base class 

83 raise NotImplementedError() 

84 

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

86 # docstring is inherited from base class 

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

88 "NOT": operator.not_, 

89 "+": operator.pos, 

90 "-": operator.neg, 

91 } 

92 return operators[operator_name](operand) 

93 

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

95 # docstring is inherited from base class 

96 operators = { 

97 "OR": operator.or_, 

98 "AND": operator.and_, 

99 "+": operator.add, 

100 "-": operator.sub, 

101 "*": operator.mul, 

102 "/": operator.truediv, 

103 "%": operator.mod, 

104 "=": operator.eq, 

105 "!=": operator.ne, 

106 "<": operator.lt, 

107 ">": operator.gt, 

108 "<=": operator.le, 

109 ">=": operator.ge, 

110 } 

111 return operators[operator_name](lhs, rhs) 

112 

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

114 # docstring is inherited from base class 

115 is_in = True 

116 for value in values: 

117 if not isinstance(value, range): 

118 value = [value] 

119 if lhs in value: 

120 break 

121 else: 

122 is_in = False 

123 if not_in: 

124 is_in = not is_in 

125 return is_in 

126 

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

128 # docstring is inherited from base class 

129 return expression 

130 

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

132 # docstring is inherited from base class 

133 raise NotImplementedError() 

134 

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

136 # docstring is inherited from base class 

137 raise NotImplementedError() 

138 

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

140 # docstring is inherited from base class 

141 raise NotImplementedError() 

142 

143 def visitCircleNode(self, ra: Any, dec: Any, radius: Any, node: Node) -> Any: 

144 # docstring is inherited from base class 

145 raise NotImplementedError() 

146 

147 def visitBoxNode(self, ra: Any, dec: Any, width: Any, height: Any, node: Node) -> Any: 

148 # docstring is inherited from base class 

149 raise NotImplementedError() 

150 

151 def visitPolygonNode(self, vertices: list[tuple[Any, Any]], node: Node) -> Any: 

152 # docstring is inherited from base class 

153 raise NotImplementedError() 

154 

155 def visitRegionNode(self, pos: Any, node: Node) -> Any: 

156 # docstring is inherited from base class 

157 raise NotImplementedError() 

158 

159 def visitGlobNode(self, expression: Any, pattern: Any, node: Node) -> Any: 

160 # docstring is inherited from base class 

161 raise NotImplementedError() 

162 

163 

164class DataIdMatch: 

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

166 

167 Parameters 

168 ---------- 

169 expression : `str` 

170 User-defined expression, supports syntax defined by daf_butler 

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

172 DataId. 

173 """ 

174 

175 def __init__(self, expression: str): 

176 self.expression = expression 

177 self.tree = parse_expression(expression) 

178 

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

180 """Match DataId contents against the expression. 

181 

182 Parameters 

183 ---------- 

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

185 DataId that is matched against an expression. 

186 

187 Returns 

188 ------- 

189 match : `bool` 

190 Result of expression evaluation. 

191 

192 Raises 

193 ------ 

194 KeyError 

195 Raised when identifier in expression is not defined for given 

196 `~lsst.daf.butler.DataId`. 

197 TypeError 

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

199 operation in expression cannot be performed on operand types. 

200 NotImplementedError 

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

202 function call. 

203 """ 

204 if self.tree is None: 

205 return True 

206 visitor = _DataIdMatchTreeVisitor(dataId) 

207 result = self.tree.visit(visitor) 

208 if not isinstance(result, bool): 

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

210 return result