From a81f2a410c64381f5e918a4249f145c0ed9706bb Mon Sep 17 00:00:00 2001 From: Thomas MALLET Date: Thu, 12 Feb 2026 15:33:35 +0100 Subject: [PATCH 01/10] add python 3.14 support in pyproject.toml (tests and build OK) --- pyproject.toml | 1 + 1 file changed, 1 insertion(+) diff --git a/pyproject.toml b/pyproject.toml index 893e7c6..8b03450 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -29,6 +29,7 @@ classifiers = [ "Programming Language :: Python :: 3.11", "Programming Language :: Python :: 3.12", "Programming Language :: Python :: 3.13", + "Programming Language :: Python :: 3.14", ] requires-python = ">=3.9, <4" dependencies = ["NumPy>=1.19", "QtPy>=1.9"] From d0f3eb055a447302aae2fa99fa4f18e1764c8a76 Mon Sep 17 00:00:00 2001 From: Thomas MALLET Date: Thu, 12 Feb 2026 15:35:22 +0100 Subject: [PATCH 02/10] Update GitHub tests Actions (add Python 3.14 in tests matrix) --- .github/workflows/test-PyQt5.yml | 2 +- .github/workflows/test-PyQt6.yml | 2 +- .github/workflows/test-PySide6.yml | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/.github/workflows/test-PyQt5.yml b/.github/workflows/test-PyQt5.yml index 1de2f6a..fab5f88 100644 --- a/.github/workflows/test-PyQt5.yml +++ b/.github/workflows/test-PyQt5.yml @@ -21,7 +21,7 @@ jobs: strategy: fail-fast: false matrix: - python-version: ["3.9", "3.11", "3.13"] + python-version: ["3.9", "3.11", "3.13", "3.14"] steps: - uses: actions/checkout@v4 diff --git a/.github/workflows/test-PyQt6.yml b/.github/workflows/test-PyQt6.yml index f721af6..2b7f5c4 100644 --- a/.github/workflows/test-PyQt6.yml +++ b/.github/workflows/test-PyQt6.yml @@ -21,7 +21,7 @@ jobs: strategy: fail-fast: false matrix: - python-version: ["3.9", "3.11", "3.13"] + python-version: ["3.9", "3.11", "3.13", "3.14"] steps: - uses: actions/checkout@v4 diff --git a/.github/workflows/test-PySide6.yml b/.github/workflows/test-PySide6.yml index c800f1a..36052f5 100644 --- a/.github/workflows/test-PySide6.yml +++ b/.github/workflows/test-PySide6.yml @@ -21,7 +21,7 @@ jobs: strategy: fail-fast: false matrix: - python-version: ["3.9", "3.11", "3.13"] + python-version: ["3.9", "3.11", "3.13", "3.14"] steps: - uses: actions/checkout@v4 From 7a7e29b8c995730c24d9ef7af8dbf979f4f94cc0 Mon Sep 17 00:00:00 2001 From: Thomas MALLET Date: Fri, 13 Feb 2026 15:39:56 +0100 Subject: [PATCH 03/10] pyproject.toml add exclusion for python venv named : venv* and .venv* --- pyproject.toml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pyproject.toml b/pyproject.toml index 8b03450..9dea14d 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -60,7 +60,7 @@ version = { attr = "qwt.__version__" } addopts = "qwt" [tool.ruff] -exclude = [".git", ".vscode", "build", "dist"] +exclude = [".git", ".vscode", "build", "dist","venv*",".venv*"] line-length = 88 # Same as Black. indent-width = 4 # Same as Black. target-version = "py39" # Assume Python 3.9. From cfc2ddb47a516cf67db62b72b7823fa8fa121b7b Mon Sep 17 00:00:00 2001 From: Thomas MALLET Date: Fri, 13 Feb 2026 15:43:52 +0100 Subject: [PATCH 04/10] add venv* to gitignore --- .gitignore | 10 ++++++++++ 1 file changed, 10 insertions(+) diff --git a/.gitignore b/.gitignore index c514cdd..c047149 100644 --- a/.gitignore +++ b/.gitignore @@ -37,6 +37,16 @@ var/ .installed.cfg *.egg +# Environments +.env* +.envrc +.venv* +env*/ +venv*/ +ENV/ +env.bak/ +venv.bak/ + # PyInstaller # Usually these files are written by a python script from a template # before PyInstaller builds the exe, so as to inject date/other infos into it. From 07e8cefe3c575a2b2d3d12fd9cd3382cf7a1281f Mon Sep 17 00:00:00 2001 From: Thomas MALLET Date: Thu, 9 Apr 2026 16:41:43 +0200 Subject: [PATCH 05/10] new .py script to run task within multiple python env context (winpython, venv, venv*,...) --- .env.template | 11 ++- scripts/run_with_env.py | 208 ++++++++++++++++++++++++++++++++++++++++ 2 files changed, 218 insertions(+), 1 deletion(-) create mode 100644 scripts/run_with_env.py diff --git a/.env.template b/.env.template index 60f01f7..b06759e 100644 --- a/.env.template +++ b/.env.template @@ -1 +1,10 @@ -PYTHONPATH=. \ No newline at end of file +# Python interpreter (explicit path, e.g. WinPython). Auto-detected if empty. +PYTHON= +# WinPython base directory (legacy, prefer PYTHON instead) +# WINPYDIRBASE= +# Virtual environment directory (e.g. .venv39). Auto-discovered if empty. +VENV_DIR= +# Python path for development (sibling packages) +PYTHONPATH=. +# Locale (e.g. fr) +LANG= \ No newline at end of file diff --git a/scripts/run_with_env.py b/scripts/run_with_env.py new file mode 100644 index 0000000..c9e7eac --- /dev/null +++ b/scripts/run_with_env.py @@ -0,0 +1,208 @@ +# Copyright (c) DataLab Platform Developers, BSD 3-Clause license, see LICENSE file. + +"""Run a command with environment variables loaded from a .env file. + +This script automatically detects the best Python interpreter to use: + +1. ``PYTHON`` variable in ``.env`` file (e.g. for WinPython distributions) +2. ``WINPYDIRBASE`` variable (legacy WinPython base directory) +3. ``VENV_DIR`` variable (explicit virtual environment directory) +4. A local virtual environment (``.venv*`` directory in the project root) +5. Falls back to ``sys.executable`` (the Python that launched this script) + +This ensures that VS Code tasks always use the correct Python environment +regardless of which interpreter is configured globally or in VS Code. +""" + +from __future__ import annotations + +import glob +import os +import subprocess +import sys +from pathlib import Path + + +def _find_venv_python(project_root: Path) -> str | None: + """Find a Python executable in a ``.venv*`` directory. + + Searches for directories matching ``.venv*`` in the project root and + returns the first valid Python executable found. + + Args: + project_root: The root directory of the project. + + Returns: + Absolute path to the venv Python executable, or None if not found. + """ + # Sort to prefer ".venv" over ".venv-xyz" etc. + venv_dirs = sorted(glob.glob(str(project_root / ".venv*"))) + for venv_dir in venv_dirs: + venv_path = Path(venv_dir) + if not venv_path.is_dir(): + continue + result = _get_venv_python(venv_path) + if result: + return result + return None + + +def _get_venv_python(venv_dir: Path) -> str | None: + """Get the Python executable from a specific venv directory. + + Args: + venv_dir: Path to the virtual environment directory. + + Returns: + Absolute path to the Python executable, or None if not found. + """ + if not venv_dir.is_dir(): + return None + # Windows: Scripts/python.exe โ€” Unix: bin/python + candidates = [ + venv_dir / "Scripts" / "python.exe", + venv_dir / "bin" / "python", + ] + for candidate in candidates: + if candidate.is_file(): + # Keep the venv-local executable path without resolving symlinks: + # on Linux/WSL, ``bin/python`` is often a symlink to a global + # interpreter (e.g. /usr/bin/python3.x). Resolving it would lose + # venv context and site-packages selection. + return str(candidate.absolute()) + return None + + +def resolve_python(project_root: Path) -> str: + """Resolve the best Python interpreter for the project. + + Priority order: + + 1. ``PYTHON`` environment variable (set in ``.env`` or externally) + 2. ``WINPYDIRBASE`` environment variable (legacy WinPython base directory) + 3. ``VENV_DIR`` environment variable (explicit venv directory) + 4. ``.venv*`` directory in *project_root* (auto-discovery) + 5. ``sys.executable`` (the interpreter running this script) + + Args: + project_root: The root directory of the project. + + Returns: + Absolute path to the Python executable to use. + """ + # 1. Explicit PYTHON variable (e.g. WinPython distribution) + python_env = os.environ.get("PYTHON") + if python_env: + python_path = Path(python_env) + if python_path.is_file(): + # Do not resolve symlinks for the same reason as in + # ``_get_venv_python``. + resolved = str(python_path.absolute()) + print(f" ๐Ÿ Using PYTHON from .env: {resolved}") + return resolved + print(f" โš ๏ธ PYTHON variable set but not found: {python_env}") + + # 2. Legacy WINPYDIRBASE variable (WinPython distribution) + winpy_base = os.environ.get("WINPYDIRBASE") + if winpy_base and Path(winpy_base).is_dir(): + # Search for python.exe in the WinPython directory structure + # (e.g. WINPYDIRBASE/python-3.11.5.amd64/python.exe) + for candidate in sorted(Path(winpy_base).glob("python-*/python.exe")): + if candidate.is_file(): + resolved = str(candidate.absolute()) + print(f" ๐Ÿ Using WINPYDIRBASE (legacy): {resolved}") + return resolved + # Also try direct python.exe in the base directory + direct = Path(winpy_base) / "python.exe" + if direct.is_file(): + resolved = str(direct.absolute()) + print(f" ๐Ÿ Using WINPYDIRBASE (legacy): {resolved}") + return resolved + print(f" โš ๏ธ WINPYDIRBASE set but no Python found in: {winpy_base}") + + # 3. Explicit VENV_DIR variable (e.g. for multiple local venvs) + venv_dir_env = os.environ.get("VENV_DIR") + if venv_dir_env: + venv_dir = Path(venv_dir_env) + if not venv_dir.is_absolute(): + venv_dir = project_root / venv_dir + venv_python = _get_venv_python(venv_dir) + if venv_python: + print(f" ๐Ÿ Using VENV_DIR from .env: {venv_python}") + return venv_python + print(f" โš ๏ธ VENV_DIR set but no Python found in: {venv_dir}") + + # 4. Auto-discover local venv + venv_python = _find_venv_python(project_root) + if venv_python: + print(f" ๐Ÿ Using venv Python: {venv_python}") + return venv_python + + # 5. Fallback + print(f" ๐Ÿ Using caller Python: {sys.executable}") + return sys.executable + + +def load_env_file(env_path: str | None = None) -> None: + """Load environment variables from a .env file.""" + if env_path is None: + env_path = Path.cwd() / ".env" + if not Path(env_path).is_file(): + raise FileNotFoundError(f"Environment file not found: {env_path}") + print(f"Loading environment variables from: {env_path}") + with open(env_path, encoding="utf-8") as f: + for line in f: + line = line.strip() + if not line or line.startswith("#") or "=" not in line: + continue + key, value = line.split("=", 1) + os.environ[key.strip()] = value.strip() + print(f" Loaded variable: {key.strip()}={value.strip()}") + + +def execute_command(command: list[str], python_exe: str) -> int: + """Execute a command, replacing ``python`` placeholders. + + Any argument that is the bare word ``python`` or that points to a Python + executable (checked via filename) is replaced by *python_exe* so that the + subprocess uses the resolved interpreter rather than the global one. + + Args: + command: The command and its arguments. + python_exe: The resolved Python interpreter path. + + Returns: + The subprocess exit code. + """ + resolved: list[str] = [] + for arg in command: + if arg.lower() == "python" or ( + Path(arg).name.lower().startswith("python") + and Path(arg).is_file() + and arg.lower() != python_exe.lower() + ): + resolved.append(python_exe) + else: + resolved.append(arg) + print("Executing command:") + print(" ".join(resolved)) + print("") + result = subprocess.call(resolved) + print(f"Process exited with code {result}") + return result + + +def main() -> None: + """Main function to load environment variables and execute a command.""" + if len(sys.argv) < 2: + print("Usage: python run_with_env.py [args ...]") + sys.exit(1) + print("๐Ÿƒ Running with environment variables") + project_root = Path.cwd() + load_env_file() + python_exe = resolve_python(project_root) + return execute_command(sys.argv[1:], python_exe) + + +if __name__ == "__main__": + main() From f79dca563bc67c695362ea1d77726d4cd08983eb Mon Sep 17 00:00:00 2001 From: Thomas MALLET Date: Thu, 9 Apr 2026 17:53:38 +0200 Subject: [PATCH 06/10] update vscode tasks --- .vscode/tasks.json | 237 ++++++++++++++++++++++++++++++--------------- 1 file changed, 160 insertions(+), 77 deletions(-) diff --git a/.vscode/tasks.json b/.vscode/tasks.json index 2360b42..ee88fe6 100644 --- a/.vscode/tasks.json +++ b/.vscode/tasks.json @@ -1,136 +1,223 @@ -{ +๏ปฟ{ // See https://go.microsoft.com/fwlink/?LinkId=733558 // for the documentation about the tasks.json format "version": "2.0.0", "tasks": [ { - "label": "Run Coverage", - "type": "shell", - "command": "cmd", + "label": "๐Ÿงฝ Ruff Formatter", + "command": "${command:python.interpreterPath}", "args": [ - "/c", - "run_coverage.bat" + "scripts/run_with_env.py", + "${command:python.interpreterPath}", + "-m", + "ruff", + "format", ], "options": { - "cwd": "scripts", - "env": { - "UNATTENDED": "1", - "PYTHON": "${env:PPSTACK_PYTHONEXE}" - } + "cwd": "${workspaceFolder}", + "statusbar": { + "hide": true, + }, }, "group": { "kind": "build", - "isDefault": true + "isDefault": true, }, "presentation": { + "clear": true, "echo": true, - "reveal": "always", "focus": false, "panel": "dedicated", + "reveal": "always", "showReuseMessage": true, - "clear": true - } + }, + "type": "shell", }, { - "label": "Run pytest", - "type": "shell", - "command": "cmd", + "label": "๐Ÿ”ฆ Ruff Linter", + "command": "${command:python.interpreterPath}", "args": [ - "/c", - "run_pytest.bat" + "scripts/run_with_env.py", + "${command:python.interpreterPath}", + "-m", + "ruff", + "check", + "--fix", ], "options": { - "cwd": "scripts", - "env": { - "UNATTENDED": "1", - "PYTHON": "${env:PPSTACK_PYTHONEXE}" - } + "cwd": "${workspaceFolder}", + "statusbar": { + "hide": true, + }, }, "group": { "kind": "build", - "isDefault": true + "isDefault": true, }, "presentation": { + "clear": true, "echo": true, - "reveal": "always", "focus": false, "panel": "dedicated", + "reveal": "always", "showReuseMessage": true, - "clear": true - } + }, + "type": "shell", }, { - "label": "Take screenshots", + "label": "๐Ÿงฝ๐Ÿ”ฆ Ruff", + "dependsOrder": "sequence", + "dependsOn": [ + "๐Ÿงฝ Ruff Formatter", + "๐Ÿ”ฆ Ruff Linter", + ], + "group": { + "kind": "build", + "isDefault": false, + }, + "presentation": { + "clear": true, + "echo": true, + "focus": false, + "panel": "dedicated", + "reveal": "always", + "showReuseMessage": true, + }, "type": "shell", - "command": "cmd", + }, + { + "label": "๐Ÿ”ฆ Pylint", + "command": "${command:python.interpreterPath}", "args": [ - "/c", - "take_screenshots.bat" + "scripts/run_with_env.py", + "${command:python.interpreterPath}", + "-m", + "pylint", + "qwt", + // "--disable=R0801,C0103,C0114,C0115,C0116,W0612,W0613", + "--disable=fixme,C,R,W", ], "options": { - "cwd": "scripts", - "env": { - "UNATTENDED": "1", - "PYTHON": "${env:PPSTACK_PYTHONEXE}" - } + "cwd": "${workspaceFolder}", }, "group": { "kind": "build", - "isDefault": true + "isDefault": true, }, "presentation": { + "clear": true, "echo": true, - "reveal": "always", "focus": false, "panel": "dedicated", + "reveal": "always", "showReuseMessage": true, - "clear": true - } + }, + "type": "shell", }, { - "label": "Clean Up", - "type": "shell", - "command": "cmd", + "label": "๐Ÿš€ Pytest", + "command": "${command:python.interpreterPath}", "args": [ - "/c", - "clean_up.bat" + "scripts/run_with_env.py", + "${command:python.interpreterPath}", + "-m", + "pytest", + "--ff", ], "options": { - "cwd": "scripts" + "cwd": "${workspaceFolder}", + "env": { + "UNATTENDED": "1", + }, }, "group": { "kind": "build", - "isDefault": true + "isDefault": true, }, "presentation": { "echo": true, "reveal": "always", "focus": false, - "panel": "shared", + "panel": "dedicated", "showReuseMessage": true, - "clear": false - } + "clear": true, + }, + "type": "shell", }, { - "label": "Run Pylint", + "label": "๐Ÿงช Coverage tests", "type": "shell", - "command": "cmd", + "command": "${command:python.interpreterPath}", "args": [ - "/c", - "run_pylint.bat", - // "--disable=R0801,C0103,C0114,C0115,C0116,W0612,W0613", - "--disable=fixme,C,R,W", + "scripts/run_with_env.py", + "${command:python.interpreterPath}", + "-m", + "coverage", + "run", + "-m", + "pytest", + "qwt", ], "options": { - "cwd": "scripts", + "cwd": "${workspaceFolder}", "env": { "UNATTENDED": "1", - "PYTHON": "${env:PPSTACK_PYTHONEXE}" - } + }, + "statusbar": { + "hide": true, + }, + }, + "group": { + "kind": "test", + "isDefault": true, + }, + "presentation": { + "panel": "dedicated", + }, + "problemMatcher": [], + }, + { + "label": "๐Ÿ“Š Coverage full", + "type": "shell", + "windows": { + "command": "${command:python.interpreterPath} -m coverage combine; if ($?) { ${command:python.interpreterPath} -m coverage html; if ($?) { start htmlcov\\index.html } }", + }, + "linux": { + "command": "${command:python.interpreterPath} -m coverage combine && ${command:python.interpreterPath} -m coverage html && xdg-open htmlcov/index.html", + }, + "osx": { + "command": "${command:python.interpreterPath} -m coverage combine && ${command:python.interpreterPath} -m coverage html && open htmlcov/index.html", + }, + "options": { + "cwd": "${workspaceFolder}", + }, + "presentation": { + "panel": "dedicated", + }, + "problemMatcher": [], + "dependsOrder": "sequence", + "dependsOn": [ + "๐Ÿงช Coverage tests", + ], + }, + { + "label": "Take screenshots", + "type": "shell", + "command": "${command:python.interpreterPath}", + "args": [ + "scripts/run_with_env.py", + "${command:python.interpreterPath}", + "scripts/take_screenshots.py", + ], + "options": { + "cwd": "${workspaceFolder}", + "env": { + "UNATTENDED": "1", + }, }, "group": { "kind": "build", - "isDefault": true + "isDefault": true, }, "presentation": { "echo": true, @@ -138,23 +225,19 @@ "focus": false, "panel": "dedicated", "showReuseMessage": true, - "clear": true - } + "clear": true, + }, }, { - "label": "Run Ruff", + "label": "Clean Up", "type": "shell", "command": "cmd", "args": [ "/c", - "run_ruff.bat", + "clean_up.bat" ], "options": { - "cwd": "scripts", - "env": { - "PYTHON": "${env:PPSTACK_PYTHONEXE}", - "UNATTENDED": "1" - } + "cwd": "scripts" }, "group": { "kind": "build", @@ -164,9 +247,9 @@ "echo": true, "reveal": "always", "focus": false, - "panel": "dedicated", + "panel": "shared", "showReuseMessage": true, - "clear": true + "clear": false } }, { @@ -176,7 +259,7 @@ "options": { "cwd": "scripts", "env": { - "PYTHON": "${env:PPSTACK_PYTHONEXE}", + "PYTHON": "${command:python.interpreterPath}", "UNATTENDED": "1" } }, @@ -205,7 +288,7 @@ "options": { "cwd": "scripts", "env": { - "PYTHON": "${env:PPSTACK_PYTHONEXE}", + "PYTHON": "${command:python.interpreterPath}", "UNATTENDED": "1" } }, @@ -238,7 +321,7 @@ "options": { "cwd": "scripts", "env": { - "PYTHON": "${env:PPSTACK_PYTHONEXE}", + "PYTHON": "${command:python.interpreterPath}", "UNATTENDED": "1" } }, @@ -261,4 +344,4 @@ }, }, ] -} \ No newline at end of file +} From cb776ab4d7cdda5e0f8cc17d7ae7bea92280e4cd Mon Sep 17 00:00:00 2001 From: Thomas MALLET Date: Fri, 10 Apr 2026 16:55:43 +0200 Subject: [PATCH 07/10] linter ruff fix --- qwt/plot_canvas.py | 2 +- qwt/tests/test_bodedemo.py | 3 +-- qwt/tests/test_relativemargin.py | 4 ++-- 3 files changed, 4 insertions(+), 5 deletions(-) diff --git a/qwt/plot_canvas.py b/qwt/plot_canvas.py index 1b90ed1..a72d9b8 100644 --- a/qwt/plot_canvas.py +++ b/qwt/plot_canvas.py @@ -787,7 +787,7 @@ def invalidatePaintCache(self): import warnings warnings.warn( - "`invalidatePaintCache` has been removed: " "please use `replot` instead", + "`invalidatePaintCache` has been removed: please use `replot` instead", RuntimeWarning, ) self.replot() diff --git a/qwt/tests/test_bodedemo.py b/qwt/tests/test_bodedemo.py index 834919e..2bbff41 100644 --- a/qwt/tests/test_bodedemo.py +++ b/qwt/tests/test_bodedemo.py @@ -144,8 +144,7 @@ def __init__(self, *args): yvalue=-20.0, align=Qt.AlignRight | Qt.AlignBottom, label=QwtText.make( - "[1-(\u03c9/\u03c90)2+2j\u03c9/Q]" - "-1", + "[1-(\u03c9/\u03c90)2+2j\u03c9/Q]-1", color=Qt.white, borderradius=2, borderpen=QPen(Qt.lightGray, 5), diff --git a/qwt/tests/test_relativemargin.py b/qwt/tests/test_relativemargin.py index 7b67d09..932d0fd 100644 --- a/qwt/tests/test_relativemargin.py +++ b/qwt/tests/test_relativemargin.py @@ -30,9 +30,9 @@ def __init__(self, *args): def_margin = plot.axisMargin(qwt.QwtPlot.yLeft) scale_str = "lin/lin" if not log_scale else "log/lin" if relative_margin is None: - margin_str = f"default ({def_margin*100:.0f}%)" + margin_str = f"default ({def_margin * 100:.0f}%)" else: - margin_str = f"{relative_margin*100:.0f}%" + margin_str = f"{relative_margin * 100:.0f}%" plot.setTitle(f"{scale_str}, margin: {margin_str}") if relative_margin is not None: plot.setAxisMargin(qwt.QwtPlot.yLeft, relative_margin) From ca09db7f77c9b3c74f8f484d2887ed1d7bddc1da Mon Sep 17 00:00:00 2001 From: Thomas MALLET Date: Fri, 10 Apr 2026 17:01:53 +0200 Subject: [PATCH 08/10] update tasks with new run_with_env script --- .vscode/tasks.json | 44 +++++++++++++++++++++---------------- scripts/take_screenshots.py | 24 ++++++++++++++++++++ 2 files changed, 49 insertions(+), 19 deletions(-) create mode 100644 scripts/take_screenshots.py diff --git a/.vscode/tasks.json b/.vscode/tasks.json index ee88fe6..d63bc7a 100644 --- a/.vscode/tasks.json +++ b/.vscode/tasks.json @@ -1,4 +1,4 @@ -๏ปฟ{ +{ // See https://go.microsoft.com/fwlink/?LinkId=733558 // for the documentation about the tasks.json format "version": "2.0.0", @@ -180,13 +180,13 @@ "label": "๐Ÿ“Š Coverage full", "type": "shell", "windows": { - "command": "${command:python.interpreterPath} -m coverage combine; if ($?) { ${command:python.interpreterPath} -m coverage html; if ($?) { start htmlcov\\index.html } }", + "command": "${command:python.interpreterPath} scripts/run_with_env.py ${command:python.interpreterPath} -m coverage combine; if ($?) { ${command:python.interpreterPath} scripts/run_with_env.py ${command:python.interpreterPath} -m coverage html; if ($?) { start htmlcov\\index.html } }", }, "linux": { - "command": "${command:python.interpreterPath} -m coverage combine && ${command:python.interpreterPath} -m coverage html && xdg-open htmlcov/index.html", + "command": "${command:python.interpreterPath} scripts/run_with_env.py ${command:python.interpreterPath} -m coverage combine && ${command:python.interpreterPath} scripts/run_with_env.py ${command:python.interpreterPath} -m coverage html && xdg-open htmlcov/index.html", }, "osx": { - "command": "${command:python.interpreterPath} -m coverage combine && ${command:python.interpreterPath} -m coverage html && open htmlcov/index.html", + "command": "${command:python.interpreterPath} scripts/run_with_env.py ${command:python.interpreterPath} -m coverage combine && ${command:python.interpreterPath} scripts/run_with_env.py ${command:python.interpreterPath} -m coverage html && open htmlcov/index.html", }, "options": { "cwd": "${workspaceFolder}", @@ -255,18 +255,21 @@ { "label": "Build documentation", "type": "shell", - "command": "cmd", + "windows": { + "command": "${command:python.interpreterPath} scripts/run_with_env.py ${command:python.interpreterPath} -m sphinx -b html doc build/doc; if ($?) { start build\\doc\\index.html }" + }, + "linux": { + "command": "${command:python.interpreterPath} scripts/run_with_env.py ${command:python.interpreterPath} -m sphinx -b html doc build/doc && xdg-open build/doc/index.html" + }, + "osx": { + "command": "${command:python.interpreterPath} scripts/run_with_env.py ${command:python.interpreterPath} -m sphinx -b html doc build/doc && open build/doc/index.html" + }, "options": { - "cwd": "scripts", + "cwd": "${workspaceFolder}", "env": { - "PYTHON": "${command:python.interpreterPath}", "UNATTENDED": "1" } }, - "args": [ - "/c", - "build_doc.bat" - ], "problemMatcher": [], "group": { "kind": "build", @@ -284,18 +287,21 @@ { "label": "Build Python packages", "type": "shell", - "command": "cmd", + "windows": { + "command": "if (Test-Path MANIFEST) { Remove-Item MANIFEST }; ${command:python.interpreterPath} scripts/run_with_env.py ${command:python.interpreterPath} -m build; if (Test-Path PythonQwt.egg-info) { Remove-Item -Recurse -Force PythonQwt.egg-info }" + }, + "linux": { + "command": "rm -f MANIFEST && ${command:python.interpreterPath} scripts/run_with_env.py ${command:python.interpreterPath} -m build && rm -rf PythonQwt.egg-info" + }, + "osx": { + "command": "rm -f MANIFEST && ${command:python.interpreterPath} scripts/run_with_env.py ${command:python.interpreterPath} -m build && rm -rf PythonQwt.egg-info" + }, "options": { - "cwd": "scripts", + "cwd": "${workspaceFolder}", "env": { - "PYTHON": "${command:python.interpreterPath}", "UNATTENDED": "1" } }, - "args": [ - "/c", - "build_dist.bat" - ], "problemMatcher": [], "group": { "kind": "build", @@ -344,4 +350,4 @@ }, }, ] -} +} \ No newline at end of file diff --git a/scripts/take_screenshots.py b/scripts/take_screenshots.py new file mode 100644 index 0000000..8f3d159 --- /dev/null +++ b/scripts/take_screenshots.py @@ -0,0 +1,24 @@ +# Copyright (c) 2026 PlotPyStack developers +# Licensed under the terms of the MIT License +# (see LICENSE file for more details) + +"""Screenshots update script.""" + +import subprocess +import sys + + +def main(): + """Run all screenshot-generating scripts.""" + scripts = [ + [sys.executable, "qwt/tests/__init__.py", "--mode", "screenshots"], + [sys.executable, "doc/plot_example.py"], + [sys.executable, "doc/symbol_path_example.py"], + ] + for cmd in scripts: + print(f"Running: {' '.join(cmd)}") + subprocess.check_call(cmd) + + +if __name__ == "__main__": + main() From 344cfb417d8b151f3c2dc187e05e9544972d0bcf Mon Sep 17 00:00:00 2001 From: Thomas MALLET Date: Fri, 10 Apr 2026 17:06:30 +0200 Subject: [PATCH 09/10] update deps and gitignore --- .gitignore | 6 ++++++ pyproject.toml | 2 +- 2 files changed, 7 insertions(+), 1 deletion(-) diff --git a/.gitignore b/.gitignore index c047149..1ef7096 100644 --- a/.gitignore +++ b/.gitignore @@ -7,6 +7,12 @@ doctmp/ # Visual Studio Code .env +.venv* +.*venv* + +#AI instructions +*.ai +*.ai* # Created by https://www.gitignore.io/api/python diff --git a/pyproject.toml b/pyproject.toml index 9dea14d..b9d90c1 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -43,7 +43,7 @@ Documentation = "https://PythonQwt.readthedocs.io/en/latest/" PythonQwt-tests = "qwt.tests:run" [project.optional-dependencies] -dev = ["ruff", "pylint", "Coverage"] +dev = ["ruff", "pylint", "Coverage", "build"] doc = ["PyQt5", "sphinx>6", "python-docs-theme"] test = ["pytest", "pytest-xvfb"] From 8509cb4b37984a8c4c49c7ca7f47afc9bba3a42d Mon Sep 17 00:00:00 2001 From: Thomas MALLET Date: Fri, 10 Apr 2026 17:14:57 +0200 Subject: [PATCH 10/10] add legacy support for env variable WINPYDIRBASE --- scripts/run_with_env.py | 18 ++++++++++-------- 1 file changed, 10 insertions(+), 8 deletions(-) diff --git a/scripts/run_with_env.py b/scripts/run_with_env.py index c9e7eac..6688cb6 100644 --- a/scripts/run_with_env.py +++ b/scripts/run_with_env.py @@ -106,12 +106,13 @@ def resolve_python(project_root: Path) -> str: winpy_base = os.environ.get("WINPYDIRBASE") if winpy_base and Path(winpy_base).is_dir(): # Search for python.exe in the WinPython directory structure - # (e.g. WINPYDIRBASE/python-3.11.5.amd64/python.exe) - for candidate in sorted(Path(winpy_base).glob("python-*/python.exe")): - if candidate.is_file(): - resolved = str(candidate.absolute()) - print(f" ๐Ÿ Using WINPYDIRBASE (legacy): {resolved}") - return resolved + # Patterns: python-3.11.5.amd64/python.exe (old) or python/python.exe (new) + for pattern in ("python-*/python.exe", "python/python.exe"): + for candidate in sorted(Path(winpy_base).glob(pattern)): + if candidate.is_file(): + resolved = str(candidate.absolute()) + print(f" ๐Ÿ Using WINPYDIRBASE (legacy): {resolved}") + return resolved # Also try direct python.exe in the base directory direct = Path(winpy_base) / "python.exe" if direct.is_file(): @@ -156,8 +157,9 @@ def load_env_file(env_path: str | None = None) -> None: if not line or line.startswith("#") or "=" not in line: continue key, value = line.split("=", 1) - os.environ[key.strip()] = value.strip() - print(f" Loaded variable: {key.strip()}={value.strip()}") + value = value.strip().strip('"').strip("'") + os.environ[key.strip()] = value + print(f" Loaded variable: {key.strip()}={value}") def execute_command(command: list[str], python_exe: str) -> int: