This commit is contained in:
kramo
2023-08-16 19:18:03 +02:00
parent a3aa7f9ccf
commit eeb18eb017
17 changed files with 102 additions and 89 deletions

View File

@@ -19,6 +19,7 @@
# SPDX-License-Identifier: GPL-3.0-or-later
import logging
from typing import Any, Optional
from gi.repository import Adw, GLib, Gtk
@@ -37,22 +38,22 @@ from src.utils.task import Task
class Importer(ErrorProducer):
"""A class in charge of scanning sources for games"""
progressbar = None
import_statuspage = None
import_dialog = None
summary_toast = None
progressbar: Gtk.ProgressBar
import_statuspage: Adw.StatusPage
import_dialog: Adw.MessageDialog
summary_toast: Adw.Toast
sources: set[Source] = None
sources: set[Source]
n_source_tasks_created: int = 0
n_source_tasks_done: int = 0
n_pipelines_done: int = 0
game_pipelines: set[Pipeline] = None
game_pipelines: set[Pipeline]
removed_game_ids: set[str] = set()
imported_game_ids: set[str] = set()
def __init__(self):
def __init__(self) -> None:
super().__init__()
# TODO: make this stateful
@@ -63,23 +64,23 @@ class Importer(ErrorProducer):
self.sources = set()
@property
def n_games_added(self):
def n_games_added(self) -> int:
return sum(
1 if not (pipeline.game.blacklisted or pipeline.game.removed) else 0
for pipeline in self.game_pipelines
)
@property
def pipelines_progress(self):
def pipelines_progress(self) -> float:
progress = sum(pipeline.progress for pipeline in self.game_pipelines)
try:
progress = progress / len(self.game_pipelines)
except ZeroDivisionError:
progress = 0
return progress
return progress # type: ignore
@property
def sources_progress(self):
def sources_progress(self) -> float:
try:
progress = self.n_source_tasks_done / self.n_source_tasks_created
except ZeroDivisionError:
@@ -87,16 +88,16 @@ class Importer(ErrorProducer):
return progress
@property
def finished(self):
def finished(self) -> bool:
return (
self.n_source_tasks_created == self.n_source_tasks_done
and len(self.game_pipelines) == self.n_pipelines_done
)
def add_source(self, source):
def add_source(self, source: Source) -> None:
self.sources.add(source)
def run(self):
def run(self) -> None:
"""Use several Gio.Task to import games from added sources"""
shared.win.get_application().lookup_action("import").set_enabled(False)
@@ -121,7 +122,7 @@ class Importer(ErrorProducer):
self.progress_changed_callback()
def create_dialog(self):
def create_dialog(self) -> None:
"""Create the import dialog"""
self.progressbar = Gtk.ProgressBar(margin_start=12, margin_end=12)
self.import_statuspage = Adw.StatusPage(
@@ -138,7 +139,9 @@ class Importer(ErrorProducer):
)
self.import_dialog.present()
def source_task_thread_func(self, _task, _obj, data, _cancellable):
def source_task_thread_func(
self, _task: Any, _obj: Any, data: tuple, _cancellable: Any
) -> None:
"""Source import task code"""
source: Source
@@ -192,27 +195,27 @@ class Importer(ErrorProducer):
pipeline.connect("advanced", self.pipeline_advanced_callback)
self.game_pipelines.add(pipeline)
def update_progressbar(self):
def update_progressbar(self) -> None:
"""Update the progressbar to show the overall import progress"""
# Reserve 10% for the sources discovery, the rest is the pipelines
self.progressbar.set_fraction(
(0.1 * self.sources_progress) + (0.9 * self.pipelines_progress)
)
def source_callback(self, _obj, _result, data):
def source_callback(self, _obj: Any, _result: Any, data: tuple) -> None:
"""Callback executed when a source is fully scanned"""
source, *_rest = data
logging.debug("Import done for source %s", source.source_id)
self.n_source_tasks_done += 1
self.progress_changed_callback()
def pipeline_advanced_callback(self, pipeline: Pipeline):
def pipeline_advanced_callback(self, pipeline: Pipeline) -> None:
"""Callback called when a pipeline for a game has advanced"""
if pipeline.is_done:
self.n_pipelines_done += 1
self.progress_changed_callback()
def progress_changed_callback(self):
def progress_changed_callback(self) -> None:
"""
Callback called when the import process has progressed
@@ -225,7 +228,7 @@ class Importer(ErrorProducer):
if self.finished:
self.import_callback()
def remove_games(self):
def remove_games(self) -> None:
"""Set removed to True for missing games"""
if not shared.schema.get_boolean("remove-missing"):
return
@@ -249,7 +252,7 @@ class Importer(ErrorProducer):
game.update()
self.removed_game_ids.add(game.game_id)
def import_callback(self):
def import_callback(self) -> None:
"""Callback called when importing has finished"""
logging.info("Import done")
self.remove_games()
@@ -261,11 +264,11 @@ class Importer(ErrorProducer):
self.create_error_dialog()
shared.win.get_application().lookup_action("import").set_enabled(True)
def create_error_dialog(self):
def create_error_dialog(self) -> None:
"""Dialog containing all errors raised by importers"""
# Collect all errors that happened in the importer and the managers
errors: list[Exception] = []
errors = []
errors.extend(self.collect_errors())
for manager in shared.store.managers.values():
errors.extend(manager.collect_errors())
@@ -313,7 +316,7 @@ class Importer(ErrorProducer):
dialog.present()
def undo_import(self, *_args):
def undo_import(self, *_args: Any) -> None:
for game_id in self.imported_game_ids:
shared.store[game_id].removed = True
shared.store[game_id].update()
@@ -330,7 +333,7 @@ class Importer(ErrorProducer):
logging.info("Import undone")
def create_summary_toast(self):
def create_summary_toast(self) -> Adw.Toast:
"""N games imported, removed toast"""
toast = Adw.Toast(timeout=0, priority=Adw.ToastPriority.HIGH)
@@ -371,16 +374,20 @@ class Importer(ErrorProducer):
shared.win.toast_overlay.add_toast(toast)
return toast
def open_preferences(self, page=None, expander_row=None):
def open_preferences(
self,
page_name: Optional[str] = None,
expander_row: Optional[Adw.ExpanderRow] = None,
) -> Adw.PreferencesWindow:
return shared.win.get_application().on_preferences_action(
page_name=page, expander_row=expander_row
page_name=page_name, expander_row=expander_row
)
def timeout_toast(self, *_args):
def timeout_toast(self, *_args: Any) -> None:
"""Manually timeout the toast after the user has dismissed all warnings"""
GLib.timeout_add_seconds(5, self.summary_toast.dismiss)
def dialog_response_callback(self, _widget, response, *args):
def dialog_response_callback(self, _widget: Any, response: str, *args: Any) -> None:
"""Handle after-import dialogs callback"""
logging.debug("After-import dialog response: %s (%s)", response, str(args))
if response == "open_preferences":

View File

@@ -1,7 +1,7 @@
import logging
from os import PathLike
from pathlib import Path
from typing import Iterable, Mapping, NamedTuple
from typing import Iterable, Mapping, NamedTuple, Optional
from src import shared
@@ -41,7 +41,7 @@ class Location:
paths: Mapping[str, LocationSubPath]
invalid_subtitle: str
root: Path = None
root: Optional[Path] = None
def __init__(
self,
@@ -94,7 +94,9 @@ class Location:
shared.schema.set_string(self.schema_key, value)
logging.debug("Resolved value for schema key %s: %s", self.schema_key, value)
def __getitem__(self, key: str):
def __getitem__(self, key: str) -> Optional[Path]:
"""Get the computed path from its key for the location"""
self.resolve()
return self.root / self.paths[key].segment
if self.root:
return self.root / self.paths[key].segment
return None

View File

@@ -20,19 +20,19 @@
import sys
from abc import abstractmethod
from collections.abc import Iterable
from typing import Any, Collection, Generator
from typing import Any, Collection, Generator, Optional
from src.game import Game
from src.importer.sources.location import Location
# Type of the data returned by iterating on a Source
SourceIterationResult = None | Game | tuple[Game, tuple[Any]]
SourceIterationResult = Optional[Game | tuple[Game, tuple[Any]]]
class SourceIterable(Iterable):
"""Data producer for a source of games"""
source: "Source" = None
source: "Source"
def __init__(self, source: "Source") -> None:
self.source = source
@@ -53,7 +53,7 @@ class Source(Iterable):
source_id: str
name: str
variant: str = None
variant: Optional[str] = None
available_on: set[str] = set()
iterable_class: type[SourceIterable]
@@ -65,7 +65,7 @@ class Source(Iterable):
def full_name(self) -> str:
"""The source's full name"""
full_name_ = self.name
if self.variant is not None:
if self.variant:
full_name_ += f" ({self.variant})"
return full_name_
@@ -75,7 +75,7 @@ class Source(Iterable):
return self.source_id + "_{game_id}"
@property
def is_available(self):
def is_available(self) -> bool:
return sys.platform in self.available_on
@property