Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 3 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,9 @@ __pycache__/
.idea
.vscode

# direnv
.envrc

# Unit test / coverage reports
htmlcov/
.coverage
Expand Down
69 changes: 53 additions & 16 deletions RATapi/project.py
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@
import copy
import functools
import json
import warnings
from enum import Enum
from pathlib import Path
from textwrap import indent
Expand Down Expand Up @@ -835,17 +836,17 @@ def classlist_script(name, classlist):
+ "\n)"
)

def save(self, path: Union[str, Path], filename: str = "project"):
def save(self, filepath: Union[str, Path] = "./project.json"):
"""Save a project to a JSON file.

Parameters
----------
path : str or Path
The path in which the project will be written.
filename : str
The name of the generated project file.
filepath : str or Path
The path to where the project file will be written.

"""
filepath = Path(filepath).with_suffix(".json")

json_dict = {}
for field in self.model_fields:
attr = getattr(self, field)
Expand All @@ -869,7 +870,7 @@ def make_custom_file_dict(item):
"name": item.name,
"filename": item.filename,
"language": item.language,
"path": str(item.path),
"path": try_relative_to(item.path, filepath),
}

json_dict["custom_files"] = [make_custom_file_dict(file) for file in attr]
Expand All @@ -879,8 +880,7 @@ def make_custom_file_dict(item):
else:
json_dict[field] = attr

file = Path(path, f"{filename.removesuffix('.json')}.json")
file.write_text(json.dumps(json_dict))
filepath.write_text(json.dumps(json_dict))

@classmethod
def load(cls, path: Union[str, Path]) -> "Project":
Expand All @@ -892,15 +892,21 @@ def load(cls, path: Union[str, Path]) -> "Project":
The path to the project file.

"""
input = Path(path).read_text()
model_dict = json.loads(input)
for i in range(0, len(model_dict["data"])):
if model_dict["data"][i]["name"] == "Simulation":
model_dict["data"][i]["data"] = np.empty([0, 3])
del model_dict["data"][i]["data_range"]
path = Path(path)
input_data = path.read_text()
model_dict = json.loads(input_data)
for dataset in model_dict["data"]:
if dataset["name"] == "Simulation":
dataset["data"] = np.empty([0, 3])
del dataset["data_range"]
else:
data = model_dict["data"][i]["data"]
model_dict["data"][i]["data"] = np.array(data)
data = dataset["data"]
dataset["data"] = np.array(data)

# file paths are saved as relative to the project directory
for file in model_dict["custom_files"]:
if not Path(file["path"]).is_absolute():
file["path"] = Path(path, file["path"])

return cls.model_validate(model_dict)

Expand Down Expand Up @@ -943,3 +949,34 @@ def wrapped_func(*args, **kwargs):
return return_value

return wrapped_func


def try_relative_to(path: Path, relative_to: Path) -> str:
"""Attempt to create a relative path and warn the user if it isn't possible.

Parameters
----------
path : Path
The path to try to find a relative path for.
relative_to: Path
The path to which we find a relative path for ``path``.

Returns
-------
str
The relative path if successful, else the absolute path.

"""
path = Path(path)
relative_to = Path(relative_to)
if path.is_relative_to(relative_to):
return str(path.relative_to(relative_to))
else:
warnings.warn(
"Could not save custom file path as relative to the project directory, "
"which means that it may not work on other devices."
"If you would like to share your project, make sure your custom files "
"are in a subfolder of the project save location.",
stacklevel=2,
)
return str(path.resolve())
40 changes: 38 additions & 2 deletions tests/test_project.py
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@

import copy
import tempfile
import warnings
from pathlib import Path
from typing import Callable

Expand Down Expand Up @@ -1556,8 +1557,43 @@ def test_save_load(project, request):
original_project = request.getfixturevalue(project)

with tempfile.TemporaryDirectory() as tmp:
original_project.save(tmp)
converted_project = RATapi.Project.load(Path(tmp, "project.json"))
# ignore relative path warnings
path = Path(tmp, "project.json")
with warnings.catch_warnings():
warnings.simplefilter("ignore")
original_project.save(path)
converted_project = RATapi.Project.load(path)

# resolve custom files in case the original project had unresolvable relative paths
for file in original_project.custom_files:
file.path = file.path.resolve()

for field in RATapi.Project.model_fields:
assert getattr(converted_project, field) == getattr(original_project, field)


def test_relative_paths():
"""Test that ``try_relative_to`` correctly creates relative paths to subfolders."""

with tempfile.TemporaryDirectory() as tmp:
data_path = Path(tmp, "data/myfile.dat")

assert Path(RATapi.project.try_relative_to(data_path, tmp)) == Path("data/myfile.dat")


def test_relative_paths_warning():
"""Test that we get a warning for trying to walk up paths."""

data_path = "/tmp/project/data/mydata.dat"
relative_path = "/tmp/project/project_path/myproj.dat"

with pytest.warns(
match="Could not save custom file path as relative to the project directory, "
"which means that it may not work on other devices."
"If you would like to share your project, make sure your custom files "
"are in a subfolder of the project save location.",
):
assert (
Path(RATapi.project.try_relative_to(data_path, relative_path))
== Path("/tmp/project/data/mydata.dat").resolve()
)