clan-vm-manager: Working pytest skeleton. clan-cli: Fixing devshell depending on itself
All checks were successful
checks / check-links (pull_request) Successful in 14s
checks / checks-impure (pull_request) Successful in 1m53s
checks / checks (pull_request) Successful in 4m13s

This commit is contained in:
Luis Hebendanz 2024-03-24 23:36:00 +01:00
parent 0ee8dceee2
commit 8ab9d20342
20 changed files with 181 additions and 336 deletions

View File

@ -1,115 +0,0 @@
{
perSystem =
{
pkgs,
self',
lib,
...
}:
let
python3 = pkgs.python3;
pypkgs = python3.pkgs;
clan-cli = self'.packages.clan-cli;
clan-vm-manager = self'.packages.clan-vm-manager;
pythonWithDeps = python3.withPackages (
ps:
clan-cli.propagatedBuildInputs
++ clan-cli.devDependencies
++ [ ps.pip ]
++ [ clan-vm-manager.externalPythonDeps ]
# clan-vm-manager deps
);
linuxOnlyPackages = lib.optionals pkgs.stdenv.isLinux [ pkgs.xdg-utils ];
in
{
devShells.python = pkgs.mkShell {
inputsFrom = [ self'.devShells.default ];
packages =
[
pythonWithDeps
pypkgs.mypy
pypkgs.ipdb
pkgs.desktop-file-utils
pkgs.gtk4.dev
pkgs.ruff
pkgs.libadwaita.devdoc # has the demo called 'adwaita-1-demo'
]
++ linuxOnlyPackages
++ clan-vm-manager.nativeBuildInputs
++ clan-vm-manager.buildInputs
++ clan-cli.nativeBuildInputs;
PYTHONBREAKPOINT = "ipdb.set_trace";
shellHook = ''
ln -sfT ${clan-cli.nixpkgs} ./pkgs/clan-cli/clan_cli/nixpkgs
## PYTHON
tmp_path="$(realpath ./.direnv/python)"
repo_root=$(realpath .)
mkdir -p "$tmp_path/${pythonWithDeps.sitePackages}"
# local dependencies
localPackages=(
$repo_root/pkgs/clan-cli
$repo_root/pkgs/clan-vm-manager
)
# Install executable wrappers for local python packages scripts
# This is done by utilizing `pip install --editable`
# As a result, executables like `clan` can be executed from within the dev-shell
# while using the current version of the code and its dependencies.
for package in "''${localPackages[@]}"; do
pname=$(basename "$package")
if
[ ! -e "$tmp_path/meta/$pname/pyproject.toml" ] \
|| [ ! -e "$package/pyproject.toml" ] \
|| ! cmp -s "$tmp_path/meta/$pname/pyproject.toml" "$package/pyproject.toml"
then
echo "==== Installing local python package $pname in editable mode ===="
mkdir -p "$tmp_path/meta/$pname"
cp $package/pyproject.toml $tmp_path/meta/$pname/pyproject.toml
${python3.pkgs.pip}/bin/pip install \
--quiet \
--disable-pip-version-check \
--no-index \
--no-build-isolation \
--prefix "$tmp_path" \
--editable "$package"
fi
done
export PATH="$tmp_path/bin:$PATH"
export PYTHONPATH="''${PYTHONPATH:+$PYTHONPATH:}$tmp_path/${pythonWithDeps.sitePackages}"
for package in "''${localPackages[@]}"; do
export PYTHONPATH="$package:$PYTHONPATH"
done
## GUI
if ! command -v xdg-mime &> /dev/null; then
echo "Warning: 'xdg-mime' is not available. The desktop file cannot be installed."
fi
# install desktop file
set -eou pipefail
DESKTOP_FILE_NAME=org.clan.vm-manager.desktop
DESKTOP_DST=~/.local/share/applications/$DESKTOP_FILE_NAME
DESKTOP_SRC=${clan-vm-manager.desktop-file}/share/applications/$DESKTOP_FILE_NAME
UI_BIN="clan-vm-manager"
cp -f $DESKTOP_SRC $DESKTOP_DST
sed -i "s|Exec=.*clan-vm-manager|Exec=$UI_BIN|" $DESKTOP_DST
xdg-mime default $DESKTOP_FILE_NAME x-scheme-handler/clan
echo "==== Validating desktop file installation ===="
set -x
desktop-file-validate $DESKTOP_DST
set +xeou pipefail
'';
};
};
}

View File

@ -35,7 +35,6 @@
imports = [
./checks/flake-module.nix
./devShell.nix
./devShell-python.nix
./formatter.nix
./templates/flake-module.nix
./clanModules/flake-module.nix

View File

@ -9,9 +9,8 @@
treefmt.programs.mypy.enable = true;
treefmt.programs.mypy.directories = {
"pkgs/clan-cli".extraPythonPackages = self'.packages.clan-cli.pytestDependencies;
"pkgs/clan-vm-manager".extraPythonPackages =
self'.packages.clan-vm-manager.externalPythonDeps ++ self'.packages.clan-cli.pytestDependencies;
"pkgs/clan-cli".extraPythonPackages = self'.packages.clan-cli.testDependencies;
"pkgs/clan-vm-manager".extraPythonPackages = self'.packages.clan-vm-manager.testDependencies;
};
treefmt.settings.formatter.nix = {

View File

@ -1,4 +1,5 @@
source_up
# BUG: If this is enabled the devshell depends on clan_cli building successfully
# source_up
watch_file flake-module.nix default.nix

View File

@ -8,7 +8,7 @@ from typing import Any
from clan_cli.clan_uri import ClanURI, MachineData
from clan_cli.dirs import vm_state_dir
from qemu.qmp import QEMUMonitorProtocol
from clan_cli.qemu.qmp import QEMUMonitorProtocol
from ..cmd import run
from ..errors import ClanError

View File

@ -1,4 +1,5 @@
{
# Inputs for the package
age,
lib,
argcomplete,
@ -10,14 +11,11 @@
pytest-xdist,
pytest-subprocess,
pytest-timeout,
remote-pdb,
ipdb,
python3,
runCommand,
setuptools,
sops,
stdenv,
wheel,
fakeroot,
rsync,
bash,
@ -30,33 +28,15 @@
gnupg,
e2fsprogs,
mypy,
rope,
clan-core-path,
}:
let
dependencies = [
argcomplete # optional dependency: if not enabled, shell completion will not work
# Dependencies that are directly used in the project
pythonDependencies = [
argcomplete # Enables shell completion; without it, this feature won't work.
];
pytestDependencies =
runtimeDependencies
++ dependencies
++ [
pytest
pytest-cov
pytest-subprocess
pytest-xdist
pytest-timeout
remote-pdb
ipdb
openssh
git
gnupg
stdenv.cc
];
# Optional dependencies for clan cli, we re-expose them here to make sure they all build.
# Runtime dependencies required by the application
runtimeDependencies = [
bash
nix
@ -74,14 +54,31 @@ let
e2fsprogs
];
# Dependencies required for running tests
testDependencies =
runtimeDependencies
++ [
gnupg
stdenv.cc # Compiler used for certain native extensions
]
++ pythonDependencies
++ [
pytest # Testing framework
pytest-cov # Generate coverage reports
pytest-subprocess # fake the real subprocess behavior to make your tests more independent.
pytest-xdist # Run tests in parallel on multiple cores
pytest-timeout # Add timeouts to your tests
];
# Convert runtimeDependencies into an attribute set for easier access
runtimeDependenciesAsSet = builtins.listToAttrs (
builtins.map (p: lib.nameValuePair (lib.getName p.name) p) runtimeDependencies
);
checkPython = python3.withPackages (_ps: pytestDependencies);
# Setup Python environment with all dependencies for running tests
pythonWithTestDeps = python3.withPackages (_ps: testDependencies);
# - vendor the jsonschema nix lib (copy instead of symlink).
# Interesting fact: using nixpkgs from flakes instead of nixpkgs.path is reduces evaluation time by 5s.
# Prepare the source code for the project, including copying over jsonschema and nixpkgs
source = runCommand "clan-cli-source" { } ''
cp -r ${./.} $out
chmod -R +w $out
@ -89,6 +86,8 @@ let
ln -s ${nixpkgs'} $out/clan_cli/nixpkgs
cp -r ${../../lib/jsonschema} $out/clan_cli/config/jsonschema
'';
# Create a custom nixpkgs for use within the project
nixpkgs' = runCommand "nixpkgs" { nativeBuildInputs = [ nix ]; } ''
mkdir $out
cat > $out/flake.nix << EOF
@ -114,36 +113,36 @@ python3.pkgs.buildPythonApplication {
src = source;
format = "pyproject";
makeWrapperArgs = [
# This prevents problems with mixed glibc versions that might occur when the
# cli is called through a browser built against another glibc
"--unset LD_LIBRARY_PATH"
];
# Arguments for the wrapper to unset LD_LIBRARY_PATH to avoid glibc version issues
makeWrapperArgs = [ "--unset LD_LIBRARY_PATH" ];
# Build-time dependencies.
nativeBuildInputs = [
setuptools
installShellFiles
];
propagatedBuildInputs = dependencies;
# also re-expose dependencies so we test them in CI
propagatedBuildInputs = pythonDependencies;
# Define and expose the tests and checks to run in CI
passthru.tests =
(lib.mapAttrs' (n: lib.nameValuePair "clan-dep-${n}") runtimeDependenciesAsSet)
// rec {
clan-pytest-without-core =
runCommand "clan-pytest-without-core" { nativeBuildInputs = [ checkPython ] ++ pytestDependencies; }
runCommand "clan-pytest-without-core"
{ nativeBuildInputs = [ pythonWithTestDeps ] ++ testDependencies; }
''
cp -r ${source} ./src
chmod +w -R ./src
cd ./src
export NIX_STATE_DIR=$TMPDIR/nix IN_NIX_SANDBOX=1
${checkPython}/bin/python -m pytest -m "not impure and not with_core" ./tests
${pythonWithTestDeps}/bin/python -m pytest -m "not impure and not with_core" ./tests
touch $out
'';
# separate the tests that can never be cached
clan-pytest-with-core =
runCommand "clan-pytest-with-core" { nativeBuildInputs = [ checkPython ] ++ pytestDependencies; }
runCommand "clan-pytest-with-core"
{ nativeBuildInputs = [ pythonWithTestDeps ] ++ testDependencies; }
''
cp -r ${source} ./src
chmod +w -R ./src
@ -151,15 +150,11 @@ python3.pkgs.buildPythonApplication {
export CLAN_CORE=${clan-core-path}
export NIX_STATE_DIR=$TMPDIR/nix IN_NIX_SANDBOX=1
${checkPython}/bin/python -m pytest -m "not impure and with_core" ./tests
${pythonWithTestDeps}/bin/python -m pytest -m "not impure and with_core" ./tests
touch $out
'';
clan-pytest = runCommand "clan-pytest" { } ''
echo ${clan-pytest-without-core}
echo ${clan-pytest-with-core}
touch $out
'';
# Utility to check for leftover debugging breakpoints in the codebase
check-for-breakpoints = runCommand "breakpoints" { } ''
if grep --include \*.py -Rq "breakpoint()" ${source}; then
echo "breakpoint() found in ${source}:"
@ -170,18 +165,13 @@ python3.pkgs.buildPythonApplication {
'';
};
# Additional pass-through attributes
passthru.nixpkgs = nixpkgs';
passthru.checkPython = checkPython;
passthru.devDependencies = [
rope
setuptools
wheel
] ++ pytestDependencies;
passthru.pytestDependencies = pytestDependencies;
passthru.testDependencies = testDependencies;
passthru.pythonWithTestDeps = pythonWithTestDeps;
passthru.runtimeDependencies = runtimeDependencies;
# Install shell completions for bash and fish using the argcomplete package
postInstall = ''
cp -r ${nixpkgs'} $out/${python3.sitePackages}/clan_cli/nixpkgs
installShellCompletion --bash --name clan \
@ -189,13 +179,17 @@ python3.pkgs.buildPythonApplication {
installShellCompletion --fish --name clan.fish \
<(${argcomplete}/bin/register-python-argcomplete --shell fish clan)
'';
# Don't leak python packages into a devshell.
# It can be very confusing if you `nix run` then load the cli from the devshell instead.
# Clean up after the package to avoid leaking python packages into a devshell
postFixup = ''
rm $out/nix-support/propagated-build-inputs
'';
# Run a basic check to ensure the application is executable
checkPhase = ''
PYTHONPATH= $out/bin/clan --help
'';
# Specify the main program for this package
meta.mainProgram = "clan";
}

View File

@ -1,59 +1,29 @@
{
nix-unit,
clan-cli,
system,
mkShell,
writeScriptBin,
openssh,
ruff,
python3,
}:
let
checkScript = writeScriptBin "check" ''
nix build .#checks.${system}.{treefmt,clan-pytest} -L "$@"
'';
pythonWithDeps = python3.withPackages (
ps: clan-cli.propagatedBuildInputs ++ clan-cli.devDependencies ++ [ ps.pip ]
);
devshellTestDeps =
clan-cli.passthru.testDependencies
++ (with python3.pkgs; [
rope
setuptools
wheel
pip
]);
in
mkShell {
packages = [
buildInputs = [
nix-unit
openssh
ruff
clan-cli.checkPython
];
] ++ devshellTestDeps;
shellHook = ''
tmp_path=$(realpath ./.direnv)
repo_root=$(realpath .)
mkdir -p "$tmp_path/python/${pythonWithDeps.sitePackages}"
# Install the package in editable mode
# This allows executing `clan` from within the dev-shell using the current
# version of the code and its dependencies.
${pythonWithDeps.interpreter} -m pip install \
--quiet \
--disable-pip-version-check \
--no-index \
--no-build-isolation \
--prefix "$tmp_path/python" \
--editable $repo_root
export PATH=$(pwd)/bin:$PATH
ln -sfT ${clan-cli.nixpkgs} clan_cli/nixpkgs
export PATH="$tmp_path/python/bin:${checkScript}/bin:$PATH"
export PYTHONPATH="$repo_root:$tmp_path/python/${pythonWithDeps.sitePackages}:"
export XDG_DATA_DIRS="$tmp_path/share''${XDG_DATA_DIRS:+:$XDG_DATA_DIRS}"
export fish_complete_path="$tmp_path/share/fish/vendor_completions.d''${fish_complete_path:+:$fish_complete_path}"
mkdir -p \
$tmp_path/share/fish/vendor_completions.d \
$tmp_path/share/bash-completion/completions \
$tmp_path/share/zsh/site-functions
register-python-argcomplete --shell fish clan > $tmp_path/share/fish/vendor_completions.d/clan.fish
register-python-argcomplete --shell bash clan > $tmp_path/share/bash-completion/completions/clan
'';
}

View File

@ -5,11 +5,13 @@ from pathlib import Path
import pytest
sys.path.append(os.path.join(os.path.dirname(__file__), "helpers"))
sys.path.append(str(Path(__file__).parent.parent)) # Also add clan_cli to PYTHONPATH
from clan_cli.custom_logger import setup_logging
from clan_cli.nix import nix_shell
sys.path.append(os.path.join(os.path.dirname(__file__), "helpers"))
pytest_plugins = [
"temporary_dir",
"root",

View File

@ -12,8 +12,8 @@ from fixtures_flakes import FlakeForTest, generate_flake
from root import CLAN_CORE
from clan_cli.dirs import vm_state_dir
from qemu.qga import QgaSession
from qemu.qmp import QEMUMonitorProtocol
from clan_cli.qemu.qga import QgaSession
from clan_cli.qemu.qmp import QEMUMonitorProtocol
if TYPE_CHECKING:
from age_keys import KeyPair

View File

@ -1,4 +1,5 @@
source_up
# See comment in clan-cli/.envrc
# source_up
watch_file flake-module.nix default.nix

View File

@ -0,0 +1,6 @@
import sys
from . import main
if __name__ == "__main__":
sys.exit(main())

View File

@ -12,7 +12,11 @@
clan-cli,
makeDesktopItem,
libadwaita,
weston,
pytest, # Testing framework
pytest-cov, # Generate coverage reports
pytest-subprocess, # fake the real subprocess behavior to make your tests more independent.
pytest-xdist, # Run tests in parallel on multiple cores
pytest-timeout, # Add timeouts to your tests
}:
let
source = ./.;
@ -24,6 +28,36 @@ let
startupWMClass = "clan";
mimeTypes = [ "x-scheme-handler/clan" ];
};
# Dependencies that are directly used in the project but nor from internal python packages
externalPythonDeps = [
pygobject3
pygobject-stubs
gtk4
libadwaita
gnome.adwaita-icon-theme
];
# Deps including python packages from the local project
allPythonDeps = [ (python3.pkgs.toPythonModule clan-cli) ] ++ externalPythonDeps;
# Runtime binary dependencies required by the application
runtimeDependencies = [ ];
# Dependencies required for running tests
testDependencies =
runtimeDependencies
++ allPythonDeps
++ [
pytest # Testing framework
pytest-cov # Generate coverage reports
pytest-subprocess # fake the real subprocess behavior to make your tests more independent.
pytest-xdist # Run tests in parallel on multiple cores
pytest-timeout # Add timeouts to your tests
];
# Setup Python environment with all dependencies for running tests
pythonWithTestDeps = python3.withPackages (_ps: testDependencies);
in
python3.pkgs.buildPythonApplication rec {
name = "clan-vm-manager";
@ -36,6 +70,7 @@ python3.pkgs.buildPythonApplication rec {
"--unset LD_LIBRARY_PATH"
];
# Deps needed only at build time
nativeBuildInputs = [
setuptools
copyDesktopItems
@ -43,50 +78,28 @@ python3.pkgs.buildPythonApplication rec {
gobject-introspection
];
buildInputs = [
gtk4
libadwaita
gnome.adwaita-icon-theme
];
# We need to propagate the build inputs to nix fmt / treefmt
propagatedBuildInputs = [
(python3.pkgs.toPythonModule clan-cli)
passthru.externalPythonDeps
];
checkPython = python3.withPackages (_ps: clan-cli.passthru.pytestDependencies);
devDependencies = [
checkPython
weston
] ++ nativeBuildInputs ++ buildInputs ++ propagatedBuildInputs;
passthru.checkPython = checkPython;
passthru.devDependencies = devDependencies;
# The necessity of setting buildInputs and propagatedBuildInputs to the
# same values for your Python package within Nix largely stems from ensuring
# that all necessary dependencies are consistently available both
# at build time and runtime,
buildInputs = allPythonDeps ++ runtimeDependencies;
propagatedBuildInputs = allPythonDeps ++ runtimeDependencies;
# also re-expose dependencies so we test them in CI
passthru = {
inherit desktop-file;
# Keep external dependencies in a separate lists to refer to thm elsewhere
# This helps avoiding issues like dev-shells accidentally depending on
# nix derivations of local packages.
externalPythonDeps = [
pygobject3
pygobject-stubs
];
tests = {
clan-vm-manager-pytest =
runCommand "clan-vm-manager-pytest" { nativeBuildInputs = devDependencies; }
runCommand "clan-vm-manager-pytest" { inherit buildInputs propagatedBuildInputs nativeBuildInputs; }
''
cp -r ${source} ./src
chmod +w -R ./src
cd ./src
export NIX_STATE_DIR=$TMPDIR/nix IN_NIX_SANDBOX=1
${checkPython}/bin/python -m pytest -m "not impure" ./tests
${pythonWithTestDeps}/bin/python -m pytest -s -m "not impure" ./tests
touch $out
'';
clan-vm-manager-no-breakpoints = runCommand "clan-vm-manager-no-breakpoints" { } ''
if grep --include \*.py -Rq "breakpoint()" ${source}; then
echo "breakpoint() found in ${source}:"
@ -98,6 +111,12 @@ python3.pkgs.buildPythonApplication rec {
};
};
# Additional pass-through attributes
passthru.desktop-file = desktop-file;
passthru.externalPythonDeps = externalPythonDeps;
passthru.testDependencies = testDependencies;
passthru.runtimeDependencies = runtimeDependencies;
# Don't leak python packages into a devshell.
# It can be very confusing if you `nix run` than load the cli from the devshell instead.
postFixup = ''

View File

@ -4,7 +4,7 @@
{ config, pkgs, ... }:
{
devShells.clan-vm-manager = pkgs.callPackage ./shell.nix {
inherit (config.packages) clan-cli clan-vm-manager;
inherit (config.packages) clan-vm-manager;
};
packages.clan-vm-manager = pkgs.python3.pkgs.callPackage ./default.nix {
inherit (config.packages) clan-cli;

View File

@ -30,10 +30,6 @@ disallow_untyped_calls = true
disallow_untyped_defs = true
no_implicit_optional = true
[[tool.mypy.overrides]]
module = "clan_cli.*"
ignore_missing_imports = true
[tool.ruff]
target-version = "py311"
line-length = 88

View File

@ -1,84 +1,46 @@
{
lib,
runCommand,
makeWrapper,
stdenv,
clan-vm-manager,
gdb,
gtk4,
libadwaita,
clan-cli,
mkShell,
ruff,
desktop-file-utils,
xdg-utils,
mypy,
python3,
python3Packages,
gtk4,
libadwaita,
}:
mkShell (
let
pygdb =
runCommand "pygdb"
{
buildInputs = [
gdb
python3
makeWrapper
];
}
''
mkdir -p "$out/bin"
makeWrapper "${gdb}/bin/gdb" "$out/bin/pygdb" \
--add-flags '-ex "source ${python3}/share/gdb/libpython.py"'
'';
in
rec {
inherit (clan-vm-manager) propagatedBuildInputs buildInputs;
linuxOnlyPackages = lib.optionals stdenv.isLinux [
xdg-utils
pygdb
];
# To debug clan-vm-manger execute pygdb --args python ./bin/clan-vm-manager
packages = [
ruff
desktop-file-utils
let
devshellTestDeps =
clan-vm-manager.testDependencies
++ (with python3.pkgs; [
rope
mypy
python3Packages.ipdb
gtk4.dev
ipdb
setuptools
wheel
pip
]);
in
mkShell {
inherit (clan-vm-manager) nativeBuildInputs;
buildInputs =
[
ruff
gtk4.dev # has the demo called 'gtk4-widget-factory'
libadwaita.devdoc # has the demo called 'adwaita-1-demo'
] ++ clan-vm-manager.devDependencies ++ linuxOnlyPackages;
]
++ devshellTestDeps
PYTHONBREAKPOINT = "ipdb.set_trace";
# Dependencies for testing for linux hosts
++ (lib.optionals stdenv.isLinux [
xdg-utils # install desktop files
desktop-file-utils # verify desktop files
]);
shellHook = ''
ln -sfT ${clan-cli.nixpkgs} ../clan-cli/clan_cli/nixpkgs
# prepend clan-cli for development
export PYTHONPATH=../clan-cli:$PYTHONPATH
if ! command -v xdg-mime &> /dev/null; then
echo "Warning: 'xdg-mime' is not available. The desktop file cannot be installed."
fi
# install desktop file
set -eou pipefail
DESKTOP_FILE_NAME=org.clan.vm-manager.desktop
DESKTOP_DST=~/.local/share/applications/$DESKTOP_FILE_NAME
DESKTOP_SRC=${clan-vm-manager}/share/applications/$DESKTOP_FILE_NAME
UI_BIN="${clan-vm-manager}/bin/clan-vm-manager"
cp -f $DESKTOP_SRC $DESKTOP_DST
sleep 2
sed -i "s|Exec=.*clan-vm-manager|Exec=$UI_BIN|" $DESKTOP_DST
xdg-mime default $DESKTOP_FILE_NAME x-scheme-handler/clan
echo "==== Validating desktop file installation ===="
set -x
desktop-file-validate $DESKTOP_DST
set +xeou pipefail
'';
}
)
shellHook = ''
export PATH=$(pwd)/bin:$PATH
'';
}

View File

@ -0,0 +1,8 @@
import time
from wayland import GtkProc
def test_open(app: GtkProc) -> None:
time.sleep(0.5)
assert app.poll() is None

View File

@ -1,5 +1,7 @@
import sys
from collections.abc import Generator
from subprocess import Popen
from typing import NewType
import pytest
@ -7,18 +9,19 @@ import pytest
@pytest.fixture(scope="session")
def wayland_compositor() -> Generator[Popen, None, None]:
# Start the Wayland compositor (e.g., Weston)
compositor = Popen(["weston", "--backend=headless-backend.so"])
# compositor = Popen(["weston", "--backend=headless-backend.so"])
compositor = Popen(["weston"])
yield compositor
# Cleanup: Terminate the compositor
compositor.terminate()
GtkProc = NewType("GtkProc", Popen)
@pytest.fixture(scope="function")
def gtk_app(wayland_compositor: Popen) -> Generator[Popen, None, None]:
# Assuming your GTK4 app can be started via a command line
# It's important to ensure it uses the Wayland session initiated by the fixture
env = {"GDK_BACKEND": "wayland"}
app = Popen(["clan-vm-manager"], env=env)
yield app
def app() -> Generator[GtkProc, None, None]:
rapp = Popen([sys.executable, "-m", "clan_vm_manager"], text=True)
yield GtkProc(rapp)
# Cleanup: Terminate your application
app.terminate()
rapp.terminate()