Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

RCAL-723 Fix model.meta.filename when saving model #295

Merged
merged 6 commits into from
Dec 14, 2023
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 2 additions & 0 deletions CHANGES.rst
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,8 @@
- Allow assignment to or creation of node attributes using dot notation of object instances
with validation. [#284]

- Bugfix for ``model.meta.filename`` not matching the filename of the file on disk. [#295]

0.18.0 (2023-11-06)
===================

Expand Down
47 changes: 29 additions & 18 deletions src/roman_datamodels/datamodels/_core.py
Original file line number Diff line number Diff line change
Expand Up @@ -11,10 +11,9 @@
import copy
import datetime
import functools
import os
import os.path
import sys
from pathlib import PurePath
from contextlib import contextmanager
from pathlib import Path, PurePath

import asdf
import numpy as np
Expand Down Expand Up @@ -48,6 +47,26 @@ def wrapper(self, *args, **kwargs):
return wrapper


@contextmanager
def _temporary_update_filename(datamodel, filename):
"""
Context manager to temporarily update the filename of a datamodel so that it
can be saved with that new file name without changing the current model's filename
"""
from roman_datamodels.stnode import Filename

if "meta" in datamodel._instance and "filename" in datamodel._instance.meta:
old_filename = datamodel._instance.meta.filename
datamodel._instance.meta.filename = Filename(filename)

yield
datamodel._instance.meta.filename = old_filename
Comment on lines +50 to +63
Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This specifically exists so that the DataModel instance being saved via save or to_asdf does not update its filename, the filename is only updated in the newly written ASDF file and the model which can be opened from that.

I chose to do it this way because AsdfFile.write_to is employed by these to_asdf (which is called by save), which writes the AsdfFile object to the new file descriptor passed to it, but it does not update the file descriptor of the AsdfFile object. This means the asdf file wrapped by the DataModel instance is not changing, so the filename of the python object being saved should not be updated.

return

yield
return


class DataModel(abc.ABC):
"""Base class for all top level datamodels"""

Expand Down Expand Up @@ -181,17 +200,9 @@ def clone(target, source, deepcopy=False, memo=None):
target._ctx = target

def save(self, path, dir_path=None, *args, **kwargs):
if callable(path):
path_head, path_tail = os.path.split(path(self.meta.filename))
else:
path_head, path_tail = os.path.split(path)
base, ext = os.path.splitext(path_tail)
if isinstance(ext, bytes):
ext = ext.decode(sys.getfilesystemencoding())

if dir_path:
path_head = dir_path
output_path = os.path.join(path_head, path_tail)
path = Path(path(self.meta.filename) if callable(path) else path)
output_path = Path(dir_path) / path.name if dir_path else path
ext = path.suffix.decode(sys.getfilesystemencoding()) if isinstance(path.suffix, bytes) else path.suffix

# TODO: Support gzip-compressed fits
if ext == ".asdf":
Expand All @@ -206,10 +217,10 @@ def open_asdf(self, init=None, **kwargs):
return asdf.open(init, **kwargs) if isinstance(init, str) else asdf.AsdfFile(init, **kwargs)

def to_asdf(self, init, *args, **kwargs):
with validate.nuke_validation():
asdffile = self.open_asdf(**kwargs)
asdffile.tree = {"roman": self._instance}
asdffile.write_to(init, *args, **kwargs)
with validate.nuke_validation(), _temporary_update_filename(self, Path(init).name):
asdf_file = self.open_asdf(**kwargs)
asdf_file.tree = {"roman": self._instance}
asdf_file.write_to(init, *args, **kwargs)

def get_primary_array_name(self):
"""
Expand Down
12 changes: 12 additions & 0 deletions tests/test_models.py
Original file line number Diff line number Diff line change
Expand Up @@ -804,3 +804,15 @@ def test_datamodel_construct_like_from_like(model):
new_mdl = model(mdl)
assert new_mdl is mdl
assert new_mdl._iscopy == "foo" # Verify that the constructor didn't override stuff


def test_datamodel_save_filename(tmp_path):
filename = tmp_path / "fancy_filename.asdf"
ramp = utils.mk_datamodel(datamodels.RampModel, shape=(2, 8, 8))
assert ramp.meta.filename != filename.name

ramp.save(filename)
assert ramp.meta.filename != filename.name

with datamodels.open(filename) as new_ramp:
assert new_ramp.meta.filename == filename.name