Skip to content

Commit

Permalink
Merge branch 'bluesky:main' into tango_support
Browse files Browse the repository at this point in the history
  • Loading branch information
burkeds authored Aug 27, 2024
2 parents 7c69f55 + d7f0748 commit 48a18f0
Show file tree
Hide file tree
Showing 14 changed files with 119 additions and 63 deletions.
2 changes: 1 addition & 1 deletion docs/how-to/make-a-simple-device.rst
Original file line number Diff line number Diff line change
Expand Up @@ -29,7 +29,7 @@ First some Signals are constructed and stored on the Device. Each one is passed
its Python type, which could be:

- A primitive (`str`, `int`, `float`)
- An array (`numpy.typing.NDArray` or ``Sequence[str]``)
- An array (`numpy.typing.NDArray` ie. ``numpy.typing.NDArray[numpy.uint16]`` or ``Sequence[str]``)
- An enum (`enum.Enum`) which **must** also extend `str`
- `str` and ``EnumClass(str, Enum)`` are the only valid ``datatype`` for an enumerated signal.

Expand Down
3 changes: 2 additions & 1 deletion pyproject.toml
Original file line number Diff line number Diff line change
Expand Up @@ -32,14 +32,15 @@ requires-python = ">=3.10"
[project.optional-dependencies]
ca = ["aioca>=1.6"]
pva = ["p4p"]
sim = ["h5py"]
dev = [
"ophyd_async[pva]",
"ophyd_async[sim]",
"ophyd_async[ca]",
"black",
"flake8",
"flake8-isort",
"Flake8-pyproject",
"h5py",
"inflection",
"ipython",
"ipywidgets",
Expand Down
15 changes: 0 additions & 15 deletions src/ophyd_async/core/_signal.py
Original file line number Diff line number Diff line change
Expand Up @@ -45,15 +45,6 @@ async def wrapper(self: Signal, *args, **kwargs):
return wrapper


def _fail(self, other, *args, **kwargs):
if isinstance(other, Signal):
raise TypeError(
"Can't compare two Signals, did you mean await signal.get_value() instead?"
)
else:
return NotImplemented


class Signal(Device, Generic[T]):
"""A Device with the concept of a value, with R, RW, W and X flavours"""

Expand Down Expand Up @@ -118,12 +109,6 @@ def source(self) -> str:
"""Like ca://PV_PREFIX:SIGNAL, or "" if not set"""
return self._backend.source(self.name)

__lt__ = __le__ = __eq__ = __ge__ = __gt__ = __ne__ = _fail

def __hash__(self):
# Restore the default implementation so we can use in a set or dict
return hash(id(self))


class _SignalCache(Generic[T]):
def __init__(self, backend: SignalBackend[T], signal: Signal):
Expand Down
17 changes: 12 additions & 5 deletions src/ophyd_async/epics/signal/_aioca.py
Original file line number Diff line number Diff line change
Expand Up @@ -47,7 +47,7 @@ def _data_key_from_augmented_value(
value: AugmentedValue,
*,
choices: Optional[List[str]] = None,
dtype: Optional[str] = None,
dtype: Optional[Dtype] = None,
) -> DataKey:
"""Use the return value of get with FORMAT_CTRL to construct a DataKey
describing the signal. See docstring of AugmentedValue for expected
Expand Down Expand Up @@ -175,7 +175,7 @@ def value(self, value: AugmentedValue) -> bool:
return bool(value)

def get_datakey(self, value: AugmentedValue) -> DataKey:
return _data_key_from_augmented_value(value, dtype="bool")
return _data_key_from_augmented_value(value, dtype="boolean")


class DisconnectedCaConverter(CaConverter):
Expand Down Expand Up @@ -229,10 +229,17 @@ def make_converter(
value = list(values.values())[0]
# Done the dbr check, so enough to check one of the values
if datatype and not isinstance(value, datatype):
raise TypeError(
f"{pv} has type {type(value).__name__.replace('ca_', '')} "
+ f"not {datatype.__name__}"
# Allow int signals to represent float records when prec is 0
is_prec_zero_float = (
isinstance(value, float)
and get_unique({k: v.precision for k, v in values.items()}, "precision")
== 0
)
if not (datatype is int and is_prec_zero_float):
raise TypeError(
f"{pv} has type {type(value).__name__.replace('ca_', '')} "
+ f"not {datatype.__name__}"
)
return CaConverter(pv_dbr, None)


Expand Down
25 changes: 14 additions & 11 deletions src/ophyd_async/epics/signal/_p4p.py
Original file line number Diff line number Diff line change
Expand Up @@ -64,7 +64,7 @@ def _data_key_from_value(
*,
shape: Optional[list[int]] = None,
choices: Optional[list[str]] = None,
dtype: Optional[str] = None,
dtype: Optional[Dtype] = None,
) -> DataKey:
"""
Args:
Expand All @@ -85,7 +85,7 @@ def _data_key_from_value(
if isinstance(type_code, tuple):
dtype_numpy = ""
if type_code[1] == "enum_t":
if dtype == "bool":
if dtype == "boolean":
dtype_numpy = "<i2"
else:
for item in type_code[2]:
Expand Down Expand Up @@ -241,7 +241,7 @@ def value(self, value):
return bool(value["value"]["index"])

def get_datakey(self, source: str, value) -> DataKey:
return _data_key_from_value(source, value, dtype="bool")
return _data_key_from_value(source, value, dtype="boolean")


class PvaTableConverter(PvaConverter):
Expand Down Expand Up @@ -335,14 +335,17 @@ def make_converter(datatype: Optional[Type], values: Dict[str, Any]) -> PvaConve
return PvaEnumConverter(
get_supported_values(pv, datatype, datatype.choices)
)
elif (
datatype
and not issubclass(typ, datatype)
and not (
typ is float and datatype is int
) # Allow float -> int since prec can be 0
):
raise TypeError(f"{pv} has type {typ.__name__} not {datatype.__name__}")
elif datatype and not issubclass(typ, datatype):
# Allow int signals to represent float records when prec is 0
is_prec_zero_float = typ is float and (
get_unique(
{k: v["display"]["precision"] for k, v in values.items()},
"precision",
)
== 0
)
if not (datatype is int and is_prec_zero_float):
raise TypeError(f"{pv} has type {typ.__name__} not {datatype.__name__}")
return PvaConverter()
elif "NTTable" in typeid:
return PvaTableConverter()
Expand Down
2 changes: 2 additions & 0 deletions src/ophyd_async/fastcs/panda/_block.py
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,8 @@ class DataBlock(Device):
hdf_file_name: SignalRW[str]
num_capture: SignalRW[int]
num_captured: SignalR[int]
create_directory: SignalRW[int]
directory_exists: SignalR[bool]
capture: SignalRW[bool]
flush_period: SignalRW[float]
datasets: SignalR[DatasetTable]
Expand Down
16 changes: 13 additions & 3 deletions src/ophyd_async/fastcs/panda/_writer.py
Original file line number Diff line number Diff line change
Expand Up @@ -46,17 +46,27 @@ async def open(self, multiplier: int = 1) -> Dict[str, DataKey]:

self._file = None
info = self._path_provider(device_name=self._name_provider())

# Set create dir depth first to guarantee that callback when setting
# directory path has correct value
await self.panda_data_block.create_directory.set(info.create_dir_depth)

# Set the initial values
await asyncio.gather(
self.panda_data_block.hdf_directory.set(info.directory_path),
self.panda_data_block.hdf_directory.set(str(info.directory_path)),
self.panda_data_block.hdf_file_name.set(
f"{info.filename}.h5",
),
self.panda_data_block.num_capture.set(0),
# TODO: Set create_dir_depth once available
# https://github.com/bluesky/ophyd-async/issues/317
)

# Make sure that directory exists or has been created.
if not await self.panda_data_block.directory_exists.get_value() == 1:
raise OSError(
f"Directory {info.directory_path} does not exist or "
"is not writable by the PandABlocks-ioc!"
)

# Wait for it to start, stashing the status that tells us when it finishes
await self.panda_data_block.capture.set(True)
if multiplier > 1:
Expand Down
21 changes: 0 additions & 21 deletions tests/core/test_signal.py
Original file line number Diff line number Diff line change
@@ -1,6 +1,5 @@
import asyncio
import logging
import re
import time
from asyncio import Event
from unittest.mock import ANY
Expand Down Expand Up @@ -37,26 +36,6 @@
from ophyd_async.plan_stubs import ensure_connected


async def test_signals_equality_raises():
s1 = epics_signal_rw(int, "pva://pv1", name="signal")
s2 = epics_signal_rw(int, "pva://pv2", name="signal")
await s1.connect(mock=True)
await s2.connect(mock=True)

with pytest.raises(
TypeError,
match=re.escape(
"Can't compare two Signals, did you mean await signal.get_value() instead?"
),
):
s1 == s2
with pytest.raises(
TypeError,
match=re.escape("'>' not supported between instances of 'SignalRW' and 'int'"),
):
s1 > 4


async def test_signal_can_be_given_backend_on_connect():
sim_signal = SignalR()
backend = MockSignalBackend(int)
Expand Down
5 changes: 4 additions & 1 deletion tests/core/test_utils.py
Original file line number Diff line number Diff line change
Expand Up @@ -123,7 +123,10 @@ async def test_error_handling_connection_timeout(caplog):
assert str(e.value) == str(ONE_WORKING_ONE_TIMEOUT_OUTPUT)

logs = caplog.get_records("call")
assert len(logs) == 3

# See https://github.com/bluesky/ophyd-async/issues/519
# assert len(logs) == 3

assert "signal ca://A_NON_EXISTENT_SIGNAL timed out" == logs[-1].message
assert logs[-1].levelname == "DEBUG"

Expand Down
14 changes: 14 additions & 0 deletions tests/epics/signal/test_records.db
Original file line number Diff line number Diff line change
Expand Up @@ -58,6 +58,20 @@ record(ao, "$(P)float") {
field(PINI, "YES")
}

record(ao, "$(P)float_prec_0") {
field(PREC, "0")
field(EGU, "mm")
field(VAL, "3")
field(PINI, "YES")
}

record(ao, "$(P)float_prec_1") {
field(PREC, "1")
field(EGU, "mm")
field(VAL, "3")
field(PINI, "YES")
}

record(stringout, "$(P)str") {
field(VAL, "hello")
field(PINI, "YES")
Expand Down
21 changes: 18 additions & 3 deletions tests/epics/signal/test_signals.py
Original file line number Diff line number Diff line change
Expand Up @@ -213,14 +213,14 @@ class MyEnum(str, Enum):

_metadata: Dict[str, Dict[str, Dict[str, Any]]] = {
"ca": {
"bool": {"units": ANY, "limits": ANY},
"boolean": {"units": ANY, "limits": ANY},
"integer": {"units": ANY, "limits": ANY},
"number": {"units": ANY, "limits": ANY, "precision": ANY},
"enum": {"limits": ANY},
"string": {"limits": ANY},
},
"pva": {
"bool": {"limits": ANY},
"boolean": {"limits": ANY},
"integer": {"units": ANY, "precision": ANY, "limits": ANY},
"number": {"units": ANY, "precision": ANY, "limits": ANY},
"enum": {"limits": ANY},
Expand All @@ -237,7 +237,7 @@ def get_internal_dtype(suffix: str) -> str:
if "int" in suffix:
return "integer"
if "bool" in suffix:
return "bool"
return "boolean"
if "enum" in suffix:
return "enum"
return "string"
Expand Down Expand Up @@ -886,3 +886,18 @@ async def test_signal_not_return_no_limits(ioc: IOC):
await sig.connect()
datakey = (await sig.describe())[""]
assert not hasattr(datakey, "limits")


async def test_signals_created_for_prec_0_float_can_use_int(ioc: IOC):
pv_name = f"{ioc.protocol}://{PV_PREFIX}:{ioc.protocol}:float_prec_0"
sig = epics_signal_rw(int, pv_name)
await sig.connect()


async def test_signals_created_for_not_prec_0_float_cannot_use_int(ioc: IOC):
pv_name = f"{ioc.protocol}://{PV_PREFIX}:{ioc.protocol}:float_prec_1"
sig = epics_signal_rw(int, pv_name)
with pytest.raises(
TypeError, match=f"{ioc.protocol}:float_prec_1 has type float not int"
):
await sig.connect()
7 changes: 7 additions & 0 deletions tests/fastcs/panda/test_hdf_panda.py
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
import os
from typing import Dict
from unittest.mock import ANY

Expand Down Expand Up @@ -41,7 +42,13 @@ class CaptureBlock(Device):
def link_function(value, **kwargs):
set_mock_value(mock_hdf_panda.pcap.active, value)

# Mimic directory exists check that happens normally in the PandA IOC
def check_dir_exits(value, **kwargs):
if os.path.exists(value):
set_mock_value(mock_hdf_panda.data.directory_exists, 1)

callback_on_mock_put(mock_hdf_panda.pcap.arm, link_function)
callback_on_mock_put(mock_hdf_panda.data.hdf_directory, check_dir_exits)

set_mock_value(
mock_hdf_panda.data.datasets,
Expand Down
1 change: 1 addition & 0 deletions tests/fastcs/panda/test_panda_utils.py
Original file line number Diff line number Diff line change
Expand Up @@ -50,6 +50,7 @@ async def test_save_panda(mock_save_to_yaml, mock_panda, RE: RunEngine):
},
{
"data.capture": False,
"data.create_directory": 0,
"data.flush_period": 0.0,
"data.hdf_directory": "",
"data.hdf_file_name": "",
Expand Down
33 changes: 31 additions & 2 deletions tests/fastcs/panda/test_writer.py
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
import os
from pathlib import Path
from unittest.mock import ANY

Expand All @@ -14,6 +15,9 @@
StaticPathProvider,
set_mock_value,
)
from ophyd_async.core._mock_signal_utils import (
callback_on_mock_put,
)
from ophyd_async.epics.pvi import create_children_from_annotations, fill_pvi_entries
from ophyd_async.fastcs.panda import (
CommonPandaBlocks,
Expand Down Expand Up @@ -84,6 +88,14 @@ async def mock_panda(panda_t):
async with DeviceCollector(mock=True):
mock_panda = panda_t("mock_PANDA", name="mock_panda")

# Mimic directory exists check that happens normally in the PandA IOC
def check_dir_exits(value, **kwargs):
if os.path.exists(os.path.abspath(os.path.dirname(value))):
set_mock_value(mock_panda.data.directory_exists, 1)

# Assume directory exists
callback_on_mock_put(mock_panda.data.hdf_directory, check_dir_exits)

set_mock_value(
mock_panda.data.datasets,
DatasetTable(
Expand All @@ -98,7 +110,7 @@ async def mock_panda(panda_t):
@pytest.fixture
async def mock_writer(tmp_path, mock_panda) -> PandaHDFWriter:
fp = StaticFilenameProvider("data")
dp = StaticPathProvider(fp, tmp_path / mock_panda.name)
dp = StaticPathProvider(fp, tmp_path / mock_panda.name, create_dir_depth=-1)
async with DeviceCollector(mock=True):
writer = PandaHDFWriter(
prefix="TEST-PANDA",
Expand Down Expand Up @@ -146,7 +158,7 @@ async def test_open_close_sets_capture(mock_writer: PandaHDFWriter):
async def test_open_sets_file_path_and_name(mock_writer: PandaHDFWriter, tmp_path):
await mock_writer.open()
path = await mock_writer.panda_data_block.hdf_directory.get_value()
assert path == tmp_path / mock_writer._name_provider()
assert path == str(tmp_path / mock_writer._name_provider())
name = await mock_writer.panda_data_block.hdf_file_name.get_value()
assert name == "data.h5"

Expand Down Expand Up @@ -204,3 +216,20 @@ def assert_resource_document(name, resource_doc):
assert_resource_document(name=name, resource_doc=resource_doc)

assert resource_doc["data_key"] == name


async def test_oserror_when_hdf_dir_does_not_exist(tmp_path, mock_panda):
fp = StaticFilenameProvider("data")
dp = StaticPathProvider(
fp, tmp_path / mock_panda.name / "extra" / "dirs", create_dir_depth=-1
)
async with DeviceCollector(mock=True):
writer = PandaHDFWriter(
prefix="TEST-PANDA",
path_provider=dp,
name_provider=lambda: "test-panda",
panda_data_block=mock_panda.data,
)

with pytest.raises(OSError):
await writer.open()

0 comments on commit 48a18f0

Please sign in to comment.