Skip to content

Commit 57b110d

Browse files
Bugfiles for v0.23.0 (#724)
- The recursive machine config update did not work for dictionaries as this caused in-place modification of the memory location of the general config - The destination selection needs to not run with string None as this causes problems if the directory is root and shorter than length 4 --------- Co-authored-by: Eu Pin Tien <eu-pin.tien@diamond.ac.uk>
1 parent 58b66ba commit 57b110d

File tree

6 files changed

+162
-24
lines changed

6 files changed

+162
-24
lines changed

src/murfey/client/analyser.py

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -398,7 +398,9 @@ def _xml_file(self, data_file: Path) -> Path:
398398
base_dir, mid_dir = find_longest_data_directory(data_file, data_directories)
399399
if not base_dir:
400400
return data_file.with_suffix(".xml")
401-
return base_dir / self._environment.visit / mid_dir / file_name
401+
# Add the visit directory to the file path and return it
402+
# The file is moved from a location where the visit name is not part of its path
403+
return base_dir / self._environment.visit / (mid_dir or "") / file_name
402404

403405
def enqueue(self, rsyncer: RSyncerUpdate):
404406
if not self._stopping and rsyncer.outcome == TransferResult.SUCCESS:

src/murfey/client/contexts/spa.py

Lines changed: 6 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,7 @@
33
import logging
44
from itertools import count
55
from pathlib import Path
6-
from typing import Any, Dict, List, Optional, OrderedDict, Tuple
6+
from typing import Any, Optional, OrderedDict
77

88
import xmltodict
99

@@ -50,10 +50,10 @@ def _file_transferred_to(
5050

5151

5252
def _grid_square_metadata_file(
53-
f: Path, data_directories: List[Path], visit: str, grid_square: int
53+
f: Path, data_directories: list[Path], visit: str, grid_square: int
5454
) -> Path:
5555
base_dir, mid_dir = find_longest_data_directory(f, data_directories)
56-
if not base_dir:
56+
if not base_dir or not mid_dir:
5757
raise ValueError(f"Could not determine grid square metadata path for {f}")
5858
metadata_file = (
5959
base_dir
@@ -113,7 +113,7 @@ def __init__(self, acquisition_software: str, basepath: Path, token: str):
113113
super().__init__("SPA", acquisition_software, token)
114114
self._basepath = basepath
115115
self._processing_job_stash: dict = {}
116-
self._foil_holes: Dict[int, List[int]] = {}
116+
self._foil_holes: dict[int, list[int]] = {}
117117

118118
def gather_metadata(
119119
self, metadata_file: Path, environment: MurfeyInstanceEnvironment | None = None
@@ -287,7 +287,7 @@ def _position_analysis(
287287
and self._foil_holes.get(grid_square) is None
288288
):
289289
self._foil_holes[grid_square] = []
290-
gs_pix_position: Tuple[
290+
gs_pix_position: tuple[
291291
Optional[int],
292292
Optional[int],
293293
Optional[float],
@@ -586,7 +586,7 @@ def _register_processing_job(
586586
self,
587587
tag: str,
588588
environment: MurfeyInstanceEnvironment,
589-
parameters: Dict[str, Any] | None = None,
589+
parameters: dict[str, Any] | None = None,
590590
):
591591
return
592592

src/murfey/client/destinations.py

Lines changed: 6 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -15,17 +15,19 @@
1515

1616
def find_longest_data_directory(
1717
match_path: Path, data_directories: list[str] | list[Path]
18-
):
18+
) -> tuple[Path | None, Path | None]:
1919
"""
2020
Determine the longest path in the data_directories list
2121
which the match path is relative to
2222
"""
2323
base_dir: Path | None = None
2424
mid_dir: Path | None = None
2525
for dd in data_directories:
26-
dd_base = str(Path(dd).absolute())
27-
if str(match_path).startswith(str(dd)) and len(dd_base) > len(str(base_dir)):
28-
base_dir = Path(dd_base)
26+
dd_base = Path(dd).absolute()
27+
if match_path.absolute().is_relative_to(dd_base) and len(dd_base.parts) > (
28+
len(base_dir.parts) if base_dir else 0
29+
):
30+
base_dir = dd_base
2931
mid_dir = match_path.absolute().relative_to(Path(base_dir)).parent
3032
return base_dir, mid_dir
3133

src/murfey/util/config.py

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
11
from __future__ import annotations
22

3+
import copy
34
import os
45
import socket
56
from functools import lru_cache
@@ -183,7 +184,7 @@ def _recursive_update(base: dict[str, Any], new: dict[str, Any]):
183184
base[key].extend(value)
184185
# Otherwise, overwrite/add values as normal
185186
else:
186-
base[key] = value
187+
base[key] = copy.deepcopy(value)
187188
return base
188189

189190
# Load the dict from the file

tests/client/test_destinations.py

Lines changed: 108 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2,8 +2,65 @@
22
from unittest import mock
33

44
import pytest
5+
from pytest_mock import MockerFixture
6+
7+
from murfey.client.destinations import (
8+
determine_default_destination,
9+
find_longest_data_directory,
10+
)
11+
12+
13+
@pytest.mark.parametrize(
14+
"test_params",
15+
(
16+
# File to match | Use Path? | Expected result
17+
(
18+
"X:/cm12345-6/Supervisor/Images-Disc1/file",
19+
True,
20+
("X:/", "cm12345-6/Supervisor/Images-Disc1"),
21+
),
22+
(
23+
"X:/DATA/cm12345-6/Supervisor/Images-Disc1/file",
24+
False,
25+
("X:/DATA", "cm12345-6/Supervisor/Images-Disc1"),
26+
),
27+
(
28+
"X:/DoseFractions/cm12345-6/Supervisor/Images-Disc1/file",
29+
True,
30+
("X:/DoseFractions", "cm12345-6/Supervisor/Images-Disc1"),
31+
),
32+
(
33+
"X:/DoseFractions/DATA/cm12345-6/Supervisor/Images-Disc1/file",
34+
False,
35+
("X:/DoseFractions/DATA", "cm12345-6/Supervisor/Images-Disc1"),
36+
),
37+
),
38+
)
39+
def test_find_longest_data_directory(
40+
mocker: MockerFixture, test_params: tuple[str, bool, tuple[str, str]]
41+
):
42+
# Unpack test params
43+
match_path, use_path, (expected_base_dir, expected_mid_dir) = test_params
44+
45+
# Construct data directories using strings or Paths as needed
46+
data_directories: list[str] | list[Path] = [
47+
"X:",
48+
"X:/DATA",
49+
"X:/DoseFractions",
50+
"X:/DoseFractions/DATA",
51+
]
52+
if use_path:
53+
data_directories = [Path(dd) for dd in data_directories]
54+
# Patch Pathlib's 'absolute()' function, since we are simulating Windows on Linux
55+
mocker.patch("murfey.client.destinations.Path.absolute", lambda self: self)
56+
57+
base_dir, mid_dir = find_longest_data_directory(
58+
Path(match_path),
59+
data_directories,
60+
)
61+
assert base_dir == Path(expected_base_dir)
62+
assert mid_dir == Path(expected_mid_dir)
563

6-
from murfey.client.destinations import determine_default_destination
764

865
source_list = [
966
["X:/DoseFractions/cm12345-6/Supervisor", "Supervisor", True, "extra_name"],
@@ -71,6 +128,56 @@ def test_determine_default_destinations_suggested_path(mock_post, mock_get, sour
71128
)
72129

73130

131+
@mock.patch("murfey.client.destinations.capture_get")
132+
@mock.patch("murfey.client.destinations.capture_post")
133+
def test_determine_default_destinations_short_path(mock_post, mock_get):
134+
"""Test for the case where the data directory is a drive"""
135+
mock_environment = mock.Mock()
136+
mock_environment.murfey_session = 2
137+
mock_environment.instrument_name = "m01"
138+
mock_environment.destination_registry = {}
139+
140+
mock_get().json.return_value = {"data_directories": ["X:/"]}
141+
mock_post().json.return_value = {"suggested_path": "/base_path/2025/cm12345-6/raw"}
142+
143+
destination = determine_default_destination(
144+
visit="cm12345-6",
145+
source=Path("X:/cm12345-6/Supervisor"),
146+
destination="2025",
147+
environment=mock_environment,
148+
token="token",
149+
touch=True,
150+
extra_directory="",
151+
use_suggested_path=True,
152+
)
153+
mock_get.assert_any_call(
154+
base_url=str(mock_environment.url.geturl()),
155+
router_name="session_control.router",
156+
function_name="machine_info_by_instrument",
157+
token="token",
158+
instrument_name="m01",
159+
)
160+
mock_post.assert_any_call(
161+
base_url=str(mock_environment.url.geturl()),
162+
router_name="file_io_instrument.router",
163+
function_name="suggest_path",
164+
token="token",
165+
visit_name="cm12345-6",
166+
session_id=2,
167+
data={
168+
"base_path": "2025/cm12345-6/raw",
169+
"touch": True,
170+
"extra_directory": "",
171+
},
172+
)
173+
174+
assert destination == "/base_path/2025/cm12345-6/raw/"
175+
assert (
176+
mock_environment.destination_registry.get("Supervisor")
177+
== "/base_path/2025/cm12345-6/raw"
178+
)
179+
180+
74181
@mock.patch("murfey.client.destinations.capture_get")
75182
@pytest.mark.parametrize("sources", source_list)
76183
def test_determine_default_destinations_skip_suggested(mock_get, sources):

tests/util/test_config.py

Lines changed: 37 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,4 @@
1+
import copy
12
from pathlib import Path
23
from typing import Any
34

@@ -132,19 +133,31 @@ def mock_instrument_config():
132133
}
133134

134135

136+
@pytest.fixture
137+
def mock_second_instrument_config(mock_instrument_config: dict[str, Any]):
138+
"""Test the case of recursive dictionaries which are present
139+
in both the general and instrument configs"""
140+
second_instrument_config = copy.deepcopy(mock_instrument_config)
141+
second_instrument_config.update(
142+
{"pkg_1": {"file_path": "/path/to/pkg_1/file_2.txt"}}
143+
)
144+
return second_instrument_config
145+
146+
135147
@pytest.fixture
136148
def mock_hierarchical_machine_config_yaml(
137149
mock_general_config: dict[str, Any],
138150
mock_tem_shared_config: dict[str, Any],
139151
mock_instrument_config: dict[str, Any],
152+
mock_second_instrument_config: dict[str, Any],
140153
tmp_path: Path,
141154
):
142155
# Create machine config (with all currently supported keys) for the instrument
143156
hierarchical_config = {
144157
"general": mock_general_config,
145158
"tem": mock_tem_shared_config,
146159
"m01": mock_instrument_config,
147-
"m02": mock_instrument_config,
160+
"m02": mock_second_instrument_config,
148161
}
149162
config_file = tmp_path / "config" / "murfey-machine-config-hierarchical.yaml"
150163
config_file.parent.mkdir(parents=True, exist_ok=True)
@@ -210,6 +223,7 @@ def test_get_machine_config(
210223
mock_general_config: dict[str, Any],
211224
mock_tem_shared_config: dict[str, Any],
212225
mock_instrument_config: dict[str, Any],
226+
mock_second_instrument_config: dict[str, Any],
213227
mock_hierarchical_machine_config_yaml: Path,
214228
mock_standard_machine_config_yaml: Path,
215229
test_params: tuple[str, list[str]],
@@ -353,14 +367,26 @@ def test_get_machine_config(
353367
== mock_instrument_config["node_creator_queue"]
354368
)
355369
# Extra keys
356-
assert config[i].pkg_1 == {
357-
"file_path": "/path/to/pkg_1/file.txt",
358-
"command": [
359-
"/path/to/executable",
360-
"--some_arg",
361-
"-a",
362-
"./path/to/file",
363-
],
364-
"step_size": 100,
365-
}
370+
if config_to_test == "standard" or i == "m01":
371+
assert config[i].pkg_1 == {
372+
"file_path": "/path/to/pkg_1/file.txt",
373+
"command": [
374+
"/path/to/executable",
375+
"--some_arg",
376+
"-a",
377+
"./path/to/file",
378+
],
379+
"step_size": 100,
380+
}
381+
elif i == "m02":
382+
assert config[i].pkg_1 == {
383+
"file_path": "/path/to/pkg_1/file_2.txt",
384+
"command": [
385+
"/path/to/executable",
386+
"--some_arg",
387+
"-a",
388+
"./path/to/file",
389+
],
390+
"step_size": 100,
391+
}
366392
assert config[i].pkg_2 == mock_general_config["pkg_2"]

0 commit comments

Comments
 (0)