diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index 6d1f38f9a..02a02c774 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -9,7 +9,7 @@ jobs: runs-on: ${{ matrix.os }} strategy: matrix: - python: ['3.10'] + python: ['3.12'] os: [ubuntu-latest] steps: - uses: actions/checkout@v4 diff --git a/.github/workflows/verify.yml b/.github/workflows/verify.yml index dd7e05a52..ccad6f271 100644 --- a/.github/workflows/verify.yml +++ b/.github/workflows/verify.yml @@ -7,7 +7,7 @@ jobs: runs-on: ${{ matrix.os }} strategy: matrix: - python: ['3.10'] + python: ['3.12'] os: [ubuntu-latest] steps: - uses: actions/checkout@v4 diff --git a/README.rst b/README.rst index 28a5a6257..fc0fe3a66 100644 --- a/README.rst +++ b/README.rst @@ -44,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.10, 3.11, 3.12, and 3.13. +The currently supported Python versions for ``pyxform`` are 3.10 to 3.13 (the primary development version is 3.12). If this is different from the version you use for other projects, consider using `pyenv `_ to manage multiple versions of Python. Running pyxform from local source --------------------------------- Note that you must uninstall any globally installed ``pyxform`` instance in order to use local modules. Please install java 8 or newer version. -From the command line, complete the following. These steps use a `virtualenv `_ to make dependency management easier, and to keep the global site-packages directory clean:: +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 @@ -59,7 +59,7 @@ From the command line, complete the following. These steps use a `virtualenv " % ( - answer_count, - placed_count, - orphan_count, - ) + return f"" diff --git a/pyxform/parsing/expression.py b/pyxform/parsing/expression.py index d94c0c940..972cff33a 100644 --- a/pyxform/parsing/expression.py +++ b/pyxform/parsing/expression.py @@ -77,7 +77,7 @@ def tokenizer(scan, value) -> ExpLexerToken | str: class ExpLexerToken: - __slots__ = ("name", "value", "start", "end") + __slots__ = ("end", "name", "start", "value") def __init__(self, name: str, value: str, start: int, end: int) -> None: self.name: str = name diff --git a/pyxform/parsing/sheet_headers.py b/pyxform/parsing/sheet_headers.py index 9c69e2421..7337e5da8 100644 --- a/pyxform/parsing/sheet_headers.py +++ b/pyxform/parsing/sheet_headers.py @@ -50,7 +50,7 @@ def merge_dicts( # Union keys but retain order (as opposed to set()), preferencing dict_a then dict_b. # E.g. {"a": 1, "b": 2} + {"c": 3, "a": 4} -> {"a": None, "b": None, "c": None} out_dict = dict_a - for key in {k: None for k in (chain(dict_a, dict_b))}: + for key in dict.fromkeys(chain(dict_a, dict_b)): out_dict[key] = merge_dicts(dict_a.get(key), dict_b.get(key), default_key) return out_dict @@ -66,7 +66,7 @@ def list_to_nested_dict(lst: Sequence) -> dict: class DealiasAndGroupHeadersResult: - __slots__ = ("headers", "data") + __slots__ = ("data", "headers") def __init__(self, headers: tuple[tuple[str, ...], ...], data: Sequence[dict]): """ diff --git a/pyxform/question.py b/pyxform/question.py index 7e3442da5..bf4850804 100644 --- a/pyxform/question.py +++ b/pyxform/question.py @@ -146,7 +146,7 @@ def xml_instance(self, survey: "Survey", **kwargs): def xml_control(self, survey: "Survey"): if self.type == "calculate" or ( - (self.bind is not None and "calculate" in self.bind or self.trigger) + ((self.bind is not None and "calculate" in self.bind) or self.trigger) and not (self.label or self.hint) ): nested_setvalues = survey.get_trigger_values_for_question_name( diff --git a/pyxform/survey.py b/pyxform/survey.py index bad9b2a2e..07d5e6321 100644 --- a/pyxform/survey.py +++ b/pyxform/survey.py @@ -48,7 +48,7 @@ class InstanceInfo: """Standardise Instance details relevant during XML generation.""" - __slots__ = ("type", "context", "name", "src", "instance") + __slots__ = ("context", "instance", "name", "src", "type") def __init__( self, diff --git a/pyxform/survey_element.py b/pyxform/survey_element.py index 242681f4d..4b1e6c455 100644 --- a/pyxform/survey_element.py +++ b/pyxform/survey_element.py @@ -276,7 +276,7 @@ def condition(e): reversed(tuple(i[0] for i in self.iter_ancestors(condition=condition))), self_element, ) - new_value = f'/{"/".join(n.name for n in lineage)}' + new_value = f"/{'/'.join(n.name for n in lineage)}" self._survey_element_xpath = new_value return new_value return current_value @@ -287,8 +287,7 @@ def _delete_keys_from_dict(self, dictionary: dict, keys: Iterable[str]): Credits: https://stackoverflow.com/a/49723101 """ for key in keys: - if key in dictionary: - del dictionary[key] + dictionary.pop(key, None) for value in dictionary.values(): if isinstance(value, dict): diff --git a/pyxform/validators/updater.py b/pyxform/validators/updater.py index bd5df1b32..66b2a964d 100644 --- a/pyxform/validators/updater.py +++ b/pyxform/validators/updater.py @@ -87,7 +87,7 @@ def _request_latest_json(url): @staticmethod def _check_path(file_path): if not os.path.exists(file_path): - raise PyXFormError(f"Expected path does not exist: {file_path}" "") + raise PyXFormError(f"Expected path does not exist: {file_path}") else: return True @@ -463,7 +463,7 @@ def check(update_info): :type update_info: _UpdateInfo """ if not os.path.exists(update_info.installed_path): - message = "\nCheck failed!\n\n" "No installed release found." + message = "\nCheck failed!\n\nNo installed release found." raise PyXFormError(message) installed = _UpdateHandler._read_json(file_path=update_info.installed_path) @@ -512,8 +512,7 @@ def _install_check(bin_file_path=None): class EnketoValidateUpdater(_UpdateService): def __init__(self): self.update_info = _UpdateInfo( - api_url="https://api.github.com/repos/enketo/enketo-validate/" - "releases/latest", + api_url="https://api.github.com/repos/enketo/enketo-validate/releases/latest", repo_url="https://github.com/enketo/enketo-validate", validate_subfolder="enketo_validate", install_check=self._install_check, diff --git a/pyxform/validators/util.py b/pyxform/validators/util.py index 835150da2..6798dcc8b 100644 --- a/pyxform/validators/util.py +++ b/pyxform/validators/util.py @@ -125,7 +125,7 @@ def request_get(url): ) from http_err except URLError as url_err: raise PyXFormError( - f"Unable to reach a server. Reason: {url_err.reason}. " f"URL: {url}" + f"Unable to reach a server. Reason: {url_err.reason}. URL: {url}" ) from url_err diff --git a/pyxform/xls2json.py b/pyxform/xls2json.py index 93ab1c386..335ad049a 100644 --- a/pyxform/xls2json.py +++ b/pyxform/xls2json.py @@ -1067,15 +1067,15 @@ def workbook_to_json( constants.NAME: constants.OR_OTHER_CHOICE[ constants.NAME ], - constants.LABEL: { - lang: constants.OR_OTHER_CHOICE[constants.LABEL] - for lang in { + constants.LABEL: dict.fromkeys( + { lang for c in itemset_choices for lang in c[constants.LABEL] if isinstance(c.get(constants.LABEL), dict) - } - }, + }, + constants.OR_OTHER_CHOICE[constants.LABEL], + ), } ) else: diff --git a/tests/pyxform_test_case.py b/tests/pyxform_test_case.py index b3eb785f6..7d0cfbd17 100644 --- a/tests/pyxform_test_case.py +++ b/tests/pyxform_test_case.py @@ -347,8 +347,7 @@ def assertContains(self, content, text, count=None, msg_prefix=""): self.assertEqual( real_count, count, - msg_prefix + "Found %d instances of %s in content" - " (expected %d)" % (real_count, text_repr, count), + f"{msg_prefix}Found {real_count} instances of {text_repr} in content (expected {count})", ) else: self.assertTrue( diff --git a/tests/test_external_instances_for_selects.py b/tests/test_external_instances_for_selects.py index 619fd61d6..f66cdc488 100644 --- a/tests/test_external_instances_for_selects.py +++ b/tests/test_external_instances_for_selects.py @@ -10,6 +10,7 @@ from pyxform import aliases from pyxform.constants import EXTERNAL_INSTANCE_EXTENSIONS from pyxform.errors import PyXFormError +from pyxform.validators.pyxform import select_from_file from pyxform.xls2json_backends import md_table_to_workbook from pyxform.xls2xform import get_xml_path, xls2xform_convert @@ -204,8 +205,6 @@ def test_param_value_and_label_validation(self): | | type | name | label | parameters | | | {q} cities{e} | city | City | {p} | """ - from pyxform.validators.pyxform import select_from_file - q_types = ("select_one_from_file", "select_multiple_from_file") good_params = ( "value=val", diff --git a/tests/test_sheet_columns.py b/tests/test_sheet_columns.py index 18ff4b9f5..980238539 100644 --- a/tests/test_sheet_columns.py +++ b/tests/test_sheet_columns.py @@ -718,7 +718,7 @@ def test_dealias_and_group_headers__use_double_colon_modes(self): observed = dealias_and_group_headers( sheet_name="test", sheet_data=case[0], - sheet_header=[{k: None for k in case[0][0]}], + sheet_header=[dict.fromkeys(case[0][0])], header_aliases={}, header_columns=set(), ) diff --git a/tests/test_xls2xform.py b/tests/test_xls2xform.py index 1cfc4ee9b..aa725a363 100644 --- a/tests/test_xls2xform.py +++ b/tests/test_xls2xform.py @@ -24,7 +24,7 @@ ) from tests import example_xls -from tests.utils import get_temp_file, path_to_text_fixture +from tests.utils import get_temp_dir, get_temp_file, path_to_text_fixture class XLS2XFormTests(TestCase): @@ -347,7 +347,6 @@ def test_args_combinations__ok(self): def test_invalid_input_raises(self): """Should raise an error for invalid input or file types.""" msg = "Argument 'definition' was not recognized as a supported type" - from tests.utils import get_temp_dir, get_temp_file with get_temp_file() as empty, get_temp_dir() as td: bad_xls = Path(td) / "bad.xls"