|
1 | 1 | from dataclasses import dataclass |
2 | 2 | from functools import partial |
3 | 3 | from typing import Generic, TypeVar |
| 4 | +from unittest.mock import call |
4 | 5 |
|
5 | 6 | import pytest |
6 | 7 | from pytest_mock import MockerFixture |
@@ -201,3 +202,66 @@ async def initialise(self): |
201 | 202 | RuntimeError, match="Could not set int_parameter to 101, max is 100" |
202 | 203 | ): |
203 | 204 | await c.int_parameter.process(101) |
| 205 | + |
| 206 | + |
| 207 | +@pytest.mark.asyncio |
| 208 | +async def test_attribute_io_defaults(mocker: MockerFixture): |
| 209 | + class MyController(Controller): |
| 210 | + no_ref = AttrRW(Int()) |
| 211 | + base_class_ref = AttrRW(Int(), io_ref=AttributeIORef()) |
| 212 | + |
| 213 | + with pytest.raises( |
| 214 | + AssertionError, |
| 215 | + match="MyController does not have an AttributeIO to handle AttributeIORef", |
| 216 | + ): |
| 217 | + c = MyController() |
| 218 | + |
| 219 | + class SimpleAttributeIO(AttributeIO[T, AttributeIORef]): |
| 220 | + async def update(self, attr): |
| 221 | + await attr.set(100) |
| 222 | + |
| 223 | + with pytest.raises( |
| 224 | + RuntimeError, match="More than one AttributeIO class handles AttributeIORef" |
| 225 | + ): |
| 226 | + MyController(ios=[AttributeIO(), SimpleAttributeIO()]) |
| 227 | + |
| 228 | + # we need to explicitly pass an AttributeIO if we want to handle instances of |
| 229 | + # the AttributeIORef base class |
| 230 | + c = MyController(ios=[AttributeIO()]) |
| 231 | + assert not c.no_ref.has_io_ref() |
| 232 | + assert c.base_class_ref.has_io_ref() |
| 233 | + |
| 234 | + await c.initialise() |
| 235 | + await c.attribute_initialise() |
| 236 | + |
| 237 | + with pytest.raises(NotImplementedError): |
| 238 | + await c.base_class_ref.update() |
| 239 | + |
| 240 | + with pytest.raises(NotImplementedError): |
| 241 | + await c.base_class_ref.process(25) |
| 242 | + |
| 243 | + # There is a difference between providing an AttributeIO for the default |
| 244 | + # AttributeIORef class and not specifying the io_ref for an Attribute |
| 245 | + # default callbacks are not provided by AttributeIO subclasses |
| 246 | + |
| 247 | + with pytest.raises( |
| 248 | + RuntimeError, match="Attributes without io_ref can not be updated" |
| 249 | + ): # TODO, we need a clearer error message for this |
| 250 | + await c.no_ref.update() |
| 251 | + |
| 252 | + process_spy = mocker.spy(c.no_ref, "update_display_without_process") |
| 253 | + await c.no_ref.process(40) |
| 254 | + process_spy.assert_called_with(40) |
| 255 | + |
| 256 | + # this is correct, but we want to reconsider this logic, it seems wasteful to |
| 257 | + # call update_display twice... |
| 258 | + assert process_spy.call_args_list == [call(40), call(40)] |
| 259 | + |
| 260 | + c2 = MyController(ios=[SimpleAttributeIO()]) |
| 261 | + |
| 262 | + await c2.initialise() |
| 263 | + await c2.attribute_initialise() |
| 264 | + |
| 265 | + assert c2.base_class_ref.get() == 0 |
| 266 | + await c2.base_class_ref.update() |
| 267 | + assert c2.base_class_ref.get() == 100 |
0 commit comments