Skip to content

Commit a79ea7b

Browse files
committed
search for custom attribute with open enum
1 parent bd5c92e commit a79ea7b

File tree

4 files changed

+166
-18
lines changed

4 files changed

+166
-18
lines changed

src/pynxtools/data/NXtest.nxdl.xml

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -99,6 +99,12 @@
9999
<item value="1st type open"/>
100100
<item value="2nd type open"/>
101101
</enumeration>
102+
<attribute name="attribute_with_open_enum" optional="true">
103+
<enumeration open="true">
104+
<item value="1st option"/>
105+
<item value="2nd option"/>
106+
</enumeration>
107+
</attribute>
102108
</field>
103109
<attribute name="group_attribute">
104110
</attribute>

src/pynxtools/dataconverter/helpers.py

Lines changed: 17 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -57,7 +57,8 @@ class ValidationProblem(Enum):
5757
UnitWithoutDocumentation = auto()
5858
InvalidUnit = auto()
5959
InvalidEnum = auto()
60-
OpenEnumWithNewItem = auto()
60+
OpenEnumWithCorrectNewItem = auto()
61+
OpenEnumWithIncorrectNewItem = auto()
6162
MissingRequiredGroup = auto()
6263
MissingRequiredField = auto()
6364
MissingRequiredAttribute = auto()
@@ -120,10 +121,21 @@ def _log(self, path: str, log_type: ValidationProblem, value: Optional[Any], *ar
120121
logger.warning(
121122
f"The value at {path} should be one of the following: {value}."
122123
)
123-
elif log_type == ValidationProblem.OpenEnumWithNewItem:
124+
elif log_type == ValidationProblem.OpenEnumWithCorrectNewItem:
124125
logger.info(
125-
f"The value at {path} does not match with the enumerated items from the open enumeration: {value}."
126+
f"The value '{args[0]}' at {path} does not match with the enumerated items from the open enumeration: {value}."
126127
)
128+
elif log_type == ValidationProblem.OpenEnumWithIncorrectNewItem:
129+
actual_value, custom_attr = args
130+
131+
log_text = f"The value '{actual_value}' at {path} does not match with the enumerated items from the open enumeration: {value}."
132+
if custom_attr == "custom_missing":
133+
log_text += f" When a different value is used, a boolean 'custom' attribute must be added."
134+
logger.warning(log_text)
135+
elif custom_attr == "custom_false":
136+
log_text += f" When a different value is used, the boolean 'custom' attribute cannot be False."
137+
logger.warning(log_text)
138+
127139
elif log_type == ValidationProblem.MissingRequiredGroup:
128140
logger.warning(f"The required group, {path}, hasn't been supplied.")
129141
elif log_type == ValidationProblem.MissingRequiredField:
@@ -254,6 +266,7 @@ def collect_and_log(
254266
ValidationProblem.UnitWithoutDocumentation,
255267
ValidationProblem.OpenEnumWithNewItem,
256268
ValidationProblem.CompressionStrengthZero,
269+
ValidationProblem.OpenEnumWithCorrectNewItem,
257270
):
258271
self.data.add(path + str(log_type) + str(value))
259272

@@ -761,9 +774,7 @@ def convert_int_to_float(value):
761774
return value
762775

763776

764-
def is_valid_data_field(
765-
value: Any, nxdl_type: str, nxdl_enum: list, nxdl_enum_open: bool, path: str
766-
) -> Any:
777+
def is_valid_data_field(value: Any, nxdl_type: str, path: str) -> Any:
767778
"""Checks whether a given value is valid according to the type defined in the NXDL."""
768779

769780
def validate_data_value(

src/pynxtools/dataconverter/validation.py

Lines changed: 76 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -593,7 +593,7 @@ def _follow_link(
593593
resolved_keys[key] = current_keys
594594
return resolved_keys
595595

596-
def handle_field(node: NexusNode, keys: Mapping[str, Any], prev_path: str):
596+
def handle_field(node: NexusEntity, keys: Mapping[str, Any], prev_path: str):
597597
full_path = remove_from_not_visited(f"{prev_path}/{node.name}")
598598
variants = get_variations_of(node, keys)
599599
if (
@@ -651,13 +651,18 @@ def handle_field(node: NexusNode, keys: Mapping[str, Any], prev_path: str):
651651
mapping[variant_path] = is_valid_data_field(
652652
mapping[variant_path],
653653
node.dtype,
654+
variant_path,
655+
)
656+
is_valid_enum(
657+
mapping[variant_path],
654658
node.items,
655659
node.open_enum,
656660
variant_path,
661+
mapping,
657662
)
658663

659-
_ = check_reserved_suffix(variant_path, mapping)
660-
_ = check_reserved_prefix(variant_path, mapping, "field")
664+
check_reserved_suffix(variant_path, mapping)
665+
check_reserved_prefix(variant_path, mapping, "field")
661666

662667
# Check unit category
663668
if node.unit is not None:
@@ -695,7 +700,7 @@ def handle_field(node: NexusNode, keys: Mapping[str, Any], prev_path: str):
695700
prev_path=variant_path,
696701
)
697702

698-
def handle_attribute(node: NexusNode, keys: Mapping[str, Any], prev_path: str):
703+
def handle_attribute(node: NexusEntity, keys: Mapping[str, Any], prev_path: str):
699704
full_path = remove_from_not_visited(f"{prev_path}/@{node.name}")
700705
variants = get_variations_of(node, keys)
701706

@@ -716,11 +721,16 @@ def handle_attribute(node: NexusNode, keys: Mapping[str, Any], prev_path: str):
716721
f"{prev_path}/{variant if variant.startswith('@') else f'@{variant}'}"
717722
],
718723
node.dtype,
724+
variant_path,
725+
)
726+
is_valid_enum(
727+
mapping[variant_path],
719728
node.items,
720729
node.open_enum,
721730
variant_path,
731+
mapping,
722732
)
723-
_ = check_reserved_prefix(variant_path, mapping, "attribute")
733+
check_reserved_prefix(variant_path, mapping, "attribute")
724734

725735
def handle_choice(node: NexusNode, keys: Mapping[str, Any], prev_path: str):
726736
global collector
@@ -866,7 +876,16 @@ def is_documented(key: str, tree: NexusNode) -> bool:
866876
# keys_to_remove.append(key)
867877
# return False
868878
resolved_link[key] = is_valid_data_field(
869-
resolved_link[key], node.dtype, node.items, node.open_enum, key
879+
resolved_link[key],
880+
node.dtype,
881+
key,
882+
)
883+
is_valid_enum(
884+
resolved_link[key],
885+
node.items,
886+
node.open_enum,
887+
key,
888+
mapping,
870889
)
871890

872891
return True
@@ -881,7 +900,16 @@ def is_documented(key: str, tree: NexusNode) -> bool:
881900

882901
# Check general validity
883902
mapping[key] = is_valid_data_field(
884-
mapping[key], node.dtype, node.items, node.open_enum, key
903+
mapping[key],
904+
node.dtype,
905+
key,
906+
)
907+
is_valid_enum(
908+
mapping[key],
909+
node.items,
910+
node.open_enum,
911+
key,
912+
mapping,
885913
)
886914

887915
# Check main field exists for units
@@ -1206,6 +1234,47 @@ def startswith_with_variations(
12061234
# default
12071235
return (False, 0)
12081236

1237+
def is_valid_enum(
1238+
value: Any,
1239+
nxdl_enum: list,
1240+
nxdl_enum_open: bool,
1241+
path: str,
1242+
mapping: MutableMapping,
1243+
):
1244+
# Check enumeration
1245+
if nxdl_enum is not None and value not in nxdl_enum:
1246+
if nxdl_enum_open:
1247+
if path.split("/")[-1].startswith("@"):
1248+
attr_name = path.split("/")[-1][1:] # remove "@"
1249+
custom_path = f"{path}_custom"
1250+
else:
1251+
custom_path = f"{path}/@custom"
1252+
1253+
custom_attr = mapping.get(custom_path)
1254+
remove_from_not_visited(custom_path)
1255+
1256+
if custom_attr is True:
1257+
collector.collect_and_log(
1258+
path,
1259+
ValidationProblem.OpenEnumWithCorrectNewItem,
1260+
nxdl_enum,
1261+
value,
1262+
)
1263+
else:
1264+
collector.collect_and_log(
1265+
path,
1266+
ValidationProblem.OpenEnumWithIncorrectNewItem,
1267+
nxdl_enum,
1268+
value,
1269+
"custom_false" if custom_attr is False else "custom_missing",
1270+
)
1271+
else:
1272+
collector.collect_and_log(
1273+
path,
1274+
ValidationProblem.InvalidEnum,
1275+
nxdl_enum,
1276+
)
1277+
12091278
def check_reserved_suffix(key: str, mapping: MutableMapping[str, Any]) -> bool:
12101279
"""
12111280
Check if an associated field exists for a key with a reserved suffix.

tests/dataconverter/test_validation.py

Lines changed: 67 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -832,16 +832,78 @@ def listify_template(data_dict: Template):
832832
),
833833
pytest.param(
834834
alter_dict(
835-
TEMPLATE,
836-
"/ENTRY[my_entry]/NXODD_name[nxodd_name]/type2",
837-
"a very different type",
835+
alter_dict(
836+
alter_dict(
837+
alter_dict(
838+
TEMPLATE,
839+
"/ENTRY[my_entry]/NXODD_name[nxodd_name]/type2",
840+
"a very different type",
841+
),
842+
"/ENTRY[my_entry]/NXODD_name[nxodd_name]/type2/@custom",
843+
True,
844+
),
845+
"/ENTRY[my_entry]/NXODD_name[nxodd_name]/type2/@attribute_with_open_enum",
846+
"3rd option",
847+
),
848+
"/ENTRY[my_entry]/NXODD_name[nxodd_name]/type2/@attribute_with_open_enum_custom",
849+
True,
838850
),
839851
[
840-
"The value at /ENTRY[my_entry]/NXODD_name[nxodd_name]/type2 does not match with the "
841-
"enumerated items from the open enumeration: ['1st type open', '2nd type open']."
852+
"The value 'a very different type' at /ENTRY[my_entry]/NXODD_name[nxodd_name]/type2 does not match "
853+
"with the enumerated items from the open enumeration: ['1st type open', '2nd type open'].",
854+
"The value '3rd option' at /ENTRY[my_entry]/NXODD_name[nxodd_name]/type2/@attribute_with_open_enum "
855+
"does not match with the enumerated items from the open enumeration: ['1st option', '2nd option'].",
842856
],
843857
id="open-enum-with-new-item",
844858
),
859+
pytest.param(
860+
alter_dict(
861+
alter_dict(
862+
alter_dict(
863+
alter_dict(
864+
TEMPLATE,
865+
"/ENTRY[my_entry]/NXODD_name[nxodd_name]/type2",
866+
"a very different type",
867+
),
868+
"/ENTRY[my_entry]/NXODD_name[nxodd_name]/type2/@custom",
869+
False,
870+
),
871+
"/ENTRY[my_entry]/NXODD_name[nxodd_name]/type2/@attribute_with_open_enum",
872+
"3rd option",
873+
),
874+
"/ENTRY[my_entry]/NXODD_name[nxodd_name]/type2/@attribute_with_open_enum_custom",
875+
False,
876+
),
877+
[
878+
"The value 'a very different type' at /ENTRY[my_entry]/NXODD_name[nxodd_name]/type2 does not match "
879+
"with the enumerated items from the open enumeration: ['1st type open', '2nd type open']. "
880+
"When a different value is used, the boolean 'custom' attribute cannot be False.",
881+
"The value '3rd option' at /ENTRY[my_entry]/NXODD_name[nxodd_name]/type2/@attribute_with_open_enum "
882+
"does not match with the enumerated items from the open enumeration: ['1st option', '2nd option']. "
883+
"When a different value is used, the boolean 'custom' attribute cannot be False.",
884+
],
885+
id="open-enum-with-new-item-custom-false",
886+
),
887+
pytest.param(
888+
alter_dict(
889+
alter_dict(
890+
TEMPLATE,
891+
"/ENTRY[my_entry]/NXODD_name[nxodd_name]/type2",
892+
"a very different type",
893+
),
894+
"/ENTRY[my_entry]/NXODD_name[nxodd_name]/type2/@attribute_with_open_enum",
895+
"3rd option",
896+
),
897+
[
898+
"The value 'a very different type' at /ENTRY[my_entry]/NXODD_name[nxodd_name]/type2 does not match "
899+
"with the enumerated items from the open enumeration: ['1st type open', '2nd type open']. "
900+
"When a different value is used, a boolean 'custom' attribute must be added.",
901+
"The value '3rd option' at /ENTRY[my_entry]/NXODD_name[nxodd_name]/type2/@attribute_with_open_enum "
902+
"does not match with the enumerated items from the open enumeration: ['1st option', '2nd option']. "
903+
"When a different value is used, a boolean 'custom' attribute must be added.",
904+
],
905+
id="open-enum-with-new-item-custom-missing",
906+
),
845907
pytest.param(
846908
set_to_none_in_dict(
847909
TEMPLATE, "/ENTRY[my_entry]/optional_parent/required_child", "required"

0 commit comments

Comments
 (0)