From 198eecbf0bc29eefeecfb4bcc4b322b56fc6da90 Mon Sep 17 00:00:00 2001 From: "i.nueske" <i.nueske@indiscale.com> Date: Sun, 15 Dec 2024 19:58:35 +0100 Subject: [PATCH] ENH: XLSXConverter now checks the paths in the given workbook for validity: - New method XLSXConverter._check_path_validity() called in __init__ - New test with test data containing various incorrect paths - Updated tests and test data to reflect new behaviour --- CHANGELOG.md | 1 + .../table_json_conversion/convert.py | 115 +++++++++++++++--- .../data/simple_data_broken.xlsx | Bin 9299 -> 9126 bytes .../data/simple_data_broken_paths_2.xlsx | Bin 0 -> 8838 bytes .../table_json_conversion/test_read_xlsx.py | 75 ++++++++---- 5 files changed, 149 insertions(+), 42 deletions(-) create mode 100644 unittests/table_json_conversion/data/simple_data_broken_paths_2.xlsx diff --git a/CHANGELOG.md b/CHANGELOG.md index 6a118d98..6628f1a7 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -33,6 +33,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 ### Fixed ### - Yaml data model parser adds data types of properties of record types and other attributes which fixes https://gitlab.indiscale.com/caosdb/customers/f-fit/management/-/issues/58 +- `XLSXConverter` now checks path validity before parsing the worksheet. ### Security ### diff --git a/src/caosadvancedtools/table_json_conversion/convert.py b/src/caosadvancedtools/table_json_conversion/convert.py index b416fc29..370dc85d 100644 --- a/src/caosadvancedtools/table_json_conversion/convert.py +++ b/src/caosadvancedtools/table_json_conversion/convert.py @@ -182,16 +182,107 @@ class XLSXConverter: self._workbook = load_workbook(xlsx) self._schema = read_or_dict(schema) self._defining_path_index = xlsx_utils.get_defining_paths(self._workbook) - try: - self._check_columns(fail_fast=strict) - except KeyError as e: - raise jsonschema.ValidationError(f"Malformed metadata: Cannot parse paths. " - f"Unknown path: '{e.args[1]}' in sheet '{e.args[0]}'." - ) from e + self._check_path_validity() + self._check_columns(fail_fast=strict) self._handled_sheets: set[str] = set() self._result: dict = {} self._errors: dict = {} + def _check_path_validity(self): + """ + Method to check the workbook paths for completeness and correctness, + and raises a jsonschema.ValidationError containing information on all + faulty paths if any are found. + + If this method does not raise an error, this does not mean the workbook + is formatted correctly, only that the contained paths are complete and + can be found in the schema. + """ + # Setup + error_message = ["There were errors during path validation:"] + only_warnings = True + for sheetname in self._workbook.sheetnames: + sheet = self._workbook[sheetname] + error_message.append(f"\nIn sheet {sheetname}:") + + # Collect path information and filter out information column + row_i_col_type = xlsx_utils.get_column_type_row_index(sheet) + path_rows = xlsx_utils.get_path_rows(sheet) + paths = [] + for col_i, col in enumerate(sheet.iter_cols()): + col_type = col[row_i_col_type].value + path = [col[row_i].value for row_i in path_rows + if col[row_i].value not in [None, '']] + if col_type == 'COL_TYPE': + continue + paths.append((col_type, path, col_i, col)) + + # Check paths + for col_type, path, col_i, col in paths: + # No column type set + if col_type in [None, '']: + if len(path) == 0: # Likely a comment column + # Check whether the column has any visible content + content_in_column = False + for cell in col: + visible_content = ''.join(str(cell.value)).split() + if cell.value is not None and visible_content != '': + content_in_column = True + # If yes - might be an error but is not forbidden, so warn + if content_in_column: + m = (f"Warning:\tIn column {_column_id_to_chars(col_i)} " + f"there is no column metadata set. This column " + f"will be ignored during parsing.") + error_message.append(m) + continue + else: # Path is set but no column type + only_warnings = False + m = (f"ERROR:\t\tIn column {_column_id_to_chars(col_i)} " + f"the column type is missing.") + error_message.append(m) + # No continue - even if column type is missing, we can check path + if len(path) == 0: # Column type is set but no path + only_warnings = False + m = (f"ERROR:\t\tIn column {_column_id_to_chars(col_i)} " + f"the path is missing.") + error_message.append(m) + continue + # Check path is in schema + try: + subschema = xlsx_utils.get_subschema(path, self._schema) + schema_type = subschema.get('type', None) + if schema_type is None and 'enum' in subschema: + schema_type = 'enum' + if schema_type is None and 'anyOf' in subschema: + schema_type = 'anyOf' + if schema_type == 'array': # Check item type instead + schema_type = subschema.get('items', {}).get('type', None) + if schema_type in ['object', 'array', None]: + m = (f"Warning:\tIn column {_column_id_to_chars(col_i)} " + f"the path may be incomplete.") + error_message.append(m) + except KeyError as e: + only_warnings = False + m = (f"ERROR:\t\tIn column {_column_id_to_chars(col_i)} " + f"parsing of the path '{'.'.join(path)}' fails " + f"on the path component {str(e)}.\n\t\t\t" + f"This likely means the path is incomplete or not " + f"present in the schema.") + error_message.append(m) + + # Cleanup if no errors were found + if error_message[-1] == f"\nIn sheet {sheetname}:": + error_message.pop(-1) + + # Determine whether error / warning / nothing should be raised + if error_message == ["There were errors during path validation:"]: + return + error_message = '\n'.join(error_message) + if only_warnings: + warn(error_message) + else: + raise jsonschema.ValidationError(error_message) + def to_dict(self, validate: bool = False, collect_errors: bool = True) -> dict: """Convert the xlsx contents to a dict. @@ -375,13 +466,6 @@ class XLSXConverter: value = self._validate_and_convert(value, path) _set_in_nested(mydict=data, path=path, value=value, prefix=parent, skip=1) continue - elif sheet.cell(col_type_row+1, col_idx+1).value is None: - mess = (f"\nNo metadata configured for column " - f"'{_column_id_to_chars(col_idx)}' in worksheet " - f"'{sheet.title}'.\n") - if mess not in warns: - print(mess, file=sys.stderr) - warns.append(mess) # Prevent multiple instances of same warning except (ValueError, KeyError, jsonschema.ValidationError) as e: # Append error for entire column only once if isinstance(e, KeyError) and 'column' in str(e): @@ -460,10 +544,7 @@ class XLSXConverter: """ if value is None: return value - try: - subschema = self._get_subschema(path) - except KeyError as e: - raise KeyError("There is no entry in the schema that corresponds to this column.") from e + subschema = self._get_subschema(path) # Array handling only if schema says it's an array. if subschema.get("type") == "array": array_type = subschema["items"]["type"] diff --git a/unittests/table_json_conversion/data/simple_data_broken.xlsx b/unittests/table_json_conversion/data/simple_data_broken.xlsx index c75da9faaefcb7610d84e16dd6ff17dcd055b008..1e61cf108365d8e825c97742924c8ffc7b9643c1 100644 GIT binary patch delta 6665 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 ZpDQbZx!fw!OjqS5YpAHQwJ3n}001QmIWGVJ delta 6895 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 diff --git a/unittests/table_json_conversion/data/simple_data_broken_paths_2.xlsx b/unittests/table_json_conversion/data/simple_data_broken_paths_2.xlsx new file mode 100644 index 0000000000000000000000000000000000000000..14bdbaaa5bcdf3e5541e8cfbddc68505e35f11f2 GIT binary patch literal 8838 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 literal 0 HcmV?d00001 diff --git a/unittests/table_json_conversion/test_read_xlsx.py b/unittests/table_json_conversion/test_read_xlsx.py index a34c046f..2a81cdc8 100644 --- a/unittests/table_json_conversion/test_read_xlsx.py +++ b/unittests/table_json_conversion/test_read_xlsx.py @@ -105,7 +105,8 @@ def test_missing_columns(): with pytest.warns(UserWarning) as caught: convert.to_dict(xlsx=rfp("data/simple_data_missing.xlsx"), schema=rfp("data/simple_schema.json")) - assert str(caught.pop().message) == "Missing column: Training.coach.given_name" + messages = {str(w.message) for w in caught} + assert "Missing column: Training.coach.given_name" in messages with pytest.warns(UserWarning) as caught: convert.to_dict(xlsx=rfp("data/multiple_choice_data_missing.xlsx"), schema=rfp("data/multiple_choice_schema.json")) @@ -122,12 +123,10 @@ def test_error_table(): convert.to_dict(xlsx=rfp("data/simple_data_broken.xlsx"), schema=rfp("data/simple_schema.json")) # Correct Errors - assert "Malformed metadata: Cannot parse paths in worksheet 'Person'." in str(caught.value) assert "'Not a num' is not of type 'number'" in str(caught.value) assert "'Yes a number?' is not of type 'number'" in str(caught.value) assert "1.5 is not of type 'integer'" in str(caught.value) assert "1.2345 is not of type 'integer'" in str(caught.value) - assert "'There is no entry in the schema" in str(caught.value) assert "'Not an enum' is not one of [" in str(caught.value) # Correct Locations matches = set() @@ -144,9 +143,6 @@ def test_error_table(): if "1.2345 is not of type 'integer'" in line: assert "K8" in line matches.add("K8") - if "'There is no entry in the schema" in line: - assert "Column M" in line - matches.add("Col M") if "'Not an enum' is not one of [" in line: assert "G8" in line matches.add("K8") @@ -157,33 +153,62 @@ def test_error_table(): if "'=NOT(TRUE())' is not of type 'boolean'" in line: assert "L10" in line matches.add("L10") - assert matches == {"J7", "J8", "K7", "K8", "Col M", "K8", "L9", "L10"} + assert matches == {"J7", "J8", "K7", "K8", "K8", "L9", "L10"} # No additional errors - assert str(caught.value).count("Malformed metadata: Cannot parse paths in worksheet") == 1 - assert str(caught.value).count("There is no entry in the schema") == 1 assert str(caught.value).count("is not one of") == 1 assert str(caught.value).count("is not of type") == 6 - # Check correct error message for completely unknown path - with pytest.raises(jsonschema.ValidationError) as caught: - convert.to_dict(xlsx=rfp("data/simple_data_broken_paths.xlsx"), - schema=rfp("data/simple_schema.json")) - assert ("Malformed metadata: Cannot parse paths. Unknown path: 'There' in sheet 'Person'." - == str(caught.value)) -def test_additional_column(): +def test_malformed_paths(): with pytest.raises(jsonschema.ValidationError) as caught: - convert.to_dict(xlsx=rfp("data/simple_data_broken.xlsx"), + convert.to_dict(xlsx=rfp("data/simple_data_broken_paths_2.xlsx"), schema=rfp("data/simple_schema.json")) - # Correct Error - assert "no entry in the schema that corresponds to this column" in str(caught.value) - # Correct Location - for line in str(caught.value).split('\n'): - if "no entry in the schema that corresponds to this column" in line: - assert " M " in line - # No additional column errors - assert str(caught.value).count("no entry in the schema that corresponds to this column") == 1 + message_lines = str(caught.value).lower().split('\n') + expected_errors = { + 'person': {'c': "column type is missing", + 'd': "parsing of the path", + 'e': "path may be incomplete"}, + 'training': {'c': "path is missing", + 'd': "column type is missing", + 'e': "path may be incomplete", + 'f': "parsing of the path", + 'g': "path may be incomplete", + 'h': "parsing of the path", + 'i': "parsing of the path"}, + 'training.coach': {'f': "no column metadata set"}} + current_sheet = None + for line in message_lines: + if 'in sheet' in line: + current_sheet = line.replace('in sheet ', '').replace(':', '') + continue + if 'in column' in line: + for column, expected_error in expected_errors[current_sheet].items(): + if f'in column {column}' in line: + assert expected_error in line + expected_errors[current_sheet].pop(column) + break + for _, errors_left in expected_errors.items(): + assert len(errors_left) == 0 + + +def test_empty_columns(): + with pytest.warns(UserWarning) as caught: + try: + convert.to_dict(xlsx=rfp("data/simple_data_broken.xlsx"), + schema=rfp("data/simple_schema.json")) + except jsonschema.ValidationError: + pass # Errors are checked in test_error_table + messages = {str(w.message).lower() for w in caught} + expected_warnings = {"column h": "no column metadata"} + for message in messages: + for column, warning in list(expected_warnings.items()): + if column in message: + assert warning in message + expected_warnings.pop(column) + else: + assert warning not in message + assert len(expected_warnings) == 0 def test_faulty_foreign(): -- GitLab