Skip to content

Commit a25dccb

Browse files
mj-willColmTalbot
andcommitted
MAINT: update file extension logic (bilby-dev#962)
* MAINT: rework file extension logic * TST: update result saving tests * BUG: fix extension precedence in `read_in_result` * TST: assert files can be read in * MAINT: tweak extension logic in `read_in_result` * TST: update extensions in tests * BUG: fix extension default * BUG: fix logic in `read_in_result` * TST: improve result save/read tests * ENH: improve error handling in `read_in_result` * TST: add tests for file loading with incorrect extensions * MAINT: tweak IOError message * TST: fix logging check in caching test --------- Co-authored-by: Colm Talbot <talbotcolm@gmail.com>
1 parent ceae2ac commit a25dccb

2 files changed

Lines changed: 241 additions & 76 deletions

File tree

bilby/core/result.py

Lines changed: 87 additions & 33 deletions
Original file line numberDiff line numberDiff line change
@@ -82,7 +82,7 @@ def _determine_file_name(filename, outdir, label, extension, gzip):
8282
return result_file_name(outdir, label, extension, gzip)
8383

8484

85-
def read_in_result(filename=None, outdir=None, label=None, extension='json', gzip=False, result_class=None):
85+
def read_in_result(filename=None, outdir=None, label=None, extension=None, gzip=False, result_class=None):
8686
""" Reads in a stored bilby result object
8787
8888
Parameters
@@ -92,33 +92,67 @@ def read_in_result(filename=None, outdir=None, label=None, extension='json', gzi
9292
outdir, label, extension: str
9393
Name of the output directory, label and extension used for the default
9494
naming scheme.
95+
extension: str, optional
96+
The file extension to use. If not given, the extension is inferred from
97+
the filename if provided. If the filename is not given, defaults to
98+
'json'.
9599
result_class: bilby.core.result.Result, or child of
96100
The result class to use. By default, `bilby.core.result.Result` is used,
97101
but objects which inherit from this class can be given providing
98102
additional methods.
99103
"""
100-
filename = _determine_file_name(filename, outdir, label, extension, gzip)
104+
filename = _determine_file_name(
105+
filename, outdir, label, extension or "json", gzip
106+
)
101107

102108
if result_class is None:
103109
result_class = Result
104110
elif not issubclass(result_class, Result):
105111
raise ValueError(f"Input result_class={result_class} not understood")
106112

107113
# Get the actual extension (may differ from the default extension if the filename is given)
108-
extension = os.path.splitext(filename)[1].lstrip('.')
114+
if extension is None:
115+
ext = os.path.splitext(filename)[1][1:]
116+
extension = ext if ext else None
109117
if extension == 'gz': # gzipped file
110118
extension = os.path.splitext(os.path.splitext(filename)[0])[1].lstrip('.')
111119

112-
if 'json' in extension:
113-
result = result_class.from_json(filename=filename)
114-
elif ('hdf5' in extension) or ('h5' in extension):
115-
result = result_class.from_hdf5(filename=filename)
116-
elif ("pkl" in extension) or ("pickle" in extension):
117-
result = result_class.from_pickle(filename=filename)
118-
elif extension is None:
119-
raise ValueError("No filetype extension provided")
120-
else:
121-
raise ValueError("Filetype {} not understood".format(extension))
120+
if extension is None:
121+
raise ValueError("No filetype extension provided and could not be inferred from filename")
122+
123+
extension = extension.lower()
124+
read_functions = {
125+
'json': result_class.from_json,
126+
'hdf5': result_class.from_hdf5,
127+
'h5': result_class.from_hdf5,
128+
'pkl': result_class.from_pickle,
129+
'pickle': result_class.from_pickle,
130+
}
131+
if extension not in read_functions:
132+
raise ValueError(
133+
f"Filetype {extension} not understood, known types are {list(read_functions.keys())}"
134+
)
135+
136+
func = read_functions[extension]
137+
138+
# Raise IO errors since this is what the caching relies on
139+
# Catch all other exceptions and raise a FileLoadError
140+
try:
141+
result = func(filename=filename)
142+
except IOError as e:
143+
raise IOError(
144+
f"Failed to read in file {filename} using "
145+
f"`{result_class.__name__}.{func.__name__}` "
146+
f"(extension={extension}). "
147+
"This is likely because the file does not exist or is not a valid bilby result."
148+
f"The error was: {e}"
149+
) from e
150+
except Exception as e:
151+
raise FileLoadError(
152+
f"Failed to read in file {filename} using "
153+
f"`{result_class.__name__}.{func.__name__}` "
154+
f"(extension={extension}). The error was: {e}"
155+
) from e
122156
return result
123157

124158

@@ -777,7 +811,8 @@ def save_to_file(self, filename=None, overwrite=False, outdir=None,
777811
Parameters
778812
==========
779813
filename: optional,
780-
Filename to write to (overwrites the default)
814+
Filename to write to (overwrites the default). Assumed to include the
815+
file extension.
781816
overwrite: bool, optional
782817
Whether or not to overwrite an existing result file.
783818
default=False
@@ -794,24 +829,39 @@ def save_to_file(self, filename=None, overwrite=False, outdir=None,
794829

795830
_outdir = None
796831
if filename is not None:
797-
_outdir, base_filename = os.path.split(filename)
832+
# Overwrite the full filename with the base filename
833+
# Well append the outdir later on
834+
_outdir, filename = os.path.split(filename)
798835
_outdir = None if _outdir == "" else _outdir
799-
base, ext = os.path.splitext(base_filename)
800-
if extension is None:
801-
extension = ext[1:] if ext else "json"
802-
filename = base_filename
803-
elif extension is True:
804-
message = "Result.save_to_file called with extension=True. "
805-
if len(ext) > 0:
806-
message += f"Overwriting extension to json from {ext}, this"
807-
else:
808-
message += "This"
809-
message += " behaviour is deprecated and will be removed."
810-
logger.warning(message)
811-
extension = "json"
812-
filename = base_filename
813-
filename = f"{base}.{extension}"
814-
if extension is None or extension is True:
836+
_, ext = os.path.splitext(filename)
837+
ext = ext[1:] if ext else None
838+
# If the extension has not been set, try to infer it from the filename
839+
# if not, it will fall back to the default
840+
if ext in EXTENSIONS:
841+
if extension is None:
842+
logger.debug(
843+
f"Inferred extension '{ext}' from filename '{filename}'. "
844+
"Using this extension for saving."
845+
)
846+
extension = ext
847+
elif ext != extension:
848+
message = (
849+
f"The specified extension '{ext}' "
850+
f"does not match the provided extension '{extension}'. "
851+
)
852+
logger.warning(message)
853+
854+
if extension is None:
855+
logger.info("No extension given, defaulting to JSON.")
856+
extension = 'json'
857+
858+
if extension is True:
859+
message = (
860+
"Result.save_to_file called with extension=True. "
861+
"This will default to json, and ignore the extension from the filename. "
862+
"This behaviour is deprecated and will be removed. "
863+
)
864+
logger.warning(message)
815865
extension = 'json'
816866

817867
outdir = _outdir if outdir is None else outdir
@@ -845,13 +895,13 @@ def save_to_file(self, filename=None, overwrite=False, outdir=None,
845895
else:
846896
with open(output_path, 'w') as file:
847897
json.dump(dictionary, file, indent=2, cls=BilbyJsonEncoder)
848-
elif extension == 'hdf5':
898+
elif extension in ['hdf5', 'h5']:
849899
import h5py
850900
dictionary["__module__"] = self.__module__
851901
dictionary["__name__"] = self.__class__.__name__
852902
with h5py.File(output_path, 'w') as h5file:
853903
recursively_save_dict_contents_to_group(h5file, '/', dictionary)
854-
elif extension == 'pkl':
904+
elif extension in ['pkl', 'pickle']:
855905
safe_file_dump(self, output_path, "dill")
856906
else:
857907
raise ValueError(f"Extension type {extension} not understood")
@@ -2291,3 +2341,7 @@ class ResultListError(ResultError):
22912341

22922342
class FileMovedError(ResultError):
22932343
""" Exceptions that occur when files have been moved """
2344+
2345+
2346+
class FileLoadError(ResultError):
2347+
""" Exceptions that occur when files cannot be loaded """

0 commit comments

Comments
 (0)