Skip to content
Merged
Show file tree
Hide file tree
Changes from 28 commits
Commits
Show all changes
36 commits
Select commit Hold shift + click to select a range
86e7479
chg: move NAMES001 error into ErrorCode enum
lindsay-stevens Dec 16, 2025
c443766
chg: move NAMES002 error into ErrorCode enum
lindsay-stevens Dec 16, 2025
b8c5265
chg: move NAMES003 error into ErrorCode enum
lindsay-stevens Dec 16, 2025
15b2283
chg: move NAMES004 error into ErrorCode enum
lindsay-stevens Dec 16, 2025
c33241e
chg: move NAMES005 error into ErrorCode enum
lindsay-stevens Dec 16, 2025
c0312b1
chg: move choices.INVALID_NAME error into ErrorCode enum
lindsay-stevens Dec 16, 2025
7342b89
chg: move choices.INVALID_LABEL error into ErrorCode enum
lindsay-stevens Dec 16, 2025
dbeb2c1
chg: move choices.INVALID_DUPLICATE error into ErrorCode enum
lindsay-stevens Dec 16, 2025
0fa7cd9
chg: move sheet_headers.INVALID_HEADER error into ErrorCode enum
lindsay-stevens Dec 16, 2025
555a511
fix: tests providing single strings to error__contains
lindsay-stevens Dec 16, 2025
76c1bc9
chg: move sheet_headers.INVALID_DUPLICATE error into ErrorCode enum
lindsay-stevens Dec 16, 2025
f9a7c74
chg: move sheet_headers.INVALID_MISSING_REQUIRED error into ErrorCode
lindsay-stevens Dec 16, 2025
318ff0b
chg: move choices.INVALID_HEADER error into ErrorCode enum
lindsay-stevens Dec 16, 2025
f37d857
chg: sort ErrorCode enum
lindsay-stevens Dec 16, 2025
abc6d11
chg: move xls2json.SURVEY_001 error into ErrorCode enum
lindsay-stevens Dec 16, 2025
c2969e3
chg: move xls2json.SURVEY_002 error into ErrorCode enum
lindsay-stevens Dec 16, 2025
cd26763
chg: move entities_parsing.ENTITY_001 error into ErrorCode enum
lindsay-stevens Dec 16, 2025
10e2dff
chg: move entities_parsing.ENTITY_002 error into ErrorCode enum
lindsay-stevens Dec 16, 2025
cbe3a0d
chg: move entities_parsing.ENTITY_003 error into ErrorCode enum
lindsay-stevens Dec 16, 2025
d498e59
chg: move entities_parsing.ENTITY_004 error into ErrorCode enum
lindsay-stevens Dec 16, 2025
751a838
chg: move entities_parsing.ENTITY_005 error into ErrorCode enum
lindsay-stevens Dec 16, 2025
6d18dc5
chg: move entities_parsing.ENTITY_006 error into ErrorCode enum
lindsay-stevens Dec 16, 2025
3d3fb24
chg: move entities_parsing.ENTITY_007 error into ErrorCode enum
lindsay-stevens Dec 16, 2025
cc6fc7e
chg: move entity dataset name character error to ErrorCode enum
lindsay-stevens Dec 17, 2025
1ab9d88
chg: replace entity save_to name error with same ErrorCode.NAMES_008
lindsay-stevens Dec 17, 2025
c3beb3e
chg: replace select_from_file name error with same ErrorCode.NAMES_008
lindsay-stevens Dec 17, 2025
0069f41
chg: replace workbook_to_json name error with same ErrorCode.NAMES_008
lindsay-stevens Dec 17, 2025
ead8652
chg: replace element/form name error with same ErrorCode.NAMES_009
lindsay-stevens Dec 17, 2025
a06d685
chg: move entity name underscores error ErrorCode enum
lindsay-stevens Dec 17, 2025
06607da
chg: move entity name period error ErrorCode enum
lindsay-stevens Dec 17, 2025
83ca653
chg: move entity name save_to reserved words error ErrorCode enum
lindsay-stevens Dec 17, 2025
d42a43c
fix: missing f-string prefix on big-image error message
lindsay-stevens Dec 17, 2025
41ac57b
add: check the "name" setting as well as "form_name"
lindsay-stevens Dec 19, 2025
c53b8c1
add: comment explaining recently added test for choice name validation
lindsay-stevens Jan 29, 2026
187b35d
add: docstrings to explain intended usage of ErrorCode and Detail
lindsay-stevens Jan 29, 2026
05f4fae
chg: make format and casing of errorcode names more consistent
lindsay-stevens Jan 29, 2026
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 0 additions & 1 deletion pyxform/constants.py
Original file line number Diff line number Diff line change
Expand Up @@ -151,7 +151,6 @@ class EntityColumns(StrEnum):
EXTERNAL_CHOICES_ITEMSET_REF_VALUE_GEOJSON = "id"

ROW_FORMAT_STRING: str = "[row : %s]"
XML_IDENTIFIER_ERROR_MESSAGE = "must begin with a letter, colon, or underscore. Other characters can include numbers, dashes, and periods."
_MSG_SUPPRESS_SPELLING = (
" If you do not mean to include a sheet, to suppress this message, "
"prefix the sheet name with an underscore. For example 'setting' "
Expand Down
89 changes: 20 additions & 69 deletions pyxform/entities/entities_parsing.py
Original file line number Diff line number Diff line change
Expand Up @@ -3,64 +3,11 @@

from pyxform import constants as const
from pyxform.elements import action
from pyxform.errors import Detail, PyXFormError
from pyxform.errors import ErrorCode, PyXFormError
from pyxform.parsing.expression import is_xml_tag
from pyxform.validators.pyxform.pyxform_reference import parse_pyxform_references

EC = const.EntityColumns
ENTITY001 = Detail(
name="Invalid entity repeat reference",
msg=(
"[row : 2] On the 'entities' sheet, the 'repeat' value '{value}' is invalid. "
"The 'repeat' column, if specified, must contain only a single reference variable "
"(like '${{q1}}'), and the reference variable must contain a valid name."
),
)
ENTITY002 = Detail(
name="Invalid entity repeat: target not found",
msg=(
"[row : 2] On the 'entities' sheet, the 'repeat' value '{value}' is invalid. "
"The entity repeat target was not found in the 'survey' sheet."
),
)
ENTITY003 = Detail(
name="Invalid entity repeat: target is not a repeat",
msg=(
"[row : 2] On the 'entities' sheet, the 'repeat' value '{value}' is invalid. "
"The entity repeat target is not a repeat."
),
)
ENTITY004 = Detail(
name="Invalid entity repeat: target is in a repeat",
msg=(
"[row : 2] On the 'entities' sheet, the 'repeat' value '{value}' is invalid. "
"The entity repeat target is inside a repeat."
),
)
ENTITY005 = Detail(
name="Invalid entity repeat save_to: question in nested repeat",
msg=(
"[row : {row}] On the 'survey' sheet, the 'save_to' value '{value}' is invalid. "
"The entity property populated with 'save_to' must not be inside of a nested "
"repeat within the entity repeat."
),
)
ENTITY006 = Detail(
name="Invalid entity repeat save_to: question not in entity repeat",
msg=(
"[row : {row}] On the 'survey' sheet, the 'save_to' value '{value}' is invalid. "
"The entity property populated with 'save_to' must be inside of the entity "
"repeat."
),
)
ENTITY007 = Detail(
name="Invalid entity repeat save_to: question in repeat but no entity repeat defined",
msg=(
"[row : {row}] On the 'survey' sheet, the 'save_to' value '{value}' is invalid. "
"The entity property populated with 'save_to' must be inside a repeat that is "
"declared in the 'repeat' column of the 'entities' sheet."
),
)


def get_entity_declaration(
Expand Down Expand Up @@ -241,11 +188,10 @@ def get_validated_dataset_name(entity):
)

if not is_xml_tag(dataset):
if isinstance(dataset, bytes):
dataset = dataset.decode("utf-8")

raise PyXFormError(
f"Invalid entity list name: '{dataset}'. Names must begin with a letter, colon, or underscore. Other characters can include numbers or dashes."
ErrorCode.NAMES_008.value.format(
sheet=const.ENTITIES, row=2, column=EC.DATASET.value
)
)

return dataset
Expand All @@ -263,7 +209,7 @@ def get_validated_repeat_name(entity) -> str | None:
raise
else:
if not match or match[0].last_saved:
raise PyXFormError(ENTITY001.format(value=value))
raise PyXFormError(ErrorCode.ENTITY_001.value.format(value=value))
else:
return match[0].name

Expand Down Expand Up @@ -297,18 +243,24 @@ def validate_entity_saveto(
elif i["control_type"] == const.REPEAT:
# Error: saveto in nested repeat inside entity repeat.
if in_repeat:
raise PyXFormError(ENTITY005.format(row=row_number, value=save_to))
raise PyXFormError(
ErrorCode.ENTITY_005.value.format(row=row_number, value=save_to)
)
elif i["control_name"] == entity_repeat:
located = True
in_repeat = True

# Error: saveto not in entity repeat
if entity_repeat and not located:
raise PyXFormError(ENTITY006.format(row=row_number, value=save_to))
raise PyXFormError(
ErrorCode.ENTITY_006.value.format(row=row_number, value=save_to)
)

# Error: saveto in repeat but no entity repeat declared
if in_repeat and not entity_repeat:
raise PyXFormError(ENTITY007.format(row=row_number, value=save_to))
raise PyXFormError(
ErrorCode.ENTITY_007.value.format(row=row_number, value=save_to)
)

error_start = f"{const.ROW_FORMAT_STRING % row_number} Invalid save_to name:"

Expand All @@ -323,11 +275,10 @@ def validate_entity_saveto(
)

if not is_xml_tag(save_to):
if isinstance(save_to, bytes):
save_to = save_to.decode("utf-8")

raise PyXFormError(
f"{error_start} '{save_to}'. Entity property names {const.XML_IDENTIFIER_ERROR_MESSAGE}"
ErrorCode.NAMES_008.value.format(
sheet=const.SURVEY, row=row_number, column=const.ENTITIES_SAVETO
)
)


Expand Down Expand Up @@ -370,7 +321,7 @@ def validate_entity_repeat_target(

# Error: repeat not found while processing survey sheet.
if not stack:
raise PyXFormError(ENTITY002.format(value=entity_repeat))
raise PyXFormError(ErrorCode.ENTITY_002.value.format(value=entity_repeat))

control_name = stack[-1]["control_name"]
control_type = stack[-1]["control_type"]
Expand All @@ -381,7 +332,7 @@ def validate_entity_repeat_target(

# Error: target is not a repeat.
if control_type and control_type != const.REPEAT:
raise PyXFormError(ENTITY003.format(value=entity_repeat))
raise PyXFormError(ErrorCode.ENTITY_003.value.format(value=entity_repeat))

# Error: repeat is in nested repeat.
located = False
Expand All @@ -390,7 +341,7 @@ def validate_entity_repeat_target(
break
elif i["control_type"] == const.REPEAT:
if located:
raise PyXFormError(ENTITY004.format(value=entity_repeat))
raise PyXFormError(ErrorCode.ENTITY_004.value.format(value=entity_repeat))
elif i["control_name"] == entity_repeat:
located = True

Expand Down
192 changes: 187 additions & 5 deletions pyxform/errors.py
Original file line number Diff line number Diff line change
Expand Up @@ -41,6 +41,182 @@ def format(self, **kwargs):


class ErrorCode(Enum):
ENTITY_001 = Detail(
name="Invalid entity repeat reference",
msg=(
"[row : 2] On the 'entities' sheet, the 'repeat' value '{value}' is invalid. "
"The 'repeat' column, if specified, must contain only a single reference variable "
"(like '${{q1}}'), and the reference variable must contain a valid name."
),
)
ENTITY_002 = Detail(
Comment thread
lindsay-stevens marked this conversation as resolved.
name="Invalid entity repeat: target not found",
msg=(
"[row : 2] On the 'entities' sheet, the 'repeat' value '{value}' is invalid. "
"The entity repeat target was not found in the 'survey' sheet."
),
)
ENTITY_003 = Detail(
name="Invalid entity repeat: target is not a repeat",
msg=(
"[row : 2] On the 'entities' sheet, the 'repeat' value '{value}' is invalid. "
"The entity repeat target is not a repeat."
),
)
ENTITY_004 = Detail(
name="Invalid entity repeat: target is in a repeat",
msg=(
"[row : 2] On the 'entities' sheet, the 'repeat' value '{value}' is invalid. "
"The entity repeat target is inside a repeat."
),
)
ENTITY_005 = Detail(
name="Invalid entity repeat save_to: question in nested repeat",
msg=(
"[row : {row}] On the 'survey' sheet, the 'save_to' value '{value}' is invalid. "
"The entity property populated with 'save_to' must not be inside of a nested "
"repeat within the entity repeat."
),
)
ENTITY_006 = Detail(
name="Invalid entity repeat save_to: question not in entity repeat",
msg=(
"[row : {row}] On the 'survey' sheet, the 'save_to' value '{value}' is invalid. "
"The entity property populated with 'save_to' must be inside of the entity "
"repeat."
),
)
ENTITY_007 = Detail(
name="Invalid entity repeat save_to: question in repeat but no entity repeat defined",
msg=(
"[row : {row}] On the 'survey' sheet, the 'save_to' value '{value}' is invalid. "
"The entity property populated with 'save_to' must be inside a repeat that is "
"declared in the 'repeat' column of the 'entities' sheet."
),
)
HEADER_001: Detail = Detail(
name="Invalid missing header row.",
msg=(
"Invalid headers provided for sheet: '{sheet_name}'. For XLSForms, this may be due "
"a missing header row, in which case add a header row as per the reference template "
"https://xlsform.org/en/ref-table/. For internal API usage, may be due to a missing "
"mapping for '{header}', in which case ensure that the full set of headers appear "
"within the first 100 rows, or specify the header row in '{sheet_name}_header'."
),
)
HEADER_002: Detail = Detail(
name="Invalid duplicate header.",
msg=(
"Invalid headers provided for sheet: '{sheet_name}'. Headers that are different "
"names for the same column were found: '{other}', '{header}'. Rename or remove one "
"of these columns."
),
)
HEADER_003: Detail = Detail(
name="Invalid missing required header.",
msg=(
"Invalid headers provided for sheet: '{sheet_name}'. One or more required column "
"headers were not found: {missing}. "
"Learn more: https://xlsform.org/en/#setting-up-your-worksheets"
),
)
HEADER_004: Detail = Detail(
name="Invalid choices header.",
msg=(
"[row : 1] On the 'choices' sheet, the '{column}' value is invalid. "
"Column headers must not be empty and must not contain spaces. "
"Learn more: https://xlsform.org/en/#setting-up-your-worksheets"
),
)
INTERNAL_001: Detail = Detail(
name="Internal error: Incorrectly Processed Question Trigger Data",
Comment thread
lindsay-stevens marked this conversation as resolved.
Outdated
msg=(
"Internal error: "
"PyXForm expected processed trigger data as a tuple, but received a "
"type '{type}' with value '{value}'."
),
)
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"
),
)
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."
),
)
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. "
"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."
),
)
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 = 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."
),
)
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",
Comment thread
lindsay-stevens marked this conversation as resolved.
Outdated
msg=(
"[row : {row}] On the 'choices' sheet, the 'name' value is invalid. "
Comment thread
lindsay-stevens marked this conversation as resolved.
"Choices must have a name. "
"Learn more: https://xlsform.org/en/#setting-up-your-worksheets"
),
)
NAMES_007: Detail = Detail(
name="Invalid duplicate name in the choices sheet",
msg=(
"[row : {row}] On the 'choices' sheet, the 'name' value is invalid. "
"Choice names must be unique for each choice list. "
"If this is intentional, use the setting 'allow_choice_duplicates'. "
"Learn more: https://xlsform.org/#choice-names."
),
)
NAMES_008: Detail = Detail(
name="Invalid character(s) in name (XML identifier).",
msg=(
"[row : {row}] On the '{sheet}' sheet, the '{column}' value is invalid. "
"Names must begin with a letter or underscore. After the first character, "
"names may contain letters, digits, underscores, hyphens, or periods."
),
)
NAMES_009: Detail = Detail(
name="Invalid character(s) in name (XML identifier)(no sheet context).",
msg=(
"The '{name}' value is invalid. "
"Names must begin with a letter or underscore. After the first character, "
"names may contain letters, digits, underscores, hyphens, or periods."
),
)
PYREF_001: Detail = Detail(
name="PyXForm Reference Parsing Failed",
msg=(
Expand Down Expand Up @@ -71,12 +247,18 @@ class ErrorCode(Enum):
"'{q}' appears more than once."
),
)
INTERNAL_001: Detail = Detail(
name="Internal error: Incorrectly Processed Question Trigger Data",
SURVEY_001 = Detail(
name="Survey Sheet Unmatched Group/Repeat/Loop End",
Comment thread
lindsay-stevens marked this conversation as resolved.
Outdated
msg=(
"Internal error: "
"PyXForm expected processed trigger data as a tuple, but received a "
"type '{type}' with value '{value}'."
"[row : {row}] Unmatched 'end_{type}'. "
"No matching 'begin_{type}' was found for the name '{name}'."
),
)
SURVEY_002 = Detail(
name="Survey Sheet Unmatched Group/Repeat/Loop Begin",
Comment thread
lindsay-stevens marked this conversation as resolved.
Outdated
msg=(
"[row : {row}] Unmatched 'begin_{type}'. "
"No matching 'end_{type}' was found for the name '{name}'."
),
)
SURVEY_003: Detail = Detail(
Expand Down
Loading