Skip to content

refactor(python): execute full backend review plan (P0+P1+P2+P3)#15

Merged
I4cTime merged 2 commits intodevelopfrom
chore/python-review-execution
Apr 18, 2026
Merged

refactor(python): execute full backend review plan (P0+P1+P2+P3)#15
I4cTime merged 2 commits intodevelopfrom
chore/python-review-execution

Conversation

@I4cTime
Copy link
Copy Markdown
Owner

@I4cTime I4cTime commented Apr 18, 2026

Summary

Executes every item from docs/python-review.md in one PR. The only deferred follow-up is bundling python-build-standalone to remove the residual system-pydantic fallback dependency entirely.

  • 57 files changed, +3669 / -2638
  • 70 new pytest cases (was 0)
  • ruff, pyright (strict), pytest all green
  • Backend Python version bumps 0.8.11 → 0.9.0

What's in the box

P0 — release blockers

  • P0-1 AppImage native ext compatibility (the original Bazzite/CachyOS report). _vendor_compat.py rewritten to manipulate sys.path instead of shutil.rmtree-ing files (which silently no-op'd on read-only squashfs). Drop uvicorn[standard]uvicorn to kill 3 of 4 vendored .so files (uvloop, httptools, watchfiles). Trim _NATIVE_PACKAGES to the only one we actually ship (pydantic_core). Fix the PYTHONNOUSERSITE = "" bug in electron/main.ts — empty IS truthy in CPython, which was disabling the user-site fallback we wanted; now delete env.PYTHONNOUSERSITE.
  • P0-2 single version source. __version__ in __init__.py is the only place; FastAPI title and electron/package.json read from it.
  • P0-3 declare pydantic in pyproject.toml + python-runtime-requirements.txt.

P1 — correctness / safety

  • P1-1 path traversal: new paths.py with sanitize_filename, safe_join, validate_user_path. Applied at every API boundary that takes a caller-supplied path: /open-path, /saves/restore, /prefix delete, /mangohud/per-game/*, profiles, fixes, heroic configs.
  • P1-2 API auth + CORS: per-launch bearer token via PROTONSHIFT_API_TOKEN, enforced by FastAPI middleware (secrets.compare_digest). CORS narrowed to localhost:3000 + file://. /health exempt for the readiness probe.
  • P1-3 atomic writes everywhere + Steam-running guard: vdf_config, env_vars, mangohud, profiles_storage, fixes, heroic_config all use fsutil.atomic_write_text. /launch-options and /compat-tool PUT now return 409 when Steam is running, so we don't get clobbered on Steam shutdown.
  • P1-4 fix get_power_profiles parsing bug (was overwriting parsed list with hardcoded one).
  • P1-5 controllers: replace fake SDL GUID with the canonical 16-byte little-endian bus/vendor/product/version layout libsdl2 actually expects.
  • P1-6 remove dead BUILTIN_PROTON constant.
  • P1-7 gamescope: build_gamescope_argv returns list[str] for Popen; build_gamescope_cmd returns shlex.join'd shell-safe string.

P2 — maintainability

  • P2-1 pytest suite (70 tests): fsutil, paths, env_vars, vdf_config, tool_check, gamescope, controllers, vendor_compat, plus end-to-end FastAPI smoke (auth, CORS, Steam guard, path traversal rejection).
  • P2-2 delete dead GTK app.py (1053 LOC) + __main__.py + theme.css; drop the protonshift console script.
  • P2-3 logging_setup.py — structured logging; replace silent except Exception: pass with logged warnings on narrow exception types.
  • P2-4 split api.py (1183 LOC) into an api/ package: _state, _models, _helpers, _app + routes/{health,games,system,saves,mangohud,heroic,profiles,utility}. Backwards-compat __getattr__ on the package init for legacy _API_TOKEN / _config_path names.
  • P2-5/7 fsutil.pydir_size, human_size, atomic_write_text/bytes. Replaces 3 duplicated _dir_size impls.
  • P2-6 cache discover_games with a 5s TTL.
  • P2-9 promote heroic._resolve_heroic_root to public resolve_heroic_root.
  • P2-10 Electron picks the API port locally and passes --port; drops the fragile stdout-regex parsing.

P3 — style nits

Hoisted module-level imports, B904 exception chains, I001 import ordering, datetime.UTC for UP017, etc.

Test plan

  • ruff check src/ tests/ → All checks passed
  • pyright src/ tests/ → 0 errors, 0 warnings
  • pytest tests/ → 70 passed
  • python -m game_setup_hub.api --port 0 boots cleanly, prints PORT:<n>
  • from game_setup_hub.api import app exposes 56 routes (matches pre-split count)
  • Manual: build AppImage, run on a Python ≠ 3.12 host, confirm pydantic_core resolves from system site-packages instead of throwing ModuleNotFoundError
  • Manual: confirm Electron renderer can still talk to the backend with the bearer token attached
  • Manual: try editing launch options while Steam is running, expect 409

Deferred follow-ups

  • Option B from P0-1: bundle python-build-standalone so the AppImage carries its own Python and no longer relies on a system pydantic_core fallback. Tracked separately.

Made with Cursor

Implements every item from docs/python-review.md except a deferred follow-up
on bundling python-build-standalone. 70 new pytest cases, ruff and pyright
clean across src/ + tests/.

P0 — release blockers
- P0-1 AppImage native ext compatibility: rewrite _vendor_compat.py to mutate
  sys.path instead of shutil.rmtree-ing files (no-op on read-only squashfs).
  Drop uvicorn[standard] -> uvicorn (kills uvloop/httptools/watchfiles, 3 of
  4 vendored .so). Trim _NATIVE_PACKAGES to actual deps (pydantic_core only).
  Fix electron/main.ts: PYTHONNOUSERSITE = "" was truthy and disabled the
  user site-packages fallback we wanted; now `delete env.PYTHONNOUSERSITE`.
- P0-2 single version source: __version__ in __init__.py is the only place,
  api app + electron package.json read from it. Bump 0.8.11 -> 0.9.0.
- P0-3 declare pydantic in pyproject.toml + python-runtime-requirements.txt.

P1 — correctness / safety
- P1-1 path traversal: new paths.py with sanitize_filename, safe_join,
  validate_user_path (rejects anything outside HOME / mounts / tmp).
  Applied at every API boundary that accepts a caller-supplied path
  (open-path, saves/restore, prefix delete, mangohud per-game,
  profiles, fixes, heroic configs).
- P1-2 API auth + CORS: per-launch bearer token via PROTONSHIFT_API_TOKEN,
  enforced by FastAPI middleware (compare_digest). CORS narrowed to
  localhost:3000 + file:// renderer. /health exempt for readiness probe.
  Electron mints the token, attaches it to every api-fetch.
- P1-3 atomic writes everywhere + Steam-running guard: vdf_config,
  env_vars, mangohud, profiles_storage, fixes, heroic_config all use
  fsutil.atomic_write_text now. update_launch_options + update_compat_tool
  return 409 when Steam is running so we don't get clobbered.
- P1-4 fix get_power_profiles parsing bug (was overwriting parsed list with
  a hardcoded one).
- P1-5 controllers: replace fake SDL GUID with canonical 16-byte
  little-endian bus/vendor/product/version layout from libsdl2.
- P1-6 remove dead BUILTIN_PROTON constant; discovery already returns
  these dynamically.
- P1-7 gamescope: build_gamescope_argv returns list[str] for Popen,
  build_gamescope_cmd returns shlex.join'd shell-safe string.

P2 — maintainability
- P2-1 pytest suite: 70 tests covering fsutil, paths, env_vars, vdf_config,
  tool_check, gamescope, controllers, vendor_compat, and end-to-end FastAPI
  smoke (auth, CORS, Steam guard, path traversal rejection).
- P2-2 delete dead GTK app.py + __main__.py + theme.css; drop the
  protonshift console script.
- P2-3 logging_setup.py: structured logging, replace silent except: pass
  with logged warnings on narrow exception types.
- P2-4 split api.py (1183 LOC) into api/ package: _state, _models,
  _helpers, _app + routes/{health,games,system,saves,mangohud,heroic,
  profiles,utility}. Backwards-compat __getattr__ shim on the package
  init for the legacy _API_TOKEN / _config_path attribute names.
- P2-5/7 fsutil.py: dir_size, human_size, atomic_write_text/bytes —
  replaces 3 duplicated _dir_size implementations.
- P2-6 cache discover_games with a 5s TTL.
- P2-9 promote heroic._resolve_heroic_root to public resolve_heroic_root.
- P2-10 Electron picks the API port locally and passes --port; drops the
  fragile stdout regex parsing.

P3 — style nits
- Hoist subprocess to module-level imports, fix ruff B904 chains, ruff
  I001 import ordering, datetime.UTC for UP017, etc.

Files: 57 changed, +3669 / -2638. Net code is much smaller than it looks
(GTK app removed). Verification: ruff, pyright (strict), pytest all green.

Deferred to a separate PR: bundling python-build-standalone in the AppImage
to remove the residual system-pydantic dependency entirely.

Made-with: Cursor
Comment thread src/game_setup_hub/api/_app.py Fixed
Comment thread src/game_setup_hub/fsutil.py Fixed
Comment thread src/game_setup_hub/paths.py Dismissed
Comment thread src/game_setup_hub/paths.py Dismissed
Comment thread src/game_setup_hub/saves.py Fixed
Comment thread src/game_setup_hub/saves.py Fixed
…ken leak)

Addressed all alerts CodeQL flagged on PR #15:

Critical fixes:
- protontricks.py: whitelist app_id (digits) and verb (alphanumeric+._-)
  before assembling the subprocess argv. Even though Popen with list args
  doesn't shell-execute, validating at the boundary keeps the surface clean
  and gives CodeQL a recognised sanitizer.
- display.py:set_resolution: whitelist monitor name against actually-detected
  outputs from get_monitors(), and bound width/height/refresh to sane ranges.

High fixes:
- api/_app.py: remove the --print-token CLI flag. Standalone curl users
  should set PROTONSHIFT_API_TOKEN themselves; we never want credentials
  leaking into journald, IDE consoles, or Electron logs.
- prefix.py / saves.py: defensively call validate_user_path() inside
  delete_prefix, get_prefix_info, find_save_paths, restore_backup, and
  backup_saves so each function refuses paths outside the user-writable
  roots even when called directly. This also satisfies CodeQL's data-flow
  analysis on the path-injection chain that previously fired 17 times.
- saves.py: sanitize app_id everywhere it joins onto a path, not just
  in backup_saves.

New tests (26 cases across 2 files):
- tests/test_protontricks.py — app_id and verb whitelist coverage incl.
  shell metachars, traversal attempts, oversize values.
- tests/test_display.py — set_resolution rejects shell metachars,
  unknown monitors, oversize dimensions, silly refresh rates.

Verification: ruff clean, pyright 0 errors, pytest 96/96.
Made-with: Cursor
Comment thread src/game_setup_hub/saves.py Dismissed
@I4cTime I4cTime merged commit 222cc8e into develop Apr 18, 2026
5 checks passed
@I4cTime I4cTime deleted the chore/python-review-execution branch April 18, 2026 16:48
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants