Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

661: update supported python versions #706

Merged
merged 1 commit into from
May 31, 2024
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.8']
python: ['3.10']
os: [ubuntu-latest]
steps:
- uses: actions/checkout@v4
Expand Down
4 changes: 2 additions & 2 deletions .github/workflows/verify.yml
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,7 @@ jobs:
runs-on: ${{ matrix.os }}
strategy:
matrix:
python: ['3.8']
python: ['3.10']
os: [ubuntu-latest]
steps:
- uses: actions/checkout@v4
Expand Down Expand Up @@ -39,7 +39,7 @@ jobs:
# Run all matrix jobs even if one of them fails.
fail-fast: false
matrix:
python: ['3.7', '3.8', '3.9']
python: ['3.10', '3.11', '3.12']
os: [ubuntu-latest, macos-latest, windows-latest]
include:
- os: windows-latest
Expand Down
15 changes: 6 additions & 9 deletions README.rst
Original file line number Diff line number Diff line change
Expand Up @@ -2,17 +2,14 @@
pyxform
========

|pypi| |python| |black|
|pypi| |python|

.. |pypi| image:: https://badge.fury.io/py/pyxform.svg
:target: https://badge.fury.io/py/pyxform

.. |python| image:: https://img.shields.io/badge/python-3.7,3.8,3.9-blue.svg
.. |python| image:: https://img.shields.io/badge/python-3.10,3.11,3.12-blue.svg
:target: https://www.python.org/downloads

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

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

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

xls2xform path_to_XLSForm [output_path]

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

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

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

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

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

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

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

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

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

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

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

from pyxform import constants

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

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

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

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

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

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

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

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

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

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

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

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

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


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

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

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


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


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


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

from pyxform.survey_element import SurveyElement


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

import glob
import os

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

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

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

from pyxform.utils import parse_expression

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

from pyxform.utils import BRACKETED_TAG_REGEX, EXPRESSION_LEXER, ExpLexerToken

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


def find_boundaries(xml_text: str) -> List[Tuple[int, int]]:
def find_boundaries(xml_text: str) -> list[tuple[int, int]]:
"""
Find token boundaries of any instance() expression.

Expand Down Expand Up @@ -91,7 +91,7 @@ def find_boundaries(xml_text: str) -> List[Tuple[int, int]]:

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


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

import os.path

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

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

from pyxform.xls2json import QuestionTypesReader, print_pyobj_to_json


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

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