ophyd-async 0.8.0a3__tar.gz → 0.8.0a4__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 (241) hide show
  1. {ophyd_async-0.8.0a3 → ophyd_async-0.8.0a4}/.copier-answers.yml +1 -1
  2. {ophyd_async-0.8.0a3 → ophyd_async-0.8.0a4}/.github/CONTRIBUTING.md +1 -1
  3. {ophyd_async-0.8.0a3 → ophyd_async-0.8.0a4}/.github/workflows/_pypi.yml +1 -1
  4. {ophyd_async-0.8.0a3 → ophyd_async-0.8.0a4}/.github/workflows/_release.yml +1 -1
  5. {ophyd_async-0.8.0a3 → ophyd_async-0.8.0a4}/PKG-INFO +1 -1
  6. {ophyd_async-0.8.0a3 → ophyd_async-0.8.0a4}/src/ophyd_async/_version.py +1 -1
  7. {ophyd_async-0.8.0a3 → ophyd_async-0.8.0a4}/src/ophyd_async/core/__init__.py +2 -0
  8. {ophyd_async-0.8.0a3 → ophyd_async-0.8.0a4}/src/ophyd_async/core/_device.py +70 -49
  9. {ophyd_async-0.8.0a3 → ophyd_async-0.8.0a4}/src/ophyd_async/core/_mock_signal_backend.py +10 -7
  10. {ophyd_async-0.8.0a3 → ophyd_async-0.8.0a4}/src/ophyd_async/core/_mock_signal_utils.py +14 -11
  11. {ophyd_async-0.8.0a3 → ophyd_async-0.8.0a4}/src/ophyd_async/core/_signal.py +22 -24
  12. {ophyd_async-0.8.0a3 → ophyd_async-0.8.0a4}/src/ophyd_async/core/_soft_signal_backend.py +2 -0
  13. {ophyd_async-0.8.0a3 → ophyd_async-0.8.0a4}/src/ophyd_async/core/_utils.py +55 -10
  14. {ophyd_async-0.8.0a3 → ophyd_async-0.8.0a4}/src/ophyd_async/epics/adcore/_single_trigger.py +2 -1
  15. {ophyd_async-0.8.0a3 → ophyd_async-0.8.0a4}/src/ophyd_async/epics/core/_pvi_connector.py +24 -25
  16. {ophyd_async-0.8.0a3 → ophyd_async-0.8.0a4}/src/ophyd_async/plan_stubs/_ensure_connected.py +2 -4
  17. {ophyd_async-0.8.0a3 → ophyd_async-0.8.0a4}/src/ophyd_async/tango/base_devices/_base_device.py +37 -36
  18. {ophyd_async-0.8.0a3 → ophyd_async-0.8.0a4}/src/ophyd_async.egg-info/PKG-INFO +1 -1
  19. {ophyd_async-0.8.0a3 → ophyd_async-0.8.0a4}/tests/core/test_device.py +22 -14
  20. {ophyd_async-0.8.0a3 → ophyd_async-0.8.0a4}/tests/core/test_signal.py +19 -59
  21. {ophyd_async-0.8.0a3 → ophyd_async-0.8.0a4}/tests/epics/demo/test_demo.py +5 -3
  22. {ophyd_async-0.8.0a3 → ophyd_async-0.8.0a4}/tests/plan_stubs/test_ensure_connected.py +2 -2
  23. {ophyd_async-0.8.0a3 → ophyd_async-0.8.0a4}/.codecov.yml +0 -0
  24. {ophyd_async-0.8.0a3 → ophyd_async-0.8.0a4}/.devcontainer/devcontainer.json +0 -0
  25. {ophyd_async-0.8.0a3 → ophyd_async-0.8.0a4}/.git-blame-ignore-revs +0 -0
  26. {ophyd_async-0.8.0a3 → ophyd_async-0.8.0a4}/.github/ISSUE_TEMPLATE/bug_report.md +0 -0
  27. {ophyd_async-0.8.0a3 → ophyd_async-0.8.0a4}/.github/ISSUE_TEMPLATE/issue.md +0 -0
  28. {ophyd_async-0.8.0a3 → ophyd_async-0.8.0a4}/.github/PULL_REQUEST_TEMPLATE/pull_request_template.md +0 -0
  29. {ophyd_async-0.8.0a3 → ophyd_async-0.8.0a4}/.github/actions/install_requirements/action.yml +0 -0
  30. {ophyd_async-0.8.0a3 → ophyd_async-0.8.0a4}/.github/dependabot.yml +0 -0
  31. {ophyd_async-0.8.0a3 → ophyd_async-0.8.0a4}/.github/pages/index.html +0 -0
  32. {ophyd_async-0.8.0a3 → ophyd_async-0.8.0a4}/.github/pages/make_switcher.py +0 -0
  33. {ophyd_async-0.8.0a3 → ophyd_async-0.8.0a4}/.github/workflows/_check.yml +0 -0
  34. {ophyd_async-0.8.0a3 → ophyd_async-0.8.0a4}/.github/workflows/_dist.yml +0 -0
  35. {ophyd_async-0.8.0a3 → ophyd_async-0.8.0a4}/.github/workflows/_docs.yml +0 -0
  36. {ophyd_async-0.8.0a3 → ophyd_async-0.8.0a4}/.github/workflows/_test.yml +0 -0
  37. {ophyd_async-0.8.0a3 → ophyd_async-0.8.0a4}/.github/workflows/_tox.yml +0 -0
  38. {ophyd_async-0.8.0a3 → ophyd_async-0.8.0a4}/.github/workflows/ci.yml +0 -0
  39. {ophyd_async-0.8.0a3 → ophyd_async-0.8.0a4}/.github/workflows/periodic.yml +0 -0
  40. {ophyd_async-0.8.0a3 → ophyd_async-0.8.0a4}/.gitignore +0 -0
  41. {ophyd_async-0.8.0a3 → ophyd_async-0.8.0a4}/.mailmap +0 -0
  42. {ophyd_async-0.8.0a3 → ophyd_async-0.8.0a4}/.pre-commit-config.yaml +0 -0
  43. {ophyd_async-0.8.0a3 → ophyd_async-0.8.0a4}/Dockerfile +0 -0
  44. {ophyd_async-0.8.0a3 → ophyd_async-0.8.0a4}/LICENSE +0 -0
  45. {ophyd_async-0.8.0a3 → ophyd_async-0.8.0a4}/README.md +0 -0
  46. {ophyd_async-0.8.0a3 → ophyd_async-0.8.0a4}/docs/_api.rst +0 -0
  47. {ophyd_async-0.8.0a3 → ophyd_async-0.8.0a4}/docs/_templates/custom-module-template.rst +0 -0
  48. {ophyd_async-0.8.0a3 → ophyd_async-0.8.0a4}/docs/conf.py +0 -0
  49. {ophyd_async-0.8.0a3 → ophyd_async-0.8.0a4}/docs/examples/epics_demo.py +0 -0
  50. {ophyd_async-0.8.0a3 → ophyd_async-0.8.0a4}/docs/examples/foo_detector.py +0 -0
  51. {ophyd_async-0.8.0a3 → ophyd_async-0.8.0a4}/docs/examples/tango_demo.py +0 -0
  52. {ophyd_async-0.8.0a3 → ophyd_async-0.8.0a4}/docs/explanations/decisions/0001-record-architecture-decisions.md +0 -0
  53. {ophyd_async-0.8.0a3 → ophyd_async-0.8.0a4}/docs/explanations/decisions/0002-switched-to-python-copier-template.md +0 -0
  54. {ophyd_async-0.8.0a3 → ophyd_async-0.8.0a4}/docs/explanations/decisions/0003-ophyd-async-migration.rst +0 -0
  55. {ophyd_async-0.8.0a3 → ophyd_async-0.8.0a4}/docs/explanations/decisions/0004-repository-structure.rst +0 -0
  56. {ophyd_async-0.8.0a3 → ophyd_async-0.8.0a4}/docs/explanations/decisions/0005-respect-black-line-length.rst +0 -0
  57. {ophyd_async-0.8.0a3 → ophyd_async-0.8.0a4}/docs/explanations/decisions/0006-procedural-device-definitions.rst +0 -0
  58. {ophyd_async-0.8.0a3 → ophyd_async-0.8.0a4}/docs/explanations/decisions/0007-subpackage-structure.md +0 -0
  59. {ophyd_async-0.8.0a3 → ophyd_async-0.8.0a4}/docs/explanations/decisions/0008-signal-types.md +0 -0
  60. {ophyd_async-0.8.0a3 → ophyd_async-0.8.0a4}/docs/explanations/decisions/0009-procedural-vs-declarative-devices.md +0 -0
  61. {ophyd_async-0.8.0a3 → ophyd_async-0.8.0a4}/docs/explanations/decisions/COPYME +0 -0
  62. {ophyd_async-0.8.0a3 → ophyd_async-0.8.0a4}/docs/explanations/decisions.md +0 -0
  63. {ophyd_async-0.8.0a3 → ophyd_async-0.8.0a4}/docs/explanations/design-goals.rst +0 -0
  64. {ophyd_async-0.8.0a3 → ophyd_async-0.8.0a4}/docs/explanations/event-loop-choice.rst +0 -0
  65. {ophyd_async-0.8.0a3 → ophyd_async-0.8.0a4}/docs/explanations/flyscanning.rst +0 -0
  66. {ophyd_async-0.8.0a3 → ophyd_async-0.8.0a4}/docs/explanations.md +0 -0
  67. {ophyd_async-0.8.0a3 → ophyd_async-0.8.0a4}/docs/genindex.rst +0 -0
  68. {ophyd_async-0.8.0a3 → ophyd_async-0.8.0a4}/docs/how-to/choose-interfaces-for-devices.md +0 -0
  69. {ophyd_async-0.8.0a3 → ophyd_async-0.8.0a4}/docs/how-to/compound-devices.rst +0 -0
  70. {ophyd_async-0.8.0a3 → ophyd_async-0.8.0a4}/docs/how-to/contribute.md +0 -0
  71. {ophyd_async-0.8.0a3 → ophyd_async-0.8.0a4}/docs/how-to/make-a-simple-device.rst +0 -0
  72. {ophyd_async-0.8.0a3 → ophyd_async-0.8.0a4}/docs/how-to/make-a-standard-detector.rst +0 -0
  73. {ophyd_async-0.8.0a3 → ophyd_async-0.8.0a4}/docs/how-to/write-tests-for-devices.rst +0 -0
  74. {ophyd_async-0.8.0a3 → ophyd_async-0.8.0a4}/docs/how-to.md +0 -0
  75. {ophyd_async-0.8.0a3 → ophyd_async-0.8.0a4}/docs/images/ophyd-async-logo.svg +0 -0
  76. {ophyd_async-0.8.0a3 → ophyd_async-0.8.0a4}/docs/images/ophyd-favicon.svg +0 -0
  77. {ophyd_async-0.8.0a3 → ophyd_async-0.8.0a4}/docs/index.md +0 -0
  78. {ophyd_async-0.8.0a3 → ophyd_async-0.8.0a4}/docs/reference.md +0 -0
  79. {ophyd_async-0.8.0a3 → ophyd_async-0.8.0a4}/docs/tutorials/installation.md +0 -0
  80. {ophyd_async-0.8.0a3 → ophyd_async-0.8.0a4}/docs/tutorials/using-existing-devices.rst +0 -0
  81. {ophyd_async-0.8.0a3 → ophyd_async-0.8.0a4}/docs/tutorials.md +0 -0
  82. {ophyd_async-0.8.0a3 → ophyd_async-0.8.0a4}/pyproject.toml +0 -0
  83. {ophyd_async-0.8.0a3 → ophyd_async-0.8.0a4}/setup.cfg +0 -0
  84. {ophyd_async-0.8.0a3 → ophyd_async-0.8.0a4}/src/ophyd_async/__init__.py +0 -0
  85. {ophyd_async-0.8.0a3 → ophyd_async-0.8.0a4}/src/ophyd_async/__main__.py +0 -0
  86. {ophyd_async-0.8.0a3 → ophyd_async-0.8.0a4}/src/ophyd_async/core/_detector.py +0 -0
  87. {ophyd_async-0.8.0a3 → ophyd_async-0.8.0a4}/src/ophyd_async/core/_device_filler.py +0 -0
  88. {ophyd_async-0.8.0a3 → ophyd_async-0.8.0a4}/src/ophyd_async/core/_device_save_loader.py +0 -0
  89. {ophyd_async-0.8.0a3 → ophyd_async-0.8.0a4}/src/ophyd_async/core/_flyer.py +0 -0
  90. {ophyd_async-0.8.0a3 → ophyd_async-0.8.0a4}/src/ophyd_async/core/_hdf_dataset.py +0 -0
  91. {ophyd_async-0.8.0a3 → ophyd_async-0.8.0a4}/src/ophyd_async/core/_log.py +0 -0
  92. {ophyd_async-0.8.0a3 → ophyd_async-0.8.0a4}/src/ophyd_async/core/_protocol.py +0 -0
  93. {ophyd_async-0.8.0a3 → ophyd_async-0.8.0a4}/src/ophyd_async/core/_providers.py +0 -0
  94. {ophyd_async-0.8.0a3 → ophyd_async-0.8.0a4}/src/ophyd_async/core/_readable.py +0 -0
  95. {ophyd_async-0.8.0a3 → ophyd_async-0.8.0a4}/src/ophyd_async/core/_signal_backend.py +0 -0
  96. {ophyd_async-0.8.0a3 → ophyd_async-0.8.0a4}/src/ophyd_async/core/_status.py +0 -0
  97. {ophyd_async-0.8.0a3 → ophyd_async-0.8.0a4}/src/ophyd_async/core/_table.py +0 -0
  98. {ophyd_async-0.8.0a3 → ophyd_async-0.8.0a4}/src/ophyd_async/epics/__init__.py +0 -0
  99. {ophyd_async-0.8.0a3 → ophyd_async-0.8.0a4}/src/ophyd_async/epics/adaravis/__init__.py +0 -0
  100. {ophyd_async-0.8.0a3 → ophyd_async-0.8.0a4}/src/ophyd_async/epics/adaravis/_aravis.py +0 -0
  101. {ophyd_async-0.8.0a3 → ophyd_async-0.8.0a4}/src/ophyd_async/epics/adaravis/_aravis_controller.py +0 -0
  102. {ophyd_async-0.8.0a3 → ophyd_async-0.8.0a4}/src/ophyd_async/epics/adaravis/_aravis_io.py +0 -0
  103. {ophyd_async-0.8.0a3 → ophyd_async-0.8.0a4}/src/ophyd_async/epics/adcore/__init__.py +0 -0
  104. {ophyd_async-0.8.0a3 → ophyd_async-0.8.0a4}/src/ophyd_async/epics/adcore/_core_io.py +0 -0
  105. {ophyd_async-0.8.0a3 → ophyd_async-0.8.0a4}/src/ophyd_async/epics/adcore/_core_logic.py +0 -0
  106. {ophyd_async-0.8.0a3 → ophyd_async-0.8.0a4}/src/ophyd_async/epics/adcore/_hdf_writer.py +0 -0
  107. {ophyd_async-0.8.0a3 → ophyd_async-0.8.0a4}/src/ophyd_async/epics/adcore/_utils.py +0 -0
  108. {ophyd_async-0.8.0a3 → ophyd_async-0.8.0a4}/src/ophyd_async/epics/adkinetix/__init__.py +0 -0
  109. {ophyd_async-0.8.0a3 → ophyd_async-0.8.0a4}/src/ophyd_async/epics/adkinetix/_kinetix.py +0 -0
  110. {ophyd_async-0.8.0a3 → ophyd_async-0.8.0a4}/src/ophyd_async/epics/adkinetix/_kinetix_controller.py +0 -0
  111. {ophyd_async-0.8.0a3 → ophyd_async-0.8.0a4}/src/ophyd_async/epics/adkinetix/_kinetix_io.py +0 -0
  112. {ophyd_async-0.8.0a3 → ophyd_async-0.8.0a4}/src/ophyd_async/epics/adpilatus/__init__.py +0 -0
  113. {ophyd_async-0.8.0a3 → ophyd_async-0.8.0a4}/src/ophyd_async/epics/adpilatus/_pilatus.py +0 -0
  114. {ophyd_async-0.8.0a3 → ophyd_async-0.8.0a4}/src/ophyd_async/epics/adpilatus/_pilatus_controller.py +0 -0
  115. {ophyd_async-0.8.0a3 → ophyd_async-0.8.0a4}/src/ophyd_async/epics/adpilatus/_pilatus_io.py +0 -0
  116. {ophyd_async-0.8.0a3 → ophyd_async-0.8.0a4}/src/ophyd_async/epics/adsimdetector/__init__.py +0 -0
  117. {ophyd_async-0.8.0a3 → ophyd_async-0.8.0a4}/src/ophyd_async/epics/adsimdetector/_sim.py +0 -0
  118. {ophyd_async-0.8.0a3 → ophyd_async-0.8.0a4}/src/ophyd_async/epics/adsimdetector/_sim_controller.py +0 -0
  119. {ophyd_async-0.8.0a3 → ophyd_async-0.8.0a4}/src/ophyd_async/epics/advimba/__init__.py +0 -0
  120. {ophyd_async-0.8.0a3 → ophyd_async-0.8.0a4}/src/ophyd_async/epics/advimba/_vimba.py +0 -0
  121. {ophyd_async-0.8.0a3 → ophyd_async-0.8.0a4}/src/ophyd_async/epics/advimba/_vimba_controller.py +0 -0
  122. {ophyd_async-0.8.0a3 → ophyd_async-0.8.0a4}/src/ophyd_async/epics/advimba/_vimba_io.py +0 -0
  123. {ophyd_async-0.8.0a3 → ophyd_async-0.8.0a4}/src/ophyd_async/epics/core/__init__.py +0 -0
  124. {ophyd_async-0.8.0a3 → ophyd_async-0.8.0a4}/src/ophyd_async/epics/core/_aioca.py +0 -0
  125. {ophyd_async-0.8.0a3 → ophyd_async-0.8.0a4}/src/ophyd_async/epics/core/_epics_connector.py +0 -0
  126. {ophyd_async-0.8.0a3 → ophyd_async-0.8.0a4}/src/ophyd_async/epics/core/_epics_device.py +0 -0
  127. {ophyd_async-0.8.0a3 → ophyd_async-0.8.0a4}/src/ophyd_async/epics/core/_p4p.py +0 -0
  128. {ophyd_async-0.8.0a3 → ophyd_async-0.8.0a4}/src/ophyd_async/epics/core/_signal.py +0 -0
  129. {ophyd_async-0.8.0a3 → ophyd_async-0.8.0a4}/src/ophyd_async/epics/core/_util.py +0 -0
  130. {ophyd_async-0.8.0a3 → ophyd_async-0.8.0a4}/src/ophyd_async/epics/demo/__init__.py +0 -0
  131. {ophyd_async-0.8.0a3 → ophyd_async-0.8.0a4}/src/ophyd_async/epics/demo/_mover.py +0 -0
  132. {ophyd_async-0.8.0a3 → ophyd_async-0.8.0a4}/src/ophyd_async/epics/demo/_sensor.py +0 -0
  133. {ophyd_async-0.8.0a3 → ophyd_async-0.8.0a4}/src/ophyd_async/epics/demo/mover.db +0 -0
  134. {ophyd_async-0.8.0a3 → ophyd_async-0.8.0a4}/src/ophyd_async/epics/demo/sensor.db +0 -0
  135. {ophyd_async-0.8.0a3 → ophyd_async-0.8.0a4}/src/ophyd_async/epics/eiger/__init__.py +0 -0
  136. {ophyd_async-0.8.0a3 → ophyd_async-0.8.0a4}/src/ophyd_async/epics/eiger/_eiger.py +0 -0
  137. {ophyd_async-0.8.0a3 → ophyd_async-0.8.0a4}/src/ophyd_async/epics/eiger/_eiger_controller.py +0 -0
  138. {ophyd_async-0.8.0a3 → ophyd_async-0.8.0a4}/src/ophyd_async/epics/eiger/_eiger_io.py +0 -0
  139. {ophyd_async-0.8.0a3 → ophyd_async-0.8.0a4}/src/ophyd_async/epics/eiger/_odin_io.py +0 -0
  140. {ophyd_async-0.8.0a3 → ophyd_async-0.8.0a4}/src/ophyd_async/epics/motor.py +0 -0
  141. {ophyd_async-0.8.0a3 → ophyd_async-0.8.0a4}/src/ophyd_async/epics/signal.py +0 -0
  142. {ophyd_async-0.8.0a3 → ophyd_async-0.8.0a4}/src/ophyd_async/fastcs/__init__.py +0 -0
  143. {ophyd_async-0.8.0a3 → ophyd_async-0.8.0a4}/src/ophyd_async/fastcs/core.py +0 -0
  144. {ophyd_async-0.8.0a3 → ophyd_async-0.8.0a4}/src/ophyd_async/fastcs/odin/__init__.py +0 -0
  145. {ophyd_async-0.8.0a3 → ophyd_async-0.8.0a4}/src/ophyd_async/fastcs/panda/__init__.py +0 -0
  146. {ophyd_async-0.8.0a3 → ophyd_async-0.8.0a4}/src/ophyd_async/fastcs/panda/_block.py +0 -0
  147. {ophyd_async-0.8.0a3 → ophyd_async-0.8.0a4}/src/ophyd_async/fastcs/panda/_control.py +0 -0
  148. {ophyd_async-0.8.0a3 → ophyd_async-0.8.0a4}/src/ophyd_async/fastcs/panda/_hdf_panda.py +0 -0
  149. {ophyd_async-0.8.0a3 → ophyd_async-0.8.0a4}/src/ophyd_async/fastcs/panda/_table.py +0 -0
  150. {ophyd_async-0.8.0a3 → ophyd_async-0.8.0a4}/src/ophyd_async/fastcs/panda/_trigger.py +0 -0
  151. {ophyd_async-0.8.0a3 → ophyd_async-0.8.0a4}/src/ophyd_async/fastcs/panda/_utils.py +0 -0
  152. {ophyd_async-0.8.0a3 → ophyd_async-0.8.0a4}/src/ophyd_async/fastcs/panda/_writer.py +0 -0
  153. {ophyd_async-0.8.0a3 → ophyd_async-0.8.0a4}/src/ophyd_async/plan_stubs/__init__.py +0 -0
  154. {ophyd_async-0.8.0a3 → ophyd_async-0.8.0a4}/src/ophyd_async/plan_stubs/_fly.py +0 -0
  155. {ophyd_async-0.8.0a3 → ophyd_async-0.8.0a4}/src/ophyd_async/plan_stubs/_nd_attributes.py +0 -0
  156. {ophyd_async-0.8.0a3 → ophyd_async-0.8.0a4}/src/ophyd_async/py.typed +0 -0
  157. {ophyd_async-0.8.0a3 → ophyd_async-0.8.0a4}/src/ophyd_async/sim/__init__.py +0 -0
  158. {ophyd_async-0.8.0a3 → ophyd_async-0.8.0a4}/src/ophyd_async/sim/demo/__init__.py +0 -0
  159. {ophyd_async-0.8.0a3 → ophyd_async-0.8.0a4}/src/ophyd_async/sim/demo/_pattern_detector/__init__.py +0 -0
  160. {ophyd_async-0.8.0a3 → ophyd_async-0.8.0a4}/src/ophyd_async/sim/demo/_pattern_detector/_pattern_detector.py +0 -0
  161. {ophyd_async-0.8.0a3 → ophyd_async-0.8.0a4}/src/ophyd_async/sim/demo/_pattern_detector/_pattern_detector_controller.py +0 -0
  162. {ophyd_async-0.8.0a3 → ophyd_async-0.8.0a4}/src/ophyd_async/sim/demo/_pattern_detector/_pattern_detector_writer.py +0 -0
  163. {ophyd_async-0.8.0a3 → ophyd_async-0.8.0a4}/src/ophyd_async/sim/demo/_pattern_detector/_pattern_generator.py +0 -0
  164. {ophyd_async-0.8.0a3 → ophyd_async-0.8.0a4}/src/ophyd_async/sim/demo/_sim_motor.py +0 -0
  165. {ophyd_async-0.8.0a3 → ophyd_async-0.8.0a4}/src/ophyd_async/sim/testing/__init__.py +0 -0
  166. {ophyd_async-0.8.0a3 → ophyd_async-0.8.0a4}/src/ophyd_async/tango/__init__.py +0 -0
  167. {ophyd_async-0.8.0a3 → ophyd_async-0.8.0a4}/src/ophyd_async/tango/base_devices/__init__.py +0 -0
  168. {ophyd_async-0.8.0a3 → ophyd_async-0.8.0a4}/src/ophyd_async/tango/base_devices/_tango_readable.py +0 -0
  169. {ophyd_async-0.8.0a3 → ophyd_async-0.8.0a4}/src/ophyd_async/tango/demo/__init__.py +0 -0
  170. {ophyd_async-0.8.0a3 → ophyd_async-0.8.0a4}/src/ophyd_async/tango/demo/_counter.py +0 -0
  171. {ophyd_async-0.8.0a3 → ophyd_async-0.8.0a4}/src/ophyd_async/tango/demo/_detector.py +0 -0
  172. {ophyd_async-0.8.0a3 → ophyd_async-0.8.0a4}/src/ophyd_async/tango/demo/_mover.py +0 -0
  173. {ophyd_async-0.8.0a3 → ophyd_async-0.8.0a4}/src/ophyd_async/tango/demo/_tango/__init__.py +0 -0
  174. {ophyd_async-0.8.0a3 → ophyd_async-0.8.0a4}/src/ophyd_async/tango/demo/_tango/_servers.py +0 -0
  175. {ophyd_async-0.8.0a3 → ophyd_async-0.8.0a4}/src/ophyd_async/tango/signal/__init__.py +0 -0
  176. {ophyd_async-0.8.0a3 → ophyd_async-0.8.0a4}/src/ophyd_async/tango/signal/_signal.py +0 -0
  177. {ophyd_async-0.8.0a3 → ophyd_async-0.8.0a4}/src/ophyd_async/tango/signal/_tango_transport.py +0 -0
  178. {ophyd_async-0.8.0a3 → ophyd_async-0.8.0a4}/src/ophyd_async.egg-info/SOURCES.txt +0 -0
  179. {ophyd_async-0.8.0a3 → ophyd_async-0.8.0a4}/src/ophyd_async.egg-info/dependency_links.txt +0 -0
  180. {ophyd_async-0.8.0a3 → ophyd_async-0.8.0a4}/src/ophyd_async.egg-info/entry_points.txt +0 -0
  181. {ophyd_async-0.8.0a3 → ophyd_async-0.8.0a4}/src/ophyd_async.egg-info/requires.txt +0 -0
  182. {ophyd_async-0.8.0a3 → ophyd_async-0.8.0a4}/src/ophyd_async.egg-info/top_level.txt +0 -0
  183. {ophyd_async-0.8.0a3 → ophyd_async-0.8.0a4}/system_tests/epics/eiger/README.md +0 -0
  184. {ophyd_async-0.8.0a3 → ophyd_async-0.8.0a4}/system_tests/epics/eiger/start_iocs_and_run_tests.sh +0 -0
  185. {ophyd_async-0.8.0a3 → ophyd_async-0.8.0a4}/system_tests/epics/eiger/test_eiger_system.py +0 -0
  186. {ophyd_async-0.8.0a3 → ophyd_async-0.8.0a4}/tests/conftest.py +0 -0
  187. {ophyd_async-0.8.0a3 → ophyd_async-0.8.0a4}/tests/core/test_device_collector.py +0 -0
  188. {ophyd_async-0.8.0a3 → ophyd_async-0.8.0a4}/tests/core/test_device_save_loader.py +0 -0
  189. {ophyd_async-0.8.0a3 → ophyd_async-0.8.0a4}/tests/core/test_flyer.py +0 -0
  190. {ophyd_async-0.8.0a3 → ophyd_async-0.8.0a4}/tests/core/test_log.py +0 -0
  191. {ophyd_async-0.8.0a3 → ophyd_async-0.8.0a4}/tests/core/test_mock_signal_backend.py +0 -0
  192. {ophyd_async-0.8.0a3 → ophyd_async-0.8.0a4}/tests/core/test_protocol.py +0 -0
  193. {ophyd_async-0.8.0a3 → ophyd_async-0.8.0a4}/tests/core/test_providers.py +0 -0
  194. {ophyd_async-0.8.0a3 → ophyd_async-0.8.0a4}/tests/core/test_readable.py +0 -0
  195. {ophyd_async-0.8.0a3 → ophyd_async-0.8.0a4}/tests/core/test_soft_signal_backend.py +0 -0
  196. {ophyd_async-0.8.0a3 → ophyd_async-0.8.0a4}/tests/core/test_status.py +0 -0
  197. {ophyd_async-0.8.0a3 → ophyd_async-0.8.0a4}/tests/core/test_subset_enum.py +0 -0
  198. {ophyd_async-0.8.0a3 → ophyd_async-0.8.0a4}/tests/core/test_table.py +0 -0
  199. {ophyd_async-0.8.0a3 → ophyd_async-0.8.0a4}/tests/core/test_utils.py +0 -0
  200. {ophyd_async-0.8.0a3 → ophyd_async-0.8.0a4}/tests/core/test_watchable_async_status.py +0 -0
  201. {ophyd_async-0.8.0a3 → ophyd_async-0.8.0a4}/tests/epics/adaravis/test_aravis.py +0 -0
  202. {ophyd_async-0.8.0a3 → ophyd_async-0.8.0a4}/tests/epics/adcore/test_drivers.py +0 -0
  203. {ophyd_async-0.8.0a3 → ophyd_async-0.8.0a4}/tests/epics/adcore/test_scans.py +0 -0
  204. {ophyd_async-0.8.0a3 → ophyd_async-0.8.0a4}/tests/epics/adcore/test_single_trigger.py +0 -0
  205. {ophyd_async-0.8.0a3 → ophyd_async-0.8.0a4}/tests/epics/adcore/test_writers.py +0 -0
  206. {ophyd_async-0.8.0a3 → ophyd_async-0.8.0a4}/tests/epics/adkinetix/test_kinetix.py +0 -0
  207. {ophyd_async-0.8.0a3 → ophyd_async-0.8.0a4}/tests/epics/adpilatus/test_pilatus.py +0 -0
  208. {ophyd_async-0.8.0a3 → ophyd_async-0.8.0a4}/tests/epics/adsimdetector/test_sim.py +0 -0
  209. {ophyd_async-0.8.0a3 → ophyd_async-0.8.0a4}/tests/epics/advimba/test_vimba.py +0 -0
  210. {ophyd_async-0.8.0a3 → ophyd_async-0.8.0a4}/tests/epics/conftest.py +0 -0
  211. {ophyd_async-0.8.0a3 → ophyd_async-0.8.0a4}/tests/epics/eiger/test_eiger_controller.py +0 -0
  212. {ophyd_async-0.8.0a3 → ophyd_async-0.8.0a4}/tests/epics/eiger/test_eiger_detector.py +0 -0
  213. {ophyd_async-0.8.0a3 → ophyd_async-0.8.0a4}/tests/epics/eiger/test_odin_io.py +0 -0
  214. {ophyd_async-0.8.0a3 → ophyd_async-0.8.0a4}/tests/epics/pvi/test_pvi.py +0 -0
  215. {ophyd_async-0.8.0a3 → ophyd_async-0.8.0a4}/tests/epics/signal/test_common.py +0 -0
  216. {ophyd_async-0.8.0a3 → ophyd_async-0.8.0a4}/tests/epics/signal/test_records.db +0 -0
  217. {ophyd_async-0.8.0a3 → ophyd_async-0.8.0a4}/tests/epics/signal/test_signals.py +0 -0
  218. {ophyd_async-0.8.0a3 → ophyd_async-0.8.0a4}/tests/epics/test_areadetector_subclass_naming.py +0 -0
  219. {ophyd_async-0.8.0a3 → ophyd_async-0.8.0a4}/tests/epics/test_motor.py +0 -0
  220. {ophyd_async-0.8.0a3 → ophyd_async-0.8.0a4}/tests/fastcs/panda/db/panda.db +0 -0
  221. {ophyd_async-0.8.0a3 → ophyd_async-0.8.0a4}/tests/fastcs/panda/test_hdf_panda.py +0 -0
  222. {ophyd_async-0.8.0a3 → ophyd_async-0.8.0a4}/tests/fastcs/panda/test_panda_connect.py +0 -0
  223. {ophyd_async-0.8.0a3 → ophyd_async-0.8.0a4}/tests/fastcs/panda/test_panda_control.py +0 -0
  224. {ophyd_async-0.8.0a3 → ophyd_async-0.8.0a4}/tests/fastcs/panda/test_panda_utils.py +0 -0
  225. {ophyd_async-0.8.0a3 → ophyd_async-0.8.0a4}/tests/fastcs/panda/test_seq_table.py +0 -0
  226. {ophyd_async-0.8.0a3 → ophyd_async-0.8.0a4}/tests/fastcs/panda/test_trigger.py +0 -0
  227. {ophyd_async-0.8.0a3 → ophyd_async-0.8.0a4}/tests/fastcs/panda/test_writer.py +0 -0
  228. {ophyd_async-0.8.0a3 → ophyd_async-0.8.0a4}/tests/plan_stubs/test_fly.py +0 -0
  229. {ophyd_async-0.8.0a3 → ophyd_async-0.8.0a4}/tests/sim/__init__.py +0 -0
  230. {ophyd_async-0.8.0a3 → ophyd_async-0.8.0a4}/tests/sim/conftest.py +0 -0
  231. {ophyd_async-0.8.0a3 → ophyd_async-0.8.0a4}/tests/sim/demo/__init__.py +0 -0
  232. {ophyd_async-0.8.0a3 → ophyd_async-0.8.0a4}/tests/sim/demo/test_sim_motor.py +0 -0
  233. {ophyd_async-0.8.0a3 → ophyd_async-0.8.0a4}/tests/sim/test_pattern_generator.py +0 -0
  234. {ophyd_async-0.8.0a3 → ophyd_async-0.8.0a4}/tests/sim/test_sim_detector.py +0 -0
  235. {ophyd_async-0.8.0a3 → ophyd_async-0.8.0a4}/tests/sim/test_sim_writer.py +0 -0
  236. {ophyd_async-0.8.0a3 → ophyd_async-0.8.0a4}/tests/sim/test_streaming_plan.py +0 -0
  237. {ophyd_async-0.8.0a3 → ophyd_async-0.8.0a4}/tests/tango/test_base_device.py +0 -0
  238. {ophyd_async-0.8.0a3 → ophyd_async-0.8.0a4}/tests/tango/test_tango_signals.py +0 -0
  239. {ophyd_async-0.8.0a3 → ophyd_async-0.8.0a4}/tests/tango/test_tango_transport.py +0 -0
  240. {ophyd_async-0.8.0a3 → ophyd_async-0.8.0a4}/tests/test_cli.py +0 -0
  241. {ophyd_async-0.8.0a3 → ophyd_async-0.8.0a4}/tests/test_data/test_yaml_save.yml +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.0a4
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.0a4'
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,23 @@ 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
+ for name, child_device in device.children():
41
+ await child_device.connect(mock=mock.child(name))
42
+
43
+ async def connect_real(self, device: Device, timeout: float, force_reconnect: bool):
47
44
  """Used during ``Device.connect``.
48
45
 
49
46
  This is called when a previous connect has not been done, or has been
50
47
  done in a different mock more. It should connect the Device and all its
51
48
  children.
52
49
  """
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
- )
50
+ # Connect in parallel, gathering up NotConnected errors
51
+ coros = {
52
+ name: child_device.connect(timeout=timeout, force_reconnect=force_reconnect)
53
+ for name, child_device in device.children()
54
+ }
59
55
  await wait_for_connection(**coros)
60
56
 
61
57
 
@@ -67,9 +63,8 @@ class Device(HasName, Connectable):
67
63
  parent: Device | None = None
68
64
  # None if connect hasn't started, a Task if it has
69
65
  _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
66
+ # The mock if we have connected in mock mode
67
+ _mock: LazyMock | None = None
73
68
 
74
69
  def __init__(
75
70
  self, name: str = "", connector: DeviceConnector | None = None
@@ -83,10 +78,18 @@ class Device(HasName, Connectable):
83
78
  """Return the name of the Device"""
84
79
  return self._name
85
80
 
81
+ @cached_property
82
+ def _child_devices(self) -> dict[str, Device]:
83
+ return {}
84
+
86
85
  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
86
+ yield from self._child_devices.items()
87
+
88
+ @cached_property
89
+ def log(self) -> LoggerAdapter:
90
+ return LoggerAdapter(
91
+ getLogger("ophyd_async.devices"), {"ophyd_async_device_name": self.name}
92
+ )
90
93
 
91
94
  def set_name(self, name: str):
92
95
  """Set ``self.name=name`` and each ``self.child.name=name+"-child"``.
@@ -97,28 +100,33 @@ class Device(HasName, Connectable):
97
100
  New name to set
98
101
  """
99
102
  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
- )
103
+ # Ensure logger is recreated after a name change
104
+ if "log" in self.__dict__:
105
+ del self.log
104
106
  for child_name, child in self.children():
105
107
  child_name = f"{self.name}-{child_name.strip('_')}" if self.name else ""
106
108
  child.set_name(child_name)
107
109
 
108
110
  def __setattr__(self, name: str, value: Any) -> None:
111
+ # Bear in mind that this function is called *a lot*, so
112
+ # we need to make sure nothing expensive happens in it...
109
113
  if name == "parent":
110
114
  if self.parent not in (value, None):
111
115
  raise TypeError(
112
116
  f"Cannot set the parent of {self} to be {value}: "
113
117
  f"it is already a child of {self.parent}"
114
118
  )
115
- elif isinstance(value, Device):
119
+ # ...hence not doing an isinstance check for attributes we
120
+ # know not to be Devices
121
+ elif name not in _not_device_attrs and isinstance(value, Device):
116
122
  value.parent = self
117
- return super().__setattr__(name, value)
123
+ self._child_devices[name] = value
124
+ # ...and avoiding the super call as we know it resolves to `object`
125
+ return object.__setattr__(self, name, value)
118
126
 
119
127
  async def connect(
120
128
  self,
121
- mock: bool | Mock = False,
129
+ mock: bool | LazyMock = False,
122
130
  timeout: float = DEFAULT_TIMEOUT,
123
131
  force_reconnect: bool = False,
124
132
  ) -> None:
@@ -133,26 +141,39 @@ class Device(HasName, Connectable):
133
141
  timeout:
134
142
  Time to wait before failing with a TimeoutError.
135
143
  """
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
144
+ if mock:
145
+ # Always connect in mock mode serially
146
+ if isinstance(mock, LazyMock):
147
+ # Use the provided mock
148
+ self._mock = mock
149
+ elif not self._mock:
150
+ # Make one
151
+ self._mock = LazyMock()
152
+ await self._connector.connect_mock(self, self._mock)
153
+ else:
154
+ # Try to cache the connect in real mode
155
+ can_use_previous_connect = (
156
+ self._mock is None
157
+ and self._connect_task
158
+ and not (self._connect_task.done() and self._connect_task.exception())
150
159
  )
151
- self._connect_task = asyncio.create_task(coro)
152
-
153
- assert self._connect_task, "Connect task not created, this shouldn't happen"
154
- # Wait for it to complete
155
- await self._connect_task
160
+ if force_reconnect or not can_use_previous_connect:
161
+ self._mock = None
162
+ coro = self._connector.connect_real(self, timeout, force_reconnect)
163
+ self._connect_task = asyncio.create_task(coro)
164
+ assert self._connect_task, "Connect task not created, this shouldn't happen"
165
+ # Wait for it to complete
166
+ await self._connect_task
167
+
168
+
169
+ _not_device_attrs = {
170
+ "_name",
171
+ "_children",
172
+ "_connector",
173
+ "_timeout",
174
+ "_mock",
175
+ "_connect_task",
176
+ }
156
177
 
157
178
 
158
179
  DeviceT = TypeVar("DeviceT", bound=Device)
@@ -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):
@@ -4,6 +4,7 @@ import time
4
4
  from abc import abstractmethod
5
5
  from collections.abc import Sequence
6
6
  from dataclasses import dataclass
7
+ from functools import lru_cache
7
8
  from typing import Any, Generic, get_origin
8
9
 
9
10
  import numpy as np
@@ -90,6 +91,7 @@ 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]:
@@ -14,6 +14,7 @@ from typing import (
14
14
  get_args,
15
15
  get_origin,
16
16
  )
17
+ from unittest.mock import Mock
17
18
 
18
19
  import numpy as np
19
20
 
@@ -120,20 +121,29 @@ async def wait_for_connection(**coros: Awaitable[None]):
120
121
 
121
122
  Expected kwargs should be a mapping of names to coroutine tasks to execute.
122
123
  """
123
- results = await asyncio.gather(*coros.values(), return_exceptions=True)
124
- exceptions = {}
124
+ exceptions: dict[str, Exception] = {}
125
+ if len(coros) == 1:
126
+ # Single device optimization
127
+ name, coro = coros.popitem()
128
+ try:
129
+ await coro
130
+ except Exception as e:
131
+ exceptions[name] = e
132
+ else:
133
+ # Use gather to connect in parallel
134
+ results = await asyncio.gather(*coros.values(), return_exceptions=True)
135
+ for name, result in zip(coros, results, strict=False):
136
+ if isinstance(result, Exception):
137
+ exceptions[name] = result
125
138
 
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):
139
+ if exceptions:
140
+ for name, exception in exceptions.items():
141
+ if not isinstance(exception, NotConnected):
130
142
  logging.exception(
131
143
  f"device `{name}` raised unexpected exception "
132
- f"{type(result).__name__}",
133
- exc_info=result,
144
+ f"{type(exception).__name__}",
145
+ exc_info=exception,
134
146
  )
135
-
136
- if exceptions:
137
147
  raise NotConnected(exceptions)
138
148
 
139
149
 
@@ -252,3 +262,38 @@ class Reference(Generic[T]):
252
262
 
253
263
  def __call__(self) -> T:
254
264
  return self._obj
265
+
266
+
267
+ class LazyMock:
268
+ """A lazily created Mock to be used when connecting in mock mode.
269
+
270
+ Creating Mocks is reasonably expensive when each Device (and Signal)
271
+ requires its own, and the tree is only used when ``Signal.set()`` is
272
+ called. This class allows a tree of lazily connected Mocks to be
273
+ constructed so that when the leaf is created, so are its parents.
274
+ Any calls to the child are then accessible from the parent mock.
275
+
276
+ >>> parent = LazyMock()
277
+ >>> child = parent.child("child")
278
+ >>> child_mock = child()
279
+ >>> child_mock() # doctest: +ELLIPSIS
280
+ <Mock name='mock.child()' id='...'>
281
+ >>> parent_mock = parent()
282
+ >>> parent_mock.mock_calls
283
+ [call.child()]
284
+ """
285
+
286
+ def __init__(self, name: str = "", parent: LazyMock | None = None) -> None:
287
+ self.parent = parent
288
+ self.name = name
289
+ self._mock: Mock | None = None
290
+
291
+ def child(self, name: str) -> LazyMock:
292
+ return LazyMock(name, self)
293
+
294
+ def __call__(self) -> Mock:
295
+ if self._mock is None:
296
+ self._mock = Mock(spec=object)
297
+ if self.parent is not None:
298
+ self.parent().attach_mock(self._mock, self.name)
299
+ 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],
@@ -1,7 +1,5 @@
1
1
  from __future__ import annotations
2
2
 
3
- from unittest.mock import Mock
4
-
5
3
  from ophyd_async.core import (
6
4
  Device,
7
5
  DeviceConnector,
@@ -11,6 +9,7 @@ from ophyd_async.core import (
11
9
  SignalRW,
12
10
  SignalX,
13
11
  )
12
+ from ophyd_async.core._utils import LazyMock
14
13
 
15
14
  from ._epics_connector import fill_backend_with_prefix
16
15
  from ._signal import PvaSignalBackend, pvget_with_timeout
@@ -64,29 +63,29 @@ class PviDeviceConnector(DeviceConnector):
64
63
  backend.read_pv = read_pv
65
64
  backend.write_pv = write_pv
66
65
 
67
- async def connect(
68
- self, device: Device, mock: bool | Mock, timeout: float, force_reconnect: bool
66
+ async def connect_mock(self, device: Device, mock: LazyMock):
67
+ self.filler.create_device_vector_entries_to_mock(2)
68
+ # Set the name of the device to name all children
69
+ device.set_name(device.name)
70
+ return await super().connect_mock(device, mock)
71
+
72
+ async def connect_real(
73
+ self, device: Device, timeout: float, force_reconnect: bool
69
74
  ) -> None:
70
- if mock:
71
- # Make 2 entries for each DeviceVector
72
- self.filler.create_device_vector_entries_to_mock(2)
73
- else:
74
- pvi_structure = await pvget_with_timeout(self.pvi_pv, timeout)
75
- entries: dict[str, Entry | list[Entry | None]] = pvi_structure[
76
- "value"
77
- ].todict()
78
- # Fill based on what PVI gives us
79
- for name, entry in entries.items():
80
- if isinstance(entry, dict):
81
- # This is a child
82
- self._fill_child(name, entry)
83
- else:
84
- # This is a DeviceVector of children
85
- for i, e in enumerate(entry):
86
- if e:
87
- self._fill_child(name, e, i)
88
- # Check that all the requested children have been filled
89
- self.filler.check_filled(f"{self.pvi_pv}: {entries}")
75
+ pvi_structure = await pvget_with_timeout(self.pvi_pv, timeout)
76
+ entries: dict[str, Entry | list[Entry | None]] = pvi_structure["value"].todict()
77
+ # Fill based on what PVI gives us
78
+ for name, entry in entries.items():
79
+ if isinstance(entry, dict):
80
+ # This is a child
81
+ self._fill_child(name, entry)
82
+ else:
83
+ # This is a DeviceVector of children
84
+ for i, e in enumerate(entry):
85
+ if e:
86
+ self._fill_child(name, e, i)
87
+ # Check that all the requested children have been filled
88
+ self.filler.check_filled(f"{self.pvi_pv}: {entries}")
90
89
  # Set the name of the device to name all children
91
90
  device.set_name(device.name)
92
- return await super().connect(device, mock, timeout, force_reconnect)
91
+ return await super().connect_real(device, timeout, force_reconnect)
@@ -1,13 +1,11 @@
1
- from unittest.mock import Mock
2
-
3
1
  import bluesky.plan_stubs as bps
4
2
 
5
- from ophyd_async.core import DEFAULT_TIMEOUT, Device, wait_for_connection
3
+ from ophyd_async.core import DEFAULT_TIMEOUT, Device, LazyMock, wait_for_connection
6
4
 
7
5
 
8
6
  def ensure_connected(
9
7
  *devices: Device,
10
- mock: bool | Mock = False,
8
+ mock: bool | LazyMock = False,
11
9
  timeout: float = DEFAULT_TIMEOUT,
12
10
  force_reconnect=False,
13
11
  ):