Skip to content

Commit

Permalink
chg: update supported python versions, deps, apply py3.10 formatting (#…
Browse files Browse the repository at this point in the history
…706)

- python 3.10, 3.11, 3.12 with 3.10 as the default
- update ruff and lxml
- formatting changes are generally one of the following:
  - line at top of file after module docstring
  - typing:
    - Union[x, y] -> x | y
    - imports from typing to use either builtins or stdlib
  - replace simple string subs "%s" with f-strings
    - many still remain due to use of common templates etc
  - explicitly state new default strict=False for zip, map
  - parenthesis allowed in with
  • Loading branch information
lindsay-stevens authored May 31, 2024
1 parent e25612e commit 1599d1f
Show file tree
Hide file tree
Showing 103 changed files with 389 additions and 311 deletions.
2 changes: 1 addition & 1 deletion .github/workflows/release.yml
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,7 @@ jobs:
runs-on: ${{ matrix.os }}
strategy:
matrix:
python: ['3.8']
python: ['3.10']
os: [ubuntu-latest]
steps:
- uses: actions/checkout@v4
Expand Down
4 changes: 2 additions & 2 deletions .github/workflows/verify.yml
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,7 @@ jobs:
runs-on: ${{ matrix.os }}
strategy:
matrix:
python: ['3.8']
python: ['3.10']
os: [ubuntu-latest]
steps:
- uses: actions/checkout@v4
Expand Down Expand Up @@ -39,7 +39,7 @@ jobs:
# Run all matrix jobs even if one of them fails.
fail-fast: false
matrix:
python: ['3.7', '3.8', '3.9']
python: ['3.10', '3.11', '3.12']
os: [ubuntu-latest, macos-latest, windows-latest]
include:
- os: windows-latest
Expand Down
15 changes: 6 additions & 9 deletions README.rst
Original file line number Diff line number Diff line change
Expand Up @@ -2,17 +2,14 @@
pyxform
========

|pypi| |python| |black|
|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.7,3.8,3.9-blue.svg
.. |python| image:: https://img.shields.io/badge/python-3.10,3.11,3.12-blue.svg
:target: https://www.python.org/downloads

.. |black| image:: https://img.shields.io/badge/code%20style-black-000000.svg
:target: https://github.com/python/black

``pyxform`` is a Python library that simplifies writing forms for ODK Collect and Enketo by converting spreadsheets that follow the `XLSForm standard <http://xlsform.org/>`_ into `ODK XForms <https://github.com/opendatakit/xforms-spec>`_. The XLSForms format is used in a `number of tools <http://xlsform.org/en/#tools-that-support-xlsforms>`_.

Project status
Expand Down Expand Up @@ -47,22 +44,22 @@ The ``xls2xform`` command can then be used::

xls2xform path_to_XLSForm [output_path]

The currently supported Python versions for ``pyxform`` are 3.7, 3.8 and 3.9.
The currently supported Python versions for ``pyxform`` are 3.10, 3.11 and 3.12.

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 <https://docs.python.org/3.8/tutorial/venv.html>`_ to make dependency management easier, and to keep the global site-packages directory clean::
From the command line, complete the following. These steps use a `virtualenv <https://docs.python.org/3.10/tutorial/venv.html>`_ 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/python3.8 -m venv venv
/usr/local/bin/python3.10 -m venv venv
. venv/bin/activate

# Install the pyxform and it's production dependencies.
Expand Down Expand Up @@ -156,7 +153,7 @@ Releases are now automatic. These instructions are provided for forks or for a f
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/python3.8 -m venv pyxform-release
/usr/local/bin/python3.10 -m venv pyxform-release
. pyxform-release/bin/activate

3. Install the production and packaging requirements::
Expand Down
8 changes: 4 additions & 4 deletions pyproject.toml
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@ authors = [
]
description = "A Python package to create XForms for ODK Collect."
readme = "README.rst"
requires-python = ">=3.7"
requires-python = ">=3.10"
dependencies = [
"xlrd==2.0.1", # Read XLS files
"openpyxl==3.1.2", # Read XLSX files
Expand All @@ -17,9 +17,9 @@ dependencies = [
# Install with `pip install pyxform[dev]`.
dev = [
"formencode==2.1.0", # Compare XML
"lxml==5.1.0", # XPath test expressions
"lxml==5.2.2", # XPath test expressions
"psutil==5.9.8", # Process info for performance tests
"ruff==0.2.1", # Format and lint
"ruff==0.4.5", # Format and lint
]

[project.urls]
Expand All @@ -42,7 +42,7 @@ exclude = ["docs", "tests"]

[tool.ruff]
line-length = 90
target-version = "py38"
target-version = "py310"
fix = true
show-fixes = true
output-format = "full"
Expand Down
1 change: 1 addition & 0 deletions pyxform/aliases.py
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@
Aliases for elements which could mean the same element in XForm but is represented
differently on the XLSForm.
"""

from pyxform import constants

# Aliases:
Expand Down
41 changes: 21 additions & 20 deletions pyxform/builder.py
Original file line number Diff line number Diff line change
@@ -1,10 +1,11 @@
"""
Survey builder functionality.
"""

import copy
import os
import re
from typing import TYPE_CHECKING, Any, Dict, List, Optional, Union
from typing import TYPE_CHECKING, Any, Union

from pyxform import constants as const
from pyxform import file_utils, utils
Expand Down Expand Up @@ -66,7 +67,7 @@ def copy_json_dict(json_dict):
items = json_dict.items()

for key, value in items:
if isinstance(value, (dict, list)):
if isinstance(value, dict | list):
json_dict_copy[key] = copy_json_dict(value)
else:
json_dict_copy[key] = value
Expand All @@ -79,13 +80,13 @@ def __init__(self, **kwargs):
# I don't know why we would need an explicit none option for
# select alls
self._add_none_option = False
self._sections: Optional[Dict[str, Dict]] = None
self._sections: dict[str, dict] | None = None
self.set_sections(kwargs.get("sections", {}))

# dictionary of setvalue target and value tuple indexed by triggering element
self.setvalues_by_triggering_ref = {}
# For tracking survey-level choices while recursing through the survey.
self._choices: Dict[str, Any] = {}
self._choices: dict[str, Any] = {}

def set_sections(self, sections):
"""
Expand All @@ -98,8 +99,8 @@ def set_sections(self, sections):
self._sections = sections

def create_survey_element_from_dict(
self, d: Dict[str, Any]
) -> Union["SurveyElement", List["SurveyElement"]]:
self, d: dict[str, Any]
) -> Union["SurveyElement", list["SurveyElement"]]:
"""
Convert from a nested python dictionary/array structure (a json dict I
call it because it corresponds directly with a json object)
Expand Down Expand Up @@ -161,10 +162,10 @@ def _save_trigger_as_setvalue_and_remove_calculate(self, d):

@staticmethod
def _create_question_from_dict(
d: Dict[str, Any],
question_type_dictionary: Dict[str, Any],
d: dict[str, Any],
question_type_dictionary: dict[str, Any],
add_none_option: bool = False,
) -> Union[Question, List[Question]]:
) -> Question | list[Question]:
question_type_str = d[const.TYPE]
d_copy = d.copy()

Expand Down Expand Up @@ -197,7 +198,7 @@ def _create_question_from_dict(
return []

@staticmethod
def _add_other_option_to_multiple_choice_question(d: Dict[str, Any]) -> None:
def _add_other_option_to_multiple_choice_question(d: dict[str, Any]) -> None:
# ideally, we'd just be pulling from children
choice_list = d.get(const.CHOICES, d.get(const.CHILDREN, []))
if len(choice_list) <= 0:
Expand All @@ -207,8 +208,8 @@ def _add_other_option_to_multiple_choice_question(d: Dict[str, Any]) -> None:

@staticmethod
def _get_or_other_choice(
choice_list: List[Dict[str, Any]],
) -> Dict[str, Union[str, Dict]]:
choice_list: list[dict[str, Any]],
) -> dict[str, str | dict]:
"""
If the choices have any translations, return an OR_OTHER choice for each lang.
"""
Expand Down Expand Up @@ -257,12 +258,12 @@ def _get_question_class(question_type_str, question_type_dictionary):
return QUESTION_CLASSES[control_tag]

@staticmethod
def _create_specify_other_question_from_dict(d: Dict[str, Any]) -> InputQuestion:
def _create_specify_other_question_from_dict(d: dict[str, Any]) -> InputQuestion:
kwargs = {
const.TYPE: "text",
const.NAME: "%s_other" % d[const.NAME],
const.NAME: f"{d[const.NAME]}_other",
const.LABEL: "Specify other.",
const.BIND: {"relevant": "selected(../%s, 'other')" % d[const.NAME]},
const.BIND: {"relevant": f"selected(../{d[const.NAME]}, 'other')"},
}
return InputQuestion(**kwargs)

Expand Down Expand Up @@ -386,11 +387,11 @@ def create_survey_from_xls(path_or_file, default_name=None):


def create_survey(
name_of_main_section: Optional[str] = None,
sections: Optional[Dict[str, Dict]] = None,
main_section: Optional[Dict[str, Any]] = None,
id_string: Optional[str] = None,
title: Optional[str] = None,
name_of_main_section: str | None = None,
sections: dict[str, dict] | None = None,
main_section: dict[str, Any] | None = None,
id_string: str | None = None,
title: str | None = None,
) -> Survey:
"""
name_of_main_section -- a string key used to find the main section in the
Expand Down
1 change: 1 addition & 0 deletions pyxform/constants.py
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@
the literal names can be easily changed, typos can be avoided, and references
are easier to find.
"""

# TODO: Replace matching strings in the json2xforms code (builder.py,
# survey.py, survey_element.py, question.py) with these constants
from pyxform.util.enum import StrEnum
Expand Down
10 changes: 5 additions & 5 deletions pyxform/entities/entities_parsing.py
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
from typing import Any, Dict, List
from typing import Any

from pyxform import constants as const
from pyxform.errors import PyXFormError
Expand All @@ -8,8 +8,8 @@


def get_entity_declaration(
entities_sheet: List[Dict], workbook_dict: Dict[str, List[Dict]], warnings: List[str]
) -> Dict[str, Any]:
entities_sheet: list[dict], workbook_dict: dict[str, list[dict]], warnings: list[str]
) -> dict[str, Any]:
if len(entities_sheet) == 0:
similar = find_sheet_misspellings(key=const.ENTITIES, keys=workbook_dict.keys())
if similar is not None:
Expand Down Expand Up @@ -82,7 +82,7 @@ def get_validated_dataset_name(entity):


def validate_entity_saveto(
row: Dict, row_number: int, entity_declaration: Dict[str, Any], in_repeat: bool
row: dict, row_number: int, entity_declaration: dict[str, Any], in_repeat: bool
):
save_to = row.get(const.BIND, {}).get("entities:saveto", "")
if not save_to:
Expand Down Expand Up @@ -124,7 +124,7 @@ def validate_entity_saveto(
)


def validate_entities_columns(row: Dict):
def validate_entities_columns(row: dict):
extra = {k: None for k in row.keys() if k not in EC.value_list()}
if 0 < len(extra):
fmt_extra = ", ".join(f"'{k}'" for k in extra.keys())
Expand Down
1 change: 1 addition & 0 deletions pyxform/external_instance.py
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
"""
ExternalInstance class module
"""

from pyxform.survey_element import SurveyElement


Expand Down
1 change: 1 addition & 0 deletions pyxform/file_utils.py
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
"""
The pyxform file utility functions.
"""

import glob
import os

Expand Down
3 changes: 2 additions & 1 deletion pyxform/instance.py
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
"""
SurveyInstance class module.
"""

from pyxform.errors import PyXFormError
from pyxform.xform_instance_parser import parse_xform_instance

Expand Down Expand Up @@ -58,7 +59,7 @@ def to_xml(self):
pumped out in order, etc)
"""
open_str = f"""<?xml version='1.0' ?><{self._name} id="{self._id}">"""
close_str = """</%s>""" % self._name
close_str = f"""</{self._name}>"""
vals = ""
for k, v in self._answers.items():
vals += f"<{k}>{v!s}</{k}>"
Expand Down
2 changes: 1 addition & 1 deletion pyxform/parsing/expression.py
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
from typing import Iterable
from collections.abc import Iterable

from pyxform.utils import parse_expression

Expand Down
6 changes: 3 additions & 3 deletions pyxform/parsing/instance_expression.py
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
import re
from typing import TYPE_CHECKING, List, Tuple
from typing import TYPE_CHECKING

from pyxform.utils import BRACKETED_TAG_REGEX, EXPRESSION_LEXER, ExpLexerToken

Expand All @@ -20,7 +20,7 @@ def instance_func_start(token: ExpLexerToken) -> bool:
return token.name == "FUNC_CALL" and token.value == "instance("


def find_boundaries(xml_text: str) -> List[Tuple[int, int]]:
def find_boundaries(xml_text: str) -> list[tuple[int, int]]:
"""
Find token boundaries of any instance() expression.
Expand Down Expand Up @@ -91,7 +91,7 @@ def find_boundaries(xml_text: str) -> List[Tuple[int, int]]:

# Pair up the boundaries [1, 2, 3, 4] -> [(1, 2), (3, 4)].
bounds = iter(boundaries)
pos_bounds = list(zip(bounds, bounds))
pos_bounds = list(zip(bounds, bounds, strict=False))
return pos_bounds


Expand Down
5 changes: 3 additions & 2 deletions pyxform/question.py
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
"""
XForm Survey element classes for different question types.
"""

import os.path

from pyxform.constants import (
Expand Down Expand Up @@ -32,7 +33,7 @@ def validate(self):
# make sure that the type of this question exists in the
# question type dictionary.
if self.type not in QUESTION_TYPE_DICT:
raise PyXFormError("Unknown question type '%s'." % self.type)
raise PyXFormError(f"Unknown question type '{self.type}'.")

def xml_instance(self, **kwargs):
survey = self.get_root()
Expand Down Expand Up @@ -74,7 +75,7 @@ def nest_setvalues(self, xml_node):
for setvalue in nested_setvalues:
setvalue_attrs = {
"ref": self.get_root()
.insert_xpaths("${%s}" % setvalue[0], self.get_root())
.insert_xpaths(f"${{{setvalue[0]}}}", self.get_root())
.strip(),
"event": "xforms-value-changed",
}
Expand Down
1 change: 1 addition & 0 deletions pyxform/question_type_dictionary.py
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
"""
XForm survey question type mapping dictionary module.
"""

from pyxform.xls2json import QuestionTypesReader, print_pyobj_to_json


Expand Down
1 change: 1 addition & 0 deletions pyxform/section.py
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
"""
Section survey element module.
"""

from pyxform.errors import PyXFormError
from pyxform.external_instance import ExternalInstance
from pyxform.survey_element import SurveyElement
Expand Down
Loading

0 comments on commit 1599d1f

Please sign in to comment.