Skip to content

Commit

Permalink
RCAL-695 generalize attribute assignment and creation (#284)
Browse files Browse the repository at this point in the history
Co-authored-by: pre-commit-ci[bot] <66853113+pre-commit-ci[bot]@users.noreply.github.com>
Co-authored-by: William Jamieson <[email protected]>
  • Loading branch information
3 people authored Nov 20, 2023
1 parent 14beded commit 3056adc
Show file tree
Hide file tree
Showing 3 changed files with 67 additions and 3 deletions.
3 changes: 2 additions & 1 deletion CHANGES.rst
Original file line number Diff line number Diff line change
@@ -1,7 +1,8 @@
0.18.1 (unreleased)
===================

-
- Allow assignment to or creation of node attributes using dot notation of object instances
with validation. [#284]

0.18.0 (2023-11-06)
===================
Expand Down
41 changes: 40 additions & 1 deletion src/roman_datamodels/stnode/_node.py
Original file line number Diff line number Diff line change
Expand Up @@ -94,6 +94,33 @@ def _get_schema_for_property(schema, attr):
return {}


def _get_attributes_from_schema(schema):
explicit_properties = schema.get("properties", {}).keys()
patterns = schema.get("patternProperties", {})
return SchemaProperties(explicit_properties, patterns)


class SchemaProperties:
"""
This class provides the capability for using the "contains" machinery
so that an attribute can be tested against patternProperties as well
as whether the attribute is explicitly a property of the schema.
"""

def __init__(self, explicit_properties, patterns):
self.explicit_properties = explicit_properties
self.patterns = patterns

def __contains__(self, attr):
if attr in self.explicit_properties:
return True
else:
for key in self.patterns.keys():
if re.match(key, attr):
return True
return False


class DNode(MutableMapping):
"""
Base class describing all "object" (dict-like) data nodes for STNode classes.
Expand All @@ -113,6 +140,7 @@ def __init__(self, node=None, parent=None, name=None):
self._schema_uri = None
self._parent = parent
self._name = name
self._x_schema_attributes = None

@property
def ctx(self):
Expand Down Expand Up @@ -153,7 +181,7 @@ def __setattr__(self, key, value):

if key[0] != "_":
value = self._convert_to_scalar(key, value)
if key in self._data:
if key in self._data or key in self._schema_attributes():
if will_validate():
schema = _get_schema_for_property(self._schema(), key)
if schema:
Expand All @@ -164,6 +192,17 @@ def __setattr__(self, key, value):
else:
self.__dict__[key] = value

def _schema_attributes(self):
if self._x_schema_attributes is None:
self._x_schema_attributes = self._get_schema_attributes()
return self._x_schema_attributes

def _get_schema_attributes(self):
"""
Extract all schema attributes for this node.
"""
return _get_attributes_from_schema(self._schema())

def to_flat_dict(self, include_arrays=True):
"""
Returns a dictionary of all of the schema items as a flat dictionary.
Expand Down
26 changes: 25 additions & 1 deletion tests/test_stnode.py
Original file line number Diff line number Diff line change
Expand Up @@ -4,8 +4,12 @@
import asdf
import astropy.units as u
import pytest
from asdf.exceptions import ValidationError

from roman_datamodels import datamodels, maker_utils, stnode, validate
from roman_datamodels import datamodels
from roman_datamodels import maker_utils
from roman_datamodels import maker_utils as utils
from roman_datamodels import stnode, validate
from roman_datamodels.testing import assert_node_equal, assert_node_is_copy, wraps_hashable

from .conftest import MANIFEST
Expand Down Expand Up @@ -181,6 +185,26 @@ def test_set_pattern_properties():
mdl.phot_table.F062.pixelareasr = None


# Test that a currently undefined attribute can be assigned using dot notation
# so long as the attribute is defined in the corresponding schema.
def test_node_new_attribute_assignment():
exp = stnode.Exposure()
with pytest.raises(AttributeError):
exp.bozo = 0
exp.ngroups = 5
assert exp.ngroups == 5
# Test patternProperties attribute case
photmod = utils.mk_wfi_img_photom()
phottab = photmod.phot_table
newphottab = {"F062": phottab["F062"]}
photmod.phot_table = newphottab
photmod.phot_table.F213 = phottab["F213"]
with pytest.raises(AttributeError):
photmod.phot_table.F214 = phottab["F213"]
with pytest.raises(ValidationError):
photmod.phot_table.F106 = 0


VALIDATION_CASES = ("true", "yes", "1", "True", "Yes", "TrUe", "YeS", "foo", "Bar", "BaZ")


Expand Down

0 comments on commit 3056adc

Please sign in to comment.