Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion .github/workflows/release.yml
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,7 @@ jobs:
runs-on: ${{ matrix.os }}
strategy:
matrix:
python: ['3.10']
python: ['3.12']
os: [ubuntu-latest]
steps:
- uses: actions/checkout@v4
Expand Down
2 changes: 1 addition & 1 deletion .github/workflows/verify.yml
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,7 @@ jobs:
runs-on: ${{ matrix.os }}
strategy:
matrix:
python: ['3.10']
python: ['3.12']
os: [ubuntu-latest]
steps:
- uses: actions/checkout@v4
Expand Down
8 changes: 4 additions & 4 deletions README.rst
Original file line number Diff line number Diff line change
Expand Up @@ -44,22 +44,22 @@ The ``xls2xform`` command can then be used::

xls2xform path_to_XLSForm [output_path]

The currently supported Python versions for ``pyxform`` are 3.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 <https://github.com/pyenv/pyenv>`_ to manage multiple versions of Python.

Running pyxform from local source
---------------------------------

Note that you must uninstall any globally installed ``pyxform`` instance in order to use local modules. Please install java 8 or newer version.

From the command line, complete the following. These steps use a `virtualenv <https://docs.python.org/3.10/tutorial/venv.html>`_ to make dependency management easier, and to keep the global site-packages directory clean::
From the command line, complete the following. These steps use a virtualenv to make dependency management easier, and to keep the global site-packages directory clean::

# Get a copy of the repository.
mkdir -P ~/repos/pyxform
cd ~/repos/pyxform
git clone https://github.com/XLSForm/pyxform.git repo

# Create and activate a virtual environment for the install.
/usr/local/bin/python3.10 -m venv venv
/usr/local/bin/python -m venv venv
. venv/bin/activate

# Install the pyxform and it's production dependencies.
Expand Down Expand Up @@ -154,7 +154,7 @@ Releases are now automatic. These instructions are provided for forks or for a f
1. In a clean new release only directory, check out master.
2. Create a new virtualenv in this directory to ensure a clean Python environment::

/usr/local/bin/python3.10 -m venv pyxform-release
/usr/local/bin/python -m venv pyxform-release
. pyxform-release/bin/activate

3. Install the production and packaging requirements::
Expand Down
10 changes: 5 additions & 5 deletions pyproject.toml
Original file line number Diff line number Diff line change
Expand Up @@ -9,17 +9,17 @@ readme = "README.rst"
requires-python = ">=3.10"
dependencies = [
"xlrd==2.0.1", # Read XLS files
"openpyxl==3.1.3", # Read XLSX files
"openpyxl==3.1.5", # Read XLSX files
"defusedxml==0.7.1", # Parse XML
]

[project.optional-dependencies]
# Install with `pip install pyxform[dev]`.
dev = [
"formencode==2.1.1", # Compare XML
"lxml==5.3.0", # XPath test expressions
"psutil==6.1.0", # Process info for performance tests
"ruff==0.7.1", # Format and lint
"lxml==6.0.0", # XPath test expressions
"psutil==7.0.0", # Process info for performance tests
"ruff==0.12.4", # Format and lint
]

[project.urls]
Expand Down Expand Up @@ -75,6 +75,7 @@ ignore = [
"F821", # undefined-name (doesn't work well with type hints, ruff 0.1.11).
"PERF401", # manual-list-comprehension (false positives on selective transforms)
"PERF402", # manual-list-copy (false positives on selective transforms)
"PLC0415", # import-outside-top-level (pyxform has a few to avoid circular imports)
"PLR0911", # too-many-return-statements (complexity not useful to warn every time)
"PLR0912", # too-many-branches (complexity not useful to warn every time)
"PLR0913", # too-many-arguments (complexity not useful to warn every time)
Expand All @@ -83,7 +84,6 @@ ignore = [
"PLW2901", # redefined-loop-name (usually not a bug)
"RUF001", # ambiguous-unicode-character-string (false positives on unicode tests)
"S310", # suspicious-url-open-usage (prone to false positives, ruff 0.1.11)
"S320", # suspicious-xmle-tree-usage (according to defusedxml author lxml is safe)
"S603", # subprocess-without-shell-equals-true (prone to false positives, ruff 0.1.11)
"TRY003", # raise-vanilla-args (reasonable lint but would require large refactor)
]
Expand Down
4 changes: 1 addition & 3 deletions pyxform/builder.py
Original file line number Diff line number Diff line change
Expand Up @@ -335,9 +335,7 @@ def create_survey(

# assert name_of_main_section in sections, name_of_main_section
if "id_string" not in main_section:
main_section["id_string"] = (
name_of_main_section if id_string is None else name_of_main_section
)
main_section["id_string"] = name_of_main_section
survey = builder.create_survey_element_from_dict(main_section)

# not sure where to do this without repeating ourselves,
Expand Down
6 changes: 1 addition & 5 deletions pyxform/instance.py
Original file line number Diff line number Diff line change
Expand Up @@ -90,8 +90,4 @@ def __unicode__(self):
orphan_count = len(self._orphan_answers.keys())
placed_count = len(self._answers.keys())
answer_count = orphan_count + placed_count
return "<Instance (%d answers: %d placed. %d orphans)>" % (
answer_count,
placed_count,
orphan_count,
)
return f"<Instance ({answer_count} answers: {placed_count} placed. {orphan_count} orphans)>"
2 changes: 1 addition & 1 deletion pyxform/parsing/expression.py
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
4 changes: 2 additions & 2 deletions pyxform/parsing/sheet_headers.py
Original file line number Diff line number Diff line change
Expand Up @@ -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

Expand All @@ -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]):
"""
Expand Down
2 changes: 1 addition & 1 deletion pyxform/question.py
Original file line number Diff line number Diff line change
Expand Up @@ -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(
Expand Down
2 changes: 1 addition & 1 deletion pyxform/survey.py
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand Down
5 changes: 2 additions & 3 deletions pyxform/survey_element.py
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand All @@ -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):
Expand Down
7 changes: 3 additions & 4 deletions pyxform/validators/updater.py
Original file line number Diff line number Diff line change
Expand Up @@ -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

Expand Down Expand Up @@ -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)
Expand Down Expand Up @@ -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,
Expand Down
2 changes: 1 addition & 1 deletion pyxform/validators/util.py
Original file line number Diff line number Diff line change
Expand Up @@ -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


Expand Down
10 changes: 5 additions & 5 deletions pyxform/xls2json.py
Original file line number Diff line number Diff line change
Expand Up @@ -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:
Expand Down
3 changes: 1 addition & 2 deletions tests/pyxform_test_case.py
Original file line number Diff line number Diff line change
Expand Up @@ -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(
Expand Down
3 changes: 1 addition & 2 deletions tests/test_external_instances_for_selects.py
Original file line number Diff line number Diff line change
Expand Up @@ -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

Expand Down Expand Up @@ -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",
Expand Down
2 changes: 1 addition & 1 deletion tests/test_sheet_columns.py
Original file line number Diff line number Diff line change
Expand Up @@ -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(),
)
Expand Down
3 changes: 1 addition & 2 deletions tests/test_xls2xform.py
Original file line number Diff line number Diff line change
Expand Up @@ -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):
Expand Down Expand Up @@ -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"
Expand Down
Loading