From 59ef17bb3170d2e1a76ad0388b6fd865ee593f8a Mon Sep 17 00:00:00 2001 From: lindsay stevens Date: Mon, 13 Jul 2026 19:10:07 +1000 Subject: [PATCH 01/50] dev: update github actions versions to latest available --- .github/workflows/release.yml | 6 +++--- .github/workflows/verify.yml | 14 +++++++------- 2 files changed, 10 insertions(+), 10 deletions(-) diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index 02a02c77..b01884ed 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -12,14 +12,14 @@ jobs: python: ['3.12'] os: [ubuntu-latest] steps: - - uses: actions/checkout@v4 + - uses: actions/checkout@v7 - name: Set up Python - uses: actions/setup-python@v5 + uses: actions/setup-python@v6 with: python-version: ${{ matrix.python }} # Install dependencies. - - uses: actions/cache@v4 + - uses: actions/cache@v6 name: Python cache with dependencies. id: python-cache with: diff --git a/.github/workflows/verify.yml b/.github/workflows/verify.yml index 6f7131ca..c0731e50 100644 --- a/.github/workflows/verify.yml +++ b/.github/workflows/verify.yml @@ -10,14 +10,14 @@ jobs: python: ['3.12'] os: [ubuntu-latest] steps: - - uses: actions/checkout@v4 + - uses: actions/checkout@v7 - name: Set up Python - uses: actions/setup-python@v5 + uses: actions/setup-python@v6 with: python-version: ${{ matrix.python }} # Install dependencies. - - uses: actions/cache@v4 + - uses: actions/cache@v6 name: Python cache with dependencies. id: python-cache with: @@ -49,14 +49,14 @@ jobs: os: ubuntu-latest PYXFORM_TESTS_RUN_ODK_VALIDATE: 'true' steps: - - uses: actions/checkout@v4 + - uses: actions/checkout@v7 - name: Set up Python - uses: actions/setup-python@v5 + uses: actions/setup-python@v6 with: python-version: ${{ matrix.python }} # Install dependencies. - - uses: actions/cache@v4 + - uses: actions/cache@v6 name: Python cache with dependencies. id: python-cache with: @@ -82,7 +82,7 @@ jobs: flit --debug build --no-use-vcs - name: Upload sdist and wheel. if: success() && matrix.PYXFORM_TESTS_RUN_ODK_VALIDATE == 'false' - uses: actions/upload-artifact@v4 + uses: actions/upload-artifact@v7 with: name: pyxform--on-${{ matrix.os }}--py${{ matrix.python }} path: ${{ github.workspace }}/dist/pyxform* From b886e77555ed0d2f9fc80f1794339b021ef9926d Mon Sep 17 00:00:00 2001 From: lindsay stevens Date: Mon, 13 Jul 2026 19:13:40 +1000 Subject: [PATCH 02/50] dev: bump python versions --- .github/workflows/release.yml | 2 +- .github/workflows/verify.yml | 6 +++--- README.rst | 4 ++-- 3 files changed, 6 insertions(+), 6 deletions(-) diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index b01884ed..f9f7c4b1 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -9,7 +9,7 @@ jobs: runs-on: ${{ matrix.os }} strategy: matrix: - python: ['3.12'] + python: ['3.13'] os: [ubuntu-latest] steps: - uses: actions/checkout@v7 diff --git a/.github/workflows/verify.yml b/.github/workflows/verify.yml index c0731e50..67ff469d 100644 --- a/.github/workflows/verify.yml +++ b/.github/workflows/verify.yml @@ -7,7 +7,7 @@ jobs: runs-on: ${{ matrix.os }} strategy: matrix: - python: ['3.12'] + python: ['3.13'] os: [ubuntu-latest] steps: - uses: actions/checkout@v7 @@ -39,13 +39,13 @@ jobs: # Run all matrix jobs even if one of them fails. fail-fast: false matrix: - python: ['3.10', '3.11', '3.12', '3.13'] + python: ['3.11', '3.12', '3.13', '3.14'] os: [ubuntu-latest, macos-latest, windows-latest] PYXFORM_TESTS_RUN_ODK_VALIDATE: ['false'] # Extra job to check PyxformTestCase tests pass with ODK Validate. # Keep versions sync with default dev version (same as 'lint' job above). include: - - python: '3.12' + - python: '3.13' os: ubuntu-latest PYXFORM_TESTS_RUN_ODK_VALIDATE: 'true' steps: diff --git a/README.rst b/README.rst index 8c7d43cf..f984549e 100644 --- a/README.rst +++ b/README.rst @@ -7,7 +7,7 @@ pyxform .. |pypi| image:: https://badge.fury.io/py/pyxform.svg :target: https://badge.fury.io/py/pyxform -.. |python| image:: https://img.shields.io/badge/python-3.10,3.11,3.12,3.13-blue.svg +.. |python| image:: https://img.shields.io/badge/python-3.11,3.12,3.13,3.14-blue.svg :target: https://www.python.org/downloads ``pyxform`` is a Python library that simplifies writing forms for ODK Collect and Enketo by converting spreadsheets that follow the `XLSForm standard `_ into `ODK XForms `_. The XLSForms format is used in a `number of tools `_. @@ -44,7 +44,7 @@ The ``xls2xform`` command can then be used:: xls2xform path_to_XLSForm [output_path] -The currently supported Python versions for ``pyxform`` are 3.10 to 3.13 (the primary development version is 3.12). If this is different from the version you use for other projects, consider using `pyenv `_ to manage multiple versions of Python. +The currently supported Python versions for ``pyxform`` are 3.11 to 3.14 (the primary development version is 3.13). If this is different from the version you use for other projects, consider using `pyenv `_ to manage multiple versions of Python. Running pyxform from local source --------------------------------- From 1054c260a39eee7b3036c9d66017290fba02bb11 Mon Sep 17 00:00:00 2001 From: lindsay stevens Date: Mon, 13 Jul 2026 19:27:50 +1000 Subject: [PATCH 03/50] dev: bump dev dependencies --- .github/workflows/release.yml | 2 +- .github/workflows/verify.yml | 2 +- README.rst | 2 +- pyproject.toml | 5 +++-- 4 files changed, 6 insertions(+), 5 deletions(-) diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index f9f7c4b1..f3666088 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -35,7 +35,7 @@ jobs: - name: Publish release to PyPI if: success() run: | - pip install flit==3.9.0 + pip install flit==3.12.0 flit --debug publish --no-use-vcs env: FLIT_USERNAME: __token__ diff --git a/.github/workflows/verify.yml b/.github/workflows/verify.yml index 67ff469d..d3451bd0 100644 --- a/.github/workflows/verify.yml +++ b/.github/workflows/verify.yml @@ -78,7 +78,7 @@ jobs: - name: Build sdist and wheel. if: success() && matrix.PYXFORM_TESTS_RUN_ODK_VALIDATE == 'false' run: | - pip install flit==3.9.0 + pip install flit==3.12.0 flit --debug build --no-use-vcs - name: Upload sdist and wheel. if: success() && matrix.PYXFORM_TESTS_RUN_ODK_VALIDATE == 'false' diff --git a/README.rst b/README.rst index f984549e..be26a1a0 100644 --- a/README.rst +++ b/README.rst @@ -163,7 +163,7 @@ Releases are now automatic. These instructions are provided for forks or for a f 3. Install the production and packaging requirements:: pip install -e . - pip install flit==3.9.0 + pip install flit==3.12.0 4. Clean up build and dist folders:: diff --git a/pyproject.toml b/pyproject.toml index 4afb9a16..6113b5e3 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -18,8 +18,8 @@ dependencies = [ # Install with `pip install pyxform[dev]`. dev = [ "formencode==2.1.1", # Compare XML - "lxml==6.0.0", # XPath test expressions - "psutil==7.0.0", # Process info for performance tests + "lxml==6.1.1", # XPath test expressions + "psutil==7.2.2", # Process info for performance tests "ruff==0.12.4", # Format and lint ] @@ -32,6 +32,7 @@ xls2xform = "pyxform.xls2xform:main_cli" pyxform_validator_update = "pyxform.validators.updater:main_cli" [build-system] +# As of 3.12.0 this is still the flit readme template. requires = ["flit_core >=3.2,<4"] build-backend = "flit_core.buildapi" From 03857cd031f7d598b272b1b983e9a4d41eb7580a Mon Sep 17 00:00:00 2001 From: lindsay stevens Date: Mon, 13 Jul 2026 19:45:36 +1000 Subject: [PATCH 04/50] dev: bump dev dependencies (ruff) + linting - resolve new warnings on RUF059 (unused vars) and B017 (broad err). - re-checked commented out rules with ruff version markers, and the noted issues are still present, so updated the noted version. --- pyproject.toml | 8 ++++---- pyxform/file_utils.py | 4 ++-- pyxform/xform2json.py | 2 +- tests/test_pyxformtestcase.py | 4 ++-- tests/utils.py | 4 ++-- tests/xform_test_case/test_bugs.py | 2 +- 6 files changed, 12 insertions(+), 12 deletions(-) diff --git a/pyproject.toml b/pyproject.toml index 6113b5e3..584af5ba 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -20,7 +20,7 @@ dev = [ "formencode==2.1.1", # Compare XML "lxml==6.1.1", # XPath test expressions "psutil==7.2.2", # Process info for performance tests - "ruff==0.12.4", # Format and lint + "ruff==0.15.21", # Format and lint ] [project.urls] @@ -74,7 +74,7 @@ select = [ ] ignore = [ "E501", # line-too-long (we have a lot of long strings) - "F821", # undefined-name (doesn't work well with type hints, ruff 0.1.11). + "F821", # undefined-name (doesn't work well with type hints, ruff 0.15.21). "PERF401", # manual-list-comprehension (false positives on selective transforms) "PERF402", # manual-list-copy (false positives on selective transforms) "PLC0415", # import-outside-top-level (pyxform has a few to avoid circular imports) @@ -85,8 +85,8 @@ ignore = [ "PLR2004", # magic-value-comparison (many tests expect certain numbers of things) "PLW2901", # redefined-loop-name (usually not a bug) "RUF001", # ambiguous-unicode-character-string (false positives on unicode tests) - "S310", # suspicious-url-open-usage (prone to false positives, ruff 0.1.11) - "S603", # subprocess-without-shell-equals-true (prone to false positives, ruff 0.1.11) + "S310", # suspicious-url-open-usage (prone to false positives, ruff 0.15.21) + "S603", # subprocess-without-shell-equals-true (prone to false positives, ruff 0.15.21) "TRY003", # raise-vanilla-args (reasonable lint but would require large refactor) ] # per-file-ignores = {"tests/*" = ["E501"]} diff --git a/pyxform/file_utils.py b/pyxform/file_utils.py index 430f40f8..41dd7566 100644 --- a/pyxform/file_utils.py +++ b/pyxform/file_utils.py @@ -10,8 +10,8 @@ def _section_name(path_or_file_name): - directory, filename = os.path.split(path_or_file_name) - section_name, extension = os.path.splitext(filename) + _, filename = os.path.split(path_or_file_name) + section_name, _ = os.path.splitext(filename) return section_name diff --git a/pyxform/xform2json.py b/pyxform/xform2json.py index edf2576b..a656cf48 100644 --- a/pyxform/xform2json.py +++ b/pyxform/xform2json.py @@ -660,7 +660,7 @@ def _get_constraint_msg(self, constraint_msg): if isinstance(constraint_msg, str): if constraint_msg.find(":jr:constraintMsg") != -1: ref = constraint_msg.replace("jr:itext('", "").replace("')", "") - k, constraint_msg = self._get_text_from_translation(ref) + _, constraint_msg = self._get_text_from_translation(ref) return constraint_msg def _get_choices(self) -> dict[str, Any]: diff --git a/tests/test_pyxformtestcase.py b/tests/test_pyxformtestcase.py index 89b4b3ea..ae164f12 100644 --- a/tests/test_pyxformtestcase.py +++ b/tests/test_pyxformtestcase.py @@ -3,7 +3,7 @@ internal conversions correctly. """ -from tests.pyxform_test_case import PyxformTestCase +from tests.pyxform_test_case import PyxformTestCase, PyxformTestError from tests.xpath_helpers.settings import xps @@ -28,7 +28,7 @@ def _no_valid_flag(): errored=False, # errored=False by default ) - self.assertRaises(Exception, _no_valid_flag) + self.assertRaises(PyxformTestError, _no_valid_flag) # however when errored=True is present, self.assertPyxformXform( diff --git a/tests/utils.py b/tests/utils.py index 7aa6c313..8a940570 100644 --- a/tests/utils.py +++ b/tests/utils.py @@ -27,10 +27,10 @@ def build_survey(filename): def create_survey_from_fixture(fixture_name, filetype="xls", include_directory=False): fixture_path = path_to_text_fixture(f"{fixture_name}.{filetype}") - noop, section_dict = file_utils.load_file_to_dict(fixture_path) + _, section_dict = file_utils.load_file_to_dict(fixture_path) pkg = {"main_section": section_dict} if include_directory: - directory, noop = os.path.split(fixture_path) + directory, _ = os.path.split(fixture_path) pkg["sections"] = file_utils.collect_compatible_files_in_directory(directory) return create_survey(**pkg) diff --git a/tests/xform_test_case/test_bugs.py b/tests/xform_test_case/test_bugs.py index 83f326c9..8f9815b2 100644 --- a/tests/xform_test_case/test_bugs.py +++ b/tests/xform_test_case/test_bugs.py @@ -42,7 +42,7 @@ def test_conversion(): filename = "ODKValidateWarnings.xlsx" path_to_excel_file = os.path.join(bug_example_xls.PATH, filename) # Get the xform output path: - root_filename, ext = os.path.splitext(filename) + root_filename, _ = os.path.splitext(filename) output_path = os.path.join(test_output.PATH, root_filename + ".xml") # Do the conversion: warnings = [] From f2171c0e996bfdd9aa66781e0d86def004129ca2 Mon Sep 17 00:00:00 2001 From: lindsay stevens Date: Mon, 13 Jul 2026 20:05:59 +1000 Subject: [PATCH 05/50] dev: bump minimum python, apply linting - removes strenum copy which is only present in py3.11+ so that change is not backwards compatible. However, ruff warns that keeping it no good either since StrEnum exists in py311. --- pyproject.toml | 4 ++-- pyxform/util/enum.py | 27 ++------------------------- pyxform/xls2json_backends.py | 2 +- tests/test_xls2json_backends.py | 8 ++++---- 4 files changed, 9 insertions(+), 32 deletions(-) diff --git a/pyproject.toml b/pyproject.toml index 584af5ba..523a452d 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -6,7 +6,7 @@ authors = [ ] description = "A Python package to create XForms for ODK Collect." readme = "README.rst" -requires-python = ">=3.10" +requires-python = ">=3.11" dependencies = [ "xlrd==2.0.1", # Read XLS files "openpyxl==3.1.5", # Read XLSX files @@ -44,7 +44,7 @@ exclude = ["docs", "tests"] [tool.ruff] line-length = 90 -target-version = "py310" +target-version = "py311" fix = true show-fixes = true output-format = "full" diff --git a/pyxform/util/enum.py b/pyxform/util/enum.py index 41bec2ae..980f2ab7 100644 --- a/pyxform/util/enum.py +++ b/pyxform/util/enum.py @@ -1,32 +1,9 @@ -from enum import Enum +from enum import StrEnum as StrEnumBase -class StrEnum(str, Enum): +class StrEnum(StrEnumBase): """Base Enum class with common helper function.""" - # Copied from Python 3.11 enum.py. In many cases can use members as strings, but - # sometimes need to deref with ".value" property e.g. `EnumClass.MEMBERNAME.value`. - def __new__(cls, *values): - "values must already be of type `str`" - if len(values) > 3: - raise TypeError(f"too many arguments for str(): {values!r}") - if len(values) == 1: - # it must be a string - if not isinstance(values[0], str): - raise TypeError(f"{values[0]!r} is not a string") - if len(values) >= 2: - # check that encoding argument is a string - if not isinstance(values[1], str): - raise TypeError(f"encoding must be a string, not {values[1]!r}") - if len(values) == 3: - # check that errors argument is a string - if not isinstance(values[2], str): - raise TypeError(f"errors must be a string, not {values[2]!r}") - value = str(*values) - member = str.__new__(cls, value) - member._value_ = value - return member - @classmethod def value_list(cls) -> list: return list(cls.__members__.values()) diff --git a/pyxform/xls2json_backends.py b/pyxform/xls2json_backends.py index 306d53e0..91b4ce0e 100644 --- a/pyxform/xls2json_backends.py +++ b/pyxform/xls2json_backends.py @@ -742,7 +742,7 @@ def definition_to_dict( return DefinitionData( fallback_form_name=definition.file_path_stem, **func(definition) ) - except PyXFormReadError: # noqa: PERF203 + except PyXFormReadError: continue raise PyXFormError( diff --git a/tests/test_xls2json_backends.py b/tests/test_xls2json_backends.py index 743f8422..45f39648 100644 --- a/tests/test_xls2json_backends.py +++ b/tests/test_xls2json_backends.py @@ -180,9 +180,9 @@ def test_xls_with_many_empty_cells(self): """Should quickly produce expected data, and find large input sheet dimensions.""" # Test fixture produced by adding data at cells IV1 and A19999. xls_path = os.path.join(bug_example_xls.PATH, "extra_columns.xls") - before = datetime.datetime.now(datetime.timezone.utc) + before = datetime.datetime.now(datetime.UTC) xls_data = xls_to_dict(xls_path) - after = datetime.datetime.now(datetime.timezone.utc) + after = datetime.datetime.now(datetime.UTC) self.assertLess((after - before).total_seconds(), 5) wb = xlrd.open_workbook(filename=xls_path) @@ -203,9 +203,9 @@ def test_xlsx_with_many_empty_cells(self): """Should quickly produce expected data, and find large input sheet dimensions.""" # Test fixture produced (presumably) by a LibreOffice serialisation bug. xlsx_path = os.path.join(bug_example_xls.PATH, "UCL_Biomass_Plot_Form.xlsx") - before = datetime.datetime.now(datetime.timezone.utc) + before = datetime.datetime.now(datetime.UTC) xlsx_data = xlsx_to_dict(xlsx_path) - after = datetime.datetime.now(datetime.timezone.utc) + after = datetime.datetime.now(datetime.UTC) self.assertLess((after - before).total_seconds(), 5) wb = openpyxl.open(filename=xlsx_path, read_only=True, data_only=True) From 9c8e758b3a8bdc3ce768876090d8db23fd57838c Mon Sep 17 00:00:00 2001 From: lindsay stevens Date: Mon, 13 Jul 2026 20:18:08 +1000 Subject: [PATCH 06/50] dev: add and apply ruff ruleset "furb" - refurb "A tool for refurbishing and modernizing Python codebases." --- pyproject.toml | 1 + pyxform/errors.py | 2 +- pyxform/validators/error_cleaner.py | 4 +++- pyxform/xform2json.py | 4 ++-- 4 files changed, 7 insertions(+), 4 deletions(-) diff --git a/pyproject.toml b/pyproject.toml index 523a452d..370cdf92 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -58,6 +58,7 @@ select = [ "E", # pycodestyle error # "ERA", # eradicate (commented out code) "F", # pyflakes + "FURB", # refurb "I", # isort "PERF", # perflint "PIE", # flake8-pie diff --git a/pyxform/errors.py b/pyxform/errors.py index f8c71c29..88ceb2fe 100644 --- a/pyxform/errors.py +++ b/pyxform/errors.py @@ -566,7 +566,7 @@ def __init__( """ super().__init__(*args) self.code: ErrorCode | None = code - self.context: dict = context if context else {} + self.context: dict = context or {} def __str__(self): return self.__repr__() diff --git a/pyxform/validators/error_cleaner.py b/pyxform/validators/error_cleaner.py index 642645e4..2a06acdb 100644 --- a/pyxform/validators/error_cleaner.py +++ b/pyxform/validators/error_cleaner.py @@ -4,7 +4,9 @@ import re -ERROR_MESSAGE_REGEX = re.compile(r"(/[a-z0-9\-_]+(?:/[a-z0-9\-_]+)+)", flags=re.I) +ERROR_MESSAGE_REGEX = re.compile( + r"(/[a-z0-9\-_]+(?:/[a-z0-9\-_]+)+)", flags=re.IGNORECASE +) class ErrorCleaner: diff --git a/pyxform/xform2json.py b/pyxform/xform2json.py index a656cf48..020ae877 100644 --- a/pyxform/xform2json.py +++ b/pyxform/xform2json.py @@ -707,9 +707,9 @@ def replace_function(match): # moving re flags into compile for python 2.6 compat pattern = "( /[a-z0-9-_]+(?:/[a-z0-9-_]+)+ )" - text = re.compile(pattern, flags=re.I).sub(replace_function, text) + text = re.compile(pattern, flags=re.IGNORECASE).sub(replace_function, text) pattern = "(/[a-z0-9-_]+(?:/[a-z0-9-_]+)+)" - text = re.compile(pattern, flags=re.I).sub(replace_function, text) + text = re.compile(pattern, flags=re.IGNORECASE).sub(replace_function, text) return text From 16a05c7db3acdd6f108d4746dc94394709e6765d Mon Sep 17 00:00:00 2001 From: lindsay stevens Date: Mon, 13 Jul 2026 21:02:44 +1000 Subject: [PATCH 07/50] dev: add and apply pydocstyle rule D403 - the full pydocstyle ruleset has many changes so enabling some of them individually to keep the diffs manageable. --- pyproject.toml | 5 ++++- pyxform/builder.py | 2 +- pyxform/file_utils.py | 2 +- pyxform/survey.py | 6 +++--- pyxform/utils.py | 2 +- pyxform/xform2json.py | 2 +- tests/test_builder.py | 2 +- tests/test_geo.py | 2 +- tests/test_j2x_creation.py | 2 +- tests/test_j2x_question.py | 12 ++++++------ tests/test_pyxformtestcase.py | 6 +++--- tests/test_sheet_columns.py | 4 ++-- 12 files changed, 25 insertions(+), 22 deletions(-) diff --git a/pyproject.toml b/pyproject.toml index 370cdf92..489136da 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -55,6 +55,7 @@ src = ["pyxform", "tests"] select = [ "B", # flake8-bugbear "C4", # flake8-comprehensions + "D403", # pydocstyle (first-word-uncapitalized) "E", # pycodestyle error # "ERA", # eradicate (commented out code) "F", # pyflakes @@ -90,4 +91,6 @@ ignore = [ "S603", # subprocess-without-shell-equals-true (prone to false positives, ruff 0.15.21) "TRY003", # raise-vanilla-args (reasonable lint but would require large refactor) ] -# per-file-ignores = {"tests/*" = ["E501"]} + +[tool.ruff.lint.pydocstyle] +convention = "pep257" diff --git a/pyxform/builder.py b/pyxform/builder.py index 6a8b2c26..51c0901a 100644 --- a/pyxform/builder.py +++ b/pyxform/builder.py @@ -68,7 +68,7 @@ def __init__(self, **kwargs): def set_sections(self, sections): """ - sections is a dict of python objects, a key in this dict is + Sections is a dict of python objects, a key in this dict is the name of the section and the value is a dict that can be used to create a whole survey. """ diff --git a/pyxform/file_utils.py b/pyxform/file_utils.py index 41dd7566..b208e416 100644 --- a/pyxform/file_utils.py +++ b/pyxform/file_utils.py @@ -33,7 +33,7 @@ def load_file_to_dict(path): def collect_compatible_files_in_directory(directory): """ - create a giant dict out of all the spreadsheets and json forms + Create a giant dict out of all the spreadsheets and json forms in the given directory """ available_files = glob.glob(os.path.join(directory, "*.xls")) + glob.glob( diff --git a/pyxform/survey.py b/pyxform/survey.py index 3d89911e..72b68a05 100644 --- a/pyxform/survey.py +++ b/pyxform/survey.py @@ -290,7 +290,7 @@ def get_nsmap(self): def xml(self): """ - calls necessary preparation methods, then returns the xml. + Calls necessary preparation methods, then returns the xml. """ self.validate() self._setup_xpath_dictionary() @@ -739,7 +739,7 @@ def _redirect_is_search_itext(self, element: MultipleChoiceQuestion) -> bool: def _setup_translations(self): """ - set up the self._translations dict which will be referenced in the + Set up the self._translations dict which will be referenced in the setup media and itext functions """ @@ -1040,7 +1040,7 @@ def _var_repl_function( def _in_secondary_instance_predicate() -> bool: """ - check if ${} expression represented by matchobj + Check if ${} expression represented by matchobj is in a predicate for a path expression for a secondary instance """ diff --git a/pyxform/utils.py b/pyxform/utils.py index 6b16f870..e0ba94c5 100644 --- a/pyxform/utils.py +++ b/pyxform/utils.py @@ -155,7 +155,7 @@ def get_pyobj_from_json(str_or_path): def print_pyobj_to_json(pyobj, path=None): """ - dump a python nested array/dict structure to the specified file + Dump a python nested array/dict structure to the specified file or stdout if no file is specified """ if path: diff --git a/pyxform/xform2json.py b/pyxform/xform2json.py index 020ae877..74b18e9d 100644 --- a/pyxform/xform2json.py +++ b/pyxform/xform2json.py @@ -683,7 +683,7 @@ def _get_choices(self) -> dict[str, Any]: @staticmethod def _get_name_from_ref(ref): - """given /xlsform_spec_test/launch, + """Given /xlsform_spec_test/launch, return the string after the last occurance of the character '/' """ pos = ref.rfind("/") diff --git a/tests/test_builder.py b/tests/test_builder.py index de0e069d..540bfe85 100644 --- a/tests/test_builder.py +++ b/tests/test_builder.py @@ -161,7 +161,7 @@ def test_specify_other(self): def test_select_one_question_with_identical_choice_name(self): """ - testing to make sure that select ones whose choice names are the same + Testing to make sure that select ones whose choice names are the same as the name of the select one get compiled. """ survey = utils.create_survey_from_fixture( diff --git a/tests/test_geo.py b/tests/test_geo.py index 6121ca1f..67b36e04 100644 --- a/tests/test_geo.py +++ b/tests/test_geo.py @@ -62,7 +62,7 @@ def test_gps_alias(self): def test_geo_widgets_types(self): """ - this test could be broken into multiple smaller tests. + This test could be broken into multiple smaller tests. """ self.assertPyxformXform( name="geos", diff --git a/tests/test_j2x_creation.py b/tests/test_j2x_creation.py index 7bb2f173..522570cf 100644 --- a/tests/test_j2x_creation.py +++ b/tests/test_j2x_creation.py @@ -37,7 +37,7 @@ def test_survey_can_be_created_in_a_slightly_less_verbose_manner(self): self.assertEqual(expected_dict, s.to_json_dict()) def test_allow_surveys_with_comment_rows(self): - """assume that a survey with rows that don't have name, type, or label + """Assume that a survey with rows that don't have name, type, or label headings raise warning only""" path = utils.path_to_text_fixture("allow_comment_rows_test.xls") survey = create_survey_from_xls(path) diff --git a/tests/test_j2x_question.py b/tests/test_j2x_question.py index ed114b98..92c1d1bd 100644 --- a/tests/test_j2x_question.py +++ b/tests/test_j2x_question.py @@ -15,7 +15,7 @@ def ctw(control): """ - ctw stands for control_test_wrap, but ctw is shorter and easier. using + Ctw stands for control_test_wrap, but ctw is shorter and easier. using begin_str and end_str to take out the wrap that xml gives us """ if isinstance(control, list) and len(control) == 1: @@ -146,7 +146,7 @@ def test_select_one_question_multilingual__common_choices(self): def test_simple_integer_question_type_multilingual(self): """ - not sure how integer questions should show up. + Not sure how integer questions should show up. """ simple_integer_question = { "label": {"f": "fc", "e": "ec"}, @@ -172,7 +172,7 @@ def test_simple_integer_question_type_multilingual(self): def test_simple_date_question_type_multilingual(self): """ - not sure how date questions should show up. + Not sure how date questions should show up. """ simple_date_question = { "label": {"f": "fd", "e": "ed"}, @@ -196,7 +196,7 @@ def test_simple_date_question_type_multilingual(self): def test_simple_phone_number_question_type_multilingual(self): """ - not sure how phone number questions should show up. + Not sure how phone number questions should show up. """ simple_phone_number_question = { "label": {"f": "fe", "e": "ee"}, @@ -233,7 +233,7 @@ def test_simple_phone_number_question_type_multilingual(self): def test_simple_select_all_question_multilingual(self): """ - not sure how select all questions should show up... + Not sure how select all questions should show up... """ survey = { "type": "survey", @@ -273,7 +273,7 @@ def test_simple_select_all_question_multilingual(self): def test_simple_decimal_question_multilingual(self): """ - not sure how decimal should show up. + Not sure how decimal should show up. """ simple_decimal_question = { "label": {"f": "f text", "e": "e text"}, diff --git a/tests/test_pyxformtestcase.py b/tests/test_pyxformtestcase.py index ae164f12..d467920f 100644 --- a/tests/test_pyxformtestcase.py +++ b/tests/test_pyxformtestcase.py @@ -10,7 +10,7 @@ class PyxformTestCaseNonMarkdownSurveyAlternatives(PyxformTestCase): def test_tainted_vanilla_survey_failure(self): """ - the _invalid_ss_structure structure should fail to compile + The _invalid_ss_structure structure should fail to compile because the note has no label. if "errored" parameter is not set to False, it should @@ -20,7 +20,7 @@ def test_tainted_vanilla_survey_failure(self): def _no_valid_flag(): """ - when the 'errored' flag is set to false (default) and the survey + When the 'errored' flag is set to false (default) and the survey fails to compile, the test should raise an exception. """ self.assertPyxformXform( @@ -39,7 +39,7 @@ def _no_valid_flag(): def test_vanilla_survey(self): """ - testing that a survey can be passed as a _spreadsheet structure_ named + Testing that a survey can be passed as a _spreadsheet structure_ named 'ss_structure'. this will be helpful when testing whitespace constraints and diff --git a/tests/test_sheet_columns.py b/tests/test_sheet_columns.py index 8f6d7ff8..ff6c7493 100644 --- a/tests/test_sheet_columns.py +++ b/tests/test_sheet_columns.py @@ -71,7 +71,7 @@ class TestSurveyColumns(PyxformTestCase): def test_missing_name(self): """ - every question needs a name (or alias of name) + Every question needs a name (or alias of name) """ self.assertPyxformXform( name="invalidcols", @@ -304,7 +304,7 @@ class TestColumnAliases(PyxformTestCase): def test_value_and_name(self): """ - confirm that both 'name' and 'value' columns of choice list work + Confirm that both 'name' and 'value' columns of choice list work """ md = """ | survey | | | | From 935e482e399c115342032fe23105f235e4fb8e26 Mon Sep 17 00:00:00 2001 From: lindsay stevens Date: Mon, 13 Jul 2026 21:04:17 +1000 Subject: [PATCH 08/50] dev: add and apply pydocstyle rule D202 - the full pydocstyle ruleset has many changes so enabling some of them individually to keep the diffs manageable. --- pyproject.toml | 1 + pyxform/parsing/sheet_headers.py | 1 - pyxform/survey.py | 2 -- pyxform/xform2json.py | 3 --- tests/test_form_name.py | 1 - tests/test_sheet_columns.py | 5 ----- 6 files changed, 1 insertion(+), 12 deletions(-) diff --git a/pyproject.toml b/pyproject.toml index 489136da..09691fcf 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -55,6 +55,7 @@ src = ["pyxform", "tests"] select = [ "B", # flake8-bugbear "C4", # flake8-comprehensions + "D202", # pydocstyle (blank-line-after-function) "D403", # pydocstyle (first-word-uncapitalized) "E", # pycodestyle error # "ERA", # eradicate (commented out code) diff --git a/pyxform/parsing/sheet_headers.py b/pyxform/parsing/sheet_headers.py index 8343071a..c960b294 100644 --- a/pyxform/parsing/sheet_headers.py +++ b/pyxform/parsing/sheet_headers.py @@ -224,7 +224,6 @@ def dealias_and_group_headers( in the data rows. :param add_row_number: If True, add a "__row" key with the row number from the input data. """ - header_key: dict[str, tuple[str, ...]] = {} tokens_key: dict[tuple[str, ...], str] = {} diff --git a/pyxform/survey.py b/pyxform/survey.py index 72b68a05..b36760e1 100644 --- a/pyxform/survey.py +++ b/pyxform/survey.py @@ -1033,7 +1033,6 @@ def _var_repl_function( Given a dictionary of xpaths, return a function we can use to replace ${varname} with the xpath to varname. """ - name = matchobj.group("ncname") last_saved = matchobj.group("last_saved") is not None is_indexed_repeat = matchobj.string.find("indexed-repeat(") > -1 @@ -1043,7 +1042,6 @@ def _in_secondary_instance_predicate() -> bool: Check if ${} expression represented by matchobj is in a predicate for a path expression for a secondary instance """ - if RE_INSTANCE.search(matchobj.string) is not None: bracket_regex_match_iter = RE_BRACKET.finditer(matchobj.string) # Check whether current ${varname} is in the correct bracket_regex_match diff --git a/pyxform/xform2json.py b/pyxform/xform2json.py index 74b18e9d..11f84c7a 100644 --- a/pyxform/xform2json.py +++ b/pyxform/xform2json.py @@ -58,7 +58,6 @@ def wrap(x): """ Static method to wrap a dictionary recursively as an XmlDictObject """ - if isinstance(x, dict): return XmlDictObject((k, XmlDictObject.Wrap(v)) for (k, v) in iter(x.items())) elif isinstance(x, list): @@ -80,7 +79,6 @@ def un_wrap(self): Recursively converts an XmlDictObject to a standard dictionary and returns the result. """ - return XmlDictObject._un_wrap(self) @@ -110,7 +108,6 @@ def convert_dict_to_xml(xmldict): """ Converts a dictionary to an XML ElementTree Element """ - roottag = xmldict.keys()[0] root = Element(roottag) _convert_dict_to_xml_recurse(root, xmldict[roottag]) diff --git a/tests/test_form_name.py b/tests/test_form_name.py index e3f39530..c3d9deb2 100644 --- a/tests/test_form_name.py +++ b/tests/test_form_name.py @@ -47,7 +47,6 @@ def test_default_form_name_to_superclass_definition(self): """ Test no form_name and setting name field, should use name field. """ - self.assertPyxformXform( md=""" | survey | | | | diff --git a/tests/test_sheet_columns.py b/tests/test_sheet_columns.py index ff6c7493..cb43032b 100644 --- a/tests/test_sheet_columns.py +++ b/tests/test_sheet_columns.py @@ -162,7 +162,6 @@ def test_label_caps_alternatives(self): re: https://github.com/SEL-Columbia/pyxform/issues/76 Capitalization of 'label' column can lead to confusing errors. """ - self.assertPyxformXform( md=""" | survey | | | | @@ -214,7 +213,6 @@ def _simple_choice_ss(choice_sheet=None): """ Return simple choices sheet """ - if choice_sheet is None: choice_sheet = [] return { @@ -232,7 +230,6 @@ def test_valid_choices_sheet_passes(self): """ Test invalid choices sheet passes """ - self.assertPyxformXform( name="valid_choices", ss_structure=self._simple_choice_ss( @@ -247,7 +244,6 @@ def test_invalid_choices_sheet_fails(self): """ Test invalid choices sheet fails """ - self.assertPyxformXform( name="missing_name", ss_structure=self._simple_choice_ss( @@ -266,7 +262,6 @@ def test_missing_list_name(self): """ Test missing sheet name """ - self.assertPyxformXform( name="missing_list_name", ss_structure=self._simple_choice_ss( From a3079cd786a5b8e0590167c4b43099d95722f4fe Mon Sep 17 00:00:00 2001 From: lindsay stevens Date: Mon, 13 Jul 2026 21:06:13 +1000 Subject: [PATCH 09/50] dev: add and apply pydocstyle rule D209 - the full pydocstyle ruleset has many changes so enabling some of them individually to keep the diffs manageable. --- pyproject.toml | 1 + tests/test_j2x_creation.py | 3 ++- tests/xform_test_case/test_bugs.py | 3 ++- 3 files changed, 5 insertions(+), 2 deletions(-) diff --git a/pyproject.toml b/pyproject.toml index 09691fcf..47181ac2 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -56,6 +56,7 @@ select = [ "B", # flake8-bugbear "C4", # flake8-comprehensions "D202", # pydocstyle (blank-line-after-function) + "D209", # pydocstyle (new-line-after-last-paragraph) "D403", # pydocstyle (first-word-uncapitalized) "E", # pycodestyle error # "ERA", # eradicate (commented out code) diff --git a/tests/test_j2x_creation.py b/tests/test_j2x_creation.py index 522570cf..46ab15e4 100644 --- a/tests/test_j2x_creation.py +++ b/tests/test_j2x_creation.py @@ -38,7 +38,8 @@ def test_survey_can_be_created_in_a_slightly_less_verbose_manner(self): def test_allow_surveys_with_comment_rows(self): """Assume that a survey with rows that don't have name, type, or label - headings raise warning only""" + headings raise warning only + """ path = utils.path_to_text_fixture("allow_comment_rows_test.xls") survey = create_survey_from_xls(path) expected_dict = { diff --git a/tests/xform_test_case/test_bugs.py b/tests/xform_test_case/test_bugs.py index 8f9815b2..e946a9f9 100644 --- a/tests/xform_test_case/test_bugs.py +++ b/tests/xform_test_case/test_bugs.py @@ -132,7 +132,8 @@ def test_xl_date_ambigous(self): class TestXLDateAmbigousNoException(TestCase): """Test date values that exceed the workbook datemode value. - (This would cause an exception with xlrd, but openpyxl handles it).""" + (This would cause an exception with xlrd, but openpyxl handles it). + """ def test_xl_date_ambigous_no_exception(self): """Test standard sheet is processed successfully.""" From f94375a3250547cd31b8e539282317a9e9ca6439 Mon Sep 17 00:00:00 2001 From: lindsay stevens Date: Mon, 13 Jul 2026 21:27:13 +1000 Subject: [PATCH 10/50] dev: add and apply pydocstyle rules D301 and D415 - the full pydocstyle ruleset has many changes so enabling some of them individually to keep the diffs manageable. - D415 is a bit strange when it comes to indented inline code blocks, so added backticks for those cases as well as URLs. --- pyproject.toml | 2 + pyxform/builder.py | 10 ++--- pyxform/entities/entity_declaration.py | 2 +- pyxform/external_instance.py | 2 +- pyxform/file_utils.py | 3 +- pyxform/instance.py | 4 +- pyxform/parsing/parameters.py | 2 +- pyxform/parsing/sheet_headers.py | 2 +- pyxform/section.py | 8 +++- pyxform/survey.py | 16 ++++---- pyxform/survey_element.py | 6 +-- pyxform/translator.py | 2 +- pyxform/utils.py | 2 +- pyxform/validators/odk_validate/__init__.py | 3 +- .../validators/pyxform/pyxform_reference.py | 6 +-- pyxform/validators/util.py | 6 +-- pyxform/xform2json.py | 10 ++--- pyxform/xls2json.py | 6 +-- pyxform/xls2json_backends.py | 23 ++++++----- pyxform/xls2xform.py | 2 +- tests/entities/test_create_repeat.py | 2 +- tests/entities/test_entities.py | 2 +- tests/pyxform_test_case.py | 4 +- tests/test_bind_conversions.py | 2 +- tests/test_dynamic_default.py | 2 +- tests/test_external_instances.py | 14 +++---- tests/test_external_instances_for_selects.py | 8 ++-- tests/test_fields.py | 2 +- tests/test_geo.py | 2 +- tests/test_guidance_hint.py | 2 +- tests/test_j2x_creation.py | 4 +- tests/test_j2x_question.py | 4 +- tests/test_json2xform.py | 4 +- tests/test_range.py | 2 +- tests/test_repeat.py | 40 +++++++++---------- tests/test_search_function.py | 10 ++--- tests/test_sheet_columns.py | 20 +++++----- tests/test_survey.py | 2 +- tests/test_translations.py | 2 +- tests/test_trigger.py | 2 +- tests/test_upload_question.py | 2 +- tests/test_validators.py | 2 +- tests/test_xform2json.py | 4 +- tests/test_xls2json.py | 4 +- tests/test_xls2json_xls.py | 6 +-- tests/test_xls2xform.py | 12 +++--- tests/xform_test_case/test_bugs.py | 4 +- 47 files changed, 144 insertions(+), 137 deletions(-) diff --git a/pyproject.toml b/pyproject.toml index 47181ac2..4424fae4 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -57,7 +57,9 @@ select = [ "C4", # flake8-comprehensions "D202", # pydocstyle (blank-line-after-function) "D209", # pydocstyle (new-line-after-last-paragraph) + "D301", # pydocstyle (escape-sequence-in-docstring) "D403", # pydocstyle (first-word-uncapitalized) + "D415", # pydocstyle (missing-terminal-punctuation) "E", # pycodestyle error # "ERA", # eradicate (commented out code) "F", # pyflakes diff --git a/pyxform/builder.py b/pyxform/builder.py index 51c0901a..156a6a5b 100644 --- a/pyxform/builder.py +++ b/pyxform/builder.py @@ -82,7 +82,7 @@ def create_survey_element_from_dict( """ Convert from a nested python dictionary/array structure (a json dict I call it because it corresponds directly with a json object) - to a survey object + to a survey object. :param d: data to use for constructing SurveyElements. """ @@ -202,7 +202,7 @@ def _get_question_class(question_type_str, question_type_dictionary): """ Read the type string from the json format, and find what class it maps to going through - type_dictionary -> QUESTION_CLASSES + type_dictionary -> QUESTION_CLASSES. """ by_type = QUESTION_CLASSES["__by_type__"].get(question_type_str, None) if by_type is not None: @@ -246,7 +246,7 @@ def _create_loop_from_dict( ): """ Takes a json_dict of "loop" type - Returns a GroupedSection + Returns a GroupedSection. """ children = d.get(const.CHILDREN) result = GroupedSection(**d) @@ -309,7 +309,7 @@ def create_survey_element_from_json(self, str_or_path): def create_survey_element_from_dict(d, sections=None): """ - Creates a Survey from a dictionary in the format provided by SurveyReader + Creates a Survey from a dictionary in the format provided by SurveyReader. """ if sections is None: sections = {} @@ -380,7 +380,7 @@ def create_survey_from_path(path: str, include_directory: bool = False) -> Surve include_directory -- Switch to indicate that all the survey forms in the same directory as the specified file should be read so they can be included through include types. - @see: create_survey + @see: create_survey. """ directory, file_name = os.path.split(path) if include_directory: diff --git a/pyxform/entities/entity_declaration.py b/pyxform/entities/entity_declaration.py index 8f01c184..b62aae69 100644 --- a/pyxform/entities/entity_declaration.py +++ b/pyxform/entities/entity_declaration.py @@ -18,7 +18,7 @@ class EntityDeclaration(Section): """ An entity declaration produces an entity instance node with optional label child, some variable attributes, and corresponding bindings. The ODK XForms Entities - specification can be found at https://getodk.github.io/xforms-spec/entities + specification can be found at `https://getodk.github.io/xforms-spec/entities`. """ __slots__ = ENTITY_EXTRA_FIELDS diff --git a/pyxform/external_instance.py b/pyxform/external_instance.py index 50301ddb..49914979 100644 --- a/pyxform/external_instance.py +++ b/pyxform/external_instance.py @@ -1,5 +1,5 @@ """ -ExternalInstance class module +ExternalInstance class module. """ from typing import TYPE_CHECKING diff --git a/pyxform/file_utils.py b/pyxform/file_utils.py index b208e416..1771bf77 100644 --- a/pyxform/file_utils.py +++ b/pyxform/file_utils.py @@ -33,8 +33,7 @@ def load_file_to_dict(path): def collect_compatible_files_in_directory(directory): """ - Create a giant dict out of all the spreadsheets and json forms - in the given directory + Create a giant dict out of all the spreadsheets and json forms in the given directory. """ available_files = glob.glob(os.path.join(directory, "*.xls")) + glob.glob( os.path.join(directory, "*.json") diff --git a/pyxform/instance.py b/pyxform/instance.py index eb36f4de..f05c3182 100644 --- a/pyxform/instance.py +++ b/pyxform/instance.py @@ -58,7 +58,7 @@ def to_json_dict(self): def to_xml(self): """ A horrible way to do this, but it works (until we need the attributes - pumped out in order, etc) + pumped out in order, etc). """ open_str = f"""<{self._name} id="{self._id}">""" close_str = f"""""" @@ -73,7 +73,7 @@ def answers(self): """ This returns "_answers", which is a dict with the key-value responses for this given instance. This could be pumped to xml - or returned as a dict for maximum convenience (i.e. testing.) + or returned as a dict for maximum convenience (i.e. testing.). """ return self._answers diff --git a/pyxform/parsing/parameters.py b/pyxform/parsing/parameters.py index 1696aead..31812d99 100644 --- a/pyxform/parsing/parameters.py +++ b/pyxform/parsing/parameters.py @@ -22,7 +22,7 @@ class ParameterTransformer(Transformer): @staticmethod def start(pairs: list[tuple[str, str]]) -> dict[str, str]: - """Combine (key, value) tuples into a dict""" + """Combine (key, value) tuples into a dict.""" return dict(pairs) @staticmethod diff --git a/pyxform/parsing/sheet_headers.py b/pyxform/parsing/sheet_headers.py index c960b294..508be6c1 100644 --- a/pyxform/parsing/sheet_headers.py +++ b/pyxform/parsing/sheet_headers.py @@ -64,7 +64,7 @@ def merge_dicts( def list_to_nested_dict(lst: Sequence) -> dict: """ - [1,2,3,4] -> {1:{2:{3:4}}} + Example: `[1,2,3,4] -> {1:{2:{3:4}}}`. """ if len(lst) > 1: return {lst[0]: list_to_nested_dict(lst[1:])} diff --git a/pyxform/section.py b/pyxform/section.py index 552e1283..a14da901 100644 --- a/pyxform/section.py +++ b/pyxform/section.py @@ -127,7 +127,7 @@ def _validate_uniqueness_of_element_names(self): def xml_instance(self, survey: "Survey", **kwargs): """ - Creates an xml representation of the section + Creates an xml representation of the section. """ append_template = kwargs.pop("append_template", False) @@ -186,7 +186,7 @@ def xml_instance_array(self, survey: "Survey"): def xml_control(self, survey: "Survey"): """ Ideally, we'll have groups up and rolling soon, but for now - let's just yield controls from all the children of this section + let's just yield controls from all the children of this section. """ for e in self.children: control = e.xml_control(survey=survey) @@ -227,6 +227,9 @@ def __init__( def xml_control(self, survey: "Survey"): """ + Example + + ``` @@ -238,6 +241,7 @@ def xml_control(self, survey: "Survey"): + ```. """ # Resolve field references in attributes if self.control: diff --git a/pyxform/survey.py b/pyxform/survey.py index b36760e1..54ed9304 100644 --- a/pyxform/survey.py +++ b/pyxform/survey.py @@ -70,7 +70,7 @@ def __init__( def register_nsmap(): - """Function to register NSMAP namespaces with ETree""" + """Function to register NSMAP namespaces with ETree.""" for prefix, uri in NSMAP.items(): prefix_no_xmlns = prefix.replace("xmlns", "").replace(":", "") ETree.register_namespace(prefix_no_xmlns, uri) @@ -262,7 +262,7 @@ def _validate_uniqueness_of_section_names(self): ) def get_nsmap(self): - """Add additional namespaces""" + """Add additional namespaces.""" if self.entity_version: entities_ns = " entities=http://www.opendatakit.org/xforms/entities" if self.namespaces is None: @@ -311,7 +311,7 @@ def _generate_static_instances( self, list_name: str, itemset: Itemset ) -> InstanceInfo: """ - Generate elements for static data (e.g. choices for selects) + Generate elements for static data (e.g. choices for selects). """ def choice_nodes(idx, choice): @@ -397,7 +397,7 @@ def get_pulldata_functions(element): """ Returns a list of different pulldata(... function strings if pulldata function is defined at least once for any of: - calculate, constraint, readonly, required, relevant + calculate, constraint, readonly, required, relevant. """ functions_present = [] for formula_name in constants.EXTERNAL_INSTANCES: @@ -607,7 +607,7 @@ def xml_model_bindings(self) -> Generator[DetachableElement | None, None, None]: def xml_model(self): """ - Generate the xform element + Generate the xform element. """ self._setup_translations() self._setup_media() @@ -740,7 +740,7 @@ def _redirect_is_search_itext(self, element: MultipleChoiceQuestion) -> bool: def _setup_translations(self): """ Set up the self._translations dict which will be referenced in the - setup media and itext functions + setup media and itext functions. """ def get_choice_content(name, idx, choice): @@ -912,7 +912,7 @@ def itext(self) -> DetachableElement: This function creates the survey's itext nodes from _translations @see _setup_media _setup_translations itext nodes are localized images/audio/video/text - @see http://code.google.com/p/opendatakit/wiki/XFormDesignGuidelines + @see `http://code.google.com/p/opendatakit/wiki/XFormDesignGuidelines`. """ result = [] for lang, translation in self._translations.items(): @@ -1040,7 +1040,7 @@ def _var_repl_function( def _in_secondary_instance_predicate() -> bool: """ Check if ${} expression represented by matchobj - is in a predicate for a path expression for a secondary instance + is in a predicate for a path expression for a secondary instance. """ if RE_INSTANCE.search(matchobj.string) is not None: bracket_regex_match_iter = RE_BRACKET.finditer(matchobj.string) diff --git a/pyxform/survey_element.py b/pyxform/survey_element.py index 668dba60..a54f1be8 100644 --- a/pyxform/survey_element.py +++ b/pyxform/survey_element.py @@ -279,7 +279,7 @@ def stop_before(e): def _delete_keys_from_dict(self, dictionary: dict, keys: Iterable[str]): """ Deletes a list of keys from a dictionary. - Credits: https://stackoverflow.com/a/49723101 + Credits: `https://stackoverflow.com/a/49723101`. """ for key in keys: dictionary.pop(key, None) @@ -294,7 +294,7 @@ def copy(self) -> dict[str, Any]: def to_json_dict(self, delete_keys: Iterable[str] | None = None) -> dict: """ Create a dict copy of this survey element by removing inappropriate - attributes and converting its children to dicts + attributes and converting its children to dicts. """ self.validate() result = self.copy() @@ -359,7 +359,7 @@ def _translation_path(self, display_element: str) -> str: def get_translations(self, default_language): """ Returns translations used by this element so they can be included in - the block. @see survey._setup_translations + the block. @see survey._setup_translations. """ bind_dict = self.bind if bind_dict and isinstance(bind_dict, dict): diff --git a/pyxform/translator.py b/pyxform/translator.py index bfd99720..7032e850 100644 --- a/pyxform/translator.py +++ b/pyxform/translator.py @@ -39,7 +39,7 @@ class Translator: def __init__(self): """ I'm being super lazy dictionary has to have the form: - {'yes' : {'English' : {'French' : 'oui'}}} + `{'yes' : {'English' : {'French' : 'oui'}}}`. """ self._dict = infinite_dict() self._languages = [] diff --git a/pyxform/utils.py b/pyxform/utils.py index e0ba94c5..975c500a 100644 --- a/pyxform/utils.py +++ b/pyxform/utils.py @@ -156,7 +156,7 @@ def get_pyobj_from_json(str_or_path): def print_pyobj_to_json(pyobj, path=None): """ Dump a python nested array/dict structure to the specified file - or stdout if no file is specified + or stdout if no file is specified. """ if path: with open(path, mode="w", encoding="utf-8") as fp: diff --git a/pyxform/validators/odk_validate/__init__.py b/pyxform/validators/odk_validate/__init__.py index 14251a44..016e2200 100644 --- a/pyxform/validators/odk_validate/__init__.py +++ b/pyxform/validators/odk_validate/__init__.py @@ -1,6 +1,5 @@ """ -odk_validate.py -A python wrapper around ODK Validate +A python wrapper around ODK Validate. """ import logging diff --git a/pyxform/validators/pyxform/pyxform_reference.py b/pyxform/validators/pyxform/pyxform_reference.py index dc474e39..0e623b41 100644 --- a/pyxform/validators/pyxform/pyxform_reference.py +++ b/pyxform/validators/pyxform/pyxform_reference.py @@ -107,7 +107,7 @@ def _parse( @lru_cache(maxsize=128) def is_pyxform_reference(value: str) -> bool: """ - Does the input string contain only a valid Pyxform reference? e.g. `${my_question}` + Does the input string contain only a valid Pyxform reference? e.g. `${my_question}`. :param value: The string to inspect. """ @@ -120,7 +120,7 @@ def is_pyxform_reference(value: str) -> bool: @lru_cache(maxsize=128) def has_pyxform_reference(value: str) -> bool: """ - Does the input string contain a valid Pyxform reference? e.g. `hi ${name}` + Does the input string contain a valid Pyxform reference? e.g. `hi ${name}`. :param value: The string to inspect. """ @@ -133,7 +133,7 @@ def has_pyxform_reference(value: str) -> bool: @lru_cache(maxsize=128) def has_pyxform_reference_with_last_saved(value: str) -> bool: """ - Does the input string contain a valid '#last-saved' reference? e.g. `${last-saved#my_question}` + Does the input string contain a valid '#last-saved'? e.g. `${last-saved#my_question}`. Needs 14 characters for "${last-saved#}", plus a name inside. This pre-check can help avoid more expensive full parsing. diff --git a/pyxform/validators/util.py b/pyxform/validators/util.py index 6798dcc8..cfd312f2 100644 --- a/pyxform/validators/util.py +++ b/pyxform/validators/util.py @@ -22,7 +22,7 @@ class PopenResult: - """Result data for run_popen_with_timeout""" + """Result data for run_popen_with_timeout.""" def __init__( self, return_code: int, timeout: bool, stdout: bytes, stderr: bytes @@ -39,7 +39,7 @@ def run_popen_with_timeout(command, timeout) -> "PopenResult": """ Run a sub-program in subprocess.Popen, pass it the input_data, kill it if the specified timeout has passed. - returns a tuple of resultcode, timeout, stdout, stderr + returns a tuple of resultcode, timeout, stdout, stderr. """ kill_check = threading.Event() @@ -84,7 +84,7 @@ def _kill_process_after_a_timeout(pid): def decode_stream(stream): - """ + r""" Decode a stream, e.g. stdout or stderr. On Windows, stderr may be latin-1; in which case utf-8 decode will fail. diff --git a/pyxform/xform2json.py b/pyxform/xform2json.py index 11f84c7a..f0d09715 100644 --- a/pyxform/xform2json.py +++ b/pyxform/xform2json.py @@ -56,7 +56,7 @@ def __str__(self): @staticmethod def wrap(x): """ - Static method to wrap a dictionary recursively as an XmlDictObject + Static method to wrap a dictionary recursively as an XmlDictObject. """ if isinstance(x, dict): return XmlDictObject((k, XmlDictObject.Wrap(v)) for (k, v) in iter(x.items())) @@ -106,7 +106,7 @@ def _convert_dict_to_xml_recurse(parent, dictitem): def convert_dict_to_xml(xmldict): """ - Converts a dictionary to an XML ElementTree Element + Converts a dictionary to an XML ElementTree Element. """ roottag = xmldict.keys()[0] root = Element(roottag) @@ -158,7 +158,7 @@ def _convert_xml_to_dict_recurse(node, dictclass): def convert_xml_to_dict(root, dictclass=XmlDictObject): """ - Converts an XML file or ElementTree Element to a dictionary + Converts an XML file or ElementTree Element to a dictionary. """ # If a string is passed in, try to open it as a file if isinstance(root, str): @@ -208,7 +208,7 @@ def create_survey_element_from_xml(xml_file): class XFormToDictBuilder: - """Experimental XFORM xml to XFORM JSON""" + """Experimental XFORM xml to XFORM JSON.""" def __init__(self, xml_file): doc_as_dict = XFormToDict(xml_file).get_dict() @@ -681,7 +681,7 @@ def _get_choices(self) -> dict[str, Any]: @staticmethod def _get_name_from_ref(ref): """Given /xlsform_spec_test/launch, - return the string after the last occurance of the character '/' + return the string after the last occurance of the character '/'. """ pos = ref.rfind("/") if pos == -1: diff --git a/pyxform/xls2json.py b/pyxform/xls2json.py index bc091098..f143b1a5 100644 --- a/pyxform/xls2json.py +++ b/pyxform/xls2json.py @@ -111,7 +111,7 @@ def add_flat_annotations(prompt_list, parent_relevant="", name_prefix=""): (However, there could be namespace collisions now.) - "and"s group relevance formulas onto that of their children. - Adds a flat property to groups - The flat property is used in the json2xform code + The flat property is used in the json2xform code. """ for prompt in prompt_list: prompt_relevant = prompt.get("bind", {}).get("relevant", "") @@ -215,7 +215,7 @@ def workbook_to_json( default language. If the default language is used as a suffix for media/labels/hints, then the suffixless version will be overwritten. - warnings -- an optional list which warnings will be appended to + warnings -- an optional list which warnings will be appended to. returns a nested dictionary equivalent to the format specified in the json form spec. @@ -1410,7 +1410,7 @@ def parse_file_to_json( file_object: IO | None = None, ) -> dict[str, Any]: """ - A wrapper for workbook_to_json + A wrapper for workbook_to_json. """ if warnings is None: warnings = [] diff --git a/pyxform/xls2json_backends.py b/pyxform/xls2json_backends.py index 91b4ce0e..669b1565 100644 --- a/pyxform/xls2json_backends.py +++ b/pyxform/xls2json_backends.py @@ -62,7 +62,7 @@ class DefinitionData: def _list_to_dict_list(list_items): """ Takes a list and creates a dict with the list values as keys. - Returns a list of the created dict or an empty list + Returns a list of the created dict or an empty list. """ if list_items: return [{str(i): None for i in list_items}] @@ -446,15 +446,18 @@ def process_csv_data(rd): def convert_file_to_csv_string(path): """ - This will open a csv or xls file and return a CSV in the format: - sheet_name1 - ,col1,col2 - ,r1c1,r1c2 - ,r2c1,r2c2 - sheet_name2 - ,col1,col2 - ,r1c1,r1c2 - ,r2c1,r2c2 + This will open a csv or xls file and return a CSV in the format. + + ``` + sheet_name1 + ,col1,col2 + ,r1c1,r1c2 + ,r2c1,r2c2 + sheet_name2 + ,col1,col2 + ,r1c1,r1c2 + ,r2c1,r2c2 + ``` Currently, it processes csv files and xls files to ensure consistent csv delimiters, etc. for tests. diff --git a/pyxform/xls2xform.py b/pyxform/xls2xform.py index a768764a..d82f1418 100644 --- a/pyxform/xls2xform.py +++ b/pyxform/xls2xform.py @@ -33,7 +33,7 @@ def get_xml_path(path): """ - Returns the xform file path + Returns the xform file path. Generates an output path for the xform file from the given xlsx input file path. diff --git a/tests/entities/test_create_repeat.py b/tests/entities/test_create_repeat.py index 226e0c11..00093b9d 100644 --- a/tests/entities/test_create_repeat.py +++ b/tests/entities/test_create_repeat.py @@ -5,7 +5,7 @@ class TestEntitiesCreateRepeat(PyxformTestCase): - """Test entity create specs for entities declared in a repeat""" + """Test entity create specs for entities declared in a repeat.""" def test_other_controls_before__ok(self): """Should find that having other control types before the entity repeat is OK.""" diff --git a/tests/entities/test_entities.py b/tests/entities/test_entities.py index 92617806..2a6055a3 100644 --- a/tests/entities/test_entities.py +++ b/tests/entities/test_entities.py @@ -1,5 +1,5 @@ """ -## Entity feature traceability test suite +## Entity feature traceability test suite. Each entities test should reference one (or more) requirements from these lists. diff --git a/tests/pyxform_test_case.py b/tests/pyxform_test_case.py index ea3ed09f..8b2b46a6 100644 --- a/tests/pyxform_test_case.py +++ b/tests/pyxform_test_case.py @@ -112,7 +112,7 @@ def assertPyxformXform( cases where testing whitespace and cells' type is important. :param survey: easy for reuse within a test # Note: XLS is not implemented at this time. You can use builder to create a - pyxform Survey object + pyxform Survey object. One or more XForm assertions: :param xml__xpath_exact: A list of tuples where the first tuple element is an @@ -367,7 +367,7 @@ def _assert_contains(content, text, msg_prefix): def assertContains(self, content, text, count=None, msg_prefix=""): """ - FROM: django source- testcases.py + FROM: django source- testcases.py. Asserts that ``text`` occurs ``count`` times in the content string. If ``count`` is None, the count doesn't matter - the assertion is diff --git a/tests/test_bind_conversions.py b/tests/test_bind_conversions.py index 2ff3d111..86a9b831 100644 --- a/tests/test_bind_conversions.py +++ b/tests/test_bind_conversions.py @@ -7,7 +7,7 @@ class BindConversionsTest(PyxformTestCase): """ - BindConversionsTest - test bind conversions + BindConversionsTest - test bind conversions. """ def test_bind_readonly_conversion(self): diff --git a/tests/test_dynamic_default.py b/tests/test_dynamic_default.py index 13ed2b98..2dd5a4cb 100644 --- a/tests/test_dynamic_default.py +++ b/tests/test_dynamic_default.py @@ -1,5 +1,5 @@ """ -Test handling dynamic default in forms +Test handling dynamic default in forms. """ from dataclasses import dataclass diff --git a/tests/test_external_instances.py b/tests/test_external_instances.py index f9bb5abf..8cb5bc90 100644 --- a/tests/test_external_instances.py +++ b/tests/test_external_instances.py @@ -14,7 +14,7 @@ class ExternalInstanceTests(PyxformTestCase): """ - External Instance Tests + External Instance Tests. """ def test_can__output_single_external_xml_item(self): @@ -407,7 +407,7 @@ def test_can__reuse_xml__external_then_selects(self): def test_external_instance_pulldata_constraint(self): """ Checks if instance node for pulldata function is added - when pulldata occurs in column with constraint title + when pulldata occurs in column with constraint title. """ md = """ | survey | | | | | @@ -507,7 +507,7 @@ def test_pulldata_calculate_single_line_expression__multiple_calls(self): def test_external_instance_pulldata_readonly(self): """ Checks if instance node for pulldata function is added - when pulldata occurs in column with readonly title + when pulldata occurs in column with readonly title. """ md = """ | survey | | | | | @@ -525,7 +525,7 @@ def test_external_instance_pulldata_readonly(self): def test_external_instance_pulldata_required(self): """ Checks if instance node for pulldata function is added - when pulldata occurs in column with required title + when pulldata occurs in column with required title. """ md = """ | survey | | | | | @@ -542,7 +542,7 @@ def test_external_instance_pulldata_required(self): def test_external_instance_pulldata_relevant(self): """ Checks if instance node for pulldata function is added - when pulldata occurs in column with relevant title + when pulldata occurs in column with relevant title. """ md = """ | survey | | | | | @@ -585,7 +585,7 @@ def test_external_instance_pulldata(self): """ Checks that only one instance node for pulldata is created if pulldata function is present in at least one columns with - the titles: constraint, relevant, required, readonly + the titles: constraint, relevant, required, readonly. """ md = """ | survey | | | | | | | @@ -610,7 +610,7 @@ def test_external_instances_multiple_diff_pulldatas(self): Checks that all instances for pulldata that needs creation are created The situation is if pulldata is present in 2 or more - columns but pulling data from different csv files + columns but pulling data from different csv files. """ md = """ | survey | | | | | | diff --git a/tests/test_external_instances_for_selects.py b/tests/test_external_instances_for_selects.py index 78233015..2c943f9e 100644 --- a/tests/test_external_instances_for_selects.py +++ b/tests/test_external_instances_for_selects.py @@ -1,5 +1,5 @@ """ -Test external instance syntax +Test external instance syntax. See also test_external_instances """ @@ -145,7 +145,7 @@ def test_with_params_no_filters(self): ) def test_no_params_with_filters(self): - """Should find that choice_filter adds a predicate to the itemset's instance ref""" + """Should find that choice_filter adds a predicate to the itemset's instance ref.""" md = """ | survey | | | | | | | type | name | label | choice_filter | @@ -307,7 +307,7 @@ class TestSelectOneExternal(PyxformTestCase): """ def test_no_params_no_filters(self): - """Should find that Pyxform errors out, not a supported use case as per #488""" + """Should find that Pyxform errors out, not a supported use case as per #488.""" md = """ | survey | | | | | | type | name | label | @@ -346,7 +346,7 @@ def test_with_params_no_filters(self): ) def test_no_params_with_filters(self): - """Should find that choice_filter generates input()s with refs to external itemsets""" + """Should find that choice_filter generates input()s with refs to external itemsets.""" md = """ | survey | | | | | | | type | name | label | choice_filter | diff --git a/tests/test_fields.py b/tests/test_fields.py index d904807d..2503c822 100644 --- a/tests/test_fields.py +++ b/tests/test_fields.py @@ -10,7 +10,7 @@ class TestQuestionParsing(PyxformTestCase): """ - Test XLSForm Fields + Test XLSForm Fields. """ def test_names__question_basic_case__ok(self): diff --git a/tests/test_geo.py b/tests/test_geo.py index 67b36e04..081ebf0e 100644 --- a/tests/test_geo.py +++ b/tests/test_geo.py @@ -1,5 +1,5 @@ """ -## Geo control traceability +## Geo control traceability. Each test should reference one (or more) requirements from these lists. diff --git a/tests/test_guidance_hint.py b/tests/test_guidance_hint.py index 896a6953..187f923b 100644 --- a/tests/test_guidance_hint.py +++ b/tests/test_guidance_hint.py @@ -21,7 +21,7 @@ def test_hint_only(self): ) def test_guidance_hint_and_label(self): - """Test guidance_hint with label""" + """Test guidance_hint with label.""" self.assertPyxformXform( name="data", md=""" diff --git a/tests/test_j2x_creation.py b/tests/test_j2x_creation.py index 46ab15e4..a20645d9 100644 --- a/tests/test_j2x_creation.py +++ b/tests/test_j2x_creation.py @@ -1,5 +1,5 @@ """ -Testing creation of Surveys using verbose methods +Testing creation of Surveys using verbose methods. """ from unittest import TestCase @@ -38,7 +38,7 @@ def test_survey_can_be_created_in_a_slightly_less_verbose_manner(self): def test_allow_surveys_with_comment_rows(self): """Assume that a survey with rows that don't have name, type, or label - headings raise warning only + headings raise warning only. """ path = utils.path_to_text_fixture("allow_comment_rows_test.xls") survey = create_survey_from_xls(path) diff --git a/tests/test_j2x_question.py b/tests/test_j2x_question.py index 92c1d1bd..b06d46a7 100644 --- a/tests/test_j2x_question.py +++ b/tests/test_j2x_question.py @@ -1,5 +1,5 @@ """ -Testing creation of Surveys using verbose methods +Testing creation of Surveys using verbose methods. """ from collections.abc import Generator @@ -16,7 +16,7 @@ def ctw(control): """ Ctw stands for control_test_wrap, but ctw is shorter and easier. using - begin_str and end_str to take out the wrap that xml gives us + begin_str and end_str to take out the wrap that xml gives us. """ if isinstance(control, list) and len(control) == 1: control = control[0] diff --git a/tests/test_json2xform.py b/tests/test_json2xform.py index b06bfe73..e5a62aa0 100644 --- a/tests/test_json2xform.py +++ b/tests/test_json2xform.py @@ -1,5 +1,5 @@ """ -Testing simple cases for pyxform +Testing simple cases for pyxform. """ from unittest import TestCase @@ -15,7 +15,7 @@ class BasicJson2XFormTests(TestCase): def test_survey_can_have_to_xml_called_twice(self): """ - Test: Survey can have "to_xml" called multiple times + Test: Survey can have "to_xml" called multiple times. (This was not being allowed before.) diff --git a/tests/test_range.py b/tests/test_range.py index 0f0b74b7..b586adab 100644 --- a/tests/test_range.py +++ b/tests/test_range.py @@ -1,5 +1,5 @@ """ -## Range control traceability +## Range control traceability. Each test should reference one (or more) requirements from these lists. diff --git a/tests/test_repeat.py b/tests/test_repeat.py index 4c41f609..cef933d2 100644 --- a/tests/test_repeat.py +++ b/tests/test_repeat.py @@ -350,7 +350,7 @@ def test_choice_from_previous_repeat_answers_not_name(self): ) def test_choice_from_previous_repeat_answers_with_choice_filter(self): - """Select one choices from previous repeat answers with choice filter""" + """Select one choices from previous repeat answers with choice filter.""" xlsform_md = """ | survey | | | | | | | type | name | label | choice_filter | @@ -378,7 +378,7 @@ def test_choice_from_previous_repeat_answers_with_choice_filter(self): def test_choice_from_previous_repeat_answers_in_child_repeat(self): """ - Select one choice from previous repeat answers when within a child of a repeat + Select one choice from previous repeat answers when within a child of a repeat. """ xlsform_md = """ | survey | | | | | @@ -399,7 +399,7 @@ def test_choice_from_previous_repeat_answers_in_child_repeat(self): ) def test_choice_from_previous_repeat_answers_in_nested_repeat(self): - """Select one choices from previous repeat answers within a nested repeat""" + """Select one choices from previous repeat answers within a nested repeat.""" xlsform_md = """ | survey | | | | | | | type | name | label | choice_filter | @@ -420,7 +420,7 @@ def test_choice_from_previous_repeat_answers_in_nested_repeat(self): def test_choice_from_previous_repeat_answers_in_nested_repeat_uses_current(self): """ - Select one choices from previous repeat answers within a nested repeat should use current if a sibling node of a select is used + Select one choices from previous repeat answers within a nested repeat should use current if a sibling node of a select is used. """ xlsform_md = """ | survey | | | | | @@ -446,7 +446,7 @@ def test_choice_from_previous_repeat_answers_in_nested_repeat_uses_current(self) ) def test_choice_from_previous_repeat_in_current_repeat_parents_out_to_repeat(self): - """Test choice from previous repeat in current repeat produces the correct reference""" + """Test choice from previous repeat in current repeat produces the correct reference.""" xlsform_md = """ | survey | | | | | | | | | | type | name | label | choice_filter | appearance | relevant | calculation | @@ -679,7 +679,7 @@ def test_repeat_using_select_with_reference_path_in_predicate_uses_current( self, ): """ - Test relative path expansion using current if reference path is inside a predicate in a survey with select choice list + Test relative path expansion using current if reference path is inside a predicate in a survey with select choice list. """ xlsform_md = """ | survey | | | | | | @@ -709,7 +709,7 @@ def test_repeat_using_select_uses_current_with_reference_path_in_predicate_and_i self, ): """ - Test relative path expansion using current if reference path is inside a predicate and instance is not first expression in a survey with select choice list + Test relative path expansion using current if reference path is inside a predicate and instance is not first expression in a survey with select choice list. """ xlsform_md = """ | survey | | | | | | @@ -739,7 +739,7 @@ def test_repeat_and_group_with_reference_path_in_predicate_uses_current( self, ): """ - Test relative path expansion using current if reference path is inside a predicate in a survey with group + Test relative path expansion using current if reference path is inside a predicate in a survey with group. """ xlsform_md = """ | survey | | | | | @@ -764,7 +764,7 @@ def test_repeat_with_reference_path_in_predicate_uses_current( self, ): """ - Test relative path expansion using current if reference path is inside a predicate + Test relative path expansion using current if reference path is inside a predicate. """ xlsform_md = """ | survey | | | | | @@ -787,7 +787,7 @@ def test_repeat_with_reference_path_with_spaces_in_predicate_uses_current( self, ): """ - Test relative path expansion using current if reference path (with whitespaces before/after an operator of ${name}) is inside a predicate + Test relative path expansion using current if reference path (with whitespaces before/after an operator of ${name}) is inside a predicate. """ xlsform_md = """ | survey | | | | | @@ -814,7 +814,7 @@ def test_repeat_with_reference_path_in_a_method_with_spaces_in_predicate_uses_cu self, ): """ - Test relative path expansion using current if reference path in a method (with whitespaces before/after an operator of ${name}) is inside apredicate + Test relative path expansion using current if reference path in a method (with whitespaces before/after an operator of ${name}) is inside apredicate. """ xlsform_md = """ | survey | | | | | @@ -837,7 +837,7 @@ def test_repeat_with_reference_path_with_spaces_in_predicate_with_parenthesis_us self, ): """ - Test relative path expansion using current if reference path (with whitespaces before/after an operator of ${name}) is inside a predicate with parenthesis + Test relative path expansion using current if reference path (with whitespaces before/after an operator of ${name}) is inside a predicate with parenthesis. """ xlsform_md = """ | survey | | | | | @@ -860,7 +860,7 @@ def test_relative_path_expansion_not_using_current_if_reference_path_is_predicat self, ): """ - Test relative path expansion using xpath without current() if reference path is inside a predicate and not inside a repeat + Test relative path expansion using xpath without current() if reference path is inside a predicate and not inside a repeat. """ xlsform_md = """ | survey | | | | | @@ -880,7 +880,7 @@ def test_relative_path_expansion_not_using_current_if_reference_path_is_predicat self, ): """ - Test relative path expansion using xpath without current() if reference path is inside a predicate but not part of the primary instance + Test relative path expansion using xpath without current() if reference path is inside a predicate but not part of the primary instance. """ xlsform_md = """ | survey | | | | | @@ -902,7 +902,7 @@ def test_repeat_with_reference_path_in_multiple_predicate_uses_current( self, ): """ - Test relative path expansion using current if reference path is in multiple predicate + Test relative path expansion using current if reference path is in multiple predicate. """ xlsform_md = """ | survey | | | | | @@ -926,7 +926,7 @@ def test_repeat_with_reference_path_in_multiple_complex_predicate_uses_current( ): """ Test relative path expansion using current if reference path is in multiple predicate, one of it is a complex one - ${pos1} is not using current because it is not in repeat + ${pos1} is not using current because it is not in repeat. """ xlsform_md = """ | survey | | | | | @@ -953,7 +953,7 @@ def test_repeat_with_reference_path_after_instance_in_predicate_uses_current( Test relative reference path expansion uses current if reference path is in predicate even if the reference path is found after the instance() expression ${pos5} that is in 'index =${pos5}'' uses current because it is in a predicate - ${pos5} that is in 'position()=${pos5}'' uses current because it is in a predicate (eventhough not in an instance) + ${pos5} that is in 'position()=${pos5}'' uses current because it is in a predicate (eventhough not in an instance). """ xlsform_md = """ | survey | | | | | @@ -978,7 +978,7 @@ def test_repeat_with_reference_path_after_instance_not_in_predicate_not_using_cu Test relative reference path expansion not using current if reference path is not in predicate and the reference path is found after the instance() expression ${pos5} that is in 'index =${pos5} uses current because it is in a predicate - ${pos5} that is in '${pos5} + 1'' not using current because it is not in a predicate (regardless in an instance or not) + ${pos5} that is in '${pos5} + 1'' not using current because it is not in a predicate (regardless in an instance or not). """ xlsform_md = """ | survey | | | | | @@ -1073,7 +1073,7 @@ def test_calculation_using_node_from_nested_repeat_has_relative_reference(self): def test_repeat_adding_template_and_instance(self): """ - Repeat should add template and instances + Repeat should add template and instances. """ self.assertPyxformXform( md=""" @@ -1117,7 +1117,7 @@ def test_repeat_adding_template_and_instance(self): def test_repeat_adding_template_and_instance_with_group(self): """ - Repeat should add template and instance even when they are inside grouping + Repeat should add template and instance even when they are inside grouping. """ self.assertPyxformXform( md=""" diff --git a/tests/test_search_function.py b/tests/test_search_function.py index 984f8129..acbe2506 100644 --- a/tests/test_search_function.py +++ b/tests/test_search_function.py @@ -64,7 +64,7 @@ def setUpClass(cls) -> None: cls.run_odk_validate = True def test_shared_choice_list(self): - """Should include translation for search() items, when sharing the choice list""" + """Should include translation for search() items, when sharing the choice list.""" md = """ | survey | | | | | | | | type | name | label::en | label::fr | appearance | @@ -91,7 +91,7 @@ def test_shared_choice_list(self): ) def test_usage_with_other_selects(self): - """Should include translation for search() items, when used with other selects""" + """Should include translation for search() items, when used with other selects.""" md = """ | survey | | | | | | | | type | name | label::en | label::fr | appearance | @@ -138,7 +138,7 @@ def test_usage_with_other_selects__invalid_list_reuse_by_non_search_question(sel ) def test_single_question_usage(self): - """Should include translation for search() items, edge case of single question""" + """Should include translation for search() items, edge case of single question.""" md = """ | survey | | | | | | | | type | name | label::en | label::fr | appearance | @@ -162,7 +162,7 @@ def test_single_question_usage(self): ) def test_additional_static_choices(self): - """Should include translation for search() items, when adding static choices""" + """Should include translation for search() items, when adding static choices.""" md = """ | survey | | | | | | | | type | name | label::en | label::fr | appearance | @@ -212,7 +212,7 @@ def test_name_clashes(self): ) def test_search_and_select_xlsx(self): - """Test to replace the old XLSX-based test fixture, 'search_and_select.xlsx'""" + """Test to replace the old XLSX-based test fixture, 'search_and_select.xlsx'.""" md = """ | survey | | | | | | | type | name | label | appearance | diff --git a/tests/test_sheet_columns.py b/tests/test_sheet_columns.py index cb43032b..3d55b2cd 100644 --- a/tests/test_sheet_columns.py +++ b/tests/test_sheet_columns.py @@ -66,12 +66,12 @@ def test_form_id_variant(self): class TestSurveyColumns(PyxformTestCase): """ - Invalid survey column tests + Invalid survey column tests. """ def test_missing_name(self): """ - Every question needs a name (or alias of name) + Every question needs a name (or alias of name). """ self.assertPyxformXform( name="invalidcols", @@ -145,7 +145,7 @@ def test_media_column__is_ignored(self): def test_column_case(self): """ - Ensure that column name is case insensitive + Ensure that column name is case insensitive. """ self.assertPyxformXform( md=""" @@ -205,13 +205,13 @@ def test_missing_survey_headers(self): class TestChoicesColumns(PyxformTestCase): """ - Invalid choice sheet column tests + Invalid choice sheet column tests. """ @staticmethod def _simple_choice_ss(choice_sheet=None): """ - Return simple choices sheet + Return simple choices sheet. """ if choice_sheet is None: choice_sheet = [] @@ -228,7 +228,7 @@ def _simple_choice_ss(choice_sheet=None): def test_valid_choices_sheet_passes(self): """ - Test invalid choices sheet passes + Test invalid choices sheet passes. """ self.assertPyxformXform( name="valid_choices", @@ -242,7 +242,7 @@ def test_valid_choices_sheet_passes(self): def test_invalid_choices_sheet_fails(self): """ - Test invalid choices sheet fails + Test invalid choices sheet fails. """ self.assertPyxformXform( name="missing_name", @@ -260,7 +260,7 @@ def test_invalid_choices_sheet_fails(self): def test_missing_list_name(self): """ - Test missing sheet name + Test missing sheet name. """ self.assertPyxformXform( name="missing_list_name", @@ -294,12 +294,12 @@ def test_missing_choice_headers(self): class TestColumnAliases(PyxformTestCase): """ - Aliases Tests + Aliases Tests. """ def test_value_and_name(self): """ - Confirm that both 'name' and 'value' columns of choice list work + Confirm that both 'name' and 'value' columns of choice list work. """ md = """ | survey | | | | diff --git a/tests/test_survey.py b/tests/test_survey.py index d1991131..ceb036b8 100644 --- a/tests/test_survey.py +++ b/tests/test_survey.py @@ -196,7 +196,7 @@ def build_survey_from_path_spec( class TestGetPathRelativeToLCAR(TestCase): """ - Tests of pyxform.survey.get_path_relative_to_lcar + Tests of `pyxform.survey.get_path_relative_to_lcar`. """ def assert_relative_path( diff --git a/tests/test_translations.py b/tests/test_translations.py index 879ee582..ed536719 100644 --- a/tests/test_translations.py +++ b/tests/test_translations.py @@ -224,7 +224,7 @@ def test_spaces_adjacent_to_translation_delimiter(self): ) def test_missing_media_itext(self): - """Test missing media itext translation + """Test missing media itext translation. Fix for https://github.com/XLSForm/pyxform/issues/32 """ diff --git a/tests/test_trigger.py b/tests/test_trigger.py index 1e5d1716..1c069039 100644 --- a/tests/test_trigger.py +++ b/tests/test_trigger.py @@ -1,5 +1,5 @@ """ -Test handling setvalue of 'trigger' column in forms +Test handling setvalue of 'trigger' column in forms. """ from itertools import product diff --git a/tests/test_upload_question.py b/tests/test_upload_question.py index 92d0708c..aa971255 100644 --- a/tests/test_upload_question.py +++ b/tests/test_upload_question.py @@ -1,5 +1,5 @@ """ -Test upload (image, audio, file) question types in XLSForm +Test upload (image, audio, file) question types in XLSForm. """ from tests.pyxform_test_case import PyxformTestCase diff --git a/tests/test_validators.py b/tests/test_validators.py index 868bb449..bc832815 100644 --- a/tests/test_validators.py +++ b/tests/test_validators.py @@ -12,7 +12,7 @@ class TestValidatorsUtil(TestCase): - """Test validators.util""" + """Test validators.util.""" def test_check_java_available__found(self): """Should not raise an error when Java is found.""" diff --git a/tests/test_xform2json.py b/tests/test_xform2json.py index 19b353f4..e3d09738 100644 --- a/tests/test_xform2json.py +++ b/tests/test_xform2json.py @@ -106,13 +106,13 @@ def test_try_parse_with_bad_file(self): class TestXForm2JSON(PyxformTestCase): """ - Test xform2json module + Test xform2json module. """ def test_convert_toJSON_multi_language(self): """ Test that it's possible to convert XLSForms with multiple languages - to JSON and back into XML without losing any of the required information + to JSON and back into XML without losing any of the required information. """ md = """ | survey | diff --git a/tests/test_xls2json.py b/tests/test_xls2json.py index 302f9d63..b1c94bd6 100644 --- a/tests/test_xls2json.py +++ b/tests/test_xls2json.py @@ -563,7 +563,7 @@ def test_workbook_to_json__optional_sheets_ok(self): ) def test_xls2xform_convert__e2e_row_with_no_column_value(self): - """Programmatically-created XLSX files may have rows without column values""" + """Programmatically-created XLSX files may have rows without column values.""" md = """ | survey | | | | | | | type | name | label | hint | @@ -602,7 +602,7 @@ def test_xls2xform_convert__e2e_with_settings_misspelling(self): def test_xls2xform_convert__e2e_with_extra_columns__does_not_use_excessive_memory( self, ): - """Degenerate form with many blank columns""" + """Degenerate form with many blank columns.""" process = psutil.Process(os.getpid()) pre_mem = process.memory_info().rss xls2xform_convert( diff --git a/tests/test_xls2json_xls.py b/tests/test_xls2json_xls.py index 121ea2a2..5e0e6853 100644 --- a/tests/test_xls2json_xls.py +++ b/tests/test_xls2json_xls.py @@ -1,5 +1,5 @@ """ -Testing simple cases for Xls2Json +Testing simple cases for Xls2Json. """ import json @@ -105,7 +105,7 @@ def test_text_and_integer(self): def test_choice_filter_choice_fields(self): """ - Test that the choice filter fields appear on children field of json + Test that the choice filter fields appear on children field of json. """ choice_filter_survey = SurveyReader( utils.path_to_text_fixture("choice_filter_test.xlsx"), @@ -179,7 +179,7 @@ class UnicodeCsvTest(TestCase): def test_a_unicode_csv_works(self): """ Simply tests that xls2json_backends.csv_to_dict does not have a problem - with a csv with unicode characters + with a csv with unicode characters. """ utf_csv_path = utils.path_to_text_fixture("utf_csv.csv") dict_value = csv_to_dict(utf_csv_path) diff --git a/tests/test_xls2xform.py b/tests/test_xls2xform.py index aa725a36..ceb53244 100644 --- a/tests/test_xls2xform.py +++ b/tests/test_xls2xform.py @@ -36,7 +36,7 @@ def test_create_parser_without_args(self): def test_create_parser_optional_output_path(self): """ Should run fine for a single argument i.e. that is the - path to the xlsx file path, while the output path is left out + path to the xlsx file path, while the output path is left out. """ try: _create_parser().parse_args(["/some/path/tofile.xlsx"]) @@ -136,7 +136,7 @@ def test_validator_args_logic_odk_and_enketo(self): self.assertEqual(True, args.enketo_validate) def test_validator_args_logic_skip_validate_override(self): - """Should deactivate both validators""" + """Should deactivate both validators.""" raw_args = _create_parser().parse_args( [ "xlsform.xlsx", @@ -166,7 +166,7 @@ def test_validator_args_logic_skip_validate_override(self): def test_xls2form_convert_parameters(self, converter_mock, parser_mock_args): """ Checks that xls2xform_convert is given the right arguments, when the - output-path is not given + output-path is not given. """ converter_mock.return_value = "{}" main_cli() @@ -195,7 +195,7 @@ def test_xls2xform_convert_params_with_flags(self, converter_mock, parser_mock_a """ Should call xlsform_convert with the correct input for output path where only the xlsform input path and json flag were provided, since - the xlsform-convert can be called if json flag was set or when not + the xlsform-convert can be called if json flag was set or when not. """ converter_mock.return_value = "{}" main_cli() @@ -221,7 +221,7 @@ def test_xls2xform_convert_params_with_flags(self, converter_mock, parser_mock_a ) def test_xls2xform_convert_throwing_odk_error(self, parser_mock_args): """ - Parse and validate bad_calc.xlsx + Parse and validate bad_calc.xlsx. """ logger = logging.getLogger("pyxform.xls2xform") with mock.patch.object(logger, "error") as mock_debug: @@ -229,7 +229,7 @@ def test_xls2xform_convert_throwing_odk_error(self, parser_mock_args): self.assertEqual(mock_debug.call_count, 1) def test_get_xml_path_function(self): - """Should return an xml path in the same directory as the xlsx file""" + """Should return an xml path in the same directory as the xlsx file.""" xlsx_path = "/home/user/Desktop/xlsform.xlsx" expected = "/home/user/Desktop/xlsform.xml" self.assertEqual(expected, get_xml_path(xlsx_path)) diff --git a/tests/xform_test_case/test_bugs.py b/tests/xform_test_case/test_bugs.py index e946a9f9..8dcf9c3d 100644 --- a/tests/xform_test_case/test_bugs.py +++ b/tests/xform_test_case/test_bugs.py @@ -144,7 +144,7 @@ def test_xl_date_ambigous_no_exception(self): class TestSpreadSheetFilesWithMacrosAreAllowed(TestCase): - """Test that spreadsheets with .xlsm extension are allowed""" + """Test that spreadsheets with .xlsm extension are allowed.""" def test_xlsm_files_are_allowed(self): filename = "excel_with_macros.xlsm" @@ -153,7 +153,7 @@ def test_xlsm_files_are_allowed(self): class TestBadCalculation(TestCase): - """Bad calculation should not kill the application""" + """Bad calculation should not kill the application.""" def test_bad_calculate_javarosa_error(self): filename = "bad_calc.xml" From 40735c9cf6218f414268bb0adbda46d3ed6efd8f Mon Sep 17 00:00:00 2001 From: lindsay stevens Date: Mon, 13 Jul 2026 21:34:49 +1000 Subject: [PATCH 11/50] dev: add and apply pydocstyle rule D200 - the full pydocstyle ruleset has many changes so enabling some of them individually to keep the diffs manageable. --- pyproject.toml | 1 + pyxform/builder.py | 8 +-- pyxform/elements/action.py | 8 +-- pyxform/elements/element.py | 4 +- pyxform/entities/entities_parsing.py | 36 +++------- pyxform/errors.py | 4 +- pyxform/external_instance.py | 4 +- pyxform/file_utils.py | 8 +-- pyxform/instance.py | 4 +- pyxform/parsing/expression.py | 4 +- pyxform/parsing/sheet_headers.py | 4 +- pyxform/question.py | 12 +--- pyxform/question_type_dictionary.py | 4 +- pyxform/section.py | 12 +--- pyxform/survey.py | 28 ++------ pyxform/survey_element.py | 16 ++--- pyxform/translator.py | 4 +- pyxform/utils.py | 8 +-- .../validators/enketo_validate/__init__.py | 12 +--- pyxform/validators/error_cleaner.py | 4 +- pyxform/validators/odk_validate/__init__.py | 12 +--- .../pyxform/iana_subtags/validation.py | 4 +- pyxform/validators/pyxform/parameters.py | 4 +- .../pyxform/question_types/__init__.py | 4 +- pyxform/validators/updater.py | 56 ++++----------- pyxform/validators/util.py | 8 +-- pyxform/xform2json.py | 28 ++------ pyxform/xform_instance_parser.py | 12 +--- pyxform/xls2json.py | 8 +-- pyxform/xls2json_backends.py | 20 ++---- pyxform/xls2xform.py | 4 +- tests/pyxform_test_case.py | 4 +- tests/test_area.py | 8 +-- tests/test_audit.py | 8 +-- tests/test_bind_conversions.py | 8 +-- tests/test_bug_round_calculation.py | 4 +- tests/test_builder.py | 4 +- tests/test_choices_sheet.py | 8 +-- tests/test_dump_and_load.py | 4 +- tests/test_dynamic_default.py | 12 +--- tests/test_external_instances.py | 12 +--- tests/test_external_instances_for_selects.py | 8 +-- tests/test_fields.py | 8 +-- tests/test_file.py | 12 +--- tests/test_file_utils.py | 4 +- tests/test_form_name.py | 12 +--- tests/test_geo.py | 4 +- tests/test_group.py | 8 +-- tests/test_guidance_hint.py | 4 +- tests/test_image_app_parameter.py | 4 +- tests/test_j2x_creation.py | 4 +- tests/test_j2x_instantiation.py | 4 +- tests/test_j2x_question.py | 24 ++----- tests/test_js2x_import_from_json.py | 4 +- tests/test_json2xform.py | 4 +- tests/test_language_warnings.py | 8 +-- tests/test_last_saved.py | 4 +- tests/test_loop.py | 4 +- tests/test_metadata.py | 8 +-- tests/test_notes.py | 8 +-- tests/test_osm.py | 8 +-- tests/test_parameters_rows.py | 4 +- tests/test_randomize_itemsets.py | 4 +- tests/test_rank.py | 4 +- tests/test_repeat.py | 72 +++++-------------- tests/test_search_function.py | 4 +- tests/test_secondary_instance_translations.py | 32 +++------ tests/test_set_geopoint.py | 4 +- tests/test_settings.py | 4 +- tests/test_settings_auto_send_delete.py | 4 +- tests/test_sheet_columns.py | 44 +++--------- tests/test_sms.py | 4 +- tests/test_survey.py | 8 +-- tests/test_translations.py | 8 +-- tests/test_trigger.py | 4 +- tests/test_tutorial_xls.py | 4 +- tests/test_unicode_rtl.py | 4 +- tests/test_upload_question.py | 4 +- tests/test_validate_unicode_exception.py | 4 +- tests/test_validator_update.py | 4 +- tests/test_validator_util.py | 4 +- tests/test_validators.py | 4 +- tests/test_warnings.py | 4 +- tests/test_whitespace.py | 4 +- tests/test_xform2json.py | 8 +-- tests/test_xls2json_backends.py | 8 +-- tests/test_xls2json_xls.py | 8 +-- tests/test_xls2xform.py | 16 ++--- tests/utils.py | 12 +--- tests/xform_test_case/test_bugs.py | 4 +- .../xform_test_case/test_xform_conversion.py | 4 +- tests/xform_test_case/test_xml.py | 4 +- tests/xpath_helpers/choices.py | 4 +- tests/xpath_helpers/entities.py | 4 +- tests/xpath_helpers/group.py | 4 +- tests/xpath_helpers/questions.py | 4 +- tests/xpath_helpers/settings.py | 4 +- 97 files changed, 221 insertions(+), 660 deletions(-) diff --git a/pyproject.toml b/pyproject.toml index 4424fae4..b55d7b91 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -55,6 +55,7 @@ src = ["pyxform", "tests"] select = [ "B", # flake8-bugbear "C4", # flake8-comprehensions + "D200", # pydocstyle (unnecessary-multiline-docstring) "D202", # pydocstyle (blank-line-after-function) "D209", # pydocstyle (new-line-after-last-paragraph) "D301", # pydocstyle (escape-sequence-in-docstring) diff --git a/pyxform/builder.py b/pyxform/builder.py index 156a6a5b..47ab4511 100644 --- a/pyxform/builder.py +++ b/pyxform/builder.py @@ -1,6 +1,4 @@ -""" -Survey builder functionality. -""" +"""Survey builder functionality.""" import os from collections import defaultdict @@ -308,9 +306,7 @@ def create_survey_element_from_json(self, str_or_path): def create_survey_element_from_dict(d, sections=None): - """ - Creates a Survey from a dictionary in the format provided by SurveyReader. - """ + """Creates a Survey from a dictionary in the format provided by SurveyReader.""" if sections is None: sections = {} builder = SurveyElementBuilder() diff --git a/pyxform/elements/action.py b/pyxform/elements/action.py index 97ccf3a3..ce4557a6 100644 --- a/pyxform/elements/action.py +++ b/pyxform/elements/action.py @@ -6,9 +6,7 @@ class Event(StrEnum): - """ - Supported W3C XForms 1.1 Events and ODK extensions. - """ + """Supported W3C XForms 1.1 Events and ODK extensions.""" # For actions in model under /html/head/model ODK_INSTANCE_FIRST_LOAD = "odk-instance-first-load" @@ -128,9 +126,7 @@ def to_dict(self): class ActionLibrary(Enum): - """ - A collection of action/event configs used by pyxform. - """ + """A collection of action/event configs used by pyxform.""" setvalue_first_load = LibraryMember( name=Setvalue.name, diff --git a/pyxform/elements/element.py b/pyxform/elements/element.py index 9240d27f..5573da5a 100644 --- a/pyxform/elements/element.py +++ b/pyxform/elements/element.py @@ -15,7 +15,5 @@ class Element: name: str def node(self) -> "DetachableElement": - """ - Create the element. - """ + """Create the element.""" raise NotImplementedError() diff --git a/pyxform/entities/entities_parsing.py b/pyxform/entities/entities_parsing.py index 6a619676..859b3dd3 100644 --- a/pyxform/entities/entities_parsing.py +++ b/pyxform/entities/entities_parsing.py @@ -15,9 +15,7 @@ @dataclass(frozen=True, slots=True) class ContainerNode: - """ - Details of a XForm container: the survey root, a group, or a repeat. - """ + """Details of a XForm container: the survey root, a group, or a repeat.""" name: str type: str @@ -38,16 +36,12 @@ def __str__(self): @classmethod def default(cls) -> "ContainerPath": - """ - Create the default ContainerPath, which is the '/survey' root path. - """ + """Create the default ContainerPath, which is the '/survey' root path.""" return cls((ContainerNode(name=const.SURVEY, type=const.SURVEY),)) @classmethod def from_stack(cls, stack: list[dict[str, Any]]) -> "ContainerPath": - """ - Create a ContainerPath from the workbook_to_json container stack. - """ + """Create a ContainerPath from the workbook_to_json container stack.""" if len(stack) > 1: return cls( ( @@ -62,9 +56,7 @@ def from_stack(cls, stack: list[dict[str, Any]]) -> "ContainerPath": return cls.default() def get_scope_boundary(self) -> "ContainerPath": - """ - Get the full path to the nearest ancestor boundary scope node. - """ + """Get the full path to the nearest ancestor boundary scope node.""" for i in range(len(self.nodes) - 1, -1, -1): if self.nodes[i].type in {const.REPEAT, const.SURVEY}: return ContainerPath(self.nodes[: i + 1]) @@ -122,9 +114,7 @@ class EntityReferences: ) def get_allocation_request(self) -> "AllocationRequest": - """ - Find/validate the preferred path for each entity declaration. - """ + """Find/validate the preferred path for each entity declaration.""" deepest_scope_ref = None deepest_scope_boundary = None deepest_scope_boundary_node_count = None @@ -530,9 +520,7 @@ def validate_saveto( def get_entity_declarations( entities_sheet: Iterable[dict], ) -> dict[str, dict[str, Any]]: - """ - Collect all entity declarations from the entities sheet. - """ + """Collect all entity declarations from the entities sheet.""" entities = {} for row_number, row in enumerate(entities_sheet, start=2): entity = get_entity_declaration(row=row, row_number=row_number) @@ -576,9 +564,7 @@ def get_entity_references_by_question( is_container_begin: bool, is_container_end: bool, ) -> None: - """ - For each question store the saveto or variable references that link it to an entity. - """ + """For each question store the saveto or variable references that link it to an entity.""" # Collect references for later reconciliation, because otherwise the first # referent found will determine the scope but there may be deeper refs. saveto = row.get(const.BIND, {}).get(const.ENTITIES_SAVETO_NS) @@ -639,9 +625,7 @@ def allocate_entities_to_containers( entity_declarations: dict[str, dict[str, Any]], entity_references_by_question: dict[str, EntityReferences], ) -> dict[ContainerPath, str]: - """ - Get the paths into which the entities will be placed. - """ + """Get the paths into which the entities will be placed.""" allocations: dict[ContainerPath, str] = {} scope_paths: defaultdict[ContainerPath, list[AllocationRequest]] = defaultdict(list) survey_path = ContainerPath.default() @@ -734,9 +718,7 @@ def inject_entities_into_json( entities_allocated: set[str] | None = None, has_repeat_ancestor: bool = False, ) -> dict[str, Any]: - """ - Recursively traverse the json_dict to inject entity declarations. - """ + """Recursively traverse the json_dict to inject entity declarations.""" if entities_allocated is None: entities_allocated = set() diff --git a/pyxform/errors.py b/pyxform/errors.py index 88ceb2fe..ec84efa2 100644 --- a/pyxform/errors.py +++ b/pyxform/errors.py @@ -1,6 +1,4 @@ -""" -Common base classes for pyxform exceptions. -""" +"""Common base classes for pyxform exceptions.""" from enum import Enum from string import Formatter diff --git a/pyxform/external_instance.py b/pyxform/external_instance.py index 49914979..130fd591 100644 --- a/pyxform/external_instance.py +++ b/pyxform/external_instance.py @@ -1,6 +1,4 @@ -""" -ExternalInstance class module. -""" +"""ExternalInstance class module.""" from typing import TYPE_CHECKING diff --git a/pyxform/file_utils.py b/pyxform/file_utils.py index 1771bf77..7cc69fd4 100644 --- a/pyxform/file_utils.py +++ b/pyxform/file_utils.py @@ -1,6 +1,4 @@ -""" -The pyxform file utility functions. -""" +"""The pyxform file utility functions.""" import glob import os @@ -32,9 +30,7 @@ def load_file_to_dict(path): def collect_compatible_files_in_directory(directory): - """ - Create a giant dict out of all the spreadsheets and json forms in the given directory. - """ + """Create a giant dict out of all the spreadsheets and json forms in the given directory.""" available_files = glob.glob(os.path.join(directory, "*.xls")) + glob.glob( os.path.join(directory, "*.json") ) diff --git a/pyxform/instance.py b/pyxform/instance.py index f05c3182..8823af05 100644 --- a/pyxform/instance.py +++ b/pyxform/instance.py @@ -1,6 +1,4 @@ -""" -SurveyInstance class module. -""" +"""SurveyInstance class module.""" import os.path diff --git a/pyxform/parsing/expression.py b/pyxform/parsing/expression.py index 014d4e35..5d960b66 100644 --- a/pyxform/parsing/expression.py +++ b/pyxform/parsing/expression.py @@ -118,9 +118,7 @@ def parse_expression(text: str) -> tuple[Token, ...]: def is_xml_tag(value: str) -> bool: - """ - Does the input string contain only a valid XML tag / element name? - """ + """Does the input string contain only a valid XML tag / element name?""" return value and bool(RE_NCNAME_NAMESPACED.fullmatch(value)) diff --git a/pyxform/parsing/sheet_headers.py b/pyxform/parsing/sheet_headers.py index 508be6c1..4242d7e3 100644 --- a/pyxform/parsing/sheet_headers.py +++ b/pyxform/parsing/sheet_headers.py @@ -63,9 +63,7 @@ def merge_dicts( def list_to_nested_dict(lst: Sequence) -> dict: - """ - Example: `[1,2,3,4] -> {1:{2:{3:4}}}`. - """ + """Example: `[1,2,3,4] -> {1:{2:{3:4}}}`.""" if len(lst) > 1: return {lst[0]: list_to_nested_dict(lst[1:])} else: diff --git a/pyxform/question.py b/pyxform/question.py index f870d308..cd00b831 100644 --- a/pyxform/question.py +++ b/pyxform/question.py @@ -1,6 +1,4 @@ -""" -XForm Survey element classes for different question types. -""" +"""XForm Survey element classes for different question types.""" import os.path from collections.abc import Callable, Generator, Iterable @@ -198,9 +196,7 @@ def xml_control(self, survey: "Survey"): return xml_node def xml_actions(self, survey: "Survey", in_repeat: bool = False): - """ - Return the action(s) for this survey element. - """ + """Return the action(s) for this survey element.""" action_fields = {"name", "event", "value"} if self.actions: for _action in self.actions: @@ -227,9 +223,7 @@ def xml_actions(self, survey: "Survey", in_repeat: bool = False): ).node() def _build_xml(self, survey: "Survey") -> DetachableElement | None: - """ - Initial control node result for further processing depending on Question type. - """ + """Initial control node result for further processing depending on Question type.""" control_dict = self.control result = node( control_dict["tag"], diff --git a/pyxform/question_type_dictionary.py b/pyxform/question_type_dictionary.py index f3ee4e24..2969623d 100644 --- a/pyxform/question_type_dictionary.py +++ b/pyxform/question_type_dictionary.py @@ -1,6 +1,4 @@ -""" -XForm survey question type mapping dictionary module. -""" +"""XForm survey question type mapping dictionary module.""" from collections.abc import Sequence from types import MappingProxyType diff --git a/pyxform/section.py b/pyxform/section.py index a14da901..e81843d8 100644 --- a/pyxform/section.py +++ b/pyxform/section.py @@ -1,6 +1,4 @@ -""" -Section survey element module. -""" +"""Section survey element module.""" from collections.abc import Callable, Generator, Iterable from itertools import chain @@ -126,9 +124,7 @@ def _validate_uniqueness_of_element_names(self): ) def xml_instance(self, survey: "Survey", **kwargs): - """ - Creates an xml representation of the section. - """ + """Creates an xml representation of the section.""" append_template = kwargs.pop("append_template", False) attributes = {} @@ -174,9 +170,7 @@ def generate_repeating_template(self, survey: "Survey", **kwargs): return result def xml_instance_array(self, survey: "Survey"): - """ - This method is used for generating flat instances. - """ + """This method is used for generating flat instances.""" for child in self.children: if hasattr(child, "flat") and child.get("flat"): yield from child.xml_instance_array(survey=survey) diff --git a/pyxform/survey.py b/pyxform/survey.py index 54ed9304..7e7f0a7c 100644 --- a/pyxform/survey.py +++ b/pyxform/survey.py @@ -1,6 +1,4 @@ -""" -Survey module with XForm Survey objects and utility functions. -""" +"""Survey module with XForm Survey objects and utility functions.""" import os import re @@ -173,9 +171,7 @@ def recursive_dict(): class Survey(Section): - """ - Survey class - represents the full XForm XML. - """ + """Survey class - represents the full XForm XML.""" __slots__ = SURVEY_EXTRA_FIELDS @@ -289,9 +285,7 @@ def get_nsmap(self): return NSMAP def xml(self): - """ - Calls necessary preparation methods, then returns the xml. - """ + """Calls necessary preparation methods, then returns the xml.""" self.validate() self._setup_xpath_dictionary() @@ -310,9 +304,7 @@ def xml(self): def _generate_static_instances( self, list_name: str, itemset: Itemset ) -> InstanceInfo: - """ - Generate elements for static data (e.g. choices for selects). - """ + """Generate elements for static data (e.g. choices for selects).""" def choice_nodes(idx, choice): # Add a unique id to the choice element in case there are itext references @@ -466,9 +458,7 @@ def _generate_from_file_instances( @staticmethod def _generate_last_saved_instance(element: Question) -> bool: - """ - True if a last-saved instance should be generated, false otherwise. - """ + """True if a last-saved instance should be generated, false otherwise.""" if element.default and has_pyxform_reference_with_last_saved(element.default): return True if element.choice_filter and has_pyxform_reference_with_last_saved( @@ -594,9 +584,7 @@ def get_element_instances(): seen[i.name] = i def xml_model_bindings(self) -> Generator[DetachableElement | None, None, None]: - """ - Yield bindings (bind or action elements) for this node and all its descendants. - """ + """Yield bindings (bind or action elements) for this node and all its descendants.""" for e in self.iter_descendants( condition=lambda i: not isinstance(i, Option | Tag) ): @@ -606,9 +594,7 @@ def xml_model_bindings(self) -> Generator[DetachableElement | None, None, None]: yield from e.xml_actions(survey=self, in_repeat=False) def xml_model(self): - """ - Generate the xform element. - """ + """Generate the xform element.""" self._setup_translations() self._setup_media() self._add_empty_translations() diff --git a/pyxform/survey_element.py b/pyxform/survey_element.py index a54f1be8..c7bcf6fc 100644 --- a/pyxform/survey_element.py +++ b/pyxform/survey_element.py @@ -1,6 +1,4 @@ -""" -Survey Element base class for all survey elements. -""" +"""Survey Element base class for all survey elements.""" import json import warnings @@ -193,9 +191,7 @@ def iter_ancestors( def lowest_common_ancestor( self, other: "SurveyElement", group_type: str | None = None ) -> tuple[str, int | None, int | None, Optional["SurveyElement"]]: - """ - Get the relation type, steps from self, steps from other, and the common ancestor. - """ + """Get the relation type, steps from self, steps from other, and the common ancestor.""" # Filtering if group_type: type_filter = {group_type} @@ -238,9 +234,7 @@ def lowest_common_ancestor( return "Common Ancestor", self_ancestors[lca], other_ancestors[lca], lca def get_xpath(self, relative_to: Optional["SurveyElement"] = None) -> str: - """ - Return the xpath of this survey element. - """ + """Return the xpath of this survey element.""" # Imported here to avoid circular references. from pyxform.survey import Survey @@ -525,9 +519,7 @@ def xml_label_and_hint(self, survey: "Survey") -> list["DetachableElement"]: def xml_bindings( self, survey: "Survey" ) -> Generator[DetachableElement | None, None, None]: - """ - Return the binding(s) for this survey element. - """ + """Return the binding(s) for this survey element.""" if not hasattr(self, "bind") or self.get("bind") is None: return None if hasattr(self, "flat") and self.get("flat"): diff --git a/pyxform/translator.py b/pyxform/translator.py index 7032e850..9936d4f7 100644 --- a/pyxform/translator.py +++ b/pyxform/translator.py @@ -1,6 +1,4 @@ -""" -Translator class module. -""" +"""Translator class module.""" from collections import defaultdict diff --git a/pyxform/utils.py b/pyxform/utils.py index 975c500a..6cc73071 100644 --- a/pyxform/utils.py +++ b/pyxform/utils.py @@ -1,6 +1,4 @@ -""" -pyxform utils module. -""" +"""pyxform utils module.""" import copy import csv @@ -199,9 +197,7 @@ def external_choices_to_csv( def has_external_choices(json_struct): - """ - Returns true if a select one external prompt is used in the survey. - """ + """Returns true if a select one external prompt is used in the survey.""" if isinstance(json_struct, dict): for k, v in json_struct.items(): if ( diff --git a/pyxform/validators/enketo_validate/__init__.py b/pyxform/validators/enketo_validate/__init__.py index becdeb91..5c4a9e59 100644 --- a/pyxform/validators/enketo_validate/__init__.py +++ b/pyxform/validators/enketo_validate/__init__.py @@ -1,6 +1,4 @@ -""" -Validate XForms using Enketo validator. -""" +"""Validate XForms using Enketo validator.""" import os from typing import TYPE_CHECKING @@ -26,9 +24,7 @@ class EnketoValidateError(Exception): def install_exists(): - """ - Check if Enketo-validate is installed. - """ + """Check if Enketo-validate is installed.""" return os.path.exists(ENKETO_VALIDATE_PATH) @@ -37,9 +33,7 @@ def _call_validator(path_to_xform, bin_file_path=ENKETO_VALIDATE_PATH) -> "Popen def install_ok(bin_file_path=ENKETO_VALIDATE_PATH): - """ - Check if Enketo-validate functions as expected. - """ + """Check if Enketo-validate functions as expected.""" check_readable(file_path=XFORM_SPEC_PATH) return_code, _, _, _ = _call_validator( path_to_xform=XFORM_SPEC_PATH, bin_file_path=bin_file_path diff --git a/pyxform/validators/error_cleaner.py b/pyxform/validators/error_cleaner.py index 2a06acdb..dd83bdc0 100644 --- a/pyxform/validators/error_cleaner.py +++ b/pyxform/validators/error_cleaner.py @@ -1,6 +1,4 @@ -""" -Cleans up error messages from the validators. -""" +"""Cleans up error messages from the validators.""" import re diff --git a/pyxform/validators/odk_validate/__init__.py b/pyxform/validators/odk_validate/__init__.py index 016e2200..953154de 100644 --- a/pyxform/validators/odk_validate/__init__.py +++ b/pyxform/validators/odk_validate/__init__.py @@ -1,6 +1,4 @@ -""" -A python wrapper around ODK Validate. -""" +"""A python wrapper around ODK Validate.""" import logging import os @@ -39,9 +37,7 @@ def _call_validator(path_to_xform, bin_file_path=ODK_VALIDATE_PATH) -> "PopenRes def install_ok(bin_file_path=ODK_VALIDATE_PATH): - """ - Check if ODK Validate functions as expected. - """ + """Check if ODK Validate functions as expected.""" check_readable(file_path=XFORM_SPEC_PATH) result = _call_validator( path_to_xform=XFORM_SPEC_PATH, @@ -54,9 +50,7 @@ def install_ok(bin_file_path=ODK_VALIDATE_PATH): def check_java_available(): - """ - Check if 'which java' returncode is 0. If not, raise an error since java is required. - """ + """Check if 'which java' returncode is 0. If not, raise an error since java is required.""" java_path = shutil.which(cmd="java") if java_path is not None: return diff --git a/pyxform/validators/pyxform/iana_subtags/validation.py b/pyxform/validators/pyxform/iana_subtags/validation.py index a65481a1..a968dd56 100644 --- a/pyxform/validators/pyxform/iana_subtags/validation.py +++ b/pyxform/validators/pyxform/iana_subtags/validation.py @@ -16,9 +16,7 @@ def read_tags(file_name: str) -> set[str]: def get_languages_with_bad_tags(languages): - """ - Returns languages with invalid or missing IANA subtags. - """ + """Returns languages with invalid or missing IANA subtags.""" languages_with_bad_tags = [] for lang in languages: # Minimum matchable lang code attempt requires 3 characters e.g. "a()". diff --git a/pyxform/validators/pyxform/parameters.py b/pyxform/validators/pyxform/parameters.py index 5bae8dac..249b520f 100644 --- a/pyxform/validators/pyxform/parameters.py +++ b/pyxform/validators/pyxform/parameters.py @@ -11,9 +11,7 @@ def validate( accepted: type[StrEnum], row_number: int, ) -> None: - """ - Raise an error if 'parameters' includes any keys not named in 'accepted'. - """ + """Raise an error if 'parameters' includes any keys not named in 'accepted'.""" extras = set(parameters) - accepted.value_set() if 0 < len(extras): raise PyXFormError( diff --git a/pyxform/validators/pyxform/question_types/__init__.py b/pyxform/validators/pyxform/question_types/__init__.py index 8e24eca4..ac6fa10a 100644 --- a/pyxform/validators/pyxform/question_types/__init__.py +++ b/pyxform/validators/pyxform/question_types/__init__.py @@ -1,6 +1,4 @@ -""" -Validations for question types. -""" +"""Validations for question types.""" from collections.abc import Collection, Iterable diff --git a/pyxform/validators/updater.py b/pyxform/validators/updater.py index 66b2a964..4172e777 100644 --- a/pyxform/validators/updater.py +++ b/pyxform/validators/updater.py @@ -1,6 +1,4 @@ -""" -pyxform_validator_update - command to update XForm validators. -""" +"""pyxform_validator_update - command to update XForm validators.""" import argparse import fnmatch @@ -23,9 +21,7 @@ class _UpdateInfo: - """ - Data class for Updater info. - """ + """Data class for Updater info.""" def __init__( self, @@ -78,9 +74,7 @@ class _UpdateHandler: @staticmethod def _request_latest_json(url): - """ - Get the GitHub API JSON response doc for the latest release from URL. - """ + """Get the GitHub API JSON response doc for the latest release from URL.""" content = request_get(url=url) return json.loads(content.decode("utf-8")) @@ -93,27 +87,21 @@ def _check_path(file_path): @staticmethod def _read_json(file_path): - """ - Read the JSON file to a string. - """ + """Read the JSON file to a string.""" _UpdateHandler._check_path(file_path=file_path) with open(file_path, encoding="utf-8") as in_file: return json.load(in_file) @staticmethod def _write_json(file_path, content): - """ - Save the JSON data to a file. - """ + """Save the JSON data to a file.""" with open(file_path, mode="w", encoding="utf-8", newline="\n") as out_file: data = json.dumps(content, indent=2, sort_keys=True) out_file.write(str(data)) @staticmethod def _read_last_check(file_path): - """ - Read the .last_check file. - """ + """Read the .last_check file.""" _UpdateHandler._check_path(file_path=file_path) with open(file_path, encoding="utf-8") as in_file: first_line = in_file.readline() @@ -126,17 +114,13 @@ def _read_last_check(file_path): @staticmethod def _write_last_check(file_path, content): - """ - Write the .last_check file. - """ + """Write the .last_check file.""" with open(file_path, mode="w", encoding="utf-8", newline="\n") as out_file: out_file.write(str(content.strftime(UTC_FMT))) @staticmethod def _check_necessary(update_info, utc_now): - """ - Determine whether a check for the latest version is necessary. - """ + """Determine whether a check for the latest version is necessary.""" if not os.path.exists(update_info.last_check_path): return True elif not os.path.exists(update_info.latest_path): @@ -156,9 +140,7 @@ def _check_necessary(update_info, utc_now): @staticmethod def _get_latest(update_info): - """ - Get the latest release info, either from GitHub or a recent file copy. - """ + """Get the latest release info, either from GitHub or a recent file copy.""" utc_now = datetime.utcnow() if _UpdateHandler._check_necessary(update_info=update_info, utc_now=utc_now): latest = _UpdateHandler._request_latest_json(url=update_info.api_url) @@ -212,9 +194,7 @@ def list(update_info): @staticmethod def _find_download_url(update_info, json_data, file_name): - """ - Find the download URL for the file in the GitHub API JSON response doc. - """ + """Find the download URL for the file in the GitHub API JSON response doc.""" rel_name = json_data["tag_name"] files = json_data["assets"] @@ -241,9 +221,7 @@ def _find_download_url(update_info, json_data, file_name): @staticmethod def _download_file(url, file_path): - """ - Save response content from the URL to a binary file at the file path. - """ + """Save response content from the URL to a binary file at the file path.""" with open(file_path, mode="wb") as out_file: file_data = request_get(url=url) out_file.write(file_data) @@ -327,9 +305,7 @@ def _unzip_extract_file(open_zip_file, zip_item, file_out_path): @staticmethod def _unzip(update_info, file_path, out_path): - """ - Unzip the contents of a zip file to an existing output path. - """ + """Unzip the contents of a zip file to an existing output path.""" _UpdateHandler._check_path(file_path=file_path) _UpdateHandler._check_path(file_path=out_path) bin_paths = _UpdateHandler._get_bin_paths( @@ -349,9 +325,7 @@ def _unzip(update_info, file_path, out_path): @staticmethod def _install(update_info, file_name): - """ - Install the latest release. - """ + """Install the latest release.""" try: latest = _UpdateHandler._get_latest(update_info=update_info) file_path = os.path.join(update_info.bin_new_path, file_name) @@ -581,9 +555,7 @@ def _build_validator_menu(main_subparser, validator_name, updater_instance): def _create_parser(): - """ - Parse command line arguments. - """ + """Parse command line arguments.""" main_title = "pyxform validator updater" epilog = ( "------------------------------------------------------\n" diff --git a/pyxform/validators/util.py b/pyxform/validators/util.py index cfd312f2..5c91121c 100644 --- a/pyxform/validators/util.py +++ b/pyxform/validators/util.py @@ -1,6 +1,4 @@ -""" -The validators utility functions. -""" +"""The validators utility functions.""" import logging import os @@ -103,9 +101,7 @@ def decode_stream(stream): def request_get(url): - """ - Get the response content from URL. - """ + """Get the response content from URL.""" try: if not url.startswith(("http:", "https:")): raise ValueError("URL must start with 'http:' or 'https:'") diff --git a/pyxform/xform2json.py b/pyxform/xform2json.py index f0d09715..35b02087 100644 --- a/pyxform/xform2json.py +++ b/pyxform/xform2json.py @@ -1,6 +1,4 @@ -""" -xform2json module - Transform an XForm to a JSON dictionary. -""" +"""xform2json module - Transform an XForm to a JSON dictionary.""" import copy import json @@ -32,9 +30,7 @@ # {{{ http://code.activestate.com/recipes/573463/ (r7) class XmlDictObject(dict): - """ - Adds object like functionality to the standard dictionary. - """ + """Adds object like functionality to the standard dictionary.""" def __init__(self, initdict=None): if initdict is None: @@ -55,9 +51,7 @@ def __str__(self): @staticmethod def wrap(x): - """ - Static method to wrap a dictionary recursively as an XmlDictObject. - """ + """Static method to wrap a dictionary recursively as an XmlDictObject.""" if isinstance(x, dict): return XmlDictObject((k, XmlDictObject.Wrap(v)) for (k, v) in iter(x.items())) elif isinstance(x, list): @@ -105,9 +99,7 @@ def _convert_dict_to_xml_recurse(parent, dictitem): def convert_dict_to_xml(xmldict): - """ - Converts a dictionary to an XML ElementTree Element. - """ + """Converts a dictionary to an XML ElementTree Element.""" roottag = xmldict.keys()[0] root = Element(roottag) _convert_dict_to_xml_recurse(root, xmldict[roottag]) @@ -157,9 +149,7 @@ def _convert_xml_to_dict_recurse(node, dictclass): def convert_xml_to_dict(root, dictclass=XmlDictObject): - """ - Converts an XML file or ElementTree Element to a dictionary. - """ + """Converts an XML file or ElementTree Element to a dictionary.""" # If a string is passed in, try to open it as a file if isinstance(root, str): root = _try_parse(root) @@ -173,9 +163,7 @@ def convert_xml_to_dict(root, dictclass=XmlDictObject): def _try_parse(root, parser=None): - """ - Try to parse the root from a string or a file/file-like object. - """ + """Try to parse the root from a string or a file/file-like object.""" root = root.encode("UTF-8") try: parsed_root = fromstring(root, parser) @@ -661,9 +649,7 @@ def _get_constraint_msg(self, constraint_msg): return constraint_msg def _get_choices(self) -> dict[str, Any]: - """ - Get all form choices, using the model/instance and model/itext. - """ + """Get all form choices, using the model/instance and model/itext.""" choices = {} for instance in self.secondary_instances: items = [] diff --git a/pyxform/xform_instance_parser.py b/pyxform/xform_instance_parser.py index 427d64c9..19b6f63d 100644 --- a/pyxform/xform_instance_parser.py +++ b/pyxform/xform_instance_parser.py @@ -1,6 +1,4 @@ -""" -XFormInstanceParser class module - parses an instance XML. -""" +"""XFormInstanceParser class module - parses an instance XML.""" # todo: this has been copied from xform_manager, we need to figure out # where this code is actually going to live. @@ -44,9 +42,7 @@ def _xml_node_to_dict(node): def _flatten_dict(d, prefix): - """ - Return a list of XPath, value pairs. - """ + """Return a list of XPath, value pairs.""" if not isinstance(d, dict): raise PyXFormError("""Invalid value for `d`.""") if not isinstance(prefix, list): @@ -75,9 +71,7 @@ def _flatten_dict(d, prefix): def _get_all_attributes(node): - """ - Go through an XML document returning all the attributes we see. - """ + """Go through an XML document returning all the attributes we see.""" if hasattr(node, "hasAttributes") and node.hasAttributes(): for key in node.attributes.keys(): yield key, node.getAttribute(key) diff --git a/pyxform/xls2json.py b/pyxform/xls2json.py index f143b1a5..7736dc4f 100644 --- a/pyxform/xls2json.py +++ b/pyxform/xls2json.py @@ -1,6 +1,4 @@ -""" -A Python script to convert excel files into JSON. -""" +"""A Python script to convert excel files into JSON.""" import os import re @@ -1409,9 +1407,7 @@ def parse_file_to_json( warnings: list[str] | None = None, file_object: IO | None = None, ) -> dict[str, Any]: - """ - A wrapper for workbook_to_json. - """ + """A wrapper for workbook_to_json.""" if warnings is None: warnings = [] workbook_dict = get_xlsform(xlsform=coalesce(path, file_object)) diff --git a/pyxform/xls2json_backends.py b/pyxform/xls2json_backends.py index 669b1565..932f1abd 100644 --- a/pyxform/xls2json_backends.py +++ b/pyxform/xls2json_backends.py @@ -1,6 +1,4 @@ -""" -XLS-to-dict and csv-to-dict are essentially backends for xls2json. -""" +"""XLS-to-dict and csv-to-dict are essentially backends for xls2json.""" import csv import datetime @@ -70,9 +68,7 @@ def _list_to_dict_list(list_items): def trim_trailing_empty(a_list: list, n_empty: int) -> list: - """ - Trim trailing empty columns or rows. Avoids `[:-0] == []`, and unnecessary list copy. - """ + """Trim trailing empty columns or rows. Avoids `[:-0] == []`, and unnecessary list copy.""" if 0 < n_empty: offset = len(a_list) - n_empty a_list = a_list[:offset] @@ -220,9 +216,7 @@ def process_workbook(wb: xlrdBook): def xls_value_to_unicode(value, value_type, datemode) -> str: - """ - Take a xls formatted value and try to make a unicode string representation. - """ + """Take a xls formatted value and try to make a unicode string representation.""" if value_type == XL_CELL_BOOLEAN: return "TRUE" if value else "FALSE" elif value_type == XL_CELL_NUMBER: @@ -313,9 +307,7 @@ def process_workbook(wb: pyxlWorkbook): def xlsx_value_to_str(value) -> str: - """ - Take a xls formatted value and try to make a string representation. - """ + """Take a xls formatted value and try to make a string representation.""" if value is True: return "TRUE" elif value is False: @@ -634,9 +626,7 @@ def process_md_data(md_: str): def md_table_to_workbook(mdstr: str) -> pyxlWorkbook: - """ - Convert Markdown table string to an openpyxl.Workbook. Call wb.save() to persist. - """ + """Convert Markdown table string to an openpyxl.Workbook. Call wb.save() to persist.""" md_data = _md_table_to_ss_structure(mdstr=mdstr) wb = pyxlWorkbook(write_only=True) for key, rows in md_data.items(): diff --git a/pyxform/xls2xform.py b/pyxform/xls2xform.py index d82f1418..0cda374a 100644 --- a/pyxform/xls2xform.py +++ b/pyxform/xls2xform.py @@ -150,9 +150,7 @@ def xls2xform_convert( def _create_parser(): - """ - Parse command line arguments. - """ + """Parse command line arguments.""" parser = argparse.ArgumentParser() parser.add_argument( "path_to_XLSForm", diff --git a/tests/pyxform_test_case.py b/tests/pyxform_test_case.py index 8b2b46a6..c786583f 100644 --- a/tests/pyxform_test_case.py +++ b/tests/pyxform_test_case.py @@ -1,6 +1,4 @@ -""" -PyxformTestCase base class using markdown to define the XLSForm. -""" +"""PyxformTestCase base class using markdown to define the XLSForm.""" import logging import os diff --git a/tests/test_area.py b/tests/test_area.py index 58ebf58f..088145d2 100644 --- a/tests/test_area.py +++ b/tests/test_area.py @@ -1,14 +1,10 @@ -""" -AreaTest - test enclosed-area(geo_shape) calculation. -""" +"""AreaTest - test enclosed-area(geo_shape) calculation.""" from tests.pyxform_test_case import PyxformTestCase class AreaTest(PyxformTestCase): - """ - AreaTest - test enclosed-area(geo_shape) calculation. - """ + """AreaTest - test enclosed-area(geo_shape) calculation.""" def test_area(self): d = ( diff --git a/tests/test_audit.py b/tests/test_audit.py index 651bb5e7..7e8c2cc1 100644 --- a/tests/test_audit.py +++ b/tests/test_audit.py @@ -1,14 +1,10 @@ -""" -AuditTest - test audit question type. -""" +"""AuditTest - test audit question type.""" from tests.pyxform_test_case import PyxformTestCase class AuditTest(PyxformTestCase): - """ - AuditTest - test audit question type. - """ + """AuditTest - test audit question type.""" def test_audit(self): self.assertPyxformXform( diff --git a/tests/test_bind_conversions.py b/tests/test_bind_conversions.py index 86a9b831..9c62d6c4 100644 --- a/tests/test_bind_conversions.py +++ b/tests/test_bind_conversions.py @@ -1,14 +1,10 @@ -""" -BindConversionsTest - test bind conversions. -""" +"""BindConversionsTest - test bind conversions.""" from tests.pyxform_test_case import PyxformTestCase class BindConversionsTest(PyxformTestCase): - """ - BindConversionsTest - test bind conversions. - """ + """BindConversionsTest - test bind conversions.""" def test_bind_readonly_conversion(self): self.assertPyxformXform( diff --git a/tests/test_bug_round_calculation.py b/tests/test_bug_round_calculation.py index 706e0138..07761db5 100644 --- a/tests/test_bug_round_calculation.py +++ b/tests/test_bug_round_calculation.py @@ -1,6 +1,4 @@ -""" -Test round(number, precision) calculation. -""" +"""Test round(number, precision) calculation.""" from tests.pyxform_test_case import PyxformTestCase diff --git a/tests/test_builder.py b/tests/test_builder.py index 540bfe85..b50b3259 100644 --- a/tests/test_builder.py +++ b/tests/test_builder.py @@ -1,6 +1,4 @@ -""" -Test builder module functionality. -""" +"""Test builder module functionality.""" import os import re diff --git a/tests/test_choices_sheet.py b/tests/test_choices_sheet.py index 610b680d..4f8b4e6e 100644 --- a/tests/test_choices_sheet.py +++ b/tests/test_choices_sheet.py @@ -7,9 +7,7 @@ class TestChoicesSheet(PyxformTestCase): def test_numeric_choice_names__for_static_selects__allowed(self): - """ - Test numeric choice names for static selects. - """ + """Test numeric choice names for static selects.""" self.assertPyxformXform( md=""" | survey | | | | @@ -27,9 +25,7 @@ def test_numeric_choice_names__for_static_selects__allowed(self): ) def test_numeric_choice_names__for_dynamic_selects__allowed(self): - """ - Test numeric choice names for dynamic selects. - """ + """Test numeric choice names for dynamic selects.""" self.assertPyxformXform( md=""" | survey | | | | | diff --git a/tests/test_dump_and_load.py b/tests/test_dump_and_load.py index 45702e51..0b63525a 100644 --- a/tests/test_dump_and_load.py +++ b/tests/test_dump_and_load.py @@ -1,6 +1,4 @@ -""" -Test multiple XLSForm can be generated successfully. -""" +"""Test multiple XLSForm can be generated successfully.""" import os from pathlib import Path diff --git a/tests/test_dynamic_default.py b/tests/test_dynamic_default.py index 2dd5a4cb..c36fac97 100644 --- a/tests/test_dynamic_default.py +++ b/tests/test_dynamic_default.py @@ -1,6 +1,4 @@ -""" -Test handling dynamic default in forms. -""" +"""Test handling dynamic default in forms.""" from dataclasses import dataclass from os import getpid @@ -39,9 +37,7 @@ class Case: class XPathHelper: - """ - XPath expressions for dynamic defaults assertions. - """ + """XPath expressions for dynamic defaults assertions.""" @staticmethod def model_setvalue(q_num: int): @@ -150,9 +146,7 @@ def body_select1(q_num: int, choices: tuple[tuple[str, str], ...]): class TestDynamicDefault(PyxformTestCase): - """ - Handling dynamic defaults. - """ + """Handling dynamic defaults.""" def test_static_default_in_repeat(self): """Should use instance repeat template and first row for static default inside a repeat.""" diff --git a/tests/test_external_instances.py b/tests/test_external_instances.py index 8cb5bc90..7c866a0b 100644 --- a/tests/test_external_instances.py +++ b/tests/test_external_instances.py @@ -13,9 +13,7 @@ class ExternalInstanceTests(PyxformTestCase): - """ - External Instance Tests. - """ + """External Instance Tests.""" def test_can__output_single_external_xml_item(self): """Simplest possible example to include an external instance.""" @@ -259,9 +257,7 @@ def test_cannot__use_different_src_same_id__select_then_internal(self): ) def test_cannot__use_different_src_same_id__external_then_pulldata(self): - """ - Duplicate instance from pulldata after xml-external raises an error. - """ + """Duplicate instance from pulldata after xml-external raises an error.""" md = """ | survey | | | | | | | type | name | label | calculation | @@ -285,9 +281,7 @@ def test_cannot__use_different_src_same_id__external_then_pulldata(self): ) def test_cannot__use_different_src_same_id__pulldata_then_external(self): - """ - Duplicate instance from xml-external after pulldata raises an error. - """ + """Duplicate instance from xml-external after pulldata raises an error.""" md = """ | survey | | | | | | | type | name | label | calculation | diff --git a/tests/test_external_instances_for_selects.py b/tests/test_external_instances_for_selects.py index 2c943f9e..39b2cfe3 100644 --- a/tests/test_external_instances_for_selects.py +++ b/tests/test_external_instances_for_selects.py @@ -21,9 +21,7 @@ @dataclass(slots=True) class XPathHelperSelectFromFile: - """ - XPath expressions for translations-related assertions. - """ + """XPath expressions for translations-related assertions.""" q_type: str q_name: str @@ -289,9 +287,7 @@ def test_expected_error_message(self): class TestSelectOneExternal(PyxformTestCase): - """ - select_one_external question type, where external_choices are converted to a CSV. - """ + """select_one_external question type, where external_choices are converted to a CSV.""" all_choices = """ | choices | | | | diff --git a/tests/test_fields.py b/tests/test_fields.py index 2503c822..292aeb7d 100644 --- a/tests/test_fields.py +++ b/tests/test_fields.py @@ -1,6 +1,4 @@ -""" -Test duplicate survey question field name. -""" +"""Test duplicate survey question field name.""" from pyxform import constants as co from pyxform.errors import ErrorCode @@ -9,9 +7,7 @@ class TestQuestionParsing(PyxformTestCase): - """ - Test XLSForm Fields. - """ + """Test XLSForm Fields.""" def test_names__question_basic_case__ok(self): """Should find that a single unique question name is ok.""" diff --git a/tests/test_file.py b/tests/test_file.py index 847f34f2..c1609299 100644 --- a/tests/test_file.py +++ b/tests/test_file.py @@ -1,19 +1,13 @@ -""" -Test file question type. -""" +"""Test file question type.""" from tests.pyxform_test_case import PyxformTestCase class FileWidgetTest(PyxformTestCase): - """ - Test file widget class. - """ + """Test file widget class.""" def test_file_type(self): - """ - Test file question type. - """ + """Test file question type.""" self.assertPyxformXform( name="data", md=""" diff --git a/tests/test_file_utils.py b/tests/test_file_utils.py index 8266235d..08cf6103 100644 --- a/tests/test_file_utils.py +++ b/tests/test_file_utils.py @@ -1,6 +1,4 @@ -""" -Test xls2json_backends util functions. -""" +"""Test xls2json_backends util functions.""" from unittest import TestCase diff --git a/tests/test_form_name.py b/tests/test_form_name.py index c3d9deb2..8b10e596 100644 --- a/tests/test_form_name.py +++ b/tests/test_form_name.py @@ -1,6 +1,4 @@ -""" -Test setting form name to data. -""" +"""Test setting form name to data.""" from tests.pyxform_test_case import PyxformTestCase @@ -24,9 +22,7 @@ def test_default_to_data_when_no_name(self): ) def test_default_to_data(self): - """ - Test using data as the name of the form which will generate . - """ + """Test using data as the name of the form which will generate .""" self.assertPyxformXform( md=""" | survey | | | | @@ -44,9 +40,7 @@ def test_default_to_data(self): ) def test_default_form_name_to_superclass_definition(self): - """ - Test no form_name and setting name field, should use name field. - """ + """Test no form_name and setting name field, should use name field.""" self.assertPyxformXform( md=""" | survey | | | | diff --git a/tests/test_geo.py b/tests/test_geo.py index 081ebf0e..bc022c9b 100644 --- a/tests/test_geo.py +++ b/tests/test_geo.py @@ -61,9 +61,7 @@ def test_gps_alias(self): ) def test_geo_widgets_types(self): - """ - This test could be broken into multiple smaller tests. - """ + """This test could be broken into multiple smaller tests.""" self.assertPyxformXform( name="geos", md=""" diff --git a/tests/test_group.py b/tests/test_group.py index 6730abbe..6e30d65b 100644 --- a/tests/test_group.py +++ b/tests/test_group.py @@ -1,6 +1,4 @@ -""" -Test groups. -""" +"""Test groups.""" from unittest import TestCase @@ -13,9 +11,7 @@ class TestGroupOutput(PyxformTestCase): - """ - Test output for groups. - """ + """Test output for groups.""" def test_group_type(self): self.assertPyxformXform( diff --git a/tests/test_guidance_hint.py b/tests/test_guidance_hint.py index 187f923b..f87177e8 100644 --- a/tests/test_guidance_hint.py +++ b/tests/test_guidance_hint.py @@ -1,6 +1,4 @@ -""" -Guidance hint test module. -""" +"""Guidance hint test module.""" from tests.pyxform_test_case import PyxformTestCase diff --git a/tests/test_image_app_parameter.py b/tests/test_image_app_parameter.py index b6fad7da..1bfe93c8 100644 --- a/tests/test_image_app_parameter.py +++ b/tests/test_image_app_parameter.py @@ -1,6 +1,4 @@ -""" -Test image max-pixels and app parameters. -""" +"""Test image max-pixels and app parameters.""" from pyxform import constants as co from pyxform.errors import ErrorCode diff --git a/tests/test_j2x_creation.py b/tests/test_j2x_creation.py index a20645d9..531c3067 100644 --- a/tests/test_j2x_creation.py +++ b/tests/test_j2x_creation.py @@ -1,6 +1,4 @@ -""" -Testing creation of Surveys using verbose methods. -""" +"""Testing creation of Surveys using verbose methods.""" from unittest import TestCase diff --git a/tests/test_j2x_instantiation.py b/tests/test_j2x_instantiation.py index d7f928bc..35b66e8b 100644 --- a/tests/test_j2x_instantiation.py +++ b/tests/test_j2x_instantiation.py @@ -1,6 +1,4 @@ -""" -Testing the instance object for pyxform. -""" +"""Testing the instance object for pyxform.""" from unittest import TestCase diff --git a/tests/test_j2x_question.py b/tests/test_j2x_question.py index b06d46a7..3a1b4fc7 100644 --- a/tests/test_j2x_question.py +++ b/tests/test_j2x_question.py @@ -1,6 +1,4 @@ -""" -Testing creation of Surveys using verbose methods. -""" +"""Testing creation of Surveys using verbose methods.""" from collections.abc import Generator @@ -145,9 +143,7 @@ def test_select_one_question_multilingual__common_choices(self): ) def test_simple_integer_question_type_multilingual(self): - """ - Not sure how integer questions should show up. - """ + """Not sure how integer questions should show up.""" simple_integer_question = { "label": {"f": "fc", "e": "ec"}, "type": "integer", @@ -171,9 +167,7 @@ def test_simple_integer_question_type_multilingual(self): self.assertEqual(ctw(q.xml_bindings(survey=self.s)), expected_integer_binding_xml) def test_simple_date_question_type_multilingual(self): - """ - Not sure how date questions should show up. - """ + """Not sure how date questions should show up.""" simple_date_question = { "label": {"f": "fd", "e": "ed"}, "type": "date", @@ -195,9 +189,7 @@ def test_simple_date_question_type_multilingual(self): self.assertEqual(ctw(q.xml_bindings(survey=self.s)), expected_date_binding_xml) def test_simple_phone_number_question_type_multilingual(self): - """ - Not sure how phone number questions should show up. - """ + """Not sure how phone number questions should show up.""" simple_phone_number_question = { "label": {"f": "fe", "e": "ee"}, "type": "phone number", @@ -232,9 +224,7 @@ def test_simple_phone_number_question_type_multilingual(self): self.assertDictEqual(expected, observed) def test_simple_select_all_question_multilingual(self): - """ - Not sure how select all questions should show up... - """ + """Not sure how select all questions should show up...""" survey = { "type": "survey", "name": "test_name", @@ -272,9 +262,7 @@ def test_simple_select_all_question_multilingual(self): ) def test_simple_decimal_question_multilingual(self): - """ - Not sure how decimal should show up. - """ + """Not sure how decimal should show up.""" simple_decimal_question = { "label": {"f": "f text", "e": "e text"}, "type": "decimal", diff --git a/tests/test_js2x_import_from_json.py b/tests/test_js2x_import_from_json.py index bc639e64..4ace585d 100644 --- a/tests/test_js2x_import_from_json.py +++ b/tests/test_js2x_import_from_json.py @@ -1,6 +1,4 @@ -""" -Testing our ability to import from a JSON text file. -""" +"""Testing our ability to import from a JSON text file.""" from unittest import TestCase diff --git a/tests/test_json2xform.py b/tests/test_json2xform.py index e5a62aa0..eb6869ab 100644 --- a/tests/test_json2xform.py +++ b/tests/test_json2xform.py @@ -1,6 +1,4 @@ -""" -Testing simple cases for pyxform. -""" +"""Testing simple cases for pyxform.""" from unittest import TestCase diff --git a/tests/test_language_warnings.py b/tests/test_language_warnings.py index 347122ee..270d3810 100644 --- a/tests/test_language_warnings.py +++ b/tests/test_language_warnings.py @@ -1,14 +1,10 @@ -""" -Test language warnings. -""" +"""Test language warnings.""" from tests.pyxform_test_case import PyxformTestCase class LanguageWarningTest(PyxformTestCase): - """ - Test language warnings. - """ + """Test language warnings.""" def test_label_with_valid_subtag_should_not_warn(self): self.assertPyxformXform( diff --git a/tests/test_last_saved.py b/tests/test_last_saved.py index 5e0f6edb..84c3c803 100644 --- a/tests/test_last_saved.py +++ b/tests/test_last_saved.py @@ -1,6 +1,4 @@ -""" -The last-saved virtual instance can be queried to get values from the last saved instance of the form being authored. -""" +"""The last-saved virtual instance can be queried to get values from the last saved instance of the form being authored.""" from pyxform.errors import ErrorCode diff --git a/tests/test_loop.py b/tests/test_loop.py index 48c61401..22c04332 100644 --- a/tests/test_loop.py +++ b/tests/test_loop.py @@ -1,6 +1,4 @@ -""" -Test loop syntax. -""" +"""Test loop syntax.""" from unittest import TestCase diff --git a/tests/test_metadata.py b/tests/test_metadata.py index 25c025eb..8b74ec33 100644 --- a/tests/test_metadata.py +++ b/tests/test_metadata.py @@ -1,6 +1,4 @@ -""" -Test language warnings. -""" +"""Test language warnings.""" from pyxform.errors import ErrorCode @@ -8,9 +6,7 @@ class TestMetadata(PyxformTestCase): - """ - Test metadata and related warnings. - """ + """Test metadata and related warnings.""" def test_metadata_bindings(self): self.assertPyxformXform( diff --git a/tests/test_notes.py b/tests/test_notes.py index ba9fc95c..7da0570a 100644 --- a/tests/test_notes.py +++ b/tests/test_notes.py @@ -1,6 +1,4 @@ -""" -Test the "note" question type. -""" +"""Test the "note" question type.""" from dataclasses import dataclass, field @@ -10,9 +8,7 @@ @dataclass(slots=True) class Case: - """ - A test case spec for note output scenarios. - """ + """A test case spec for note output scenarios.""" label: str match: set[str] diff --git a/tests/test_osm.py b/tests/test_osm.py index 1f52019a..a6f36386 100644 --- a/tests/test_osm.py +++ b/tests/test_osm.py @@ -1,6 +1,4 @@ -""" -Test OSM widgets. -""" +"""Test OSM widgets.""" from tests.pyxform_test_case import PyxformTestCase from tests.xpath_helpers.choices import xpc @@ -19,9 +17,7 @@ class OSMWidgetsTest(PyxformTestCase): - """ - Test OSM widgets. - """ + """Test OSM widgets.""" def test_osm_type(self): self.assertPyxformXform( diff --git a/tests/test_parameters_rows.py b/tests/test_parameters_rows.py index 943cc3fe..2eed427f 100644 --- a/tests/test_parameters_rows.py +++ b/tests/test_parameters_rows.py @@ -1,6 +1,4 @@ -""" -Test text rows parameter. -""" +"""Test text rows parameter.""" from tests.pyxform_test_case import PyxformTestCase diff --git a/tests/test_randomize_itemsets.py b/tests/test_randomize_itemsets.py index e2d97e35..e9dbf25a 100644 --- a/tests/test_randomize_itemsets.py +++ b/tests/test_randomize_itemsets.py @@ -1,6 +1,4 @@ -""" -Test randomize itemsets. -""" +"""Test randomize itemsets.""" from pyxform import constants as co from pyxform.errors import ErrorCode diff --git a/tests/test_rank.py b/tests/test_rank.py index f0ed2b89..d55b8ef7 100644 --- a/tests/test_rank.py +++ b/tests/test_rank.py @@ -1,6 +1,4 @@ -""" -Test rank widget. -""" +"""Test rank widget.""" from tests.pyxform_test_case import PyxformTestCase from tests.xpath_helpers.choices import xpc diff --git a/tests/test_repeat.py b/tests/test_repeat.py index cef933d2..76ac45b8 100644 --- a/tests/test_repeat.py +++ b/tests/test_repeat.py @@ -1,6 +1,4 @@ -""" -Test repeat structure. -""" +"""Test repeat structure.""" from os import getpid from time import perf_counter @@ -16,14 +14,10 @@ class TestRepeatOutput(PyxformTestCase): - """ - Test output for repeats. - """ + """Test output for repeats.""" def test_repeat_relative_reference(self): - """ - Test relative reference in repeats. - """ + """Test relative reference in repeats.""" self.assertPyxformXform( md=""" | survey | | | | | @@ -377,9 +371,7 @@ def test_choice_from_previous_repeat_answers_with_choice_filter(self): ) def test_choice_from_previous_repeat_answers_in_child_repeat(self): - """ - Select one choice from previous repeat answers when within a child of a repeat. - """ + """Select one choice from previous repeat answers when within a child of a repeat.""" xlsform_md = """ | survey | | | | | | | type | name | label | choice_filter | @@ -419,9 +411,7 @@ def test_choice_from_previous_repeat_answers_in_nested_repeat(self): ) def test_choice_from_previous_repeat_answers_in_nested_repeat_uses_current(self): - """ - Select one choices from previous repeat answers within a nested repeat should use current if a sibling node of a select is used. - """ + """Select one choices from previous repeat answers within a nested repeat should use current if a sibling node of a select is used.""" xlsform_md = """ | survey | | | | | | | type | name | label | choice_filter | @@ -678,9 +668,7 @@ def test_indexed_repeat_math_expression_with_double_variable_in_nested_repeat_re def test_repeat_using_select_with_reference_path_in_predicate_uses_current( self, ): - """ - Test relative path expansion using current if reference path is inside a predicate in a survey with select choice list. - """ + """Test relative path expansion using current if reference path is inside a predicate in a survey with select choice list.""" xlsform_md = """ | survey | | | | | | | | type | name | label | choice_filter | calculation | @@ -708,9 +696,7 @@ def test_repeat_using_select_with_reference_path_in_predicate_uses_current( def test_repeat_using_select_uses_current_with_reference_path_in_predicate_and_instance_is_not_first_expression( self, ): - """ - Test relative path expansion using current if reference path is inside a predicate and instance is not first expression in a survey with select choice list. - """ + """Test relative path expansion using current if reference path is inside a predicate and instance is not first expression in a survey with select choice list.""" xlsform_md = """ | survey | | | | | | | | type | name | label | choice_filter | calculation | @@ -738,9 +724,7 @@ def test_repeat_using_select_uses_current_with_reference_path_in_predicate_and_i def test_repeat_and_group_with_reference_path_in_predicate_uses_current( self, ): - """ - Test relative path expansion using current if reference path is inside a predicate in a survey with group. - """ + """Test relative path expansion using current if reference path is inside a predicate in a survey with group.""" xlsform_md = """ | survey | | | | | | | type | name | label | calculation | @@ -763,9 +747,7 @@ def test_repeat_and_group_with_reference_path_in_predicate_uses_current( def test_repeat_with_reference_path_in_predicate_uses_current( self, ): - """ - Test relative path expansion using current if reference path is inside a predicate. - """ + """Test relative path expansion using current if reference path is inside a predicate.""" xlsform_md = """ | survey | | | | | | | type | name | label | calculation | @@ -786,9 +768,7 @@ def test_repeat_with_reference_path_in_predicate_uses_current( def test_repeat_with_reference_path_with_spaces_in_predicate_uses_current( self, ): - """ - Test relative path expansion using current if reference path (with whitespaces before/after an operator of ${name}) is inside a predicate. - """ + """Test relative path expansion using current if reference path (with whitespaces before/after an operator of ${name}) is inside a predicate.""" xlsform_md = """ | survey | | | | | | | type | name | label | calculation | @@ -813,9 +793,7 @@ def test_repeat_with_reference_path_with_spaces_in_predicate_uses_current( def test_repeat_with_reference_path_in_a_method_with_spaces_in_predicate_uses_current( self, ): - """ - Test relative path expansion using current if reference path in a method (with whitespaces before/after an operator of ${name}) is inside apredicate. - """ + """Test relative path expansion using current if reference path in a method (with whitespaces before/after an operator of ${name}) is inside apredicate.""" xlsform_md = """ | survey | | | | | | | type | name | label | calculation | @@ -836,9 +814,7 @@ def test_repeat_with_reference_path_in_a_method_with_spaces_in_predicate_uses_cu def test_repeat_with_reference_path_with_spaces_in_predicate_with_parenthesis_uses_current( self, ): - """ - Test relative path expansion using current if reference path (with whitespaces before/after an operator of ${name}) is inside a predicate with parenthesis. - """ + """Test relative path expansion using current if reference path (with whitespaces before/after an operator of ${name}) is inside a predicate with parenthesis.""" xlsform_md = """ | survey | | | | | | | type | name | label | calculation | @@ -859,9 +835,7 @@ def test_repeat_with_reference_path_with_spaces_in_predicate_with_parenthesis_us def test_relative_path_expansion_not_using_current_if_reference_path_is_predicate_but_not_in_a_repeat( self, ): - """ - Test relative path expansion using xpath without current() if reference path is inside a predicate and not inside a repeat. - """ + """Test relative path expansion using xpath without current() if reference path is inside a predicate and not inside a repeat.""" xlsform_md = """ | survey | | | | | | | type | name | label | calculation | @@ -879,9 +853,7 @@ def test_relative_path_expansion_not_using_current_if_reference_path_is_predicat def test_relative_path_expansion_not_using_current_if_reference_path_is_predicate_but_not_part_of_primary_instance( self, ): - """ - Test relative path expansion using xpath without current() if reference path is inside a predicate but not part of the primary instance. - """ + """Test relative path expansion using xpath without current() if reference path is inside a predicate but not part of the primary instance.""" xlsform_md = """ | survey | | | | | | | type | name | label | calculation | @@ -901,9 +873,7 @@ def test_relative_path_expansion_not_using_current_if_reference_path_is_predicat def test_repeat_with_reference_path_in_multiple_predicate_uses_current( self, ): - """ - Test relative path expansion using current if reference path is in multiple predicate. - """ + """Test relative path expansion using current if reference path is in multiple predicate.""" xlsform_md = """ | survey | | | | | | | type | name | label | calculation | @@ -1072,9 +1042,7 @@ def test_calculation_using_node_from_nested_repeat_has_relative_reference(self): ) def test_repeat_adding_template_and_instance(self): - """ - Repeat should add template and instances. - """ + """Repeat should add template and instances.""" self.assertPyxformXform( md=""" | survey | | | | @@ -1116,9 +1084,7 @@ def test_repeat_adding_template_and_instance(self): ) def test_repeat_adding_template_and_instance_with_group(self): - """ - Repeat should add template and instance even when they are inside grouping. - """ + """Repeat should add template and instance even when they are inside grouping.""" self.assertPyxformXform( md=""" | survey | | | | @@ -1498,9 +1464,7 @@ def test_unlabeled_repeat_fieldlist_alternate_syntax(self): class TestRepeatCount(PyxformTestCase): - """ - Test usages of the survey repeat_count column. - """ + """Test usages of the survey repeat_count column.""" def test_single_reference__generated_element_same_name__ok(self): """Should not have a name clash, the referenced item should be used directly.""" diff --git a/tests/test_search_function.py b/tests/test_search_function.py index acbe2506..396e4db1 100644 --- a/tests/test_search_function.py +++ b/tests/test_search_function.py @@ -245,9 +245,7 @@ def test_search_and_select_xlsx(self): class TestSecondaryInstances(PyxformTestCase): - """ - Test behaviour of the search() appearance with other sources of secondary instances. - """ + """Test behaviour of the search() appearance with other sources of secondary instances.""" @classmethod def setUpClass(cls) -> None: diff --git a/tests/test_secondary_instance_translations.py b/tests/test_secondary_instance_translations.py index df83879d..35858614 100644 --- a/tests/test_secondary_instance_translations.py +++ b/tests/test_secondary_instance_translations.py @@ -1,6 +1,4 @@ -""" -Testing inlining translation when no translation is specified. -""" +"""Testing inlining translation when no translation is specified.""" from tests.pyxform_test_case import PyxformTestCase from tests.xpath_helpers.choices import xpc @@ -42,9 +40,7 @@ def test_inline_translations(self): ) def test_multiple_translations(self): - """ - Dynamic choice with potential translation should generate itext fields. - """ + """Dynamic choice with potential translation should generate itext fields.""" self.assertPyxformXform( md=""" | survey | | | | | @@ -76,9 +72,7 @@ def test_multiple_translations(self): def test_select_with_media_and_choice_filter_and_no_translations_generates_media( self, ): - """ - Selects with media and choice filter should generate itext fields for the media. - """ + """Selects with media and choice filter should generate itext fields for the media.""" md = """ | survey | | | | | | | type | name | label | choice_filter | @@ -118,9 +112,7 @@ def test_select_with_media_and_choice_filter_and_no_translations_generates_media def test_select_with_choice_filter_and_translations_generates_single_translation( self, ): - """ - Selects with choice filter and translations should only have a single itext entry. - """ + """Selects with choice filter and translations should only have a single itext entry.""" xform_md = """ | survey | | | | | | | type | name | label | choice_filter | @@ -148,9 +140,7 @@ def test_select_with_choice_filter_and_translations_generates_single_translation def test_select_with_dynamic_option_label__and_choice_filter__and_no_translations__generates_itext( self, ): - """ - A select with a choice filter and no translations in which the first option label is dynamic should generate itext for choice labels. - """ + """A select with a choice filter and no translations in which the first option label is dynamic should generate itext for choice labels.""" xform_md = """ | survey | | | | | | | | type | name | label | choice_filter | default | @@ -177,9 +167,7 @@ def test_select_with_dynamic_option_label__and_choice_filter__and_no_translation def test_select_with_dynamic_option_label_for_second_choice__and_choice_filter__and_no_translations__generates_itext( self, ): - """ - A select with a choice filter and no translations in which the second option label is dynamic should generate itext for choice labels. - """ + """A select with a choice filter and no translations in which the second option label is dynamic should generate itext for choice labels.""" xform_md = """ | survey | | | | | | | | type | name | label | choice_filter | default | @@ -211,9 +199,7 @@ def test_select_with_dynamic_option_label_for_second_choice__and_choice_filter__ def test_select_with_dynamic_option_label__and_choice_filter__and_no_translations__maintains_additional_columns( self, ): - """ - A select with a choice filter and no translations in which the first option label is dynamic should maintain data columns. - """ + """A select with a choice filter and no translations in which the first option label is dynamic should maintain data columns.""" xform_md = """ | survey | | | | | | | | type | name | label | choice_filter | default | @@ -232,9 +218,7 @@ def test_select_with_dynamic_option_label__and_choice_filter__and_no_translation def test_select_with_dynamic_option_label__and_no_choice_filter__and_no_translations__inlines_output( self, ): - """ - A select without a choice filter and no translations in which the first option label is dynamic should not use itext. - """ + """A select without a choice filter and no translations in which the first option label is dynamic should not use itext.""" md = """ | survey | | | | | | type | name | label | diff --git a/tests/test_set_geopoint.py b/tests/test_set_geopoint.py index c52196ed..68469c6e 100644 --- a/tests/test_set_geopoint.py +++ b/tests/test_set_geopoint.py @@ -1,6 +1,4 @@ -""" -Test setgeopoint widget. -""" +"""Test setgeopoint widget.""" from tests.pyxform_test_case import PyxformTestCase diff --git a/tests/test_settings.py b/tests/test_settings.py index c954901c..f83bbef8 100644 --- a/tests/test_settings.py +++ b/tests/test_settings.py @@ -358,9 +358,7 @@ def test_instance_id__can_be_used_as_reference_variable__error(self): class TestNamespaces(PyxformTestCase): - """ - Test namespaces, for the XForm and in relation to settings that can be namespaced. - """ + """Test namespaces, for the XForm and in relation to settings that can be namespaced.""" def test_standard_namespaces(self): """Should find the standard namespaces in the XForm output.""" diff --git a/tests/test_settings_auto_send_delete.py b/tests/test_settings_auto_send_delete.py index eec4a7c7..58e8e13c 100644 --- a/tests/test_settings_auto_send_delete.py +++ b/tests/test_settings_auto_send_delete.py @@ -1,6 +1,4 @@ -""" -Test settins auto settings. -""" +"""Test settins auto settings.""" from tests.pyxform_test_case import PyxformTestCase diff --git a/tests/test_sheet_columns.py b/tests/test_sheet_columns.py index 3d55b2cd..4a6c3fa8 100644 --- a/tests/test_sheet_columns.py +++ b/tests/test_sheet_columns.py @@ -1,6 +1,4 @@ -""" -Test XLSForm sheet names. -""" +"""Test XLSForm sheet names.""" from collections.abc import Container from dataclasses import dataclass @@ -65,14 +63,10 @@ def test_form_id_variant(self): class TestSurveyColumns(PyxformTestCase): - """ - Invalid survey column tests. - """ + """Invalid survey column tests.""" def test_missing_name(self): - """ - Every question needs a name (or alias of name). - """ + """Every question needs a name (or alias of name).""" self.assertPyxformXform( name="invalidcols", ss_structure={"survey": [{"type": "text", "label": "label"}]}, @@ -144,9 +138,7 @@ def test_media_column__is_ignored(self): ) def test_column_case(self): - """ - Ensure that column name is case insensitive. - """ + """Ensure that column name is case insensitive.""" self.assertPyxformXform( md=""" | Survey | | | | @@ -204,15 +196,11 @@ def test_missing_survey_headers(self): class TestChoicesColumns(PyxformTestCase): - """ - Invalid choice sheet column tests. - """ + """Invalid choice sheet column tests.""" @staticmethod def _simple_choice_ss(choice_sheet=None): - """ - Return simple choices sheet. - """ + """Return simple choices sheet.""" if choice_sheet is None: choice_sheet = [] return { @@ -227,9 +215,7 @@ def _simple_choice_ss(choice_sheet=None): } def test_valid_choices_sheet_passes(self): - """ - Test invalid choices sheet passes. - """ + """Test invalid choices sheet passes.""" self.assertPyxformXform( name="valid_choices", ss_structure=self._simple_choice_ss( @@ -241,9 +227,7 @@ def test_valid_choices_sheet_passes(self): ) def test_invalid_choices_sheet_fails(self): - """ - Test invalid choices sheet fails. - """ + """Test invalid choices sheet fails.""" self.assertPyxformXform( name="missing_name", ss_structure=self._simple_choice_ss( @@ -259,9 +243,7 @@ def test_invalid_choices_sheet_fails(self): ) def test_missing_list_name(self): - """ - Test missing sheet name. - """ + """Test missing sheet name.""" self.assertPyxformXform( name="missing_list_name", ss_structure=self._simple_choice_ss( @@ -293,14 +275,10 @@ def test_missing_choice_headers(self): class TestColumnAliases(PyxformTestCase): - """ - Aliases Tests. - """ + """Aliases Tests.""" def test_value_and_name(self): - """ - Confirm that both 'name' and 'value' columns of choice list work. - """ + """Confirm that both 'name' and 'value' columns of choice list work.""" md = """ | survey | | | | | | type | name | label | diff --git a/tests/test_sms.py b/tests/test_sms.py index 6bfa434b..c501255d 100644 --- a/tests/test_sms.py +++ b/tests/test_sms.py @@ -1,6 +1,4 @@ -""" -Test sms syntax. -""" +"""Test sms syntax.""" from tests.pyxform_test_case import PyxformTestCase diff --git a/tests/test_survey.py b/tests/test_survey.py index ceb036b8..6ac7af3f 100644 --- a/tests/test_survey.py +++ b/tests/test_survey.py @@ -11,9 +11,7 @@ class TestSurvey(PyxformTestCase): - """ - Tests for the Survey class. - """ + """Tests for the Survey class.""" def test_many_xpath_references_do_not_hit_64_recursion_limit__one_to_one(self): """Should be able to pipe a question into one note more than 64 times.""" @@ -195,9 +193,7 @@ def build_survey_from_path_spec( class TestGetPathRelativeToLCAR(TestCase): - """ - Tests of `pyxform.survey.get_path_relative_to_lcar`. - """ + """Tests of `pyxform.survey.get_path_relative_to_lcar`.""" def assert_relative_path( self, diff --git a/tests/test_translations.py b/tests/test_translations.py index ed536719..5510a914 100644 --- a/tests/test_translations.py +++ b/tests/test_translations.py @@ -1,6 +1,4 @@ -""" -Test translations syntax. -""" +"""Test translations syntax.""" from dataclasses import dataclass from os import getpid @@ -26,9 +24,7 @@ @dataclass(slots=True) class XPathHelper: - """ - XPath expressions for translations-related assertions. - """ + """XPath expressions for translations-related assertions.""" question_type: str question_name: str diff --git a/tests/test_trigger.py b/tests/test_trigger.py index 1c069039..b02c9158 100644 --- a/tests/test_trigger.py +++ b/tests/test_trigger.py @@ -1,6 +1,4 @@ -""" -Test handling setvalue of 'trigger' column in forms. -""" +"""Test handling setvalue of 'trigger' column in forms.""" from itertools import product diff --git a/tests/test_tutorial_xls.py b/tests/test_tutorial_xls.py index f57264d8..89e60a13 100644 --- a/tests/test_tutorial_xls.py +++ b/tests/test_tutorial_xls.py @@ -1,6 +1,4 @@ -""" -Test tutorial XLSForm. -""" +"""Test tutorial XLSForm.""" from unittest import TestCase diff --git a/tests/test_unicode_rtl.py b/tests/test_unicode_rtl.py index 664159bc..b7771d08 100644 --- a/tests/test_unicode_rtl.py +++ b/tests/test_unicode_rtl.py @@ -1,6 +1,4 @@ -""" -Test unicode rtl in XLSForms. -""" +"""Test unicode rtl in XLSForms.""" from tests.pyxform_test_case import PyxformTestCase diff --git a/tests/test_upload_question.py b/tests/test_upload_question.py index aa971255..623daaf7 100644 --- a/tests/test_upload_question.py +++ b/tests/test_upload_question.py @@ -1,6 +1,4 @@ -""" -Test upload (image, audio, file) question types in XLSForm. -""" +"""Test upload (image, audio, file) question types in XLSForm.""" from tests.pyxform_test_case import PyxformTestCase diff --git a/tests/test_validate_unicode_exception.py b/tests/test_validate_unicode_exception.py index 4648f0cd..d71b106a 100644 --- a/tests/test_validate_unicode_exception.py +++ b/tests/test_validate_unicode_exception.py @@ -1,6 +1,4 @@ -""" -Test unicode characters in validate error messages. -""" +"""Test unicode characters in validate error messages.""" from tests.pyxform_test_case import PyxformTestCase diff --git a/tests/test_validator_update.py b/tests/test_validator_update.py index 5aeb0283..a1be1cf5 100644 --- a/tests/test_validator_update.py +++ b/tests/test_validator_update.py @@ -1,6 +1,4 @@ -""" -Test validator update cli command. -""" +"""Test validator update cli command.""" import os import platform diff --git a/tests/test_validator_util.py b/tests/test_validator_util.py index 550ab0c0..31fb7711 100644 --- a/tests/test_validator_util.py +++ b/tests/test_validator_util.py @@ -1,6 +1,4 @@ -""" -Test pyxform.validators.utils module. -""" +"""Test pyxform.validators.utils module.""" import os from unittest import TestCase diff --git a/tests/test_validators.py b/tests/test_validators.py index bc832815..58328ee0 100644 --- a/tests/test_validators.py +++ b/tests/test_validators.py @@ -1,6 +1,4 @@ -""" -Test validators. -""" +"""Test validators.""" from unittest import TestCase from unittest.mock import patch diff --git a/tests/test_warnings.py b/tests/test_warnings.py index 470efd53..a099ead3 100644 --- a/tests/test_warnings.py +++ b/tests/test_warnings.py @@ -1,6 +1,4 @@ -""" -Test warnings. -""" +"""Test warnings.""" from tests.pyxform_test_case import PyxformTestCase diff --git a/tests/test_whitespace.py b/tests/test_whitespace.py index 29750455..e7baa6ef 100644 --- a/tests/test_whitespace.py +++ b/tests/test_whitespace.py @@ -1,6 +1,4 @@ -""" -Test whitespace around output variables in XForms. -""" +"""Test whitespace around output variables in XForms.""" from tests.pyxform_test_case import PyxformTestCase diff --git a/tests/test_xform2json.py b/tests/test_xform2json.py index e3d09738..d474c71f 100644 --- a/tests/test_xform2json.py +++ b/tests/test_xform2json.py @@ -1,6 +1,4 @@ -""" -Test xform2json module. -""" +"""Test xform2json module.""" import json import os @@ -105,9 +103,7 @@ def test_try_parse_with_bad_file(self): class TestXForm2JSON(PyxformTestCase): - """ - Test xform2json module. - """ + """Test xform2json module.""" def test_convert_toJSON_multi_language(self): """ diff --git a/tests/test_xls2json_backends.py b/tests/test_xls2json_backends.py index 45f39648..32d4432f 100644 --- a/tests/test_xls2json_backends.py +++ b/tests/test_xls2json_backends.py @@ -1,6 +1,4 @@ -""" -Test xls2json_backends module functionality. -""" +"""Test xls2json_backends module functionality.""" import datetime import os @@ -28,9 +26,7 @@ class TestXLS2JSONBackends(PyxformTestCase): - """ - Test xls2json_backends module. - """ + """Test xls2json_backends module.""" maxDiff = None diff --git a/tests/test_xls2json_xls.py b/tests/test_xls2json_xls.py index 5e0e6853..fab479bd 100644 --- a/tests/test_xls2json_xls.py +++ b/tests/test_xls2json_xls.py @@ -1,6 +1,4 @@ -""" -Testing simple cases for Xls2Json. -""" +"""Testing simple cases for Xls2Json.""" import json from pathlib import Path @@ -104,9 +102,7 @@ def test_text_and_integer(self): self.assertEqual(x.to_json_dict()["children"], expected_dict) def test_choice_filter_choice_fields(self): - """ - Test that the choice filter fields appear on children field of json. - """ + """Test that the choice filter fields appear on children field of json.""" choice_filter_survey = SurveyReader( utils.path_to_text_fixture("choice_filter_test.xlsx"), default_name="choice_filter_test", diff --git a/tests/test_xls2xform.py b/tests/test_xls2xform.py index ceb53244..de0b67a0 100644 --- a/tests/test_xls2xform.py +++ b/tests/test_xls2xform.py @@ -1,6 +1,4 @@ -""" -Test xls2xform module. -""" +"""Test xls2xform module.""" # The Django application xls2xform uses the function # pyxform.create_survey. We have a test here to make sure no one @@ -220,9 +218,7 @@ def test_xls2xform_convert_params_with_flags(self, converter_mock, parser_mock_a ), ) def test_xls2xform_convert_throwing_odk_error(self, parser_mock_args): - """ - Parse and validate bad_calc.xlsx. - """ + """Parse and validate bad_calc.xlsx.""" logger = logging.getLogger("pyxform.xls2xform") with mock.patch.object(logger, "error") as mock_debug: main_cli() @@ -240,9 +236,7 @@ def test_get_xml_path_function(self): class TestXLS2XFormConvert(TestCase): - """ - Tests for `xls2xform_convert`. - """ + """Tests for `xls2xform_convert`.""" def test_xls2xform_convert__ok(self): """Should find the expected output files for the conversion.""" @@ -276,9 +270,7 @@ def test_xls2xform_convert__ok(self): class TestXLS2XFormConvertAPI(TestCase): - """ - Tests for the `convert` library API entrypoint (not xls2xform_convert). - """ + """Tests for the `convert` library API entrypoint (not xls2xform_convert).""" @staticmethod def with_xlsform_path_str(**kwargs): diff --git a/tests/utils.py b/tests/utils.py index 8a940570..f6cec54b 100644 --- a/tests/utils.py +++ b/tests/utils.py @@ -1,6 +1,4 @@ -""" -The tests utils module functionality. -""" +"""The tests utils module functionality.""" import configparser import os @@ -79,9 +77,7 @@ def get_temp_dir(): def truncate_temp_files(temp_dir): - """ - Truncate files in a folder, recursing into directories. - """ + """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. @@ -97,9 +93,7 @@ def truncate_temp_files(temp_dir): def cleanup_pyxform_temp_files(prefix: str): - """ - Try to clean up temp pyxform files from previous test runs. - """ + """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): diff --git a/tests/xform_test_case/test_bugs.py b/tests/xform_test_case/test_bugs.py index 8dcf9c3d..cf117d2d 100644 --- a/tests/xform_test_case/test_bugs.py +++ b/tests/xform_test_case/test_bugs.py @@ -1,6 +1,4 @@ -""" -Some tests for the new (v0.9) spec is properly implemented. -""" +"""Some tests for the new (v0.9) spec is properly implemented.""" import os from pathlib import Path diff --git a/tests/xform_test_case/test_xform_conversion.py b/tests/xform_test_case/test_xform_conversion.py index 8e67451e..62e7587e 100644 --- a/tests/xform_test_case/test_xform_conversion.py +++ b/tests/xform_test_case/test_xform_conversion.py @@ -1,6 +1,4 @@ -""" -Some tests for the new (v0.9) spec is properly implemented. -""" +"""Some tests for the new (v0.9) spec is properly implemented.""" from pathlib import Path diff --git a/tests/xform_test_case/test_xml.py b/tests/xform_test_case/test_xml.py index c7374bfa..4d52e32c 100644 --- a/tests/xform_test_case/test_xml.py +++ b/tests/xform_test_case/test_xml.py @@ -1,6 +1,4 @@ -""" -Test XForm XML syntax. -""" +"""Test XForm XML syntax.""" from sys import version_info from unittest import TestCase diff --git a/tests/xpath_helpers/choices.py b/tests/xpath_helpers/choices.py index b3990714..66bb4ccb 100644 --- a/tests/xpath_helpers/choices.py +++ b/tests/xpath_helpers/choices.py @@ -7,9 +7,7 @@ class XPathHelper: - """ - XPath expressions for choices assertions. - """ + """XPath expressions for choices assertions.""" @staticmethod def model_instance_choices_label(cname: str, choices: tuple[tuple[str, str], ...]): diff --git a/tests/xpath_helpers/entities.py b/tests/xpath_helpers/entities.py index 08c5966a..a96c2522 100644 --- a/tests/xpath_helpers/entities.py +++ b/tests/xpath_helpers/entities.py @@ -1,7 +1,5 @@ class XPathHelper: - """ - XPath expressions for entities assertions. - """ + """XPath expressions for entities assertions.""" @staticmethod def model_entities_version(version: str): diff --git a/tests/xpath_helpers/group.py b/tests/xpath_helpers/group.py index a7292952..db637381 100644 --- a/tests/xpath_helpers/group.py +++ b/tests/xpath_helpers/group.py @@ -1,7 +1,5 @@ class XPathHelper: - """ - XPath expressions for settings assertions. - """ + """XPath expressions for settings assertions.""" @staticmethod def group_no_label(ref: str) -> str: diff --git a/tests/xpath_helpers/questions.py b/tests/xpath_helpers/questions.py index c1a66611..612f93f5 100644 --- a/tests/xpath_helpers/questions.py +++ b/tests/xpath_helpers/questions.py @@ -2,9 +2,7 @@ class XPathHelper: - """ - XPath expressions for questions assertions. - """ + """XPath expressions for questions assertions.""" @staticmethod def model_instance_exists(i_id: str) -> str: diff --git a/tests/xpath_helpers/settings.py b/tests/xpath_helpers/settings.py index 498fe413..e42de686 100644 --- a/tests/xpath_helpers/settings.py +++ b/tests/xpath_helpers/settings.py @@ -1,7 +1,5 @@ class XPathHelper: - """ - XPath expressions for settings assertions. - """ + """XPath expressions for settings assertions.""" @staticmethod def form_title(value: str) -> str: From 95e8dc586e4e9ec629ea625c9f75f4e0360bee85 Mon Sep 17 00:00:00 2001 From: lindsay stevens Date: Mon, 13 Jul 2026 21:39:32 +1000 Subject: [PATCH 12/50] dev: add and apply pydocstyle rule D400 - the full pydocstyle ruleset has many changes so enabling some of them individually to keep the diffs manageable. --- pyproject.toml | 1 + pyxform/parsing/expression.py | 2 +- pyxform/section.py | 1 - pyxform/validators/pyxform/pyxform_reference.py | 2 +- 4 files changed, 3 insertions(+), 3 deletions(-) diff --git a/pyproject.toml b/pyproject.toml index b55d7b91..1cd7f841 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -59,6 +59,7 @@ select = [ "D202", # pydocstyle (blank-line-after-function) "D209", # pydocstyle (new-line-after-last-paragraph) "D301", # pydocstyle (escape-sequence-in-docstring) + "D400", # pydocstyle (missing-trailing-period) "D403", # pydocstyle (first-word-uncapitalized) "D415", # pydocstyle (missing-terminal-punctuation) "E", # pycodestyle error diff --git a/pyxform/parsing/expression.py b/pyxform/parsing/expression.py index 5d960b66..c43a5dac 100644 --- a/pyxform/parsing/expression.py +++ b/pyxform/parsing/expression.py @@ -118,7 +118,7 @@ def parse_expression(text: str) -> tuple[Token, ...]: def is_xml_tag(value: str) -> bool: - """Does the input string contain only a valid XML tag / element name?""" + """Check if the input string contains only a valid XML tag / element name.""" return value and bool(RE_NCNAME_NAMESPACED.fullmatch(value)) diff --git a/pyxform/section.py b/pyxform/section.py index e81843d8..1b710cc6 100644 --- a/pyxform/section.py +++ b/pyxform/section.py @@ -222,7 +222,6 @@ def __init__( def xml_control(self, survey: "Survey"): """ Example - ``` diff --git a/pyxform/validators/pyxform/pyxform_reference.py b/pyxform/validators/pyxform/pyxform_reference.py index 0e623b41..484a81fe 100644 --- a/pyxform/validators/pyxform/pyxform_reference.py +++ b/pyxform/validators/pyxform/pyxform_reference.py @@ -37,7 +37,7 @@ def __init__(self, name: str, last_saved: bool = False): def is_pyxform_reference_candidate(value: str) -> bool: """ - Does the string look like a pyxform reference? + Check if the string looks like a pyxform reference. Needs 2 characters for "${", plus at least 1 more for a name inside. Does not look for closing brace because full parsing will try to detect malformed references. This From 754016ded1336a69084708832e23667a0a461005 Mon Sep 17 00:00:00 2001 From: lindsay stevens Date: Mon, 13 Jul 2026 21:47:55 +1000 Subject: [PATCH 13/50] dev: invert pydocstyle rule selection to ignore those to defer - The previous few commits selectively enabled the pydocstyle rules that resulted in changes that seemed reasonable to accept now. - The rules now in the ignore list are ones that would require a lot of manual rewrites (most of them have dozens of hits). - So this rearrangement enables other potentially useful pydocstyle rules that aren't currently triggering changes or warnings. --- pyproject.toml | 17 ++++++++++------- 1 file changed, 10 insertions(+), 7 deletions(-) diff --git a/pyproject.toml b/pyproject.toml index 1cd7f841..934d2d0d 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -55,13 +55,7 @@ src = ["pyxform", "tests"] select = [ "B", # flake8-bugbear "C4", # flake8-comprehensions - "D200", # pydocstyle (unnecessary-multiline-docstring) - "D202", # pydocstyle (blank-line-after-function) - "D209", # pydocstyle (new-line-after-last-paragraph) - "D301", # pydocstyle (escape-sequence-in-docstring) - "D400", # pydocstyle (missing-trailing-period) - "D403", # pydocstyle (first-word-uncapitalized) - "D415", # pydocstyle (missing-terminal-punctuation) + "D", # pydocstyle "E", # pycodestyle error # "ERA", # eradicate (commented out code) "F", # pyflakes @@ -81,6 +75,15 @@ select = [ "W", # pycodestyle warning ] ignore = [ + "D100", # undocumented-public-module (many instances requiring manual resolution) + "D101", # undocumented-public-class (many instances requiring manual resolution) + "D102", # undocumented-public-method (many instances requiring manual resolution) + "D103", # undocumented-public-function (many instances requiring manual resolution) + "D104", # undocumented-public-package (many instances requiring manual resolution) + "D105", # undocumented-magic-method (many instances requiring manual resolution) + "D107", # undocumented-public-init (many instances requiring manual resolution) + "D205", # missing-blank-line-after-summary (many instances requiring manual resolution) + "D401", # non-imperative-mood (many instances requiring manual resolution) "E501", # line-too-long (we have a lot of long strings) "F821", # undefined-name (doesn't work well with type hints, ruff 0.15.21). "PERF401", # manual-list-comprehension (false positives on selective transforms) From 9fa8262557020916deb31315194193ec361d8f51 Mon Sep 17 00:00:00 2001 From: lindsay stevens Date: Mon, 13 Jul 2026 21:54:53 +1000 Subject: [PATCH 14/50] dev: add and apply ruff ruleset "flynt" - `flynt is a command line tool to automatically convert a project's Python code from old "%-formatted" and .format(...) strings into Python 3.6+'s "f-strings".` --- pyproject.toml | 1 + pyxform/xform2json.py | 2 +- tests/test_dynamic_default.py | 4 ++-- tests/test_repeat.py | 2 +- tests/test_translations.py | 2 +- 5 files changed, 6 insertions(+), 5 deletions(-) diff --git a/pyproject.toml b/pyproject.toml index 934d2d0d..fcb67904 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -59,6 +59,7 @@ select = [ "E", # pycodestyle error # "ERA", # eradicate (commented out code) "F", # pyflakes + "FLY", # flynt "FURB", # refurb "I", # isort "PERF", # perflint diff --git a/pyxform/xform2json.py b/pyxform/xform2json.py index 35b02087..bb4d0b34 100644 --- a/pyxform/xform2json.py +++ b/pyxform/xform2json.py @@ -639,7 +639,7 @@ def _get_text_from_translation(self, ref, key="label"): def _get_bracketed_name(self, ref): name = self._get_name_from_ref(ref) - return "".join(["${", name.strip(), "}"]) + return f"${{{name.strip()}}}" def _get_constraint_msg(self, constraint_msg): if isinstance(constraint_msg, str): diff --git a/tests/test_dynamic_default.py b/tests/test_dynamic_default.py index c36fac97..fc6db9cb 100644 --- a/tests/test_dynamic_default.py +++ b/tests/test_dynamic_default.py @@ -918,7 +918,7 @@ def test_dynamic_default_performance__time(self): process = Process(getpid()) for count in (500, 1000, 2000, 5000, 10000): questions = "\n".join(question.format(i=i) for i in range(count)) - md = "".join((survey_header, questions)) + md = f"{survey_header}{questions}" def run(name, case): runs = 0 @@ -958,7 +958,7 @@ def test_dynamic_default_performance__memory(self): | | text | q{i} | Q{i} | if(../t2 = 'test', 1, 2) + 15 - int(1.2) | """ questions = "\n".join(question.format(i=i) for i in range(1, 2000)) - md = "".join((survey_header, questions)) + md = f"{survey_header}{questions}" process = Process(getpid()) pre_mem = process.memory_info().rss self.assertPyxformXform(md=md) diff --git a/tests/test_repeat.py b/tests/test_repeat.py index 76ac45b8..46333089 100644 --- a/tests/test_repeat.py +++ b/tests/test_repeat.py @@ -996,7 +996,7 @@ def test_check_performance__relative_reference(self): process = Process(getpid()) for count in (500, 1000, 2000, 5000, 10000): questions = "\n".join(question.format(i=i) for i in range(count)) - md = "".join((survey_header, questions, survey_footer)) + md = f"{survey_header}{questions}{survey_footer}" def run(name, case): runs = 0 diff --git a/tests/test_translations.py b/tests/test_translations.py index 5510a914..d0d354ed 100644 --- a/tests/test_translations.py +++ b/tests/test_translations.py @@ -405,7 +405,7 @@ def test_missing_translations_check_performance(self): for count in (500, 1000, 2000, 5000, 10000): questions = "\n".join(question.format(i=i) for i in range(count)) choice_lists = "\n".join(choice_list.format(i=i) for i in range(count)) - md = "".join((survey_header, questions, choices_header, choice_lists)) + md = f"{survey_header}{questions}{choices_header}{choice_lists}" def run(name, case): runs = 0 From 0154ffedd9bc28b0df29b81e4a54e37b740ec46c Mon Sep 17 00:00:00 2001 From: lindsay stevens Date: Mon, 13 Jul 2026 22:36:55 +1000 Subject: [PATCH 15/50] dev: rename readme from .rst to .md - conversion result in next commit so that file history is kept. --- README.rst => README.md | 0 pyproject.toml | 2 +- 2 files changed, 1 insertion(+), 1 deletion(-) rename README.rst => README.md (100%) diff --git a/README.rst b/README.md similarity index 100% rename from README.rst rename to README.md diff --git a/pyproject.toml b/pyproject.toml index fcb67904..2bc9ba8c 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -5,7 +5,7 @@ authors = [ {name = "github.com/xlsform", email = "support@getodk.org"}, ] description = "A Python package to create XForms for ODK Collect." -readme = "README.rst" +readme = "README.md" requires-python = ">=3.11" dependencies = [ "xlrd==2.0.1", # Read XLS files From 15206bb06584c3400cfcd9593177085183c368d5 Mon Sep 17 00:00:00 2001 From: lindsay stevens Date: Mon, 13 Jul 2026 22:37:37 +1000 Subject: [PATCH 16/50] dev: convert readme from .rst to .md with pandoc v3.10 - used `pandoc --wrap=preserve -f rst -t gfm README.rst -o README.md` - markdown easier to remember and maintain - fixed grammatical error L112 "in a easily navigable format" - removed badges since this info is available on the same page as the readme (either GitHub or PyPI) and so one less thing to update. --- README.md | 223 ++++++++++++++++++++++++++---------------------------- 1 file changed, 108 insertions(+), 115 deletions(-) diff --git a/README.md b/README.md index be26a1a0..0f003fee 100644 --- a/README.md +++ b/README.md @@ -1,57 +1,46 @@ -======== -pyxform -======== +# pyxform -|pypi| |python| +`pyxform` is a Python library that simplifies writing forms for ODK Collect and Enketo by converting spreadsheets that follow the [XLSForm standard](http://xlsform.org/) into [ODK XForms](https://github.com/opendatakit/xforms-spec). The XLSForms format is used in a [number of tools](http://xlsform.org/en/#tools-that-support-xlsforms). -.. |pypi| image:: https://badge.fury.io/py/pyxform.svg - :target: https://badge.fury.io/py/pyxform +## Project status -.. |python| image:: https://img.shields.io/badge/python-3.11,3.12,3.13,3.14-blue.svg - :target: https://www.python.org/downloads +`pyxform` is actively maintained by [ODK](https://getodk.org/about/team.html). -``pyxform`` is a Python library that simplifies writing forms for ODK Collect and Enketo by converting spreadsheets that follow the `XLSForm standard `_ into `ODK XForms `_. The XLSForms format is used in a `number of tools `_. +Current goals for the project include: -Project status -=============== -``pyxform`` is actively maintained by `ODK `_. +- Enable more complex workflows through sophisticated XPath expressions and [entities](https://getodk.github.io/xforms-spec/entities) +- Improve error messages and make troubleshooting easier +- Improve experience, particularly for multi-language forms -Current goals for the project include: +`pyxform` was started at the [Sustainable Engineering Lab at Columbia University](https://qsel.columbia.edu/), and until 2018 was maintained primarily by [Ona](https://ona.io/). -* Enable more complex workflows through sophisticated XPath expressions and `entities `_ -* Improve error messages and make troubleshooting easier -* Improve experience, particularly for multi-language forms +## Using `pyxform` -``pyxform`` was started at the `Sustainable Engineering Lab at Columbia University `_, and until 2018 was maintained primarily by `Ona `_. +For user support, please start by posting to [the ODK forum](https://forum.getodk.org/c/support/6) where your question will get the most visibility. -Using ``pyxform`` -================== -For user support, please start by posting to `the ODK forum `__ where your question will get the most visibility. +There are 3 main ways that `pyxform` is used: -There are 3 main ways that ``pyxform`` is used: +- Through a form server, such as the [pyxform-http service wrapper](https://github.com/getodk/pyxform-http), or [ODK Central](https://docs.getodk.org/getting-started/). +- The command line utility `xls2xform`, which can be helpful for troubleshooting or as part of a broader form creation pipeline. +- As a library, meaning that another python project imports functionality from `pyxform`. -* Through a form server, such as the `pyxform-http service wrapper `_, or `ODK Central `_. -* The command line utility ``xls2xform``, which can be helpful for troubleshooting or as part of a broader form creation pipeline. -* As a library, meaning that another python project imports functionality from ``pyxform``. +### Running the latest release of pyxform -Running the latest release of pyxform -------------------------------------- -To convert forms at the command line, the latest official release of pyxform can be installed using `pip `_:: +To convert forms at the command line, the latest official release of pyxform can be installed using [pip](https://en.wikipedia.org/wiki/Pip_(package_manager)): pip install pyxform -The ``xls2xform`` command can then be used:: +The `xls2xform` command can then be used: xls2xform path_to_XLSForm [output_path] -The currently supported Python versions for ``pyxform`` are 3.11 to 3.14 (the primary development version is 3.13). If this is different from the version you use for other projects, consider using `pyenv `_ to manage multiple versions of Python. +The currently supported Python versions for `pyxform` are 3.11 to 3.14 (the primary development version is 3.13). If this is different from the version you use for other projects, consider using [pyenv](https://github.com/pyenv/pyenv) to manage multiple versions of Python. -Running pyxform from local source ---------------------------------- +### Running pyxform from local source -Note that you must uninstall any globally installed ``pyxform`` instance in order to use local modules. Please install java 8 or newer version. +Note that you must uninstall any globally installed `pyxform` instance in order to use local modules. Please install java 8 or newer version. -From the command line, complete the following. These steps use a virtualenv to make dependency management easier, and to keep the global site-packages directory clean:: +From the command line, complete the following. These steps use a virtualenv to make dependency management easier, and to keep the global site-packages directory clean: # Get a copy of the repository. mkdir -P ~/repos/pyxform @@ -70,7 +59,7 @@ From the command line, complete the following. These steps use a virtualenv to m (venv)$ xls2xform --help # same effect as previous line (venv)$ which xls2xform # ~/repos/pyxform/venv/bin/xls2xform -To leave and return to the virtualenv:: +To leave and return to the virtualenv: (venv)$ deactivate # leave the venv, scripts not on $PATH $ xls2xform --help @@ -79,104 +68,108 @@ To leave and return to the virtualenv:: (venv)$ which xls2xform # scripts available on $PATH again ~/repos/pyxform/venv/bin/xls2xform -Installing pyxform from remote source -------------------------------------- -``pip`` can install from the GitHub repository. Only do this if you want to install from the master branch, which is likely to have pre-release code. To install the latest release, see above.:: +### Installing pyxform from remote source + +`pip` can install from the GitHub repository. Only do this if you want to install from the master branch, which is likely to have pre-release code. To install the latest release, see above.: pip install git+https://github.com/XLSForm/pyxform.git@master#egg=pyxform -You can then run xls2xform from the commandline:: +You can then run xls2xform from the commandline: xls2xform path_to_XLSForm [output_path] -Development -=========== -To set up for development / contributing, first complete the above steps for "Running pyxform from local source". Then repeat the command used to install pyxform, but with ``[dev]`` appended to the end, e.g.:: +## Development + +To set up for development / contributing, first complete the above steps for "Running pyxform from local source". Then repeat the command used to install pyxform, but with `[dev]` appended to the end, e.g.: pip install -e .[dev] -You can run tests with:: +You can run tests with: python -m unittest -Before committing, make sure to format and lint the code using ``ruff``:: +Before committing, make sure to format and lint the code using `ruff`: ruff format pyxform tests ruff check pyxform tests -If you are using a copy of ``ruff`` outside your virtualenv, make sure it is the same version as listed in ``pyproject.toml``. Use the project configuration for ``ruff`` in ``pyproject.toml``, which occurs automatically if ``ruff`` is run from the project root (where ``pyproject.toml`` is). +If you are using a copy of `ruff` outside your virtualenv, make sure it is the same version as listed in `pyproject.toml`. Use the project configuration for `ruff` in `pyproject.toml`, which occurs automatically if `ruff` is run from the project root (where `pyproject.toml` is). + +### Contributions + +We welcome contributions that have a clearly-stated goal and are tightly focused. In general, successful contributions will first be discussed on [the ODK forum](https://forum.getodk.org/) or in an issue. We prefer discussion threads on the ODK forum because `pyxform` issues generally involve considerations for other tools and specifications in ODK and its broader ecosystem. Opening up an issue or a pull request directly may be appropriate if there is a clear bug or an issue that only affects `pyxform` developers. + +### Writing tests + +Make sure to include tests for the changes you're working on. When writing new tests you should add them in `tests` folder. Add to an existing test module, or create a new test module. Test modules are named after the corresponding source file, or if the tests concern many files then module name is the topic or feature under test. -Contributions -------------- -We welcome contributions that have a clearly-stated goal and are tightly focused. In general, successful contributions will first be discussed on `the ODK forum `__ or in an issue. We prefer discussion threads on the ODK forum because ``pyxform`` issues generally involve considerations for other tools and specifications in ODK and its broader ecosystem. Opening up an issue or a pull request directly may be appropriate if there is a clear bug or an issue that only affects ``pyxform`` developers. +When creating new test cases, where possible use `PyxformTestCase` as a base class instead of `unittest.TestCase`. The `PyxformTestCase` is a toolkit for writing XLSForms as MarkDown tables, compiling example XLSForms, and making assertions on the resulting XForm. This makes code review much easier by putting the XLSForm content inline with the test, instead of in a separate file. A `unittest.TestCase` may be used if the new tests do not involve compiling an XLSForm (but most will). Do not add new tests using the old style `XFormTestCase`. -Writing tests -------------- -Make sure to include tests for the changes you're working on. When writing new tests you should add them in ``tests`` folder. Add to an existing test module, or create a new test module. Test modules are named after the corresponding source file, or if the tests concern many files then module name is the topic or feature under test. +When writing new `PyxformTestCase` tests that make content assertions, it is strongly recommended that the `xml__xpath*` matchers are used, in particular `xml__xpath_match`. Most older tests use matchers like `xml__contains` and `xml__excludes`, which are simple string matches of XML snippets against the result XForm. The `xml__xpath_match` kwarg accepts an XPath expression and expects 1 match. The main benefits of using XPath are 1) it allows specifying a document location, and 2) it does not require a particular document order for elements or attributes or whitespace output. To take full advantage of 1), the XPath expressions should specify the full document path (e.g. `/h:html/h:head/x:model`) rather than a search (e.g. `.//x:model`). To take full advantage of 2), the expression should include element predicates that specify the expected attribute values, e.g. `/h:html/h:body/x:input[@ref='/trigger-column/a']`. To specify the absence of an element, an expression like the following may be used with `xml__xpath_match`: `/h:html[not(descendant::x:input)]`, or alternatively `xml__xpath_count`: `.//x:input` with an expected count of 0 (zero). -When creating new test cases, where possible use ``PyxformTestCase`` as a base class instead of ``unittest.TestCase``. The ``PyxformTestCase`` is a toolkit for writing XLSForms as MarkDown tables, compiling example XLSForms, and making assertions on the resulting XForm. This makes code review much easier by putting the XLSForm content inline with the test, instead of in a separate file. A ``unittest.TestCase`` may be used if the new tests do not involve compiling an XLSForm (but most will). Do not add new tests using the old style ``XFormTestCase``. +## Documentation -When writing new ``PyxformTestCase`` tests that make content assertions, it is strongly recommended that the ``xml__xpath*`` matchers are used, in particular ``xml__xpath_match``. Most older tests use matchers like ``xml__contains`` and ``xml__excludes``, which are simple string matches of XML snippets against the result XForm. The ``xml__xpath_match`` kwarg accepts an XPath expression and expects 1 match. The main benefits of using XPath are 1) it allows specifying a document location, and 2) it does not require a particular document order for elements or attributes or whitespace output. To take full advantage of 1), the XPath expressions should specify the full document path (e.g. ``/h:html/h:head/x:model``) rather than a search (e.g. ``.//x:model``). To take full advantage of 2), the expression should include element predicates that specify the expected attribute values, e.g. ``/h:html/h:body/x:input[@ref='/trigger-column/a']``. To specify the absence of an element, an expression like the following may be used with ``xml__xpath_match``: ``/h:html[not(descendant::x:input)]``, or alternatively ``xml__xpath_count``: ``.//x:input`` with an expected count of 0 (zero). +For developers, `pyxform` uses docstrings, type annotations, and test cases. Most modern IDEs can display docstrings and type annotations in an easily navigable format, so no additional docs are compiled (e.g. sphinx). In addition to the user documentation, developers should be familiar with the ODK XForms Specification https://getodk.github.io/xforms-spec/. -Documentation -============= -For developers, ``pyxform`` uses docstrings, type annotations, and test cases. Most modern IDEs can display docstrings and type annotations in a easily navigable format, so no additional docs are compiled (e.g. sphinx). In addition to the user documentation, developers should be familiar with the `ODK XForms Specification https://getodk.github.io/xforms-spec/`. +For users, `pyxform` has documentation at the following locations: -For users, ``pyxform`` has documentation at the following locations: +- [XLSForm docs](https://xlsform.org/) +- [XLSForm template](https://docs.google.com/spreadsheets/d/1v9Bumt3R0vCOGEKQI6ExUf2-8T72-XXp_CbKKTACuko/edit#gid=1052905058) +- [ODK Docs](https://docs.getodk.org/) -* `XLSForm docs `_ -* `XLSForm template `_ -* `ODK Docs `_ +## Change Log -Change Log -========== -`Changelog `_ +[Changelog](CHANGES.txt) -Releasing pyxform -================= +## Releasing pyxform -1. Make sure the version of ODK Validate in the repo is up-to-date:: +1. Make sure the version of ODK Validate in the repo is up-to-date: - pyxform_validator_update odk update ODK-Validate-vx.x.x.jar + pyxform_validator_update odk update ODK-Validate-vx.x.x.jar -2. Run all tests through ODK Validate as follows: +2. Run all tests through ODK Validate as follows: - PYXFORM_TESTS_RUN_ODK_VALIDATE=true python -m unittest --verbose + > PYXFORM_TESTS_RUN_ODK_VALIDATE=true python -m unittest --verbose -3. Draft a new GitHub release with the list of merged PRs. Follow the title and description pattern of the previous release. -4. Checkout a release branch from latest upstream master. -5. Update ``CHANGES.txt`` with the text of the draft release. -6. Update ``pyproject.toml``, ``pyxform/__init__.py`` with the new release version number. -7. Commit, push the branch, and initiate a pull request. Wait for tests to pass, then merge the PR. -8. Tag the release and it will automatically be published +3. Draft a new GitHub release with the list of merged PRs. Follow the title and description pattern of the previous release. + +4. Checkout a release branch from latest upstream master. + +5. Update `CHANGES.txt` with the text of the draft release. + +6. Update `pyproject.toml`, `pyxform/__init__.py` with the new release version number. + +7. Commit, push the branch, and initiate a pull request. Wait for tests to pass, then merge the PR. + +8. Tag the release and it will automatically be published + +## Manually releasing -Manually releasing -=================== Releases are now automatic. These instructions are provided for forks or for a future change in process. -1. In a clean new release only directory, check out master. -2. Create a new virtualenv in this directory to ensure a clean Python environment:: +1. In a clean new release only directory, check out master. + +2. Create a new virtualenv in this directory to ensure a clean Python environment: - /usr/local/bin/python -m venv pyxform-release - . pyxform-release/bin/activate + /usr/local/bin/python -m venv pyxform-release + . pyxform-release/bin/activate -3. Install the production and packaging requirements:: +3. Install the production and packaging requirements: - pip install -e . - pip install flit==3.12.0 + pip install -e . + pip install flit==3.12.0 -4. Clean up build and dist folders:: +4. Clean up build and dist folders: - rm -rf build dist pyxform.egg-info + rm -rf build dist pyxform.egg-info -5. Prepare ``sdist`` and ``bdist_wheel`` distributions, and publish to PyPI:: +5. Prepare `sdist` and `bdist_wheel` distributions, and publish to PyPI: - flit --debug publish --no-use-vcs + flit --debug publish --no-use-vcs -6. Tag the GitHub release and publish it. +6. Tag the GitHub release and publish it. -Related projects -================ +## Related projects These projects are not vetted or endorsed but are linked here for reference. @@ -184,34 +177,34 @@ These projects are not vetted or endorsed but are linked here for reference. *To XLSForm* -* `cueform `_ (Go): from CUE -* `md2xlsform `_ (Python): from MarkDown -* `xlsform `_ (Python): from JSON -* `yxf `_ (Python): from YAML +- [cueform](https://github.com/freddieptf/cueform) (Go): from CUE +- [md2xlsform](https://github.com/joshuaberetta/md2xlsform) (Python): from MarkDown +- [xlsform](https://github.com/networkearth/xlsform) (Python): from JSON +- [yxf](https://github.com/Sjlver/yxf) (Python): from YAML *From XLSForm* -* `ODK2Doc `_ (R): to Word -* `OdkGraph `_ (Python): to a graph -* `Pureser `_ (Swift): to HTML -* `ppp `_ (Python): to HTML, PDF, Word -* `QuestionnaireHTML `_ (R): to HTML -* `xlsform-converter `_ (Python): to Django modules -* `xlsform `_ (Python): to JSON -* `xlsform2json `_ (Java): to JSON -* `XLSform2PDF `_ (Python): to PDF -* `xlson `_ (Python): to OpenSRP JSON -* `yxf `_ (Python): to YAML +- [ODK2Doc](https://github.com/zaeendesouza/ODK2Doc) (R): to Word +- [OdkGraph](https://github.com/jkpr/OdkGraph) (Python): to a graph +- [Pureser](https://github.com/SwissTPH/Pureser) (Swift): to HTML +- [ppp](https://github.com/pmaengineering/ppp) (Python): to HTML, PDF, Word +- [QuestionnaireHTML](https://github.com/hedibmustapha/QuestionnaireHTML) (R): to HTML +- [xlsform-converter](https://github.com/wq/xlsform-converter) (Python): to Django modules +- [xlsform](https://github.com/networkearth/xlsform) (Python): to JSON +- [xlsform2json](https://github.com/owengrant/xlsform2json) (Java): to JSON +- [XLSform2PDF](https://github.com/HEDERA-PLATFORM/XLSform2PDF) (Python): to PDF +- [xlson](https://github.com/opensrp/xlson) (Python): to OpenSRP JSON +- [yxf](https://github.com/Sjlver/yxf) (Python): to YAML **Management Tools** -* `surveydesignr `_ (R): compare XLSForms -* `ipacheckscto `_ (Stata): check XLSForm for errors or design issues -* `kobocruncher `_ (R): generate analysis Rmd from XLSForm -* `odkmeta `_ (Stata): use XLSForm to import ODK data to Stata -* `odktools `_ (C++): convert pyxform internal data model to MySQL -* `pmix `_ (Python): manage XLSForm authoring -* `pyxform-docker `_ (Dockerfile): image for pyxform development -* `xform-test `_ (Java): test XLSForms -* `xlsformpo `_ (Python): use .po files for XLSForm translations -* `XlsFormUtil `_ (R): manage XLSForm authoring +- [surveydesignr](https://github.com/williameoswald/surveydesignr) (R): compare XLSForms +- [ipacheckscto](https://github.com/PovertyAction/ipacheckscto) (Stata): check XLSForm for errors or design issues +- [kobocruncher](https://github.com/Edouard-Legoupil/kobocruncher) (R): generate analysis Rmd from XLSForm +- [odkmeta](https://github.com/PovertyAction/odkmeta) (Stata): use XLSForm to import ODK data to Stata +- [odktools](https://github.com/ilri/odktools) (C++): convert pyxform internal data model to MySQL +- [pmix](https://github.com/pmaengineering/pmix) (Python): manage XLSForm authoring +- [pyxform-docker](https://github.com/seadowg/pyxform-docker) (Dockerfile): image for pyxform development +- [xform-test](https://github.com/PMA-2020/xform-test) (Java): test XLSForms +- [xlsformpo](https://github.com/delcroip/xlsformpo) (Python): use .po files for XLSForm translations +- [XlsFormUtil](https://github.com/unhcr-americas/XlsFormUtil) (R): manage XLSForm authoring From b5ba83d9db90bd46b7d399f2f8ff69ddcf3f6955 Mon Sep 17 00:00:00 2001 From: lindsay stevens Date: Tue, 14 Jul 2026 01:53:36 +1000 Subject: [PATCH 17/50] chg: move tests *xls folders to /fixtures/*forms - most of the files already are not xls anyway. - if they are being renamed already then it is a good time to move them into the fixtures directory so that their role is clearer. --- pyxform/xls2json.py | 11 +++++--- tests/fixtures/__init__.py | 0 .../ODKValidateWarnings.xlsx | Bin .../UCL_Biomass_Plot_Form.xlsx | Bin .../bug_example_forms}/__init__.py | 0 .../bug_example_forms}/bad_calc.xlsx | Bin .../badly_named_choices_sheet.xls | Bin .../bug_example_forms}/blank_second_row.xls | Bin .../calculate_without_calculation.xls | Bin .../bug_example_forms}/duplicate_columns.xlsx | Bin .../bug_example_forms}/excel_with_macros.xlsm | Bin .../bug_example_forms}/extra_columns.xls | Bin .../bug_example_forms}/group_name_test.xls | Bin .../bug_example_forms}/ict_survey_fails.xls | Bin .../spaces_in_choices_header.xls | Bin .../bug_example_forms}/xl_date_ambiguous.xlsx | Bin .../xl_date_ambiguous_v1.xlsx | Bin .../example_forms}/README.rst | 0 .../example_forms}/__init__.py | 0 .../allow_comment_rows_test.xls | Bin .../attribute_columns_test.xlsx | Bin .../example_forms}/bad_calc.xlsx | Bin .../example_forms}/calculate.xls | Bin .../cascading_select_test_equivalent.xls | Bin .../example_forms}/case_insensitivity.csv | 0 .../example_forms}/case_insensitivity.md | 0 .../example_forms}/case_insensitivity.xls | Bin .../example_forms}/case_insensitivity.xlsx | Bin .../example_forms}/choice_filter_test.xlsx | Bin .../example_forms}/choice_name_as_type.xls | Bin .../choice_name_same_as_select_name.xls | Bin .../example_forms}/default_time_demo.xls | Bin .../example_forms}/extra_columns.xlsx | Bin .../example_forms}/extra_sheet_names.xlsx | Bin .../example_forms}/field-list.xlsx | Bin .../example_forms}/flat_xlsform_test.xlsx | Bin .../example_forms}/fruits.csv | 0 .../example_forms}/gps.csv | 0 .../example_forms}/gps.xls | Bin .../example_forms}/group.csv | 0 .../example_forms}/group.md | 0 .../example_forms}/group.xls | Bin .../example_forms}/group.xlsx | Bin .../example_forms}/hidden.xls | Bin .../example_forms}/include.csv | 0 .../example_forms}/include.md | 0 .../example_forms}/include.xls | Bin .../example_forms}/include.xlsx | Bin .../example_forms}/include_json.csv | 0 .../example_forms}/include_json.md | 0 .../example_forms}/include_json.xls | Bin .../example_forms}/include_json.xlsx | Bin .../example_forms}/loop.csv | 0 .../example_forms}/loop.md | 0 .../example_forms}/loop.xls | Bin .../example_forms}/loop.xlsx | Bin .../example_forms}/or_other.xlsx | Bin .../example_forms}/pull_data.xlsx | Bin .../example_forms}/repeat_date_test.xls | Bin .../example_forms}/simple_loop.csv | 0 .../example_forms}/simple_loop.xls | Bin .../example_forms}/sms_info.xls | Bin .../spec_test_expected_output.xml | 0 .../example_forms}/specify_other.csv | 0 .../example_forms}/specify_other.md | 0 .../example_forms}/specify_other.xls | Bin .../example_forms}/specify_other.xlsx | Bin .../example_forms}/style_settings.xls | Bin .../example_forms}/survey_no_name.xlsx | Bin .../example_forms}/table-list.xls | Bin .../example_forms}/text_and_integer.csv | 0 .../example_forms}/text_and_integer.md | 0 .../example_forms}/text_and_integer.xls | Bin .../example_forms}/text_and_integer.xlsx | Bin .../example_forms}/tutorial.xls | Bin .../example_forms}/unknown_question_type.xls | Bin .../example_forms}/utf_csv.csv | 0 .../example_forms}/widgets-media/a.jpg | Bin .../example_forms}/widgets-media/b.jpg | Bin .../example_forms}/widgets-media/happy.jpg | Bin .../example_forms}/widgets-media/img_test.jpg | Bin .../example_forms}/widgets-media/sad.jpg | Bin .../example_forms}/widgets.csv | 0 .../example_forms}/widgets.xls | Bin .../example_forms}/widgets.xml | 0 .../example_forms}/xlsform_spec_test.xlsx | Bin .../example_forms}/xml_escaping.xls | Bin .../example_forms}/yes_or_no_question.csv | 0 .../example_forms}/yes_or_no_question.md | 0 .../example_forms}/yes_or_no_question.xls | Bin .../example_forms}/yes_or_no_question.xlsx | Bin tests/test_xls2json.py | 9 ++++--- tests/test_xls2json_backends.py | 7 ++--- tests/test_xls2json_xls.py | 5 ++-- tests/test_xls2xform.py | 22 +++++++-------- tests/utils.py | 4 +-- tests/xform_test_case/base.py | 5 ++-- tests/xform_test_case/test_bugs.py | 25 ++++++++++-------- 98 files changed, 50 insertions(+), 38 deletions(-) create mode 100644 tests/fixtures/__init__.py rename tests/{bug_example_xls => fixtures/bug_example_forms}/ODKValidateWarnings.xlsx (100%) rename tests/{bug_example_xls => fixtures/bug_example_forms}/UCL_Biomass_Plot_Form.xlsx (100%) rename tests/{bug_example_xls => fixtures/bug_example_forms}/__init__.py (100%) rename tests/{bug_example_xls => fixtures/bug_example_forms}/bad_calc.xlsx (100%) rename tests/{bug_example_xls => fixtures/bug_example_forms}/badly_named_choices_sheet.xls (100%) rename tests/{bug_example_xls => fixtures/bug_example_forms}/blank_second_row.xls (100%) rename tests/{bug_example_xls => fixtures/bug_example_forms}/calculate_without_calculation.xls (100%) rename tests/{bug_example_xls => fixtures/bug_example_forms}/duplicate_columns.xlsx (100%) rename tests/{bug_example_xls => fixtures/bug_example_forms}/excel_with_macros.xlsm (100%) rename tests/{bug_example_xls => fixtures/bug_example_forms}/extra_columns.xls (100%) rename tests/{bug_example_xls => fixtures/bug_example_forms}/group_name_test.xls (100%) rename tests/{bug_example_xls => fixtures/bug_example_forms}/ict_survey_fails.xls (100%) rename tests/{bug_example_xls => fixtures/bug_example_forms}/spaces_in_choices_header.xls (100%) rename tests/{bug_example_xls => fixtures/bug_example_forms}/xl_date_ambiguous.xlsx (100%) rename tests/{bug_example_xls => fixtures/bug_example_forms}/xl_date_ambiguous_v1.xlsx (100%) rename tests/{example_xls => fixtures/example_forms}/README.rst (100%) rename tests/{example_xls => fixtures/example_forms}/__init__.py (100%) rename tests/{example_xls => fixtures/example_forms}/allow_comment_rows_test.xls (100%) rename tests/{example_xls => fixtures/example_forms}/attribute_columns_test.xlsx (100%) rename tests/{example_xls => fixtures/example_forms}/bad_calc.xlsx (100%) rename tests/{example_xls => fixtures/example_forms}/calculate.xls (100%) rename tests/{example_xls => fixtures/example_forms}/cascading_select_test_equivalent.xls (100%) rename tests/{example_xls => fixtures/example_forms}/case_insensitivity.csv (100%) rename tests/{example_xls => fixtures/example_forms}/case_insensitivity.md (100%) rename tests/{example_xls => fixtures/example_forms}/case_insensitivity.xls (100%) rename tests/{example_xls => fixtures/example_forms}/case_insensitivity.xlsx (100%) rename tests/{example_xls => fixtures/example_forms}/choice_filter_test.xlsx (100%) rename tests/{example_xls => fixtures/example_forms}/choice_name_as_type.xls (100%) rename tests/{example_xls => fixtures/example_forms}/choice_name_same_as_select_name.xls (100%) rename tests/{example_xls => fixtures/example_forms}/default_time_demo.xls (100%) rename tests/{example_xls => fixtures/example_forms}/extra_columns.xlsx (100%) rename tests/{example_xls => fixtures/example_forms}/extra_sheet_names.xlsx (100%) rename tests/{example_xls => fixtures/example_forms}/field-list.xlsx (100%) rename tests/{example_xls => fixtures/example_forms}/flat_xlsform_test.xlsx (100%) rename tests/{example_xls => fixtures/example_forms}/fruits.csv (100%) rename tests/{example_xls => fixtures/example_forms}/gps.csv (100%) rename tests/{example_xls => fixtures/example_forms}/gps.xls (100%) rename tests/{example_xls => fixtures/example_forms}/group.csv (100%) rename tests/{example_xls => fixtures/example_forms}/group.md (100%) rename tests/{example_xls => fixtures/example_forms}/group.xls (100%) rename tests/{example_xls => fixtures/example_forms}/group.xlsx (100%) rename tests/{example_xls => fixtures/example_forms}/hidden.xls (100%) rename tests/{example_xls => fixtures/example_forms}/include.csv (100%) rename tests/{example_xls => fixtures/example_forms}/include.md (100%) rename tests/{example_xls => fixtures/example_forms}/include.xls (100%) rename tests/{example_xls => fixtures/example_forms}/include.xlsx (100%) rename tests/{example_xls => fixtures/example_forms}/include_json.csv (100%) rename tests/{example_xls => fixtures/example_forms}/include_json.md (100%) rename tests/{example_xls => fixtures/example_forms}/include_json.xls (100%) rename tests/{example_xls => fixtures/example_forms}/include_json.xlsx (100%) rename tests/{example_xls => fixtures/example_forms}/loop.csv (100%) rename tests/{example_xls => fixtures/example_forms}/loop.md (100%) rename tests/{example_xls => fixtures/example_forms}/loop.xls (100%) rename tests/{example_xls => fixtures/example_forms}/loop.xlsx (100%) rename tests/{example_xls => fixtures/example_forms}/or_other.xlsx (100%) rename tests/{example_xls => fixtures/example_forms}/pull_data.xlsx (100%) rename tests/{example_xls => fixtures/example_forms}/repeat_date_test.xls (100%) rename tests/{example_xls => fixtures/example_forms}/simple_loop.csv (100%) rename tests/{example_xls => fixtures/example_forms}/simple_loop.xls (100%) rename tests/{example_xls => fixtures/example_forms}/sms_info.xls (100%) rename tests/{example_xls => fixtures/example_forms}/spec_test_expected_output.xml (100%) rename tests/{example_xls => fixtures/example_forms}/specify_other.csv (100%) rename tests/{example_xls => fixtures/example_forms}/specify_other.md (100%) rename tests/{example_xls => fixtures/example_forms}/specify_other.xls (100%) rename tests/{example_xls => fixtures/example_forms}/specify_other.xlsx (100%) rename tests/{example_xls => fixtures/example_forms}/style_settings.xls (100%) rename tests/{example_xls => fixtures/example_forms}/survey_no_name.xlsx (100%) rename tests/{example_xls => fixtures/example_forms}/table-list.xls (100%) rename tests/{example_xls => fixtures/example_forms}/text_and_integer.csv (100%) rename tests/{example_xls => fixtures/example_forms}/text_and_integer.md (100%) rename tests/{example_xls => fixtures/example_forms}/text_and_integer.xls (100%) rename tests/{example_xls => fixtures/example_forms}/text_and_integer.xlsx (100%) rename tests/{example_xls => fixtures/example_forms}/tutorial.xls (100%) mode change 100755 => 100644 rename tests/{example_xls => fixtures/example_forms}/unknown_question_type.xls (100%) rename tests/{example_xls => fixtures/example_forms}/utf_csv.csv (100%) rename tests/{example_xls => fixtures/example_forms}/widgets-media/a.jpg (100%) mode change 100755 => 100644 rename tests/{example_xls => fixtures/example_forms}/widgets-media/b.jpg (100%) mode change 100755 => 100644 rename tests/{example_xls => fixtures/example_forms}/widgets-media/happy.jpg (100%) mode change 100755 => 100644 rename tests/{example_xls => fixtures/example_forms}/widgets-media/img_test.jpg (100%) mode change 100755 => 100644 rename tests/{example_xls => fixtures/example_forms}/widgets-media/sad.jpg (100%) mode change 100755 => 100644 rename tests/{example_xls => fixtures/example_forms}/widgets.csv (100%) rename tests/{example_xls => fixtures/example_forms}/widgets.xls (100%) rename tests/{example_xls => fixtures/example_forms}/widgets.xml (100%) rename tests/{example_xls => fixtures/example_forms}/xlsform_spec_test.xlsx (100%) rename tests/{example_xls => fixtures/example_forms}/xml_escaping.xls (100%) rename tests/{example_xls => fixtures/example_forms}/yes_or_no_question.csv (100%) rename tests/{example_xls => fixtures/example_forms}/yes_or_no_question.md (100%) rename tests/{example_xls => fixtures/example_forms}/yes_or_no_question.xls (100%) rename tests/{example_xls => fixtures/example_forms}/yes_or_no_question.xlsx (100%) diff --git a/pyxform/xls2json.py b/pyxform/xls2json.py index 7736dc4f..db2373e5 100644 --- a/pyxform/xls2json.py +++ b/pyxform/xls2json.py @@ -4,6 +4,7 @@ import re import sys from collections import Counter +from pathlib import Path from typing import IO, Any from pyxform import aliases, constants @@ -1477,9 +1478,13 @@ def print_warning_log(self, warn_out_file): # convert that file to json, then print it if len(sys.argv) < 2: # print "You must supply a file argument." - _filename = "xlsform_spec_test.xls" - _path = "/home/user/python-dev/xlsform/pyxform/tests/example_xls/" - _path += _filename + _path = str( + Path(__file__).parent.parent + / "tests" + / "fixtures" + / "example_forms" + / "xlsform_spec_test.xlsx" + ) else: _path = sys.argv[1] diff --git a/tests/fixtures/__init__.py b/tests/fixtures/__init__.py new file mode 100644 index 00000000..e69de29b diff --git a/tests/bug_example_xls/ODKValidateWarnings.xlsx b/tests/fixtures/bug_example_forms/ODKValidateWarnings.xlsx similarity index 100% rename from tests/bug_example_xls/ODKValidateWarnings.xlsx rename to tests/fixtures/bug_example_forms/ODKValidateWarnings.xlsx diff --git a/tests/bug_example_xls/UCL_Biomass_Plot_Form.xlsx b/tests/fixtures/bug_example_forms/UCL_Biomass_Plot_Form.xlsx similarity index 100% rename from tests/bug_example_xls/UCL_Biomass_Plot_Form.xlsx rename to tests/fixtures/bug_example_forms/UCL_Biomass_Plot_Form.xlsx diff --git a/tests/bug_example_xls/__init__.py b/tests/fixtures/bug_example_forms/__init__.py similarity index 100% rename from tests/bug_example_xls/__init__.py rename to tests/fixtures/bug_example_forms/__init__.py diff --git a/tests/bug_example_xls/bad_calc.xlsx b/tests/fixtures/bug_example_forms/bad_calc.xlsx similarity index 100% rename from tests/bug_example_xls/bad_calc.xlsx rename to tests/fixtures/bug_example_forms/bad_calc.xlsx diff --git a/tests/bug_example_xls/badly_named_choices_sheet.xls b/tests/fixtures/bug_example_forms/badly_named_choices_sheet.xls similarity index 100% rename from tests/bug_example_xls/badly_named_choices_sheet.xls rename to tests/fixtures/bug_example_forms/badly_named_choices_sheet.xls diff --git a/tests/bug_example_xls/blank_second_row.xls b/tests/fixtures/bug_example_forms/blank_second_row.xls similarity index 100% rename from tests/bug_example_xls/blank_second_row.xls rename to tests/fixtures/bug_example_forms/blank_second_row.xls diff --git a/tests/bug_example_xls/calculate_without_calculation.xls b/tests/fixtures/bug_example_forms/calculate_without_calculation.xls similarity index 100% rename from tests/bug_example_xls/calculate_without_calculation.xls rename to tests/fixtures/bug_example_forms/calculate_without_calculation.xls diff --git a/tests/bug_example_xls/duplicate_columns.xlsx b/tests/fixtures/bug_example_forms/duplicate_columns.xlsx similarity index 100% rename from tests/bug_example_xls/duplicate_columns.xlsx rename to tests/fixtures/bug_example_forms/duplicate_columns.xlsx diff --git a/tests/bug_example_xls/excel_with_macros.xlsm b/tests/fixtures/bug_example_forms/excel_with_macros.xlsm similarity index 100% rename from tests/bug_example_xls/excel_with_macros.xlsm rename to tests/fixtures/bug_example_forms/excel_with_macros.xlsm diff --git a/tests/bug_example_xls/extra_columns.xls b/tests/fixtures/bug_example_forms/extra_columns.xls similarity index 100% rename from tests/bug_example_xls/extra_columns.xls rename to tests/fixtures/bug_example_forms/extra_columns.xls diff --git a/tests/bug_example_xls/group_name_test.xls b/tests/fixtures/bug_example_forms/group_name_test.xls similarity index 100% rename from tests/bug_example_xls/group_name_test.xls rename to tests/fixtures/bug_example_forms/group_name_test.xls diff --git a/tests/bug_example_xls/ict_survey_fails.xls b/tests/fixtures/bug_example_forms/ict_survey_fails.xls similarity index 100% rename from tests/bug_example_xls/ict_survey_fails.xls rename to tests/fixtures/bug_example_forms/ict_survey_fails.xls diff --git a/tests/bug_example_xls/spaces_in_choices_header.xls b/tests/fixtures/bug_example_forms/spaces_in_choices_header.xls similarity index 100% rename from tests/bug_example_xls/spaces_in_choices_header.xls rename to tests/fixtures/bug_example_forms/spaces_in_choices_header.xls diff --git a/tests/bug_example_xls/xl_date_ambiguous.xlsx b/tests/fixtures/bug_example_forms/xl_date_ambiguous.xlsx similarity index 100% rename from tests/bug_example_xls/xl_date_ambiguous.xlsx rename to tests/fixtures/bug_example_forms/xl_date_ambiguous.xlsx diff --git a/tests/bug_example_xls/xl_date_ambiguous_v1.xlsx b/tests/fixtures/bug_example_forms/xl_date_ambiguous_v1.xlsx similarity index 100% rename from tests/bug_example_xls/xl_date_ambiguous_v1.xlsx rename to tests/fixtures/bug_example_forms/xl_date_ambiguous_v1.xlsx diff --git a/tests/example_xls/README.rst b/tests/fixtures/example_forms/README.rst similarity index 100% rename from tests/example_xls/README.rst rename to tests/fixtures/example_forms/README.rst diff --git a/tests/example_xls/__init__.py b/tests/fixtures/example_forms/__init__.py similarity index 100% rename from tests/example_xls/__init__.py rename to tests/fixtures/example_forms/__init__.py diff --git a/tests/example_xls/allow_comment_rows_test.xls b/tests/fixtures/example_forms/allow_comment_rows_test.xls similarity index 100% rename from tests/example_xls/allow_comment_rows_test.xls rename to tests/fixtures/example_forms/allow_comment_rows_test.xls diff --git a/tests/example_xls/attribute_columns_test.xlsx b/tests/fixtures/example_forms/attribute_columns_test.xlsx similarity index 100% rename from tests/example_xls/attribute_columns_test.xlsx rename to tests/fixtures/example_forms/attribute_columns_test.xlsx diff --git a/tests/example_xls/bad_calc.xlsx b/tests/fixtures/example_forms/bad_calc.xlsx similarity index 100% rename from tests/example_xls/bad_calc.xlsx rename to tests/fixtures/example_forms/bad_calc.xlsx diff --git a/tests/example_xls/calculate.xls b/tests/fixtures/example_forms/calculate.xls similarity index 100% rename from tests/example_xls/calculate.xls rename to tests/fixtures/example_forms/calculate.xls diff --git a/tests/example_xls/cascading_select_test_equivalent.xls b/tests/fixtures/example_forms/cascading_select_test_equivalent.xls similarity index 100% rename from tests/example_xls/cascading_select_test_equivalent.xls rename to tests/fixtures/example_forms/cascading_select_test_equivalent.xls diff --git a/tests/example_xls/case_insensitivity.csv b/tests/fixtures/example_forms/case_insensitivity.csv similarity index 100% rename from tests/example_xls/case_insensitivity.csv rename to tests/fixtures/example_forms/case_insensitivity.csv diff --git a/tests/example_xls/case_insensitivity.md b/tests/fixtures/example_forms/case_insensitivity.md similarity index 100% rename from tests/example_xls/case_insensitivity.md rename to tests/fixtures/example_forms/case_insensitivity.md diff --git a/tests/example_xls/case_insensitivity.xls b/tests/fixtures/example_forms/case_insensitivity.xls similarity index 100% rename from tests/example_xls/case_insensitivity.xls rename to tests/fixtures/example_forms/case_insensitivity.xls diff --git a/tests/example_xls/case_insensitivity.xlsx b/tests/fixtures/example_forms/case_insensitivity.xlsx similarity index 100% rename from tests/example_xls/case_insensitivity.xlsx rename to tests/fixtures/example_forms/case_insensitivity.xlsx diff --git a/tests/example_xls/choice_filter_test.xlsx b/tests/fixtures/example_forms/choice_filter_test.xlsx similarity index 100% rename from tests/example_xls/choice_filter_test.xlsx rename to tests/fixtures/example_forms/choice_filter_test.xlsx diff --git a/tests/example_xls/choice_name_as_type.xls b/tests/fixtures/example_forms/choice_name_as_type.xls similarity index 100% rename from tests/example_xls/choice_name_as_type.xls rename to tests/fixtures/example_forms/choice_name_as_type.xls diff --git a/tests/example_xls/choice_name_same_as_select_name.xls b/tests/fixtures/example_forms/choice_name_same_as_select_name.xls similarity index 100% rename from tests/example_xls/choice_name_same_as_select_name.xls rename to tests/fixtures/example_forms/choice_name_same_as_select_name.xls diff --git a/tests/example_xls/default_time_demo.xls b/tests/fixtures/example_forms/default_time_demo.xls similarity index 100% rename from tests/example_xls/default_time_demo.xls rename to tests/fixtures/example_forms/default_time_demo.xls diff --git a/tests/example_xls/extra_columns.xlsx b/tests/fixtures/example_forms/extra_columns.xlsx similarity index 100% rename from tests/example_xls/extra_columns.xlsx rename to tests/fixtures/example_forms/extra_columns.xlsx diff --git a/tests/example_xls/extra_sheet_names.xlsx b/tests/fixtures/example_forms/extra_sheet_names.xlsx similarity index 100% rename from tests/example_xls/extra_sheet_names.xlsx rename to tests/fixtures/example_forms/extra_sheet_names.xlsx diff --git a/tests/example_xls/field-list.xlsx b/tests/fixtures/example_forms/field-list.xlsx similarity index 100% rename from tests/example_xls/field-list.xlsx rename to tests/fixtures/example_forms/field-list.xlsx diff --git a/tests/example_xls/flat_xlsform_test.xlsx b/tests/fixtures/example_forms/flat_xlsform_test.xlsx similarity index 100% rename from tests/example_xls/flat_xlsform_test.xlsx rename to tests/fixtures/example_forms/flat_xlsform_test.xlsx diff --git a/tests/example_xls/fruits.csv b/tests/fixtures/example_forms/fruits.csv similarity index 100% rename from tests/example_xls/fruits.csv rename to tests/fixtures/example_forms/fruits.csv diff --git a/tests/example_xls/gps.csv b/tests/fixtures/example_forms/gps.csv similarity index 100% rename from tests/example_xls/gps.csv rename to tests/fixtures/example_forms/gps.csv diff --git a/tests/example_xls/gps.xls b/tests/fixtures/example_forms/gps.xls similarity index 100% rename from tests/example_xls/gps.xls rename to tests/fixtures/example_forms/gps.xls diff --git a/tests/example_xls/group.csv b/tests/fixtures/example_forms/group.csv similarity index 100% rename from tests/example_xls/group.csv rename to tests/fixtures/example_forms/group.csv diff --git a/tests/example_xls/group.md b/tests/fixtures/example_forms/group.md similarity index 100% rename from tests/example_xls/group.md rename to tests/fixtures/example_forms/group.md diff --git a/tests/example_xls/group.xls b/tests/fixtures/example_forms/group.xls similarity index 100% rename from tests/example_xls/group.xls rename to tests/fixtures/example_forms/group.xls diff --git a/tests/example_xls/group.xlsx b/tests/fixtures/example_forms/group.xlsx similarity index 100% rename from tests/example_xls/group.xlsx rename to tests/fixtures/example_forms/group.xlsx diff --git a/tests/example_xls/hidden.xls b/tests/fixtures/example_forms/hidden.xls similarity index 100% rename from tests/example_xls/hidden.xls rename to tests/fixtures/example_forms/hidden.xls diff --git a/tests/example_xls/include.csv b/tests/fixtures/example_forms/include.csv similarity index 100% rename from tests/example_xls/include.csv rename to tests/fixtures/example_forms/include.csv diff --git a/tests/example_xls/include.md b/tests/fixtures/example_forms/include.md similarity index 100% rename from tests/example_xls/include.md rename to tests/fixtures/example_forms/include.md diff --git a/tests/example_xls/include.xls b/tests/fixtures/example_forms/include.xls similarity index 100% rename from tests/example_xls/include.xls rename to tests/fixtures/example_forms/include.xls diff --git a/tests/example_xls/include.xlsx b/tests/fixtures/example_forms/include.xlsx similarity index 100% rename from tests/example_xls/include.xlsx rename to tests/fixtures/example_forms/include.xlsx diff --git a/tests/example_xls/include_json.csv b/tests/fixtures/example_forms/include_json.csv similarity index 100% rename from tests/example_xls/include_json.csv rename to tests/fixtures/example_forms/include_json.csv diff --git a/tests/example_xls/include_json.md b/tests/fixtures/example_forms/include_json.md similarity index 100% rename from tests/example_xls/include_json.md rename to tests/fixtures/example_forms/include_json.md diff --git a/tests/example_xls/include_json.xls b/tests/fixtures/example_forms/include_json.xls similarity index 100% rename from tests/example_xls/include_json.xls rename to tests/fixtures/example_forms/include_json.xls diff --git a/tests/example_xls/include_json.xlsx b/tests/fixtures/example_forms/include_json.xlsx similarity index 100% rename from tests/example_xls/include_json.xlsx rename to tests/fixtures/example_forms/include_json.xlsx diff --git a/tests/example_xls/loop.csv b/tests/fixtures/example_forms/loop.csv similarity index 100% rename from tests/example_xls/loop.csv rename to tests/fixtures/example_forms/loop.csv diff --git a/tests/example_xls/loop.md b/tests/fixtures/example_forms/loop.md similarity index 100% rename from tests/example_xls/loop.md rename to tests/fixtures/example_forms/loop.md diff --git a/tests/example_xls/loop.xls b/tests/fixtures/example_forms/loop.xls similarity index 100% rename from tests/example_xls/loop.xls rename to tests/fixtures/example_forms/loop.xls diff --git a/tests/example_xls/loop.xlsx b/tests/fixtures/example_forms/loop.xlsx similarity index 100% rename from tests/example_xls/loop.xlsx rename to tests/fixtures/example_forms/loop.xlsx diff --git a/tests/example_xls/or_other.xlsx b/tests/fixtures/example_forms/or_other.xlsx similarity index 100% rename from tests/example_xls/or_other.xlsx rename to tests/fixtures/example_forms/or_other.xlsx diff --git a/tests/example_xls/pull_data.xlsx b/tests/fixtures/example_forms/pull_data.xlsx similarity index 100% rename from tests/example_xls/pull_data.xlsx rename to tests/fixtures/example_forms/pull_data.xlsx diff --git a/tests/example_xls/repeat_date_test.xls b/tests/fixtures/example_forms/repeat_date_test.xls similarity index 100% rename from tests/example_xls/repeat_date_test.xls rename to tests/fixtures/example_forms/repeat_date_test.xls diff --git a/tests/example_xls/simple_loop.csv b/tests/fixtures/example_forms/simple_loop.csv similarity index 100% rename from tests/example_xls/simple_loop.csv rename to tests/fixtures/example_forms/simple_loop.csv diff --git a/tests/example_xls/simple_loop.xls b/tests/fixtures/example_forms/simple_loop.xls similarity index 100% rename from tests/example_xls/simple_loop.xls rename to tests/fixtures/example_forms/simple_loop.xls diff --git a/tests/example_xls/sms_info.xls b/tests/fixtures/example_forms/sms_info.xls similarity index 100% rename from tests/example_xls/sms_info.xls rename to tests/fixtures/example_forms/sms_info.xls diff --git a/tests/example_xls/spec_test_expected_output.xml b/tests/fixtures/example_forms/spec_test_expected_output.xml similarity index 100% rename from tests/example_xls/spec_test_expected_output.xml rename to tests/fixtures/example_forms/spec_test_expected_output.xml diff --git a/tests/example_xls/specify_other.csv b/tests/fixtures/example_forms/specify_other.csv similarity index 100% rename from tests/example_xls/specify_other.csv rename to tests/fixtures/example_forms/specify_other.csv diff --git a/tests/example_xls/specify_other.md b/tests/fixtures/example_forms/specify_other.md similarity index 100% rename from tests/example_xls/specify_other.md rename to tests/fixtures/example_forms/specify_other.md diff --git a/tests/example_xls/specify_other.xls b/tests/fixtures/example_forms/specify_other.xls similarity index 100% rename from tests/example_xls/specify_other.xls rename to tests/fixtures/example_forms/specify_other.xls diff --git a/tests/example_xls/specify_other.xlsx b/tests/fixtures/example_forms/specify_other.xlsx similarity index 100% rename from tests/example_xls/specify_other.xlsx rename to tests/fixtures/example_forms/specify_other.xlsx diff --git a/tests/example_xls/style_settings.xls b/tests/fixtures/example_forms/style_settings.xls similarity index 100% rename from tests/example_xls/style_settings.xls rename to tests/fixtures/example_forms/style_settings.xls diff --git a/tests/example_xls/survey_no_name.xlsx b/tests/fixtures/example_forms/survey_no_name.xlsx similarity index 100% rename from tests/example_xls/survey_no_name.xlsx rename to tests/fixtures/example_forms/survey_no_name.xlsx diff --git a/tests/example_xls/table-list.xls b/tests/fixtures/example_forms/table-list.xls similarity index 100% rename from tests/example_xls/table-list.xls rename to tests/fixtures/example_forms/table-list.xls diff --git a/tests/example_xls/text_and_integer.csv b/tests/fixtures/example_forms/text_and_integer.csv similarity index 100% rename from tests/example_xls/text_and_integer.csv rename to tests/fixtures/example_forms/text_and_integer.csv diff --git a/tests/example_xls/text_and_integer.md b/tests/fixtures/example_forms/text_and_integer.md similarity index 100% rename from tests/example_xls/text_and_integer.md rename to tests/fixtures/example_forms/text_and_integer.md diff --git a/tests/example_xls/text_and_integer.xls b/tests/fixtures/example_forms/text_and_integer.xls similarity index 100% rename from tests/example_xls/text_and_integer.xls rename to tests/fixtures/example_forms/text_and_integer.xls diff --git a/tests/example_xls/text_and_integer.xlsx b/tests/fixtures/example_forms/text_and_integer.xlsx similarity index 100% rename from tests/example_xls/text_and_integer.xlsx rename to tests/fixtures/example_forms/text_and_integer.xlsx diff --git a/tests/example_xls/tutorial.xls b/tests/fixtures/example_forms/tutorial.xls old mode 100755 new mode 100644 similarity index 100% rename from tests/example_xls/tutorial.xls rename to tests/fixtures/example_forms/tutorial.xls diff --git a/tests/example_xls/unknown_question_type.xls b/tests/fixtures/example_forms/unknown_question_type.xls similarity index 100% rename from tests/example_xls/unknown_question_type.xls rename to tests/fixtures/example_forms/unknown_question_type.xls diff --git a/tests/example_xls/utf_csv.csv b/tests/fixtures/example_forms/utf_csv.csv similarity index 100% rename from tests/example_xls/utf_csv.csv rename to tests/fixtures/example_forms/utf_csv.csv diff --git a/tests/example_xls/widgets-media/a.jpg b/tests/fixtures/example_forms/widgets-media/a.jpg old mode 100755 new mode 100644 similarity index 100% rename from tests/example_xls/widgets-media/a.jpg rename to tests/fixtures/example_forms/widgets-media/a.jpg diff --git a/tests/example_xls/widgets-media/b.jpg b/tests/fixtures/example_forms/widgets-media/b.jpg old mode 100755 new mode 100644 similarity index 100% rename from tests/example_xls/widgets-media/b.jpg rename to tests/fixtures/example_forms/widgets-media/b.jpg diff --git a/tests/example_xls/widgets-media/happy.jpg b/tests/fixtures/example_forms/widgets-media/happy.jpg old mode 100755 new mode 100644 similarity index 100% rename from tests/example_xls/widgets-media/happy.jpg rename to tests/fixtures/example_forms/widgets-media/happy.jpg diff --git a/tests/example_xls/widgets-media/img_test.jpg b/tests/fixtures/example_forms/widgets-media/img_test.jpg old mode 100755 new mode 100644 similarity index 100% rename from tests/example_xls/widgets-media/img_test.jpg rename to tests/fixtures/example_forms/widgets-media/img_test.jpg diff --git a/tests/example_xls/widgets-media/sad.jpg b/tests/fixtures/example_forms/widgets-media/sad.jpg old mode 100755 new mode 100644 similarity index 100% rename from tests/example_xls/widgets-media/sad.jpg rename to tests/fixtures/example_forms/widgets-media/sad.jpg diff --git a/tests/example_xls/widgets.csv b/tests/fixtures/example_forms/widgets.csv similarity index 100% rename from tests/example_xls/widgets.csv rename to tests/fixtures/example_forms/widgets.csv diff --git a/tests/example_xls/widgets.xls b/tests/fixtures/example_forms/widgets.xls similarity index 100% rename from tests/example_xls/widgets.xls rename to tests/fixtures/example_forms/widgets.xls diff --git a/tests/example_xls/widgets.xml b/tests/fixtures/example_forms/widgets.xml similarity index 100% rename from tests/example_xls/widgets.xml rename to tests/fixtures/example_forms/widgets.xml diff --git a/tests/example_xls/xlsform_spec_test.xlsx b/tests/fixtures/example_forms/xlsform_spec_test.xlsx similarity index 100% rename from tests/example_xls/xlsform_spec_test.xlsx rename to tests/fixtures/example_forms/xlsform_spec_test.xlsx diff --git a/tests/example_xls/xml_escaping.xls b/tests/fixtures/example_forms/xml_escaping.xls similarity index 100% rename from tests/example_xls/xml_escaping.xls rename to tests/fixtures/example_forms/xml_escaping.xls diff --git a/tests/example_xls/yes_or_no_question.csv b/tests/fixtures/example_forms/yes_or_no_question.csv similarity index 100% rename from tests/example_xls/yes_or_no_question.csv rename to tests/fixtures/example_forms/yes_or_no_question.csv diff --git a/tests/example_xls/yes_or_no_question.md b/tests/fixtures/example_forms/yes_or_no_question.md similarity index 100% rename from tests/example_xls/yes_or_no_question.md rename to tests/fixtures/example_forms/yes_or_no_question.md diff --git a/tests/example_xls/yes_or_no_question.xls b/tests/fixtures/example_forms/yes_or_no_question.xls similarity index 100% rename from tests/example_xls/yes_or_no_question.xls rename to tests/fixtures/example_forms/yes_or_no_question.xls diff --git a/tests/example_xls/yes_or_no_question.xlsx b/tests/fixtures/example_forms/yes_or_no_question.xlsx similarity index 100% rename from tests/example_xls/yes_or_no_question.xlsx rename to tests/fixtures/example_forms/yes_or_no_question.xlsx diff --git a/tests/test_xls2json.py b/tests/test_xls2json.py index b1c94bd6..aa8398b0 100644 --- a/tests/test_xls2json.py +++ b/tests/test_xls2json.py @@ -4,7 +4,8 @@ from pyxform.xls2json_backends import get_xlsform, md_table_to_workbook from pyxform.xls2xform import get_xml_path, xls2xform_convert -from tests import example_xls, test_output +from tests import test_output +from tests.fixtures import example_forms from tests.pyxform_test_case import PyxformTestCase from tests.utils import get_temp_dir @@ -587,7 +588,7 @@ def test_xls2xform_convert__e2e_with_settings_misspelling(self): """Should warn about settings misspelling when running full pipeline.""" file_name = "extra_sheet_names" warnings = xls2xform_convert( - xlsform_path=os.path.join(example_xls.PATH, file_name + ".xlsx"), + xlsform_path=os.path.join(example_forms.PATH, file_name + ".xlsx"), xform_path=os.path.join(test_output.PATH, file_name + ".xml"), validate=False, pretty_print=False, @@ -606,7 +607,7 @@ def test_xls2xform_convert__e2e_with_extra_columns__does_not_use_excessive_memor process = psutil.Process(os.getpid()) pre_mem = process.memory_info().rss xls2xform_convert( - xlsform_path=os.path.join(example_xls.PATH, "extra_columns.xlsx"), + xlsform_path=os.path.join(example_forms.PATH, "extra_columns.xlsx"), xform_path=os.path.join(test_output.PATH, "extra_columns.xml"), ) post_mem = process.memory_info().rss @@ -615,7 +616,7 @@ def test_xls2xform_convert__e2e_with_extra_columns__does_not_use_excessive_memor 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 = get_xlsform(os.path.join(example_xls.PATH, "extra_sheet_names.xlsx")) + d = get_xlsform(os.path.join(example_forms.PATH, "extra_sheet_names.xlsx")) self.assertIn("survey", d.sheet_names) self.assertIn("my_sheet", d.sheet_names) self.assertIn("stettings", d.sheet_names) diff --git a/tests/test_xls2json_backends.py b/tests/test_xls2json_backends.py index 32d4432f..f1b37836 100644 --- a/tests/test_xls2json_backends.py +++ b/tests/test_xls2json_backends.py @@ -17,7 +17,8 @@ xlsx_value_to_str, ) -from tests import bug_example_xls, utils +from tests import utils +from tests.fixtures import bug_example_forms from tests.pyxform_test_case import PyxformTestCase from tests.xpath_helpers.choices import xpc from tests.xpath_helpers.entities import xpe @@ -175,7 +176,7 @@ def test_equivalency(self): def test_xls_with_many_empty_cells(self): """Should quickly produce expected data, and find large input sheet dimensions.""" # Test fixture produced by adding data at cells IV1 and A19999. - xls_path = os.path.join(bug_example_xls.PATH, "extra_columns.xls") + xls_path = os.path.join(bug_example_forms.PATH, "extra_columns.xls") before = datetime.datetime.now(datetime.UTC) xls_data = xls_to_dict(xls_path) after = datetime.datetime.now(datetime.UTC) @@ -198,7 +199,7 @@ def test_xls_with_many_empty_cells(self): def test_xlsx_with_many_empty_cells(self): """Should quickly produce expected data, and find large input sheet dimensions.""" # Test fixture produced (presumably) by a LibreOffice serialisation bug. - xlsx_path = os.path.join(bug_example_xls.PATH, "UCL_Biomass_Plot_Form.xlsx") + xlsx_path = os.path.join(bug_example_forms.PATH, "UCL_Biomass_Plot_Form.xlsx") before = datetime.datetime.now(datetime.UTC) xlsx_data = xlsx_to_dict(xlsx_path) after = datetime.datetime.now(datetime.UTC) diff --git a/tests/test_xls2json_xls.py b/tests/test_xls2json_xls.py index fab479bd..85bbfcd2 100644 --- a/tests/test_xls2json_xls.py +++ b/tests/test_xls2json_xls.py @@ -8,7 +8,8 @@ from pyxform.xls2json_backends import csv_to_dict, xlsx_to_dict from pyxform.xls2xform import convert -from tests import example_xls, test_expected_output, utils +from tests import test_expected_output, utils +from tests.fixtures import example_forms class BasicXls2JsonApiTests(TestCase): @@ -16,7 +17,7 @@ class BasicXls2JsonApiTests(TestCase): def test_simple_yes_or_no_question(self): filename = "yes_or_no_question.xls" - path_to_excel_file = Path(example_xls.PATH) / filename + path_to_excel_file = Path(example_forms.PATH) / filename expected_output_path = Path(test_expected_output.PATH) / ( path_to_excel_file.stem + ".json" ) diff --git a/tests/test_xls2xform.py b/tests/test_xls2xform.py index de0b67a0..fd651435 100644 --- a/tests/test_xls2xform.py +++ b/tests/test_xls2xform.py @@ -21,7 +21,7 @@ xls2xform_convert, ) -from tests import example_xls +from tests.fixtures import example_forms from tests.utils import get_temp_dir, get_temp_file, path_to_text_fixture @@ -241,11 +241,11 @@ class TestXLS2XFormConvert(TestCase): def test_xls2xform_convert__ok(self): """Should find the expected output files for the conversion.""" xlsforms = ( - Path(example_xls.PATH) / "group.xlsx", - Path(example_xls.PATH) / "group.xls", - Path(example_xls.PATH) / "group.csv", - Path(example_xls.PATH) / "group.md", - Path(example_xls.PATH) / "choice_name_as_type.xls", # has external choices + Path(example_forms.PATH) / "group.xlsx", + Path(example_forms.PATH) / "group.xls", + Path(example_forms.PATH) / "group.csv", + Path(example_forms.PATH) / "group.md", + Path(example_forms.PATH) / "choice_name_as_type.xls", # has external choices ) kwargs = ( ("validate", (True, False)), @@ -309,12 +309,12 @@ def test_args_combinations__ok(self): ("str (data)", self.with_xlsform_data_str), # Only for .csv, .md. ] xlsforms = ( - (Path(example_xls.PATH) / "group.xlsx", funcs[:4]), - (Path(example_xls.PATH) / "group.xls", funcs[:4]), - (Path(example_xls.PATH) / "group.csv", funcs), - (Path(example_xls.PATH) / "group.md", funcs), + (Path(example_forms.PATH) / "group.xlsx", funcs[:4]), + (Path(example_forms.PATH) / "group.xls", funcs[:4]), + (Path(example_forms.PATH) / "group.csv", funcs), + (Path(example_forms.PATH) / "group.md", funcs), ( - Path(example_xls.PATH) / "choice_name_as_type.xls", + Path(example_forms.PATH) / "choice_name_as_type.xls", funcs[:4], ), # has external choices ) diff --git a/tests/utils.py b/tests/utils.py index f6cec54b..e6b59fe2 100644 --- a/tests/utils.py +++ b/tests/utils.py @@ -11,11 +11,11 @@ from pyxform import file_utils from pyxform.builder import create_survey, create_survey_from_path -from tests import example_xls +from tests.fixtures import example_forms def path_to_text_fixture(filename): - return os.path.join(example_xls.PATH, filename) + return os.path.join(example_forms.PATH, filename) def build_survey(filename): diff --git a/tests/xform_test_case/base.py b/tests/xform_test_case/base.py index b99dd031..4959c33c 100644 --- a/tests/xform_test_case/base.py +++ b/tests/xform_test_case/base.py @@ -4,7 +4,8 @@ from defusedxml.ElementTree import fromstring from formencode.doctest_xml_compare import xml_compare -from tests import example_xls, test_output +from tests import test_output +from tests.fixtures import example_forms # Do not use this class for new tests. Use PyxformTestCase instead. @@ -21,7 +22,7 @@ class XFormTestCase(TestCase): """ def get_file_path(self, filename): - self.path_to_excel_file = os.path.join(example_xls.PATH, filename) + self.path_to_excel_file = os.path.join(example_forms.PATH, filename) # Get the xform output path: self.root_filename, self.ext = os.path.splitext(filename) diff --git a/tests/xform_test_case/test_bugs.py b/tests/xform_test_case/test_bugs.py index cf117d2d..af063f54 100644 --- a/tests/xform_test_case/test_bugs.py +++ b/tests/xform_test_case/test_bugs.py @@ -12,7 +12,8 @@ from pyxform.xls2json_backends import DefinitionData, get_xlsform, xlsx_to_dict from pyxform.xls2xform import convert -from tests import bug_example_xls, example_xls, test_output +from tests import test_output +from tests.fixtures import bug_example_forms, example_forms class TestXFormConversion(TestCase): @@ -28,7 +29,7 @@ def test_conversion_raises(self): for i, (case, err_msg) in enumerate(cases): with self.subTest(msg=f"{i}: {case}"): with self.assertRaises(PyXFormError) as err: - convert(xlsform=Path(bug_example_xls.PATH) / case, warnings=[]) + convert(xlsform=Path(bug_example_forms.PATH) / case, warnings=[]) self.assertIn(err_msg, err.exception.args[0]) @@ -38,7 +39,7 @@ class ValidateWrapper(TestCase): @staticmethod def test_conversion(): filename = "ODKValidateWarnings.xlsx" - path_to_excel_file = os.path.join(bug_example_xls.PATH, filename) + path_to_excel_file = os.path.join(bug_example_forms.PATH, filename) # Get the xform output path: root_filename, _ = os.path.splitext(filename) output_path = os.path.join(test_output.PATH, root_filename + ".xml") @@ -54,7 +55,9 @@ def test_conversion(): class EmptyStringOnRelevantColumnTest(TestCase): def test_conversion(self): filename = "ict_survey_fails.xls" - workbook_dict = get_xlsform(xlsform=os.path.join(bug_example_xls.PATH, filename)) + workbook_dict = get_xlsform( + xlsform=os.path.join(bug_example_forms.PATH, filename) + ) with self.assertRaises(KeyError): # bind:relevant should not be part of workbook_dict workbook_dict.survey[0]["bind: relevant"].strip() @@ -63,7 +66,7 @@ def test_conversion(self): class BadChoicesSheetHeaders(TestCase): def test_conversion(self): filename = "spaces_in_choices_header.xls" - path_to_excel_file = os.path.join(bug_example_xls.PATH, filename) + path_to_excel_file = os.path.join(bug_example_forms.PATH, filename) warnings = [] pyxform.xls2json.parse_file_to_json( path_to_excel_file, @@ -87,7 +90,7 @@ def test_values_with_spaces_are_cleaned(self): of leading and trailing whitespaces. """ filename = "spaces_in_choices_header.xls" - path_to_excel_file = os.path.join(bug_example_xls.PATH, filename) + path_to_excel_file = os.path.join(bug_example_forms.PATH, filename) survey_reader = SurveyReader( path_to_excel_file, default_name="spaces_in_choices_header" ) @@ -101,7 +104,7 @@ def test_values_with_spaces_are_cleaned(self): class TestChoiceNameAsType(TestCase): def test_choice_name_as_type(self): filename = "choice_name_as_type.xls" - path_to_excel_file = os.path.join(example_xls.PATH, filename) + path_to_excel_file = os.path.join(example_forms.PATH, filename) xls_reader = SurveyReader(path_to_excel_file, default_name="choice_name_as_type") survey_dict = xls_reader.to_json_dict() self.assertTrue(has_external_choices(survey_dict)) @@ -110,7 +113,7 @@ def test_choice_name_as_type(self): class TestBlankSecondRow(TestCase): def test_blank_second_row(self): filename = "blank_second_row.xls" - path_to_excel_file = os.path.join(bug_example_xls.PATH, filename) + path_to_excel_file = os.path.join(bug_example_forms.PATH, filename) xls_reader = SurveyReader(path_to_excel_file, default_name="blank_second_row") survey_dict = xls_reader.to_json_dict() self.assertTrue(len(survey_dict) > 0) @@ -122,7 +125,7 @@ class TestXLDateAmbigous(TestCase): def test_xl_date_ambigous(self): """Test non standard sheet with exception is processed successfully.""" filename = "xl_date_ambiguous.xlsx" - path_to_excel_file = os.path.join(bug_example_xls.PATH, filename) + path_to_excel_file = os.path.join(bug_example_forms.PATH, filename) xls_reader = SurveyReader(path_to_excel_file, default_name="xl_date_ambiguous") survey_dict = xls_reader.to_json_dict() self.assertTrue(len(survey_dict) > 0) @@ -136,7 +139,7 @@ class TestXLDateAmbigousNoException(TestCase): def test_xl_date_ambigous_no_exception(self): """Test standard sheet is processed successfully.""" filename = "xl_date_ambiguous_v1.xlsx" - path_to_excel_file = os.path.join(bug_example_xls.PATH, filename) + path_to_excel_file = os.path.join(bug_example_forms.PATH, filename) survey_dict = xlsx_to_dict(path_to_excel_file) self.assertEqual(survey_dict["survey"][4]["default"], "1900-01-01 00:00:00") @@ -146,7 +149,7 @@ class TestSpreadSheetFilesWithMacrosAreAllowed(TestCase): def test_xlsm_files_are_allowed(self): filename = "excel_with_macros.xlsm" - result = get_xlsform(xlsform=os.path.join(bug_example_xls.PATH, filename)) + result = get_xlsform(xlsform=os.path.join(bug_example_forms.PATH, filename)) self.assertIsInstance(result, DefinitionData) From e6eb8f4d6f92d6bf05b98452cb5b753ff6b45d97 Mon Sep 17 00:00:00 2001 From: lindsay stevens Date: Tue, 14 Jul 2026 02:04:19 +1000 Subject: [PATCH 18/50] fix: delete choices sheet name test fixture not referenced anywhere - the form had a sheet named "Choices" (capitalised) but that is now a supported use case per test_xls2json.py --- .../badly_named_choices_sheet.xls | Bin 24064 -> 0 bytes 1 file changed, 0 insertions(+), 0 deletions(-) delete mode 100644 tests/fixtures/bug_example_forms/badly_named_choices_sheet.xls diff --git a/tests/fixtures/bug_example_forms/badly_named_choices_sheet.xls b/tests/fixtures/bug_example_forms/badly_named_choices_sheet.xls deleted file mode 100644 index 9b9cf43b0bf405266b45953024c3ebc0714c68a0..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 24064 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 Date: Tue, 14 Jul 2026 03:11:02 +1000 Subject: [PATCH 19/50] fix: delete blank row test case and fixture covered by another test - the deleted case is essentially a valid survey header row with a blank line before a single question. That is a pattern repeated in test_xlsx_with_many_empty_rows, which has a few blank rows among the survey questions (which seem to be intended as visual separators). - thought about ways to refactor the old test anyway: - can't do it with md_table_to_workbook because the markdown pre-processing strips out empty rows, and re-inserting a blank row after that seemed a bit fragile. - using a dict passed to get_xlsform, but then trying a convert() with that fails because the headers are taken from the first row. - ultimately the old test seemed to be more about checking that conversion still works in general for that blank row case (rather than asserting anything specific about the processed form), so the test_xlsx_with_many_empty_rows seemed like a complete repro. --- .../bug_example_forms/blank_second_row.xls | Bin 7168 -> 0 bytes tests/test_xls2json_backends.py | 2 +- tests/xform_test_case/test_bugs.py | 9 --------- 3 files changed, 1 insertion(+), 10 deletions(-) delete mode 100644 tests/fixtures/bug_example_forms/blank_second_row.xls diff --git a/tests/fixtures/bug_example_forms/blank_second_row.xls b/tests/fixtures/bug_example_forms/blank_second_row.xls deleted file mode 100644 index 41dbd0c5a3fb4b3a206e417a6c4733a98d7528fb..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 7168 zcmeHLTWnNC82-<0w`^%?w}n;&!9$gDX^Z8efCgDERY}<-x=UjF*H2d@9Blq7RGFCUbL0l{weJ6bhzEJfZfCPuSBz{W z+(#C;<&2eUScOIO0bKKGfFGyTLfJMM!UA00DIoAR;eyn1(PrjIOkuG0-|5Y&J!>S9)i=QCdB?rij zO`&Mm{I9}B9Ye?c!Q`Xa#G_~ zjq4E`@|m{zQ6kaY=fG6{duejcx={UMa`sxN{{L5g7B)3GJtw$!Zny1_$(JuFBJU_7 zUs^<7Q&28of{tg)-FolHc@5K$HsA7KAEqSBG?4OY+L)@=v?*1k>CDtLO=qQi*_V{- zqu4K9yx3uPCr082ts|rG->>U_pC;kGu}IiTjKZCt<82SuzJ!%bMurCyZ(!RS_+2pk zfJom-Lw=EcbpDxsR+Z`uoitAd3``FZ9wekJLCP_8Wh_P6J6;DKB!)VmUG7NR=<}5~BD`9^#0EJHA zk;dTRJ=pe_fxG-=9h&WdQRrP7by3vdi>YHRI_3|R?thjqy8jSYo{J9@3bk!UF1+O~W}+lFSGjyNkPsMix3 zw9K}i-%lk^^g2HaXDd4mr5O64cm^9CDDe_MWPcV`9HE zIdag(`F>}d93%{x;+1ikO$vU-a8tX4Y0)PUmoP2xj&>Pax9Bqo%ea~?x|?6Yals3# z-AgXDx^k(7!>(*;05Pr0Bb85 zfI4mh?El&=`d5)gAhL+xNVv$={d$T6G?I zWHl7CAN%oK`sUKy$$B%ZQlyz1-w4SWxw?6JLLXC)$b{50$a!z?mPD~b#m{ZOwLEuu`6ICI0iRj=?9-$q42@ zgclFK{cgo8@PMufB}u%5#2_gi7CZXs?@FEjD%4%$co3g#hK8_?!O|D#^2W1f67ydR Y{mJ4}D!Zgy`{%yDmaVKngMXv{AMw7EYXATM diff --git a/tests/test_xls2json_backends.py b/tests/test_xls2json_backends.py index f1b37836..48eba5e3 100644 --- a/tests/test_xls2json_backends.py +++ b/tests/test_xls2json_backends.py @@ -196,7 +196,7 @@ def test_xls_with_many_empty_cells(self): wb.release_resources() - def test_xlsx_with_many_empty_cells(self): + def test_xlsx_with_many_empty_rows(self): """Should quickly produce expected data, and find large input sheet dimensions.""" # Test fixture produced (presumably) by a LibreOffice serialisation bug. xlsx_path = os.path.join(bug_example_forms.PATH, "UCL_Biomass_Plot_Form.xlsx") diff --git a/tests/xform_test_case/test_bugs.py b/tests/xform_test_case/test_bugs.py index af063f54..5c285840 100644 --- a/tests/xform_test_case/test_bugs.py +++ b/tests/xform_test_case/test_bugs.py @@ -110,15 +110,6 @@ def test_choice_name_as_type(self): self.assertTrue(has_external_choices(survey_dict)) -class TestBlankSecondRow(TestCase): - def test_blank_second_row(self): - filename = "blank_second_row.xls" - path_to_excel_file = os.path.join(bug_example_forms.PATH, filename) - xls_reader = SurveyReader(path_to_excel_file, default_name="blank_second_row") - survey_dict = xls_reader.to_json_dict() - self.assertTrue(len(survey_dict) > 0) - - class TestXLDateAmbigous(TestCase): """Test non standard sheet with exception is processed successfully.""" From e165ee5b587b9eabe595c7f0f7495cbcd4823cec Mon Sep 17 00:00:00 2001 From: lindsay stevens Date: Tue, 14 Jul 2026 03:25:29 +1000 Subject: [PATCH 20/50] fix: replace old xls missing calc test with equivalent markdown test --- .../calculate_without_calculation.xls | Bin 11776 -> 0 bytes tests/test_typed_calculates.py | 13 +++++++++++++ tests/xform_test_case/test_bugs.py | 1 - 3 files changed, 13 insertions(+), 1 deletion(-) delete mode 100644 tests/fixtures/bug_example_forms/calculate_without_calculation.xls diff --git a/tests/fixtures/bug_example_forms/calculate_without_calculation.xls b/tests/fixtures/bug_example_forms/calculate_without_calculation.xls deleted file mode 100644 index 3592c1a0a624b4a881e6374990fcdd9f81eeae50..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 11776 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 VbN&w_`DfyFs`gKRfA{~E{~tW~bvFP2 diff --git a/tests/test_typed_calculates.py b/tests/test_typed_calculates.py index 9daef906..0bb22572 100644 --- a/tests/test_typed_calculates.py +++ b/tests/test_typed_calculates.py @@ -138,6 +138,19 @@ def test_row_without_label_or_calculation_throws_error(self): error__contains=["The survey element named 'a' has no label or hint."], ) + def test_calculate_without_calculation__error(self): + """Should find an error is raised when a calculate question has no calculation.""" + md = """ + | survey | + | | type | name | label | calculation | + | | calculate | q1 | Q1 | | + """ + self.assertPyxformXform( + md=md, + errored=True, + error__contains=["[row : 2] Missing calculation."], + ) + def test_calculate_without_calculation_without_default(self): self.assertPyxformXform( name="calculate-without-calculation-without-default", diff --git a/tests/xform_test_case/test_bugs.py b/tests/xform_test_case/test_bugs.py index 5c285840..2c33b501 100644 --- a/tests/xform_test_case/test_bugs.py +++ b/tests/xform_test_case/test_bugs.py @@ -24,7 +24,6 @@ def test_conversion_raises(self): cases = ( ("group_name_test.xls", "[row : 3] Question or group with no name."), ("duplicate_columns.xlsx", "Duplicate column header: label"), - ("calculate_without_calculation.xls", "[row : 34] Missing calculation."), ) for i, (case, err_msg) in enumerate(cases): with self.subTest(msg=f"{i}: {case}"): From a9949df0375eea1e416eaaf7314c74c38fe3a463 Mon Sep 17 00:00:00 2001 From: lindsay stevens Date: Tue, 14 Jul 2026 03:31:57 +1000 Subject: [PATCH 21/50] fix: move single calculate test into module for testing calculate --- tests/test_bug_round_calculation.py | 17 ----------------- tests/test_typed_calculates.py | 10 ++++++++++ 2 files changed, 10 insertions(+), 17 deletions(-) delete mode 100644 tests/test_bug_round_calculation.py diff --git a/tests/test_bug_round_calculation.py b/tests/test_bug_round_calculation.py deleted file mode 100644 index 07761db5..00000000 --- a/tests/test_bug_round_calculation.py +++ /dev/null @@ -1,17 +0,0 @@ -"""Test round(number, precision) calculation.""" - -from tests.pyxform_test_case import PyxformTestCase - - -class RoundCalculationTest(PyxformTestCase): - def test_non_existent_itext_reference(self): - self.assertPyxformXform( - name="ecsv", - md=""" - | survey | | | | | - | | type | name | label | calculation | - | | decimal | amount | Counter | | - | | calculate | rounded | Rounded | round(${amount}, 0) | - """, - xml__contains=[""""""], - ) diff --git a/tests/test_typed_calculates.py b/tests/test_typed_calculates.py index 0bb22572..3a786a4b 100644 --- a/tests/test_typed_calculates.py +++ b/tests/test_typed_calculates.py @@ -185,3 +185,13 @@ def test_calculate_without_calculation_with_dynamic_default(self): """, instance__contains=[""], ) + + def test_round_function__ok(self): + """Should find that using the round() function does not raise an ODK Validate error.""" + md = """ + | survey | + | | type | name | label | calculation | + | | decimal | q1 | Q1 | | + | | calculate | q2 | Q2 | round(${q1}, 0) | + """ + self.assertPyxformXform(md=md) From e1cf7f4c08ad29c69b2d9fccff4184be5710c64d Mon Sep 17 00:00:00 2001 From: lindsay stevens Date: Tue, 14 Jul 2026 03:33:54 +1000 Subject: [PATCH 22/50] chg: rename calculate test module to follow pattern of other tests - no other question types use the pattern "test_typed_[name]", but rather just use the type name e.g. "test_audit", "test_area", etc. --- tests/{test_typed_calculates.py => test_calculate.py} | 0 1 file changed, 0 insertions(+), 0 deletions(-) rename tests/{test_typed_calculates.py => test_calculate.py} (100%) diff --git a/tests/test_typed_calculates.py b/tests/test_calculate.py similarity index 100% rename from tests/test_typed_calculates.py rename to tests/test_calculate.py From f7a06029d74705eb173f1069c194d57149ed6adb Mon Sep 17 00:00:00 2001 From: lindsay stevens Date: Tue, 14 Jul 2026 03:51:30 +1000 Subject: [PATCH 23/50] chg: replace empty columns xls fixture with xlsx - seems to be different from example_forms/extra_columns in that the file modified here has data at the 20k-th row and 256th column, whereas the other file is a trickier case where empty columns exist in the file (as seen in the zipped XML) but no content after the apparent survey data (one question only). --- .../bug_example_forms/extra_columns.xls | Bin 48640 -> 0 bytes .../bug_example_forms/extra_columns.xlsx | Bin 0 -> 96159 bytes tests/test_xls2json_backends.py | 19 +++++++++--------- 3 files changed, 10 insertions(+), 9 deletions(-) delete mode 100644 tests/fixtures/bug_example_forms/extra_columns.xls create mode 100644 tests/fixtures/bug_example_forms/extra_columns.xlsx diff --git a/tests/fixtures/bug_example_forms/extra_columns.xls b/tests/fixtures/bug_example_forms/extra_columns.xls deleted file mode 100644 index d05366933282eb2283bd329b0855f9a21a151e46..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 48640 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 B);0hD diff --git a/tests/fixtures/bug_example_forms/extra_columns.xlsx b/tests/fixtures/bug_example_forms/extra_columns.xlsx new file mode 100644 index 0000000000000000000000000000000000000000..1c861e4a14b4d2134f3bc9306abc0b6e0be013af GIT binary patch literal 96159 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 literal 0 HcmV?d00001 diff --git a/tests/test_xls2json_backends.py b/tests/test_xls2json_backends.py index 48eba5e3..c333ad09 100644 --- a/tests/test_xls2json_backends.py +++ b/tests/test_xls2json_backends.py @@ -173,28 +173,29 @@ def test_equivalency(self): self.assertEqual(xlsx_inp, csv_inp) self.assertEqual(xlsx_inp, md_inp) - def test_xls_with_many_empty_cells(self): + def test_form_with_many_empty_cells(self): """Should quickly produce expected data, and find large input sheet dimensions.""" # Test fixture produced by adding data at cells IV1 and A19999. - xls_path = os.path.join(bug_example_forms.PATH, "extra_columns.xls") + xlsx_path = os.path.join(bug_example_forms.PATH, "extra_columns.xlsx") before = datetime.datetime.now(datetime.UTC) - xls_data = xls_to_dict(xls_path) + xlsx_data = xlsx_to_dict(xlsx_path) after = datetime.datetime.now(datetime.UTC) self.assertLess((after - before).total_seconds(), 5) - wb = xlrd.open_workbook(filename=xls_path) + wb = openpyxl.open(filename=xlsx_path, read_only=True, data_only=True) survey_headers = [ "type", "name", "label", ] - self.assertEqual(survey_headers, list(xls_data["survey_header"][0].keys())) - self.assertEqual(3, len(xls_data["survey"])) - self.assertEqual("b", xls_data["survey"][2]["name"]) + self.assertEqual(survey_headers, list(xlsx_data["survey_header"][0].keys())) + self.assertEqual(3, len(xlsx_data["survey"])) + self.assertEqual("b", xlsx_data["survey"][2]["name"]) survey = wb["survey"] - self.assertTupleEqual((19999, 256), (survey.nrows, survey.ncols)) + self.assertEqual(19999, survey.max_row) + self.assertEqual(256, survey.max_column) - wb.release_resources() + wb.close() def test_xlsx_with_many_empty_rows(self): """Should quickly produce expected data, and find large input sheet dimensions.""" From b39858db3e8e1994e22278a024a2d672fecd8b32 Mon Sep 17 00:00:00 2001 From: lindsay stevens Date: Tue, 14 Jul 2026 22:45:47 +1000 Subject: [PATCH 24/50] fix: delete missing group name test case and fixture covered elsewhere - scenario same as test_group.py test_group__no_end_error__no_name --- .../bug_example_forms/group_name_test.xls | Bin 20480 -> 0 bytes tests/xform_test_case/test_bugs.py | 1 - 2 files changed, 1 deletion(-) delete mode 100644 tests/fixtures/bug_example_forms/group_name_test.xls diff --git a/tests/fixtures/bug_example_forms/group_name_test.xls b/tests/fixtures/bug_example_forms/group_name_test.xls deleted file mode 100644 index 0469e4433994c9897ce32c9bf7549f1ae84336e4..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 20480 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 diff --git a/tests/xform_test_case/test_bugs.py b/tests/xform_test_case/test_bugs.py index 2c33b501..954642fc 100644 --- a/tests/xform_test_case/test_bugs.py +++ b/tests/xform_test_case/test_bugs.py @@ -22,7 +22,6 @@ class TestXFormConversion(TestCase): def test_conversion_raises(self): """Should find that conversion results in an error being raised by pyxform.""" cases = ( - ("group_name_test.xls", "[row : 3] Question or group with no name."), ("duplicate_columns.xlsx", "Duplicate column header: label"), ) for i, (case, err_msg) in enumerate(cases): From 75fa6a101c1a64cac8f7ec14d5a83781a1c52b39 Mon Sep 17 00:00:00 2001 From: lindsay stevens Date: Tue, 14 Jul 2026 23:20:29 +1000 Subject: [PATCH 25/50] chg: replace empty relevant xls test with md/dict test cases - The deleted file had a single question where the "bind: relevant" header column had a single "LF" character. The "bind: relevant" header is an alternative spelling of just "relevant". - The replacement tests document existing behaviour in two ways: - md data processed via xls2json_backends removes the empty col. - dict data passed straight to xls2json just strips the LF. - pyxform does not warn or error, Validate throws an error. --- .../bug_example_forms/ict_survey_fails.xls | Bin 24576 -> 0 bytes tests/test_sheet_columns.py | 31 ++++++++++++++++++ tests/xform_test_case/test_bugs.py | 15 +-------- 3 files changed, 32 insertions(+), 14 deletions(-) delete mode 100644 tests/fixtures/bug_example_forms/ict_survey_fails.xls diff --git a/tests/fixtures/bug_example_forms/ict_survey_fails.xls b/tests/fixtures/bug_example_forms/ict_survey_fails.xls deleted file mode 100644 index f979fecfd419443dd39b9ce0c4fcf157663ed532..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 24576 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 WKWTMAtRqf+`PY$bozecC`TsvQ_*%UH diff --git a/tests/test_sheet_columns.py b/tests/test_sheet_columns.py index 4a6c3fa8..cc16970e 100644 --- a/tests/test_sheet_columns.py +++ b/tests/test_sheet_columns.py @@ -194,6 +194,37 @@ def test_missing_survey_headers(self): ], ) + def test_empty_bind_attribute__via_md__ignored(self): + """Should find that if provided as md data, a LF ignored entirely.""" + md = """ + | survey | + | | type | name | label | relevant | + | | text | q1 | Q1 | \n | + """ + self.assertPyxformXform( + md=md, + xml__xpath_match=[ + """/h:html/h:head/x:model/x:bind[@nodeset='/test_name/q1' and not(@relevant)]""" + ], + ) + + def test_empty_bind_attribute__via_dict__empty_string(self): + """Should find that if provided as dict data, a LF is stripped back to an empty string.""" + self.assertPyxformXform( + ss_structure={ + "survey": [ + {"type": "text", "name": "q1", "label": "Q1", "relevant": "\n"} + ] + }, + xml__xpath_match=[ + """/h:html/h:head/x:model/x:bind[@nodeset='/test_name/q1' and @relevant='']""" + ], + odk_validate_error__contains=( + "Encountered a problem with display condition for node [${q1}] at line: , " + "Bad node: org.javarosa.xpath.parser.ast.ASTNodeAbstractExpr" + ), + ) + class TestChoicesColumns(PyxformTestCase): """Invalid choice sheet column tests.""" diff --git a/tests/xform_test_case/test_bugs.py b/tests/xform_test_case/test_bugs.py index 954642fc..580ffd9b 100644 --- a/tests/xform_test_case/test_bugs.py +++ b/tests/xform_test_case/test_bugs.py @@ -21,9 +21,7 @@ class TestXFormConversion(TestCase): def test_conversion_raises(self): """Should find that conversion results in an error being raised by pyxform.""" - cases = ( - ("duplicate_columns.xlsx", "Duplicate column header: label"), - ) + cases = (("duplicate_columns.xlsx", "Duplicate column header: label"),) for i, (case, err_msg) in enumerate(cases): with self.subTest(msg=f"{i}: {case}"): with self.assertRaises(PyXFormError) as err: @@ -50,17 +48,6 @@ def test_conversion(): survey.print_xform_to_file(output_path, warnings=warnings) -class EmptyStringOnRelevantColumnTest(TestCase): - def test_conversion(self): - filename = "ict_survey_fails.xls" - workbook_dict = get_xlsform( - xlsform=os.path.join(bug_example_forms.PATH, filename) - ) - with self.assertRaises(KeyError): - # bind:relevant should not be part of workbook_dict - workbook_dict.survey[0]["bind: relevant"].strip() - - class BadChoicesSheetHeaders(TestCase): def test_conversion(self): filename = "spaces_in_choices_header.xls" From 62a2d59756e7e4ca2a6de36f9e346f3fccfa26a0 Mon Sep 17 00:00:00 2001 From: lindsay stevens Date: Wed, 15 Jul 2026 00:03:59 +1000 Subject: [PATCH 26/50] fix: delete empty/spaces xls test and fixture covered by other tests - The deleted file had three test cases, with existing coverage: - choices header with spaces: test_sheet_columns.py test_choice_filter_columns_not_normalised (case "e f") - missing choices header: test_sheet_columns.py test_process_row__bad_header_info* tests. - row values with spaces: test_whitespace.py --- .../spaces_in_choices_header.xls | Bin 8192 -> 0 bytes tests/xform_test_case/test_bugs.py | 40 +----------------- 2 files changed, 1 insertion(+), 39 deletions(-) delete mode 100644 tests/fixtures/bug_example_forms/spaces_in_choices_header.xls diff --git a/tests/fixtures/bug_example_forms/spaces_in_choices_header.xls b/tests/fixtures/bug_example_forms/spaces_in_choices_header.xls deleted file mode 100644 index 987ba5ff21ad7cdc5ebb1c94552feff3109c8dc8..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 8192 zcmeHMU2Ggz6+W{+&N_cyr*#M^jVFfsCvohieINuCyG=u>N=TH3pNC4@+1;@{$?nW* zXH2j}gqx5jQVG-tUaF#^yp$qZgcSaZ^9E9Zsv=V0$>9YGh;C64iqvHJzI$hO#@lRd zhqkDZ&*sj(=iGDex%b@jbFcHt?|T=Y{o(d2(gvTDv|Mj?OILz!puH)=^CBD2ChdB& z*^J0&uE7UL1J}Z}rg4B7lq?F*d>u*-r5j~E3ePwyn?w8qw9U1pcF@34@$d`8l!M}; zuFD)wcjd4Z7L%NMB_osZVkP{`@vO;@HL#qn*q_vYcMgHPD=RoC z8}WCdbJiR2-@5ek(5cDog%Ig^x7I_Woj%fmKH7o4s{_6JhIF30S`U-5f(7%b6ufd? znymBlX;t^mcd2^Id`8u`&977S?ep2@u4$-~x4GoDkj z{j-vnGwQCF6Wschp+i|#V!SfDtet-#WpLBPqyk@A?&d#m8}t**X6!n);d;Tap2sc^kByo`rJ)hUwS-t68W{nzR|_1^ZB0|MTnsmhwiz+E zW`=q#vxGZbP@lDjM_Su3J=Zp;>RwfC1Ilh~W4*|uYNzI5*G31oX;P~=of+FI+jaA# z6U>-?&4Ot_#bm%qTCl}%T^5hSR@7olE#jtFb$lPzW9o@8sucH^fR$x{xJybk0QBd@G zb$Qkk-9&6)%H2g`QEgudDAM#=rhg!e1ta676BMUmZGyf3HuB zB-+nnLk3nw4`Z4fHAamM7aqsI#fhjko10W@X+<5-QQ)LBJX8W1Zqqj1M4lC69v{jYqdXd&#wd?LR%4V$&1j7BxF=%Va}OeNgi&5^3!^;7 z(SOJhv{H@Fpbd*`P_tYqH;6qq?nV>4RWaI-?uOPm8DdDudNqz?sEJRdw#Wt5JsC5} zot%%0q5ztT$TWiVG$n=r=isH3*sbYD-H{&|*F!sx?3#Mh7wO2ZGYNYoji=I1qY}H? z_-Lo?e6-dijrKw4k=yXq*mCyIh@-8H;~0B1#&O7JAx67G<_$60@wO1t_85(09PE(~ zxB574dkb4Sjuwx^sOIGEV(Do$%ST#$oaN3ImYjteh{y3x(t~Tfo-+eu?2-N!7L7~# zhMDvYnCz;317nu)jkIDg5NOcKzHh!EM&}&!G1)0Hcwlhgm{YZV^K>GR!G5$xjF>oQ7+{df9phllh79*N{o%mEZhr?&{Be*L&`?d`}*EZUZ$ymmNi! z!C!L%yxe`+FXQ-m(Dn%#L(O;X7+$a*sCBem^mtO00=W71^%c)J>RBm*?x5u>ei!pD zw#od`dnikXHV9aQ_ecXYw&?G=46L+L;=JmxUV~d(0|rhx4L9R1^En+s7jX7~Pcu;; za!Pg2_of2#;8#j^#r(v__Z;IxyJD-m@dM7(6rWT#4X{5g^d@Jxjw>ebpzT1rUOfAq ze_cI1lYjnOIoUn*qd)Qj{8bbNVD@w2ytYrGFw%S$g;#Y(Gz4swop?R3p-{qW*F;@3 z*uDp45@r6E&z~dCb_A1Xy!eN2Y`uKtWrOWgkIY>d-g${4-v*u5&pA_yJ*8J8BhN2hOZw(4nS@dXM7lIIKGnMOL!=s7g@?CEa`j)7_x{!HpZ*_&^52bWwepky|MLHq|6dQ+Cbj?o diff --git a/tests/xform_test_case/test_bugs.py b/tests/xform_test_case/test_bugs.py index 580ffd9b..7e4740be 100644 --- a/tests/xform_test_case/test_bugs.py +++ b/tests/xform_test_case/test_bugs.py @@ -5,7 +5,7 @@ from unittest import TestCase import pyxform -from pyxform.errors import ErrorCode, PyXFormError +from pyxform.errors import PyXFormError from pyxform.utils import has_external_choices from pyxform.validators.odk_validate import ODKValidateError, check_xform from pyxform.xls2json import SurveyReader @@ -48,44 +48,6 @@ def test_conversion(): survey.print_xform_to_file(output_path, warnings=warnings) -class BadChoicesSheetHeaders(TestCase): - def test_conversion(self): - filename = "spaces_in_choices_header.xls" - path_to_excel_file = os.path.join(bug_example_forms.PATH, filename) - warnings = [] - pyxform.xls2json.parse_file_to_json( - path_to_excel_file, - default_name="spaces_in_choices_header", - warnings=warnings, - ) - # The "column with no header" warning is probably not reachable since XLS/X - # pre-processing ignores any columns without a header. - observed = [ - w - for w in warnings - if w == ErrorCode.HEADER_004.value.format(column="header with spaces") - ] - self.assertEqual(1, len(observed), warnings) - - def test_values_with_spaces_are_cleaned(self): - """ - Test that values with leading and trailing whitespaces are processed. - - This test checks that the submission_url provided is cleaned - of leading and trailing whitespaces. - """ - filename = "spaces_in_choices_header.xls" - path_to_excel_file = os.path.join(bug_example_forms.PATH, filename) - survey_reader = SurveyReader( - path_to_excel_file, default_name="spaces_in_choices_header" - ) - result = survey_reader.to_json_dict() - - self.assertEqual( - result["submission_url"], "https://odk.ona.io/random_person/submission" - ) - - class TestChoiceNameAsType(TestCase): def test_choice_name_as_type(self): filename = "choice_name_as_type.xls" From a6979341aa466df052bf4da4668b02dfe91da4d2 Mon Sep 17 00:00:00 2001 From: lindsay stevens Date: Wed, 15 Jul 2026 00:30:35 +1000 Subject: [PATCH 27/50] chg: replace comment column/row xlst test with md test case - The deleted file had a single question, laid out as shown in the new markdown file (but in a more wordy way). - The xpath assertion checks that the "_comment" row doesn't end up as an attribute in the question binding, which is the most likely place if were to be processed. --- .../example_forms/allow_comment_rows_test.xls | Bin 29184 -> 0 bytes tests/test_j2x_creation.py | 39 +----------------- tests/test_sheet_columns.py | 15 +++++++ 3 files changed, 16 insertions(+), 38 deletions(-) delete mode 100644 tests/fixtures/example_forms/allow_comment_rows_test.xls diff --git a/tests/fixtures/example_forms/allow_comment_rows_test.xls b/tests/fixtures/example_forms/allow_comment_rows_test.xls deleted file mode 100644 index 6e51ada37c074174d95d78733ed78d1daab7dcfc..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 29184 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�VHY;bVd)d`$3!kKy?EL4hSym50;? zOG~Kt7$|eLVV%!cRyEQAnb|ijHv?+RPnt_r6IugLkmf==VSSmBTY=>6$f?N6#RjLg z8dP;T6l58hny$izzUs@rCjB+5IppD3Y`0Pv^q4?1>Nb0E476#@4U?Cg2 zkuXTHMp3+Hb2U6$J&?>6?TO5V(mBAZFDOaiO4|oq9>;x)g0kb-ho`&SLoKF}F&u-~ zDFVqnIO~j0Jz(;7-~cHqDuPu8*o8POHsBK@I$$(h`nLl3EWikS!8`@1uRv`DoG#SO>*F+VIshP$iOW|3`E|NtQVeljG2{ikFs_h*WE}~p zr~n*NV~re#AU9ujB7>rqWsvL-n`dxSO{ zvS2dE5*HSqt4)EMK8&)wEDj~Gc+H4U&}MwlB{K!Irt08mxtgqnU!MT6Nqu-qH3>e8 z)J1khBHsZbF+EfkPEe5uHzrGe9U1)wzBPbi)q>C1u&m5n*x`%o3m)eT@yQ#J@y!<} zhadK}@A7KOyTa2uE@kibTzl!qujUMCH0y=RYFx&wOYV)MLjQQ>+QDamqjE~7#o3DT zR~73AH#!#b*stmR4QDs)JzeelzmD}9@Y_$PdW_l9FJYyXRrJ8$+?$q{Y}xX|DZgR8 zzxulGZ(p|W=rTWJj{BNx)*c(Scb)Xiwb@i^de7(YM|Ma|eMAuobwf6$f>>IKC=&`HUJs+M(xpVYUG})w^@$SdK z=Ql6JK~?BrelD%7wHMaiKLC<8Kn*;L5eGjs$Z6$+?~~{J1eIhr?a7q#!ZZ23&K`DY zGN#qqxnZqV=aXBW6H?vkU#idYJ1d^*F1T(q0_sPS+8#`xUX?uveD}B?XaMe zskg>-+CO2^_fyuaYod1;_oPIfaZ|{=Oq%+^! zpY7h|YG8uLgmqtxio4io@t1Wg+eH5S(z*A=w!=#1tp8rO?XYjfm(d3&Z1MV~i$!MJ z1(9iY6IQKnb*E4H-1XXx=Ng_a+xhy!TPTWYLDR<-ITu0!XF|Z|a=EG65!zOw_b8Z& zrf9Q~9(EN`)edXygKy=p8awxL+r&MdT@p%9mhAbHge|KVa(w046<06*a;LEEk2jsp zFYQr%kk>Nn>NB;ce5QZ*biCKP`s=SmG&t&&aqD>Pd%xwme82tkS~t6$$mx~uetzzh z&1Kg$7s^_mZ~VmT`p{h-!#mCTaKNuWziNKF%fpsUt0~*Do%gLdJ>3B&3^$;YIkTMi zL$@Cdiq5Lv#c99$GuN)4WF%Qp>mPe|4|?=HCGm{(Mk& zXYr~qH;-Ss1*{)hVlmyLaigICj`9bU)lb}hnDeiX()U~+I`*km(ldzszVO(`r;gT-Ig*;~ zJMDtv@tA;})qjzDo~qfM(#n%tf?mHpS@TR*^!|@u|0^J9XwkEv39rsX2Yvd$ zI@I!%*T>IZ&yV}E)3|3xkMIBV`6bm&=e(GyuhJqtc1O)V-FeQzwqc*8x34?>dAAqt zk*+okCT7lgFneZ$%E!%$3eMulE2UURzj^EJ6UvpziHmow*9_$|L~(?kDq z4bmLBb3MPi>(H@jo;&ZlPx-yixkKlAbX!qlS@;s0>nr_^XdgVU8~S+Uwohu7wR=9U zJgmkL4=0P{hIjs++&*yV@mf<7SNza<_MqaYu>-qwcK@W0L(wSL4VtrwhuS@`o3WsC zaR=oaZ_Bq$r+)fN^}l_V9`ot#`EljX<7{(=cK!Wn;h4#H&P{uK#-l~z&H2`IzjApz z>X*VN+24h}2sol>W_vh)j$^ZHDQze1$$hjqu!sHIWVeo6_x9X5e}8%N`tb?7?Y8Hx z>vKGC+>oouPJiuKerv?lOslKkPJcb~{;>_oD5(* zy7AM7_B~nW=Ax9%F1xqin0u*w$K`RyZQI^(|6yQsRNY-oPThz-qzrs?Xk}5rw4S{i z96780^g;KjHVv)~9OhnVF`!XQ?U!!L4(%(im)az;ctgV;3$ktQcL{iOe)F?$Uk&(k z`=E>W?XR}?=)Y%t>a>e5Mt*wD^PJ13WA`GLMsNSRwpWMCR~AM#|JR>Q=l3h?)#IoB zt-mh*`qzs7JHKh(=6?3dy(6nz*|v#KDI6AL;kD)P*0|{E_gyPa#5~VTTrllu_^mBA zz3!Eb_Ba%l5O&vY(WP&eT>7PX;QbY2e!6$!^(C)?srEzxd9Iq)ka;h>pJr6log|@h0OoB)%43QC9^`CWL)@i(5$Z`QW~~> zGpOH`ld~)>?<^SAV$GF<{-u6_TS{ZLhX0iGIBxUB+|nVRslH4uT~<52{#-ZR;@bAZ z+ip(^bX(QJdQae2ySB|4)a-7pF5#`}HC&}wIz8X+mUYjvps*IV4jppZ{%OJ0)~9xi z`{mCI-~7Db$#%ORXD1eQZTGO~uQ5%xw6ksdcFng3J1(qH-Q2sQ{{0`H%q;oWtn9VR zj_CphyvQbP)#+ib6YYJ*T6W#tdx@e>i`%ECoSi;(h~nYC-(2TTsf*OUSJgS!`R246Gb^7L{!%lff85z2)%_#dM*N*l z{%+VIb$+)^rRS16G&=RszJYO1yOu3}<#hQ`Z z(K`Ik+!=>Xdj?Hgd|+|SA-`nx8vm3i%T?dKtChUJ=l4!uM!K%~l;u zDScbd74d=dn*5zK`PZlIf+jdcUUDpri}=I4G$h?`dFqtPB~MOQR!h$F9zNh?Xx__= zTcL+4?7a8bx2ZF{v*pu#uk>qwrCR4*P1<~M_s;CE2mX=b8G9%%NPA=cPo)bk7v4@+ zy|h}e;m*ZAMShldPc>^e{+5%=?4g~0o)CX#YjI}pF%R*uxaka_`kA>ac7UWl%n$G( zxai*Bil6pyh-e<5p9V!$JLKy$`PHPc>a47vciVr_^!oJEo6o5x-)&vJO^`>gM~|zQ zc&UA+#qTRl+p}o-_q7uJZ}_Edtf&6qw}qpY92~Lp(d^*f&OK*b?vV8Dut~GC+CHwG z@TrQAU$Hl4C=t`c(h4(L!fObWz821$xu5OxKkv1d{Jhsz>4jqMIt=gI;P_lw zC?OLg2ssr6Khf_?NWYH=SphTjxMBFyI{c}u9lR&Ok`TW;z&rl@UrpX@cy=}_Atoq} z!&`88OYVIVhZJyYAkGwN0^1z0`9mgx&yKux_q_J}J34<~px2#t|=QV2rC^>3iPO7>8d;kbVxvIM!c_ zTE;k%RV;_Kq5|W%r9xmU6ECbm{&b33i6{MV0d2v1t*A*5!-y5BlweqYR^%xr2c3L6 z6?~;JTv^i?Z2XvD*!;9zn4g=Z!xbs54_DYU2HZ2jkW_R!TnW<{teZ_RtSdSl)+3E! z4O&w5f@cM3%#nfN+L}&>ETgg73=9`BbUHjhnK8I9rbDjLJ%MKp#hKN`am zi8O{M@@T9Pg}vo^Jb|AmBOCY_{tOto#>bQpL-z176~vGa`?!1-Bp4o&T5>X^YADCs zeXuQry(*M@h2egV(u&g<&ZWfBARJQjI9js%I*lRIX$+Z8W5{$GL#ERhGM&bd=`@B+r!izYjUm%%44F=2 z$aES*rt?@ALu(?_>2%0+8bhYj7&2X~?;^y813fa7U5N(x?!>*E^`Cs~eco|&)_`t$ z6l%=C+!&ZU1M^^DO&C~H28Odh+80j-)|`R0U|=m77~cD78{P~IwmD7GwPIktfT^hw z8y_^PskDFF(fFBAwk6jMQ6b|gR_%@k%X>q!Pz1rIExIX zYXTV@4vz@K;fb9vVk3jY;lU3Y6)>NsE4b4*bAM_|z6VceYtSyaV!r8&Ggt7!eP=G zc_<@Y7y}DuU|kqkR|fVW1B+l_-56MR2G)atMKUn#=X4$PWMI(@EQW#gVqm>FOj@UX z80q>luzn1zKLZ=Uzy>m~K@9981{TY}1~afB3~VR^i=!~?0l|Pt+3pB)Y9nTxu_6Pd zDeYk-F~yL9bUI`pjUfYR3>ipc$UquH2GSTZkj9XKG=>bMF=QZ(Ap>a)8AxNuKpH~^ zN-;nD7}!__mdC)xF|dyr*e48Z zJOj&TV4pIu2^6*u{&eUtz=S>!0uw_1rG}Y?3mo<_%KA|<5V0f!CHaxBLjEvHhP;)NA%Ep$$YVJf@>x!Xyq1$8zvX1eb2%CE-6$I* zDLeAsD3jI;^4}oVg^>sz?Lwur3`Et16$6(zT~i{@D7*$ zNy-ZNDxSOGJ);0HBgds<8}iU7la5iyN25$SwjnQ#GHE{Kr<@FVDknp}%E^$oax&yE z%07ZXk3+~~l;MsAEkiz|j5Y2dujT9^zvX1eb2%CET~3C)my;p?GVr6o&l9 zyy38yhV>=12j=a3P+DrDg@!g0iNT*U%n{ebWV!84oTsXa6-Vi|A(Lo zf4>7zHzC*dctSo(AS4Mw283J);~`9juph$z-v^-1*E1ei!lN1mF2*<>$mrM57*|1) zhz~RYvrG;&VT~YB;87)z1}~Lp8&Ei863ZRq5DuBda_NObCb6*HLZIob;L$u@7rEmd zRez$r8<18^QY-k+F^ENRn($2ndR^bZ|IN5cR0q&V04G9j!4DE3z~2~#caB?}BpBYS z<`xzcxy7B-RJ|9sAnkwrg#}hZ!2g?r)rRekJrd*Ciun28>wj+zywd^kt04=d7b2BY zR{ZfgI@NyD7i!Y7*|y8RfZ?(aIf8PGBS+dnaD>noLUjl@;MamM83J<4z8U{NpPb-3 z{ZC{7;{G301Ne+eOU%y7$x6=EM8zkjW#(q(q-wh4C2BK377Sr!GyeYv@wWj0;4|-(3U$1MpOZ Ah5!Hn diff --git a/tests/test_j2x_creation.py b/tests/test_j2x_creation.py index 531c3067..4ef1d544 100644 --- a/tests/test_j2x_creation.py +++ b/tests/test_j2x_creation.py @@ -2,9 +2,7 @@ from unittest import TestCase -from pyxform import MultipleChoiceQuestion, Survey, create_survey_from_xls - -from tests import utils +from pyxform import MultipleChoiceQuestion, Survey class Json2XformVerboseSurveyCreationTests(TestCase): @@ -33,38 +31,3 @@ def test_survey_can_be_created_in_a_slightly_less_verbose_manner(self): } self.assertEqual(expected_dict, s.to_json_dict()) - - def test_allow_surveys_with_comment_rows(self): - """Assume that a survey with rows that don't have name, type, or label - headings raise warning only. - """ - path = utils.path_to_text_fixture("allow_comment_rows_test.xls") - survey = create_survey_from_xls(path) - expected_dict = { - "children": [ - { - "label": {"English": "First and last name of farmer"}, - "name": "farmer_name", - "type": "text", - }, - { - "children": [ - { - "bind": {"jr:preload": "uid", "readonly": "true()"}, - "name": "instanceID", - "type": "calculate", - } - ], - "control": {"bodyless": True}, - "name": "meta", - "type": "group", - }, - ], - "default_language": "default", - "id_string": "allow_comment_rows_test", - "name": "data", - "sms_keyword": "allow_comment_rows_test", - "title": "allow_comment_rows_test", - "type": "survey", - } - self.assertEqual(expected_dict, survey.to_json_dict()) diff --git a/tests/test_sheet_columns.py b/tests/test_sheet_columns.py index cc16970e..064f3ef6 100644 --- a/tests/test_sheet_columns.py +++ b/tests/test_sheet_columns.py @@ -225,6 +225,21 @@ def test_empty_bind_attribute__via_dict__empty_string(self): ), ) + def test_ignore_additional_columns_or_rows(self): + """Should find that additional columns or rows (such as for comments) are ignored.""" + md = """ + | survey | + | | _comment | type | name | label | + | | looks good | | | | + | | | text | q1 | Q1 | + """ + self.assertPyxformXform( + md=md, + xml__xpath_match=[ + """/h:html/h:head/x:model/x:bind[@nodeset='/test_name/q1' and not(@_comment)]""" + ], + ) + class TestChoicesColumns(PyxformTestCase): """Invalid choice sheet column tests.""" From 4cab37f5168943492d196deecdee22418bc452e2 Mon Sep 17 00:00:00 2001 From: lindsay stevens Date: Wed, 15 Jul 2026 00:52:31 +1000 Subject: [PATCH 28/50] chg: move last "j2x_creation" test to similar j2x test file - there does not seem to be a meaningful difference between the purpose and structure of this test and those in j2x_instantiation --- tests/test_j2x_creation.py | 33 --------------------------------- tests/test_j2x_instantiation.py | 28 +++++++++++++++++++++++++++- 2 files changed, 27 insertions(+), 34 deletions(-) delete mode 100644 tests/test_j2x_creation.py diff --git a/tests/test_j2x_creation.py b/tests/test_j2x_creation.py deleted file mode 100644 index 4ef1d544..00000000 --- a/tests/test_j2x_creation.py +++ /dev/null @@ -1,33 +0,0 @@ -"""Testing creation of Surveys using verbose methods.""" - -from unittest import TestCase - -from pyxform import MultipleChoiceQuestion, Survey - - -class Json2XformVerboseSurveyCreationTests(TestCase): - def test_survey_can_be_created_in_a_slightly_less_verbose_manner(self): - choices = { - "test": [ - {"name": "red", "label": "Red"}, - {"name": "blue", "label": "Blue"}, - ] - } - s = Survey(name="Roses_are_Red", choices=choices) - q = MultipleChoiceQuestion( - name="Favorite_Color", - type="select one", - list_name="test", - ) - s.add_child(q) - - expected_dict = { - "name": "Roses_are_Red", - "type": "survey", - "children": [ - {"name": "Favorite_Color", "type": "select one", "list_name": "test"} - ], - "choices": choices, - } - - self.assertEqual(expected_dict, s.to_json_dict()) diff --git a/tests/test_j2x_instantiation.py b/tests/test_j2x_instantiation.py index 35b66e8b..3af7bb8a 100644 --- a/tests/test_j2x_instantiation.py +++ b/tests/test_j2x_instantiation.py @@ -2,7 +2,7 @@ from unittest import TestCase -from pyxform import Survey, SurveyInstance +from pyxform import MultipleChoiceQuestion, Survey, SurveyInstance from pyxform.builder import create_survey_element_from_dict from tests.utils import prep_class_config @@ -95,3 +95,29 @@ def test_simple_registration_xml(self): self.cls_name, "test_simple_registration_xml" ).format(reg_xform.id_string) self.assertEqual(rx, expected_xml) + + def test_survey_can_be_created_in_a_slightly_less_verbose_manner(self): + choices = { + "test": [ + {"name": "red", "label": "Red"}, + {"name": "blue", "label": "Blue"}, + ] + } + s = Survey(name="Roses_are_Red", choices=choices) + q = MultipleChoiceQuestion( + name="Favorite_Color", + type="select one", + list_name="test", + ) + s.add_child(q) + + expected_dict = { + "name": "Roses_are_Red", + "type": "survey", + "children": [ + {"name": "Favorite_Color", "type": "select one", "list_name": "test"} + ], + "choices": choices, + } + + self.assertEqual(expected_dict, s.to_json_dict()) From 9f9b38ccb117b8d6a6540ab092c2d07b2ca3a882 Mon Sep 17 00:00:00 2001 From: lindsay stevens Date: Wed, 15 Jul 2026 01:13:18 +1000 Subject: [PATCH 29/50] fix: delete calculate test fixture not referenced anywhere - the form had calculate scenarios covered by test_calculate.py: - a static calculation `2+2` - a dynamic calculation of the form `if(${q1}=${q2},'y','n')` - note items with label that displayed each calculate value --- tests/fixtures/example_forms/calculate.xls | Bin 6656 -> 0 bytes 1 file changed, 0 insertions(+), 0 deletions(-) delete mode 100644 tests/fixtures/example_forms/calculate.xls diff --git a/tests/fixtures/example_forms/calculate.xls b/tests/fixtures/example_forms/calculate.xls deleted file mode 100644 index 9aa840fdaa72014112f327bfc1c781fab2a332fb..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 6656 zcmeHLU2IfE6h3!<$`%UU7K#c|uN7!Z3(}$jA*IVt8N*=}#k%C_6=-ZeDZ zxTqvPNFu?+7ZO9li$C!vCitLxk{;U@ePz`=jot`2W9eZqA2!hsaL;D}0iY6K-5e*sjixp;)G}6JuO#qiiz!_)h&(C7*y-}qLRgGj zO*uW?=S4Z~Z`Ni;e_;h~wpOfnuRqUkp5tDil>hV0r~g+0bAfq4HGmKxH9##eA6Nj? z0SkdUfIES^fJMM!;BH_EP!HS#n83Y218^U(6j%l<2O5DUpcz;Jv;eKZN?;YhIjnYa zme-*C0Pvv0TkGV7m6<6`wgQy_>|%vqKAwA)N4`4yFTx_;_nlF8@jZA?$q;R0OC%mO z|EaWDjUi&tHb-Hk9fl8g4Ue1v=E-}C|B3o`DEmXRkeKU`@lG-e1bZG;-v;%ahp!;nohRPEqWceZ7Sv0zqtgz zwFG}f34Z0Id_OF~5R)$GB&}ETaXz~E$35`TjO3UHGhUU~WGYl%oAIf9erA@+7i9do z{qO`sj=-B%sY`x^q|(V}tl=Y=|3+T>V4o?=wqt|+saN3t2L8?%0YGHmQA2)~9!7!e zlWJrYSyOjWk%aa{&_0QLd5z4;GFp5J*#S9hKuuuO2;m!&`paB*#Fqf#OaSpC@CQQ7 zH3=JxC5$hapFKQeAsdVgSh%UiBRy6ektvFkDIJg4vBV%G<|zwtYP=k_t(1)**WS|3 z{t4Scq?%Y8PNeOjv<+=&WOkTKpPg7gg7Hn{gxhzB#QK_wTRM_PI!aZvnvJP+G-{<% zjVM~lWFpzv48_Ib9YL7R;S_bIr?AvmFdUMMpwBB<#UZA_`lTI>?)s>2E1H)p^{eXkyfMCm~(nR{>M7=xgn3C zrbH56zstUR(_)8gTMNWYx)9V+0VC9}Mf#xEhGTF4_& z%%f*`i`wbC*u=iroeAx(J1=p)J(@Cb`=Sm_d2kP-bWM3tN4ccfrjFj^Q0&KdpW&%M zod=X|<;AY@Xdp#Nb?Q~9DfZ!>YKnb$WRRkkf^;t=)~T`TI?Ci-k`zZ!kV&hkQ+uK7 zD3iC%Ut|xhE4v+KSXP!=)v}^{wRp3uL05|}+obrFS-tikT*)J|RX?iaOgDOJ<+M8W zLatUzC~vl!vVq~DRMC%vsGl001S97yiq!P_Lr(r1{!lsY4?ip{*gO5YKk_) z{nr$2p6?S)`OuEBhm`gJT7sr1g_PUYOt-DU1g30j=re9>kS!8YU;RP~WFnR5v(2tUQ7dk)U%PT2qh{PvcD^~* z*QYPdE}2s)VkNcGsdRF+y#Zh1nNys&Lt{tY`0eWM1HreC1*E0%!*A*7uK|3hus#IP zH@5>keVzblA-w?4m?3~6?Kyxa$;$vW{d8TwL3seAQ&!SM&NnH~qqf!UN^Yht-CO~o zA@K4klrJDxW#ho*ufI9Vq1KDkVjc460%>N|v)` From b9d24d4ad21416574bfdafe2ec7fded2d8023b18 Mon Sep 17 00:00:00 2001 From: lindsay stevens Date: Wed, 15 Jul 2026 02:11:32 +1000 Subject: [PATCH 30/50] fix: delete cascading select test fixture not referenced anywhere - the form had an interesting approach to implementing a cascading select via calculate rather than choice_filter, so that was translated into a new markdown test case. --- .../cascading_select_test_equivalent.xls | Bin 22016 -> 0 bytes tests/test_calculate.py | 56 ++++++++++++++++++ 2 files changed, 56 insertions(+) delete mode 100644 tests/fixtures/example_forms/cascading_select_test_equivalent.xls diff --git a/tests/fixtures/example_forms/cascading_select_test_equivalent.xls b/tests/fixtures/example_forms/cascading_select_test_equivalent.xls deleted file mode 100644 index a8e63f9014428203a13f0f9d8a9e75516f9e3dfe..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 22016 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 diff --git a/tests/test_calculate.py b/tests/test_calculate.py index 3a786a4b..85e26e41 100644 --- a/tests/test_calculate.py +++ b/tests/test_calculate.py @@ -4,6 +4,7 @@ """ from tests.pyxform_test_case import PyxformTestCase +from tests.xpath_helpers.questions import xpq class TypedCalculatesTest(PyxformTestCase): @@ -195,3 +196,58 @@ def test_round_function__ok(self): | | calculate | q2 | Q2 | round(${q1}, 0) | """ self.assertPyxformXform(md=md) + + def test_cascading_select_via_calculate__ok(self): + """Should find that cascading selects can be done via calculate without error.""" + # Easier with choice_filter, but it could be done this way. + md = """ + | survey | + | | type | name | label | relevant | calculation | + | | select_one c1 | q1 | Q1 | | | + | | select_one c2 | q2 | Q2 | ${q1}='n1' | | + | | select_one c3 | q3 | Q3 | ${q1}='n2' | | + | | calculate | q4 | Q4 | | if(${q1}='n1', ${q2}, if(${q1}='n2', ${q3}, 'Error')) | + | | select_one c4 | q5 | Q5 | ${q4}='n3' | | + | | select_one c5 | q6 | Q6 | ${q4}='n4' | | + | | select_one c6 | q7 | Q7 | ${q4}='n5' | | + | | select_one c7 | q8 | Q8 | ${q4}='n6' | | + | | calculate | q9 | Q9 | | if(${q4}='n3', ${q5}, if(${q4}='n4', ${q6}, if(${q4}='n5', ${q7}, if(${q4}='n6', ${q8}, 'Error')))) | + + | choices | + | | list_name | name | label | + | | c1 | n1 | N1 | + | | c1 | n2 | N2 | + | | c2 | n3 | N3 | + | | c2 | n4 | N4 | + | | c3 | n5 | N5 | + | | c3 | n6 | N6 | + | | c4 | n7 | N7 | + | | c4 | n8 | N8 | + | | c5 | n9 | N9 | + | | c5 | n10 | N10 | + | | c6 | n11 | N11 | + | | c6 | n12 | N12 | + | | c7 | n13 | N13 | + | | c7 | n14 | N14 | + """ + self.assertPyxformXform( + md=md, + xml__xpath_match=[ + xpq.model_instance_bind_attr("q2", "relevant", " /test_name/q1 ='n1'"), + xpq.model_instance_bind_attr("q3", "relevant", " /test_name/q1 ='n2'"), + xpq.model_instance_bind_attr( + "q4", + "calculate", + "if( /test_name/q1 ='n1', /test_name/q2 , if( /test_name/q1 ='n2', /test_name/q3 , 'Error'))", + ), + xpq.model_instance_bind_attr("q5", "relevant", " /test_name/q4 ='n3'"), + xpq.model_instance_bind_attr("q6", "relevant", " /test_name/q4 ='n4'"), + xpq.model_instance_bind_attr("q7", "relevant", " /test_name/q4 ='n5'"), + xpq.model_instance_bind_attr("q8", "relevant", " /test_name/q4 ='n6'"), + xpq.model_instance_bind_attr( + "q9", + "calculate", + "if( /test_name/q4 ='n3', /test_name/q5 , if( /test_name/q4 ='n4', /test_name/q6 , if( /test_name/q4 ='n5', /test_name/q7 , if( /test_name/q4 ='n6', /test_name/q8 , 'Error'))))", + ), + ], + ) From 8e4cd16874bd0c1a6212c105848ab6606963e817 Mon Sep 17 00:00:00 2001 From: lindsay stevens Date: Wed, 15 Jul 2026 02:20:26 +1000 Subject: [PATCH 31/50] fix: delete unused func and constants related to "cascading select" - There are no references to these symbols in the code or tests. --- pyxform/aliases.py | 4 ---- pyxform/constants.py | 1 - pyxform/xls2json_backends.py | 32 -------------------------------- 3 files changed, 37 deletions(-) diff --git a/pyxform/aliases.py b/pyxform/aliases.py index 140f43e3..88ed2b7c 100644 --- a/pyxform/aliases.py +++ b/pyxform/aliases.py @@ -38,10 +38,6 @@ **select_from_file, "rank": constants.RANK, } -cascading = { - "cascading select": constants.CASCADING_SELECT, - "cascading_select": constants.CASCADING_SELECT, -} settings_header = { "form_title": constants.TITLE, "set_form_title": constants.TITLE, diff --git a/pyxform/constants.py b/pyxform/constants.py index 722f11ba..2fdda5fb 100644 --- a/pyxform/constants.py +++ b/pyxform/constants.py @@ -72,7 +72,6 @@ # XLS Specific constants LIST_NAME_S = "list name" LIST_NAME_U = "list_name" -CASCADING_SELECT = "cascading_select" TABLE_LIST = "table-list" # hyphenated because it goes in appearance, and convention for appearance column is dashes FIELD_LIST = "field-list" LIST_NOLABEL = "list-nolabel" diff --git a/pyxform/xls2json_backends.py b/pyxform/xls2json_backends.py index 932f1abd..0f2d2c0f 100644 --- a/pyxform/xls2json_backends.py +++ b/pyxform/xls2json_backends.py @@ -338,38 +338,6 @@ def is_empty(value): return False -def get_cascading_json(sheet_list, prefix, level): - return_list = [] - for row in sheet_list: - if "stopper" in row: - if row["stopper"] == level: - # last element's name IS the prefix; doesn't need level - return_list[-1]["name"] = prefix - return return_list - else: - continue - elif "lambda" in row: - - def replace_prefix(d, prefix): - for k, v in d.items(): - if isinstance(v, str): - d[k] = v.replace("$PREFIX$", prefix) - elif isinstance(v, dict): - d[k] = replace_prefix(v, prefix) - elif isinstance(v, list): - d[k] = (replace_prefix(x, prefix) for x in v) - return d - - return_list.append(replace_prefix(row["lambda"], prefix)) - raise PyXFormError( - "Found a cascading_select " - + level - + ", but could not find " - + level - + "in cascades sheet." - ) - - def csv_to_dict(path_or_file): def first_column_as_sheet_name(row): if len(row) == 0: From 95120c7ad09517c9702d854b473b34bc4efd291c Mon Sep 17 00:00:00 2001 From: lindsay stevens Date: Wed, 15 Jul 2026 02:37:39 +1000 Subject: [PATCH 32/50] chg: delete case_insensitivity xls test fixture - used in retained case insensitivity test that checks other formats. --- .../example_forms/case_insensitivity.xls | Bin 9728 -> 0 bytes tests/test_xls2json_backends.py | 2 +- 2 files changed, 1 insertion(+), 1 deletion(-) delete mode 100644 tests/fixtures/example_forms/case_insensitivity.xls diff --git a/tests/fixtures/example_forms/case_insensitivity.xls b/tests/fixtures/example_forms/case_insensitivity.xls deleted file mode 100644 index 853b759dbcf882d3c2edc4754e8293663cf0eb28..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 9728 zcmeHNU2IfE6h3$R!`qVnJi(U9S3N2Td$*K4>&1B#1BI8_|cwUU+yGte@E~S`rtEemlX1i#OX(pR zfm^n(WthNnetF?bcJaVr5 zJgy?YBkz_&+#8zW$%Oe|r7g;bxIrIAamZ{oX5y^z;8ZFH;wt4d=-KkQy8o@ZZkD;w zMGA1GO_lS9 zD>8c&c25HTBPu+rl&jk%wnDZ@ zdKkOmUT9}D)ENo3#$5V!Jky~cibTWFFzc8h2j@{%Chc8qZ({{+#_zo0MHY#^W5^#e zg0?073bmv%9bmCYM-g<9nFwYk0$DQRV9BV1B_j`(j_wATk>-m24z76RxPgv&N3ov8 z8<1t!7>6Pj@uF|u$B_R)Z*Iz)#xs!DBBR#mu8t6jNU$vg+t%QgQ0t~mp$M#WXJakl zR`eeHfR(fogRxYfWhVUa@30@jV83M!rH+{0snov4g>I#ob#&0`?~5mG?FJ0yjsq6- z4jcZ)=8CikQdLO#S z--y&1b%tZ_l!k0a-CjAS#!!s6W?+didDP-n3r8;>uZPP)(Of|o2%k_!2D5pG3{ywT zJ7ipnZoOM7Q$2Y{{n{NHj+~qD*tN!|Ihkr2~{Fy$v+LlV;7T)95_BO|J(JKhw2J zUX|7(2*#B>GF{oRJM6;?p1JZB-Io9_c3R5%ysA3}IsnSkjXkNGE!qhQ++4hQmX*xn8DZao5p$*106Xs$54HK4f^s7c>_>Me-o2sfUWINFqIUp=u`scM0@enfXS(!954wACZ7uWHAlek zY}6b9gSf+n9W@c?xIl>b%I2u~I5;=7*1MsF?Qzo3g3Scb(w*j7CRKvggMpUb^BCVa zdtUzW&BdKBfw0~vcy%BQS`WG_QgzF6a9s|o z99$iM(%6R>THn)$@>Ag4@ab^F2X9s;4IkK);B#Lebf$K=>K%|*2R@+3z$afuxVdFH zNENdKYJfR=mj>9%EZn$vt+F|`lfTR18B&3kC!ruVEf4X6f(?1R@KN&ImxU7bD2{-ESuVqq2>Vv;neaL*3bO@O( zT)ibKwk7RH4j>O7{BQ>fMYCiIAdmWoF12<4W{^Jj{-K5lXpm!4A^+Ki=faN{PCvb* z`qT&TzwX9o&5%{($#z4A<3RO?4It#r`dUa1m2A47-+xv6c^JwzVbJsEA|5y^9zw#!Bh)$r{O!CbZm(Ic|p!FKOWRcPZ=L-1AI84`_NzIeWDws#Q;-x+ zwRZcAN9aRg&D Date: Wed, 15 Jul 2026 03:11:14 +1000 Subject: [PATCH 33/50] chg: replace choice name "type" xls test with equivalent md tests - looking at the original commit that added this file, it's not entirely clear why this test was added, but certainly it should be allowed for users to have a choice named "type" so the test case is retained. - deleted file had: - a select_one where one of the names was "type": now covered in test_choices_sheet.py test_choice_name_is_type__ok - a select_one_external where one of the names was "type": now covered in test_external_instances_for_selects.py test_choice_name_is_type__ok - the assertion in test_xls2xform.py that an external_choices form produces itemsets.csv is covered by test_external_instances_for_selects.py test_itemset_csv_generated_from_external_choices --- .../example_forms/choice_name_as_type.xls | Bin 7168 -> 0 bytes tests/test_choices_sheet.py | 16 ++++++++++++ tests/test_external_instances_for_selects.py | 23 ++++++++++++++++++ tests/test_xls2xform.py | 9 ------- tests/xform_test_case/test_bugs.py | 12 +-------- 5 files changed, 40 insertions(+), 20 deletions(-) delete mode 100644 tests/fixtures/example_forms/choice_name_as_type.xls diff --git a/tests/fixtures/example_forms/choice_name_as_type.xls b/tests/fixtures/example_forms/choice_name_as_type.xls deleted file mode 100644 index 906ac31f767ba483a1b994b0ce9a8f1218dda018..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 7168 zcmeHLU2IfE6h3#iKU*m6w)}`7a4FCNZTS%u5NlgXRg6GWN=yt%xm|ACrQ0sqT@W)Rophgo*;)4$o2?+`MRD4mQk;PyP5m>)(?!D}#?sj*95J1lE zcW36znR{l=oH;XdPybLceDsqk=Vb(~5U*TMmPnC{uOUCyDf>k{$dh+HnM^utWFOLf zWPxjTUpa>bn8ZSWb6yPifD&K~!1_`;0%ikqfNG!ymT3V50Ch`;H2`4pTXNG zTj?7uMkt`)En2DiU}*HsAvkG+R)n*L2Yz}O&oVi#YQ9#_2IYYeY!ZDLYI!QTtTn0J ze)X(U&oVqU`CPU6NpTj#7ZZ^8$pL#I6l*PFNJxUxg;*Gbe@&6^Ad!4SNg!9KHLDs* z-A?WFf>JrgefIg*wXN;VFAb~pb4>n9>d@lQkWM~3r}&q1&8 zs&jZYId-nK>_DebjhX1~s4 z7djgXMq@gaV=x9zz?9OPlQ{gA$y-o^783AH1dy?bn;pqR8(Wdoe^Vqag_$MWi z%4B{R=j!{H&aCNbYwT3~I5-Z?p)W|Cwi`7k3+Ag9wd%P?$vQjiUd7v|p4Di7@ek53 zjWw?PT;$Kx?85(I<2+aXs8pdho1ur(?}4^DZS09n_zyQasU{mxGe&l)VT)|=bH4|9 zI9b-DTW;a=aB7*aqZi<%yOo4RVkh`XC7S>rp#42g976q)5N7>FPc~PPZ zvW_z4$}z1H+i^KqCAQ-&h9zn(SN9pKL`hoQDp8Vm9IHePoXg*(4LMKZ1>`Yf3CgQ( zV|vhs#3GcTT`dqzN)nb0z;qGpr`oZT?Af!PN;#qGn_N=U_&#>=#;Wr6V1@Lgd;$Z| zct)YfgX(8@!mm>P$GIextFIMSDVtxVe248)rEGphJ(llKl0m{M(XV(`wo3FXzSHej zy4$ZX-Ff>J+GO=BCt}0H?w()CDDi71Pyf{|)m0tAelx1CHDA@+!~MoUUAx({Eo4Nh zs+%X`0x)ZIp*Ew()T`S@hQTF@E~qf*(f1Ot(9G~6B2TXzJ^kiL9reFu*gAyaJ-D}&wLye8fNGV=~KAv5&m z@o9&=r!?l@UH;Gd?K4h0uLdVOWwJkYdCs0Xdw9WA|G`7Bf8CXjR)JSIg0~U8$pDoS z91GjhW|{b?zGvu8e*aVBAF&404V1nbwi_@% diff --git a/tests/test_choices_sheet.py b/tests/test_choices_sheet.py index 4f8b4e6e..6c8c467a 100644 --- a/tests/test_choices_sheet.py +++ b/tests/test_choices_sheet.py @@ -605,3 +605,19 @@ def test_name_not_validated_as_xml_name(self): md=md, xml__xpath_match=[xpc.model_instance_choices_label("c1", ((".n", "N1"),))], ) + + def test_choice_name_is_type__ok(self): + """Should find that a choice name can be 'type' without error.""" + md = """ + | survey | + | | type | name | label | + | | select_one c1 | q1 | Q1 | + + | choices | + | | list_name | name | label | + | | c1 | type | N1 | + """ + self.assertPyxformXform( + md=md, + xml__xpath_match=[xpc.model_instance_choices_label("c1", (("type", "N1"),))], + ) diff --git a/tests/test_external_instances_for_selects.py b/tests/test_external_instances_for_selects.py index 39b2cfe3..1e6b970d 100644 --- a/tests/test_external_instances_for_selects.py +++ b/tests/test_external_instances_for_selects.py @@ -508,3 +508,26 @@ def test_external_choices_with_only_header__errors(self): self.assertContains( str(e), "should be an external_choices sheet in this xlsform" ) + + def test_choice_name_is_type__ok(self): + """Should find that a choice name can be 'type' without error.""" + md = """ + | survey | + | | type | name | label | choice_filter | + | | select_one_external c1 | q1 | Q1 | true() | + + | external_choices | + | | list_name | name | label | + | | c1 | type | N1 | + """ + self.assertPyxformXform( + md=md, + xml__xpath_match=[ + """ + /h:html/h:body/x:input[ + @ref='/test_name/q1' + and @query="instance('c1')/root/item[true()]" + ] + """ + ], + ) diff --git a/tests/test_xls2xform.py b/tests/test_xls2xform.py index fd651435..bb3b3346 100644 --- a/tests/test_xls2xform.py +++ b/tests/test_xls2xform.py @@ -245,7 +245,6 @@ def test_xls2xform_convert__ok(self): Path(example_forms.PATH) / "group.xls", Path(example_forms.PATH) / "group.csv", Path(example_forms.PATH) / "group.md", - Path(example_forms.PATH) / "choice_name_as_type.xls", # has external choices ) kwargs = ( ("validate", (True, False)), @@ -263,10 +262,6 @@ def test_xls2xform_convert__ok(self): self.assertIsInstance(observed, list) self.assertEqual(len(observed), 0) self.assertGreater(len(Path(xform).read_text()), 0) - if x.name == "choice_name_as_type.xls": - self.assertTrue( - (Path(xform).parent / "itemsets.csv").is_file() - ) class TestXLS2XFormConvertAPI(TestCase): @@ -313,10 +308,6 @@ def test_args_combinations__ok(self): (Path(example_forms.PATH) / "group.xls", funcs[:4]), (Path(example_forms.PATH) / "group.csv", funcs), (Path(example_forms.PATH) / "group.md", funcs), - ( - Path(example_forms.PATH) / "choice_name_as_type.xls", - funcs[:4], - ), # has external choices ) # Not including validate here because it's slow, the test is more about input, # and these same forms are checked with validate=True above via xls2xform_convert. diff --git a/tests/xform_test_case/test_bugs.py b/tests/xform_test_case/test_bugs.py index 7e4740be..4d942c27 100644 --- a/tests/xform_test_case/test_bugs.py +++ b/tests/xform_test_case/test_bugs.py @@ -6,14 +6,13 @@ import pyxform from pyxform.errors import PyXFormError -from pyxform.utils import has_external_choices from pyxform.validators.odk_validate import ODKValidateError, check_xform from pyxform.xls2json import SurveyReader from pyxform.xls2json_backends import DefinitionData, get_xlsform, xlsx_to_dict from pyxform.xls2xform import convert from tests import test_output -from tests.fixtures import bug_example_forms, example_forms +from tests.fixtures import bug_example_forms class TestXFormConversion(TestCase): @@ -48,15 +47,6 @@ def test_conversion(): survey.print_xform_to_file(output_path, warnings=warnings) -class TestChoiceNameAsType(TestCase): - def test_choice_name_as_type(self): - filename = "choice_name_as_type.xls" - path_to_excel_file = os.path.join(example_forms.PATH, filename) - xls_reader = SurveyReader(path_to_excel_file, default_name="choice_name_as_type") - survey_dict = xls_reader.to_json_dict() - self.assertTrue(has_external_choices(survey_dict)) - - class TestXLDateAmbigous(TestCase): """Test non standard sheet with exception is processed successfully.""" From d8f7b7a37a5acab961b3832604eafd48f513a696 Mon Sep 17 00:00:00 2001 From: lindsay stevens Date: Wed, 15 Jul 2026 03:30:27 +1000 Subject: [PATCH 34/50] chg: replace choice name match xls test with equivalent md test - The deleted file had the same data as the new test, just with longer names. The old test was quite specific about the question and choice data so these assertions are included in the new test. - Other tests in test_builder.py continue to assert the internal pyxform dict structure that the old test was checking for. --- .../choice_name_same_as_select_name.xls | Bin 24576 -> 0 bytes tests/test_builder.py | 41 ------------------ tests/test_choices_sheet.py | 21 +++++++++ 3 files changed, 21 insertions(+), 41 deletions(-) delete mode 100644 tests/fixtures/example_forms/choice_name_same_as_select_name.xls diff --git a/tests/fixtures/example_forms/choice_name_same_as_select_name.xls b/tests/fixtures/example_forms/choice_name_same_as_select_name.xls deleted file mode 100644 index 48a05abe2eda34a145b8d1f7a749f7a4933960e7..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 24576 zcmeHP30#a@|35QrQ(1}-iP0uh+DI2g(IssLA=IsDno=5?2%}qM>Eg9!iR@h4l`FzU z*S=jLE?FXFU$0%)I{)AAd3u_er%^)?0)B=(vq?V9cL9&9>8j>}n zHjvsv;zMc&3H^sj_K6%sR-k&if{FQ`De$_t;XF~2m z8cD`tUk^)(j){+!^1~#F@zE)wB>w;8;YaZh%|RY|f`vJd&x2$R5e3V@?r?5Oop(~T z4$*a~XatX94|VJY7kop#{#YAO?IRf&71}5f5J1jT}NH)Nnecn$Kv8QztbwRAtexv&JpZ5u1uC25QWl(HPOB zu|{>)=uu}4pE_!Ah%ciL4SaBjXB{;N)Tp+;MhxRRF-+>f;KJLnE-Jh_sTga-Xrtd6 z;tIThZR~7poosDgqdIgS>BwY8c4(vD4&L$bHfD<8`3Qk1c9d!*8yEz@>zb+L;;M=R zd4y^Y9ma-Ti-lHQ)eGL_?u0}kD;@a^XVuw}j#Rs3Tvcg)a;jzEdSeOQ1@RVoiypudjk=R~U7;DG_Db*U)7eQ=}CwE2xnW zA@AjU=pD>)gpcJITB!1Ox|L%GS65$CIijko|L%{ywU{0mKlh{BNj09gKf=&`|EAp}eEoa-4Tj_^fz>b2h5H#{aib{1`FiS4bG# z!ZeKL2j_ZJeYzaPr^|8vS6$ABTM(aB&Z3L+L#lp_erWr`_>sZ+0d8SRNaNs~QkBmY z)q1Ng&oeh~ZrU95yqzk?xhI29>zUTS0nAjXa+aQ9I7^qa__SckufPzG_|)hDhvgKG zQvE4B3o->2RCD-{rqZ&>={#qWu2PN_xG+bypymS@>+vNCua}OE;El|!-2ki9s{uXW zs|N*~nAD2`&Zg?s07GiM8lX+;)c~EgUJcMF^=g1_Uatn&zv|Th14_LbV9%&m1MGVB zYJh#IUJWq#)vH0L`ZmBNo_bNh^`Lq+aH($t7(~{OLU)x0*#DG+04JT-n-w)+j0y3A z>C@~6TwZlJ!H!9fhxz>4ATY458$|A_9mGQy5O)t%5Oxss1Q3NuYKc>ub=MlI7~LQq z%as+z)d%_U!Ks%QUUhD3=*4mBFJUt%`D|n<$m7_=jQ1==o!SAvROqET(+b zGxg@pn_5iy`uf&lO1X?-3d`6`VVP#8aB)|msZZ$|)UA#wWv%>knqv3HuD}%C8&UhU zm|}>@GdVSys;zEZvZf_N|HnhEOK;k=sRo4c0cF)P#FWc)S2w$3_5?QQj){_ityF!W zwiLL4rjVl6aE9raa(QtrDGCY-YB`N^86yRj0TmTb#xjM|u%+m%A_e+^ifYq&Ls z2Q<*H39q%JK)s`pl!F@Tohf7bW9_(Q1FaNs8#1+x%z$W)TR1RQfYjm+b*x83amw7) z<^;dE)Cp1?q#Xo5&nQ43qfQ)l@MrfPTU|it&rF(if|Dr)CmMBv15eE$_ywv?km4Zi zAo#gT0RkEII?)bdrwa)E*-f)JI5AOhqERO}w$=<{sS8MPkaiIKe5c?9GU|1r9mHN2 z5R5+B#KFn0f)kB8(GG%NDC-0%4$=++^G78R$f(zeb`S?$KQw%TPPGIr@AfT1(QXny6S_Y-$vdLR@YsLJ>kPwaz5z| zUloOf{-#LJFgWl9-$-8+ckAJ6xdGl|kAYEuJ+4h48FMxn_u6D^w2-0aAQTw`QQVtb zW$MW^VUzKwO~yeB8G4pNkuem-J$qfQo=j6V8PD2eI%*+<^B6r?um?pMiQ?|xd9I#} z1)EHl+GJd`kiqFfH5r~L?&e=_)stz)CeyVx8FwvY=wU4-jj<^1>ysPm$uvhYp4vUF zEs*il!PAO^_7~q*Po@Q%j2cg~)j~$u(~5&0?%$`Lj3t|l8c%Dlg^aSN6$f3ve@H!< zmTWR=Jgt)!GRmG-98`Sdrg}21*ksgrT4ya}ls&CDXkYnb^<=ErWYlm<6 z(x z$*A!(M=fNOJ`$+TsYQR8V&TF5ASn!CrYwWrmS;j_u8@ibR0WRyM4-DA=D zTk6TQW0O(iX`WihD0`Z_$EU;d)sblcpQmBmRrB8L&3bQI%Z)U?D+vjmTBIJ-Rsjkg z#RhH10#)`RnC*rIs|U4HfP(+9LG4+f%05HL%7eqzgW4-V!CTm%9ax~s9zsZF?0WT} z4hm544K}DV3sl)32pRCpDfOW36`}$$r-EU7IPcJt_t`X z;U6y&Ci4A7NlA-%87#h9$W4H+co+-{VT&Cf?nS^|5{Rwf3YVg&L7{R0Ww^riC&L=9 z=p4fponyG7bBkfowo|5_d=ZbY%8G#=5b=i{Z}g5kphd8*Q>G!*b!62wg#E{gx_mMM z-UOqt3Dw3qQy)5o-M$uxo%F02+t@&>h9sM2aiI^+Jg zz7xkLMJGq6N>V2A2a6`AF5-Q!3dQXrHzR$?MA$Bewoj()kJ}q3fzAg5I}^bla0!mF zJ{r5)y^KLNSf2S_A^}J3u8PZPCD$k4gVH1{SH%@^1`CKWSdC!U3d|t-iK1f@lEr+O zE3g>VGXu@`1BTG|6M#`MFi6i4C>MYZ)3$#)4Tr911f0=^xL5x&j3fCHO)@_*BZ`X)L1606LV?L2a8WhkT5XDmKRg1t)Zl;e^gHoX|N8nBQd@fbQ&6 zSuijFLxh3MDK!&+R2jNzcXZXk38_h-EjQIxdX2$6gTXse!EmUr+TPAVh#nM#Bo#e? zwf3Obl7c{m)7ICgoJwVV0rk$r5n5U%hc0GLdV-C}(CCz83|Vw|^;Kn*YYLHiC{Rd& ze#JyBbzv1>;*^Hx@rIFkN{b{5Kss~yHyA)-C}{@)%af?PV2~YyA#Miu1}!F#36uw^ zui;4+!PaQ}J_590*Y|CnA!)nM0&-W#m$wGw4R1Oj+n*AoNMdn4djVN6?mO5IzX|+4 zlN>p61TL9Gxa}Lc?d(qE@8^<`yF-cCTu#1i4j?lJ4JGS~7S#Ei6wBd1refeDm*hfT zo?A>H#t;ZJ#3N2lJc^5<9QeeP3n`9}xVT)hbm>w86^KVJ6hI91fRsxmDD!}}P%zT0 z5e}c=Kq>L~f!Z=X1FV^kAG)DONXMs45KWo^dxO&O3loG2VZ0>c@w8 zK))p$VBh%$*0Z_rgaiMMa(Ivksj%UaQm9v|R1d#geIbG9EEtmzD|OOX5nPg^$B4km zlcHlpNw85tDoPRwQ==rwBK~AJL51o|1mxfamXsh(TJ2OKwDzp#G(sJ3(7=^0K+nseQ@U9jJqD(mhpIZe1TlJzea7gZH+8QJz! z?-y8{;Ooz})}Te15$;>#~!Ie#oD{<*a=7ao6J2p+_@zIz8yCpX`+rlyE<0-ImVx1{E#X zBHDJPO-bSYkELHhl-hzOPFmVL3j~}E2|uKz#*4;?IxC%nVdy9prJ#E>C=NC`&Nu5- znZ7Pi9J7rcpTilqGkKn4GX`wv+L7)5-h*q*pI#e9`X1RS%2qeD=ur@U-yW@^$_;b`Sb{Y>CX*&$esV z_Hyj427i4xxU4JJu35;;<*%oEW_4J2v~Wk-o!@4A4e1jzt(VWjmHS)v3EZ@@Qz!T7 z(N3M;=GeVTKGAn>vmUcw{}EPUTVl0)$<;tny8S6nnwG zbN89_?})^Mw@Je>aiONYttznndLT-?5KMz=}t9^bF53SVYBQLp{If{JrH zeLsFZ*Z7ho^zh`5e|z{wE`9BrQFSTQ_s6G3eg@~ACcpl;IBIpzNv}_yKK$d`>w0&Z zPYjz`l@MfiAb4Jh&-|la{&N$(TV}uQ|IRkZ+PL-9sc zW6F_di;d>(DC!qv@xFM9$875gmq)*xoUtr(<>dS($G`S>{}AfpAiCvqYS!<+m315E zSC+f6q|4ilsrx10pKz1Tuk2Il)<4_tleI7Z#J$_;f!2{}36A^k+s^oF(3N9X2J~Ok zD6h{7~2}(!ftOT75O(BWoMxu*XG{m>E51^r<=|YuKB}f-tgR4 zBZl?$v6T%nUpmfuEB~_aShuGQe#-I5?auw;Z1B}#=8q2=es)=T%0=Kfx#G`BrqalO zzg}fcn11idtQVK;ItuSBHd?UW^2N9ZSua!e`MvWv!D(-LJbiwX_P4}d(+;LSU+y`; z>}#A&k6njC_AfqM)WIq`=0JlzshbC#_M9}TEY9N7-c^-j%90Jse$DQ_D{#m`mtLr`GV_3dv8oSZR%BS`^T`* z;FiDHpD!PAjO+RQ*xIEYvqA)|Ph95x_%v{)aqC;dez48bAKEso*?XJ3W4{-*inkZ$ zZf!FlC&l>IuEAzGsKR6|R*0pzIf4t>*#d7Cp92%GyI?8ycE+jTUG9;H&Sn^Z2f) z(1wq!i_e6;O%~?NI@zakr}4mth2!mx`N#O*Z?Nq8FDtG;=-~Ni&4h~&&wRY@G)(XE zg|5dop1k{I_@etGkIs!X>oF?Wsl&DA14;&Z#9tZujAJtHW`lsSmuIXQXVQD|Uxp8vac+)*!M&Vu9XH-Q>R#aHxw9Z_SD%ZqFQRr_ODz~R zS8sJ(L0+>&s|7al<;~1Sd+mw!v{~2D=%DBN-*(R*-u`~mzI{5kYO{{BGCRFNrBO(s zuYbqNW5+D^{FqtR<^0}B4?dLs@@LM=Jq<3*6D|$t_H5~=2@X5EnRd+$&Aa{Ge>cr{r<#y zf%8~@)16bN?2qXxH%-|h-`7PvetE&nthF~c3+)`8+IMU+{dw;Z?l;nA7L4BeZrBp1 zL9y=JV)KJ`+|WzuH!P~^$oSh?dKSI6txR52$jR!yD&sF{c<-!TC2-zpB7U9dS+KRdWV)z% z#pQ^^?xlIBx4*yDE&J}F)2_pwiv|0Q{64iECf~x#{TjKh#TB1Bv&v^zyv=&hxOaHe zIoJc;m=^nqh|Mr3_aoyXVkNAC9)T@BPF3C`)hYS5e( z{U@CI^?0H6h<;uAd`SK2Sc#+WtmQ|RHy-ssGH}W(!Y$I<_pNE%;gGWytAngJ%8tce z%WS;)MySJ!X^Fc-)Rs4*K6)#IFOyVXw zj~;r?Z{qu;O21>p4V({}b!{=)$KX}EQ{t^p@kSHNVs~6Sus`MJVQ<8aBaTh<6_qc( zSdeog>u#dnd&8Wq_pS|E>Sl2NeET+2DlIJMMfUtNBl^;=++@LoXSfZv_6VTir3GvD z!V@bPAGBw^MCoAB+md2Av{ybo|(BXD=$&M>})9-g_*wxo= z;PV#^^PPAuv!Z{`O*pu0)!C*(_j0%RZLN6UJy|kt#nCbQpU>+hXdd#@jqb6({xEHx z#Op;f@xlR)!47r;M;Eu{DUI3(F3u~NJ@{j0ezy(t!|&T{___C)gC+f{#;31+y^=HK zq+L;rnd~ZQxioBreD~LGt$q@BZrgm+w}HK-H)a?(u0K^=VY_l$kp3Sd4jedfr$%%R z*BQ1ZX&MwmHKjw%5IJmsDgb++sA%mB`cxV=vKz7oUW$gzVSuhQ0HX(EO6q4Lu}47) zq(9XmQDY)D2HOb~7?!X`tIUlSmk1kRV2BM3;NK{azA28JsbAsKzt%NV`L(Vo7pI-{ zfwQC0u-~l_OvD<(L>?DDxG=44Ox>Hq8Gp6UBVV?@z8oAA<{L$occjWYQRit?NhUSd zp)u`YxdDEA&zTBOj>0^M{*8YYReL&hUI$-TI8X!UZX9rljqOa1&}}PxnX4Wd0(DJD zIyK88WQZB^1Y*LPA0EgVt{TDi7A|gy!+R%?9S%-osthyAWnU?c(0_@55IV;JNwFTC z<5F_dIV^2V;o~#pvoSRfrE^#l=8ohV$UOzF7Og(6j{jJ7v5R^IAY+TA_T@&$RXW=x#_4A3{z;`oA)yKE*UGLTy9-cNh+}tDE7_ z1?y!vitS$1nLcSYoGIlN9GIWNon9S=Q$qZ=xW#^f{)#;hm5hH|1I>ch{~Z7R8u$jQ zdU_`41*!+-7gx_;&2zCbTfB@%IyrsJcZDhykYM$Uin3vF#j(&ZNVwY;zjNTL3L_}S z@ad3ng={V)Tvo_|1baT&4R=pZY8dN_dviDeS*}Vh3=+3BnYKR1%lU?>kWle>20|3-<82ySjSvYsgc5ty@5UKYSBXwOj@v zTQ2nSa{YO#PYZ4Kub8ps&BxGqv+WDP{_S_)a0TFDP3R4%=pX$cVay1Fgs-|JNbs0s zL&CMxUm&5q_d`NIH~|Tl!iyoX?auz=B2cBqCp0~44RsXy4UQHjB&SNG@wBGlSuaRO zX703J27hygx*zI4e>vP^ti$7bNYj8JjCEoC{Rf**riurVOk&Q3Zw>NqkVhdg;ek)^ zXafc}$>d~mGUOR@vI4pgnMysVu0a!ETAD#;z^`oJ=!sTcP>J-xpwU-*k{D4XkrA=? zt+XPJG%qpYNm>zxt=r+Yf$$JxR262uB=>WDYOJAZdy@zVMM7CC?~)zM2Zj; zBnfbPrvw*KDk~2Z05lCZ6e@j|$n>RovhobM*qerotP*>NNR=Rg&p?4RPr+x1G%-RV zQ|1%-j;yKy?)51#LSJYp_7;0blnO0xDP#uOW#u$`@1ZoUh|bYyvT`h=6le26Xj!9N zi42u1k)d*_dyYf~QgtV3MPI8y;0n1iILpePWXKKDiZbNVJT$m851&c%-cU8*84!hA zQFUXq2~;pfPS6^%^0Xpsm{yclbU#CGf+d(oiueTYrFn1R3Bm|+P2v(+=Q0MkVK}Dkl0&62&EI`ghv6j7QX zk0`|&EY&%q3xjvk7C_xs%F4lb(!3|Kaxmy)>J0bL{lK8!py_W^j}UuHWS|1be}Qvi zL?xUluh2syBM;B0r!~GXzq)Fm0Gja`y9#pBico`*(9Z=q-WSB)=uU|df*e`7AO~T> z3(+S+q?jbK@WSxIv?6Tv8T<*mDEfYQA*v=Z0($}oNEs7rMOK9uB9=4{DX>IGhJY0g z>JF}^=!|G}BoQL*vI9K`%?t9H2MUlhnlivGzztOqC_qLS17m!k8Nyf*D2OOU!sul^ zUos3K8ny})AZv)vVrU^Sgvu3r2e?I60ZTk&2}#gC;%637`c05yLF*_`5a5Qgp_`+Q z5+l4XL{=dIWHYh~rKVZx`7)vu-5Pa_0)!XRETt8>%|+k!Go!VQ_$-#3fh7z{X+;oD zAP!6SJZLP*+$P)Q&qfR5XK?~nx#=bm%+i-Kd2SX`Q~qRHeIv zRvYRP;}|w^l&7;D&?$5Xh>XVQ!GO-__bd{A-pivaWos?R;N^q zf^h&RwLOQtyY}C;Yor6ULDmj#v^S>LkoG!Qmhd|6vGJml_$p zV*|8#oedbhi@p8KaO@f=&_VCIxrq(~B1S^7HxA=)Zc-=1$`azBvR(}$N-4M0?urak z(mbK1jxM50U}P9a|C7kzT~U`U89s2Ni{`JVtYU8+8ZhR^IcA2Oo}t#G4~e}|K{~jy zj(tdPDkz1>9riJcBLBJm?`nWk{R@k+uy;9zjIK9v{5!mIr9&40TndZPrJ{~?l*r>! M-2eLVuT1~H0Gbd-)Bpeg diff --git a/tests/test_builder.py b/tests/test_builder.py index b50b3259..125a2ee3 100644 --- a/tests/test_builder.py +++ b/tests/test_builder.py @@ -157,47 +157,6 @@ def test_specify_other(self): } self.assertEqual(survey.to_json_dict(), expected_dict) - def test_select_one_question_with_identical_choice_name(self): - """ - Testing to make sure that select ones whose choice names are the same - as the name of the select one get compiled. - """ - survey = utils.create_survey_from_fixture( - "choice_name_same_as_select_name", filetype=FIXTURE_FILETYPE - ) - expected_dict = { - "name": "choice_name_same_as_select_name", - "title": "choice_name_same_as_select_name", - "sms_keyword": "choice_name_same_as_select_name", - "default_language": "default", - "id_string": "choice_name_same_as_select_name", - "type": "survey", - "choices": {"zone": [{"label": "Zone", "name": "zone"}]}, - "children": [ - { - "children": [{"name": "zone", "label": "Zone"}], - "type": "select one", - "name": "zone", - "label": "Zone", - "list_name": "zone", - "itemset": "zone", - }, - { - "children": [ - { - "bind": {"jr:preload": "uid", "readonly": "true()"}, - "name": "instanceID", - "type": "calculate", - } - ], - "control": {"bodyless": True}, - "name": "meta", - "type": "group", - }, - ], - } - self.assertEqual(expected_dict, survey.to_json_dict()) - def test_loop(self): survey = utils.create_survey_from_fixture("loop", filetype=FIXTURE_FILETYPE) expected_dict = { diff --git a/tests/test_choices_sheet.py b/tests/test_choices_sheet.py index 6c8c467a..0e0facb8 100644 --- a/tests/test_choices_sheet.py +++ b/tests/test_choices_sheet.py @@ -621,3 +621,24 @@ def test_choice_name_is_type__ok(self): md=md, xml__xpath_match=[xpc.model_instance_choices_label("c1", (("type", "N1"),))], ) + + def test_choice_name_same_as_select_name__ok(self): + """Should find that the select and choice list/name can be the same without error.""" + md = """ + | survey | + | | type | name | label | + | | select_one q1 | q1 | Q1 | + + | choices | + | | list_name | name | label | + | | q1 | q1 | Q1 | + """ + self.assertPyxformXform( + md=md, + xml__xpath_match=[ + xpq.model_instance_item("q1"), + xpc.model_instance_choices_label("q1", (("q1", "Q1"),)), + xpq.model_instance_bind("q1", "string"), + xpq.body_control("q1", "select1"), + ], + ) From 95787bf80fe4a0d3527bb239c942ce8d9ec49e88 Mon Sep 17 00:00:00 2001 From: lindsay stevens Date: Thu, 16 Jul 2026 00:53:19 +1000 Subject: [PATCH 35/50] chg: replace static default xls/xform test with improved md test - The deleted file had a single 'time' question with a static default value. The handling of static defaults is covered by test_static_defaults.py but that test used the old string comparison assertions, so it was updated to use xpath. - The handling of time default values specifically is covered by test_dynamic_default.py TestDynamicDefaultSimpleInput, which e.g. checks that "01:02:55" is not a dynamic default, which is the same format pattern as the deleted test file "09:30:00". --- .../example_forms/default_time_demo.xls | Bin 6656 -> 0 bytes .../default_time_demo.xml | 23 ------------- tests/test_static_defaults.py | 31 +++++++++++------- .../xform_test_case/test_xform_conversion.py | 1 - tests/xpath_helpers/questions.py | 7 ++-- 5 files changed, 25 insertions(+), 37 deletions(-) delete mode 100644 tests/fixtures/example_forms/default_time_demo.xls delete mode 100644 tests/test_expected_output/default_time_demo.xml diff --git a/tests/fixtures/example_forms/default_time_demo.xls b/tests/fixtures/example_forms/default_time_demo.xls deleted file mode 100644 index f52133a759b5e96c544e84d1e4ca9e2afa7d7e79..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 6656 zcmeHLU2GIp6h1S%Tc$shZD~=#)}hGuZ_Dq4fWdBQt8GG|X-hPQq%Q5WR(8A1cB_(T zT)-wg2+{E13ki^5qS1)PL?0xvee=OaV?qR<3O=DeEJR~4(DnQ7%-UtMYqzG+Kzb(M z{W8&wsl1nq<(E;*z;!p%kR?HI$1|b+3qyGI?{!WHQA@ahdKT z3*2?)$~|;r6FmU;d<9?tg#g=%o%}vp+VWKMut1MQ@WjNFb{Rz;l_S{c{Amq5CS6TF zJ=5=teB>9BS=K*qfrV_v_H_I6{^mXI0&<;y-uXQLCBRCc6j%iy1V|ZB4y*<$fJ$Hu z@DT7Y@CdLLSO+``Q~}k%V}J>)2Wo%~z~jJ1zz5UA4nd8+IRz)(t-0~8 zq01SdR4yw1C6(@0{s*yum^&cDcaon`Vb7B)tx;(yQcb>4eSTD&t?g;E1EJ->!`#f$=n-vsk|&+pz`v#Tji_cD^y+)_at9{uWK@i^Rkn# zbvWDO6VaEgBa^tfFR9aR$hZ}Yjf@VDzl9U7;hEM9K&0=aCcnrbdS&8yrN1CyGRI3H zV+H+&pdS*c@^&dsFdlpbK|_vecz-mev;fk8RNrL8Km;)mNen~?;|~OpJ90ur7e*MY zFLq?iLN*!0a3U0A`$*VAKpKw)qp^Oc(#wkJRE8qaFdAYZ3;k4O zWDrrR%^tz*>kM0Fn-z|L=Zva`=^q3yiSL_YIR!P0bpu8vfPzDeZ6qD~#lASb_gtJM zFvB2BMzfbGlXzi-J0INqzUxqTbHC!pX+eiJaziC4N6|9vZcsfMReDUxRv;v8vkX1k&r z&9!OeI+Tw$T{+ftCDB};Rz5E^_#zm@^7tGRzvkCc{ZqLn2QYS#ysm60+KFPmD}Z#q zH`tzHziSve{ek6MDi>{)kVk(?=iPO_I_+`nng%md&aUao=&70uHTo#qD6@)w<HptIfYkH`??>2U@X-32qw4;!qepu?0npo2XO#!i2pN3d|F% zp!%^7bv~t+%jas8fD-Ce={Yp_!lqRmmy@6(J}%cV4$1JmyU-^X-`Q>11(l8Td|Xgw z%52x6OVYCMr`2(B<#176X(<=gv$$wF&vrw)1n*V5MpxtJ&ATB#rtKQrxP|F%W~RFt zIxgDH(C2SAqgP9S37m^`kDGC6bTb`~z{UR2&6I4eM8g}p3oD^tdJTKrDO>nyzVmmD z3`8U2k)fE`{_21gGIu`F)W&-(1- z*;}{wzv%t&9YY#xKfB5^{~AEsvYiv>mIeTB=3anN>^Xq(V-Vo?(ilJuH}2ZsJ}hqo z0zkaV&&x@bnUexQ{Nkzj^*?7AIYoXrSMn3eEM`)!`BR5}dFJw&P3yd;-^Kj>w?A$J z&y&GB2p+%FI5@$3jD0!RTzoVy(*t6GrTqV+{3qiM$Yv&nxBw3b=vn-IippWUY4ShR zVVk*#v>e__f8`-c#m0QPZ{UFiY0?V4VVu$Zn8Qmpa}g=^b3N2e%BO2RggPBT4SGXj z<LT - - - Default TIme Demo - - - - 09:30:00 - - - - - - - - - - - - - - - diff --git a/tests/test_static_defaults.py b/tests/test_static_defaults.py index 98764225..79e3fbfc 100644 --- a/tests/test_static_defaults.py +++ b/tests/test_static_defaults.py @@ -1,20 +1,29 @@ from tests.pyxform_test_case import PyxformTestCase +from tests.xpath_helpers.questions import xpq class StaticDefaultTests(PyxformTestCase): def test_static_defaults(self): + """Should find that static defaults are placed in the model with no setvalue nodes.""" + md = """ + | survey | + | | type | name | label | default | + | | integer | numba | Foo | foo | + | | begin repeat | repeat | | | + | | integer | bar | Bar | 12 | + | | end repeat | repeat | | | + """ self.assertPyxformXform( - name="static", - md=""" - | survey | | | | | - | | type | name | label | default | - | | integer | numba | Foo | foo | - | | begin repeat | repeat | | | - | | integer | bar | Bar | 12 | - | | end repeat | repeat | | | - """, - model__contains=["foo", "12"], - model__excludes=["setvalue", ""], + md=md, + xml__xpath_match=[ + xpq.model_instance_item("numba", "foo"), + xpq.model_instance_item("repeat[@jr:template='']/x:bar", "12"), + xpq.model_instance_item("repeat[not(@jr:template)]/x:bar", "12"), + ], + xml__xpath_count=[ + ("""/h:html/h:head/x:model/x:setvalue""", 0), + ("""/h:html/h:body//x:setvalue""", 0), + ], ) def test_static_image_defaults(self): diff --git a/tests/xform_test_case/test_xform_conversion.py b/tests/xform_test_case/test_xform_conversion.py index 62e7587e..f72b3f3a 100644 --- a/tests/xform_test_case/test_xform_conversion.py +++ b/tests/xform_test_case/test_xform_conversion.py @@ -23,7 +23,6 @@ def test_conversion_vs_expected(self): ("widgets.xls", True), ("xlsform_spec_test.xlsx", True), ("xml_escaping.xls", True), - ("default_time_demo.xls", True), ) for i, (case, set_name) in enumerate(cases): with self.subTest(msg=f"{i}: {case}"): diff --git a/tests/xpath_helpers/questions.py b/tests/xpath_helpers/questions.py index 612f93f5..40afc699 100644 --- a/tests/xpath_helpers/questions.py +++ b/tests/xpath_helpers/questions.py @@ -12,10 +12,13 @@ def model_instance_exists(i_id: str) -> str: """ @staticmethod - def model_instance_item(q_name: str) -> str: + def model_instance_item(q_name: str, value: str | None = None) -> str: """Model instance contains the question item.""" + pred = "" + if value is not None: + pred = f"[. = '{value}']" return rf""" - /h:html/h:head/x:model/x:instance/x:test_name/x:{q_name} + /h:html/h:head/x:model/x:instance/x:test_name/x:{q_name}{pred} """ @staticmethod From 3e012d1454ae8ab7d33970d886f943815ff1930f Mon Sep 17 00:00:00 2001 From: lindsay stevens Date: Thu, 16 Jul 2026 01:47:55 +1000 Subject: [PATCH 36/50] fix: delete unknown question type xls test covered by other tests - The deleted test was misleading in that the name of the test and the file suggested that it was about the "unknown" question type in the XLS file. But the test assertion is catching an error from the use of "bind:relevant" alongside the alias "relevant". - Unknown question types are covered by test_xlsform_spec.py test_warnings__unknown_control_group__with_name - Conflicting alias headers are covered by test_sheet_columns.py test_conflicting_aliased_values_raises_error --- .../example_forms/unknown_question_type.xls | Bin 23040 -> 0 bytes tests/test_builder.py | 10 ---------- 2 files changed, 10 deletions(-) delete mode 100644 tests/fixtures/example_forms/unknown_question_type.xls diff --git a/tests/fixtures/example_forms/unknown_question_type.xls b/tests/fixtures/example_forms/unknown_question_type.xls deleted file mode 100644 index 5289ed91d35967de3cec6041d343810217f448bc..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 23040 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 Date: Thu, 16 Jul 2026 19:02:31 +1000 Subject: [PATCH 37/50] chg: replace style settings xls tests with equivalent md tests - The deleted file set a (seemingly invalid) style "ltr" and the replaced tests checked that this value appears in the body as "class" when a "style" is set, and no "class" is emitted otherwise. These two test cases are now in test_settings.py - There currently is no conversion or validation for the style value, it is passed through as-is. This may or may not be intended but a test is added to document this behaviour. --- pyxform/survey.py | 2 +- .../fixtures/example_forms/style_settings.xls | Bin 20480 -> 0 bytes tests/test_builder.py | 67 ------------------ tests/test_settings.py | 44 ++++++++++++ 4 files changed, 45 insertions(+), 68 deletions(-) delete mode 100644 tests/fixtures/example_forms/style_settings.xls diff --git a/pyxform/survey.py b/pyxform/survey.py index 7e7f0a7c..936f8007 100644 --- a/pyxform/survey.py +++ b/pyxform/survey.py @@ -157,7 +157,7 @@ def recursive_dict(): "sms_keyword", "sms_response", "sms_separator", - "style", + constants.STYLE, "submission_url", "title", "version", diff --git a/tests/fixtures/example_forms/style_settings.xls b/tests/fixtures/example_forms/style_settings.xls deleted file mode 100644 index 933720644d5e77cc18b6e3c26b2acfe10b65cc7e..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 20480 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 diff --git a/tests/test_builder.py b/tests/test_builder.py index 8fe9a79f..ebeb95c6 100644 --- a/tests/test_builder.py +++ b/tests/test_builder.py @@ -1,11 +1,9 @@ """Test builder module functionality.""" import os -import re from pathlib import Path from unittest import TestCase -import defusedxml.ElementTree as ETree from pyxform import InputQuestion, Survey from pyxform.builder import SurveyElementBuilder, create_survey_from_xls from pyxform.errors import ErrorCode, PyXFormError @@ -436,71 +434,6 @@ def test_sms_columns(self): } self.assertEqual(expected_dict, survey.to_json_dict()) - def test_style_column(self): - survey = utils.create_survey_from_fixture( - "style_settings", filetype=FIXTURE_FILETYPE - ) - expected_dict = { - "children": [ - { - "label": {"english": "What is your name?"}, - "name": "your_name", - "type": "text", - }, - { - "label": {"english": "How many years old are you?"}, - "name": "your_age", - "type": "integer", - }, - { - "children": [ - { - "bind": {"jr:preload": "uid", "readonly": "true()"}, - "name": "instanceID", - "type": "calculate", - } - ], - "control": {"bodyless": True}, - "name": "meta", - "type": "group", - }, - ], - "default_language": "default", - "id_string": "new_id", - "name": "style_settings", - "sms_keyword": "new_id", - "style": "ltr", - "title": "My Survey", - "type": "survey", - } - self.assertEqual(expected_dict, survey.to_json_dict()) - - STRIP_NS_FROM_TAG_RE = re.compile(r"\{.+\}") - - def test_style_not_added_to_body_if_not_present(self): - survey = utils.create_survey_from_fixture("widgets", filetype=FIXTURE_FILETYPE) - xml = survey.to_xml(pretty_print=False) - # find the body tag - root_elm = ETree.fromstring(xml.encode("utf-8")) - body_elms = [ - c for c in root_elm if self.STRIP_NS_FROM_TAG_RE.sub("", c.tag) == "body" - ] - self.assertEqual(len(body_elms), 1) - self.assertIsNone(body_elms[0].get("class")) - - def test_style_added_to_body_if_present(self): - survey = utils.create_survey_from_fixture( - "style_settings", filetype=FIXTURE_FILETYPE - ) - xml = survey.to_xml() - # find the body tag - root_elm = ETree.fromstring(xml.encode("utf-8")) - body_elms = [ - c for c in root_elm if self.STRIP_NS_FROM_TAG_RE.sub("", c.tag) == "body" - ] - self.assertEqual(len(body_elms), 1) - self.assertEqual(body_elms[0].get("class"), "ltr") - def test_trigger_data_wrong_type__error(self): """Should raise an error if a trigger is truthy and something other than tuple.""" # Should only happen if the builder is used incorrectly, rather than any user diff --git a/tests/test_settings.py b/tests/test_settings.py index f83bbef8..40352420 100644 --- a/tests/test_settings.py +++ b/tests/test_settings.py @@ -356,6 +356,50 @@ def test_instance_id__can_be_used_as_reference_variable__error(self): ], ) + def test_style__no_default_output(self): + """Should find that no default style is set.""" + md = """ + | survey | + | | type | name | label | + | | text | q1 | Q1 | + """ + self.assertPyxformXform( + md=md, + xml__xpath_match=["""/h:html/h:body[not(@class)]"""], + ) + + def test_style__output_body_class_attribute(self): + """Should find that the 'style' setting is output as a body 'class' attribute.""" + md = """ + | settings | + | | style | + | | theme-grid | + + | survey | + | | type | name | label | + | | text | q1 | Q1 | + """ + self.assertPyxformXform( + md=md, + xml__xpath_match=["""/h:html/h:body[@class='theme-grid']"""], + ) + + def test_style__output_body_class_attribute_verbatim(self): + """Should find that the 'style' setting has no processing or validation.""" + md = r""" + | settings | + | | style | + | | theme-grids\n\n | + + | survey | + | | type | name | label | + | | text | q1 | Q1 | + """ + self.assertPyxformXform( + md=md, + xml__xpath_match=[r"""/h:html/h:body[@class='theme-grids\n\n']"""], + ) + class TestNamespaces(PyxformTestCase): """Test namespaces, for the XForm and in relation to settings that can be namespaced.""" From d9c17e3f30a1186f86b0a3334d201d1467a5269e Mon Sep 17 00:00:00 2001 From: lindsay stevens Date: Thu, 16 Jul 2026 19:26:03 +1000 Subject: [PATCH 38/50] fix: delete xml escaping xls test covered by other tests - The deleted files seem to be checking that a question label like `${a} < ${b}` has the `<` escaped to `<` in the output. This case and other escaping scenarios is covered by the test_xml.py MinidomTextWriterMonkeyPatchTest cases. - The patterns for how pyxform/variable references in labels are emitted are covered by test_whitespace.py test_whitespace_output_permutations. Since that test already is running through a set of variations, a case that matches the deleted file is added to avoid regressions. --- tests/fixtures/example_forms/xml_escaping.xls | Bin 20992 -> 0 bytes tests/test_expected_output/xml_escaping.xml | 139 ------------------ tests/test_whitespace.py | 7 + .../xform_test_case/test_xform_conversion.py | 1 - 4 files changed, 7 insertions(+), 140 deletions(-) delete mode 100644 tests/fixtures/example_forms/xml_escaping.xls delete mode 100644 tests/test_expected_output/xml_escaping.xml diff --git a/tests/fixtures/example_forms/xml_escaping.xls b/tests/fixtures/example_forms/xml_escaping.xls deleted file mode 100644 index fb7d6b3ee3dfbe8b6ce56f1656d7e44afd258741..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 20992 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 diff --git a/tests/test_expected_output/xml_escaping.xml b/tests/test_expected_output/xml_escaping.xml deleted file mode 100644 index f74eb588..00000000 --- a/tests/test_expected_output/xml_escaping.xml +++ /dev/null @@ -1,139 +0,0 @@ - - - - xml_escaping - - - - - jr://images/a.jpg - - - jr://images/b.jpg - - - jr://images/happy.jpg - - - jr://images/sad.jpg - - - - - - - - - - - - - - - - - a - - - - b - - - - c - - - - d - - - - - - - - 1 - - - - 2 - - - - 3 - - - - 4 - - - - 5 - - - - 6 - - - - 7 - - - - 8 - - - - - - - - yes - - - - no - - - - - - - - a_b-0 - a - - - a_b-1 - b - - - - - - - happy_sad-0 - happy - - - happy_sad-1 - sad - - - - - - - - - - - - - - - - - diff --git a/tests/test_whitespace.py b/tests/test_whitespace.py index e7baa6ef..09cba33e 100644 --- a/tests/test_whitespace.py +++ b/tests/test_whitespace.py @@ -30,6 +30,7 @@ def test_whitespace_output_permutations(self): | | text | G1 | Wrap {0} in text | | | text | H1 | Wrap {0} in {0} text | | | text | I1 | Wrap {0} in {0} | + | | text | J1 | Encode {0} < {0} | """ xp = "/h:html/h:body/x:input[@ref='/test_name/{}']/x:label" test_cases = ("A", "B1") @@ -87,6 +88,12 @@ def test_whitespace_output_permutations(self): f"""""" }, ), + ( + xp.format("J1"), + { + f"""""" + }, + ), ], ) diff --git a/tests/xform_test_case/test_xform_conversion.py b/tests/xform_test_case/test_xform_conversion.py index f72b3f3a..b996e808 100644 --- a/tests/xform_test_case/test_xform_conversion.py +++ b/tests/xform_test_case/test_xform_conversion.py @@ -22,7 +22,6 @@ def test_conversion_vs_expected(self): ("survey_no_name.xlsx", False), ("widgets.xls", True), ("xlsform_spec_test.xlsx", True), - ("xml_escaping.xls", True), ) for i, (case, set_name) in enumerate(cases): with self.subTest(msg=f"{i}: {case}"): From 243dda1f14adaabf954590ae3966422b89119a55 Mon Sep 17 00:00:00 2001 From: lindsay stevens Date: Thu, 16 Jul 2026 19:48:34 +1000 Subject: [PATCH 39/50] fix: replace hidden question type xls test with equivalent md test - The deleted file had a single question of the type "hidden". The new test checks that the output for this question type matches the defaults from question_type_dict, which seems more useful than the old test checking only the internal dict representation. - There did not seem to be anywhere obvious to put this new test other than a new module, and many of the existing test modules are about validation and output of question types, so a new package is created to collect those tests. --- tests/fixtures/example_forms/hidden.xls | Bin 18944 -> 0 bytes tests/question_types/__init__.py | 0 tests/question_types/test_hidden.py | 19 +++++++++++++++++++ tests/test_xls2json_xls.py | 21 --------------------- 4 files changed, 19 insertions(+), 21 deletions(-) delete mode 100644 tests/fixtures/example_forms/hidden.xls create mode 100644 tests/question_types/__init__.py create mode 100644 tests/question_types/test_hidden.py diff --git a/tests/fixtures/example_forms/hidden.xls b/tests/fixtures/example_forms/hidden.xls deleted file mode 100644 index 99808f589796b7fe142363ac75e0574101c25d80..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 18944 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 diff --git a/tests/question_types/__init__.py b/tests/question_types/__init__.py new file mode 100644 index 00000000..e69de29b diff --git a/tests/question_types/test_hidden.py b/tests/question_types/test_hidden.py new file mode 100644 index 00000000..a737683e --- /dev/null +++ b/tests/question_types/test_hidden.py @@ -0,0 +1,19 @@ +from tests.pyxform_test_case import PyxformTestCase +from tests.xpath_helpers.questions import xpq + + +class TestHiddenOutput(PyxformTestCase): + def test_hidden(self): + """Should find that the hidden type is recognised and output as a string.""" + md = """ + | survey | + | | type | name | label | + | | hidden | q1 | Q1 | + """ + self.assertPyxformXform( + md=md, + xml__xpath_match=[ + xpq.model_instance_item("q1"), + xpq.model_instance_bind("q1", "string"), + ], + ) diff --git a/tests/test_xls2json_xls.py b/tests/test_xls2json_xls.py index 85bbfcd2..729a8215 100644 --- a/tests/test_xls2json_xls.py +++ b/tests/test_xls2json_xls.py @@ -27,27 +27,6 @@ def test_simple_yes_or_no_question(self): with open(expected_output_path, encoding="utf-8") as expected: self.assertEqual(json.load(expected), result._pyxform) - def test_hidden(self): - x = SurveyReader(utils.path_to_text_fixture("hidden.xls"), default_name="hidden") - x_results = x.to_json_dict() - - expected_dict = [ - {"type": "hidden", "name": "hidden_test"}, - { - "children": [ - { - "bind": {"jr:preload": "uid", "readonly": "true()"}, - "name": "instanceID", - "type": "calculate", - } - ], - "control": {"bodyless": True}, - "name": "meta", - "type": "group", - }, - ] - self.assertEqual(x_results["children"], expected_dict) - def test_gps(self): x = SurveyReader(utils.path_to_text_fixture("gps.xls"), default_name="gps") From 2c00a1179d918d4fbd938e269e3fde8617eccc91 Mon Sep 17 00:00:00 2001 From: lindsay stevens Date: Thu, 16 Jul 2026 22:57:49 +1000 Subject: [PATCH 40/50] chg: replace include question type xls tests with equivalent md tests - The deleted files had a question of the type "include", and the "include.xls" fixture had an additional text question. The test usages were not using the create_survey_from_path parameter "include_directory" which activates the "include" directive behaviour. As a result, the tests were mainly on serialisation equivalence of: - for "include.xls", a "text" question plus an unknown question type - for "include_json.xls", an unknown question type. These scenarios are covered by the remaining alternative files. - Since `create_survey_from_path` is exposed as public in the top level __init__.py, tests were added for the "include_directory" parameter. There were no existing tests for this feature. A small edit to file_utils.py was needed to load files other than .xls and .json. --- pyxform/file_utils.py | 10 +- tests/fixtures/example_forms/include.csv | 6 - tests/fixtures/example_forms/include.md | 6 - tests/fixtures/example_forms/include.xls | Bin 6656 -> 0 bytes tests/fixtures/example_forms/include.xlsx | Bin 11379 -> 0 bytes tests/fixtures/example_forms/include_json.csv | 5 - tests/fixtures/example_forms/include_json.md | 5 - tests/fixtures/example_forms/include_json.xls | Bin 6656 -> 0 bytes .../fixtures/example_forms/include_json.xlsx | Bin 11296 -> 0 bytes tests/question_types/test_include.py | 104 ++++++++++++++++++ tests/test_dump_and_load.py | 5 - tests/test_xform2json.py | 5 - tests/test_xls2json_backends.py | 2 - 13 files changed, 109 insertions(+), 39 deletions(-) delete mode 100644 tests/fixtures/example_forms/include.csv delete mode 100644 tests/fixtures/example_forms/include.md delete mode 100644 tests/fixtures/example_forms/include.xls delete mode 100644 tests/fixtures/example_forms/include.xlsx delete mode 100644 tests/fixtures/example_forms/include_json.csv delete mode 100644 tests/fixtures/example_forms/include_json.md delete mode 100644 tests/fixtures/example_forms/include_json.xls delete mode 100644 tests/fixtures/example_forms/include_json.xlsx create mode 100644 tests/question_types/test_include.py diff --git a/pyxform/file_utils.py b/pyxform/file_utils.py index 7cc69fd4..c5e506ea 100644 --- a/pyxform/file_utils.py +++ b/pyxform/file_utils.py @@ -1,9 +1,10 @@ """The pyxform file utility functions.""" -import glob import os +from pathlib import Path from pyxform import utils +from pyxform.constants import SUPPORTED_FILE_EXTENSIONS from pyxform.xls2json import SurveyReader @@ -31,8 +32,7 @@ def load_file_to_dict(path): def collect_compatible_files_in_directory(directory): """Create a giant dict out of all the spreadsheets and json forms in the given directory.""" - available_files = glob.glob(os.path.join(directory, "*.xls")) + glob.glob( - os.path.join(directory, "*.json") - ) - + types = SUPPORTED_FILE_EXTENSIONS | {".json", ".md", ".csv"} + path = Path(directory) + available_files = [str(p) for t in types for p in path.glob(f"*{t}")] return dict([load_file_to_dict(f) for f in available_files]) diff --git a/tests/fixtures/example_forms/include.csv b/tests/fixtures/example_forms/include.csv deleted file mode 100644 index d8478aa0..00000000 --- a/tests/fixtures/example_forms/include.csv +++ /dev/null @@ -1,6 +0,0 @@ -survey,,, -,type,name,label:English -,text,name,What's your name? -,include,yes_or_no_question,Yes or no question section -choices,,, -,list name,name,label:english diff --git a/tests/fixtures/example_forms/include.md b/tests/fixtures/example_forms/include.md deleted file mode 100644 index b83d34d7..00000000 --- a/tests/fixtures/example_forms/include.md +++ /dev/null @@ -1,6 +0,0 @@ -| survey | -| | type | name | label:English | -| | text | name | What's your name? | -| | include | yes_or_no_question | Yes or no question section | -| choices | -| | list name | name | label:english | diff --git a/tests/fixtures/example_forms/include.xls b/tests/fixtures/example_forms/include.xls deleted file mode 100644 index f8612f75933d23397722d09bce301ac55aeceb25..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 6656 zcmeHLTWnNS6y0|^Q*J5HX`zU`yi{omZIQ=^_`ptqDkK6;!4Q?C4%6FqWIBWMQ2JpU zUVoB6Ffk@Iis1ue;v<@vs6qH6@fi~m^b3qXL_Y?jF+^ZoYu`KcGMS-MLNsX4%{q^L z_Ia;!_Br>vZ|lz=`*7|BDRK`>m0Zi$NVP|9AU?H_4vP2?r|eojpD&0Jopg853|w{l z$~E+35d#3%dw`QG{G`OR~@3MjY#dFFHfPXnd{Gk`h(CP3pvJ^1RtMy>>0j~4*R%fu81aGswDRI7$actXa${XM7P0;*w^sWQHptCWAn6*z zkGqCXMu8dfzREvWe~t1FY9X;!Lx*>g5hDaWkwfY?sXqh0A)jcOA62pf%sd0F{L6<~ z3{i;3#}B~DT{J;Oy$2< zCg-S2>6eqU)>8WaUHNg$sm0;Bt}xGS&OGFbMX+o??Q1$*Qm zPVY8tZ;C{Q648jAdIcNYz<1g(0g-Ko4EaTN@|e#(t<6{GEQTmXQ$acrq$g2`uaepv z&A}H?G-a=W>!W!Jn^DY=EWAX60Rv*fkeDzK=AW=3SLAgyEHpA0UV3D=jUXN#wxJmd z@3dnNb;k!|(bN!g?LBE&ob5y5bW6$_No0~1H9N7FqVY&9({H2k5j)kFNcP1Oea~m? zR63f7!vH;Hr>q3D6IMZJrR)eZ(M8mr){3q-X**5k@!pv#htM)@U&L$#5OE!C9i5MT zcMj6v4F~Bo%u7G!kjqf7a}>m^TzTix_nUV1boQw{%PDo;VlJ@cWiN70`VNfU8pw5xX`oJhB_%~(yaa}J)}Wy z1LB}Sj!UjxLp5k8w_I!S=UP=>lXJ$2gva@0s+MEZ+|G~s8swx_5>lg)kxC4tt?oS$J7zujK*u&3m6)yD zbZvBCV6~%uYd)6AY4p~Lqxa7KD=yCEKY#Ge-!5+%3ch>9l(v?Se&8wbF+d(Le*?gC zs0W~I2;l#kJpfJ80f6te!vO8qD8TpI34ooxc-47}@tp07_a|XYHieV@b`oj6(O73@ zS2#WxwlqzKu0$t72(y2^@hRVPj2B!8k?huI=JcKX(qLTPK2y7_0PP!c~+1p>~y0zp1PNtIhT1J}4Ngn-*|0k8p#)8L|%T4&#Np1$}tV5l+Bo z`y3D3rmLiA8iU!(^iCsytk~+L|I4wJVNu?rT2h#jkLl=;(lEL_g@#f{fzl}KI z2Xsv+N#iY?aL;^4zu0ee+o!Flbo|Sp`?Fh7K-Qy$i1xwOKadma&+1zk|4Ou-cH6%l O!9?fZ`~U6VZvStayMI6c diff --git a/tests/fixtures/example_forms/include.xlsx b/tests/fixtures/example_forms/include.xlsx deleted file mode 100644 index b2a9cee70f3e5c9afe880cff32307c3045dcd686..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 11379 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 Date: Thu, 16 Jul 2026 23:22:05 +1000 Subject: [PATCH 41/50] fix: delete tutorial xls test covered by other tests - The deleted file had a repeat group containing some items such as select_one, select_multiple, integer, etc., and showed using pyxform variable syntax, constraints, and "relevant" expressions. There was also a "tutorial notes" column which explained these features. - The demonstrated features are covered by test_fields.py and test_repeat.py (and some other places as well) - The documentation role of this fixture is covered by xlsform.org, the template XLSForm reference, docs.getodk.org, and the forum. --- tests/fixtures/example_forms/README.rst | 6 ------ tests/fixtures/example_forms/tutorial.xls | Bin 11776 -> 0 bytes tests/test_tutorial_xls.py | 14 -------------- 3 files changed, 20 deletions(-) delete mode 100644 tests/fixtures/example_forms/README.rst delete mode 100644 tests/fixtures/example_forms/tutorial.xls delete mode 100644 tests/test_tutorial_xls.py diff --git a/tests/fixtures/example_forms/README.rst b/tests/fixtures/example_forms/README.rst deleted file mode 100644 index 961a4a44..00000000 --- a/tests/fixtures/example_forms/README.rst +++ /dev/null @@ -1,6 +0,0 @@ -====================== -Example Questionnaires -====================== - -The Microsoft Excel files in this directory demonstrate the xls2xform -syntax. To get a good overview of the syntax look at tutorial.xls. diff --git a/tests/fixtures/example_forms/tutorial.xls b/tests/fixtures/example_forms/tutorial.xls deleted file mode 100644 index 247f08177a5018139c30bc71ac26172086c74532..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 11776 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 YKc6KT`%l-gQTel`I$dj%&PQ4Qzff2a5C8xG diff --git a/tests/test_tutorial_xls.py b/tests/test_tutorial_xls.py deleted file mode 100644 index 89e60a13..00000000 --- a/tests/test_tutorial_xls.py +++ /dev/null @@ -1,14 +0,0 @@ -"""Test tutorial XLSForm.""" - -from unittest import TestCase - -from pyxform.builder import create_survey_from_path - -from tests import utils - - -class TutorialTests(TestCase): - @staticmethod - def test_create_from_path(): - path = utils.path_to_text_fixture("tutorial.xls") - create_survey_from_path(path) From 29cf8c2d4c0aac62a21c9bae248e3024ef0afd7d Mon Sep 17 00:00:00 2001 From: lindsay stevens Date: Fri, 17 Jul 2026 18:59:20 +1000 Subject: [PATCH 42/50] fix: delete specify_other xls tests covered by other tests - The deleted files had a "select_one or_other" question with a choices list. There existing coverage for or_other in test_translations.py, and new cases were added in test_select_or_other.py to document the current behaviour with all select types. - there are some expectedFailure tests which seem to be bugs. - to make the new tests less repetitive or fragile, the select dict in aliases.py was split into collections by type. - The test in test_file_utils.py that was using specify_other was not specific to that fixture, so new tests for the function convert_file_to_csv_string were added to test_xls2json_backends.py. - there are some expectedFailure tests which seem to be bugs. - in preparation for removing xls support, the function was modified to default to "xlsx" for non-csv input. - Other serialisation tests in test_dump_and_load.py and test_xform2json.py are still covered by other fixtures. --- pyxform/aliases.py | 14 +- pyxform/xls2json_backends.py | 4 +- .../fixtures/example_forms/specify_other.csv | 8 -- tests/fixtures/example_forms/specify_other.md | 7 - .../fixtures/example_forms/specify_other.xls | Bin 18432 -> 0 bytes .../fixtures/example_forms/specify_other.xlsx | Bin 10861 -> 0 bytes tests/question_types/test_select_or_other.py | 136 ++++++++++++++++++ tests/test_builder.py | 55 ------- tests/test_dump_and_load.py | 1 - tests/test_file_utils.py | 16 --- tests/test_xform2json.py | 1 - tests/test_xls2json_backends.py | 65 ++++++++- 12 files changed, 212 insertions(+), 95 deletions(-) delete mode 100644 tests/fixtures/example_forms/specify_other.csv delete mode 100644 tests/fixtures/example_forms/specify_other.md delete mode 100644 tests/fixtures/example_forms/specify_other.xls delete mode 100644 tests/fixtures/example_forms/specify_other.xlsx create mode 100644 tests/question_types/test_select_or_other.py delete mode 100644 tests/test_file_utils.py diff --git a/pyxform/aliases.py b/pyxform/aliases.py index 88ed2b7c..36d8bef4 100644 --- a/pyxform/aliases.py +++ b/pyxform/aliases.py @@ -24,18 +24,24 @@ "select one from file": constants.SELECT_ONE, "select multiple from file": constants.SELECT_ALL_THAT_APPLY, } -select = { +select_one = { "add select one prompt using": constants.SELECT_ONE, - "add select multiple prompt using": constants.SELECT_ALL_THAT_APPLY, - "select all that apply from": constants.SELECT_ALL_THAT_APPLY, "select one from": constants.SELECT_ONE, "select1": constants.SELECT_ONE, "select_one": constants.SELECT_ONE, "select one": constants.SELECT_ONE, +} +select_multiple = { + "add select multiple prompt using": constants.SELECT_ALL_THAT_APPLY, + "select all that apply from": constants.SELECT_ALL_THAT_APPLY, "select_multiple": constants.SELECT_ALL_THAT_APPLY, "select all that apply": constants.SELECT_ALL_THAT_APPLY, - "select_one_external": constants.SELECT_ONE_EXTERNAL, +} +select = { + **select_one, + **select_multiple, **select_from_file, + "select_one_external": constants.SELECT_ONE_EXTERNAL, "rank": constants.RANK, } settings_header = { diff --git a/pyxform/xls2json_backends.py b/pyxform/xls2json_backends.py index 0f2d2c0f..c6e1be4b 100644 --- a/pyxform/xls2json_backends.py +++ b/pyxform/xls2json_backends.py @@ -406,7 +406,7 @@ def process_csv_data(rd): def convert_file_to_csv_string(path): """ - This will open a csv or xls file and return a CSV in the format. + This will open a csv or xlsx file and return a CSV in the format. ``` sheet_name1 @@ -425,7 +425,7 @@ def convert_file_to_csv_string(path): if path.endswith(".csv"): imported_sheets = csv_to_dict(path) else: - imported_sheets = xls_to_dict(path) + imported_sheets = xlsx_to_dict(path) foo = StringIO(newline="") writer = csv.writer(foo, delimiter=",", quotechar='"', quoting=csv.QUOTE_MINIMAL) for sheet_name, rows in imported_sheets.items(): diff --git a/tests/fixtures/example_forms/specify_other.csv b/tests/fixtures/example_forms/specify_other.csv deleted file mode 100644 index 593a1506..00000000 --- a/tests/fixtures/example_forms/specify_other.csv +++ /dev/null @@ -1,8 +0,0 @@ -"survey",,, -,"type","name","label:English" -,"select one from sexes or specify other","sex","What sex are you?" -,,, -"choices",,, -,"list name","name","label:English" -,"sexes","male","Male" -,"sexes","female","Female" diff --git a/tests/fixtures/example_forms/specify_other.md b/tests/fixtures/example_forms/specify_other.md deleted file mode 100644 index 0d888ff9..00000000 --- a/tests/fixtures/example_forms/specify_other.md +++ /dev/null @@ -1,7 +0,0 @@ -| survey | -| | type | name | label:English | -| | select one from sexes or specify other | sex | What sex are you? | -| choices | -| | list name | name | label:English | -| | sexes | male | Male | -| | sexes | female | Female | diff --git a/tests/fixtures/example_forms/specify_other.xls b/tests/fixtures/example_forms/specify_other.xls deleted file mode 100644 index 5d403ff98b28221355aef530e7df5e5dcd0f52b6..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 18432 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 diff --git a/tests/fixtures/example_forms/specify_other.xlsx b/tests/fixtures/example_forms/specify_other.xlsx deleted file mode 100644 index 9f9735596dfe82675a6857fbf16746839cc3ed0b..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 10861 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 diff --git a/tests/question_types/test_select_or_other.py b/tests/question_types/test_select_or_other.py new file mode 100644 index 00000000..21511b50 --- /dev/null +++ b/tests/question_types/test_select_or_other.py @@ -0,0 +1,136 @@ +from collections.abc import Iterable +from unittest import expectedFailure + +from pyxform.aliases import select, select_multiple, select_one + +from tests.pyxform_test_case import PyxformTestCase +from tests.xpath_helpers.choices import xpc +from tests.xpath_helpers.questions import xpq + + +class TestSelectOrOtherOutput(PyxformTestCase): + @staticmethod + def get_cases( + commands: Iterable[str], control: str, list_name: str = "c1" + ) -> tuple[tuple[str, str], ...]: + return tuple( + (f"{s} {list_name} {o}", control) + for s in commands + for o in ("or specify other", "or_other", "or other") + ) + + def test_aliases__select_one__ok(self): + """Should find that all supported aliases result in the same output.""" + md = """ + | survey | + | | type | name | label | + | | {} | q1 | Q1 | + + | choices | + | | list_name | name | label | + | | c1 | n1 | N1 | + """ + for case in self.get_cases(select_one, "select1"): + with self.subTest(msg=case): + self.assertPyxformXform( + md=md.format(case[0]), + xml__xpath_match=[ + xpc.model_instance_choices_label( + "c1", (("n1", "N1"), ("other", "Other")) + ), + xpq.model_instance_item("q1"), + xpq.model_instance_bind("q1", "string"), + xpq.body_control("q1", case[1]), + xpq.model_instance_item("q1_other"), + xpq.model_instance_bind("q1_other", "string"), + xpq.body_control("q1_other", "input"), + ], + ) + + def test_aliases__select_multiple__ok(self): + """Should find that all supported aliases result in the same output.""" + md = """ + | survey | + | | type | name | label | + | | {} | q1 | Q1 | + + | choices | + | | list_name | name | label | + | | c1 | n1 | N1 | + """ + for case in self.get_cases(select_multiple, "select"): + with self.subTest(msg=case): + self.assertPyxformXform( + md=md.format(case[0]), + xml__xpath_match=[ + xpc.model_instance_choices_label( + "c1", (("n1", "N1"), ("other", "Other")) + ), + xpq.model_instance_item("q1"), + xpq.model_instance_bind("q1", "string"), + xpq.body_control("q1", case[1]), + xpq.model_instance_item("q1_other"), + xpq.model_instance_bind("q1_other", "string"), + xpq.body_control("q1_other", "input"), + ], + ) + + def test_aliases__select_from_file__error(self): + """Should raise an error for unsupported select types used with or_other.""" + md = """ + | survey | + | | type | name | label | + | | {} | q1 | Q1 | + + | choices | + | | list_name | name | label | + | | c1 | n1 | N1 | + """ + for case in self.get_cases(select_multiple, "select", "c1.csv"): + with self.subTest(msg=case): + self.assertPyxformXform( + md=md.format(case[0]), + errored=True, + error__contains=[ + "[row : 2] Please specify choices for this 'or other' question." + ], + ) + + # Does not raise an error, just outputs a form with `q1` but no `q1_other`. + @expectedFailure + def test_aliases__rank__error(self): + """Should raise an error for unsupported select types used with or_other.""" + md = """ + | survey | + | | type | name | label | + | | rank c1 | q1 | Q1 | + + | choices | + | | list_name | name | label | + | | c1 | n1 | N1 | + """ + for case in self.get_cases(("rank",), "odk:rank"): + with self.subTest(msg=case): + self.assertPyxformXform( + md=md.format(case[0]), + errored=True, + ) + + # Does not raise an error, just outputs a form with `q1` but no `q1_other`. + @expectedFailure + def test_aliases__select_one_external__error(self): + """Should raise an error for unsupported select types used with or_other.""" + md = """ + | survey | + | | type | name | label | choice_filter | + | | select_one_external c1 | q1 | Q1 | false() | + + | external_choices | + | | list_name | name | label | + | | c1 | n1 | N1 | + """ + self.assertIn("select_one_external", select) + self.assertPyxformXform( + md=md, + errored=True, + ) diff --git a/tests/test_builder.py b/tests/test_builder.py index ebeb95c6..9ed91e42 100644 --- a/tests/test_builder.py +++ b/tests/test_builder.py @@ -90,61 +90,6 @@ def test_create_table_from_dict(self): } self.assertEqual(expected_dict, g.to_json_dict()) - def test_specify_other(self): - survey = utils.create_survey_from_fixture( - "specify_other", filetype=FIXTURE_FILETYPE - ) - expected_dict = { - "name": "specify_other", - "type": "survey", - "title": "specify_other", - "default_language": "default", - "id_string": "specify_other", - "sms_keyword": "specify_other", - "choices": { - "sexes": [ - {"label": {"English": "Male"}, "name": "male"}, - {"label": {"English": "Female"}, "name": "female"}, - {"label": {"English": "Other"}, "name": "other"}, - ] - }, - "children": [ - { - "name": "sex", - "label": {"English": "What sex are you?"}, - "type": "select one", - "list_name": "sexes", - "itemset": "sexes", - "children": [ - # TODO Change to choices (there is stuff in the - # json2xform half that will need to change) - {"name": "male", "label": {"English": "Male"}}, - {"name": "female", "label": {"English": "Female"}}, - {"name": "other", "label": {"English": "Other"}}, - ], - }, - { - "name": "sex_other", - "bind": {"relevant": "selected(../sex, 'other')"}, - "label": "Specify other.", - "type": "text", - }, - { - "children": [ - { - "bind": {"jr:preload": "uid", "readonly": "true()"}, - "name": "instanceID", - "type": "calculate", - } - ], - "control": {"bodyless": True}, - "name": "meta", - "type": "group", - }, - ], - } - self.assertEqual(survey.to_json_dict(), expected_dict) - def test_loop(self): survey = utils.create_survey_from_fixture("loop", filetype=FIXTURE_FILETYPE) expected_dict = { diff --git a/tests/test_dump_and_load.py b/tests/test_dump_and_load.py index c4c01121..50f56cce 100644 --- a/tests/test_dump_and_load.py +++ b/tests/test_dump_and_load.py @@ -13,7 +13,6 @@ class DumpAndLoadTests(TestCase): def setUp(self): self.excel_files = [ "gps.xls", - "specify_other.xls", "group.xls", "loop.xls", "text_and_integer.xls", diff --git a/tests/test_file_utils.py b/tests/test_file_utils.py deleted file mode 100644 index 08cf6103..00000000 --- a/tests/test_file_utils.py +++ /dev/null @@ -1,16 +0,0 @@ -"""Test xls2json_backends util functions.""" - -from unittest import TestCase - -from pyxform.xls2json_backends import convert_file_to_csv_string - -from tests import utils - - -class BackendUtilsTests(TestCase): - def test_xls_to_csv(self): - specify_other_xls = utils.path_to_text_fixture("specify_other.xls") - converted_xls = convert_file_to_csv_string(specify_other_xls) - specify_other_csv = utils.path_to_text_fixture("specify_other.csv") - converted_csv = convert_file_to_csv_string(specify_other_csv) - self.assertEqual(converted_csv, converted_xls) diff --git a/tests/test_xform2json.py b/tests/test_xform2json.py index beae4713..8732c918 100644 --- a/tests/test_xform2json.py +++ b/tests/test_xform2json.py @@ -22,7 +22,6 @@ def setUp(self): self.excel_files = [ "gps.xls", "choice_filter_test.xlsx", - "specify_other.xls", "loop.xls", "text_and_integer.xls", "simple_loop.xls", diff --git a/tests/test_xls2json_backends.py b/tests/test_xls2json_backends.py index e7e09b0b..e39c1529 100644 --- a/tests/test_xls2json_backends.py +++ b/tests/test_xls2json_backends.py @@ -2,14 +2,18 @@ import datetime import os +from pathlib import Path +from unittest import expectedFailure import openpyxl import xlrd from pyxform.builder import create_survey_element_from_dict from pyxform.xls2json import workbook_to_json from pyxform.xls2json_backends import ( + convert_file_to_csv_string, csv_to_dict, get_xlsform, + md_table_to_workbook, md_to_dict, xls_to_dict, xls_value_to_unicode, @@ -157,7 +161,6 @@ def test_equivalency(self): equivalent_fixtures = [ "group", "loop", - "specify_other", "text_and_integer", "yes_or_no_question", ] @@ -253,3 +256,63 @@ def test_xlsx_with_many_empty_rows(self): self.assertTupleEqual((2, 2), (settings.max_row, settings.max_column)) wb.close() + + def test_convert_file_to_csv_string(self): + """Should find that the same data read from xlsx or csv results in the same csv.""" + md = """ + | survey | + | | type | name | label | + | | text | q1 | Q1 | + """ + csv = "survey\n,type,name,label\n,text,q1,Q1" + wb = md_table_to_workbook(md) + with utils.get_temp_dir() as tmp: + form_path = Path(tmp) / "test_name.xlsx" + wb.save(form_path) + wb.close() + converted_form = convert_file_to_csv_string(str(form_path)) + csv_path = Path(tmp) / "test_name.csv" + csv_path.write_text(csv) + converted_csv = convert_file_to_csv_string(str(csv_path)) + self.assertEqual(converted_csv, converted_form) + + @expectedFailure + def test_convert_file_to_csv_string__column_order_bug(self): + """Should find that the header order is preserved when writing to csv.""" + md = """ + | survey | + | | type | name | label | hint | constraint | relevant | + | | text | q1 | Q1 | Hmm | | true() | + | | text | q2 | Q2 | | . != '0' | | + """ + csv = ( + "survey" + "\n,type,name,label,hint,constraint,relevant" + "\n,text,q1,Q1,Hmm,,true()" + "\n,text,q2,Q2,,\". != '0'\"," + ) + wb = md_table_to_workbook(md) + with utils.get_temp_dir() as tmp: + form_path = Path(tmp) / "test_name.xlsx" + wb.save(form_path) + wb.close() + converted_form = convert_file_to_csv_string(str(form_path)) + csv_path = Path(tmp) / "test_name.csv" + csv_path.write_text(csv) + converted_csv = convert_file_to_csv_string(str(csv_path)) + self.assertEqual(converted_csv, converted_form) + + # Bug seems to occur because convert_file_to_csv collects the headers from the + # rows rather than using the "survey_header" (or similar). In this case the first + # row has no constraint, but the second row does, so constraint is put last. + expected_header = ",type,name,label,hint,constraint,relevant" + bugged_header = ",type,name,label,hint,relevant,constraint" + # Expected header is there but only in the the "survey_header" dict. + self.assertEqual(1, converted_csv.count(expected_header)) + self.assertEqual(1, converted_form.count(expected_header)) + # Bugged header appears in the "survey" dict. + self.assertEqual(1, converted_csv.count(bugged_header)) + self.assertEqual(1, converted_form.count(bugged_header)) + # Fail the test since the "survey" data should retain column order. + self.assertEqual(2, converted_csv.count(expected_header)) + self.assertEqual(2, converted_form.count(expected_header)) From 9d88f2cfadc14739ee65c18720b29e123c8635d4 Mon Sep 17 00:00:00 2001 From: lindsay stevens Date: Fri, 17 Jul 2026 19:23:14 +1000 Subject: [PATCH 43/50] fix: delete gps.csv test fixture not referenced anywhere - The deleted file is not the same as the adjacent file "gps.xls": the xls has a question label "GPS", and no "choices" sheet. Both have a single "gps" type question named "location". - There is coverage of gps-related types in test_geo.py. --- tests/fixtures/example_forms/gps.csv | 5 ----- 1 file changed, 5 deletions(-) delete mode 100644 tests/fixtures/example_forms/gps.csv diff --git a/tests/fixtures/example_forms/gps.csv b/tests/fixtures/example_forms/gps.csv deleted file mode 100644 index a4b60bce..00000000 --- a/tests/fixtures/example_forms/gps.csv +++ /dev/null @@ -1,5 +0,0 @@ -"survey",, -,"type","name" -,"gps","location" -"choices",, -,"list name","value","text:english" From b75cc8f3016c2032d3f23ab50d630fd974598d40 Mon Sep 17 00:00:00 2001 From: lindsay stevens Date: Fri, 17 Jul 2026 19:30:26 +1000 Subject: [PATCH 44/50] fix: delete gps xls test covered by other tests - The deleted file had a single "gps" type question named "location", and a non-localised label "GPS". There is coverage of this scenario and other gps-related types in test_geo.py. The serialisation tests this fixture was used in are covered by other fixtures. --- tests/fixtures/example_forms/gps.xls | Bin 5632 -> 0 bytes tests/test_dump_and_load.py | 1 - tests/test_xform2json.py | 1 - tests/test_xls2json_xls.py | 21 --------------------- 4 files changed, 23 deletions(-) delete mode 100644 tests/fixtures/example_forms/gps.xls diff --git a/tests/fixtures/example_forms/gps.xls b/tests/fixtures/example_forms/gps.xls deleted file mode 100644 index c8be86c541b12c7d98dc132a12fa32a58bf11baa..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 5632 zcmeHLT})h65T3iR^zy$@s4Xpci*^eIjMy~Rn95Sbwu!U^Fd8+f3tT96VaWn255_H( zw>Ht#L?4>8O?c3lC`J<>Y$`lyVvWY6N%e{P*4T%w(U>NM_50@DEem1GuA-)f+06Nw zGv~~iIdf*t-S59EnY-{w#Wh(5k4u&;#B(JliEf~6Ci*@R589+H#N+V<8O?RLhb%Df z?v-bljYISTdFC5{q~*HxJ*3k5Rx_|bhlKGP6-!!W2z5lpapLL2CU{J;xO8SF$BT6M zSL0cizi5HgY{l_p`!jwsj%NXv-+#t>-v2ycKCl2-2rL2?1Caux1Xv2(2rL7Z12+Nh z18xRx0agIF0=EGxfs}v0TeDq__5;8NU7Fvm7qwb%{ZkgmHF1hLUiqT%GDE(`{%i1v z>Finci=RPzQHJRoEx}Na^>4v`-9ylzZ_dC;_n~FTSx}4QW2Iiya-Vu1m;z$$Ma37% zSLkrwX)UX@EJA6>=Q`&nr8L4XzXn(QjknxO;5C{N{{|!|Az3yGzO@>y$}W&8=GOzQ zhg&Pr0oU-}emYEeBG5_Yb0>=0W{ zuT4R(PeI?6f}Xn~UEm42pF7X$8S9J(cP%P$*1l+#)+Ny#txKcXT5pVQ(7G(@jlZNf zV-ojb8WVYgN5&$r*yE>gXAS&j4HGERb=r_$s0loJLrfjMIYPzI#xCSyNxfr*@8BG#LKARXM6JsLmwe#D}q=SfoQnEltS<@4>|G5w8R49pd?@iEi7?4fsxtlT{@#q%L(NzLnhiT;&1U+cMS~M zBi3R2RjVUB7#ynau=~eC!AP~g@ezM>xzq}B)hc}L!G7EFx1Ur2Z))ksW_BxFZ`zsE zu0i``!_|p~tFflqr1k}=hHAY){wAc}s5cVx6SX1FVC{T)U2TYqi<{bI?E)hInT=Ij)mB=AU{ z&g1f2^9fI>T+-wgP_7{@&to_r`)XD#Xjj^x(%mCEDi$?K9z*qT?CabLM1RLQ6+z#xtD>3((pz&e8>Fy}8MAzur1>%=h8jrqu2H^hX}vMPTAk528&M zXm@GHQ1iVR1=5bEfP8np2V`KF0`mPi4dkY;&x=%{rUN$*^+s?@+k%l^JA(e*=mDKSi;|?){vJ@7i z4e3XTJDAS&E)57ghZaa4#0&Qb_V98#Q Date: Fri, 17 Jul 2026 22:37:29 +1000 Subject: [PATCH 45/50] chg: replace table-list xls test with equivalent md tests - The deleted file was similar to the test case in test_group.py test_table_list_appearance except that there were two such groups with a table-list appearance. - The existing test test_table_list_appearance is updated to use exhaustive xpath checks to cover the previous XML segment check. - An extra test is added to check the code path for preserving other appearance values - the previous usage of "minimal" was misleading since that is an appearance for selects so it does not appear in the result xform. --- tests/fixtures/example_forms/table-list.xls | Bin 9216 -> 0 bytes tests/test_expected_output/table-list.xml | 147 -------------------- tests/test_group.py | 97 +++++++++---- tests/test_xform2json.py | 1 - tests/xpath_helpers/questions.py | 8 +- 5 files changed, 77 insertions(+), 176 deletions(-) delete mode 100644 tests/fixtures/example_forms/table-list.xls delete mode 100644 tests/test_expected_output/table-list.xml diff --git a/tests/fixtures/example_forms/table-list.xls b/tests/fixtures/example_forms/table-list.xls deleted file mode 100644 index 2a37fbf3474dbca9ce48c0db76f04591cda2dfa4..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 9216 zcmeHMU2I%O6+U;@Ywy}|uJa#Kn%;({`EeX@oHl7nYA2~fEj9fk{HoyI-MzLq-Q8<; z*A12^xX`>&si+UUR6>>Vfi+1>L#Iqtlw!n>S#rCBA3wi9~&Yb+b_cd;L-TQAy zMt;(99;L{e$dAi3W5Y4GT5|qZ>8R@9TGZhj4w=JN2hN&|yokJ7eysFgXn9!Hf(L0t zoP%1iWYQV|pU87sZbNCI7t5b%pFb#lP`1i%z_sv==Xs2wLzneg@vsNV;$kN6=nDTlK$eHL~-vwCtDz>Rm^>tsSf6EF<-&Mjsk|?8t+cWIZ%IEB{(>@C?&A zhjdO*>HlnxPU~HdeYc9>7ocP&BRvjlzj67<4#={c;s z>hr*2x%3B1u*WyY^cOPN&2zHAXWv{_pKIni^|^MgL!ayBy7akzF1PT6?wK>#L4V|x z05PID(|F38J%iieiPfZZNomTjl)UD*5hyJDowxGHA}61-H7dGWyIGmi^NeiQP)Mf%>AKV`oJ z`f+4TuQfaE;Zb*M9p^FhJD+DKF-dXTlSpOmDZHw)*a7n zR=kE&^IB7Wc~`&WGjejO(sc0eIx|hLF#v@tb+7Dj*3N|Aa9UHIGYGj2|MYgO;trhM zu!hQ0h1Q5Py{cDgjr(=anf03EbssiPC^UoxdUUXDVf*Ukq_D+f?T73~XE4KxUw8Jk zJ?2nj(U|ZIuU;NkN!p{^dd3EM#ZgTd5`k>k!!m}y9lNzhzn0%rULLp<`YokBtL2^O|MuUR{xZBHQQwLBg&j}h|Lx&j ziTX>j4ce433L|Gm_E>wYo>!m1|E1yZX~}2MQjn*0+9G3{&5JlOdr1Okz**~$+p~zY z{z!~F5Ff{SW1Q2MKAWTH_?-w17w(ALyAa-M-7#iF9$OXT0$&nW$G96y^h#fh5#X#> z3I5ETHgtu%rxET5#okMJV8m=6wu`7EY!&C)bD7if#eWe8>O!c5a+p3FQ;tFLcXoL#vqQeG2-MXIMa0$S|skrn}x0S820OcZW{yv zH{fJHL~TX1oh`SZ{3SSZ%DDk@*^ictXVj@BLdh9%Rc*Ng<=^BH)H3rAppMuz&k?zvbRT%s z{3lR_FU{{%dSTw|E;7U^Bi-7MeQ2xuGHc~k8b(G~1nE|e*9((SKrj9>nMqu?CMhvU z2%~)UB)Yi~x8wgL;$l`o#4`71dSn=}5J%PUVZ@@LvMCxpKTYEpX&P4{O{0&cX|#Qs zM&C%&xEg61cYm732%M%d5~OL22Wc8TD@|iGNYfYv(vg?vW}3zrkhUo!L6XKD%w)@U zlm;;>a3Ku3Hp<=^v~hiGTzU;BZveJ%m#`ntOj~vZh^rQJns~K4Y9nWVfVh@1r-@K| zqBe341jxjsPJK3^7;G{proIT1J#_=BI#O+&kp_j#+ys_9( ztmci(mb(K^`d-Z0tDIq!Whk=cK)^}uW6ssexh-lVCw~m0CFrNN+!G;6KNKNy-Wwo# zZ_K$y$NXf}M$W?lqEFj$Uxbprv<^GKmf@g{o*r}ZhZtKzUs|u6BLOFEXUmZYk@IMT zIOeecaaY8gn{>?kqc(Dm0wqrhCwWr9ZI*pfpbtG*PYOImMzl1Ao)nv~JUl5j;qQhg zMWXXc_5HK}184X>hrx`COw1s}=Suo;Tfo99XA$!`%Rc;s#B@$eInE0-ex7LfO@E^0 zjDEG`Rh@ed44mYTX;n{e(nl*36Xr*E(`VTw{Lctoc2CpcDlVUVX-+5l^`$f4`^P&E zPxbxaJ9+8f@$&C^{Cy3Xhbh~sI1k1VWFC7DBJ)uDJTi~0GBOXOX=F-x>x$@=pVxyy zoU9{w&b^h-^7VoBFpaP>eeCj^e}2u<`j>Bi`T1YJPLbE7rSzw~%*EF)zPR_6z8Ag= z{r9}{BZl}~iuO2Yn~^y - - - table-list - - - - - jr://images/sad.jpg - - - jr://images/happy.jpg - - - jr://images/happy.jpg - - - jr://images/sad.jpg - - - jr://images/sad.jpg - - - jr://images/happy.jpg - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - This is a user-defined hint for the 1st row - - - - - - yes - - - - no - - - - - This is a user-defined hint for the 2nd row - - - yes - - - - no - - - - - This is a user-defined hint for the 3rd row - - - yes - - - - no - - - - - - - This is a user-defined hint for the 1st row - - - - - - - diff --git a/tests/test_group.py b/tests/test_group.py index 6e30d65b..1051834c 100644 --- a/tests/test_group.py +++ b/tests/test_group.py @@ -8,6 +8,7 @@ from tests.pyxform_test_case import PyxformTestCase from tests.xpath_helpers.group import xpg +from tests.xpath_helpers.questions import xpq class TestGroupOutput(PyxformTestCase): @@ -76,37 +77,79 @@ def test_group_relevant_included_in_bind(self): ) def test_table_list_appearance(self): + """Should find that the table-list shortcut applies field-list and list-nolabel.""" md = """ | survey | - | | type | name | label | hint | appearance | - | | begin_group | tablelist1 | Table_Y_N | | table-list minimal | - | | select_one yes_no | options1a | Q1 | first row! | | - | | select_one yes_no | options1b | Q2 | | | - | | end_group | | | | | + | | type | name | label | hint | appearance | + | | begin_group | g1 | G1 | | table-list | + | | select_one c1 | q1 | Q1 | first row! | | + | | select_one c1 | q2 | Q2 | | | + | | end_group | | | | | + + | choices | + | | list_name | name | label | + | | c1 | n1 | N1 | + """ + self.assertPyxformXform( + md=md, + xml__xpath_match=[ + # Model instance for specified + generated items. + xpq.model_instance_item("g1"), + xpq.model_instance_item("g1/x:generated_table_list_label_2"), + xpq.model_instance_item("g1/x:reserved_name_for_field_list_labels_3"), + xpq.model_instance_item("g1/x:q1"), + xpq.model_instance_item("g1/x:q2"), + # Model bind for specified + generated items. + xpq.model_instance_bind("g1/generated_table_list_label_2", "string"), + xpq.model_instance_bind( + "g1/reserved_name_for_field_list_labels_3", "string" + ), + xpq.model_instance_bind("g1/q1", "string"), + xpq.model_instance_bind("g1/q2", "string"), + # Body control group and selects are assigned appearances. + xpg.group_no_label_appearance("/test_name/g1", "field-list"), + xpq.body_label_inline( + "group/x:input", "g1/generated_table_list_label_2", "G1" + ), + xpq.body_group_select1_itemset( + "g1", "reserved_name_for_field_list_labels_3", "label" + ), + xpq.body_label_inline( + "group/x:select1", "g1/reserved_name_for_field_list_labels_3", " " + ), + xpq.body_group_select1_itemset("g1", "q1", "list-nolabel"), + xpq.body_label_inline("group/x:select1", "g1/q1", "Q1"), + """ + /h:html/h:body/x:group/x:select1[@ref='/test_name/g1/q1'] + /x:hint[text()='first row!'] + """, + xpq.body_group_select1_itemset("g1", "q2", "list-nolabel"), + xpq.body_label_inline("group/x:select1", "g1/q2", "Q2"), + ], + ) + + def test_table_list_appearance__preserve_additional_appearances(self): + """Should find that the table-list shortcut keeps any extra appearances.""" + # Currently the documented / supported appearances for groups are 'table-list' and + # 'field-list', so an extra 'fake' appearance is added to check that anything else + # will be preserved (without adding confusion as to what is supported). + md = """ + | survey | + | | type | name | label | hint | appearance | + | | begin_group | g1 | G1 | | table-list fake | + | | select_one c1 | q1 | Q1 | first row! | | + | | select_one c1 | q2 | Q2 | | | + | | end_group | | | | | + | choices | | | list_name | name | label | - | | yes_no | yes | Yes | - """ - xml_contains = """ - - - - - - - - - - - - - first row! -""".strip() - self.assertPyxformXform( - name="table-list-appearance-mod", - md=md, - xml__contains=[xml_contains], + | | c1 | n1 | N1 | + """ + self.assertPyxformXform( + md=md, + xml__xpath_match=[ + xpg.group_no_label_appearance("/test_name/g1", "field-list fake"), + ], ) def test_group__label__ok(self): diff --git a/tests/test_xform2json.py b/tests/test_xform2json.py index ac5e1825..d213a581 100644 --- a/tests/test_xform2json.py +++ b/tests/test_xform2json.py @@ -27,7 +27,6 @@ def setUp(self): "yes_or_no_question.xls", "xlsform_spec_test.xlsx", "field-list.xlsx", - "table-list.xls", "group.xls", ] self.surveys = {} diff --git a/tests/xpath_helpers/questions.py b/tests/xpath_helpers/questions.py index 40afc699..d140a512 100644 --- a/tests/xpath_helpers/questions.py +++ b/tests/xpath_helpers/questions.py @@ -138,13 +138,19 @@ def body_select1_itemset(q_name: str) -> str: """ @staticmethod - def body_group_select1_itemset(g_name: str, q_name: str) -> str: + def body_group_select1_itemset( + g_name: str, q_name: str, appearance: str | None = None + ) -> str: """Body has a select1 with an itemset, and no inline items.""" + appearance_expr = "" + if appearance is not None: + appearance_expr = f"and @appearance='{appearance}'" return rf""" /h:html/h:body/x:group[@ref='/test_name/{g_name}']/x:select1[ @ref = '/test_name/{g_name}/{q_name}' and ./x:itemset and not(./x:item) + {appearance_expr} ] """ From 16d6ff621dfa78a94e4235bf315fe85fb54698bc Mon Sep 17 00:00:00 2001 From: lindsay stevens Date: Fri, 17 Jul 2026 23:59:08 +1000 Subject: [PATCH 46/50] chg: replace sms_info xls test with equivalent md test - The deleted file was similar to the new test case in test_sms.py test_sms_info except that there were some additional items in the first group "a" and also a "meta" group with instance metadata (like phonenumber, imei). - As noted in the new test, there does not seem to be any special handling of any sms* fields in relation to the xform output. So the deleted test in test_builder.py was asserting that the sms* fields are read and stored internally, but for no apparent reason. - The new test at least demonstrates that old forms which may have these items will still convert without error - though whether they would still work as intended is unclear. --- tests/fixtures/example_forms/sms_info.xls | Bin 10240 -> 0 bytes tests/test_builder.py | 150 ---------------------- tests/test_sms.py | 66 ++++++++++ 3 files changed, 66 insertions(+), 150 deletions(-) delete mode 100644 tests/fixtures/example_forms/sms_info.xls diff --git a/tests/fixtures/example_forms/sms_info.xls b/tests/fixtures/example_forms/sms_info.xls deleted file mode 100644 index a1a00312b1924aac8540a78aae80014da0f83f81..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 10240 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 diff --git a/tests/test_builder.py b/tests/test_builder.py index 9ed91e42..cee23bb1 100644 --- a/tests/test_builder.py +++ b/tests/test_builder.py @@ -229,156 +229,6 @@ def test_loop(self): } self.assertEqual(expected_dict, survey.to_json_dict()) - def test_sms_columns(self): - survey = utils.create_survey_from_fixture("sms_info", filetype=FIXTURE_FILETYPE) - expected_dict = { - "children": [ - { - "children": [ - { - "label": "How old are you?", - "name": "age", - "sms_field": "q1", - "type": "integer", - }, - { - "children": [ - {"label": "no", "name": "0", "sms_option": "n"}, - {"label": "yes", "name": "1", "sms_option": "y"}, - ], - "label": "Do you have any children?", - "list_name": "yes_no", - "itemset": "yes_no", - "name": "has_children", - "sms_field": "q2", - "type": "select one", - }, - { - "label": "What's your birth day?", - "name": "bday", - "sms_field": "q3", - "type": "date", - }, - { - "label": "What is your name?", - "name": "name", - "sms_field": "q4", - "type": "text", - }, - ], - "name": "section1", - "sms_field": "a", - "type": "group", - }, - { - "children": [ - { - "label": "May I take your picture?", - "name": "picture", - "type": "photo", - }, - { - "label": "Record your GPS coordinates.", - "name": "gps", - "type": "geopoint", - }, - ], - "name": "medias", - "sms_field": "c", - "type": "group", - }, - { - "children": [ - { - "children": [ - { - "label": "Mozilla Firefox", - "name": "firefox", - "sms_option": "ff", - }, - { - "label": "Google Chrome", - "name": "chrome", - "sms_option": "gc", - }, - { - "label": "Internet Explorer", - "name": "ie", - "sms_option": "ie", - }, - { - "label": "Safari", - "name": "safari", - "sms_option": "saf", - }, - ], - "label": "What web browsers do you use?", - "list_name": "browsers", - "itemset": "browsers", - "name": "web_browsers", - "sms_field": "q5", - "type": "select all that apply", - } - ], - "name": "browsers", - "sms_field": "b", - "type": "group", - }, - { - "children": [ - { - "label": "Phone Number", - "name": "phone", - "type": "phonenumber", - }, - {"label": "Start DT", "name": "start", "type": "start"}, - {"label": "End DT", "name": "end", "type": "end"}, - {"label": "Send Day", "name": "today", "type": "today"}, - {"label": "IMEI", "name": "imei", "type": "deviceid"}, - {"label": "Hey!", "name": "nope", "type": "note"}, - ], - "name": "metadata", - "sms_field": "meta", - "type": "group", - }, - { - "children": [ - { - "bind": {"jr:preload": "uid", "readonly": "true()"}, - "name": "instanceID", - "type": "calculate", - } - ], - "control": {"bodyless": True}, - "name": "meta", - "type": "group", - }, - ], - "default_language": "default", - "id_string": "sms_info_form", - "name": "sms_info", - "sms_allow_media": "TRUE", - "sms_date_format": "%Y-%m-%d", - "sms_datetime_format": "%Y-%m-%d-%H:%M", - "sms_keyword": "inf", - "sms_separator": "+", - "title": "SMS Example", - "type": "survey", - "choices": { - "browsers": [ - {"label": "Mozilla Firefox", "name": "firefox", "sms_option": "ff"}, - {"label": "Google Chrome", "name": "chrome", "sms_option": "gc"}, - {"label": "Internet Explorer", "name": "ie", "sms_option": "ie"}, - {"label": "Safari", "name": "safari", "sms_option": "saf"}, - ], - "yes_no": [ - {"label": "no", "name": "0", "sms_option": "n"}, - {"label": "yes", "name": "1", "sms_option": "y"}, - ], - }, - } - self.assertEqual(expected_dict, survey.to_json_dict()) - def test_trigger_data_wrong_type__error(self): """Should raise an error if a trigger is truthy and something other than tuple.""" # Should only happen if the builder is used incorrectly, rather than any user diff --git a/tests/test_sms.py b/tests/test_sms.py index c501255d..b241bed6 100644 --- a/tests/test_sms.py +++ b/tests/test_sms.py @@ -62,3 +62,69 @@ def test_sms_tag(self): "", ], ) + + def test_sms_info(self): + md = """ + | settings | + | | form_title | form_id | sms_keyword | sms_separator | sms_allow_media | sms_date_format | sms_datetime_format | + | | SMS | sms_info | inf | + | 1 | %Y-%m-%d | %Y-%m-%d-%H:%M | + + | survey | + | | type | name | sms_field | label | + | | begin_group | section1 | a | | + | | integer | age | q1 | How old are you? | + | | select_one yes_no | has_children | q2 | Do you have any children? | + | | end_group | | | | + | | begin_group | medias | c | | + | | image | picture | | May I take your picture? | + | | geopoint | gps | | Record your GPS coordinates. | + | | end_group | | | | + | | begin_group | browsers | b | | + | | select_multiple browsers | web_browsers | q5 | What web browsers do you use? | + | | end_group | | | | + + | choices | + | | list_name | name | sms_option | label | + | | yes_no | n | n | no | + | | yes_no | y | y | yes | + | | browsers | firefox | ff | Mozilla Firefox | + | | browsers | chrome | gc | Google Chrome | + """ + + def get_cases(value) -> tuple[tuple[str, int], ...]: + """No element text and no attribute values match the input 'value'.""" + return ( + (f"""/h:html//*[text()='{value}']""", 0), + (f"""/h:html//*[@*='{value}']""", 0), + ) + + sms_values = ( + "inf", + "+", + "1", + "%Y-%m-%d", + "%Y-%m-%d-%H:%M", + "a", + "q1", + "q2", + "b", + "c", + ) + + self.assertPyxformXform( + md=md, + xml__xpath_count=[ + # The sms_* settings and sms_field values do not affect the xform output. + case + for val in sms_values + for case in get_cases(val) + ], + xml__xpath_match=[ + # Model instance for choices contains sms_option values. Currently occurs + # due to passing through all choices data, not specific to sms_option. + """/h:html/h:head/x:model/x:instance[@id='yes_no']/x:root/x:item[./x:name='n' and ./x:sms_option='n']""", + """/h:html/h:head/x:model/x:instance[@id='yes_no']/x:root/x:item[./x:name='y' and ./x:sms_option='y']""", + """/h:html/h:head/x:model/x:instance[@id='browsers']/x:root/x:item[./x:name='firefox' and ./x:sms_option='ff']""", + """/h:html/h:head/x:model/x:instance[@id='browsers']/x:root/x:item[./x:name='chrome' and ./x:sms_option='gc']""", + ], + ) From 2a5871099c994c16d9d1ba7abac210af48e8ba07 Mon Sep 17 00:00:00 2001 From: lindsay stevens Date: Sat, 18 Jul 2026 02:50:40 +1000 Subject: [PATCH 47/50] chg: delete simple_loop xls test covered by other tests - The deleted file has the structure shown in test_loop.py test_survey_to_json_output (simple_loop), so the design is still in a test. It is nearly identical to the loop.xlsx fixture, in that it has a single integer item inside a loop directive, but the loop.xlsx feature also has a select item outside the loop. --- tests/fixtures/example_forms/simple_loop.xls | Bin 19968 -> 0 bytes tests/test_dump_and_load.py | 1 - tests/test_xform2json.py | 1 - 3 files changed, 2 deletions(-) delete mode 100644 tests/fixtures/example_forms/simple_loop.xls diff --git a/tests/fixtures/example_forms/simple_loop.xls b/tests/fixtures/example_forms/simple_loop.xls deleted file mode 100644 index 19597ecbc4e3cbea802a8b34de010f33e1c70172..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 19968 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 Date: Sat, 18 Jul 2026 02:59:10 +1000 Subject: [PATCH 48/50] chg: convert widgets and repeat_date_test from xls to xlsx - These files seem potentially useful to keep in some way: - widgets is a relatively large form with many different control and data types, appearances, and features. - repeat_date_test is less clearly useful except for an interesting serialisation scenario: a note label has a formatted date visible as "12/12/2012" in the spreadsheet but which is output as "2012-12-12 00:00:00" in the xform. --- .../fixtures/example_forms/repeat_date_test.xls | Bin 21504 -> 0 bytes .../example_forms/repeat_date_test.xlsx | Bin 0 -> 7629 bytes tests/fixtures/example_forms/widgets.xls | Bin 43008 -> 0 bytes tests/fixtures/example_forms/widgets.xlsx | Bin 0 -> 12746 bytes tests/xform_test_case/test_xform_conversion.py | 4 ++-- 5 files changed, 2 insertions(+), 2 deletions(-) delete mode 100644 tests/fixtures/example_forms/repeat_date_test.xls create mode 100644 tests/fixtures/example_forms/repeat_date_test.xlsx delete mode 100644 tests/fixtures/example_forms/widgets.xls create mode 100644 tests/fixtures/example_forms/widgets.xlsx diff --git a/tests/fixtures/example_forms/repeat_date_test.xls b/tests/fixtures/example_forms/repeat_date_test.xls deleted file mode 100644 index 43edec6e101f89624ee72f1bb9b01ce2d858d610..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 21504 zcmeG^2Ut^CvnL@y03{%ZsHhPT0g*1ksuZy+VlQif0HI4*2ns3+2=)qV$F;I{?1*C5 zRawDa7aOatb?s$cSMz4hO-OP{BK!aEd+*!#{tM^cd*;lUnKLtIPPzBy{5kWxt9LZ1 zCalAs=#einBVwQe=fFKCdYBQyg&WL&k;!BXkPx{2&+!k^z-N%PwcLh|aMg!|HaCRC zhhzlF7?KI3dXVZvGKD071TH|#AvJ(x0m%|lLr7MT8bPv#)EH6|NKGNxKxzi5Iiwbl zgpgW7vV~*^$sSTGNDh$DrxfWQq|Sewr~`-u{-j`wt|SSrDI^25U-!cbJVw!Ij0HkI zMg>BS&@<_Vm*QhPTYi76l9PO?TVMZ6l^i{Q1u|%4C~!|92_%Bbk-*gY8|Mau%Ry2| z6XmE&RUROX@4{emNGM4p;<{ARh+_==kuUa}y7EUt*jMn-Cj&_u83TVJs{5W`g=nf1 zgh)vo+$S-p8qbb${@A9v@<%&m$*ExlaE0}={# z9}sb@K>Wy5F$sigmci)|MGS!~_@{RM+$)t=3SFC;6)Xn+E+31L&jCot6Zs<_9H&qw z9N&gPGJ_Nj3FU}|gkxDUB$O{55{`k3At~g;xPwrvkB7Evb0Ca$a9mAIOBpN9pkhSE zO5!5LsidB~7@LbEhDuTrQe(wpX)29uSX1-C0S)NE0F55Xj=eaJhQvjtNKz%yQekLx zG_VqOO^*~O3O(K3y@bfbh!30%^vUKHm4-J!D+ZwKYLIk0?427KA%`RE`(-jFi{Mu!v<1<^POk_XIYR4@QZpA!V- zaZr}{Z=*Hke|C6^?3k{rM>cVj0aEPx?F1)W|+ypPsO#?ONuTu`h@ct@B$FE>;NPy0^ z#A;~E7`bK9H_^5nu)c1s(p<0aq;zGVrXU4QZogWoVg=Ij1lBo}A z1NKldRn)KHtEfy_D{J1B&kWzmgy2z%3>}=KQ+SP>o2lLm6!1cGb1M@o(DO11w^YD^KBH$w|2R*fbXa=Ey(|i6$%$(M zxjZ767u4th*AklWaZOF*Ob@uB2O~e;a&X0g8)fuRz)cx^T=>Z06Zw{8B6gUXBZ#=Z zENzv^4!736frxJFpwrcX zh;HlP_HG@B=(Y~-57&W+ZtLI%a~+82whqi|Rfs@FQ{VCQ(rq2w;#Nwcm56TZ;6^|l zi0HNsM5ekB(QO@J-H7P6PRqIx(QO@ADtx^;%yyCX4ro_5BD$?(UpFGUt<$P*M08un zp>9NUTc>s1i0HPCV+|2pUlbApS*>vg3uLFx%9Pewz#6=q0Ku6tml>Au6B%MMXumgbD}i6r3<1u)VQMF5vV&r!aDxzL}UTFiFnn82s>1>mFNvb7$u_AwVG4g71{xs(mw+N z#%flgtYT>`SNQMqPqtBfYE*#+tkr20ok5nxsS&ESd$d<0QYhBq9CT-yWkgOyAsB_p)uZSTsxU=|sDS~O+NSXXsH$lKYIxVodr>{| z&_Q`Cgp&72%3C3n97cI7gi`I13ZYbCwR>p?;HcffIGOyxEDd&q3Owzzca&JAgHd7e zq<}&Eqr%pfq*IL~s09c%>gp-rHh`4~to%IPTM63=gCZlvNm7q|ehA6Ou|s}vc+F+? zVIR&Np3@6STS!4968^;W*)bCwIPf&YkSC6=dhiOKio!iEL4aK=Tzw!Lb2b~#+H7of zuwlXnWuq^SetqkvW;PAjY`ki-anQkr2{4q6fjIift6Q4cSg_f6*Jk6agAIltJy`C6 zq722+5AHtG%*K+>0qPkX%&5b+gGZYjWwH%22X3PgN>@E zRrI-b|A1yTjoEB8cv>4BY*an1qEE$S+~y{<^YTGaDOZ+E6g-Mev?YtEsu#f~X}=K7MD3MC!GG99Td|0$`V1k<%Z6(v+Db_jyoF8F ziA7Y^LkP)=TBDh$gOVut2AgPG7Ex7yAf)$}lbVUPRuTnIU=#IV5mmK5As-fX(?paH z@3O!Ytg|R8pYNw`c}`QA9_b9rt*Dw2SRZF!unfl85#HlkSsqEu;7KLyp9hN)BhwN^ z(l|*{K0jNXAlFJ}LYRm8k?`tRB9KAds=f08hw|jvbwuO0*=~2oi3*dya3o8C`nkZiaP=dTEI*}%M_k( z!;Vl7u_!7oDMkou1(u+CW}w*~AQ1X~90(c%0y1j^>H^SVTJ+3h$e}A5f@Dk}EZ~~L zB145Z#6C80a}C6q_@{-qEMUWt+t3gs?xji`Vj!D%0}aHPIH-jSBDvLmOv{jdtLx5#p z+H3GNTO@_u-3VR-f*O4#QgJ>%LcI#t7`g{mk%A1k%Ylm=${BV!6cG$}D3o!P$tfZj zlT$=6CRe~UA$|n5i9^$*)WG9dOdA5}P}=0$CRUB`7!cJ=i~yx1Oim#QlT%2-ww-`_SA-3wwTc{ilsV}HHX=hsDM=Wim}nZHE-2Ro z0`pMdkOJLE5u`LlQ5>_WhUfB!k+~{+BJ-hj=I}QNh(yrV4g`@i=v@f39b+H9;q3=n zj3Z;I2PrYI!^FU#DTT=BL&E91hPlErXI5{(gqVG~6W z;zY>C#wAG`5)u)Y6!lGf$~Tdcq*Umj01QcuL2T$O@U%EpoG6ZzhDnmd!VGb0Sds)3 zFBL^3io+7)Ql(*Du<6v_LmlP~^NIdx;#8dF2t6U%Bc@Q!J#LIB28BTwNGpQYNRlb0 z2(l5%Yy@N@mDxzhMk%vV*bpG$sRVf_Z&jdPN~pK;ww?0UM|s;`dFu;(muWAAZMcE+ zaB(U$Hc0}fz7oib!XiKjk!yT%3|u4d3T=%QB`0TurHW8MdarJ=Lm@yQKr;wo2!TrST%;r|Nec8M<+DmLyoq5yLA5Xw zswzss8ZvnMjZci=xd#v-v2aa*>n^yK!1WDW-@~h3hYHy+y5IMJ)m- z^stg&xnvRFu>K+tSoP@FTaRZ6j*j4#BxmLUi$CGc2DE1Z*Ac;zBq{7fh7AJWOdaK# zJ|=O?f-~{Q-Oajw$on$oe985c{fpU9aSM9Ud9}F5rpVLxU051zSqGKmK*(o9Ce}G>|(F3 zRo)Tylh-f!F6>&1B|kL2>(l4bJF5ZL+J_g<+fXjsb=@BGRLkFJI@rCy(htiO@dCXy126H+Jk#j+Mm5+d1ZO;`omoc z4eQSoSGrE$`*Nbo`lcIx>0$GeOJen@hQB>dw=Um3$Kp=U)2aP4?XJwv+q&nL@amp6 zS6aSsxixy9eSD``-G@H9^ug)Zu20)I7;}9!3%}j`uj9G6F(72VB~THYX@Uoe{cL!s zq-j^n!*;Klb(b7IV<$LMlu6C$=cGlCY@2^ec=k@3@J*QOo z-My@=URzHTOR*haa zX#b(z_L)mgzIG1n$$in;HDb%%{<9Zzvjy2n6^j-n?AcRfed$E7(cJ83x7^qH2L*>* ztU5j8gLOxTUQdix&d7EMDQsxa^x3s`o%@Ox8%eWHn=&8sA&PvyPw2S@K6>h`EH zd7pWGFE)oJZ{2e>`IUjC$FgVVR__V=^m+Dpj*(wgac+Cg z?JeS$jg1eie(73idw<5a_Y*uOo$lI#G&#L}jnRNh-{)R8bbJ15y1nDodN02@TGW2* z&x=zBE^Cy3Xw}B}2`|=L+eUPI@ov_O(TCHgLK^?qqmQrFwwTugV#8^?dt!*80Q#lmC2uCUnxL z_dWF|4!qmHvHv=c1&>Vqx=iRe^M;hAI^yOL0UCtw? zmHu4jCd?Sbk@Ri;%}!HWkN&%J-fVRoo7JIkmuaJg#-&9W3%xrQ+TV-+En>jQ-$fJq z{Zux~;?(YgJ&lKZjqBC@!>#GQ5zjpf9XmQ0_3v@`-lyC_&z^IN=8hh>!L9WE>dpek zC*j|2%x~GuZ?5-<-64_N&+Lvr7arfeeTNVIr|sG_O0qz&kr5|*c;3t4V>az>pRmZg z=bRsGvuo!uNo2#pMt)0-56^zuQ-4u&iv{03jakp{Vtd(HN0;9^I!E6pVgB7GdYje+lOHUCp84xPIB&x-Ij*(c(s8IB#_HS}TkDLJ#9{327oR5aci%Z(A2q_nj& zu9{>Ko!0j9(tTIv#5!E@yE(J%l==_%8WiODtTqWcUzWA#+TH2J!;jzKc{PYVSj8<| z{?o8(+Yoy@oB0*p#@~74v#F`wgF_LndPl}NC$2k}u|9U|iLFKDhxGUEvffxxyd!H= z(!squ20TCN)#h<&C-b>3PKK>}pBGgnY<1l>{`9H1DwFDACdYmCH}}5vhsbAYS<5h^ zr|;Jr?3(d)6I3ALB<}|M-dM7I_LhVtLzkLt?~=OnwcECwZ{6bc zu3owmICG(c@9_W!{YMwtwKWR&L?<{l01Y z%B)_cE8H6g1soZ?>}pBI1BZt$kA7}6%pm5WM{m2rq7lv3WY~`=opG_N7$WVI{WND`;D{sBmXFL{F!x%?sl1uKQLDYzObQFU7oK96 zZZh)Jv4mvj(oi3nzTbJ-n2O;|9JW+nUjP1)wUP0=9$jagBM($~}}e%qYv z!)0%F9IdxJ<<0iE*p$zWY=;aQTz1>#T!JLS?$4jLRL0-$&#CBDv}$4A=cY50&kjiN zx_Wc&;-RV4M`ph1FtvP5%U;zN4Ij7IFnQ1)6_X0j>!+RmAlz&F$JJFcva7O&hV7VR zc+u!$-?^*4+gWgRskhVSU$&%O{PxVOclzV@>))~X`D*@(pKd=KH1TV z$WfkagMB?yy04!8Zbb8`k4~>Ejazhg)zoHA_m4+4TgF>bx%|n*837BPI8;ab`Dbm3 zm^jYnzMahjJDFqoG}|}9Pp7;zan24)?i49LP`u)7j%csgDrv0q&@m4S7VAwY8@r?8 z_#dL`=?{uig3e64roYlY`$Eqy$>)1>Q>Xp%zQG?pj~`n$FCfk)**~x8Zm`i^921n2 zlo&i*+U)8)d+XuLQ%_8BTGI5;!cAo_Ej&&Cn%He%c)c{!@ec&8hhjOGFX;9$Jil|?Ea4byH}d>qNxWZGMftFl$73YvzWd(#NV`eS6xNFf_rDc% zwt={dVoKY$!Pj z3pD1J9i~w1TzYl@>=)1Rco={maw+Z|;r?RlO!$8h;G}ZD8lI4GKq`Fa9lzvs;ka;2 zO2Xm)Zh+#-A%(ElUdYk`BN21n9j?d?1{!>lNtdV`5@?OBh4+GJ+u=?nuLt{t;cb-V zJlO0A4^-ySGJ*A#c>~;$1MR^woYI5oJLiZIESm9v-D5l$5ut?ex%kFC?##dz5gr@? zg`q{+FqDuDLoRF>>YfdQje=S*l#`8zGO5BoQy*e}v4apM$AQ-2`+!W23ptdQ$>~83 z2>f+3}F4FYfvP}OYMMYMq}IbT}ahWNuPQzbwed>rucKrPLJ zSTRejpBTdjm*BIw(~ijK^!oZsLGU9+G5kg(Mg%DI11f%7&qp*)SZ2*f8Ag zW5eM6nOZQ6fowdCf@~OU4A+9;wjLV~#}_sXw+-1a{P0B;hJ6VS0o?&(au_L;(G2@7 zT7oIV*oJMy96|;7YKT0C^{`z`8P-FKV~&YM3QQ&zVQZN(#Kd-D4sEHViP1+ue~W-A zLriQv=Fq}QOpG)F=>Li`#DqO}ipfL{)CNXLe8MN7@eQdM(zt|L!5n&nfc|0yV)7{_ z>W3*qn&`EdLmyFM8c|F|+l?qq{ICZxvAz-$BfGLis6zq$btl9`e^O$aP)wAXDML*B zLGpd_z%&n2LI6 zIRX9sDXeEkF@V#ZiZtd}Y*;ll{--e^mqe zqzgQe#21Oiz_UvH1+c&OdsMz)YMe_bj=D4kvPO%$yA#}tJH_24xD?kw@#0bnMG6!x8d{tp0g6L`;uLpxr+BdkJ@>x$ z_T6*Vdb3xuv$OV0cD|YU{+WN&6k*|Tp%4)fp-RJ#4WWJ$tcTB*&K7EJ&MqK!3uiYg zHZMm9M-8BJJ0xoLVy2lOkID)7-UM9fY;V?lHk4|E!Bt;efuU+s?a~}^1Q#2bLpLYHa@NM_nZPq_ zL)_qMnmJsX)g~%Jp=Rh#Lu^vP$lV#H4cKZr-st;E>>~!{4|+k?!QSDsty8G4wg?$7 zF>N7`9T>Q7wIG;%vcUw@dtVW~b*H;wMr#$i+qPEaU3t%g%9VhvPxDUWDLBLRD4Aag;V;}ixwnOK=%pLk7rdCJLa z7g>9xm2L6*5K@!TD_5dtWc3V650(EK>Wjd5#~s zG%YZ|+;T6x_INllp6o%oE>FlbHE zaBnmqXQsopxz6ql)rsA-DS<&!Me^weuV^9U6WIjIhw>ml#GIOJjb^!92R@PJ3_1n^ zI`(e5mVjE7>%+Rclvg!`1OYx1-mg_dpAl_>;7{VAJ5F(VDP=UE*!g>Fj0?vazyq z2eJP=aXy0Dq_^mt$B+54r3L1=d6|z^J3%|0rvczmUTlA%^O+ApsltxKIwODIB*20M z5xS^UrD=^SIQ7_X(a!>h{$yxK;3;~FZYUngXzEl!(CZYs{3+Icir92j&C!o=OxVx# zg9tWI%KC@;7B^(OvWieG%*2Cp4N_7u*ORQYUBf|I%6hL*j`E{7dS}gf z^%ZGA96Ny18c(s4_PMvRquzRdTITpw|NMP&4^s9RY<#9$x~2EcdnVAAtn+oiSEFDJ zpyVqrb@=qY`ri9Y62=^u6K9qn`VWyhgno20%Lptl6MNaor#(4%oa_Z^cRKsLC9^3K zQrlcVbChQmvuhINz}!(^>UosKFj;h?9nETLJ>5<`MgjDYF>>g^FHUxeO}ln(=?=p{ zY$uQTo_r&Q1vH=%E-Y(RGs4e^nnqa!y*>DR-INXW{V2hi$PtH@U)q<67Cz9T@e@Vh zaXU+X2Chb>hGYo`80DmkGe)oL4*g1Mk;j;*kHY+!1Z_7&=px|bx_@!v33^jSQv&$B z_ic^Uo_w06F_2eYn`)gkUO4P2%BZ-hi0Q!dRnNmSjl3VSPvQI=TP#-&JVw)EW;lm; z43N~80rJlv9M*5YdS=?!2IeecraTf!3@?=#Ck-tdt4++vtxQlZeQS4=R_X+lNq{5Bt3c+K9YVnwR99I8sfw-{Th3a- zx)V=k)hX1(hMn5UIirrG_dln=*q)5LL(D%^Zroa>3L)kq3_`uVhvzDx>cEog_6=aS z2Zj_9MN7#%wd8?e9EuhM`7G`%;?fSg4NiYsvKz8BZ0b~P{lGn5?0XuCmV(wBQbe~< zGq=VBgRg5U!m?lQ5r?`PnQj&eBL74zQ@4q=YZmHB?kW_0G(^MO5xFG1(#JvVK*NGr zr#;ZMj69H=$~U;!z!Tuy8Za{p+Y!0Irtj+!rZYNxkdRxuR$_QR zvi>8){CSfUXS9pY=JTi6Bk!I?)RofKl69oQw1M#uak0u3n>`Dh)8fK!m9wu4K^pMUOnyf#TEW#6;259K`Qs-p$)OYy(aG>=w-l0p%r_ zk!7YYIvY=5U|{PgkB7~+iS|fVgkV~=(D-;8^`CJA97NScZlZlmHR8^v$7<(4UWpe1 z>BW!LbqnZOw~X*_9_)ja0_YsYKp^Y! zHD8|oaewgpDyXdy7AxVq+j^e*mT$h2mqK56()~&IenkDDd{`eYw{b$!y=4Bq>+S6K zubprEk<`gC6W!J~6}0&zDBot_>b>!jRyeXW#MNsn^+L6zLsZ#~K*oP&#*c=qq}XCs zhjqBTq%VV0x!%)E3AU4iGt-m!!Sx}@Vfy{3=P?+DLc%UxmA%t(b^B7y;TVqx%kaBW z47PEGMwK>OaA(+{HP)>dJr8stLXi+&jivN=@+5eC)$1Kt7hSbNgx-%nh49zARs;7& zUPM+=k>$c+^wufzFwMwf31sSG#WUqEj}A?3=G%OTGRTeDHIZ+JQ@w*NneTBvPs@A? zAB${Z6zE;30f1gMhQVE+_9d6|-ci~3$ST6<(47NTO>XLz_fT+Q7e<>`DPXki(Sk)c z;#SCdv}6m9I^J+3FeDg<)-N(YQGdbS32RLzrNGr_t*{AoTxYY7Am_dQ?sGOOzc<*z z0nx(X;~^dKH2^N9tNEhW`;#I^2v-ir-h4nY1z+jsY|qQE!`;srjX#`sgH8aBBaJyu znygX;b?rY6j}pTU%__*{-VQvmTj1sy^7+2_8IWE;B{3Ox=P{c(`=d1IMV1}3$ECDp z(r&-ZYw6I9DtTRotv-U>V)O-@e6k*6CgbWZ%{G%9#qd{rXDu`c?SWvGD(8@jn7t+H zV$B3#$&nA^<+l+099}!v9m&(mUfZ;U$`k&#d$Q%DL1kQqJtwcLjOhj6AJmm#9aX^X zn#JJvEkV`O1HM|5U>i~M@|yd-niAx1N5>AlEQ49iVdj8nO3A>JVphSSf1P6GAU80* zfrul}^1=yH;?%bG48s7NQQXA~!o?9RH}Lt%43D9)jU#Ib;W1of@$jL?IYBjp}N%7Om{A zDdmLl3@XK-r!h-mp^2Wyi+pm7!BNZ;z&GMKVJ5Z?vCo+9(O(>LR3c5ICQD)Nc(~~{ z-ogz??Wp8%i|T4xrW9*)ghVu*$I2_yVU5srlk>^r*igVAuT9p{a)=k0zglbR5aoVk zC;-fB+myAWxH;~}-EQqyjz{lLVqg2V1WQ|~`VyEQlNwqehtMVL6}&*6k6o>Gq1^<1 z<`#UmbKa=sE6hHo)c5A&i;g6ago$?{$84Mi);HigvPdm>#-S$7iuJ4QgC4-Vl_G4K%Vm$Pip7y zp(pO4ADPmoU!Z&KfhiRq%E7;9O0>UfMUcCXiTV3* zT1J3^CZ+K-mA6Hrb*8OFVsWlB!zN-Rut6^!d+47gEp?tRzxP!|=jA+C)rk%kVo{{_ zf&yH#!KHLlQwgIity9kk1!rqT0wR&$!0IhyzYnFj(a^>uSbTh7! za%&yDh5`8l@DixN5nP~NN%O?S@Gl>^J?85>Fqb(_uM(t{k|^;N%V7BTDNWOzUrq2{ zP*!XYKFbCBb&@JR%QR}MDeQxXhN8bnTOqBESJuG+j|je&9A#BHluSI(gd|;qugght zd&!K1@)RW>H(7WDcKDP)JBNaNQgf)FaPOIw6HM@Y{zhT4 zWno-icYD-(a5u>Kjg0STO==o66S9GHJFU9gE0QG;8CReRV-J5vEX)vCjbG=ivV3`1 z){*jS7%hfijd!4aRewKFGJZ<@{6aGONZ}R}d78ywwCP!=WJu<#P=JS-e|bOMaxDs0 z`~d_~xMc~&v;xeirUoV0*s+i3gJUz?CPkGYXNBLcrap?!c@||pIh5^p$2|#7DV>~P zc#g{*MbsH>4#8R_+Zz!B^G!28T~H}ZJxEAf<-=b+tv!6fy*S(LJL|NnyREtIFLKXt z+qA!(j&bn~qU^Xk!0XICd$%a)Jz3_+KYh1QLm>K@kC%0VQ?EJ9R^_B}m(0way;)!{ zregCVs0zXc44)04Y5=?Ys+Cb)u&ol!F`Bzo>;KY<}?QQ{5 zm&Qr{5Cd7*g=J79h-KhphoKubN+iP|y%1p+cKnF%F(II3f(#}ey3i!Ne@Y1AzY@a4 z&B_7vvxPEucD8>^47SH_niBf#Iyf+;FRqX|uk|3;*|>eviINbL zvz;a}LhyFGJKcr?=um=te8YnD3yH>Flct)b0HS*rU(+sb;~x2>XGNjzI1YN+=v$j> zOz9n0^&l(s3k?|y*D=l$Xrs@zA~9*2@|mbJdma&${U<Y@1^dBrHZsw=SsO9=%dpr%2JJ8#-^fgL?$5a&X;Y`vDGK z*na{H<5#!&Z>YeDM$R)FxZ#Hv%+!lgD^Nt);YE4UrPb^;M!x&}fEeBGHx4J`w)g%3 zbZDl6!W}lsPq9O*CqEhor>_bU+K{9148Wo1G}wVFf&@E^bYcetO`J$27EIO}%yK>W zECN~n{6PDY4BJ~$)8#?0amW;WhPcvb#JA?1DZDWuq?H7fyI`Dvg+76 zN(>{|1sklWM!3Np;Eg`yM0=_yv>?4ER%u5egU-d^;uLS=36Zj5Vyx7-c1Mxw4u`F+ zP)#JK>hZ8$i-odW99JG>Mt`s~i0Q~NpNh%wRe>0S)^`=1N^3G&V&YrA7#Re^8|HC79Uc2EZpJWi6G+1ANvAZ(SgKi=H@^(H1BKb%J#8Z}ErAz{UZ6FJU~l6zS=Z z+%^JLa?KPn^xN%&U({iGw)H$;k!YPEUl|yNEw901HxQsdREtg&N{ltjR5@yqJ|OnK zV5yp4+fe^#Sgsk201@#qxt^4bZ@_jo<08apsJF*d1uOQXsF-8(C(rXbsrE3mS4$G^ z-q62qSL;D`X?Y$R+)g8Xm)~HWed9d?)^CF_;%z#)%}txUh{!3xN&ONvpk>-Rw-yra zl`)_g6H_g_eJnN-(VSg|EX?|59tRz@ENd4m311y=nSGfHdS+Krh({M~D*R5tD z7G)C>7IW>88F1jh@U%ILP(7+&d{->sUcQ2dnyy8Z-j$02;L6^d9YPM)&0F};<}wB} zWX1P7IO)5xJSI`zF4VjknEhVLeVN%9w&L`9aH{%A(-B=*UD54DQdnlr^MHrt3;_|0 zZtQOQkj-xJ{}h}!e+8$zjg_Mn`_K2EyCCyP%CR#XnBj-Fc*%!GIF*#*p?L7QE(2!t{)HPiWvr)-O9lygcOH z08;|(8Dv^pB4??(do*G;6Nqwe+OE2{_A0Q5n*t+`zKW%kB(EVO&Tg9Dj~SxTb{_+M zRu#&JJWhq75Sh&0<7X!J=-rsep z`rJGhT{Jw*f4FZON(?>N==FsfTb&^K!sUUaNqjYC7Q%54t=^F6_+<=r%yHpLk|Ua? z#T!~-Ji!-%&SmN9VS9y^jB4GbPZ5IDdsqi7;tVqkqX@r|R+~*cJ@MmVA>=WK>>)La z<%uwyyr6v=fG;ojO0jIg7O(b;e3g2$W2LPN)%iK?RNk?4n&c7f3l(RtJwbe{gXAe@ zV^5}Hywf2(xfYh38GN6ZBc6|xB)O2^Vb+Z0hMbwxWIRJIGUp`$3F#hsnaqIQG zG4nY+>O^eJ-Q9j)1W74vJjGY|p%~FmTeIgztO;7^XCqsTV6PG;PMjgy2F&SA0ESsA z<9eS}2`!sah*H1pi*d7-mZImC#>_Z6S;YSS!**xqo4r{kPKM;}z}JqupoF|%%0w&rbah(f%DeWS(Sk6wt1tH1K; z&a6uK9IMtk`C4OMHRT`;BMl%)MwxM|X5I=SIBCb2`np+!*GT!I0LzdL#XuetZ>8RZ zaPFO)c?Dzq7t{VMWo$kil0s$t^g}c@jV^qLkWZ!4OMTdvMR3h*LC%zQOZ$oHk=M{} zrZc{%dFVjg84;O1-|>#MXPoZ^sT`07c~si9v#+=>461tXwf9XPos6$vN0Rrjo0^35 zPfkX8aPnUj!Otq7G+{^qmJ>7L&_629*As&G%(AK}ayVViKQN{L&WtaWgUG|*Hw4k3 z5R4Ej|MFyjPbzI5`}tW^hHR9KbCCga^qU((-@GjJi(Qf?d))z=K^+BJ42#~Pu`dp% zlnGAIl_VTfhU)El#)2!RVAA>s%Xv%RP)8#Z*n7*Z^T@*U1)GKI7@dfV(03JuyD|}; zYsN4#0^e42bK_Vq7-ZPF8?jV}o@r`l6RR{lYy)5Usvt5<06#57x@OjCRYj+?cnH@= zxMhmrY=)NIfduLhWn%9d6O_06Y{px^0c^y6^6T@W;M%2!zb*8S`}U9g3Y2noa<_7F zH_`HTu>u)A)=HgPeNMsRxbxR|p^mV5Y-t71MyH<~l=A39Oil9$I|WTF`fvF@Fh3U!Cp{2y<{drR?KcI3{r z^S?N7HYZFhVlH7sa1edWS#2620BML%QRCti=Kob6MRB@=xmntDY?<-)KW z=RO>tKg7pl#fU-~6GN?loC5>B^6aqSxk*>4KYZZ(n(nm~)CIw%Ts%z=pJ6fX?$azc zuetH1a)*Ui?}u9>ICt%RBe=BK*(e(~9cRs4ljcR& zka(S=Jw`8-LhA?1hv=>Nxd=kT;6nWxdHp_kc^rBDr~ID*n402$s{cM}c%07slEjA} z{9pBd&1(MC{(YP7aUAhWmLKBj?}ih9TKT<&{ZA{a2>;c}&kepmt^8j1KeoHS1nuGS z;aPq)zkjO!UI9Ng2ET;-0h0&SziAWxwDWth@>nnb64eLS{0okMSI>Vc|Go}A=G!mX zd$@d1{_qbgB5*7Tv0Qht9`qRSiWbnv^zl8h2FTXM4pNhW+=zl)D648HU W4mCyiCy&-q9*(YuP&xhy`~Lt$+Bp;e literal 0 HcmV?d00001 diff --git a/tests/fixtures/example_forms/widgets.xls b/tests/fixtures/example_forms/widgets.xls deleted file mode 100644 index 6016966881ade82c4c5be2af8c611e29b50230d1..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 43008 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 HYvBI@NoO_D diff --git a/tests/fixtures/example_forms/widgets.xlsx b/tests/fixtures/example_forms/widgets.xlsx new file mode 100644 index 0000000000000000000000000000000000000000..e9f6dc5a501860ea20abf1ec7413237e1f0c013f GIT binary patch literal 12746 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 Jj#IsT`akO=%~=2d literal 0 HcmV?d00001 diff --git a/tests/xform_test_case/test_xform_conversion.py b/tests/xform_test_case/test_xform_conversion.py index b996e808..3319dacf 100644 --- a/tests/xform_test_case/test_xform_conversion.py +++ b/tests/xform_test_case/test_xform_conversion.py @@ -18,9 +18,9 @@ def test_conversion_vs_expected(self): ("flat_xlsform_test.xlsx", True), ("or_other.xlsx", True), ("pull_data.xlsx", True), - ("repeat_date_test.xls", True), + ("repeat_date_test.xlsx", True), ("survey_no_name.xlsx", False), - ("widgets.xls", True), + ("widgets.xlsx", True), ("xlsform_spec_test.xlsx", True), ) for i, (case, set_name) in enumerate(cases): From 46ebebb1cc3e6dcba1d5d6e2a7cf1467fa705c07 Mon Sep 17 00:00:00 2001 From: lindsay stevens Date: Sat, 18 Jul 2026 03:09:47 +1000 Subject: [PATCH 49/50] chg: swap remaining xls tests to use existing equivalent xlsx file - each of the deleted xls files already had a xlsx equivalent, and the tests they were used in generally do not assume a file type. - retaining a small collection of files for serialisation tests seems useful for now to minimise the risk of regressions (vs deleting all). --- tests/fixtures/example_forms/group.xls | Bin 6144 -> 0 bytes tests/fixtures/example_forms/loop.xls | Bin 7168 -> 0 bytes .../fixtures/example_forms/text_and_integer.xls | Bin 6656 -> 0 bytes .../example_forms/yes_or_no_question.xls | Bin 6656 -> 0 bytes tests/test_builder.py | 6 ++---- tests/test_dump_and_load.py | 8 ++++---- tests/test_xform2json.py | 8 ++++---- tests/test_xls2json_backends.py | 3 --- tests/test_xls2json_xls.py | 4 ++-- tests/test_xls2xform.py | 5 ----- tests/utils.py | 2 +- tests/xform_test_case/test_xml.py | 2 +- 12 files changed, 14 insertions(+), 24 deletions(-) delete mode 100644 tests/fixtures/example_forms/group.xls delete mode 100644 tests/fixtures/example_forms/loop.xls delete mode 100644 tests/fixtures/example_forms/text_and_integer.xls delete mode 100644 tests/fixtures/example_forms/yes_or_no_question.xls diff --git a/tests/fixtures/example_forms/group.xls b/tests/fixtures/example_forms/group.xls deleted file mode 100644 index 7b89251ba14dec6134b359b75db8d077d71fd891..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 6144 zcmeHLU2IfE6h8O1TW+DW+d>h+)@v2o(rP3=h#K7GS1=+3Yaj*#+xB+5vfXXATcwG_ zMdVEq4G+GM7!qFm*ZAOrM%yQS&}c9`$W!sf7>z6jVo0Ft_nojesV=62MYGGhi9u zHo$Vg?SK`4m4G_{Er3?Qod6eb7oZJ5o9^*j;_bjYe0q)FPOowvn=230I@raUkbJuM zYo>h7{pS%8A68FjT>J>$J{e|gY>FlO+<&WVR2yOzV{;rqx*nJ%C&8_k50v|v*6TI; zAQTmM9U8t!E?H5v{kYa`TGykt{XoE_4rL#4nU;mxFx^HGtAui-Kwr8&kW6@ z&G=xf1>pf%lxJrD0`EXaj##(}oN=5iaiY?Ckv_y-;NVVhkn5d4a2;Gln37Mxq}j1y z4^1jIW>N;f=^* z^BOhjHvG4ATL<^|V_TS@8g(neaQM}?E}q%ZyDPF^`8j=z>pZdwfniBOj-Y3vdX0|g z()wkUEndS%mG_F)t1n8@bB9XpJ_jQA<`MZPe~g-_4=XE!q1j&tJ_*$ zS95gYz+|M@TJkh{967AZ#udJbj{;Geqk3AVX&aI~PfpWtuhr3LXtDXAFjtRxh&VPw zh`9N#j9Lvs9>cSg$I!*6D!Zg8huN>=VRx?M@w`}pvDfseV;(UghO+Ra;pB$0K~W3~I)U2L2 z6eamMGZZE1RZ_H~0Y8ynWfxjIzYREIEKjZKI826>>U@XtYx8CoKm>{{3w0a~ z;LMNOP4YPxBT-g<*`QW43Yc74IJ@albwJ5N9MdxvrUGWikx1e+$uBq$eC3d$3(6O4 zU!e-!lngtZ4mnh7*~&NPjznK3J(3>Cx?2wQc}e%72k-4+CQ5p`9~%<`1LoOa#>~3L zdW_Opk2C|Ous$BJ(n3E?9((imD?10n@4W6vSNq35aC=?^Fp$_!!}&gs0_dS901S`4 z0A`|o07GIJKn*`%GjDI=YXMO}?(t{2i;A;liE3>s;?eh#3tGQCSKPWlFW>Fu?WYd= z>^Emm-nTM*;tkl}edYbl;Dt(fy!epwbv;zTkmFd|Iujq=^JR`aFqhx|x_>2Su$-Ap z1>^pa-h+Q1GICIQ^?!(i<-~8uYeKXzUXYD62i#CN(D}BXXe`+T%|kdNJ7L3f0g1{` z>gRfxe^@FRbvJ~z`+^qqCdI2{SXmifE=IGd%LONXzYNu<*CuZHZq{*)ov3=Ms1gQx#M&CZ^6|6u){(4F=5&qXs^ M{|Enn`LFf=37e8&7XSbN diff --git a/tests/fixtures/example_forms/loop.xls b/tests/fixtures/example_forms/loop.xls deleted file mode 100644 index 85a5ae1bde3fa305a09c5e5c09931901e86ee68a..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 7168 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 diff --git a/tests/fixtures/example_forms/yes_or_no_question.xls b/tests/fixtures/example_forms/yes_or_no_question.xls deleted file mode 100644 index 3984f3f4888132570a4abe5af38a448a860c22ba..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 6656 zcmeHLO>9(E6h7~@Q>GM{Y55UB@L8o4+9HL8_=BASRfrZEs4-DY9cEsCWM&?j8PLWU z2P!*AB$(iW#E^8MAyJGA7ZNBt6EqqV5_APFOo%K7qcJ4F_q%y)WPW zIrrSZbI(2J-uL}?^_Nb3zUZ=y3r|ax+{)ETO$psVerBN@6fuz}?N%7+ zZszC8J*>ti`T*|v48RZ60<5dcr}xp)rl;yb0^7yGmljL9B!M(3!`Nx>VF5IzbQmu! zOYMah{(JFEk6)C)y=2AuQu)(<(~egGmCiqHKF|LwU^XxZ2mo*aQV%o$bAfrld|(0a z0I(2v5Lg5(1|9;I084>~0SkBpXapVw9s`yEO~7)X8CU_V1UTo`e9HXe$gc+4^R%`3 zv?z(`oEHhy`mu{OKKW|)H#GTb`!7QyPF0^%z4#flowA3zu`wKrSpQbopmPWt)Xgy{ z>3X9YcMVgH19RjvrJvS#z3Ly>LSn505AP%!j1c%l4r|<~@f^g4e5q}IQc62C^J{SB z-hH3V(77$UT#pUHgLC!6%9J1r-D=N@iE6`U~px53-KWgGsWhBQm zn5ojVK2xJ#D-FX za1ALtW=GPNld!G+q!YJ>?NpzW>`OS9)_~(g`=a4tc#FaCKHD00_F9ACs1>$YW1+@M zJBT|lCv=y#I0o{!`>4`9R+4aXc(=@y*Dw+Ku!w&?fJ{EnXEB(ZvuhB8cNoN3kVh1& zqWJ4oHbF?^#wXW)=-$=S(Wmr`UYE5WS;mc(oa{%R{KqByAuy5TDLmMcsSPB)V`*vb3QyE{NdlT^@Im(tEp#1yMIsPCQN0mhASP* zOY+UgkF;Gp&~`D~v7#h@LK?BKD8TQ4v>L5Op?x7WQc~Yv^R;ga2n+fYR+T7M_^(rBZPzYZ2nriOqwPD;tuM1sIDmCyjsKV*sK?(XP zE!-D1mgo59=pKqBos`p`wz}Sm*fHzLC)#@HgkrW{bvF$4_pfu=Cl_O#?x2%vj@eUA zcXvVgT{`&DZ`ZdC22URGOUv>vex!9e4Nx~&zZsy7>j6j`1gH%efCl3bz<1k+0R7%5 z!1voJfFoYH>AuBip*s`NB%DxpI2pB*DD#b0iO#dqzD6X3RDNkVOaG~PmJvcgX2+`w z`_7y>OGgwa0YH_KE2ruLBQ&M*0YRdmmyItZ0;s)$C_nYUS;4Hl~ zAK?K(Fk~afjpK#84Rd(IB~Bpb_*@UiX7JLq1x#D=j0VPyiS4CXSswZXcVss%%QTMI zhBE)vF!*GL?8W-yc=Ph#-xmA@?AJXZC5^YR14`x4oFAVa!|VE2f%j^@OP_4Q2oarw gt$!dVHlKBqSU*e`RyK8EqVg~O|27q~_$TB41(4KwBme*a diff --git a/tests/test_builder.py b/tests/test_builder.py index cee23bb1..8d041c56 100644 --- a/tests/test_builder.py +++ b/tests/test_builder.py @@ -11,8 +11,6 @@ from tests import utils -FIXTURE_FILETYPE = "xls" - class BuilderTests(TestCase): maxDiff = None @@ -29,7 +27,7 @@ def setUp(self): @staticmethod def test_create_from_file_object(): - path = utils.path_to_text_fixture("yes_or_no_question.xls") + path = utils.path_to_text_fixture("yes_or_no_question.xlsx") with open(path, "rb") as f: create_survey_from_xls(f) @@ -91,7 +89,7 @@ def test_create_table_from_dict(self): self.assertEqual(expected_dict, g.to_json_dict()) def test_loop(self): - survey = utils.create_survey_from_fixture("loop", filetype=FIXTURE_FILETYPE) + survey = utils.create_survey_from_fixture("loop", filetype="xlsx") expected_dict = { "name": "loop", "id_string": "loop", diff --git a/tests/test_dump_and_load.py b/tests/test_dump_and_load.py index f026a10b..8eafb846 100644 --- a/tests/test_dump_and_load.py +++ b/tests/test_dump_and_load.py @@ -12,10 +12,10 @@ class DumpAndLoadTests(TestCase): def setUp(self): self.excel_files = [ - "group.xls", - "loop.xls", - "text_and_integer.xls", - "yes_or_no_question.xls", + "group.xlsx", + "loop.xlsx", + "text_and_integer.xlsx", + "yes_or_no_question.xlsx", ] self.surveys = {} self.this_directory = os.path.dirname(__file__) diff --git a/tests/test_xform2json.py b/tests/test_xform2json.py index 89790a4d..1f6d4e07 100644 --- a/tests/test_xform2json.py +++ b/tests/test_xform2json.py @@ -21,12 +21,12 @@ class DumpAndLoadXForm2JsonTests(XFormTestCase): def setUp(self): self.excel_files = [ "choice_filter_test.xlsx", - "loop.xls", - "text_and_integer.xls", - "yes_or_no_question.xls", + "loop.xlsx", + "text_and_integer.xlsx", + "yes_or_no_question.xlsx", "xlsform_spec_test.xlsx", "field-list.xlsx", - "group.xls", + "group.xlsx", ] self.surveys = {} self.this_directory = os.path.dirname(__file__) diff --git a/tests/test_xls2json_backends.py b/tests/test_xls2json_backends.py index e39c1529..0159f757 100644 --- a/tests/test_xls2json_backends.py +++ b/tests/test_xls2json_backends.py @@ -15,7 +15,6 @@ get_xlsform, md_table_to_workbook, md_to_dict, - xls_to_dict, xls_value_to_unicode, xlsx_to_dict, xlsx_value_to_str, @@ -166,11 +165,9 @@ def test_equivalency(self): ] for fixture in equivalent_fixtures: xlsx_inp = xlsx_to_dict(utils.path_to_text_fixture(f"{fixture}.xlsx")) - xls_inp = xls_to_dict(utils.path_to_text_fixture(f"{fixture}.xls")) csv_inp = csv_to_dict(utils.path_to_text_fixture(f"{fixture}.csv")) md_inp = md_to_dict(utils.path_to_text_fixture(f"{fixture}.md")) - self.assertEqual(xlsx_inp, xls_inp) self.assertEqual(xlsx_inp, csv_inp) self.assertEqual(xlsx_inp, md_inp) diff --git a/tests/test_xls2json_xls.py b/tests/test_xls2json_xls.py index eb015b98..eec81db1 100644 --- a/tests/test_xls2json_xls.py +++ b/tests/test_xls2json_xls.py @@ -16,7 +16,7 @@ class BasicXls2JsonApiTests(TestCase): maxDiff = None def test_simple_yes_or_no_question(self): - filename = "yes_or_no_question.xls" + filename = "yes_or_no_question.xlsx" path_to_excel_file = Path(example_forms.PATH) / filename expected_output_path = Path(test_expected_output.PATH) / ( path_to_excel_file.stem + ".json" @@ -29,7 +29,7 @@ def test_simple_yes_or_no_question(self): def test_text_and_integer(self): x = SurveyReader( - utils.path_to_text_fixture("text_and_integer.xls"), + utils.path_to_text_fixture("text_and_integer.xlsx"), default_name="text_and_integer", ) diff --git a/tests/test_xls2xform.py b/tests/test_xls2xform.py index bb3b3346..e5c83d20 100644 --- a/tests/test_xls2xform.py +++ b/tests/test_xls2xform.py @@ -242,7 +242,6 @@ def test_xls2xform_convert__ok(self): """Should find the expected output files for the conversion.""" xlsforms = ( Path(example_forms.PATH) / "group.xlsx", - Path(example_forms.PATH) / "group.xls", Path(example_forms.PATH) / "group.csv", Path(example_forms.PATH) / "group.md", ) @@ -305,7 +304,6 @@ def test_args_combinations__ok(self): ] xlsforms = ( (Path(example_forms.PATH) / "group.xlsx", funcs[:4]), - (Path(example_forms.PATH) / "group.xls", funcs[:4]), (Path(example_forms.PATH) / "group.csv", funcs), (Path(example_forms.PATH) / "group.md", funcs), ) @@ -332,8 +330,6 @@ def test_invalid_input_raises(self): msg = "Argument 'definition' was not recognized as a supported type" with get_temp_file() as empty, get_temp_dir() as td: - bad_xls = Path(td) / "bad.xls" - bad_xls.write_text("bad") bad_xlsx = Path(td) / "bad.xlsx" bad_xlsx.write_text("bad") bad_type = Path(td) / "bad.txt" @@ -345,7 +341,6 @@ def test_invalid_input_raises(self): "ok", b"ok", empty, - bad_xls, bad_xlsx, bad_type, ) diff --git a/tests/utils.py b/tests/utils.py index e6b59fe2..7ddf1c26 100644 --- a/tests/utils.py +++ b/tests/utils.py @@ -23,7 +23,7 @@ def build_survey(filename): return create_survey_from_path(path) -def create_survey_from_fixture(fixture_name, filetype="xls", include_directory=False): +def create_survey_from_fixture(fixture_name, filetype="xlsx", include_directory=False): fixture_path = path_to_text_fixture(f"{fixture_name}.{filetype}") _, section_dict = file_utils.load_file_to_dict(fixture_path) pkg = {"main_section": section_dict} diff --git a/tests/xform_test_case/test_xml.py b/tests/xform_test_case/test_xml.py index 4d52e32c..9483450e 100644 --- a/tests/xform_test_case/test_xml.py +++ b/tests/xform_test_case/test_xml.py @@ -16,7 +16,7 @@ class XMLTests(XFormTestCase): def setUp(self): self.survey = create_survey_from_xls( - path_to_text_fixture("yes_or_no_question.xls"), "yes_or_no_question" + path_to_text_fixture("yes_or_no_question.xlsx"), "yes_or_no_question" ) def test_to_xml(self): From 53d4a0ec3fdff36d482711231f0283247dd47599 Mon Sep 17 00:00:00 2001 From: lindsay stevens Date: Sat, 18 Jul 2026 03:23:14 +1000 Subject: [PATCH 50/50] chg: remove xls support and xlrd dependency - no remaining functions or tests using this - xlrd has not been maintained much for about 5 years now - the public converter "xlsform online" (currently at https://getodk.org/xlsform/) very minor XLS usage - current spreadsheet programs and tools can convert to XLSX --- pyproject.toml | 1 - pyxform/xls2json_backends.py | 153 +---------------------------- tests/test_xls2json_backends.py | 22 ----- tests/xform_test_case/test_bugs.py | 4 +- 4 files changed, 4 insertions(+), 176 deletions(-) diff --git a/pyproject.toml b/pyproject.toml index 2bc9ba8c..854a7b3a 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -8,7 +8,6 @@ description = "A Python package to create XForms for ODK Collect." readme = "README.md" requires-python = ">=3.11" dependencies = [ - "xlrd==2.0.1", # Read XLS files "openpyxl==3.1.5", # Read XLSX files "defusedxml==0.7.1", # Parse XML "lark==1.3.1", # Parse custom grammars diff --git a/pyxform/xls2json_backends.py b/pyxform/xls2json_backends.py index c6e1be4b..cecaa5c2 100644 --- a/pyxform/xls2json_backends.py +++ b/pyxform/xls2json_backends.py @@ -17,17 +17,10 @@ from openpyxl.reader.excel import ExcelReader from openpyxl.workbook import Workbook as pyxlWorkbook from openpyxl.worksheet.worksheet import Worksheet as pyxlWorksheet -from xlrd import XL_CELL_BOOLEAN, XL_CELL_DATE, XL_CELL_NUMBER, XLRDError -from xlrd import open_workbook as xlrd_open -from xlrd.book import Book as xlrdBook -from xlrd.sheet import Cell as xlrdCell -from xlrd.sheet import Sheet as xlrdSheet -from xlrd.xldate import XLDateAmbiguous, xldate_as_tuple from pyxform import constants from pyxform.errors import PyXFormError, PyXFormReadError -aCell = xlrdCell | pyxlCell XL_DATE_AMBIGOUS_MSG = ( "The xls file provided has an invalid date on the %s sheet, under" " the %s column on row number %s" @@ -102,8 +95,8 @@ def get_excel_column_headers(first_row: Iterable[str | None]) -> list[str | None def get_excel_rows( headers: Iterable[str | None], - rows: Iterable[tuple[aCell, ...]], - cell_func: Callable[[aCell, int, str], Any], + rows: Iterable[tuple[pyxlCell, ...]], + cell_func: Callable[[pyxlCell, int, str], Any], ) -> list[dict[str, Any]]: """Get rows of cleaned data; stop if there's a run of empty rows.""" max_adjacent_empty_rows = 60 @@ -137,112 +130,6 @@ def get_excel_rows( return trim_trailing_empty(result_rows, adjacent_empty_rows) -def xls_to_dict(path_or_file): - """ - Return a Python dictionary with a key for each worksheet - name. For each sheet there is a list of dictionaries, each - dictionary corresponds to a single row in the worksheet. A - dictionary has keys taken from the column headers and values - equal to the cell value for that row and column. - All the keys and leaf elements are unicode text. - """ - - def xls_clean_cell( - wb: xlrdBook, wb_sheet: xlrdSheet, cell: xlrdCell, row_n: int, col_key: str - ) -> str | None: - value = cell.value - if isinstance(value, str): - value = value.strip() - if not is_empty(value): - try: - return xls_value_to_unicode(value, cell.ctype, wb.datemode) - except XLDateAmbiguous as date_err: - raise PyXFormError( - XL_DATE_AMBIGOUS_MSG % (wb_sheet.name, col_key, row_n) - ) from date_err - - return None - - def xls_to_dict_normal_sheet(wb: xlrdBook, wb_sheet: xlrdSheet): - # XLS format: max cols 256, max rows 65536 - first_row = (c.value for c in next(wb_sheet.get_rows(), [])) - headers = get_excel_column_headers(first_row=first_row) - row_iter = ( - tuple(wb_sheet.cell(r, c) for c in range(len(headers))) - for r in range(1, wb_sheet.nrows) - ) - - # Inject wb/sheet as closure since functools.partial isn't typing friendly. - def clean_func(cell: xlrdCell, row_n: int, col_key: str) -> str | None: - return xls_clean_cell( - wb=wb, wb_sheet=wb_sheet, cell=cell, row_n=row_n, col_key=col_key - ) - - rows = get_excel_rows(headers=headers, rows=row_iter, cell_func=clean_func) - column_header_list = [key for key in headers if key is not None] - return rows, _list_to_dict_list(column_header_list) - - def process_workbook(wb: xlrdBook): - result_book = {"sheet_names": []} - for wb_sheet in wb.sheets(): - # Note original in sheet_names for spelling check. - result_book["sheet_names"].append(wb_sheet.name) - sheet_name = wb_sheet.name.lower() - # Do not process sheets that have nothing to do with XLSForm. - if sheet_name not in constants.SUPPORTED_SHEET_NAMES: - if len(wb.sheets()) == 1: - ( - result_book[constants.SURVEY], - result_book[f"{constants.SURVEY}_header"], - ) = xls_to_dict_normal_sheet(wb=wb, wb_sheet=wb_sheet) - else: - continue - else: - ( - result_book[sheet_name], - result_book[f"{sheet_name}_header"], - ) = xls_to_dict_normal_sheet(wb=wb, wb_sheet=wb_sheet) - return result_book - - try: - wb_file = get_definition_data(definition=path_or_file) - workbook = xlrd_open(file_contents=wb_file.data.getvalue()) - try: - return process_workbook(wb=workbook) - finally: - workbook.release_resources() - except (AttributeError, TypeError, XLRDError) as read_err: - raise PyXFormReadError(f"Error reading .xls file: {read_err}") from read_err - - -def xls_value_to_unicode(value, value_type, datemode) -> str: - """Take a xls formatted value and try to make a unicode string representation.""" - if value_type == XL_CELL_BOOLEAN: - return "TRUE" if value else "FALSE" - elif value_type == XL_CELL_NUMBER: - # Try to display as an int if possible. - int_value = int(value) - if int_value == value: - return str(int_value) - else: - return str(value) - elif value_type is XL_CELL_DATE: - # Warn that it is better to single quote as a string. - # error_location = cellFormatString % (ss_row_idx, ss_col_idx) - # raise Exception( - # "Cannot handle excel formatted date at " + error_location) - datetime_or_time_only = xldate_as_tuple(value, datemode) - if datetime_or_time_only[:3] == (0, 0, 0): - # must be time only - return str(datetime.time(*datetime_or_time_only[3:])) - return str(datetime.datetime(*datetime_or_time_only)) - else: - # ensure unicode and replace nbsp spaces with normal ones - # to avoid this issue: - # https://github.com/modilabs/pyxform/issues/83 - return str(value).replace(chr(160), " ") - - def xlsx_to_dict(path_or_file): """ Return a Python dictionary with a key for each worksheet @@ -449,39 +336,7 @@ def convert_file_to_csv_string(path): def sheet_to_csv(workbook_path, csv_path, sheet_name): - if workbook_path.endswith(".xls"): - return xls_sheet_to_csv(workbook_path, csv_path, sheet_name) - else: - return xlsx_sheet_to_csv(workbook_path, csv_path, sheet_name) - - -def xls_sheet_to_csv(workbook_path, csv_path, sheet_name): - wb = xlrd_open(workbook_path) - try: - sheet = wb.sheet_by_name(sheet_name) - except XLRDError: - return False - if not sheet or sheet.nrows < 2: - return False - with open(csv_path, mode="w", encoding="utf-8", newline="") as f: - writer = csv.writer(f, quoting=csv.QUOTE_ALL) - mask = [v and len(v.strip()) > 0 for v in sheet.row_values(0)] - for row_idx in range(sheet.nrows): - csv_data = [] - try: - for v, m in zip(sheet.row(row_idx), mask, strict=False): - if m: - value = v.value - value_type = v.ctype - data = xls_value_to_unicode(value, value_type, wb.datemode) - # clean the values of leading and trailing whitespaces - data = data.strip() - csv_data.append(data) - except TypeError: - continue - writer.writerow(csv_data) - - return True + return xlsx_sheet_to_csv(workbook_path, csv_path, sheet_name) def xlsx_sheet_to_csv(workbook_path, csv_path, sheet_name): @@ -652,7 +507,6 @@ def is_csv(data: str) -> bool: class SupportedFileTypes(Enum): xlsx = ".xlsx" xlsm = ".xlsm" - xls = ".xls" md = ".md" csv = ".csv" @@ -661,7 +515,6 @@ def get_processors(): return { SupportedFileTypes.xlsx: xlsx_to_dict, SupportedFileTypes.xlsm: xlsx_to_dict, - SupportedFileTypes.xls: xls_to_dict, SupportedFileTypes.md: md_to_dict, SupportedFileTypes.csv: csv_to_dict, } diff --git a/tests/test_xls2json_backends.py b/tests/test_xls2json_backends.py index 0159f757..df62f4e6 100644 --- a/tests/test_xls2json_backends.py +++ b/tests/test_xls2json_backends.py @@ -6,7 +6,6 @@ from unittest import expectedFailure import openpyxl -import xlrd from pyxform.builder import create_survey_element_from_dict from pyxform.xls2json import workbook_to_json from pyxform.xls2json_backends import ( @@ -15,7 +14,6 @@ get_xlsform, md_table_to_workbook, md_to_dict, - xls_value_to_unicode, xlsx_to_dict, xlsx_value_to_str, ) @@ -34,26 +32,6 @@ class TestXLS2JSONBackends(PyxformTestCase): maxDiff = None - def test_xls_value_to_unicode(self): - """ - Test external choices sheet with numeric values is processed successfully. - - The test ensures that the integer values within the external choices sheet - are returned as they were initially received. - """ - value = 32.0 - value_type = xlrd.XL_CELL_NUMBER - datemode = 1 - csv_data = xls_value_to_unicode(value, value_type, datemode) - expected_output = "32" - self.assertEqual(csv_data, expected_output) - - # Test that the decimal value is not changed during conversion. - value = 46.9 - csv_data = xls_value_to_unicode(value, value_type, datemode) - expected_output = "46.9" - self.assertEqual(csv_data, expected_output) - def test_xlsx_value_to_str(self): value = 32.0 csv_data = xlsx_value_to_str(value) diff --git a/tests/xform_test_case/test_bugs.py b/tests/xform_test_case/test_bugs.py index 4d942c27..a88539a4 100644 --- a/tests/xform_test_case/test_bugs.py +++ b/tests/xform_test_case/test_bugs.py @@ -60,9 +60,7 @@ def test_xl_date_ambigous(self): class TestXLDateAmbigousNoException(TestCase): - """Test date values that exceed the workbook datemode value. - (This would cause an exception with xlrd, but openpyxl handles it). - """ + """Test date values that exceed the workbook datemode value.""" def test_xl_date_ambigous_no_exception(self): """Test standard sheet is processed successfully."""