Skip to content
Merged
Show file tree
Hide file tree
Changes from 6 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": str(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) -> Path:
"""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
-------
Path
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()
)