Skip to content

Commit c52a9a5

Browse files
feat(tidy3d): FXC-3722-better-file-path-handling
1 parent 4929337 commit c52a9a5

File tree

30 files changed

+642
-362
lines changed

30 files changed

+642
-362
lines changed

CHANGELOG.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -25,6 +25,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
2525
- Added `custom_source_time` parameter to `ComponentModeler` classes (`ModalComponentModeler` and `TerminalComponentModeler`), allowing specification of custom source time dependence.
2626
- Validation for `run_only` field in component modelers to catch duplicate or invalid matrix indices early with clear error messages.
2727
- Introduced a profile-based configuration manager with TOML persistence and runtime overrides exposed via `tidy3d.config`.
28+
- Added support of `os.PathLike` objects as paths like `pathlib.Path` alongside `str` paths in all path-related functions.
2829

2930
### Changed
3031
- Improved performance of antenna metrics calculation by utilizing cached wave amplitude calculations instead of recomputing wave amplitudes for each port excitation in the `TerminalComponentModelerData`.

tests/test_web/test_tidy3d_stub.py

Lines changed: 36 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,10 @@
11
from __future__ import annotations
22

33
import os
4+
from pathlib import Path
45

56
import numpy as np
7+
import pytest
68
import responses
79

810
import tidy3d as td
@@ -162,6 +164,40 @@ def test_stub_data_lazy_loading(tmp_path):
162164
_ = sim_data.monitor_data
163165

164166

167+
@pytest.mark.parametrize(
168+
"path_builder",
169+
(
170+
lambda tmp_path, name: Path(tmp_path) / name,
171+
lambda tmp_path, name: str(Path(tmp_path) / name),
172+
),
173+
)
174+
def test_stub_pathlike_roundtrip(tmp_path, path_builder):
175+
"""Ensure stub read/write helpers accept pathlib.Path and posixpath inputs."""
176+
177+
# Simulation stub roundtrip
178+
sim = make_sim()
179+
stub = Tidy3dStub(simulation=sim)
180+
sim_path = path_builder(tmp_path, "pathlike_sim.json")
181+
stub.to_file(sim_path)
182+
assert os.path.exists(sim_path)
183+
sim_loaded = Tidy3dStub.from_file(sim_path)
184+
assert sim_loaded == sim
185+
186+
# Simulation data stub roundtrip
187+
sim_data = make_sim_data()
188+
stub_data = Tidy3dStubData(data=sim_data)
189+
data_path = path_builder(tmp_path, "pathlike_data.hdf5")
190+
stub_data.to_file(data_path)
191+
assert os.path.exists(data_path)
192+
193+
data_loaded = Tidy3dStubData.from_file(data_path)
194+
assert data_loaded.simulation == sim_data.simulation
195+
196+
# Postprocess using the same PathLike ensures downstream helpers accept the type
197+
processed = Tidy3dStubData.postprocess(data_path, lazy=True)
198+
assert isinstance(processed, SimulationData)
199+
200+
165201
def test_default_task_name():
166202
sim = make_sim()
167203
stub = Tidy3dStub(simulation=sim)

tests/test_web/test_tidy3d_task.py

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -376,3 +376,16 @@ def mock(*args, **kwargs):
376376
task.get_log(LOG_FNAME)
377377
with open(LOG_FNAME) as f:
378378
assert f.read() == "0.3,5.7"
379+
380+
381+
@responses.activate
382+
def test_get_running_tasks(set_api_key):
383+
responses.add(
384+
responses.GET,
385+
f"{Env.current.web_api_endpoint}/tidy3d/py/tasks",
386+
json={"data": [{"taskId": "1234", "status": "queued"}]},
387+
status=200,
388+
)
389+
390+
tasks = SimulationTask.get_running_tasks()
391+
assert len(tasks) == 1

tests/test_web/test_webapi.py

Lines changed: 98 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,10 @@
22
from __future__ import annotations
33

44
import os
5+
import posixpath
56
from concurrent.futures import Future
7+
from os import PathLike
8+
from pathlib import Path
69
from types import SimpleNamespace
710

811
import numpy as np
@@ -739,8 +742,8 @@ def status(self):
739742
events.append((self.task_id, "status", status))
740743
return status
741744

742-
def download(self, path: str):
743-
events.append((self.task_id, "download", path))
745+
def download(self, path: PathLike):
746+
events.append((self.task_id, "download", str(path)))
744747

745748
monkeypatch.setattr("tidy3d.web.api.container.ThreadPoolExecutor", ImmediateExecutor)
746749
monkeypatch.setattr("tidy3d.web.api.container.time.sleep", lambda *_args, **_kwargs: None)
@@ -766,7 +769,7 @@ def download(self, path: str):
766769
}
767770

768771
for task_id, _, path in downloads:
769-
assert path == expected_paths[task_id]
772+
assert str(path) == expected_paths[task_id]
770773

771774
job1_download_idx = next(
772775
i
@@ -797,8 +800,8 @@ def status(self):
797800
events.append((self.task_id, "status", status))
798801
return status
799802

800-
def download(self, path: str):
801-
events.append((self.task_id, "download", path))
803+
def download(self, path: PathLike):
804+
events.append((self.task_id, "download", str(path)))
802805

803806
monkeypatch.setattr("tidy3d.web.api.container.ThreadPoolExecutor", ImmediateExecutor)
804807
monkeypatch.setattr("tidy3d.web.api.container.time.sleep", lambda *_args, **_kwargs: None)
@@ -819,6 +822,7 @@ def download(self, path: str):
819822
batch.monitor(download_on_success=True, path_dir=str(tmp_path))
820823

821824
downloads = [event for event in events if event[1] == "download"]
825+
822826
assert downloads == [("task_b_id", "download", os.path.join(str(tmp_path), "task_b_id.hdf5"))]
823827

824828

@@ -996,3 +1000,92 @@ def test_run_single_offline_eager(monkeypatch, tmp_path):
9961000

9971001
assert isinstance(sim_data, SimulationData)
9981002
assert sim_data.__class__.__name__ == "SimulationData" # no proxy
1003+
1004+
1005+
class FauxPath:
1006+
"""Minimal PathLike to exercise __fspath__ support."""
1007+
1008+
def __init__(self, path: PathLike | str):
1009+
self._p = os.fspath(path)
1010+
1011+
def __fspath__(self) -> str:
1012+
return self._p
1013+
1014+
1015+
def _pathlib_builder(tmp_path, name: str):
1016+
return Path(tmp_path) / name
1017+
1018+
1019+
def _posix_builder(tmp_path, name: str):
1020+
return posixpath.join(tmp_path.as_posix(), name)
1021+
1022+
1023+
def _str_builder(tmp_path, name: str):
1024+
return str(Path(tmp_path) / name)
1025+
1026+
1027+
def _fspath_builder(tmp_path, name: str):
1028+
return FauxPath(Path(tmp_path) / name)
1029+
1030+
1031+
@pytest.mark.parametrize(
1032+
"path_builder",
1033+
[_pathlib_builder, _posix_builder, _str_builder, _fspath_builder],
1034+
ids=["pathlib.Path", "posixpath_str", "str", "PathLike"],
1035+
)
1036+
def test_run_single_offline_eager_accepts_pathlikes(monkeypatch, tmp_path, path_builder):
1037+
"""run(sim, path=...) accepts any PathLike."""
1038+
sim = make_sim()
1039+
task_name = "pathlike_single"
1040+
out_file = path_builder(tmp_path, "sim.hdf5")
1041+
1042+
# Patch webapi for offline run and to write to the provided path
1043+
apply_common_patches(monkeypatch, tmp_path, taskid_to_sim={task_name: sim})
1044+
1045+
sim_data = run(sim, task_name=task_name, path=out_file)
1046+
1047+
# File existed (written via patched load) and types are correct
1048+
assert os.path.exists(os.fspath(out_file))
1049+
assert isinstance(sim_data, SimulationData)
1050+
assert sim_data.simulation == sim
1051+
1052+
1053+
@pytest.mark.parametrize(
1054+
"path_builder",
1055+
[_pathlib_builder, _posix_builder, _str_builder, _fspath_builder],
1056+
ids=["pathlib.Path", "posixpath_str", "str", "PathLike"],
1057+
)
1058+
def test_job_run_accepts_pathlikes(monkeypatch, tmp_path, path_builder):
1059+
"""Job.run(path=...) accepts any PathLike."""
1060+
sim = make_sim()
1061+
task_name = "job_pathlike"
1062+
out_file = path_builder(tmp_path, "job_out.hdf5")
1063+
1064+
apply_common_patches(monkeypatch, tmp_path, taskid_to_sim={task_name: sim})
1065+
1066+
j = Job(simulation=sim, task_name=task_name, folder_name=PROJECT_NAME)
1067+
_ = j.run(path=out_file)
1068+
1069+
assert os.path.exists(os.fspath(out_file))
1070+
1071+
1072+
@pytest.mark.parametrize(
1073+
"dir_builder",
1074+
[_pathlib_builder, _posix_builder, _str_builder, _fspath_builder],
1075+
ids=["pathlib.Path", "posixpath_str", "str", "PathLike"],
1076+
)
1077+
def test_batch_run_accepts_pathlike_dir(monkeypatch, tmp_path, dir_builder):
1078+
"""Batch.run(path_dir=...) accepts any PathLike directory location."""
1079+
sims = {"A": make_sim(), "B": make_sim()}
1080+
out_dir = dir_builder(tmp_path, "batch_out")
1081+
1082+
# Map task_ids to sims: upload() is patched to return task_name, which for dict input
1083+
# corresponds to the dict keys ("A", "B"), so we map those.
1084+
apply_common_patches(monkeypatch, tmp_path, taskid_to_sim={"A": sims["A"], "B": sims["B"]})
1085+
1086+
b = Batch(simulations=sims, folder_name=PROJECT_NAME)
1087+
b.run(path_dir=out_dir)
1088+
1089+
# Directory created and two .hdf5 outputs produced
1090+
out_dir_str = os.fspath(out_dir)
1091+
assert os.path.isdir(out_dir_str)

0 commit comments

Comments
 (0)