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
zcmZ4He%XUJz?+#xgn@&DgTbb(Wh1W&BeP9e%VrbCU?vb_au>5gec<}pW+HX>*E`g`
zxXs16(`1e3svgbPOEOng<zMx@EGJ{Ya`na;i`+}`_R`FaN1m_qU9P)Uq3gi&AJ)%5
zKTlRT_vq-t1?Q@jR?XS0I?Lwk1m}n|ljo#*e|eVp`LB%S)FV0vrx<T?aSvK3zWTk`
zRGXlp{mL#@T1=~_WvHkAY<?Y9|FKVGN7X6sLsQhBE;<lo>iR5fse;8ylNAfzUHQzk
z{eY$OYS!E0vz@~H&V2XLn|ObwOz|((z&lNnMLs(^)VofdZxL+NELuDFecRmmf3`mV
z^z-)D)7PK9vP`(KrhadK+^lScB8E!^vkTXVX{IlKcxHm(8nLVH&uy1!b%Y;3#%lPs
ze(MR}77=kvTdBt((vM`trV8CW@JdDPiYVjW*^SrLb%Xt0CS2Q^xK(506qgOXSr%u%
z9^M!dWgOb9=5zPGd{sJ=y!z$c{_G)ho^_^8be&W5up?);%j{nf>{e&KT@rLH{S$Xa
zFS<5p@tvZHD@3pL6*omqp8Uk(G0$c5r$S3-gt63@E}Q>@{hCxbw^z{0OC2`1Uv7S~
zH0|)xC#}0O3}<z8-iclD#c<)5IQ{GQFZUh3670zT^696fQs=v@Cf}c3a><g~q4Tov
zaH)Xiw<U&uBFZIaTr)Wz;dst!pHR%Ch->AuZ|#YCux;_?$4dWKIK3`9IV*K(zMoP5
z){{NrmXl<jWb4&$>|A6~E3l&`;F#Z$IjW^ePgj>IrN5W^q4xfc*RoBE9|p+nSN8NS
zny$`u>-9^yOU6t0O!k|cU7A%fkNc|m_o-Q@AG}sHmOaUkvMw%KdFI439%(AZlXkY9
zIkSJM`qP}E;G8t`qiI#%Ifwqw?73ZEYp(x!@An@MZe9Q3Ty=8K`RDcAE9-OLfB8Q#
z=l(VRh5CYTFNdGpzU=1Rh>ZFAQ3hS#^4Fewxx?!Btet0tw{G`0tJqZ*=Kelk?_z${
z>*?Q8ZoezmDo%@fr>gMCwej}9$-5a3urr>Mzc*i2L!yC6J42r(@#DtBHqr@c{fTTl
zpE2JtmVPHHxN%l9pAery+WxO9%jyrkwhb=57F@32dBwzao9vrqRbLlgd@fgf!&czf
z=P%daU9{8SKf-V*WA|NS_T!H$i!Us${gxq+#+S6^?z>K#zm1=67%sGWqs;4kpP$Ji
zaI<sWL4~FgZl?3iiyZHmFr8X#Ts!aDhn&NrpDJV>iho74Iw-el3rcCO+4}89srBEN
z^@WzV1LPiWe%_dM{6~&lY;al6``3}JI~=tozO^VmxwtiB{Y8ZhoB5dkZamMw#jD1i
z=dE1tM&l2)Cl-e*ziE`n;G1xK{ruo}H~-$wegFF1%kvMe-Vg9*=a{?W{M>WQ3=DyC
z3=AlxKqLbakYJd+j$N*PwuMOT{q+eIFD_rZp>oW#>+Z>0g|}y#oICQeEbV@ZfSa&l
zpyv(GeRb<q4oJ+g%1xidemq9@+}`go%6*6W`z1@wB0LV=ydnGL*ShA&Bgu!ZC4ayD
z_s`#%uRU5LdNk52-q<C+tef*kG;e3c1kGco7@5?L=dB8<dsu1YRoIr&|Ga+2L9auu
zZ6YyUTh)sj19S3Dt$6<ZcjI)$Loufd6ypvboZRA4acr^DVV_8$M$MpreLkLg+S`u*
z2(8)Z5S_7g)$+hcXVzV*X}-)C@1}H6)jvf4QDkJ)_S41hYu2B4fB0YSastQNypDNO
z;wPqF%-(foLWRbs?+4~K2WdNXRvGc`uAhA2!+kDw#gN30Q;!=wVF~O^)OF9^bL3TY
z-nFZ?`&%8hzVUThzo$~@7|XMT+Qlq>$KE}CkR+GU!uZGgLzvk<Yon*PEw6+JsAyXW
z|55R%@s(&>A>=B#=kaaL<?|i6f6O!!|94m+N>m_au2buEi$K$*PODXySp-?!^WLl>
z(yCK`dU?*#%(DWISwur#N-4;Tht@Ew_Qx*!y5zvg;<?FPnpbOg`7PNPui#kl%If@_
z^wS)H0ht%xo3G11^Y-nEYoDcTKOTA!wC<IA_R*ise3|$5&6||z{Cc+XUx{g<JNTMj
zu-(qG>z#G|SD1$XKLcUQ{Ssef8Lk9heslKy)rM!!%9-n#^`A0m?wcI3)WPgUZ;ibK
z+rJHB+-aXBv!5p3e19<RBaiV)rKOMVtg7ubpTQJozD2y5XPamK`UX#qQ(m7}nl96r
z-fQUA8e*fZy}8)SQzkhq*3@{$6v--+rj(Y*#4D2}g*oEBo0~BGEjq^B`!0wr_0)ra
zQwzjz%)Alz_FS__{j8RzeA&beW*et2J-0P@PiTv4xN?PKw_o{%E$5cipTDHiTCM70
zKDq8iZb_}`qxpJC8~SI+xt{4$TGyxa%-*>|A#Ux0|J%5J&E559T2t&1Pw#_^J8lLZ
zxq4op`q!L2YwgtCzrIxRi~dz2czBv!tjmQ<%ey6HuRZ^g`_=O8mC|eVmzH;DyuR$Z
z<?BnOTfa&K^<Nm*MAx=~G@Xmy8_Kc%s@I3~wR^ruRjizzneghv?COZ@-E(CdwtPRu
zJmH$MDgUn7snzBmq~6bq5m+X+)9|Vv*D>y%59d~_EmvDB%eXmWzw}Op^K&#^mOr?%
zF=LPC!yOAfuEc&=^Y~BKky#E&S@mmT_M2Nt^Lp2B-_63D(x>fQAM*cTO<2r92fnpS
zj@iHC5IPt7_}1#Xc@vwDpV}BPDZL_mesdA8eR@p6)Am!RZ!zrbTNc4FS^Zgb+5Dt!
z4S5gE6*)|@`!tiypF4579N4>7qf1UN@{g_RDzj^RGG0<@W=#6#PiNY__sDgYyjEG?
z8gk3#p2$tE=-B}tHu>$h7teUgVpen4!?Zb8<&ovVtI3I1XG!<YnW4gYrc?9qe(lX%
z`j38OZLZc*Q|wR9zOMd0U|Ri*S?9~`xz^d;Iqc|YWnRFs<FRB?{r!igbC_CUO?EWC
zpYebC!tIYQPT4l={*o)JBJM>Wj-J#Sw>fynn)8ZVC;w*z6<Q}=cI~ZaVPKHvM=P`@
z&u3SeoXI0r|B;nr0@K_nEq4~YQ~?!EBGD|qIj5(xs{H(-eNz0)M4|HCyp<dqzkJ^L
zx4$Ks*WUKn#I474luaaW#+}}(JBP(Y<>cQJ|H`cYUf902XR(Tlpznthb%$pD&HQt`
z!1<>0wj(p54>?TgeWUvP<-*4)2ZfGnWM33fPfBx)?Afs+<$Jx~G@~A;(qI|OeXr##
z43#?PMq8xVCpYVfsT??N5^>bx;ZDIe<s+^?ZcbVfnfcc6k7l)^v-HF)Nq^lVk874~
zdR}T$ZOAEc_C?cnCMofSFC+Qp9Q2#lf9AibcU<_AU1~v^`%T2y?I)-9+&HZDS0QKl
zqM&jWr9YmkIsNUY{)MO2>v|k9oP9Z<V~;~%+tHY=cOQA5-48N7pZ_x4Fu>3H@xtIc
zHgat)FBFwGOT_iOQGQ?WZl_S-k^3z9n{M3?+qPu(N{#~2g=@AfXY-fjOjkLWG-Y$B
zVTG@`u+-enqz=2a{(qWXubyIbdpBL_h-BP{Q!4J|<`#z-k0<`!w_3EnWpe%O<v;q|
zrZx)bU(kAF#PYCjQ&EkslHASCHR&oomqK>Uc%U(<@>ZC_&EmxW3zDPHPdR4J*yS?S
z=K9*$`oQgn10&B%RxQ#Fot6})x7ONA{>g+buf3**h;7=ZIqCF<jzilfNW?x?n&tS{
zYX8+!bsDa0?K|cCRGt3551O+wOKM?RU_EzW`+Joq-hwl)zuIZL{q4`YtAd=S8|$pr
zTlRO_4UKuNa!)-LGMx3cJ)AAW8P^&jFD&<oXYO2vW6puseEv>j?F*RY$;rL=pw?p(
zJ*CsGQIiagcvgmfUl4QbMIei&o#!Gu<9|y`gs!gpRBXM;;s4FWiWk><?=N^M9kcE4
zwCjKW*6ZDFJ-zMV()Zh{@+YJ&FSuzax=r2c@%HLBVsg9H`s=bkuz7ytn$Es#SJm2P
z^Anrmcf6S$yRCZv{ih2GEDd(P*?KEKAmY64^`+Sbx9TUG?9I}zNHqR%U+H3z`3Ild
zjo0%kv)a#<@B5?3QfMM)v}@xGr{eQJ)gmo#zuT|SHAm09{)u>*o38D|tzXpk-!gg{
za%Xnu#jiJHt+FNK9c!*}=1-qtB6Pg>NN3}VH%gb?l$ZA&iBvAU=MiHfG`;u8nu#;^
zyICv>U)A>U`)z}LV$=Dh+|>8zy_k{qWQU|>rrWZ!Co+wTuJz0@R~A2eBGuR^+wIwU
zCF{MH@8!SWB3<pSen79@;3DUHhAn-JFB`8JD%4)EVoPC&S}tjs?RIYVL~iGY2h3_S
z9Ud%8c5wLneEqRam#yah-R8G^W$7|2#;E5Z>JOZ*b6$vkZp9dto!7A?S1+-Azv`Q;
z#>Q#?*9aGUec7_@7Y|2FEgQ=^JK2v5=diK7%5+$;fQ_Z4I?>_5D`SO%ul14wJAM@%
zS+87M{95MC_EJ8nXm|Aka%LAf<rR)wU1DpUx9?f{a;Lrj!xv7M`xvivamn)IeDj-l
zIbJjfGhbrMU9T*sacL%FWB*PzmLo-p9jrPBPg_lT5*GELQ8oRR=;jRp4Xa-H_NSSv
zzyB{|#eMj{T=Cs~{|^Nh)SoZ<{`Jv;di4lqCbRj%%uG+O3Hs(SGd(p^2*`8RNl@PN
zM*7LO+p;Sv`btI0mTT5k|IO95__N;qd%$g#au)eFksli4<TkL&Nek@Q$IBrS<8E@0
zmqX-EgTq1gU9%kCyCs=bguc=b-~PUvbshJ<#mj0mw}e|>jn-Je^2f@^Fd#sfS=7{E
zR{at+w`$YGgas?Cdm^u<rGzQ0e>qD(x%Tn7)Y2^dZ9#5AbyHUzw$ARHXj6J;9{b(=
z^Gy#<@UD{!oi=4+`m6#G--1JitM@LCPCn+jz~AEM$!jtjtCeKjUmZD-`}Nn}yuzL>
zd!Lxdo=m>D?*8gJm(zCx^{UR8Yz{dd63xu*Rr&T-{Yt;9VJoLD64@;Msps?ewOfmo
zOK*kA?G>4RHL^R`Q#jn`C+ADGow1Lec%^9Hnj<)IjTN8lP0iFfiiccx2y)5wP5SVA
z(uY?J=eZ<T&7I0>(WPcG?fI&iHt)R3x~k_0eGJug)?U3!mNUa^!z<~DvGe|iq~^A!
zY!sO4|6|IAoY$-BrS@?hp8U1!Wna4q&*$QOAFV}}%d5ZozFS`Vl>NO)66x2(_iS93
z*)wVXx{rUgoOI@g%y}OD>T|}6oVOm6D^2!i1kJnnv~0iD?$(V9gU^S=gsDGzTo0~#
zp8kI662->A5Gs#W^`NwuC$C`_uQ%4K$j#Y1Ber<9g-F}`@8Je9U$U}!%#QixmIqmv
zSxq*ZsdD$+o4GuPuCWM+Zkqe#`umAL45oRUDBJwbRjM!U-0@<ThZ%o<8us<brb#Y5
zd1Q-q?BxTU{>+oss6Xrfef0Rk%oG__#;%Me^=HE8&YZt&f2{hH+$P>*cNo|`%?!=!
z=PXa3Hua~p%;Gi9!E+9Hc|`VD#BAC+bt%tchkGLK(=NZ|TO%>?$Qj!?8@K;hkmylk
zv;3dgG0yXI4P+GgUZzV=N?W@9_yga6Ol{u+vS#}09<AIL^mFs?S^H=F=t(nqB0K+>
zYgW}%<?ND>=gP%(=O@2)T4}9h^>oU)r2o31$2x55b1wHkux1U?783lY!fm!Z`Q`sM
zNySc!wEGzhzI=gG5+l4XO5IQVr#$ubpLzEh7RGE_mb`jLr4=8?7w42T0~x<F#lPPa
zX@~@U5I^{P&9~hXb6?ak{>)XLGWFe(!jm2au0lLv=eD(eoc1o=WEYFr2H)4*^~nuk
zQWBB*3Wv32KGmO6bzf~QlQ{qJ1C29Zr@0F}eWw|Bam6Qr2LidudPNNtQ>K2CU1DP>
ze9)L_zKN(|nnjn|vpZ(I@frJ_8XWJN%+u_BUn9b(y8HIp*!t<)SBD+^%w>CV?USH2
zUGCYgFPjZG_s3pTjcj``Tj`^OSmX>grq_JWZ^qn;yQ}c7_0OjG`c0~fjdy2rxL!4X
zxc^`E%|)$uEH8Xw;yCcblW8Age%bNjIgQo@ceun$o(pk*+wi93LGDMMSWR8l;#r^f
z9kP~bjGI!D(#%mg^I9O&Wy1|;pZGgkI%S4sFWG<AqPP3ZC%;?|fd@<Wta6{AnEXYf
z^N7mHlONP?YB<DQeeUJ(-zurzd13p6y-G_iixmC%_wkqOo;v3}aqEq%{&(aFUd&yy
zA$foO5l6wo3nCX^|KShWtF-7lSE%;=4L{`tj$AA`^Wt7Y(5&NUzn6)=`fx+T^2w`D
zmQ{()*+(7ED&5+7<BpNWo!O10=5PDf&ONa{pyr}zt*cS?^fRTMwQF`{gz7|zp1xi$
zBF3;z!+QDBHyM?dT&b~%ySbj0o!k>!bWtyIi}$qkUVR?l{k(Tw_lgR?!Q5KXd?eW8
z^`w2f7T#9vE<5=t^rYa+f0>@&Z+o0Cb&YpiqJ3rW3hgOrx}~kx&m6bal@C9iz2eQ0
zn1|PXYH+0UPMYTw*}pjGny6N4?82aHqWS@HbLyvkS}=9fx)<6FX>TX()RUCe){~yI
z=Y!Xmf6|Ml@3W0d`f~KLaq#^LUZJPG<rftK1N5a-xRx%}E8Kl{N}KvJlbq#CysJ(z
zS6_VKx^#(m%-5y&6$(YX!ropsOjUV!eWlGU12cnvlTvCWreALljNE$QL|pC7Ah&s$
zhXPFwbDsIWwf_AB*|a%7el<-!|M$K?v@nOv$;Mr`Keo+__PN>aE#drh;l$S!SM666
zxmtc%lbNc#{6};1grK<H+g2I3=W<QX>$)jh*{1(iaKdWetDkfh&GGC$oyB3w`|#1M
zlNOA3)xS&6iE-Y#Ay=X?>1pD{BR-msc?4%H3>W+)e{S-F=WQQe)mI$mkXR+F`okdL
ze1?wST%$sX2{H%gE@#=->5`>-wfX3B&4ZiX2gJtxcM8j8QQ>K<IV9ea+8!|Vrxk}@
zS-X3p)iTEk^36W>3oQPcOt!cGb1UfI%XvpEjQ2g>Tj-rU<xgez_1hd9YXV>89G~!i
ze*fBL`G-d5pP9#Y3HRK2-}^-?+Cn<|tJlL#R{C@PF@u`zuH2#_|5z9pibc@M&dr;|
z)xblJlW$9C);}^3XnX%#TSxxewU+g3!*V&21UGopXkJ)XwW`Nh;JsSewZ{%WzngAI
zwG0*RbQYR-eV*j`Z0lEhMJ;bPyQY>*H*aQ3U$E43>b=sRm#a#?&owS_TCQX+wd)VZ
ziRzEjIW8LNU%l~kX7N*JiA84?2uREj+VVd=J4bO&{np(A-rP#{M|lp2R^G@md3V@J
z<Lpe6sbzJye)FYI4Xf9dh}l<@{n^4v)ctv++y;?L{_olyH&j3Pm|$2NyI-e0@Wmqy
zo5uMERy)?;^7nYLY5zY@=Gv1zY73)89GJE*3i$bP>Y4Xf<V<AaZ_K^G)f#-{fcoQ~
zN(-k}%w@D@J#=Q8<^MSiCT8`i=Qi*fybQnZ=oPKaUL9qn9(3`-$sCtQ$0k2loXs0q
zf3D4Q(}JyMN;XAZ&Pu60UmZX3!IkhHmM}B*!ie0u<`Rz<@Ba%fL_}?vy0ko0b4_s2
zR43u)jR6u#hDZ92{XMU*aQnGi@bwkumuAh|8+dKo``&3$(<k@(&;6d5{C#~YbN!}Y
znlmTeSii{R$BnElx5|5${ogw=r2VjBfK{l5D_^4ttEZ{3UUf{Gd&s0~g~g9&B<xzm
zTkpFx*jzL>=5J0MgZ{zX1K&KlvZvnu`hkyA{-^Pw`64P|Qi_|SW~Vyb<5<u2sgL8p
zxpJYh$b<cidih`Zc)qQke@6Xt;a(>5%<t^BhgWE>{U_7ex4`se!G~tEd5dyA(~~pn
z5AWF;_Tv4dkJg~1BrPYcpuohy(8-0ClqRc7>(zID&S;Fv)N)*D5y080AYh)HAME@n
zV%zUu)pM5ka<4s-o1SiS@1xb+oYj}2o6QzYOBP{O{N%7{7I$RN#caFJ`4iK1rp}qR
zwZ-sOqvPZ^TB%(dq7H9fCz_#spyHf|)S)X9x@$vOk`GS{2o$zxEqSu&n&?)(S4`Tc
z_GL0!@Ey*sk7-;wC#p-@IsR*3M*hjj{FPz5a(FEc2RN}_l9FK+w9C?caVo~<f|2?u
zuN!llx-T)`T)o}x%b~Mr!a0+|UOnso@cYlprM<7XZ`z15Pg(lgk>j$1ZozrU3Ga3>
zf1Y;7|Fh(&M+R~~65ky6lygt9P)YnT@sa&=>BH;Rm0sK6{vfNKuPt-on%jx)|FSG%
zf+E-4X-=2<^U_)SW%OAs@2od>bZZqG?!VonIhD!l{l;rWs@)><wSU!I-S<8IfX27O
zj&i&w6I|NN)fxHO-pWW_TlBN@rbcG|+d|<(YwY$n+|*0tkI8M-U6?I@YV-S<e;Gj0
zF?B}G{c=VIhF46W=-_5XOeRdeD5FyEf7pPh?fsuFty^0?qrJj?Gt~qhnQ-!@J_>6+
za<^#~zv;^>f2%k7=2$<T{PucP&i)eH*NT=0UQ9LRJyqByWN^qUwT$;y&izYLs&)m+
zKNEOOUU0~*y~+GnVr{db#s^OoiTev`Ir3aP#H%mdd8{#e#h0=xHb;u?2;TngbzrsV
z<a#z?kBO<Gjg}1)6@trI7jYMsa{O=g()RoQGq<{A;_(dbTf8<5i=Rx}X7*uWs8qzi
z2=|9km(riJsaAB%+tB+<?&04(Ejopr(|1JVOqu!Y-RgC_R{6YR+x6oJe}}`VoR3B;
z9hb4He%~NmS#3D`slP>cq1=zxfkmIE{$&CM&Tfg>Uc3J=Ffhzv1ceTA;Bd)Z1x>JQ
zek8XJQvOX=FarrpKBHj8XH(V^i5Q-pETX6@4-!Hi{E$Ezqh(-do1CDit^iVl+)71N
z<IOWUQBidA9z{X0LgXSDRpDzPkU~RnBR0UBkx7IBK0y>&q#2>}kb!~0o|%C`3dITf
z5|a;#%S`rH;sG0voJ~bg6h=r+u2xb4dk8u42%;$3HF=Mc0@$G4NI@uoqNYH0@*gER
maG=R5OEayKn{1`52<FBqOEWdePyQ$;I(dS!0GpvANFM-WkE8DZ

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