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):