From bbe786818fb8d61575d0330a4ad9a1ef91eb3c0f Mon Sep 17 00:00:00 2001 From: alexhroom Date: Tue, 4 Feb 2025 07:40:02 +0000 Subject: [PATCH 1/7] added envrc to gitignore --- .gitignore | 3 +++ 1 file changed, 3 insertions(+) diff --git a/.gitignore b/.gitignore index 093e835f..c1b6a85d 100644 --- a/.gitignore +++ b/.gitignore @@ -7,6 +7,9 @@ __pycache__/ .idea .vscode +# direnv +.envrc + # Unit test / coverage reports htmlcov/ .coverage From cb5e6b633135466eb1a94b47f75a6ecc878cc1ef Mon Sep 17 00:00:00 2001 From: alexhroom Date: Tue, 4 Feb 2025 13:16:18 +0000 Subject: [PATCH 2/7] paths are now saved as relative to the project directory --- RATapi/project.py | 74 ++++++++++++++++++++++++++++++++++++------- tests/test_project.py | 37 +++++++++++++++++++++- 2 files changed, 98 insertions(+), 13 deletions(-) diff --git a/RATapi/project.py b/RATapi/project.py index 9b38f88e..c9bbfce2 100644 --- a/RATapi/project.py +++ b/RATapi/project.py @@ -4,8 +4,10 @@ import copy import functools import json +import warnings from enum import Enum from pathlib import Path +from sys import version_info from textwrap import indent from typing import Annotated, Any, Callable, Union @@ -835,17 +837,18 @@ def classlist_script(name, classlist): + "\n)" ) - def save(self, path: Union[str, Path], filename: str = "project"): + def save(self, filepath: Union[str, Path], filename: str = "project"): """Save a project to a JSON file. Parameters ---------- - path : str or Path + filepath : str or Path The path in which the project will be written. filename : str The name of the generated project file. """ + filepath = Path(filepath) json_dict = {} for field in self.model_fields: attr = getattr(self, field) @@ -869,7 +872,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] @@ -879,7 +882,7 @@ def make_custom_file_dict(item): else: json_dict[field] = attr - file = Path(path, f"{filename.removesuffix('.json')}.json") + file = Path(filepath, f"{filename.removesuffix('.json')}.json") file.write_text(json.dumps(json_dict)) @classmethod @@ -892,15 +895,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) @@ -943,3 +952,44 @@ 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. + + """ + # we use the absolute paths to resolve symlinks and so on + abs_path = Path(path).resolve() + abs_base = Path(relative_to).resolve() + + # 'walking up' paths is only added in Python 3.12 + if version_info.minor < 12: + try: + relative_path = abs_path.relative_to(abs_base) + except ValueError as err: + warnings.warn("Could not save a custom file path as relative to the project directory. " + "This may mean the project may not open on other devices. " + f"Error message: {err}", stacklevel=2) + return abs_path + else: + try: + relative_path = abs_path.relative_to(abs_base, walk_up = True) + except ValueError as err: + warnings.warn("Could not save a custom file path as relative to the project directory. " + "This may mean the project may not open on other devices. " + f"Error message: {err}", stacklevel=2) + return abs_path + + return relative_path diff --git a/tests/test_project.py b/tests/test_project.py index ee2136ba..17703dcc 100644 --- a/tests/test_project.py +++ b/tests/test_project.py @@ -2,7 +2,9 @@ import copy import tempfile +import warnings from pathlib import Path +from sys import version_info from typing import Callable import numpy as np @@ -1556,8 +1558,41 @@ def test_save_load(project, request): original_project = request.getfixturevalue(project) with tempfile.TemporaryDirectory() as tmp: - original_project.save(tmp) + # ignore relative path warnings + with warnings.catch_warnings(): + warnings.simplefilter("ignore") + original_project.save(tmp) converted_project = RATapi.Project.load(Path(tmp, "project.json")) + # 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.""" + + with tempfile.TemporaryDirectory() as tmp: + data_path = Path(tmp, "data/myfile.dat") + + assert RATapi.project.try_relative_to(data_path, tmp) == Path("./data/myfile.dat") + + +def test_relative_paths_version(): + """Test that we only walk up paths on Python 3.12 or greater.""" + + data_path = "/tmp/project/data/mydata.dat" + relative_path = "/tmp/project/project_path/myproj.dat" + + if version_info.minor >= 12: + with warnings.catch_warnings(): + warnings.simplefilter("error") + assert RATapi.project.try_relative_to(data_path, relative_path) == Path("../../data/mydata.dat") + else: + with pytest.warns(match="Could not save a custom file path as relative to the project directory. " + "This may mean the project may not open on other devices. " + "Error message:"): + assert RATapi.project.try_relative_to(data_path, relative_path) == Path("/tmp/project/data/mydata.dat") From 827a8876d77f267354a63b608c3b4d3f043416dd Mon Sep 17 00:00:00 2001 From: alexhroom Date: Tue, 4 Feb 2025 13:22:35 +0000 Subject: [PATCH 3/7] save now just has one filename parameter --- RATapi/project.py | 36 ++++++++++++++++++++---------------- tests/test_project.py | 17 ++++++++++------- 2 files changed, 30 insertions(+), 23 deletions(-) diff --git a/RATapi/project.py b/RATapi/project.py index c9bbfce2..dfa94fa8 100644 --- a/RATapi/project.py +++ b/RATapi/project.py @@ -837,18 +837,17 @@ def classlist_script(name, classlist): + "\n)" ) - def save(self, filepath: Union[str, Path], filename: str = "project"): + def save(self, filepath: Union[str, Path] = "./project.json"): """Save a project to a JSON file. Parameters ---------- filepath : str or Path - The path in which the project will be written. - filename : str - The name of the generated project file. + The path to where the project file will be written. """ - filepath = Path(filepath) + filepath = Path(filepath).with_suffix(".json") + json_dict = {} for field in self.model_fields: attr = getattr(self, field) @@ -882,8 +881,7 @@ def make_custom_file_dict(item): else: json_dict[field] = attr - file = Path(filepath, 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": @@ -979,17 +977,23 @@ def try_relative_to(path: Path, relative_to: Path) -> Path: try: relative_path = abs_path.relative_to(abs_base) except ValueError as err: - warnings.warn("Could not save a custom file path as relative to the project directory. " - "This may mean the project may not open on other devices. " - f"Error message: {err}", stacklevel=2) - return abs_path + warnings.warn( + "Could not save a custom file path as relative to the project directory. " + "This may mean the project may not open on other devices. " + f"Error message: {err}", + stacklevel=2, + ) + return abs_path else: try: - relative_path = abs_path.relative_to(abs_base, walk_up = True) + relative_path = abs_path.relative_to(abs_base, walk_up=True) except ValueError as err: - warnings.warn("Could not save a custom file path as relative to the project directory. " - "This may mean the project may not open on other devices. " - f"Error message: {err}", stacklevel=2) - return abs_path + warnings.warn( + "Could not save a custom file path as relative to the project directory. " + "This may mean the project may not open on other devices. " + f"Error message: {err}", + stacklevel=2, + ) + return abs_path return relative_path diff --git a/tests/test_project.py b/tests/test_project.py index 17703dcc..e47d8b36 100644 --- a/tests/test_project.py +++ b/tests/test_project.py @@ -1559,10 +1559,11 @@ def test_save_load(project, request): with tempfile.TemporaryDirectory() as tmp: # ignore relative path warnings + path = Path(tmp, "project.json") with warnings.catch_warnings(): warnings.simplefilter("ignore") - original_project.save(tmp) - converted_project = RATapi.Project.load(Path(tmp, "project.json")) + 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: @@ -1583,7 +1584,7 @@ def test_relative_paths(): def test_relative_paths_version(): """Test that we only walk up paths on Python 3.12 or greater.""" - + data_path = "/tmp/project/data/mydata.dat" relative_path = "/tmp/project/project_path/myproj.dat" @@ -1591,8 +1592,10 @@ def test_relative_paths_version(): with warnings.catch_warnings(): warnings.simplefilter("error") assert RATapi.project.try_relative_to(data_path, relative_path) == Path("../../data/mydata.dat") - else: - with pytest.warns(match="Could not save a custom file path as relative to the project directory. " - "This may mean the project may not open on other devices. " - "Error message:"): + else: + with pytest.warns( + match="Could not save a custom file path as relative to the project directory. " + "This may mean the project may not open on other devices. " + "Error message:" + ): assert RATapi.project.try_relative_to(data_path, relative_path) == Path("/tmp/project/data/mydata.dat") From db59ec97db3835e69ccfd5f66a7f1c2e27888dff Mon Sep 17 00:00:00 2001 From: alexhroom Date: Tue, 4 Feb 2025 13:58:50 +0000 Subject: [PATCH 4/7] removed walkup as it is unstable --- RATapi/project.py | 34 ++++++++++------------------------ tests/test_project.py | 24 ++++++++++-------------- 2 files changed, 20 insertions(+), 38 deletions(-) diff --git a/RATapi/project.py b/RATapi/project.py index dfa94fa8..a9c6be25 100644 --- a/RATapi/project.py +++ b/RATapi/project.py @@ -7,7 +7,6 @@ import warnings from enum import Enum from pathlib import Path -from sys import version_info from textwrap import indent from typing import Annotated, Any, Callable, Union @@ -972,28 +971,15 @@ def try_relative_to(path: Path, relative_to: Path) -> Path: abs_path = Path(path).resolve() abs_base = Path(relative_to).resolve() - # 'walking up' paths is only added in Python 3.12 - if version_info.minor < 12: - try: - relative_path = abs_path.relative_to(abs_base) - except ValueError as err: - warnings.warn( - "Could not save a custom file path as relative to the project directory. " - "This may mean the project may not open on other devices. " - f"Error message: {err}", - stacklevel=2, - ) - return abs_path - else: - try: - relative_path = abs_path.relative_to(abs_base, walk_up=True) - except ValueError as err: - warnings.warn( - "Could not save a custom file path as relative to the project directory. " - "This may mean the project may not open on other devices. " - f"Error message: {err}", - stacklevel=2, - ) - return abs_path + try: + relative_path = abs_path.relative_to(abs_base) + except ValueError: + warnings.warn( + "Could not save custom file path as relative to the project directory. " + "To ensure that your project works on other devices, make sure your custom files " + "are in a subfolder of the project save location.", + stacklevel=2, + ) + return abs_path return relative_path diff --git a/tests/test_project.py b/tests/test_project.py index e47d8b36..20024c86 100644 --- a/tests/test_project.py +++ b/tests/test_project.py @@ -4,7 +4,6 @@ import tempfile import warnings from pathlib import Path -from sys import version_info from typing import Callable import numpy as np @@ -1574,7 +1573,7 @@ def test_save_load(project, request): def test_relative_paths(): - """Test that ``try_relative_to`` correctly creates 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") @@ -1583,19 +1582,16 @@ def test_relative_paths(): def test_relative_paths_version(): - """Test that we only walk up paths on Python 3.12 or greater.""" + """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" - if version_info.minor >= 12: - with warnings.catch_warnings(): - warnings.simplefilter("error") - assert RATapi.project.try_relative_to(data_path, relative_path) == Path("../../data/mydata.dat") - else: - with pytest.warns( - match="Could not save a custom file path as relative to the project directory. " - "This may mean the project may not open on other devices. " - "Error message:" - ): - assert RATapi.project.try_relative_to(data_path, relative_path) == Path("/tmp/project/data/mydata.dat") + with pytest.warns( + match="Could not save custom file path as relative to the project directory. " + "To ensure that your project works on other devices, make sure your custom files " + "are in a subfolder of the project save location." + ): + assert ( + RATapi.project.try_relative_to(data_path, relative_path) == Path("/tmp/project/data/mydata.dat").resolve() + ) From 4f9a65295d86fd525bd3078af0b9a6d1dd98606b Mon Sep 17 00:00:00 2001 From: alexhroom Date: Thu, 13 Feb 2025 14:01:53 +0000 Subject: [PATCH 5/7] simplified --- RATapi/project.py | 21 +++++++++------------ tests/test_project.py | 15 +++++++-------- 2 files changed, 16 insertions(+), 20 deletions(-) diff --git a/RATapi/project.py b/RATapi/project.py index a9c6be25..87527086 100644 --- a/RATapi/project.py +++ b/RATapi/project.py @@ -967,19 +967,16 @@ def try_relative_to(path: Path, relative_to: Path) -> Path: The relative path if successful, else the absolute path. """ - # we use the absolute paths to resolve symlinks and so on - abs_path = Path(path).resolve() - abs_base = Path(relative_to).resolve() - - try: - relative_path = abs_path.relative_to(abs_base) - except ValueError: + 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. " - "To ensure that your project works on other devices, make sure your custom files " + "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 abs_path - - return relative_path + return str(path.resolve()) diff --git a/tests/test_project.py b/tests/test_project.py index 20024c86..1e6481ff 100644 --- a/tests/test_project.py +++ b/tests/test_project.py @@ -1578,20 +1578,19 @@ def test_relative_paths(): with tempfile.TemporaryDirectory() as tmp: data_path = Path(tmp, "data/myfile.dat") - assert RATapi.project.try_relative_to(data_path, tmp) == Path("./data/myfile.dat") + assert RATapi.project.try_relative_to(data_path, tmp) == "data/myfile.dat" -def test_relative_paths_version(): +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. " - "To ensure that your project works on other devices, make sure your custom files " - "are in a subfolder of the project save location." + 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 ( - RATapi.project.try_relative_to(data_path, relative_path) == Path("/tmp/project/data/mydata.dat").resolve() - ) + assert RATapi.project.try_relative_to(data_path, relative_path) == "/tmp/project/data/mydata.dat" From 736e7d7ccf8062753f991aee2637d71845736e30 Mon Sep 17 00:00:00 2001 From: alexhroom Date: Thu, 13 Feb 2025 14:17:55 +0000 Subject: [PATCH 6/7] portable tests --- tests/test_project.py | 7 +++++-- 1 file changed, 5 insertions(+), 2 deletions(-) diff --git a/tests/test_project.py b/tests/test_project.py index 1e6481ff..eb2b2d8f 100644 --- a/tests/test_project.py +++ b/tests/test_project.py @@ -1578,7 +1578,7 @@ def test_relative_paths(): with tempfile.TemporaryDirectory() as tmp: data_path = Path(tmp, "data/myfile.dat") - assert RATapi.project.try_relative_to(data_path, tmp) == "data/myfile.dat" + assert Path(RATapi.project.try_relative_to(data_path, tmp)) == Path("data/myfile.dat") def test_relative_paths_warning(): @@ -1593,4 +1593,7 @@ def test_relative_paths_warning(): "If you would like to share your project, make sure your custom files " "are in a subfolder of the project save location.", ): - assert RATapi.project.try_relative_to(data_path, relative_path) == "/tmp/project/data/mydata.dat" + assert ( + Path(RATapi.project.try_relative_to(data_path, relative_path)) + == Path("/tmp/project/data/mydata.dat").resolve() + ) From 56405fc42c2a51be244414862396fe98db5721c9 Mon Sep 17 00:00:00 2001 From: "Alex H. Room" <69592136+alexhroom@users.noreply.github.com> Date: Mon, 17 Feb 2025 15:41:04 +0000 Subject: [PATCH 7/7] review fixes --- RATapi/project.py | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/RATapi/project.py b/RATapi/project.py index 87527086..da36b5ed 100644 --- a/RATapi/project.py +++ b/RATapi/project.py @@ -870,7 +870,7 @@ def make_custom_file_dict(item): "name": item.name, "filename": item.filename, "language": item.language, - "path": str(try_relative_to(item.path, filepath)), + "path": try_relative_to(item.path, filepath), } json_dict["custom_files"] = [make_custom_file_dict(file) for file in attr] @@ -951,7 +951,7 @@ def wrapped_func(*args, **kwargs): return wrapped_func -def try_relative_to(path: Path, relative_to: Path) -> Path: +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 @@ -963,7 +963,7 @@ def try_relative_to(path: Path, relative_to: Path) -> Path: Returns ------- - Path + str The relative path if successful, else the absolute path. """