-
Notifications
You must be signed in to change notification settings - Fork 23
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
New signal typing #594
base: main
Are you sure you want to change the base?
New signal typing #594
Changes from 1 commit
fd5e699
2e9dd61
2f6d171
fdb4504
c70d449
4ecce34
4a5c27f
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -13,12 +13,12 @@ classifiers = [ | |
description = "Asynchronous Bluesky hardware abstraction code, compatible with control systems like EPICS and Tango" | ||
dependencies = [ | ||
"networkx>=2.0", | ||
"numpy<2.0.0", | ||
"numpy", | ||
"packaging", | ||
"pint", | ||
"bluesky>=1.13.0a3", | ||
"event_model", | ||
"p4p", | ||
"event-model @ git+https://github.com/bluesky/event-model@main", | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Why do we want to pin main here? Using latest release would incentivize regular event-model patch releases. There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Ah I see you're using There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. There will probably be a few other import level changes to event model before I'm done, then we can release event model too and go back to a release here |
||
"p4p>=4.2.0a3", | ||
"pyyaml", | ||
"colorlog", | ||
"pydantic>=2.0", | ||
|
@@ -37,10 +37,6 @@ dev = [ | |
"ophyd_async[pva]", | ||
"ophyd_async[sim]", | ||
"ophyd_async[ca]", | ||
"black", | ||
"flake8", | ||
"flake8-isort", | ||
"Flake8-pyproject", | ||
"inflection", | ||
"ipython", | ||
"ipywidgets", | ||
|
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -1,39 +1,63 @@ | ||
"""Base device""" | ||
from __future__ import annotations | ||
|
||
import asyncio | ||
import sys | ||
from collections.abc import Coroutine, Generator, Iterator | ||
from abc import abstractmethod | ||
from collections.abc import Callable, Coroutine, Iterator, Mapping | ||
from functools import cached_property | ||
from logging import LoggerAdapter, getLogger | ||
from typing import ( | ||
Any, | ||
Optional, | ||
TypeVar, | ||
) | ||
from typing import Any, Generic, TypeVar, cast | ||
|
||
from bluesky.protocols import HasName | ||
from bluesky.run_engine import call_in_bluesky_event_loop | ||
|
||
from ._utils import DEFAULT_TIMEOUT, NotConnected, wait_for_connection | ||
|
||
|
||
class Device(HasName): | ||
class DeviceConnector: | ||
@abstractmethod | ||
async def connect( | ||
self, device: Device, mock: bool, timeout: float, force_reconnect: bool | ||
) -> None: ... | ||
|
||
|
||
class DeviceChildConnector(DeviceConnector): | ||
async def connect( | ||
self, device: Device, mock: bool, timeout: float, force_reconnect: bool | ||
) -> Any: | ||
coretl marked this conversation as resolved.
Show resolved
Hide resolved
|
||
coros = { | ||
name: child_device.connect(mock, timeout, force_reconnect) | ||
for name, child_device in device.children().items() | ||
coretl marked this conversation as resolved.
Show resolved
Hide resolved
|
||
} | ||
await wait_for_connection(**coros) | ||
|
||
|
||
DeviceConnectorType = TypeVar("DeviceConnectorType", bound=DeviceConnector) | ||
|
||
|
||
class Device(HasName, Generic[DeviceConnectorType]): | ||
"""Common base class for all Ophyd Async Devices. | ||
|
||
By default, names and connects all Device children. | ||
""" | ||
|
||
_name: str = "" | ||
#: The parent Device if it exists | ||
parent: Optional["Device"] = None | ||
parent: Device | None = None | ||
# None if connect hasn't started, a Task if it has | ||
_connect_task: asyncio.Task | None = None | ||
# The value of the mock arg to connect | ||
_connect_mock: bool | None = None | ||
# The connector to use | ||
coretl marked this conversation as resolved.
Show resolved
Hide resolved
|
||
_connector: DeviceConnectorType = DeviceChildConnector() | ||
|
||
# Used to check if the previous connect was mocked, | ||
# if the next mock value differs then we fail | ||
_previous_connect_was_mock = None | ||
|
||
def __init__(self, name: str = "") -> None: | ||
def __init__( | ||
self, | ||
name: str = "", | ||
connector: DeviceConnectorType | None = None, | ||
) -> None: | ||
if connector is not None: | ||
self._connector = connector | ||
self.set_name(name) | ||
|
||
@property | ||
|
@@ -47,10 +71,12 @@ def log(self): | |
getLogger("ophyd_async.devices"), {"ophyd_async_device_name": self.name} | ||
) | ||
|
||
def children(self) -> Iterator[tuple[str, "Device"]]: | ||
for attr_name, attr in self.__dict__.items(): | ||
if attr_name != "parent" and isinstance(attr, Device): | ||
yield attr_name, attr | ||
def children(self) -> dict[str, Device]: | ||
return { | ||
attr_name: attr | ||
for attr_name, attr in self.__dict__.items() | ||
if attr_name != "parent" and isinstance(attr, Device) | ||
} | ||
|
||
Comment on lines
+84
to
+89
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Since the attributes of a class won't change post init, how about making this a cached property? The only place we actually There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Let's discuss this more
Comment on lines
+84
to
+89
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Looking at the rest of the code I've had an idea... Following from what was said in the meeting (having both There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Tbh we don't have to define use an abstract class, instead we can: class Device(HasName):
@property
def children(self) -> "DeviceVector":
return DeviceVector(
{
attr_name: attr
for attr_name, attr in self.__dict__.items()
if attr_name != "parent" and isinstance(attr, Device)
}
)
def connect(self, ...):
# DO STUFF
self.children.connect(...)
class DeviceVector(dict[int, VT], Device):
def connect(self, *args, **kwargs):
# Logic for connecting which can be used for by Device
# For its children
gather({
device.connect(*args, **kwargs)
for device_name, device in self.items():
})
def children(self) -> DeviceVector:
return self There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. This reduces the code a bit for the current use case, but I think it will break what I had planned for PVI. Let me write that first then we can revisit this and see if this still applies... |
||
def set_name(self, name: str): | ||
"""Set ``self.name=name`` and each ``self.child.name=name+"-child"``. | ||
|
@@ -66,7 +92,7 @@ def set_name(self, name: str): | |
del self.log | ||
|
||
self._name = name | ||
for attr_name, child in self.children(): | ||
for attr_name, child in self.children().items(): | ||
child_name = f"{name}-{attr_name.rstrip('_')}" if name else "" | ||
coretl marked this conversation as resolved.
Show resolved
Hide resolved
|
||
child.set_name(child_name) | ||
child.parent = self | ||
|
@@ -89,40 +115,27 @@ async def connect( | |
Time to wait before failing with a TimeoutError. | ||
""" | ||
|
||
if ( | ||
self._previous_connect_was_mock is not None | ||
and self._previous_connect_was_mock != mock | ||
): | ||
raise RuntimeError( | ||
f"`connect(mock={mock})` called on a `Device` where the previous " | ||
f"connect was `mock={self._previous_connect_was_mock}`. Changing mock " | ||
"value between connects is not permitted." | ||
) | ||
self._previous_connect_was_mock = mock | ||
|
||
# If previous connect with same args has started and not errored, can use it | ||
can_use_previous_connect = self._connect_task and not ( | ||
self._connect_task.done() and self._connect_task.exception() | ||
can_use_previous_connect = ( | ||
mock is self._connect_mock | ||
and self._connect_task | ||
and not (self._connect_task.done() and self._connect_task.exception()) | ||
) | ||
if force_reconnect or not can_use_previous_connect: | ||
# Kick off a connection | ||
coros = { | ||
name: child_device.connect( | ||
mock, timeout=timeout, force_reconnect=force_reconnect | ||
) | ||
for name, child_device in self.children() | ||
} | ||
self._connect_task = asyncio.create_task(wait_for_connection(**coros)) | ||
|
||
# Use the connector to make a new connection | ||
self._connect_mock = mock | ||
self._connect_task = asyncio.create_task( | ||
self._connector.connect(self, mock, timeout, force_reconnect) | ||
) | ||
assert self._connect_task, "Connect task not created, this shouldn't happen" | ||
# Wait for it to complete | ||
await self._connect_task | ||
|
||
|
||
VT = TypeVar("VT", bound=Device) | ||
DeviceType = TypeVar("DeviceType", bound=Device) | ||
|
||
|
||
class DeviceVector(dict[int, VT], Device): | ||
class DeviceVector(Mapping[int, DeviceType], Device): | ||
""" | ||
Defines device components with indices. | ||
|
||
|
@@ -131,10 +144,26 @@ class DeviceVector(dict[int, VT], Device): | |
:class:`~ophyd_async.epics.demo.DynamicSensorGroup` | ||
""" | ||
|
||
def children(self) -> Generator[tuple[str, Device], None, None]: | ||
for attr_name, attr in self.items(): | ||
if isinstance(attr, Device): | ||
yield str(attr_name), attr | ||
def __init__( | ||
self, | ||
children: dict[int, DeviceType], | ||
name: str = "", | ||
connector: DeviceConnector | None = None, | ||
) -> None: | ||
self._children = children | ||
super().__init__(name, connector) | ||
|
||
def __getitem__(self, key: int) -> DeviceType: | ||
return self._children[key] | ||
|
||
def __iter__(self) -> Iterator[int]: | ||
yield from self._children | ||
|
||
def __len__(self) -> int: | ||
return len(self._children) | ||
|
||
def children(self) -> dict[str, Device]: | ||
return {str(key): value for key, value in self.items()} | ||
|
||
|
||
class DeviceCollector: | ||
|
This file was deleted.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Do we not have this working with
numpy >=2
? That's what's being installed currently right?There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
These changes will make use of
np.dtypes.StringDType()
as discussed with @danielballan which is only available in numpy 2, so yes I should make itnumpy >= 2