ophyd-async 0.8.0a3__tar.gz → 0.8.0a5__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 (243) hide show
  1. {ophyd_async-0.8.0a3 → ophyd_async-0.8.0a5}/.copier-answers.yml +1 -1
  2. {ophyd_async-0.8.0a3 → ophyd_async-0.8.0a5}/.github/CONTRIBUTING.md +1 -1
  3. {ophyd_async-0.8.0a3 → ophyd_async-0.8.0a5}/.github/workflows/_pypi.yml +1 -1
  4. {ophyd_async-0.8.0a3 → ophyd_async-0.8.0a5}/.github/workflows/_release.yml +1 -1
  5. {ophyd_async-0.8.0a3 → ophyd_async-0.8.0a5}/PKG-INFO +1 -1
  6. {ophyd_async-0.8.0a3 → ophyd_async-0.8.0a5}/src/ophyd_async/_version.py +1 -1
  7. {ophyd_async-0.8.0a3 → ophyd_async-0.8.0a5}/src/ophyd_async/core/__init__.py +2 -0
  8. {ophyd_async-0.8.0a3 → ophyd_async-0.8.0a5}/src/ophyd_async/core/_device.py +77 -49
  9. {ophyd_async-0.8.0a3 → ophyd_async-0.8.0a5}/src/ophyd_async/core/_mock_signal_backend.py +10 -7
  10. {ophyd_async-0.8.0a3 → ophyd_async-0.8.0a5}/src/ophyd_async/core/_mock_signal_utils.py +14 -11
  11. {ophyd_async-0.8.0a3 → ophyd_async-0.8.0a5}/src/ophyd_async/core/_signal.py +26 -31
  12. {ophyd_async-0.8.0a3 → ophyd_async-0.8.0a5}/src/ophyd_async/core/_soft_signal_backend.py +8 -2
  13. {ophyd_async-0.8.0a3 → ophyd_async-0.8.0a5}/src/ophyd_async/core/_utils.py +74 -24
  14. {ophyd_async-0.8.0a3 → ophyd_async-0.8.0a5}/src/ophyd_async/epics/adcore/_single_trigger.py +2 -1
  15. {ophyd_async-0.8.0a3 → ophyd_async-0.8.0a5}/src/ophyd_async/epics/core/_aioca.py +1 -1
  16. {ophyd_async-0.8.0a3 → ophyd_async-0.8.0a5}/src/ophyd_async/epics/core/_p4p.py +1 -0
  17. {ophyd_async-0.8.0a3 → ophyd_async-0.8.0a5}/src/ophyd_async/epics/core/_pvi_connector.py +24 -25
  18. {ophyd_async-0.8.0a3 → ophyd_async-0.8.0a5}/src/ophyd_async/fastcs/panda/_table.py +1 -1
  19. ophyd_async-0.8.0a5/src/ophyd_async/plan_stubs/_ensure_connected.py +33 -0
  20. {ophyd_async-0.8.0a3 → ophyd_async-0.8.0a5}/src/ophyd_async/tango/base_devices/_base_device.py +37 -36
  21. {ophyd_async-0.8.0a3 → ophyd_async-0.8.0a5}/src/ophyd_async.egg-info/PKG-INFO +1 -1
  22. {ophyd_async-0.8.0a3 → ophyd_async-0.8.0a5}/src/ophyd_async.egg-info/SOURCES.txt +1 -0
  23. {ophyd_async-0.8.0a3 → ophyd_async-0.8.0a5}/tests/core/test_device.py +27 -14
  24. {ophyd_async-0.8.0a3 → ophyd_async-0.8.0a5}/tests/core/test_device_save_loader.py +10 -15
  25. ophyd_async-0.8.0a5/tests/core/test_observe.py +90 -0
  26. {ophyd_async-0.8.0a3 → ophyd_async-0.8.0a5}/tests/core/test_signal.py +48 -72
  27. {ophyd_async-0.8.0a3 → ophyd_async-0.8.0a5}/tests/core/test_soft_signal_backend.py +12 -15
  28. {ophyd_async-0.8.0a3 → ophyd_async-0.8.0a5}/tests/core/test_utils.py +32 -0
  29. {ophyd_async-0.8.0a3 → ophyd_async-0.8.0a5}/tests/epics/demo/test_demo.py +5 -3
  30. {ophyd_async-0.8.0a3 → ophyd_async-0.8.0a5}/tests/fastcs/panda/test_hdf_panda.py +1 -1
  31. {ophyd_async-0.8.0a3 → ophyd_async-0.8.0a5}/tests/fastcs/panda/test_writer.py +4 -4
  32. {ophyd_async-0.8.0a3 → ophyd_async-0.8.0a5}/tests/plan_stubs/test_ensure_connected.py +16 -2
  33. {ophyd_async-0.8.0a3 → ophyd_async-0.8.0a5}/tests/test_data/test_yaml_save.yml +0 -1
  34. ophyd_async-0.8.0a3/src/ophyd_async/plan_stubs/_ensure_connected.py +0 -28
  35. {ophyd_async-0.8.0a3 → ophyd_async-0.8.0a5}/.codecov.yml +0 -0
  36. {ophyd_async-0.8.0a3 → ophyd_async-0.8.0a5}/.devcontainer/devcontainer.json +0 -0
  37. {ophyd_async-0.8.0a3 → ophyd_async-0.8.0a5}/.git-blame-ignore-revs +0 -0
  38. {ophyd_async-0.8.0a3 → ophyd_async-0.8.0a5}/.github/ISSUE_TEMPLATE/bug_report.md +0 -0
  39. {ophyd_async-0.8.0a3 → ophyd_async-0.8.0a5}/.github/ISSUE_TEMPLATE/issue.md +0 -0
  40. {ophyd_async-0.8.0a3 → ophyd_async-0.8.0a5}/.github/PULL_REQUEST_TEMPLATE/pull_request_template.md +0 -0
  41. {ophyd_async-0.8.0a3 → ophyd_async-0.8.0a5}/.github/actions/install_requirements/action.yml +0 -0
  42. {ophyd_async-0.8.0a3 → ophyd_async-0.8.0a5}/.github/dependabot.yml +0 -0
  43. {ophyd_async-0.8.0a3 → ophyd_async-0.8.0a5}/.github/pages/index.html +0 -0
  44. {ophyd_async-0.8.0a3 → ophyd_async-0.8.0a5}/.github/pages/make_switcher.py +0 -0
  45. {ophyd_async-0.8.0a3 → ophyd_async-0.8.0a5}/.github/workflows/_check.yml +0 -0
  46. {ophyd_async-0.8.0a3 → ophyd_async-0.8.0a5}/.github/workflows/_dist.yml +0 -0
  47. {ophyd_async-0.8.0a3 → ophyd_async-0.8.0a5}/.github/workflows/_docs.yml +0 -0
  48. {ophyd_async-0.8.0a3 → ophyd_async-0.8.0a5}/.github/workflows/_test.yml +0 -0
  49. {ophyd_async-0.8.0a3 → ophyd_async-0.8.0a5}/.github/workflows/_tox.yml +0 -0
  50. {ophyd_async-0.8.0a3 → ophyd_async-0.8.0a5}/.github/workflows/ci.yml +0 -0
  51. {ophyd_async-0.8.0a3 → ophyd_async-0.8.0a5}/.github/workflows/periodic.yml +0 -0
  52. {ophyd_async-0.8.0a3 → ophyd_async-0.8.0a5}/.gitignore +0 -0
  53. {ophyd_async-0.8.0a3 → ophyd_async-0.8.0a5}/.mailmap +0 -0
  54. {ophyd_async-0.8.0a3 → ophyd_async-0.8.0a5}/.pre-commit-config.yaml +0 -0
  55. {ophyd_async-0.8.0a3 → ophyd_async-0.8.0a5}/Dockerfile +0 -0
  56. {ophyd_async-0.8.0a3 → ophyd_async-0.8.0a5}/LICENSE +0 -0
  57. {ophyd_async-0.8.0a3 → ophyd_async-0.8.0a5}/README.md +0 -0
  58. {ophyd_async-0.8.0a3 → ophyd_async-0.8.0a5}/docs/_api.rst +0 -0
  59. {ophyd_async-0.8.0a3 → ophyd_async-0.8.0a5}/docs/_templates/custom-module-template.rst +0 -0
  60. {ophyd_async-0.8.0a3 → ophyd_async-0.8.0a5}/docs/conf.py +0 -0
  61. {ophyd_async-0.8.0a3 → ophyd_async-0.8.0a5}/docs/examples/epics_demo.py +0 -0
  62. {ophyd_async-0.8.0a3 → ophyd_async-0.8.0a5}/docs/examples/foo_detector.py +0 -0
  63. {ophyd_async-0.8.0a3 → ophyd_async-0.8.0a5}/docs/examples/tango_demo.py +0 -0
  64. {ophyd_async-0.8.0a3 → ophyd_async-0.8.0a5}/docs/explanations/decisions/0001-record-architecture-decisions.md +0 -0
  65. {ophyd_async-0.8.0a3 → ophyd_async-0.8.0a5}/docs/explanations/decisions/0002-switched-to-python-copier-template.md +0 -0
  66. {ophyd_async-0.8.0a3 → ophyd_async-0.8.0a5}/docs/explanations/decisions/0003-ophyd-async-migration.rst +0 -0
  67. {ophyd_async-0.8.0a3 → ophyd_async-0.8.0a5}/docs/explanations/decisions/0004-repository-structure.rst +0 -0
  68. {ophyd_async-0.8.0a3 → ophyd_async-0.8.0a5}/docs/explanations/decisions/0005-respect-black-line-length.rst +0 -0
  69. {ophyd_async-0.8.0a3 → ophyd_async-0.8.0a5}/docs/explanations/decisions/0006-procedural-device-definitions.rst +0 -0
  70. {ophyd_async-0.8.0a3 → ophyd_async-0.8.0a5}/docs/explanations/decisions/0007-subpackage-structure.md +0 -0
  71. {ophyd_async-0.8.0a3 → ophyd_async-0.8.0a5}/docs/explanations/decisions/0008-signal-types.md +0 -0
  72. {ophyd_async-0.8.0a3 → ophyd_async-0.8.0a5}/docs/explanations/decisions/0009-procedural-vs-declarative-devices.md +0 -0
  73. {ophyd_async-0.8.0a3 → ophyd_async-0.8.0a5}/docs/explanations/decisions/COPYME +0 -0
  74. {ophyd_async-0.8.0a3 → ophyd_async-0.8.0a5}/docs/explanations/decisions.md +0 -0
  75. {ophyd_async-0.8.0a3 → ophyd_async-0.8.0a5}/docs/explanations/design-goals.rst +0 -0
  76. {ophyd_async-0.8.0a3 → ophyd_async-0.8.0a5}/docs/explanations/event-loop-choice.rst +0 -0
  77. {ophyd_async-0.8.0a3 → ophyd_async-0.8.0a5}/docs/explanations/flyscanning.rst +0 -0
  78. {ophyd_async-0.8.0a3 → ophyd_async-0.8.0a5}/docs/explanations.md +0 -0
  79. {ophyd_async-0.8.0a3 → ophyd_async-0.8.0a5}/docs/genindex.rst +0 -0
  80. {ophyd_async-0.8.0a3 → ophyd_async-0.8.0a5}/docs/how-to/choose-interfaces-for-devices.md +0 -0
  81. {ophyd_async-0.8.0a3 → ophyd_async-0.8.0a5}/docs/how-to/compound-devices.rst +0 -0
  82. {ophyd_async-0.8.0a3 → ophyd_async-0.8.0a5}/docs/how-to/contribute.md +0 -0
  83. {ophyd_async-0.8.0a3 → ophyd_async-0.8.0a5}/docs/how-to/make-a-simple-device.rst +0 -0
  84. {ophyd_async-0.8.0a3 → ophyd_async-0.8.0a5}/docs/how-to/make-a-standard-detector.rst +0 -0
  85. {ophyd_async-0.8.0a3 → ophyd_async-0.8.0a5}/docs/how-to/write-tests-for-devices.rst +0 -0
  86. {ophyd_async-0.8.0a3 → ophyd_async-0.8.0a5}/docs/how-to.md +0 -0
  87. {ophyd_async-0.8.0a3 → ophyd_async-0.8.0a5}/docs/images/ophyd-async-logo.svg +0 -0
  88. {ophyd_async-0.8.0a3 → ophyd_async-0.8.0a5}/docs/images/ophyd-favicon.svg +0 -0
  89. {ophyd_async-0.8.0a3 → ophyd_async-0.8.0a5}/docs/index.md +0 -0
  90. {ophyd_async-0.8.0a3 → ophyd_async-0.8.0a5}/docs/reference.md +0 -0
  91. {ophyd_async-0.8.0a3 → ophyd_async-0.8.0a5}/docs/tutorials/installation.md +0 -0
  92. {ophyd_async-0.8.0a3 → ophyd_async-0.8.0a5}/docs/tutorials/using-existing-devices.rst +0 -0
  93. {ophyd_async-0.8.0a3 → ophyd_async-0.8.0a5}/docs/tutorials.md +0 -0
  94. {ophyd_async-0.8.0a3 → ophyd_async-0.8.0a5}/pyproject.toml +0 -0
  95. {ophyd_async-0.8.0a3 → ophyd_async-0.8.0a5}/setup.cfg +0 -0
  96. {ophyd_async-0.8.0a3 → ophyd_async-0.8.0a5}/src/ophyd_async/__init__.py +0 -0
  97. {ophyd_async-0.8.0a3 → ophyd_async-0.8.0a5}/src/ophyd_async/__main__.py +0 -0
  98. {ophyd_async-0.8.0a3 → ophyd_async-0.8.0a5}/src/ophyd_async/core/_detector.py +0 -0
  99. {ophyd_async-0.8.0a3 → ophyd_async-0.8.0a5}/src/ophyd_async/core/_device_filler.py +0 -0
  100. {ophyd_async-0.8.0a3 → ophyd_async-0.8.0a5}/src/ophyd_async/core/_device_save_loader.py +0 -0
  101. {ophyd_async-0.8.0a3 → ophyd_async-0.8.0a5}/src/ophyd_async/core/_flyer.py +0 -0
  102. {ophyd_async-0.8.0a3 → ophyd_async-0.8.0a5}/src/ophyd_async/core/_hdf_dataset.py +0 -0
  103. {ophyd_async-0.8.0a3 → ophyd_async-0.8.0a5}/src/ophyd_async/core/_log.py +0 -0
  104. {ophyd_async-0.8.0a3 → ophyd_async-0.8.0a5}/src/ophyd_async/core/_protocol.py +0 -0
  105. {ophyd_async-0.8.0a3 → ophyd_async-0.8.0a5}/src/ophyd_async/core/_providers.py +0 -0
  106. {ophyd_async-0.8.0a3 → ophyd_async-0.8.0a5}/src/ophyd_async/core/_readable.py +0 -0
  107. {ophyd_async-0.8.0a3 → ophyd_async-0.8.0a5}/src/ophyd_async/core/_signal_backend.py +0 -0
  108. {ophyd_async-0.8.0a3 → ophyd_async-0.8.0a5}/src/ophyd_async/core/_status.py +0 -0
  109. {ophyd_async-0.8.0a3 → ophyd_async-0.8.0a5}/src/ophyd_async/core/_table.py +0 -0
  110. {ophyd_async-0.8.0a3 → ophyd_async-0.8.0a5}/src/ophyd_async/epics/__init__.py +0 -0
  111. {ophyd_async-0.8.0a3 → ophyd_async-0.8.0a5}/src/ophyd_async/epics/adaravis/__init__.py +0 -0
  112. {ophyd_async-0.8.0a3 → ophyd_async-0.8.0a5}/src/ophyd_async/epics/adaravis/_aravis.py +0 -0
  113. {ophyd_async-0.8.0a3 → ophyd_async-0.8.0a5}/src/ophyd_async/epics/adaravis/_aravis_controller.py +0 -0
  114. {ophyd_async-0.8.0a3 → ophyd_async-0.8.0a5}/src/ophyd_async/epics/adaravis/_aravis_io.py +0 -0
  115. {ophyd_async-0.8.0a3 → ophyd_async-0.8.0a5}/src/ophyd_async/epics/adcore/__init__.py +0 -0
  116. {ophyd_async-0.8.0a3 → ophyd_async-0.8.0a5}/src/ophyd_async/epics/adcore/_core_io.py +0 -0
  117. {ophyd_async-0.8.0a3 → ophyd_async-0.8.0a5}/src/ophyd_async/epics/adcore/_core_logic.py +0 -0
  118. {ophyd_async-0.8.0a3 → ophyd_async-0.8.0a5}/src/ophyd_async/epics/adcore/_hdf_writer.py +0 -0
  119. {ophyd_async-0.8.0a3 → ophyd_async-0.8.0a5}/src/ophyd_async/epics/adcore/_utils.py +0 -0
  120. {ophyd_async-0.8.0a3 → ophyd_async-0.8.0a5}/src/ophyd_async/epics/adkinetix/__init__.py +0 -0
  121. {ophyd_async-0.8.0a3 → ophyd_async-0.8.0a5}/src/ophyd_async/epics/adkinetix/_kinetix.py +0 -0
  122. {ophyd_async-0.8.0a3 → ophyd_async-0.8.0a5}/src/ophyd_async/epics/adkinetix/_kinetix_controller.py +0 -0
  123. {ophyd_async-0.8.0a3 → ophyd_async-0.8.0a5}/src/ophyd_async/epics/adkinetix/_kinetix_io.py +0 -0
  124. {ophyd_async-0.8.0a3 → ophyd_async-0.8.0a5}/src/ophyd_async/epics/adpilatus/__init__.py +0 -0
  125. {ophyd_async-0.8.0a3 → ophyd_async-0.8.0a5}/src/ophyd_async/epics/adpilatus/_pilatus.py +0 -0
  126. {ophyd_async-0.8.0a3 → ophyd_async-0.8.0a5}/src/ophyd_async/epics/adpilatus/_pilatus_controller.py +0 -0
  127. {ophyd_async-0.8.0a3 → ophyd_async-0.8.0a5}/src/ophyd_async/epics/adpilatus/_pilatus_io.py +0 -0
  128. {ophyd_async-0.8.0a3 → ophyd_async-0.8.0a5}/src/ophyd_async/epics/adsimdetector/__init__.py +0 -0
  129. {ophyd_async-0.8.0a3 → ophyd_async-0.8.0a5}/src/ophyd_async/epics/adsimdetector/_sim.py +0 -0
  130. {ophyd_async-0.8.0a3 → ophyd_async-0.8.0a5}/src/ophyd_async/epics/adsimdetector/_sim_controller.py +0 -0
  131. {ophyd_async-0.8.0a3 → ophyd_async-0.8.0a5}/src/ophyd_async/epics/advimba/__init__.py +0 -0
  132. {ophyd_async-0.8.0a3 → ophyd_async-0.8.0a5}/src/ophyd_async/epics/advimba/_vimba.py +0 -0
  133. {ophyd_async-0.8.0a3 → ophyd_async-0.8.0a5}/src/ophyd_async/epics/advimba/_vimba_controller.py +0 -0
  134. {ophyd_async-0.8.0a3 → ophyd_async-0.8.0a5}/src/ophyd_async/epics/advimba/_vimba_io.py +0 -0
  135. {ophyd_async-0.8.0a3 → ophyd_async-0.8.0a5}/src/ophyd_async/epics/core/__init__.py +0 -0
  136. {ophyd_async-0.8.0a3 → ophyd_async-0.8.0a5}/src/ophyd_async/epics/core/_epics_connector.py +0 -0
  137. {ophyd_async-0.8.0a3 → ophyd_async-0.8.0a5}/src/ophyd_async/epics/core/_epics_device.py +0 -0
  138. {ophyd_async-0.8.0a3 → ophyd_async-0.8.0a5}/src/ophyd_async/epics/core/_signal.py +0 -0
  139. {ophyd_async-0.8.0a3 → ophyd_async-0.8.0a5}/src/ophyd_async/epics/core/_util.py +0 -0
  140. {ophyd_async-0.8.0a3 → ophyd_async-0.8.0a5}/src/ophyd_async/epics/demo/__init__.py +0 -0
  141. {ophyd_async-0.8.0a3 → ophyd_async-0.8.0a5}/src/ophyd_async/epics/demo/_mover.py +0 -0
  142. {ophyd_async-0.8.0a3 → ophyd_async-0.8.0a5}/src/ophyd_async/epics/demo/_sensor.py +0 -0
  143. {ophyd_async-0.8.0a3 → ophyd_async-0.8.0a5}/src/ophyd_async/epics/demo/mover.db +0 -0
  144. {ophyd_async-0.8.0a3 → ophyd_async-0.8.0a5}/src/ophyd_async/epics/demo/sensor.db +0 -0
  145. {ophyd_async-0.8.0a3 → ophyd_async-0.8.0a5}/src/ophyd_async/epics/eiger/__init__.py +0 -0
  146. {ophyd_async-0.8.0a3 → ophyd_async-0.8.0a5}/src/ophyd_async/epics/eiger/_eiger.py +0 -0
  147. {ophyd_async-0.8.0a3 → ophyd_async-0.8.0a5}/src/ophyd_async/epics/eiger/_eiger_controller.py +0 -0
  148. {ophyd_async-0.8.0a3 → ophyd_async-0.8.0a5}/src/ophyd_async/epics/eiger/_eiger_io.py +0 -0
  149. {ophyd_async-0.8.0a3 → ophyd_async-0.8.0a5}/src/ophyd_async/epics/eiger/_odin_io.py +0 -0
  150. {ophyd_async-0.8.0a3 → ophyd_async-0.8.0a5}/src/ophyd_async/epics/motor.py +0 -0
  151. {ophyd_async-0.8.0a3 → ophyd_async-0.8.0a5}/src/ophyd_async/epics/signal.py +0 -0
  152. {ophyd_async-0.8.0a3 → ophyd_async-0.8.0a5}/src/ophyd_async/fastcs/__init__.py +0 -0
  153. {ophyd_async-0.8.0a3 → ophyd_async-0.8.0a5}/src/ophyd_async/fastcs/core.py +0 -0
  154. {ophyd_async-0.8.0a3 → ophyd_async-0.8.0a5}/src/ophyd_async/fastcs/odin/__init__.py +0 -0
  155. {ophyd_async-0.8.0a3 → ophyd_async-0.8.0a5}/src/ophyd_async/fastcs/panda/__init__.py +0 -0
  156. {ophyd_async-0.8.0a3 → ophyd_async-0.8.0a5}/src/ophyd_async/fastcs/panda/_block.py +0 -0
  157. {ophyd_async-0.8.0a3 → ophyd_async-0.8.0a5}/src/ophyd_async/fastcs/panda/_control.py +0 -0
  158. {ophyd_async-0.8.0a3 → ophyd_async-0.8.0a5}/src/ophyd_async/fastcs/panda/_hdf_panda.py +0 -0
  159. {ophyd_async-0.8.0a3 → ophyd_async-0.8.0a5}/src/ophyd_async/fastcs/panda/_trigger.py +0 -0
  160. {ophyd_async-0.8.0a3 → ophyd_async-0.8.0a5}/src/ophyd_async/fastcs/panda/_utils.py +0 -0
  161. {ophyd_async-0.8.0a3 → ophyd_async-0.8.0a5}/src/ophyd_async/fastcs/panda/_writer.py +0 -0
  162. {ophyd_async-0.8.0a3 → ophyd_async-0.8.0a5}/src/ophyd_async/plan_stubs/__init__.py +0 -0
  163. {ophyd_async-0.8.0a3 → ophyd_async-0.8.0a5}/src/ophyd_async/plan_stubs/_fly.py +0 -0
  164. {ophyd_async-0.8.0a3 → ophyd_async-0.8.0a5}/src/ophyd_async/plan_stubs/_nd_attributes.py +0 -0
  165. {ophyd_async-0.8.0a3 → ophyd_async-0.8.0a5}/src/ophyd_async/py.typed +0 -0
  166. {ophyd_async-0.8.0a3 → ophyd_async-0.8.0a5}/src/ophyd_async/sim/__init__.py +0 -0
  167. {ophyd_async-0.8.0a3 → ophyd_async-0.8.0a5}/src/ophyd_async/sim/demo/__init__.py +0 -0
  168. {ophyd_async-0.8.0a3 → ophyd_async-0.8.0a5}/src/ophyd_async/sim/demo/_pattern_detector/__init__.py +0 -0
  169. {ophyd_async-0.8.0a3 → ophyd_async-0.8.0a5}/src/ophyd_async/sim/demo/_pattern_detector/_pattern_detector.py +0 -0
  170. {ophyd_async-0.8.0a3 → ophyd_async-0.8.0a5}/src/ophyd_async/sim/demo/_pattern_detector/_pattern_detector_controller.py +0 -0
  171. {ophyd_async-0.8.0a3 → ophyd_async-0.8.0a5}/src/ophyd_async/sim/demo/_pattern_detector/_pattern_detector_writer.py +0 -0
  172. {ophyd_async-0.8.0a3 → ophyd_async-0.8.0a5}/src/ophyd_async/sim/demo/_pattern_detector/_pattern_generator.py +0 -0
  173. {ophyd_async-0.8.0a3 → ophyd_async-0.8.0a5}/src/ophyd_async/sim/demo/_sim_motor.py +0 -0
  174. {ophyd_async-0.8.0a3 → ophyd_async-0.8.0a5}/src/ophyd_async/sim/testing/__init__.py +0 -0
  175. {ophyd_async-0.8.0a3 → ophyd_async-0.8.0a5}/src/ophyd_async/tango/__init__.py +0 -0
  176. {ophyd_async-0.8.0a3 → ophyd_async-0.8.0a5}/src/ophyd_async/tango/base_devices/__init__.py +0 -0
  177. {ophyd_async-0.8.0a3 → ophyd_async-0.8.0a5}/src/ophyd_async/tango/base_devices/_tango_readable.py +0 -0
  178. {ophyd_async-0.8.0a3 → ophyd_async-0.8.0a5}/src/ophyd_async/tango/demo/__init__.py +0 -0
  179. {ophyd_async-0.8.0a3 → ophyd_async-0.8.0a5}/src/ophyd_async/tango/demo/_counter.py +0 -0
  180. {ophyd_async-0.8.0a3 → ophyd_async-0.8.0a5}/src/ophyd_async/tango/demo/_detector.py +0 -0
  181. {ophyd_async-0.8.0a3 → ophyd_async-0.8.0a5}/src/ophyd_async/tango/demo/_mover.py +0 -0
  182. {ophyd_async-0.8.0a3 → ophyd_async-0.8.0a5}/src/ophyd_async/tango/demo/_tango/__init__.py +0 -0
  183. {ophyd_async-0.8.0a3 → ophyd_async-0.8.0a5}/src/ophyd_async/tango/demo/_tango/_servers.py +0 -0
  184. {ophyd_async-0.8.0a3 → ophyd_async-0.8.0a5}/src/ophyd_async/tango/signal/__init__.py +0 -0
  185. {ophyd_async-0.8.0a3 → ophyd_async-0.8.0a5}/src/ophyd_async/tango/signal/_signal.py +0 -0
  186. {ophyd_async-0.8.0a3 → ophyd_async-0.8.0a5}/src/ophyd_async/tango/signal/_tango_transport.py +0 -0
  187. {ophyd_async-0.8.0a3 → ophyd_async-0.8.0a5}/src/ophyd_async.egg-info/dependency_links.txt +0 -0
  188. {ophyd_async-0.8.0a3 → ophyd_async-0.8.0a5}/src/ophyd_async.egg-info/entry_points.txt +0 -0
  189. {ophyd_async-0.8.0a3 → ophyd_async-0.8.0a5}/src/ophyd_async.egg-info/requires.txt +0 -0
  190. {ophyd_async-0.8.0a3 → ophyd_async-0.8.0a5}/src/ophyd_async.egg-info/top_level.txt +0 -0
  191. {ophyd_async-0.8.0a3 → ophyd_async-0.8.0a5}/system_tests/epics/eiger/README.md +0 -0
  192. {ophyd_async-0.8.0a3 → ophyd_async-0.8.0a5}/system_tests/epics/eiger/start_iocs_and_run_tests.sh +0 -0
  193. {ophyd_async-0.8.0a3 → ophyd_async-0.8.0a5}/system_tests/epics/eiger/test_eiger_system.py +0 -0
  194. {ophyd_async-0.8.0a3 → ophyd_async-0.8.0a5}/tests/conftest.py +0 -0
  195. {ophyd_async-0.8.0a3 → ophyd_async-0.8.0a5}/tests/core/test_device_collector.py +0 -0
  196. {ophyd_async-0.8.0a3 → ophyd_async-0.8.0a5}/tests/core/test_flyer.py +0 -0
  197. {ophyd_async-0.8.0a3 → ophyd_async-0.8.0a5}/tests/core/test_log.py +0 -0
  198. {ophyd_async-0.8.0a3 → ophyd_async-0.8.0a5}/tests/core/test_mock_signal_backend.py +0 -0
  199. {ophyd_async-0.8.0a3 → ophyd_async-0.8.0a5}/tests/core/test_protocol.py +0 -0
  200. {ophyd_async-0.8.0a3 → ophyd_async-0.8.0a5}/tests/core/test_providers.py +0 -0
  201. {ophyd_async-0.8.0a3 → ophyd_async-0.8.0a5}/tests/core/test_readable.py +0 -0
  202. {ophyd_async-0.8.0a3 → ophyd_async-0.8.0a5}/tests/core/test_status.py +0 -0
  203. {ophyd_async-0.8.0a3 → ophyd_async-0.8.0a5}/tests/core/test_subset_enum.py +0 -0
  204. {ophyd_async-0.8.0a3 → ophyd_async-0.8.0a5}/tests/core/test_table.py +0 -0
  205. {ophyd_async-0.8.0a3 → ophyd_async-0.8.0a5}/tests/core/test_watchable_async_status.py +0 -0
  206. {ophyd_async-0.8.0a3 → ophyd_async-0.8.0a5}/tests/epics/adaravis/test_aravis.py +0 -0
  207. {ophyd_async-0.8.0a3 → ophyd_async-0.8.0a5}/tests/epics/adcore/test_drivers.py +0 -0
  208. {ophyd_async-0.8.0a3 → ophyd_async-0.8.0a5}/tests/epics/adcore/test_scans.py +0 -0
  209. {ophyd_async-0.8.0a3 → ophyd_async-0.8.0a5}/tests/epics/adcore/test_single_trigger.py +0 -0
  210. {ophyd_async-0.8.0a3 → ophyd_async-0.8.0a5}/tests/epics/adcore/test_writers.py +0 -0
  211. {ophyd_async-0.8.0a3 → ophyd_async-0.8.0a5}/tests/epics/adkinetix/test_kinetix.py +0 -0
  212. {ophyd_async-0.8.0a3 → ophyd_async-0.8.0a5}/tests/epics/adpilatus/test_pilatus.py +0 -0
  213. {ophyd_async-0.8.0a3 → ophyd_async-0.8.0a5}/tests/epics/adsimdetector/test_sim.py +0 -0
  214. {ophyd_async-0.8.0a3 → ophyd_async-0.8.0a5}/tests/epics/advimba/test_vimba.py +0 -0
  215. {ophyd_async-0.8.0a3 → ophyd_async-0.8.0a5}/tests/epics/conftest.py +0 -0
  216. {ophyd_async-0.8.0a3 → ophyd_async-0.8.0a5}/tests/epics/eiger/test_eiger_controller.py +0 -0
  217. {ophyd_async-0.8.0a3 → ophyd_async-0.8.0a5}/tests/epics/eiger/test_eiger_detector.py +0 -0
  218. {ophyd_async-0.8.0a3 → ophyd_async-0.8.0a5}/tests/epics/eiger/test_odin_io.py +0 -0
  219. {ophyd_async-0.8.0a3 → ophyd_async-0.8.0a5}/tests/epics/pvi/test_pvi.py +0 -0
  220. {ophyd_async-0.8.0a3 → ophyd_async-0.8.0a5}/tests/epics/signal/test_common.py +0 -0
  221. {ophyd_async-0.8.0a3 → ophyd_async-0.8.0a5}/tests/epics/signal/test_records.db +0 -0
  222. {ophyd_async-0.8.0a3 → ophyd_async-0.8.0a5}/tests/epics/signal/test_signals.py +0 -0
  223. {ophyd_async-0.8.0a3 → ophyd_async-0.8.0a5}/tests/epics/test_areadetector_subclass_naming.py +0 -0
  224. {ophyd_async-0.8.0a3 → ophyd_async-0.8.0a5}/tests/epics/test_motor.py +0 -0
  225. {ophyd_async-0.8.0a3 → ophyd_async-0.8.0a5}/tests/fastcs/panda/db/panda.db +0 -0
  226. {ophyd_async-0.8.0a3 → ophyd_async-0.8.0a5}/tests/fastcs/panda/test_panda_connect.py +0 -0
  227. {ophyd_async-0.8.0a3 → ophyd_async-0.8.0a5}/tests/fastcs/panda/test_panda_control.py +0 -0
  228. {ophyd_async-0.8.0a3 → ophyd_async-0.8.0a5}/tests/fastcs/panda/test_panda_utils.py +0 -0
  229. {ophyd_async-0.8.0a3 → ophyd_async-0.8.0a5}/tests/fastcs/panda/test_seq_table.py +0 -0
  230. {ophyd_async-0.8.0a3 → ophyd_async-0.8.0a5}/tests/fastcs/panda/test_trigger.py +0 -0
  231. {ophyd_async-0.8.0a3 → ophyd_async-0.8.0a5}/tests/plan_stubs/test_fly.py +0 -0
  232. {ophyd_async-0.8.0a3 → ophyd_async-0.8.0a5}/tests/sim/__init__.py +0 -0
  233. {ophyd_async-0.8.0a3 → ophyd_async-0.8.0a5}/tests/sim/conftest.py +0 -0
  234. {ophyd_async-0.8.0a3 → ophyd_async-0.8.0a5}/tests/sim/demo/__init__.py +0 -0
  235. {ophyd_async-0.8.0a3 → ophyd_async-0.8.0a5}/tests/sim/demo/test_sim_motor.py +0 -0
  236. {ophyd_async-0.8.0a3 → ophyd_async-0.8.0a5}/tests/sim/test_pattern_generator.py +0 -0
  237. {ophyd_async-0.8.0a3 → ophyd_async-0.8.0a5}/tests/sim/test_sim_detector.py +0 -0
  238. {ophyd_async-0.8.0a3 → ophyd_async-0.8.0a5}/tests/sim/test_sim_writer.py +0 -0
  239. {ophyd_async-0.8.0a3 → ophyd_async-0.8.0a5}/tests/sim/test_streaming_plan.py +0 -0
  240. {ophyd_async-0.8.0a3 → ophyd_async-0.8.0a5}/tests/tango/test_base_device.py +0 -0
  241. {ophyd_async-0.8.0a3 → ophyd_async-0.8.0a5}/tests/tango/test_tango_signals.py +0 -0
  242. {ophyd_async-0.8.0a3 → ophyd_async-0.8.0a5}/tests/tango/test_tango_transport.py +0 -0
  243. {ophyd_async-0.8.0a3 → ophyd_async-0.8.0a5}/tests/test_cli.py +0 -0
@@ -1,5 +1,5 @@
1
1
  # Changes here will be overwritten by Copier
2
- _commit: 2.4.0
2
+ _commit: 2.5.0
3
3
  _src_path: gh:DiamondLightSource/python-copier-template
4
4
  author_email: tom.cobb@diamond.ac.uk
5
5
  author_name: Tom Cobb
@@ -24,4 +24,4 @@ It is recommended that developers use a [vscode devcontainer](https://code.visua
24
24
 
25
25
  This project was created using the [Diamond Light Source Copier Template](https://github.com/DiamondLightSource/python-copier-template) for Python projects.
26
26
 
27
- For more information on common tasks like setting up a developer environment, running the tests, and setting a pre-commit hook, see the template's [How-to guides](https://diamondlightsource.github.io/python-copier-template/2.4.0/how-to.html).
27
+ For more information on common tasks like setting up a developer environment, running the tests, and setting a pre-commit hook, see the template's [How-to guides](https://diamondlightsource.github.io/python-copier-template/2.5.0/how-to.html).
@@ -16,4 +16,4 @@ jobs:
16
16
  - name: Publish to PyPI using trusted publishing
17
17
  uses: pypa/gh-action-pypi-publish@release/v1
18
18
  with:
19
- attestations: false
19
+ attestations: false
@@ -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@c062e08bd532815e2082a85e87e3ef29c3e6d191 # v2.0.8
26
+ uses: softprops/action-gh-release@e7a8f85e1c67a31e6ed99a94b41bd0b71bbee6b8 # v2.0.9
27
27
  with:
28
28
  prerelease: ${{ contains(github.ref_name, 'a') || contains(github.ref_name, 'b') || contains(github.ref_name, 'rc') }}
29
29
  files: "*"
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.1
2
2
  Name: ophyd-async
3
- Version: 0.8.0a3
3
+ Version: 0.8.0a5
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
@@ -12,5 +12,5 @@ __version__: str
12
12
  __version_tuple__: VERSION_TUPLE
13
13
  version_tuple: VERSION_TUPLE
14
14
 
15
- __version__ = version = '0.8.0a3'
15
+ __version__ = version = '0.8.0a5'
16
16
  __version_tuple__ = version_tuple = (0, 8, 0)
@@ -83,6 +83,7 @@ from ._utils import (
83
83
  DEFAULT_TIMEOUT,
84
84
  CalculatableTimeout,
85
85
  Callback,
86
+ LazyMock,
86
87
  NotConnected,
87
88
  Reference,
88
89
  StrictEnum,
@@ -176,6 +177,7 @@ __all__ = [
176
177
  "DEFAULT_TIMEOUT",
177
178
  "CalculatableTimeout",
178
179
  "Callback",
180
+ "LazyMock",
179
181
  "CALCULATE_TIMEOUT",
180
182
  "NotConnected",
181
183
  "Reference",
@@ -3,17 +3,15 @@ from __future__ import annotations
3
3
  import asyncio
4
4
  import sys
5
5
  from collections.abc import Coroutine, Iterator, Mapping, MutableMapping
6
+ from functools import cached_property
6
7
  from logging import LoggerAdapter, getLogger
7
8
  from typing import Any, TypeVar
8
- from unittest.mock import Mock
9
9
 
10
10
  from bluesky.protocols import HasName
11
11
  from bluesky.run_engine import call_in_bluesky_event_loop, in_bluesky_event_loop
12
12
 
13
13
  from ._protocol import Connectable
14
- from ._utils import DEFAULT_TIMEOUT, NotConnected, wait_for_connection
15
-
16
- _device_mocks: dict[Device, Mock] = {}
14
+ from ._utils import DEFAULT_TIMEOUT, LazyMock, NotConnected, wait_for_connection
17
15
 
18
16
 
19
17
  class DeviceConnector:
@@ -37,25 +35,29 @@ class DeviceConnector:
37
35
  during ``__init__``.
38
36
  """
39
37
 
40
- async def connect(
41
- self,
42
- device: Device,
43
- mock: bool | Mock,
44
- timeout: float,
45
- force_reconnect: bool,
46
- ):
38
+ async def connect_mock(self, device: Device, mock: LazyMock):
39
+ # Connect serially, no errors to gather up as in mock mode
40
+ exceptions: dict[str, Exception] = {}
41
+ for name, child_device in device.children():
42
+ try:
43
+ await child_device.connect(mock=mock.child(name))
44
+ except Exception as e:
45
+ exceptions[name] = e
46
+ if exceptions:
47
+ raise NotConnected.with_other_exceptions_logged(exceptions)
48
+
49
+ async def connect_real(self, device: Device, timeout: float, force_reconnect: bool):
47
50
  """Used during ``Device.connect``.
48
51
 
49
52
  This is called when a previous connect has not been done, or has been
50
53
  done in a different mock more. It should connect the Device and all its
51
54
  children.
52
55
  """
53
- coros = {}
54
- for name, child_device in device.children():
55
- child_mock = getattr(mock, name) if mock else mock # Mock() or False
56
- coros[name] = child_device.connect(
57
- mock=child_mock, timeout=timeout, force_reconnect=force_reconnect
58
- )
56
+ # Connect in parallel, gathering up NotConnected errors
57
+ coros = {
58
+ name: child_device.connect(timeout=timeout, force_reconnect=force_reconnect)
59
+ for name, child_device in device.children()
60
+ }
59
61
  await wait_for_connection(**coros)
60
62
 
61
63
 
@@ -67,9 +69,8 @@ class Device(HasName, Connectable):
67
69
  parent: Device | None = None
68
70
  # None if connect hasn't started, a Task if it has
69
71
  _connect_task: asyncio.Task | None = None
70
- # If not None, then this is the mock arg of the previous connect
71
- # to let us know if we can reuse an existing connection
72
- _connect_mock_arg: bool | None = None
72
+ # The mock if we have connected in mock mode
73
+ _mock: LazyMock | None = None
73
74
 
74
75
  def __init__(
75
76
  self, name: str = "", connector: DeviceConnector | None = None
@@ -83,10 +84,18 @@ class Device(HasName, Connectable):
83
84
  """Return the name of the Device"""
84
85
  return self._name
85
86
 
87
+ @cached_property
88
+ def _child_devices(self) -> dict[str, Device]:
89
+ return {}
90
+
86
91
  def children(self) -> Iterator[tuple[str, Device]]:
87
- for attr_name, attr in self.__dict__.items():
88
- if attr_name != "parent" and isinstance(attr, Device):
89
- yield attr_name, attr
92
+ yield from self._child_devices.items()
93
+
94
+ @cached_property
95
+ def log(self) -> LoggerAdapter:
96
+ return LoggerAdapter(
97
+ getLogger("ophyd_async.devices"), {"ophyd_async_device_name": self.name}
98
+ )
90
99
 
91
100
  def set_name(self, name: str):
92
101
  """Set ``self.name=name`` and each ``self.child.name=name+"-child"``.
@@ -97,28 +106,33 @@ class Device(HasName, Connectable):
97
106
  New name to set
98
107
  """
99
108
  self._name = name
100
- # Ensure self.log is recreated after a name change
101
- self.log = LoggerAdapter(
102
- getLogger("ophyd_async.devices"), {"ophyd_async_device_name": self.name}
103
- )
109
+ # Ensure logger is recreated after a name change
110
+ if "log" in self.__dict__:
111
+ del self.log
104
112
  for child_name, child in self.children():
105
113
  child_name = f"{self.name}-{child_name.strip('_')}" if self.name else ""
106
114
  child.set_name(child_name)
107
115
 
108
116
  def __setattr__(self, name: str, value: Any) -> None:
117
+ # Bear in mind that this function is called *a lot*, so
118
+ # we need to make sure nothing expensive happens in it...
109
119
  if name == "parent":
110
120
  if self.parent not in (value, None):
111
121
  raise TypeError(
112
122
  f"Cannot set the parent of {self} to be {value}: "
113
123
  f"it is already a child of {self.parent}"
114
124
  )
115
- elif isinstance(value, Device):
125
+ # ...hence not doing an isinstance check for attributes we
126
+ # know not to be Devices
127
+ elif name not in _not_device_attrs and isinstance(value, Device):
116
128
  value.parent = self
117
- return super().__setattr__(name, value)
129
+ self._child_devices[name] = value
130
+ # ...and avoiding the super call as we know it resolves to `object`
131
+ return object.__setattr__(self, name, value)
118
132
 
119
133
  async def connect(
120
134
  self,
121
- mock: bool | Mock = False,
135
+ mock: bool | LazyMock = False,
122
136
  timeout: float = DEFAULT_TIMEOUT,
123
137
  force_reconnect: bool = False,
124
138
  ) -> None:
@@ -133,26 +147,39 @@ class Device(HasName, Connectable):
133
147
  timeout:
134
148
  Time to wait before failing with a TimeoutError.
135
149
  """
136
- uses_mock = bool(mock)
137
- can_use_previous_connect = (
138
- uses_mock is self._connect_mock_arg
139
- and self._connect_task
140
- and not (self._connect_task.done() and self._connect_task.exception())
141
- )
142
- if mock is True:
143
- mock = Mock() # create a new Mock if one not provided
144
- if force_reconnect or not can_use_previous_connect:
145
- self._connect_mock_arg = uses_mock
146
- if self._connect_mock_arg:
147
- _device_mocks[self] = mock
148
- coro = self._connector.connect(
149
- device=self, mock=mock, timeout=timeout, force_reconnect=force_reconnect
150
+ if mock:
151
+ # Always connect in mock mode serially
152
+ if isinstance(mock, LazyMock):
153
+ # Use the provided mock
154
+ self._mock = mock
155
+ elif not self._mock:
156
+ # Make one
157
+ self._mock = LazyMock()
158
+ await self._connector.connect_mock(self, self._mock)
159
+ else:
160
+ # Try to cache the connect in real mode
161
+ can_use_previous_connect = (
162
+ self._mock is None
163
+ and self._connect_task
164
+ and not (self._connect_task.done() and self._connect_task.exception())
150
165
  )
151
- self._connect_task = asyncio.create_task(coro)
166
+ if force_reconnect or not can_use_previous_connect:
167
+ self._mock = None
168
+ coro = self._connector.connect_real(self, timeout, force_reconnect)
169
+ self._connect_task = asyncio.create_task(coro)
170
+ assert self._connect_task, "Connect task not created, this shouldn't happen"
171
+ # Wait for it to complete
172
+ await self._connect_task
173
+
152
174
 
153
- assert self._connect_task, "Connect task not created, this shouldn't happen"
154
- # Wait for it to complete
155
- await self._connect_task
175
+ _not_device_attrs = {
176
+ "_name",
177
+ "_children",
178
+ "_connector",
179
+ "_timeout",
180
+ "_mock",
181
+ "_connect_task",
182
+ }
156
183
 
157
184
 
158
185
  DeviceT = TypeVar("DeviceT", bound=Device)
@@ -172,7 +199,8 @@ class DeviceVector(MutableMapping[int, DeviceT], Device):
172
199
  children: Mapping[int, DeviceT],
173
200
  name: str = "",
174
201
  ) -> None:
175
- self._children = dict(children)
202
+ self._children: dict[int, DeviceT] = {}
203
+ self.update(children)
176
204
  super().__init__(name=name)
177
205
 
178
206
  def __setattr__(self, name: str, child: Any) -> None:
@@ -1,13 +1,13 @@
1
1
  import asyncio
2
2
  from collections.abc import Callable
3
3
  from functools import cached_property
4
- from unittest.mock import AsyncMock, Mock
4
+ from unittest.mock import AsyncMock
5
5
 
6
6
  from bluesky.protocols import Descriptor, Reading
7
7
 
8
8
  from ._signal_backend import SignalBackend, SignalDatatypeT
9
9
  from ._soft_signal_backend import SoftSignalBackend
10
- from ._utils import Callback
10
+ from ._utils import Callback, LazyMock
11
11
 
12
12
 
13
13
  class MockSignalBackend(SignalBackend[SignalDatatypeT]):
@@ -16,7 +16,7 @@ class MockSignalBackend(SignalBackend[SignalDatatypeT]):
16
16
  def __init__(
17
17
  self,
18
18
  initial_backend: SignalBackend[SignalDatatypeT],
19
- mock: Mock,
19
+ mock: LazyMock,
20
20
  ) -> None:
21
21
  if isinstance(initial_backend, MockSignalBackend):
22
22
  raise ValueError("Cannot make a MockSignalBackend for a MockSignalBackend")
@@ -34,11 +34,14 @@ class MockSignalBackend(SignalBackend[SignalDatatypeT]):
34
34
 
35
35
  # use existing Mock if provided
36
36
  self.mock = mock
37
- self.put_mock = AsyncMock(name="put", spec=Callable)
38
- self.mock.attach_mock(self.put_mock, "put")
39
-
40
37
  super().__init__(datatype=self.initial_backend.datatype)
41
38
 
39
+ @cached_property
40
+ def put_mock(self) -> AsyncMock:
41
+ put_mock = AsyncMock(name="put", spec=Callable)
42
+ self.mock().attach_mock(put_mock, "put")
43
+ return put_mock
44
+
42
45
  def set_value(self, value: SignalDatatypeT):
43
46
  self.soft_backend.set_value(value)
44
47
 
@@ -46,7 +49,7 @@ class MockSignalBackend(SignalBackend[SignalDatatypeT]):
46
49
  return f"mock+{self.initial_backend.source(name, read)}"
47
50
 
48
51
  async def connect(self, timeout: float) -> None:
49
- pass
52
+ raise RuntimeError("It is not possible to connect a MockSignalBackend")
50
53
 
51
54
  @cached_property
52
55
  def put_proceeds(self) -> asyncio.Event:
@@ -2,17 +2,26 @@ from collections.abc import Awaitable, Callable, Iterable
2
2
  from contextlib import asynccontextmanager, contextmanager
3
3
  from unittest.mock import AsyncMock, Mock
4
4
 
5
- from ._device import Device, _device_mocks
5
+ from ._device import Device
6
6
  from ._mock_signal_backend import MockSignalBackend
7
- from ._signal import Signal, SignalR, _mock_signal_backends
7
+ from ._signal import Signal, SignalConnector, SignalR
8
8
  from ._soft_signal_backend import SignalDatatypeT
9
+ from ._utils import LazyMock
10
+
11
+
12
+ def get_mock(device: Device | Signal) -> Mock:
13
+ mock = device._mock # noqa: SLF001
14
+ assert isinstance(mock, LazyMock), f"Device {device} not connected in mock mode"
15
+ return mock()
9
16
 
10
17
 
11
18
  def _get_mock_signal_backend(signal: Signal) -> MockSignalBackend:
12
- assert (
13
- signal in _mock_signal_backends
19
+ connector = signal._connector # noqa: SLF001
20
+ assert isinstance(connector, SignalConnector), f"Expected Signal, got {signal}"
21
+ assert isinstance(
22
+ connector.backend, MockSignalBackend
14
23
  ), f"Signal {signal} not connected in mock mode"
15
- return _mock_signal_backends[signal]
24
+ return connector.backend
16
25
 
17
26
 
18
27
  def set_mock_value(signal: Signal[SignalDatatypeT], value: SignalDatatypeT):
@@ -45,12 +54,6 @@ def get_mock_put(signal: Signal) -> AsyncMock:
45
54
  return _get_mock_signal_backend(signal).put_mock
46
55
 
47
56
 
48
- def get_mock(device: Device | Signal) -> Mock:
49
- if isinstance(device, Signal):
50
- return _get_mock_signal_backend(device).mock
51
- return _device_mocks[device]
52
-
53
-
54
57
  def reset_mock_put_calls(signal: Signal):
55
58
  backend = _get_mock_signal_backend(signal)
56
59
  backend.put_mock.reset_mock()
@@ -4,7 +4,6 @@ import asyncio
4
4
  import functools
5
5
  from collections.abc import AsyncGenerator, Awaitable, Callable, Mapping
6
6
  from typing import Any, Generic, cast
7
- from unittest.mock import Mock
8
7
 
9
8
  from bluesky.protocols import (
10
9
  Locatable,
@@ -30,9 +29,14 @@ from ._signal_backend import (
30
29
  )
31
30
  from ._soft_signal_backend import SoftSignalBackend
32
31
  from ._status import AsyncStatus
33
- from ._utils import CALCULATE_TIMEOUT, DEFAULT_TIMEOUT, CalculatableTimeout, Callback, T
34
-
35
- _mock_signal_backends: dict[Device, MockSignalBackend] = {}
32
+ from ._utils import (
33
+ CALCULATE_TIMEOUT,
34
+ DEFAULT_TIMEOUT,
35
+ CalculatableTimeout,
36
+ Callback,
37
+ LazyMock,
38
+ T,
39
+ )
36
40
 
37
41
 
38
42
  async def _wait_for(coro: Awaitable[T], timeout: float | None, source: str) -> T:
@@ -54,26 +58,28 @@ class SignalConnector(DeviceConnector):
54
58
  def __init__(self, backend: SignalBackend):
55
59
  self.backend = self._init_backend = backend
56
60
 
57
- async def connect(
58
- self,
59
- device: Device,
60
- mock: bool | Mock,
61
- timeout: float,
62
- force_reconnect: bool,
63
- ):
64
- if mock:
65
- self.backend = MockSignalBackend(self._init_backend, mock)
66
- _mock_signal_backends[device] = self.backend
67
- else:
68
- self.backend = self._init_backend
61
+ async def connect_mock(self, device: Device, mock: LazyMock):
62
+ self.backend = MockSignalBackend(self._init_backend, mock)
63
+
64
+ async def connect_real(self, device: Device, timeout: float, force_reconnect: bool):
65
+ self.backend = self._init_backend
69
66
  device.log.debug(f"Connecting to {self.backend.source(device.name, read=True)}")
70
67
  await self.backend.connect(timeout)
71
68
 
72
69
 
70
+ class _ChildrenNotAllowed(dict[str, Device]):
71
+ def __setitem__(self, key: str, value: Device) -> None:
72
+ raise AttributeError(
73
+ f"Cannot add Device or Signal child {key}={value} of Signal, "
74
+ "make a subclass of Device instead"
75
+ )
76
+
77
+
73
78
  class Signal(Device, Generic[SignalDatatypeT]):
74
79
  """A Device with the concept of a value, with R, RW, W and X flavours"""
75
80
 
76
81
  _connector: SignalConnector
82
+ _child_devices = _ChildrenNotAllowed() # type: ignore
77
83
 
78
84
  def __init__(
79
85
  self,
@@ -89,14 +95,6 @@ class Signal(Device, Generic[SignalDatatypeT]):
89
95
  """Like ca://PV_PREFIX:SIGNAL, or "" if not set"""
90
96
  return self._connector.backend.source(self.name, read=True)
91
97
 
92
- def __setattr__(self, name: str, value: Any) -> None:
93
- if name != "parent" and isinstance(value, Device):
94
- raise AttributeError(
95
- f"Cannot add Device or Signal {value} as a child of Signal {self}, "
96
- "make a subclass of Device instead"
97
- )
98
- return super().__setattr__(name, value)
99
-
100
98
 
101
99
  class _SignalCache(Generic[SignalDatatypeT]):
102
100
  def __init__(self, backend: SignalBackend[SignalDatatypeT], signal: Signal):
@@ -451,12 +449,6 @@ async def observe_value(
451
449
  """
452
450
 
453
451
  q: asyncio.Queue[SignalDatatypeT | Status] = asyncio.Queue()
454
- if timeout is None:
455
- get_value = q.get
456
- else:
457
-
458
- async def get_value():
459
- return await asyncio.wait_for(q.get(), timeout)
460
452
 
461
453
  if done_status is not None:
462
454
  done_status.add_callback(q.put_nowait)
@@ -464,7 +456,10 @@ async def observe_value(
464
456
  signal.subscribe_value(q.put_nowait)
465
457
  try:
466
458
  while True:
467
- item = await get_value()
459
+ # yield here in case something else is filling the queue
460
+ # like in test_observe_value_times_out_with_no_external_task()
461
+ await asyncio.sleep(0)
462
+ item = await asyncio.wait_for(q.get(), timeout)
468
463
  if done_status and item is done_status:
469
464
  if exc := done_status.exception():
470
465
  raise exc
@@ -4,7 +4,8 @@ import time
4
4
  from abc import abstractmethod
5
5
  from collections.abc import Sequence
6
6
  from dataclasses import dataclass
7
- from typing import Any, Generic, get_origin
7
+ from functools import lru_cache
8
+ from typing import Any, Generic, get_args, get_origin
8
9
 
9
10
  import numpy as np
10
11
  from bluesky.protocols import Reading
@@ -57,7 +58,7 @@ class SequenceEnumSoftConverter(SoftConverter[Sequence[EnumT]]):
57
58
 
58
59
  @dataclass
59
60
  class NDArraySoftConverter(SoftConverter[Array1D]):
60
- datatype: np.dtype
61
+ datatype: np.dtype | None = None
61
62
 
62
63
  def write_value(self, value: Any) -> Array1D:
63
64
  return np.array(() if value is None else value, dtype=self.datatype)
@@ -90,13 +91,18 @@ class TableSoftConverter(SoftConverter[TableT]):
90
91
  raise TypeError(f"Cannot convert {value} to {self.datatype}")
91
92
 
92
93
 
94
+ @lru_cache
93
95
  def make_converter(datatype: type[SignalDatatype]) -> SoftConverter:
94
96
  enum_cls = get_enum_cls(datatype)
95
97
  if datatype == Sequence[str]:
96
98
  return SequenceStrSoftConverter()
97
99
  elif get_origin(datatype) == Sequence and enum_cls:
98
100
  return SequenceEnumSoftConverter(enum_cls)
101
+ elif datatype is np.ndarray:
102
+ return NDArraySoftConverter()
99
103
  elif get_origin(datatype) == np.ndarray:
104
+ if datatype not in get_args(SignalDatatype):
105
+ raise TypeError(f"Expected Array1D[dtype], got {datatype}")
100
106
  return NDArraySoftConverter(get_dtype(datatype))
101
107
  elif enum_cls:
102
108
  return EnumSoftConverter(enum_cls)
@@ -2,18 +2,11 @@ from __future__ import annotations
2
2
 
3
3
  import asyncio
4
4
  import logging
5
- from collections.abc import Awaitable, Callable, Iterable, Sequence
5
+ from collections.abc import Awaitable, Callable, Iterable, Mapping, Sequence
6
6
  from dataclasses import dataclass
7
7
  from enum import Enum, EnumMeta
8
- from typing import (
9
- Any,
10
- Generic,
11
- Literal,
12
- ParamSpec,
13
- TypeVar,
14
- get_args,
15
- get_origin,
16
- )
8
+ from typing import Any, Generic, Literal, ParamSpec, TypeVar, get_args, get_origin
9
+ from unittest.mock import Mock
17
10
 
18
11
  import numpy as np
19
12
 
@@ -21,7 +14,7 @@ T = TypeVar("T")
21
14
  P = ParamSpec("P")
22
15
  Callback = Callable[[T], None]
23
16
  DEFAULT_TIMEOUT = 10.0
24
- ErrorText = str | dict[str, Exception]
17
+ ErrorText = str | Mapping[str, Exception]
25
18
 
26
19
 
27
20
  class StrictEnum(str, Enum):
@@ -69,6 +62,13 @@ class NotConnected(Exception):
69
62
 
70
63
  self._errors = errors
71
64
 
65
+ @property
66
+ def sub_errors(self) -> Mapping[str, Exception]:
67
+ if isinstance(self._errors, dict):
68
+ return self._errors.copy()
69
+ else:
70
+ return {}
71
+
72
72
  def _format_sub_errors(self, name: str, error: Exception, indent="") -> str:
73
73
  if isinstance(error, NotConnected):
74
74
  error_txt = ":" + error.format_error_string(indent + self._indent_width)
@@ -99,6 +99,19 @@ class NotConnected(Exception):
99
99
  def __str__(self) -> str:
100
100
  return self.format_error_string(indent="")
101
101
 
102
+ @classmethod
103
+ def with_other_exceptions_logged(
104
+ cls, exceptions: Mapping[str, Exception]
105
+ ) -> NotConnected:
106
+ for name, exception in exceptions.items():
107
+ if not isinstance(exception, NotConnected):
108
+ logging.exception(
109
+ f"device `{name}` raised unexpected exception "
110
+ f"{type(exception).__name__}",
111
+ exc_info=exception,
112
+ )
113
+ return NotConnected(exceptions)
114
+
102
115
 
103
116
  @dataclass(frozen=True)
104
117
  class WatcherUpdate(Generic[T]):
@@ -120,21 +133,23 @@ async def wait_for_connection(**coros: Awaitable[None]):
120
133
 
121
134
  Expected kwargs should be a mapping of names to coroutine tasks to execute.
122
135
  """
123
- results = await asyncio.gather(*coros.values(), return_exceptions=True)
124
- exceptions = {}
125
-
126
- for name, result in zip(coros, results, strict=False):
127
- if isinstance(result, Exception):
128
- exceptions[name] = result
129
- if not isinstance(result, NotConnected):
130
- logging.exception(
131
- f"device `{name}` raised unexpected exception "
132
- f"{type(result).__name__}",
133
- exc_info=result,
134
- )
136
+ exceptions: dict[str, Exception] = {}
137
+ if len(coros) == 1:
138
+ # Single device optimization
139
+ name, coro = coros.popitem()
140
+ try:
141
+ await coro
142
+ except Exception as e:
143
+ exceptions[name] = e
144
+ else:
145
+ # Use gather to connect in parallel
146
+ results = await asyncio.gather(*coros.values(), return_exceptions=True)
147
+ for name, result in zip(coros, results, strict=False):
148
+ if isinstance(result, Exception):
149
+ exceptions[name] = result
135
150
 
136
151
  if exceptions:
137
- raise NotConnected(exceptions)
152
+ raise NotConnected.with_other_exceptions_logged(exceptions)
138
153
 
139
154
 
140
155
  def get_dtype(datatype: type) -> np.dtype:
@@ -252,3 +267,38 @@ class Reference(Generic[T]):
252
267
 
253
268
  def __call__(self) -> T:
254
269
  return self._obj
270
+
271
+
272
+ class LazyMock:
273
+ """A lazily created Mock to be used when connecting in mock mode.
274
+
275
+ Creating Mocks is reasonably expensive when each Device (and Signal)
276
+ requires its own, and the tree is only used when ``Signal.set()`` is
277
+ called. This class allows a tree of lazily connected Mocks to be
278
+ constructed so that when the leaf is created, so are its parents.
279
+ Any calls to the child are then accessible from the parent mock.
280
+
281
+ >>> parent = LazyMock()
282
+ >>> child = parent.child("child")
283
+ >>> child_mock = child()
284
+ >>> child_mock() # doctest: +ELLIPSIS
285
+ <Mock name='mock.child()' id='...'>
286
+ >>> parent_mock = parent()
287
+ >>> parent_mock.mock_calls
288
+ [call.child()]
289
+ """
290
+
291
+ def __init__(self, name: str = "", parent: LazyMock | None = None) -> None:
292
+ self.parent = parent
293
+ self.name = name
294
+ self._mock: Mock | None = None
295
+
296
+ def child(self, name: str) -> LazyMock:
297
+ return LazyMock(name, self)
298
+
299
+ def __call__(self) -> Mock:
300
+ if self._mock is None:
301
+ self._mock = Mock(spec=object)
302
+ if self.parent is not None:
303
+ self.parent().attach_mock(self._mock, self.name)
304
+ return self._mock
@@ -19,7 +19,8 @@ class SingleTriggerDetector(StandardReadable, Triggerable):
19
19
  **plugins: NDPluginBaseIO,
20
20
  ) -> None:
21
21
  self.drv = drv
22
- self.__dict__.update(plugins)
22
+ for k, v in plugins.items():
23
+ setattr(self, k, v)
23
24
 
24
25
  self.add_readables(
25
26
  [self.drv.array_counter, *read_uncached],