Skip to content

Commit cb5e6b6

Browse files
committed
paths are now saved as relative to the project directory
1 parent bbe7868 commit cb5e6b6

File tree

2 files changed

+98
-13
lines changed

2 files changed

+98
-13
lines changed

RATapi/project.py

Lines changed: 62 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -4,8 +4,10 @@
44
import copy
55
import functools
66
import json
7+
import warnings
78
from enum import Enum
89
from pathlib import Path
10+
from sys import version_info
911
from textwrap import indent
1012
from typing import Annotated, Any, Callable, Union
1113

@@ -835,17 +837,18 @@ def classlist_script(name, classlist):
835837
+ "\n)"
836838
)
837839

838-
def save(self, path: Union[str, Path], filename: str = "project"):
840+
def save(self, filepath: Union[str, Path], filename: str = "project"):
839841
"""Save a project to a JSON file.
840842
841843
Parameters
842844
----------
843-
path : str or Path
845+
filepath : str or Path
844846
The path in which the project will be written.
845847
filename : str
846848
The name of the generated project file.
847849
848850
"""
851+
filepath = Path(filepath)
849852
json_dict = {}
850853
for field in self.model_fields:
851854
attr = getattr(self, field)
@@ -869,7 +872,7 @@ def make_custom_file_dict(item):
869872
"name": item.name,
870873
"filename": item.filename,
871874
"language": item.language,
872-
"path": str(item.path),
875+
"path": str(try_relative_to(item.path, filepath)),
873876
}
874877

875878
json_dict["custom_files"] = [make_custom_file_dict(file) for file in attr]
@@ -879,7 +882,7 @@ def make_custom_file_dict(item):
879882
else:
880883
json_dict[field] = attr
881884

882-
file = Path(path, f"{filename.removesuffix('.json')}.json")
885+
file = Path(filepath, f"{filename.removesuffix('.json')}.json")
883886
file.write_text(json.dumps(json_dict))
884887

885888
@classmethod
@@ -892,15 +895,21 @@ def load(cls, path: Union[str, Path]) -> "Project":
892895
The path to the project file.
893896
894897
"""
895-
input = Path(path).read_text()
896-
model_dict = json.loads(input)
897-
for i in range(0, len(model_dict["data"])):
898-
if model_dict["data"][i]["name"] == "Simulation":
899-
model_dict["data"][i]["data"] = np.empty([0, 3])
900-
del model_dict["data"][i]["data_range"]
898+
path = Path(path)
899+
input_data = path.read_text()
900+
model_dict = json.loads(input_data)
901+
for dataset in model_dict["data"]:
902+
if dataset["name"] == "Simulation":
903+
dataset["data"] = np.empty([0, 3])
904+
del dataset["data_range"]
901905
else:
902-
data = model_dict["data"][i]["data"]
903-
model_dict["data"][i]["data"] = np.array(data)
906+
data = dataset["data"]
907+
dataset["data"] = np.array(data)
908+
909+
# file paths are saved as relative to the project directory
910+
for file in model_dict["custom_files"]:
911+
if not Path(file["path"]).is_absolute():
912+
file["path"] = Path(path, file["path"])
904913

905914
return cls.model_validate(model_dict)
906915

@@ -943,3 +952,44 @@ def wrapped_func(*args, **kwargs):
943952
return return_value
944953

945954
return wrapped_func
955+
956+
957+
def try_relative_to(path: Path, relative_to: Path) -> Path:
958+
"""Attempt to create a relative path and warn the user if it isn't possible.
959+
960+
Parameters
961+
----------
962+
path : Path
963+
The path to try to find a relative path for.
964+
relative_to: Path
965+
The path to which we find a relative path for ``path``.
966+
967+
Returns
968+
-------
969+
Path
970+
The relative path if successful, else the absolute path.
971+
972+
"""
973+
# we use the absolute paths to resolve symlinks and so on
974+
abs_path = Path(path).resolve()
975+
abs_base = Path(relative_to).resolve()
976+
977+
# 'walking up' paths is only added in Python 3.12
978+
if version_info.minor < 12:
979+
try:
980+
relative_path = abs_path.relative_to(abs_base)
981+
except ValueError as err:
982+
warnings.warn("Could not save a custom file path as relative to the project directory. "
983+
"This may mean the project may not open on other devices. "
984+
f"Error message: {err}", stacklevel=2)
985+
return abs_path
986+
else:
987+
try:
988+
relative_path = abs_path.relative_to(abs_base, walk_up = True)
989+
except ValueError as err:
990+
warnings.warn("Could not save a custom file path as relative to the project directory. "
991+
"This may mean the project may not open on other devices. "
992+
f"Error message: {err}", stacklevel=2)
993+
return abs_path
994+
995+
return relative_path

tests/test_project.py

Lines changed: 36 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,9 @@
22

33
import copy
44
import tempfile
5+
import warnings
56
from pathlib import Path
7+
from sys import version_info
68
from typing import Callable
79

810
import numpy as np
@@ -1556,8 +1558,41 @@ def test_save_load(project, request):
15561558
original_project = request.getfixturevalue(project)
15571559

15581560
with tempfile.TemporaryDirectory() as tmp:
1559-
original_project.save(tmp)
1561+
# ignore relative path warnings
1562+
with warnings.catch_warnings():
1563+
warnings.simplefilter("ignore")
1564+
original_project.save(tmp)
15601565
converted_project = RATapi.Project.load(Path(tmp, "project.json"))
15611566

1567+
# resolve custom files in case the original project had unresolvable relative paths
1568+
for file in original_project.custom_files:
1569+
file.path = file.path.resolve()
1570+
15621571
for field in RATapi.Project.model_fields:
15631572
assert getattr(converted_project, field) == getattr(original_project, field)
1573+
1574+
1575+
def test_relative_paths():
1576+
"""Test that ``try_relative_to`` correctly creates relative paths."""
1577+
1578+
with tempfile.TemporaryDirectory() as tmp:
1579+
data_path = Path(tmp, "data/myfile.dat")
1580+
1581+
assert RATapi.project.try_relative_to(data_path, tmp) == Path("./data/myfile.dat")
1582+
1583+
1584+
def test_relative_paths_version():
1585+
"""Test that we only walk up paths on Python 3.12 or greater."""
1586+
1587+
data_path = "/tmp/project/data/mydata.dat"
1588+
relative_path = "/tmp/project/project_path/myproj.dat"
1589+
1590+
if version_info.minor >= 12:
1591+
with warnings.catch_warnings():
1592+
warnings.simplefilter("error")
1593+
assert RATapi.project.try_relative_to(data_path, relative_path) == Path("../../data/mydata.dat")
1594+
else:
1595+
with pytest.warns(match="Could not save a custom file path as relative to the project directory. "
1596+
"This may mean the project may not open on other devices. "
1597+
"Error message:"):
1598+
assert RATapi.project.try_relative_to(data_path, relative_path) == Path("/tmp/project/data/mydata.dat")

0 commit comments

Comments
 (0)