diff --git a/src/pynxtools/data/NXtest.nxdl.xml b/src/pynxtools/data/NXtest.nxdl.xml
index 8695a20c9..45f37896c 100644
--- a/src/pynxtools/data/NXtest.nxdl.xml
+++ b/src/pynxtools/data/NXtest.nxdl.xml
@@ -28,9 +28,13 @@
+
A dummy entry for a float value.
+
+ A dummy entry for a number value.
+
A dummy entry for a bool value.
@@ -53,6 +57,12 @@
+
+
+
+
+
+
diff --git a/src/pynxtools/dataconverter/helpers.py b/src/pynxtools/dataconverter/helpers.py
index 71d4a4b9f..ef20dc178 100644
--- a/src/pynxtools/dataconverter/helpers.py
+++ b/src/pynxtools/dataconverter/helpers.py
@@ -85,7 +85,7 @@ def _log(self, path: str, log_type: ValidationProblem, value: Optional[Any], *ar
)
elif log_type == ValidationProblem.InvalidEnum:
logger.warning(
- f"The value at {path} should be on of the following strings: {value}"
+ f"The value at {path} should be one of the following: {value}"
)
elif log_type == ValidationProblem.MissingRequiredGroup:
logger.warning(f"The required group, {path}, hasn't been supplied.")
@@ -96,7 +96,7 @@ def _log(self, path: str, log_type: ValidationProblem, value: Optional[Any], *ar
)
elif log_type == ValidationProblem.InvalidType:
logger.warning(
- f"The value at {path} should be one of: {value}"
+ f"The value at {path} should be one of the following Python types: {value}"
f", as defined in the NXDL as {args[0] if args else ''}."
)
elif log_type == ValidationProblem.InvalidDatetime:
@@ -158,9 +158,9 @@ def collect_and_log(
"NX_ANY",
):
return
- if self.logging:
+ if self.logging and path + str(log_type) + str(value) not in self.data:
self._log(path, log_type, value, *args, **kwargs)
- self.data.add(path)
+ self.data.add(path + str(log_type) + str(value))
def has_validation_problems(self):
"""Returns True if there were any validation problems."""
@@ -578,66 +578,22 @@ def is_value_valid_element_of_enum(value, elist) -> Tuple[bool, list]:
NUMPY_FLOAT_TYPES = (np.half, np.float16, np.single, np.double, np.longdouble)
NUMPY_INT_TYPES = (np.short, np.intc, np.int_)
NUMPY_UINT_TYPES = (np.ushort, np.uintc, np.uint)
-# np int for np version 1.26.0
-np_int = (
- np.intc,
- np.int_,
- np.intp,
- np.int8,
- np.int16,
- np.int32,
- np.int64,
- np.uint8,
- np.uint16,
- np.uint32,
- np.uint64,
- np.unsignedinteger,
- np.signedinteger,
-)
-np_float = (np.float16, np.float32, np.float64, np.floating)
-np_bytes = (np.bytes_, np.byte, np.ubyte)
-np_char = (np.str_, np.char.chararray, *np_bytes)
-np_bool = (np.bool_,)
-np_complex = (np.complex64, np.complex128, np.cdouble, np.csingle)
+
NEXUS_TO_PYTHON_DATA_TYPES = {
- "ISO8601": (str,),
- "NX_BINARY": (
- bytes,
- bytearray,
- np.ndarray,
- *np_bytes,
- ),
- "NX_BOOLEAN": (bool, np.ndarray, *np_bool),
- "NX_CHAR": (str, np.ndarray, *np_char),
- "NX_DATE_TIME": (str,),
- "NX_FLOAT": (float, np.ndarray, *np_float),
- "NX_INT": (int, np.ndarray, *np_int),
- "NX_UINT": (np.ndarray, np.unsignedinteger),
- "NX_NUMBER": (
- int,
- float,
- np.ndarray,
- *np_int,
- *np_float,
- dict,
- ),
+ "ISO8601": (str),
+ "NX_BINARY": (bytes, bytearray, np.byte, np.ubyte),
+ "NX_BOOLEAN": (bool, np.bool_),
+ "NX_CHAR": (str, np.chararray),
+ "NX_DATE_TIME": (str),
+ "NX_FLOAT": (float, np.floating),
+ "NX_INT": (int, np.integer),
+ "NX_UINT": (np.unsignedinteger),
+ "NX_NUMBER": (int, float, np.integer, np.floating),
"NX_POSINT": (
int,
- np.ndarray,
- np.signedinteger,
+ np.integer,
), # > 0 is checked in is_valid_data_field()
- "NX_COMPLEX": (complex, np.ndarray, *np_complex),
- "NXDL_TYPE_UNAVAILABLE": (str,), # Defaults to a string if a type is not provided.
- "NX_CHAR_OR_NUMBER": (
- str,
- int,
- float,
- np.ndarray,
- *np_char,
- *np_int,
- *np_float,
- dict,
- ),
+ "NXDL_TYPE_UNAVAILABLE": (str), # Defaults to a string if a type is not provided.
}
@@ -650,9 +606,14 @@ def check_all_children_for_callable(objects: list, check: Callable, *args) -> bo
return True
+def is_list_like(object) -> bool:
+ """Checks whether the given object is a list-like object (ndarray, list)."""
+ return isinstance(object, (list, np.ndarray))
+
+
def is_valid_data_type(value, accepted_types):
"""Checks whether the given value or its children are of an accepted type."""
- if not isinstance(value, list):
+ if not is_list_like(value):
return isinstance(value, accepted_types)
return check_all_children_for_callable(value, isinstance, accepted_types)
@@ -664,7 +625,7 @@ def is_positive_int(value):
def is_greater_than(num):
return num.flat[0] > 0 if isinstance(num, np.ndarray) else num > 0
- if isinstance(value, list):
+ if is_list_like(value):
return check_all_children_for_callable(value, is_greater_than)
return value.flat[0] > 0 if isinstance(value, np.ndarray) else value > 0
@@ -700,17 +661,16 @@ def is_valid_data_field(value, nxdl_type, path):
output_value = value
if not isinstance(value, dict) and not is_valid_data_type(value, accepted_types):
- try:
- if accepted_types[0] is bool and isinstance(value, str):
- value = convert_str_to_bool_safe(value)
- if value is None:
- raise ValueError
- output_value = accepted_types[0](value)
- except ValueError:
- collector.collect_and_log(
- path, ValidationProblem.InvalidType, accepted_types, nxdl_type
- )
- return False, value
+ if accepted_types[0] is bool and isinstance(value, str):
+ converted_value = convert_str_to_bool_safe(value)
+ if converted_value is not None:
+ output_value = converted_value
+ return True, converted_value
+
+ collector.collect_and_log(
+ path, ValidationProblem.InvalidType, accepted_types, nxdl_type
+ )
+ return False, value
if nxdl_type == "NX_POSINT" and not is_positive_int(value):
collector.collect_and_log(path, ValidationProblem.IsNotPosInt, value)
diff --git a/src/pynxtools/dataconverter/nexus_tree.py b/src/pynxtools/dataconverter/nexus_tree.py
index bbba22c09..77349df49 100644
--- a/src/pynxtools/dataconverter/nexus_tree.py
+++ b/src/pynxtools/dataconverter/nexus_tree.py
@@ -761,7 +761,7 @@ class NexusEntity(NexusNode):
type: Literal["field", "attribute"]
unit: Optional[NexusUnitCategory] = None
dtype: NexusType = "NX_CHAR"
- items: Optional[List[str]] = None
+ items: Optional[List[Any]] = None
shape: Optional[Tuple[Optional[int], ...]] = None
def _set_type(self):
@@ -790,14 +790,23 @@ def _set_items(self):
based on the values in the inheritance chain.
The first vale found is used.
"""
- if not self.dtype == "NX_CHAR":
- return
for elem in self.inheritance:
enum = elem.find(f"nx:enumeration", namespaces=namespaces)
if enum is not None:
self.items = []
for items in enum.findall(f"nx:item", namespaces=namespaces):
- self.items.append(items.attrib["value"])
+ value = items.attrib["value"]
+ if value[0] == "[" and value[-1] == "]":
+ import ast
+
+ try:
+ self.items.append(ast.literal_eval(value))
+ except (ValueError, SyntaxError):
+ raise Exception(
+ f"Error parsing enumeration item in the provided NXDL: {value}"
+ )
+ else:
+ self.items.append(value)
return
def _set_shape(self):
diff --git a/src/pynxtools/dataconverter/readers/example/reader.py b/src/pynxtools/dataconverter/readers/example/reader.py
index fefe37f5c..7e368a264 100644
--- a/src/pynxtools/dataconverter/readers/example/reader.py
+++ b/src/pynxtools/dataconverter/readers/example/reader.py
@@ -58,7 +58,11 @@ def read(
# outputs with --generate-template for a provided NXDL file
if (
k.startswith("/ENTRY[entry]/required_group")
- or k == "/ENTRY[entry]/optional_parent/req_group_in_opt_group"
+ or k
+ in (
+ "/ENTRY[entry]/optional_parent/req_group_in_opt_group",
+ "/ENTRY[entry]/NXODD_name[nxodd_name]/anamethatRENAMES[anamethatrenames]",
+ )
or k.startswith("/ENTRY[entry]/OPTIONAL_group")
):
continue
diff --git a/src/pynxtools/dataconverter/validation.py b/src/pynxtools/dataconverter/validation.py
index 4b599b43a..7c5410faf 100644
--- a/src/pynxtools/dataconverter/validation.py
+++ b/src/pynxtools/dataconverter/validation.py
@@ -168,7 +168,7 @@ def validate_dict_against(
appdef: str, mapping: Mapping[str, Any], ignore_undocumented: bool = False
) -> Tuple[bool, List]:
"""
- Validates a mapping against the NeXus tree for applicationd definition `appdef`.
+ Validates a mapping against the NeXus tree for application definition `appdef`.
Args:
appdef (str): The appdef name to validate against.
@@ -248,6 +248,14 @@ def check_nxdata():
prev_path=prev_path,
)
+ # check NXdata attributes
+ for attr in ("signal", "auxiliary_signals", "axes"):
+ handle_attribute(
+ node.search_add_child_for(attr),
+ keys,
+ prev_path=prev_path,
+ )
+
for i, axis in enumerate(axes):
if axis == ".":
continue
@@ -392,17 +400,17 @@ def _follow_link(
def handle_field(node: NexusNode, keys: Mapping[str, Any], prev_path: str):
full_path = remove_from_not_visited(f"{prev_path}/{node.name}")
variants = get_variations_of(node, keys)
- if not variants:
- if node.optionality == "required" and node.type in missing_type_err:
- collector.collect_and_log(
- full_path, missing_type_err.get(node.type), None
- )
-
+ if (
+ not variants
+ and node.optionality == "required"
+ and node.type in missing_type_err
+ ):
+ collector.collect_and_log(full_path, missing_type_err.get(node.type), None)
return
for variant in variants:
if node.optionality == "required" and isinstance(keys[variant], Mapping):
- # Check if all fields in the dict are actual attributes (startwith @)
+ # Check if all fields in the dict are actual attributes (startswith @)
all_attrs = True
for entry in keys[variant]:
if not entry.startswith("@"):
@@ -454,17 +462,20 @@ def handle_field(node: NexusNode, keys: Mapping[str, Any], prev_path: str):
prev_path=f"{prev_path}/{variant}",
)
+ remove_from_not_visited(f"{prev_path}/{variant}")
+
# TODO: Build variadic map for fields and attributes
# Introduce variadic siblings in NexusNode?
def handle_attribute(node: NexusNode, keys: Mapping[str, Any], prev_path: str):
full_path = remove_from_not_visited(f"{prev_path}/@{node.name}")
variants = get_variations_of(node, keys)
- if not variants:
- if node.optionality == "required" and node.type in missing_type_err:
- collector.collect_and_log(
- full_path, missing_type_err.get(node.type), None
- )
+ if (
+ not variants
+ and node.optionality == "required"
+ and node.type in missing_type_err
+ ):
+ collector.collect_and_log(full_path, missing_type_err.get(node.type), None)
return
for variant in variants:
@@ -476,6 +487,20 @@ def handle_attribute(node: NexusNode, keys: Mapping[str, Any], prev_path: str):
f"{prev_path}/{variant if variant.startswith('@') else f'@{variant}'}",
)
+ # Check enumeration
+ if (
+ node.items is not None
+ and mapping[
+ f"{prev_path}/{variant if variant.startswith('@') else f'@{variant}'}"
+ ]
+ not in node.items
+ ):
+ collector.collect_and_log(
+ f"{prev_path}/{variant if variant.startswith('@') else f'@{variant}'}",
+ ValidationProblem.InvalidEnum,
+ node.items,
+ )
+
def handle_choice(node: NexusNode, keys: Mapping[str, Any], prev_path: str):
global collector
old_collector = collector
@@ -556,7 +581,7 @@ def check_attributes_of_nonexisting_field(
) -> list:
"""
This method runs through the mapping dictionary and checks if there are any
- attributes assigned to the fields (not groups!) which are not expicitly
+ attributes assigned to the fields (not groups!) which are not explicitly
present in the mapping.
If there are any found, a warning is logged and the corresponding items are
added to the list returned by the method.
@@ -657,7 +682,7 @@ def check_type_with_tree(
if (next_child_class is not None) or (next_child_name is not None):
output = None
for child in node.children:
- # regexs to separarte the class and the name from full name of the child
+ # regexs to separate the class and the name from full name of the child
child_class_from_node = re.sub(
r"(\@.*)*(\[.*?\])*(\(.*?\))*([a-z]\_)*(\_[a-z])*[a-z]*\s*",
"",
diff --git a/tests/data/dataconverter/readers/example/testdata.json b/tests/data/dataconverter/readers/example/testdata.json
index 21deb40c3..e66af9962 100644
--- a/tests/data/dataconverter/readers/example/testdata.json
+++ b/tests/data/dataconverter/readers/example/testdata.json
@@ -7,6 +7,8 @@
"float_value_units": "nm",
"int_value": -3,
"int_value_units": "eV",
+ "number_value": 3,
+ "number_value_units": "eV",
"posint_value": 7,
"posint_value_units": "kg",
"definition": "NXtest",
@@ -17,5 +19,6 @@
"date_value_units": "",
"required_child": 1,
"optional_child": 1,
- "@version": "1.0"
+ "@version": "1.0",
+ "@array": [0, 1, 2]
}
\ No newline at end of file
diff --git a/tests/dataconverter/test_helpers.py b/tests/dataconverter/test_helpers.py
index 0ef64ea0d..eb330fe96 100644
--- a/tests/dataconverter/test_helpers.py
+++ b/tests/dataconverter/test_helpers.py
@@ -96,7 +96,7 @@ def listify_template(data_dict: Template):
"type",
"definition",
"date_value",
- ):
+ ) or isinstance(data_dict[optionality][path], list):
listified_template[optionality][path] = data_dict[optionality][path]
else:
listified_template[optionality][path] = [data_dict[optionality][path]]
@@ -155,6 +155,9 @@ def fixture_filled_test_data(template, tmp_path):
)
template.clear()
+ template[
+ "/ENTRY[my_entry]/NXODD_name[nxodd_name]/anamethatRENAMES[anamethatichangetothis]"
+ ] = 2
template["/ENTRY[my_entry]/NXODD_name[nxodd_name]/float_value"] = 2.0
template["/ENTRY[my_entry]/NXODD_name[nxodd_name]/float_value/@units"] = "nm"
template["/ENTRY[my_entry]/optional_parent/required_child"] = 1
@@ -162,9 +165,9 @@ def fixture_filled_test_data(template, tmp_path):
template["/ENTRY[my_entry]/NXODD_name[nxodd_name]/bool_value"] = True
template["/ENTRY[my_entry]/NXODD_name[nxodd_name]/int_value"] = 2
template["/ENTRY[my_entry]/NXODD_name[nxodd_name]/int_value/@units"] = "eV"
- template["/ENTRY[my_entry]/NXODD_name[nxodd_name]/posint_value"] = np.array(
- [1, 2, 3], dtype=np.int8
- )
+ template["/ENTRY[my_entry]/NXODD_name[nxodd_name]/number_value"] = 2
+ template["/ENTRY[my_entry]/NXODD_name[nxodd_name]/number_value/@units"] = "eV"
+ template["/ENTRY[my_entry]/NXODD_name[nxodd_name]/posint_value"] = 1
template["/ENTRY[my_entry]/NXODD_name[nxodd_name]/posint_value/@units"] = "kg"
template["/ENTRY[my_entry]/NXODD_name[nxodd_name]/char_value"] = "just chars"
template["/ENTRY[my_entry]/definition"] = "NXtest"
@@ -184,6 +187,9 @@ def fixture_filled_test_data(template, tmp_path):
TEMPLATE = Template()
+TEMPLATE["optional"][
+ "/ENTRY[my_entry]/NXODD_name[nxodd_name]/anamethatRENAMES[anamethatichangetothis]"
+] = 2
TEMPLATE["optional"]["/ENTRY[my_entry]/NXODD_name[nxodd_name]/float_value"] = 2.0 # pylint: disable=E1126
TEMPLATE["optional"]["/ENTRY[my_entry]/NXODD_name[nxodd_name]/float_value/@units"] = (
"nm" # pylint: disable=E1126
@@ -194,10 +200,11 @@ def fixture_filled_test_data(template, tmp_path):
TEMPLATE["required"]["/ENTRY[my_entry]/NXODD_name[nxodd_name]/bool_value/@units"] = ""
TEMPLATE["required"]["/ENTRY[my_entry]/NXODD_name[nxodd_name]/int_value"] = 2 # pylint: disable=E1126
TEMPLATE["required"]["/ENTRY[my_entry]/NXODD_name[nxodd_name]/int_value/@units"] = "eV" # pylint: disable=E1126
-TEMPLATE["required"]["/ENTRY[my_entry]/NXODD_name[nxodd_name]/posint_value"] = np.array(
- [1, 2, 3], # pylint: disable=E1126
- dtype=np.int8,
-) # pylint: disable=E1126
+TEMPLATE["required"]["/ENTRY[my_entry]/NXODD_name[nxodd_name]/number_value"] = 2
+TEMPLATE["required"]["/ENTRY[my_entry]/NXODD_name[nxodd_name]/number_value/@units"] = (
+ "eV"
+)
+TEMPLATE["required"]["/ENTRY[my_entry]/NXODD_name[nxodd_name]/posint_value"] = 1
TEMPLATE["required"]["/ENTRY[my_entry]/NXODD_name[nxodd_name]/posint_value/@units"] = (
"kg" # pylint: disable=E1126
)
@@ -209,16 +216,14 @@ def fixture_filled_test_data(template, tmp_path):
TEMPLATE["required"][
"/ENTRY[my_entry]/NXODD_name[nxodd_two_name]/bool_value/@units"
] = ""
+TEMPLATE["required"][
+ "/ENTRY[my_entry]/NXODD_name[nxodd_two_name]/anamethatRENAMES[anamethatichangetothis]"
+] = 2 # pylint: disable=E1126
TEMPLATE["required"]["/ENTRY[my_entry]/NXODD_name[nxodd_two_name]/int_value"] = 2 # pylint: disable=E1126
TEMPLATE["required"]["/ENTRY[my_entry]/NXODD_name[nxodd_two_name]/int_value/@units"] = (
"eV" # pylint: disable=E1126
)
-TEMPLATE["required"]["/ENTRY[my_entry]/NXODD_name[nxodd_two_name]/posint_value"] = (
- np.array(
- [1, 2, 3], # pylint: disable=E1126
- dtype=np.int8,
- )
-) # pylint: disable=E1126
+TEMPLATE["required"]["/ENTRY[my_entry]/NXODD_name[nxodd_two_name]/posint_value"] = 1 # pylint: disable=E1126
TEMPLATE["required"][
"/ENTRY[my_entry]/NXODD_name[nxodd_two_name]/posint_value/@units"
] = "kg" # pylint: disable=E1126
@@ -229,6 +234,11 @@ def fixture_filled_test_data(template, tmp_path):
"/ENTRY[my_entry]/NXODD_name[nxodd_two_name]/char_value/@units"
] = ""
TEMPLATE["required"]["/ENTRY[my_entry]/NXODD_name[nxodd_two_name]/type"] = "2nd type" # pylint: disable=E1126
+TEMPLATE["required"]["/ENTRY[my_entry]/NXODD_name[nxodd_two_name]/type/@array"] = [
+ 0,
+ 1,
+ 2,
+]
TEMPLATE["required"]["/ENTRY[my_entry]/NXODD_name[nxodd_two_name]/date_value"] = (
"2022-01-22T12:14:12.05018+00:00" # pylint: disable=E1126
)
@@ -240,6 +250,7 @@ def fixture_filled_test_data(template, tmp_path):
TEMPLATE["required"]["/ENTRY[my_entry]/definition/@version"] = "2.4.6" # pylint: disable=E1126
TEMPLATE["required"]["/ENTRY[my_entry]/program_name"] = "Testing program" # pylint: disable=E1126
TEMPLATE["required"]["/ENTRY[my_entry]/NXODD_name[nxodd_name]/type"] = "2nd type" # pylint: disable=E1126
+TEMPLATE["required"]["/ENTRY[my_entry]/NXODD_name[nxodd_name]/type/@array"] = [0, 1, 2]
TEMPLATE["required"]["/ENTRY[my_entry]/NXODD_name[nxodd_name]/date_value"] = (
"2022-01-22T12:14:12.05018+00:00" # pylint: disable=E1126
)
@@ -271,6 +282,19 @@ def fixture_filled_test_data(template, tmp_path):
@pytest.mark.parametrize(
"data_dict,error_message",
[
+ pytest.param(
+ alter_dict(
+ TEMPLATE,
+ "/ENTRY[my_entry]/NXODD_name[nxodd_name]/anamethatRENAMES[anamethatichangetothis]",
+ "not_a_num",
+ ),
+ (
+ "The value at /ENTRY[my_entry]/NXODD_name[nxodd_name]/anamethatRENAMES[anamethatichangetothis]"
+ " should be one of the following Python types: (, ), as defined in "
+ "the NXDL as NX_INT."
+ ),
+ id="variadic-field-str-instead-of-int",
+ ),
pytest.param(
alter_dict(
TEMPLATE,
@@ -279,13 +303,7 @@ def fixture_filled_test_data(template, tmp_path):
),
(
"The value at /ENTRY[my_entry]/NXODD_name[nxodd_name]/in"
- "t_value should be one of: (, , , ,"
- " , , , , , "
- ", , , , , ), as defined in "
+ "t_value should be one of the following Python types: (, ), as defined in "
"the NXDL as NX_INT."
),
id="string-instead-of-int",
@@ -297,11 +315,107 @@ def fixture_filled_test_data(template, tmp_path):
"NOT_TRUE_OR_FALSE",
),
(
- "The value at /ENTRY[my_entry]/NXODD_name[nxodd_name]/bool_value sh"
- "ould be one of: (, , , ), as defined in the NXDL as NX_BOOLEAN."
),
- id="string-instead-of-int",
+ id="string-instead-of-bool",
+ ),
+ pytest.param(
+ alter_dict(
+ TEMPLATE,
+ "/ENTRY[my_entry]/NXODD_name[nxodd_name]/int_value",
+ ["1", "2", "3"],
+ ),
+ (
+ "The value at /ENTRY[my_entry]/NXODD_name[nxodd_name]/int_value should"
+ " be one of the following Python types: (, ), as defined in the NXDL as NX_INT."
+ ),
+ id="list-of-int-str-instead-of-int",
+ ),
+ pytest.param(
+ alter_dict(
+ TEMPLATE,
+ "/ENTRY[my_entry]/NXODD_name[nxodd_name]/int_value",
+ np.array([2.0, 3.0, 4.0], dtype=np.float32),
+ ),
+ (
+ "The value at /ENTRY[my_entry]/NXODD_name[nxodd_name]/int_value should be"
+ " one of the following Python types: (, ), as defined in the NXDL as NX_INT."
+ ),
+ id="array-of-float-instead-of-int",
+ ),
+ pytest.param(
+ alter_dict(
+ TEMPLATE,
+ "/ENTRY[my_entry]/NXODD_name[nxodd_name]/int_value",
+ [2, 3, 4],
+ ),
+ (""),
+ id="list-of-int-instead-of-int",
+ ),
+ pytest.param(
+ alter_dict(
+ TEMPLATE,
+ "/ENTRY[my_entry]/NXODD_name[nxodd_name]/int_value",
+ np.array([2, 3, 4], dtype=np.int32),
+ ),
+ (""),
+ id="array-of-int32-instead-of-int",
+ ),
+ pytest.param(
+ alter_dict(
+ TEMPLATE,
+ "/ENTRY[my_entry]/NXODD_name[nxodd_name]/date_value",
+ "2022-01-22T12:14:12.05018-00:00",
+ ),
+ "The value at /ENTRY[my_entry]/NXODD_name[nxodd_name]/date_value"
+ " = 2022-01-22T12:14:12.05018-00:00 should be a timezone aware"
+ " ISO8601 formatted str. For example, 2022-01-22T12:14:12.05018Z or 2022-01-22"
+ "T12:14:12.05018+00:00.",
+ id="int-instead-of-date",
+ ),
+ pytest.param(
+ alter_dict(
+ TEMPLATE,
+ "/ENTRY[my_entry]/NXODD_name[nxodd_name]/float_value",
+ 0,
+ ),
+ (
+ "The value at /ENTRY[my_entry]/NXODD_name[nxodd_name]/float_value should be one of the following Python types: (, ), as defined in the NXDL as NX_FLOAT."
+ ),
+ id="int-instead-of-float",
+ ),
+ pytest.param(
+ alter_dict(
+ TEMPLATE,
+ "/ENTRY[my_entry]/NXODD_name[nxodd_name]/number_value",
+ "0",
+ ),
+ (
+ "The value at /ENTRY[my_entry]/NXODD_name[nxodd_name]/number_value should be one of the following Python types: (, , , ), as defined in the NXDL as NX_NUMBER."
+ ),
+ id="str-instead-of-number",
+ ),
+ pytest.param(
+ alter_dict(
+ TEMPLATE,
+ "/ENTRY[my_entry]/NXODD_name[nxodd_name]/char_value",
+ np.array([0.0, 2]),
+ ),
+ (
+ "The value at /ENTRY[my_entry]/NXODD_name[nxodd_name]/char_value should be one"
+ " of the following Python types: (, ), as"
+ " defined in the NXDL as NX_CHAR."
+ ),
+ id="wrong-type-ndarray-instead-of-char",
+ ),
+ pytest.param(
+ alter_dict(
+ TEMPLATE,
+ "/ENTRY[my_entry]/NXODD_name[nxodd_name]/char_value",
+ np.array(["x", "2"]),
+ ),
+ (""),
+ id="valid-ndarray-instead-of-char",
),
pytest.param(
alter_dict(
@@ -327,8 +441,8 @@ def fixture_filled_test_data(template, tmp_path):
TEMPLATE, "/ENTRY[my_entry]/NXODD_name[nxodd_name]/char_value", 3
),
(
- "The value at /ENTRY[my_entry]/NXODD_name[nxodd_name]/char_value should be of Python type:"
- " (, , ),"
+ "The value at /ENTRY[my_entry]/NXODD_name[nxodd_name]/char_value should be one of the following Python types:"
+ " (, ),"
" as defined in the NXDL as NX_CHAR."
),
id="int-instead-of-chars",
@@ -433,8 +547,8 @@ def fixture_filled_test_data(template, tmp_path):
),
(
"The value at /ENTRY[my_entry]/NXODD_name[nxodd_name]/type should "
- "be on of the following"
- " strings: ['1st type', '2nd type', '3rd type', '4th type']"
+ "be one of the following"
+ ": ['1st type', '2nd type', '3rd type', '4th type']"
),
id="wrong-enum-choice",
),
@@ -519,10 +633,34 @@ def fixture_filled_test_data(template, tmp_path):
pytest.param(
remove_optional_parent(TEMPLATE), (""), id="opt-group-completely-removed"
),
+ pytest.param(
+ alter_dict(
+ TEMPLATE,
+ "/ENTRY[my_entry]/NXODD_name[nxodd_name]/type/@array",
+ ["0", 1, 2],
+ ),
+ (
+ "The value at /ENTRY[my_entry]/NXODD_name[nxodd_name]/type/@array should be one of the following: [[0, 1, 2], [2, 3, 4]]"
+ ),
+ id="wrong-type-array-in-attribute",
+ ),
+ pytest.param(
+ alter_dict(
+ TEMPLATE, "/ENTRY[my_entry]/NXODD_name[nxodd_name]/type/@array", [1, 2]
+ ),
+ (
+ "The value at /ENTRY[my_entry]/NXODD_name[nxodd_name]/type/@array should be one of the following: [[0, 1, 2], [2, 3, 4]]"
+ ),
+ id="wrong-value-array-in-attribute",
+ ),
],
)
def test_validate_data_dict(caplog, data_dict, error_message, request):
"""Unit test for the data validation routine."""
+
+ def format_error_message(msg: str) -> str:
+ return msg[msg.rfind("G: ") + 3 :].rstrip("\n")
+
if request.node.callspec.id in (
"valid-data-dict",
"lists",
@@ -530,10 +668,12 @@ def test_validate_data_dict(caplog, data_dict, error_message, request):
"UTC-with-+00:00",
"UTC-with-Z",
"no-child-provided-optional-parent",
- "int-instead-of-chars",
"link-dict-instead-of-bool",
"opt-group-completely-removed",
"required-field-provided-in-variadic-optional-group",
+ "valid-ndarray-instead-of-char",
+ "list-of-int-instead-of-int",
+ "array-of-int32-instead-of-int",
):
with caplog.at_level(logging.WARNING):
assert validate_dict_against("NXtest", data_dict)[0]
@@ -549,12 +689,15 @@ def test_validate_data_dict(caplog, data_dict, error_message, request):
assert "" == caplog.text
captured_logs = caplog.records
assert not validate_dict_against("NXtest", data_dict)[0]
- assert any(error_message in rec.message for rec in captured_logs)
+ assert any(
+ error_message == format_error_message(rec.message) for rec in captured_logs
+ )
else:
with caplog.at_level(logging.WARNING):
assert not validate_dict_against("NXtest", data_dict)[0]
-
- assert error_message in caplog.text
+ assert any(
+ error_message == format_error_message(rec.message) for rec in caplog.records
+ )
@pytest.mark.parametrize(
diff --git a/tests/dataconverter/test_validation.py b/tests/dataconverter/test_validation.py
index 2c946a3a1..8bb892998 100644
--- a/tests/dataconverter/test_validation.py
+++ b/tests/dataconverter/test_validation.py
@@ -28,6 +28,7 @@ def get_data_dict():
return {
"/ENTRY[my_entry]/optional_parent/required_child": 1,
"/ENTRY[my_entry]/optional_parent/optional_child": 1,
+ "/ENTRY[my_entry]/NXODD_name[nxodd_name]/anamethatRENAMES[anamethatichangetothis]": 2,
"/ENTRY[my_entry]/NXODD_name[nxodd_name]/float_value_no_attr": 2.0,
"/ENTRY[my_entry]/NXODD_name[nxodd_name]/float_value": 2.0,
"/ENTRY[my_entry]/NXODD_name[nxodd_name]/float_value/@units": "nm",
@@ -42,8 +43,10 @@ def get_data_dict():
"/ENTRY[my_entry]/NXODD_name[nxodd_name]/char_value": "just chars",
"/ENTRY[my_entry]/NXODD_name[nxodd_name]/char_value/@units": "",
"/ENTRY[my_entry]/NXODD_name[nxodd_name]/type": "2nd type",
+ "/ENTRY[my_entry]/NXODD_name[nxodd_name]/type/@array": [0, 1, 2],
"/ENTRY[my_entry]/NXODD_name[nxodd_name]/date_value": "2022-01-22T12:14:12.05018+00:00",
"/ENTRY[my_entry]/NXODD_name[nxodd_name]/date_value/@units": "",
+ "/ENTRY[my_entry]/NXODD_name[nxodd_two_name]/anamethatRENAMES[anamethatichangetothis]": 2,
"/ENTRY[my_entry]/NXODD_name[nxodd_two_name]/bool_value": True,
"/ENTRY[my_entry]/NXODD_name[nxodd_two_name]/bool_value/@units": "",
"/ENTRY[my_entry]/NXODD_name[nxodd_two_name]/int_value": 2,
@@ -55,6 +58,7 @@ def get_data_dict():
"/ENTRY[my_entry]/NXODD_name[nxodd_two_name]/char_value": "just chars",
"/ENTRY[my_entry]/NXODD_name[nxodd_two_name]/char_value/@units": "",
"/ENTRY[my_entry]/NXODD_name[nxodd_two_name]/type": "2nd type",
+ "/ENTRY[my_entry]/NXODD_name[nxodd_two_name]/type/@array": [0, 1, 2],
"/ENTRY[my_entry]/NXODD_name[nxodd_two_name]/date_value": "2022-01-22T12:14:12.05018+00:00",
"/ENTRY[my_entry]/NXODD_name[nxodd_two_name]/date_value/@units": "",
"/ENTRY[my_entry]/OPTIONAL_group[my_group]/required_field": 1,
diff --git a/tests/dataconverter/test_writer.py b/tests/dataconverter/test_writer.py
index acc84d8d5..2a3a8594a 100644
--- a/tests/dataconverter/test_writer.py
+++ b/tests/dataconverter/test_writer.py
@@ -56,7 +56,6 @@ def test_write(writer):
test_nxs = h5py.File(writer.output_path, "r")
assert test_nxs["/my_entry/nxodd_name/int_value"][()] == 2
assert test_nxs["/my_entry/nxodd_name/int_value"].attrs["units"] == "eV"
- assert test_nxs["/my_entry/nxodd_name/posint_value"].shape == (3,) # pylint: disable=no-member
def test_write_link(writer):