Source code for qt_dev_helper.qt_tools
"""Qt implementation cross compatibility module."""
from __future__ import annotations
import contextlib
import os
import shutil
import subprocess
from collections.abc import Sequence
from functools import lru_cache
from importlib.resources import as_file
from importlib.resources import files
from pathlib import Path
[docs]
class QtToolNotFoundError(Exception):
"""Error thrown when a Qt tool can't be found.
See Also
--------
find_qt_tool
"""
def __init__(self, tool_name: str) -> None: # noqa: DOC
super().__init__(
f"Can not find Qt tool {tool_name=} please install a Qt distributions. "
"E.g. 'pip install PySide6'."
)
[docs]
class QtToolExecutionError(Exception):
"""Error thrown when a Qt tool returns a non-zero exit status code.
See Also
--------
call_qt_tool
"""
def __init__( # noqa: DOC
self, returncode: int, cmd: str, stdout: bytes, stderr: bytes
) -> None:
output = (stdout + b"\n\n" + stderr).decode()
super().__init__(
f"Command {cmd!r} returned non-zero exit status {returncode}.\n"
f"Command output:\n{output}"
)
[docs]
@lru_cache(maxsize=1)
def extend_qt_tool_path() -> str:
"""Prepend path variable with package dirs to qt-tools if present.
Returns
-------
str
Path extended with library executable paths.
"""
additional_paths: list[str] = []
tool_packages = {
"PySide6": ["", "Qt/libexec"],
"shiboken6": [""],
"qt6_applications": ["Qt/bin", "Qt/lib"],
"qt5_applications": ["Qt/bin", "Qt/lib"],
}
for package, rel_paths in tool_packages.items():
with contextlib.suppress(ModuleNotFoundError), as_file(files(package)) as p:
additional_paths += [str(p / rel_path) for rel_path in rel_paths]
return os.pathsep.join((*additional_paths, os.environ.get("PATH", "")))
[docs]
@lru_cache
def find_qt_tool(tool_name: str) -> str:
"""Find path to Qt tool executable like ``rcc``, ``uic`` or ``designer``.
Parameters
----------
tool_name : str
Name of the Qt tool to look up.
Returns
-------
str
Path to the tool executable.
Raises
------
QtToolNotFoundError
If the tool could not be found in the path.
"""
extended_path = extend_qt_tool_path()
command_path = shutil.which(tool_name, path=extended_path)
if command_path is not None:
return Path(command_path).as_posix()
raise QtToolNotFoundError(tool_name)
[docs]
def call_qt_tool(tool_name: str, *, arguments: Sequence[str] = (), no_wait: bool = False) -> None:
"""Call qt tools in a generic way.
Parameters
----------
tool_name : str
Name of the Qt tool to use (e.g. ``rcc``, ``uic`` or ``designer``)
arguments : Sequence[str]
Additional arguments for options for the tool. Defaults to ()
no_wait : bool
Whether or not to wait for the process to finish
(used for CLI not to wait for designer application to close). Defaults to False
Raises
------
ValueError
If ``arguments`` is not of type Sequence[str]
QtToolExecutionError
If the tool returns a non-zero exit code.
"""
if not isinstance(arguments, Sequence) or isinstance(arguments, str):
msg = f"arguments needs to be of type Sequence[str],\n Got:\n\t{arguments=}"
raise ValueError(msg)
tool_exe = find_qt_tool(tool_name)
cmd = " ".join((tool_exe, *arguments))
env = os.environ.copy()
env["PATH"] = extend_qt_tool_path()
if no_wait is True:
subprocess.Popen(
cmd, shell=True, stdin=None, stdout=None, stderr=None, close_fds=True, env=env
)
else:
out = subprocess.run(cmd, capture_output=True, shell=True, env=env)
if out.returncode != 0:
raise QtToolExecutionError(
returncode=out.returncode, cmd=cmd, stdout=out.stdout, stderr=out.stderr
)
print(out.stdout.decode()) # noqa: T201