ophyd-async 0.3.4__tar.gz → 0.4.0__tar.gz

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (210) hide show
  1. {ophyd_async-0.3.4 → ophyd_async-0.4.0}/.github/workflows/_release.yml +1 -1
  2. {ophyd_async-0.3.4 → ophyd_async-0.4.0}/.github/workflows/_test.yml +0 -1
  3. {ophyd_async-0.3.4 → ophyd_async-0.4.0}/PKG-INFO +7 -2
  4. {ophyd_async-0.3.4 → ophyd_async-0.4.0}/docs/conf.py +6 -0
  5. {ophyd_async-0.3.4 → ophyd_async-0.4.0}/pyproject.toml +7 -2
  6. {ophyd_async-0.3.4 → ophyd_async-0.4.0}/src/ophyd_async/_version.py +2 -2
  7. {ophyd_async-0.3.4 → ophyd_async-0.4.0}/src/ophyd_async/core/__init__.py +20 -8
  8. ophyd_async-0.4.0/src/ophyd_async/core/_providers.py +230 -0
  9. {ophyd_async-0.3.4 → ophyd_async-0.4.0}/src/ophyd_async/core/detector.py +14 -15
  10. {ophyd_async-0.3.4 → ophyd_async-0.4.0}/src/ophyd_async/core/device.py +18 -6
  11. {ophyd_async-0.3.4 → ophyd_async-0.4.0}/src/ophyd_async/core/signal.py +32 -8
  12. {ophyd_async-0.3.4 → ophyd_async-0.4.0}/src/ophyd_async/core/soft_signal_backend.py +20 -2
  13. {ophyd_async-0.3.4 → ophyd_async-0.4.0}/src/ophyd_async/epics/_backend/_aioca.py +3 -0
  14. {ophyd_async-0.3.4 → ophyd_async-0.4.0}/src/ophyd_async/epics/_backend/_p4p.py +50 -2
  15. {ophyd_async-0.3.4 → ophyd_async-0.4.0}/src/ophyd_async/epics/_backend/common.py +3 -1
  16. {ophyd_async-0.3.4 → ophyd_async-0.4.0}/src/ophyd_async/epics/areadetector/aravis.py +3 -3
  17. {ophyd_async-0.3.4 → ophyd_async-0.4.0}/src/ophyd_async/epics/areadetector/controllers/aravis_controller.py +1 -0
  18. {ophyd_async-0.3.4 → ophyd_async-0.4.0}/src/ophyd_async/epics/areadetector/drivers/ad_base.py +3 -2
  19. {ophyd_async-0.3.4 → ophyd_async-0.4.0}/src/ophyd_async/epics/areadetector/kinetix.py +3 -3
  20. {ophyd_async-0.3.4 → ophyd_async-0.4.0}/src/ophyd_async/epics/areadetector/pilatus.py +3 -3
  21. {ophyd_async-0.3.4 → ophyd_async-0.4.0}/src/ophyd_async/epics/areadetector/vimba.py +3 -3
  22. {ophyd_async-0.3.4 → ophyd_async-0.4.0}/src/ophyd_async/epics/areadetector/writers/__init__.py +2 -2
  23. ophyd_async-0.4.0/src/ophyd_async/epics/areadetector/writers/general_hdffile.py +97 -0
  24. {ophyd_async-0.3.4 → ophyd_async-0.4.0}/src/ophyd_async/epics/areadetector/writers/hdf_writer.py +27 -10
  25. {ophyd_async-0.3.4 → ophyd_async-0.4.0}/src/ophyd_async/epics/areadetector/writers/nd_file_hdf.py +3 -0
  26. {ophyd_async-0.3.4 → ophyd_async-0.4.0}/src/ophyd_async/epics/areadetector/writers/nd_plugin.py +30 -0
  27. {ophyd_async-0.3.4 → ophyd_async-0.4.0}/src/ophyd_async/epics/demo/demo_ad_sim_detector.py +3 -3
  28. ophyd_async-0.4.0/src/ophyd_async/epics/motion/motor.py +227 -0
  29. {ophyd_async-0.3.4 → ophyd_async-0.4.0}/src/ophyd_async/panda/__init__.py +15 -1
  30. {ophyd_async-0.3.4 → ophyd_async-0.4.0}/src/ophyd_async/panda/_common_blocks.py +22 -1
  31. {ophyd_async-0.3.4 → ophyd_async-0.4.0}/src/ophyd_async/panda/_hdf_panda.py +5 -3
  32. {ophyd_async-0.3.4 → ophyd_async-0.4.0}/src/ophyd_async/panda/_table.py +20 -18
  33. ophyd_async-0.4.0/src/ophyd_async/panda/_trigger.py +94 -0
  34. {ophyd_async-0.3.4 → ophyd_async-0.4.0}/src/ophyd_async/panda/writers/_hdf_writer.py +17 -8
  35. {ophyd_async-0.3.4 → ophyd_async-0.4.0}/src/ophyd_async/plan_stubs/ensure_connected.py +7 -2
  36. {ophyd_async-0.3.4 → ophyd_async-0.4.0}/src/ophyd_async/plan_stubs/fly.py +58 -7
  37. ophyd_async-0.4.0/src/ophyd_async/sim/pattern_generator.py +207 -0
  38. {ophyd_async-0.3.4 → ophyd_async-0.4.0}/src/ophyd_async/sim/sim_pattern_detector_control.py +3 -3
  39. {ophyd_async-0.3.4 → ophyd_async-0.4.0}/src/ophyd_async/sim/sim_pattern_detector_writer.py +9 -5
  40. {ophyd_async-0.3.4 → ophyd_async-0.4.0}/src/ophyd_async/sim/sim_pattern_generator.py +12 -5
  41. {ophyd_async-0.3.4 → ophyd_async-0.4.0}/src/ophyd_async.egg-info/PKG-INFO +7 -2
  42. {ophyd_async-0.3.4 → ophyd_async-0.4.0}/src/ophyd_async.egg-info/SOURCES.txt +3 -3
  43. {ophyd_async-0.3.4 → ophyd_async-0.4.0}/src/ophyd_async.egg-info/requires.txt +6 -1
  44. {ophyd_async-0.3.4 → ophyd_async-0.4.0}/tests/conftest.py +23 -3
  45. {ophyd_async-0.3.4 → ophyd_async-0.4.0}/tests/core/test_async_status.py +2 -2
  46. {ophyd_async-0.3.4 → ophyd_async-0.4.0}/tests/core/test_device.py +47 -17
  47. {ophyd_async-0.3.4 → ophyd_async-0.4.0}/tests/core/test_device_collector.py +1 -22
  48. {ophyd_async-0.3.4 → ophyd_async-0.4.0}/tests/core/test_flyer.py +64 -7
  49. {ophyd_async-0.3.4 → ophyd_async-0.4.0}/tests/core/test_mock_signal_backend.py +2 -0
  50. ophyd_async-0.4.0/tests/core/test_providers.py +85 -0
  51. {ophyd_async-0.3.4 → ophyd_async-0.4.0}/tests/core/test_signal.py +97 -2
  52. {ophyd_async-0.3.4 → ophyd_async-0.4.0}/tests/core/test_soft_signal_backend.py +19 -17
  53. {ophyd_async-0.3.4 → ophyd_async-0.4.0}/tests/epics/areadetector/test_aravis.py +21 -16
  54. {ophyd_async-0.3.4 → ophyd_async-0.4.0}/tests/epics/areadetector/test_kinetix.py +12 -15
  55. {ophyd_async-0.3.4 → ophyd_async-0.4.0}/tests/epics/areadetector/test_pilatus.py +18 -7
  56. {ophyd_async-0.3.4 → ophyd_async-0.4.0}/tests/epics/areadetector/test_scans.py +3 -4
  57. {ophyd_async-0.3.4 → ophyd_async-0.4.0}/tests/epics/areadetector/test_vimba.py +12 -17
  58. {ophyd_async-0.3.4 → ophyd_async-0.4.0}/tests/epics/areadetector/test_writers.py +7 -7
  59. {ophyd_async-0.3.4 → ophyd_async-0.4.0}/tests/epics/demo/test_demo.py +3 -0
  60. {ophyd_async-0.3.4 → ophyd_async-0.4.0}/tests/epics/demo/test_demo_ad_sim_detector.py +17 -14
  61. {ophyd_async-0.3.4 → ophyd_async-0.4.0}/tests/epics/motion/test_motor.py +129 -1
  62. {ophyd_async-0.3.4 → ophyd_async-0.4.0}/tests/epics/test_signals.py +33 -2
  63. {ophyd_async-0.3.4 → ophyd_async-0.4.0}/tests/panda/test_hdf_panda.py +28 -14
  64. {ophyd_async-0.3.4 → ophyd_async-0.4.0}/tests/panda/test_panda_utils.py +20 -4
  65. ophyd_async-0.4.0/tests/panda/test_trigger.py +104 -0
  66. {ophyd_async-0.3.4 → ophyd_async-0.4.0}/tests/panda/test_writer.py +28 -23
  67. ophyd_async-0.4.0/tests/plan_stubs/test_ensure_connected.py +40 -0
  68. {ophyd_async-0.3.4 → ophyd_async-0.4.0}/tests/plan_stubs/test_fly.py +54 -39
  69. {ophyd_async-0.3.4 → ophyd_async-0.4.0}/tests/protocols/test_protocols.py +4 -2
  70. ophyd_async-0.4.0/tests/sim/test_pattern_generator.py +34 -0
  71. {ophyd_async-0.3.4 → ophyd_async-0.4.0}/tests/sim/test_sim_detector.py +21 -18
  72. {ophyd_async-0.3.4 → ophyd_async-0.4.0}/tests/sim/test_sim_writer.py +9 -11
  73. ophyd_async-0.3.4/src/ophyd_async/core/_providers.py +0 -68
  74. ophyd_async-0.3.4/src/ophyd_async/epics/areadetector/writers/_hdfdataset.py +0 -10
  75. ophyd_async-0.3.4/src/ophyd_async/epics/areadetector/writers/_hdffile.py +0 -54
  76. ophyd_async-0.3.4/src/ophyd_async/epics/motion/motor.py +0 -97
  77. ophyd_async-0.3.4/src/ophyd_async/panda/_trigger.py +0 -39
  78. ophyd_async-0.3.4/src/ophyd_async/panda/writers/_panda_hdf_file.py +0 -54
  79. ophyd_async-0.3.4/src/ophyd_async/sim/pattern_generator.py +0 -318
  80. ophyd_async-0.3.4/tests/panda/test_trigger.py +0 -33
  81. ophyd_async-0.3.4/tests/sim/test_pattern_generator.py +0 -72
  82. {ophyd_async-0.3.4 → ophyd_async-0.4.0}/.codecov.yml +0 -0
  83. {ophyd_async-0.3.4 → ophyd_async-0.4.0}/.copier-answers.yml +0 -0
  84. {ophyd_async-0.3.4 → ophyd_async-0.4.0}/.devcontainer/devcontainer.json +0 -0
  85. {ophyd_async-0.3.4 → ophyd_async-0.4.0}/.git-blame-ignore-revs +0 -0
  86. {ophyd_async-0.3.4 → ophyd_async-0.4.0}/.github/CONTRIBUTING.md +0 -0
  87. {ophyd_async-0.3.4 → ophyd_async-0.4.0}/.github/actions/install_requirements/action.yml +0 -0
  88. {ophyd_async-0.3.4 → ophyd_async-0.4.0}/.github/dependabot.yml +0 -0
  89. {ophyd_async-0.3.4 → ophyd_async-0.4.0}/.github/pages/index.html +0 -0
  90. {ophyd_async-0.3.4 → ophyd_async-0.4.0}/.github/pages/make_switcher.py +0 -0
  91. {ophyd_async-0.3.4 → ophyd_async-0.4.0}/.github/workflows/_check.yml +0 -0
  92. {ophyd_async-0.3.4 → ophyd_async-0.4.0}/.github/workflows/_dist.yml +0 -0
  93. {ophyd_async-0.3.4 → ophyd_async-0.4.0}/.github/workflows/_docs.yml +0 -0
  94. {ophyd_async-0.3.4 → ophyd_async-0.4.0}/.github/workflows/_pypi.yml +0 -0
  95. {ophyd_async-0.3.4 → ophyd_async-0.4.0}/.github/workflows/_tox.yml +0 -0
  96. {ophyd_async-0.3.4 → ophyd_async-0.4.0}/.github/workflows/ci.yml +0 -0
  97. {ophyd_async-0.3.4 → ophyd_async-0.4.0}/.github/workflows/periodic.yml +0 -0
  98. {ophyd_async-0.3.4 → ophyd_async-0.4.0}/.gitignore +0 -0
  99. {ophyd_async-0.3.4 → ophyd_async-0.4.0}/.mailmap +0 -0
  100. {ophyd_async-0.3.4 → ophyd_async-0.4.0}/.pre-commit-config.yaml +0 -0
  101. {ophyd_async-0.3.4 → ophyd_async-0.4.0}/Dockerfile +0 -0
  102. {ophyd_async-0.3.4 → ophyd_async-0.4.0}/LICENSE +0 -0
  103. {ophyd_async-0.3.4 → ophyd_async-0.4.0}/README.md +0 -0
  104. {ophyd_async-0.3.4 → ophyd_async-0.4.0}/docs/_templates/README +0 -0
  105. {ophyd_async-0.3.4 → ophyd_async-0.4.0}/docs/_templates/custom-class-template.rst +0 -0
  106. {ophyd_async-0.3.4 → ophyd_async-0.4.0}/docs/_templates/custom-module-template.rst +0 -0
  107. {ophyd_async-0.3.4 → ophyd_async-0.4.0}/docs/examples/epics_demo.py +0 -0
  108. {ophyd_async-0.3.4 → ophyd_async-0.4.0}/docs/examples/foo_detector.py +0 -0
  109. {ophyd_async-0.3.4 → ophyd_async-0.4.0}/docs/explanations/decisions/0001-record-architecture-decisions.md +0 -0
  110. {ophyd_async-0.3.4 → ophyd_async-0.4.0}/docs/explanations/decisions/0002-switched-to-python-copier-template.md +0 -0
  111. {ophyd_async-0.3.4 → ophyd_async-0.4.0}/docs/explanations/decisions/0003-ophyd-async-migration.rst +0 -0
  112. {ophyd_async-0.3.4 → ophyd_async-0.4.0}/docs/explanations/decisions/0004-repository-structure.rst +0 -0
  113. {ophyd_async-0.3.4 → ophyd_async-0.4.0}/docs/explanations/decisions/0005-respect-black-line-length.rst +0 -0
  114. {ophyd_async-0.3.4 → ophyd_async-0.4.0}/docs/explanations/decisions/0006-procedural-device-definitions.rst +0 -0
  115. {ophyd_async-0.3.4 → ophyd_async-0.4.0}/docs/explanations/decisions/COPYME +0 -0
  116. {ophyd_async-0.3.4 → ophyd_async-0.4.0}/docs/explanations/decisions.md +0 -0
  117. {ophyd_async-0.3.4 → ophyd_async-0.4.0}/docs/explanations/design-goals.rst +0 -0
  118. {ophyd_async-0.3.4 → ophyd_async-0.4.0}/docs/explanations/event-loop-choice.rst +0 -0
  119. {ophyd_async-0.3.4 → ophyd_async-0.4.0}/docs/explanations/flyscanning.rst +0 -0
  120. {ophyd_async-0.3.4 → ophyd_async-0.4.0}/docs/explanations.md +0 -0
  121. {ophyd_async-0.3.4 → ophyd_async-0.4.0}/docs/genindex.rst +0 -0
  122. {ophyd_async-0.3.4 → ophyd_async-0.4.0}/docs/how-to/choose-interfaces-for-devices.md +0 -0
  123. {ophyd_async-0.3.4 → ophyd_async-0.4.0}/docs/how-to/compound-devices.rst +0 -0
  124. {ophyd_async-0.3.4 → ophyd_async-0.4.0}/docs/how-to/contribute.md +0 -0
  125. {ophyd_async-0.3.4 → ophyd_async-0.4.0}/docs/how-to/make-a-simple-device.rst +0 -0
  126. {ophyd_async-0.3.4 → ophyd_async-0.4.0}/docs/how-to/make-a-standard-detector.rst +0 -0
  127. {ophyd_async-0.3.4 → ophyd_async-0.4.0}/docs/how-to/write-tests-for-devices.rst +0 -0
  128. {ophyd_async-0.3.4 → ophyd_async-0.4.0}/docs/how-to.md +0 -0
  129. {ophyd_async-0.3.4 → ophyd_async-0.4.0}/docs/images/bluesky_ophyd_epics_devices_logo.svg +0 -0
  130. {ophyd_async-0.3.4 → ophyd_async-0.4.0}/docs/images/bluesky_ophyd_logo.svg +0 -0
  131. {ophyd_async-0.3.4 → ophyd_async-0.4.0}/docs/images/ophyd_favicon.svg +0 -0
  132. {ophyd_async-0.3.4 → ophyd_async-0.4.0}/docs/index.md +0 -0
  133. {ophyd_async-0.3.4 → ophyd_async-0.4.0}/docs/reference/api.rst +0 -0
  134. {ophyd_async-0.3.4 → ophyd_async-0.4.0}/docs/reference.md +0 -0
  135. {ophyd_async-0.3.4 → ophyd_async-0.4.0}/docs/tutorials/installation.md +0 -0
  136. {ophyd_async-0.3.4 → ophyd_async-0.4.0}/docs/tutorials/using-existing-devices.rst +0 -0
  137. {ophyd_async-0.3.4 → ophyd_async-0.4.0}/docs/tutorials.md +0 -0
  138. {ophyd_async-0.3.4 → ophyd_async-0.4.0}/setup.cfg +0 -0
  139. {ophyd_async-0.3.4 → ophyd_async-0.4.0}/src/ophyd_async/__init__.py +0 -0
  140. {ophyd_async-0.3.4 → ophyd_async-0.4.0}/src/ophyd_async/__main__.py +0 -0
  141. {ophyd_async-0.3.4 → ophyd_async-0.4.0}/src/ophyd_async/core/async_status.py +0 -0
  142. {ophyd_async-0.3.4 → ophyd_async-0.4.0}/src/ophyd_async/core/device_save_loader.py +0 -0
  143. {ophyd_async-0.3.4 → ophyd_async-0.4.0}/src/ophyd_async/core/flyer.py +0 -0
  144. {ophyd_async-0.3.4 → ophyd_async-0.4.0}/src/ophyd_async/core/mock_signal_backend.py +0 -0
  145. {ophyd_async-0.3.4 → ophyd_async-0.4.0}/src/ophyd_async/core/mock_signal_utils.py +0 -0
  146. {ophyd_async-0.3.4 → ophyd_async-0.4.0}/src/ophyd_async/core/signal_backend.py +0 -0
  147. {ophyd_async-0.3.4 → ophyd_async-0.4.0}/src/ophyd_async/core/standard_readable.py +0 -0
  148. {ophyd_async-0.3.4 → ophyd_async-0.4.0}/src/ophyd_async/core/utils.py +0 -0
  149. {ophyd_async-0.3.4 → ophyd_async-0.4.0}/src/ophyd_async/epics/__init__.py +0 -0
  150. {ophyd_async-0.3.4 → ophyd_async-0.4.0}/src/ophyd_async/epics/_backend/__init__.py +0 -0
  151. {ophyd_async-0.3.4 → ophyd_async-0.4.0}/src/ophyd_async/epics/areadetector/__init__.py +0 -0
  152. {ophyd_async-0.3.4 → ophyd_async-0.4.0}/src/ophyd_async/epics/areadetector/controllers/__init__.py +0 -0
  153. {ophyd_async-0.3.4 → ophyd_async-0.4.0}/src/ophyd_async/epics/areadetector/controllers/ad_sim_controller.py +0 -0
  154. {ophyd_async-0.3.4 → ophyd_async-0.4.0}/src/ophyd_async/epics/areadetector/controllers/kinetix_controller.py +0 -0
  155. {ophyd_async-0.3.4 → ophyd_async-0.4.0}/src/ophyd_async/epics/areadetector/controllers/pilatus_controller.py +0 -0
  156. {ophyd_async-0.3.4 → ophyd_async-0.4.0}/src/ophyd_async/epics/areadetector/controllers/vimba_controller.py +0 -0
  157. {ophyd_async-0.3.4 → ophyd_async-0.4.0}/src/ophyd_async/epics/areadetector/drivers/__init__.py +0 -0
  158. {ophyd_async-0.3.4 → ophyd_async-0.4.0}/src/ophyd_async/epics/areadetector/drivers/aravis_driver.py +0 -0
  159. {ophyd_async-0.3.4 → ophyd_async-0.4.0}/src/ophyd_async/epics/areadetector/drivers/kinetix_driver.py +0 -0
  160. {ophyd_async-0.3.4 → ophyd_async-0.4.0}/src/ophyd_async/epics/areadetector/drivers/pilatus_driver.py +0 -0
  161. {ophyd_async-0.3.4 → ophyd_async-0.4.0}/src/ophyd_async/epics/areadetector/drivers/vimba_driver.py +0 -0
  162. {ophyd_async-0.3.4 → ophyd_async-0.4.0}/src/ophyd_async/epics/areadetector/single_trigger_det.py +0 -0
  163. {ophyd_async-0.3.4 → ophyd_async-0.4.0}/src/ophyd_async/epics/areadetector/utils.py +0 -0
  164. {ophyd_async-0.3.4 → ophyd_async-0.4.0}/src/ophyd_async/epics/demo/__init__.py +0 -0
  165. {ophyd_async-0.3.4 → ophyd_async-0.4.0}/src/ophyd_async/epics/demo/mover.db +0 -0
  166. {ophyd_async-0.3.4 → ophyd_async-0.4.0}/src/ophyd_async/epics/demo/sensor.db +0 -0
  167. {ophyd_async-0.3.4 → ophyd_async-0.4.0}/src/ophyd_async/epics/motion/__init__.py +0 -0
  168. {ophyd_async-0.3.4 → ophyd_async-0.4.0}/src/ophyd_async/epics/pvi/__init__.py +0 -0
  169. {ophyd_async-0.3.4 → ophyd_async-0.4.0}/src/ophyd_async/epics/pvi/pvi.py +0 -0
  170. {ophyd_async-0.3.4 → ophyd_async-0.4.0}/src/ophyd_async/epics/signal/__init__.py +0 -0
  171. {ophyd_async-0.3.4 → ophyd_async-0.4.0}/src/ophyd_async/epics/signal/_epics_transport.py +0 -0
  172. {ophyd_async-0.3.4 → ophyd_async-0.4.0}/src/ophyd_async/epics/signal/signal.py +0 -0
  173. {ophyd_async-0.3.4 → ophyd_async-0.4.0}/src/ophyd_async/log.py +0 -0
  174. {ophyd_async-0.3.4 → ophyd_async-0.4.0}/src/ophyd_async/panda/_panda_controller.py +0 -0
  175. {ophyd_async-0.3.4 → ophyd_async-0.4.0}/src/ophyd_async/panda/_utils.py +0 -0
  176. {ophyd_async-0.3.4 → ophyd_async-0.4.0}/src/ophyd_async/panda/writers/__init__.py +0 -0
  177. {ophyd_async-0.3.4 → ophyd_async-0.4.0}/src/ophyd_async/plan_stubs/__init__.py +0 -0
  178. {ophyd_async-0.3.4 → ophyd_async-0.4.0}/src/ophyd_async/protocols.py +0 -0
  179. {ophyd_async-0.3.4 → ophyd_async-0.4.0}/src/ophyd_async/sim/__init__.py +0 -0
  180. {ophyd_async-0.3.4 → ophyd_async-0.4.0}/src/ophyd_async/sim/demo/__init__.py +0 -0
  181. {ophyd_async-0.3.4 → ophyd_async-0.4.0}/src/ophyd_async/sim/demo/sim_motor.py +0 -0
  182. {ophyd_async-0.3.4 → ophyd_async-0.4.0}/src/ophyd_async.egg-info/dependency_links.txt +0 -0
  183. {ophyd_async-0.3.4 → ophyd_async-0.4.0}/src/ophyd_async.egg-info/entry_points.txt +0 -0
  184. {ophyd_async-0.3.4 → ophyd_async-0.4.0}/src/ophyd_async.egg-info/top_level.txt +0 -0
  185. {ophyd_async-0.3.4 → ophyd_async-0.4.0}/tests/core/test_device_save_loader.py +0 -0
  186. {ophyd_async-0.3.4 → ophyd_async-0.4.0}/tests/core/test_standard_readable.py +0 -0
  187. {ophyd_async-0.3.4 → ophyd_async-0.4.0}/tests/core/test_subset_enum.py +0 -0
  188. {ophyd_async-0.3.4 → ophyd_async-0.4.0}/tests/core/test_utils.py +0 -0
  189. {ophyd_async-0.3.4 → ophyd_async-0.4.0}/tests/core/test_watchable_async_status.py +0 -0
  190. {ophyd_async-0.3.4 → ophyd_async-0.4.0}/tests/epics/_backend/test_common.py +0 -0
  191. {ophyd_async-0.3.4 → ophyd_async-0.4.0}/tests/epics/areadetector/__init__.py +0 -0
  192. {ophyd_async-0.3.4 → ophyd_async-0.4.0}/tests/epics/areadetector/test_controllers.py +0 -0
  193. {ophyd_async-0.3.4 → ophyd_async-0.4.0}/tests/epics/areadetector/test_drivers.py +0 -0
  194. {ophyd_async-0.3.4 → ophyd_async-0.4.0}/tests/epics/areadetector/test_single_trigger_det.py +0 -0
  195. {ophyd_async-0.3.4 → ophyd_async-0.4.0}/tests/epics/areadetector/test_utils.py +0 -0
  196. {ophyd_async-0.3.4 → ophyd_async-0.4.0}/tests/epics/motion/__init__.py +0 -0
  197. {ophyd_async-0.3.4 → ophyd_async-0.4.0}/tests/epics/test_pvi.py +0 -0
  198. {ophyd_async-0.3.4 → ophyd_async-0.4.0}/tests/epics/test_records.db +0 -0
  199. {ophyd_async-0.3.4 → ophyd_async-0.4.0}/tests/panda/db/panda.db +0 -0
  200. {ophyd_async-0.3.4 → ophyd_async-0.4.0}/tests/panda/test_panda_connect.py +0 -0
  201. {ophyd_async-0.3.4 → ophyd_async-0.4.0}/tests/panda/test_panda_controller.py +0 -0
  202. {ophyd_async-0.3.4 → ophyd_async-0.4.0}/tests/panda/test_table.py +0 -0
  203. {ophyd_async-0.3.4 → ophyd_async-0.4.0}/tests/sim/__init__.py +0 -0
  204. {ophyd_async-0.3.4 → ophyd_async-0.4.0}/tests/sim/conftest.py +0 -0
  205. {ophyd_async-0.3.4 → ophyd_async-0.4.0}/tests/sim/demo/__init__.py +0 -0
  206. {ophyd_async-0.3.4 → ophyd_async-0.4.0}/tests/sim/demo/test_sim_motor.py +0 -0
  207. {ophyd_async-0.3.4 → ophyd_async-0.4.0}/tests/sim/test_streaming_plan.py +0 -0
  208. {ophyd_async-0.3.4 → ophyd_async-0.4.0}/tests/test_cli.py +0 -0
  209. {ophyd_async-0.3.4 → ophyd_async-0.4.0}/tests/test_data/test_yaml_save.yml +0 -0
  210. {ophyd_async-0.3.4 → ophyd_async-0.4.0}/tests/test_log.py +0 -0
@@ -23,7 +23,7 @@ jobs:
23
23
  - name: Create GitHub Release
24
24
  # We pin to the SHA, not the tag, for security reasons.
25
25
  # https://docs.github.com/en/actions/learn-github-actions/security-hardening-for-github-actions#using-third-party-actions
26
- uses: softprops/action-gh-release@69320dbe05506a9a39fc8ae11030b214ec2d1f87 # v2.0.5
26
+ uses: softprops/action-gh-release@a74c6b72af54cfa997e81df42d94703d6313a2d0 # v2.0.6
27
27
  with:
28
28
  prerelease: ${{ contains(github.ref_name, 'a') || contains(github.ref_name, 'b') || contains(github.ref_name, 'rc') }}
29
29
  files: "*"
@@ -49,7 +49,6 @@ jobs:
49
49
  with:
50
50
  python-version: ${{ inputs.python-version }}
51
51
  pip-install: ".[dev]"
52
-
53
52
  - name: Run tests
54
53
  run: tox -e tests
55
54
 
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.1
2
2
  Name: ophyd-async
3
- Version: 0.3.4
3
+ Version: 0.4.0
4
4
  Summary: Asynchronous Bluesky hardware abstraction code, compatible with control systems like EPICS and Tango
5
5
  Author-email: Tom Cobb <tom.cobb@diamond.ac.uk>
6
6
  License: BSD 3-Clause License
@@ -45,10 +45,12 @@ Requires-Dist: numpy<2.0.0
45
45
  Requires-Dist: packaging
46
46
  Requires-Dist: pint
47
47
  Requires-Dist: bluesky>=1.13.0a3
48
- Requires-Dist: event-model<1.21.0
48
+ Requires-Dist: event_model
49
49
  Requires-Dist: p4p
50
50
  Requires-Dist: pyyaml
51
51
  Requires-Dist: colorlog
52
+ Requires-Dist: pydantic>=2.0
53
+ Requires-Dist: pydantic-numpy
52
54
  Provides-Extra: ca
53
55
  Requires-Dist: aioca>=1.6; extra == "ca"
54
56
  Provides-Extra: pva
@@ -81,10 +83,13 @@ Requires-Dist: pytest-faulthandler; extra == "dev"
81
83
  Requires-Dist: pytest-rerunfailures; extra == "dev"
82
84
  Requires-Dist: pytest-timeout; extra == "dev"
83
85
  Requires-Dist: ruff; extra == "dev"
86
+ Requires-Dist: sphinx<7.4.0; extra == "dev"
84
87
  Requires-Dist: sphinx-autobuild; extra == "dev"
88
+ Requires-Dist: autodoc-pydantic; extra == "dev"
85
89
  Requires-Dist: sphinxcontrib-mermaid; extra == "dev"
86
90
  Requires-Dist: sphinx-copybutton; extra == "dev"
87
91
  Requires-Dist: sphinx-design; extra == "dev"
92
+ Requires-Dist: super_state_machine; extra == "dev"
88
93
  Requires-Dist: tox-direct; extra == "dev"
89
94
  Requires-Dist: types-mock; extra == "dev"
90
95
  Requires-Dist: types-pyyaml; extra == "dev"
@@ -34,6 +34,8 @@ else:
34
34
  extensions = [
35
35
  # for diagrams
36
36
  "sphinxcontrib.mermaid",
37
+ # Used for BaseModel autodoc
38
+ "sphinxcontrib.autodoc_pydantic",
37
39
  # Use this for generating API docs
38
40
  "sphinx.ext.autodoc",
39
41
  "sphinx.ext.doctest",
@@ -239,5 +241,9 @@ autodoc_docstring_signature = True
239
241
  # numpydoc config
240
242
  numpydoc_show_class_members = False
241
243
 
244
+ # pydantic models
245
+ autodoc_pydantic_model_show_json = True
246
+ autodoc_pydantic_model_show_config_summary = False
247
+
242
248
  # Where to put Ipython savefigs
243
249
  ipython_savefig_dir = "../build/savefig"
@@ -17,10 +17,12 @@ dependencies = [
17
17
  "packaging",
18
18
  "pint",
19
19
  "bluesky>=1.13.0a3",
20
- "event-model<1.21.0",
20
+ "event_model",
21
21
  "p4p",
22
22
  "pyyaml",
23
23
  "colorlog",
24
+ "pydantic>=2.0",
25
+ "pydantic-numpy",
24
26
  ]
25
27
  dynamic = ["version"]
26
28
  license.file = "LICENSE"
@@ -58,10 +60,13 @@ dev = [
58
60
  "pytest-rerunfailures",
59
61
  "pytest-timeout",
60
62
  "ruff",
63
+ "sphinx<7.4.0", # https://github.com/bluesky/ophyd-async/issues/459
61
64
  "sphinx-autobuild",
65
+ "autodoc-pydantic",
62
66
  "sphinxcontrib-mermaid",
63
67
  "sphinx-copybutton",
64
68
  "sphinx-design",
69
+ "super_state_machine",
65
70
  "tox-direct",
66
71
  "types-mock",
67
72
  "types-pyyaml",
@@ -127,7 +132,7 @@ allowlist_externals =
127
132
  commands =
128
133
  tests: pytest --cov=ophyd_async --cov-report term --cov-report xml:cov.xml {posargs}
129
134
  type-checking: ruff check src tests {posargs}
130
- pre-commit: pre-commit run --all-files {posargs}
135
+ pre-commit: pre-commit run --all-files --show-diff-on-failure {posargs}
131
136
  docs: sphinx-{posargs:build -EW --keep-going} -T docs build/html
132
137
  """
133
138
 
@@ -12,5 +12,5 @@ __version__: str
12
12
  __version_tuple__: VERSION_TUPLE
13
13
  version_tuple: VERSION_TUPLE
14
14
 
15
- __version__ = version = '0.3.4'
16
- __version_tuple__ = version_tuple = (0, 3, 4)
15
+ __version__ = version = '0.4.0'
16
+ __version_tuple__ = version_tuple = (0, 4, 0)
@@ -1,9 +1,15 @@
1
1
  from ._providers import (
2
- DirectoryInfo,
3
- DirectoryProvider,
2
+ AutoIncrementFilenameProvider,
3
+ AutoIncrementingPathProvider,
4
+ FilenameProvider,
4
5
  NameProvider,
6
+ PathInfo,
7
+ PathProvider,
5
8
  ShapeProvider,
6
- StaticDirectoryProvider,
9
+ StaticFilenameProvider,
10
+ StaticPathProvider,
11
+ UUIDFilenameProvider,
12
+ YMDPathProvider,
7
13
  )
8
14
  from .async_status import AsyncStatus, WatchableAsyncStatus
9
15
  from .detector import (
@@ -69,6 +75,8 @@ from .utils import (
69
75
 
70
76
  __all__ = [
71
77
  "AsyncStatus",
78
+ "AutoIncrementFilenameProvider",
79
+ "AutoIncrementingPathProvider",
72
80
  "CalculatableTimeout",
73
81
  "CalculateTimeout",
74
82
  "Callback",
@@ -80,16 +88,16 @@ __all__ = [
80
88
  "Device",
81
89
  "DeviceCollector",
82
90
  "DeviceVector",
83
- "DirectoryInfo",
84
- "DirectoryProvider",
91
+ "FilenameProvider",
85
92
  "HardwareTriggeredFlyable",
86
93
  "HintedSignal",
87
94
  "MockSignalBackend",
88
95
  "NameProvider",
89
96
  "NotConnected",
97
+ "PathInfo",
98
+ "PathProvider",
90
99
  "ReadingValueCallback",
91
100
  "RuntimeSubsetEnum",
92
- "SubsetEnum",
93
101
  "ShapeProvider",
94
102
  "Signal",
95
103
  "SignalBackend",
@@ -100,14 +108,18 @@ __all__ = [
100
108
  "SoftSignalBackend",
101
109
  "StandardDetector",
102
110
  "StandardReadable",
103
- "StaticDirectoryProvider",
111
+ "StaticFilenameProvider",
112
+ "StaticPathProvider",
113
+ "SubsetEnum",
104
114
  "T",
105
115
  "TriggerInfo",
106
116
  "TriggerLogic",
117
+ "UUIDFilenameProvider",
107
118
  "WatchableAsyncStatus",
119
+ "YMDPathProvider",
120
+ # Lower-cased imports
108
121
  "assert_configuration",
109
122
  "assert_emitted",
110
- "assert_mock_put_called_with",
111
123
  "assert_reading",
112
124
  "assert_value",
113
125
  "callback_on_mock_put",
@@ -0,0 +1,230 @@
1
+ import os
2
+ import uuid
3
+ from abc import abstractmethod
4
+ from collections.abc import Callable
5
+ from dataclasses import dataclass
6
+ from datetime import date
7
+ from pathlib import Path
8
+ from typing import List, Optional, Protocol
9
+
10
+
11
+ @dataclass
12
+ class PathInfo:
13
+ """
14
+ Information about where and how to write a file.
15
+
16
+ The bluesky event model splits the URI for a resource into two segments to aid in
17
+ different applications mounting filesystems at different mount points.
18
+ The portion of this path which is relevant only for the writer is the 'root',
19
+ while the path from an agreed upon mutual mounting is the resource_path.
20
+ The resource_dir is used with the filename to construct the resource_path.
21
+
22
+ :param root: Path of a root directory, relevant only for the file writer
23
+ :param resource_dir: Directory into which files should be written, relative to root
24
+ :param filename: Base filename to use generated by FilenameProvider, w/o extension
25
+ :param create_dir_depth: Optional depth of directories to create if they do not
26
+ exist
27
+ """
28
+
29
+ root: Path
30
+ resource_dir: Path
31
+ filename: str
32
+ create_dir_depth: int = 0
33
+
34
+
35
+ class FilenameProvider(Protocol):
36
+ @abstractmethod
37
+ def __call__(self) -> str:
38
+ """Get a filename to use for output data, w/o extension"""
39
+
40
+
41
+ class PathProvider(Protocol):
42
+ _filename_provider: FilenameProvider
43
+
44
+ @abstractmethod
45
+ def __call__(self, device_name: Optional[str] = None) -> PathInfo:
46
+ """Get the current directory to write files into"""
47
+
48
+
49
+ class StaticFilenameProvider(FilenameProvider):
50
+ def __init__(self, filename: str):
51
+ self._static_filename = filename
52
+
53
+ def __call__(self) -> str:
54
+ return self._static_filename
55
+
56
+
57
+ class UUIDFilenameProvider(FilenameProvider):
58
+ def __init__(
59
+ self,
60
+ uuid_call_func: Callable = uuid.uuid4,
61
+ uuid_call_args: Optional[List] = None,
62
+ ):
63
+ self._uuid_call_func = uuid_call_func
64
+ self._uuid_call_args = uuid_call_args or []
65
+
66
+ def __call__(self) -> str:
67
+ if (
68
+ self._uuid_call_func in [uuid.uuid3, uuid.uuid5]
69
+ and len(self._uuid_call_args) < 2
70
+ ):
71
+ raise ValueError(
72
+ f"To use {self._uuid_call_func} to generate UUID filenames,"
73
+ " UUID namespace and name must be passed as args!"
74
+ )
75
+
76
+ uuid_str = self._uuid_call_func(*self._uuid_call_args)
77
+ return f"{uuid_str}"
78
+
79
+
80
+ class AutoIncrementFilenameProvider(FilenameProvider):
81
+ def __init__(
82
+ self,
83
+ base_filename: str = "",
84
+ max_digits: int = 5,
85
+ starting_value: int = 0,
86
+ increment: int = 1,
87
+ inc_delimeter: str = "_",
88
+ ):
89
+ self._base_filename = base_filename
90
+ self._max_digits = max_digits
91
+ self._current_value = starting_value
92
+ self._increment = increment
93
+ self._inc_delimeter = inc_delimeter
94
+
95
+ def __call__(self):
96
+ if len(str(self._current_value)) > self._max_digits:
97
+ raise ValueError(
98
+ f"Auto incrementing filename counter \
99
+ exceeded maximum of {self._max_digits} digits!"
100
+ )
101
+
102
+ padded_counter = f"{self._current_value:0{self._max_digits}}"
103
+
104
+ filename = f"{self._base_filename}{self._inc_delimeter}{padded_counter}"
105
+
106
+ self._current_value += self._increment
107
+ return filename
108
+
109
+
110
+ class StaticPathProvider(PathProvider):
111
+ def __init__(
112
+ self,
113
+ filename_provider: FilenameProvider,
114
+ directory_path: Path,
115
+ resource_dir: Path = Path("."),
116
+ create_dir_depth: int = 0,
117
+ ) -> None:
118
+ self._filename_provider = filename_provider
119
+ self._directory_path = directory_path
120
+ self._resource_dir = resource_dir
121
+ self._create_dir_depth = create_dir_depth
122
+
123
+ def __call__(self, device_name: Optional[str] = None) -> PathInfo:
124
+ filename = self._filename_provider()
125
+
126
+ return PathInfo(
127
+ root=self._directory_path,
128
+ resource_dir=self._resource_dir,
129
+ filename=filename,
130
+ create_dir_depth=self._create_dir_depth,
131
+ )
132
+
133
+
134
+ class AutoIncrementingPathProvider(PathProvider):
135
+ def __init__(
136
+ self,
137
+ filename_provider: FilenameProvider,
138
+ directory_path: Path,
139
+ create_dir_depth: int = 0,
140
+ max_digits: int = 5,
141
+ starting_value: int = 0,
142
+ num_calls_per_inc: int = 1,
143
+ increment: int = 1,
144
+ inc_delimeter: str = "_",
145
+ base_name: str = None,
146
+ ) -> None:
147
+ self._filename_provider = filename_provider
148
+ self._directory_path = directory_path
149
+ self._create_dir_depth = create_dir_depth
150
+ self._base_name = base_name
151
+ self._starting_value = starting_value
152
+ self._current_value = starting_value
153
+ self._num_calls_per_inc = num_calls_per_inc
154
+ self._inc_counter = 0
155
+ self._max_digits = max_digits
156
+ self._increment = increment
157
+ self._inc_delimeter = inc_delimeter
158
+
159
+ def __call__(self, device_name: Optional[str] = None) -> PathInfo:
160
+ filename = self._filename_provider()
161
+
162
+ padded_counter = f"{self._current_value:0{self._max_digits}}"
163
+
164
+ resource_dir = str(padded_counter)
165
+ if self._base_name is not None:
166
+ resource_dir = f"{self._base_name}{self._inc_delimeter}{padded_counter}"
167
+ elif device_name is not None:
168
+ resource_dir = f"{device_name}{self._inc_delimeter}{padded_counter}"
169
+
170
+ self._inc_counter += 1
171
+ if self._inc_counter == self._num_calls_per_inc:
172
+ self._inc_counter = 0
173
+ self._current_value += self._increment
174
+
175
+ return PathInfo(
176
+ root=self._directory_path,
177
+ resource_dir=resource_dir,
178
+ filename=filename,
179
+ create_dir_depth=self._create_dir_depth,
180
+ )
181
+
182
+
183
+ class YMDPathProvider(PathProvider):
184
+ def __init__(
185
+ self,
186
+ filename_provider: FilenameProvider,
187
+ directory_path: Path,
188
+ create_dir_depth: int = -3, # Default to -3 to create YMD dirs
189
+ device_name_as_base_dir: bool = False,
190
+ ) -> None:
191
+ self._filename_provider = filename_provider
192
+ self._directory_path = Path(directory_path)
193
+ self._create_dir_depth = create_dir_depth
194
+ self._device_name_as_base_dir = device_name_as_base_dir
195
+
196
+ def __call__(self, device_name: Optional[str] = None) -> PathInfo:
197
+ sep = os.path.sep
198
+ current_date = date.today().strftime(f"%Y{sep}%m{sep}%d")
199
+ if device_name is None:
200
+ resource_dir = current_date
201
+ elif self._device_name_as_base_dir:
202
+ resource_dir = os.path.join(
203
+ current_date,
204
+ device_name,
205
+ )
206
+ else:
207
+ resource_dir = os.path.join(
208
+ device_name,
209
+ current_date,
210
+ )
211
+
212
+ filename = self._filename_provider()
213
+ return PathInfo(
214
+ root=self._directory_path,
215
+ resource_dir=resource_dir,
216
+ filename=filename,
217
+ create_dir_depth=self._create_dir_depth,
218
+ )
219
+
220
+
221
+ class NameProvider(Protocol):
222
+ @abstractmethod
223
+ def __call__(self) -> str:
224
+ """Get the name to be used as a data_key in the descriptor document"""
225
+
226
+
227
+ class ShapeProvider(Protocol):
228
+ @abstractmethod
229
+ async def __call__(self) -> tuple:
230
+ """Get the shape of the data collection"""
@@ -3,7 +3,6 @@
3
3
  import asyncio
4
4
  import time
5
5
  from abc import ABC, abstractmethod
6
- from dataclasses import dataclass
7
6
  from enum import Enum
8
7
  from typing import (
9
8
  AsyncGenerator,
@@ -28,6 +27,7 @@ from bluesky.protocols import (
28
27
  Triggerable,
29
28
  WritesStreamAssets,
30
29
  )
30
+ from pydantic import BaseModel, Field
31
31
 
32
32
  from ophyd_async.protocols import AsyncConfigurable, AsyncReadable
33
33
 
@@ -51,20 +51,19 @@ class DetectorTrigger(str, Enum):
51
51
  variable_gate = "variable_gate"
52
52
 
53
53
 
54
- @dataclass(frozen=True)
55
- class TriggerInfo:
54
+ class TriggerInfo(BaseModel):
56
55
  """Minimal set of information required to setup triggering on a detector"""
57
56
 
58
- #: Number of triggers that will be sent
59
- num: int
57
+ #: Number of triggers that will be sent, 0 means infinite
58
+ number: int = Field(gt=0)
60
59
  #: Sort of triggers that will be sent
61
- trigger: DetectorTrigger
60
+ trigger: DetectorTrigger = Field()
62
61
  #: What is the minimum deadtime between triggers
63
- deadtime: float
62
+ deadtime: float = Field(ge=0)
64
63
  #: What is the maximum high time of the triggers
65
- livetime: float
64
+ livetime: float = Field(ge=0)
66
65
  #: What is the maximum timeout on waiting for a frame
67
- frame_timeout: float | None = None
66
+ frame_timeout: float | None = Field(None, gt=0)
68
67
 
69
68
 
70
69
  class DetectorControl(ABC):
@@ -243,12 +242,12 @@ class StandardDetector(
243
242
  async def trigger(self) -> None:
244
243
  # set default trigger_info
245
244
  self._trigger_info = TriggerInfo(
246
- num=1, trigger=DetectorTrigger.internal, deadtime=0.0, livetime=0.0
245
+ number=1, trigger=DetectorTrigger.internal, deadtime=0.0, livetime=0.0
247
246
  )
248
247
  # Arm the detector and wait for it to finish.
249
248
  indices_written = await self.writer.get_indices_written()
250
249
  written_status = await self.controller.arm(
251
- num=self._trigger_info.num,
250
+ num=self._trigger_info.number,
252
251
  trigger=self._trigger_info.trigger,
253
252
  )
254
253
  await written_status
@@ -285,7 +284,7 @@ class StandardDetector(
285
284
  assert type(value) is TriggerInfo
286
285
  self._trigger_info = value
287
286
  self._initial_frame = await self.writer.get_indices_written()
288
- self._last_frame = self._initial_frame + self._trigger_info.num
287
+ self._last_frame = self._initial_frame + self._trigger_info.number
289
288
 
290
289
  required = self.controller.get_deadtime(self._trigger_info.livetime)
291
290
  assert required <= self._trigger_info.deadtime, (
@@ -293,7 +292,7 @@ class StandardDetector(
293
292
  f"but trigger logic provides only {self._trigger_info.deadtime}s"
294
293
  )
295
294
  self._arm_status = await self.controller.arm(
296
- num=self._trigger_info.num,
295
+ num=self._trigger_info.number,
297
296
  trigger=self._trigger_info.trigger,
298
297
  exposure=self._trigger_info.livetime,
299
298
  )
@@ -320,12 +319,12 @@ class StandardDetector(
320
319
  name=self.name,
321
320
  current=index,
322
321
  initial=self._initial_frame,
323
- target=self._trigger_info.num,
322
+ target=self._trigger_info.number,
324
323
  unit="",
325
324
  precision=0,
326
325
  time_elapsed=time.monotonic() - self._fly_start,
327
326
  )
328
- if index >= self._trigger_info.num:
327
+ if index >= self._trigger_info.number:
329
328
  break
330
329
 
331
330
  async def describe_collect(self) -> Dict[str, DataKey]:
@@ -33,7 +33,10 @@ class Device(HasName):
33
33
  parent: Optional["Device"] = None
34
34
  # None if connect hasn't started, a Task if it has
35
35
  _connect_task: Optional[asyncio.Task] = None
36
- _connect_mock_arg: bool = False
36
+
37
+ # Used to check if the previous connect was mocked,
38
+ # if the next mock value differs then we fail
39
+ _previous_connect_was_mock = None
37
40
 
38
41
  def __init__(self, name: str = "") -> None:
39
42
  self.set_name(name)
@@ -90,11 +93,21 @@ class Device(HasName):
90
93
  timeout:
91
94
  Time to wait before failing with a TimeoutError.
92
95
  """
96
+
97
+ if (
98
+ self._previous_connect_was_mock is not None
99
+ and self._previous_connect_was_mock != mock
100
+ ):
101
+ raise RuntimeError(
102
+ f"`connect(mock={mock})` called on a `Device` where the previous "
103
+ f"connect was `mock={self._previous_connect_was_mock}`. Changing mock "
104
+ "value between connects is not permitted."
105
+ )
106
+ self._previous_connect_was_mock = mock
107
+
93
108
  # If previous connect with same args has started and not errored, can use it
94
- can_use_previous_connect = (
95
- self._connect_task
96
- and not (self._connect_task.done() and self._connect_task.exception())
97
- and self._connect_mock_arg == mock
109
+ can_use_previous_connect = self._connect_task and not (
110
+ self._connect_task.done() and self._connect_task.exception()
98
111
  )
99
112
  if force_reconnect or not can_use_previous_connect:
100
113
  # Kick off a connection
@@ -105,7 +118,6 @@ class Device(HasName):
105
118
  for name, child_device in self.children()
106
119
  }
107
120
  self._connect_task = asyncio.create_task(wait_for_connection(**coros))
108
- self._connect_mock_arg = mock
109
121
 
110
122
  assert self._connect_task, "Connect task not created, this shouldn't happen"
111
123
  # Wait for it to complete
@@ -62,7 +62,7 @@ class Signal(Device, Generic[T]):
62
62
  name: str = "",
63
63
  ) -> None:
64
64
  self._timeout = timeout
65
- self._initial_backend = self._backend = backend
65
+ self._backend = backend
66
66
  super().__init__(name)
67
67
 
68
68
  async def connect(
@@ -73,19 +73,43 @@ class Signal(Device, Generic[T]):
73
73
  backend: Optional[SignalBackend[T]] = None,
74
74
  ):
75
75
  if backend:
76
- if self._initial_backend and backend is not self._initial_backend:
77
- raise ValueError(
78
- "Backend at connection different from initialised one."
79
- )
76
+ if self._backend and backend is not self._backend:
77
+ raise ValueError("Backend at connection different from previous one.")
78
+
80
79
  self._backend = backend
81
- if mock and not isinstance(self._backend, MockSignalBackend):
80
+ if (
81
+ self._previous_connect_was_mock is not None
82
+ and self._previous_connect_was_mock != mock
83
+ ):
84
+ raise RuntimeError(
85
+ f"`connect(mock={mock})` called on a `Signal` where the previous "
86
+ f"connect was `mock={self._previous_connect_was_mock}`. Changing mock "
87
+ "value between connects is not permitted."
88
+ )
89
+ self._previous_connect_was_mock = mock
90
+
91
+ if mock and not issubclass(type(self._backend), MockSignalBackend):
82
92
  # Using a soft backend, look to the initial value
83
93
  self._backend = MockSignalBackend(initial_backend=self._backend)
84
94
 
85
95
  if self._backend is None:
86
96
  raise RuntimeError("`connect` called on signal without backend")
87
- self.log.debug(f"Connecting to {self.source}")
88
- await self._backend.connect(timeout=timeout)
97
+
98
+ can_use_previous_connection: bool = self._connect_task is not None and not (
99
+ self._connect_task.done() and self._connect_task.exception()
100
+ )
101
+
102
+ if force_reconnect or not can_use_previous_connection:
103
+ self.log.debug(f"Connecting to {self.source}")
104
+ self._connect_task = asyncio.create_task(
105
+ self._backend.connect(timeout=timeout)
106
+ )
107
+ else:
108
+ self.log.debug(f"Reusing previous connection to {self.source}")
109
+ assert (
110
+ self._connect_task
111
+ ), "this assert is for type analysis and will never fail"
112
+ await self._connect_task
89
113
 
90
114
  @property
91
115
  def source(self) -> str:
@@ -10,7 +10,6 @@ from typing import (
10
10
  Optional,
11
11
  Tuple,
12
12
  Type,
13
- TypedDict,
14
13
  Union,
15
14
  cast,
16
15
  get_origin,
@@ -18,6 +17,7 @@ from typing import (
18
17
 
19
18
  import numpy as np
20
19
  from bluesky.protocols import DataKey, Dtype, Reading
20
+ from typing_extensions import TypedDict
21
21
 
22
22
  from .signal_backend import RuntimeSubsetEnum, SignalBackend
23
23
  from .utils import DEFAULT_TIMEOUT, ReadingValueCallback, T, get_dtype
@@ -60,6 +60,10 @@ class SoftConverter(Generic[T]):
60
60
  dtype in primitive_dtypes
61
61
  ), f"invalid converter for value of type {type(value)}"
62
62
  dk["dtype"] = primitive_dtypes[dtype]
63
+ try:
64
+ dk["dtype_numpy"] = np.dtype(dtype).descr[0][1]
65
+ except TypeError:
66
+ dk["dtype_numpy"] = ""
63
67
  return dk
64
68
 
65
69
  def make_initial_value(self, datatype: Optional[Type[T]]) -> T:
@@ -71,7 +75,20 @@ class SoftConverter(Generic[T]):
71
75
 
72
76
  class SoftArrayConverter(SoftConverter):
73
77
  def get_datakey(self, source: str, value, **metadata) -> DataKey:
74
- return {"source": source, "dtype": "array", "shape": [len(value)], **metadata}
78
+ dtype_numpy = ""
79
+ if isinstance(value, list):
80
+ if len(value) > 0:
81
+ dtype_numpy = np.dtype(type(value[0])).descr[0][1]
82
+ else:
83
+ dtype_numpy = np.dtype(value.dtype).descr[0][1]
84
+
85
+ return {
86
+ "source": source,
87
+ "dtype": "array",
88
+ "dtype_numpy": dtype_numpy,
89
+ "shape": [len(value)],
90
+ **metadata,
91
+ }
75
92
 
76
93
  def make_initial_value(self, datatype: Optional[Type[T]]) -> T:
77
94
  if datatype is None:
@@ -99,6 +116,7 @@ class SoftEnumConverter(SoftConverter):
99
116
  return {
100
117
  "source": source,
101
118
  "dtype": "string",
119
+ "dtype_numpy": "|S40",
102
120
  "shape": [],
103
121
  "choices": self.choices,
104
122
  **metadata,
@@ -66,9 +66,12 @@ def _data_key_from_augmented_value(
66
66
  scalar = value.element_count == 1
67
67
  dtype = dtype or dbr_to_dtype[value.datatype]
68
68
 
69
+ dtype_numpy = np.dtype(dbr.DbrCodeToType[value.datatype].dtype).descr[0][1]
70
+
69
71
  d = DataKey(
70
72
  source=source,
71
73
  dtype=dtype if scalar else "array",
74
+ dtype_numpy=dtype_numpy,
72
75
  # strictly value.element_count >= len(value)
73
76
  shape=[] if scalar else [len(value)],
74
77
  )