44import copy
55import functools
66import json
7+ import warnings
78from enum import Enum
89from pathlib import Path
910from 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 ())
0 commit comments