From 548f058905b937f474c3e515b5333e797191283c Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?H=C3=A9l=C3=A8ne=20Martin?= Date: Mon, 14 Mar 2022 12:33:53 -0700 Subject: [PATCH 1/6] Use read-only openpyxl workbook to produce itemsets.csv --- pyxform/utils.py | 5 ++--- tests/test_external_instances_for_selects.py | 23 ++++++++++++++++---- 2 files changed, 21 insertions(+), 7 deletions(-) diff --git a/pyxform/utils.py b/pyxform/utils.py index 24c29e7a..8e3e07a8 100644 --- a/pyxform/utils.py +++ b/pyxform/utils.py @@ -190,13 +190,12 @@ def xls_sheet_to_csv(workbook_path, csv_path, sheet_name): def xlsx_sheet_to_csv(workbook_path, csv_path, sheet_name): - wb = openpyxl.open(workbook_path) + wb = openpyxl.open(workbook_path, read_only=True, data_only=True) try: sheet = wb.get_sheet_by_name(sheet_name) except KeyError: return False - if sheet.max_row < 2: - return False + with open(csv_path, "w", newline="") as f: writer = csv.writer(f, quoting=csv.QUOTE_ALL) mask = [not is_empty(cell.value) for cell in sheet[1]] diff --git a/tests/test_external_instances_for_selects.py b/tests/test_external_instances_for_selects.py index 601ce77a..2287e167 100644 --- a/tests/test_external_instances_for_selects.py +++ b/tests/test_external_instances_for_selects.py @@ -402,9 +402,6 @@ def test_itemset_csv_generated_from_external_choices(self): xls2xform_convert( xlsform_path=wb_path, xform_path=get_xml_path(wb_path), - validate=True, - pretty_print=False, - enketo=False, ) # Should have written the itemsets.csv file as part of XLSForm conversion. @@ -420,6 +417,24 @@ def test_itemset_csv_generated_from_external_choices(self): # Should have excluded column with "empty header" in the last row. self.assertEqual('"suburb","Footscray","vic","melbourne"\n', rows[-1]) + def test_external_choices_with_only_header__errors(self): + self.assertPyxformXform( + md=""" + | survey | | | | | + | | type | name | label |choice_filter | + | | select_one state | state | State | | + | | select_one_external city | city | City |state=${state} | + | choices | | | | + | | list_name | name | label | + | | state | nsw | NSW | + | | state | vic | VIC | + | external_choices | | | | + | | list_name | name | state | city | + """, + errored=True, + error__contains=["should be an external_choices sheet"], + ) + class TestInvalidExternalFileInstances(PyxformTestCase): def test_external_other_extension_instances(self): @@ -433,7 +448,7 @@ def test_external_other_extension_instances(self): | | select_multiple_from_file neighbourhoods.pdf | neighbourhoods | Neighbourhoods | """, # noqa errored=True, - error_contains=["should be a choices sheet in this xlsform"], + error__contains=["should be a choices sheet in this xlsform"], ) def test_external_choices_sheet_included_instances(self): From b85ac186a4aa128f797d9972c88aec9627fd5d68 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?H=C3=A9l=C3=A8ne=20Martin?= Date: Mon, 14 Mar 2022 14:20:43 -0700 Subject: [PATCH 2/6] Use read-only openpyxl workbook to read form definition --- pyxform/xls2json_backends.py | 44 +++++++++++++++------------ tests/example_xls/empty_sheets.xlsx | Bin 0 -> 9170 bytes tests/example_xls/group.xls | Bin 7168 -> 19456 bytes tests/example_xls/group.xlsx | Bin 9732 -> 9448 bytes tests/example_xls/specify_other.xls | Bin 7680 -> 18432 bytes tests/example_xls/specify_other.xlsx | Bin 11080 -> 10861 bytes tests/test_xls2json.py | 9 +++++- 7 files changed, 33 insertions(+), 20 deletions(-) create mode 100644 tests/example_xls/empty_sheets.xlsx diff --git a/pyxform/xls2json_backends.py b/pyxform/xls2json_backends.py index 14989ac2..52d509fe 100644 --- a/pyxform/xls2json_backends.py +++ b/pyxform/xls2json_backends.py @@ -175,7 +175,7 @@ def xlsx_to_dict(path_or_file): All the keys and leaf elements are strings. """ try: - workbook = openpyxl.open(filename=path_or_file, data_only=True) + workbook = openpyxl.open(filename=path_or_file, read_only=True, data_only=True) except (OSError, BadZipFile, KeyError) as error: raise PyXFormError("Error reading .xlsx file: %s" % error) @@ -183,19 +183,22 @@ def xlsx_to_dict_normal_sheet(sheet): # Check for duplicate column headers column_header_list = list() - for cell in sheet[1]: - column_header = cell.value - # xls file with 3 columns mostly have a 3 more columns that are - # blank by default or something, skip during check - if is_empty(column_header): - # Preserve column order (will filter later) - column_header_list.append(None) - else: - if column_header in column_header_list: - raise PyXFormError("Duplicate column header: %s" % column_header) - # strip whitespaces from the header - clean_header = re.sub(r"( )+", " ", column_header.strip()) - column_header_list.append(clean_header) + try: + for cell in sheet[1]: + column_header = cell.value + # xls file with 3 columns mostly have a 3 more columns that are + # blank by default or something, skip during check + if is_empty(column_header): + # Preserve column order (will filter later) + column_header_list.append(None) + else: + if column_header in column_header_list: + raise PyXFormError("Duplicate column header: %s" % column_header) + # strip whitespaces from the header + clean_header = re.sub(r"( )+", " ", column_header.strip()) + column_header_list.append(clean_header) + except IndexError: + pass # skip empty sheet result = [] for row in sheet.iter_rows(min_row=2): @@ -204,12 +207,15 @@ def xlsx_to_dict_normal_sheet(sheet): if key is None: continue - value = row[column].value - if isinstance(value, str): - value = value.strip() + try: + value = row[column].value + if isinstance(value, str): + value = value.strip() - if not is_empty(value): - row_dict[key] = xlsx_value_to_str(value) + if not is_empty(value): + row_dict[key] = xlsx_value_to_str(value) + except IndexError: + pass # rows may not have values for every column result.append(row_dict) diff --git a/tests/example_xls/empty_sheets.xlsx b/tests/example_xls/empty_sheets.xlsx new file mode 100644 index 0000000000000000000000000000000000000000..20261cffc4a800e29d0a360b3fda42f1299b5253 GIT binary patch literal 9170 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 a|7BxU72qHk`FR)s1ptP)|4Z_p@BR;+#HLgL literal 0 HcmV?d00001 diff --git a/tests/example_xls/group.xls b/tests/example_xls/group.xls index a9465f0a930fe66631c301b6b30ccb5d0b2251a6..b4958e24dc6d8481d8b4bbf09eb1040294a4f052 100644 GIT binary patch literal 19456 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 V`2qWW;Jq4{tJ?1C}jWu literal 7168 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 z%0A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 diff --git a/tests/example_xls/specify_other.xls b/tests/example_xls/specify_other.xls index 876f231d69882c15bdbb2273316dda1d4e08c36c..5d403ff98b28221355aef530e7df5e5dcd0f52b6 100644 GIT binary patch literal 18432 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 literal 7680 zcmeHMTWnNS6y0~)!j$r8d8r_HX`#GYo+2-?Q=k--7J@Y-hA6{uODoe3nNF2{5Qo>F zBp6Km!59*LjERq!;D;~xFfkFMAt6B%6Y$3nhzv%eC@`+I?_6f?$!$9jqJeOx>z;G= z+2^(QK4?~7vi2jh7-e_jHE$%^fT^5^}{d%Oe~?ELfY=lLHAi~`^SqzV`fi~+_1Pb1KjtDT+VU??Pn~O!5UP?0HC^)%qNXrzM|ipP!U6AC~zIxU#PwW;b*wvl?*&c1l#@uqoA`RH`1 z=3~-BH6NQU%Wj2DTXGQRViChPoZ+6{C-C$83G>Q(n)R?pN9Yeb%D? zmEV|tZJ1tAo`Le2>3i^ZrD0}4`IuBAfZvV@sq>K3S#?&$iLLlM-{9qzynvQU+2g(1 z)zY<#*4e9hQobGnXLD=5UIMp2ttwx~nys2#T|x{KL^kM?;YB#)9=j6knkiz9QW|Z` zq>&#En@zX*pc$l_G{@UCwReLCjUxH^)VCQlzS@ZVtwVTgp?1x_eDyKcqNmDOA44Ov zuPX!pT3mVgX|81aG=^5@csz3=|EMUuH8b=QxuGKSXbizjK89o_AHyFr9()+}%S*URfZY_iwl|cKjkr&QG+7zYv z_gtei|DLBR%`d}gO7q(h?iVCKjbWxKFNU2aAHz_r+h>DyO1q|}0{9r-n!01SYmUe8*Q7BVHuuKx*pxFvWs{Ggv$jU^&l;+<@tHF0!Hqw`5 zxE}bC3_TG~5swW{Qie8yGnAR3^zw{*e~=-cLNZMIu@BGJGojJ)r4ACwGvNh@hRd6t z-hC2BcUzScMUp~l@Zg~b5ZM8*zJp83CHOqPDY3@E?v;B)WIljd$4=(|TokTFdaXULP-aJ#B^OX|1;^Tu}cDnx0YuJf%mYK0NP9ntS`wI_L-8 z8t+@G)2;EoWiR*kB`2?Xw?h}v|9{%$99W8 zp52~Mryju!UQ$bS&6ZF6tU=7u>*>oad=)6cK4`y{VQvx;xISeKonuR9EScy@w5RN* z-7zO_FI`;U#$Yw>sC-v;wzu;$!(At+#hvsnN4SMk$75+Gs{8!GcYnXWp`-HsH^VY} z`X@i~(l`oG-E5BlRQ6hcYTgP^rE!4D+zl|$IRG%YISg>7i#OeGI1FwYySFEC@>-(F z?M@PPe%m1rolXQu|GfAm1|uwcDdH87vz-3=NLSnCZ@Hv9PW|nlYY3hF?(ESy6DyCr zjs4eM|6ny}Wd@C}UHneP$q8Ph?8`m&M@RDmlkVmBUzMM6hJpmeD=y+jx3U>upOdl^ zub})6y)%e?K-y?%i~53((=$9Eb3x-JH!_FLS7F{ReBs)FHM~;5QCyMpb3eR4n2PDe zV95c4u~BauL@Xd?g{@3BZ0PU(1Qd0O9 zmH?&yG$S`ZFY03V&w%fAZpboOjTvIP21ow`acApUau@qwg}GC?`R_$`r}8iS{{DZ@ F|1V@jf-#g#-S*)AAn4@oM0p6fS1`?K&Zc_sQ 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 Ys}cc&$uElTKPRCFcwRtyg#XO`1q?Jm9smFU diff --git a/tests/test_xls2json.py b/tests/test_xls2json.py index b00009e8..d459be48 100644 --- a/tests/test_xls2json.py +++ b/tests/test_xls2json.py @@ -41,7 +41,6 @@ class TestXLS2JSONSheetNameHeuristics(PyxformTestCase): - err_similar_found = "the following sheets with similar names were found" err_survey_required = "You must have a sheet named 'survey'." err_choices_required = "There should be a choices sheet in this xlsform." @@ -617,6 +616,14 @@ def test_xls2xform_convert__e2e_with_settings_misspelling(self): ) self.assertIn(expected, "\n".join(warnings)) + def test_xls2xform_convert__e2e_empty_sheets_are_ignored(self): + file_name = "empty_sheets" + warnings = xls2xform_convert( + xlsform_path=os.path.join(example_xls.PATH, file_name + ".xlsx"), + xform_path=os.path.join(test_output.PATH, file_name + ".xml"), + ) + self.assertEquals(len(warnings), 0) + def test_xlsx_to_dict__extra_sheet_names_are_returned_by_parser(self): """Should return all sheet names so that later steps can do spellcheck.""" d = xlsx_to_dict(os.path.join(example_xls.PATH, "extra_sheet_names.xlsx")) From efe22a2aee6503ee2b8ddc8d2bc593a6e846bd06 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?H=C3=A9l=C3=A8ne=20Martin?= Date: Tue, 15 Mar 2022 20:31:35 -0700 Subject: [PATCH 3/6] Improve tests, close workbooks --- pyxform/utils.py | 2 +- pyxform/xls2json_backends.py | 33 ++++---- tests/example_xls/empty_sheets.xlsx | Bin 9170 -> 0 bytes tests/test_external_instances_for_selects.py | 81 ++++++++++++------- tests/test_xls2json.py | 34 +++++--- 5 files changed, 97 insertions(+), 53 deletions(-) delete mode 100644 tests/example_xls/empty_sheets.xlsx diff --git a/pyxform/utils.py b/pyxform/utils.py index 8e3e07a8..6e9c63c1 100644 --- a/pyxform/utils.py +++ b/pyxform/utils.py @@ -211,7 +211,7 @@ def xlsx_sheet_to_csv(workbook_path, csv_path, sheet_name): except TypeError: continue writer.writerow(csv_data) - + wb.close() return True diff --git a/pyxform/xls2json_backends.py b/pyxform/xls2json_backends.py index 52d509fe..6fe14bab 100644 --- a/pyxform/xls2json_backends.py +++ b/pyxform/xls2json_backends.py @@ -183,22 +183,21 @@ def xlsx_to_dict_normal_sheet(sheet): # Check for duplicate column headers column_header_list = list() - try: - for cell in sheet[1]: - column_header = cell.value - # xls file with 3 columns mostly have a 3 more columns that are - # blank by default or something, skip during check - if is_empty(column_header): - # Preserve column order (will filter later) - column_header_list.append(None) - else: - if column_header in column_header_list: - raise PyXFormError("Duplicate column header: %s" % column_header) - # strip whitespaces from the header - clean_header = re.sub(r"( )+", " ", column_header.strip()) - column_header_list.append(clean_header) - except IndexError: - pass # skip empty sheet + + first_row = next(sheet.rows, []) + for cell in first_row: + column_header = cell.value + # xls file with 3 columns mostly have a 3 more columns that are + # blank by default or something, skip during check + if is_empty(column_header): + # Preserve column order (will filter later) + column_header_list.append(None) + else: + if column_header in column_header_list: + raise PyXFormError("Duplicate column header: %s" % column_header) + # strip whitespaces from the header + clean_header = re.sub(r"( )+", " ", column_header.strip()) + column_header_list.append(clean_header) result = [] for row in sheet.iter_rows(min_row=2): @@ -220,6 +219,7 @@ def xlsx_to_dict_normal_sheet(sheet): result.append(row_dict) column_header_list = [key for key in column_header_list if key is not None] + return result, _list_to_dict_list(column_header_list) result = OrderedDict() @@ -242,6 +242,7 @@ def xlsx_to_dict_normal_sheet(sheet): result[f"{sheetname}_header"], ) = xlsx_to_dict_normal_sheet(sheet) + workbook.close() return result diff --git a/tests/example_xls/empty_sheets.xlsx b/tests/example_xls/empty_sheets.xlsx deleted file mode 100644 index 20261cffc4a800e29d0a360b3fda42f1299b5253..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 9170 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 a|7BxU72qHk`FR)s1ptP)|4Z_p@BR;+#HLgL diff --git a/tests/test_external_instances_for_selects.py b/tests/test_external_instances_for_selects.py index 2287e167..383294c0 100644 --- a/tests/test_external_instances_for_selects.py +++ b/tests/test_external_instances_for_selects.py @@ -8,6 +8,7 @@ from dataclasses import dataclass, field from pyxform.constants import EXTERNAL_INSTANCE_EXTENSIONS +from pyxform.errors import PyXFormError from pyxform.xls2xform import get_xml_path, xls2xform_convert from tests.pyxform_test_case import PyxformTestCase from tests.test_utils.md_table import md_table_to_workbook @@ -383,21 +384,12 @@ def test_itemset_csv_generated_from_external_choices(self): | | select_one state | state | State | | | | select_one_external city | city | City | state=${state} | | | select_one_external suburb | suburb | Suburb | state=${state} and city=${city} | - | choices | | | | - | | list_name | name | label | - | | state | nsw | NSW | - | | state | vic | VIC | - | external_choices | | | | | | - | | list_name | name | state | | city | - | | city | Sydney | nsw | | | - | | city | Melbourne | vic | | | - | | suburb | Balmain | nsw | | sydney | - | | suburb | Footscray | vic | empty header | melbourne | """ - wb = md_table_to_workbook(md) + wb = md_table_to_workbook(md + self.all_choices) with get_temp_dir() as tmp: wb_path = os.path.join(tmp, "select_one_external.xlsx") wb.save(wb_path) + wb.close() with self.assertLogs("pyxform") as log: xls2xform_convert( xlsform_path=wb_path, @@ -417,23 +409,58 @@ def test_itemset_csv_generated_from_external_choices(self): # Should have excluded column with "empty header" in the last row. self.assertEqual('"suburb","Footscray","vic","melbourne"\n', rows[-1]) + def test_empty_external_choices__errors(self): + md = """ + | survey | | | | | + | | type | name | label |choice_filter | + | | select_one state | state | State | | + | | select_one_external city | city | City |state=${state} | + | choices | | | | + | | list_name | name | label | + | | state | nsw | NSW | + | external_choices | | | | + """ + wb = md_table_to_workbook(md) + with get_temp_dir() as tmp: + wb_path = os.path.join(tmp, "empty_sheet.xlsx") + wb.save(wb_path) + wb.close() + try: + xls2xform_convert( + xlsform_path=wb_path, + xform_path=get_xml_path(wb_path), + ) + except PyXFormError as e: + self.assertContains( + str(e), "should be an external_choices sheet in this xlsform" + ) + def test_external_choices_with_only_header__errors(self): - self.assertPyxformXform( - md=""" - | survey | | | | | - | | type | name | label |choice_filter | - | | select_one state | state | State | | - | | select_one_external city | city | City |state=${state} | - | choices | | | | - | | list_name | name | label | - | | state | nsw | NSW | - | | state | vic | VIC | - | external_choices | | | | - | | list_name | name | state | city | - """, - errored=True, - error__contains=["should be an external_choices sheet"], - ) + md = """ + | survey | | | | | + | | type | name | label |choice_filter | + | | select_one state | state | State | | + | | select_one_external city | city | City |state=${state} | + | choices | | | | + | | list_name | name | label | + | | state | nsw | NSW | + | external_choices | | | | + | | list_name | name | state | city | + """ + wb = md_table_to_workbook(md) + with get_temp_dir() as tmp: + wb_path = os.path.join(tmp, "empty_sheet.xlsx") + wb.save(wb_path) + wb.close() + try: + xls2xform_convert( + xlsform_path=wb_path, + xform_path=get_xml_path(wb_path), + ) + except PyXFormError as e: + self.assertContains( + str(e), "should be an external_choices sheet in this xlsform" + ) class TestInvalidExternalFileInstances(PyxformTestCase): diff --git a/tests/test_xls2json.py b/tests/test_xls2json.py index d459be48..496aeed4 100644 --- a/tests/test_xls2json.py +++ b/tests/test_xls2json.py @@ -1,11 +1,14 @@ import os from pyxform.xls2json_backends import xlsx_to_dict -from pyxform.xls2xform import xls2xform_convert +from pyxform.xls2xform import xls2xform_convert, get_xml_path from tests import example_xls, test_output from tests.pyxform_test_case import PyxformTestCase # Common XLSForms used in below TestCases +from tests.test_utils.md_table import md_table_to_workbook +from tests.utils import get_temp_dir + CHOICES = """ | survey | | | | | | type | name | label | @@ -600,6 +603,27 @@ def test_workbook_to_json__optional_sheets_ok(self): warnings_count=0, ) + def test_xls2xform_convert__e2e_row_with_no_column_value(self): + """Programmatically-created XLSX files may have rows without column values""" + md = """ + | survey | | | | | + | | type | name | label | hint | + | | text | state | State | | + | | text | city | City | A hint | + """ + wb = md_table_to_workbook(md) + with get_temp_dir() as tmp: + wb_path = os.path.join(tmp, "empty_cell.xlsx") + wb.save(wb_path) + wb.close() + xls2xform_convert( + xlsform_path=wb_path, + xform_path=get_xml_path(wb_path), + ) + + xform_path = os.path.join(tmp, "empty_cell.xml") + self.assertTrue(os.path.exists(xform_path)) + def test_xls2xform_convert__e2e_with_settings_misspelling(self): """Should warn about settings misspelling when running full pipeline.""" file_name = "extra_sheet_names" @@ -616,14 +640,6 @@ def test_xls2xform_convert__e2e_with_settings_misspelling(self): ) self.assertIn(expected, "\n".join(warnings)) - def test_xls2xform_convert__e2e_empty_sheets_are_ignored(self): - file_name = "empty_sheets" - warnings = xls2xform_convert( - xlsform_path=os.path.join(example_xls.PATH, file_name + ".xlsx"), - xform_path=os.path.join(test_output.PATH, file_name + ".xml"), - ) - self.assertEquals(len(warnings), 0) - def test_xlsx_to_dict__extra_sheet_names_are_returned_by_parser(self): """Should return all sheet names so that later steps can do spellcheck.""" d = xlsx_to_dict(os.path.join(example_xls.PATH, "extra_sheet_names.xlsx")) From 0fa6096a17ebb3826a0aa972eedb53659de4f9fa Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?H=C3=A9l=C3=A8ne=20Martin?= Date: Tue, 15 Mar 2022 20:56:35 -0700 Subject: [PATCH 4/6] Add test to document memory usage regression --- dev_requirements.in | 3 ++- dev_requirements.pip | 1 + tests/example_xls/extra_columns.xlsx | Bin 0 -> 85154 bytes tests/test_xls2json.py | 18 +++++++++++++++++- 4 files changed, 20 insertions(+), 2 deletions(-) create mode 100644 tests/example_xls/extra_columns.xlsx diff --git a/dev_requirements.in b/dev_requirements.in index a1e9955c..c9cb5f0f 100644 --- a/dev_requirements.in +++ b/dev_requirements.in @@ -9,4 +9,5 @@ isort yapf black formencode -lxml \ No newline at end of file +lxml +psutil \ No newline at end of file diff --git a/dev_requirements.pip b/dev_requirements.pip index 847b2191..ccbc4c83 100644 --- a/dev_requirements.pip +++ b/dev_requirements.pip @@ -23,6 +23,7 @@ pathspec==0.9.0 # via black pep517==0.11.0 # via pip-tools pip-tools==6.3.0 # via -r dev_requirements.in platformdirs==2.3.0 # via black, pylint +psutil==5.9.0 # via -r dev_requirements.in pycodestyle==2.7.0 # via flake8 pyflakes==2.3.1 # via flake8 pylint==2.11.1 # via -r dev_requirements.in diff --git a/tests/example_xls/extra_columns.xlsx b/tests/example_xls/extra_columns.xlsx new file mode 100644 index 0000000000000000000000000000000000000000..c521086ac79d09aa25c3cbeb166a9002fd60458f GIT binary patch literal 85154 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 literal 0 HcmV?d00001 diff --git a/tests/test_xls2json.py b/tests/test_xls2json.py index 496aeed4..bfcda092 100644 --- a/tests/test_xls2json.py +++ b/tests/test_xls2json.py @@ -1,14 +1,16 @@ import os +import psutil from pyxform.xls2json_backends import xlsx_to_dict from pyxform.xls2xform import xls2xform_convert, get_xml_path from tests import example_xls, test_output from tests.pyxform_test_case import PyxformTestCase -# Common XLSForms used in below TestCases from tests.test_utils.md_table import md_table_to_workbook from tests.utils import get_temp_dir + +# Common XLSForms used in below TestCases CHOICES = """ | survey | | | | | | type | name | label | @@ -640,6 +642,20 @@ def test_xls2xform_convert__e2e_with_settings_misspelling(self): ) self.assertIn(expected, "\n".join(warnings)) + def test_xls2xform_convert__e2e_with_extra_columns__does_not_use_excessive_memory( + self, + ): + """Degenerate form with many blank columns""" + process = psutil.Process(os.getpid()) + pre_mem = process.memory_info().rss + xls2xform_convert( + xlsform_path=os.path.join(example_xls.PATH, "extra_columns.xlsx"), + xform_path=os.path.join(test_output.PATH, "extra_columns.xml"), + ) + post_mem = process.memory_info().rss + # in v1.8.0, memory usage grew by over 16x + self.assertLess(post_mem, pre_mem * 2) + def test_xlsx_to_dict__extra_sheet_names_are_returned_by_parser(self): """Should return all sheet names so that later steps can do spellcheck.""" d = xlsx_to_dict(os.path.join(example_xls.PATH, "extra_sheet_names.xlsx")) From 367b3f7285f344717901b855bc9fdfb24d92c838 Mon Sep 17 00:00:00 2001 From: lindsay stevens Date: Wed, 16 Mar 2022 22:45:27 +1100 Subject: [PATCH 5/6] add: workaround for windows test failures - presumably, windows antivirus scanning is being activated by 1) a new file created and 2) that file being read. Seems to take a while to release the file lock so tests fail on permission error. - approach here is truncate files and clear them up on a subsequent run. - possibly a nicer approach is to not use files at all, but refactor so that we can pass around an io.BytesIO object or similar. --- tests/utils.py | 47 ++++++++++++++++++++++++++++++++++++++++++++--- 1 file changed, 44 insertions(+), 3 deletions(-) diff --git a/tests/utils.py b/tests/utils.py index f3701037..e4dbd077 100644 --- a/tests/utils.py +++ b/tests/utils.py @@ -66,9 +66,50 @@ def get_temp_file(): @contextmanager def get_temp_dir(): - temp_dir = tempfile.mkdtemp() + temp_dir_prefix = "pyxform_tmp_" + if os.name == "nt": + cleanup_pyxform_temp_files(prefix=temp_dir_prefix) + + temp_dir = tempfile.mkdtemp(prefix=temp_dir_prefix) try: yield temp_dir finally: - if os.path.exists(temp_dir): - shutil.rmtree(temp_dir) + try: + if os.path.exists(temp_dir): + shutil.rmtree(temp_dir) + except PermissionError: + truncate_temp_files(temp_dir=temp_dir) + + +def truncate_temp_files(temp_dir): + """ + Truncate files in a folder, recursing into directories. + """ + # If we can't delete, at least the files can be truncated, + # so that they don't take up disk space until next cleanup. + # Seems to be a Windows-specific error for newly-created files. + temp_root = tempfile.gettempdir() + if os.path.exists(temp_dir): + for f in os.scandir(temp_dir): + if os.path.isdir(f.path): + truncate_temp_files(f.path) + else: + # Check still in temp directory + if f.path.startswith(temp_root): + with open(f.path, mode="w") as _: + pass + + +def cleanup_pyxform_temp_files(prefix: str): + """ + Try to clean up temp pyxform files from previous test runs. + """ + temp_root = tempfile.gettempdir() + if os.path.exists(temp_root): + for f in os.scandir(temp_root): + if os.path.isdir(f.path): + if f.name.startswith(prefix) and f.path.startswith(temp_root): + try: + shutil.rmtree(f.path) + except PermissionError: + pass From be969c11b9a1a8d42d4a872bd2504131c5d74313 Mon Sep 17 00:00:00 2001 From: lindsay stevens Date: Wed, 16 Mar 2022 22:47:15 +1100 Subject: [PATCH 6/6] dev: linter / formatting --- pyxform/question.py | 4 ++-- pyxform/xls2json.py | 3 +-- tests/test_xls2json.py | 5 ++--- 3 files changed, 5 insertions(+), 7 deletions(-) diff --git a/pyxform/question.py b/pyxform/question.py index 00e19e93..4043224c 100644 --- a/pyxform/question.py +++ b/pyxform/question.py @@ -6,11 +6,11 @@ import re from pyxform.constants import ( - EXTERNAL_INSTANCE_EXTENSIONS, EXTERNAL_CHOICES_ITEMSET_REF_LABEL, + EXTERNAL_CHOICES_ITEMSET_REF_LABEL_GEOJSON, EXTERNAL_CHOICES_ITEMSET_REF_VALUE, EXTERNAL_CHOICES_ITEMSET_REF_VALUE_GEOJSON, - EXTERNAL_CHOICES_ITEMSET_REF_LABEL_GEOJSON, + EXTERNAL_INSTANCE_EXTENSIONS, ) from pyxform.errors import PyXFormError from pyxform.question_type_dictionary import QUESTION_TYPE_DICT diff --git a/pyxform/xls2json.py b/pyxform/xls2json.py index ee09858a..1f068ba6 100644 --- a/pyxform/xls2json.py +++ b/pyxform/xls2json.py @@ -11,8 +11,7 @@ from typing import TYPE_CHECKING from pyxform import aliases, constants -from pyxform.constants import ROW_FORMAT_STRING -from pyxform.constants import EXTERNAL_INSTANCE_EXTENSIONS +from pyxform.constants import EXTERNAL_INSTANCE_EXTENSIONS, ROW_FORMAT_STRING from pyxform.errors import PyXFormError from pyxform.utils import default_is_dynamic, is_valid_xml_tag, levenshtein_distance from pyxform.validators.pyxform import select_from_file_params diff --git a/tests/test_xls2json.py b/tests/test_xls2json.py index bfcda092..00aa9821 100644 --- a/tests/test_xls2json.py +++ b/tests/test_xls2json.py @@ -1,15 +1,14 @@ import os + import psutil from pyxform.xls2json_backends import xlsx_to_dict -from pyxform.xls2xform import xls2xform_convert, get_xml_path +from pyxform.xls2xform import get_xml_path, xls2xform_convert from tests import example_xls, test_output from tests.pyxform_test_case import PyxformTestCase - from tests.test_utils.md_table import md_table_to_workbook from tests.utils import get_temp_dir - # Common XLSForms used in below TestCases CHOICES = """ | survey | | | |