Coverage for tests/test_dynamic_connections.py: 12%
180 statements
« prev ^ index » next coverage.py v7.5.0, created at 2024-04-24 10:01 +0000
« prev ^ index » next coverage.py v7.5.0, created at 2024-04-24 10:01 +0000
1# This file is part of pipe_base.
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 software is dual licensed under the GNU General Public License and also
10# under a 3-clause BSD license. Recipients may choose which of these licenses
11# to use; please see the files gpl-3.0.txt and/or bsd_license.txt,
12# respectively. If you choose the GPL option then the following text applies
13# (but note that there is still no warranty even if you opt for BSD instead):
14#
15# This program is free software: you can redistribute it and/or modify
16# it under the terms of the GNU General Public License as published by
17# the Free Software Foundation, either version 3 of the License, or
18# (at your option) any later version.
19#
20# This program is distributed in the hope that it will be useful,
21# but WITHOUT ANY WARRANTY; without even the implied warranty of
22# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
23# GNU General Public License for more details.
24#
25# You should have received a copy of the GNU General Public License
26# along with this program. If not, see <http://www.gnu.org/licenses/>.
28from __future__ import annotations
30import unittest
31from collections.abc import Callable
32from types import MappingProxyType
34from lsst.pipe.base import PipelineTaskConfig, PipelineTaskConnections
35from lsst.pipe.base.connectionTypes import Input, Output, PrerequisiteInput
37# Keyword arguments for defining our lone test connection in its class.
38DEFAULT_CONNECTION_KWARGS = dict(
39 doc="field docs",
40 name="unconfigured",
41 dimensions=(),
42 storageClass="Dummy",
43)
45# Keyword arguments for making our lone test connection post-configuration.
46# We always rename the dataset type via configuration to make sure those
47# changes are never dropped.
48RENAMED_CONNECTION_KWARGS = DEFAULT_CONNECTION_KWARGS.copy()
49RENAMED_CONNECTION_KWARGS["name"] = "configured"
52class TestDynamicConnectionsClass(unittest.TestCase):
53 """Test modifying connections in derived __init__ implementations."""
55 def build_dynamic_connections(
56 self, init_callback: Callable[[PipelineTaskConnections], None] = None
57 ) -> PipelineTaskConnections:
58 """Define and construct a connections class instance with a callback
59 run in ``__init__``.
61 Parameters
62 ----------
63 init_callback
64 Callback to invoke with the `PipelineTaskConnections` instance
65 as its only argument. Return value is ignored.
67 Returns
68 -------
69 connections : `PipelineTaskConnections`
70 Constructed connections instance.
71 """
73 class ExampleConnections(PipelineTaskConnections, dimensions=()):
74 the_connection = Input(**DEFAULT_CONNECTION_KWARGS)
76 def __init__(self, config: ExampleConfig):
77 # Calling super() is harmless but now unnecessary, so don't do
78 # it to make sure that works. Old code calling it is fine.
79 if init_callback is not None:
80 init_callback(self)
82 class ExampleConfig(PipelineTaskConfig, pipelineConnections=ExampleConnections):
83 pass
85 config = ExampleConfig()
86 config.connections.the_connection = RENAMED_CONNECTION_KWARGS["name"]
87 return ExampleConnections(config=config)
89 def test_freeze_after_construction(self):
90 connections = self.build_dynamic_connections()
91 self.assertIsInstance(connections.dimensions, frozenset)
92 self.assertIsInstance(connections.inputs, frozenset)
93 self.assertIsInstance(connections.prerequisiteInputs, frozenset)
94 self.assertIsInstance(connections.outputs, frozenset)
95 self.assertIsInstance(connections.initInputs, frozenset)
96 self.assertIsInstance(connections.initOutputs, frozenset)
97 self.assertIsInstance(connections.allConnections, MappingProxyType)
99 def test_change_attr_after_construction(self):
100 connections = self.build_dynamic_connections()
101 with self.assertRaises(TypeError):
102 connections.the_connection = PrerequisiteInput(**RENAMED_CONNECTION_KWARGS)
104 def test_delete_attr_after_construction(self):
105 connections = self.build_dynamic_connections()
106 with self.assertRaises(TypeError):
107 del connections.the_connection
109 def test_change_dimensions(self):
110 def callback(instance):
111 instance.dimensions = {"new", "dimensions"}
112 instance.the_connection = Input(
113 doc=instance.the_connection.doc,
114 name=instance.the_connection.name,
115 dimensions={"new", "dimensions"},
116 storageClass=instance.the_connection.storageClass,
117 )
119 connections = self.build_dynamic_connections(callback)
120 self.assertEqual(connections.dimensions, {"new", "dimensions"})
121 self.assertIsInstance(connections.dimensions, frozenset)
122 self.assertEqual(connections.inputs, {"the_connection"})
123 self.assertEqual(connections.allConnections.keys(), {"the_connection"})
124 updated_connection_kwargs = RENAMED_CONNECTION_KWARGS.copy()
125 updated_connection_kwargs["dimensions"] = {"new", "dimensions"}
126 self.assertEqual(connections.allConnections["the_connection"], Input(**updated_connection_kwargs))
127 self.assertEqual(connections.the_connection, Input(**updated_connection_kwargs))
129 def test_change_connection_type(self):
130 def callback(instance):
131 instance.the_connection = PrerequisiteInput(
132 doc=instance.the_connection.doc,
133 name=instance.the_connection.name,
134 dimensions=instance.the_connection.dimensions,
135 storageClass=instance.the_connection.storageClass,
136 )
138 connections = self.build_dynamic_connections(callback)
139 self.assertEqual(connections.inputs, set())
140 self.assertEqual(connections.prerequisiteInputs, {"the_connection"})
141 self.assertEqual(connections.allConnections.keys(), {"the_connection"})
142 self.assertEqual(
143 connections.allConnections["the_connection"], PrerequisiteInput(**RENAMED_CONNECTION_KWARGS)
144 )
145 self.assertEqual(connections.the_connection, PrerequisiteInput(**RENAMED_CONNECTION_KWARGS))
147 def test_change_connection_type_twice(self):
148 def callback(instance):
149 instance.the_connection = PrerequisiteInput(
150 doc=instance.the_connection.doc,
151 name=instance.the_connection.name,
152 dimensions=instance.the_connection.dimensions,
153 storageClass=instance.the_connection.storageClass,
154 )
155 instance.the_connection = Output(
156 doc=instance.the_connection.doc,
157 name=instance.the_connection.name,
158 dimensions=instance.the_connection.dimensions,
159 storageClass=instance.the_connection.storageClass,
160 )
162 connections = self.build_dynamic_connections(callback)
163 self.assertEqual(connections.inputs, set())
164 self.assertEqual(connections.prerequisiteInputs, set())
165 self.assertEqual(connections.outputs, {"the_connection"})
166 self.assertEqual(connections.allConnections.keys(), {"the_connection"})
167 self.assertEqual(connections.allConnections["the_connection"], Output(**RENAMED_CONNECTION_KWARGS))
168 self.assertEqual(connections.the_connection, Output(**RENAMED_CONNECTION_KWARGS))
170 def test_remove_from_set(self):
171 def callback(instance):
172 instance.inputs.remove("the_connection")
173 # We can't make this remove corresponding attribute or the entry in
174 # allConnections *immediately* without using a custom set class for
175 # 'inputs' etc, which we haven't bothered to do, because even that
176 # wouldn't be enough to have additions to those sets update the
177 # attributes and allConnections. Instead updates to those happen
178 # after __init__.
180 connections = self.build_dynamic_connections(callback)
181 self.assertEqual(connections.inputs, set())
182 self.assertEqual(connections.allConnections, {})
183 with self.assertRaises(AttributeError):
184 connections.the_connection
186 def test_delete_attr(self):
187 def callback(instance):
188 del instance.the_connection
189 # This updates the corresponding entry from the inputs set and
190 # the allConnections dict.
191 self.assertEqual(instance.inputs, set())
192 self.assertEqual(instance.allConnections, {})
194 connections = self.build_dynamic_connections(callback)
195 self.assertEqual(connections.inputs, set())
196 self.assertEqual(connections.allConnections, {})
197 with self.assertRaises(AttributeError):
198 connections.the_connection
200 def test_delete_attr_twice(self):
201 def callback(instance):
202 del instance.the_connection
203 with self.assertRaises(AttributeError):
204 del instance.the_connection
206 self.build_dynamic_connections(callback)
208 def test_change_connection_type_then_remove_from_set(self):
209 def callback(instance):
210 instance.the_connection = PrerequisiteInput(
211 doc=instance.the_connection.doc,
212 name=instance.the_connection.name,
213 dimensions=instance.the_connection.dimensions,
214 storageClass=instance.the_connection.storageClass,
215 )
216 instance.prerequisiteInputs.remove("the_connection")
218 connections = self.build_dynamic_connections(callback)
219 self.assertEqual(connections.inputs, set())
220 self.assertEqual(connections.prerequisiteInputs, set())
221 self.assertEqual(connections.allConnections, {})
222 with self.assertRaises(AttributeError):
223 connections.the_connection
224 with self.assertRaises(KeyError):
225 connections.allConnections["the_connection"]
227 def test_change_connection_type_then_delete_attr(self):
228 def callback(instance):
229 instance.the_connection = PrerequisiteInput(
230 doc=instance.the_connection.doc,
231 name=instance.the_connection.name,
232 dimensions=instance.the_connection.dimensions,
233 storageClass=instance.the_connection.storageClass,
234 )
235 del instance.the_connection
236 # This updates the corresponding entry from the inputs set and
237 # the allConnections dict.
238 self.assertEqual(instance.inputs, set())
239 self.assertEqual(instance.prerequisiteInputs, set())
240 self.assertEqual(instance.allConnections, {})
241 with self.assertRaises(AttributeError):
242 instance.the_connection
243 with self.assertRaises(KeyError):
244 instance.allConnections["the_connection"]
246 connections = self.build_dynamic_connections(callback)
247 self.assertEqual(connections.inputs, set())
248 self.assertEqual(connections.prerequisiteInputs, set())
249 self.assertEqual(connections.allConnections, {})
250 with self.assertRaises(AttributeError):
251 connections.the_connection
252 with self.assertRaises(KeyError):
253 connections.allConnections["the_connection"]
255 def test_add_new_connection(self):
256 new_connection = Output(
257 name="new_dataset_type",
258 doc="new connection_docs",
259 storageClass="Dummy",
260 dimensions=(),
261 )
263 def callback(instance):
264 instance.new_connection = new_connection
265 self.assertEqual(instance.outputs, {"new_connection"})
266 self.assertEqual(instance.allConnections.keys(), {"new_connection", "the_connection"})
267 self.assertIs(instance.new_connection, new_connection)
268 self.assertIs(instance.allConnections["new_connection"], new_connection)
270 connections = self.build_dynamic_connections(callback)
271 self.assertEqual(connections.outputs, {"new_connection"})
272 self.assertEqual(connections.allConnections.keys(), {"new_connection", "the_connection"})
273 self.assertIs(connections.new_connection, new_connection)
274 self.assertIs(connections.allConnections["new_connection"], new_connection)
276 def test_add_and_change_new_connection(self):
277 new_connection = Output(
278 name="new_dataset_type",
279 doc="new connection_docs",
280 storageClass="Dummy",
281 dimensions=(),
282 )
283 changed_connection = PrerequisiteInput(
284 name="new_dataset_type",
285 doc="new connection_docs",
286 storageClass="Dummy",
287 dimensions=(),
288 )
290 def callback(instance):
291 instance.new_connection = new_connection
292 self.assertEqual(instance.outputs, {"new_connection"})
293 self.assertEqual(instance.allConnections.keys(), {"new_connection", "the_connection"})
294 self.assertIs(instance.new_connection, new_connection)
295 self.assertIs(instance.allConnections["new_connection"], new_connection)
296 instance.new_connection = changed_connection
297 self.assertEqual(instance.outputs, set())
298 self.assertEqual(instance.allConnections.keys(), {"new_connection", "the_connection"})
299 self.assertIs(instance.new_connection, changed_connection)
300 self.assertIs(instance.allConnections["new_connection"], changed_connection)
302 connections = self.build_dynamic_connections(callback)
303 self.assertEqual(connections.outputs, set())
304 self.assertEqual(connections.allConnections.keys(), {"new_connection", "the_connection"})
305 self.assertIs(connections.new_connection, changed_connection)
306 self.assertIs(connections.allConnections["new_connection"], changed_connection)
308 def test_add_and_remove_new_connection(self):
309 new_connection = Output(
310 name="new_dataset_type",
311 doc="new connection_docs",
312 storageClass="Dummy",
313 dimensions=(),
314 )
316 def callback(instance):
317 instance.new_connection = new_connection
318 self.assertEqual(instance.outputs, {"new_connection"})
319 self.assertEqual(instance.allConnections.keys(), {"new_connection", "the_connection"})
320 self.assertIs(instance.new_connection, new_connection)
321 self.assertIs(instance.allConnections["new_connection"], new_connection)
322 del instance.new_connection
323 self.assertEqual(instance.outputs, set())
324 self.assertEqual(instance.allConnections.keys(), {"the_connection"})
325 with self.assertRaises(AttributeError):
326 instance.new_connection
327 with self.assertRaises(KeyError):
328 instance.allConnections["new_connection"]
330 connections = self.build_dynamic_connections(callback)
331 self.assertEqual(connections.outputs, set())
332 self.assertEqual(connections.allConnections.keys(), {"the_connection"})
333 with self.assertRaises(AttributeError):
334 connections.new_connection
335 with self.assertRaises(KeyError):
336 connections.allConnections["new_connection"]
339if __name__ == "__main__":
340 unittest.main()