Coverage for python/lsst/daf/relation/_operations/_slice.py: 32%

87 statements  

« prev     ^ index     » next       coverage.py v6.5.0, created at 2023-03-31 02:35 -0700

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

25 

26import dataclasses 

27from collections.abc import Set 

28from typing import TYPE_CHECKING, Literal, final 

29 

30from .._columns import ColumnTag 

31from .._operation_relations import UnaryOperationRelation 

32from .._unary_operation import Identity, RowFilter, UnaryCommutator, UnaryOperation 

33 

34if TYPE_CHECKING: 34 ↛ 35line 34 didn't jump to line 35, because the condition on line 34 was never true

35 from .._engine import Engine 

36 from .._relation import Relation 

37 

38 

39@final 

40@dataclasses.dataclass(frozen=True) 

41class Slice(RowFilter): 

42 """A relation relation that filters rows that are outside a range of 

43 positional indices. 

44 """ 

45 

46 start: int = 0 

47 """First index to include the output relation (`int`). 

48 """ 

49 

50 stop: int | None = None 

51 """One past the last index to include in the output relation 

52 (`int` or `None`). 

53 """ 

54 

55 def __post_init__(self) -> None: 

56 if self.start < 0: 

57 raise ValueError(f"Slice start {self.start} is negative.") 

58 if self.stop is not None and self.stop < self.start: 

59 raise ValueError(f"Slice stop {self.stop} is less than its start {self.start}.") 

60 

61 @property 

62 def limit(self) -> int | None: 

63 """The maximum number of rows to include (`int` or `None`).""" 

64 return None if self.stop is None else self.stop - self.start 

65 

66 @property 

67 def columns_required(self) -> Set[ColumnTag]: 

68 # Docstring inherited. 

69 return frozenset() 

70 

71 @property 

72 def is_empty_invariant(self) -> Literal[False]: 

73 # Docstring inherited. 

74 return False 

75 

76 @property 

77 def is_order_dependent(self) -> Literal[True]: 

78 # Docstring inherited. 

79 return True 

80 

81 @property 

82 def is_count_dependent(self) -> bool: 

83 # Docstring inherited. 

84 return True 

85 

86 def __str__(self) -> str: 

87 return f"slice[{self.start}:{self.stop}]" 

88 

89 def _begin_apply( 

90 self, target: Relation, preferred_engine: Engine | None 

91 ) -> tuple[UnaryOperation, Engine]: 

92 # Docstring inherited. 

93 if not self.start and self.stop is None: 

94 return Identity(), target.engine 

95 return super()._begin_apply(target, preferred_engine) 

96 

97 def _finish_apply(self, target: Relation) -> Relation: 

98 # Docstring inherited. 

99 if not self.start and self.stop is None: 

100 return target 

101 return super()._finish_apply(target) 

102 

103 def then(self, next: Slice) -> Slice: 

104 """Compose this slice with another one. 

105 

106 Parameters 

107 ---------- 

108 next : `Slice` 

109 Slice that acts after ``self``. 

110 

111 Returns 

112 ------- 

113 composition : `Slice` 

114 Slice that is equivalent to ``self`` and ``next`` being applied 

115 back-to-back. 

116 """ 

117 new_start = self.start + next.start 

118 if self.stop is None: 

119 if next.stop is None: 

120 new_stop = None 

121 else: 

122 new_stop = next.stop + self.start 

123 else: 

124 if next.stop is None: 

125 new_stop = self.stop 

126 else: 

127 new_stop = min(self.stop, next.stop + self.start) 

128 return Slice(new_start, new_stop) 

129 

130 def applied_min_rows(self, target: Relation) -> int: 

131 # Docstring inherited. 

132 if self.stop is not None: 

133 stop = min(self.stop, target.min_rows) 

134 else: 

135 stop = target.min_rows 

136 return max(stop - self.start, 0) 

137 

138 def applied_max_rows(self, target: Relation) -> int | None: 

139 # Docstring inherited. 

140 if self.stop is not None: 

141 if target.max_rows is not None: 

142 stop = min(self.stop, target.max_rows) 

143 else: 

144 stop = self.stop 

145 else: 

146 if target.max_rows is not None: 

147 stop = target.max_rows 

148 else: 

149 return None 

150 return max(stop - self.start, 0) 

151 

152 def commute(self, current: UnaryOperationRelation) -> UnaryCommutator: 

153 # Docstring inherited. 

154 from ._calculation import Calculation 

155 from ._projection import Projection 

156 

157 match current.operation: 

158 case Projection() | Calculation(): 

159 return UnaryCommutator(first=self, second=current.operation) 

160 case _: 

161 return UnaryCommutator( 

162 first=None, 

163 second=current.operation, 

164 done=False, 

165 messages=( 

166 f"Slice only commutes with Projection and Calculation, not {current.operation}", 

167 ), 

168 ) 

169 

170 def simplify(self, upstream: UnaryOperation) -> UnaryOperation | None: 

171 # Docstring inherited. 

172 if not self.start and self.stop is None: 

173 return upstream 

174 match upstream: 

175 case Slice(): 

176 return upstream.then(self) 

177 return None