Skip to content
Merged
Show file tree
Hide file tree
Changes from 1 commit
Commits
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
61 changes: 61 additions & 0 deletions pyxform/constants.py
Original file line number Diff line number Diff line change
Expand Up @@ -177,3 +177,64 @@ class EntityColumns(StrEnum):
SUPPORTED_MEDIA_TYPES = {"image", "big-image", "audio", "video"}
OR_OTHER_CHOICE = {NAME: "other", LABEL: "Other"}
RESERVED_NAMES_SURVEY_SHEET = {META}


##########################################################################################
# Parameters
##########################################################################################
# Name enums by question type, or if shared then use a sensible question type prefix.
# For aliased question types, use the primary documented name e.g. "image" not "photo".
# The module question_type_dictionary.py handles default parameter values, if any.
# Add new enums or keys alphabetical order.
class ParametersAudio(StrEnum):
QUALITY = "quality"


class ParametersAudit(StrEnum):
IDENTIFY_USER = "identify-user"
LOCATION_MAX_AGE = "location-max-age"
LOCATION_MIN_INTERVAL = "location-min-interval"
LOCATION_PRIORITY = "location-priority"
TRACK_CHANGES = "track-changes"
TRACK_CHANGES_REASONS = "track-changes-reasons"


class ParametersGeo(StrEnum):
ALLOW_MOCK_ACCURACY = "allow-mock-accuracy"
INCREMENTAL = "incremental"


class ParametersGeoPoint(StrEnum):
ALLOW_MOCK_ACCURACY = "allow-mock-accuracy"
CAPTURE_ACCURACY = "capture-accuracy"
WARNING_ACCURACY = "warning-accuracy"


class ParametersImage(StrEnum):
APP = "app"
MAX_PIXELS = "max-pixels"


class ParametersRange(StrEnum):
END = "end"
PLACEHOLDER = "placeholder"
START = "start"
STEP = "step"
TICK_INTERVAL = "tick_interval"
TICK_LABELSET = "tick_labelset"


class ParametersSelect(StrEnum):
RANDOMIZE = "randomize"
SEED = "seed"


class ParametersSelectFromFile(StrEnum):
LABEL = "label"
RANDOMIZE = "randomize"
SEED = "seed"
VALUE = "value"


class ParametersText(StrEnum):
ROWS = "rows"
15 changes: 15 additions & 0 deletions pyxform/errors.py
Original file line number Diff line number Diff line change
Expand Up @@ -506,6 +506,21 @@ class ErrorCode(Enum):
"be 'true' or not included."
),
)
SURVEY_004: Detail = Detail(
name="Survey sheet - parameters parsing failed",
msg=(
"[row : {row}] On the 'survey' sheet, the 'parameters' value is invalid. "
"Parameters must be in the form of 'key1=value1 key2=value2."
),
)
SURVEY_005: Detail = Detail(
name="Survey sheet - parameters unknown key",
msg=(
"[row : {row}] On the 'survey' sheet, the 'parameters' value is invalid. "
"The accepted parameter keys for this question type are '{accepted}'. "
"The following are invalid parameter key(s): '{rejected}'."
),
)


class PyXFormError(Exception):
Expand Down
55 changes: 55 additions & 0 deletions pyxform/parsing/parameters.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,55 @@
from lark import Lark, Token, Transformer
from lark.exceptions import LarkError

from pyxform.errors import ErrorCode, PyXFormError
from pyxform.parsing.expression import maybe_strip

# Label and value are used to match against user-specified files so case should be preserved.
CASE_SENSITIVE_VALUES = {"label", "value"}

PARAMETER_GRAMMAR = r"""
start: pair*
pair: TOKEN "=" TOKEN
// Anything that's not a delimiter.
TOKEN: /[^\s=,;]+/
// Delimiters between key-value pairs.
%ignore /[\s,;]+/
"""

_PARAMETER_PARSER = Lark(PARAMETER_GRAMMAR, parser="lalr", start="start")


class ParameterTransformer(Transformer):
@staticmethod
def start(pairs: list[tuple[str, str]]) -> dict[str, str]:
"""Combine (key, value) tuples into a dict"""
return dict(pairs)

@staticmethod
def pair(items: list[Token, Token]) -> tuple[str, str]:
"""Normalise matched (key, value) tokens."""
raw_key, raw_value = items
key = maybe_strip(str(raw_key).lower())
value = maybe_strip(str(raw_value))

if key not in CASE_SENSITIVE_VALUES:
value = value.lower()

return key, value


# No token-specific (ALL_CAPS) methods, so visit_tokens=False.
_PARAMETER_TRANSFORMER = ParameterTransformer(visit_tokens=False)


def parse(
raw_parameters: str,
row_number: int,
) -> dict[str, str]:
if not raw_parameters or not raw_parameters.strip():
return {}

try:
return _PARAMETER_TRANSFORMER.transform(_PARAMETER_PARSER.parse(raw_parameters))
except LarkError as e:
raise PyXFormError(code=ErrorCode.SURVEY_004, context={"row": row_number}) from e
10 changes: 9 additions & 1 deletion pyxform/util/enum.py
Original file line number Diff line number Diff line change
Expand Up @@ -28,5 +28,13 @@ def __new__(cls, *values):
return member

@classmethod
def value_list(cls):
def value_list(cls) -> list:
return list(cls.__members__.values())

@classmethod
def value_set(cls) -> set:
return set(cls.__members__.values())

@classmethod
def value_str_sorted(cls) -> str:
return ", ".join(sorted(cls.__members__.values()))
26 changes: 26 additions & 0 deletions pyxform/validators/pyxform/parameters.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,26 @@
from typing import Any

from pyxform.errors import ErrorCode, PyXFormError
from pyxform.util.enum import StrEnum

PARAMETERS_TYPE = dict[str, Any]


def validate(
parameters: PARAMETERS_TYPE,
accepted: type[StrEnum],
row_number: int,
) -> dict[str, str]:
"""
Raise an error if 'parameters' includes any keys not named in 'accepted'.
"""
extras = set(parameters) - accepted.value_set()
if 0 < len(extras):
raise PyXFormError(
ErrorCode.SURVEY_005.value.format(
row=row_number,
accepted=accepted.value_str_sorted(),
rejected=", ".join(sorted(extras)),
)
)
return parameters
49 changes: 0 additions & 49 deletions pyxform/validators/pyxform/parameters_generic.py

This file was deleted.

13 changes: 9 additions & 4 deletions pyxform/validators/pyxform/question_types.py
Original file line number Diff line number Diff line change
Expand Up @@ -8,9 +8,13 @@
from typing import Any

from pyxform import aliases
from pyxform import constants as co
from pyxform.errors import ErrorCode, PyXFormError
from pyxform.question_type_dictionary import QUESTION_TYPE_DICT
from pyxform.validators.pyxform import parameters_generic
from pyxform.validators.pyxform import parameters as pv
from pyxform.validators.pyxform.parameters import (
PARAMETERS_TYPE,
)
from pyxform.validators.pyxform.pyxform_reference import (
is_pyxform_reference_candidate,
parse_pyxform_references,
Expand Down Expand Up @@ -110,7 +114,7 @@ def validate_geo_parameter_incremental(value: str) -> None:
def process_range_question_type(
row_number: int,
row: dict[str, Any],
parameters: parameters_generic.PARAMETERS_TYPE,
parameters: PARAMETERS_TYPE,
appearance: str,
choices: dict[str, Any],
) -> dict[str, Any]:
Expand All @@ -119,9 +123,10 @@ def process_range_question_type(

Raises PyXFormError when invalid range parameters are used.
"""
parameters = parameters_generic.validate(
parameters = pv.validate(
parameters=parameters,
allowed={"start", "end", "step", "tick_interval", "placeholder", "tick_labelset"},
accepted=co.ParametersRange,
row_number=row_number,
)
if (
appearance
Expand Down
Loading