Coverage for python/lsst/daf/relation/_binary_operation.py: 66%

43 statements  

« prev     ^ index     » next       coverage.py v6.5.0, created at 2023-02-22 02:59 -0800

1# This file is part of daf_relation. 

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__ = ("BinaryOperation", "IgnoreOne") 

25 

26import dataclasses 

27from abc import ABC, abstractmethod 

28from collections.abc import Set 

29from typing import final 

30 

31from ._columns import ColumnTag 

32from ._relation import Relation 

33 

34 

35class BinaryOperation(ABC): 

36 """An abstract base class for operations that act on a pair of relations. 

37 

38 Notes 

39 ----- 

40 A `BinaryOperation` represents the operation itself; the combination of an 

41 operation and the "lhs" and "rhs" relations it acts on to form a new 

42 relation is represented by the `BinaryOperationRelation` class, which 

43 should always be performed via a call to the `apply` method (or something 

44 that calls it, like the convenience methods on the `Relation` class). In 

45 many cases, applying a `BinaryOperation` doesn't return something involving 

46 the original operation, because of some combination of defaulted-parameter 

47 population and simplification, and there are even some `BinaryOperation` 

48 classes that should never actually appear in a `BinaryOperationRelation`. 

49 

50 `BinaryOperation` cannot be subclassed by external code. 

51 

52 All concrete `BinaryOperation` types are frozen, equality-comparable 

53 `dataclasses`. They also provide a very concise `str` representation (in 

54 addition to the dataclass-provided `repr`) suitable for summarizing an 

55 entire relation tree. 

56 

57 See Also 

58 -------- 

59 :ref:`lsst.daf.relation-overview-operations` 

60 """ 

61 

62 def __init_subclass__(cls) -> None: 

63 assert cls.__name__ in { 

64 "Join", 

65 "Chain", 

66 "IgnoreOne", 

67 }, "BinaryOperation inheritance is closed to predefined types in daf_relation." 

68 

69 @final 

70 def apply(self, lhs: Relation, rhs: Relation) -> Relation: 

71 """Create a new relation that represents the action of this operation 

72 on a pair of existing relations. 

73 

74 Parameters 

75 ---------- 

76 lhs : `Relation` 

77 One relation the operation will act on. 

78 rhs : `Relation` 

79 The other relation the operation will act on. 

80 

81 Returns 

82 ------- 

83 new_relation : `Relation` 

84 Relation that includes this operation. This may be ``self`` if the 

85 operation is a no-op, and it may not be a `BinaryOperationRelation` 

86 holding this operation (or even a similar one) if the operation was 

87 inserted earlier in the tree via commutation relations. 

88 

89 Raises 

90 ------ 

91 ColumnError 

92 Raised if the operation could not be applied due to problems with 

93 the target relations' columns. 

94 EngineError 

95 Raised if the operation could not be applied due to problems with 

96 the target relations' engine(s). 

97 """ 

98 operation = self._begin_apply(lhs, rhs) 

99 return lhs.engine.append_binary(operation, lhs, rhs) 

100 

101 def _begin_apply(self, lhs: Relation, rhs: Relation) -> BinaryOperation: 

102 """A customization hook for the beginning of operation application. 

103 

104 Parameter 

105 --------- 

106 lhs : `Relation` 

107 One relation the operation should act on. 

108 rhs : `Relation` 

109 The other relation the operation should act on. 

110 

111 Returns 

112 ------- 

113 operation : `BinaryOperation` 

114 The operation to actually apply. The default implementation 

115 returns ``self``. 

116 

117 Notes 

118 ----- 

119 This method provides an opportunity for operations to establish any 

120 invariants that must be satisfied only when the operation is part of 

121 a relation. 

122 """ 

123 return self 

124 

125 def _finish_apply(self, lhs: Relation, rhs: Relation) -> Relation: 

126 """A customization hook for the end of operation application. 

127 

128 Parameters 

129 ---------- 

130 target : `Relation` 

131 Relation the operation will act upon directly. 

132 

133 Returns 

134 ------- 

135 applied : `Relation` 

136 Result of applying this operation to the given target. Usually - 

137 but not always - a `UnaryOperationRelation` that holds ``self`` and 

138 ``target``. 

139 

140 Notes 

141 ----- 

142 This method provides an opportunity for operations to change the kind 

143 of relation produced (the default implementation constructs a 

144 `BinaryOperationRelation`). 

145 """ 

146 from ._operation_relations import BinaryOperationRelation 

147 

148 return BinaryOperationRelation( 

149 operation=self, 

150 lhs=lhs, 

151 rhs=rhs, 

152 columns=self.applied_columns(lhs, rhs), 

153 ) 

154 

155 @abstractmethod 

156 def applied_columns(self, lhs: Relation, rhs: Relation) -> Set[ColumnTag]: 

157 """Return the columns of the relation that results from applying this 

158 operation to the given targets. 

159 

160 Parameters 

161 ---------- 

162 lhs : `Relation` 

163 On relation the operation will act on. 

164 rhs : `Relation` 

165 The other relation the operation will act on. 

166 

167 Returns 

168 ------- 

169 columns : `~collections.abc.Set` [ `ColumnTag` ] 

170 Columns the new relation would have. 

171 """ 

172 raise NotImplementedError() 

173 

174 @abstractmethod 

175 def applied_min_rows(self, lhs: Relation, rhs: Relation) -> int: 

176 """Return the minimum number of rows of the relation that results from 

177 applying this operation to the given targets. 

178 

179 Parameters 

180 ---------- 

181 lhs : `Relation` 

182 On relation the operation will act on. 

183 rhs : `Relation` 

184 The other relation the operation will act on. 

185 

186 Returns 

187 ------- 

188 min_rows : `int` 

189 Minimum number of rows the new relation would have. 

190 """ 

191 raise NotImplementedError() 

192 

193 @abstractmethod 

194 def applied_max_rows(self, lhs: Relation, rhs: Relation) -> int | None: 

195 """Return the maximum number of rows of the relation that results from 

196 applying this operation to the given target. 

197 

198 Parameters 

199 ---------- 

200 lhs : `Relation` 

201 On relation the operation will act on. 

202 rhs : `Relation` 

203 The other relation the operation will act on. 

204 

205 Returns 

206 ------- 

207 max_rows : `int` or `None` 

208 Maximum number of rows the new relation would have. 

209 """ 

210 raise NotImplementedError() 

211 

212 

213@dataclasses.dataclass 

214class IgnoreOne(BinaryOperation): 

215 """A binary operation that passes through one of its operands and ignores 

216 the other. 

217 """ 

218 

219 ignore_lhs: bool 

220 """Whether the ignored operand is the left-hand-side one (`bool`). 

221 """ 

222 

223 def applied_columns(self, lhs: Relation, rhs: Relation) -> Set[ColumnTag]: 

224 # Docstring inherited. 

225 return self._finish_apply(lhs, rhs).columns 

226 

227 def applied_min_rows(self, lhs: Relation, rhs: Relation) -> int: 

228 # Docstring inherited. 

229 return self._finish_apply(lhs, rhs).min_rows 

230 

231 def applied_max_rows(self, lhs: Relation, rhs: Relation) -> int | None: 

232 # Docstring inherited. 

233 return self._finish_apply(lhs, rhs).max_rows 

234 

235 def _finish_apply(self, lhs: Relation, rhs: Relation) -> Relation: 

236 # Docstring inherited. 

237 if self.ignore_lhs: 

238 return rhs 

239 else: 

240 return lhs