clan-vm-manager: Add library for mypy pygobject types
All checks were successful
checks / check-links (pull_request) Successful in 22s
checks / checks-impure (pull_request) Successful in 2m7s
checks / checks (pull_request) Successful in 3m41s

This commit is contained in:
Luis Hebendanz 2024-03-09 23:15:32 +07:00
parent b985215cd6
commit 01351ff5a1
11 changed files with 68 additions and 44 deletions

View File

@ -33,10 +33,8 @@ class MainApplication(Adw.Application):
def __init__(self, *args: Any, **kwargs: Any) -> None: def __init__(self, *args: Any, **kwargs: Any) -> None:
super().__init__( super().__init__(
*args,
application_id="org.clan.vm-manager", application_id="org.clan.vm-manager",
flags=Gio.ApplicationFlags.HANDLES_COMMAND_LINE, flags=Gio.ApplicationFlags.HANDLES_COMMAND_LINE,
**kwargs,
) )
self.add_main_option( self.add_main_option(
@ -48,7 +46,7 @@ class MainApplication(Adw.Application):
None, None,
) )
self.window: Adw.ApplicationWindow | None = None self.window: "MainWindow" | None = None
self.connect("activate", self.on_activate) self.connect("activate", self.on_activate)
self.connect("shutdown", self.on_shutdown) self.connect("shutdown", self.on_shutdown)
@ -113,8 +111,10 @@ class MainApplication(Adw.Application):
log.debug(f"Style css path: {resource_path}") log.debug(f"Style css path: {resource_path}")
css_provider = Gtk.CssProvider() css_provider = Gtk.CssProvider()
css_provider.load_from_path(str(resource_path)) css_provider.load_from_path(str(resource_path))
display = Gdk.Display.get_default()
assert display is not None
Gtk.StyleContext.add_provider_for_display( Gtk.StyleContext.add_provider_for_display(
Gdk.Display.get_default(), display,
css_provider, css_provider,
Gtk.STYLE_PROVIDER_PRIORITY_APPLICATION, Gtk.STYLE_PROVIDER_PRIORITY_APPLICATION,
) )

View File

@ -134,8 +134,8 @@ class GKVStore(GObject.GObject, Gio.ListModel, Generic[K, V]):
def do_get_item(self, position: int) -> V | None: def do_get_item(self, position: int) -> V | None:
return self.get_item(position) return self.get_item(position)
def get_item_type(self) -> GObject.GType: def get_item_type(self) -> Any:
return self.gtype.__gtype__ return self.gtype.__gtype__ # type: ignore[attr-defined]
def do_get_item_type(self) -> GObject.GType: def do_get_item_type(self) -> GObject.GType:
return self.get_item_type() return self.get_item_type()
@ -187,10 +187,10 @@ class GKVStore(GObject.GObject, Gio.ListModel, Generic[K, V]):
return len(self._items) return len(self._items)
# O(1) operation # O(1) operation
def __getitem__(self, key: K) -> V: def __getitem__(self, key: K) -> V: # type: ignore[override]
return self._items[key] return self._items[key]
def __contains__(self, key: K) -> bool: def __contains__(self, key: K) -> bool: # type: ignore[override]
return key in self._items return key in self._items
def __str__(self) -> str: def __str__(self) -> str:

View File

@ -24,6 +24,9 @@ import sys
from collections.abc import Callable from collections.abc import Callable
from typing import Any, ClassVar from typing import Any, ClassVar
import gi
gi.require_version("Gtk", "4.0")
from gi.repository import GdkPixbuf, Gio, GLib, Gtk from gi.repository import GdkPixbuf, Gio, GLib, Gtk
@ -168,7 +171,7 @@ class ImplUnavailableError(Exception):
class BaseImplementation: class BaseImplementation:
def __init__(self, application: Gtk.Application) -> None: def __init__(self, application: Any) -> None:
self.application = application self.application = application
self.menu_items: dict[int, Any] = {} self.menu_items: dict[int, Any] = {}
self.menu_item_id: int = 1 self.menu_item_id: int = 1
@ -1090,8 +1093,8 @@ class Win32Implementation(BaseImplementation):
class TrayIcon: class TrayIcon:
def __init__(self, application: Gtk.Application) -> None: def __init__(self, application: Gio.Application) -> None:
self.application: Gtk.Application = application self.application: Gio.Application = application
self.available: bool = True self.available: bool = True
self.implementation: Any = None self.implementation: Any = None

View File

@ -32,13 +32,6 @@ class VMObject(GObject.Object):
"vm_status_changed": (GObject.SignalFlags.RUN_FIRST, None, []) "vm_status_changed": (GObject.SignalFlags.RUN_FIRST, None, [])
} }
def _vm_status_changed_task(self) -> bool:
self.emit("vm_status_changed")
return GLib.SOURCE_REMOVE
def update(self, data: HistoryEntry) -> None:
self.data = data
def __init__( def __init__(
self, self,
icon: Path, icon: Path,
@ -47,16 +40,20 @@ class VMObject(GObject.Object):
super().__init__() super().__init__()
# Store the data from the history entry # Store the data from the history entry
self.data = data self.data: HistoryEntry = data
# Create a process object to store the VM process # Create a process object to store the VM process
self.vm_process = MPProcess("vm_dummy", mp.Process(), Path("./dummy")) self.vm_process: MPProcess = MPProcess(
self.build_process = MPProcess("build_dummy", mp.Process(), Path("./dummy")) "vm_dummy", mp.Process(), Path("./dummy")
)
self.build_process: MPProcess = MPProcess(
"build_dummy", mp.Process(), Path("./dummy")
)
self._start_thread: threading.Thread = threading.Thread() self._start_thread: threading.Thread = threading.Thread()
self.machine: Machine | None = None self.machine: Machine | None = None
# Watcher to stop the VM # Watcher to stop the VM
self.KILL_TIMEOUT = 20 # seconds self.KILL_TIMEOUT: int = 20 # seconds
self._stop_thread: threading.Thread = threading.Thread() self._stop_thread: threading.Thread = threading.Thread()
# Build progress bar vars # Build progress bar vars
@ -66,7 +63,7 @@ class VMObject(GObject.Object):
self.prog_bar_id: int = 0 self.prog_bar_id: int = 0
# Create a temporary directory to store the logs # Create a temporary directory to store the logs
self.log_dir = tempfile.TemporaryDirectory( self.log_dir: tempfile.TemporaryDirectory = tempfile.TemporaryDirectory(
prefix="clan_vm-", suffix=f"-{self.data.flake.flake_attr}" prefix="clan_vm-", suffix=f"-{self.data.flake.flake_attr}"
) )
self._logs_id: int = 0 self._logs_id: int = 0
@ -75,14 +72,21 @@ class VMObject(GObject.Object):
# To be able to set the switch state programmatically # To be able to set the switch state programmatically
# we need to store the handler id returned by the connect method # we need to store the handler id returned by the connect method
# and block the signal while we change the state. This is cursed. # and block the signal while we change the state. This is cursed.
self.switch = Gtk.Switch() self.switch: Gtk.Switch = Gtk.Switch()
self.switch_handler_id: int = self.switch.connect( self.switch_handler_id: int = self.switch.connect(
"notify::active", self._on_switch_toggle "notify::active", self._on_switch_toggle
) )
self.connect("vm_status_changed", self._on_vm_status_changed) self.connect("vm_status_changed", self._on_vm_status_changed)
# Make sure the VM is killed when the reference to this object is dropped # Make sure the VM is killed when the reference to this object is dropped
self._finalizer = weakref.finalize(self, self._kill_ref_drop) self._finalizer: weakref.finalize = weakref.finalize(self, self._kill_ref_drop)
def _vm_status_changed_task(self) -> bool:
self.emit("vm_status_changed")
return GLib.SOURCE_REMOVE
def update(self, data: HistoryEntry) -> None:
self.data = data
def _on_vm_status_changed(self, source: "VMObject") -> None: def _on_vm_status_changed(self, source: "VMObject") -> None:
self.switch.set_state(self.is_running() and not self.is_building()) self.switch.set_state(self.is_running() and not self.is_building())
@ -93,9 +97,8 @@ class VMObject(GObject.Object):
exit_build = self.build_process.proc.exitcode exit_build = self.build_process.proc.exitcode
exitc = exit_vm or exit_build exitc = exit_vm or exit_build
if not self.is_running() and exitc != 0: if not self.is_running() and exitc != 0:
self.switch.handler_block(self.switch_handler_id) with self.switch.handler_block(self.switch_handler_id):
self.switch.set_active(False) self.switch.set_active(False)
self.switch.handler_unblock(self.switch_handler_id)
log.error(f"VM exited with error. Exitcode: {exitc}") log.error(f"VM exited with error. Exitcode: {exitc}")
def _on_switch_toggle(self, switch: Gtk.Switch, user_state: bool) -> None: def _on_switch_toggle(self, switch: Gtk.Switch, user_state: bool) -> None:

View File

@ -1,7 +1,7 @@
import logging import logging
import threading import threading
from collections.abc import Callable from collections.abc import Callable
from typing import Any, ClassVar from typing import Any, ClassVar, cast
import gi import gi
from clan_cli.clan_uri import ClanURI from clan_cli.clan_uri import ClanURI
@ -31,8 +31,8 @@ class JoinValue(GObject.Object):
def __init__(self, url: ClanURI) -> None: def __init__(self, url: ClanURI) -> None:
super().__init__() super().__init__()
self.url = url self.url: ClanURI = url
self.entry = None self.entry: HistoryEntry | None = None
def __join(self) -> None: def __join(self) -> None:
new_entry = add_history(self.url) new_entry = add_history(self.url)
@ -84,7 +84,7 @@ class JoinList:
value = JoinValue(uri) value = JoinValue(uri)
if value.url.machine.get_id() in [ if value.url.machine.get_id() in [
item.url.machine.get_id() for item in self.list_store cast(JoinValue, item).url.machine.get_id() for item in self.list_store
]: ]:
log.info(f"Join request already exists: {value.url}. Ignoring.") log.info(f"Join request already exists: {value.url}. Ignoring.")
return return

View File

@ -1,16 +1,19 @@
import os import os
from collections.abc import Callable from collections.abc import Callable
from functools import partial from functools import partial
from typing import Any, Literal from typing import Any, Literal, TypeVar
import gi import gi
gi.require_version("Adw", "1") gi.require_version("Adw", "1")
from gi.repository import Adw, Gio, GObject, Gtk from gi.repository import Adw, Gio, GObject, Gtk
# Define a TypeVar that is bound to GObject.Object
ListItem = TypeVar("ListItem", bound=GObject.Object)
def create_details_list( def create_details_list(
model: Gio.ListStore, render_row: Callable[[Gtk.ListBox, GObject], Gtk.Widget] model: Gio.ListStore, render_row: Callable[[Gtk.ListBox, ListItem], Gtk.Widget]
) -> Gtk.ListBox: ) -> Gtk.ListBox:
boxed_list = Gtk.ListBox() boxed_list = Gtk.ListBox()
boxed_list.set_selection_mode(Gtk.SelectionMode.NONE) boxed_list.set_selection_mode(Gtk.SelectionMode.NONE)
@ -49,7 +52,10 @@ class Details(Gtk.Box):
def render_entry_row( def render_entry_row(
self, boxed_list: Gtk.ListBox, item: PreferencesValue self, boxed_list: Gtk.ListBox, item: PreferencesValue
) -> Gtk.Widget: ) -> Gtk.Widget:
row = Adw.SpinRow.new_with_range(0, os.cpu_count(), 1) cores: int | None = os.cpu_count()
fcores = float(cores) if cores else 1.0
row = Adw.SpinRow.new_with_range(0, fcores, 1)
row.set_value(item.data) row.set_value(item.data)
return row return row

View File

@ -1,7 +1,7 @@
import logging import logging
from collections.abc import Callable from collections.abc import Callable
from functools import partial from functools import partial
from typing import Any from typing import Any, TypeVar
import gi import gi
from clan_cli import history from clan_cli import history
@ -17,9 +17,13 @@ from gi.repository import Adw, Gdk, Gio, GLib, GObject, Gtk
log = logging.getLogger(__name__) log = logging.getLogger(__name__)
ListItem = TypeVar("ListItem", bound=GObject.Object)
CustomStore = TypeVar("CustomStore", bound=Gio.ListModel)
def create_boxed_list( def create_boxed_list(
model: Gio.ListStore, render_row: Callable[[Gtk.ListBox, GObject], Gtk.Widget] model: CustomStore,
render_row: Callable[[Gtk.ListBox, ListItem], Gtk.Widget],
) -> Gtk.ListBox: ) -> Gtk.ListBox:
boxed_list = Gtk.ListBox() boxed_list = Gtk.ListBox()
boxed_list.set_selection_mode(Gtk.SelectionMode.NONE) boxed_list.set_selection_mode(Gtk.SelectionMode.NONE)
@ -47,8 +51,9 @@ class ClanList(Gtk.Box):
def __init__(self, config: ClanConfig) -> None: def __init__(self, config: ClanConfig) -> None:
super().__init__(orientation=Gtk.Orientation.VERTICAL) super().__init__(orientation=Gtk.Orientation.VERTICAL)
self.app = Gio.Application.get_default() app = Gio.Application.get_default()
self.app.connect("join_request", self.on_join_request) assert app is not None
app.connect("join_request", self.on_join_request)
self.log_label: Gtk.Label = Gtk.Label() self.log_label: Gtk.Label = Gtk.Label()
self.__init_machines = history.add.list_history() self.__init_machines = history.add.list_history()
@ -78,6 +83,7 @@ class ClanList(Gtk.Box):
add_action = Gio.SimpleAction.new("add", GLib.VariantType.new("s")) add_action = Gio.SimpleAction.new("add", GLib.VariantType.new("s"))
add_action.connect("activate", self.on_add) add_action.connect("activate", self.on_add)
app = Gio.Application.get_default() app = Gio.Application.get_default()
assert app is not None
app.add_action(add_action) app.add_action(add_action)
menu_model = Gio.Menu() menu_model = Gio.Menu()
@ -158,6 +164,7 @@ class ClanList(Gtk.Box):
open_action = Gio.SimpleAction.new("edit", GLib.VariantType.new("s")) open_action = Gio.SimpleAction.new("edit", GLib.VariantType.new("s"))
open_action.connect("activate", self.on_edit) open_action.connect("activate", self.on_edit)
app = Gio.Application.get_default() app = Gio.Application.get_default()
assert app is not None
app.add_action(open_action) app.add_action(open_action)
menu_model = Gio.Menu() menu_model = Gio.Menu()
menu_model.append("Edit", f"app.edit::{vm.get_id()}") menu_model.append("Edit", f"app.edit::{vm.get_id()}")
@ -199,6 +206,7 @@ class ClanList(Gtk.Box):
# Can't do this here because clan store is empty at this point # Can't do this here because clan store is empty at this point
if vm is not None: if vm is not None:
sub = row.get_subtitle() sub = row.get_subtitle()
assert sub is not None
row.set_subtitle( row.set_subtitle(
sub + "\nClan already exists. Joining again will update it" sub + "\nClan already exists. Joining again will update it"
) )

View File

@ -32,6 +32,7 @@ class MainWindow(Adw.ApplicationWindow):
view.add_top_bar(header) view.add_top_bar(header)
app = Gio.Application.get_default() app = Gio.Application.get_default()
assert app is not None
self.tray_icon: TrayIcon = TrayIcon(app) self.tray_icon: TrayIcon = TrayIcon(app)
# Initialize all ClanStore # Initialize all ClanStore

View File

@ -6,6 +6,7 @@
, wrapGAppsHook , wrapGAppsHook
, gtk4 , gtk4
, gnome , gnome
, pygobject-stubs
, gobject-introspection , gobject-introspection
, clan-cli , clan-cli
, makeDesktopItem , makeDesktopItem
@ -41,7 +42,9 @@ python3.pkgs.buildPythonApplication {
]; ];
buildInputs = [ gtk4 libadwaita gnome.adwaita-icon-theme ]; buildInputs = [ gtk4 libadwaita gnome.adwaita-icon-theme ];
propagatedBuildInputs = [ pygobject3 clan-cli ];
# We need to propagate the build inputs to nix fmt / treefmt
propagatedBuildInputs = [ pygobject3 clan-cli pygobject-stubs ];
# also re-expose dependencies so we test them in CI # also re-expose dependencies so we test them in CI
passthru = { passthru = {

View File

@ -22,9 +22,9 @@ disallow_untyped_calls = true
disallow_untyped_defs = true disallow_untyped_defs = true
no_implicit_optional = true no_implicit_optional = true
[[tool.mypy.overrides]] # [[tool.mypy.overrides]]
module = "gi.*" # module = "gi.*"
ignore_missing_imports = true # ignore_missing_imports = true
[[tool.mypy.overrides]] [[tool.mypy.overrides]]
module = "clan_cli.*" module = "clan_cli.*"

View File

@ -24,7 +24,7 @@ mkShell (
python3Packages.ipdb python3Packages.ipdb
gtk4.dev gtk4.dev
libadwaita.devdoc # has the demo called 'adwaita-1-demo' libadwaita.devdoc # has the demo called 'adwaita-1-demo'
] ++ clan-vm-manager.nativeBuildInputs; ] ++ clan-vm-manager.nativeBuildInputs ++ clan-vm-manager.propagatedBuildInputs;
PYTHONBREAKPOINT = "ipdb.set_trace"; PYTHONBREAKPOINT = "ipdb.set_trace";