From 7881981ffb703251cf4ec6f75fd3dac679add82a Mon Sep 17 00:00:00 2001 From: Paul Sharp <44529197+DrPaulSharp@users.noreply.github.com> Date: Thu, 12 Jun 2025 10:37:02 +0100 Subject: [PATCH 1/4] Adds support for relative paths for custom files --- RATapi/models.py | 8 +++++++- 1 file changed, 7 insertions(+), 1 deletion(-) diff --git a/RATapi/models.py b/RATapi/models.py index af557419..ae20fcf6 100644 --- a/RATapi/models.py +++ b/RATapi/models.py @@ -304,7 +304,13 @@ class CustomFile(RATModel): filename: str = "" function_name: str = "" language: Languages = Languages.Python - path: pathlib.Path = pathlib.Path(".") + path: pathlib.Path = pathlib.Path(".").resolve() + + @field_validator("path") + @classmethod + def resolve_relative_paths(cls, path: pathlib.Path) -> pathlib.Path: + """Return the absolute path of the given path.""" + return path.resolve() def model_post_init(self, __context: Any) -> None: """Autogenerate the function name from the filename if not set. From 027bca6bdabc7dfaedda963d336388430da61591 Mon Sep 17 00:00:00 2001 From: Paul Sharp <44529197+DrPaulSharp@users.noreply.github.com> Date: Thu, 12 Jun 2025 12:31:11 +0100 Subject: [PATCH 2/4] Bug fix --- RATapi/project.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/RATapi/project.py b/RATapi/project.py index c35e92a6..6fafffc5 100644 --- a/RATapi/project.py +++ b/RATapi/project.py @@ -868,7 +868,7 @@ def write_item(item): + f"'filename': '{item.filename}', " + f"'function_name': '{item.function_name}', " + f"'language': '{str(item.language)}', " - + f"'path': '{str(item.path)}'" + + f"'path': r'{str(item.path)}'" # Raw string to ensure backslash is not interpreted as escape + "}" ) return item_str From c021d8fc29218e291b1b652e757ac08e0cb80387 Mon Sep 17 00:00:00 2001 From: Paul Sharp <44529197+DrPaulSharp@users.noreply.github.com> Date: Mon, 16 Jun 2025 13:40:40 +0100 Subject: [PATCH 3/4] Changes checks in save and load to parent directories --- RATapi/examples/absorption/absorption.py | 2 +- RATapi/examples/absorption/volume_thiol_bilayer.py | 2 +- RATapi/examples/domains/domains_custom_XY.py | 2 +- RATapi/examples/domains/domains_custom_layers.py | 2 +- RATapi/examples/languages/run_custom_file_languages.py | 2 +- RATapi/examples/languages/setup_problem.py | 2 +- RATapi/examples/normal_reflectivity/DSPC_custom_XY.py | 2 +- .../examples/normal_reflectivity/DSPC_custom_layers.py | 2 +- .../normal_reflectivity/DSPC_function_background.py | 2 +- RATapi/project.py | 9 +++++---- tests/conftest.py | 2 +- tests/test_inputs.py | 2 +- tests/test_project.py | 6 +----- 13 files changed, 17 insertions(+), 20 deletions(-) diff --git a/RATapi/examples/absorption/absorption.py b/RATapi/examples/absorption/absorption.py index 63b1ba3e..416a3b32 100644 --- a/RATapi/examples/absorption/absorption.py +++ b/RATapi/examples/absorption/absorption.py @@ -108,7 +108,7 @@ def absorption(): name="DPPC absorption", filename="volume_thiol_bilayer.py", language="python", - path=pathlib.Path(__file__).parent.resolve(), + path=pathlib.Path(__file__).parent, ) # Finally add the contrasts diff --git a/RATapi/examples/absorption/volume_thiol_bilayer.py b/RATapi/examples/absorption/volume_thiol_bilayer.py index 4398f2b6..444d5cac 100644 --- a/RATapi/examples/absorption/volume_thiol_bilayer.py +++ b/RATapi/examples/absorption/volume_thiol_bilayer.py @@ -133,7 +133,7 @@ def volume_thiol_bilayer(params, bulk_in, bulk_out, contrast): CW = [cwThick, bulk_out[contrast], 0, bilayerRough] - if contrast == 0 or contrast == 2: + if contrast == 1 or contrast == 3: output = [alloyUp, gold, SAMTAILS, SAMHEAD, CW, *BILAYER] else: output = [alloyDown, gold, SAMTAILS, SAMHEAD, CW, *BILAYER] diff --git a/RATapi/examples/domains/domains_custom_XY.py b/RATapi/examples/domains/domains_custom_XY.py index 1eb7222d..a9c1846e 100644 --- a/RATapi/examples/domains/domains_custom_XY.py +++ b/RATapi/examples/domains/domains_custom_XY.py @@ -35,7 +35,7 @@ def domains_custom_XY(): name="Domain Layer", filename="domains_XY_model.py", language="python", - path=pathlib.Path(__file__).parent.resolve(), + path=pathlib.Path(__file__).parent, ) # Make contrasts diff --git a/RATapi/examples/domains/domains_custom_layers.py b/RATapi/examples/domains/domains_custom_layers.py index 99c2d372..439728a0 100644 --- a/RATapi/examples/domains/domains_custom_layers.py +++ b/RATapi/examples/domains/domains_custom_layers.py @@ -32,7 +32,7 @@ def domains_custom_layers(): name="Alloy domains", filename="alloy_domains.py", language="python", - path=pathlib.Path(__file__).parent.resolve(), + path=pathlib.Path(__file__).parent, ) # Make a contrast diff --git a/RATapi/examples/languages/run_custom_file_languages.py b/RATapi/examples/languages/run_custom_file_languages.py index f91c0317..29776b52 100644 --- a/RATapi/examples/languages/run_custom_file_languages.py +++ b/RATapi/examples/languages/run_custom_file_languages.py @@ -7,7 +7,7 @@ import RATapi as RAT -path = pathlib.Path(__file__).parent.resolve() +path = pathlib.Path(__file__).parent project = setup_problem.make_example_problem() controls = RAT.Controls() diff --git a/RATapi/examples/languages/setup_problem.py b/RATapi/examples/languages/setup_problem.py index 4eea0965..7c294126 100644 --- a/RATapi/examples/languages/setup_problem.py +++ b/RATapi/examples/languages/setup_problem.py @@ -55,7 +55,7 @@ def make_example_problem(): name="DSPC Model", filename="custom_bilayer.py", language="python", - path=pathlib.Path(__file__).parent.resolve(), + path=pathlib.Path(__file__).parent, ) # Also, add the relevant background parameters - one each for each contrast: diff --git a/RATapi/examples/normal_reflectivity/DSPC_custom_XY.py b/RATapi/examples/normal_reflectivity/DSPC_custom_XY.py index 27b15ffe..442d6707 100644 --- a/RATapi/examples/normal_reflectivity/DSPC_custom_XY.py +++ b/RATapi/examples/normal_reflectivity/DSPC_custom_XY.py @@ -75,7 +75,7 @@ def DSPC_custom_XY(): name="DSPC Model", filename="custom_XY_DSPC.py", language="python", - path=pathlib.Path(__file__).parent.resolve(), + path=pathlib.Path(__file__).parent, ) # Also, add the relevant background parameters - one each for each contrast: diff --git a/RATapi/examples/normal_reflectivity/DSPC_custom_layers.py b/RATapi/examples/normal_reflectivity/DSPC_custom_layers.py index 66a53589..adcba5d3 100644 --- a/RATapi/examples/normal_reflectivity/DSPC_custom_layers.py +++ b/RATapi/examples/normal_reflectivity/DSPC_custom_layers.py @@ -52,7 +52,7 @@ def DSPC_custom_layers(): name="DSPC Model", filename="custom_bilayer_DSPC.py", language="python", - path=pathlib.Path(__file__).parent.resolve(), + path=pathlib.Path(__file__).parent, ) # Also, add the relevant background parameters - one each for each contrast: diff --git a/RATapi/examples/normal_reflectivity/DSPC_function_background.py b/RATapi/examples/normal_reflectivity/DSPC_function_background.py index 54cc9d47..f149cfbb 100644 --- a/RATapi/examples/normal_reflectivity/DSPC_function_background.py +++ b/RATapi/examples/normal_reflectivity/DSPC_function_background.py @@ -148,7 +148,7 @@ def DSPC_function_background(): name="D2O Background Function", filename="background_function.py", language="python", - path=pathlib.Path(__file__).parent.resolve(), + path=pathlib.Path(__file__).parent, ) problem.background_parameters.append(name="Fn Ao", min=5e-7, value=8e-6, max=5e-5) diff --git a/RATapi/project.py b/RATapi/project.py index 6fafffc5..02c2cfec 100644 --- a/RATapi/project.py +++ b/RATapi/project.py @@ -868,7 +868,8 @@ def write_item(item): + f"'filename': '{item.filename}', " + f"'function_name': '{item.function_name}', " + f"'language': '{str(item.language)}', " - + f"'path': r'{str(item.path)}'" # Raw string to ensure backslash is not interpreted as escape + # Raw string to ensure backslash is not interpreted as escape + + f"'path': r'{str(try_relative_to(item.path, script_path.parent))}'" + "}" ) return item_str @@ -932,7 +933,7 @@ def make_custom_file_dict(item): "name": item.name, "filename": item.filename, "language": item.language, - "path": try_relative_to(item.path, filepath), + "path": try_relative_to(item.path, filepath.parent), } json_dict["custom_files"] = [make_custom_file_dict(file) for file in attr] @@ -968,7 +969,7 @@ def load(cls, path: Union[str, Path]) -> "Project": # 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"]) + file["path"] = Path(path.parent, file["path"]).resolve() return cls.model_validate(model_dict) @@ -1038,7 +1039,7 @@ def try_relative_to(path: Path, relative_to: Path) -> str: return str(path.relative_to(relative_to)) else: warnings.warn( - "Could not save custom file path as relative to the project directory, " + "Could not write 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, diff --git a/tests/conftest.py b/tests/conftest.py index c109c92d..f92ad836 100644 --- a/tests/conftest.py +++ b/tests/conftest.py @@ -6858,7 +6858,7 @@ def r1_monolayer(): ), custom_files=RATapi.ClassList( RATapi.models.CustomFile( - name="Model_IIb", filename="Model_IIb.m", function_name="Model_IIb", language="matlab", path="" + name="Model_IIb", filename="Model_IIb.m", function_name="Model_IIb", language="matlab" ) ), ) diff --git a/tests/test_inputs.py b/tests/test_inputs.py index d3ed276e..7d768495 100644 --- a/tests/test_inputs.py +++ b/tests/test_inputs.py @@ -663,7 +663,7 @@ def test_append_data_background_error(): def test_get_python_handle(): - path = pathlib.Path(__file__).parent.resolve() + path = pathlib.Path(__file__).parent assert RATapi.inputs.get_python_handle("utils.py", "dummy_function", path).__code__ == dummy_function.__code__ diff --git a/tests/test_project.py b/tests/test_project.py index ee6d6c59..2cdd5ead 100644 --- a/tests/test_project.py +++ b/tests/test_project.py @@ -1590,10 +1590,6 @@ def test_save_load(project, request): 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) @@ -1614,7 +1610,7 @@ def test_relative_paths_warning(): relative_path = "/tmp/project/project_path/myproj.dat" with pytest.warns( - match="Could not save custom file path as relative to the project directory, " + match="Could not write 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.", ): From 30fdc6be2dbb7c1e88f1cb064ecc93fb3d675422 Mon Sep 17 00:00:00 2001 From: Paul Sharp <44529197+DrPaulSharp@users.noreply.github.com> Date: Mon, 16 Jun 2025 14:46:04 +0100 Subject: [PATCH 4/4] Adds test for relative path --- tests/test_models.py | 8 ++++++++ 1 file changed, 8 insertions(+) diff --git a/tests/test_models.py b/tests/test_models.py index 703e1f38..8e90183e 100644 --- a/tests/test_models.py +++ b/tests/test_models.py @@ -1,5 +1,6 @@ """Test the pydantic models.""" +import pathlib import re from typing import Callable @@ -100,6 +101,13 @@ def test_initialise_with_extra_fields(self, model: Callable, model_params: dict) model(new_field=1, **model_params) +def test_custom_file_path_is_absolute() -> None: + """If we use provide a relative path to the custom file model, it should be converted to an absolute path.""" + relative_path = pathlib.Path("./relative_path") + custom_file = RATapi.models.CustomFile(path=relative_path) + assert custom_file.path.is_absolute() + + def test_data_eq() -> None: """If we use the Data.__eq__ method with an object that is not a pydantic BaseModel, we should return "NotImplemented".