Coverage for vibefpmgui/console.py: 88%
28 statements
« prev ^ index » next coverage.py v7.16.2, created at 2026-11-01 20:51 +0000
« prev ^ index » next coverage.py v7.16.2, created at 2026-11-01 20:51 +0000
1"""Starting console programs (git, fpm, fortitude, fprettify…) from the app
2without a console window flashing up on Windows (no Qt).
4A GUI app on Windows has no console; a console program it starts with
5Python's subprocess gets a new one, shown for as long as the program runs,
6unless it's started with CREATE_NO_WINDOW. (QProcess does that by itself
7when the app has no console.)
8"""
10from __future__ import annotations
12import os
13import subprocess
14import sys
15import tempfile
16from pathlib import Path
18# subprocess's keyword arguments for no console window: **NO_WINDOW in each call.
19NO_WINDOW: dict = {"creationflags": subprocess.CREATE_NO_WINDOW} if sys.platform == "win32" else {}
22def gui_python() -> str:
23 """The Python to start another window of the app with: on Windows its
24 pythonw.exe (python.exe would open a console window beside it, there
25 for as long as the window is)."""
26 if sys.platform == "win32":
27 windowless = Path(sys.executable).with_name("pythonw.exe")
28 if windowless.is_file():
29 return str(windowless)
30 return sys.executable
33def run(args: list[str], timeout: float, limit: int | None = None, env: dict | None = None,
34 cwd: str | None = None) -> tuple[int, bytes | None, str]:
35 """Run a program to its end, without a console window: (exit code, its
36 output, or None if over ``limit`` bytes, its error message). Raises
37 OSError if it can't be started, TimeoutError after ``timeout`` seconds
38 (it's killed).
40 Its output goes to temporary files, not pipes, and only it is waited for
41 (without Python's lock: the window goes on): a pipe is only done when
42 every process holding it has closed it, and a process started meanwhile
43 by another thread (the Terminal's shell, fortls, fpm) could hold it too,
44 for as long as it runs."""
45 with tempfile.TemporaryFile() as out, tempfile.TemporaryFile() as err:
46 process = subprocess.Popen(args, stdin=subprocess.DEVNULL, stdout=out, stderr=err, env=env, cwd=cwd,
47 **NO_WINDOW)
48 try:
49 code = process.wait(timeout=timeout)
50 except subprocess.TimeoutExpired:
51 process.kill()
52 process.wait()
53 raise TimeoutError(f"{Path(args[0]).name} took over {timeout:.0f} s") from None
54 size = out.seek(0, os.SEEK_END)
55 out.seek(0)
56 err.seek(0)
57 output = None if limit is not None and size > limit else out.read()
58 message = err.read().decode("utf-8", "replace").strip()
59 return code, output, message