From 86e74793578cd156fa1fe4357045e6f7b6fc83e9 Mon Sep 17 00:00:00 2001 From: lindsay stevens Date: Tue, 16 Dec 2025 19:39:35 +1100 Subject: [PATCH 01/33] chg: move NAMES001 error into ErrorCode enum - organising error messages --- pyxform/errors.py | 8 ++++++++ pyxform/validators/pyxform/unique_names.py | 14 ++++---------- tests/test_external_instances.py | 4 ++-- tests/test_fields.py | 18 +++++++++--------- tests/test_group.py | 13 +++++++------ tests/test_repeat.py | 13 +++++++------ 6 files changed, 37 insertions(+), 33 deletions(-) diff --git a/pyxform/errors.py b/pyxform/errors.py index 59fb2e3d..30bfbadd 100644 --- a/pyxform/errors.py +++ b/pyxform/errors.py @@ -41,6 +41,14 @@ def format(self, **kwargs): class ErrorCode(Enum): + NAMES_001: Detail = Detail( + name="Invalid duplicate name in same context", + msg=( + "[row : {row}] On the 'survey' sheet, the 'name' value '{value}' is invalid. " + "Questions, groups, and repeats must be unique within their nearest parent group " + "or repeat, or the survey if not inside a group or repeat." + ), + ) PYREF_001: Detail = Detail( name="PyXForm Reference Parsing Failed", msg=( diff --git a/pyxform/validators/pyxform/unique_names.py b/pyxform/validators/pyxform/unique_names.py index 566ec50c..c441d6b3 100644 --- a/pyxform/validators/pyxform/unique_names.py +++ b/pyxform/validators/pyxform/unique_names.py @@ -1,14 +1,6 @@ from pyxform import constants as const -from pyxform.errors import Detail, PyXFormError +from pyxform.errors import Detail, ErrorCode, PyXFormError -NAMES001 = Detail( - name="Invalid duplicate name in same context", - msg=( - "[row : {row}] On the 'survey' sheet, the 'name' value '{value}' is invalid. " - "Questions, groups, and repeats must be unique within their nearest parent group " - "or repeat, or the survey if not inside a group or repeat." - ), -) NAMES002 = Detail( name="Invalid duplicate name in context (case-insensitive)", msg=( @@ -75,7 +67,9 @@ def validate_question_group_repeat_name( if name == const.META: raise PyXFormError(NAMES005.format(row=row_number)) else: - raise PyXFormError(NAMES001.format(row=row_number, value=name)) + raise PyXFormError( + ErrorCode.NAMES_001.value.format(row=row_number, value=name) + ) seen_names.add(name) question_name_lower = name.lower() diff --git a/tests/test_external_instances.py b/tests/test_external_instances.py index f20c2b21..f9bb5abf 100644 --- a/tests/test_external_instances.py +++ b/tests/test_external_instances.py @@ -6,7 +6,7 @@ from textwrap import dedent -from pyxform.validators.pyxform import unique_names +from pyxform.errors import ErrorCode from tests.pyxform_test_case import PyxformTestCase, PyxformTestError from tests.xpath_helpers.choices import xpc @@ -50,7 +50,7 @@ def test_cannot__use_same_external_xml_id_in_same_section(self): self.assertPyxformXform( md=md, errored=True, - error__contains=[unique_names.NAMES001.format(row=3, value="mydata")], + error__contains=[ErrorCode.NAMES_001.value.format(row=3, value="mydata")], ) def test_can__use_unique_external_xml_in_same_section(self): diff --git a/tests/test_fields.py b/tests/test_fields.py index 96d995a0..acc5ffe2 100644 --- a/tests/test_fields.py +++ b/tests/test_fields.py @@ -173,7 +173,7 @@ def test_names__question_same_as_question_in_same_context_in_survey__error(self) self.assertPyxformXform( md=md, errored=True, - error__contains=[unique_names.NAMES001.format(row=3, value="q1")], + error__contains=[ErrorCode.NAMES_001.value.format(row=3, value="q1")], ) def test_names__question_same_as_group_in_same_context_in_survey__error(self): @@ -189,7 +189,7 @@ def test_names__question_same_as_group_in_same_context_in_survey__error(self): self.assertPyxformXform( md=md, errored=True, - error__contains=[unique_names.NAMES001.format(row=3, value="q1")], + error__contains=[ErrorCode.NAMES_001.value.format(row=3, value="q1")], ) def test_names__question_same_as_repeat_in_same_context_in_survey__error(self): @@ -205,7 +205,7 @@ def test_names__question_same_as_repeat_in_same_context_in_survey__error(self): self.assertPyxformXform( md=md, errored=True, - error__contains=[unique_names.NAMES001.format(row=3, value="q1")], + error__contains=[ErrorCode.NAMES_001.value.format(row=3, value="q1")], ) def test_names__question_same_as_question_in_same_context_in_group__error(self): @@ -221,7 +221,7 @@ def test_names__question_same_as_question_in_same_context_in_group__error(self): self.assertPyxformXform( md=md, errored=True, - error__contains=[unique_names.NAMES001.format(row=4, value="q1")], + error__contains=[ErrorCode.NAMES_001.value.format(row=4, value="q1")], ) def test_names__question_same_as_group_in_same_context_in_group__error(self): @@ -239,7 +239,7 @@ def test_names__question_same_as_group_in_same_context_in_group__error(self): self.assertPyxformXform( md=md, errored=True, - error__contains=[unique_names.NAMES001.format(row=4, value="q1")], + error__contains=[ErrorCode.NAMES_001.value.format(row=4, value="q1")], ) def test_names__question_same_as_repeat_in_same_context_in_group__error(self): @@ -257,7 +257,7 @@ def test_names__question_same_as_repeat_in_same_context_in_group__error(self): self.assertPyxformXform( md=md, errored=True, - error__contains=[unique_names.NAMES001.format(row=4, value="q1")], + error__contains=[ErrorCode.NAMES_001.value.format(row=4, value="q1")], ) def test_names__question_same_as_question_in_same_context_in_repeat__error(self): @@ -273,7 +273,7 @@ def test_names__question_same_as_question_in_same_context_in_repeat__error(self) self.assertPyxformXform( md=md, errored=True, - error__contains=[unique_names.NAMES001.format(row=4, value="q1")], + error__contains=[ErrorCode.NAMES_001.value.format(row=4, value="q1")], ) def test_names__question_same_as_group_in_same_context_in_repeat__error(self): @@ -291,7 +291,7 @@ def test_names__question_same_as_group_in_same_context_in_repeat__error(self): self.assertPyxformXform( md=md, errored=True, - error__contains=[unique_names.NAMES001.format(row=4, value="q1")], + error__contains=[ErrorCode.NAMES_001.value.format(row=4, value="q1")], ) def test_names__question_same_as_repeat_in_same_context_in_repeat__error(self): @@ -309,7 +309,7 @@ def test_names__question_same_as_repeat_in_same_context_in_repeat__error(self): self.assertPyxformXform( md=md, errored=True, - error__contains=[unique_names.NAMES001.format(row=4, value="q1")], + error__contains=[ErrorCode.NAMES_001.value.format(row=4, value="q1")], ) def test_names__question_same_as_question_in_same_context_in_survey__case_insensitive_warning( diff --git a/tests/test_group.py b/tests/test_group.py index afec55f2..aa09cb03 100644 --- a/tests/test_group.py +++ b/tests/test_group.py @@ -5,6 +5,7 @@ from unittest import TestCase from pyxform.builder import create_survey_element_from_dict +from pyxform.errors import ErrorCode from pyxform.validators.pyxform import unique_names from pyxform.xls2json import SURVEY_001, SURVEY_002 from pyxform.xls2xform import convert @@ -269,7 +270,7 @@ def test_names__group_same_as_group_in_same_context_in_survey__error(self): self.assertPyxformXform( md=md, errored=True, - error__contains=[unique_names.NAMES001.format(row=5, value="g1")], + error__contains=[ErrorCode.NAMES_001.value.format(row=5, value="g1")], ) def test_names__group_same_as_repeat_in_same_context_in_survey__error(self): @@ -287,7 +288,7 @@ def test_names__group_same_as_repeat_in_same_context_in_survey__error(self): self.assertPyxformXform( md=md, errored=True, - error__contains=[unique_names.NAMES001.format(row=5, value="g1")], + error__contains=[ErrorCode.NAMES_001.value.format(row=5, value="g1")], ) def test_names__group_same_as_group_in_same_context_in_group__error(self): @@ -307,7 +308,7 @@ def test_names__group_same_as_group_in_same_context_in_group__error(self): self.assertPyxformXform( md=md, errored=True, - error__contains=[unique_names.NAMES001.format(row=6, value="g2")], + error__contains=[ErrorCode.NAMES_001.value.format(row=6, value="g2")], ) def test_names__group_same_as_repeat_in_same_context_in_group__error(self): @@ -327,7 +328,7 @@ def test_names__group_same_as_repeat_in_same_context_in_group__error(self): self.assertPyxformXform( md=md, errored=True, - error__contains=[unique_names.NAMES001.format(row=6, value="g2")], + error__contains=[ErrorCode.NAMES_001.value.format(row=6, value="g2")], ) def test_names__group_same_as_group_in_same_context_in_repeat__error(self): @@ -347,7 +348,7 @@ def test_names__group_same_as_group_in_same_context_in_repeat__error(self): self.assertPyxformXform( md=md, errored=True, - error__contains=[unique_names.NAMES001.format(row=6, value="g2")], + error__contains=[ErrorCode.NAMES_001.value.format(row=6, value="g2")], ) def test_names__group_same_as_repeat_in_same_context_in_repeat__error(self): @@ -367,7 +368,7 @@ def test_names__group_same_as_repeat_in_same_context_in_repeat__error(self): self.assertPyxformXform( md=md, errored=True, - error__contains=[unique_names.NAMES001.format(row=6, value="g2")], + error__contains=[ErrorCode.NAMES_001.value.format(row=6, value="g2")], ) def test_names__group_same_as_group_in_same_context_in_survey__case_insensitive_warning( diff --git a/tests/test_repeat.py b/tests/test_repeat.py index ab131a22..fd3475f8 100644 --- a/tests/test_repeat.py +++ b/tests/test_repeat.py @@ -7,6 +7,7 @@ from unittest import skip from psutil import Process +from pyxform.errors import ErrorCode from pyxform.validators.pyxform import unique_names from pyxform.xls2json_backends import SupportedFileTypes from pyxform.xls2xform import convert @@ -1237,7 +1238,7 @@ def test_names__repeat_same_as_repeat_in_same_context_in_survey__error(self): self.assertPyxformXform( md=md, errored=True, - error__contains=[unique_names.NAMES001.format(row=5, value="r1")], + error__contains=[ErrorCode.NAMES_001.value.format(row=5, value="r1")], ) def test_names__repeat_same_as_repeat_in_same_context_in_group__error(self): @@ -1257,7 +1258,7 @@ def test_names__repeat_same_as_repeat_in_same_context_in_group__error(self): self.assertPyxformXform( md=md, errored=True, - error__contains=[unique_names.NAMES001.format(row=6, value="r1")], + error__contains=[ErrorCode.NAMES_001.value.format(row=6, value="r1")], ) def test_names__repeat_same_as_repeat_in_same_context_in_repeat__error(self): @@ -1277,7 +1278,7 @@ def test_names__repeat_same_as_repeat_in_same_context_in_repeat__error(self): self.assertPyxformXform( md=md, errored=True, - error__contains=[unique_names.NAMES001.format(row=6, value="r2")], + error__contains=[ErrorCode.NAMES_001.value.format(row=6, value="r2")], ) def test_names__repeat_same_as_repeat_in_same_context_in_survey__case_insensitive_warning( @@ -1587,7 +1588,7 @@ def test_expression__generated_element_same_name__error(self): self.assertPyxformXform( md=md, errored=True, - error__contains=[unique_names.NAMES001.format(value="r1_count")], + error__contains=[ErrorCode.NAMES_001.value.format(value="r1_count")], ) def test_expression__generated_element_different_name__ok(self): @@ -1643,7 +1644,7 @@ def test_manual_xpath__generated_element_same_name__error(self): self.assertPyxformXform( md=md, errored=True, - error__contains=[unique_names.NAMES001.format(value="r1_count")], + error__contains=[ErrorCode.NAMES_001.value.format(value="r1_count")], ) def test_manual_xpath__generated_element_different_name__ok(self): @@ -1694,7 +1695,7 @@ def test_constant_integer__generated_element_same_name__error(self): self.assertPyxformXform( md=md, errored=True, - error__contains=[unique_names.NAMES001.format(value="r1_count")], + error__contains=[ErrorCode.NAMES_001.value.format(value="r1_count")], ) def test_constant_integer__generated_element_different_name__ok(self): From c4437662a4c473a7d55f542aeeb4f46a6195abc9 Mon Sep 17 00:00:00 2001 From: lindsay stevens Date: Tue, 16 Dec 2025 19:44:27 +1100 Subject: [PATCH 02/33] chg: move NAMES002 error into ErrorCode enum - organising error messages --- pyxform/errors.py | 10 ++++++++ pyxform/validators/pyxform/unique_names.py | 12 +--------- tests/test_fields.py | 28 ++++++++++++++-------- tests/test_group.py | 19 +++++++++------ tests/test_repeat.py | 9 ++++--- 5 files changed, 47 insertions(+), 31 deletions(-) diff --git a/pyxform/errors.py b/pyxform/errors.py index 30bfbadd..7b775d7d 100644 --- a/pyxform/errors.py +++ b/pyxform/errors.py @@ -49,6 +49,16 @@ class ErrorCode(Enum): "or repeat, or the survey if not inside a group or repeat." ), ) + NAMES_002 = Detail( + name="Invalid duplicate name in context (case-insensitive)", + msg=( + "[row : {row}] On the 'survey' sheet, the 'name' value '{value}' is problematic. " + "The name is a case-insensitive match to another name. Questions, groups, and " + "repeats should be unique within the nearest parent group or repeat, or the survey " + "if not inside a group or repeat. Some data processing tools are not " + "case-sensitive, so the current names may make analysis difficult." + ), + ) PYREF_001: Detail = Detail( name="PyXForm Reference Parsing Failed", msg=( diff --git a/pyxform/validators/pyxform/unique_names.py b/pyxform/validators/pyxform/unique_names.py index c441d6b3..eee56a63 100644 --- a/pyxform/validators/pyxform/unique_names.py +++ b/pyxform/validators/pyxform/unique_names.py @@ -1,16 +1,6 @@ from pyxform import constants as const from pyxform.errors import Detail, ErrorCode, PyXFormError -NAMES002 = Detail( - name="Invalid duplicate name in context (case-insensitive)", - msg=( - "[row : {row}] On the 'survey' sheet, the 'name' value '{value}' is problematic. " - "The name is a case-insensitive match to another name. Questions, groups, and " - "repeats should be unique within the nearest parent group or repeat, or the survey " - "if not inside a group or repeat. Some data processing tools are not " - "case-sensitive, so the current names may make analysis difficult." - ), -) NAMES003 = Detail( name="Invalid repeat name same as survey", msg=( @@ -75,7 +65,7 @@ def validate_question_group_repeat_name( question_name_lower = name.lower() if question_name_lower in seen_names_lower: # No case-insensitive warning for 'meta' since it's not an exported data table. - warnings.append(NAMES002.format(row=row_number, value=name)) + warnings.append(ErrorCode.NAMES_002.value.format(row=row_number, value=name)) seen_names_lower.add(question_name_lower) diff --git a/tests/test_fields.py b/tests/test_fields.py index acc5ffe2..f99ed87e 100644 --- a/tests/test_fields.py +++ b/tests/test_fields.py @@ -3,7 +3,6 @@ """ from pyxform.errors import ErrorCode -from pyxform.validators.pyxform import unique_names from tests.pyxform_test_case import PyxformTestCase @@ -323,7 +322,8 @@ def test_names__question_same_as_question_in_same_context_in_survey__case_insens | | text | Q1 | Q2 | """ self.assertPyxformXform( - md=md, warnings__contains=[unique_names.NAMES002.format(row=3, value="Q1")] + md=md, + warnings__contains=[ErrorCode.NAMES_002.value.format(row=3, value="Q1")], ) def test_names__question_same_as_group_in_same_context_in_survey__case_insensitive_warning( @@ -339,7 +339,8 @@ def test_names__question_same_as_group_in_same_context_in_survey__case_insensiti | | end group | | | """ self.assertPyxformXform( - md=md, warnings__contains=[unique_names.NAMES002.format(row=3, value="Q1")] + md=md, + warnings__contains=[ErrorCode.NAMES_002.value.format(row=3, value="Q1")], ) def test_names__question_same_as_repeat_in_same_context_in_survey__case_insensitive_warning( @@ -355,7 +356,8 @@ def test_names__question_same_as_repeat_in_same_context_in_survey__case_insensit | | end repeat | | | """ self.assertPyxformXform( - md=md, warnings__contains=[unique_names.NAMES002.format(row=3, value="Q1")] + md=md, + warnings__contains=[ErrorCode.NAMES_002.value.format(row=3, value="Q1")], ) def test_names__question_same_as_question_in_same_context_in_group__case_insensitive_warning( @@ -371,7 +373,8 @@ def test_names__question_same_as_question_in_same_context_in_group__case_insensi | | end group | | | """ self.assertPyxformXform( - md=md, warnings__contains=[unique_names.NAMES002.format(row=4, value="Q1")] + md=md, + warnings__contains=[ErrorCode.NAMES_002.value.format(row=4, value="Q1")], ) def test_names__question_same_as_group_in_same_context_in_group__case_insensitive_warning( @@ -389,7 +392,8 @@ def test_names__question_same_as_group_in_same_context_in_group__case_insensitiv | | end group | | | """ self.assertPyxformXform( - md=md, warnings__contains=[unique_names.NAMES002.format(row=4, value="Q1")] + md=md, + warnings__contains=[ErrorCode.NAMES_002.value.format(row=4, value="Q1")], ) def test_names__question_same_as_repeat_in_same_context_in_group__case_insensitive_warning( @@ -407,7 +411,8 @@ def test_names__question_same_as_repeat_in_same_context_in_group__case_insensiti | | end group | | | """ self.assertPyxformXform( - md=md, warnings__contains=[unique_names.NAMES002.format(row=4, value="Q1")] + md=md, + warnings__contains=[ErrorCode.NAMES_002.value.format(row=4, value="Q1")], ) def test_names__question_same_as_question_in_same_context_in_repeat__case_insensitive_warning( @@ -423,7 +428,8 @@ def test_names__question_same_as_question_in_same_context_in_repeat__case_insens | | end repeat | | | """ self.assertPyxformXform( - md=md, warnings__contains=[unique_names.NAMES002.format(row=4, value="Q1")] + md=md, + warnings__contains=[ErrorCode.NAMES_002.value.format(row=4, value="Q1")], ) def test_names__question_same_as_group_in_same_context_in_repeat__case_insensitive_warning( @@ -441,7 +447,8 @@ def test_names__question_same_as_group_in_same_context_in_repeat__case_insensiti | | end repeat | | | """ self.assertPyxformXform( - md=md, warnings__contains=[unique_names.NAMES002.format(row=4, value="Q1")] + md=md, + warnings__contains=[ErrorCode.NAMES_002.value.format(row=4, value="Q1")], ) def test_names__question_same_as_repeat_in_same_context_in_repeat__case_insensitive_warning( @@ -459,7 +466,8 @@ def test_names__question_same_as_repeat_in_same_context_in_repeat__case_insensit | | end repeat | | | """ self.assertPyxformXform( - md=md, warnings__contains=[unique_names.NAMES002.format(row=4, value="Q1")] + md=md, + warnings__contains=[ErrorCode.NAMES_002.value.format(row=4, value="Q1")], ) def test_reference_name_not_found__target_after_source__error(self): diff --git a/tests/test_group.py b/tests/test_group.py index aa09cb03..60164184 100644 --- a/tests/test_group.py +++ b/tests/test_group.py @@ -6,7 +6,6 @@ from pyxform.builder import create_survey_element_from_dict from pyxform.errors import ErrorCode -from pyxform.validators.pyxform import unique_names from pyxform.xls2json import SURVEY_001, SURVEY_002 from pyxform.xls2xform import convert @@ -386,7 +385,8 @@ def test_names__group_same_as_group_in_same_context_in_survey__case_insensitive_ | | end group | | | """ self.assertPyxformXform( - md=md, warnings__contains=[unique_names.NAMES002.format(row=5, value="G1")] + md=md, + warnings__contains=[ErrorCode.NAMES_002.value.format(row=5, value="G1")], ) def test_names__group_same_as_repeat_in_same_context_in_survey__case_insensitive_warning( @@ -404,7 +404,8 @@ def test_names__group_same_as_repeat_in_same_context_in_survey__case_insensitive | | end group | | | """ self.assertPyxformXform( - md=md, warnings__contains=[unique_names.NAMES002.format(row=5, value="G1")] + md=md, + warnings__contains=[ErrorCode.NAMES_002.value.format(row=5, value="G1")], ) def test_names__group_same_as_group_in_same_context_in_group__case_insensitive_warning( @@ -424,7 +425,8 @@ def test_names__group_same_as_group_in_same_context_in_group__case_insensitive_w | | end group | | | """ self.assertPyxformXform( - md=md, warnings__contains=[unique_names.NAMES002.format(row=6, value="G2")] + md=md, + warnings__contains=[ErrorCode.NAMES_002.value.format(row=6, value="G2")], ) def test_names__group_same_as_repeat_in_same_context_in_group__case_insensitive_warning( @@ -444,7 +446,8 @@ def test_names__group_same_as_repeat_in_same_context_in_group__case_insensitive_ | | end group | | | """ self.assertPyxformXform( - md=md, warnings__contains=[unique_names.NAMES002.format(row=6, value="G2")] + md=md, + warnings__contains=[ErrorCode.NAMES_002.value.format(row=6, value="G2")], ) def test_names__group_same_as_group_in_same_context_in_repeat__case_insensitive_warning( @@ -464,7 +467,8 @@ def test_names__group_same_as_group_in_same_context_in_repeat__case_insensitive_ | | end repeat | | | """ self.assertPyxformXform( - md=md, warnings__contains=[unique_names.NAMES002.format(row=6, value="G2")] + md=md, + warnings__contains=[ErrorCode.NAMES_002.value.format(row=6, value="G2")], ) def test_names__group_same_as_repeat_in_same_context_in_repeat__case_insensitive_warning( @@ -484,7 +488,8 @@ def test_names__group_same_as_repeat_in_same_context_in_repeat__case_insensitive | | end repeat | | | """ self.assertPyxformXform( - md=md, warnings__contains=[unique_names.NAMES002.format(row=6, value="G2")] + md=md, + warnings__contains=[ErrorCode.NAMES_002.value.format(row=6, value="G2")], ) def test_group__no_end_error__no_name(self): diff --git a/tests/test_repeat.py b/tests/test_repeat.py index fd3475f8..015ef50d 100644 --- a/tests/test_repeat.py +++ b/tests/test_repeat.py @@ -1296,7 +1296,8 @@ def test_names__repeat_same_as_repeat_in_same_context_in_survey__case_insensitiv | | end repeat | | | """ self.assertPyxformXform( - md=md, warnings__contains=[unique_names.NAMES002.format(row=5, value="R1")] + md=md, + warnings__contains=[ErrorCode.NAMES_002.value.format(row=5, value="R1")], ) def test_names__repeat_same_as_repeat_in_same_context_in_group__case_insensitive_warning( @@ -1316,7 +1317,8 @@ def test_names__repeat_same_as_repeat_in_same_context_in_group__case_insensitive | | end group | | | """ self.assertPyxformXform( - md=md, warnings__contains=[unique_names.NAMES002.format(row=6, value="R1")] + md=md, + warnings__contains=[ErrorCode.NAMES_002.value.format(row=6, value="R1")], ) def test_names__repeat_same_as_repeat_in_same_context_in_repeat__case_insensitive_warning( @@ -1336,7 +1338,8 @@ def test_names__repeat_same_as_repeat_in_same_context_in_repeat__case_insensitiv | | end repeat | | | """ self.assertPyxformXform( - md=md, warnings__contains=[unique_names.NAMES002.format(row=6, value="R2")] + md=md, + warnings__contains=[ErrorCode.NAMES_002.value.format(row=6, value="R2")], ) def test_names__repeat_same_as_survey_root__error(self): From b8c5265c4b72977f6b68341cc076d484b8c13b92 Mon Sep 17 00:00:00 2001 From: lindsay stevens Date: Tue, 16 Dec 2025 19:47:28 +1100 Subject: [PATCH 03/33] chg: move NAMES003 error into ErrorCode enum - organising error messages --- pyxform/errors.py | 7 +++++++ pyxform/validators/pyxform/unique_names.py | 11 +++-------- tests/test_repeat.py | 2 +- 3 files changed, 11 insertions(+), 9 deletions(-) diff --git a/pyxform/errors.py b/pyxform/errors.py index 7b775d7d..bfe632dc 100644 --- a/pyxform/errors.py +++ b/pyxform/errors.py @@ -59,6 +59,13 @@ class ErrorCode(Enum): "case-sensitive, so the current names may make analysis difficult." ), ) + NAMES_003 = Detail( + name="Invalid repeat name same as survey", + msg=( + "[row : {row}] On the 'survey' sheet, the 'name' value '{value}' is invalid. " + "Repeat names must not be the same as the survey root (which defaults to 'data')." + ), + ) PYREF_001: Detail = Detail( name="PyXForm Reference Parsing Failed", msg=( diff --git a/pyxform/validators/pyxform/unique_names.py b/pyxform/validators/pyxform/unique_names.py index eee56a63..458baa09 100644 --- a/pyxform/validators/pyxform/unique_names.py +++ b/pyxform/validators/pyxform/unique_names.py @@ -1,13 +1,6 @@ from pyxform import constants as const from pyxform.errors import Detail, ErrorCode, PyXFormError -NAMES003 = Detail( - name="Invalid repeat name same as survey", - msg=( - "[row : {row}] On the 'survey' sheet, the 'name' value '{value}' is invalid. " - "Repeat names must not be the same as the survey root (which defaults to 'data')." - ), -) NAMES004 = Detail( name="Invalid duplicate repeat name in the survey", msg=( @@ -91,7 +84,9 @@ def validate_repeat_name( """ if control_type == const.REPEAT: if name == instance_element_name: - raise PyXFormError(NAMES003.format(row=row_number, value=name)) + raise PyXFormError( + ErrorCode.NAMES_003.value.format(row=row_number, value=name) + ) elif name in seen_names: raise PyXFormError(NAMES004.format(row=row_number, value=name)) seen_names.add(name) diff --git a/tests/test_repeat.py b/tests/test_repeat.py index 015ef50d..4b73ed1e 100644 --- a/tests/test_repeat.py +++ b/tests/test_repeat.py @@ -1355,7 +1355,7 @@ def test_names__repeat_same_as_survey_root__error(self): md=md, name="data", errored=True, - error__contains=[unique_names.NAMES003.format(row=2, value="data")], + error__contains=[ErrorCode.NAMES_003.value.format(row=2, value="data")], ) def test_names__repeat_same_as_repeat_in_different_context_in_group__error(self): From 15b2283a1d21894d0be6ef6fbab7ee64701d1450 Mon Sep 17 00:00:00 2001 From: lindsay stevens Date: Tue, 16 Dec 2025 19:55:39 +1100 Subject: [PATCH 04/33] chg: move NAMES004 error into ErrorCode enum - organising error messages --- pyxform/errors.py | 8 ++++++++ pyxform/validators/pyxform/unique_names.py | 12 +++--------- tests/test_loop.py | 4 ++-- tests/test_repeat.py | 5 ++--- 4 files changed, 15 insertions(+), 14 deletions(-) diff --git a/pyxform/errors.py b/pyxform/errors.py index bfe632dc..31cbe992 100644 --- a/pyxform/errors.py +++ b/pyxform/errors.py @@ -66,6 +66,14 @@ class ErrorCode(Enum): "Repeat names must not be the same as the survey root (which defaults to 'data')." ), ) + NAMES_004 = Detail( + name="Invalid duplicate repeat name in the survey", + msg=( + "[row : {row}] On the 'survey' sheet, the 'name' value '{value}' is invalid. " + "Repeat names must unique anywhere in the survey, at all levels of group or " + "repeat nesting." + ), + ) PYREF_001: Detail = Detail( name="PyXForm Reference Parsing Failed", msg=( diff --git a/pyxform/validators/pyxform/unique_names.py b/pyxform/validators/pyxform/unique_names.py index 458baa09..261433d9 100644 --- a/pyxform/validators/pyxform/unique_names.py +++ b/pyxform/validators/pyxform/unique_names.py @@ -1,14 +1,6 @@ from pyxform import constants as const from pyxform.errors import Detail, ErrorCode, PyXFormError -NAMES004 = Detail( - name="Invalid duplicate repeat name in the survey", - msg=( - "[row : {row}] On the 'survey' sheet, the 'name' value '{value}' is invalid. " - "Repeat names must unique anywhere in the survey, at all levels of group or " - "repeat nesting." - ), -) NAMES005 = Detail( name="Invalid duplicate meta name in the survey", msg=( @@ -88,5 +80,7 @@ def validate_repeat_name( ErrorCode.NAMES_003.value.format(row=row_number, value=name) ) elif name in seen_names: - raise PyXFormError(NAMES004.format(row=row_number, value=name)) + raise PyXFormError( + ErrorCode.NAMES_004.value.format(row=row_number, value=name) + ) seen_names.add(name) diff --git a/tests/test_loop.py b/tests/test_loop.py index 9dd0e8ba..8d9e429b 100644 --- a/tests/test_loop.py +++ b/tests/test_loop.py @@ -4,7 +4,7 @@ from unittest import TestCase -from pyxform.validators.pyxform import unique_names +from pyxform.errors import ErrorCode from pyxform.xls2xform import convert from tests.pyxform_test_case import PyxformTestCase @@ -193,7 +193,7 @@ def test_loop__repeats_error(self): md=md, errored=True, # Not caught by xls2json since loops are currently generated in builder.py - error__contains=[unique_names.NAMES004.format(row=None, value="r1")], + error__contains=[ErrorCode.NAMES_004.value.format(row=None, value="r1")], ) def test_loop__references_error(self): diff --git a/tests/test_repeat.py b/tests/test_repeat.py index 4b73ed1e..4c41f609 100644 --- a/tests/test_repeat.py +++ b/tests/test_repeat.py @@ -8,7 +8,6 @@ from psutil import Process from pyxform.errors import ErrorCode -from pyxform.validators.pyxform import unique_names from pyxform.xls2json_backends import SupportedFileTypes from pyxform.xls2xform import convert @@ -1375,7 +1374,7 @@ def test_names__repeat_same_as_repeat_in_different_context_in_group__error(self) self.assertPyxformXform( md=md, errored=True, - error__contains=[unique_names.NAMES004.format(row=7, value="r1")], + error__contains=[ErrorCode.NAMES_004.value.format(row=7, value="r1")], ) def test_names__repeat_same_as_repeat_in_different_context_in_repeat__error(self): @@ -1395,7 +1394,7 @@ def test_names__repeat_same_as_repeat_in_different_context_in_repeat__error(self self.assertPyxformXform( md=md, errored=True, - error__contains=[unique_names.NAMES004.format(row=7, value="r2")], + error__contains=[ErrorCode.NAMES_004.value.format(row=7, value="r2")], ) def test_empty_repeat__no_question__ok(self): From c33241e52cb1ee4da7a15a665093eeef22667672 Mon Sep 17 00:00:00 2001 From: lindsay stevens Date: Tue, 16 Dec 2025 20:04:58 +1100 Subject: [PATCH 05/33] chg: move NAMES005 error into ErrorCode enum - organising error messages --- pyxform/errors.py | 7 +++++++ pyxform/validators/pyxform/unique_names.py | 12 ++---------- tests/test_metadata.py | 20 ++++++++++---------- 3 files changed, 19 insertions(+), 20 deletions(-) diff --git a/pyxform/errors.py b/pyxform/errors.py index 31cbe992..662ada57 100644 --- a/pyxform/errors.py +++ b/pyxform/errors.py @@ -74,6 +74,13 @@ class ErrorCode(Enum): "repeat nesting." ), ) + NAMES_005 = Detail( + name="Invalid duplicate meta name in the survey", + msg=( + "[row : {row}] On the 'survey' sheet, the 'name' value 'meta' is invalid. " + "The name 'meta' is reserved for form metadata." + ), + ) PYREF_001: Detail = Detail( name="PyXForm Reference Parsing Failed", msg=( diff --git a/pyxform/validators/pyxform/unique_names.py b/pyxform/validators/pyxform/unique_names.py index 261433d9..728c7cda 100644 --- a/pyxform/validators/pyxform/unique_names.py +++ b/pyxform/validators/pyxform/unique_names.py @@ -1,13 +1,5 @@ from pyxform import constants as const -from pyxform.errors import Detail, ErrorCode, PyXFormError - -NAMES005 = Detail( - name="Invalid duplicate meta name in the survey", - msg=( - "[row : {row}] On the 'survey' sheet, the 'name' value 'meta' is invalid. " - "The name 'meta' is reserved for form metadata." - ), -) +from pyxform.errors import ErrorCode, PyXFormError def validate_question_group_repeat_name( @@ -40,7 +32,7 @@ def validate_question_group_repeat_name( if name in seen_names: if name == const.META: - raise PyXFormError(NAMES005.format(row=row_number)) + raise PyXFormError(ErrorCode.NAMES_005.value.format(row=row_number)) else: raise PyXFormError( ErrorCode.NAMES_001.value.format(row=row_number, value=name) diff --git a/tests/test_metadata.py b/tests/test_metadata.py index 7ff645a6..25c025eb 100644 --- a/tests/test_metadata.py +++ b/tests/test_metadata.py @@ -2,7 +2,7 @@ Test language warnings. """ -from pyxform.validators.pyxform import unique_names +from pyxform.errors import ErrorCode from tests.pyxform_test_case import PyxformTestCase @@ -115,7 +115,7 @@ def test_names__question_named_meta__in_survey__error(self): | | text | meta | Q1 | """ self.assertPyxformXform( - md=md, errored=True, error__contains=[unique_names.NAMES005.format(row=2)] + md=md, errored=True, error__contains=[ErrorCode.NAMES_005.value.format(row=2)] ) def test_names__group_named_meta__in_survey__error(self): @@ -128,7 +128,7 @@ def test_names__group_named_meta__in_survey__error(self): | | end group | | | """ self.assertPyxformXform( - md=md, errored=True, error__contains=[unique_names.NAMES005.format(row=2)] + md=md, errored=True, error__contains=[ErrorCode.NAMES_005.value.format(row=2)] ) def test_names__repeat_named_meta__in_survey__error(self): @@ -141,7 +141,7 @@ def test_names__repeat_named_meta__in_survey__error(self): | | end repeat | | | """ self.assertPyxformXform( - md=md, errored=True, error__contains=[unique_names.NAMES005.format(row=2)] + md=md, errored=True, error__contains=[ErrorCode.NAMES_005.value.format(row=2)] ) def test_names__question_named_meta__in_group__error(self): @@ -154,7 +154,7 @@ def test_names__question_named_meta__in_group__error(self): | | end group | | | """ self.assertPyxformXform( - md=md, errored=True, error__contains=[unique_names.NAMES005.format(row=3)] + md=md, errored=True, error__contains=[ErrorCode.NAMES_005.value.format(row=3)] ) def test_names__group_named_meta__in_group__error(self): @@ -169,7 +169,7 @@ def test_names__group_named_meta__in_group__error(self): | | end group | | | """ self.assertPyxformXform( - md=md, errored=True, error__contains=[unique_names.NAMES005.format(row=3)] + md=md, errored=True, error__contains=[ErrorCode.NAMES_005.value.format(row=3)] ) def test_names__repeat_named_meta__in_group__error(self): @@ -184,7 +184,7 @@ def test_names__repeat_named_meta__in_group__error(self): | | end group | | | """ self.assertPyxformXform( - md=md, errored=True, error__contains=[unique_names.NAMES005.format(row=3)] + md=md, errored=True, error__contains=[ErrorCode.NAMES_005.value.format(row=3)] ) def test_names__question_named_meta__in_repeat__error(self): @@ -197,7 +197,7 @@ def test_names__question_named_meta__in_repeat__error(self): | | end group | | | """ self.assertPyxformXform( - md=md, errored=True, error__contains=[unique_names.NAMES005.format(row=3)] + md=md, errored=True, error__contains=[ErrorCode.NAMES_005.value.format(row=3)] ) def test_names__group_named_meta__in_repeat__error(self): @@ -212,7 +212,7 @@ def test_names__group_named_meta__in_repeat__error(self): | | end repeat | | | """ self.assertPyxformXform( - md=md, errored=True, error__contains=[unique_names.NAMES005.format(row=3)] + md=md, errored=True, error__contains=[ErrorCode.NAMES_005.value.format(row=3)] ) def test_names__repeat_named_meta__in_repeat__error(self): @@ -227,5 +227,5 @@ def test_names__repeat_named_meta__in_repeat__error(self): | | end repeat | | | """ self.assertPyxformXform( - md=md, errored=True, error__contains=[unique_names.NAMES005.format(row=3)] + md=md, errored=True, error__contains=[ErrorCode.NAMES_005.value.format(row=3)] ) From c0312b12582551a58dfe57f05fbb719484e8d440 Mon Sep 17 00:00:00 2001 From: lindsay stevens Date: Tue, 16 Dec 2025 20:27:05 +1100 Subject: [PATCH 06/33] chg: move choices.INVALID_NAME error into ErrorCode enum - organising error messages - add test to verify error raised, since no other test seems to check for this error case or message. --- pyxform/errors.py | 16 ++++++++++++---- pyxform/validators/pyxform/choices.py | 9 ++------- tests/test_choices_sheet.py | 17 +++++++++++++++++ 3 files changed, 31 insertions(+), 11 deletions(-) diff --git a/pyxform/errors.py b/pyxform/errors.py index 662ada57..fb3beca0 100644 --- a/pyxform/errors.py +++ b/pyxform/errors.py @@ -49,7 +49,7 @@ class ErrorCode(Enum): "or repeat, or the survey if not inside a group or repeat." ), ) - NAMES_002 = Detail( + NAMES_002: Detail = Detail( name="Invalid duplicate name in context (case-insensitive)", msg=( "[row : {row}] On the 'survey' sheet, the 'name' value '{value}' is problematic. " @@ -59,14 +59,14 @@ class ErrorCode(Enum): "case-sensitive, so the current names may make analysis difficult." ), ) - NAMES_003 = Detail( + NAMES_003: Detail = Detail( name="Invalid repeat name same as survey", msg=( "[row : {row}] On the 'survey' sheet, the 'name' value '{value}' is invalid. " "Repeat names must not be the same as the survey root (which defaults to 'data')." ), ) - NAMES_004 = Detail( + NAMES_004: Detail = Detail( name="Invalid duplicate repeat name in the survey", msg=( "[row : {row}] On the 'survey' sheet, the 'name' value '{value}' is invalid. " @@ -74,13 +74,21 @@ class ErrorCode(Enum): "repeat nesting." ), ) - NAMES_005 = Detail( + NAMES_005: Detail = Detail( name="Invalid duplicate meta name in the survey", msg=( "[row : {row}] On the 'survey' sheet, the 'name' value 'meta' is invalid. " "The name 'meta' is reserved for form metadata." ), ) + NAMES_006: Detail = Detail( + name="Invalid missing name in the choices sheet", + msg=( + "[row : {row}] On the 'choices' sheet, the 'name' value is invalid. " + "Choices must have a name. " + "Learn more: https://xlsform.org/en/#setting-up-your-worksheets" + ), + ) PYREF_001: Detail = Detail( name="PyXForm Reference Parsing Failed", msg=( diff --git a/pyxform/validators/pyxform/choices.py b/pyxform/validators/pyxform/choices.py index 3e2ab55c..06fb2b92 100644 --- a/pyxform/validators/pyxform/choices.py +++ b/pyxform/validators/pyxform/choices.py @@ -1,11 +1,6 @@ from pyxform import constants -from pyxform.errors import PyXFormError +from pyxform.errors import ErrorCode, PyXFormError -INVALID_NAME = ( - "[row : {row}] On the 'choices' sheet, the 'name' value is invalid. " - "Choices must have a name. " - "Learn more: https://xlsform.org/en/#setting-up-your-worksheets" -) INVALID_LABEL = ( "[row : {row}] On the 'choices' sheet, the 'label' value is invalid. " "Choices should have a label. " @@ -44,7 +39,7 @@ def validate_choice_list( duplicate_errors = [] for option in options: if constants.NAME not in option: - raise PyXFormError(INVALID_NAME.format(row=option["__row"])) + raise PyXFormError(ErrorCode.NAMES_006.value.format(row=option["__row"])) elif constants.LABEL not in option: warnings.append(INVALID_LABEL.format(row=option["__row"])) diff --git a/tests/test_choices_sheet.py b/tests/test_choices_sheet.py index 593372c1..fffcc2c9 100644 --- a/tests/test_choices_sheet.py +++ b/tests/test_choices_sheet.py @@ -558,3 +558,20 @@ def test_reference_in_extra_columns__between_columns_of_interest(self): """, ], ) + + def test_missing_name__error(self): + """Should raise an error if name is missing in the choices sheet.""" + md = """ + | survey | + | | type | name | label | + | | select_one c1 | q1 | Q1 | + + | choices | + | | list_name | name | label | + | | c1 | | N1 | + """ + self.assertPyxformXform( + md=md, + errored=True, + error__contains=[ErrorCode.NAMES_006.value.format(row=2)], + ) From 7342b89ac4148b371d16e9d2c29151ff55b088a5 Mon Sep 17 00:00:00 2001 From: lindsay stevens Date: Tue, 16 Dec 2025 21:12:27 +1100 Subject: [PATCH 07/33] chg: move choices.INVALID_LABEL error into ErrorCode enum - organising error messages --- pyxform/errors.py | 8 ++++++++ pyxform/validators/pyxform/choices.py | 7 +------ tests/test_choices_sheet.py | 22 +++++++++++++--------- tests/test_xlsform_spec.py | 10 +++++----- 4 files changed, 27 insertions(+), 20 deletions(-) diff --git a/pyxform/errors.py b/pyxform/errors.py index fb3beca0..bde08f62 100644 --- a/pyxform/errors.py +++ b/pyxform/errors.py @@ -89,6 +89,14 @@ class ErrorCode(Enum): "Learn more: https://xlsform.org/en/#setting-up-your-worksheets" ), ) + LABEL_001: Detail = Detail( + name="Invalid missing label in the choices sheet", + msg=( + "[row : {row}] On the 'choices' sheet, the 'label' value is invalid. " + "Choices should have a label. " + "Learn more: https://xlsform.org/en/#setting-up-your-worksheets" + ), + ) PYREF_001: Detail = Detail( name="PyXForm Reference Parsing Failed", msg=( diff --git a/pyxform/validators/pyxform/choices.py b/pyxform/validators/pyxform/choices.py index 06fb2b92..82979ca1 100644 --- a/pyxform/validators/pyxform/choices.py +++ b/pyxform/validators/pyxform/choices.py @@ -1,11 +1,6 @@ from pyxform import constants from pyxform.errors import ErrorCode, PyXFormError -INVALID_LABEL = ( - "[row : {row}] On the 'choices' sheet, the 'label' value is invalid. " - "Choices should have a label. " - "Learn more: https://xlsform.org/en/#setting-up-your-worksheets" -) INVALID_HEADER = ( "[row : 1] On the 'choices' sheet, the '{column}' value is invalid. " "Column headers must not be empty and must not contain spaces. " @@ -41,7 +36,7 @@ def validate_choice_list( if constants.NAME not in option: raise PyXFormError(ErrorCode.NAMES_006.value.format(row=option["__row"])) elif constants.LABEL not in option: - warnings.append(INVALID_LABEL.format(row=option["__row"])) + warnings.append(ErrorCode.LABEL_001.value.format(row=option["__row"])) if not allow_duplicates: name = option[constants.NAME] diff --git a/tests/test_choices_sheet.py b/tests/test_choices_sheet.py index fffcc2c9..8e01b27c 100644 --- a/tests/test_choices_sheet.py +++ b/tests/test_choices_sheet.py @@ -47,10 +47,8 @@ def test_numeric_choice_names__for_dynamic_selects__allowed(self): ], ) - def test_choices_without_labels__for_static_selects__forbidden(self): - """ - Test choices without labels for static selects. Validate will fail. - """ + def test_choices_without_labels__for_static_selects__warning_and_error(self): + """Should warn (and Validate error) if a label is missing in the choices sheet.""" self.assertPyxformXform( md=""" | survey | | | | @@ -75,15 +73,17 @@ def test_choices_without_labels__for_static_selects__forbidden(self): ] """, ], + warnings__contains=[ + ErrorCode.LABEL_001.value.format(row=2), + ErrorCode.LABEL_001.value.format(row=3), + ], odk_validate_error__contains=[ "