From b68d7e1afce3677df9298b14ab0234e64c66288e Mon Sep 17 00:00:00 2001 From: Tom Cobb Date: Wed, 11 Nov 2026 16:42:47 +0000 Subject: [PATCH] Type-check the whole tests directory with pyright Add tests to the pyright include in the type-checking tox env, previously only src and tests/unit_tests/core/test_status_types.py were checked. Fix the errors this exposes with corrected annotations, casts and narrowing, and use rule-specific pyright ignores where a test deliberately passes a bad value or touches an untyped backend. Assisted-by: Claude Opus 5.5 --- CLAUDE.md | 2 +- pyproject.toml | 4 +- tests/conftest.py | 9 ++- tests/container_tests/test_adsim_system.py | 6 +- .../epics/core/test_epics_signal_lifecycle.py | 2 +- .../core/test_epics_signal_mechanisms.py | 17 ++--- .../epics/eiger/test_eiger_system.py | 9 ++- tests/system_tests/tango/core/conftest.py | 6 +- .../tango/core/test_base_device.py | 24 ++++--- .../tango/core/test_tango_command.py | 37 +++++----- .../core/test_tango_declarative_device.py | 2 +- .../tango/core/test_tango_signal_lifecycle.py | 10 +-- .../tango/core/test_tango_transport.py | 67 ++++++++++++------- .../unit_tests/core/test_auto_init_devices.py | 24 +++++-- tests/unit_tests/core/test_command.py | 20 +++--- tests/unit_tests/core/test_device.py | 25 ++++--- tests/unit_tests/core/test_flyer.py | 4 +- tests/unit_tests/core/test_log.py | 4 +- .../core/test_mock_signal_backend.py | 16 +++-- .../core/test_multi_derived_signal.py | 2 +- tests/unit_tests/core/test_readable.py | 12 ++-- tests/unit_tests/core/test_signal.py | 15 ++--- .../core/test_soft_signal_backend.py | 29 ++++---- .../unit_tests/core/test_standard_detector.py | 14 ++-- tests/unit_tests/core/test_subset_enum.py | 13 ++-- tests/unit_tests/core/test_utils.py | 8 +-- .../core/test_watchable_async_status.py | 6 +- .../epics/adcore/test_cont_acq_detector.py | 2 +- .../unit_tests/epics/demo/test_epics_demo.py | 2 +- .../epics/test_default_trigger_info.py | 4 +- .../unit_tests/fastcs/panda/test_seq_table.py | 47 +++++++------ .../plan_stubs/test_ensure_connected.py | 2 + .../unit_tests/sim/test_sim_blob_detector.py | 9 +-- tests/unit_tests/test_branching.py | 6 +- 34 files changed, 265 insertions(+), 194 deletions(-) diff --git a/CLAUDE.md b/CLAUDE.md index e1032b8b40..a968b96ef4 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -30,7 +30,7 @@ tox -p # all envs in parallel (CI equivalent) ``` - `ruff` runs on save in VS Code; `pytest` also runs doctests in `docs/` and `src/`. -- **pyright ad hoc** (not via tox): always `pyright src --pythonpath "$(which python)"`. A bare `pyright src` reports ~115 false positives here (stale numpy-stub resolution) — never trust its count. +- **pyright ad hoc** (not via tox): always `pyright src tests --pythonpath "$(which python)"`. A bare `pyright src tests` reports ~115 false positives here (stale numpy-stub resolution) — never trust its count. - System tests need a live backend; scope runs to `tests/system_tests/epics` or `.../tango` and run 2–3× to catch flakiness. - `tests/container_tests` must be a **separate** `pytest` invocation from `tests/system_tests` — never one session. They reach their IOC via the ca-gateway, which needs `EPICS_CA_NAME_SERVERS`, and EPICS reads that once at libca init; `epics/core` imports pyepics, which initialises libca during *collection*, so a shared session silently breaks them. Needs `EXAMPLE_SERVICES_PATH` (a host path). diff --git a/pyproject.toml b/pyproject.toml index b387728898..43c689cdb4 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -162,9 +162,7 @@ commands = [ [ "pyright", "src", - # Its `assert_type` guards only bite if pyright checks them; `tests` as a - # whole is not clean enough to check, so this is listed by name - "tests/unit_tests/core/test_status_types.py", + "tests", "--pythonpath", "{env:VIRTUAL_ENV}/bin/python", { replace = "posargs", default = [ diff --git a/tests/conftest.py b/tests/conftest.py index b49c963d09..90967e4777 100644 --- a/tests/conftest.py +++ b/tests/conftest.py @@ -11,7 +11,10 @@ from typing import Any import pytest -from bluesky.run_engine import RunEngine, TransitionError +from bluesky.run_engine import ( + RunEngine, + TransitionError, # pyright: ignore[reportPrivateImportUsage] +) from pytest import FixtureRequest from ophyd_async.core import ( @@ -139,8 +142,8 @@ def _error_and_kill_pending_tasks( task for task in asyncio.all_tasks(loop) if (coro := task.get_coro()) is not None - and hasattr(coro, "__name__") - and coro.__name__ not in _ALLOWED_PYTEST_TASKS + and (name := getattr(coro, "__name__", None)) is not None + and name not in _ALLOWED_PYTEST_TASKS and not task.done() } for task in unfinished_tasks: diff --git a/tests/container_tests/test_adsim_system.py b/tests/container_tests/test_adsim_system.py index f1791639f0..db74e2fd04 100644 --- a/tests/container_tests/test_adsim_system.py +++ b/tests/container_tests/test_adsim_system.py @@ -155,7 +155,7 @@ def prepare_then_count() -> MsgGenerator[None]: RE(prepare_then_count()) - actual_exposure_time: float = RE(bps.rd(adsim.driver.acquire_time)).plan_result + actual_exposure_time: float = RE(bps.rd(adsim.driver.acquire_time)).plan_result # pyright: ignore[reportAttributeAccessIssue] assert actual_exposure_time == 0.2 @@ -165,7 +165,7 @@ def test_software_triggering( ) -> None: docs = run_plan_and_get_documents(RE, bp.count([adsim], num=2)) assert docs == [ - RunStart( + RunStart( # pyright: ignore[reportCallIssue] uid=ANY, time=ANY, versions=ANY, @@ -240,7 +240,7 @@ def test_software_triggering( }, object_keys={"adsim": ["adsim"]}, run_start=ANY, - hints={"adsim": {"fields": ["adsim"]}}, + hints={"adsim": {"fields": ["adsim"]}}, # pyright: ignore[reportArgumentType] ), StreamResource( uid=ANY, diff --git a/tests/system_tests/epics/core/test_epics_signal_lifecycle.py b/tests/system_tests/epics/core/test_epics_signal_lifecycle.py index 264a3fdf09..2a71692c27 100644 --- a/tests/system_tests/epics/core/test_epics_signal_lifecycle.py +++ b/tests/system_tests/epics/core/test_epics_signal_lifecycle.py @@ -560,7 +560,7 @@ async def test_signal_error_paths(ioc: LifecycleIoc, protocol: str): # 0 is simply accepted as "the choice at index 0" - there's no # EPICS-side equivalent of Tango's set()-time TypeError.) with pytest.raises(ValueError): - await device.subset_enum.set("NOT_A_REAL_CHOICE") + await device.subset_enum.set("NOT_A_REAL_CHOICE") # pyright: ignore[reportArgumentType] # Wrong inferred datatype at connect time: a_str is a string record. with pytest.raises(TypeError, match="cannot be coerced to int"): diff --git a/tests/system_tests/epics/core/test_epics_signal_mechanisms.py b/tests/system_tests/epics/core/test_epics_signal_mechanisms.py index 9d5f8f8c5a..d9366fc181 100644 --- a/tests/system_tests/epics/core/test_epics_signal_mechanisms.py +++ b/tests/system_tests/epics/core/test_epics_signal_mechanisms.py @@ -556,7 +556,7 @@ async def test_can_read_using_ophyd_async_then_ophyd( def a_plan(): yield from bps.rd(ophyd_async_sig) - yield from bps.rd(ophyd_signal) + yield from bps.rd(ophyd_signal) # pyright: ignore[reportArgumentType] RE(a_plan()) @@ -621,16 +621,17 @@ async def test_all_updates_defaults_to_true( async def default_all_updates(pv: str) -> bool: sig = epics_signal_rw(int, pv) await sig.connect() - backend = sig._connector.backend + backend = _get_epics_backend(sig) + sig.subscribe_reading(lambda v: None) + subscription = backend.subscription + assert isinstance(subscription, Subscription) try: - sig.subscribe_reading(lambda v: ...) - assert isinstance(backend.subscription, Subscription) - assert isinstance(backend.subscription.all_updates, bool) - return backend.subscription.all_updates + assert isinstance(subscription.all_updates, bool) + return subscription.all_updates finally: - backend.subscription.close() - sig.clear_sub(lambda v: ...) + subscription.close() + sig.clear_sub(lambda v: None) @pytest.mark.timeout(TIMEOUT + 0.5) diff --git a/tests/system_tests/epics/eiger/test_eiger_system.py b/tests/system_tests/epics/eiger/test_eiger_system.py index f4dc4718bd..5e168aa130 100644 --- a/tests/system_tests/epics/eiger/test_eiger_system.py +++ b/tests/system_tests/epics/eiger/test_eiger_system.py @@ -115,7 +115,12 @@ async def setup_device(RE, ioc_prefixes): async def test_eiger(RE, ioc_prefixes) -> EigerDetector: provider = StaticPathProvider(StaticFilenameProvider("test_eiger"), Path(SAVE_PATH)) async with init_devices(): - test_eiger = EigerDetector("", provider, ioc_prefixes[0], ioc_prefixes[1]) + test_eiger = EigerDetector( + "", + provider, + ioc_prefixes[0], + ioc_prefixes[1], # pyright: ignore[reportCallIssue] + ) return test_eiger @@ -123,10 +128,8 @@ async def test_eiger(RE, ioc_prefixes) -> EigerDetector: @pytest.mark.skip(reason="Flaky test, see GH-819") async def test_trigger_saves_file(test_eiger: EigerDetector, setup_device: SetupDevice): single_shot = TriggerInfo( - exposure_timeout=None, number_of_events=1, trigger=DetectorTrigger.INTERNAL, - livetime=None, ) await test_eiger.stage() diff --git a/tests/system_tests/tango/core/conftest.py b/tests/system_tests/tango/core/conftest.py index d5d426a0c7..39ba65e760 100644 --- a/tests/system_tests/tango/core/conftest.py +++ b/tests/system_tests/tango/core/conftest.py @@ -139,7 +139,7 @@ def distinct_value(self): serves. """ for value in self.random_put_values: - if not np.array_equal(value, self.initial): + if not np.array_equal(value, self.initial): # pyright: ignore[reportArgumentType] return value raise ValueError(f"No put value for {self.name} differs from its initial") @@ -235,7 +235,7 @@ def add_ads( name="strenum", py_type=ExampleStrEnum, initial=ExampleStrEnum.B, - random_put_values=[ + random_put_values=[ # pyright: ignore[reportArgumentType] ExampleStrEnum.A.value, ExampleStrEnum.B.value, ExampleStrEnum.C.value, @@ -278,7 +278,7 @@ def add_ads( "my_state", DevStateEnum, DevStateEnum.INIT, - random_put_values=[e.name for e in DevStateEnum], + random_put_values=[e.name for e in DevStateEnum], # pyright: ignore[reportArgumentType] cmd_name=None, ) diff --git a/tests/system_tests/tango/core/test_base_device.py b/tests/system_tests/tango/core/test_base_device.py index bf4efd6fbb..cb54b15011 100644 --- a/tests/system_tests/tango/core/test_base_device.py +++ b/tests/system_tests/tango/core/test_base_device.py @@ -26,7 +26,12 @@ init_devices, ) from ophyd_async.core import StandardReadableFormat as Format -from ophyd_async.tango.core import TangoDevice, get_full_attr_trl, get_python_type +from ophyd_async.tango.core import ( + TangoDevice, + TangoDeviceConnector, + get_full_attr_trl, + get_python_type, +) from ophyd_async.tango.demo import ( DemoMotor, DemoPointDetector, @@ -57,7 +62,7 @@ class TestTangoReadable(TangoDevice, StandardReadable): async def describe_class(fqtrl): description = {} values = {} - dev = await AsyncDeviceProxy(fqtrl) + dev = await AsyncDeviceProxy(fqtrl) # pyright: ignore[reportGeneralTypeIssues] for name in TESTED_FEATURES: dtype = "none" @@ -89,7 +94,7 @@ async def describe_class(fqtrl): elif name in dev.get_command_list(): cmd_conf = await dev.get_command_config(name) - _, _, descr = get_python_type(cmd_conf) + _, _, descr = get_python_type(cmd_conf) # pyright: ignore[reportGeneralTypeIssues] shape = [] value = getattr(dev, name)() @@ -122,7 +127,7 @@ def get_test_descriptor(python_type: type[T], value: T, is_cmd: bool) -> dict: if python_type in [str]: return {"dtype": "string", "shape": []} if issubclass(python_type, DevState): - return {"dtype": "string", "shape": [], "choices": list(DevState.names.keys())} + return {"dtype": "string", "shape": [], "choices": list(DevState.names.keys())} # pyright: ignore[reportAttributeAccessIssue] if issubclass(python_type, Enum): return { "dtype": "string", @@ -132,7 +137,7 @@ def get_test_descriptor(python_type: type[T], value: T, is_cmd: bool) -> dict: return { "dtype": "array", - "shape": [np.Inf] if is_cmd else list(np.array(value).shape), + "shape": [np.inf] if is_cmd else list(np.array(value).shape), } @@ -159,6 +164,7 @@ async def test_set_trl(tango_test_device): values, description = await describe_class(tango_test_device) test_device = TestTangoReadable(trl="", name="test_device") + assert isinstance(test_device._connector, TangoDeviceConnector) test_device._connector.set_trl(tango_test_device) await test_device.connect() @@ -275,13 +281,13 @@ async def test_command_autofill(tango_test_device): await test_device.connect() assert hasattr(test_device, "echo") - echo = test_device.echo + echo = test_device.echo # pyright: ignore[reportAttributeAccessIssue] assert hasattr(test_device, "set_msg") - set_msg = test_device.set_msg + set_msg = test_device.set_msg # pyright: ignore[reportAttributeAccessIssue] assert hasattr(test_device, "get_msg") - get_msg = test_device.get_msg + get_msg = test_device.get_msg # pyright: ignore[reportAttributeAccessIssue] assert hasattr(test_device, "clear") - clear = test_device.clear + clear = test_device.clear # pyright: ignore[reportAttributeAccessIssue] assert isinstance(echo, Command) assert isinstance(set_msg, Command) diff --git a/tests/system_tests/tango/core/test_tango_command.py b/tests/system_tests/tango/core/test_tango_command.py index a5155062b9..027614e554 100644 --- a/tests/system_tests/tango/core/test_tango_command.py +++ b/tests/system_tests/tango/core/test_tango_command.py @@ -50,12 +50,12 @@ (Sequence[str], ["a", "b"], "a_str_spectrum_cmd"), ( TangoLongStringTable, - TangoLongStringTable(long=[1, 2], string=["a", "b"]), + TangoLongStringTable(long=[1, 2], string=["a", "b"]), # pyright: ignore[reportArgumentType] "long_string_cmd", ), ( TangoDoubleStringTable, - TangoDoubleStringTable(double=[1.1, 2.2], string=["a", "b"]), + TangoDoubleStringTable(double=[1.1, 2.2], string=["a", "b"]), # pyright: ignore[reportArgumentType] "double_string_cmd", ), ] @@ -104,6 +104,7 @@ async def test_tango_command( assert isinstance(cmd, TriggerableCommand) else: assert isinstance(cmd, Command) + assert cmd.signature is not None if name in ["int8_spectrum_cmd", "uint8_spectrum_cmd"]: assert Array1D[np.uint8] == cmd.signature.return_annotation assert ( @@ -119,9 +120,9 @@ async def test_tango_command( assert ctype == list(cmd.signature.parameters.values())[0].annotation if isinstance(val, np.ndarray): - assert np.array_equal(val, await cmd.execute(val)) + assert np.array_equal(val, await cmd.execute(val)) # pyright: ignore[reportCallIssue] else: - assert val == await cmd.execute(val) + assert val == await cmd.execute(val) # pyright: ignore[reportCallIssue] @pytest.mark.parametrize("ctype, val, name", TEST_PARAMS) @@ -142,7 +143,7 @@ async def test_tango_command_factory( trl = "" - def spec(x: ctype) -> ctype: ... + def spec(x: ctype) -> ctype: ... # pyright: ignore[reportInvalidTypeForm] if everything_device_trl.endswith("#dbase=no"): trl = everything_device_trl[:-9] + f"/{name}" + everything_device_trl[-9:] @@ -171,12 +172,13 @@ def spec(x: ctype) -> ctype: ... if name == "void_cmd": assert cmd.signature is None else: + assert cmd.signature is not None assert expected_datatype == cmd.signature.return_annotation if isinstance(val, np.ndarray): - assert np.array_equal(val, await cmd.execute(val)) + assert np.array_equal(val, await cmd.execute(val)) # pyright: ignore[reportCallIssue] else: - assert val == await cmd.execute(val) + assert val == await cmd.execute(val) # pyright: ignore[reportCallIssue] @pytest.mark.asyncio @@ -184,56 +186,56 @@ async def test_tango_command_validation( everything_device_trl: str, ): # This should pass - def call_spec(x: float) -> float: ... + def float_to_float_spec(x: float) -> float: ... trl = "" if everything_device_trl.endswith("#dbase=no"): trl = everything_device_trl[:-9] + "/float64_cmd" + everything_device_trl[-9:] - cmd = tango_command(call_spec=call_spec, trl=trl, name="float64_cmd") + cmd = tango_command(call_spec=float_to_float_spec, trl=trl, name="float64_cmd") await cmd.connect() ret = await cmd.execute(1.0) assert ret == 1.0 # This should pass - def call_spec(x: float) -> None: ... + def float_to_none_spec(x: float) -> None: ... trl = "" if everything_device_trl.endswith("#dbase=no"): trl = everything_device_trl[:-9] + "/float64_cmd" + everything_device_trl[-9:] - cmd = tango_command(call_spec=call_spec, trl=trl, name="float64_cmd") + cmd = tango_command(call_spec=float_to_none_spec, trl=trl, name="float64_cmd") await cmd.connect() ret = await cmd.execute(1.0) assert ret == 1.0 # Multiple input params should fail # Commands with more than one input parameter are not yet supported. - def call_spec(x: float, y: int) -> float: ... + def two_inputs_spec(x: float, y: int) -> float: ... trl = "" if everything_device_trl.endswith("#dbase=no"): trl = everything_device_trl[:-9] + "/float64_cmd" + everything_device_trl[-9:] with pytest.raises(TypeError) as excinfo: - tango_command(call_spec=call_spec, trl=trl, name="float64_cmd") + tango_command(call_spec=two_inputs_spec, trl=trl, name="float64_cmd") assert "Commands with more than one input parameter" in str(excinfo.value) # Mistyped return type should fail unless it is None - def call_spec(x: float) -> int: ... + def float_to_int_spec(x: float) -> int: ... trl = "" if everything_device_trl.endswith("#dbase=no"): trl = everything_device_trl[:-9] + "/float64_cmd" + everything_device_trl[-9:] - cmd = tango_command(call_spec=call_spec, trl=trl, name="float64_cmd") + cmd = tango_command(call_spec=float_to_int_spec, trl=trl, name="float64_cmd") with pytest.raises(TypeError) as excinfo: await cmd.connect() assert "not " in str(excinfo.value) # Mistyped input parameter should fail unless it is None - def call_spec(x: Array1D[np.float32]) -> float: ... + def array_input_spec(x: Array1D[np.float32]) -> float: ... trl = "" if everything_device_trl.endswith("#dbase=no"): trl = everything_device_trl[:-9] + "/float64_cmd" + everything_device_trl[-9:] - cmd = tango_command(call_spec=call_spec, trl=trl, name="float64_cmd") + cmd = tango_command(call_spec=array_input_spec, trl=trl, name="float64_cmd") with pytest.raises(TypeError) as excinfo: await cmd.connect() assert "has input parameter of type , not" in str(excinfo.value) @@ -290,6 +292,7 @@ def call_spec(x: float) -> bool: ... ) cmd = tango_command(call_spec=call_spec, trl=trl, name="float_to_bool_cmd") await cmd.connect() + assert cmd.signature is not None assert cmd.signature.return_annotation is bool assert list(cmd.signature.parameters.values())[0].annotation is float assert await cmd.execute(1.0) is True diff --git a/tests/system_tests/tango/core/test_tango_declarative_device.py b/tests/system_tests/tango/core/test_tango_declarative_device.py index f4b0945617..bc99ae8f7b 100644 --- a/tests/system_tests/tango/core/test_tango_declarative_device.py +++ b/tests/system_tests/tango/core/test_tango_declarative_device.py @@ -73,7 +73,7 @@ class _BadAnnotationDevice(TangoDevice, StandardReadable): # strenum is really a StrictEnum-backed attribute (see TangoTestDevice) - # datatype of enum commands must be explicitly hinted, so annotating it # SignalRW[None] here is a genuine mismatch, not a missing hint. - strenum: A[SignalRW[None], Format.HINTED_UNCACHED_SIGNAL] + strenum: A[SignalRW[None], Format.HINTED_UNCACHED_SIGNAL] # pyright: ignore[reportInvalidTypeArguments] @pytest.mark.asyncio diff --git a/tests/system_tests/tango/core/test_tango_signal_lifecycle.py b/tests/system_tests/tango/core/test_tango_signal_lifecycle.py index f2b6d867a3..ce02d90e04 100644 --- a/tests/system_tests/tango/core/test_tango_signal_lifecycle.py +++ b/tests/system_tests/tango/core/test_tango_signal_lifecycle.py @@ -150,7 +150,7 @@ async def reset_everything_device(everything_device_trl: str) -> None: return resetter = TangoDevice(everything_device_trl, name="resetter") await resetter.connect() - await resetter.reset_values.trigger() + await resetter.reset_values.trigger() # pyright: ignore[reportAttributeAccessIssue] await asyncio.sleep(1) _reset_everything_device_done = True @@ -380,11 +380,11 @@ async def test_signal_error_paths(everything_device_trl: str): # Wrong Python type for a StrictEnum signal with pytest.raises(TypeError): - await device.strenum.set(0) + await device.strenum.set(0) # pyright: ignore[reportArgumentType] # Right type, not a valid choice with pytest.raises(ValueError): - await device.strenum.set("NOT_A_REAL_CHOICE") + await device.strenum.set("NOT_A_REAL_CHOICE") # pyright: ignore[reportArgumentType] # A well-formed TRL (real host:port, so PyTango accepts the syntax) but # for a device name nothing is serving @@ -413,9 +413,9 @@ async def test_enum_set_accepts_name_value_and_member( device = TangoTestDevice(everything_device_trl, name="enum_converter") await device.connect() try: - await device.strenum.set("AAA") + await device.strenum.set("AAA") # pyright: ignore[reportArgumentType] await device.strenum.set(ExampleStrEnum.B) - await device.strenum.set(ExampleStrEnum.C.value) + await device.strenum.set(ExampleStrEnum.C.value) # pyright: ignore[reportArgumentType] await device.my_state.set(DevStateEnum.EXTRACT) finally: # Leave the server as documented-default for the next test/module. diff --git a/tests/system_tests/tango/core/test_tango_transport.py b/tests/system_tests/tango/core/test_tango_transport.py index 12268f9d3d..e9384ddaf7 100644 --- a/tests/system_tests/tango/core/test_tango_transport.py +++ b/tests/system_tests/tango/core/test_tango_transport.py @@ -1,13 +1,13 @@ import asyncio import logging -from collections.abc import Sequence -from typing import Any, TypeVar, get_origin +from collections.abc import Awaitable, Callable, Sequence +from typing import Any, TypeVar, cast, get_origin import numpy as np import numpy.typing as npt import pytest from tango import AttrDataFormat, CmdArgType, DevState -from tango.asyncio import DeviceProxy +from tango.asyncio import DeviceProxy as _AsyncioDeviceProxy from tango.asyncio_executor import ( AsyncioExecutor, get_global_executor, @@ -25,6 +25,7 @@ CommandProxy, DevStateEnum, TangoDevice, + TangoDeviceConnector, TangoDoubleStringTable, TangoLongStringTable, TangoSignalBackend, @@ -44,6 +45,10 @@ # tango_test_device fixture comes from conftest.py, shared with every other # test module in this directory. +# tango's stubs say the asyncio DeviceProxy constructor returns a DeviceProxy, +# but in asyncio green mode it returns an awaitable of one +DeviceProxy = cast(Callable[..., Awaitable[Any]], _AsyncioDeviceProxy) + # -------------------------------------------------------------------- async def prepare_device(trl: str, pv: str, put_value: Any) -> None: @@ -62,7 +67,7 @@ async def make_backend( typ: type | None, pv: str, connect: bool = True, - allow_events: bool | None = True, + allow_events: bool = True, ) -> TangoSignalBackend: """Wrapper for making the tango signal backend.""" @@ -254,6 +259,7 @@ def test_get_python_type(tango_type, tango_format, expected): if tango_type is CmdArgType.DevEnum: config.enum_labels = ["A", "B", "C"] py_type = get_python_type(config) + assert py_type is not None assert issubclass(py_type, StrictEnum) assert [e.name for e in py_type] == ["A", "B", "C"] elif tango_format == "bad_format": @@ -310,7 +316,7 @@ def test_try_to_cast_as_float(): (npt.NDArray[np.uint32], np.dtype("uint32")), (npt.NDArray[np.uint64], np.dtype("uint64")), (npt.NDArray[np.bool_], np.dtype("bool")), - (npt.NDArray[DevState], CmdArgType.DevState), + (npt.NDArray[DevState], CmdArgType.DevState), # pyright: ignore[reportInvalidTypeForm] (npt.NDArray[np.str_], np.dtype("str")), (npt.NDArray[np.float32], np.dtype("float32")), (npt.NDArray[np.complex64], np.dtype("complex64")), @@ -373,7 +379,7 @@ async def test_attribute_proxy_get(tango_test_device, attr): async def test_attribute_proxy_put(tango_test_device, attr): device_proxy = await DeviceProxy(tango_test_device) attr_proxy = AttributeProxy(device_proxy, attr) - old_value = await attr_proxy.get() + old_value: Any = await attr_proxy.get() new_value = old_value + 1 await attr_proxy.put(new_value) await asyncio.sleep( @@ -412,7 +418,7 @@ async def test_attribute_get_config(tango_test_device): device_proxy = await DeviceProxy(tango_test_device) attr_proxy = AttributeProxy(device_proxy, "justvalue") config = await attr_proxy.get_config() - assert config.writable is not None + assert config.writable is not None # pyright: ignore[reportAttributeAccessIssue] # -------------------------------------------------------------------- @@ -467,7 +473,7 @@ def bad_callback(): attr_proxy.set_polling(True, 0.1, 1, 1.0) attr_proxy.subscribe_callback(callback) - current_value = await attr_proxy.get() + current_value: Any = await attr_proxy.get() new_value = current_value + 2 await attr_proxy.put(new_value) polling_period = attr_proxy._polling_period @@ -501,7 +507,7 @@ def bad_callback(): attr_proxy.unsubscribe_callback() # Test polling with bad callback - attr_proxy.subscribe_callback(bad_callback) + attr_proxy.subscribe_callback(bad_callback) # pyright: ignore[reportArgumentType] await asyncio.sleep(0.2) assert "Could not poll the attribute" in str(attr_proxy.exception) attr_proxy.unsubscribe_callback() @@ -550,7 +556,7 @@ async def test_attribute_poll_exceptions(tango_test_device): def callback(reading, value): pass - attr_proxy.subscribe_callback(callback) + attr_proxy.subscribe_callback(callback) # pyright: ignore[reportArgumentType] await asyncio.sleep(0.2) assert "Could not poll the attribute" in str(attr_proxy.exception) attr_proxy.unsubscribe_callback() @@ -658,7 +664,9 @@ async def test_tango_transport_put(tango_test_device): await transport.connect(1) source = transport.source("", True) await transport.put(2.0) - val = await transport.proxies[source].get_w_value() + proxy = transport.proxies[source] + assert proxy is not None + val = await proxy.get_w_value() assert val == 2.0 @@ -681,13 +689,14 @@ async def test_tango_transport_get_datakey(tango_test_device): assert datakey["source"] == trl assert datakey["dtype"] == "number" assert datakey["shape"] == [] + limits = datakey.get("limits") + assert limits is not None for key in ["alarm", "control", "rds", "warning"]: - assert key in datakey["limits"] - limits = datakey["limits"] - assert limits["alarm"] == {"high": 5.0, "low": 1.0} - assert limits["control"] == {"high": 6.0, "low": 0.0} - assert limits["rds"] == {"time_difference": 1.0, "value_difference": 1.0} - assert limits["warning"] == {"high": 4.0, "low": 2.0} + assert key in limits + assert limits.get("alarm") == {"high": 5.0, "low": 1.0} + assert limits.get("control") == {"high": 6.0, "low": 0.0} + assert limits.get("rds") == {"time_difference": 1.0, "value_difference": 1.0} + assert limits.get("warning") == {"high": 4.0, "low": 2.0} # -------------------------------------------------------------------- @@ -843,7 +852,7 @@ async def test_type_mismatch_array(tango_test_device): device_proxy = await DeviceProxy(tango_test_device) trl = get_full_attr_trl(tango_test_device, "array") - transport = TangoSignalBackend(npt.NDArray[int], trl, trl, device_proxy) + transport = TangoSignalBackend(npt.NDArray[int], trl, trl, device_proxy) # pyright: ignore[reportInvalidTypeForm] with pytest.raises(TypeError) as exc: await transport.connect(1) val = str(exc.value) @@ -859,7 +868,12 @@ async def test_type_mismatch_sequence(tango_test_device): device_proxy = await DeviceProxy(tango_test_device) trl = get_full_attr_trl(tango_test_device, "sequence") - transport = TangoSignalBackend(Sequence[int], trl, trl, device_proxy) + transport = TangoSignalBackend( + Sequence[int], # pyright: ignore[reportArgumentType] + trl, + trl, + device_proxy, + ) with pytest.raises(TypeError) as exc: await transport.connect(1) val = str(exc.value) @@ -930,7 +944,7 @@ def callback(reading, value): pass with caplog.at_level(logging.DEBUG, logger="ophyd_async"): - attr_proxy.subscribe_callback(callback) + attr_proxy.subscribe_callback(callback) # pyright: ignore[reportArgumentType] await asyncio.sleep(0.1) assert any( @@ -943,7 +957,7 @@ def callback(reading, value): # now-deleted test_tango_signals.py (see the block comment above # `make_backend` near the top of this file). # -------------------------------------------------------------------- -def get_test_descriptor(python_type: type[T], value: T, is_cmd: bool) -> dict: +def get_test_descriptor(python_type: Any, value: Any, is_cmd: bool) -> dict: if python_type in [bool]: return {"dtype": "boolean", "shape": []} if python_type in [int]: @@ -1015,10 +1029,13 @@ async def test_backend_get_put_monitor_attr( """ device = TangoDevice(everything_device_trl, name="backend_get_put_monitor") await device.connect() + connector = device._connector + assert isinstance(connector, TangoDeviceConnector) + assert connector.trl is not None try: for attr_data in everything_signal_info.values(): signal = getattr(device, attr_data.name) - source = get_full_attr_trl(device._connector.trl, attr_data.name) + source = get_full_attr_trl(connector.trl, attr_data.name) await asyncio.wait_for( assert_monitor_then_put( signal, @@ -1084,7 +1101,7 @@ def callback(reading): # Try to add a non-callable callback transport.allow_events(True) with pytest.raises(RuntimeError) as exc: - transport.set_callback(1) + transport.set_callback(1) # pyright: ignore[reportArgumentType] assert "Callback must be a callable" in str(exc.value) @@ -1111,6 +1128,7 @@ async def test_attribute_subscribe_callback(everything_device_trl): source = get_full_attr_trl(everything_device_trl, "int64") backend = await make_backend(int, source) attr_proxy = backend.proxies[source] + assert isinstance(attr_proxy, AttributeProxy) val = None def callback(reading): @@ -1119,7 +1137,7 @@ def callback(reading): attr_proxy.subscribe_callback(callback) assert attr_proxy.has_subscription() - old_value = await attr_proxy.get() + old_value: Any = await attr_proxy.get() new_value = old_value + 1 await attr_proxy.put(new_value) await asyncio.sleep(0.2) @@ -1138,6 +1156,7 @@ async def test_attribute_unsubscribe_callback(everything_device_trl): source = get_full_attr_trl(everything_device_trl, "float32") backend = await make_backend(float, source) attr_proxy = backend.proxies[source] + assert isinstance(attr_proxy, AttributeProxy) def callback(reading): pass diff --git a/tests/unit_tests/core/test_auto_init_devices.py b/tests/unit_tests/core/test_auto_init_devices.py index 754966704d..2f93545c28 100644 --- a/tests/unit_tests/core/test_auto_init_devices.py +++ b/tests/unit_tests/core/test_auto_init_devices.py @@ -3,11 +3,15 @@ import pytest from bluesky import plan_stubs as bps -from bluesky.run_engine import RunEngine, TransitionError +from bluesky.run_engine import ( + RunEngine, + TransitionError, # pyright: ignore[reportPrivateImportUsage] +) from ophyd_async.core import ( DEFAULT_TIMEOUT, Device, + DeviceMock, NotConnectedError, init_devices, print_connection_status, @@ -18,7 +22,10 @@ class FailingDevice(Device): async def connect( - self, mock: bool = False, timeout=DEFAULT_TIMEOUT, force_reconnect=False + self, + mock: bool | DeviceMock = False, + timeout=DEFAULT_TIMEOUT, + force_reconnect=False, ): raise AttributeError() @@ -27,7 +34,10 @@ class WorkingDevice(Device): connected = False async def connect( - self, mock: bool = True, timeout=DEFAULT_TIMEOUT, force_reconnect=False + self, + mock: bool | DeviceMock = True, + timeout=DEFAULT_TIMEOUT, + force_reconnect=False, ): self.connected = True return await super().connect(mock=True) @@ -103,7 +113,7 @@ def test_sync_init_devices_no_run_engine_raises_error(): "https://blueskyproject.io/ophyd-async/main/" "user/explanations/event-loop-choice.html for more info." ) - assert not working_device.connected + assert not working_device.connected # pyright: ignore[reportPossiblyUnboundVariable] def test_sync_init_devices_run_engine_created_connects(RE): @@ -162,14 +172,14 @@ def my_plan(): ) finally: - if RE.state not in ("idle", "panicked"): + if RE.state not in ("idle", "panicked"): # pyright: ignore[reportPossiblyUnboundVariable] try: - RE.halt() + RE.halt() # pyright: ignore[reportPossiblyUnboundVariable] except TransitionError: pass loop.call_soon_threadsafe(loop.stop) checking_loop.call_soon_threadsafe(checking_loop.stop) - RE._th.join() + RE._th.join() # pyright: ignore[reportPossiblyUnboundVariable] loop.close() checking_loop.close() diff --git a/tests/unit_tests/core/test_command.py b/tests/unit_tests/core/test_command.py index 96c9c6ab8e..7427332961 100644 --- a/tests/unit_tests/core/test_command.py +++ b/tests/unit_tests/core/test_command.py @@ -78,7 +78,7 @@ class MyTable(Table): @pytest.mark.parametrize("datatype, value", TEST_PARAMS) async def test_soft_command_execution(datatype, value): - def callback(v: datatype) -> datatype: + def callback(v: datatype) -> datatype: # pyright: ignore[reportInvalidTypeForm] return v cmd = soft_command(callback, name="test_cmd") @@ -138,23 +138,23 @@ async def callback(a: int, b: int) -> int: backend = SoftCommandBackend(callback, sig) # Too few arguments with pytest.raises(TypeError, match="missing a required argument"): - await backend.execute(1) + await backend.execute(1) # pyright: ignore[reportCallIssue] # Too many positional arguments with pytest.raises(TypeError, match="too many positional arguments"): - await backend.execute(1, 2, 3) + await backend.execute(1, 2, 3) # pyright: ignore[reportCallIssue] # Unexpected keyword argument with pytest.raises(TypeError, match="unexpected keyword argument"): - await backend.execute(a=1, b=2, c=3) + await backend.execute(a=1, b=2, c=3) # pyright: ignore[reportCallIssue] # Multiple values for the same argument with pytest.raises(TypeError, match="multiple values for argument"): - await backend.execute(1, a=2) + await backend.execute(1, a=2) # pyright: ignore[reportCallIssue] @pytest.mark.parametrize("datatype, value", TEST_PARAMS) async def test_mock_command_backend_default_values(datatype, value): """Test MockCommandBackend returns manufactured defaults for various types.""" - async def callback(v: datatype) -> datatype: + async def callback(v: datatype) -> datatype: # pyright: ignore[reportInvalidTypeForm] return v cmd = soft_command(callback, name="test_cmd") @@ -274,6 +274,7 @@ async def callback(a: int) -> str: assert cmd._connector.backend.source("test") == "mock+softcmd://test" # Verify return type + assert cmd.signature is not None assert cmd.signature.return_annotation is str # Verify connect raises error @@ -380,8 +381,8 @@ async def callback() -> int: filler = DeviceFiller( device=vector, - signal_backend_factory=lambda _: None, - device_connector_factory=lambda: None, + signal_backend_factory=lambda _: None, # pyright: ignore[reportArgumentType] + device_connector_factory=lambda: None, # pyright: ignore[reportArgumentType] command_backend_factory=backend_factory, ) @@ -422,6 +423,7 @@ async def test_mock_command_null_sig_backend_uses_no_arg_void_signature(): await cmd.connect(mock=mock) assert cmd.signature == NO_ARG_VOID_SIGNATURE + assert cmd.signature is not None assert cmd.signature.return_annotation is None assert list(cmd.signature.parameters) == [] @@ -456,7 +458,7 @@ async def test_mock_command_applies_conversion_like_soft(datatype, raw, expected converted, exactly as SoftCommandBackend.execute() would convert it. """ - def callback(v: datatype) -> datatype: + def callback(v: datatype) -> datatype: # pyright: ignore[reportInvalidTypeForm] return v cmd = soft_command(callback, name="test_cmd") diff --git a/tests/unit_tests/core/test_device.py b/tests/unit_tests/core/test_device.py index fad45e194f..33e88a7a7a 100644 --- a/tests/unit_tests/core/test_device.py +++ b/tests/unit_tests/core/test_device.py @@ -1,7 +1,6 @@ import asyncio import os import time -import traceback from unittest.mock import AsyncMock, MagicMock, Mock import pytest @@ -11,6 +10,7 @@ Device, DeviceFiller, DeviceMap, + DeviceMock, DeviceProcessor, DeviceVector, NotConnectedError, @@ -63,7 +63,10 @@ def __init__(self) -> None: super().__init__() async def connect( - self, mock=False, timeout=DEFAULT_TIMEOUT, force_reconnect: bool = False + self, + mock: bool | DeviceMock = False, + timeout: float = DEFAULT_TIMEOUT, + force_reconnect: bool = False, ): self.connected = True @@ -123,7 +126,7 @@ def test_reserved_attr_allowed_by_env_var(monkeypatch, value): device = Device() mock = AsyncMock() device.set = mock - assert device.set is mock + assert device.set is mock # pyright: ignore[reportAttributeAccessIssue] @pytest.mark.parametrize("value", ["NO", "", "true", "1"]) @@ -137,7 +140,7 @@ def test_reserved_attr_still_raises_when_env_var_not_yes(monkeypatch, value): def test_set_mock_attr_overrides_reserved_name_and_returns_mock(): device = Device() mock = set_mock_attr(device, "set", AsyncMock()) - assert device.set is mock + assert device.set is mock # pyright: ignore[reportAttributeAccessIssue] async def test_device_connect_missing_connector() -> None: @@ -245,7 +248,12 @@ class DummyDeviceWithSleep(DummyBaseDevice): def __init__(self, name) -> None: self.set_name(name) - async def connect(self, mock=False, timeout=DEFAULT_TIMEOUT): + async def connect( + self, + mock: bool | DeviceMock = False, + timeout: float = DEFAULT_TIMEOUT, + force_reconnect: bool = False, + ): await asyncio.sleep(0.01) self.connected = True @@ -267,11 +275,12 @@ async def test_wait_for_connection_propagates_error( with pytest.raises(NotConnectedError) as exc: await wait_for_connection(**failing_coros) - assert traceback.extract_tb(exc.__traceback__)[-1].name == "failing_coroutine" + assert list(exc.value.sub_errors) == ["failing"] async def test_device_log_has_correct_name(): device = DummyBaseDevice() + assert device.log.extra is not None assert device.log.extra["ophyd_async_device_name"] == "" device.set_name("device") assert device.log.extra["ophyd_async_device_name"] == "device" @@ -468,8 +477,8 @@ async def process_devices(devices: dict[str, Device]): ok = DummyDeviceGroup("parent") not_ok = DummyDisconnectDevice("not_ok") - assert ok.name in registry - assert not_ok.name not in registry + assert ok.name in registry # pyright: ignore[reportPossiblyUnboundVariable] + assert not_ok.name not in registry # pyright: ignore[reportPossiblyUnboundVariable] def test_device_repr_and_str_with_name(): diff --git a/tests/unit_tests/core/test_flyer.py b/tests/unit_tests/core/test_flyer.py index 70faeb4ce5..5e113ba005 100644 --- a/tests/unit_tests/core/test_flyer.py +++ b/tests/unit_tests/core/test_flyer.py @@ -185,7 +185,7 @@ async def test_flyable_complete_without_movable_logic_yields_no_updates( status = flyer.complete() assert isinstance(status, WatchableAsyncStatus) updates: list[dict] = [] - status.watch(lambda **kwargs: updates.append(kwargs)) + status.watch(lambda *_, **kwargs: updates.append(kwargs)) await status assert logic.calls == ["on_prepare", "on_kickoff", "on_complete"] @@ -204,7 +204,7 @@ async def test_flyable_complete_watches_movable_logic(): status = flyer.complete() assert isinstance(status, WatchableAsyncStatus) updates: list[dict] = [] - status.watch(lambda **kwargs: updates.append(kwargs)) + status.watch(lambda *_, **kwargs: updates.append(kwargs)) await status # Progress was reported by observing the readback up to the target, with the diff --git a/tests/unit_tests/core/test_log.py b/tests/unit_tests/core/test_log.py index d80a59509d..d4e4b11856 100644 --- a/tests/unit_tests/core/test_log.py +++ b/tests/unit_tests/core/test_log.py @@ -85,10 +85,10 @@ def test_logger_adapter_ophyd_async_device(): logger.addHandler(log_stream) device = Device(name="test_device") - device._log = logging.LoggerAdapter( + device_log = logging.LoggerAdapter( logging.getLogger("ophyd_async.devices"), {"ophyd_async_device_name": device.name}, ) - device._log.warning("here is a warning") + device_log.warning("here is a warning") assert log_buffer.getvalue().startswith("[test_device]") assert log_buffer.getvalue().endswith("here is a warning\n") diff --git a/tests/unit_tests/core/test_mock_signal_backend.py b/tests/unit_tests/core/test_mock_signal_backend.py index a516fcd414..f6188e0f9b 100644 --- a/tests/unit_tests/core/test_mock_signal_backend.py +++ b/tests/unit_tests/core/test_mock_signal_backend.py @@ -78,17 +78,17 @@ async def test_set_mock_value_timestamp_and_alarm_severity(): signal = soft_signal_rw(int) await signal.connect(mock=True) reading = (await signal.read())[signal.name] - assert reading["alarm_severity"] == 0 + assert reading.get("alarm_severity") == 0 set_mock_value(signal, 5, timestamp=123.0, alarm_severity=2) reading = (await signal.read())[signal.name] assert reading["value"] == 5 assert reading["timestamp"] == 123.0 - assert reading["alarm_severity"] == 2 + assert reading.get("alarm_severity") == 2 # Defaults mean now and ok set_mock_value(signal, 6) reading = (await signal.read())[signal.name] assert reading["timestamp"] > 123.0 - assert reading["alarm_severity"] == 0 + assert reading.get("alarm_severity") == 0 async def test_set_mock_put_proceeds(): @@ -284,7 +284,9 @@ async def slow_put(value: float): handle.cancel() elif cancelled_by == "other": (callback_task,) = ( - t for t in asyncio.all_tasks() if t.get_coro().__name__ == "slow_put" + t + for t in asyncio.all_tasks() + if getattr(t.get_coro(), "__name__", None) == "slow_put" ) callback_task.cancel() if expected_error: @@ -302,7 +304,7 @@ async def test_callback_on_mock_put_fails_if_args_are_not_correct(): def callback(): pass - callback_on_mock_put(mock_signal, callback) + callback_on_mock_put(mock_signal, callback) # pyright: ignore[reportArgumentType] with pytest.raises(TypeError) as exc: await mock_signal.set(10.0) assert str(exc.value).endswith( @@ -356,10 +358,10 @@ async def test_set_mock_values_exhausted_fails(mock_signals): break with pytest.raises(AssertionError): - iterator.__del__() + iterator.__del__() # pyright: ignore[reportAttributeAccessIssue] # Set so it doesn't raise the same error on teardown - iterator.require_all_consumed = False + iterator.require_all_consumed = False # pyright: ignore[reportAttributeAccessIssue] async def test_reset_mock_put_calls(mock_signals): diff --git a/tests/unit_tests/core/test_multi_derived_signal.py b/tests/unit_tests/core/test_multi_derived_signal.py index dd252af6fa..a98ff7a56e 100644 --- a/tests/unit_tests/core/test_multi_derived_signal.py +++ b/tests/unit_tests/core/test_multi_derived_signal.py @@ -362,7 +362,7 @@ def _get_generic(value: T) -> T: return value -async def _set_generic(value: T) -> None: +async def _set_generic(value: T) -> None: # pyright: ignore[reportInvalidTypeVarUse] pass diff --git a/tests/unit_tests/core/test_readable.py b/tests/unit_tests/core/test_readable.py index c0d682bc1d..df0fdc0783 100644 --- a/tests/unit_tests/core/test_readable.py +++ b/tests/unit_tests/core/test_readable.py @@ -155,7 +155,7 @@ def test_standard_readable_add_children_context_manager(): # Can't use assert_called_once_with() as the order of items returned from # internal dict comprehension is not guaranteed mock.assert_called_once() - assert set(mock.call_args.args[0]) == {sr.a, sr.b, sr.c} + assert set(mock.call_args.args[0]) == {sr.a, sr.b, sr.c} # pyright: ignore[reportAttributeAccessIssue] @pytest.mark.parametrize( @@ -198,7 +198,7 @@ def test_standard_readable_add_children_cm_filters_non_devices(): # Can't use assert_called_once_with() as the order of items returned from # internal dict comprehension is not guaranteed mock.assert_called_once() - assert set(mock.call_args.args[0]) == {sr.a, sr.b} + assert set(mock.call_args.args[0]) == {sr.a, sr.b} # pyright: ignore[reportAttributeAccessIssue] def assert_sr_has_attrs(sr: StandardReadable, expected_attrs: dict[str, tuple]): @@ -302,7 +302,7 @@ def test_standard_readable_hinted_signal(): assert sr._read_funcs == (signal_r.read,) assert sr._stage_funcs == (signal_r.stage,) assert sr._unstage_funcs == (signal_r.unstage,) - assert sr._has_hints[0].device == signal_r + assert sr._has_hints[0].device == signal_r # pyright: ignore[reportAttributeAccessIssue] def test_standard_readable_uncached_signal(): @@ -310,7 +310,7 @@ def test_standard_readable_uncached_signal(): sr = StandardReadable() sr.add_readables([signal_r], Format.UNCACHED_SIGNAL) assert sr._describe_funcs == (signal_r.describe,) - assert sr._read_funcs[0].signal == signal_r + assert sr._read_funcs[0].signal == signal_r # pyright: ignore[reportFunctionMemberAccess] def test_standard_readable_hinted_uncached_signal(): @@ -318,8 +318,8 @@ def test_standard_readable_hinted_uncached_signal(): sr = StandardReadable() sr.add_readables([signal_r], Format.HINTED_UNCACHED_SIGNAL) assert sr._describe_funcs == (signal_r.describe,) - assert sr._read_funcs[0].signal == signal_r - assert sr._has_hints[0].device == signal_r + assert sr._read_funcs[0].signal == signal_r # pyright: ignore[reportFunctionMemberAccess] + assert sr._has_hints[0].device == signal_r # pyright: ignore[reportAttributeAccessIssue] def test_standard_readable_add_children_multi_nested(): diff --git a/tests/unit_tests/core/test_signal.py b/tests/unit_tests/core/test_signal.py index e9a7b7a973..8e4e2f8a16 100644 --- a/tests/unit_tests/core/test_signal.py +++ b/tests/unit_tests/core/test_signal.py @@ -1178,14 +1178,11 @@ def test_notify_with_reading(signal_cache): mock_function.assert_called_once_with({"test_signal": partial_reading(42)}) -async def test_notify_runtime_error(signal_cache: _SignalCache[Any]) -> None: +def test_notify_runtime_error(signal_cache: _SignalCache[Any]) -> None: function = MagicMock() with pytest.raises(RuntimeError, match="Monitor not working"): - await asyncio.wait_for( - signal_cache._notify(function), - timeout=1.0, - ) + signal_cache._notify(function) def test_signal_backend_throws_type_error() -> None: @@ -1269,7 +1266,7 @@ async def test_soft_signal_rw_with_getter(): async def test_soft_signal_rw_with_setter(): - store = [0.0] + store: list[Any] = [0.0] signal = soft_signal_rw(float, setter=lambda v: store.__setitem__(0, v)) await signal.connect() await signal.set(7.0) @@ -1277,7 +1274,7 @@ async def test_soft_signal_rw_with_setter(): async def test_soft_signal_rw_with_getter_and_setter(): - store = [0.0] + store: list[Any] = [0.0] signal = soft_signal_rw( float, setter=lambda v: store.__setitem__(0, v), @@ -1319,7 +1316,9 @@ async def test_soft_signal_r_and_setter_with_getter(): assert await signal.get_value() == pytest.approx(42.0) # set_value still works independently of the getter set_value(99.0) - assert signal._connector.backend.reading["value"] == pytest.approx(99.0) + backend = signal._connector.backend + assert isinstance(backend, SoftSignalBackend) + assert backend.reading["value"] == pytest.approx(99.0) async def test_soft_signal_r_and_setter_with_poll_period(): diff --git a/tests/unit_tests/core/test_soft_signal_backend.py b/tests/unit_tests/core/test_soft_signal_backend.py index 4bb0d683ef..6e1d99e8ce 100644 --- a/tests/unit_tests/core/test_soft_signal_backend.py +++ b/tests/unit_tests/core/test_soft_signal_backend.py @@ -2,7 +2,7 @@ import os import typing from collections.abc import Callable, Sequence -from typing import Any, TypeVar +from typing import Any import numpy as np import numpy.typing as npt @@ -11,14 +11,13 @@ from ophyd_async.core import ( Array1D, + SignalDatatypeT, SoftSignalBackend, StrictEnum, soft_signal_rw, ) from ophyd_async.testing import ExampleEnum, ExampleTable, MonitorQueue -T = TypeVar("T") - class MyEnum(StrictEnum): A = "Aaa" @@ -131,9 +130,9 @@ def uint_array_2d(): ], ) async def test_soft_signal_backend_get_put_monitor( - datatype: type[T], - initial_value: T, - put_value: T, + datatype: type[SignalDatatypeT], + initial_value: SignalDatatypeT, + put_value: SignalDatatypeT, descriptor: Callable[[Any], dict], dtype_numpy: str, ): @@ -162,10 +161,10 @@ async def test_soft_signal_backend_get_put_monitor( (Array1D[np.float64], 1), ], ) -async def test_soft_signal_backend_records_ndim(datatype: type[T], ndim: int | None): +async def test_soft_signal_backend_records_ndim(datatype: Any, ndim: int | None): backend = SoftSignalBackend(datatype) await backend.connect(timeout=1) - assert backend.converter.ndim == ndim + assert backend.converter.ndim == ndim # pyright: ignore[reportAttributeAccessIssue] @pytest.mark.parametrize( @@ -180,7 +179,7 @@ async def test_soft_signal_backend_records_ndim(datatype: type[T], ndim: int | N ], ) async def test_soft_signal_backend_checks_shape( - datatype: type[T], put_value: Any, expected: tuple[int, ...] | str + datatype: Any, put_value: Any, expected: tuple[int, ...] | str ): backend = SoftSignalBackend(datatype) await backend.connect(timeout=1) @@ -229,20 +228,20 @@ def __init__(self) -> None: pass with pytest.raises(TypeError): - SoftSignalBackend(MyClass) + SoftSignalBackend(MyClass) # pyright: ignore[reportArgumentType] async def test_soft_signal_descriptor_with_metadata(): soft_signal = soft_signal_rw(int, 0, units="mm", precision=0) await soft_signal.connect() datakey = await soft_signal.describe() - assert datakey[""]["units"] == "mm" - assert datakey[""]["precision"] == 0 + assert datakey[""].get("units") == "mm" + assert datakey[""].get("precision") == 0 soft_signal = soft_signal_rw(int, units="") await soft_signal.connect() datakey = await soft_signal.describe() - assert datakey[""]["units"] == "" + assert datakey[""].get("units") == "" assert not hasattr(datakey[""], "precision") @@ -262,7 +261,9 @@ async def test_soft_signal_coerces_numpy_types(): await soft_signal.set(np.float64(1.1)) assert await soft_signal.get_value() == 1.1 assert type(await soft_signal.get_value()) is float - soft_signal._connector.backend.set_value(np.float64(2.2)) + backend = soft_signal._connector.backend + assert isinstance(backend, SoftSignalBackend) + backend.set_value(np.float64(2.2)) assert await soft_signal.get_value() == 2.2 assert type(await soft_signal.get_value()) is float diff --git a/tests/unit_tests/core/test_standard_detector.py b/tests/unit_tests/core/test_standard_detector.py index 0c9b9ab814..b8b2b2804f 100644 --- a/tests/unit_tests/core/test_standard_detector.py +++ b/tests/unit_tests/core/test_standard_detector.py @@ -518,7 +518,7 @@ async def default_trigger_info(self) -> TriggerInfo: # Collect watcher updates via watch() callback updates: list[dict] = [] status = det.trigger() - status.watch(lambda **kwargs: updates.append(kwargs)) + status.watch(lambda *_, **kwargs: updates.append(kwargs)) await wait_for_pending_wakeups(raise_if_exceeded=False) assert (await det.describe())["multidet"]["shape"] == [5, 10, 15] @@ -531,7 +531,9 @@ async def default_trigger_info(self) -> TriggerInfo: await status docs = [doc async for doc in det.collect_asset_docs()] assert [name for name, _ in docs] == ["stream_resource", "stream_datum"] - assert docs[1][1]["indices"] == {"start": 0, "stop": 1} + stream_datum = docs[1] + assert stream_datum[0] == "stream_datum" + assert stream_datum[1]["indices"] == {"start": 0, "stop": 1} assert [update["current"] for update in updates] == [0, 1, 2, 3, 4, 5] assert all(update["target"] == 5 for update in updates) @@ -551,7 +553,7 @@ async def test_collect_asset_docs_uses_trigger_observed_event( and ``collect_asset_docs``.""" det = StandardDetector(name="det") dl = StreamableOnlyDataLogic(tmp_path) - dl.collections_written, set_collections_written = soft_signal_r_and_setter( + dl.collections_written, set_collections_written = soft_signal_r_and_setter( # pyright: ignore[reportAttributeAccessIssue] int, getter=lambda: 0 ) det.add_detector_logics(JustInternalTriggerLogic(), dl) @@ -564,7 +566,9 @@ async def test_collect_asset_docs_uses_trigger_observed_event( docs = [doc async for doc in det.collect_asset_docs()] assert [name for name, _ in docs] == ["stream_resource", "stream_datum"] - assert docs[1][1]["indices"] == {"start": 0, "stop": 1} + stream_datum = docs[1] + assert stream_datum[0] == "stream_datum" + assert stream_datum[1]["indices"] == {"start": 0, "stop": 1} async def test_trigger_times_out_on_collection_timeout(tmp_path): @@ -856,7 +860,7 @@ class UnknownLogic: pass with pytest.raises(TypeError, match="Unknown logic type"): - det.add_detector_logics(UnknownLogic()) + det.add_detector_logics(UnknownLogic()) # pyright: ignore[reportArgumentType] @pytest.mark.parametrize("initial_shutter_closed", [True, False]) diff --git a/tests/unit_tests/core/test_subset_enum.py b/tests/unit_tests/core/test_subset_enum.py index 23005682fa..64039f13c0 100644 --- a/tests/unit_tests/core/test_subset_enum.py +++ b/tests/unit_tests/core/test_subset_enum.py @@ -50,10 +50,11 @@ def __init__(self): epics_value = EpicsValue() converter = ca_make_converter( - AB, values={"READ_PV": epics_value, "WRITE_PV": epics_value} + AB, + values={"READ_PV": epics_value, "WRITE_PV": epics_value}, # pyright: ignore[reportArgumentType] ) - assert converter.supported_values == {"A": "A", "B": "B", "C": "C"} - assert set(AB).issubset(set(converter.supported_values.keys())) + assert converter.supported_values == {"A": "A", "B": "B", "C": "C"} # pyright: ignore[reportAttributeAccessIssue] + assert set(AB).issubset(set(converter.supported_values.keys())) # pyright: ignore[reportAttributeAccessIssue] async def test_pva_runtime_enum_converter(): @@ -67,7 +68,7 @@ async def test_pva_runtime_enum_converter(): converter = pva_make_converter( AB, values={"READ_PV": epics_value, "WRITE_PV": epics_value} ) - assert {"A", "B"}.issubset(set(converter.supported_values)) + assert {"A", "B"}.issubset(set(converter.supported_values)) # pyright: ignore[reportAttributeAccessIssue] async def test_runtime_enum_signal(): @@ -77,8 +78,8 @@ async def test_runtime_enum_signal(): await signal_rw_ca.connect(mock=True) assert await signal_rw_pva.get_value() == "A1" assert await signal_rw_ca.get_value() == "A2" - await signal_rw_pva.set("B1") - await signal_rw_ca.set("B2") + await signal_rw_pva.set("B1") # pyright: ignore[reportArgumentType] + await signal_rw_ca.set("B2") # pyright: ignore[reportArgumentType] assert await signal_rw_pva.get_value() == "B1" assert await signal_rw_ca.get_value() == "B2" diff --git a/tests/unit_tests/core/test_utils.py b/tests/unit_tests/core/test_utils.py index 378d4449a6..c85a3e41f7 100644 --- a/tests/unit_tests/core/test_utils.py +++ b/tests/unit_tests/core/test_utils.py @@ -188,7 +188,7 @@ async def test_error_handling_value_errors(caplog): class BadDatatypeDevice(Device): def __init__(self, name: str = "") -> None: - self.sig = epics_signal_rw(object, "", "") + self.sig = epics_signal_rw(object, "", "") # pyright: ignore[reportArgumentType] super().__init__(name) @@ -328,13 +328,13 @@ async def test_format_error_string_input(): RuntimeError, match=("Unexpected type `` expected `str` or `dict`"), ): - not_connected = NotConnectedError(123) + not_connected = NotConnectedError(123) # pyright: ignore[reportArgumentType] str(not_connected) with pytest.raises( RuntimeError, match=("Unexpected type ``, expected an Exception") ): - not_connected = NotConnectedError({"test": 123}) + not_connected = NotConnectedError({"test": 123}) # pyright: ignore[reportArgumentType] str(not_connected) @@ -452,7 +452,7 @@ async def set_omega(self, value: int, *args, **kwargs): def _contains_message(e: BaseException, expected_message: str) -> bool: - return ( + return bool( expected_message is None or (e.args and re.search(expected_message, e.args[0])) or (e.__cause__ and _contains_message(e.__cause__, expected_message)) diff --git a/tests/unit_tests/core/test_watchable_async_status.py b/tests/unit_tests/core/test_watchable_async_status.py index cabdc18b44..a335ee1899 100644 --- a/tests/unit_tests/core/test_watchable_async_status.py +++ b/tests/unit_tests/core/test_watchable_async_status.py @@ -163,8 +163,8 @@ async def test_asyncstatus_wraps_set_iterator_with_class_or_func_watcher(RE): updates = [] w = TWatcher() - st.watch(partial(watcher_test, updates)) - st.watch(w) + st.watch(partial(watcher_test, updates)) # pyright: ignore[reportArgumentType] + st.watch(w) # pyright: ignore[reportArgumentType] await st assert st.done assert st.success @@ -179,7 +179,7 @@ async def test_watchableasyncstatus_wraps_failing_set_iterator(RE): st = td.set(6) updates = [] - st.watch(partial(watcher_test, updates)) + st.watch(partial(watcher_test, updates)) # pyright: ignore[reportArgumentType] try: await st except Exception: diff --git a/tests/unit_tests/epics/adcore/test_cont_acq_detector.py b/tests/unit_tests/epics/adcore/test_cont_acq_detector.py index 1657152176..65fb896c78 100644 --- a/tests/unit_tests/epics/adcore/test_cont_acq_detector.py +++ b/tests/unit_tests/epics/adcore/test_cont_acq_detector.py @@ -31,7 +31,7 @@ async def cont_acq_detector() -> adcore.AreaDetector[adcore.ADBaseIO]: set_mock_value(det.driver.acquire, True) # The CB plugin Trigger is a busy record that resets itself to False once the # requested frames are buffered; simulate that immediate completion. - callback_on_mock_put(det.cb.trigger_, lambda value: False) + callback_on_mock_put(det.cb.trigger_, lambda value: False) # pyright: ignore[reportAttributeAccessIssue] return det diff --git a/tests/unit_tests/epics/demo/test_epics_demo.py b/tests/unit_tests/epics/demo/test_epics_demo.py index 753630bf71..5548d371a7 100644 --- a/tests/unit_tests/epics/demo/test_epics_demo.py +++ b/tests/unit_tests/epics/demo/test_epics_demo.py @@ -222,7 +222,7 @@ async def test_read_point_detector(mock_point_detector: demo.DemoPointDetector): "mock_point_detector-channel-1-mode" ] assert desc["dtype"] == "string" - assert desc["choices"] == ["Low Energy", "High Energy"] + assert desc.get("choices") == ["Low Energy", "High Energy"] set_mock_value(channel.mode, demo.EnergyMode.HIGH) assert (await channel.read_configuration())["mock_point_detector-channel-1-mode"][ "value" diff --git a/tests/unit_tests/epics/test_default_trigger_info.py b/tests/unit_tests/epics/test_default_trigger_info.py index 8904c99918..8b039ee0fe 100644 --- a/tests/unit_tests/epics/test_default_trigger_info.py +++ b/tests/unit_tests/epics/test_default_trigger_info.py @@ -82,7 +82,9 @@ def on_start(value: bool) -> bool | None: await det.stage() await det.trigger() description = await det.describe() - return description["det"]["shape"][0] + events_per_collection = description["det"]["shape"][0] + assert events_per_collection is not None + return events_per_collection @pytest.fixture(autouse=True) diff --git a/tests/unit_tests/fastcs/panda/test_seq_table.py b/tests/unit_tests/fastcs/panda/test_seq_table.py index d42567e5a1..f87a28c958 100644 --- a/tests/unit_tests/fastcs/panda/test_seq_table.py +++ b/tests/unit_tests/fastcs/panda/test_seq_table.py @@ -1,4 +1,5 @@ from functools import reduce +from typing import Any import numpy as np import pytest @@ -8,7 +9,10 @@ def test_seq_table_converts_lists(): - seq_table_dict_with_lists = {field_name: [] for field_name, _ in SeqTable()} + seq_table_dict_with_lists: dict[str, Any] = { + field_name: [] + for field_name, _ in SeqTable() # pyright: ignore[reportCallIssue] + } # Validation passes seq_table = SeqTable(**seq_table_dict_with_lists) for field_name, field_value in seq_table: @@ -20,25 +24,26 @@ def test_seq_table_converts_lists(): def test_seq_table_validation_errors(): with pytest.raises(ValidationError, match="17 validation errors for SeqTable"): - SeqTable( - repeats=0, - trigger=SeqTrigger.IMMEDIATE, - position=0, - time1=0, - outa1=False, - outb1=False, - outc1=False, - outd1=False, - oute1=False, - outf1=False, - time2=0, - outa2=False, - outb2=False, - outc2=False, - outd2=False, - oute2=False, - outf2=False, - ) + not_arrays: dict[str, Any] = { + "repeats": 0, + "trigger": SeqTrigger.IMMEDIATE, + "position": 0, + "time1": 0, + "outa1": False, + "outb1": False, + "outc1": False, + "outd1": False, + "oute1": False, + "outf1": False, + "time2": 0, + "outa2": False, + "outb2": False, + "outc2": False, + "outd2": False, + "oute2": False, + "outf2": False, + } + SeqTable(**not_arrays) large_seq_table = SeqTable( repeats=np.zeros(4096, dtype=np.uint16), @@ -65,7 +70,7 @@ def test_seq_table_validation_errors(): "1 validation error for SeqTable\n Value error, Length 4097 is too long." ), ): - large_seq_table + SeqTable.row() + _ = large_seq_table + SeqTable.row() # TODO: validation of numpy types disabled until bool -> uint8 CA issue resolved # with pytest.raises( # ValidationError, diff --git a/tests/unit_tests/plan_stubs/test_ensure_connected.py b/tests/unit_tests/plan_stubs/test_ensure_connected.py index 6d34c6dbe6..9cd708ac55 100644 --- a/tests/unit_tests/plan_stubs/test_ensure_connected.py +++ b/tests/unit_tests/plan_stubs/test_ensure_connected.py @@ -24,10 +24,12 @@ def connect(): ): RE(connect()) + assert device1.signal._connect_task is not None assert isinstance(device1.signal._connect_task.exception(), NotConnectedError) device1.signal = soft_signal_rw(str) RE(connect()) + assert device1.signal._connect_task is not None assert device1.signal._connect_task.exception() is None device2 = MyDevice("PREFIX2", name="device2") diff --git a/tests/unit_tests/sim/test_sim_blob_detector.py b/tests/unit_tests/sim/test_sim_blob_detector.py index cc191ca98b..a1cde316e1 100644 --- a/tests/unit_tests/sim/test_sim_blob_detector.py +++ b/tests/unit_tests/sim/test_sim_blob_detector.py @@ -1,6 +1,7 @@ import asyncio import os from collections import defaultdict +from typing import cast import bluesky.plan_stubs as bps import bluesky.plans as bp @@ -44,9 +45,9 @@ async def test_sim_blob_detector_count( # Check data looks right assert path == (tmp_path / "file.h5").as_posix() h5file = h5py.File(path) - assert list(h5file["/entry"]) == ["data", "sum"] - assert list(h5file["/entry/sum"]) == [det_sum, det_sum] - assert np.sum(h5file["/entry/data/data"][0]) == det_sum + assert list(cast(h5py.Group, h5file["/entry"])) == ["data", "sum"] + assert list(cast(h5py.Dataset, h5file["/entry/sum"])) == [det_sum, det_sum] + assert np.sum(cast(h5py.Dataset, h5file["/entry/data/data"])[0]) == det_sum # Check descriptor looks right assert docs["descriptor"][0]["data_keys"] == { "det": { @@ -121,7 +122,7 @@ async def set_next_x(t: float): if os.name == "nt": path = path.lstrip("/") h5file = h5py.File(path) - assert list(h5file["/entry/sum"]) == [ + assert list(cast(h5py.Dataset, h5file["/entry/sum"])) == [ 506344, 527596, 542140, diff --git a/tests/unit_tests/test_branching.py b/tests/unit_tests/test_branching.py index 41a58b3e66..adf830cbce 100644 --- a/tests/unit_tests/test_branching.py +++ b/tests/unit_tests/test_branching.py @@ -8,7 +8,7 @@ # import pytest from autodoc2.config import Config from autodoc2.db import InMemoryDb -from bluesky.protocols import Locatable +from bluesky.protocols import Locatable, Location, Status # _docs_parser is Sphinx autodoc2 build tooling, not runtime code at all - # already excluded from the layers contract as such @@ -47,8 +47,8 @@ def test_format_annotation_without_annotation(): def test_get_locatable_type(): class DummyLocatable(Locatable[int]): - def set(self, _): ... - def locate(self): ... + def set(self, value: int) -> Status: ... + def locate(self) -> Location[int]: ... class NonLocatable: ...