diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml
index a3f9e939..191c0de2 100644
--- a/.github/workflows/release.yml
+++ b/.github/workflows/release.yml
@@ -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
diff --git a/.github/workflows/verify.yml b/.github/workflows/verify.yml
index f8648b10..87721363 100644
--- a/.github/workflows/verify.yml
+++ b/.github/workflows/verify.yml
@@ -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
@@ -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
diff --git a/README.rst b/README.rst
index 271e4686..eac4c64b 100644
--- a/README.rst
+++ b/README.rst
@@ -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 `_ into `ODK XForms `_. The XLSForms format is used in a `number of tools `_.
Project status
@@ -47,14 +44,14 @@ 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 `_ to make dependency management easier, and to keep the global site-packages directory clean::
+From the command line, complete the following. These steps use a `virtualenv `_ to make dependency management easier, and to keep the global site-packages directory clean::
# Get a copy of the repository.
mkdir -P ~/repos/pyxform
@@ -62,7 +59,7 @@ From the command line, complete the following. These steps use a `virtualenv =3.7"
+requires-python = ">=3.10"
dependencies = [
"xlrd==2.0.1", # Read XLS files
"openpyxl==3.1.2", # Read XLSX files
@@ -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]
@@ -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"
diff --git a/pyxform/aliases.py b/pyxform/aliases.py
index 480cd2f4..17cfa5c6 100644
--- a/pyxform/aliases.py
+++ b/pyxform/aliases.py
@@ -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:
diff --git a/pyxform/builder.py b/pyxform/builder.py
index 1b8ecb58..6675df70 100644
--- a/pyxform/builder.py
+++ b/pyxform/builder.py
@@ -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
@@ -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
@@ -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):
"""
@@ -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)
@@ -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()
@@ -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:
@@ -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.
"""
@@ -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)
@@ -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
diff --git a/pyxform/constants.py b/pyxform/constants.py
index 3759cf12..6bd41cc6 100644
--- a/pyxform/constants.py
+++ b/pyxform/constants.py
@@ -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
diff --git a/pyxform/entities/entities_parsing.py b/pyxform/entities/entities_parsing.py
index 948d61db..34853797 100644
--- a/pyxform/entities/entities_parsing.py
+++ b/pyxform/entities/entities_parsing.py
@@ -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
@@ -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:
@@ -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:
@@ -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())
diff --git a/pyxform/external_instance.py b/pyxform/external_instance.py
index c90f06c2..50ea4a70 100644
--- a/pyxform/external_instance.py
+++ b/pyxform/external_instance.py
@@ -1,6 +1,7 @@
"""
ExternalInstance class module
"""
+
from pyxform.survey_element import SurveyElement
diff --git a/pyxform/file_utils.py b/pyxform/file_utils.py
index 862ef115..430f40f8 100644
--- a/pyxform/file_utils.py
+++ b/pyxform/file_utils.py
@@ -1,6 +1,7 @@
"""
The pyxform file utility functions.
"""
+
import glob
import os
diff --git a/pyxform/instance.py b/pyxform/instance.py
index 98a60087..fb427af2 100644
--- a/pyxform/instance.py
+++ b/pyxform/instance.py
@@ -1,6 +1,7 @@
"""
SurveyInstance class module.
"""
+
from pyxform.errors import PyXFormError
from pyxform.xform_instance_parser import parse_xform_instance
@@ -58,7 +59,7 @@ def to_xml(self):
pumped out in order, etc)
"""
open_str = f"""<{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}>"
diff --git a/pyxform/parsing/expression.py b/pyxform/parsing/expression.py
index edab5371..de99b66b 100644
--- a/pyxform/parsing/expression.py
+++ b/pyxform/parsing/expression.py
@@ -1,4 +1,4 @@
-from typing import Iterable
+from collections.abc import Iterable
from pyxform.utils import parse_expression
diff --git a/pyxform/parsing/instance_expression.py b/pyxform/parsing/instance_expression.py
index 9b908e0f..5a913c37 100644
--- a/pyxform/parsing/instance_expression.py
+++ b/pyxform/parsing/instance_expression.py
@@ -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
@@ -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.
@@ -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
diff --git a/pyxform/question.py b/pyxform/question.py
index 684b4097..9c4810c6 100644
--- a/pyxform/question.py
+++ b/pyxform/question.py
@@ -1,6 +1,7 @@
"""
XForm Survey element classes for different question types.
"""
+
import os.path
from pyxform.constants import (
@@ -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()
@@ -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",
}
diff --git a/pyxform/question_type_dictionary.py b/pyxform/question_type_dictionary.py
index 84f35fbf..669504f1 100644
--- a/pyxform/question_type_dictionary.py
+++ b/pyxform/question_type_dictionary.py
@@ -1,6 +1,7 @@
"""
XForm survey question type mapping dictionary module.
"""
+
from pyxform.xls2json import QuestionTypesReader, print_pyobj_to_json
diff --git a/pyxform/section.py b/pyxform/section.py
index 30f467cc..a52ea9e9 100644
--- a/pyxform/section.py
+++ b/pyxform/section.py
@@ -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
diff --git a/pyxform/survey.py b/pyxform/survey.py
index 2c1cc996..3a2b8e15 100644
--- a/pyxform/survey.py
+++ b/pyxform/survey.py
@@ -1,14 +1,15 @@
"""
Survey module with XForm Survey objects and utility functions.
"""
+
import os
import re
import tempfile
import xml.etree.ElementTree as ETree
from collections import defaultdict
+from collections.abc import Generator, Iterator
from datetime import datetime
from functools import lru_cache
-from typing import Generator, Iterator, List, Optional, Tuple
from pyxform import aliases, constants
from pyxform.constants import EXTERNAL_INSTANCE_EXTENSIONS, NSMAP
@@ -50,16 +51,16 @@ class InstanceInfo:
def __init__(
self,
type: str,
- context: Optional[str],
+ context: str | None,
name: str,
- src: Optional[str],
+ src: str | None,
instance: "DetachableElement",
):
self.type: str = type
- self.context: Optional[str] = context
+ self.context: str | None = context
self.name: str = name
- self.src: Optional[str] = src
- self.instance: "DetachableElement" = instance
+ self.src: str | None = src
+ self.instance: DetachableElement = instance
def register_nsmap():
@@ -232,15 +233,14 @@ def _validate_uniqueness_of_section_names(self):
if element.name == root_node_name:
# The root node name is rarely explictly set; explain
# the problem in a more helpful way (#510)
- raise PyXFormError(
- 'The name "%s" is the same as the form name. '
- "Use a different section name "
- '(or change the form name in the "name" column of '
- "the settings sheet)." % element.name
+ msg = (
+ f"The name '{element.name}' is the same as the form name. "
+ "Use a different section name (or change the form name in "
+ "the 'name' column of the settings sheet)."
)
- raise PyXFormError(
- "There are two sections with the name %s." % element.name
- )
+ raise PyXFormError(msg)
+ msg = f"There are two sections with the name {element.name}."
+ raise PyXFormError(msg)
section_names.append(element.name)
def get_nsmap(self):
@@ -297,7 +297,7 @@ def xml(self):
)
def get_setvalues_for_question_name(self, question_name):
- return self.setvalues_by_triggering_ref.get("${%s}" % question_name)
+ return self.setvalues_by_triggering_ref.get(f"${{{question_name}}}")
def _generate_static_instances(self, list_name, choice_list) -> InstanceInfo:
"""
@@ -348,7 +348,7 @@ def _generate_static_instances(self, list_name, choice_list) -> InstanceInfo:
)
@staticmethod
- def _generate_external_instances(element) -> Optional[InstanceInfo]:
+ def _generate_external_instances(element) -> InstanceInfo | None:
if isinstance(element, ExternalInstance):
name = element["name"]
extension = element["type"].split("-")[0]
@@ -394,7 +394,7 @@ def _validate_external_instances(instances) -> None:
raise ValidationError("\n".join(errors))
@staticmethod
- def _generate_pulldata_instances(element) -> Optional[List[InstanceInfo]]:
+ def _generate_pulldata_instances(element) -> list[InstanceInfo] | None:
def get_pulldata_functions(element):
"""
Returns a list of different pulldata(... function strings if
@@ -444,7 +444,7 @@ def get_instance_info(element, file_id):
return None
@staticmethod
- def _generate_from_file_instances(element) -> Optional[InstanceInfo]:
+ def _generate_from_file_instances(element) -> InstanceInfo | None:
itemset = element.get("itemset")
file_id, ext = os.path.splitext(itemset)
if itemset and ext in EXTERNAL_INSTANCE_EXTENSIONS:
@@ -586,13 +586,13 @@ def xml_model(self):
entity_features = getattr(self, constants.ENTITY_FEATURES, [])
if len(entity_features) > 0:
if "update" in entity_features:
- model_kwargs[
- "entities:entities-version"
- ] = constants.CURRENT_ENTITIES_VERSION
+ model_kwargs["entities:entities-version"] = (
+ constants.CURRENT_ENTITIES_VERSION
+ )
else:
- model_kwargs[
- "entities:entities-version"
- ] = constants.ENTITIES_CREATE_VERSION
+ model_kwargs["entities:entities-version"] = (
+ constants.ENTITIES_CREATE_VERSION
+ )
model_children = []
if self._translations:
@@ -701,7 +701,7 @@ def _setup_translations(self):
def _setup_choice_translations(
name, choice_value, itext_id
- ) -> Generator[Tuple[List[str], str], None, None]:
+ ) -> Generator[tuple[list[str], str], None, None]:
for media_or_lang, value in choice_value.items():
if isinstance(value, dict):
for language, val in value.items():
@@ -981,12 +981,12 @@ def __repr__(self):
return self.__unicode__()
def __unicode__(self):
- return "" % hex(id(self))
+ return f""
def _setup_xpath_dictionary(self):
self._xpath = {} # pylint: disable=attribute-defined-outside-init
for element in self.iter_descendants():
- if isinstance(element, (Question, Section)):
+ if isinstance(element, Question | Section):
if element.name in self._xpath:
self._xpath[element.name] = None
else:
@@ -1022,7 +1022,7 @@ def _in_secondary_instance_predicate() -> bool:
return False
return False
- def _relative_path(ref_name: str, _use_current: bool) -> Optional[str]:
+ def _relative_path(ref_name: str, _use_current: bool) -> str | None:
"""Given name in ${name}, return relative xpath to ${name}."""
return_path = None
xpath = self._xpath[ref_name]
@@ -1038,7 +1038,7 @@ def _relative_path(ref_name: str, _use_current: bool) -> Optional[str]:
self, xpath, context_xpath, reference_parent
)
if steps:
- ref_path = ref_path if ref_path.endswith(ref_name) else "/%s" % name
+ ref_path = ref_path if ref_path.endswith(ref_name) else f"/{name}"
prefix = " current()/" if _use_current else " "
return_path = prefix + "/".join([".."] * steps) + ref_path + " "
@@ -1135,8 +1135,8 @@ def _var_repl_output_function(self, matchobj, context):
def insert_output_values(
self,
text: str,
- context: Optional[SurveyElement] = None,
- ) -> Tuple[str, bool]:
+ context: SurveyElement | None = None,
+ ) -> tuple[str, bool]:
"""
Replace all the ${variables} in text with xpaths.
Returns that and a boolean indicating if there were any ${variables}
diff --git a/pyxform/survey_element.py b/pyxform/survey_element.py
index 490a4449..504e83d0 100644
--- a/pyxform/survey_element.py
+++ b/pyxform/survey_element.py
@@ -1,11 +1,12 @@
"""
Survey Element base class for all survey elements.
"""
+
import json
import re
from collections import deque
from functools import lru_cache
-from typing import TYPE_CHECKING, Any, ClassVar, Dict, List
+from typing import TYPE_CHECKING, Any, ClassVar
from pyxform import aliases as alias
from pyxform import constants as const
@@ -82,7 +83,7 @@ class SurveyElement(dict):
"""
__name__ = "SurveyElement"
- FIELDS: ClassVar[Dict[str, Any]] = FIELDS.copy()
+ FIELDS: ClassVar[dict[str, Any]] = FIELDS.copy()
def _default(self):
# TODO: need way to override question type dictionary
@@ -349,7 +350,7 @@ def get_media_keys(self):
@deprected
I'm leaving this in just in case it has outside references.
"""
- return {"media": "%s:media" % self.get_xpath()}
+ return {"media": f"{self.get_xpath()}:media"}
def needs_itext_ref(self):
return isinstance(self.label, dict) or (
@@ -378,7 +379,7 @@ def xml_label(self):
if self.needs_itext_ref():
# If there is a dictionary label, or non-empty media dict,
# then we need to make a label with an itext ref
- ref = "jr:itext('%s')" % self._translation_path("label")
+ ref = f"""jr:itext('{self._translation_path("label")}')"""
return node("label", ref=ref)
else:
survey = self.get_root()
@@ -388,12 +389,12 @@ def xml_label(self):
def xml_hint(self):
if isinstance(self.hint, dict) or self.guidance_hint:
path = self._translation_path("hint")
- return node("hint", ref="jr:itext('%s')" % path)
+ return node("hint", ref=f"jr:itext('{path}')")
else:
hint, output_inserted = self.get_root().insert_output_values(self.hint, self)
return node("hint", hint, toParseString=output_inserted)
- def xml_label_and_hint(self) -> List["DetachableElement"]:
+ def xml_label_and_hint(self) -> list["DetachableElement"]:
"""
Return a list containing one node for the label and if there
is a hint one node for the hint.
@@ -409,7 +410,7 @@ def xml_label_and_hint(self) -> List["DetachableElement"]:
result.append(self.xml_label())
result.append(self.xml_hint())
- msg = "The survey element named '%s' has no label or hint." % self.name
+ msg = f"The survey element named '{self.name}' has no label or hint."
if len(result) == 0:
raise PyXFormError(msg)
@@ -451,13 +452,13 @@ def xml_bindings(self):
if k == "jr:constraintMsg" and (
isinstance(v, dict) or re.search(BRACKETED_TAG_REGEX, v)
):
- v = "jr:itext('%s')" % self._translation_path("jr:constraintMsg")
+ v = f"""jr:itext('{self._translation_path("jr:constraintMsg")}')"""
if k == "jr:requiredMsg" and (
isinstance(v, dict) or re.search(BRACKETED_TAG_REGEX, v)
):
- v = "jr:itext('%s')" % self._translation_path("jr:requiredMsg")
+ v = f"""jr:itext('{self._translation_path("jr:requiredMsg")}')"""
if k == "jr:noAppErrorString" and isinstance(v, dict):
- v = "jr:itext('%s')" % self._translation_path("jr:noAppErrorString")
+ v = f"""jr:itext('{self._translation_path("jr:noAppErrorString")}')"""
bind_dict[k] = survey.insert_xpaths(v, context=self)
return [node("bind", nodeset=self.get_xpath(), **bind_dict)]
return None
diff --git a/pyxform/translator.py b/pyxform/translator.py
index e8556496..bfd99720 100644
--- a/pyxform/translator.py
+++ b/pyxform/translator.py
@@ -1,6 +1,7 @@
"""
Translator class module.
"""
+
from collections import defaultdict
diff --git a/pyxform/utils.py b/pyxform/utils.py
index d6f34f21..a29b2d6c 100644
--- a/pyxform/utils.py
+++ b/pyxform/utils.py
@@ -1,13 +1,14 @@
"""
pyxform utils module.
"""
+
import copy
import csv
import json
import os
import re
from json.decoder import JSONDecodeError
-from typing import Dict, List, NamedTuple, Tuple
+from typing import NamedTuple
from xml.dom import Node
from xml.dom.minidom import Element, Text, _write_data
@@ -53,7 +54,7 @@ def writexml(self, writer, indent="", addindent="", newl=""):
attrs = self._get_attributes()
for a_name in attrs.keys():
- writer.write(' %s="' % a_name)
+ writer.write(f' {a_name}="')
_write_data(writer, attrs[a_name].value)
writer.write('"')
if self.childNodes:
@@ -136,7 +137,7 @@ def node(*args, **kwargs) -> DetachableElement:
text_node.data = unicode_args[0]
result.appendChild(text_node)
for n in args:
- if isinstance(n, (int, float, bytes)):
+ if isinstance(n, int | float | bytes):
text_node = PatchedText()
text_node.data = str(n)
result.appendChild(text_node)
@@ -187,7 +188,7 @@ def xls_sheet_to_csv(workbook_path, csv_path, sheet_name):
for row_idx in range(sheet.nrows):
csv_data = []
try:
- for v, m in zip(sheet.row(row_idx), mask):
+ for v, m in zip(sheet.row(row_idx), mask, strict=False):
if m:
value = v.value
value_type = v.ctype
@@ -215,7 +216,7 @@ def xlsx_sheet_to_csv(workbook_path, csv_path, sheet_name):
for row in sheet.rows:
csv_data = []
try:
- for v, m in zip(row, mask):
+ for v, m in zip(row, mask, strict=False):
if m:
data = xlsx_value_to_str(v.value)
# clean the values of leading and trailing whitespaces
@@ -299,7 +300,7 @@ def default_is_dynamic(element_default, element_type=None):
return False
-def has_dynamic_label(choice_list: "List[Dict[str, str]]") -> bool:
+def has_dynamic_label(choice_list: "list[dict[str, str]]") -> bool:
"""
If the first or second choice label includes a reference, we must use itext.
@@ -442,7 +443,7 @@ class ExpLexerToken(NamedTuple):
EXPRESSION_LEXER = get_expression_lexer()
-def parse_expression(text: str) -> Tuple[List[ExpLexerToken], str]:
+def parse_expression(text: str) -> tuple[list[ExpLexerToken], str]:
"""
Parse a "default" expression, well enough to identify dynamic defaults vs. not.
diff --git a/pyxform/validators/enketo_validate/__init__.py b/pyxform/validators/enketo_validate/__init__.py
index ef2935da..becdeb91 100644
--- a/pyxform/validators/enketo_validate/__init__.py
+++ b/pyxform/validators/enketo_validate/__init__.py
@@ -1,6 +1,7 @@
"""
Validate XForms using Enketo validator.
"""
+
import os
from typing import TYPE_CHECKING
diff --git a/pyxform/validators/error_cleaner.py b/pyxform/validators/error_cleaner.py
index f837f150..8305780c 100644
--- a/pyxform/validators/error_cleaner.py
+++ b/pyxform/validators/error_cleaner.py
@@ -1,6 +1,7 @@
"""
Cleans up error messages from the validators.
"""
+
import re
@@ -17,7 +18,7 @@ def _replace_xpath_with_tokens(match):
) or strmatch.endswith("/item/value"):
return strmatch
line = match.group().split("/")
- return "${%s}" % line[len(line) - 1]
+ return f"${{{line[len(line) - 1]}}}"
@staticmethod
def _cleanup_errors(error_message):
diff --git a/pyxform/validators/odk_validate/__init__.py b/pyxform/validators/odk_validate/__init__.py
index 9616fafb..14251a44 100644
--- a/pyxform/validators/odk_validate/__init__.py
+++ b/pyxform/validators/odk_validate/__init__.py
@@ -2,6 +2,7 @@
odk_validate.py
A python wrapper around ODK Validate
"""
+
import logging
import os
import shutil
diff --git a/pyxform/validators/pyxform/android_package_name.py b/pyxform/validators/pyxform/android_package_name.py
index b1c0cd03..643a339d 100644
--- a/pyxform/validators/pyxform/android_package_name.py
+++ b/pyxform/validators/pyxform/android_package_name.py
@@ -1,10 +1,9 @@
import re
-from typing import Optional
PACKAGE_NAME_REGEX = re.compile(r"[^a-zA-Z0-9._]")
-def validate_android_package_name(name: str) -> Optional[str]:
+def validate_android_package_name(name: str) -> str | None:
prefix = "Parameter 'app' has an invalid Android package name - "
if not name.strip():
diff --git a/pyxform/validators/pyxform/parameters_generic.py b/pyxform/validators/pyxform/parameters_generic.py
index c855e422..cd95843d 100644
--- a/pyxform/validators/pyxform/parameters_generic.py
+++ b/pyxform/validators/pyxform/parameters_generic.py
@@ -1,8 +1,9 @@
-from typing import Any, Dict, Sequence
+from collections.abc import Sequence
+from typing import Any
from pyxform.errors import PyXFormError
-PARAMETERS_TYPE = Dict[str, Any]
+PARAMETERS_TYPE = dict[str, Any]
# Label and value are used to match against user-specified files so case should be preserved.
CASE_SENSITIVE_VALUES = ["label", "value"]
@@ -32,7 +33,7 @@ def parse(raw_parameters: str) -> PARAMETERS_TYPE:
def validate(
parameters: PARAMETERS_TYPE,
allowed: Sequence[str],
-) -> Dict[str, str]:
+) -> dict[str, str]:
"""
Raise an error if 'parameters' includes any keys not named in 'allowed'.
"""
diff --git a/pyxform/validators/pyxform/translations_checks.py b/pyxform/validators/pyxform/translations_checks.py
index 01121d76..588814a6 100644
--- a/pyxform/validators/pyxform/translations_checks.py
+++ b/pyxform/validators/pyxform/translations_checks.py
@@ -6,10 +6,10 @@
from pyxform.errors import PyXFormError
if TYPE_CHECKING:
- from typing import Dict, List, Optional, Sequence, Set, Tuple
+ from collections.abc import Sequence
- SheetData = Tuple[Tuple[str, ...]]
- Warnings = List[str]
+ SheetData = tuple[tuple[str, ...]]
+ Warnings = list[str]
OR_OTHER_WARNING = (
@@ -20,8 +20,8 @@
def format_missing_translations_msg(
- _in: "Dict[str, Dict[str, Sequence]]",
-) -> "Optional[str]":
+ _in: "dict[str, dict[str, Sequence]]",
+) -> str | None:
"""
Format the missing translations data into a warning message.
@@ -76,22 +76,22 @@ class Translations:
def __init__(
self,
sheet_data: "SheetData",
- translatable_columns: "Dict[str, str]",
+ translatable_columns: dict[str, str],
):
"""
:param sheet_data: The survey or choices sheet data.
:param translatable_columns: The translatable columns for a sheet. The structure
should be Dict[internal_name, external_name]. See the aliases module.
"""
- self.seen: "defaultdict[str, List[str]]" = defaultdict(list)
- self.columns_seen: "Set[str]" = set()
- self.missing: "defaultdict[str, List[str]]" = defaultdict(list)
+ self.seen: defaultdict[str, list[str]] = defaultdict(list)
+ self.columns_seen: set[str] = set()
+ self.missing: defaultdict[str, list[str]] = defaultdict(list)
self._find_translations(sheet_data, translatable_columns)
self._find_missing()
def _find_translations(
- self, sheet_data: "SheetData", translatable_columns: "Dict[str, str]"
+ self, sheet_data: "SheetData", translatable_columns: dict[str, str]
):
def process_header(head):
if head[0] in translatable_columns.keys():
@@ -134,11 +134,11 @@ def __init__(
:param survey_sheet: The survey sheet data.
:param choices_sheet: The choices sheet data.
"""
- self.survey: "Translations" = Translations(
+ self.survey: Translations = Translations(
sheet_data=survey_sheet,
translatable_columns=aliases.TRANSLATABLE_SURVEY_COLUMNS,
)
- self.choices: "Translations" = Translations(
+ self.choices: Translations = Translations(
sheet_data=choices_sheet,
translatable_columns=aliases.TRANSLATABLE_CHOICES_COLUMNS,
)
diff --git a/pyxform/validators/updater.py b/pyxform/validators/updater.py
index 6cefc099..adb2a7d0 100644
--- a/pyxform/validators/updater.py
+++ b/pyxform/validators/updater.py
@@ -1,6 +1,7 @@
"""
pyxform_validator_update - command to update XForm validators.
"""
+
import argparse
import fnmatch
import json
@@ -232,10 +233,8 @@ def _find_download_url(update_info, json_data, file_name):
)
elif 1 < urls_len:
raise PyXFormError(
- "{c} files with the name '{n}' attached to release '{r}'."
- "\n\n{h}".format(
- c=urls_len, n=file_name, r=rel_name, h=update_info.manual_msg
- )
+ f"{urls_len} files with the name '{file_name}' attached to release '{rel_name}'."
+ f"\n\n{update_info.manual_msg}"
)
else:
return file_urls[0]
@@ -267,9 +266,7 @@ def _get_bin_paths(update_info, file_path):
main_bin = "*validate"
else:
raise PyXFormError(
- "Did not find a supported main binary for file: {p}.\n\n{h}".format(
- p=file_path, h=update_info.manual_msg
- )
+ f"Did not find a supported main binary for file: {file_path}.\n\n{update_info.manual_msg}"
)
return [
(main_bin, update_info.validator_basename),
diff --git a/pyxform/validators/util.py b/pyxform/validators/util.py
index ce09682c..835150da 100644
--- a/pyxform/validators/util.py
+++ b/pyxform/validators/util.py
@@ -1,6 +1,7 @@
"""
The validators utility functions.
"""
+
import logging
import os
import signal
@@ -10,7 +11,7 @@
import time
from contextlib import closing
from subprocess import PIPE, Popen
-from typing import Dict, List, NamedTuple
+from typing import NamedTuple
from urllib.error import HTTPError, URLError
from urllib.request import Request, urlopen
@@ -75,7 +76,7 @@ def _kill_process_after_a_timeout(pid):
watchdog.start()
(stdout, stderr) = p.communicate()
watchdog.cancel() # if it's still waiting to run
- timeout = kill_check.isSet()
+ timeout = kill_check.is_set()
kill_check.clear()
return PopenResult(
return_code=p.returncode, timeout=timeout, stdout=stdout, stderr=stderr
@@ -129,8 +130,8 @@ def request_get(url):
class _LoggingWatcher(NamedTuple):
- records: List
- output: Dict
+ records: list
+ output: dict
class CapturingHandler(logging.Handler):
diff --git a/pyxform/xform2json.py b/pyxform/xform2json.py
index caa72a5b..e2011e6c 100644
--- a/pyxform/xform2json.py
+++ b/pyxform/xform2json.py
@@ -1,13 +1,14 @@
"""
xform2json module - Transform an XForm to a JSON dictionary.
"""
+
import copy
import json
import logging
import re
from collections.abc import Mapping
from operator import itemgetter
-from typing import Any, Dict, List
+from typing import Any
from xml.etree.ElementTree import Element
from defusedxml.ElementTree import ParseError, XMLParser, fromstring, parse
@@ -200,7 +201,7 @@ def __init__(self, root):
def get_dict(self):
json_str = json.dumps(self._dict)
for uri in NSMAP.values():
- json_str = json_str.replace("{%s}" % uri, "")
+ json_str = json_str.replace(f"{{{uri}}}", "")
return json.loads(json_str)
@@ -325,7 +326,7 @@ def _cleanup_bind_list(self):
for item in self._bind_list:
ref = item["nodeset"]
name = self._get_name_from_ref(ref)
- parent_ref = ref[: ref.find("/%s" % name)]
+ parent_ref = ref[: ref.find(f"/{name}")]
question = self._get_question_params_from_bindings(ref)
question["name"] = name
question["__order"] = self._get_question_order(ref)
@@ -562,7 +563,7 @@ def _get_question_type(question_type):
return QUESTION_TYPES[question_type]
return question_type
- def _get_translations(self) -> List[Dict]:
+ def _get_translations(self) -> list[dict]:
if "itext" not in self.model:
return []
if "translation" not in self.model["itext"]:
@@ -617,7 +618,7 @@ def _get_text_from_translation(self, ref, key="label"):
if value["form"] == "image":
v = v.replace("jr://images/", "")
else:
- v = v.replace("jr://%s/" % value["form"], "")
+ v = v.replace(f"jr://{value['form']}/", "")
if v == "-": # skip blank
continue
text = {value["form"]: v}
@@ -630,7 +631,7 @@ def _get_text_from_translation(self, ref, key="label"):
if m_type == "image":
v = v.replace("jr://images/", "")
else:
- v = v.replace("jr://%s/" % m_type, "")
+ v = v.replace(f"jr://{m_type}/", "")
if v == "-":
continue
if k not in label:
@@ -664,7 +665,7 @@ def _get_constraint_msg(self, constraint_msg):
k, constraint_msg = self._get_text_from_translation(ref)
return constraint_msg
- def _get_choices(self) -> Dict[str, Any]:
+ def _get_choices(self) -> dict[str, Any]:
"""
Get all form choices, using the model/instance and model/itext.
"""
@@ -704,7 +705,7 @@ def get_last_item(xpath_str):
return last_item[len(last_item) - 1].strip()
def replace_function(match):
- return "${%s}" % get_last_item(match.group())
+ return f"${{{get_last_item(match.group())}}}"
# moving re flags into compile for python 2.6 compat
pattern = "( /[a-z0-9-_]+(?:/[a-z0-9-_]+)+ )"
diff --git a/pyxform/xform_instance_parser.py b/pyxform/xform_instance_parser.py
index 7c41dc1d..427d64c9 100644
--- a/pyxform/xform_instance_parser.py
+++ b/pyxform/xform_instance_parser.py
@@ -64,7 +64,7 @@ def _flatten_dict(d, prefix):
# implemented that [0] should be the first node, but
# according to the W3C standard it should have been
# [1]. I'm adding 1 to i to start at 1.
- item_prefix[-1] += "[%s]" % str(i + 1)
+ item_prefix[-1] += f"[{i + 1}]"
if isinstance(item, dict):
for pair in _flatten_dict(item, item_prefix):
yield pair
diff --git a/pyxform/xls2json.py b/pyxform/xls2json.py
index d4ea1a64..ad25baa6 100644
--- a/pyxform/xls2json.py
+++ b/pyxform/xls2json.py
@@ -1,12 +1,13 @@
"""
A Python script to convert excel files into JSON.
"""
+
import json
import os
import re
import sys
from collections import Counter
-from typing import IO, Any, Dict, List, Optional, Tuple
+from typing import IO, Any
from pyxform import aliases, constants
from pyxform.constants import (
@@ -100,18 +101,18 @@ def replace_smart_quotes_in_dict(_d):
class DealiasAndGroupHeadersResult:
__slots__ = ("headers", "data")
- def __init__(self, headers: Tuple[Tuple[str, ...], ...], data: List[Dict]):
+ def __init__(self, headers: tuple[tuple[str, ...], ...], data: list[dict]):
"""
:param headers: Distinct headers seen in the sheet, parsed / split if applicable.
:param data: Sheet data rows, in grouped dict format.
"""
- self.headers: Tuple[Tuple[str, ...], ...] = headers
- self.data: List[Dict] = data
+ self.headers: tuple[tuple[str, ...], ...] = headers
+ self.data: list[dict] = data
def dealias_and_group_headers(
- dict_array: List[Dict],
- header_aliases: Dict[str, str],
+ dict_array: list[dict],
+ header_aliases: dict[str, str],
use_double_colons: bool,
default_language: str = constants.DEFAULT_LANGUAGE_VALUE,
ignore_case: bool = False,
@@ -292,8 +293,8 @@ def add_flat_annotations(prompt_list, parent_relevant="", name_prefix=""):
def process_range_question_type(
- row: Dict[str, Any], parameters: parameters_generic.PARAMETERS_TYPE
-) -> Dict[str, Any]:
+ row: dict[str, Any], parameters: parameters_generic.PARAMETERS_TYPE
+) -> dict[str, Any]:
"""
Returns a new row that includes the Range parameters start, end and step.
@@ -341,9 +342,9 @@ def process_image_default(default_value):
def add_choices_info_to_question(
- question: Dict[str, Any],
+ question: dict[str, Any],
list_name: str,
- choices: Dict[str, list],
+ choices: dict[str, list],
choice_filter: str,
file_extension: str,
):
@@ -392,11 +393,11 @@ def add_choices_info_to_question(
def workbook_to_json(
workbook_dict,
- form_name: Optional[str] = None,
- fallback_form_name: Optional[str] = None,
+ form_name: str | None = None,
+ fallback_form_name: str | None = None,
default_language: str = constants.DEFAULT_LANGUAGE_VALUE,
- warnings: Optional[List[str]] = None,
-) -> Dict[str, Any]:
+ warnings: list[str] | None = None,
+) -> dict[str, Any]:
"""
workbook_dict -- nested dictionaries representing a spreadsheet.
should be similar to those returned by xls_to_dict
@@ -771,10 +772,11 @@ def workbook_to_json(
parameters[constants.TRACK_CHANGES] != "true"
and parameters[constants.TRACK_CHANGES] != "false"
):
- raise PyXFormError(
- constants.TRACK_CHANGES + " must be set to true or false: "
- "'%s' is an invalid value" % parameters[constants.TRACK_CHANGES]
+ msg = (
+ f"{constants.TRACK_CHANGES} must be set to true or false: "
+ f"'{parameters[constants.TRACK_CHANGES]}' is an invalid value."
)
+ raise PyXFormError(msg)
else:
new_dict["bind"] = new_dict.get("bind", {})
new_dict["bind"].update(
@@ -801,10 +803,11 @@ def workbook_to_json(
parameters[constants.IDENTIFY_USER] != "true"
and parameters[constants.IDENTIFY_USER] != "false"
):
- raise PyXFormError(
- constants.IDENTIFY_USER + " must be set to true or false: "
- "'%s' is an invalid value" % parameters[constants.IDENTIFY_USER]
+ msg = (
+ f"{constants.IDENTIFY_USER} must be set to true or false: "
+ f"'{parameters[constants.IDENTIFY_USER]}' is an invalid value."
)
+ raise PyXFormError(msg)
else:
new_dict["bind"] = new_dict.get("bind", {})
new_dict["bind"].update(
@@ -828,13 +831,12 @@ def workbook_to_json(
"balanced",
"high-accuracy",
]:
- raise PyXFormError(
- "Parameter "
- + constants.LOCATION_PRIORITY
- + " must be set to no-power, low-power, balanced,"
- " or high-accuracy: '%s' is an invalid value"
- % parameters[constants.LOCATION_PRIORITY]
+ msg = (
+ f"Parameter {constants.LOCATION_PRIORITY} must be set to "
+ "no-power, low-power, balanced, or high-accuracy:"
+ f"'{parameters[constants.LOCATION_PRIORITY]}' is an invalid value"
)
+ raise PyXFormError(msg)
try:
int(parameters[constants.LOCATION_MIN_INTERVAL])
@@ -1016,8 +1018,7 @@ def workbook_to_json(
msg_dict = {"name": row.get("name"), "type": row.get("type")}
warnings.append(
ROW_FORMAT_STRING % row_number
- + " %s has no label: " % control_type.capitalize()
- + str(msg_dict)
+ + f" {control_type.capitalize()} has no label: {msg_dict}"
)
new_json_dict = row.copy()
@@ -1122,10 +1123,8 @@ def workbook_to_json(
and constants.CHOICE_FILTER not in row
):
warnings.append(
- ROW_FORMAT_STRING
- % row_number
- + " select one external is only meant for"
- " filtered selects."
+ ROW_FORMAT_STRING % row_number
+ + " select one external is only meant for filtered selects."
)
list_name = parse_dict[constants.LIST_NAME_U]
file_extension = os.path.splitext(list_name)[1]
@@ -1226,7 +1225,7 @@ def workbook_to_json(
):
raise PyXFormError(
"randomize must be set to true or false: "
- "'%s' is an invalid value" % parameters["randomize"]
+ f"""'{parameters["randomize"]}' is an invalid value"""
)
if "seed" in parameters.keys():
@@ -1308,9 +1307,9 @@ def workbook_to_json(
if constants.CONTROL not in new_json_dict:
new_json_dict[constants.CONTROL] = {}
- new_json_dict[constants.CONTROL][
- constants.APPEARANCE
- ] = constants.LIST_NOLABEL
+ new_json_dict[constants.CONTROL][constants.APPEARANCE] = (
+ constants.LIST_NOLABEL
+ )
parent_children_array.append(new_json_dict)
if specify_other_question:
parent_children_array.append(specify_other_question)
@@ -1594,9 +1593,9 @@ def parse_file_to_json(
path: str,
default_name: str = "data",
default_language: str = constants.DEFAULT_LANGUAGE_VALUE,
- warnings: Optional[List[str]] = None,
- file_object: Optional[IO] = None,
-) -> Dict[str, Any]:
+ warnings: list[str] | None = None,
+ file_object: IO | None = None,
+) -> dict[str, Any]:
"""
A wrapper for workbook_to_json
"""
diff --git a/pyxform/xls2json_backends.py b/pyxform/xls2json_backends.py
index eca89014..9a5b10d4 100644
--- a/pyxform/xls2json_backends.py
+++ b/pyxform/xls2json_backends.py
@@ -1,15 +1,17 @@
"""
XLS-to-dict and csv-to-dict are essentially backends for xls2json.
"""
+
import csv
import datetime
import os
import re
from collections import OrderedDict
+from collections.abc import Callable, Iterator
from contextlib import closing
from functools import reduce
from io import StringIO
-from typing import Any, Callable, Dict, Iterator, List, Optional, Tuple, Union
+from typing import Any
from zipfile import BadZipFile
import xlrd
@@ -25,7 +27,7 @@
from pyxform import constants
from pyxform.errors import PyXFormError
-aCell = Union[xlrdCell, pyxlCell]
+aCell = xlrdCell | pyxlCell
XL_DATE_AMBIGOUS_MSG = (
"The xls file provided has an invalid date on the %s sheet, under"
" the %s column on row number %s"
@@ -40,7 +42,7 @@ def _list_to_dict_list(list_items):
if list_items:
k = OrderedDict()
for item in list_items:
- k["%s" % item] = ""
+ k[str(item)] = ""
return [k]
return []
@@ -55,7 +57,7 @@ def trim_trailing_empty(a_list: list, n_empty: int) -> list:
return a_list
-def get_excel_column_headers(first_row: Iterator[Optional[str]]) -> List[Optional[str]]:
+def get_excel_column_headers(first_row: Iterator[str | None]) -> list[str | None]:
"""Get column headers from the first row; stop if there's a run of empty columns."""
max_adjacent_empty_columns = 20
column_header_list = []
@@ -72,7 +74,7 @@ def get_excel_column_headers(first_row: Iterator[Optional[str]]) -> List[Optiona
adjacent_empty_cols = 0
# Check for duplicate column headers.
if column_header in column_header_list:
- raise PyXFormError("Duplicate column header: %s" % column_header)
+ raise PyXFormError(f"Duplicate column header: {column_header}")
# Strip whitespaces from the header.
clean_header = re.sub(r"( )+", " ", column_header.strip())
column_header_list.append(clean_header)
@@ -81,10 +83,10 @@ def get_excel_column_headers(first_row: Iterator[Optional[str]]) -> List[Optiona
def get_excel_rows(
- headers: Iterator[Optional[str]],
- rows: Iterator[Tuple[aCell, ...]],
+ headers: Iterator[str | None],
+ rows: Iterator[tuple[aCell, ...]],
cell_func: Callable[[aCell, int, str], Any],
-) -> List[Dict[str, Any]]:
+) -> list[dict[str, Any]]:
"""Get rows of cleaned data; stop if there's a run of empty rows."""
max_adjacent_empty_rows = 60
col_header_enum = list(enumerate(headers))
@@ -129,7 +131,7 @@ def xls_to_dict(path_or_file):
def xls_clean_cell(
wb: xlrdBook, wb_sheet: xlrdSheet, cell: xlrdCell, row_n: int, col_key: str
- ) -> Optional[str]:
+ ) -> str | None:
value = cell.value
if isinstance(value, str):
value = value.strip()
@@ -153,7 +155,7 @@ def xls_to_dict_normal_sheet(wb: xlrdBook, wb_sheet: xlrdSheet):
)
# Inject wb/sheet as closure since functools.partial isn't typing friendly.
- def clean_func(cell: xlrdCell, row_n: int, col_key: str) -> Optional[str]:
+ def clean_func(cell: xlrdCell, row_n: int, col_key: str) -> str | None:
return xls_clean_cell(
wb=wb, wb_sheet=wb_sheet, cell=cell, row_n=row_n, col_key=col_key
)
@@ -172,19 +174,19 @@ def process_workbook(wb: xlrdBook):
if len(wb.sheets()) == 1:
(
result_book[constants.SURVEY],
- result_book["%s_header" % constants.SURVEY],
+ result_book[f"{constants.SURVEY}_header"],
) = xls_to_dict_normal_sheet(wb=wb, wb_sheet=wb_sheet)
else:
continue
else:
(
result_book[wb_sheet.name],
- result_book["%s_header" % wb_sheet.name],
+ result_book[f"{wb_sheet.name}_header"],
) = xls_to_dict_normal_sheet(wb=wb, wb_sheet=wb_sheet)
return result_book
try:
- if isinstance(path_or_file, (str, bytes, os.PathLike)):
+ if isinstance(path_or_file, str | bytes | os.PathLike):
file = open(path_or_file, mode="rb")
else:
file = path_or_file
@@ -195,7 +197,7 @@ def process_workbook(wb: xlrdBook):
finally:
workbook.release_resources()
except xlrd.XLRDError as read_err:
- raise PyXFormError("Error reading .xls file: %s" % read_err) from read_err
+ raise PyXFormError(f"Error reading .xls file: {read_err}") from read_err
def xls_value_to_unicode(value, value_type, datemode) -> str:
@@ -238,7 +240,7 @@ def xlsx_to_dict(path_or_file):
All the keys and leaf elements are strings.
"""
- def xlsx_clean_cell(cell: pyxlCell, row_n: int, col_key: str) -> Optional[str]:
+ def xlsx_clean_cell(cell: pyxlCell, row_n: int, col_key: str) -> str | None:
value = cell.value
if isinstance(value, str):
value = value.strip()
@@ -279,7 +281,7 @@ def process_workbook(wb: pyxlWorkbook):
return result_book
try:
- if isinstance(path_or_file, (str, bytes, os.PathLike)):
+ if isinstance(path_or_file, str | bytes | os.PathLike):
file = open(path_or_file, mode="rb")
else:
file = path_or_file
@@ -292,7 +294,7 @@ def process_workbook(wb: pyxlWorkbook):
reader.wb.close()
reader.archive.close()
except (OSError, BadZipFile, KeyError) as read_err:
- raise PyXFormError("Error reading .xlsx file: %s" % read_err) from read_err
+ raise PyXFormError(f"Error reading .xlsx file: {read_err}") from read_err
def xlsx_value_to_str(value) -> str:
@@ -306,7 +308,7 @@ def xlsx_value_to_str(value) -> str:
elif isinstance(value, float) and value.is_integer():
# Try to display as an int if possible.
return str(int(value))
- elif isinstance(value, (int, datetime.datetime, datetime.time)):
+ elif isinstance(value, int | datetime.datetime | datetime.time):
return str(value)
else:
# ensure unicode and replace nbsp spaces with normal ones
@@ -394,10 +396,10 @@ def first_column_as_sheet_name(row):
if content is not None:
if current_headers is None:
current_headers = content
- _dict["%s_header" % sheet_name] = _list_to_dict_list(current_headers)
+ _dict[f"{sheet_name}_header"] = _list_to_dict_list(current_headers)
else:
_d = OrderedDict()
- for key, val in zip(current_headers, content):
+ for key, val in zip(current_headers, content, strict=False):
if val != "":
# Slight modification so values are striped
# this is because csvs often spaces following commas
diff --git a/pyxform/xls2xform.py b/pyxform/xls2xform.py
index e31d5e1a..d9644b60 100644
--- a/pyxform/xls2xform.py
+++ b/pyxform/xls2xform.py
@@ -2,6 +2,7 @@
xls2xform converts properly formatted Excel documents into XForms for
use with ODK Collect.
"""
+
import argparse
import json
import logging
diff --git a/pyxform/xlsparseutils.py b/pyxform/xlsparseutils.py
index 7b1c995d..73d07823 100644
--- a/pyxform/xlsparseutils.py
+++ b/pyxform/xlsparseutils.py
@@ -1,5 +1,5 @@
import re
-from typing import KeysView, Optional
+from collections.abc import KeysView
from pyxform import constants
from pyxform.utils import levenshtein_distance
@@ -10,7 +10,7 @@
XFORM_TAG_REGEXP = f"{TAG_START_CHAR}{TAG_CHAR}*"
-def find_sheet_misspellings(key: str, keys: "KeysView") -> "Optional[str]":
+def find_sheet_misspellings(key: str, keys: "KeysView") -> "str | None":
"""
Find possible sheet name misspellings to warn the user about.
diff --git a/tests/pyxform_test_case.py b/tests/pyxform_test_case.py
index 835668bb..1ca027f9 100644
--- a/tests/pyxform_test_case.py
+++ b/tests/pyxform_test_case.py
@@ -1,12 +1,14 @@
"""
PyxformTestCase base class using markdown to define the XLSForm.
"""
+
import logging
import os
import re
import tempfile
+from collections.abc import Iterable
from dataclasses import dataclass
-from typing import TYPE_CHECKING, Dict, Iterable, List, Optional, Set, Tuple, Union
+from typing import TYPE_CHECKING, Optional
from unittest import TestCase
from lxml import etree
@@ -30,7 +32,7 @@
if TYPE_CHECKING:
from pyxform.survey import Survey
- NSMAPSubs: "List[Tuple[str, str]]"
+ NSMAPSubs: "list[tuple[str, str]]"
class PyxformTestError(Exception):
@@ -40,7 +42,7 @@ class PyxformTestError(Exception):
@dataclass
class MatcherContext:
debug: bool
- nsmap_xpath: "Dict[str, str]"
+ nsmap_xpath: "dict[str, str]"
nsmap_subs: "NSMAPSubs"
content_str: str
@@ -51,12 +53,12 @@ class PyxformMarkdown:
def md_to_pyxform_survey(
self,
md_raw: str,
- name: Optional[str] = None,
- title: Optional[str] = None,
- id_string: Optional[str] = None,
+ name: str | None = None,
+ title: str | None = None,
+ id_string: str | None = None,
debug: bool = False,
autoname: bool = True,
- warnings: Optional[List[str]] = None,
+ warnings: list[str] | None = None,
):
if autoname:
kwargs = self._autoname_inputs(name=name, title=title, id_string=id_string)
@@ -106,11 +108,11 @@ def _row_to_dict(row):
@staticmethod
def _ss_structure_to_pyxform_survey(
- ss_structure: Dict,
- name: Optional[str] = None,
- title: Optional[str] = None,
- id_string: Optional[str] = None,
- warnings: Optional[List[str]] = None,
+ ss_structure: dict,
+ name: str | None = None,
+ title: str | None = None,
+ id_string: str | None = None,
+ warnings: list[str] | None = None,
):
# using existing methods from the builder
imported_survey_json = workbook_to_json(
@@ -147,10 +149,10 @@ def _run_odk_validate(xml):
@staticmethod
def _autoname_inputs(
- name: Optional[str] = None,
- title: Optional[str] = None,
- id_string: Optional[str] = None,
- ) -> Dict[str, str]:
+ name: str | None = None,
+ title: str | None = None,
+ id_string: str | None = None,
+ ) -> dict[str, str]:
"""
Fill in any blank inputs with default values.
"""
@@ -167,34 +169,34 @@ class PyxformTestCase(PyxformMarkdown, TestCase):
def assertPyxformXform(
self,
# Survey input
- md: Optional[str] = None,
- ss_structure: Optional[Dict] = None,
+ md: str | None = None,
+ ss_structure: dict | None = None,
survey: Optional["Survey"] = None,
# XForm assertions
- xml__xpath_match: Optional[Iterable[str]] = None,
- xml__xpath_exact: Optional[Iterable[Tuple[str, Set[str]]]] = None,
- xml__xpath_count: Optional[Iterable[Tuple[str, int]]] = None,
+ xml__xpath_match: Iterable[str] | None = None,
+ xml__xpath_exact: Iterable[tuple[str, set[str]]] | None = None,
+ xml__xpath_count: Iterable[tuple[str, int]] | None = None,
# XForm assertions - deprecated
- xml__contains: Optional[Iterable[str]] = None,
- xml__excludes: Optional[Iterable[str]] = None,
- model__contains: Optional[Iterable[str]] = None,
- model__excludes: Optional[Iterable[str]] = None,
- itext__contains: Optional[Iterable[str]] = None,
- itext__excludes: Optional[Iterable[str]] = None,
- instance__contains: Optional[Iterable[str]] = None,
+ xml__contains: Iterable[str] | None = None,
+ xml__excludes: Iterable[str] | None = None,
+ model__contains: Iterable[str] | None = None,
+ model__excludes: Iterable[str] | None = None,
+ itext__contains: Iterable[str] | None = None,
+ itext__excludes: Iterable[str] | None = None,
+ instance__contains: Iterable[str] | None = None,
# Errors assertions
- error__contains: Optional[Iterable[str]] = None,
- error__not_contains: Optional[Iterable[str]] = None,
- odk_validate_error__contains: Optional[Iterable[str]] = None,
- warnings__contains: Optional[Iterable[str]] = None,
- warnings__not_contains: Optional[Iterable[str]] = None,
- warnings_count: Optional[int] = None,
+ error__contains: Iterable[str] | None = None,
+ error__not_contains: Iterable[str] | None = None,
+ odk_validate_error__contains: Iterable[str] | None = None,
+ warnings__contains: Iterable[str] | None = None,
+ warnings__not_contains: Iterable[str] | None = None,
+ warnings_count: int | None = None,
errored: bool = False,
# Optional extras
- name: Optional[str] = None,
- id_string: Optional[str] = None,
- title: Optional[str] = None,
- warnings: Optional[List[str]] = None,
+ name: str | None = None,
+ id_string: str | None = None,
+ title: str | None = None,
+ warnings: list[str] | None = None,
run_odk_validate: bool = False,
debug: bool = False,
):
@@ -295,7 +297,7 @@ def assertPyxformXform(
def _pull_xml_node_from_root(element_selector):
_r = root.findall(
- ".//n:%s" % element_selector,
+ f".//n:{element_selector}",
namespaces={"n": "http://www.w3.org/2002/xforms"},
)
if _r:
@@ -455,7 +457,7 @@ def assertContains(self, content, text, count=None, msg_prefix=""):
else:
self.assertTrue(
real_count != 0,
- msg_prefix + "Couldn't find %s in content:\n" % text_repr + content,
+ f"{msg_prefix}Couldn't find {text_repr} in content:\n{content}",
)
def assertNotContains(self, content, text, msg_prefix=""):
@@ -469,7 +471,7 @@ def assertNotContains(self, content, text, msg_prefix=""):
)
self.assertEqual(
- real_count, 0, msg_prefix + "Response should not contain %s" % text_repr
+ real_count, 0, f"{msg_prefix}Response should not contain {text_repr}"
)
def assert_xpath_exact(
@@ -477,7 +479,7 @@ def assert_xpath_exact(
matcher_context: "MatcherContext",
content: "_Element",
xpath: str,
- expected: "Set[str]",
+ expected: "set[str]",
case_num: int,
) -> None:
"""
@@ -565,8 +567,8 @@ def reorder_attributes(root):
def xpath_clean_result_strings(
- nsmap_subs: "NSMAPSubs", results: "Set[_Element]"
-) -> "Set[str]":
+ nsmap_subs: "NSMAPSubs", results: "set[_Element]"
+) -> "set[str]":
"""
Clean XPath results: stringify, remove namespace declarations, clean up whitespace.
@@ -589,7 +591,7 @@ def xpath_clean_result_strings(
def xpath_evaluate(
matcher_context: "MatcherContext", content: "_Element", xpath: str, for_exact=False
-) -> "Union[Set[_Element], Set[str]]":
+) -> "set[_Element] | set[str]":
"""
Evaluate an XPath and return the results.
diff --git a/tests/test_area.py b/tests/test_area.py
index 10cf6360..e09bb3e3 100644
--- a/tests/test_area.py
+++ b/tests/test_area.py
@@ -1,6 +1,7 @@
"""
AreaTest - test enclosed-area(geo_shape) calculation.
"""
+
from tests.pyxform_test_case import PyxformTestCase
diff --git a/tests/test_audit.py b/tests/test_audit.py
index 6ef65a29..651bb5e7 100644
--- a/tests/test_audit.py
+++ b/tests/test_audit.py
@@ -1,6 +1,7 @@
"""
AuditTest - test audit question type.
"""
+
from tests.pyxform_test_case import PyxformTestCase
diff --git a/tests/test_bind_conversions.py b/tests/test_bind_conversions.py
index d6d58016..2ff3d111 100644
--- a/tests/test_bind_conversions.py
+++ b/tests/test_bind_conversions.py
@@ -1,6 +1,7 @@
"""
BindConversionsTest - test bind conversions.
"""
+
from tests.pyxform_test_case import PyxformTestCase
diff --git a/tests/test_bug_missing_headers.py b/tests/test_bug_missing_headers.py
index 7013d871..dd61942e 100644
--- a/tests/test_bug_missing_headers.py
+++ b/tests/test_bug_missing_headers.py
@@ -1,6 +1,7 @@
"""
Test missing headers in XLSForm.
"""
+
from tests.pyxform_test_case import PyxformTestCase
diff --git a/tests/test_bug_round_calculation.py b/tests/test_bug_round_calculation.py
index 64f52e35..706e0138 100644
--- a/tests/test_bug_round_calculation.py
+++ b/tests/test_bug_round_calculation.py
@@ -1,6 +1,7 @@
"""
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 209d15fe..dc20f7c1 100644
--- a/tests/test_builder.py
+++ b/tests/test_builder.py
@@ -1,6 +1,7 @@
"""
Test builder module functionality.
"""
+
import os
import re
from unittest import TestCase
diff --git a/tests/test_dump_and_load.py b/tests/test_dump_and_load.py
index 5ddb2d03..f114d45d 100644
--- a/tests/test_dump_and_load.py
+++ b/tests/test_dump_and_load.py
@@ -1,6 +1,7 @@
"""
Test multiple XLSForm can be generated successfully.
"""
+
import os
from unittest import TestCase
diff --git a/tests/test_dynamic_default.py b/tests/test_dynamic_default.py
index 79644f5d..3cf68057 100644
--- a/tests/test_dynamic_default.py
+++ b/tests/test_dynamic_default.py
@@ -1,11 +1,11 @@
"""
Test handling dynamic default in forms
"""
+
import os
import unittest
from dataclasses import dataclass
from time import perf_counter
-from typing import Optional, Tuple
from unittest.mock import patch
import psutil
@@ -32,7 +32,7 @@ class Case:
is_dynamic: bool
q_type: str
q_default: str = ""
- q_value: Optional[str] = None
+ q_value: str | None = None
q_label_fr: str = ""
@@ -127,7 +127,7 @@ def body_input(qnum: int, case: Case):
"""
@staticmethod
- def body_select1(q_num: int, choices: Tuple[Tuple[str, str], ...]):
+ def body_select1(q_num: int, choices: tuple[tuple[str, str], ...]):
"""Expected structure for body elements for select1 types."""
choices_xp = "\n and ".join(
(
diff --git a/tests/test_external_instances.py b/tests/test_external_instances.py
index 1f072d0c..e0d33ab9 100644
--- a/tests/test_external_instances.py
+++ b/tests/test_external_instances.py
@@ -3,6 +3,7 @@
See also test_external_instances_for_selects
"""
+
from textwrap import dedent
from pyxform.errors import PyXFormError
diff --git a/tests/test_external_instances_for_selects.py b/tests/test_external_instances_for_selects.py
index 32b78b28..b49aa9eb 100644
--- a/tests/test_external_instances_for_selects.py
+++ b/tests/test_external_instances_for_selects.py
@@ -3,6 +3,7 @@
See also test_external_instances
"""
+
import os
from dataclasses import dataclass, field
diff --git a/tests/test_fieldlist_labels.py b/tests/test_fieldlist_labels.py
index bf3a4bad..bd7d5be7 100644
--- a/tests/test_fieldlist_labels.py
+++ b/tests/test_fieldlist_labels.py
@@ -1,6 +1,7 @@
"""
Test field-list labels
"""
+
from tests.pyxform_test_case import PyxformTestCase
diff --git a/tests/test_fields.py b/tests/test_fields.py
index 86afb9c7..6c44fc9e 100644
--- a/tests/test_fields.py
+++ b/tests/test_fields.py
@@ -1,6 +1,7 @@
"""
Test duplicate survey question field name.
"""
+
from tests.pyxform_test_case import PyxformTestCase
from tests.xpath_helpers.choices import xpc
from tests.xpath_helpers.questions import xpq
@@ -193,7 +194,7 @@ def test_duplicate_form_name_in_section_name(self):
| | end group | | |
""",
errored=True,
- error__contains=['The name "foo" is the same as the form name'],
+ error__contains=["The name 'foo' is the same as the form name"],
)
def test_field_name_may_match_form_name(self):
diff --git a/tests/test_file.py b/tests/test_file.py
index 21970c01..847f34f2 100644
--- a/tests/test_file.py
+++ b/tests/test_file.py
@@ -1,6 +1,7 @@
"""
Test file question type.
"""
+
from tests.pyxform_test_case import PyxformTestCase
diff --git a/tests/test_file_utils.py b/tests/test_file_utils.py
index df50d123..8266235d 100644
--- a/tests/test_file_utils.py
+++ b/tests/test_file_utils.py
@@ -1,6 +1,7 @@
"""
Test xls2json_backends util functions.
"""
+
from unittest import TestCase
from pyxform.xls2json_backends import convert_file_to_csv_string
diff --git a/tests/test_for_loop.py b/tests/test_for_loop.py
index 03f7d258..0720ea21 100644
--- a/tests/test_for_loop.py
+++ b/tests/test_for_loop.py
@@ -1,6 +1,7 @@
"""
Test loop question type.
"""
+
from tests.pyxform_test_case import PyxformTestCase
diff --git a/tests/test_form_name.py b/tests/test_form_name.py
index 090f643f..866fec60 100644
--- a/tests/test_form_name.py
+++ b/tests/test_form_name.py
@@ -1,6 +1,7 @@
"""
Test setting form name to data.
"""
+
from tests.pyxform_test_case import PyxformTestCase
diff --git a/tests/test_geo.py b/tests/test_geo.py
index 548295e5..0144bbda 100644
--- a/tests/test_geo.py
+++ b/tests/test_geo.py
@@ -1,6 +1,7 @@
"""
Test geo widgets.
"""
+
from tests.pyxform_test_case import PyxformTestCase
diff --git a/tests/test_group.py b/tests/test_group.py
index 68597cc7..9d99fbe5 100644
--- a/tests/test_group.py
+++ b/tests/test_group.py
@@ -1,6 +1,7 @@
"""
Testing simple cases for Xls2Json
"""
+
from unittest import TestCase
from pyxform.builder import create_survey_element_from_dict
diff --git a/tests/test_groups.py b/tests/test_groups.py
index 5853fcfe..1d8d6efe 100644
--- a/tests/test_groups.py
+++ b/tests/test_groups.py
@@ -1,6 +1,7 @@
"""
Test XForm groups.
"""
+
from tests.pyxform_test_case import PyxformTestCase
diff --git a/tests/test_guidance_hint.py b/tests/test_guidance_hint.py
index 8566a9c4..896a6953 100644
--- a/tests/test_guidance_hint.py
+++ b/tests/test_guidance_hint.py
@@ -1,6 +1,7 @@
"""
Guidance hint test module.
"""
+
from tests.pyxform_test_case import PyxformTestCase
diff --git a/tests/test_image_app_parameter.py b/tests/test_image_app_parameter.py
index 36f55bab..eab62071 100644
--- a/tests/test_image_app_parameter.py
+++ b/tests/test_image_app_parameter.py
@@ -1,6 +1,7 @@
"""
Test image max-pixels and app parameters.
"""
+
from tests.pyxform_test_case import PyxformTestCase
diff --git a/tests/test_j2x_creation.py b/tests/test_j2x_creation.py
index e8f79ad3..c0d7b41c 100644
--- a/tests/test_j2x_creation.py
+++ b/tests/test_j2x_creation.py
@@ -1,6 +1,7 @@
"""
Testing creation of Surveys using verbose methods
"""
+
from unittest import TestCase
from pyxform import MultipleChoiceQuestion, Survey, create_survey_from_xls
diff --git a/tests/test_j2x_instantiation.py b/tests/test_j2x_instantiation.py
index 0b419be1..d7f928bc 100644
--- a/tests/test_j2x_instantiation.py
+++ b/tests/test_j2x_instantiation.py
@@ -1,6 +1,7 @@
"""
Testing the instance object for pyxform.
"""
+
from unittest import TestCase
from pyxform import Survey, SurveyInstance
diff --git a/tests/test_j2x_question.py b/tests/test_j2x_question.py
index 7ba69f9e..8e332995 100644
--- a/tests/test_j2x_question.py
+++ b/tests/test_j2x_question.py
@@ -1,6 +1,7 @@
"""
Testing creation of Surveys using verbose methods
"""
+
from unittest import TestCase
from pyxform import Survey
diff --git a/tests/test_j2x_xform_build_preparation.py b/tests/test_j2x_xform_build_preparation.py
index 3740c908..062a2280 100644
--- a/tests/test_j2x_xform_build_preparation.py
+++ b/tests/test_j2x_xform_build_preparation.py
@@ -1,6 +1,7 @@
"""
Testing preparation of values for XForm exporting
"""
+
from unittest import TestCase
from pyxform import MultipleChoiceQuestion, Survey
diff --git a/tests/test_js2x_import_from_json.py b/tests/test_js2x_import_from_json.py
index 7a698c8d..bc639e64 100644
--- a/tests/test_js2x_import_from_json.py
+++ b/tests/test_js2x_import_from_json.py
@@ -1,6 +1,7 @@
"""
Testing our ability to import from a JSON text file.
"""
+
from unittest import TestCase
from pyxform.builder import (
diff --git a/tests/test_json2xform.py b/tests/test_json2xform.py
index c169debd..b06bfe73 100644
--- a/tests/test_json2xform.py
+++ b/tests/test_json2xform.py
@@ -1,6 +1,7 @@
"""
Testing simple cases for pyxform
"""
+
from unittest import TestCase
from pyxform.builder import create_survey_element_from_dict
diff --git a/tests/test_language_warnings.py b/tests/test_language_warnings.py
index 2d17a9a7..9892d0ca 100644
--- a/tests/test_language_warnings.py
+++ b/tests/test_language_warnings.py
@@ -1,6 +1,7 @@
"""
Test language warnings.
"""
+
import os
import tempfile
diff --git a/tests/test_last_saved.py b/tests/test_last_saved.py
index 05f771a0..094efde0 100644
--- a/tests/test_last_saved.py
+++ b/tests/test_last_saved.py
@@ -1,6 +1,7 @@
"""
The last-saved virtual instance can be queried to get values from the last saved instance of the form being authored.
"""
+
from tests.pyxform_test_case import PyxformTestCase
diff --git a/tests/test_loop.py b/tests/test_loop.py
index 5a079950..d4ca0d61 100644
--- a/tests/test_loop.py
+++ b/tests/test_loop.py
@@ -1,6 +1,7 @@
"""
Test loop syntax.
"""
+
from unittest import TestCase
from pyxform.builder import create_survey_from_xls
diff --git a/tests/test_metadata.py b/tests/test_metadata.py
index 0fcd1a8e..2861d06c 100644
--- a/tests/test_metadata.py
+++ b/tests/test_metadata.py
@@ -1,6 +1,7 @@
"""
Test language warnings.
"""
+
import os
import tempfile
diff --git a/tests/test_notes.py b/tests/test_notes.py
index 9c8e24f9..6cbb9a55 100644
--- a/tests/test_notes.py
+++ b/tests/test_notes.py
@@ -1,8 +1,8 @@
"""
Test the "note" question type.
"""
+
from dataclasses import dataclass
-from typing import Set
from tests.pyxform_test_case import PyxformTestCase
from tests.xpath_helpers.questions import xpq
@@ -16,7 +16,7 @@ class Case:
label: str
xpath: str
- match: Set[str]
+ match: set[str]
class TestNotes(PyxformTestCase):
diff --git a/tests/test_osm.py b/tests/test_osm.py
index 1905a4df..7740a21d 100644
--- a/tests/test_osm.py
+++ b/tests/test_osm.py
@@ -1,6 +1,7 @@
"""
Test OSM widgets.
"""
+
from tests.pyxform_test_case import PyxformTestCase
expected_xml_output = """
diff --git a/tests/test_parameters_rows.py b/tests/test_parameters_rows.py
index 03555608..50c14be3 100644
--- a/tests/test_parameters_rows.py
+++ b/tests/test_parameters_rows.py
@@ -1,6 +1,7 @@
"""
Test text rows parameter.
"""
+
from tests.pyxform_test_case import PyxformTestCase
diff --git a/tests/test_pyxform_test_case.py b/tests/test_pyxform_test_case.py
index 8de8fa1f..ceea0c4b 100644
--- a/tests/test_pyxform_test_case.py
+++ b/tests/test_pyxform_test_case.py
@@ -1,17 +1,13 @@
import unittest
from dataclasses import dataclass
-from typing import TYPE_CHECKING
from tests.pyxform_test_case import PyxformTestCase
-if TYPE_CHECKING:
- from typing import Set
-
@dataclass
class CaseData:
xpath: str
- exact: "Set[str]"
+ exact: "set[str]"
count: int
@property
diff --git a/tests/test_pyxformtestcase.py b/tests/test_pyxformtestcase.py
index 35a35a02..f6e2599a 100644
--- a/tests/test_pyxformtestcase.py
+++ b/tests/test_pyxformtestcase.py
@@ -2,6 +2,7 @@
Ensuring that the pyxform_test_case.PyxformTestCase class does some
internal conversions correctly.
"""
+
from tests.pyxform_test_case import PyxformTestCase
diff --git a/tests/test_randomize_itemsets.py b/tests/test_randomize_itemsets.py
index 6496e441..b3e41d73 100644
--- a/tests/test_randomize_itemsets.py
+++ b/tests/test_randomize_itemsets.py
@@ -1,6 +1,7 @@
"""
Test randomize itemsets.
"""
+
from tests.pyxform_test_case import PyxformTestCase
diff --git a/tests/test_range.py b/tests/test_range.py
index 14bb5553..10cd8682 100644
--- a/tests/test_range.py
+++ b/tests/test_range.py
@@ -1,6 +1,7 @@
"""
Test range widget.
"""
+
from tests.pyxform_test_case import PyxformTestCase
diff --git a/tests/test_rank.py b/tests/test_rank.py
index f41386e4..f0ed2b89 100644
--- a/tests/test_rank.py
+++ b/tests/test_rank.py
@@ -1,6 +1,7 @@
"""
Test rank widget.
"""
+
from tests.pyxform_test_case import PyxformTestCase
from tests.xpath_helpers.choices import xpc
from tests.xpath_helpers.questions import xpq
diff --git a/tests/test_repeat.py b/tests/test_repeat.py
index 4bb0cb99..aa06a28c 100644
--- a/tests/test_repeat.py
+++ b/tests/test_repeat.py
@@ -1,6 +1,7 @@
"""
Test reapeat structure.
"""
+
from tests.pyxform_test_case import PyxformTestCase
diff --git a/tests/test_repeat_template.py b/tests/test_repeat_template.py
index 15595216..2607243c 100644
--- a/tests/test_repeat_template.py
+++ b/tests/test_repeat_template.py
@@ -1,6 +1,7 @@
"""
Test repeat template and instance structure.
"""
+
from tests.pyxform_test_case import PyxformTestCase
diff --git a/tests/test_secondary_instance_translations.py b/tests/test_secondary_instance_translations.py
index bd80f709..78e47e5c 100644
--- a/tests/test_secondary_instance_translations.py
+++ b/tests/test_secondary_instance_translations.py
@@ -1,6 +1,7 @@
"""
Testing inlining translation when no translation is specified.
"""
+
from tests.pyxform_test_case import PyxformTestCase
from tests.xpath_helpers.choices import xpc
from tests.xpath_helpers.questions import xpq
diff --git a/tests/test_set_geopoint.py b/tests/test_set_geopoint.py
index 0540cbbf..c52196ed 100644
--- a/tests/test_set_geopoint.py
+++ b/tests/test_set_geopoint.py
@@ -1,6 +1,7 @@
"""
Test setgeopoint widget.
"""
+
from tests.pyxform_test_case import PyxformTestCase
diff --git a/tests/test_settings.py b/tests/test_settings.py
index 86fd70f9..f05ac211 100644
--- a/tests/test_settings.py
+++ b/tests/test_settings.py
@@ -38,7 +38,6 @@ def test_form_id(self):
"""
self.assertPyxformXform(
md=md,
- debug=True,
xml__xpath_match=[
"/h:html/h:head/x:model/x:instance/x:test_name[@id='my_form']"
],
diff --git a/tests/test_settings_auto_send_delete.py b/tests/test_settings_auto_send_delete.py
index ac8df5af..eec4a7c7 100644
--- a/tests/test_settings_auto_send_delete.py
+++ b/tests/test_settings_auto_send_delete.py
@@ -1,6 +1,7 @@
"""
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 5b60c549..a5b85a42 100644
--- a/tests/test_sheet_columns.py
+++ b/tests/test_sheet_columns.py
@@ -1,6 +1,7 @@
"""
Test XLSForm sheet names.
"""
+
from tests.pyxform_test_case import PyxformTestCase
from tests.utils import prep_for_xml_contains
from tests.xpath_helpers.choices import xpc
diff --git a/tests/test_sms.py b/tests/test_sms.py
index 00578071..6bfa434b 100644
--- a/tests/test_sms.py
+++ b/tests/test_sms.py
@@ -1,6 +1,7 @@
"""
Test sms syntax.
"""
+
from tests.pyxform_test_case import PyxformTestCase
diff --git a/tests/test_table_list.py b/tests/test_table_list.py
index 155c157c..3cee939e 100644
--- a/tests/test_table_list.py
+++ b/tests/test_table_list.py
@@ -1,6 +1,7 @@
"""
Test table list appearance syntax.
"""
+
from tests.pyxform_test_case import PyxformTestCase
MD = '''
diff --git a/tests/test_translations.py b/tests/test_translations.py
index 4da39620..f4228e41 100644
--- a/tests/test_translations.py
+++ b/tests/test_translations.py
@@ -1,6 +1,7 @@
"""
Test translations syntax.
"""
+
import unittest
from dataclasses import dataclass
from time import perf_counter
@@ -372,13 +373,13 @@ def test_missing_translations_check_performance(self):
"""
Should find the translations check costs a fraction of a second for large forms.
- Results with Python 3.8.9 on VM with 4CPU 8GB RAM, x questions with 2 choices
- each, average of 10 runs (seconds), with and without the check, per question:
+ Results with Python 3.10.14 on VM with 2vCPU (i7-7700HQ) 4GB RAM, x questions
+ with 2 choices each, average of 10 runs (seconds), with and without the check,
+ per question:
| num | with | without |
- | 500 | 1.0192 | 0.9950 |
- | 1000 | 2.0054 | 2.1026 |
- | 2000 | 4.0714 | 4.0926 |
- | 3000 | 6.0266 | 6.2476 |
+ | 500 | 3.0420 | 3.0427 |
+ | 1000 | 9.7641 | 9.6972 |
+ | 2000 | 30.645 | 28.869 |
"""
survey_header = """
| survey | | | | |
diff --git a/tests/test_tutorial_xls.py b/tests/test_tutorial_xls.py
index 57b15d53..f57264d8 100644
--- a/tests/test_tutorial_xls.py
+++ b/tests/test_tutorial_xls.py
@@ -1,6 +1,7 @@
"""
Test tutorial XLSForm.
"""
+
from unittest import TestCase
from pyxform.builder import create_survey_from_path
diff --git a/tests/test_unicode_rtl.py b/tests/test_unicode_rtl.py
index ec28ad80..322bec1b 100644
--- a/tests/test_unicode_rtl.py
+++ b/tests/test_unicode_rtl.py
@@ -1,6 +1,7 @@
"""
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 c611e940..92d0708c 100644
--- a/tests/test_upload_question.py
+++ b/tests/test_upload_question.py
@@ -1,6 +1,7 @@
"""
Test upload (image, audio, file) question types in XLSForm
"""
+
from tests.pyxform_test_case import PyxformTestCase
diff --git a/tests/test_utils/md_table.py b/tests/test_utils/md_table.py
index 9903c417..7d17821b 100644
--- a/tests/test_utils/md_table.py
+++ b/tests/test_utils/md_table.py
@@ -1,8 +1,8 @@
"""
Markdown table utility functions.
"""
+
import re
-from typing import List, Tuple
from openpyxl import Workbook
@@ -35,7 +35,7 @@ def _is_null_row(r_arr):
return True
-def md_table_to_ss_structure(mdstr: str) -> List[Tuple[str, List[List[str]]]]:
+def md_table_to_ss_structure(mdstr: str) -> list[tuple[str, list[list[str]]]]:
ss_arr = []
for item in mdstr.split("\n"):
arr = _extract_array(item)
diff --git a/tests/test_validator_update.py b/tests/test_validator_update.py
index 9f7e3cb9..5aeb0283 100644
--- a/tests/test_validator_update.py
+++ b/tests/test_validator_update.py
@@ -1,11 +1,11 @@
"""
Test validator update cli command.
"""
+
import os
import platform
from datetime import datetime, timedelta
from stat import S_IXGRP, S_IXUSR
-from typing import Optional
from unittest import TestCase, skipIf
from zipfile import ZipFile
@@ -77,7 +77,7 @@ def test_get_temp_dir(self):
class TestUpdateHandler(TestCase):
- server: "Optional[ThreadingServerInThread]" = None
+ server: "ThreadingServerInThread | None" = None
@classmethod
def setUpClass(cls):
@@ -366,9 +366,10 @@ def test_unzip_find_zip_jobs__ok_real_current(self):
def test_unzip_find_zip_jobs__ok_real_ideal(self):
"""Should return a list of zip jobs same length as search."""
- with get_temp_dir() as temp_dir, ZipFile(
- self.zip_file_ideal, mode="r"
- ) as zip_file:
+ with (
+ get_temp_dir() as temp_dir,
+ ZipFile(self.zip_file_ideal, mode="r") as zip_file,
+ ):
bin_paths = self.updater._get_bin_paths(
update_info=self.update_info, file_path=self.zip_file_ideal
)
@@ -380,9 +381,10 @@ def test_unzip_find_zip_jobs__ok_real_ideal(self):
def test_unzip_find_zip_jobs__ok_real_dupes(self):
"""Should return a list of zip jobs same length as search."""
- with get_temp_dir() as temp_dir, ZipFile(
- self.zip_file_dupes, mode="r"
- ) as zip_file:
+ with (
+ get_temp_dir() as temp_dir,
+ ZipFile(self.zip_file_dupes, mode="r") as zip_file,
+ ):
bin_paths = self.updater._get_bin_paths(
update_info=self.update_info, file_path=self.zip_file_dupes
)
@@ -396,9 +398,11 @@ def test_unzip_find_zip_jobs__not_found_raises(self):
"""Should raise an error if zip jobs isn't same length as search."""
bin_paths = [(".non_existent", ".non_existent")]
- with get_temp_dir() as temp_dir, ZipFile(
- self.zip_file, mode="r"
- ) as zip_file, self.assertRaises(PyXFormError) as ctx:
+ with (
+ get_temp_dir() as temp_dir,
+ ZipFile(self.zip_file, mode="r") as zip_file,
+ self.assertRaises(PyXFormError) as ctx,
+ ):
self.updater._unzip_find_jobs(
open_zip_file=zip_file, bin_paths=bin_paths, out_path=temp_dir
)
@@ -416,9 +420,11 @@ def test_unzip_extract_file__ok(self):
def test_unzip_extract_file__bad_crc_raises(self):
"""Should raise an error if the zip file CRC doesn't match."""
- with get_temp_dir() as temp_dir, ZipFile(
- self.zip_file, mode="r"
- ) as zip_file, self.assertRaises(BadZipFile) as ctx:
+ with (
+ get_temp_dir() as temp_dir,
+ ZipFile(self.zip_file, mode="r") as zip_file,
+ self.assertRaises(BadZipFile) as ctx,
+ ):
zip_item = next(
x for x in zip_file.infolist() if x.filename.endswith("validate")
)
diff --git a/tests/test_validator_util.py b/tests/test_validator_util.py
index 147bf728..550ab0c0 100644
--- a/tests/test_validator_util.py
+++ b/tests/test_validator_util.py
@@ -1,6 +1,7 @@
"""
Test pyxform.validators.utils module.
"""
+
import os
from unittest import TestCase
diff --git a/tests/test_validators.py b/tests/test_validators.py
index 0d5a08e0..868bb449 100644
--- a/tests/test_validators.py
+++ b/tests/test_validators.py
@@ -1,6 +1,7 @@
"""
Test validators.
"""
+
from unittest import TestCase
from unittest.mock import patch
diff --git a/tests/test_warnings.py b/tests/test_warnings.py
index 2f8a3872..470efd53 100644
--- a/tests/test_warnings.py
+++ b/tests/test_warnings.py
@@ -1,6 +1,7 @@
"""
Test warnings.
"""
+
from tests.pyxform_test_case import PyxformTestCase
diff --git a/tests/test_whitespace.py b/tests/test_whitespace.py
index 9e3c9c01..29750455 100644
--- a/tests/test_whitespace.py
+++ b/tests/test_whitespace.py
@@ -1,6 +1,7 @@
"""
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 afc1d8d7..4f74fc60 100644
--- a/tests/test_xform2json.py
+++ b/tests/test_xform2json.py
@@ -1,6 +1,7 @@
"""
Test xform2json module.
"""
+
import json
import os
from unittest import TestCase
diff --git a/tests/test_xls2json_backends.py b/tests/test_xls2json_backends.py
index 55a9fcfc..088bb51c 100644
--- a/tests/test_xls2json_backends.py
+++ b/tests/test_xls2json_backends.py
@@ -1,6 +1,7 @@
"""
Test xls2json_backends module functionality.
"""
+
import os
from datetime import datetime
from unittest import TestCase
@@ -68,8 +69,8 @@ def test_equivalency(self):
"yes_or_no_question",
]
for fixture in equivalent_fixtures:
- xls_path = utils.path_to_text_fixture("%s.xls" % fixture)
- xlsx_path = utils.path_to_text_fixture("%s.xlsx" % fixture)
+ xls_path = utils.path_to_text_fixture(f"{fixture}.xls")
+ xlsx_path = utils.path_to_text_fixture(f"{fixture}.xlsx")
xls_inp = xls_to_dict(xls_path)
xlsx_inp = xlsx_to_dict(xlsx_path)
self.maxDiff = None
diff --git a/tests/test_xls2json_xls.py b/tests/test_xls2json_xls.py
index 1f914968..efe7eb8d 100644
--- a/tests/test_xls2json_xls.py
+++ b/tests/test_xls2json_xls.py
@@ -1,6 +1,7 @@
"""
Testing simple cases for Xls2Json
"""
+
import json
import os
from unittest import TestCase
@@ -34,9 +35,10 @@ def test_simple_yes_or_no_question(self):
with open(output_path, mode="w", encoding="utf-8") as fp:
json.dump(x_results, fp=fp, ensure_ascii=False, indent=4)
# Compare with the expected output:
- with open(expected_output_path, encoding="utf-8") as expected, open(
- output_path, encoding="utf-8"
- ) as observed:
+ with (
+ open(expected_output_path, encoding="utf-8") as expected,
+ open(output_path, encoding="utf-8") as observed,
+ ):
self.assertEqual(json.load(expected), json.load(observed))
def test_hidden(self):
@@ -128,9 +130,10 @@ def test_table(self):
with open(output_path, mode="w", encoding="utf-8") as fp:
json.dump(x_results, fp=fp, ensure_ascii=False, indent=4)
# Compare with the expected output:
- with open(expected_output_path, encoding="utf-8") as expected, open(
- output_path, encoding="utf-8"
- ) as observed:
+ with (
+ open(expected_output_path, encoding="utf-8") as expected,
+ open(output_path, encoding="utf-8") as observed,
+ ):
self.assertEqual(json.load(expected), json.load(observed))
def test_choice_filter_choice_fields(self):
@@ -217,8 +220,8 @@ def test_equivalency(self):
"yes_or_no_question",
]
for fixture in equivalent_fixtures:
- xls_path = utils.path_to_text_fixture("%s.xls" % fixture)
- csv_path = utils.path_to_text_fixture("%s.csv" % fixture)
+ xls_path = utils.path_to_text_fixture(f"{fixture}.xls")
+ csv_path = utils.path_to_text_fixture(f"{fixture}.csv")
xls_inp = xls_to_dict(xls_path)
csv_inp = csv_to_dict(csv_path)
self.maxDiff = None
diff --git a/tests/test_xlsform_headers.py b/tests/test_xlsform_headers.py
index b1fa3422..fdf2ee97 100644
--- a/tests/test_xlsform_headers.py
+++ b/tests/test_xlsform_headers.py
@@ -1,6 +1,7 @@
"""
Test XLSForm headers syntax.
"""
+
from tests.pyxform_test_case import PyxformTestCase
diff --git a/tests/utils.py b/tests/utils.py
index b2b2344d..89edc588 100644
--- a/tests/utils.py
+++ b/tests/utils.py
@@ -1,22 +1,19 @@
"""
The tests utils module functionality.
"""
+
import configparser
import os
import shutil
import tempfile
import textwrap
from contextlib import contextmanager
-from typing import TYPE_CHECKING
from pyxform import file_utils
from pyxform.builder import create_survey, create_survey_from_path
from tests import example_xls
-if TYPE_CHECKING:
- from typing import Tuple
-
def path_to_text_fixture(filename):
return os.path.join(example_xls.PATH, filename)
@@ -46,7 +43,7 @@ def prep_class_config(cls, test_dir="tests"):
cls.cls_name = cls.__name__
-def prep_for_xml_contains(text: str) -> "Tuple[str]":
+def prep_for_xml_contains(text: str) -> "tuple[str]":
"""Prep string for finding an exact match to formatted XML text."""
# noinspection PyRedundantParentheses
return (textwrap.indent(textwrap.dedent(text), " "),)
diff --git a/tests/xform_test_case/test_attribute_columns.py b/tests/xform_test_case/test_attribute_columns.py
index e560ff3b..612c87e6 100644
--- a/tests/xform_test_case/test_attribute_columns.py
+++ b/tests/xform_test_case/test_attribute_columns.py
@@ -1,6 +1,7 @@
"""
Some tests for the new (v0.9) spec is properly implemented.
"""
+
import os
import pyxform
@@ -30,7 +31,8 @@ def test_conversion(self):
survey.print_xform_to_file(self.output_path, warnings=warnings)
# print warnings
# Compare with the expected output:
- with open(expected_output_path, encoding="utf-8") as expected, open(
- self.output_path, encoding="utf-8"
- ) as observed:
+ with (
+ open(expected_output_path, encoding="utf-8") as expected,
+ open(self.output_path, encoding="utf-8") as observed,
+ ):
self.assertXFormEqual(expected.read(), observed.read())
diff --git a/tests/xform_test_case/test_bugs.py b/tests/xform_test_case/test_bugs.py
index df627593..350fbb2a 100644
--- a/tests/xform_test_case/test_bugs.py
+++ b/tests/xform_test_case/test_bugs.py
@@ -1,6 +1,7 @@
"""
Some tests for the new (v0.9) spec is properly implemented.
"""
+
import os
from unittest import TestCase
@@ -93,9 +94,10 @@ def test_conversion(self):
survey.print_xform_to_file(self.output_path, warnings=warnings)
# print warnings
# Compare with the expected output:
- with open(expected_output_path, encoding="utf-8") as expected, open(
- self.output_path, encoding="utf-8"
- ) as observed:
+ with (
+ open(expected_output_path, encoding="utf-8") as expected,
+ open(self.output_path, encoding="utf-8") as observed,
+ ):
self.assertXFormEqual(expected.read(), observed.read())
@@ -118,9 +120,10 @@ def test_conversion(self):
survey.print_xform_to_file(self.output_path, warnings=warnings)
# print warnings
# Compare with the expected output:
- with open(expected_output_path, encoding="utf-8") as expected, open(
- self.output_path, encoding="utf-8"
- ) as observed:
+ with (
+ open(expected_output_path, encoding="utf-8") as expected,
+ open(self.output_path, encoding="utf-8") as observed,
+ ):
self.assertXFormEqual(expected.read(), observed.read())
@@ -145,9 +148,10 @@ def test_conversion(self):
survey.print_xform_to_file(output_path, warnings=warnings)
# print warnings
# Compare with the expected output:
- with open(expected_output_path, encoding="utf-8") as expected, open(
- output_path, encoding="utf-8"
- ) as observed:
+ with (
+ open(expected_output_path, encoding="utf-8") as expected,
+ open(output_path, encoding="utf-8") as observed,
+ ):
self.assertXFormEqual(expected.read(), observed.read())
diff --git a/tests/xform_test_case/test_xlsform_spec.py b/tests/xform_test_case/test_xlsform_spec.py
index 80c5ba5c..a5f21513 100644
--- a/tests/xform_test_case/test_xlsform_spec.py
+++ b/tests/xform_test_case/test_xlsform_spec.py
@@ -1,6 +1,7 @@
"""
Some tests for the new (v0.9) spec is properly implemented.
"""
+
import os
from unittest import TestCase
@@ -35,9 +36,10 @@ def compare_xform(self, file_name: str, set_default_name: bool = True):
survey = builder.create_survey_element_from_dict(json_survey)
survey.print_xform_to_file(self.output_path, warnings=warnings)
# Compare with the expected output:
- with open(expected_output_path, encoding="utf-8") as ef, open(
- self.output_path, encoding="utf-8"
- ) as af:
+ with (
+ open(expected_output_path, encoding="utf-8") as ef,
+ open(self.output_path, encoding="utf-8") as af,
+ ):
expected = ef.read()
observed = af.read()
self.assertXFormEqual(expected, observed)
diff --git a/tests/xform_test_case/test_xml.py b/tests/xform_test_case/test_xml.py
index cfeb4271..e1e19f86 100644
--- a/tests/xform_test_case/test_xml.py
+++ b/tests/xform_test_case/test_xml.py
@@ -1,6 +1,7 @@
"""
Test XForm XML syntax.
"""
+
from unittest import TestCase
from xml.dom.minidom import getDOMImplementation
diff --git a/tests/xpath_helpers/choices.py b/tests/xpath_helpers/choices.py
index 4489199c..967ba08b 100644
--- a/tests/xpath_helpers/choices.py
+++ b/tests/xpath_helpers/choices.py
@@ -1,5 +1,3 @@
-from typing import Tuple
-
JR_PREFIXES = {
"audio": "jr://audio/",
"image": "jr://images/",
@@ -14,7 +12,7 @@ class XPathHelper:
"""
@staticmethod
- def model_instance_choices_label(cname: str, choices: Tuple[Tuple[str, str], ...]):
+ def model_instance_choices_label(cname: str, choices: tuple[tuple[str, str], ...]):
"""Model instance has choices elements with name and label."""
choices_xp = "\n and ".join(
(
@@ -29,7 +27,7 @@ def model_instance_choices_label(cname: str, choices: Tuple[Tuple[str, str], ...
"""
@staticmethod
- def model_instance_choices_itext(cname: str, choices: Tuple[str, ...]):
+ def model_instance_choices_itext(cname: str, choices: tuple[str, ...]):
"""Model instance has choices elements with name but no label."""
choices_xp = "\n and ".join(
(
@@ -67,7 +65,7 @@ def model_itext_choice_text_label_by_ref(qname, lang, cname, label):
"""
@staticmethod
- def model_itext_choice_text_label_by_pos(lang, cname, choices: Tuple[str, ...]):
+ def model_itext_choice_text_label_by_pos(lang, cname, choices: tuple[str, ...]):
"""Model itext has a text label and no other forms. Lookup by position."""
choices_xp = "\n and ".join(
(
@@ -89,7 +87,7 @@ def model_itext_choice_text_label_by_pos(lang, cname, choices: Tuple[str, ...]):
@staticmethod
def model_itext_choice_media_by_pos(
- lang, cname, choices: Tuple[Tuple[Tuple[str, ...]]]
+ lang, cname, choices: tuple[tuple[tuple[str, ...]]]
):
"""
Model itext has a text label and no other forms. Lookup by position.
@@ -118,7 +116,7 @@ def model_itext_choice_media_by_pos(
@staticmethod
def model_no_itext_choice_media_by_pos(
- lang, cname, choices: Tuple[Tuple[Tuple[str, ...]]]
+ lang, cname, choices: tuple[tuple[tuple[str, ...]]]
):
"""
Model itext has a text label and no other forms. Lookup by position.