From b11c1b314fe7465a36806a846c6e4d92b34dbefa Mon Sep 17 00:00:00 2001 From: Jamie Gravendeel Date: Sat, 29 Nov 2025 18:53:34 +0100 Subject: [PATCH] games: Use Python's JSON module --- cartridges/__init__.py | 6 ++-- cartridges/games.py | 81 ++++++++++++++++++++++++++++++------------ 2 files changed, 62 insertions(+), 25 deletions(-) diff --git a/cartridges/__init__.py b/cartridges/__init__.py index 4fb6de3..5dbe7e0 100644 --- a/cartridges/__init__.py +++ b/cartridges/__init__.py @@ -1,5 +1,6 @@ # SPDX-License-Identifier: GPL-3.0-or-later # SPDX-FileCopyrightText: Copyright 2025 Zoey Ahmed +# SPDX-FileCopyrightText: Copyright 2025 Jamie Gravendeel import gettext import locale @@ -12,13 +13,14 @@ import gi gi.require_versions({ "Gtk": "4.0", "Adw": "1", - "Json": "1.0", }) -from gi.repository import Gio +from gi.repository import Gio, GLib from .config import LOCALEDIR, PKGDATADIR +DATA_DIR = Path(GLib.get_user_data_dir(), "cartridges") + _RESOURCES = ("data", "icons", "ui") signal.signal(signal.SIGINT, signal.SIG_DFL) diff --git a/cartridges/games.py b/cartridges/games.py index bc6bcff..ad0a680 100644 --- a/cartridges/games.py +++ b/cartridges/games.py @@ -1,22 +1,35 @@ # SPDX-License-Identifier: GPL-3.0-or-later # SPDX-FileCopyrightText: Copyright 2025 Zoey Ahmed # SPDX-FileCopyrightText: Copyright 2025 kramo +# SPDX-FileCopyrightText: Copyright 2025 Jamie Gravendeel +import json from collections.abc import Generator -from pathlib import Path -from typing import cast +from json import JSONDecodeError +from types import UnionType +from typing import Any -from gi.repository import ( - Gdk, - Gio, - GLib, - GObject, - Json, # pyright: ignore[reportAttributeAccessIssue] -) +from gi.repository import Gdk, Gio, GLib, GObject -DATA_DIR = Path(GLib.get_user_data_dir(), "cartridges") -GAMES_DIR = DATA_DIR / "games" -COVERS_DIR = DATA_DIR / "covers" +from cartridges import DATA_DIR + +_GAMES_DIR = DATA_DIR / "games" +_COVERS_DIR = DATA_DIR / "covers" + +_SPEC_VERSION = 2.0 +_PROPERTIES: dict[str, tuple[type | UnionType, bool]] = { + "added": (int, False), + "executable": (str | list[str], True), + "game_id": (str, True), + "source": (str, True), + "hidden": (bool, False), + "last_played": (int, False), + "name": (str, True), + "developer": (str, False), + "removed": (bool, False), + "blacklisted": (bool, False), + "version": (float, False), +} class Game(GObject.Object): @@ -29,30 +42,52 @@ class Game(GObject.Object): game_id = GObject.Property(type=str) source = GObject.Property(type=str) hidden = GObject.Property(type=bool, default=False) - last_played = GObject.Property(type=int, default=0) + last_played = GObject.Property(type=int) name = GObject.Property(type=str) developer = GObject.Property(type=str) removed = GObject.Property(type=bool, default=False) blacklisted = GObject.Property(type=bool, default=False) - version = GObject.Property(type=float, default=2.0) + version = GObject.Property(type=float, default=_SPEC_VERSION) cover = GObject.Property(type=Gdk.Texture) + def __init__(self, data: dict[str, Any]): + super().__init__() -def load() -> Generator[Game]: - """Load the user's games from disk.""" - for path in GAMES_DIR.glob("*.json"): + for name, (type_, required) in _PROPERTIES.items(): + value = data.get(name) + + if not required and value is None: + continue + + if not isinstance(value, type_): + raise TypeError + + match name: + case "executable" if isinstance(value, list): + value = " ".join(value) + case "version" if value and value > _SPEC_VERSION: + raise TypeError + case "version": + continue + + setattr(self, name, value) + + +def _load() -> Generator[Game]: + for path in _GAMES_DIR.glob("*.json"): try: - data = path.read_text("utf-8") - except UnicodeError: + with path.open(encoding="utf-8") as f: + data = json.load(f) + except (JSONDecodeError, UnicodeDecodeError): continue try: - game = cast(Game, Json.gobject_from_data(Game, data, len(data))) - except GLib.Error: + game = Game(data) + except TypeError: continue - cover_path = COVERS_DIR / game.game_id + cover_path = _COVERS_DIR / game.game_id for ext in ".gif", ".tiff": filename = str(cover_path.with_suffix(ext)) try: @@ -66,4 +101,4 @@ def load() -> Generator[Game]: model = Gio.ListStore.new(Game) -model.splice(0, 0, tuple(load())) +model.splice(0, 0, tuple(_load()))