Skip to content

Commit 148d0d9

Browse files
authored
Improves Project.save (RascalSoftware#127)
* added envrc to gitignore * paths are now saved as relative to the project directory * save now just has one filename parameter * removed walkup as it is unstable * simplified * portable tests * review fixes
1 parent 58d1141 commit 148d0d9

File tree

3 files changed

+94
-18
lines changed

3 files changed

+94
-18
lines changed

.gitignore

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,9 @@ __pycache__/
77
.idea
88
.vscode
99

10+
# direnv
11+
.envrc
12+
1013
# Unit test / coverage reports
1114
htmlcov/
1215
.coverage

RATapi/project.py

Lines changed: 53 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@
44
import copy
55
import functools
66
import json
7+
import warnings
78
from enum import Enum
89
from pathlib import Path
910
from textwrap import indent
@@ -835,17 +836,17 @@ def classlist_script(name, classlist):
835836
+ "\n)"
836837
)
837838

838-
def save(self, path: Union[str, Path], filename: str = "project"):
839+
def save(self, filepath: Union[str, Path] = "./project.json"):
839840
"""Save a project to a JSON file.
840841
841842
Parameters
842843
----------
843-
path : str or Path
844-
The path in which the project will be written.
845-
filename : str
846-
The name of the generated project file.
844+
filepath : str or Path
845+
The path to where the project file will be written.
847846
848847
"""
848+
filepath = Path(filepath).with_suffix(".json")
849+
849850
json_dict = {}
850851
for field in self.model_fields:
851852
attr = getattr(self, field)
@@ -869,7 +870,7 @@ def make_custom_file_dict(item):
869870
"name": item.name,
870871
"filename": item.filename,
871872
"language": item.language,
872-
"path": str(item.path),
873+
"path": try_relative_to(item.path, filepath),
873874
}
874875

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

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

885885
@classmethod
886886
def load(cls, path: Union[str, Path]) -> "Project":
@@ -892,15 +892,21 @@ def load(cls, path: Union[str, Path]) -> "Project":
892892
The path to the project file.
893893
894894
"""
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"]
895+
path = Path(path)
896+
input_data = path.read_text()
897+
model_dict = json.loads(input_data)
898+
for dataset in model_dict["data"]:
899+
if dataset["name"] == "Simulation":
900+
dataset["data"] = np.empty([0, 3])
901+
del dataset["data_range"]
901902
else:
902-
data = model_dict["data"][i]["data"]
903-
model_dict["data"][i]["data"] = np.array(data)
903+
data = dataset["data"]
904+
dataset["data"] = np.array(data)
905+
906+
# file paths are saved as relative to the project directory
907+
for file in model_dict["custom_files"]:
908+
if not Path(file["path"]).is_absolute():
909+
file["path"] = Path(path, file["path"])
904910

905911
return cls.model_validate(model_dict)
906912

@@ -943,3 +949,34 @@ def wrapped_func(*args, **kwargs):
943949
return return_value
944950

945951
return wrapped_func
952+
953+
954+
def try_relative_to(path: Path, relative_to: Path) -> str:
955+
"""Attempt to create a relative path and warn the user if it isn't possible.
956+
957+
Parameters
958+
----------
959+
path : Path
960+
The path to try to find a relative path for.
961+
relative_to: Path
962+
The path to which we find a relative path for ``path``.
963+
964+
Returns
965+
-------
966+
str
967+
The relative path if successful, else the absolute path.
968+
969+
"""
970+
path = Path(path)
971+
relative_to = Path(relative_to)
972+
if path.is_relative_to(relative_to):
973+
return str(path.relative_to(relative_to))
974+
else:
975+
warnings.warn(
976+
"Could not save custom file path as relative to the project directory, "
977+
"which means that it may not work on other devices."
978+
"If you would like to share your project, make sure your custom files "
979+
"are in a subfolder of the project save location.",
980+
stacklevel=2,
981+
)
982+
return str(path.resolve())

tests/test_project.py

Lines changed: 38 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@
22

33
import copy
44
import tempfile
5+
import warnings
56
from pathlib import Path
67
from typing import Callable
78

@@ -1556,8 +1557,43 @@ def test_save_load(project, request):
15561557
original_project = request.getfixturevalue(project)
15571558

15581559
with tempfile.TemporaryDirectory() as tmp:
1559-
original_project.save(tmp)
1560-
converted_project = RATapi.Project.load(Path(tmp, "project.json"))
1560+
# ignore relative path warnings
1561+
path = Path(tmp, "project.json")
1562+
with warnings.catch_warnings():
1563+
warnings.simplefilter("ignore")
1564+
original_project.save(path)
1565+
converted_project = RATapi.Project.load(path)
1566+
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()
15611570

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 to subfolders."""
1577+
1578+
with tempfile.TemporaryDirectory() as tmp:
1579+
data_path = Path(tmp, "data/myfile.dat")
1580+
1581+
assert Path(RATapi.project.try_relative_to(data_path, tmp)) == Path("data/myfile.dat")
1582+
1583+
1584+
def test_relative_paths_warning():
1585+
"""Test that we get a warning for trying to walk up paths."""
1586+
1587+
data_path = "/tmp/project/data/mydata.dat"
1588+
relative_path = "/tmp/project/project_path/myproj.dat"
1589+
1590+
with pytest.warns(
1591+
match="Could not save custom file path as relative to the project directory, "
1592+
"which means that it may not work on other devices."
1593+
"If you would like to share your project, make sure your custom files "
1594+
"are in a subfolder of the project save location.",
1595+
):
1596+
assert (
1597+
Path(RATapi.project.try_relative_to(data_path, relative_path))
1598+
== Path("/tmp/project/data/mydata.dat").resolve()
1599+
)

0 commit comments

Comments
 (0)