From 51b76f58caa6a2a514a1a650c4d58d3525cf25cc Mon Sep 17 00:00:00 2001 From: Michael Tinning Date: Wed, 1 Jul 2026 10:13:17 +0000 Subject: [PATCH 1/3] Update Python support to 3.14 --- .circleci/config.yml | 6 ++--- .gitignore | 4 +-- .python-version | 1 + pyqttoolkit/scripting/script_runner.py | 4 ++- pyqttoolkit/services/task_runner.py | 3 +++ scripts/check_license_headers.py | 37 ++++++++++++++++++++++++++ setup.py | 5 ++-- tests/services/test_task_runner.py | 9 ++++--- tox.ini | 4 +-- versioneer.py | 4 +-- 10 files changed, 60 insertions(+), 17 deletions(-) create mode 100644 .python-version create mode 100644 scripts/check_license_headers.py diff --git a/.circleci/config.yml b/.circleci/config.yml index c709e11..824c8e2 100644 --- a/.circleci/config.yml +++ b/.circleci/config.yml @@ -19,7 +19,7 @@ jobs: build: docker: # specify the version you desire here - - image: circleci/python:3.6 + - image: cimg/python:3.14.6 working_directory: ~/repo @@ -47,7 +47,7 @@ jobs: name: run tests command: | . .venv/bin/activate - tox -e py36 + tox -e py314 - save_cache: paths: @@ -71,7 +71,7 @@ jobs: deploy: docker: - - image: circleci/python:3.6 + - image: cimg/python:3.14.6 steps: - checkout # Download and cache dependencies diff --git a/.gitignore b/.gitignore index 814b673..28d7dd6 100644 --- a/.gitignore +++ b/.gitignore @@ -1,5 +1,5 @@ .* +!.gitignore +!.python-version *.egg-info **/__pycache__/** -!.gitignore -*.egg-info \ No newline at end of file diff --git a/.python-version b/.python-version new file mode 100644 index 0000000..3f0a10f --- /dev/null +++ b/.python-version @@ -0,0 +1 @@ +3.14.6 diff --git a/pyqttoolkit/scripting/script_runner.py b/pyqttoolkit/scripting/script_runner.py index 3c378a1..1417b26 100644 --- a/pyqttoolkit/scripting/script_runner.py +++ b/pyqttoolkit/scripting/script_runner.py @@ -72,8 +72,10 @@ def run(self, script, context): try: self._context = context with redirect_output(self._context, self._skip_empty_output_lines): + namespace = self._globals(context) + namespace.update(context.locals) #pylint: disable=exec-used - exec(code, self._globals(context), context.locals) + exec(code, namespace, namespace) #pylint: enable=exec-used except ScriptStopped: pass diff --git a/pyqttoolkit/services/task_runner.py b/pyqttoolkit/services/task_runner.py index f089cdb..8ba9029 100644 --- a/pyqttoolkit/services/task_runner.py +++ b/pyqttoolkit/services/task_runner.py @@ -177,6 +177,9 @@ def __init__(self, parent): self._is_cancelled = False self._executor = ThreadPoolExecutor(max_workers=1, thread_name_prefix='TaskRunner_Thread') + def shutdown(self, wait=True): + self._executor.shutdown(wait=wait) + def run_task(self, task_function, task_args=None, on_completed=None, on_cancelled=None, on_error=None, description=None, error_description=None, show_progress=True, cancellable=False, force_indeterminate_start=False, **kwargs): """function::runTask(self, task_function, task_args, on_completed, on_error) :param task_function: The function to execute diff --git a/scripts/check_license_headers.py b/scripts/check_license_headers.py new file mode 100644 index 0000000..2fc5b78 --- /dev/null +++ b/scripts/check_license_headers.py @@ -0,0 +1,37 @@ +from pathlib import Path + + +ROOT = Path(__file__).resolve().parents[1] +PACKAGE_DIR = ROOT / "pyqttoolkit" +LICENSE_PATH = ROOT / "LICENSE_SHORT" +EXCLUDED_FILENAMES = {"_version.py"} + + +def commented_license_text() -> str: + lines = LICENSE_PATH.read_text().splitlines() + return "\n".join(f"# {line}" if line else "#" for line in lines) + + +def main() -> int: + expected_header = commented_license_text() + missing_headers = [] + + for path in sorted(PACKAGE_DIR.rglob("*.py")): + if path.name in EXCLUDED_FILENAMES: + continue + + content = path.read_text().replace("\r\n", "\n") + if not content.startswith(expected_header): + missing_headers.append(path.relative_to(ROOT)) + + if missing_headers: + print("Missing license header:") + for path in missing_headers: + print(path) + return 1 + + return 0 + + +if __name__ == "__main__": + raise SystemExit(main()) diff --git a/setup.py b/setup.py index d6a1a00..1bf37e9 100644 --- a/setup.py +++ b/setup.py @@ -23,8 +23,7 @@ 'pytest-cov', 'pytest-xvfb', 'pytest-asyncio', - 'pytest-qt', - 'licensify' + 'pytest-qt' ] extras_require = { @@ -49,5 +48,5 @@ description='A toolkit for PyQt 5', long_description=README_CONTENTS, long_description_content_type='text/markdown', - python_requires='>=3.6' + python_requires='>=3.14' ) diff --git a/tests/services/test_task_runner.py b/tests/services/test_task_runner.py index dc917c4..edbcacd 100644 --- a/tests/services/test_task_runner.py +++ b/tests/services/test_task_runner.py @@ -7,7 +7,6 @@ from pyqttoolkit.services.task_runner import * from pytestqt.exceptions import capture_exceptions -from pytestqt.qt_compat import qt_api class Result: def __init__(self): @@ -22,8 +21,10 @@ def value(self): @pytest.fixture def task_runner(qtbot): - app = QApplication([]) - yield TaskRunner(app) + app = QApplication.instance() or QApplication([]) + runner = TaskRunner(app) + yield runner + runner.shutdown() def _handler(event, result=None): def _(value): @@ -115,7 +116,7 @@ def _task(): with capture_exceptions() as exceptions: task_runner.run_task(_task) time.sleep(1) - qt_api.QApplication.instance().processEvents() + QApplication.instance().processEvents() assert len(exceptions) == 1 assert exceptions[0][1] == exception diff --git a/tox.ini b/tox.ini index 4120c0f..1889a01 100644 --- a/tox.ini +++ b/tox.ini @@ -1,9 +1,9 @@ [tox] -envlist=py36 +envlist=py314 [testenv] extras= test commands= ; pytest tests --cov pyqttoolkit --ignore tests/services/test_task_runner.py - licensify LICENSE_SHORT --directory pyqttoolkit --files *.py --exclude _version.py --check + python scripts/check_license_headers.py diff --git a/versioneer.py b/versioneer.py index 64fea1c..3aa5da3 100644 --- a/versioneer.py +++ b/versioneer.py @@ -339,9 +339,9 @@ def get_config_from_root(root): # configparser.NoOptionError (if it lacks "VCS="). See the docstring at # the top of versioneer.py for instructions on writing your setup.cfg . setup_cfg = os.path.join(root, "setup.cfg") - parser = configparser.SafeConfigParser() + parser = configparser.ConfigParser() with open(setup_cfg, "r") as f: - parser.readfp(f) + parser.read_file(f) VCS = parser.get("versioneer", "VCS") # mandatory def get(parser, name): From 15704e5c15ce904e129e772e2f414c8b51441ce6 Mon Sep 17 00:00:00 2001 From: Michael Tinning Date: Wed, 1 Jul 2026 10:20:07 +0000 Subject: [PATCH 2/3] Explain Python 3.14 compatibility fixes --- pyqttoolkit/scripting/script_runner.py | 2 ++ pyqttoolkit/services/task_runner.py | 2 ++ 2 files changed, 4 insertions(+) diff --git a/pyqttoolkit/scripting/script_runner.py b/pyqttoolkit/scripting/script_runner.py index 1417b26..0e45967 100644 --- a/pyqttoolkit/scripting/script_runner.py +++ b/pyqttoolkit/scripting/script_runner.py @@ -72,6 +72,8 @@ def run(self, script, context): try: self._context = context with redirect_output(self._context, self._skip_empty_output_lines): + # Use one namespace so functions defined by the script can + # resolve each other recursively through their globals. namespace = self._globals(context) namespace.update(context.locals) #pylint: disable=exec-used diff --git a/pyqttoolkit/services/task_runner.py b/pyqttoolkit/services/task_runner.py index 8ba9029..12ac458 100644 --- a/pyqttoolkit/services/task_runner.py +++ b/pyqttoolkit/services/task_runner.py @@ -178,6 +178,8 @@ def __init__(self, parent): self._executor = ThreadPoolExecutor(max_workers=1, thread_name_prefix='TaskRunner_Thread') def shutdown(self, wait=True): + # Tests and short-lived apps need an explicit way to join worker + # threads before QApplication teardown. self._executor.shutdown(wait=wait) def run_task(self, task_function, task_args=None, on_completed=None, on_cancelled=None, on_error=None, description=None, error_description=None, show_progress=True, cancellable=False, force_indeterminate_start=False, **kwargs): From b1ba30656aa9ca36005ade1db4f14f251f5cc51b Mon Sep 17 00:00:00 2001 From: Michael Tinning Date: Wed, 1 Jul 2026 10:30:05 +0000 Subject: [PATCH 3/3] Fix deploy packaging dependencies --- setup.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/setup.py b/setup.py index 1bf37e9..4aea8b0 100644 --- a/setup.py +++ b/setup.py @@ -28,7 +28,7 @@ extras_require = { 'test': tests_require, - 'publish': ['twine'], + 'publish': ['setuptools', 'twine'], 'ui': ['pyqt5'] }