From 4ee4eb1be62f69c6e226c66b110856eac70b9b67 Mon Sep 17 00:00:00 2001 From: Ollie Copping Date: Wed, 4 Dec 2024 15:01:06 +0000 Subject: [PATCH] Add Python 3.12 to CI now that p4p is updated (#655) * Add Python 3.12 to CI and pyproject.toml now that p4p is updated * skipped tango tests on 3.12 - this will go in a seperate issue https://github.com/bluesky/ophyd-async/issues/681 --------- Co-authored-by: Eva Lott --- .github/workflows/ci.yml | 2 +- pyproject.toml | 1 + src/ophyd_async/epics/adcore/_core_logic.py | 3 +- tests/conftest.py | 34 +++++++++++++++------ tests/core/test_mock_signal_backend.py | 1 + tests/core/test_observe.py | 4 +-- tests/core/test_readable.py | 16 +++++----- tests/epics/adcore/test_drivers.py | 12 +++++--- tests/sim/demo/test_sim_motor.py | 6 ++-- 9 files changed, 51 insertions(+), 28 deletions(-) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 1f57059612..ecdbae1f56 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -21,7 +21,7 @@ jobs: strategy: matrix: runs-on: ["ubuntu-latest", "windows-latest"] # can add macos-latest - python-version: ["3.10", "3.11"] # 3.12 should be added when p4p is updated + python-version: ["3.10","3.11","3.12"] include: # Include one that runs in the dev environment - runs-on: "ubuntu-latest" diff --git a/pyproject.toml b/pyproject.toml index ef4834750c..9246ec9ba7 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -9,6 +9,7 @@ classifiers = [ "License :: OSI Approved :: BSD License", "Programming Language :: Python :: 3.10", "Programming Language :: Python :: 3.11", + "Programming Language :: Python :: 3.12", ] description = "Asynchronous Bluesky hardware abstraction code, compatible with control systems like EPICS and Tango" dependencies = [ diff --git a/src/ophyd_async/epics/adcore/_core_logic.py b/src/ophyd_async/epics/adcore/_core_logic.py index 54e642a081..9e6a545b9e 100644 --- a/src/ophyd_async/epics/adcore/_core_logic.py +++ b/src/ophyd_async/epics/adcore/_core_logic.py @@ -102,7 +102,8 @@ async def complete_acquisition() -> None: state = await driver.detector_state.get_value() if state not in good_states: raise ValueError( - f"Final detector state {state} not in valid end states: {good_states}" + f"Final detector state {state.value} not in valid end " + f"states: {good_states}" ) return AsyncStatus(complete_acquisition()) diff --git a/tests/conftest.py b/tests/conftest.py index ce62218413..9e6c8c0209 100644 --- a/tests/conftest.py +++ b/tests/conftest.py @@ -89,7 +89,9 @@ def _error_and_kill_pending_tasks( unfinished_tasks = { task for task in asyncio.all_tasks(loop) - if task.get_coro().__name__ not in _ALLOWED_PYTEST_TASKS and not task.done() + if (coro := task.get_coro()) is not None + and coro.__name__ not in _ALLOWED_PYTEST_TASKS + and not task.done() } for task in unfinished_tasks: task.cancel() @@ -113,15 +115,22 @@ def fail_test_on_unclosed_tasks(request: FixtureRequest): by the end of the test. """ - fail_count = request.session.testsfailed - loop = asyncio.get_event_loop() - loop.set_debug(True) + try: + fail_count = request.session.testsfailed + loop = asyncio.get_running_loop() + + loop.set_debug(True) - request.addfinalizer( - lambda: _error_and_kill_pending_tasks( - loop, request.node.name, request.session.testsfailed == fail_count + request.addfinalizer( + lambda: _error_and_kill_pending_tasks( + loop, request.node.name, request.session.testsfailed == fail_count + ) ) - ) + # Once https://github.com/bluesky/ophyd-async/issues/683 + # is finished we can remove this try, except. + except RuntimeError as error: + if str(error) != "no running event loop": + raise error @pytest.fixture(scope="function") @@ -242,4 +251,11 @@ def pytest_collection_modifyitems(config, items): for item in items: if tango_dir in str(item.fspath): - item.add_marker(pytest.mark.forked) + if sys.version_info >= (3, 12): + item.add_marker( + pytest.mark.skip( + reason="Tango is currently not supported on Python 3.12: https://github.com/bluesky/ophyd-async/issues/681" + ) + ) + else: + item.add_marker(pytest.mark.forked) diff --git a/tests/core/test_mock_signal_backend.py b/tests/core/test_mock_signal_backend.py index 046f0097f6..f44e8ef5f7 100644 --- a/tests/core/test_mock_signal_backend.py +++ b/tests/core/test_mock_signal_backend.py @@ -185,6 +185,7 @@ async def test_blocks_during_put(mock_signals): with mock_puts_blocked(signal1, signal2): status1 = signal1.set("second_value", wait=True, timeout=None) status2 = signal2.set("second_value", wait=True, timeout=None) + await asyncio.sleep(0.1) assert await signal1.get_value() == "second_value" assert await signal2.get_value() == "second_value" assert not status1.done diff --git a/tests/core/test_observe.py b/tests/core/test_observe.py index 50e0a167b5..a13bfe0728 100644 --- a/tests/core/test_observe.py +++ b/tests/core/test_observe.py @@ -86,7 +86,7 @@ async def tick(): async def watch(): async for val in observe_value(sig, done_timeout=0.2): - time.sleep(0.15) + await asyncio.sleep(0.1) recv.append(val) t = asyncio.create_task(tick()) @@ -95,7 +95,7 @@ async def watch(): with pytest.raises(asyncio.TimeoutError): await watch() assert recv == [0, 1] - assert time.time() - start == pytest.approx(0.3, abs=0.05) + assert time.time() - start == pytest.approx(0.2, abs=0.05) finally: t.cancel() diff --git a/tests/core/test_readable.py b/tests/core/test_readable.py index 8b39fb404d..d0c781d198 100644 --- a/tests/core/test_readable.py +++ b/tests/core/test_readable.py @@ -32,13 +32,13 @@ def test_standard_readable_hints(): assert sr.hints == {} - hint1 = MagicMock() + hint1 = MagicMock(spec=HasHints) hint1.hints = {"fields": ["abc"], "dimensions": [(["f1", "f2"], "s1")]} - hint2 = MagicMock() + hint2 = MagicMock(spec=HasHints) hint2.hints = {"fields": ["def", "ghi"]} - hint3 = MagicMock() + hint3 = MagicMock(spec=HasHints) hint3.hints = {"fields": ["jkl"], "gridding": "rectilinear_nonsequential"} sr.add_readables([hint1, hint2, hint3]) @@ -53,10 +53,10 @@ def test_standard_readable_hints(): def test_standard_readable_hints_raises_when_overriding_string_literal(): sr = StandardReadable() - hint1 = MagicMock() + hint1 = MagicMock(spec=HasHints) hint1.hints = {"gridding": "rectilinear_nonsequential"} - hint2 = MagicMock() + hint2 = MagicMock(spec=HasHints) hint2.hints = {"gridding": "a different string"} sr._has_hints = ( @@ -71,10 +71,10 @@ def test_standard_readable_hints_raises_when_overriding_string_literal(): def test_standard_readable_hints_raises_when_overriding_sequence(): sr = StandardReadable() - hint1 = MagicMock() + hint1 = MagicMock(spec=HasHints) hint1.hints = {"fields": ["field1", "field2"]} - hint2 = MagicMock() + hint2 = MagicMock(spec=HasHints) hint2.hints = {"fields": ["field2"]} sr._has_hints = ( @@ -90,7 +90,7 @@ def test_standard_readable_hints_raises_when_overriding_sequence(): def test_standard_readable_hints_invalid_types(invalid_type): sr = StandardReadable() - hint1 = MagicMock() + hint1 = MagicMock(spec=HasHints) hint1.hints = {"test": invalid_type} sr._has_hints = (hint1,) diff --git a/tests/epics/adcore/test_drivers.py b/tests/epics/adcore/test_drivers.py index 6f006ba0b7..2f0dd8ba88 100644 --- a/tests/epics/adcore/test_drivers.py +++ b/tests/epics/adcore/test_drivers.py @@ -87,13 +87,15 @@ async def test_start_acquiring_driver_and_ensure_status_fails_after_some_time( set_mock_value(driver.detector_state, adcore.DetectorState.IDLE) async def wait_then_fail(): - await asyncio.sleep(0) + await asyncio.sleep(0.1) set_mock_value(driver.detector_state, adcore.DetectorState.DISCONNECTED) - acquiring = await adcore.start_acquiring_driver_and_ensure_status( - driver, timeout=0.1 - ) await wait_then_fail() - with pytest.raises(ValueError): + acquiring = await adcore.start_acquiring_driver_and_ensure_status( + driver, timeout=0.2 + ) + with pytest.raises( + ValueError, match="Final detector state Disconnected not in valid end states:" + ): await acquiring diff --git a/tests/sim/demo/test_sim_motor.py b/tests/sim/demo/test_sim_motor.py index 9a8ac75a6a..d879c717b8 100644 --- a/tests/sim/demo/test_sim_motor.py +++ b/tests/sim/demo/test_sim_motor.py @@ -1,6 +1,7 @@ import asyncio import time +import pytest from bluesky.plans import spiral_square from bluesky.run_engine import RunEngine @@ -49,6 +50,7 @@ async def test_stop(): new_pos = await m1.user_readback.get_value() assert new_pos < 10 assert new_pos >= 0.1 - # move should not be successful as we stopped it - assert move_status.done + assert not move_status.success + with pytest.raises(RuntimeError, match="Motor was stopped"): + await move_status