Skip to content

Commit 01646df

Browse files
authored
fix: Make the path provider stack start docs (#1251)
Makes the `StartDocumentPathProvider` stack docs, using the latest for path determination.
1 parent 77129d1 commit 01646df

File tree

2 files changed

+117
-63
lines changed

2 files changed

+117
-63
lines changed

src/blueapi/utils/path_provider.py

Lines changed: 23 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -17,19 +17,21 @@ class StartDocumentPathProvider(PathProvider):
1717
"""
1818

1919
def __init__(self) -> None:
20-
self._doc: RunStart | None = None
20+
self._docs: list[RunStart] = []
2121

2222
def run_start(self, name: str, start_document: RunStart) -> None:
23-
if name == "start" and self._doc is None:
24-
self._doc = start_document
23+
if name == "start":
24+
self._docs.append(start_document)
2525

2626
def run_stop(self, name: str, stop_document: RunStop) -> None:
27-
if (
28-
name == "stop"
29-
and self._doc is not None
30-
and stop_document.get("run_start") == self._doc["uid"]
31-
):
32-
self._doc = None
27+
if name == "stop":
28+
if stop_document.get("run_start") == self._docs[-1]["uid"]:
29+
self._docs.pop()
30+
else:
31+
raise BlueskyRunStructureError(
32+
"Close run called, but not for the inner most run. "
33+
"This is not supported. If you need to do this speak to core DAQ."
34+
)
3335

3436
def __call__(self, device_name: str | None = None) -> PathInfo:
3537
"""Returns the directory path and filename for a given data_session.
@@ -40,14 +42,21 @@ def __call__(self, device_name: str | None = None) -> PathInfo:
4042
4143
If you do not provide a data_session_directory it will default to "/tmp".
4244
"""
43-
if self._doc is None:
44-
raise AttributeError(
45+
if not self._docs:
46+
raise BlueskyRunStructureError(
4547
"Start document not found. This call must be made inside a run."
4648
)
4749
else:
48-
template = self._doc.get("data_file_path_template", DEFAULT_TEMPLATE)
49-
sub_path = template.format_map(self._doc | {"device_name": device_name})
50+
template = self._docs[-1].get("data_file_path_template", DEFAULT_TEMPLATE)
51+
sub_path = template.format_map(
52+
self._docs[-1] | {"device_name": device_name}
53+
)
5054
data_session_directory = Path(
51-
self._doc.get("data_session_directory", "/tmp")
55+
self._docs[-1].get("data_session_directory", "/tmp")
5256
)
5357
return PathInfo(directory_path=data_session_directory, filename=sub_path)
58+
59+
60+
class BlueskyRunStructureError(Exception):
61+
def __init__(self, message):
62+
super().__init__(message)

tests/unit_tests/utils/test_path_provider.py

Lines changed: 94 additions & 49 deletions
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,10 @@
44
from event_model import RunStart, RunStop
55
from ophyd_async.core import PathInfo
66

7-
from blueapi.utils.path_provider import StartDocumentPathProvider
7+
from blueapi.utils.path_provider import (
8+
BlueskyRunStructureError,
9+
StartDocumentPathProvider,
10+
)
811

912

1013
@pytest.fixture
@@ -217,7 +220,7 @@ def test_start_document_path_provider_run_start_called_with_different_document_s
217220
pp = StartDocumentPathProvider()
218221
pp.run_start(name="stop", start_document=stop_doc_default_template) # type: ignore
219222

220-
assert pp._doc is None
223+
assert pp._docs == []
221224

222225

223226
def test_start_document_path_provider_run_stop_called_with_different_document_skips(
@@ -226,18 +229,18 @@ def test_start_document_path_provider_run_stop_called_with_different_document_sk
226229
pp = StartDocumentPathProvider()
227230
pp.run_stop(name="start", stop_document=start_doc_default_template) # type: ignore
228231

229-
assert pp._doc is None
232+
assert pp._docs == []
230233

231234

232235
@pytest.fixture
233-
def start_doc() -> dict:
236+
def start_doc_1() -> dict:
234237
return {
235238
"uid": "fa2feced-4098-4c0e-869d-285d2a69c24a",
236239
"time": 1690463918.3893268,
237240
"versions": {"ophyd": "1.10.0", "bluesky": "1.13"},
238241
"data_session": "ab123",
239-
"instrument": "p02",
240-
"data_session_directory": "/p02/ab123",
242+
"instrument": "p01",
243+
"data_session_directory": "/p01/ab123",
241244
"scan_id": 50,
242245
"plan_type": "generator",
243246
"plan_name": "count",
@@ -257,7 +260,7 @@ def start_doc() -> dict:
257260

258261

259262
@pytest.fixture
260-
def stop_doc() -> dict:
263+
def stop_doc_1() -> dict:
261264
return {
262265
"run_start": "fa2feced-4098-4c0e-869d-285d2a69c24a",
263266
"time": 1690463920.3893268,
@@ -268,68 +271,110 @@ def stop_doc() -> dict:
268271
}
269272

270273

271-
def test_start_document_path_provider_start_doc_persists_until_stop_with_matching_id(
272-
start_doc: RunStart,
273-
stop_doc: RunStop,
274-
start_doc_default_template: RunStart,
275-
stop_doc_default_template: RunStop,
276-
):
277-
pp = StartDocumentPathProvider()
278-
pp.run_start(name="start", start_document=start_doc)
279-
280-
assert pp._doc == start_doc
281-
assert pp._doc["uid"] == "fa2feced-4098-4c0e-869d-285d2a69c24a" # type: ignore
282-
283-
pp.run_start(name="start", start_document=start_doc_default_template)
284-
assert pp._doc == start_doc
285-
assert pp._doc["uid"] == "fa2feced-4098-4c0e-869d-285d2a69c24a" # type: ignore
274+
@pytest.fixture
275+
def start_doc_2() -> dict:
276+
return {
277+
"uid": "27c48d2f-d8c6-4ac0-8146-fedf467ce11f",
278+
"time": 1690463918.3893268,
279+
"versions": {"ophyd": "1.10.0", "bluesky": "1.13"},
280+
"data_session": "ab123",
281+
"instrument": "p02",
282+
"data_session_directory": "/p02/ab123",
283+
"scan_id": 51,
284+
"plan_type": "generator",
285+
"plan_name": "count",
286+
"detectors": ["det"],
287+
"num_points": 1,
288+
"num_intervals": 0,
289+
"plan_args": {
290+
"detectors": [
291+
"<ophyd_async.epics.adaravis._aravis.AravisDetector object at 0x7f74c02b8710>" # NOQA: E501
292+
],
293+
"num": 1,
294+
"delay": 0.0,
295+
},
296+
"hints": {"dimensions": [[["time"], "primary"]]},
297+
"shape": [1],
298+
}
286299

287-
pp.run_stop(name="stop", stop_document=stop_doc_default_template)
288-
assert pp._doc == start_doc
289-
assert pp._doc["uid"] == "fa2feced-4098-4c0e-869d-285d2a69c24a" # type: ignore
290300

291-
pp.run_stop(name="stop", stop_document=stop_doc)
292-
assert pp._doc is None
301+
@pytest.fixture
302+
def stop_doc_2() -> dict:
303+
return {
304+
"run_start": "27c48d2f-d8c6-4ac0-8146-fedf467ce11f",
305+
"time": 1690463920.3893268,
306+
"uid": "401ad197-5456-4a7d-ba5b-9cf8ad38d914",
307+
"exit_status": "success",
308+
"reason": "",
309+
"num_events": {"primary": 1},
310+
}
293311

294312

295-
def test_start_document_path_provider_nested_runs_use_parent_run_info(
296-
start_doc_default_template: RunStart,
297-
stop_doc_default_template: RunStop,
298-
start_doc: RunStart,
299-
stop_doc: RunStop,
313+
def test_start_document_path_provider_nested_runs_use_info_from_last_start_doc(
314+
start_doc_1: RunStart,
315+
stop_doc_1: RunStop,
316+
start_doc_2: RunStart,
317+
stop_doc_2: RunStop,
300318
):
301319
pp = StartDocumentPathProvider()
302-
pp.run_start(name="start", start_document=start_doc_default_template)
303-
parent_path_info = pp("det")
304320

305-
assert pp._doc == start_doc_default_template
306-
assert pp._doc["uid"] == "27c48d2f-d8c6-4ac0-8146-fedf467ce11f" # type: ignore
307-
assert parent_path_info == PathInfo(
321+
pp.run_start(name="start", start_document=start_doc_1)
322+
start_doc_1_path_info = PathInfo(
308323
directory_path=PosixPath("/p01/ab123"),
309-
filename="det-p01-22",
324+
filename="det-p01-50",
310325
create_dir_depth=0,
311326
)
312327

313-
pp.run_start(name="start", start_document=start_doc)
314-
assert pp._doc == start_doc_default_template
315-
assert pp._doc["uid"] == "27c48d2f-d8c6-4ac0-8146-fedf467ce11f" # type: ignore
328+
assert pp._docs[-1] == start_doc_1
329+
assert pp._docs[-1]["uid"] == "fa2feced-4098-4c0e-869d-285d2a69c24a"
330+
assert pp("det") == start_doc_1_path_info
331+
332+
pp.run_start(name="start", start_document=start_doc_2)
333+
start_doc_2_path_info = PathInfo(
334+
directory_path=PosixPath("/p02/ab123"),
335+
filename="det-p02-51",
336+
create_dir_depth=0,
337+
)
338+
339+
assert pp._docs[-1] == start_doc_2
340+
assert pp._docs[-1]["uid"] == "27c48d2f-d8c6-4ac0-8146-fedf467ce11f"
341+
assert pp("det") == start_doc_2_path_info
342+
343+
assert pp._docs[-2] == start_doc_1
344+
assert pp._docs[-2]["uid"] == "fa2feced-4098-4c0e-869d-285d2a69c24a"
316345

317-
assert pp("det") == parent_path_info
346+
pp.run_stop(name="stop", stop_document=stop_doc_2)
318347

319-
pp.run_stop(name="stop", stop_document=stop_doc)
320-
assert pp._doc == start_doc_default_template
321-
assert pp._doc["uid"] == "27c48d2f-d8c6-4ac0-8146-fedf467ce11f" # type: ignore
348+
assert pp._docs[-1] == start_doc_1
349+
assert pp._docs[-1]["uid"] == "fa2feced-4098-4c0e-869d-285d2a69c24a"
322350

323-
assert pp("det") == parent_path_info
351+
assert pp("det") == start_doc_1_path_info
324352

325-
pp.run_stop(name="stop", stop_document=stop_doc_default_template)
326-
assert pp._doc is None
353+
pp.run_stop(name="stop", stop_document=stop_doc_1)
354+
assert pp._docs == []
327355

328356

329357
def test_start_document_path_provider_called_without_start_raises():
330358
pp = StartDocumentPathProvider()
331359
with pytest.raises(
332-
AttributeError,
360+
BlueskyRunStructureError,
333361
match="Start document not found. This call must be made inside a run.",
334362
):
335363
pp("det")
364+
365+
366+
def test_start_document_path_provider_run_stop_called_out_of_order_raises(
367+
start_doc_1: RunStart,
368+
stop_doc_1: RunStop,
369+
start_doc_2: RunStart,
370+
):
371+
pp = StartDocumentPathProvider()
372+
pp.run_start(name="start", start_document=start_doc_1)
373+
pp.run_start(name="start", start_document=start_doc_2)
374+
375+
with pytest.raises(
376+
BlueskyRunStructureError,
377+
match="Close run called, but not for the inner most run. "
378+
"This is not supported. If you need to do this speak to core DAQ.",
379+
):
380+
pp.run_stop(name="stop", stop_document=stop_doc_1)

0 commit comments

Comments
 (0)