Coverage for python/lsst/daf/relation/_binary_operation.py: 75%
40 statements
« prev ^ index » next coverage.py v7.3.1, created at 2023-09-27 10:10 +0000
« prev ^ index » next coverage.py v7.3.1, created at 2023-09-27 10:10 +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__ = ("BinaryOperation", "IgnoreOne")
26import dataclasses
27from abc import ABC, abstractmethod
28from collections.abc import Set
29from typing import final
31from ._columns import ColumnTag
32from ._relation import Relation
35class BinaryOperation(ABC):
36 """An abstract base class for operations that act on a pair of relations.
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`.
50 `BinaryOperation` cannot be subclassed by external code.
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.
57 See Also
58 --------
59 :ref:`lsst.daf.relation-overview-operations`
60 """
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."
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.
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.
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.
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)
101 def _begin_apply(self, lhs: Relation, rhs: Relation) -> BinaryOperation:
102 """A customization hook for the beginning of operation application.
104 Parameters
105 ----------
106 lhs : `Relation`
107 One relation the operation should act on.
108 rhs : `Relation`
109 The other relation the operation should act on.
111 Returns
112 -------
113 operation : `BinaryOperation`
114 The operation to actually apply. The default implementation
115 returns ``self``.
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 """ # noqa: D401
123 return self
125 def _finish_apply(self, lhs: Relation, rhs: Relation) -> Relation:
126 """A customization hook for the end of operation application.
128 Parameters
129 ----------
130 target : `Relation`
131 Relation the operation will act upon directly.
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``.
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 """ # noqa: D401
146 from ._operation_relations import BinaryOperationRelation
148 return BinaryOperationRelation(
149 operation=self,
150 lhs=lhs,
151 rhs=rhs,
152 columns=self.applied_columns(lhs, rhs),
153 )
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.
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.
167 Returns
168 -------
169 columns : `~collections.abc.Set` [ `ColumnTag` ]
170 Columns the new relation would have.
171 """
172 raise NotImplementedError()
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.
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.
186 Returns
187 -------
188 min_rows : `int`
189 Minimum number of rows the new relation would have.
190 """
191 raise NotImplementedError()
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.
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.
205 Returns
206 -------
207 max_rows : `int` or `None`
208 Maximum number of rows the new relation would have.
209 """
210 raise NotImplementedError()
213@dataclasses.dataclass
214class IgnoreOne(BinaryOperation):
215 """A binary operation that passes through one of its operands and ignores
216 the other.
217 """
219 ignore_lhs: bool
220 """Whether the ignored operand is the left-hand-side one (`bool`).
221 """
223 def applied_columns(self, lhs: Relation, rhs: Relation) -> Set[ColumnTag]:
224 # Docstring inherited.
225 return self._finish_apply(lhs, rhs).columns
227 def applied_min_rows(self, lhs: Relation, rhs: Relation) -> int:
228 # Docstring inherited.
229 return self._finish_apply(lhs, rhs).min_rows
231 def applied_max_rows(self, lhs: Relation, rhs: Relation) -> int | None:
232 # Docstring inherited.
233 return self._finish_apply(lhs, rhs).max_rows
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