Skip to content

Commit 9de949c

Browse files
author
James Souter
committed
Add ref to update and send signatures to simplify type checking
fix some test pyright errors
1 parent 8ef9a8b commit 9de949c

File tree

9 files changed

+77
-59
lines changed

9 files changed

+77
-59
lines changed

src/fastcs/attribute_io.py

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -12,12 +12,15 @@ def __init_subclass__(cls) -> None:
1212
args = get_args(cast(Any, cls).__orig_bases__[0])
1313
cls.ref_type = args[0]
1414

15-
async def update(self, attr: AttrR[T, AttributeIORefT]) -> None:
15+
async def update(
16+
self, attr: AttrR[T, AttributeIORefT], ref: AttributeIORefT
17+
) -> None:
1618
raise NotImplementedError()
1719

1820
async def send(
1921
self,
2022
attr: AttrRW[T, AttributeIORefT],
23+
ref: AttributeIORefT,
2124
value, # TODO, type this
2225
) -> None:
2326
raise NotImplementedError()

src/fastcs/attribute_io_ref.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -7,4 +7,4 @@ class AttributeIORef:
77
update_period: float | None = None
88

99

10-
AttributeIORefT = TypeVar("AttributeIORefT", bound=AttributeIORef, covariant=True)
10+
AttributeIORefT = TypeVar("AttributeIORefT", bound=AttributeIORef)

src/fastcs/attributes.py

Lines changed: 12 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -1,15 +1,15 @@
11
from __future__ import annotations
22

33
import asyncio
4-
from collections.abc import Callable, Coroutine
5-
from typing import Generic, Self
4+
from collections.abc import Callable
5+
from typing import Generic
66

77
from typing_extensions import TypeVar
88

99
import fastcs
1010
from fastcs.attribute_io_ref import AttributeIORef
1111

12-
from .datatypes import ATTRIBUTE_TYPES, AttrCallback, DataType, T
12+
from .datatypes import ATTRIBUTE_TYPES, AttrSetCallback, AttrUpdateCallback, DataType, T
1313

1414
# TODO rename this: typevar with default
1515
AttributeIORefTD = TypeVar(
@@ -97,8 +97,8 @@ def __init__(
9797
self._value: T = (
9898
datatype.initial_value if initial_value is None else initial_value
9999
)
100-
self._on_set_callbacks: list[AttrCallback[T]] | None = None
101-
self._on_update_callbacks: list[AttrCallback[T]] | None = None
100+
self._on_set_callbacks: list[AttrSetCallback[T]] | None = None
101+
self._on_update_callbacks: list[AttrUpdateCallback] | None = None
102102

103103
def get(self) -> T:
104104
return self._value
@@ -109,21 +109,19 @@ async def set(self, value: T) -> None:
109109
if self._on_set_callbacks is not None:
110110
await asyncio.gather(*[cb(self._value) for cb in self._on_set_callbacks])
111111

112-
def add_set_callback(self, callback: AttrCallback[T]) -> None:
112+
def add_set_callback(self, callback: AttrSetCallback[T]) -> None:
113113
if self._on_set_callbacks is None:
114114
self._on_set_callbacks = []
115115
self._on_set_callbacks.append(callback)
116116

117-
def add_update_callback(
118-
self, callback: Callable[[Self], Coroutine[None, None, None]]
119-
):
117+
def add_update_callback(self, callback: AttrUpdateCallback):
120118
if self._on_update_callbacks is None:
121119
self._on_update_callbacks = []
122120
self._on_update_callbacks.append(callback)
123121

124122
async def update(self):
125123
if self._on_update_callbacks is not None:
126-
await asyncio.gather(*[cb(self) for cb in self._on_update_callbacks])
124+
await asyncio.gather(*[cb() for cb in self._on_update_callbacks])
127125

128126

129127
class AttrW(Attribute[T, AttributeIORefTD]):
@@ -142,8 +140,8 @@ def __init__(
142140
group,
143141
description=description,
144142
)
145-
self._process_callbacks: list[AttrCallback[T]] | None = None
146-
self._write_display_callbacks: list[AttrCallback[T]] | None = None
143+
self._process_callbacks: list[AttrSetCallback[T]] | None = None
144+
self._write_display_callbacks: list[AttrSetCallback[T]] | None = None
147145

148146
async def process(self, value: T) -> None:
149147
await self.process_without_display_update(value)
@@ -159,15 +157,15 @@ async def update_display_without_process(self, value: T) -> None:
159157
if self._write_display_callbacks:
160158
await asyncio.gather(*[cb(value) for cb in self._write_display_callbacks])
161159

162-
def add_process_callback(self, callback: AttrCallback[T]) -> None:
160+
def add_process_callback(self, callback: AttrSetCallback[T]) -> None:
163161
if self._process_callbacks is None:
164162
self._process_callbacks = []
165163
self._process_callbacks.append(callback)
166164

167165
def has_process_callback(self) -> bool:
168166
return bool(self._process_callbacks)
169167

170-
def add_write_display_callback(self, callback: AttrCallback[T]) -> None:
168+
def add_write_display_callback(self, callback: AttrSetCallback[T]) -> None:
171169
if self._write_display_callbacks is None:
172170
self._write_display_callbacks = []
173171
self._write_display_callbacks.append(callback)

src/fastcs/controller.py

Lines changed: 9 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -72,26 +72,29 @@ def _create_send_callback(self, io, attr):
7272

7373
async def send_callback(value):
7474
await attr.update_display_without_process(value)
75-
7675
if isinstance(attr, AttrRW):
7776
await attr.set(value)
78-
7977
else:
8078

8179
async def send_callback(value):
82-
await io.send(attr, value)
80+
await io.send(attr, attr.io_ref, value)
81+
# TODO, should we just then call the above send_callback here?
8382

8483
return send_callback
8584

8685
def _create_update_callback(self, io, attr):
8786
if io is None or attr.io_ref is None:
8887

89-
async def update_callback(attr):
88+
async def error_callback():
9089
raise RuntimeError("No AttributeIO registered to handle update")
9190

92-
return update_callback
91+
return error_callback
9392
else:
94-
return io.update
93+
94+
async def update_callback():
95+
await io.update(attr, attr.io_ref)
96+
97+
return update_callback
9598

9699
@property
97100
def path(self) -> list[str]:

src/fastcs/datatypes.py

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -24,7 +24,8 @@
2424
ATTRIBUTE_TYPES: tuple[type] = T.__constraints__ # type: ignore
2525

2626

27-
AttrCallback = Callable[[T], Awaitable[None]]
27+
AttrSetCallback = Callable[[T], Awaitable[None]]
28+
AttrUpdateCallback = Callable[[], Awaitable[None]]
2829

2930

3031
@dataclass(frozen=True)

src/fastcs/demo/controllers.py

Lines changed: 6 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -39,16 +39,16 @@ def __init__(self, connection: IPConnection, suffix: str):
3939
self.suffix = suffix
4040

4141
async def send(
42-
self, attr: AttrW[T, TemperatureControllerAttributeIORef], value: T
42+
self, attr: AttrW, ref: TemperatureControllerAttributeIORef, value: T
4343
) -> None:
4444
await self._connection.send_command(
45-
f"{attr.io_ref.name}{self.suffix}={attr.dtype(value)}\r\n"
45+
f"{ref.name}{self.suffix}={attr.dtype(value)}\r\n"
4646
)
4747

48-
async def update(self, attr: AttrR[T, TemperatureControllerAttributeIORef]) -> None:
49-
response = await self._connection.send_query(
50-
f"{attr.io_ref.name}{self.suffix}?\r\n"
51-
)
48+
async def update(
49+
self, attr: AttrR, ref: TemperatureControllerAttributeIORef
50+
) -> None:
51+
response = await self._connection.send_query(f"{ref.name}{self.suffix}?\r\n")
5252
response = response.strip("\r\n")
5353

5454
await attr.set(attr.dtype(response))

tests/assertable_controller.py

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -24,11 +24,11 @@ class MyTestAttributeIORef(AttributeIORef):
2424

2525

2626
class MyTestAttributeIO(AttributeIO[MyTestAttributeIORef, T]):
27-
async def update(self, attr: AttrR[T]):
28-
print(f"{self.controller} update {attr}")
27+
async def update(self, attr: AttrR[T], ref: MyTestAttributeIORef):
28+
print(f"update {attr}")
2929

30-
async def send(self, attr: AttrW[T], value: T):
31-
print(f"{self.controller}: {attr} = {value}")
30+
async def send(self, attr: AttrW[T], ref: MyTestAttributeIORef, value: T):
31+
print(f"sending {attr} = {value}")
3232

3333

3434
test_attribute_io = MyTestAttributeIO() # instance

tests/test_attribute.py

Lines changed: 33 additions & 20 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
from dataclasses import dataclass
22
from functools import partial
3-
from typing import Generic
3+
from typing import Generic, TypeVar
44

55
import pytest
66
from pytest_mock import MockerFixture
@@ -13,7 +13,9 @@
1313
AttrW,
1414
)
1515
from fastcs.controller import Controller
16-
from fastcs.datatypes import Float, Int, String, T
16+
from fastcs.datatypes import Float, Int, String
17+
18+
NumberT = TypeVar("NumberT", int, float)
1719

1820

1921
@pytest.mark.asyncio
@@ -68,8 +70,8 @@ class MyAttributeIORef(AttributeIORef):
6870
cool: int
6971

7072
class MyAttributeIO(AttributeIO[MyAttributeIORef, int]):
71-
async def update(self, attr: AttrR[Int, MyAttributeIORef]):
72-
print("I am updating", self.ref_type, attr.io_ref.cool)
73+
async def update(self, attr: AttrR, ref: MyAttributeIORef):
74+
print("I am updating", self.ref_type, ref.cool)
7375

7476
class MyController(Controller):
7577
my_attr = AttrR(Int(), io_ref=MyAttributeIORef(cool=5))
@@ -114,38 +116,49 @@ async def test_dynamic_attribute_io_specification():
114116
]
115117

116118
@dataclass
117-
class DemoParameterAttributeIORef(AttributeIORef, Generic[T]):
119+
class DemoParameterAttributeIORef(AttributeIORef, Generic[NumberT]):
118120
name: str
119-
min: T | None = None
120-
max: T | None = None
121+
# TODO, this is weird, we should just use the attributes's min and max fields
122+
min: NumberT | None = None
123+
max: NumberT | None = None
121124
read_only: bool = False
122125

123-
class DemoParameterAttributeIO(AttributeIO[DemoParameterAttributeIORef, T]):
124-
async def update(self, attr: AttrR[T]):
126+
class DemoParameterAttributeIO(AttributeIO[DemoParameterAttributeIORef, NumberT]):
127+
async def update(
128+
self,
129+
attr: AttrR[NumberT],
130+
ref: DemoParameterAttributeIORef,
131+
):
125132
# OK, so this doesn't really work when we have min and maxes...
126133
await attr.set(attr.get() + 1)
127134

128-
async def send(self, attr: AttrW[T], value) -> None:
135+
async def send(
136+
self,
137+
attr: AttrW[NumberT, DemoParameterAttributeIORef],
138+
ref: DemoParameterAttributeIORef,
139+
value: NumberT,
140+
) -> None:
129141
if (
130-
attr.io_ref.read_only
142+
ref.read_only
131143
): # TODO, this isn't necessary as we can not call process on this anyway
132-
raise RuntimeError(
133-
f"Could not set read only attribute {attr.io_ref.name}"
134-
)
144+
raise RuntimeError(f"Could not set read only attribute {ref.name}")
135145

136-
if (io_min := attr.io_ref.min) is not None and value < io_min:
146+
if (io_min := ref.min) is not None and value < io_min:
137147
raise RuntimeError(
138-
f"Could not set {attr.io_ref.name} to {value}, "
139-
f"min is {attr.io_ref.min}"
148+
f"Could not set {ref.name} to {value}, min is {ref.min}"
140149
)
141150

142-
if (io_max := attr.io_ref.max) is not None and value > io_max:
151+
if (io_max := ref.max) is not None and value > io_max:
143152
raise RuntimeError(
144-
f"Could not set {attr.io_ref.name} to {value}, "
145-
f"max is {attr.io_ref.max}"
153+
f"Could not set {ref.name} to {value}, max is {ref.max}"
146154
)
155+
# TODO: we should always end send with a update_display_without_process...
147156

148157
class DemoParameterController(Controller):
158+
ro_int_parameter: AttrR
159+
int_parameter: AttrRW
160+
float_parameter: AttrRW # hint to satisfy pyright
161+
149162
async def initialise(self):
150163
dtype_mapping = {"int": Int(), "float": Float()}
151164
for parameter_response in example_introspection_response:

tests/test_backend.py

Lines changed: 6 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -7,7 +7,7 @@
77
from fastcs.backend import Backend, build_controller_api
88
from fastcs.controller import Controller
99
from fastcs.cs_methods import Command
10-
from fastcs.datatypes import Int, T
10+
from fastcs.datatypes import Int
1111
from fastcs.exceptions import FastCSError
1212
from fastcs.wrappers import command, scan
1313

@@ -98,13 +98,13 @@ async def test_wrapper():
9898
def test_update_periods():
9999
@dataclass
100100
class AttributeIORefTimesCalled(AttributeIORef):
101-
update_period: float | None
101+
update_period: float | None = None
102102
_times_called = 0
103103

104-
class AttributeIOTimesCalled(AttributeIO[AttributeIORefTimesCalled, T]):
105-
async def update(self, attr):
106-
attr.io_ref._times_called += 1
107-
await attr.set(attr.io_ref._times_called)
104+
class AttributeIOTimesCalled(AttributeIO[AttributeIORefTimesCalled, int]):
105+
async def update(self, attr: AttrR[int], ref: AttributeIORefTimesCalled):
106+
ref._times_called += 1
107+
await attr.set(ref._times_called)
108108

109109
class MyController(Controller):
110110
update_once = AttrR(Int(), io_ref=AttributeIORefTimesCalled(update_period=ONCE))

0 commit comments

Comments
 (0)