diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index 02a02c774..f3666088b 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -9,17 +9,17 @@ jobs: runs-on: ${{ matrix.os }} strategy: matrix: - python: ['3.12'] + python: ['3.13'] 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: @@ -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 6f7131cac..d3451bd0d 100644 --- a/.github/workflows/verify.yml +++ b/.github/workflows/verify.yml @@ -7,17 +7,17 @@ jobs: runs-on: ${{ matrix.os }} strategy: matrix: - python: ['3.12'] + python: ['3.13'] 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: @@ -39,24 +39,24 @@ 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: - - 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: @@ -78,11 +78,11 @@ 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' - uses: actions/upload-artifact@v4 + uses: actions/upload-artifact@v7 with: name: pyxform--on-${{ matrix.os }}--py${{ matrix.python }} path: ${{ github.workspace }}/dist/pyxform* diff --git a/README.md b/README.md new file mode 100644 index 000000000..df9cf4765 --- /dev/null +++ b/README.md @@ -0,0 +1,210 @@ +# pyxform + +`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 + +`pyxform` is actively maintained by [ODK](https://getodk.org/about/team.html). + +Current goals for the project include: + +- 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 + +`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/). + +## Using `pyxform` + +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. + +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`. + +### Running the latest release of pyxform + +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: + + 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](https://github.com/pyenv/pyenv) to manage multiple versions of Python. + +### 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. + +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 + cd ~/repos/pyxform + git clone https://github.com/XLSForm/pyxform.git repo + + # Create and activate a virtual environment for the install. + /usr/local/bin/python -m venv venv + . venv/bin/activate + + # Install the pyxform and it's production dependencies. + (venv)$ cd repo + # If this doesn't work, upgrade pip ``pip install --upgrade pip`` and retry. + (venv)$ pip install -e . + (venv)$ python pyxform/xls2xform.py --help + (venv)$ xls2xform --help # same effect as previous line + (venv)$ which xls2xform # ~/repos/pyxform/venv/bin/xls2xform + +To leave and return to the virtualenv: + + (venv)$ deactivate # leave the venv, scripts not on $PATH + $ xls2xform --help + # -bash: xls2xform: command not found + $ . ~/repos/pyxform/venv/bin/activate # reactivate the venv + (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.: + + pip install git+https://github.com/XLSForm/pyxform.git@master#egg=pyxform + +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.: + + pip install -e .[dev] + +You can run tests with: + + python -m unittest + +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). + +### 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. + +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`. + +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). + +## Documentation + +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/. + +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/) + +## Change Log + +[Changelog](CHANGES.txt) + +## Releasing pyxform + +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 + +2. Run all tests through ODK Validate as follows: + + > 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 + +## 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: + + /usr/local/bin/python -m venv pyxform-release + . pyxform-release/bin/activate + +3. Install the production and packaging requirements: + + pip install -e . + pip install flit==3.12.0 + +4. Clean up build and dist folders: + + rm -rf build dist pyxform.egg-info + +5. Prepare `sdist` and `bdist_wheel` distributions, and publish to PyPI: + + flit --debug publish --no-use-vcs + +6. Tag the GitHub release and publish it. + +## Related projects + +These projects are not vetted or endorsed but are linked here for reference. + +**Converters** + +*To XLSForm* + +- [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](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](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 diff --git a/README.rst b/README.rst deleted file mode 100644 index 8c7d43cf7..000000000 --- a/README.rst +++ /dev/null @@ -1,217 +0,0 @@ -======== -pyxform -======== - -|pypi| |python| - -.. |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 - :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 `_. - -Project status -=============== -``pyxform`` is actively maintained by `ODK `_. - -Current goals for the project include: - -* Enable more complex workflows through sophisticated XPath expressions and `entities `_ -* Improve error messages and make troubleshooting easier -* Improve experience, particularly for multi-language forms - -``pyxform`` was started at the `Sustainable Engineering Lab at Columbia University `_, and until 2018 was maintained primarily by `Ona `_. - -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: - -* 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 -------------------------------------- -To convert forms at the command line, the latest official release of pyxform can be installed using `pip `_:: - - pip install pyxform - -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. - -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. - -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 - cd ~/repos/pyxform - git clone https://github.com/XLSForm/pyxform.git repo - - # Create and activate a virtual environment for the install. - /usr/local/bin/python -m venv venv - . venv/bin/activate - - # Install the pyxform and it's production dependencies. - (venv)$ cd repo - # If this doesn't work, upgrade pip ``pip install --upgrade pip`` and retry. - (venv)$ pip install -e . - (venv)$ python pyxform/xls2xform.py --help - (venv)$ xls2xform --help # same effect as previous line - (venv)$ which xls2xform # ~/repos/pyxform/venv/bin/xls2xform - -To leave and return to the virtualenv:: - - (venv)$ deactivate # leave the venv, scripts not on $PATH - $ xls2xform --help - # -bash: xls2xform: command not found - $ . ~/repos/pyxform/venv/bin/activate # reactivate the venv - (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.:: - - pip install git+https://github.com/XLSForm/pyxform.git@master#egg=pyxform - -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.:: - - pip install -e .[dev] - -You can run tests with:: - - python -m unittest - -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). - -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. - -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 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``. - -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). - -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: - -* `XLSForm docs `_ -* `XLSForm template `_ -* `ODK Docs `_ - -Change Log -========== -`Changelog `_ - -Releasing pyxform -================= - -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 - -2. Run all tests through ODK Validate as follows: - - 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 - -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:: - - /usr/local/bin/python -m venv pyxform-release - . pyxform-release/bin/activate - -3. Install the production and packaging requirements:: - - pip install -e . - pip install flit==3.9.0 - -4. Clean up build and dist folders:: - - rm -rf build dist pyxform.egg-info - -5. Prepare ``sdist`` and ``bdist_wheel`` distributions, and publish to PyPI:: - - flit --debug publish --no-use-vcs - -6. Tag the GitHub release and publish it. - -Related projects -================ - -These projects are not vetted or endorsed but are linked here for reference. - -**Converters** - -*To XLSForm* - -* `cueform `_ (Go): from CUE -* `md2xlsform `_ (Python): from MarkDown -* `xlsform `_ (Python): from JSON -* `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 - -**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 diff --git a/pyproject.toml b/pyproject.toml index 4afb9a166..2bc9ba8c5 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -5,8 +5,8 @@ authors = [ {name = "github.com/xlsform", email = "support@getodk.org"}, ] description = "A Python package to create XForms for ODK Collect." -readme = "README.rst" -requires-python = ">=3.10" +readme = "README.md" +requires-python = ">=3.11" dependencies = [ "xlrd==2.0.1", # Read XLS files "openpyxl==3.1.5", # Read XLSX files @@ -18,9 +18,9 @@ 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 - "ruff==0.12.4", # Format and lint + "lxml==6.1.1", # XPath test expressions + "psutil==7.2.2", # Process info for performance tests + "ruff==0.15.21", # Format and lint ] [project.urls] @@ -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" @@ -43,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" @@ -54,9 +55,12 @@ src = ["pyxform", "tests"] select = [ "B", # flake8-bugbear "C4", # flake8-comprehensions + "D", # pydocstyle "E", # pycodestyle error # "ERA", # eradicate (commented out code) "F", # pyflakes + "FLY", # flynt + "FURB", # refurb "I", # isort "PERF", # perflint "PIE", # flake8-pie @@ -72,8 +76,17 @@ 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.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) @@ -84,8 +97,10 @@ 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"]} + +[tool.ruff.lint.pydocstyle] +convention = "pep257" diff --git a/pyxform/builder.py b/pyxform/builder.py index 6a8b2c260..47ab4511e 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 @@ -68,7 +66,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. """ @@ -82,7 +80,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 +200,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 +244,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) @@ -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() @@ -380,7 +376,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/elements/action.py b/pyxform/elements/action.py index 97ccf3a3b..ce4557a67 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 9240d27f9..5573da5aa 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 6a619676b..859b3dd3b 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/entities/entity_declaration.py b/pyxform/entities/entity_declaration.py index 8f01c1840..b62aae69c 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/errors.py b/pyxform/errors.py index f8c71c296..ec84efa20 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 @@ -566,7 +564,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/external_instance.py b/pyxform/external_instance.py index 50301ddb5..130fd5911 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 430f40f88..7cc69fd4d 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 @@ -10,8 +8,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 @@ -32,10 +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 eb36f4de9..8823af052 100644 --- a/pyxform/instance.py +++ b/pyxform/instance.py @@ -1,6 +1,4 @@ -""" -SurveyInstance class module. -""" +"""SurveyInstance class module.""" import os.path @@ -58,7 +56,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 +71,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/expression.py b/pyxform/parsing/expression.py index 014d4e354..c43a5dacb 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? - """ + """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/parsing/parameters.py b/pyxform/parsing/parameters.py index 1696aead9..31812d99e 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 8343071a2..4242d7e33 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: - """ - [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: @@ -224,7 +222,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/question.py b/pyxform/question.py index f870d308f..cd00b8313 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 f3ee4e24c..2969623d1 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 552e1283c..1b710cc60 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) @@ -186,7 +180,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 +221,8 @@ def __init__( def xml_control(self, survey: "Survey"): """ + Example + ``` @@ -238,6 +234,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 3d89911ee..6fa23596b 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 @@ -29,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 ( @@ -70,7 +68,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) @@ -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 @@ -262,7 +258,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: @@ -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 @@ -397,7 +389,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: @@ -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() @@ -739,8 +725,8 @@ 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 + Set up the self._translations dict which will be referenced in the + setup media and itext functions. """ def get_choice_content(name, idx, choice): @@ -912,7 +898,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(): @@ -1033,17 +1019,15 @@ 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 def _in_secondary_instance_predicate() -> bool: """ - check if ${} expression represented by matchobj - is in a predicate for a path expression for a secondary instance + 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 @@ -1222,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 @@ -1245,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 @@ -1262,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. + + :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 - Return XForm XML string. + :returns: XForm XML string. """ # On Windows, NamedTemporaryFile must be opened exclusively. # So it must be explicitly created, opened, closed, and removed. @@ -1284,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/survey_element.py b/pyxform/survey_element.py index 668dba60c..c7bcf6fc0 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 @@ -279,7 +273,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 +288,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 +353,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): @@ -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 bfd99720b..9936d4f7a 100644 --- a/pyxform/translator.py +++ b/pyxform/translator.py @@ -1,6 +1,4 @@ -""" -Translator class module. -""" +"""Translator class module.""" from collections import defaultdict @@ -39,7 +37,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/util/enum.py b/pyxform/util/enum.py index 41bec2aef..980f2ab77 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/utils.py b/pyxform/utils.py index 6b16f870a..6cc730713 100644 --- a/pyxform/utils.py +++ b/pyxform/utils.py @@ -1,6 +1,4 @@ -""" -pyxform utils module. -""" +"""pyxform utils module.""" import copy import csv @@ -155,8 +153,8 @@ 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 + Dump a python nested array/dict structure to the specified file + or stdout if no file is specified. """ if path: with open(path, mode="w", encoding="utf-8") as fp: @@ -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 deleted file mode 100644 index becdeb91b..000000000 --- a/pyxform/validators/enketo_validate/__init__.py +++ /dev/null @@ -1,85 +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 642645e40..57cea5808 100644 --- a/pyxform/validators/error_cleaner.py +++ b/pyxform/validators/error_cleaner.py @@ -1,10 +1,10 @@ -""" -Cleans up error messages from the validators. -""" +"""Cleans up error messages from the validators.""" 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: @@ -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/odk_validate/__init__.py b/pyxform/validators/odk_validate/__init__.py index 14251a443..953154de1 100644 --- a/pyxform/validators/odk_validate/__init__.py +++ b/pyxform/validators/odk_validate/__init__.py @@ -1,7 +1,4 @@ -""" -odk_validate.py -A python wrapper around ODK Validate -""" +"""A python wrapper around ODK Validate.""" import logging import os @@ -40,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, @@ -55,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 a65481a1b..a968dd567 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 5bae8dacd..249b520f6 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/pyxform_reference.py b/pyxform/validators/pyxform/pyxform_reference.py index dc474e39c..484a81fef 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 @@ -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/pyxform/question_types/__init__.py b/pyxform/validators/pyxform/question_types/__init__.py index 8e24eca46..ac6fa10a5 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 66b2a964d..a4123296a 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 @@ -14,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" @@ -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) @@ -509,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( @@ -581,16 +536,14 @@ 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" "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" @@ -604,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/validators/util.py b/pyxform/validators/util.py index 6798dcc8b..5c91121c8 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 @@ -22,7 +20,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 +37,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 +82,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. @@ -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 edf2576bd..bb4d0b347 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,10 +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): @@ -80,7 +73,6 @@ def un_wrap(self): Recursively converts an XmlDictObject to a standard dictionary and returns the result. """ - return XmlDictObject._un_wrap(self) @@ -107,10 +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]) @@ -160,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) @@ -176,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) @@ -211,7 +196,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() @@ -654,19 +639,17 @@ 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): 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]: - """ - 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 = [] @@ -683,8 +666,8 @@ 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 '/' + """Given /xlsform_spec_test/launch, + return the string after the last occurance of the character '/'. """ pos = ref.rfind("/") if pos == -1: @@ -707,9 +690,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 diff --git a/pyxform/xform_instance_parser.py b/pyxform/xform_instance_parser.py index 427d64c98..19b6f63dc 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 bc091098d..7736dc4fb 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 @@ -111,7 +109,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 +213,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. @@ -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 306d53e0f..932f1abde 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 @@ -62,7 +60,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}] @@ -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: @@ -446,15 +438,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. @@ -631,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(): @@ -742,7 +735,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/pyxform/xls2xform.py b/pyxform/xls2xform.py index a768764a2..f91700d7d 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. @@ -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: @@ -150,9 +144,7 @@ def xls2xform_convert( def _create_parser(): - """ - Parse command line arguments. - """ + """Parse command line arguments.""" parser = argparse.ArgumentParser() parser.add_argument( "path_to_XLSForm", @@ -178,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", @@ -204,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 @@ -238,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 @@ -261,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/entities/test_create_repeat.py b/tests/entities/test_create_repeat.py index 226e0c110..00093b9df 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 926178067..2a6055a39 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 ea3ed09f3..c786583f7 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 @@ -112,7 +110,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 +365,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_area.py b/tests/test_area.py index 58ebf58fb..088145d27 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 651bb5e71..7e8c2cc10 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 2ff3d1110..9c62d6c46 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 706e0138f..07761db54 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 de0e069de..b50b32596 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 @@ -161,7 +159,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_choices_sheet.py b/tests/test_choices_sheet.py index 610b680dd..4f8b4e6e1 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 45702e513..0b63525a5 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 13ed2b982..fc6db9cb7 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.""" @@ -924,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 @@ -964,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_external_instances.py b/tests/test_external_instances.py index f9bb5abfe..7c866a0bd 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 | @@ -407,7 +401,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 +501,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 +519,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 +536,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 +579,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 +604,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 782330152..39b2cfe3e 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 """ @@ -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 @@ -145,7 +143,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 | @@ -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 | | | | @@ -307,7 +303,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 +342,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 d904807db..292aeb7d4 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 847f34f2b..c1609299e 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 8266235d1..08cf61033 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 e3f395307..8b10e596a 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,10 +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 6121ca1fc..bc022c9bf 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. @@ -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 6730abbe7..6e30d65b2 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 896a6953e..f87177e82 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 @@ -21,7 +19,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_image_app_parameter.py b/tests/test_image_app_parameter.py index b6fad7da5..1bfe93c86 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 7bb2f1733..531c30673 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 @@ -37,8 +35,9 @@ def test_survey_can_be_created_in_a_slightly_less_verbose_manner(self): self.assertEqual(expected_dict, s.to_json_dict()) def test_allow_surveys_with_comment_rows(self): - """assume that a survey with rows that don't have name, type, or label - headings raise warning only""" + """Assume that a survey with rows that don't have name, type, or label + headings raise warning only. + """ path = utils.path_to_text_fixture("allow_comment_rows_test.xls") survey = create_survey_from_xls(path) expected_dict = { diff --git a/tests/test_j2x_instantiation.py b/tests/test_j2x_instantiation.py index d7f928bcf..35b66e8b3 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 ed114b98a..3a1b4fc72 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 @@ -15,8 +13,8 @@ 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 + 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: control = control[0] @@ -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 bc639e64f..4ace585dd 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 b06bfe735..eb6869ab9 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 @@ -15,7 +13,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_language_warnings.py b/tests/test_language_warnings.py index 347122eec..270d3810e 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 5e0f6edb6..84c3c8031 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 48c61401a..22c043324 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 25c025eb9..8b74ec33b 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 ba9fc95ce..7da0570ae 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 1f52019a5..a6f363865 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 943cc3fea..2eed427f5 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_pyxformtestcase.py b/tests/test_pyxformtestcase.py index 89b4b3ea5..d467920f7 100644 --- a/tests/test_pyxformtestcase.py +++ b/tests/test_pyxformtestcase.py @@ -3,14 +3,14 @@ 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 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( @@ -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( @@ -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_randomize_itemsets.py b/tests/test_randomize_itemsets.py index e2d97e350..e9dbf25a8 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_range.py b/tests/test_range.py index 0f0b74b74..b586adab8 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_rank.py b/tests/test_rank.py index f0ed2b896..d55b8ef72 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 4c41f6096..463330892 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 | | | | | @@ -350,7 +344,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 | @@ -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 | @@ -399,7 +391,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 | @@ -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 | @@ -446,7 +436,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 | @@ -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 | @@ -926,7 +896,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 +923,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 +948,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 | | | | | @@ -1026,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 @@ -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 984f81296..396e4db10 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 | @@ -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 df83879d6..35858614b 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 c52196ed1..68469c6ee 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 c954901c7..f83bbef89 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 eec4a7c75..58e8e13cb 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 8f6d7ff82..4a6c3fa88 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 | | | | @@ -162,7 +154,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 | | | | @@ -205,16 +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 { @@ -229,10 +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( @@ -244,10 +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( @@ -263,10 +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( @@ -298,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 6bfa434be..c501255d1 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 d19911318..6ac7af3ff 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 879ee5827..d0d354eda 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 @@ -224,7 +220,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 """ @@ -409,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 diff --git a/tests/test_trigger.py b/tests/test_trigger.py index 1e5d17163..b02c91584 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 f57264d8b..89e60a132 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 664159bc6..b7771d089 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 92d0708c0..623daaf7a 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 4648f0cd4..d71b106a3 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 5aeb0283b..eafbc0056 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 @@ -11,7 +9,6 @@ from pyxform.errors import PyXFormError from pyxform.validators.updater import ( - EnketoValidateUpdater, _UpdateHandler, _UpdateInfo, capture_handler, @@ -93,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") @@ -108,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) @@ -128,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) @@ -174,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 @@ -184,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) @@ -197,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 @@ -206,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: @@ -214,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.""" @@ -225,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) @@ -243,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) @@ -251,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: @@ -263,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( @@ -293,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: @@ -304,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]) @@ -317,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( @@ -636,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 550ab0c02..c8fef24dd 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 @@ -74,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_validators.py b/tests/test_validators.py index 868bb449f..58328ee00 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 @@ -12,7 +10,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_warnings.py b/tests/test_warnings.py index 470efd534..a099ead39 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 29750455d..e7baa6efa 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 19b353f47..d474c71f5 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,14 +103,12 @@ 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 302f9d63f..98d48038d 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 | @@ -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 " @@ -602,7 +601,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_backends.py b/tests/test_xls2json_backends.py index 743f84224..32d4432f8 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 @@ -180,9 +176,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 +199,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) diff --git a/tests/test_xls2json_xls.py b/tests/test_xls2json_xls.py index 121ea2a26..fab479bd2 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", @@ -179,7 +175,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 aa725a363..180aec3c5 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 @@ -36,7 +34,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"]) @@ -85,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", "."]) @@ -103,52 +93,31 @@ 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""" + """Should deactivate both validators.""" raw_args = _create_parser().parse_args( [ "xlsform.xlsx", ".", "--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", @@ -158,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, ), ) @@ -166,7 +134,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() @@ -175,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( @@ -186,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, ), ) @@ -195,7 +161,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() @@ -204,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( @@ -215,21 +180,18 @@ 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, ), ) 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() 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)) @@ -240,9 +202,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 +236,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 7aa6c313e..f6cec54bd 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 @@ -27,10 +25,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) @@ -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/validators/data/latest_enketo.json b/tests/validators/data/latest_enketo.json deleted file mode 100644 index e75e11e27..000000000 --- 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 ab633afed..1dff576ec 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 000000000..196448d08 --- /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 000000000..ab633afed --- /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 diff --git a/tests/xform_test_case/test_bugs.py b/tests/xform_test_case/test_bugs.py index 83f326c93..cf117d2d0 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 @@ -42,7 +40,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 = [] @@ -132,7 +130,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.""" @@ -143,7 +142,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" @@ -152,7 +151,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" diff --git a/tests/xform_test_case/test_xform_conversion.py b/tests/xform_test_case/test_xform_conversion.py index 8e67451e7..62e7587e8 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 c7374bfae..4d52e32cf 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 b39907140..66bb4ccb5 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 08c5966a6..a96c2522c 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 a72929527..db6373817 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 c1a666113..612f93f57 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 498fe413e..e42de686c 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: