Coverage for python/lsst/daf/relation/_operations/_slice.py: 32%
87 statements
« prev ^ index » next coverage.py v6.5.0, created at 2023-01-13 10:31 +0000
« prev ^ index » next coverage.py v6.5.0, created at 2023-01-13 10:31 +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/>.
22from __future__ import annotations
24__all__ = ("Slice",)
26import dataclasses
27from collections.abc import Set
28from typing import TYPE_CHECKING, Literal, final
30from .._columns import ColumnTag
31from .._operation_relations import UnaryOperationRelation
32from .._unary_operation import Identity, RowFilter, UnaryCommutator, UnaryOperation
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
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 """
46 start: int = 0
47 """First index to include the output relation (`int`).
48 """
50 stop: int | None = None
51 """One past the last index to include in the output relation
52 (`int` or `None`).
53 """
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}.")
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
66 @property
67 def columns_required(self) -> Set[ColumnTag]:
68 # Docstring inherited.
69 return frozenset()
71 @property
72 def is_empty_invariant(self) -> Literal[False]:
73 # Docstring inherited.
74 return False
76 @property
77 def is_order_dependent(self) -> Literal[True]:
78 # Docstring inherited.
79 return True
81 @property
82 def is_count_dependent(self) -> bool:
83 # Docstring inherited.
84 return True
86 def __str__(self) -> str:
87 return f"slice[{self.start}:{self.stop}]"
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)
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)
103 def then(self, next: Slice) -> Slice:
104 """Compose this slice with another one.
106 Parameters
107 ----------
108 next : `Slice`
109 Slice that acts after ``self``.
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)
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)
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)
152 def commute(self, current: UnaryOperationRelation) -> UnaryCommutator:
153 # Docstring inherited.
154 from ._calculation import Calculation
155 from ._projection import Projection
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 )
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