Coverage for python/lsst/daf/relation/_operation_relations.py: 62%

82 statements  

« prev     ^ index     » next       coverage.py v7.4.4, created at 2024-04-09 10:34 +0000

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__ = ( 

25 "UnaryOperationRelation", 

26 "BinaryOperationRelation", 

27) 

28 

29import dataclasses 

30from collections.abc import Set 

31from typing import TYPE_CHECKING, Literal, final 

32 

33from ._leaf_relation import LeafRelation 

34from ._relation import BaseRelation, Relation 

35 

36if TYPE_CHECKING: 

37 from ._binary_operation import BinaryOperation 

38 from ._columns import ColumnTag 

39 from ._engine import Engine 

40 from ._unary_operation import UnaryOperation 

41 

42 

43@dataclasses.dataclass(frozen=True) 

44class UnaryOperationRelation(BaseRelation): 

45 """A concrete `Relation` that represents the action of a `UnaryOperation` 

46 on a target `Relation`. 

47 

48 `UnaryOperationRelation` instances must only be constructed via calls to 

49 `UnaryOperation.apply` or `Relation` convenience methods. Direct calls to 

50 the constructor are not guaranteed to satisfy invariants imposed by the 

51 operations classes. 

52 """ 

53 

54 operation: UnaryOperation 

55 """The unary operation whose action this relation represents 

56 (`UnaryOperation`). 

57 """ 

58 

59 target: Relation 

60 """The target relation the operation acts upon (`Relation`). 

61 """ 

62 

63 columns: Set[ColumnTag] = dataclasses.field(repr=False, compare=False) 

64 """The columns in this relation (`~collections.abc.Set` [ `ColumnTag` ] ). 

65 """ 

66 

67 @property 

68 def payload(self) -> None: 

69 """The engine-specific contents of the relation. 

70 

71 This is always `None` for binary operation relations. 

72 """ 

73 return None 

74 

75 @property 

76 def is_locked(self) -> Literal[False]: 

77 """Whether this relation and those upstream of it should be considered 

78 fixed by tree-manipulation algorithms (`bool`). 

79 """ 

80 return False 

81 

82 @property 

83 def engine(self) -> Engine: 

84 """The engine that is responsible for interpreting this relation 

85 (`Engine`). 

86 """ 

87 return self.target.engine 

88 

89 @property 

90 def min_rows(self) -> int: 

91 """The minimum number of rows this relation might have (`int`).""" 

92 return self.operation.applied_min_rows(self.target) 

93 

94 @property 

95 def max_rows(self) -> int | None: 

96 """The maximum number of rows this relation might have (`int` or 

97 `None`). 

98 

99 This is `None` for relations whose size is not bounded from above. 

100 """ 

101 return self.operation.applied_max_rows(self.target) 

102 

103 def __str__(self) -> str: 

104 return f"{self.operation!s}({self.target!s})" 

105 

106 def reapply(self, target: Relation) -> Relation: 

107 """Reapply this relation's operation with a possibly-new target. 

108 

109 Parameters 

110 ---------- 

111 target : `Relation` 

112 Possibly-new target. 

113 

114 Returns 

115 ------- 

116 relation : `Relation` 

117 Relation that applies `operation` to the new target. Will be 

118 ``self`` if ``target is self.target`` (this avoidance of an 

119 unnecessary new `UnaryOperationRelation` instance is the main 

120 reason for this convenience method's existence). 

121 """ 

122 if target is self.target: 

123 return self 

124 else: 

125 return self.operation.apply(target) 

126 

127 

128@final 

129@dataclasses.dataclass(frozen=True) 

130class BinaryOperationRelation(BaseRelation): 

131 """A concrete `Relation` that represents the action of a `BinaryOperation` 

132 on a pair of target `Relation` objects. 

133 

134 `BinaryOperationRelation` instances must only be constructed via calls to 

135 `BinaryOperation.apply` or `Relation` convenience methods. Direct calls to 

136 the constructor are not guaranteed to satisfy invariants imposed by the 

137 operations classes. 

138 """ 

139 

140 operation: BinaryOperation 

141 """The binary operation whose action this relation represents 

142 (`BinaryOperation`). 

143 """ 

144 

145 lhs: Relation 

146 """One target relation the operation acts upon (`Relation`). 

147 """ 

148 

149 rhs: Relation 

150 """The other target relation the operation acts upon (`Relation`). 

151 """ 

152 

153 columns: Set[ColumnTag] = dataclasses.field(repr=False, compare=False) 

154 """The columns in this relation (`~collections.abc.Set` [ `ColumnTag` ] ). 

155 """ 

156 

157 @property 

158 def is_locked(self) -> Literal[False]: 

159 """Whether this relation and those upstream of it should be considered 

160 fixed by tree-manipulation algorithms (`bool`). 

161 """ 

162 return False 

163 

164 @property 

165 def engine(self) -> Engine: 

166 """The engine that is responsible for interpreting this relation 

167 (`Engine`). 

168 """ 

169 return self.lhs.engine 

170 

171 @property 

172 def payload(self) -> None: 

173 """The engine-specific contents of the relation. 

174 

175 This is always `None` for binary operation relations. 

176 """ 

177 return None 

178 

179 @property 

180 def min_rows(self) -> int: 

181 """The minimum number of rows this relation might have (`int`).""" 

182 return self.operation.applied_min_rows(self.lhs, self.rhs) 

183 

184 @property 

185 def max_rows(self) -> int | None: 

186 """The maximum number of rows this relation might have (`int` or 

187 `None`). 

188 

189 This is `None` for relations whose size is not bounded from above. 

190 """ 

191 return self.operation.applied_max_rows(self.lhs, self.rhs) 

192 

193 def __str__(self) -> str: 

194 lhs_str = f"({self.lhs!s})" 

195 match self.lhs: 

196 case LeafRelation(): 

197 lhs_str = str(self.lhs) 

198 case BinaryOperationRelation(operation=lhs_operation): 

199 if type(lhs_operation) is type(self.operation): # noqa: E721 

200 lhs_str = str(self.lhs) 

201 rhs_str = f"({self.rhs!s})" 

202 match self.rhs: 

203 case LeafRelation(): 

204 rhs_str = str(self.rhs) 

205 case BinaryOperationRelation(operation=rhs_operation): 

206 if type(rhs_operation) is type(self.operation): # noqa: E721 

207 rhs_str = str(self.rhs) 

208 return f"{lhs_str} {self.operation!s} {rhs_str}" 

209 

210 def reapply(self, lhs: Relation, rhs: Relation) -> Relation: 

211 """Reapply this relation's operation with possibly-new targets. 

212 

213 Parameters 

214 ---------- 

215 lhs : `Relation` 

216 One possibly-new target. 

217 rhs : `Relation` 

218 The other possibly-new target. 

219 

220 Returns 

221 ------- 

222 relation : `Relation` 

223 Relation that applies `operation` to the new targets. Will be 

224 ``self`` if ``lhs is self.lhs and rhs is self.rhs`` (this avoidance 

225 of an unnecessary new `BinaryOperationRelation` instance is the 

226 main reason for this convenience method's existence). 

227 """ 

228 if lhs is self.lhs and rhs is self.rhs: 

229 return self 

230 else: 

231 return self.operation.apply(lhs, rhs)