From b1d87a660c9e4dc9bba121d8d9b1f5878e733bce Mon Sep 17 00:00:00 2001 From: Ned Molter Date: Thu, 30 Apr 2026 11:06:57 -0400 Subject: [PATCH 1/3] Support update from ObjectNode --- src/stdatamodels/model_base.py | 3 ++- src/stdatamodels/properties.py | 6 ++++++ tests/jwst/datamodels/test_multislit.py | 15 +++++++++++++++ 3 files changed, 23 insertions(+), 1 deletion(-) diff --git a/src/stdatamodels/model_base.py b/src/stdatamodels/model_base.py index 8fb41a05c..19719aae5 100644 --- a/src/stdatamodels/model_base.py +++ b/src/stdatamodels/model_base.py @@ -19,6 +19,7 @@ from . import filetype, fits_support, properties, validate from . import schema as mschema from .history import HistoryList +from .properties import ObjectNode from .util import convert_fitsrec_to_array_in_tree, get_envar_as_boolean, remove_none_from_tree # This minimal schema creates metadata fields that @@ -925,7 +926,7 @@ def hdu_keywords_from_schema(subschema, path, combiner, ctx, recurse): # calling ObjectNode.__setattr__ # This triggers validation if validate_on_assignment is True. def assign_leaves(node, path=()): - if isinstance(node, dict): + if isinstance(node, (dict, ObjectNode)): for key, val in node.items(): # skip extra_fits - handled separately below if not path and key == "extra_fits": diff --git a/src/stdatamodels/properties.py b/src/stdatamodels/properties.py index 01fa86543..fd56f3e56 100644 --- a/src/stdatamodels/properties.py +++ b/src/stdatamodels/properties.py @@ -440,6 +440,12 @@ def __delattr__(self, attr): def __iter__(self): return NodeIterator(self) + def pop(self, a, b): # noqa: D102 + try: + return self._instance.pop(a) + except KeyError: + return b + def hasattr(self, attr): """ Check if the node has an attribute in its instance. diff --git a/tests/jwst/datamodels/test_multislit.py b/tests/jwst/datamodels/test_multislit.py index 82b86bf97..e3e484147 100644 --- a/tests/jwst/datamodels/test_multislit.py +++ b/tests/jwst/datamodels/test_multislit.py @@ -6,6 +6,7 @@ from numpy.testing import assert_array_equal from stdatamodels.jwst.datamodels import ImageModel, MultiSlitModel, SlitModel +from stdatamodels.properties import ObjectNode def test_multislit_model(): @@ -168,3 +169,17 @@ def test_slit_from_multislit(): slit.int_times = slit.get_default("int_times") model.slits.append(slit) slit = SlitModel(model.slits[0].instance) + + +def test_slit_update_from_multislit(): + """Cover a bug where JwstDataModel.update() would raise for ObjectNode input.""" + multislit = MultiSlitModel() + slit = SlitModel() + slit.meta.telescope = "foo" + multislit.slits.append(slit) + slit_objnode = multislit.slits[0] + assert isinstance(slit_objnode, ObjectNode) + + slit2 = SlitModel() + slit2.update(slit_objnode) + assert slit2.meta.telescope == "foo" From 14a52a3bd933b5476718c5b953425c23887bff45 Mon Sep 17 00:00:00 2001 From: Ned Molter Date: Thu, 30 Apr 2026 11:16:54 -0400 Subject: [PATCH 2/3] Update docstring, add changelog --- changes/724.feature.rst | 1 + src/stdatamodels/jwst/datamodels/model_base.py | 3 ++- src/stdatamodels/model_base.py | 3 ++- 3 files changed, 5 insertions(+), 2 deletions(-) create mode 100644 changes/724.feature.rst diff --git a/changes/724.feature.rst b/changes/724.feature.rst new file mode 100644 index 000000000..2cd5f52ba --- /dev/null +++ b/changes/724.feature.rst @@ -0,0 +1 @@ +Allow model.update to accept ObjectNode as input. diff --git a/src/stdatamodels/jwst/datamodels/model_base.py b/src/stdatamodels/jwst/datamodels/model_base.py index 850d55302..d6a6ee37d 100644 --- a/src/stdatamodels/jwst/datamodels/model_base.py +++ b/src/stdatamodels/jwst/datamodels/model_base.py @@ -79,7 +79,8 @@ def update(self, d, only=None, extra_fits=False, cal_logs=True): ---------- d : `~stdatamodels.jwst.datamodels.JwstDataModel` or dictionary-like object The model to copy the metadata elements from. Can also be a - dictionary or dictionary of dictionaries or lists. + dictionary or dictionary of dictionaries or lists, or an + `~stdatamodels.properties.ObjectNode`. only : str, list of str, or None Update only the named hdu, e.g. ``only='PRIMARY'``. Can either be a string or list of hdu names. If None, all hdus will be updated. diff --git a/src/stdatamodels/model_base.py b/src/stdatamodels/model_base.py index 19719aae5..eaaa54d91 100644 --- a/src/stdatamodels/model_base.py +++ b/src/stdatamodels/model_base.py @@ -881,7 +881,8 @@ def update(self, d, only=None, extra_fits=False): ---------- d : `~jwst.datamodels.DataModel` or dictionary-like object The model to copy the metadata elements from. Can also be a - dictionary or dictionary of dictionaries or lists. + dictionary or dictionary of dictionaries or lists, or an + `~stdatamodels.properties.ObjectNode`. only : str, None Update only the named hdu, e.g. ``only='PRIMARY'``. Can either be a string or list of hdu names. Default is to update all the hdus. From 4c69cf9817a1afc7f48733dca01829ade8e1310d Mon Sep 17 00:00:00 2001 From: Ned Molter Date: Thu, 30 Apr 2026 12:14:31 -0400 Subject: [PATCH 3/3] Respect schema in objectnode case --- src/stdatamodels/jwst/datamodels/model_base.py | 3 ++- src/stdatamodels/model_base.py | 5 ++--- src/stdatamodels/properties.py | 6 ------ tests/jwst/datamodels/test_multislit.py | 10 ++++++++-- 4 files changed, 12 insertions(+), 12 deletions(-) diff --git a/src/stdatamodels/jwst/datamodels/model_base.py b/src/stdatamodels/jwst/datamodels/model_base.py index d6a6ee37d..5c5d52ea3 100644 --- a/src/stdatamodels/jwst/datamodels/model_base.py +++ b/src/stdatamodels/jwst/datamodels/model_base.py @@ -3,6 +3,7 @@ from astropy.time import Time from stdatamodels import DataModel as _DataModel +from stdatamodels import properties from stdatamodels.dynamicdq import dynamic_mask from .dqflags import pixel @@ -90,7 +91,7 @@ def update(self, d, only=None, extra_fits=False, cal_logs=True): Update from ``cal_logs`` as well as ``meta``. """ # Get the cal logs first - if isinstance(d, _DataModel): + if isinstance(d, properties.Node): # Get cal logs if present if d.hasattr("cal_logs"): logs = copy.deepcopy(d.cal_logs._instance) diff --git a/src/stdatamodels/model_base.py b/src/stdatamodels/model_base.py index eaaa54d91..763c1e5e4 100644 --- a/src/stdatamodels/model_base.py +++ b/src/stdatamodels/model_base.py @@ -19,7 +19,6 @@ from . import filetype, fits_support, properties, validate from . import schema as mschema from .history import HistoryList -from .properties import ObjectNode from .util import convert_fitsrec_to_array_in_tree, get_envar_as_boolean, remove_none_from_tree # This minimal schema creates metadata fields that @@ -909,7 +908,7 @@ def hdu_names_from_schema(subschema, path, combiner, ctx, recurse): # Resolve the source dict and, for DataModel input, the set of # schema-approved leaf paths (those with a fits_keyword in the # appropriate HDU). - if isinstance(d, DataModel): + if isinstance(d, properties.Node): hdu_keywords = set() def hdu_keywords_from_schema(subschema, path, combiner, ctx, recurse): @@ -927,7 +926,7 @@ def hdu_keywords_from_schema(subschema, path, combiner, ctx, recurse): # calling ObjectNode.__setattr__ # This triggers validation if validate_on_assignment is True. def assign_leaves(node, path=()): - if isinstance(node, (dict, ObjectNode)): + if isinstance(node, dict): for key, val in node.items(): # skip extra_fits - handled separately below if not path and key == "extra_fits": diff --git a/src/stdatamodels/properties.py b/src/stdatamodels/properties.py index fd56f3e56..01fa86543 100644 --- a/src/stdatamodels/properties.py +++ b/src/stdatamodels/properties.py @@ -440,12 +440,6 @@ def __delattr__(self, attr): def __iter__(self): return NodeIterator(self) - def pop(self, a, b): # noqa: D102 - try: - return self._instance.pop(a) - except KeyError: - return b - def hasattr(self, attr): """ Check if the node has an attribute in its instance. diff --git a/tests/jwst/datamodels/test_multislit.py b/tests/jwst/datamodels/test_multislit.py index e3e484147..d66fe78a3 100644 --- a/tests/jwst/datamodels/test_multislit.py +++ b/tests/jwst/datamodels/test_multislit.py @@ -175,11 +175,17 @@ def test_slit_update_from_multislit(): """Cover a bug where JwstDataModel.update() would raise for ObjectNode input.""" multislit = MultiSlitModel() slit = SlitModel() - slit.meta.telescope = "foo" + # name is in both SlitModel and MultiSlitModel slitdata schema + slit.name = "foo" + # telescope is not in MultiSlitModel slitdata schema + slit.meta.telescope = "bar" multislit.slits.append(slit) slit_objnode = multislit.slits[0] assert isinstance(slit_objnode, ObjectNode) slit2 = SlitModel() slit2.update(slit_objnode) - assert slit2.meta.telescope == "foo" + # schema-defined in both -> updated + assert slit2.name == "foo" + # schema-undefined in source -> not updated + assert slit2.meta.telescope is None