From 5d28e2fffd847905b847173bac48670668087075 Mon Sep 17 00:00:00 2001 From: "bb-agent-dev[bot]" <325975826+bb-agent-dev[bot]@users.noreply.github.com> Date: Mon, 19 Oct 2026 09:46:12 +0000 Subject: [PATCH] Validate dependency baseline and modernize isolated builds --- .circleci/config.yml | 26 ++-- README.md | 26 +++- docs/dependency-baseline.txt | 83 ++++++++++++ docs/dependency-candidate.txt | 82 +++++++++++ docs/dependency-validation.md | 128 ++++++++++++++++++ pyproject.toml | 3 + setup.py | 10 +- tests/services/test_task_runner.py | 209 +++++++++++++++-------------- tox.ini | 7 +- 9 files changed, 455 insertions(+), 119 deletions(-) create mode 100644 docs/dependency-baseline.txt create mode 100644 docs/dependency-candidate.txt create mode 100644 docs/dependency-validation.md create mode 100644 pyproject.toml diff --git a/.circleci/config.yml b/.circleci/config.yml index b3afda7..1f4865e 100644 --- a/.circleci/config.yml +++ b/.circleci/config.yml @@ -35,9 +35,9 @@ jobs: # Download and cache dependencies - restore_cache: keys: - - v1-dependencies-{{ checksum "setup.py" }} + - v2-dependencies-{{ checksum "setup.py" }}-{{ checksum "pyproject.toml" }} # fallback to using the latest cache if no exact match is found - - v1-dependencies- + - v2-dependencies- # install dependencies - run: @@ -46,7 +46,7 @@ jobs: python3 -m venv .venv . .venv/bin/activate pip install --upgrade pip - pip install .[ui,test,publish] + pip install '.[ui,test,publish]' # run tests! - run: @@ -55,12 +55,19 @@ jobs: . .venv/bin/activate tox -e py314 + - run: + name: Validate distribution packages + command: | + . .venv/bin/activate + python -m build + python -m twine check --strict dist/* + - save_cache: paths: - ./.venv - ./.tox - .coverage - key: v1-dependencies-{{ checksum "setup.py" }} + key: v2-dependencies-{{ checksum "setup.py" }}-{{ checksum "pyproject.toml" }} # - run: # name: Generate reports @@ -83,9 +90,9 @@ jobs: # Download and cache dependencies - restore_cache: keys: - - v1-dependencies-{{ checksum "setup.py" }} + - v2-dependencies-{{ checksum "setup.py" }}-{{ checksum "pyproject.toml" }} # fallback to using the latest cache if no exact match is found - - v1-dependencies- + - v2-dependencies- # install dependencies - run: @@ -94,13 +101,14 @@ jobs: python3 -m venv .venv . .venv/bin/activate pip install --upgrade pip - pip install .[test,publish] + pip install '.[publish]' - run: name: Build packages command: | . .venv/bin/activate - python setup.py sdist + python -m build + python -m twine check --strict dist/* - run: name: Initialise .pypirc @@ -114,4 +122,4 @@ jobs: name: Upload package command: | . .venv/bin/activate - twine upload dist/* + python -m twine upload dist/* diff --git a/README.md b/README.md index d6e9a5d..5838c5f 100644 --- a/README.md +++ b/README.md @@ -1,3 +1,27 @@ # PyQtToolkit -A toolkit for PyQt \ No newline at end of file +A toolkit for PyQt + +## Development and packaging + +Use Python 3.14 or newer. Install the optional Qt bindings when developing UI +features; installing the base package does not install PyQt5. + +```sh +python -m venv .venv +. .venv/bin/activate +python -m pip install --upgrade pip +python -m pip install '.[ui,test,publish]' +python -m tox -e py314 +python -m build +python -m twine check --strict dist/* +``` + +The isolated build backend uses setuptools and the bundled Versioneer code to +produce a wheel and source distribution. Build from a Git checkout or an unpacked +source distribution so the release version can be determined. The `publish` extra +provides the build and upload tools; it is not needed by library consumers. + +The tox environment runs the full Qt test suite offscreen, including the task-runner +tests, and checks package license headers. See [the validation report](docs/dependency-validation.md) +for tested dependency versions, platform limits and source-review boundaries. diff --git a/docs/dependency-baseline.txt b/docs/dependency-baseline.txt new file mode 100644 index 0000000..a3cd428 --- /dev/null +++ b/docs/dependency-baseline.txt @@ -0,0 +1,83 @@ +# Resolved Python 3.14.6 environment, 2026-10-19; includes validation tools. +appdirs==1.4.4 +boolean.py==5.0 +build==1.6.1 +CacheControl==0.14.4 +cachetools==7.2.3 +certifi==2026.10.18 +cffi==2.1.1 +charset-normalizer==3.5.2 +colorama==0.4.6 +contourpy==1.4.0 +coverage==7.16.2 +cryptography==50.0.2 +cycler==0.12.1 +cyclonedx-python-lib==11.12.0 +defusedxml==0.7.1 +distlib==0.4.4 +docutils==0.23 +filelock==4.2.4 +fonttools==4.67.0 +id==1.6.1 +idna==3.20 +iniconfig==2.3.1 +jaraco.classes==3.4.0 +jaraco.context==6.1.2 +jaraco.functools==4.6.0 +jeepney==0.9.0 +Jinja2==3.1.6 +keyring==25.7.0 +kiwisolver==1.5.1 +license-expression==30.4.4 +markdown-it-py==4.2.0 +MarkupSafe==3.0.4 +matplotlib==3.11.2 +mdurl==0.1.2 +more-itertools==11.2.1 +msgpack==1.2.3 +nh3==0.3.7 +numpy==2.5.4 +packageurl-python==0.17.6 +packaging==26.3 +pandas==3.0.6 +pillow==13.0.0 +pip==26.2.1 +pip-requirements-parser==32.0.1 +pip_api==0.0.35 +pip_audit==2.10.1 +platformdirs==4.13.5 +pluggy==1.7.0 +py-serializable==2.1.0 +pycparser==3.11 +Pygments==2.21.0 +pyparsing==3.3.3 +pyproject-api==1.12.0 +pyproject_hooks==1.3.3 +PyQt5==5.15.11 +PyQt5-Qt5==5.15.19 +PyQt5_sip==12.20.0 +pyqttoolkit==0.9.0+3.g4d4f4e5 +pytest==9.1.1 +pytest-asyncio==1.4.0 +pytest-cov==7.1.0 +pytest-qt==4.5.0 +pytest-xvfb==3.1.1 +python-dateutil==2.9.0.post0 +python-discovery==1.6.4 +PyVirtualDisplay==3.0 +readme_renderer==46.0 +requests==2.34.2 +requests-toolbelt==1.0.0 +rfc3986==2.0.0 +rich==15.0.0 +SecretStorage==3.5.0 +setuptools==84.0.0 +six==1.17.0 +sortedcontainers==2.4.0 +tomli==2.5.0 +tomli_w==1.2.0 +tox==4.67.1 +twine==7.0.0 +typing_extensions==4.16.0 +urllib3==2.8.0 +virtualenv==21.14.11 diff --git a/docs/dependency-candidate.txt b/docs/dependency-candidate.txt new file mode 100644 index 0000000..32f830f --- /dev/null +++ b/docs/dependency-candidate.txt @@ -0,0 +1,82 @@ +# Resolved Python 3.14.6 environment, 2026-10-19; includes validation tools. +appdirs==1.4.4 +boolean.py==5.0 +build==1.6.1 +CacheControl==0.14.4 +cachetools==7.2.3 +certifi==2026.10.18 +cffi==2.1.1 +charset-normalizer==3.5.2 +colorama==0.4.6 +contourpy==1.4.0 +coverage==7.16.2 +cryptography==50.0.2 +cycler==0.12.1 +cyclonedx-python-lib==11.12.0 +defusedxml==0.7.1 +distlib==0.4.4 +docutils==0.23 +filelock==4.2.4 +fonttools==4.67.0 +id==1.6.1 +idna==3.20 +iniconfig==2.3.1 +jaraco.classes==3.4.0 +jaraco.context==6.1.2 +jaraco.functools==4.6.0 +jeepney==0.9.0 +Jinja2==3.1.6 +keyring==25.7.0 +kiwisolver==1.5.1 +license-expression==30.4.4 +markdown-it-py==4.2.0 +MarkupSafe==3.0.4 +matplotlib==3.11.2 +mdurl==0.1.2 +more-itertools==11.2.1 +msgpack==1.2.3 +nh3==0.3.7 +numpy==2.5.4 +packageurl-python==0.17.6 +packaging==26.3 +pandas==3.0.6 +pillow==13.0.0 +pip==26.2.1 +pip-requirements-parser==32.0.1 +pip_api==0.0.35 +pip_audit==2.10.1 +platformdirs==4.13.5 +pluggy==1.7.0 +py-serializable==2.1.0 +pycparser==3.11 +Pygments==2.21.0 +pyparsing==3.3.3 +pyproject-api==1.12.0 +pyproject_hooks==1.3.3 +PyQt5==5.15.11 +PyQt5-Qt5==5.15.19 +PyQt5_sip==12.20.0 +pyqttoolkit==0.9.0+3.g4d4f4e5.dirty +pytest==9.1.1 +pytest-asyncio==1.4.0 +pytest-cov==7.1.0 +pytest-qt==4.5.0 +pytest-xvfb==3.1.1 +python-dateutil==2.9.0.post0 +python-discovery==1.6.4 +PyVirtualDisplay==3.0 +readme_renderer==46.0 +requests==2.34.2 +requests-toolbelt==1.0.0 +rfc3986==2.0.0 +rich==15.0.0 +SecretStorage==3.5.0 +six==1.17.0 +sortedcontainers==2.4.0 +tomli==2.5.0 +tomli_w==1.2.0 +tox==4.67.1 +twine==7.0.0 +typing_extensions==4.16.0 +urllib3==2.8.0 +virtualenv==21.14.11 diff --git a/docs/dependency-validation.md b/docs/dependency-validation.md new file mode 100644 index 0000000..ded1c24 --- /dev/null +++ b/docs/dependency-validation.md @@ -0,0 +1,128 @@ +# Dependency and build validation — 2026-10-19 + +This review starts from master `4d4f4e505e6aee7fe05d2d5cabcd1249c2d63048` +(version `0.9.0+3.g4d4f4e5`). It preserves the untagged script-runner and Python +3.14 changes already merged after 0.9.0. No release version is invented here; +Versioneer continues to derive checkout and distribution versions from Git. + +## Changes and compatibility choices + +- Set the Jinja2 floor to 3.1.6, the fix for the public + [attr-filter advisory](https://github.com/advisories/GHSA-cpwx-vrp4-4pq7). + This is a dependency baseline; it does not turn ordinary templates into a sandbox. +- Declare the isolated PEP 517 backend as `setuptools>=83`, covering the public + [macOS sdist exclusion advisory](https://github.com/advisories/GHSA-h35f-9h28-mq5c). + The backend resolved to 84.0.0. Setuptools is now an isolated backend dependency; + the `publish` extra supplies build 1.6.1+ and twine 7.0.0+ instead. +- Keep matplotlib, pandas and appdirs interfaces and Qt optionality. No incompatible + package replacement or speculative upper bound was justified by these tests. + These open ranges are not a reproducible lock; the snapshots below record the + actual versions tested on this date. +- Use tox 4 and wheel packaging. Run tests outside the checkout so imports and + coverage come from the installed wheel. CircleCI builds and strictly checks + both distributions; existing tag filters, upload destination and credential + configuration are preserved. +- Retain bundled Versioneer 0.18 and its existing Python 3.14 compatibility patches. + The explicit source-directory insertion in setup.py is needed: removing it in + a disposable copy reproduced `ModuleNotFoundError: versioneer` with the PEP 517 + backend. No wholesale Versioneer/API migration was needed. + +## Exact resolved versions and live audit + +Both clean environments used Linux x86_64 / CPython 3.14.6 and pip 26.2.1. +[Baseline snapshot](dependency-baseline.txt) and +[candidate snapshot](dependency-candidate.txt) include all installed dependencies +plus the audit/build validation tools, not just runtime requirements. The local +project entry in the candidate snapshot is the pre-commit dirty build; the PR +records the clean commit/distribution version validated for delivery. + +| Component | Before | After | +| --- | --- | --- | +| matplotlib | 3.11.2 | 3.11.2 | +| Jinja2 | 3.1.6 | 3.1.6 | +| pandas / numpy | 3.0.6 / 2.5.4 | 3.0.6 / 2.5.4 | +| appdirs | 1.4.4 | 1.4.4 | +| PyQt5 / Qt runtime / sip | 5.15.11 / 5.15.19 / 12.20.0 | same | +| pytest / pytest-asyncio | 9.1.1 / 1.4.0 | same | +| pytest-qt / pytest-xvfb | 4.5.0 / 3.1.1 | same | +| pytest-cov / coverage | 7.1.0 / 7.16.2 | same | +| tox / virtualenv | 4.67.1 / 21.14.11 | same | +| build / twine | 1.6.1 / 7.0.0 | same | +| setuptools | 84.0.0 (publish environment) | 84.0.0 (isolated backend only) | +| pip-audit | 2.10.1 | 2.10.1 | + +`python -m pip_audit --format json` ran live against each environment. Before: +82 entries, 81 public packages audited, zero known vulnerabilities, one skipped +local pyqttoolkit version. After: 81 entries, 80 public packages audited, zero +known vulnerabilities, one skipped local pyqttoolkit version. An additional exact +requirements audit of isolated `setuptools==84.0.0` and `pip==26.2.1` found zero +known vulnerabilities and no skips. The public advisories above were also +rechecked directly. Fresh baseline resolution already selected patched packages; +this change establishes floors/build isolation rather than claiming a decrease +in the observed finding count. `pip check` passed before and after. + +The unpublished Git-derived project versions cannot be audited as PyPI releases. +The source review below complements that skip; neither it nor the scanner proves +absence of vulnerabilities. Future dependency releases require fresh validation. + +## Tests and distribution checks + +The original tox selection passed 72 tests. Investigating the seven excluded +task-runner tests first produced passing isolated runs, but the complete clean +wheel/tox run exposed a timeout despite an asyncio event being set by another +thread. The tests incorrectly combined cross-thread `asyncio.Event` use, busy +loops and Qt event processing. Thread-safe `threading.Event` gates, bounded waits, +Qt polling and failure cleanup now preserve the same completion/result, +cancellation, error and queue-order assertions. Production task-runner behavior +is unchanged; the blanket exclusion is removed. + +Final `python -m tox -e py314` passed **79 tests**, including all seven task-runner +cases and the existing script-runner regression, followed by the license-header +checker. Installed-wheel statement coverage was **30%**, not comprehensive +behavioral coverage. Existing syntax-escape warnings remain in UI highlighting +code. There is no configured pre-commit hook set in this repository; `git diff +--check` was also run. + +`python -m build` uses isolated setuptools and builds the wheel from the generated +sdist as well as producing that sdist. `python -m twine check --strict` passes for +both artifacts. Separate clean base and UI environments for **each** artifact +validate install, `pip check`, and imports from outside the checkout. Each checks +installed metadata against `pyqttoolkit.__version__`; base environments assert +PyQt5 is absent, and UI environments create an offscreen QApplication and +TaskRunner. Smoke checks also exercise pandas, metadata imports, Jinja templates +and recursive scripts, preserving the already-merged script fix. The PR records +the final clean commit, artifact version and hashes after commit. + +Reproduce with the README's setup and tox commands, then install each generated +artifact in separate virtual environments, using the artifact path with `[ui]` +for the Qt variant. Run all import checks from a temporary directory rather than +the checkout; do not bypass dependencies or reuse a UI environment for base proof. + +## Platform inventory and bounded source review + +The configured CI image is `cimg/python:3.14.6` on Linux, installing libgl1. +Local validation covers Linux x86_64 / Python 3.14.6, headless Qt and Python +wheel/sdist packaging. Metadata declares Python >=3.14 and platforms `any`; that +is broader than this verified matrix. Windows, macOS (including APFS/HFS+), other +architectures, Python versions beyond 3.14, interactive desktop rendering and +actual CircleCI execution are unrun here. No upload or publication was performed. + +Reviewed surfaces were setup/backend/Versioneer metadata paths, CircleCI/tox, +script execution/templates, JSON metadata decoding, task-runner event delivery +and the email launcher. The source review is bounded static review plus the +listed tests, not a comprehensive security assessment. + +- ScriptRunner executes Python and permits broad imports while restricting a + small set of names. Its import/builtins regression tests do not establish a + hostile-code sandbox. Use trusted scripts or an external isolation boundary. +- ScriptTemplate uses a normal Jinja Environment to generate code, not a + SandboxedEnvironment or an HTML sanitizer. Template source must be trusted; + output escaping and input trust remain caller responsibilities. +- Metadata uses JSON decoding; no additional confirmed source vulnerability was + established in this bounded review. Input-size/resource-exhaustion behavior, + every widget/plugin, native Qt components and platform-specific email launching + were not dynamically audited. + +Residual coverage includes these unreviewed surfaces, the local-project scanner +skip and unrun platforms. This report accepts no security exception and does not +claim complete package or deployment safety. diff --git a/pyproject.toml b/pyproject.toml new file mode 100644 index 0000000..354cfa3 --- /dev/null +++ b/pyproject.toml @@ -0,0 +1,3 @@ +[build-system] +requires = ["setuptools>=83"] +build-backend = "setuptools.build_meta" diff --git a/setup.py b/setup.py index 4aea8b0..d833817 100644 --- a/setup.py +++ b/setup.py @@ -1,6 +1,9 @@ from setuptools import setup, find_packages from os import path +import sys +# The build backend may execute setup.py without the source root on sys.path. +sys.path.insert(0, path.abspath(path.dirname(__file__))) import versioneer PACKAGE_NAME = 'pyqttoolkit' @@ -12,13 +15,13 @@ install_requires = [ 'matplotlib', - 'jinja2', + 'jinja2>=3.1.6', 'pandas', 'appdirs' ] tests_require = [ - 'tox', + 'tox>=4', 'pytest', 'pytest-cov', 'pytest-xvfb', @@ -28,14 +31,13 @@ extras_require = { 'test': tests_require, - 'publish': ['setuptools', 'twine'], + 'publish': ['build>=1.6.1', 'twine>=7.0.0'], 'ui': ['pyqt5'] } setup( name=PACKAGE_NAME, install_requires=install_requires, - tests_require=tests_require, extras_require=extras_require, packages=find_packages(exclude=('tests*',)), cmdclass=versioneer.get_cmdclass(), diff --git a/tests/services/test_task_runner.py b/tests/services/test_task_runner.py index edbcacd..70626bb 100644 --- a/tests/services/test_task_runner.py +++ b/tests/services/test_task_runner.py @@ -1,17 +1,15 @@ import pytest -import time -from asyncio import Event, wait_for +from threading import Event -from PyQt5.QtWidgets import QApplication - -from pyqttoolkit.services.task_runner import * +from pyqttoolkit.services.task_runner import TaskRunner from pytestqt.exceptions import capture_exceptions + class Result: def __init__(self): self._value = None - + def set(self, value): self._value = value @@ -19,132 +17,137 @@ def set(self, value): def value(self): return self._value + @pytest.fixture -def task_runner(qtbot): - app = QApplication.instance() or QApplication([]) - runner = TaskRunner(app) +def task_runner(qapp): + runner = TaskRunner(qapp) yield runner runner.shutdown() -def _handler(event, result=None): - def _(value): - event.set() - if result: - result.set(value) - return _ -@pytest.mark.asyncio -async def test_simple_task_completes(qtbot, task_runner): - event = Event() +def _wait_for(event): + # Worker threads must always finish, including when a test assertion fails. + assert event.wait(5), 'Timed out waiting for the test to release the worker' + + +def test_simple_task_completes(qtbot, task_runner): + completed = Event() with qtbot.waitSignal(task_runner.taskCompleted, timeout=1000): - task_runner.run_task(lambda: 42, on_completed=_handler(event)) - await wait_for(event.wait(), 1) + task_runner.run_task(lambda: 42, on_completed=lambda value: completed.set()) + assert completed.is_set() + -@pytest.mark.asyncio -async def test_result_is_correct(qtbot, task_runner): - event = Event() +def test_result_is_correct(qtbot, task_runner): result = Result() with qtbot.waitSignal(task_runner.taskCompleted, timeout=1000): - task_runner.run_task(lambda: 42, on_completed=_handler(event, result)) - await wait_for(event.wait(), 1) + task_runner.run_task(lambda: 42, on_completed=result.set) assert result.value == 42 -@pytest.mark.asyncio -async def test_will_terminate_on_cancel_check(qtbot, task_runner): - event_1 = Event() - event_2 = Event() - event_3 = Event() + +def test_will_terminate_on_cancel_check(qtbot, task_runner): + started = Event() + proceed = Event() result = Result() - def _task(): + + def task(): result.set(42) - event_1.set() - while not event_2.is_set(): - pass + started.set() + _wait_for(proceed) result.set(24) task_runner.raiseForCancelled() - while not event_3.is_set(): - pass - with qtbot.waitSignal(task_runner.taskCancelled, timeout=10000): - task_runner.run_task(_task) - await wait_for(event_1.wait(), 2) - task_runner.cancel() - event_2.set() - assert result.value == 24 - -@pytest.mark.asyncio -async def test_will_wait_for_cancel_before_end(qtbot, task_runner): - event_1 = Event() - event_2 = Event() - event_3 = Event() - event_4 = Event() + result.set(7) + + try: + with qtbot.waitSignal(task_runner.taskCancelled, timeout=2000): + task_runner.run_task(task) + qtbot.waitUntil(started.is_set, timeout=2000) + task_runner.cancel() + proceed.set() + assert result.value == 24 + finally: + proceed.set() + + +def test_will_wait_for_cancel_before_end(qtbot, task_runner): + started = Event() + proceed = Event() + reached_cancel_check = Event() + finish = Event() result = Result() - def _task(): + + def task(): result.set(42) - event_1.set() - while not event_2.is_set(): - pass + started.set() + _wait_for(proceed) result.set(24) - event_3.set() + reached_cancel_check.set() task_runner.raiseForCancelled() - while not event_4.is_set(): - pass + _wait_for(finish) result.set(7) - with qtbot.waitSignal(task_runner.taskCancelled, timeout=10000): - task_runner.run_task(_task) - await wait_for(event_1.wait(), 2) - task_runner.cancel() - event_2.set() - await wait_for(event_3.wait(), 2) - event_4.set() - assert result.value == 24 - -@pytest.mark.asyncio -async def test_error_handler_called_if_error_raised(qtbot, task_runner): + + try: + with qtbot.waitSignal(task_runner.taskCancelled, timeout=2000): + task_runner.run_task(task) + qtbot.waitUntil(started.is_set, timeout=2000) + task_runner.cancel() + proceed.set() + qtbot.waitUntil(reached_cancel_check.is_set, timeout=2000) + assert result.value == 24 + finally: + proceed.set() + finish.set() + + +def test_error_handler_called_if_error_raised(qtbot, task_runner): result = Result() - event = Event() - def _task(): + + def task(): raise ValueError('value') - with qtbot.waitSignal(task_runner.taskErrored, timeout=10000): - task_runner.run_task(_task, on_error=_handler(event, result)) - await wait_for(event.wait(), 1) + + with qtbot.waitSignal(task_runner.taskErrored, timeout=1000): + task_runner.run_task(task, on_error=result.set) assert isinstance(result.value, ValueError) + def test_error_raised_if_no_error_handler(qtbot, task_runner): exception = ValueError('value') - def _task(): + + def task(): raise exception + with capture_exceptions() as exceptions: - task_runner.run_task(_task) - time.sleep(1) - QApplication.instance().processEvents() + task_runner.run_task(task) + qtbot.waitUntil(lambda: bool(exceptions), timeout=1000) assert len(exceptions) == 1 assert exceptions[0][1] == exception -@pytest.mark.asyncio -async def test_can_queue_tasks(qtbot, task_runner): - event_0 = Event() - event_1 = Event() - result_1 = Result() - event_2 = Event() - result_2 = Result() - def _task(start_event): - def _(): - while not start_event.is_set(): - pass + +def test_can_queue_tasks(qtbot, task_runner): + release_first = Event() + first_completed = Event() + completed = [] + signals = [] + task_runner.taskCompleted.connect(lambda: signals.append(True)) + + def task(gate): + def run(): + _wait_for(gate) return 42 - return _ - - handler_1 = _handler(event_1, result_1) - handler_2 = _handler(event_2, result_2) - - with qtbot.waitSignal(task_runner.taskCompleted, timeout=10000): - task_runner.run_task(_task(event_0), on_completed=handler_1) - task_runner.run_task(_task(event_1), on_completed=handler_2) - event_0.set() - - with qtbot.waitSignal(task_runner.taskCompleted, timeout=10000): - pass - - await wait_for(event_2.wait(), 2) - assert result_1.value == 42 - assert result_2.value == 42 + return run + + def complete_first(value): + completed.append(('first', value)) + first_completed.set() + + try: + task_runner.run_task(task(release_first), on_completed=complete_first) + task_runner.run_task( + task(first_completed), + on_completed=lambda value: completed.append(('second', value)), + ) + release_first.set() + qtbot.waitUntil(lambda: len(signals) == 2, timeout=2000) + assert completed == [('first', 42), ('second', 42)] + finally: + release_first.set() + first_completed.set() diff --git a/tox.ini b/tox.ini index fcc5b18..396075c 100644 --- a/tox.ini +++ b/tox.ini @@ -1,12 +1,15 @@ [tox] +requires = tox>=4 envlist=py314 [testenv] +package = wheel +changedir = {envtmpdir} extras= test ui setenv= QT_QPA_PLATFORM=offscreen commands= - pytest --no-xvfb tests --cov pyqttoolkit --ignore tests/services/test_task_runner.py - python scripts/check_license_headers.py + pytest --no-xvfb {toxinidir}/tests --cov pyqttoolkit + python {toxinidir}/scripts/check_license_headers.py