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..0e45967 100644 --- a/pyqttoolkit/scripting/script_runner.py +++ b/pyqttoolkit/scripting/script_runner.py @@ -72,8 +72,12 @@ 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 - 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..12ac458 100644 --- a/pyqttoolkit/services/task_runner.py +++ b/pyqttoolkit/services/task_runner.py @@ -177,6 +177,11 @@ def __init__(self, parent): self._is_cancelled = False 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): """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..4aea8b0 100644 --- a/setup.py +++ b/setup.py @@ -23,13 +23,12 @@ 'pytest-cov', 'pytest-xvfb', 'pytest-asyncio', - 'pytest-qt', - 'licensify' + 'pytest-qt' ] extras_require = { 'test': tests_require, - 'publish': ['twine'], + 'publish': ['setuptools', 'twine'], 'ui': ['pyqt5'] } @@ -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):