refactor(python): execute full backend review plan (P0+P1+P2+P3)#15
Merged
refactor(python): execute full backend review plan (P0+P1+P2+P3)#15
Conversation
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
…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
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Summary
Executes every item from
docs/python-review.mdin one PR. The only deferred follow-up is bundlingpython-build-standaloneto remove the residual system-pydantic fallback dependency entirely.ruff,pyright(strict),pytestall greenWhat's in the box
P0 — release blockers
_vendor_compat.pyrewritten to manipulatesys.pathinstead ofshutil.rmtree-ing files (which silently no-op'd on read-only squashfs). Dropuvicorn[standard]→uvicornto kill 3 of 4 vendored.sofiles (uvloop, httptools, watchfiles). Trim_NATIVE_PACKAGESto the only one we actually ship (pydantic_core). Fix thePYTHONNOUSERSITE = ""bug inelectron/main.ts— empty IS truthy in CPython, which was disabling the user-site fallback we wanted; nowdelete env.PYTHONNOUSERSITE.__version__in__init__.pyis the only place; FastAPI title andelectron/package.jsonread from it.pydanticinpyproject.toml+python-runtime-requirements.txt.P1 — correctness / safety
paths.pywithsanitize_filename,safe_join,validate_user_path. Applied at every API boundary that takes a caller-supplied path:/open-path,/saves/restore,/prefixdelete,/mangohud/per-game/*, profiles, fixes, heroic configs.PROTONSHIFT_API_TOKEN, enforced by FastAPI middleware (secrets.compare_digest). CORS narrowed tolocalhost:3000+file://./healthexempt for the readiness probe.vdf_config,env_vars,mangohud,profiles_storage,fixes,heroic_configall usefsutil.atomic_write_text./launch-optionsand/compat-toolPUT now return 409 when Steam is running, so we don't get clobbered on Steam shutdown.get_power_profilesparsing bug (was overwriting parsed list with hardcoded one).BUILTIN_PROTONconstant.build_gamescope_argvreturnslist[str]forPopen;build_gamescope_cmdreturnsshlex.join'd shell-safe string.P2 — maintainability
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).app.py(1053 LOC) +__main__.py+theme.css; drop theprotonshiftconsole script.logging_setup.py— structured logging; replace silentexcept Exception: passwith logged warnings on narrow exception types.api.py(1183 LOC) into anapi/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_pathnames.fsutil.py—dir_size,human_size,atomic_write_text/bytes. Replaces 3 duplicated_dir_sizeimpls.discover_gameswith a 5s TTL.heroic._resolve_heroic_rootto publicresolve_heroic_root.--port; drops the fragile stdout-regex parsing.P3 — style nits
Hoisted module-level imports,
B904exception chains,I001import ordering,datetime.UTCforUP017, etc.Test plan
ruff check src/ tests/→ All checks passedpyright src/ tests/→ 0 errors, 0 warningspytest tests/→ 70 passedpython -m game_setup_hub.api --port 0boots cleanly, printsPORT:<n>from game_setup_hub.api import appexposes 56 routes (matches pre-split count)ModuleNotFoundErrorDeferred follow-ups
python-build-standaloneso the AppImage carries its own Python and no longer relies on a system pydantic_core fallback. Tracked separately.Made with Cursor