From 59ef17bb3170d2e1a76ad0388b6fd865ee593f8a Mon Sep 17 00:00:00 2001 From: lindsay stevens Date: Mon, 13 Jul 2026 19:10:07 +1000 Subject: [PATCH 01/17] 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/17] 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/17] 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/17] 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/17] 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/17] 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/17] 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/17] 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/17] 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/17] 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/17] 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/17] 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/17] 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/17] 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/17] 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/17] 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 70cfdf78fb0a27cfc6db6c80a2cb7a1f9e692cab Mon Sep 17 00:00:00 2001 From: lindsay stevens Date: Tue, 14 Jul 2026 00:28:45 +1000 Subject: [PATCH 17/17] chg: remove support for enketo-validate - updater script has not worked since 2020 - enketo-validate dropped support for binary builds in 2022 - odk is moving towards web-forms instead of enketo --- README.md | 2 +- pyxform/survey.py | 21 +-- .../validators/enketo_validate/__init__.py | 79 --------- pyxform/validators/error_cleaner.py | 6 - pyxform/validators/updater.py | 30 +--- pyxform/xls2xform.py | 28 +-- tests/test_validator_update.py | 73 +++----- tests/test_validator_util.py | 5 - tests/test_xls2json.py | 1 - tests/test_xls2xform.py | 34 ---- tests/validators/data/latest_enketo.json | 166 ------------------ tests/validators/data/latest_odk.json | 97 +++++++--- .../validators/data/latest_odk_no_files.json | 43 +++++ tests/validators/data/latest_odk_old.json | 37 ++++ 14 files changed, 197 insertions(+), 425 deletions(-) delete mode 100644 pyxform/validators/enketo_validate/__init__.py delete mode 100644 tests/validators/data/latest_enketo.json create mode 100644 tests/validators/data/latest_odk_no_files.json create mode 100644 tests/validators/data/latest_odk_old.json diff --git a/README.md b/README.md index 0f003fee..df9cf476 100644 --- a/README.md +++ b/README.md @@ -1,6 +1,6 @@ # pyxform -`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). +`pyxform` is a Python library that simplifies writing forms for ODK Collect, ODK Web Forms, 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). ## Project status diff --git a/pyxform/survey.py b/pyxform/survey.py index 7e7f0a7c..6fa23596 100644 --- a/pyxform/survey.py +++ b/pyxform/survey.py @@ -27,7 +27,7 @@ escape_text_for_xml, node, ) -from pyxform.validators import enketo_validate, odk_validate +from pyxform.validators import odk_validate from pyxform.validators.pyxform import unique_names from pyxform.validators.pyxform.iana_subtags.validation import get_languages_with_bad_tags from pyxform.validators.pyxform.pyxform_reference import ( @@ -1206,7 +1206,7 @@ def _var_repl_output_function(matchobj): return text, False def print_xform_to_file( - self, path=None, validate=True, pretty_print=True, warnings=None, enketo=False + self, path=None, validate=True, pretty_print=True, warnings=None ) -> str: """ Print the xForm to a file and optionally validate it as well by @@ -1229,8 +1229,6 @@ def print_xform_to_file( raise if validate: warnings.extend(odk_validate.check_xform(path)) - if enketo: - warnings.extend(enketo_validate.check_xform(path)) # Warn if one or more translation is missing a valid IANA subtag translations = self._translations @@ -1246,15 +1244,15 @@ def print_xform_to_file( ) return xml - def to_xml(self, validate=True, pretty_print=True, warnings=None, enketo=False): + def to_xml(self, validate=True, pretty_print=True, warnings=None): """ - Generates the XForm XML. - validate is True by default - pass the XForm XML through ODK Validator. - pretty_print is True by default - formats the XML for readability. - warnings - if a list is passed it stores all warnings generated - enketo - pass the XForm XML though Enketo Validator. + Generate the XForm XML. - Return XForm XML string. + :param validate: True by default - pass the XForm XML through ODK Validator. + :param pretty_print: True by default - formats the XML for readability. + :param warnings: if a list is passed it stores all warnings generated + + :returns: XForm XML string. """ # On Windows, NamedTemporaryFile must be opened exclusively. # So it must be explicitly created, opened, closed, and removed. @@ -1268,7 +1266,6 @@ def to_xml(self, validate=True, pretty_print=True, warnings=None, enketo=False): validate=validate, pretty_print=pretty_print, warnings=warnings, - enketo=enketo, ) finally: tmp_path.unlink(missing_ok=True) diff --git a/pyxform/validators/enketo_validate/__init__.py b/pyxform/validators/enketo_validate/__init__.py deleted file mode 100644 index 5c4a9e59..00000000 --- a/pyxform/validators/enketo_validate/__init__.py +++ /dev/null @@ -1,79 +0,0 @@ -"""Validate XForms using Enketo validator.""" - -import os -from typing import TYPE_CHECKING - -from pyxform.validators.error_cleaner import ErrorCleaner -from pyxform.validators.util import ( - XFORM_SPEC_PATH, - check_readable, - decode_stream, - run_popen_with_timeout, -) - -if TYPE_CHECKING: - from pyxform.validators.util import PopenResult - - -CURRENT_DIRECTORY = os.path.dirname(os.path.realpath(__file__)) -ENKETO_VALIDATE_PATH = os.path.join(CURRENT_DIRECTORY, "bin", "validate") - - -class EnketoValidateError(Exception): - """Common base class for Enketo validate exceptions.""" - - -def install_exists(): - """Check if Enketo-validate is installed.""" - return os.path.exists(ENKETO_VALIDATE_PATH) - - -def _call_validator(path_to_xform, bin_file_path=ENKETO_VALIDATE_PATH) -> "PopenResult": - return run_popen_with_timeout([bin_file_path, path_to_xform], 100) - - -def install_ok(bin_file_path=ENKETO_VALIDATE_PATH): - """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 - ) - if return_code == 1: - return False - else: - return True - - -def check_xform(path_to_xform): - """ - Check the form with the Enketo validator. - - - return code 1: raise error with the stderr content. - - return code 0: append warning with the stdout content (possibly none). - - :param path_to_xform: Path to the XForm to be validated. - :return: warnings or List[str] - """ - if not install_exists(): - raise OSError( - "Enketo-validate dependency not found. " - "Please use the updater tool to install the latest version." - ) - - returncode, timeout, stdout, stderr = _call_validator(path_to_xform=path_to_xform) - warnings = [] - stderr = decode_stream(stderr) - stdout = decode_stream(stdout) - - if timeout: - return ["XForm took to long to completely validate."] - elif returncode > 0: # Error invalid - raise EnketoValidateError( - "Enketo Validate Errors:\n" + ErrorCleaner.enketo_validate(stderr) - ) - elif returncode == 0: - if stdout: - warnings.append("Enketo Validate Warnings:\n" + stdout) - return warnings - elif returncode < 0: - return ["Bad return code from Enketo Validate."] diff --git a/pyxform/validators/error_cleaner.py b/pyxform/validators/error_cleaner.py index dd83bdc0..57cea580 100644 --- a/pyxform/validators/error_cleaner.py +++ b/pyxform/validators/error_cleaner.py @@ -66,9 +66,3 @@ def odk_validate(error_message): java_clean = [ErrorCleaner._remove_java_content(i) for i in common] final_message = ErrorCleaner._join_final(java_clean) return final_message - - @staticmethod - def enketo_validate(error_message): - common = ErrorCleaner._cleanup_errors(error_message) - final_message = ErrorCleaner._join_final(common) - return final_message diff --git a/pyxform/validators/updater.py b/pyxform/validators/updater.py index 4172e777..a4123296 100644 --- a/pyxform/validators/updater.py +++ b/pyxform/validators/updater.py @@ -12,7 +12,7 @@ from zipfile import ZipFile, is_zipfile from pyxform.errors import PyXFormError -from pyxform.validators import enketo_validate, odk_validate +from pyxform.validators import odk_validate from pyxform.validators.util import HERE, CapturingHandler, request_get UTC_FMT = "%Y-%m-%dT%H:%M:%SZ" @@ -483,25 +483,6 @@ def _install_check(bin_file_path=None): raise NotImplementedError() -class EnketoValidateUpdater(_UpdateService): - def __init__(self): - self.update_info = _UpdateInfo( - api_url="https://api.github.com/repos/enketo/enketo-validate/releases/latest", - repo_url="https://github.com/enketo/enketo-validate", - validate_subfolder="enketo_validate", - install_check=self._install_check, - validator_basename=os.path.basename(enketo_validate.ENKETO_VALIDATE_PATH), - ) - - @staticmethod - def _install_check(bin_file_path=None): - if bin_file_path is None: - return enketo_validate.install_ok() - else: - extracted = os.path.join(os.path.dirname(bin_file_path), "validate") - return enketo_validate.install_ok(bin_file_path=extracted) - - class ODKValidateUpdater(_UpdateService): def __init__(self): self.update_info = _UpdateInfo( @@ -561,8 +542,8 @@ def _create_parser(): "------------------------------------------------------\n" "Use this tool to update external validators.\n\n" "Example usage:\n\n" - "updater.py enketo list\n" - "updater.py enketo update linux.zip\n\n" + "updater.py ODK list\n" + "updater.py ODK update linux.zip\n\n" "First, use the 'list' sub-command for the validator\n" "to check for a new release and to show what (if any) \n" "files are attached to it.\n\n" @@ -576,11 +557,6 @@ def _create_parser(): formatter_class=argparse.RawDescriptionHelpFormatter, ) sub_parsers = main_parser.add_subparsers(metavar="") - _build_validator_menu( - main_subparser=sub_parsers, - validator_name="Enketo", - updater_instance=EnketoValidateUpdater(), - ) _build_validator_menu( main_subparser=sub_parsers, validator_name="ODK", diff --git a/pyxform/xls2xform.py b/pyxform/xls2xform.py index 0cda374a..f91700d7 100644 --- a/pyxform/xls2xform.py +++ b/pyxform/xls2xform.py @@ -65,7 +65,6 @@ def convert( warnings: list[str] | None = None, validate: bool = False, pretty_print: bool = False, - enketo: bool = False, form_name: str | None = None, default_language: str | None = None, file_type: str | None = None, @@ -75,10 +74,9 @@ def convert( This function avoids result file IO so it is more suited to library usage of pyxform. - If validate=True or Enketo=True, then the XForm will be written to a temporary file - to be checked by ODK Validate and/or Enketo Validate. These validators are run as - external processes. A recent version of ODK Validate is distributed with pyxform, - while Enketo Validate is not. A script to download or update these validators is + If validate=True, then the XForm will be written to a temporary file to be checked by + ODK Validate. This validator is run as an external process. A recent version of ODK + Validate is distributed with pyxform. A script to download or update ODK Validate is provided in `validators/updater.py`. :param xlsform: The input XLSForm file path or content. If the content is bytes or @@ -87,7 +85,6 @@ def convert( :param warnings: The conversions warnings list. :param validate: If True, check the XForm with ODK Validate :param pretty_print: If True, format the XForm with spaces, line breaks, etc. - :param enketo: If True, check the XForm with Enketo Validate. :param form_name: Used for the main instance root node name. :param default_language: The name of the default language for the form. :param file_type: If provided, attempt parsing the data only as this type. Otherwise, @@ -113,7 +110,6 @@ def convert( validate=validate, pretty_print=pretty_print, warnings=warnings, - enketo=enketo, ) return ConvertResult( xform=xform, @@ -129,14 +125,12 @@ def xls2xform_convert( xform_path: str | PathLike[str], validate: bool = True, pretty_print: bool = True, - enketo: bool = False, ) -> list[str]: warnings = [] result = convert( xlsform=xlsform_path, validate=validate, pretty_print=pretty_print, - enketo=enketo, warnings=warnings, ) with open(xform_path, mode="w", encoding="utf-8") as f: @@ -176,12 +170,6 @@ def _create_parser(): default=False, help="Run the ODK Validate XForm external validator.", ) - parser.add_argument( - "--enketo_validate", - action="store_true", - default=False, - help="Run the Enketo Validate XForm external validator.", - ) parser.add_argument( "--pretty_print", action="store_true", @@ -202,17 +190,13 @@ def _validator_args_logic(args): `xls2xform.py myform`: ODK only **new** - `xls2xform.py myform --enketo_validate`: Enketo only `xls2xform.py myform --odk_validate`: ODK only - `xls2xform.py myform --enketo_validate --odk_validate`: both - `xls2xform.py myform --enketo_validate --odk_validate --skip_validate`: no validators + `xls2xform.py myform --odk_validate --skip_validate`: no validators """ if not args.skip_validate: args.odk_validate = False - args.enketo_validate = False - elif args.skip_validate and not (args.odk_validate or args.enketo_validate): + elif args.skip_validate and not args.odk_validate: args.odk_validate = True - args.enketo_validate = False return args @@ -236,7 +220,6 @@ def main_cli(): xform_path=args.output_path, validate=args.odk_validate, pretty_print=args.pretty_print, - enketo=args.enketo_validate, ) response["code"] = 100 @@ -259,7 +242,6 @@ def main_cli(): xform_path=args.output_path, validate=args.odk_validate, pretty_print=args.pretty_print, - enketo=args.enketo_validate, ) except OSError: # Do not crash if 'java' not installed diff --git a/tests/test_validator_update.py b/tests/test_validator_update.py index a1be1cf5..eafbc005 100644 --- a/tests/test_validator_update.py +++ b/tests/test_validator_update.py @@ -9,7 +9,6 @@ from pyxform.errors import PyXFormError from pyxform.validators.updater import ( - EnketoValidateUpdater, _UpdateHandler, _UpdateInfo, capture_handler, @@ -91,8 +90,10 @@ def setUp(self): self.update_info = get_update_info(check_ok=True) self.updater = _UpdateHandler() data_dir = os.path.join(TEST_PATH, "data") - self.latest_enketo = os.path.join(data_dir, "latest_enketo.json") self.latest_odk = os.path.join(data_dir, "latest_odk.json") + self.latest_odk_old = os.path.join(data_dir, "latest_odk_old.json") + self.latest_odk_no_files = os.path.join(data_dir, "latest_odk_no_files.json") + self.latest_odk_file_name = "ODK-Validate-v1.20.0.jar" self.last_check = os.path.join(TEST_PATH, ".last_check") self.last_check_none = os.path.join(TEST_PATH, ".last_check_none") self.phantom_file = os.path.join(TEST_PATH, ".not_there") @@ -106,7 +107,7 @@ def setUp(self): def test_request_latest_json(self): """Should return version info dict containing asset list.""" - self.update_info.api_url = "http://localhost:8000/latest_enketo.json" + self.update_info.api_url = "http://localhost:8000/latest_odk.json" observed = self.updater._request_latest_json(url=self.update_info.api_url) self.assertIn("assets", observed) @@ -126,7 +127,7 @@ def test_check_path__dir(self): def test_read_json(self): """Should return version info dict containing asset list.""" - file_path = self.latest_enketo + file_path = self.latest_odk observed = self.updater._read_json(file_path=file_path) self.assertIn("assets", observed) @@ -172,7 +173,7 @@ def test_check_necessary__true_if_latest_json_not_found(self): def test_check_necessary__true_if_last_check_empty(self): """Should return true if the last check file was empty.""" self.update_info.last_check_path = os.path.join(TEST_PATH, ".last_check_none") - self.update_info.latest_path = self.latest_enketo + self.update_info.latest_path = self.latest_odk self.assertTrue( self.updater._check_necessary( update_info=self.update_info, utc_now=self.utc_now @@ -182,7 +183,7 @@ def test_check_necessary__true_if_last_check_empty(self): def test_check_necessary__true_if_last_check_too_old(self): """Should return true if the last check was too long ago.""" self.update_info.last_check_path = self.last_check - self.update_info.latest_path = self.latest_enketo + self.update_info.latest_path = self.latest_odk old = self.utc_now - timedelta(minutes=45.0) self.assertTrue( self.updater._check_necessary(update_info=self.update_info, utc_now=old) @@ -195,7 +196,7 @@ def test_check_necessary__false_last_check_very_recent(self): with get_temp_file() as temp_file: self.updater._write_last_check(file_path=temp_file, content=new) self.update_info.last_check_path = temp_file - self.update_info.latest_path = self.latest_enketo + self.update_info.latest_path = self.latest_odk self.assertFalse( self.updater._check_necessary( update_info=self.update_info, utc_now=self.utc_now @@ -204,7 +205,7 @@ def test_check_necessary__false_last_check_very_recent(self): def test_get_latest__if_check_necessary_true(self): """Should get latest from remote, rather than file.""" - self.update_info.api_url = "http://localhost:8000/latest_enketo.json" + self.update_info.api_url = "http://localhost:8000/latest_odk.json" old = self.utc_now - timedelta(minutes=45.0) with get_temp_file() as temp_check, get_temp_file() as temp_json: @@ -212,7 +213,7 @@ def test_get_latest__if_check_necessary_true(self): self.update_info.last_check_path = temp_check self.update_info.latest_path = temp_json latest = self.updater._get_latest(update_info=self.update_info) - self.assertEqual("1.0.3", latest["name"]) + self.assertEqual("v1.20.0", latest["name"]) def test_get_latest__if_check_necessary_false(self): """Should get latest from file, rather than remote.""" @@ -223,12 +224,12 @@ def test_get_latest__if_check_necessary_false(self): self.updater._write_last_check(file_path=temp_check, content=new) self.update_info.last_check_path = temp_check latest = self.updater._get_latest(update_info=self.update_info) - self.assertEqual("ODK Validate v1.8.0", latest["name"]) + self.assertEqual("v1.20.0", latest["name"]) def test_list__not_installed_no_files(self): """Should log an info message - no installed version, no files.""" self.update_info.installed_path = self.phantom_file - self.update_info.latest_path = self.latest_odk + self.update_info.latest_path = self.latest_odk_no_files with get_temp_file() as temp_check: self.updater._write_last_check(file_path=temp_check, content=self.utc_now) @@ -241,7 +242,7 @@ def test_list__not_installed_no_files(self): def test_list__not_installed_with_files(self): """Should log an info message - no installed version, with files.""" self.update_info.installed_path = self.phantom_file - self.update_info.latest_path = self.latest_enketo + self.update_info.latest_path = self.latest_odk with get_temp_file() as temp_check: self.updater._write_last_check(file_path=temp_check, content=self.utc_now) @@ -249,11 +250,11 @@ def test_list__not_installed_with_files(self): self.updater.list(update_info=self.update_info) info = capture_handler.watcher.output["INFO"][0] self.assertIn("Installed release:\n\n- None!", info) - self.assertIn("- windows.zip", info) + self.assertIn(f"- {self.latest_odk_file_name}", info) def test_list__installed_no_files(self): """Should log an info message - installed version, no files.""" - self.update_info.installed_path = self.latest_enketo + self.update_info.installed_path = self.latest_odk_old self.update_info.latest_path = self.latest_odk with get_temp_file() as temp_check: @@ -261,26 +262,26 @@ def test_list__installed_no_files(self): self.update_info.last_check_path = temp_check self.updater.list(update_info=self.update_info) info = capture_handler.watcher.output["INFO"][0] - self.assertIn("Installed release:\n\n- Tag name = 1.0.3", info) - self.assertIn("Files available:\n\n- None!", info) + self.assertIn("Installed release:\n\n- Tag name = v1.8.0", info) + self.assertIn(f"Files available:\n\n- {self.latest_odk_file_name}", info) def test_list__installed_with_files(self): """Should log an info message - installed version, with files.""" - self.update_info.installed_path = self.latest_enketo - self.update_info.latest_path = self.latest_enketo + self.update_info.installed_path = self.latest_odk + self.update_info.latest_path = self.latest_odk with get_temp_file() as temp_check: self.updater._write_last_check(file_path=temp_check, content=self.utc_now) self.update_info.last_check_path = temp_check self.updater.list(update_info=self.update_info) info = capture_handler.watcher.output["INFO"][0] - self.assertIn("Installed release:\n\n- Tag name = 1.0.3", info) - self.assertIn("- windows.zip", info) + self.assertIn("Installed release:\n\n- Tag name = v1.20.0", info) + self.assertIn(self.latest_odk_file_name, info) def test_find_download_url__no_files(self): """Should raise an error if no files attached to release.""" - file_name = "windows.zip" - json_data = self.updater._read_json(file_path=self.latest_odk) + file_name = self.latest_odk_file_name + json_data = self.updater._read_json(file_path=self.latest_odk_no_files) with self.assertRaises(PyXFormError) as ctx: self.updater._find_download_url( @@ -291,7 +292,7 @@ def test_find_download_url__no_files(self): def test_find_download_url__not_found(self): """Should raise an error if the file was not found.""" file_name = "windows.zip" - json_data = self.updater._read_json(file_path=self.latest_enketo) + json_data = self.updater._read_json(file_path=self.latest_odk) json_data["assets"] = [x for x in json_data["assets"] if x["name"] != file_name] with self.assertRaises(PyXFormError) as ctx: @@ -302,8 +303,8 @@ def test_find_download_url__not_found(self): def test_find_download_url__duplicates(self): """Should raise an error if the file was found more than once.""" - file_name = "windows.zip" - json_data = self.updater._read_json(file_path=self.latest_enketo) + file_name = self.latest_odk_file_name + json_data = self.updater._read_json(file_path=self.latest_odk) file_dicts = [x for x in json_data["assets"] if x["name"] == file_name] json_data["assets"].append(file_dicts[0]) @@ -315,11 +316,10 @@ def test_find_download_url__duplicates(self): def test_find_download_url__ok(self): """Should return the url for the matching file name.""" - file_name = "windows.zip" - json_data = self.updater._read_json(file_path=self.latest_enketo) + file_name = self.latest_odk_file_name + json_data = self.updater._read_json(file_path=self.latest_odk) expected = ( - "https://github.com/enketo/enketo-validate/releases/" - "download/1.0.3/windows.zip" + f"https://github.com/getodk/validate/releases/download/v1.20.0/{file_name}" ) observed = self.updater._find_download_url( @@ -634,18 +634,3 @@ def test_check__fail__install_check(self): error = str(ctx.exception) self.assertIn("Check failed!", error) self.assertIn("installed release does not appear to work", error) - - def test_enketo_validate_updater__install_check_routing_ok(self): - """Should call the install check on the UpdateInfo instance.""" - ev = EnketoValidateUpdater() - ev.update_info.install_check = install_check_ok - ev.update_info.installed_path = self.install_fake - self.assertTrue(ev.check()) - - def test_enketo_validate_updater__install_check_routing_fail(self): - """Should raise if the install check function is bogus.""" - ev = EnketoValidateUpdater() - ev.update_info.install_check = None - ev.update_info.installed_path = self.install_fake - with self.assertRaises(TypeError): - ev.check() diff --git a/tests/test_validator_util.py b/tests/test_validator_util.py index 31fb7711..c8fef24d 100644 --- a/tests/test_validator_util.py +++ b/tests/test_validator_util.py @@ -72,8 +72,3 @@ def should_clean_odk_validate_stacktrace(self): message = """java.lang.NullPointerException Null Pointer\norg.javarosa.xform.parse.XFormParseException Parser""" cleaned = ErrorCleaner.odk_validate(message) self.assertEqual(cleaned, " Null Pointer\n Parser") - - def should_clean_enketo_validate_stacktrace(self): - message = """Error in gugu/gaga\nError in gugu/gaga""" - cleaned = ErrorCleaner.enketo_validate(message) - self.assertEqual(cleaned, "Error in gugu/gaga") diff --git a/tests/test_xls2json.py b/tests/test_xls2json.py index b1c94bd6..98d48038 100644 --- a/tests/test_xls2json.py +++ b/tests/test_xls2json.py @@ -591,7 +591,6 @@ def test_xls2xform_convert__e2e_with_settings_misspelling(self): xform_path=os.path.join(test_output.PATH, file_name + ".xml"), validate=False, pretty_print=False, - enketo=False, ) expected = ( "When looking for a sheet named 'settings', the following sheets " diff --git a/tests/test_xls2xform.py b/tests/test_xls2xform.py index de0b67a0..180aec3c 100644 --- a/tests/test_xls2xform.py +++ b/tests/test_xls2xform.py @@ -83,14 +83,6 @@ def test_create_parser_skip_validate_default_true(self): args = _create_parser().parse_args(arg_list) self.assertEqual(True, args.skip_validate) - def test_create_parser_no_enketo_default_false(self): - """Should have enketo_validate=False if not specified.""" - arg_xlsform = "xlsform.xlsx" - arg_output = "." - arg_list = [arg_xlsform, arg_output] - args = _create_parser().parse_args(arg_list) - self.assertEqual(False, args.enketo_validate) - def test_create_parser_pretty_print_default_False(self): """Should have pretty_print=False if not specified.""" args = _create_parser().parse_args(["xlsform.xlsx", "."]) @@ -101,37 +93,18 @@ def test_validator_args_logic_skip_validate_alone(self): raw_args = _create_parser().parse_args(["xlsform.xlsx", ".", "--skip_validate"]) args = _validator_args_logic(args=raw_args) self.assertEqual(False, args.odk_validate) - self.assertEqual(False, args.enketo_validate) def test_validator_args_logic_odk_default(self): """Should activate ODK only.""" raw_args = _create_parser().parse_args(["xlsform.xlsx", "."]) args = _validator_args_logic(args=raw_args) self.assertEqual(True, args.odk_validate) - self.assertEqual(False, args.enketo_validate) - - def test_validator_args_logic_enketo_only(self): - """Should activate Enketo only.""" - raw_args = _create_parser().parse_args(["xlsform.xlsx", ".", "--enketo_validate"]) - args = _validator_args_logic(args=raw_args) - self.assertEqual(False, args.odk_validate) - self.assertEqual(True, args.enketo_validate) def test_validator_args_logic_odk_only(self): """Should activate ODK only.""" raw_args = _create_parser().parse_args(["xlsform.xlsx", ".", "--odk_validate"]) args = _validator_args_logic(args=raw_args) self.assertEqual(True, args.odk_validate) - self.assertEqual(False, args.enketo_validate) - - def test_validator_args_logic_odk_and_enketo(self): - """Should activate ODK and Enketo.""" - raw_args = _create_parser().parse_args( - ["xlsform.xlsx", ".", "--odk_validate", "--enketo_validate"] - ) - args = _validator_args_logic(args=raw_args) - self.assertEqual(True, args.odk_validate) - self.assertEqual(True, args.enketo_validate) def test_validator_args_logic_skip_validate_override(self): """Should deactivate both validators.""" @@ -141,12 +114,10 @@ def test_validator_args_logic_skip_validate_override(self): ".", "--skip_validate", "--odk_validate", - "--enketo_validate", ] ) args = _validator_args_logic(args=raw_args) self.assertEqual(False, args.odk_validate) - self.assertEqual(False, args.enketo_validate) @mock.patch( "argparse.ArgumentParser.parse_args", @@ -156,7 +127,6 @@ def test_validator_args_logic_skip_validate_override(self): json=False, skip_validate=False, odk_validate=False, - enketo_validate=False, pretty_print=False, ), ) @@ -173,7 +143,6 @@ def test_xls2form_convert_parameters(self, converter_mock, parser_mock_args): xform_path="xlsform.xml", validate=False, pretty_print=False, - enketo=False, ) @mock.patch( @@ -184,7 +153,6 @@ def test_xls2form_convert_parameters(self, converter_mock, parser_mock_args): json=True, skip_validate=False, odk_validate=False, - enketo_validate=False, pretty_print=False, ), ) @@ -202,7 +170,6 @@ def test_xls2xform_convert_params_with_flags(self, converter_mock, parser_mock_a xform_path="xlsform.xml", validate=False, pretty_print=False, - enketo=False, ) @mock.patch( @@ -213,7 +180,6 @@ def test_xls2xform_convert_params_with_flags(self, converter_mock, parser_mock_a json=False, skip_validate=True, odk_validate=True, - enketo_validate=True, pretty_print=True, ), ) diff --git a/tests/validators/data/latest_enketo.json b/tests/validators/data/latest_enketo.json deleted file mode 100644 index e75e11e2..00000000 --- a/tests/validators/data/latest_enketo.json +++ /dev/null @@ -1,166 +0,0 @@ -{ - "assets": [ - { - "browser_download_url": "https://github.com/enketo/enketo-validate/releases/download/1.0.3/linux-oc.zip", - "content_type": "application/zip", - "created_at": "2018-01-08T22:23:11Z", - "download_count": 1, - "id": 5814321, - "label": null, - "name": "linux-oc.zip", - "size": 20767868, - "state": "uploaded", - "updated_at": "2018-01-08T22:24:55Z", - "uploader": { - "avatar_url": "https://avatars1.githubusercontent.com/u/627350?v=4", - "events_url": "https://api.github.com/users/MartijnR/events{/privacy}", - "followers_url": "https://api.github.com/users/MartijnR/followers", - "following_url": "https://api.github.com/users/MartijnR/following{/other_user}", - "gists_url": "https://api.github.com/users/MartijnR/gists{/gist_id}", - "gravatar_id": "", - "html_url": "https://github.com/MartijnR", - "id": 627350, - "login": "MartijnR", - "organizations_url": "https://api.github.com/users/MartijnR/orgs", - "received_events_url": "https://api.github.com/users/MartijnR/received_events", - "repos_url": "https://api.github.com/users/MartijnR/repos", - "site_admin": false, - "starred_url": "https://api.github.com/users/MartijnR/starred{/owner}{/repo}", - "subscriptions_url": "https://api.github.com/users/MartijnR/subscriptions", - "type": "User", - "url": "https://api.github.com/users/MartijnR" - }, - "url": "https://api.github.com/repos/enketo/enketo-validate/releases/assets/5814321" - }, - { - "browser_download_url": "https://github.com/enketo/enketo-validate/releases/download/1.0.3/linux.zip", - "content_type": "application/zip", - "created_at": "2018-01-08T22:23:14Z", - "download_count": 0, - "id": 5814324, - "label": null, - "name": "linux.zip", - "size": 20758351, - "state": "uploaded", - "updated_at": "2018-01-08T22:25:22Z", - "uploader": { - "avatar_url": "https://avatars1.githubusercontent.com/u/627350?v=4", - "events_url": "https://api.github.com/users/MartijnR/events{/privacy}", - "followers_url": "https://api.github.com/users/MartijnR/followers", - "following_url": "https://api.github.com/users/MartijnR/following{/other_user}", - "gists_url": "https://api.github.com/users/MartijnR/gists{/gist_id}", - "gravatar_id": "", - "html_url": "https://github.com/MartijnR", - "id": 627350, - "login": "MartijnR", - "organizations_url": "https://api.github.com/users/MartijnR/orgs", - "received_events_url": "https://api.github.com/users/MartijnR/received_events", - "repos_url": "https://api.github.com/users/MartijnR/repos", - "site_admin": false, - "starred_url": "https://api.github.com/users/MartijnR/starred{/owner}{/repo}", - "subscriptions_url": "https://api.github.com/users/MartijnR/subscriptions", - "type": "User", - "url": "https://api.github.com/users/MartijnR" - }, - "url": "https://api.github.com/repos/enketo/enketo-validate/releases/assets/5814324" - }, - { - "browser_download_url": "https://github.com/enketo/enketo-validate/releases/download/1.0.3/macos.zip", - "content_type": "application/zip", - "created_at": "2018-01-08T22:23:05Z", - "download_count": 1, - "id": 5814318, - "label": null, - "name": "macos.zip", - "size": 20071257, - "state": "uploaded", - "updated_at": "2018-01-08T22:23:33Z", - "uploader": { - "avatar_url": "https://avatars1.githubusercontent.com/u/627350?v=4", - "events_url": "https://api.github.com/users/MartijnR/events{/privacy}", - "followers_url": "https://api.github.com/users/MartijnR/followers", - "following_url": "https://api.github.com/users/MartijnR/following{/other_user}", - "gists_url": "https://api.github.com/users/MartijnR/gists{/gist_id}", - "gravatar_id": "", - "html_url": "https://github.com/MartijnR", - "id": 627350, - "login": "MartijnR", - "organizations_url": "https://api.github.com/users/MartijnR/orgs", - "received_events_url": "https://api.github.com/users/MartijnR/received_events", - "repos_url": "https://api.github.com/users/MartijnR/repos", - "site_admin": false, - "starred_url": "https://api.github.com/users/MartijnR/starred{/owner}{/repo}", - "subscriptions_url": "https://api.github.com/users/MartijnR/subscriptions", - "type": "User", - "url": "https://api.github.com/users/MartijnR" - }, - "url": "https://api.github.com/repos/enketo/enketo-validate/releases/assets/5814318" - }, - { - "browser_download_url": "https://github.com/enketo/enketo-validate/releases/download/1.0.3/windows.zip", - "content_type": "application/zip", - "created_at": "2018-01-08T22:23:09Z", - "download_count": 6, - "id": 5814320, - "label": null, - "name": "windows.zip", - "size": 40170066, - "state": "uploaded", - "updated_at": "2018-01-08T22:24:27Z", - "uploader": { - "avatar_url": "https://avatars1.githubusercontent.com/u/627350?v=4", - "events_url": "https://api.github.com/users/MartijnR/events{/privacy}", - "followers_url": "https://api.github.com/users/MartijnR/followers", - "following_url": "https://api.github.com/users/MartijnR/following{/other_user}", - "gists_url": "https://api.github.com/users/MartijnR/gists{/gist_id}", - "gravatar_id": "", - "html_url": "https://github.com/MartijnR", - "id": 627350, - "login": "MartijnR", - "organizations_url": "https://api.github.com/users/MartijnR/orgs", - "received_events_url": "https://api.github.com/users/MartijnR/received_events", - "repos_url": "https://api.github.com/users/MartijnR/repos", - "site_admin": false, - "starred_url": "https://api.github.com/users/MartijnR/starred{/owner}{/repo}", - "subscriptions_url": "https://api.github.com/users/MartijnR/subscriptions", - "type": "User", - "url": "https://api.github.com/users/MartijnR" - }, - "url": "https://api.github.com/repos/enketo/enketo-validate/releases/assets/5814320" - } - ], - "assets_url": "https://api.github.com/repos/enketo/enketo-validate/releases/9115848/assets", - "author": { - "avatar_url": "https://avatars1.githubusercontent.com/u/627350?v=4", - "events_url": "https://api.github.com/users/MartijnR/events{/privacy}", - "followers_url": "https://api.github.com/users/MartijnR/followers", - "following_url": "https://api.github.com/users/MartijnR/following{/other_user}", - "gists_url": "https://api.github.com/users/MartijnR/gists{/gist_id}", - "gravatar_id": "", - "html_url": "https://github.com/MartijnR", - "id": 627350, - "login": "MartijnR", - "organizations_url": "https://api.github.com/users/MartijnR/orgs", - "received_events_url": "https://api.github.com/users/MartijnR/received_events", - "repos_url": "https://api.github.com/users/MartijnR/repos", - "site_admin": false, - "starred_url": "https://api.github.com/users/MartijnR/starred{/owner}{/repo}", - "subscriptions_url": "https://api.github.com/users/MartijnR/subscriptions", - "type": "User", - "url": "https://api.github.com/users/MartijnR" - }, - "body": "changed: executable-friendly update of enketo-xslt", - "created_at": "2018-01-03T19:59:31Z", - "draft": false, - "html_url": "https://github.com/enketo/enketo-validate/releases/tag/1.0.3", - "id": 9115848, - "name": "1.0.3", - "prerelease": false, - "published_at": "2018-01-05T01:15:48Z", - "tag_name": "1.0.3", - "tarball_url": "https://api.github.com/repos/enketo/enketo-validate/tarball/1.0.3", - "target_commitish": "master", - "upload_url": "https://uploads.github.com/repos/enketo/enketo-validate/releases/9115848/assets{?name,label}", - "url": "https://api.github.com/repos/enketo/enketo-validate/releases/9115848", - "zipball_url": "https://api.github.com/repos/enketo/enketo-validate/zipball/1.0.3" -} \ No newline at end of file diff --git a/tests/validators/data/latest_odk.json b/tests/validators/data/latest_odk.json index ab633afe..1dff576e 100644 --- a/tests/validators/data/latest_odk.json +++ b/tests/validators/data/latest_odk.json @@ -1,37 +1,80 @@ { - "assets": [], - "assets_url": "https://api.github.com/repos/opendatakit/validate/releases/8424483/assets", + "assets": [ + { + "browser_download_url": "https://github.com/getodk/validate/releases/download/v1.20.0/ODK-Validate-v1.20.0.jar", + "content_type": "application/java-archive", + "created_at": "2026-01-08T20:48:03Z", + "digest": "sha256:92756ea4aed195355a07e5572f025f0921a31282387a870ae63e1f5cdf37e0c3", + "download_count": 0, + "id": 338040656, + "label": null, + "name": "ODK-Validate-v1.20.0.jar", + "node_id": "RA_kwDOAmc2ms4UJhdQ", + "size": 5885216, + "state": "uploaded", + "updated_at": "2026-01-08T20:48:04Z", + "uploader": { + "avatar_url": "https://avatars.githubusercontent.com/u/967540?v=4", + "events_url": "https://api.github.com/users/lognaturel/events{/privacy}", + "followers_url": "https://api.github.com/users/lognaturel/followers", + "following_url": "https://api.github.com/users/lognaturel/following{/other_user}", + "gists_url": "https://api.github.com/users/lognaturel/gists{/gist_id}", + "gravatar_id": "", + "html_url": "https://github.com/lognaturel", + "id": 967540, + "login": "lognaturel", + "node_id": "MDQ6VXNlcjk2NzU0MA==", + "organizations_url": "https://api.github.com/users/lognaturel/orgs", + "received_events_url": "https://api.github.com/users/lognaturel/received_events", + "repos_url": "https://api.github.com/users/lognaturel/repos", + "site_admin": false, + "starred_url": "https://api.github.com/users/lognaturel/starred{/owner}{/repo}", + "subscriptions_url": "https://api.github.com/users/lognaturel/subscriptions", + "type": "User", + "url": "https://api.github.com/users/lognaturel", + "user_view_type": "public" + }, + "url": "https://api.github.com/repos/getodk/validate/releases/assets/338040656" + } + ], + "assets_url": "https://api.github.com/repos/getodk/validate/releases/275305335/assets", "author": { - "avatar_url": "https://avatars3.githubusercontent.com/u/32369?v=4", - "events_url": "https://api.github.com/users/yanokwa/events{/privacy}", - "followers_url": "https://api.github.com/users/yanokwa/followers", - "following_url": "https://api.github.com/users/yanokwa/following{/other_user}", - "gists_url": "https://api.github.com/users/yanokwa/gists{/gist_id}", + "avatar_url": "https://avatars.githubusercontent.com/u/967540?v=4", + "events_url": "https://api.github.com/users/lognaturel/events{/privacy}", + "followers_url": "https://api.github.com/users/lognaturel/followers", + "following_url": "https://api.github.com/users/lognaturel/following{/other_user}", + "gists_url": "https://api.github.com/users/lognaturel/gists{/gist_id}", "gravatar_id": "", - "html_url": "https://github.com/yanokwa", - "id": 32369, - "login": "yanokwa", - "organizations_url": "https://api.github.com/users/yanokwa/orgs", - "received_events_url": "https://api.github.com/users/yanokwa/received_events", - "repos_url": "https://api.github.com/users/yanokwa/repos", + "html_url": "https://github.com/lognaturel", + "id": 967540, + "login": "lognaturel", + "node_id": "MDQ6VXNlcjk2NzU0MA==", + "organizations_url": "https://api.github.com/users/lognaturel/orgs", + "received_events_url": "https://api.github.com/users/lognaturel/received_events", + "repos_url": "https://api.github.com/users/lognaturel/repos", "site_admin": false, - "starred_url": "https://api.github.com/users/yanokwa/starred{/owner}{/repo}", - "subscriptions_url": "https://api.github.com/users/yanokwa/subscriptions", + "starred_url": "https://api.github.com/users/lognaturel/starred{/owner}{/repo}", + "subscriptions_url": "https://api.github.com/users/lognaturel/subscriptions", "type": "User", - "url": "https://api.github.com/users/yanokwa" + "url": "https://api.github.com/users/lognaturel", + "user_view_type": "public" }, - "body": "[All changes](https://github.com/opendatakit/collect/compare/v1.7.0...v1.8.0)\r\n\r\n**Highlights**\r\n* Upgrade to [JavaRosa v2.6.1](https://github.com/opendatakit/javarosa/releases/tag/v2.6.1)", - "created_at": "2017-11-07T23:47:15Z", + "body": "## What's Changed\r\n* Update JavaRosa and CircleCI image by @lognaturel in https://github.com/getodk/validate/pull/101\r\n* Use Github Actions for CI by @lognaturel in https://github.com/getodk/validate/pull/103\r\n* Add `intersects` function handler by @lognaturel in https://github.com/getodk/validate/pull/102\r\n\r\n\r\n**Full Changelog**: https://github.com/getodk/validate/compare/v1.19.2...v1.20.0", + "created_at": "2026-01-08T12:03:32Z", "draft": false, - "html_url": "https://github.com/opendatakit/validate/releases/tag/v1.8.0", - "id": 8424483, - "name": "ODK Validate v1.8.0", + "html_url": "https://github.com/getodk/validate/releases/tag/v1.20.0", + "id": 275305335, + "immutable": false, + "mentions_count": 1, + "name": "v1.20.0", + "node_id": "RE_kwDOAmc2ms4QaNN3", "prerelease": false, - "published_at": "2017-11-07T23:47:37Z", - "tag_name": "v1.8.0", - "tarball_url": "https://api.github.com/repos/opendatakit/validate/tarball/v1.8.0", + "published_at": "2026-01-08T20:44:05Z", + "tag_name": "v1.20.0", + "tarball_url": "https://api.github.com/repos/getodk/validate/tarball/v1.20.0", "target_commitish": "master", - "upload_url": "https://uploads.github.com/repos/opendatakit/validate/releases/8424483/assets{?name,label}", - "url": "https://api.github.com/repos/opendatakit/validate/releases/8424483", - "zipball_url": "https://api.github.com/repos/opendatakit/validate/zipball/v1.8.0" + "updated_at": "2026-01-08T20:48:04Z", + "upload_url": "https://uploads.github.com/repos/getodk/validate/releases/275305335/assets{?name,label}", + "url": "https://api.github.com/repos/getodk/validate/releases/275305335", + "zipball_url": "https://api.github.com/repos/getodk/validate/zipball/v1.20.0" } \ No newline at end of file diff --git a/tests/validators/data/latest_odk_no_files.json b/tests/validators/data/latest_odk_no_files.json new file mode 100644 index 00000000..196448d0 --- /dev/null +++ b/tests/validators/data/latest_odk_no_files.json @@ -0,0 +1,43 @@ +{ + "assets": [], + "assets_url": "https://api.github.com/repos/getodk/validate/releases/275305335/assets", + "author": { + "avatar_url": "https://avatars.githubusercontent.com/u/967540?v=4", + "events_url": "https://api.github.com/users/lognaturel/events{/privacy}", + "followers_url": "https://api.github.com/users/lognaturel/followers", + "following_url": "https://api.github.com/users/lognaturel/following{/other_user}", + "gists_url": "https://api.github.com/users/lognaturel/gists{/gist_id}", + "gravatar_id": "", + "html_url": "https://github.com/lognaturel", + "id": 967540, + "login": "lognaturel", + "node_id": "MDQ6VXNlcjk2NzU0MA==", + "organizations_url": "https://api.github.com/users/lognaturel/orgs", + "received_events_url": "https://api.github.com/users/lognaturel/received_events", + "repos_url": "https://api.github.com/users/lognaturel/repos", + "site_admin": false, + "starred_url": "https://api.github.com/users/lognaturel/starred{/owner}{/repo}", + "subscriptions_url": "https://api.github.com/users/lognaturel/subscriptions", + "type": "User", + "url": "https://api.github.com/users/lognaturel", + "user_view_type": "public" + }, + "body": "## What's Changed\r\n* Update JavaRosa and CircleCI image by @lognaturel in https://github.com/getodk/validate/pull/101\r\n* Use Github Actions for CI by @lognaturel in https://github.com/getodk/validate/pull/103\r\n* Add `intersects` function handler by @lognaturel in https://github.com/getodk/validate/pull/102\r\n\r\n\r\n**Full Changelog**: https://github.com/getodk/validate/compare/v1.19.2...v1.20.0", + "created_at": "2026-01-08T12:03:32Z", + "draft": false, + "html_url": "https://github.com/getodk/validate/releases/tag/v1.20.0", + "id": 275305335, + "immutable": false, + "mentions_count": 1, + "name": "v1.20.0", + "node_id": "RE_kwDOAmc2ms4QaNN3", + "prerelease": false, + "published_at": "2026-01-08T20:44:05Z", + "tag_name": "v1.20.0", + "tarball_url": "https://api.github.com/repos/getodk/validate/tarball/v1.20.0", + "target_commitish": "master", + "updated_at": "2026-01-08T20:48:04Z", + "upload_url": "https://uploads.github.com/repos/getodk/validate/releases/275305335/assets{?name,label}", + "url": "https://api.github.com/repos/getodk/validate/releases/275305335", + "zipball_url": "https://api.github.com/repos/getodk/validate/zipball/v1.20.0" +} \ No newline at end of file diff --git a/tests/validators/data/latest_odk_old.json b/tests/validators/data/latest_odk_old.json new file mode 100644 index 00000000..ab633afe --- /dev/null +++ b/tests/validators/data/latest_odk_old.json @@ -0,0 +1,37 @@ +{ + "assets": [], + "assets_url": "https://api.github.com/repos/opendatakit/validate/releases/8424483/assets", + "author": { + "avatar_url": "https://avatars3.githubusercontent.com/u/32369?v=4", + "events_url": "https://api.github.com/users/yanokwa/events{/privacy}", + "followers_url": "https://api.github.com/users/yanokwa/followers", + "following_url": "https://api.github.com/users/yanokwa/following{/other_user}", + "gists_url": "https://api.github.com/users/yanokwa/gists{/gist_id}", + "gravatar_id": "", + "html_url": "https://github.com/yanokwa", + "id": 32369, + "login": "yanokwa", + "organizations_url": "https://api.github.com/users/yanokwa/orgs", + "received_events_url": "https://api.github.com/users/yanokwa/received_events", + "repos_url": "https://api.github.com/users/yanokwa/repos", + "site_admin": false, + "starred_url": "https://api.github.com/users/yanokwa/starred{/owner}{/repo}", + "subscriptions_url": "https://api.github.com/users/yanokwa/subscriptions", + "type": "User", + "url": "https://api.github.com/users/yanokwa" + }, + "body": "[All changes](https://github.com/opendatakit/collect/compare/v1.7.0...v1.8.0)\r\n\r\n**Highlights**\r\n* Upgrade to [JavaRosa v2.6.1](https://github.com/opendatakit/javarosa/releases/tag/v2.6.1)", + "created_at": "2017-11-07T23:47:15Z", + "draft": false, + "html_url": "https://github.com/opendatakit/validate/releases/tag/v1.8.0", + "id": 8424483, + "name": "ODK Validate v1.8.0", + "prerelease": false, + "published_at": "2017-11-07T23:47:37Z", + "tag_name": "v1.8.0", + "tarball_url": "https://api.github.com/repos/opendatakit/validate/tarball/v1.8.0", + "target_commitish": "master", + "upload_url": "https://uploads.github.com/repos/opendatakit/validate/releases/8424483/assets{?name,label}", + "url": "https://api.github.com/repos/opendatakit/validate/releases/8424483", + "zipball_url": "https://api.github.com/repos/opendatakit/validate/zipball/v1.8.0" +} \ No newline at end of file