Coverage for python/lsst/daf/relation/_operation_relations.py: 51%
87 statements
« prev ^ index » next coverage.py v6.5.0, created at 2023-01-10 02:26 -0800
« prev ^ index » next coverage.py v6.5.0, created at 2023-01-10 02:26 -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/>.
22from __future__ import annotations
24__all__ = (
25 "UnaryOperationRelation",
26 "BinaryOperationRelation",
27)
29import dataclasses
30from collections.abc import Set
31from typing import TYPE_CHECKING, Literal, final
33from ._leaf_relation import LeafRelation
34from ._relation import BaseRelation, Relation
36if TYPE_CHECKING: 36 ↛ 37line 36 didn't jump to line 37, because the condition on line 36 was never true
37 from ._binary_operation import BinaryOperation
38 from ._columns import ColumnTag
39 from ._engine import Engine
40 from ._unary_operation import UnaryOperation
43@dataclasses.dataclass(frozen=True)
44class UnaryOperationRelation(BaseRelation):
45 """A concrete `Relation` that represents the action of a `UnaryOperation`
46 on a target `Relation`.
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 """
54 operation: UnaryOperation
55 """The unary operation whose action this relation represents
56 (`UnaryOperation`).
57 """
59 target: Relation
60 """The target relation the operation acts upon (`Relation`).
61 """
63 columns: Set[ColumnTag] = dataclasses.field(repr=False, compare=False)
64 """The columns in this relation (`~collections.abc.Set` [ `ColumnTag` ] ).
65 """
67 @property
68 def payload(self) -> None:
69 """The engine-specific contents of the relation.
71 This is always `None` for binary operation relations.
72 """
73 return None
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
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
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)
94 @property
95 def max_rows(self) -> int | None:
96 """The maximum number of rows this relation might have (`int` or
97 `None`).
99 This is `None` for relations whose size is not bounded from above.
100 """
101 return self.operation.applied_max_rows(self.target)
103 def __str__(self) -> str:
104 return f"{self.operation!s}({self.target!s})"
106 def reapply(self, target: Relation) -> Relation:
107 """Reapply this relation's operation with a possibly-new target.
109 Parameters
110 ----------
111 target : `Relation`
112 Possibly-new target.
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)
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.
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 """
140 operation: BinaryOperation
141 """The binary operation whose action this relation represents
142 (`BinaryOperation`).
143 """
145 lhs: Relation
146 """One target relation the operation acts upon (`Relation`).
147 """
149 rhs: Relation
150 """The other target relation the operation acts upon (`Relation`).
151 """
153 columns: Set[ColumnTag] = dataclasses.field(repr=False, compare=False)
154 """The columns in this relation (`~collections.abc.Set` [ `ColumnTag` ] ).
155 """
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
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
171 @property
172 def payload(self) -> None:
173 """The engine-specific contents of the relation.
175 This is always `None` for binary operation relations.
176 """
177 return None
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)
184 @property
185 def max_rows(self) -> int | None:
186 """The maximum number of rows this relation might have (`int` or
187 `None`).
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)
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}"
210 def reapply(self, lhs: Relation, rhs: Relation) -> Relation:
211 """Reapply this relation's operation with possibly-new targets.
213 Parameters
214 ----------
215 lhs : `Relation`
216 One possibly-new target.
217 rhs : `Relation`
218 The other possibly-new target.
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)