Skip to content

Conversation

@Gatsik
Copy link
Contributor

@Gatsik Gatsik commented Dec 27, 2025

Summary by CodeRabbit

  • New Features

    • Rating-history paging (Load next / Load all) with persisted default pages; status bar now shows ICE polling and game-launch progress; background lazy-loading for heavy modules and disk-backed image loading exposes image size.
  • Bug Fixes

    • Replay table alignment fixed; improved multi-team win/tie detection; single-call game termination to stop running game processes.
  • Refactor

    • Centralized API parsing/conversion and widespread lazy UI/setup to reduce startup cost and streamline data flows.
  • Chores

    • Core dependencies upgraded; numpy added.

✏️ Tip: You can customize this high-level summary in your review settings.

@coderabbitai
Copy link

coderabbitai bot commented Dec 27, 2025

📝 Walkthrough

Walkthrough

Centralized JSON API via JsonApiBase/ApiAccessManager; connectors migrated to parsed/converted flows; many thin parser modules removed; lazy heavy-module imports and BackgroundImporter added; rating-history pagination UI introduced; updater rewired to FilesObtainer async flow; ICE polling signals and status-bar wiring added.

Changes

Cohort / File(s) Summary
Dependencies
requirements.txt
Bumped cx_Freeze, idna, pydantic, pyqt6; added numpy 2.3.5.
Build config
setup.py
Added build_exe_options["includes"] for pyqtgraph and scipy_ndimage.ndimage._filters.
UI resources
res/*
res/client/client.ui: labelAutomatchInfolabelStatusBarInfo. res/player_card/playercard.ui: added defaultRatingPagesSpinBox, label_9, loadNextRatingHistoryPageButton; changed loadMoreRatingHistoryButton text to "Load all".
API core
src/api/ApiBase.py, src/api/ApiAccessors.py
ApiBaseJsonApiBase; added ApiAccessManager global instance; introduced ParsedApiResponse types; new decode/_decode_and_handle; new parsed/converted entrypoints (get_by_query_parsed, post_and_parse, convert_parsed) and handler adapters.
Connectors & converters
src/api/*_api.py
Many connectors moved from prepare_data/requestData to get_by_query_parsed/convert_parsed; added signals and new methods in featured_mod, coop, stats, vaults, sim_mod_updater, matchmaker, etc.
Parsers removed
src/api/parsers/*
Removed numerous thin parser modules (CoopResultParser, CoopScenarioParser, FeaturedModParser/FileParser, Map/MapVersion, Mod/ModVersion, PlayerParser, Leaderboard* parsers, ReviewsSummaryParser, GeneratedMapParamsParser, etc.).
Updater / FA flow
src/fa/game_updater/*, src/fa/check.py, src/fa/game_process.py, src/fa/__init__.py
Added FileGroup and FilesObtainer; Updater now accepts obtainer+worker and coordinates async file requests; UpdaterWorker API changed to file-driven update; added kill_all_if_running and exported it.
Client & ICE polling
src/fa/game_session.py, src/client/_clientwindow.py
Added ice_poll_started/ice_poll_finished signals; ClientWindow wired handlers, switched UI label usage to labelStatusBarInfo, started BackgroundImporter, and added on_ice_poll_started / on_ice_poll_finished.
Heavy modules & background import
src/heavy_modules.py
New lazy_import proxies for np, pg, scipy_ndimage; _BackgroundImporter warms heavy modules; BackgroundImporter instance exposed and started from client.
Download manager
src/downloadManager/__init__.py
Disk-backed image index with lazy in-memory caching; _try_load_from_disk helper; added ImageDownloader.image_size() accessor.
Sound caching
src/chat/channel_tab.py, src/notifications/ns_dialog.py
Added cached module-level sound factories (channel_ping_sound, sound_effect) and replaced per-instance sound objects.
Playercard & plotting
src/playercard/*
Injected shared image downloader into formatters/dialogs; rating-history pagination UI and handlers; replaced direct numpy/pyqtgraph imports with heavy module proxies and updated NDArray typing.
Plot/graphics composition
src/qt/graphics/*
Replaced subclassing with composition for bar/pie items (introducing .item), adjusted constructors and rendering delegation; PieChartItem constructor extended (labels, colors, radius).
Replays & details
src/replays/*
Moved QNetworkAccessManager to class-level downloader; deferred appearance setup and tab handling; handlers accept ParsedApiResponse; added gaussian_filter indirection; plotting uses .item.
Vaults & lazy setup
src/vaults/*
Vault classes converted to lazy setup() pattern; connectors use get_by_query_parsed/post_and_parse; convert_parsed replaces many prepare_data uses.
Widgets lazy-init flags
src/coop/_coopwidget.py, src/vaults/vault.py, src/vaults/*vault.py
Added setup() methods and flags (ui_loaded, scenarios_loaded, _loaded) to gate on-demand initialization.
Miscellaneous
various
Wide adoption of ParsedApiResponse, updated type hints/signals, removal of many parser adapters, changes to image/sound IO, and UI label renames across modules.

Sequence Diagram(s)

sequenceDiagram
    participant Client as Client/UI
    participant JsonApiBase as JsonApiBase
    participant ApiAccessManager as ApiAccessManager
    participant QNAM as QNetworkAccessManager
    participant Handler as ResponseHandler

    Client->>JsonApiBase: get_by_query_parsed(query, handler)
    JsonApiBase->>ApiAccessManager: get(endpoint, _decode_and_handle(handler))
    ApiAccessManager->>QNAM: send request
    QNAM-->>ApiAccessManager: reply
    ApiAccessManager->>ApiAccessManager: on_request_finished(reply)
    ApiAccessManager->>JsonApiBase: decode_reply(reply) -> ApiResponse
    JsonApiBase->>JsonApiBase: parse_message(ApiResponse) -> ParsedApiResponse
    JsonApiBase->>Handler: invoke handler(parsed)
    Handler-->>Client: processed result
Loading
sequenceDiagram
    participant Updater as Updater
    participant Obtainer as FilesObtainer
    participant FModApi as FeaturedModApiConnector
    participant FModFiles as FeaturedModFilesApiConnector
    participant Worker as UpdaterWorker

    Updater->>Obtainer: request_files()
    Obtainer->>FModApi: get_by_query_parsed(...)
    FModApi-->>Obtainer: convert_parsed(parsed) -> main_files
    Obtainer->>FModFiles: get_by_query_parsed(...)
    FModFiles-->>Obtainer: convert_parsed(parsed) -> fmod_files
    Obtainer->>Obtainer: check_finish()
    Obtainer-->>Updater: finished(main_files, fmod_files)
    Updater->>Worker: do_update((main_files, fmod_files))
    Worker->>Worker: update_featured_mod(files)
    Worker-->>Updater: progress / completion events
Loading

Estimated code review effort

🎯 5 (Critical) | ⏱️ ~120 minutes

Possibly related PRs

  • Add veto support and more #1145: Shares map-pool/matchmaker and connector changes (map-pool types, connector flows) and likely overlaps with parsed/converted API migration.

Poem

🐰 I hopped through parsers, one by one,
Signals chimed and background imports spun,
Pages for ratings flip with a click,
Files obtain, the updater hums quick,
A rabbit bows — the client's work is done!

Pre-merge checks and finishing touches

❌ Failed checks (1 warning, 1 inconclusive)
Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 3.82% which is insufficient. The required threshold is 80.00%. You can run @coderabbitai generate docstrings to improve docstring coverage.
Title check ❓ Inconclusive The title "Improve performance and more" is vague and generic. While it hints at performance improvements, it doesn't clearly summarize the main changes in the changeset, which include major API refactoring (moving from PreParsedApiResponse to ParsedApiResponse), dependency updates, UI improvements, and the introduction of lazy loading mechanisms. Use a more specific title that captures the primary change, such as "Refactor API response handling and add lazy loading" or "Migrate to ParsedApiResponse and optimize resource loading".
✅ Passed checks (1 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
✨ Finishing touches
  • 📝 Generate docstrings
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Post copyable unit tests in a comment

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands and usage tips.

Copy link

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 14

Caution

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (1)
src/api/player_api.py (1)

42-50: Potential KeyError or TypeError when player has no clan membership.

Line 45 accesses player_dict["clanMembership"]["clan"] without checking if clanMembership exists or is not None. If a player has no clan membership, this will raise an exception.

🔎 Proposed fix with defensive check
     def handle_player(self, message: PreProcessedApiResponse) -> None:
         player_dict, = message["data"]
         player = Player(**player_dict)
-        if "clan" in player_dict["clanMembership"]:
+        clan_membership_dict = player_dict.get("clanMembership")
+        if clan_membership_dict and "clan" in clan_membership_dict:
-            clan = Clan(**player_dict["clanMembership"]["clan"])
-            clan_membership = ClanMembership(**player_dict["clanMembership"])
+            clan = Clan(**clan_membership_dict["clan"])
+            clan_membership = ClanMembership(**clan_membership_dict)
             clan_membership.custom_clan = clan
             player.custom_clan_membership = clan_membership
         self.player_ready.emit(player)
🧹 Nitpick comments (6)
src/replays/replayitem.py (1)

337-342: Clean refactoring to sorting-based winner detection.

The new logic correctly identifies the highest-scoring team and handles ties by clearing teamWin when multiple teams share the top score.

Minor: Unused loop variable flagged by Ruff.

The team variable is unpacked but not used within the loop body. Consider using _ to indicate it's intentionally unused:

🔎 Suggested refactor
-            for team, score in sorted_scores[1:]:
+            for _, score in sorted_scores[1:]:
                 if score >= mvt:
                     self.teamWin = None
                     break
src/api/ApiBase.py (1)

237-254: Consider defensive handling for missing reply entries.

If on_request_finished is invoked for a reply that was never registered (edge case), handlers.pop(reply) at line 252 would raise a KeyError. While unlikely in normal operation, using pop(reply, None) would be more defensive.

🔎 Optional defensive fix
-        self.handlers.pop(reply)
-        self.error_handlers.pop(reply)
+        self.handlers.pop(reply, None)
+        self.error_handlers.pop(reply, None)
res/player_card/playercard.ui (1)

361-365: Consider adding a maximum value to the spin box.

The defaultRatingPagesSpinBox has a minimum of 1 but no maximum. Consider setting a reasonable maximum (e.g., 50 or 100) to prevent users from accidentally requesting an excessive number of pages at once.

🔎 Suggested addition
 <widget class="QSpinBox" name="defaultRatingPagesSpinBox">
  <property name="minimum">
   <number>1</number>
  </property>
+ <property name="maximum">
+  <number>50</number>
+ </property>
 </widget>
src/stats/leaderboard_widget.py (1)

163-166: Consider guarding against missing meta field.

The ParsedApiResponse TypedDict shows meta as NotRequired, but process_rating_info directly accesses parsed["meta"]. If the API ever returns a response without meta, this will raise a KeyError.

🔎 Suggested defensive access
 def process_rating_info(self, parsed: ParsedApiResponse) -> None:
     self.createLeaderboard(parsed)
-    self.processMeta(parsed["meta"])
+    if "meta" in parsed:
+        self.processMeta(parsed["meta"])
     self.resetLoading()
     self.timer.stop()
src/api/stats_api.py (1)

189-193: Generator return type may cause single-consumption issue.

The convert_parsed returns a generator expression (Achievement(...) for ...) typed as Iterator[Achievement]. Generators can only be iterated once; if the caller stores the result and tries to iterate twice, the second iteration will be empty.

Consider using a list if multiple iterations are expected, or document that this is intentional for memory efficiency.

🔎 Alternative using list for multi-iteration safety
     def convert_parsed(
         self,
         parsed: ParsedApiResponse,
-    ) -> dict[str, Iterator[Achievement]]:
-        return {"values": (Achievement(**entry) for entry in parsed["data"])}
+    ) -> dict[str, list[Achievement]]:
+        return {"values": [Achievement(**entry) for entry in parsed["data"]]}
src/replays/_replayswidget.py (1)

606-610: Consider adding type guard for robustness.

While the current code structure ensures only LocalReplayBucketItem instances with LocalReplayItem children trigger this handler, adding an explicit type check would make the code more defensive and self-documenting.

🔎 Proposed enhancement
     def on_item_expanded(self, item: LocalReplayBucketItem) -> None:
+        if not isinstance(item, LocalReplayBucketItem):
+            return
         for index in range(item.childCount()):
-            cast(LocalReplayItem, item.child(index))._setup_appearance()
+            child = item.child(index)
+            if isinstance(child, LocalReplayItem):
+                child._setup_appearance()
📜 Review details

Configuration used: defaults

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between 5294f80 and 04ec72f.

📒 Files selected for processing (62)
  • requirements.txt
  • res/client/client.ui
  • res/player_card/playercard.ui
  • setup.py
  • src/api/ApiAccessors.py
  • src/api/ApiBase.py
  • src/api/coop_api.py
  • src/api/featured_mod_api.py
  • src/api/matchmaker_queue_api.py
  • src/api/parsers/CoopResultParser.py
  • src/api/parsers/CoopScenarioParser.py
  • src/api/parsers/FeaturedModFileParser.py
  • src/api/parsers/FeaturedModParser.py
  • src/api/parsers/GeneratedMapParamsParser.py
  • src/api/parsers/LeaderboardParser.py
  • src/api/parsers/LeaderboardRatingJournalParser.py
  • src/api/parsers/LeaderboardRatingParser.py
  • src/api/parsers/MapParser.py
  • src/api/parsers/MapVersionParser.py
  • src/api/parsers/ModParser.py
  • src/api/parsers/ModVersionParser.py
  • src/api/parsers/PlayerParser.py
  • src/api/parsers/ReviewsSummaryParser.py
  • src/api/parsers/__init__.py
  • src/api/player_api.py
  • src/api/sim_mod_updater.py
  • src/api/stats_api.py
  • src/api/vaults_api.py
  • src/chat/channel_tab.py
  • src/client/_clientwindow.py
  • src/coop/_coopwidget.py
  • src/downloadManager/__init__.py
  • src/fa/__init__.py
  • src/fa/check.py
  • src/fa/game_process.py
  • src/fa/game_session.py
  • src/fa/game_updater/updater.py
  • src/fa/game_updater/worker.py
  • src/games/_gameswidget.py
  • src/heavy_modules.py
  • src/notifications/ns_dialog.py
  • src/playercard/leagueformatter.py
  • src/playercard/playerinfodialog.py
  • src/playercard/plot.py
  • src/playercard/ratingtabwidget.py
  • src/playercard/statistics.py
  • src/qt/graphics/labeledbargraphitem.py
  • src/qt/graphics/piechartitem.py
  • src/replays/_replayswidget.py
  • src/replays/replaydetails/replaycard.py
  • src/replays/replaydetails/replayreader.py
  • src/replays/replaydetails/tabs/gamestats.py
  • src/replays/replaydetails/tabs/heatmap.py
  • src/replays/replayitem.py
  • src/stats/_statswidget.py
  • src/stats/leaderboard_widget.py
  • src/vaults/detailswidget.py
  • src/vaults/mapvault/mapdetails.py
  • src/vaults/mapvault/mapvault.py
  • src/vaults/modvault/modvault.py
  • src/vaults/reviewwidget.py
  • src/vaults/vault.py
💤 Files with no reviewable changes (15)
  • src/api/parsers/FeaturedModParser.py
  • src/api/parsers/FeaturedModFileParser.py
  • src/api/parsers/CoopScenarioParser.py
  • src/api/parsers/MapParser.py
  • src/api/parsers/PlayerParser.py
  • src/api/parsers/LeaderboardParser.py
  • src/api/parsers/GeneratedMapParamsParser.py
  • src/api/parsers/CoopResultParser.py
  • src/api/parsers/ModParser.py
  • src/api/parsers/LeaderboardRatingParser.py
  • src/api/parsers/MapVersionParser.py
  • src/stats/_statswidget.py
  • src/api/parsers/LeaderboardRatingJournalParser.py
  • src/api/parsers/ReviewsSummaryParser.py
  • src/api/parsers/ModVersionParser.py
🧰 Additional context used
🧠 Learnings (1)
📚 Learning: 2025-11-23T00:07:39.893Z
Learnt from: Gatsik
Repo: FAForever/client PR: 1145
File: src/api/vaults_api.py:114-116
Timestamp: 2025-11-23T00:07:39.893Z
Learning: In src/api/vaults_api.py, the MapPoolApiConnector class uses intentional stateful filter mutation. The request_pool_for_queue method sets self._filters and is the intended public API; request_data() should not be called directly on instances of this class.

Applied to files:

  • src/api/vaults_api.py
🧬 Code graph analysis (29)
src/vaults/vault.py (2)
src/vaults/mapvault/mapvault.py (1)
  • setup (26-40)
src/vaults/modvault/modvault.py (1)
  • setup (27-47)
src/api/matchmaker_queue_api.py (3)
src/api/ApiBase.py (1)
  • ParsedApiResponse (38-40)
src/api/featured_mod_api.py (2)
  • convert_parsed (17-21)
  • convert_parsed (32-36)
src/api/ApiAccessors.py (1)
  • convert_parsed (198-199)
src/replays/replayitem.py (1)
src/replays/models.py (1)
  • score (48-49)
src/api/sim_mod_updater.py (1)
src/api/ApiAccessors.py (1)
  • get_by_query_parsed (43-53)
src/playercard/playerinfodialog.py (2)
src/config/__init__.py (2)
  • Settings (43-207)
  • group (51-56)
src/playercard/leagueformatter.py (1)
  • league_formatter_factory (108-116)
src/chat/channel_tab.py (1)
src/util/theme.py (3)
  • theme (142-143)
  • sound (118-120)
  • sound (355-357)
src/fa/__init__.py (1)
src/fa/game_process.py (1)
  • kill_all_if_running (198-200)
src/api/featured_mod_api.py (5)
src/api/ApiAccessors.py (4)
  • DataApiAccessor (32-199)
  • convert_parsed (198-199)
  • requestData (187-196)
  • get_by_endpoint_converted (101-111)
src/api/ApiBase.py (1)
  • ParsedApiResponse (38-40)
src/api/coop_api.py (2)
  • convert_parsed (14-18)
  • convert_parsed (57-63)
src/api/models/FeaturedMod.py (1)
  • FeaturedMod (6-12)
src/api/models/FeaturedModFile.py (1)
  • FeaturedModFile (6-15)
src/api/stats_api.py (4)
src/api/ApiBase.py (2)
  • ParsedApiResponse (38-40)
  • get_by_query (92-99)
src/api/models/Leaderboard.py (2)
  • Leaderboard (8-37)
  • order (27-31)
src/api/models/LeaderboardRating.py (1)
  • LeaderboardRating (7-14)
src/api/ApiAccessors.py (1)
  • DataApiAccessor (32-199)
src/stats/leaderboard_widget.py (2)
src/api/ApiBase.py (1)
  • ParsedApiResponse (38-40)
src/api/ApiAccessors.py (1)
  • get_by_query_parsed (43-53)
src/api/vaults_api.py (5)
src/api/ApiBase.py (1)
  • ParsedApiResponse (38-40)
src/api/ApiAccessors.py (3)
  • requestData (187-196)
  • get_by_query_parsed (43-53)
  • post_and_parse (67-79)
src/api/models/MapVersion.py (2)
  • Map (37-48)
  • MapVersion (51-73)
src/api/models/Map.py (1)
  • Map (10-21)
src/util/__init__.py (1)
  • decapitalize (527-528)
src/vaults/reviewwidget.py (3)
src/api/ApiBase.py (1)
  • ParsedApiResponse (38-40)
src/replays/models.py (1)
  • score (48-49)
src/vaults/detailswidget.py (1)
  • on_reviews_data (379-391)
src/fa/check.py (2)
src/fa/game_updater/updater.py (2)
  • FilesObtainer (243-292)
  • Updater (157-235)
src/fa/game_updater/worker.py (1)
  • UpdaterWorker (39-289)
src/vaults/modvault/modvault.py (1)
src/vaults/vault.py (1)
  • setup (41-109)
src/api/ApiBase.py (1)
src/api/ApiAccessors.py (2)
  • handle (39-40)
  • handle (85-86)
src/fa/game_updater/worker.py (2)
src/api/models/FeaturedModFile.py (1)
  • FeaturedModFile (6-15)
src/model/transaction.py (1)
  • emit (18-19)
src/playercard/leagueformatter.py (1)
src/downloadManager/__init__.py (2)
  • CachedImageDownloader (480-512)
  • image_size (428-429)
src/notifications/ns_dialog.py (1)
src/util/theme.py (2)
  • sound (118-120)
  • sound (355-357)
src/api/player_api.py (1)
src/api/ApiAccessors.py (1)
  • get_by_query_parsed (43-53)
src/api/ApiAccessors.py (1)
src/api/ApiBase.py (10)
  • ApiResourceObject (25-29)
  • ApiResponse (32-35)
  • JsonApiBase (53-147)
  • ParsedApiResponse (38-40)
  • _do_nothing (49-50)
  • handle (64-65)
  • get_by_query (92-99)
  • get_by_endpoint (101-108)
  • post (110-122)
  • post (181-197)
src/coop/_coopwidget.py (1)
src/coop/coopmodel.py (1)
  • CoopGameFilterModel (5-14)
src/vaults/mapvault/mapdetails.py (3)
src/api/ApiBase.py (1)
  • ParsedApiResponse (38-40)
src/api/ApiAccessors.py (1)
  • get_by_query_parsed (43-53)
src/vaults/detailswidget.py (1)
  • allow_review (91-92)
src/api/coop_api.py (8)
src/api/ApiBase.py (1)
  • ParsedApiResponse (38-40)
src/api/featured_mod_api.py (2)
  • convert_parsed (17-21)
  • convert_parsed (32-36)
src/api/stats_api.py (2)
  • convert_parsed (51-60)
  • convert_parsed (189-193)
src/api/vaults_api.py (3)
  • convert_parsed (78-82)
  • convert_parsed (98-105)
  • convert_parsed (123-129)
src/api/ApiAccessors.py (1)
  • convert_parsed (198-199)
src/api/matchmaker_queue_api.py (1)
  • convert_parsed (13-26)
src/api/models/CoopScenario.py (1)
  • CoopScenario (7-13)
src/api/models/CoopResult.py (1)
  • CoopResult (7-14)
src/vaults/detailswidget.py (3)
src/api/ApiBase.py (1)
  • ParsedApiResponse (38-40)
src/vaults/mapvault/mapdetails.py (1)
  • allow_review (55-58)
src/vaults/reviewwidget.py (1)
  • on_reviews_data (330-337)
src/client/_clientwindow.py (2)
src/fa/game_process.py (1)
  • kill_all_if_running (198-200)
src/connectivity/IceAdapterProcess.py (1)
  • start (238-241)
src/fa/game_updater/updater.py (5)
src/api/featured_mod_api.py (5)
  • FeaturedModApiConnector (13-25)
  • FeaturedModFilesApiConnector (28-46)
  • request_fmod_by_name (23-25)
  • get_main_files (42-43)
  • get_mod_files (45-46)
src/api/models/FeaturedMod.py (1)
  • FeaturedMod (6-12)
src/api/models/FeaturedModFile.py (1)
  • FeaturedModFile (6-15)
src/fa/game_updater/worker.py (2)
  • UpdaterWorker (39-289)
  • do_update (258-284)
src/config/__init__.py (1)
  • group (51-56)
src/playercard/ratingtabwidget.py (2)
src/api/models/LeaderboardRating.py (1)
  • LeaderboardRating (7-14)
src/api/models/Leaderboard.py (1)
  • pretty_name (14-15)
src/replays/_replayswidget.py (5)
src/api/ApiBase.py (1)
  • ParsedApiResponse (38-40)
src/model/playerset.py (1)
  • get_by_name (19-22)
src/replays/models.py (1)
  • icon (123-124)
src/util/theme.py (1)
  • icon (369-409)
src/api/ApiAccessors.py (1)
  • get_by_query_parsed (43-53)
src/vaults/mapvault/mapvault.py (1)
src/vaults/vault.py (1)
  • setup (41-109)
🪛 Ruff (0.14.10)
src/replays/replayitem.py

339-339: Loop control variable team not used within loop body

(B007)

src/downloadManager/__init__.py

500-500: Consider moving this statement to an else block

(TRY300)

src/api/ApiBase.py

176-176: Avoid specifying long messages outside the exception class

(TRY003)


194-194: Avoid specifying long messages outside the exception class

(TRY003)


210-210: Avoid specifying long messages outside the exception class

(TRY003)


230-230: Avoid specifying long messages outside the exception class

(TRY003)

src/heavy_modules.py

11-11: Abstract raise to an inner function

(TRY301)

src/fa/game_updater/updater.py

164-164: Undefined name FilesObtainer

(F821)

🔇 Additional comments (100)
src/replays/replaydetails/replayreader.py (1)

463-464: LGTM! Proper fix for table column alignment.

The empty <td></td> ensures consistent column structure in the HTML table when a player has no country. Without this, missing countries would cause subsequent columns (player name, CPM, enhancements) to shift left, breaking the table layout.

src/replays/replayitem.py (1)

6-6: LGTM!

The itemgetter import is correctly used to support the new sorting-based team highscore logic on line 337.

src/notifications/ns_dialog.py (1)

84-85: Good defensive check with isLoaded().

The isLoaded() guard prevents attempting to play a sound that hasn't finished loading, which avoids potential runtime issues.

src/chat/channel_tab.py (1)

26-30: Function signature looks good.

The channel_ping_sound(theme) function correctly accepts a theme parameter to allow caching per theme instance.

src/fa/game_session.py (2)

44-45: LGTM!

The new ice_poll_started and ice_poll_finished signals are correctly defined and provide a clean way to coordinate UI updates during the ICE polling lifecycle.


96-99: LGTM!

The signal wiring and emission order is correct: the ice_poll_finished handler is connected before ice_poll_started is emitted and before the async request is made, ensuring no race condition where the response arrives before the handler is connected.

src/downloadManager/__init__.py (2)

428-430: LGTM!

Simple accessor method for exposing the configured image size.


484-484: LGTM!

Using os.scandir to build the index is efficient and avoids eagerly loading all images into memory.

src/playercard/ratingtabwidget.py (4)

104-106: LGTM!

The connection to valueChanged and initialization of _default_pages is correct. The signal connection ensures that changes to the spinbox are propagated to the tab.


118-120: LGTM!

Restricting updates to _default_pages only when _current_page == 0 prevents mid-loading changes from disrupting an ongoing pagination session, which is sensible behavior.


130-133: LGTM!

The logic correctly advances _default_pages by one when the tab is already loaded, enabling incremental page loading via the "load next" button.


211-217: LGTM!

The default_pages_box is correctly passed to each RatingsPlotTab during setup.

src/api/ApiBase.py (4)

49-50: LGTM!

The _do_nothing helper provides a clean no-op default for optional handlers.


53-58: LGTM!

The refactored JsonApiBase correctly delegates network operations to the shared ApiAccessManagerInstance, centralizing request handling.


150-158: LGTM!

The ApiAccessManager properly encapsulates network access management with handler tracking. The oauth class attribute binding at class level is acceptable for this singleton pattern.


266-266: LGTM!

The module-level singleton ApiAccessManagerInstance is appropriate for shared network access across the application.

res/client/client.ui (1)

322-343: LGTM!

The rename from labelAutomatchInfo to labelStatusBarInfo appropriately reflects its broader purpose of displaying various status messages (ICE polling, game launching, etc.) rather than just automatch-specific information.

src/games/_gameswidget.py (1)

252-253: LGTM!

The label reference update from labelAutomatchInfo to labelStatusBarInfo correctly aligns with the UI resource rename in res/client/client.ui.

src/fa/game_process.py (1)

198-200: LGTM!

The kill_all_if_running utility function provides a clean single entry point to terminate both game and replay processes, simplifying cleanup operations.

src/fa/__init__.py (2)

14-14: LGTM!

The import correctly exposes kill_all_if_running from the submodule.


24-24: LGTM!

Properly added to __all__ for public API exposure.

src/vaults/mapvault/mapdetails.py (1)

36-46: LGTM!

The migration to get_by_query_parsed with callback-based handling and updated ParsedApiResponse type annotation aligns well with the broader API refactoring. The implementation correctly accesses response["data"] which matches the ParsedApiResponse structure.

Also applies to: 55-58

src/vaults/modvault/modvault.py (1)

27-48: LGTM!

The refactoring to a setup() method follows the lazy initialization pattern consistently applied across vault modules. The super().setup() call correctly precedes module-specific initialization.

src/replays/replaydetails/tabs/gamestats.py (2)

12-13: LGTM!

The lazy loading of numpy and pyqtgraph via src.heavy_modules is a good performance optimization for deferred import of heavy dependencies.


153-153: The item attribute exists and is properly exposed by LabeledBarGraphItem.

The class correctly assigns self.item = pg.BarGraphItem(**opts) in __init__ (line 18 of labeledbargraphitem.py), exposing the underlying pyqtgraph item for all usage sites. All seven occurrences in gamestats.py (lines 153, 190, 227, 285, 310, 343, 366) correctly access this attribute.

src/vaults/vault.py (1)

39-42: LGTM!

The lazy initialization pattern with _loaded flag and deferred setup() is a solid performance optimization. UI setup is correctly deferred until the vault is first shown via showEventbusy_entered.

Also applies to: 258-261

src/playercard/statistics.py (3)

6-7: LGTM!

Consistent use of lazy-loaded heavy modules.


50-50: LGTM!

Consistent .item access pattern for both LabeledBarGraphItem and PieChartItem.

Also applies to: 89-89


97-102: Good defensive improvement.

The Generator[pg.PlotWidget] type simplification is valid, and the if player_event.event is not None guard at line 101 prevents AttributeError when accessing .xd on None.

src/vaults/detailswidget.py (2)

18-18: LGTM!

Type annotation updated to ParsedApiResponse aligning with the API layer refactoring.

Also applies to: 91-92


379-391: LGTM!

The on_reviews_data handler correctly processes the parsed response structure. The assumption that message["data"] is a dict with a "versions" key is valid given it's fetching a single item by ID at line 415.

src/vaults/reviewwidget.py (2)

317-318: LGTM: Rating initialization correctly handles both new and edit flows.

The _rating and _hovered_rating now initialize from self.review.score, which correctly handles:

  • New reviews: score is 0 (set in line 314)
  • Edit reviews: score is the existing review's score

352-354: Verify consistency between _rating and review.score for button state.

Line 354 enables the submit button based on self.review.score > 0, while line 373 in the event handler uses self._rating > 0. If update_appearance() is called after the user selects a rating but before submitting, it might incorrectly disable the button based on the old review.score rather than the current _rating.

Consider using self._rating consistently:

 def update_appearance(self) -> None:
-    self.star_rating.set_rating(self.review.score)
-    self.ui.submitButton.setEnabled(self.review.score > 0)
+    self.star_rating.set_rating(self._rating)
+    self.ui.submitButton.setEnabled(self._rating > 0)
     self.ui.commentTextEdit.setText(self.review.text)

However, verify whether update_appearance() is intended to reset the UI to the saved review state, in which case the current logic may be correct.

src/coop/_coopwidget.py (2)

53-54: LGTM: Clear separation of initialization states.

The two flags (ui_loaded and scenarios_loaded) provide fine-grained control over initialization stages, supporting the lazy-loading pattern effectively.


146-152: LGTM: Lazy initialization pattern correctly implemented.

The method properly guards UI initialization and scenario loading with flags, ensuring each happens exactly once. The pattern supports the PR's lazy-loading objectives.

src/api/coop_api.py (2)

14-18: LGTM: Conversion follows established pattern.

The convert_parsed implementation correctly constructs CoopScenario objects from parsed data, consistent with similar implementations in featured_mod_api.py and stats_api.py.


57-63: LGTM: Result conversion with filtering correctly implemented.

The method properly constructs CoopResult objects and applies the unique team filter before returning. The pattern aligns with the broader API refactoring.

src/api/matchmaker_queue_api.py (1)

13-26: LGTM: Queue data conversion follows the established pattern.

The convert_parsed method correctly extracts and transforms matchmaker queue data, consistent with similar implementations across the API layer.

src/fa/check.py (1)

121-123: LGTM: Updater composition refactoring improves separation of concerns.

The changes correctly separate file obtainment (FilesObtainer) from update execution (UpdaterWorker), with Updater coordinating both. This aligns with the new architecture described in the relevant code snippets.

src/vaults/mapvault/mapvault.py (1)

26-28: The code is safe. self.client is initialized in the parent class Vault.__init__() before setup() is invoked via the busy_entered() slot.

src/client/_clientwindow.py (4)

62-62: LGTM! Background import initialization looks correct.

The BackgroundImporter is started after UI setup completes, which will trigger lazy loading of heavy modules (numpy, pyqtgraph, scipy) in a background thread without blocking the UI.

Also applies to: 886-886


182-182: Good improvement: Now kills both game and replay processes.

Using fa.kill_all_if_running() instead of fa.instance.kill_if_running() ensures both the main FA process and any replay process are terminated when the user selects "Kill FA process" from the tray menu.


1810-1811: LGTM! ICE polling lifecycle handlers are well-implemented.

The new signal connections and handlers properly update the status bar to inform users about ICE adapter initialization stages ("Getting ICE Servers..." → "Launching ICE Adapter...").

Also applies to: 2173-2179


845-845: Consistent renaming of status bar label throughout.

The label has been consistently renamed from labelAutomatchInfo to labelStatusBarInfo across all usages, reflecting its broader purpose for general status updates beyond just automatch.

Also applies to: 1855-1856, 1866-1867, 1918-1918, 1993-1993, 2034-2034

src/playercard/plot.py (2)

6-8: LGTM! Lazy imports from heavy_modules.

Switching to imports from src.heavy_modules enables deferred loading of numpy and pyqtgraph, improving startup performance.


63-63: LGTM! More precise type annotations.

The updated np.typing.NDArray[np.float64] type hints are more explicit about the expected array dtype.

Also applies to: 138-139, 141-141, 144-144

res/player_card/playercard.ui (1)

354-398: LGTM! Good UX improvement for rating history pagination.

The new pagination controls (spin box for page count, "Load next page" button, and renamed "Load all" button) provide users with more control over data loading, which helps work around API restrictions.

src/qt/graphics/labeledbargraphitem.py (2)

16-21: Good refactor: Composition over inheritance.

Switching from inheritance to composition by wrapping pg.BarGraphItem is a cleaner design. The hover event is properly delegated to this wrapper.


23-34: LGTM! Correct data access pattern after composition refactor.

The hoverEvent correctly accesses bar graph properties via self.item.opts instead of self.opts, and the type annotation for the event parameter is correct.

src/heavy_modules.py (2)

8-22: LGTM! Well-implemented lazy import mechanism.

The lazy_import function correctly uses importlib.util.LazyLoader to defer module loading until first attribute access. The fallback to backup_name provides graceful degradation.

The static analysis hint about TRY301 (abstracting raise to inner function) is a minor style preference and can be safely ignored here.


30-38: LGTM! Background importer triggers lazy loading.

Using dir() to trigger module loading is a clever approach. The thread correctly connects finished to deleteLater for cleanup.

src/playercard/leagueformatter.py (3)

16-34: LGTM! Good dependency injection for image downloader.

Injecting CachedImageDownloader instead of creating it internally allows for sharing the downloader across multiple components, reducing redundant downloads and memory usage.


92-94: LGTM! Consistent image scaling using downloader's configured size.

Using self._downloader.image_size() ensures the scaled pixmap matches the downloader's configuration, maintaining consistency across the application.


108-116: LGTM! Factory properly propagates the injected downloader.

The factory function correctly passes img_dler to both GlobalLeagueFormatter and LeagueFormatter constructors.

src/stats/leaderboard_widget.py (1)

209-214: LGTM! Consistent migration to parsed API responses.

The widget correctly uses get_by_query_parsed for all API calls and handles the parsed responses appropriately.

Also applies to: 283-294, 314-314

src/qt/graphics/piechartitem.py (3)

20-32: Good refactor: Composition over inheritance for PieChartItem.

The class now wraps a pg.GraphicsObject instead of inheriting from it. The boundingRect and paint methods are correctly patched onto the wrapped object.


53-79: LGTM! Child items correctly parented to wrapped object.

All graphical elements (sectors, legend rectangles, text items) are properly parented to self.item, ensuring they move and render correctly with the wrapped GraphicsObject.


96-104: LGTM! Empty paint method is intentional.

The paint method is correctly left empty with an explanatory comment since the sectors are drawn as child items.

src/fa/game_updater/worker.py (2)

248-249: LGTM!

The simplified signature accepting a file list directly aligns with the new file-based workflow, removing the previous modname/modversion indirection.


258-274: LGTM!

The two-list file flow is well-implemented:

  • Correctly unpacks main_files and fmod_files from the tuple
  • Properly branches based on whether fmod_files is present (FAF-first update vs single mod update)
  • Progress signals appropriately reflect the update stage (1/2 vs 2/2 for dual updates, 1/1 for single)
src/playercard/playerinfodialog.py (5)

45-46: LGTM!

Settings retrieval and spin box initialization are correctly implemented with appropriate defaults.


64-66: LGTM!

The RatingTabWidgetController now receives the pagination controls, enabling the new UI pagination features.


107-118: LGTM on lazy loading logic.

The _loaded_tabs set correctly prevents redundant chart rendering when revisiting tabs. The early return pattern is clean.


122-130: LGTM!

Passing the image downloader to league_formatter_factory and storing ratings for deferred use is correct.


164-169: LGTM!

Settings persistence for defaultRatingPages on close ensures user preference is retained.

src/fa/game_updater/updater.py (4)

95-96: LGTM!

The on_started method provides appropriate UI feedback when file fetching begins.


238-241: LGTM!

FileGroup as a NamedTuple is a clean, lightweight data structure for grouping mod name and version.


243-268: LGTM on FilesObtainer initialization and signal wiring.

The logic correctly distinguishes base FAF mods from custom featured mods, setting up the appropriate file groups for sequential fetching.


289-292: LGTM!

The check_finish method correctly uses list mutation to track completion and emits the finished signal with the file tuple when all groups are processed.

src/api/vaults_api.py (6)

44-44: LGTM!

Using requestData aligns with the new converted data flow in DataApiAccessor.


78-82: LGTM!

The convert_parsed implementation correctly builds Mod objects from parsed data and includes meta information.


93-105: LGTM!

The _convert_mapversion helper cleanly handles the embedded version assignment, and convert_parsed correctly processes the list comprehension with latestVersion extraction.


123-129: LGTM!

The convert_parsed implementation correctly builds MatchmakerQueueMapPool objects. Based on learnings, the intentional stateful filter mutation in this class is by design.


138-141: LGTM!

Overriding request_data to use get_by_query_parsed with data_ready.emit is correct for the parsed data flow. This maintains backward compatibility while using the new parsing infrastructure.


175-184: LGTM!

The handler signature update to Callable[[dict[str, Any]], None] and switch to post_and_parse correctly aligns with the new parsed response flow.

src/api/featured_mod_api.py (3)

17-25: LGTM!

The convert_parsed implementation correctly constructs FeaturedMod objects from parsed API data, and request_fmod_by_name properly stores the reply reference.


29-36: LGTM!

The signals main_ready and mod_ready enable separate handling of main files vs mod files, and convert_parsed correctly builds FeaturedModFile objects.


38-46: Fix falsy version check to handle version 0 correctly.

The condition if version evaluates to False for version=0, causing it to use "latest" instead of "0". Since the parameter is typed as int | None and version fields elsewhere in the codebase can be 0, use if version is not None instead.

src/api/stats_api.py (7)

37-44: LGTM!

The handler correctly builds LeaderboardRating objects and sorts them by leaderboard order with a safe fallback for null leaderboards.


51-60: LGTM!

Using methodcaller("order") for sorting is a clean and idiomatic approach.


78-89: LGTM!

Tracking replies for abort capability is a good addition. The reply is correctly appended before the async operation.


94-100: LGTM on abort implementation.

The try/except RuntimeError correctly handles the case where the reply's underlying C++ object may have been deleted. Clearing the list after aborting is correct.


134-147: LGTM!

Migration to get_by_query_parsed and building LeagueSeasonScore objects from parsed data is correct.


161-164: LGTM!

PlayerEventApiAccessor correctly migrated to parsed data flow.


179-182: LGTM!

PlayerAchievementApiAccessor correctly migrated to parsed data flow.

src/api/ApiAccessors.py (6)

20-29: LGTM!

Both ApiAccessor and UserApiAccessor now correctly inherit from JsonApiBase, consolidating the API access foundation.


35-79: LGTM on parsed response handlers.

The _parse_and_handle wrapper and the corresponding get_by_query_parsed, get_by_endpoint_parsed, and post_and_parse methods form a clean abstraction for working with ParsedApiResponse.


81-111: LGTM on converted response handlers.

The _convert_and_handle wrapper combines parsing and conversion in a single step, and the get_by_query_converted and get_by_endpoint_converted methods complete the two-tier API access pattern.


113-118: LGTM!

The parse_message method now correctly returns ParsedApiResponse type, properly initializing the result structure.


182-196: LGTM!

Returning {"page": {}} as the default for missing meta is a safe pattern. The requestData method now correctly uses get_by_query_converted.


198-199: LGTM!

The abstract convert_parsed method with NotImplementedError enforces that concrete subclasses must implement their own conversion logic.

src/replays/_replayswidget.py (8)

24-24: LGTM: Import aligns with API refactoring.

The ParsedApiResponse import supports the new parsed response pattern used in process_replays_data and get_by_query_parsed.


143-144: LGTM: Type annotations improve clarity.

The explicit type hints for _is_online are correct and make the method signature more readable.


381-394: LGTM: Well-implemented lazy loading guard.

The _loaded flag pattern prevents redundant appearance setup calls and improves performance. The implementation is clean and correct.


398-432: LGTM: Consistent use of class-level icon constants.

The migration from direct icon loading to class-level constants improves performance by avoiding repeated icon loads.


949-953: LGTM: API migration is correct.

The migration from requestData to get_by_query_parsed follows the new parsed response pattern. The callback signature matches the expected ParsedApiResponse type.


1203-1203: LGTM: Signature matches new API pattern.

The updated signature correctly uses ParsedApiResponse, which is consistent with the get_by_query_parsed call at line 949.


1267-1292: LGTM: Effective performance optimization.

The tab visibility checks prevent unnecessary updates to the local replay tree when it's not visible, improving performance. The on_tab_changed handler correctly triggers updates only when the local tab becomes active.


338-340: The refactoring of class-level icon constants is safe and correct. util.THEME is fully initialized when the util module is imported (which occurs at the top of _replayswidget.py), well before the class definition. The icon() method does not depend on loadTheme() being called—it works with the default theme immediately upon THEME initialization.

Likely an incorrect or invalid review comment.

setup.py (1)

83-86: No issues found — the module path and configuration are correct.

The path scipy_ndimage.ndimage._filters is intentional, not a typo. This project uses a custom minimized scipy package (minimize_scipy.py) that extracts only the ndimage module to reduce binary size. The lazy_import in heavy_modules.py tries to load this custom package first and falls back to standard scipy if unavailable. Excluding scipy while including scipy_ndimage is the intended design to bundle only necessary components.

Copy link

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 2

♻️ Duplicate comments (2)
src/api/sim_mod_updater.py (2)

20-22: Add validation for empty data array.

The code assumes message["data"] is non-empty when accessing [0]. If the API returns an empty data array, this will raise an IndexError. This concern was raised in a previous review and remains unaddressed.


26-27: Error handler signature mismatch will cause runtime crash.

self.ready.emit is passed as the error handler, but ready = pyqtSignal() takes no arguments. According to the get_by_query_parsed signature, the error handler is Callable[[QNetworkReply], None] and will be called with a QNetworkReply argument. This will raise a TypeError when a network error occurs.

🔎 Proposed fix
+    def _on_error(self, reply: QNetworkReply) -> None:
+        self.mod_url = ""
+        self.ready.emit()
+
     def request_and_get_sim_mod_url_by_id(self, uid: str) -> str:
         query_dict = {"filter": f"uid=={uid}"}
-        self.get_by_query_parsed(query_dict, self.get_url_from_message, self.ready.emit)
+        self.get_by_query_parsed(query_dict, self.get_url_from_message, self._on_error)
         self.wait_loop.exec()
         return self.mod_url

You'll also need to add the import for QNetworkReply:

from PyQt6.QtNetwork import QNetworkReply
🧹 Nitpick comments (1)
src/chat/channel_tab.py (1)

109-110: Consider storing the sound effect in a local variable.

The previous critical issue (missing theme parameter) has been resolved. However, calling channel_ping_sound(self._theme) twice is slightly inefficient, even though the result is cached.

🔎 Optional refactor to eliminate duplicate function call
-        if channel_ping_sound(self._theme).isLoaded():
-            channel_ping_sound(self._theme).play()
+        sound = channel_ping_sound(self._theme)
+        if sound.isLoaded():
+            sound.play()
📜 Review details

Configuration used: defaults

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between 04ec72f and 6d3c362.

📒 Files selected for processing (6)
  • requirements.txt
  • src/api/sim_mod_updater.py
  • src/chat/channel_tab.py
  • src/downloadManager/__init__.py
  • src/playercard/playerinfodialog.py
  • src/playercard/plot.py
🚧 Files skipped from review as they are similar to previous changes (2)
  • requirements.txt
  • src/playercard/plot.py
🧰 Additional context used
🧬 Code graph analysis (3)
src/api/sim_mod_updater.py (1)
src/api/ApiAccessors.py (2)
  • DataApiAccessor (32-199)
  • get_by_query_parsed (43-53)
src/chat/channel_tab.py (1)
src/util/theme.py (3)
  • theme (142-143)
  • sound (118-120)
  • sound (355-357)
src/playercard/playerinfodialog.py (3)
src/downloadManager/__init__.py (1)
  • CachedImageDownloader (480-513)
src/config/__init__.py (2)
  • Settings (43-207)
  • group (51-56)
src/playercard/leagueformatter.py (1)
  • league_formatter_factory (108-116)
🪛 Ruff (0.14.10)
src/downloadManager/__init__.py

501-501: Consider moving this statement to an else block

(TRY300)

🔇 Additional comments (10)
src/api/sim_mod_updater.py (2)

3-4: LGTM!

The PyQt6 imports are correct for the signal and event loop functionality introduced.


12-18: LGTM!

The signal-to-QEventLoop pattern is a standard Qt approach for synchronous waiting. The connection ensures the loop exits when ready is emitted.

src/playercard/playerinfodialog.py (4)

45-46: LGTM! Settings initialization and persistence look correct.

The default rating pages value is properly read from settings with a sensible default of 10, and line 169 correctly persists it on close. The flow is consistent.


86-93: LGTM! Attribute initialization is well-structured.

The leagues image downloader and state tracking attributes are properly initialized. The previous typo (leauges_img_dler) has been corrected to leagues_img_dler.


120-130: LGTM! Rating processing correctly wires up the image downloader.

The league formatter factory receives the new leagues_img_dler dependency as expected, and ratings are stored for chart rendering in the stats tab.


163-169: LGTM! State storage and settings persistence are correctly implemented.

Player events are stored for later chart rendering, and the rating pages preference is properly persisted on dialog close using the settings group context.

src/downloadManager/__init__.py (4)

428-430: LGTM! Simple accessor for image size.

Clean addition providing read access to the configured size.


496-503: LGTM! Previous scaling bug is fixed.

The method now correctly creates, caches, and returns the scaled pixmap. The earlier issue where an unscaled pixmap was returned while caching a scaled one has been resolved.

The static analysis hint (TRY300) about moving the return to an else block is a stylistic preference; the current structure is clear and correct.


510-513: LGTM! In-memory caching on save is correctly implemented.

Newly downloaded images are properly added to the in-memory cache with appropriate scaling, ensuring consistency with the lazy-load path.


484-484: The cache directories (AVATARS_CACHE_DIR and DIVISIONS_CACHE_DIR) are created at module import time in src/util/__init__.py (lines 182-191) before any CachedImageDownloader instances are instantiated. The os.scandir(self.save_dir) call will not raise FileNotFoundError.

Likely an incorrect or invalid review comment.

Copy link

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 1

📜 Review details

Configuration used: defaults

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between 6d3c362 and f994055.

📒 Files selected for processing (1)
  • src/replays/_replayswidget.py
🧰 Additional context used
🧬 Code graph analysis (1)
src/replays/_replayswidget.py (5)
src/api/ApiBase.py (1)
  • ParsedApiResponse (38-40)
src/model/playerset.py (1)
  • get_by_name (19-22)
src/replays/models.py (1)
  • icon (123-124)
src/util/theme.py (1)
  • icon (369-409)
src/api/ApiAccessors.py (1)
  • get_by_query_parsed (43-53)
🔇 Additional comments (8)
src/replays/_replayswidget.py (8)

24-24: LGTM!

The import of ParsedApiResponse improves type safety for API response handling.


143-144: LGTM!

The type annotation correctly reflects that this method takes a string and returns a boolean.


348-348: LGTM! Idempotent lazy loading correctly implemented.

The _loaded flag properly guards against redundant _setup_appearance calls, and the early return at lines 380-381 ensures the method is idempotent. This addresses the previous review comment about dead code, as the problematic if self.isExpanded() check has been removed.

Also applies to: 379-392


396-396: Icon constant usage is consistent.

The usage of self.NO_METADATA, self.BROKEN, and self.UNKNOWN is appropriate and consistent across all _setup_* methods. However, this depends on the class-level icon initialization issue (lines 338-340) being addressed.

Also applies to: 401-401, 410-410, 430-430


604-604: LGTM! Lazy loading on expansion correctly implemented.

The on_item_expanded handler appropriately triggers _setup_appearance for child items when a bucket is expanded, enabling lazy loading of replay appearance data. The cast is safe since bucket items only contain LocalReplayItem children, and the idempotent _setup_appearance prevents redundant work.

Also applies to: 606-608


947-951: LGTM! API call correctly updated to use parsed response.

The transition to get_by_query_parsed with typed callbacks aligns with the new API architecture and improves type safety.


1201-1201: LGTM! Type annotation correctly updated.

The signature change to accept ParsedApiResponse improves type safety and aligns with the refactored API layer.


1265-1265: LGTM! Performance optimizations appropriately applied.

The visibility checks and tab-change handler prevent unnecessary UI updates when the local replays tab is not active, improving performance without affecting functionality.

Also applies to: 1277-1278, 1283-1284, 1288-1290

Copy link

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 0

🧹 Nitpick comments (1)
src/replays/_replayswidget.py (1)

616-620: Consider defensive type checking.

The cast to LocalReplayItem is safe based on the current code structure (buckets only contain LocalReplayItem children per line 702), but adding a runtime type guard would make this more defensive against future changes.

🔎 Optional defensive pattern
     def on_item_expanded(self, item: LocalReplayBucketItem) -> None:
         for index in range(item.childCount()):
-            cast(LocalReplayItem, item.child(index))._setup_appearance()
+            child = item.child(index)
+            if isinstance(child, LocalReplayItem):
+                child._setup_appearance()
📜 Review details

Configuration used: defaults

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between 8cb9e49 and de5a847.

📒 Files selected for processing (1)
  • src/replays/_replayswidget.py
🧰 Additional context used
🧬 Code graph analysis (1)
src/replays/_replayswidget.py (6)
src/api/ApiBase.py (1)
  • ParsedApiResponse (38-40)
src/replays/models.py (1)
  • icon (123-124)
src/util/theme.py (1)
  • icon (369-409)
src/fa/maps.py (1)
  • preview (324-367)
src/model/playerset.py (1)
  • get_by_name (19-22)
src/api/ApiAccessors.py (1)
  • get_by_query_parsed (43-53)
🔇 Additional comments (10)
src/replays/_replayswidget.py (10)

338-350: LGTM! Icon lazy loading properly implemented.

The module-level cached functions elegantly address the previous concern about eager class-level icon initialization. The @cache decorator ensures icons are loaded only on first access and cached thereafter.


144-145: LGTM! Online status check improved.

The explicit signature and use of get_by_name with a None check is clearer and more type-safe than the previous membership test.


360-360: LGTM! Lazy loading flag initialized.

The _loaded flag properly supports the deferred appearance setup pattern.


391-404: LGTM! Idempotent setup pattern implemented correctly.

The early return when _loaded is True ensures _setup_appearance can be safely called multiple times without redundant work. This addresses the previous review concern about dead code in __init__ and enables efficient lazy loading.


959-963: LGTM! API call aligned with refactored interface.

The switch to get_by_query_parsed correctly adapts to the new API layer that returns parsed responses. This aligns with the broader API refactoring mentioned in the PR objectives.


1213-1213: LGTM! Handler signature updated correctly.

The parameter type change to ParsedApiResponse is correct and type-safe. The handler correctly accesses the data key (line 1217) which is guaranteed to exist in the TypedDict.


1277-1277: LGTM! Tab change wiring added.

Connecting to currentChanged enables efficient updates only when the local replays tab becomes active.


1300-1302: LGTM! Tab-specific update logic.

The index check ensures the local replay tree is only updated when its tab is actually selected, avoiding unnecessary work.


1289-1290: LGTM! Visibility guard in focusEvent.

The visibility check prevents unnecessary tree updates when the local replays tab isn't visible, improving performance.


1295-1296: LGTM! Visibility guard in showEvent.

Consistent with the focusEvent logic, this ensures updates only occur when the local replays tab is actually visible.

@Gatsik Gatsik force-pushed the perf branch 2 times, most recently from b78173f to a95dc95 Compare December 28, 2025 01:57
* Map/Mod vaults
* QSoundEffects
* local replay items
* CoopWidget
to improve application start time
they may be not opened at all, and it reduces
default RAM consumption
so they all also share proxy and cache configurations

Qt docs say that one QNetworkAccessManager instance should
be enough for the whole Qt application, so maybe one day
we'll arrive there

Game updater now fetches lists of files to update in the
main thread with FileObtainer, because QNetworkAccessManager
is based on QObject and can only be used from the thread it
belongs to
(e.g. only parsing result into dictionary, or parsing and
converting further to pydantic API models)

this way it is more clear what kind of result will be returned
from the request
don't assume any base value as a minimum score
because a team can win with total of 0 (and less??)
score, e.g:
--------------------------
|team 1 | team 2 (winner)|
|-------+-----------------
|-10    |  -10           |
|-10    |  10            |
--------------------------
display information about getting ICE servers
from the API -- it can be noticeably long
(a couple of seconds), so the client doesn't
look like it's doing nothing for no reason
they are almost all trivial and not reused
* QSpinBox to change default amount of pages to load
* QPushButton to load next page
wether this has any effect was not investigated
but Qt docs recommend this approach
Copy link

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 5

Caution

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (1)
src/api/player_api.py (1)

42-50: Add defensive checks for missing data.

The implementation has two potential issues:

  1. Line 43: The unpacking player_dict, = message["data"] will raise ValueError if the API returns zero players (player not found) or multiple players.

  2. Line 45: Accessing player_dict["clanMembership"]["clan"] could raise KeyError if the "clanMembership" key is missing from player_dict.

Consider adding validation to handle these edge cases gracefully.

🔎 Proposed fix
 def handle_player(self, message: PreProcessedApiResponse) -> None:
-    player_dict, = message["data"]
+    data = message["data"]
+    if not data:
+        logger.warning("Player not found in API response")
+        return
+    if len(data) > 1:
+        logger.warning("Multiple players returned, expected one")
+    player_dict = data[0]
+    
     player = Player(**player_dict)
-    if "clan" in player_dict["clanMembership"]:
+    if "clanMembership" in player_dict and "clan" in player_dict["clanMembership"]:
         clan = Clan(**player_dict["clanMembership"]["clan"])
         clan_membership = ClanMembership(**player_dict["clanMembership"])
         clan_membership.custom_clan = clan
         player.custom_clan_membership = clan_membership
     self.player_ready.emit(player)
♻️ Duplicate comments (2)
src/fa/game_updater/updater.py (1)

164-164: Forward reference is acceptable for Python 3.14+.

The static analysis warning about FilesObtainer being undefined is a false positive given the project's Python 3.14+ target, where PEP 649 provides deferred annotation evaluation by default. Based on learnings, this pattern has been discussed and confirmed intentional.

Based on learnings, the forward reference pattern is intentional for Python 3.14+.

src/api/ApiAccessors.py (1)

198-199: Template method requires subclass implementation.

This method raises NotImplementedError and must be implemented by all subclasses that use the converted response methods. See the critical issue comment on lines 81-88 for details and verification script.

🧹 Nitpick comments (9)
src/chat/channel_tab.py (1)

109-110: Consider storing the cached sound in a local variable.

While the code works correctly due to caching, calling channel_ping_sound(self._theme) twice in succession is slightly redundant. Storing the result improves readability and makes the intent clearer.

🔎 Proposed refactor
-        if channel_ping_sound(self._theme).isLoaded():
-            channel_ping_sound(self._theme).play()
+        sound = channel_ping_sound(self._theme)
+        if sound.isLoaded():
+            sound.play()
src/replays/replayitem.py (1)

337-342: Approve the sort-based tie handling; consider minor refinements.

The sort-based approach with tie detection is correct and more explicit than iterative max-detection. The logic properly handles multi-team scenarios and sets teamWin to None when ties are detected.

Two minor refinements to consider:

  1. Unused loop variable: As flagged by Ruff, the team variable in line 339 is unused. Consider using _ to indicate this is intentional:

    for _, score in sorted_scores[1:]:
  2. Variable naming: mvt stores the top score value but the name suggests a team reference. Consider renaming for clarity:

    self.teamWin, top_score = sorted_scores[0]
    for _, score in sorted_scores[1:]:
        if score >= top_score:
🔎 Combined refactor
 sorted_scores = sorted(scores.items(), key=itemgetter(1), reverse=True)
-self.teamWin, mvt = sorted_scores[0]
-for team, score in sorted_scores[1:]:
-    if score >= mvt:
+self.teamWin, top_score = sorted_scores[0]
+for _, score in sorted_scores[1:]:
+    if score >= top_score:
         self.teamWin = None
         break

Based on static analysis hints.

res/chat/channel.css (1)

108-113: Redundant font-style: normal declaration.

Line 112 sets font-style: normal alongside font-weight: bold. The font-style property controls italic/oblique/normal, not boldness, so setting it to normal when font-weight: bold is already specified is redundant.

🔎 Suggested simplification
 span.my_mention {
 	font-family: 'Segoe UI';
 	font-size: 9pt;
 	font-weight: bold;
-	font-style: normal;
 	color: "{{ colors['you'] }}";
 }
src/api/ApiBase.py (1)

92-147: Note the different response handler signatures.

The method signatures show intentional differences in response handling:

  • get_by_query and get_by_endpoint: handlers receive decoded ApiResponse
  • post: handler receives decoded ApiResponse
  • delete and patch: handlers receive raw QNetworkReply

This asymmetry suggests different use cases, but consider whether a consistent interface would improve maintainability. If the differences are intentional, consider documenting why delete and patch handlers work with raw replies instead of decoded responses.

src/vaults/reviewwidget.py (1)

345-354: Minor duplication in submit button state logic.

The submit button's enabled state is set both in init_ui (line 349) and update_appearance (line 354). Since init_ui calls update_appearance at line 350, line 349 is redundant.

🔎 Optional simplification
     def init_ui(self) -> None:
         self.ui.setupUi(self)
         self.ui.ratingLayout.addWidget(self.star_rating)
         self.ui.submitButton.clicked.connect(self.submit_review)
-        self.ui.submitButton.setEnabled(self.review.score > 0)
         self.update_appearance()
res/player_card/playercard.ui (1)

356-373: Consider improving the label text flow.

The label is split across multiple elements: "Due to API restrictions, max" + [spinbox] + "pages are loaded at first. Here are the buttons to try more:". The phrase "Here are the buttons to try more" is slightly awkward.

Suggestion: "pages are loaded initially. Use these buttons to load more:" or similar phrasing would read more naturally.

src/replays/_replayswidget.py (1)

691-695: Remove debug print statement.

The print statement on line 695 appears to be timing instrumentation left from development. Consider using the logger instead or removing it entirely.

🔎 Proposed fix
         start = time.perf_counter()
         for bucket, items in buckets.items():
             bucket_item = LocalReplayBucketItem(bucket, items)
             self.myTree.addTopLevelItem(bucket_item)
-        print(time.perf_counter() - start)
+        logger.debug("Bucket rendering took %.3f seconds", time.perf_counter() - start)

Or remove entirely if not needed:

-        start = time.perf_counter()
         for bucket, items in buckets.items():
             bucket_item = LocalReplayBucketItem(bucket, items)
             self.myTree.addTopLevelItem(bucket_item)
-        print(time.perf_counter() - start)
src/api/ApiAccessors.py (2)

142-159: Consider simplifying the redundant isinstance check.

Line 153 checks isinstance(message["data"], (dict)) which Pyright flags as unnecessary since we've already checked for list on line 148. While this is defensive programming, you could simplify it to just an else: clause if you trust the API spec.

🔎 Optional simplification
     if "data" in message:
         if isinstance(message["data"], (list)):
             result: list[dict[str, Any]] = []
             for data in message["data"]:
                 result.append(self.parseSingleData(data, included))
             return result
-        elif isinstance(message["data"], (dict)):  # pyright: ignore[reportUnnecessaryIsInstance]  # noqa: E501
+        else:
             return self.parseSingleData(message["data"], included)

However, keeping the explicit check provides runtime safety if the API returns unexpected data types.


187-196: requestData now depends on convert_parsed implementation.

The method has been updated to use get_by_query_converted, which requires subclasses to implement convert_parsed (see comment on lines 81-88). Ensure all subclasses that use requestData properly implement this method.

The conditional on lines 193-196 can be simplified:

🔎 Optional simplification
 def requestData(
     self,
     query_dict: QueryOptions | None = None,
     error_handler: Callable[[QNetworkReply], None] | None = None,
 ) -> QNetworkReply:
     query_dict = query_dict or {}
-    if error_handler is None:
-        return self.get_by_query_converted(query_dict, self.data_ready.emit)
-    else:
-        return self.get_by_query_converted(query_dict, self.data_ready.emit, error_handler)
+    error_handler = error_handler or _do_nothing
+    return self.get_by_query_converted(query_dict, self.data_ready.emit, error_handler)
📜 Review details

Configuration used: defaults

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between de5a847 and a95dc95.

📒 Files selected for processing (66)
  • requirements.txt
  • res/chat/channel.css
  • res/client/client.css
  • res/client/client.ui
  • res/player_card/playercard.ui
  • setup.py
  • src/api/ApiAccessors.py
  • src/api/ApiBase.py
  • src/api/coop_api.py
  • src/api/featured_mod_api.py
  • src/api/matchmaker_queue_api.py
  • src/api/parsers/CoopResultParser.py
  • src/api/parsers/CoopScenarioParser.py
  • src/api/parsers/FeaturedModFileParser.py
  • src/api/parsers/FeaturedModParser.py
  • src/api/parsers/GeneratedMapParamsParser.py
  • src/api/parsers/LeaderboardParser.py
  • src/api/parsers/LeaderboardRatingJournalParser.py
  • src/api/parsers/LeaderboardRatingParser.py
  • src/api/parsers/MapParser.py
  • src/api/parsers/MapVersionParser.py
  • src/api/parsers/ModParser.py
  • src/api/parsers/ModVersionParser.py
  • src/api/parsers/PlayerParser.py
  • src/api/parsers/ReviewsSummaryParser.py
  • src/api/parsers/__init__.py
  • src/api/player_api.py
  • src/api/sim_mod_updater.py
  • src/api/stats_api.py
  • src/api/vaults_api.py
  • src/chat/channel_tab.py
  • src/chat/channel_view.py
  • src/client/_clientwindow.py
  • src/coop/_coopwidget.py
  • src/downloadManager/__init__.py
  • src/fa/__init__.py
  • src/fa/check.py
  • src/fa/game_process.py
  • src/fa/game_session.py
  • src/fa/game_updater/updater.py
  • src/fa/game_updater/worker.py
  • src/games/_gameswidget.py
  • src/heavy_modules.py
  • src/model/chat/chatline.py
  • src/notifications/ns_dialog.py
  • src/playercard/leagueformatter.py
  • src/playercard/playerinfodialog.py
  • src/playercard/plot.py
  • src/playercard/ratingtabwidget.py
  • src/playercard/statistics.py
  • src/qt/graphics/labeledbargraphitem.py
  • src/qt/graphics/piechartitem.py
  • src/replays/_replayswidget.py
  • src/replays/replaydetails/replaycard.py
  • src/replays/replaydetails/replayreader.py
  • src/replays/replaydetails/tabs/gamestats.py
  • src/replays/replaydetails/tabs/heatmap.py
  • src/replays/replayitem.py
  • src/stats/_statswidget.py
  • src/stats/leaderboard_widget.py
  • src/vaults/detailswidget.py
  • src/vaults/mapvault/mapdetails.py
  • src/vaults/mapvault/mapvault.py
  • src/vaults/modvault/modvault.py
  • src/vaults/reviewwidget.py
  • src/vaults/vault.py
💤 Files with no reviewable changes (16)
  • src/api/parsers/LeaderboardRatingParser.py
  • src/api/parsers/ModParser.py
  • src/stats/_statswidget.py
  • src/api/parsers/ReviewsSummaryParser.py
  • src/api/parsers/FeaturedModFileParser.py
  • res/client/client.css
  • src/api/parsers/CoopResultParser.py
  • src/api/parsers/CoopScenarioParser.py
  • src/api/parsers/PlayerParser.py
  • src/api/parsers/ModVersionParser.py
  • src/api/parsers/LeaderboardParser.py
  • src/api/parsers/FeaturedModParser.py
  • src/api/parsers/LeaderboardRatingJournalParser.py
  • src/api/parsers/MapVersionParser.py
  • src/api/parsers/GeneratedMapParamsParser.py
  • src/api/parsers/MapParser.py
🚧 Files skipped from review as they are similar to previous changes (8)
  • src/replays/replaydetails/replayreader.py
  • res/client/client.ui
  • src/replays/replaydetails/replaycard.py
  • src/vaults/mapvault/mapvault.py
  • src/fa/game_process.py
  • src/api/coop_api.py
  • src/notifications/ns_dialog.py
  • src/playercard/plot.py
🧰 Additional context used
🧠 Learnings (6)
📚 Learning: 2025-12-27T22:19:06.765Z
Learnt from: Gatsik
Repo: FAForever/client PR: 1148
File: requirements.txt:0-0
Timestamp: 2025-12-27T22:19:06.765Z
Learning: Do not propose upgrading the irc package in requirements.txt from 20.4.0 to 20.5.0 due to a known importlib.resources.files() bug in 20.5.0. In reviews of FAForever/client, maintain the pin at 20.4.0 and flag any upgrade discussion for this package as blocked unless there is a documented, validated workaround.

Applied to files:

  • requirements.txt
📚 Learning: 2025-12-27T22:31:12.379Z
Learnt from: Gatsik
Repo: FAForever/client PR: 1148
File: src/replays/replaydetails/tabs/heatmap.py:39-43
Timestamp: 2025-12-27T22:31:12.379Z
Learning: In src/replays/replaydetails/tabs/heatmap.py, the gaussian_filter() function checks for "scipy_ndimage.ndimage._filters" in sys.modules to support a build optimization. The project uses minimize_scipy.py to create a minimal scipy distribution containing only the gaussian_filter function and dependencies, avoiding shipping the entire scipy package. This sys.modules check is intentional and should not be replaced with hasattr() or other approaches.

Applied to files:

  • src/heavy_modules.py
📚 Learning: 2025-12-27T22:23:44.249Z
Learnt from: Gatsik
Repo: FAForever/client PR: 1148
File: src/fa/game_updater/updater.py:274-279
Timestamp: 2025-12-27T22:23:44.249Z
Learning: In src/fa/game_updater/updater.py, the single-element unpacking `mod, = data["values"]` in the `process_fmod` method is intentional and serves as validation that exactly one featured mod is returned. Multiple or zero values should raise a ValueError to signal an API error.

Applied to files:

  • src/fa/check.py
  • src/api/sim_mod_updater.py
  • src/fa/game_updater/updater.py
  • src/fa/game_updater/worker.py
📚 Learning: 2025-12-27T22:30:11.905Z
Learnt from: Gatsik
Repo: FAForever/client PR: 1148
File: src/replays/replaydetails/replaycard.py:89-90
Timestamp: 2025-12-27T22:30:11.905Z
Learning: In the FAForever/client repository, ReplayDetailsCard (src/replays/replaydetails/replaycard.py) is used as a modal dialog with only one instance at a time, so class-level QNetworkAccessManager doesn't cause cross-instance issues.

Applied to files:

  • src/fa/game_updater/worker.py
📚 Learning: 2025-12-27T22:31:12.379Z
Learnt from: Gatsik
Repo: FAForever/client PR: 1148
File: src/replays/replaydetails/tabs/heatmap.py:39-43
Timestamp: 2025-12-27T22:31:12.379Z
Learning: In src/replays/replaydetails/tabs/heatmap.py, keep the explicit sys.modules check for scipy_ndimage.ndimage._filters around gaussian_filter(), as used by minimize_scipy.py to build a minimal scipy distribution. Do not replace with hasattr() or similar checks; this is intentional to detect the presence of the reduced module for the build optimization.

Applied to files:

  • src/replays/replaydetails/tabs/heatmap.py
📚 Learning: 2025-11-23T00:07:39.893Z
Learnt from: Gatsik
Repo: FAForever/client PR: 1145
File: src/api/vaults_api.py:114-116
Timestamp: 2025-11-23T00:07:39.893Z
Learning: In src/api/vaults_api.py, the MapPoolApiConnector class uses intentional stateful filter mutation. The request_pool_for_queue method sets self._filters and is the intended public API; request_data() should not be called directly on instances of this class.

Applied to files:

  • src/api/vaults_api.py
🧬 Code graph analysis (25)
src/vaults/reviewwidget.py (2)
src/api/ApiBase.py (1)
  • ParsedApiResponse (38-40)
src/vaults/detailswidget.py (1)
  • on_reviews_data (379-391)
src/chat/channel_view.py (1)
src/model/chat/chatline.py (1)
  • ChatLineMetadata (34-37)
src/fa/check.py (1)
src/fa/game_updater/updater.py (2)
  • FilesObtainer (243-292)
  • Updater (157-235)
src/fa/__init__.py (1)
src/fa/game_process.py (1)
  • kill_all_if_running (198-200)
src/client/_clientwindow.py (1)
src/fa/game_process.py (1)
  • kill_all_if_running (198-200)
src/replays/_replayswidget.py (3)
src/api/ApiBase.py (1)
  • ParsedApiResponse (38-40)
src/model/playerset.py (1)
  • get_by_name (19-22)
src/api/ApiAccessors.py (1)
  • get_by_query_parsed (43-53)
src/api/sim_mod_updater.py (1)
src/api/ApiAccessors.py (2)
  • DataApiAccessor (32-199)
  • get_by_query_parsed (43-53)
src/vaults/modvault/modvault.py (2)
src/vaults/mapvault/mapvault.py (1)
  • setup (26-40)
src/vaults/vault.py (1)
  • setup (41-109)
src/fa/game_session.py (2)
src/chat/socketadapter.py (2)
  • connect (85-93)
  • connect (124-127)
src/model/transaction.py (1)
  • emit (18-19)
src/vaults/detailswidget.py (3)
src/api/ApiBase.py (1)
  • ParsedApiResponse (38-40)
src/vaults/mapvault/mapdetails.py (1)
  • allow_review (55-58)
src/vaults/reviewwidget.py (1)
  • on_reviews_data (330-337)
src/api/matchmaker_queue_api.py (6)
src/api/ApiBase.py (1)
  • ParsedApiResponse (38-40)
src/api/coop_api.py (2)
  • convert_parsed (14-18)
  • convert_parsed (57-63)
src/api/featured_mod_api.py (2)
  • convert_parsed (17-21)
  • convert_parsed (32-36)
src/api/stats_api.py (2)
  • convert_parsed (51-60)
  • convert_parsed (189-193)
src/api/vaults_api.py (3)
  • convert_parsed (78-82)
  • convert_parsed (98-105)
  • convert_parsed (123-129)
src/api/ApiAccessors.py (1)
  • convert_parsed (198-199)
src/playercard/ratingtabwidget.py (5)
src/api/models/LeaderboardRating.py (1)
  • LeaderboardRating (7-14)
src/api/stats_api.py (1)
  • LeaderboardRatingJournalApiConnector (63-100)
src/replays/models.py (1)
  • rating (87-90)
src/playercard/plot.py (1)
  • PlotController (163-207)
src/api/models/Leaderboard.py (1)
  • pretty_name (14-15)
src/playercard/leagueformatter.py (1)
src/downloadManager/__init__.py (2)
  • CachedImageDownloader (480-513)
  • image_size (428-429)
src/chat/channel_tab.py (1)
src/util/theme.py (3)
  • theme (142-143)
  • sound (118-120)
  • sound (355-357)
src/fa/game_updater/worker.py (3)
src/api/models/FeaturedModFile.py (1)
  • FeaturedModFile (6-15)
src/model/transaction.py (1)
  • emit (18-19)
src/fa/game_updater/misc.py (1)
  • ProgressInfo (32-35)
src/playercard/statistics.py (1)
src/api/models/PlayerEvent.py (1)
  • PlayerEvent (8-12)
src/api/player_api.py (1)
src/api/ApiAccessors.py (1)
  • get_by_query_parsed (43-53)
src/model/chat/chatline.py (2)
src/util/magic_dict.py (1)
  • MagicDict (1-20)
src/replays/models.py (1)
  • login (51-52)
src/playercard/playerinfodialog.py (4)
src/downloadManager/__init__.py (2)
  • CachedImageDownloader (480-513)
  • run (108-118)
src/contextmenu/playercontextmenu.py (1)
  • PlayerContextMenu (50-284)
src/config/__init__.py (2)
  • Settings (43-207)
  • group (51-56)
src/playercard/leagueformatter.py (1)
  • league_formatter_factory (108-116)
src/replays/replaydetails/tabs/heatmap.py (2)
src/qt/utils.py (1)
  • block_signals (41-46)
src/replays/replaydetails/helpers.py (1)
  • seconds_to_human (1-9)
src/stats/leaderboard_widget.py (2)
src/api/ApiBase.py (1)
  • ParsedApiResponse (38-40)
src/api/ApiAccessors.py (1)
  • get_by_query_parsed (43-53)
src/api/ApiBase.py (2)
src/api/ApiAccessors.py (2)
  • handle (39-40)
  • handle (85-86)
src/oauth/oauth_flow.py (1)
  • OAuth2Flow (25-99)
src/api/vaults_api.py (2)
src/api/ApiBase.py (1)
  • ParsedApiResponse (38-40)
src/api/ApiAccessors.py (2)
  • requestData (187-196)
  • post_and_parse (67-79)
src/coop/_coopwidget.py (1)
src/coop/coopmodel.py (1)
  • CoopGameFilterModel (5-14)
src/vaults/mapvault/mapdetails.py (3)
src/api/ApiBase.py (1)
  • ParsedApiResponse (38-40)
src/api/ApiAccessors.py (1)
  • get_by_query_parsed (43-53)
src/vaults/detailswidget.py (1)
  • allow_review (91-92)
🪛 Ruff (0.14.10)
src/replays/replayitem.py

339-339: Loop control variable team not used within loop body

(B007)

src/heavy_modules.py

11-11: Abstract raise to an inner function

(TRY301)

src/downloadManager/__init__.py

501-501: Consider moving this statement to an else block

(TRY300)

src/fa/game_updater/updater.py

164-164: Undefined name FilesObtainer

(F821)

src/api/ApiBase.py

176-176: Avoid specifying long messages outside the exception class

(TRY003)


194-194: Avoid specifying long messages outside the exception class

(TRY003)


210-210: Avoid specifying long messages outside the exception class

(TRY003)


230-230: Avoid specifying long messages outside the exception class

(TRY003)

🔇 Additional comments (83)
src/games/_gameswidget.py (1)

252-253: Label reference update looks good.

The change from labelAutomatchInfo to labelStatusBarInfo is straightforward and preserves the intended behavior. All references to the old label name have been updated consistently across the codebase—no remaining instances found.

src/chat/channel_tab.py (1)

26-30: LGTM! Well-structured cached sound effect.

The implementation correctly uses @cache to reuse QSoundEffect instances per theme. The hashability concern flagged in previous reviews has been confirmed as resolved—ThemeSet inherits identity-based hashing from QObject, making it compatible with the cache decorator. This aligns with the PR's pattern of centralized, cached sound effects.

src/replays/replayitem.py (1)

6-6: LGTM!

The itemgetter import is appropriately used in the new sort-based team winner determination logic.

src/coop/_coopwidget.py (3)

53-54: LGTM: Clear state flags for lazy initialization.

The new flags ui_loaded and scenarios_loaded provide explicit tracking of initialization stages, making the lazy-loading logic easy to understand.


56-104: LGTM: Well-structured lazy initialization.

Extracting UI setup into a dedicated setup() method enables lazy loading and improves startup performance by deferring heavy initialization (widget setup, signal connections, network manager creation) until the widget is first displayed.


223-223: LGTM: Explicit state tracking.

Setting scenarios_loaded after processing coop scenarios provides clear state management and prevents redundant scenario requests in subsequent busy_entered() calls (assuming the duplicate request issue is addressed).

src/model/chat/chatline.py (2)

34-37: LGTM!

The constructor signature change to use MagicDict is correct and aligns with how the metadata is built and accessed elsewhere in the codebase.


92-98: LGTM!

The my_mention attribute is correctly set based on the mentions_me boolean, providing the user's login string when mentioned or None otherwise. This integrates properly with the usage in src/chat/channel_view.py.

src/downloadManager/__init__.py (3)

428-429: LGTM!

The image_size() method provides a clean way to expose the configured image size for external callers.


484-484: LGTM!

The disk-backed index using os.scandir is efficient and correctly maps image names to their file paths for lazy loading.


489-503: LGTM! Previous issue resolved.

The lazy loading implementation correctly:

  1. Checks the memory cache first (line 492)
  2. Falls back to disk loading (line 494)
  3. Loads from disk, scales consistently, caches, and returns the same pixmap (lines 499-501)

The previous review concern about returning an unscaled pixmap while caching a scaled one has been properly addressed.

src/api/ApiBase.py (4)

5-5: LGTM! Improved type safety.

The type system enhancements provide better type safety:

  • Literal["page"] restricts meta keys more precisely than generic strings
  • ParsedApiResponse is a clearer name than the previous PreParsedApiResponse
  • The union type PreProcessedApiResponse properly captures both response types

Also applies to: 35-35, 38-40


237-254: LGTM! Proper error handling and resource cleanup.

The request finished handler correctly:

  1. Routes to the appropriate error or success handler based on reply status
  2. Cleans up handler registrations
  3. Schedules reply deletion with deleteLater()

The pattern ensures no memory leaks and proper resource management.


256-263: LGTM! Safe abort implementation.

The abort logic correctly:

  1. Uses handlers.copy() to avoid mutation during iteration
  2. Wraps abort calls in try/except to handle already-deleted C++ objects
  3. Provides appropriate error logging

166-179: None checks are necessary and align with Qt documentation.

According to Qt documentation for QNetworkAccessManager, while methods normally return a valid QNetworkReply object, failures can occur during application shutdown, threading issues, or internal errors. The documentation explicitly recommends "check for nullptr if your code must be defensive," which is exactly what these methods do by checking for None and raising RuntimeError. These defensive checks are appropriate and should be retained rather than simplified to assertions, which would be disabled in production builds.

Likely an incorrect or invalid review comment.

src/replays/replaydetails/tabs/gamestats.py (2)

12-13: LGTM! Using lazy-loaded heavy modules.

The imports from src.heavy_modules enable lazy loading of numpy and pyqtgraph, which can improve application startup time by deferring expensive imports until needed.


153-153: LabeledBarGraphItem.item attribute is properly defined and all usages are correct.

The .item attribute is defined at line 18 of src/qt/graphics/labeledbargraphitem.py as self.item = pg.BarGraphItem(**opts). All usages across the codebase (gamestats.py lines 153, 190, 227, 285, 310, 343, 366 and statistics.py line 48) correctly access the .item attribute. No action required.

src/fa/__init__.py (1)

14-14: LGTM! Clean API extension.

The addition of kill_all_if_running to the public API provides a convenient way to kill all running game instances. The export is properly added to both the import statement and __all__.

Also applies to: 24-24

setup.py (1)

83-86: The module path "scipy_ndimage.ndimage._filters" is correct. The project uses a custom minimized scipy distribution created by minimize_scipy.py, not the standard scipy package. This selective bundling is intentional to reduce distribution size.

Likely an incorrect or invalid review comment.

requirements.txt (1)

1-2: All dependency versions are valid on PyPI.

The package upgrades (cx_Freeze 8.5.1, idna 3.11, numpy 2.3.5, pydantic 2.12.5, pyqt6 6.10.1) are all confirmed released on PyPI with no security concerns identified. The irc package remains pinned at 20.4.0 as intended.

src/vaults/vault.py (2)

39-43: LGTM! Clean lazy initialization pattern.

The deferred UI setup approach effectively delays expensive setupUi calls until the vault is actually displayed. The _loaded flag properly guards against redundant initialization.


258-263: Verify thread safety of the lazy initialization.

The _loaded check and setup() call are not atomic. If busy_entered can be invoked from multiple threads or re-entered before setup() completes, there's a potential race condition. Given this is a Qt slot triggered by UI events, this is likely safe in practice since Qt's event loop is single-threaded, but worth confirming.

src/fa/game_session.py (2)

44-45: LGTM! New signals for ICE polling lifecycle.

The signals provide hooks for UI components to track ICE server polling status.


92-101: Consider symmetry for Go adapter path.

The ice_poll_started/ice_poll_finished signals are only emitted for the Java adapter path. The Go adapter path at line 101 (start_go_process) bypasses this entirely. If the UI relies on these signals for status indication, the Go path may leave the UI in an inconsistent state.

Is this asymmetry intentional because the Go adapter handles ICE server discovery differently (internally)?

src/vaults/mapvault/mapdetails.py (2)

36-46: LGTM! Clean migration to parsed API response pattern.

The switch from signal-based requestData to callback-based get_by_query_parsed simplifies the data flow and aligns with the PR's API refactoring.


55-58: LGTM! Correct TypedDict access.

The method signature and dictionary access (response["data"]) correctly match the ParsedApiResponse TypedDict definition.

src/vaults/reviewwidget.py (2)

317-318: LGTM! Correct initialization from existing review score.

When editing an existing review, the rating fields now preserve the original score rather than resetting to zero. For new reviews, model_construct at line 314 sets score=0, so the initial state remains correct.


330-337: LGTM! Correct ParsedApiResponse handling.

The method properly handles the TypedDict response and updates the review state.

src/replays/replaydetails/tabs/heatmap.py (2)

39-43: LGTM! Intentional sys.modules check for build optimization.

Based on learnings, this check supports the minimize_scipy.py build optimization that creates a minimal scipy distribution. The sys.modules approach correctly detects which structure is present.


377-378: LGTM! Correct lazy invocation pattern.

The gaussian_filter()() double-call pattern correctly obtains the implementation at runtime and then invokes it with the image data.

res/player_card/playercard.ui (1)

375-399: LGTM! Good pagination UX.

The new "Load next page" button alongside "Load all" provides users with granular control over data loading, which is a sensible improvement for handling API restrictions on rating history.

src/playercard/statistics.py (3)

6-7: LGTM! Centralized heavy module imports.

Using np and pg from src.heavy_modules aligns with the PR's lazy loading strategy for heavy dependencies.


98-102: Good defensive filter for optional event field.

The if player_event.event is not None guard correctly handles the case where PlayerEvent.event is None, preventing a potential AttributeError when accessing event.xd.


50-50: The .item property access pattern is correctly implemented.

Both LabeledBarGraphItem (initialized in src/qt/graphics/labeledbargraphitem.py:18) and PieChartItem (initialized in src/qt/graphics/piechartitem.py:30) properly expose the .item property, returning valid pyqtgraph graphics items (pg.BarGraphItem and pg.GraphicsObject respectively) suitable for use with plot.addItem().

src/fa/check.py (1)

121-124: LGTM! Clean composition pattern for update orchestration.

The refactoring separates concerns effectively:

  • FilesObtainer: handles API requests for featured mod files
  • UpdaterWorker: handles actual file update operations
  • Updater: orchestrates the workflow

This improves testability and follows single responsibility principle.

src/api/matchmaker_queue_api.py (1)

13-26: LGTM!

The convert_parsed implementation correctly follows the established pattern used by other API connectors in this codebase (e.g., FeaturedModApiConnector, CoopApiConnector). The method properly extracts queue data and builds the expected output structure.

src/vaults/detailswidget.py (1)

18-18: LGTM!

The signature updates to use ParsedApiResponse align with the broader API refactor pattern visible in related files (mapdetails.py, reviewwidget.py). The data access patterns remain consistent with the expected response structure.

Also applies to: 91-92, 379-391

src/heavy_modules.py (1)

8-22: LGTM - Lazy import mechanism is well-implemented.

The lazy_import function correctly uses importlib.util.LazyLoader to defer module loading. The fallback mechanism with backup_name provides flexibility for development vs. production environments.

src/client/_clientwindow.py (3)

182-182: LGTM!

Using fa.kill_all_if_running() instead of fa.instance.kill_if_running() correctly ensures both the main game instance and replay instance are terminated, as shown in the relevant snippet from game_process.py.


1810-1811: LGTM - ICE polling lifecycle handlers provide good user feedback.

The new on_ice_poll_started and on_ice_poll_finished handlers update the status bar label appropriately during the ICE adapter initialization phase, improving user experience during game launch.

Also applies to: 2173-2179


845-845: LGTM!

The label rename to labelStatusBarInfo is consistently applied throughout the file, and BackgroundImporter.start() is appropriately called after UI setup is complete.

Also applies to: 886-886

src/playercard/leagueformatter.py (2)

16-39: LGTM - Good dependency injection refactor.

Injecting CachedImageDownloader instead of creating it internally improves testability and allows consistent sizing across the application. The constructor properly stores and uses the injected downloader.


92-94: LGTM!

The image scaling logic correctly handles the optional size from the downloader - scaling only when a size is configured.

src/stats/leaderboard_widget.py (2)

163-167: LGTM!

The method signature update to accept ParsedApiResponse aligns with the broader API refactor. The data flow through createLeaderboard and processMeta correctly uses the parsed response structure.


209-214: LGTM!

The migration to get_by_query_parsed is consistent with the new parsed API response workflow. The f-string usage for filter construction is cleaner and more readable.

Also applies to: 283-294, 314-314

src/playercard/playerinfodialog.py (3)

45-46: LGTM!

The rating history pagination settings are properly initialized from persisted settings and saved on close, providing good user preference persistence.

Also applies to: 64-67, 169-169


86-93: LGTM!

The dependency injection of CachedImageDownloader for league icons and the introduction of instance attributes (player, player_ratings, player_events) for deferred rendering align well with the lazy-loading tab pattern. The typo fix from the past review (leaugesleagues) has been addressed.

Also applies to: 120-130, 148-148, 163-164


106-118: Lazy tab loading implementation looks good.

The _loaded_tabs tracking prevents redundant rendering on tab revisits. The potential race condition with stats tab (if API data hasn't arrived) was discussed in previous reviews and accepted given the typically fast API responses.

src/qt/graphics/labeledbargraphitem.py (1)

16-21: Composition refactor correctly updated—all callers access .item.

The class now uses composition instead of inheritance. All callers have been properly updated to use plot.addItem(bar.item) instead of attempting to add the wrapper directly to a scene.

src/fa/game_updater/worker.py (2)

248-249: LGTM! Clean signature simplification.

The refactored signature aligns well with the new FilesObtainer-based architecture, moving file acquisition logic out of the worker into a dedicated component.


258-274: LGTM! Clear dual-update flow.

The tuple unpacking and conditional logic correctly implement both single-update and FAF-first dual-update paths. The progress signaling properly reflects which update path is being taken.

src/qt/graphics/piechartitem.py (3)

10-11: LGTM! Lazy-loading optimization.

Replacing direct imports with heavy_modules proxies defers loading until first use, improving startup performance.


20-32: Verify the composition pattern intent.

The class no longer inherits from pg.GraphicsObject and instead uses composition by creating an instance and assigning methods to it (lines 31-32). This pattern works but is unconventional—when self.item.boundingRect() or self.item.paint() are called, self will refer to the PieChartItem instance.

Is this intentional to avoid eagerly loading pg.GraphicsObject at class definition time?

If the goal is lazy loading, an alternative pattern would be to keep inheritance but defer it:

# In heavy_modules.py or similar
def _get_graphics_object_class():
    import pyqtgraph as pg
    return pg.GraphicsObject

# Then use:
class PieChartItem(_get_graphics_object_class()):
    ...

However, if the current composition pattern is working as intended, it's acceptable.


96-100: LGTM! Signature aligns with composition pattern.

The paint method signature correctly places self first to align with the method assignment pattern on line 32.

src/fa/game_updater/updater.py (3)

188-193: LGTM! Clean separation of concerns.

The new wiring clearly separates file acquisition (FilesObtainer) from file processing (UpdaterWorker), improving modularity. The event loop coordination ensures proper sequencing.


243-292: LGTM! Well-structured file acquisition logic.

The FilesObtainer class cleanly encapsulates the logic for fetching file lists via API connectors and determining whether a dual-update (FAF + featured mod) or single-update flow is needed. The signal-based coordination with the worker is clear.


274-279: Intentional single-element unpacking for validation.

Based on learnings, the unpacking mod, = data["values"] is intentional—it validates that exactly one featured mod is returned. A ValueError will correctly signal an API error if zero or multiple values are returned.

Based on learnings, this unpacking pattern serves as validation.

src/api/vaults_api.py (4)

78-82: LGTM! Clean model construction.

The convert_parsed method explicitly constructs Mod instances from parsed data and preserves pagination metadata.


93-105: LGTM! Proper version embedding.

The _convert_mapversion helper cleanly combines map info with version data, and convert_parsed applies it consistently.


123-129: LGTM! Consistent conversion pattern.

The convert_parsed method follows the established pattern for converting parsed data to domain models.


175-184: LGTM! Updated to parsed response flow.

The submit_review method correctly uses post_and_parse and accepts a handler compatible with dict[str, Any].

src/replays/_replayswidget.py (6)

143-144: LGTM! Simplified online status check.

Using get_by_name directly simplifies the logic and removes the intermediate getID call.


375-388: LGTM! Idempotency guard prevents duplicate work.

The _loaded flag ensures _setup_appearance only executes once, which is important given the lazy-loading pattern introduced with on_item_expanded.


600-604: LGTM! Lazy loading for tree items.

The on_item_expanded handler defers appearance setup until a bucket is actually expanded, improving initial rendering performance.


945-949: LGTM! Consistent ParsedApiResponse migration.

The update to get_by_query_parsed aligns with the broader API refactoring.


1199-1199: LGTM! Signature updated for ParsedApiResponse.

The method correctly consumes the new ParsedApiResponse type.


1275-1288: LGTM! Smart visibility optimization.

Checking myTree.isVisible() before updating avoids unnecessary work when the local replays tab is not active. The on_tab_changed handler (line 1286-1288) ensures updates trigger when the tab becomes visible.

src/playercard/ratingtabwidget.py (3)

84-106: LGTM! Clean paging control integration.

The default_pages_box parameter wires the UI spinbox to the tab's paging logic, allowing users to control how many pages load by default. The on_default_pages_changed handler ensures the setting only applies before any data is loaded.


130-133: LGTM! Incremental page loading.

The load_next_rating_page method advances _default_pages by one if already loaded, providing fine-grained control alongside load_more_ratings (which loads all remaining pages).


191-217: LGTM! Controller wiring for pagination UI.

The controller correctly wires the load_next_button and passes default_pages_box to each RatingsPlotTab, enabling coordinated paging control across the UI.

src/api/stats_api.py (5)

37-44: LGTM! Explicit model construction with sorting.

The method constructs LeaderboardRating instances and sorts them by leaderboard order, providing a consistent presentation to consumers.


51-60: LGTM! Idiomatic sorting with methodcaller.

Using methodcaller("order") is a clean, functional approach to sorting by the leaderboard's order method.


78-100: LGTM! Request tracking and cancellation.

The replies list and abort method enable proper cleanup of in-flight requests, preventing stale handlers from firing after the connector is torn down.


134-147: LGTM! Consistent ParsedApiResponse migration.

Both get_player_score_in_leaderboard and get_player_scores correctly use get_by_query_parsed and construct LeagueSeasonScore instances.


161-193: LGTM! Accessor updates consistent.

All accessors (PlayerEventApiAccessor, PlayerAchievementApiAccessor, AchievementsApiAccessor) correctly migrate to get_by_query_parsed.

src/api/featured_mod_api.py (2)

17-21: LGTM! Explicit FeaturedMod construction.

The convert_parsed method constructs FeaturedMod instances from parsed data, aligning with the broader API refactoring.


29-46: LGTM! Signal-based file retrieval.

The new signals (main_ready, mod_ready) and corresponding methods (get_main_files, get_mod_files) provide a clean, signal-driven API for retrieving featured mod files. The endpoint method correctly handles version resolution with a "latest" fallback.

src/api/ApiAccessors.py (6)

1-18: LGTM! Imports align with new functionality.

The new imports (Literal, QByteArray, JsonApiBase, ParsedApiResponse, _do_nothing) are all used in the refactored code and support the parsed/converted response workflow.


43-79: LGTM! Parsed response entry points are well-designed.

The new get_by_query_parsed, get_by_endpoint_parsed, and post_and_parse methods provide a clean API for working with parsed responses. They properly delegate to existing methods and use the _parse_and_handle wrapper pattern correctly.


89-111: Converted entry points depend on convert_parsed implementation.

These methods (get_by_query_converted and get_by_endpoint_converted) are correctly implemented and follow the same pattern as the parsed variants. However, they depend on convert_parsed being implemented by subclasses (see comment on lines 81-88).


113-118: LGTM! parse_message correctly structures parsed responses.

The method properly parses included items, data, and metadata into a well-typed ParsedApiResponse. Making this public allows consumers to parse responses directly when needed.


182-185: LGTM! Type hint improvement with Literal.

The updated signature with Literal["page"] provides better type safety by constraining the metadata keys. The default return value correctly matches the type constraint.


20-30: The base class change is compatible with all existing subclasses and usages. ApiAccessor, UserApiAccessor, and all descendant classes (DataApiAccessor and its many subclasses) properly initialize and work with JsonApiBase. The host_config_key pattern is correctly implemented, with the base class providing an empty default that subclasses appropriately override.

Copy link

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 0

Caution

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (1)
src/vaults/reviewwidget.py (1)

352-358: Inconsistent state management between self._rating and self.review.score.

Lines 353-354 use self.review.score (the persisted value) while line 373 in _handle_mouse_over_star() uses self._rating (the user's draft selection). This creates an inconsistency:

  • self._rating tracks the user's current selection
  • self.review.score is the loaded/persisted value
  • If update_appearance() is called after the user selects a star, it would reset both the star display and button state to the persisted value, undoing the user's selection visualization

While this currently doesn't cause a bug (since update_appearance() is only called during initialization and data loading), it's fragile. The button enablement and star display should consistently use self._rating to reflect the user's draft state.

🔎 Proposed fix for consistent state management
 def update_appearance(self) -> None:
-    self.star_rating.set_rating(self.review.score)
-    self.ui.submitButton.setEnabled(self.review.score > 0)
+    self.star_rating.set_rating(self._rating)
+    self.ui.submitButton.setEnabled(self._rating > 0)
     self.ui.commentTextEdit.setText(self.review.text)
     cursor = self.ui.commentTextEdit.textCursor()
     cursor.movePosition(cursor.MoveOperation.End)
     self.ui.commentTextEdit.setTextCursor(cursor)
♻️ Duplicate comments (1)
src/api/ApiAccessors.py (1)

198-199: Subclasses must implement convert_parsed to use converted methods.

As noted in the past review, calling get_by_query_converted, get_by_endpoint_converted, or requestData will fail at runtime if the subclass doesn't implement convert_parsed. This is acceptable if DataApiAccessor is intended as an abstract base requiring subclass implementation.

Consider adding @abstractmethod decorator and inheriting from ABC to make this contract explicit and catch missing implementations at import time rather than runtime.

🧹 Nitpick comments (3)
src/chat/channel_tab.py (1)

109-110: Consider storing the sound effect in a local variable.

The function channel_ping_sound(self._theme) is called twice. While the @cache decorator ensures the same object is returned, storing it in a local variable improves readability and eliminates the redundant lookup.

🔎 Suggested refactor
-        if channel_ping_sound(self._theme).isLoaded():
-            channel_ping_sound(self._theme).play()
+        sound = channel_ping_sound(self._theme)
+        if sound.isLoaded():
+            sound.play()
src/replays/replayitem.py (1)

337-342: Improved team winner detection with proper tie handling.

The sort-based approach is cleaner and correctly handles ties. However, the loop variable team is unused.

🔎 Use underscore for unused loop variable
             sorted_scores = sorted(scores.items(), key=itemgetter(1), reverse=True)
             self.teamWin, mvt = sorted_scores[0]
-            for team, score in sorted_scores[1:]:
+            for _, score in sorted_scores[1:]:
                 if score >= mvt:
                     self.teamWin = None
                     break
src/api/ApiBase.py (1)

174-176: Consider extracting exception message to a constant (optional).

The static analysis tool flags long exception messages inline. While the current message is helpful for debugging, you could optionally extract it to a module-level constant if consistency is desired. This is a minor style preference and not blocking.

📜 Review details

Configuration used: defaults

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between a95dc95 and a8bd614.

📒 Files selected for processing (66)
  • requirements.txt
  • res/chat/channel.css
  • res/client/client.css
  • res/client/client.ui
  • res/player_card/playercard.ui
  • setup.py
  • src/api/ApiAccessors.py
  • src/api/ApiBase.py
  • src/api/coop_api.py
  • src/api/featured_mod_api.py
  • src/api/matchmaker_queue_api.py
  • src/api/parsers/CoopResultParser.py
  • src/api/parsers/CoopScenarioParser.py
  • src/api/parsers/FeaturedModFileParser.py
  • src/api/parsers/FeaturedModParser.py
  • src/api/parsers/GeneratedMapParamsParser.py
  • src/api/parsers/LeaderboardParser.py
  • src/api/parsers/LeaderboardRatingJournalParser.py
  • src/api/parsers/LeaderboardRatingParser.py
  • src/api/parsers/MapParser.py
  • src/api/parsers/MapVersionParser.py
  • src/api/parsers/ModParser.py
  • src/api/parsers/ModVersionParser.py
  • src/api/parsers/PlayerParser.py
  • src/api/parsers/ReviewsSummaryParser.py
  • src/api/parsers/__init__.py
  • src/api/player_api.py
  • src/api/sim_mod_updater.py
  • src/api/stats_api.py
  • src/api/vaults_api.py
  • src/chat/channel_tab.py
  • src/chat/channel_view.py
  • src/client/_clientwindow.py
  • src/coop/_coopwidget.py
  • src/downloadManager/__init__.py
  • src/fa/__init__.py
  • src/fa/check.py
  • src/fa/game_process.py
  • src/fa/game_session.py
  • src/fa/game_updater/updater.py
  • src/fa/game_updater/worker.py
  • src/games/_gameswidget.py
  • src/heavy_modules.py
  • src/model/chat/chatline.py
  • src/notifications/ns_dialog.py
  • src/playercard/leagueformatter.py
  • src/playercard/playerinfodialog.py
  • src/playercard/plot.py
  • src/playercard/ratingtabwidget.py
  • src/playercard/statistics.py
  • src/qt/graphics/labeledbargraphitem.py
  • src/qt/graphics/piechartitem.py
  • src/replays/_replayswidget.py
  • src/replays/replaydetails/replaycard.py
  • src/replays/replaydetails/replayreader.py
  • src/replays/replaydetails/tabs/gamestats.py
  • src/replays/replaydetails/tabs/heatmap.py
  • src/replays/replayitem.py
  • src/stats/_statswidget.py
  • src/stats/leaderboard_widget.py
  • src/vaults/detailswidget.py
  • src/vaults/mapvault/mapdetails.py
  • src/vaults/mapvault/mapvault.py
  • src/vaults/modvault/modvault.py
  • src/vaults/reviewwidget.py
  • src/vaults/vault.py
💤 Files with no reviewable changes (16)
  • res/client/client.css
  • src/api/parsers/ModParser.py
  • src/api/parsers/CoopScenarioParser.py
  • src/api/parsers/GeneratedMapParamsParser.py
  • src/stats/_statswidget.py
  • src/api/parsers/MapParser.py
  • src/api/parsers/LeaderboardParser.py
  • src/api/parsers/LeaderboardRatingJournalParser.py
  • src/api/parsers/MapVersionParser.py
  • src/api/parsers/FeaturedModParser.py
  • src/api/parsers/ModVersionParser.py
  • src/api/parsers/FeaturedModFileParser.py
  • src/api/parsers/PlayerParser.py
  • src/api/parsers/LeaderboardRatingParser.py
  • src/api/parsers/CoopResultParser.py
  • src/api/parsers/ReviewsSummaryParser.py
🚧 Files skipped from review as they are similar to previous changes (15)
  • src/fa/game_process.py
  • src/api/sim_mod_updater.py
  • res/client/client.ui
  • res/chat/channel.css
  • src/coop/_coopwidget.py
  • res/player_card/playercard.ui
  • src/fa/init.py
  • src/replays/replaydetails/replayreader.py
  • src/api/player_api.py
  • src/games/_gameswidget.py
  • src/fa/check.py
  • src/fa/game_session.py
  • src/chat/channel_view.py
  • src/model/chat/chatline.py
  • src/vaults/mapvault/mapvault.py
🧰 Additional context used
🧠 Learnings (7)
📚 Learning: 2025-12-27T22:19:06.765Z
Learnt from: Gatsik
Repo: FAForever/client PR: 1148
File: requirements.txt:0-0
Timestamp: 2025-12-27T22:19:06.765Z
Learning: Do not propose upgrading the irc package in requirements.txt from 20.4.0 to 20.5.0 due to a known importlib.resources.files() bug in 20.5.0. In reviews of FAForever/client, maintain the pin at 20.4.0 and flag any upgrade discussion for this package as blocked unless there is a documented, validated workaround.

Applied to files:

  • requirements.txt
📚 Learning: 2025-12-27T22:30:11.905Z
Learnt from: Gatsik
Repo: FAForever/client PR: 1148
File: src/replays/replaydetails/replaycard.py:89-90
Timestamp: 2025-12-27T22:30:11.905Z
Learning: In the FAForever/client repository, ReplayDetailsCard (src/replays/replaydetails/replaycard.py) is used as a modal dialog with only one instance at a time, so class-level QNetworkAccessManager doesn't cause cross-instance issues.

Applied to files:

  • src/notifications/ns_dialog.py
  • src/fa/game_updater/worker.py
📚 Learning: 2025-12-27T22:23:44.249Z
Learnt from: Gatsik
Repo: FAForever/client PR: 1148
File: src/fa/game_updater/updater.py:274-279
Timestamp: 2025-12-27T22:23:44.249Z
Learning: In src/fa/game_updater/updater.py, the single-element unpacking `mod, = data["values"]` in the `process_fmod` method is intentional and serves as validation that exactly one featured mod is returned. Multiple or zero values should raise a ValueError to signal an API error.

Applied to files:

  • src/fa/game_updater/worker.py
  • src/fa/game_updater/updater.py
📚 Learning: 2025-12-27T22:31:12.379Z
Learnt from: Gatsik
Repo: FAForever/client PR: 1148
File: src/replays/replaydetails/tabs/heatmap.py:39-43
Timestamp: 2025-12-27T22:31:12.379Z
Learning: In src/replays/replaydetails/tabs/heatmap.py, keep the explicit sys.modules check for scipy_ndimage.ndimage._filters around gaussian_filter(), as used by minimize_scipy.py to build a minimal scipy distribution. Do not replace with hasattr() or similar checks; this is intentional to detect the presence of the reduced module for the build optimization.

Applied to files:

  • src/replays/replaydetails/tabs/heatmap.py
📚 Learning: 2025-12-27T22:30:11.905Z
Learnt from: Gatsik
Repo: FAForever/client PR: 1148
File: src/replays/replaydetails/replaycard.py:89-90
Timestamp: 2025-12-27T22:30:11.905Z
Learning: In src/replays/replaydetails/replaycard.py, ReplayDetailsCard is used as a modal dialog with a single instance at a time. Therefore a class-level QNetworkAccessManager does not cause cross-instance issues. If future dialogs could be opened concurrently, prefer an instance-scoped QNetworkAccessManager to avoid shared state across dialogs.

Applied to files:

  • src/replays/replaydetails/replaycard.py
📚 Learning: 2025-11-23T00:07:39.893Z
Learnt from: Gatsik
Repo: FAForever/client PR: 1145
File: src/api/vaults_api.py:114-116
Timestamp: 2025-11-23T00:07:39.893Z
Learning: In src/api/vaults_api.py, the MapPoolApiConnector class uses intentional stateful filter mutation. The request_pool_for_queue method sets self._filters and is the intended public API; request_data() should not be called directly on instances of this class.

Applied to files:

  • src/api/vaults_api.py
📚 Learning: 2025-12-27T22:31:12.379Z
Learnt from: Gatsik
Repo: FAForever/client PR: 1148
File: src/replays/replaydetails/tabs/heatmap.py:39-43
Timestamp: 2025-12-27T22:31:12.379Z
Learning: In src/replays/replaydetails/tabs/heatmap.py, the gaussian_filter() function checks for "scipy_ndimage.ndimage._filters" in sys.modules to support a build optimization. The project uses minimize_scipy.py to create a minimal scipy distribution containing only the gaussian_filter function and dependencies, avoiding shipping the entire scipy package. This sys.modules check is intentional and should not be replaced with hasattr() or other approaches.

Applied to files:

  • src/heavy_modules.py
🧬 Code graph analysis (20)
src/vaults/reviewwidget.py (3)
src/api/ApiBase.py (1)
  • ParsedApiResponse (38-40)
src/replays/models.py (1)
  • score (48-49)
src/vaults/detailswidget.py (1)
  • on_reviews_data (379-391)
src/playercard/statistics.py (1)
src/api/models/PlayerEvent.py (1)
  • PlayerEvent (8-12)
src/chat/channel_tab.py (1)
src/util/theme.py (3)
  • theme (142-143)
  • sound (118-120)
  • sound (355-357)
src/vaults/mapvault/mapdetails.py (3)
src/api/ApiBase.py (1)
  • ParsedApiResponse (38-40)
src/api/ApiAccessors.py (1)
  • get_by_query_parsed (43-53)
src/vaults/detailswidget.py (1)
  • allow_review (91-92)
src/api/matchmaker_queue_api.py (5)
src/api/ApiBase.py (1)
  • ParsedApiResponse (38-40)
src/api/coop_api.py (2)
  • convert_parsed (14-18)
  • convert_parsed (57-63)
src/api/featured_mod_api.py (2)
  • convert_parsed (17-21)
  • convert_parsed (32-36)
src/api/vaults_api.py (3)
  • convert_parsed (78-82)
  • convert_parsed (98-105)
  • convert_parsed (123-129)
src/api/ApiAccessors.py (1)
  • convert_parsed (198-199)
src/playercard/leagueformatter.py (1)
src/downloadManager/__init__.py (2)
  • CachedImageDownloader (480-513)
  • image_size (428-429)
src/notifications/ns_dialog.py (1)
src/util/theme.py (2)
  • sound (118-120)
  • sound (355-357)
src/fa/game_updater/worker.py (3)
src/api/models/FeaturedModFile.py (1)
  • FeaturedModFile (6-15)
src/model/transaction.py (1)
  • emit (18-19)
src/fa/game_updater/misc.py (1)
  • ProgressInfo (32-35)
src/api/coop_api.py (4)
src/api/ApiBase.py (1)
  • ParsedApiResponse (38-40)
src/api/ApiAccessors.py (1)
  • convert_parsed (198-199)
src/api/models/CoopScenario.py (1)
  • CoopScenario (7-13)
src/api/models/CoopResult.py (1)
  • CoopResult (7-14)
src/stats/leaderboard_widget.py (3)
src/api/ApiBase.py (1)
  • ParsedApiResponse (38-40)
src/api/ApiAccessors.py (1)
  • get_by_query_parsed (43-53)
src/api/stats_api.py (1)
  • filters (116-122)
src/replays/_replayswidget.py (3)
src/api/ApiBase.py (1)
  • ParsedApiResponse (38-40)
src/model/playerset.py (1)
  • get_by_name (19-22)
src/api/ApiAccessors.py (1)
  • get_by_query_parsed (43-53)
src/vaults/detailswidget.py (3)
src/api/ApiBase.py (1)
  • ParsedApiResponse (38-40)
src/vaults/mapvault/mapdetails.py (1)
  • allow_review (55-58)
src/vaults/reviewwidget.py (1)
  • on_reviews_data (330-337)
src/api/ApiAccessors.py (1)
src/api/ApiBase.py (10)
  • ApiResourceObject (25-29)
  • ApiResponse (32-35)
  • JsonApiBase (53-147)
  • ParsedApiResponse (38-40)
  • _do_nothing (49-50)
  • handle (64-65)
  • get_by_query (92-99)
  • get_by_endpoint (101-108)
  • post (110-122)
  • post (181-197)
src/api/stats_api.py (5)
src/api/ApiBase.py (2)
  • ParsedApiResponse (38-40)
  • get_by_query (92-99)
src/api/models/Leaderboard.py (2)
  • Leaderboard (8-37)
  • order (27-31)
src/api/models/LeaderboardRating.py (1)
  • LeaderboardRating (7-14)
src/api/ApiAccessors.py (2)
  • get_by_query_parsed (43-53)
  • convert_parsed (198-199)
src/api/coop_api.py (2)
  • convert_parsed (14-18)
  • convert_parsed (57-63)
src/client/_clientwindow.py (1)
src/fa/game_process.py (1)
  • kill_all_if_running (198-200)
src/api/ApiBase.py (1)
src/api/ApiAccessors.py (2)
  • handle (39-40)
  • handle (85-86)
src/vaults/vault.py (2)
src/vaults/mapvault/mapvault.py (1)
  • setup (26-40)
src/vaults/modvault/modvault.py (1)
  • setup (27-47)
src/playercard/playerinfodialog.py (4)
src/contextmenu/playercontextmenu.py (1)
  • PlayerContextMenu (50-284)
src/config/__init__.py (2)
  • Settings (43-207)
  • group (51-56)
src/qt/utils.py (1)
  • center_widget_on_screen (49-54)
src/playercard/leagueformatter.py (1)
  • league_formatter_factory (108-116)
src/api/vaults_api.py (5)
src/api/ApiBase.py (1)
  • ParsedApiResponse (38-40)
src/api/ApiAccessors.py (3)
  • requestData (187-196)
  • get_by_query_parsed (43-53)
  • post_and_parse (67-79)
src/api/models/Mod.py (1)
  • Mod (9-15)
src/api/models/MapVersion.py (2)
  • Map (37-48)
  • MapVersion (51-73)
src/util/__init__.py (1)
  • decapitalize (527-528)
src/fa/game_updater/updater.py (5)
src/api/featured_mod_api.py (3)
  • FeaturedModApiConnector (13-25)
  • FeaturedModFilesApiConnector (28-46)
  • request_fmod_by_name (23-25)
src/api/models/FeaturedMod.py (1)
  • FeaturedMod (6-12)
src/api/models/FeaturedModFile.py (1)
  • FeaturedModFile (6-15)
src/fa/game_updater/worker.py (2)
  • UpdaterWorker (39-289)
  • do_update (258-284)
src/fa/game_updater/misc.py (2)
  • UpdaterResult (25-29)
  • log (47-49)
🪛 Ruff (0.14.10)
src/replays/replayitem.py

339-339: Loop control variable team not used within loop body

(B007)

src/downloadManager/__init__.py

501-501: Consider moving this statement to an else block

(TRY300)

src/api/ApiBase.py

176-176: Avoid specifying long messages outside the exception class

(TRY003)


194-194: Avoid specifying long messages outside the exception class

(TRY003)


210-210: Avoid specifying long messages outside the exception class

(TRY003)


230-230: Avoid specifying long messages outside the exception class

(TRY003)

src/fa/game_updater/updater.py

164-164: Undefined name FilesObtainer

(F821)

src/heavy_modules.py

11-11: Abstract raise to an inner function

(TRY301)

🔇 Additional comments (68)
src/notifications/ns_dialog.py (1)

5-5: LGTM! Clean refactoring to module-level cached sound effect.

The change from per-instance to module-level cached QSoundEffect is well-executed. Since notifications are queued and displayed sequentially (confirmed in the previous discussion), a single shared instance is appropriate and avoids unnecessary object creation. The isLoaded() check provides safety, and this pattern aligns with similar refactorings elsewhere in the codebase.

Based on learnings, the single cached instance doesn't cause cross-instance issues for modal dialogs with sequential display.

Also applies to: 17-24, 84-85

src/chat/channel_tab.py (1)

26-30: Verify concurrent playback behavior with shared QSoundEffect.

The cached function means all ChannelTab instances sharing the same theme will use a single QSoundEffect instance. If multiple channels ping simultaneously, calling play() on an already-playing QSoundEffect will restart it from the beginning (per Qt documentation), potentially causing sound interruption or missed pings.

Confirm this behavior is acceptable compared to the previous implementation where each tab likely had its own sound effect instance.

src/vaults/reviewwidget.py (2)

20-20: LGTM! API type refactoring is consistent.

The migration from PreParsedApiResponse to ParsedApiResponse aligns with the broader API refactoring mentioned in the PR objectives. The usage in on_reviews_data() correctly handles the response structure.

Also applies to: 330-330


317-318: LGTM! Initialization now context-aware.

Deriving _rating and _hovered_rating from the review's score makes the dialog correctly handle both new reviews (score=0) and editing existing reviews (score=existing value).

setup.py (1)

83-86: LGTM! Build includes for lazy-loaded heavy modules.

The additions correctly ensure pyqtgraph and the custom minimized scipy_ndimage.ndimage._filters are bundled by cx_Freeze. This aligns with the new heavy_modules.py lazy import mechanism and the existing scipy exclusion on line 90.

src/vaults/vault.py (2)

39-42: Good lazy initialization pattern for deferred UI setup.

The _loaded flag and setup() method correctly defer setupUi until the vault is first accessed via busy_entered(). This aligns with the broader performance optimization pattern used consistently across MapVault and ModVault.


258-263: Lazy loading trigger implemented correctly.

The guard ensures setup() runs exactly once on first use, then proceeds with the existing pagination logic.

src/vaults/modvault/modvault.py (1)

27-29: Consistent lazy setup pattern.

The setup() method correctly delegates to super().setup() first, then performs ModVault-specific initialization. This maintains consistency with the vault hierarchy's deferred initialization approach.

src/downloadManager/__init__.py (2)

428-430: Good addition of size accessor.

The image_size() method provides read-only access to the configured size, which is useful for consumers that need to know the scaling dimensions.


484-503: Efficient disk-backed lazy loading implementation.

The refactored CachedImageDownloader now uses a two-tier cache strategy:

  1. _images_index provides O(1) path lookup without loading images into memory
  2. get_image checks in-memory cache first, then lazy-loads from disk on demand

This avoids the startup cost of loading all cached images while maintaining fast access for frequently used images.

src/heavy_modules.py (2)

8-22: Well-implemented lazy import mechanism.

The lazy_import function correctly:

  1. Attempts to find the primary module spec
  2. Falls back to backup_name if not found
  3. Uses LazyLoader to defer actual module loading until first attribute access
  4. Registers the module in sys.modules for subsequent imports

30-38: Background module preloader design is correctly implemented.

The _BackgroundImporter triggers lazy module loading by calling dir() on each module, which forces LazyLoader to execute. The deleteLater connection ensures proper cleanup. BackgroundImporter is instantiated at module import time, and BackgroundImporter.start() is called in the application lifecycle at src/client/_clientwindow.py:886, ensuring background loading occurs at the appropriate point after window initialization.

src/replays/replaydetails/tabs/gamestats.py (2)

12-13: Imports updated to use lazy-loaded heavy modules.

Switching from direct numpy/pyqtgraph imports to src.heavy_modules enables deferred loading, improving application startup time.


153-153: All plot.addItem(bar) calls properly updated to use bar.item.

The LabeledBarGraphItem class correctly exposes the underlying BarGraphItem via the .item property (defined in src/qt/graphics/labeledbargraphitem.py), and all seven locations in gamestats.py (lines 153, 190, 227, 285, 310, 343, 366) consistently use this pattern.

requirements.txt (1)

1-7: Dependency updates and new numpy addition look good.

The version upgrades (cx_Freeze, idna, pydantic, pyqt6) are all patch-level updates. The new numpy==2.3.5 dependency aligns with the lazy import mechanism in heavy_modules.py. Keeping irc==20.4.0 is correct due to the known importlib.resources.files() bug in 20.5.0.

src/replays/replaydetails/tabs/heatmap.py (2)

39-43: LGTM - Intentional build optimization for minimal scipy distribution.

Based on the learnings from prior discussion, this sys.modules check is intentional to support the minimized scipy distribution created by minimize_scipy.py. The explicit check correctly detects whether the reduced module structure is present at runtime.

Consider adding a brief comment documenting this intent for future maintainers, as suggested in the prior review.


378-378: Correct usage of the new gaussian_filter helper.

The double-call pattern gaussian_filter()(img, ...) correctly retrieves the callable first, then invokes it with the image data and sigma parameters.

src/client/_clientwindow.py (3)

182-182: Good improvement: Now kills both game and replay processes.

Per the relevant snippet from src/fa/game_process.py, kill_all_if_running() terminates both instance and replay_instance, which is more comprehensive for the tray menu action.


1810-1811: ICE polling signals properly wired to new handlers.

The signal connections are correctly placed after the GameSession is created, ensuring the UI can respond to ICE adapter lifecycle events.


2173-2179: New ICE polling handlers provide useful status feedback.

The handlers appropriately update the status bar to inform users about the ICE adapter initialization stages ("Getting ICE Servers..." → "Launching ICE Adapter..."), improving transparency during game launch.

src/api/ApiBase.py (3)

49-50: Good addition of a shared no-op handler.

The _do_nothing helper provides a clean default for optional callbacks, avoiding None checks throughout the codebase.


150-158: Well-structured centralized API access manager.

The ApiAccessManager class cleanly centralizes OAuth integration and request lifecycle management. Storing handlers keyed by QNetworkReply is an appropriate pattern for async response routing.


237-254: Correct cleanup of reply handlers.

The on_request_finished method properly:

  1. Routes to error or success handler based on reply status
  2. Removes entries from both handler dictionaries
  3. Schedules reply deletion via deleteLater()

This prevents memory leaks from accumulated handlers.

src/api/vaults_api.py (2)

138-141: Correct migration to parsed response flow.

ReviewsApiConnector.request_data now uses get_by_query_parsed and directly passes self.data_ready.emit as the response handler, which correctly emits the parsed response for downstream consumers.


93-96: [rewritten review comment]
[classification tag]

src/api/ApiAccessors.py (3)

35-41: Clean closure pattern for response transformation.

The _parse_and_handle method creates a closure that wraps the user's handler, applying parse_message before invoking it. This is a clean functional approach for the transformation pipeline.


81-87: Conversion pipeline correctly chains parsing and conversion.

_convert_and_handle properly chains parse_messageconvert_parsed → handler, ensuring the full transformation pipeline is applied before the handler receives the data.


187-196: requestData correctly routes through the converted pipeline.

The method properly uses get_by_query_converted and emits results via data_ready, integrating with the new architecture while maintaining the existing signal-based interface.

src/vaults/mapvault/mapdetails.py (2)

36-46: Clean migration to direct callback pattern.

The refactor from requestData with signal connection to get_by_query_parsed with direct callback simplifies the control flow. The query structure and callback are now co-located, improving readability.


55-58: Type annotation correctly updated for ParsedApiResponse.

The allow_review method signature properly reflects the new ParsedApiResponse type, and the access pattern response["data"] is correct for the typed dict structure.

src/playercard/statistics.py (3)

98-102: Good defensive filtering for None events.

The comprehension now correctly filters out PlayerEvent instances where event is None, preventing AttributeError when accessing event.xd. This aligns with the model definition showing event: Event | None.


97-97: Generator type annotation correctly simplified.

The simplified Generator[pg.PlotWidget] syntax is valid for generators that don't use send() or return values, and is cleaner than the full three-parameter form.


50-50: .item property confirmed on both chart item classes.

The code correctly accesses bar.item and pie.item. LabeledBarGraphItem initializes .item as pg.BarGraphItem(**opts) (src/qt/graphics/labeledbargraphitem.py:18) and PieChartItem initializes .item as pg.GraphicsObject(parent_item) (src/qt/graphics/piechartitem.py:30). Both properties are available and correctly expose the underlying graphics items for the plot.

src/api/matchmaker_queue_api.py (2)

13-26: Follows established convert_parsed pattern.

The implementation correctly migrates from prepare_data to convert_parsed, following the same structure as other connectors in the codebase (e.g., FeaturedModApiConnector, CoopScenarioApiConnector).


17-25: API include parameter guarantees leaderboard presence — defensive handling unnecessary.

The code at lines 21 and 23 accesses nested queue["leaderboard"] data. However, the API connector is always invoked with requestData({"include": "leaderboard"}) in src/games/automatchframe.py (line 101), which guarantees the leaderboard relationship is included in the response. The KeyError risk does not apply to the current usage pattern.

No defensive handling is required unless the API call contract changes or the method is repurposed for other endpoints without the leaderboard include parameter.

src/replays/replaydetails/replaycard.py (1)

89-90: LGTM - Class-level downloader is appropriate for modal dialog.

The class-level QNetworkAccessManager is acceptable given that ReplayDetailsCard is used as a modal dialog with only one instance at a time.

Based on learnings, this architectural pattern prevents cross-instance signal conflicts in the modal dialog use case.

src/vaults/detailswidget.py (1)

18-18: LGTM - Consistent with ParsedApiResponse migration.

The type updates from ApiResponse/PreParsedApiResponse to ParsedApiResponse align with the broader refactoring to use centralized parsed API responses throughout the codebase.

Also applies to: 91-91, 379-379

src/api/coop_api.py (1)

14-18: LGTM - Direct model construction simplifies the code.

The refactoring removes trivial parser modules in favor of direct model instantiation (CoopScenario(**scenario), CoopResult(**result)). The filter_unique_teams logic is correctly preserved in the new flow.

Also applies to: 57-63

src/playercard/plot.py (1)

7-8: LGTM - Consistent heavy module proxy adoption.

The changes introduce lazy-loaded heavy module proxies (np, pg) and update type hints accordingly. The simplified pg.DateAxisItem() call is the correct public API usage.

Also applies to: 63-63, 138-144, 167-167

src/qt/graphics/labeledbargraphitem.py (1)

16-19: LGTM - Composition over inheritance refactoring.

The class correctly transitions from inheriting pg.BarGraphItem to using composition with an internal self.item. Event delegation and data access patterns are properly updated.

Also applies to: 23-23, 28-30, 37-37

src/playercard/leagueformatter.py (1)

17-17: LGTM - Proper dependency injection for image downloader.

The refactoring injects the image downloader as a dependency instead of creating it internally. The image_size() delegation and factory updates are consistent with the broader image handling improvements.

Also applies to: 21-21, 34-34, 93-94, 112-116

src/stats/leaderboard_widget.py (1)

7-7: LGTM - Complete ParsedApiResponse migration.

The file consistently migrates to the parsed API response pattern, updating method signatures, call sites (get_by_query_parsed), and filter construction (f-strings). All changes are coherent and correct.

Also applies to: 163-165, 211-214, 244-245, 285-286, 291-291, 314-314

src/playercard/playerinfodialog.py (1)

86-93: LGTM - Deferred rendering pattern implemented consistently.

The changes introduce state management (player, player_ratings, player_events) and deferred tab rendering with _loaded_tabs tracking. The stats tab rendering pattern was previously discussed and deemed acceptable given fast API responses and infrequent access patterns. The defensive check on Line 116-117 (if self.player is not None) appropriately guards against null access.

Also applies to: 107-118, 122-130, 148-148, 164-164, 169-169

src/qt/graphics/piechartitem.py (3)

10-11: LGTM: Lazy loading via heavy_modules.

The deferred imports for np and pg align with the PR's goal to improve startup performance by delaying heavy module initialization.


96-104: LGTM: Paint delegation is correctly implemented.

The empty paint method is appropriate since rendering is handled by child items attached to self.item. The updated signature with proper type hints is also good.


20-32: The composition pattern with method delegation works correctly and poses no issues in practice. The codebase explicitly uses pie.item when adding to the Graphics View (plot.addItem(pie.item)), and there are no isinstance checks that would be affected by the wrapper pattern. The delegated boundingRect and paint methods function properly with PyQt6's Graphics View framework, as evidenced by the working implementation and child item parenting.

src/fa/game_updater/worker.py (2)

248-249: LGTM: Simplified signature.

The refactored signature directly accepts the file list, moving version resolution responsibility to the caller (FilesObtainer). This aligns with the single-responsibility principle.


258-284: LGTM: Two-list update flow is correctly implemented.

The logic properly handles two scenarios:

  1. FAF + featured mod: Updates FAF files first (1/2), then mod-specific files (2/2)
  2. FAF only: Single update pass (1/1)

Progress signaling correctly reflects the current step in each path.

src/playercard/ratingtabwidget.py (3)

78-120: LGTM: Pagination state management is well-designed.

The on_default_pages_changed guard (lines 118-120) prevents modifying the page count during active navigation, which avoids confusing state transitions. The spin box is properly wired to update _default_pages.


130-133: LGTM: Incremental page loading is correctly implemented.

The method increments _default_pages when already loaded, enabling "load next page" functionality. This complements the existing "load all" behavior in load_more_ratings.


191-226: LGTM: Controller pagination wiring is well-structured.

The controller properly:

  • Centralizes button click handling (line 202)
  • Shares the default_pages_box across all tabs (lines 211-217)
  • Delegates to the active tab (lines 224-226)

This maintains consistent pagination behavior across all rating tabs.

src/replays/_replayswidget.py (5)

143-144: LGTM: Correct usage of get_by_name.

The change from a membership check to calling get_by_name(name) is not None aligns with the Playerset API shown in the relevant snippets.


375-388: LGTM: Idempotent setup pattern correctly implemented.

The _loaded guard (lines 376-378) prevents redundant setup calls, which is important since _setup_appearance can be triggered from multiple sources (expansion, map preview downloads). Setting _loaded = True at the end (line 388) ensures the setup runs exactly once.


600-604: LGTM: Lazy loading on bucket expansion.

The on_item_expanded handler triggers appearance setup only when a bucket is expanded, deferring expensive operations (icon loading, metadata parsing) until needed. This complements the _loaded guard in LocalReplayItem.


943-947: LGTM: ParsedApiResponse migration is consistent.

The switch from requestData to get_by_query_parsed (lines 943-947) and the updated process_replays_data signature (line 1197) properly adopt the new parsed-response pattern introduced across the API layer.

Also applies to: 1197-1207


1261-1286: LGTM: Tab visibility checks optimize UI updates.

The isVisible() guards (lines 1273, 1279) prevent unnecessary tree updates when tabs aren't active. The dedicated on_tab_changed handler (lines 1284-1286) ensures the local replays list is refreshed when the user switches to that tab.

src/fa/game_updater/updater.py (4)

95-96: LGTM: Progress feedback for update start.

The on_started method provides clear UI feedback when file acquisition completes and the actual update begins.


164-193: LGTM: Threading and signal wiring correctly implemented.

The refactored initialization properly:

  • Moves the worker to a separate thread (lines 184-186)
  • Connects FilesObtainer.finished to trigger both UI update and worker execution (lines 189-190)
  • Sets up an event loop to wait for thread completion (lines 192-193)

Note: The forward reference at line 164 is acceptable given Python 3.14's deferred annotation evaluation, as confirmed in previous comments.


197-212: LGTM: Asynchronous update flow is well-designed.

The new flow properly separates file acquisition from the actual update:

  1. Request phase: Shows "Requesting files from API..." and initiates async file fetching (lines 202-205)
  2. Update phase: When files arrive, files_obtainer.finished triggers worker.do_update (wired at line 190)
  3. Synchronization: Event loop (line 208) blocks until worker_thread.finished emits, ensuring clean shutdown

This design keeps the UI responsive during file fetching.


238-292: LGTM: FilesObtainer logic is correctly implemented.

The asynchronous file acquisition flow is well-designed:

  1. Setup: Determines group strategy (lines 255-258)

    • Single group for FAF variants
    • Dual group (FAF + custom mod) for other mods
  2. Request: Fetches mod metadata for each group (lines 270-272)

  3. Routing: Routes to appropriate file API based on group index (lines 274-279)

  4. Completion: Collects responses and emits when all groups processed (lines 289-292)

The check_finish pop logic (line 290) correctly handles both single-group and dual-group scenarios.

Based on learnings, the single-element unpacking at line 275 is intentional to validate exactly one mod is returned.

src/api/featured_mod_api.py (3)

17-25: LGTM: Domain model conversion is properly implemented.

The convert_parsed hook (lines 17-21) cleanly transforms raw API data into FeaturedMod instances. Storing self.reply (line 25) enables request tracking for potential cancellation.


28-36: LGTM: Separate signals for main and mod files.

The dual-signal approach (main_ready, mod_ready) allows consumers to distinguish between FAF base files and mod-specific files, which aligns with the two-list update strategy in UpdaterWorker.


38-46: LGTM: File fetching API is clean and well-designed.

The endpoint method (lines 38-40) appropriately defaults to "latest" when no version is specified. The separate get_main_files and get_mod_files methods (lines 42-46) emit to distinct signals, enabling the consumer to track completion of each file group independently.

src/api/stats_api.py (5)

32-44: LGTM: LeaderboardRatingApiConnector properly migrated to ParsedApiResponse.

The refactored handle_player_ratings (lines 39-44) correctly:

  • Constructs LeaderboardRating instances from parsed data (line 41)
  • Sorts by leaderboard order with a safe default for None (line 42)
  • Emits the expected {"values": ratings} structure (line 44)

47-60: LGTM: LeaderboardApiConnector conversion is well-implemented.

The convert_parsed method (lines 51-60) properly constructs and sorts Leaderboard instances. Using methodcaller("order") (line 58) is a clean approach for the sorting key.


78-100: LGTM: Reply tracking enables proper cancellation.

The addition of self.replies (line 78) and the abort method (lines 94-100) enable proper cleanup of in-flight requests. The RuntimeError exception handling (lines 96-98) gracefully handles already-deleted reply objects.


103-147: LGTM: LeagueSeasonScoreApiConnector properly migrated.

The migration to get_by_query_parsed (lines 134, 147) and the updated handle_season_scores signature (line 136) are consistent with the broader API refactor. The cast at line 147 is appropriate given the string-joined include parameter.


150-193: LGTM: Remaining connectors consistently migrated to ParsedApiResponse.

The updates to PlayerEventApiAccessor (lines 156-161), PlayerAchievementApiAccessor (lines 173-179), and AchievementsApiAccessor (lines 189-193) all follow the established pattern. The use of an Iterator in AchievementsApiAccessor.convert_parsed (line 192) is a good choice for memory efficiency with potentially large datasets.

Copy link

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 1

♻️ Duplicate comments (1)
src/chat/channel_view.py (1)

329-331: Critical: XSS vulnerability - mention parameter must be escaped.

The me parameter (username/mention) is inserted into HTML without escaping. This creates two problems:

  1. Search mismatch: If me contains HTML special characters (e.g., User<tag>), it won't match in the already-escaped text where it appears as User&lt;tag&gt;.
  2. XSS injection: The unescaped me is inserted directly into the HTML span, allowing script injection if usernames contain malicious HTML.
🔎 Proposed fix to escape the mention parameter
 def _wrap_me(self, text: str, me: str) -> str:
-    return text.replace(me, f"<span class=\"my_mention\">{me}</span>")
+    escaped_me = irc_escape(me)
+    return text.replace(escaped_me, f"<span class=\"my_mention\">{escaped_me}</span>")

This ensures:

  1. The escaped mention is searched in the already-escaped text
  2. The escaped mention is safely inserted into HTML
📜 Review details

Configuration used: defaults

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between a8bd614 and f7b7cdb.

📒 Files selected for processing (2)
  • src/api/sim_mod_updater.py
  • src/chat/channel_view.py
🧰 Additional context used
🧠 Learnings (1)
📚 Learning: 2025-12-27T22:23:44.249Z
Learnt from: Gatsik
Repo: FAForever/client PR: 1148
File: src/fa/game_updater/updater.py:274-279
Timestamp: 2025-12-27T22:23:44.249Z
Learning: In src/fa/game_updater/updater.py, the single-element unpacking `mod, = data["values"]` in the `process_fmod` method is intentional and serves as validation that exactly one featured mod is returned. Multiple or zero values should raise a ValueError to signal an API error.

Applied to files:

  • src/api/sim_mod_updater.py
🧬 Code graph analysis (1)
src/api/sim_mod_updater.py (2)
src/api/ApiAccessors.py (2)
  • DataApiAccessor (32-199)
  • get_by_query_parsed (43-53)
src/api/ApiBase.py (1)
  • ParsedApiResponse (38-40)
🔇 Additional comments (5)
src/chat/channel_view.py (1)

347-351: Verify the integration after fixing the XSS issue.

The flow correctly escapes text at line 343 before passing it to _wrap_me at line 351, and the conditional check for mention is appropriate. However, ensure that the mention parameter is escaped within _wrap_me (as flagged in the separate review comment) for this integration to be secure.

src/api/sim_mod_updater.py (4)

3-7: LGTM! Imports support the new asynchronous flow.

The added imports for QEventLoop, pyqtSignal, and ParsedApiResponse correctly support the refactored async coordination pattern using signals and event loops.


13-13: LGTM! Signal declaration is correct.

The ready signal with no arguments is properly declared and used to coordinate asynchronous completion of the mod URL fetch.


18-19: LGTM! Event loop synchronization pattern is correctly implemented.

The initialization of wait_loop and connection to the ready signal correctly implements the standard PyQt pattern for coordinating asynchronous operations with blocking waits.


27-28: Error handler correctly implemented; blocking pattern acknowledged.

The callback signatures are correct:

  • Success handler matches Callable[[ParsedApiResponse], None]
  • Error handler lambda correctly accepts QNetworkReply and emits ready

The blocking pattern using wait_loop.exec() remains as noted in previous reviews, with the maintainer acknowledging this as a future improvement.

Copy link

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 3

Caution

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (1)
src/api/stats_api.py (1)

120-143: Type hint inconsistency between handlers.

handle_score uses dict type hint (line 120) while handle_season_scores uses ParsedApiResponse (line 132). Since both are callbacks for get_by_query_parsed, they should have consistent type annotations for clarity.

🔎 Suggested fix
-    def handle_score(self, message: dict) -> None:
+    def handle_score(self, message: ParsedApiResponse) -> None:
         if message["data"]:
             self.score_ready.emit(LeagueSeasonScore(**message["data"][0]))
📜 Review details

Configuration used: defaults

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between f7b7cdb and 57e9045.

📒 Files selected for processing (2)
  • src/api/sim_mod_updater.py
  • src/api/stats_api.py
🧰 Additional context used
🧠 Learnings (2)
📚 Learning: 2025-12-27T22:23:44.249Z
Learnt from: Gatsik
Repo: FAForever/client PR: 1148
File: src/fa/game_updater/updater.py:274-279
Timestamp: 2025-12-27T22:23:44.249Z
Learning: In src/fa/game_updater/updater.py, the single-element unpacking `mod, = data["values"]` in the `process_fmod` method is intentional and serves as validation that exactly one featured mod is returned. Multiple or zero values should raise a ValueError to signal an API error.

Applied to files:

  • src/api/sim_mod_updater.py
📚 Learning: 2025-12-28T02:07:52.863Z
Learnt from: Gatsik
Repo: FAForever/client PR: 1148
File: src/api/ApiAccessors.py:81-88
Timestamp: 2025-12-28T02:07:52.863Z
Learning: In src/api/ApiAccessors.py, DataApiAccessor.convert_parsed intentionally raises NotImplementedError as a template method pattern - subclasses using get_by_query_converted, get_by_endpoint_converted, or requestData must implement convert_parsed.

Applied to files:

  • src/api/stats_api.py
🧬 Code graph analysis (1)
src/api/sim_mod_updater.py (2)
src/api/ApiAccessors.py (2)
  • DataApiAccessor (32-199)
  • get_by_query_parsed (43-53)
src/api/ApiBase.py (1)
  • ParsedApiResponse (38-40)
🔇 Additional comments (9)
src/api/sim_mod_updater.py (4)

3-4: LGTM! Appropriate imports for PyQt6 signal-based async handling.

The imports support the new event loop and signal pattern introduced in this file.


7-7: LGTM! Proper type import and signal declaration.

The ParsedApiResponse import enables correct type annotations, and the ready signal follows standard PyQt6 class-level signal patterns.

Also applies to: 13-13


18-19: Blocking pattern acknowledged by the team.

The event loop setup enables synchronous-style waiting. While this will block the calling thread, the developer has acknowledged this trade-off for the current implementation.


31-32: Error handler signature issue resolved.

The error handler now correctly accepts the QNetworkReply parameter via lambda and emits the ready signal to prevent hanging. The blocking pattern on line 32 has been acknowledged by the team as an intentional trade-off.

src/api/stats_api.py (5)

1-22: LGTM!

The import changes correctly align with the migration to ParsedApiResponse-based flows and the addition of the LeaderboardRating model.


31-43: LGTM!

The migration to get_by_query_parsed is clean, and the handler correctly constructs LeaderboardRating objects with proper defensive handling for None leaderboard cases in the sort key.


50-59: LGTM!

The convert_parsed implementation follows the template method pattern correctly. The use of methodcaller("order") is clean and efficient.


77-96: Good addition for in-flight request management.

The abort mechanism correctly handles Qt's "wrapped C++ object deleted" scenario with the RuntimeError catch. This is a common and appropriate pattern for Qt objects.

One minor observation: the broad RuntimeError catch could theoretically mask unrelated runtime errors, but in this specific context (calling abort() on a QNetworkReply), it's unlikely to cause issues.


185-189: LGTM!

The convert_parsed implementation correctly follows the template method pattern. Using a generator expression for lazy evaluation is a good choice for performance, and the return type Iterator[Achievement] is properly annotated.

Copy link

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 0

🧹 Nitpick comments (1)
src/playercard/ratingtabwidget.py (1)

118-120: Consider clarifying the conditional update logic.

The method only updates _default_pages when _current_page == 0, meaning spin box changes are ignored once loading starts. While this prevents mid-load state inconsistencies, a brief comment explaining this constraint would improve maintainability.

🔎 Suggested clarification
 def on_default_pages_changed(self, new_default: int) -> None:
+    # Only update default pages before any loading starts to avoid mid-cycle inconsistency
     if self._current_page == 0:
         self._default_pages = new_default
📜 Review details

Configuration used: defaults

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between 57e9045 and 6e9846f.

📒 Files selected for processing (1)
  • src/playercard/ratingtabwidget.py
🧰 Additional context used
🧬 Code graph analysis (1)
src/playercard/ratingtabwidget.py (3)
src/api/models/LeaderboardRating.py (1)
  • LeaderboardRating (7-14)
src/playercard/plot.py (1)
  • PlotController (163-207)
src/api/models/Leaderboard.py (1)
  • pretty_name (14-15)
🔇 Additional comments (6)
src/playercard/ratingtabwidget.py (6)

9-9: LGTM: Import additions support pagination UI.

The QSpinBox import is necessary for the new pagination control, and the pg import from src.heavy_modules suggests a lazy loading optimization pattern to defer heavy module imports until needed.

Also applies to: 15-15


84-84: LGTM: QSpinBox integration follows PyQt6 patterns correctly.

The constructor properly accepts the default_pages_box parameter, stores it, wires its valueChanged signal, and initializes internal state from the box value.

Also applies to: 104-106


130-135: LGTM: Pagination logic correctly manages state transitions.

The method properly checks boundaries, manipulates _default_pages to enable loading exactly one more page, and integrates correctly with the existing _loaded property and load_ratings() guard conditions.


197-197: LGTM: Controller wiring extends pagination UI correctly.

The constructor properly accepts the new button and spin box parameters, wires the button click signal, and stores the spin box reference for propagation to child tabs.

Also applies to: 199-199, 204-204, 206-206


213-219: LGTM: Spin box correctly shared across all rating tabs.

The setup method properly passes default_pages_box to each RatingsPlotTab, allowing a single control to govern pagination defaults across all leaderboard tabs.


226-228: LGTM: Clean delegation to active tab.

The method correctly delegates to the currently active tab's load_next_rating_page(), mirroring the existing load_more_ratings() pattern.

Copy link

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 1

🧹 Nitpick comments (1)
src/playercard/plot.py (1)

63-63: Type hint pattern is verbose but consistent with proxy approach.

The np.typing.NDArray[np.float64] pattern is more verbose than the standard npt.NDArray[np.float64] (with import numpy.typing as npt). However, since this aligns with the lazy-loading strategy via heavy_modules proxies, the verbosity is acceptable. Just ensure your type checker handles attribute access through the proxy correctly.

Also applies to: 138-139, 141-141, 144-144

📜 Review details

Configuration used: defaults

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between 6e9846f and 1d32cc5.

📒 Files selected for processing (9)
  • requirements.txt
  • res/chat/channel.css
  • res/client/client.css
  • src/api/sim_mod_updater.py
  • src/api/stats_api.py
  • src/chat/channel_view.py
  • src/model/chat/chatline.py
  • src/playercard/plot.py
  • src/playercard/ratingtabwidget.py
💤 Files with no reviewable changes (1)
  • res/client/client.css
🚧 Files skipped from review as they are similar to previous changes (3)
  • src/chat/channel_view.py
  • res/chat/channel.css
  • requirements.txt
🧰 Additional context used
🧠 Learnings (3)
📚 Learning: 2025-12-28T16:29:07.806Z
Learnt from: Gatsik
Repo: FAForever/client PR: 1148
File: src/api/sim_mod_updater.py:21-27
Timestamp: 2025-12-28T16:29:07.806Z
Learning: In src/api/sim_mod_updater.py, the assertion `assert isinstance(message["data"], list)` in the `get_url_from_message` method is intentional. If the API returns a dict instead of a list, the application should crash to signal an API contract violation rather than handling it gracefully.

Applied to files:

  • src/api/sim_mod_updater.py
📚 Learning: 2025-12-27T22:23:44.249Z
Learnt from: Gatsik
Repo: FAForever/client PR: 1148
File: src/fa/game_updater/updater.py:274-279
Timestamp: 2025-12-27T22:23:44.249Z
Learning: In src/fa/game_updater/updater.py, the single-element unpacking `mod, = data["values"]` in the `process_fmod` method is intentional and serves as validation that exactly one featured mod is returned. Multiple or zero values should raise a ValueError to signal an API error.

Applied to files:

  • src/api/sim_mod_updater.py
📚 Learning: 2025-12-28T02:07:52.863Z
Learnt from: Gatsik
Repo: FAForever/client PR: 1148
File: src/api/ApiAccessors.py:81-88
Timestamp: 2025-12-28T02:07:52.863Z
Learning: In src/api/ApiAccessors.py, DataApiAccessor.convert_parsed intentionally raises NotImplementedError as a template method pattern - subclasses using get_by_query_converted, get_by_endpoint_converted, or requestData must implement convert_parsed.

Applied to files:

  • src/api/stats_api.py
🧬 Code graph analysis (4)
src/api/sim_mod_updater.py (2)
src/api/ApiAccessors.py (2)
  • DataApiAccessor (32-199)
  • get_by_query_parsed (43-53)
src/api/ApiBase.py (1)
  • ParsedApiResponse (38-40)
src/model/chat/chatline.py (1)
src/util/magic_dict.py (1)
  • MagicDict (1-20)
src/playercard/ratingtabwidget.py (2)
src/api/models/LeaderboardRating.py (1)
  • LeaderboardRating (7-14)
src/playercard/playerinfodialog.py (1)
  • on_tab_changed (106-118)
src/api/stats_api.py (4)
src/api/ApiBase.py (2)
  • ParsedApiResponse (38-40)
  • get_by_query (92-99)
src/api/models/Leaderboard.py (2)
  • Leaderboard (8-37)
  • order (27-31)
src/api/models/LeaderboardRating.py (1)
  • LeaderboardRating (7-14)
src/api/ApiAccessors.py (2)
  • get_by_query_parsed (43-53)
  • DataApiAccessor (32-199)
🔇 Additional comments (22)
src/model/chat/chatline.py (2)

34-37: LGTM!

The type hint correction from ChatLineMetadata to MagicDict accurately reflects the actual parameter type, as seen in get_meta() where a MagicDict instance is passed at line 66.


92-98: LGTM!

The meta.mentions_me() call correctly retrieves the wrapped boolean through MagicDict.__call__(), and my_mention is safely assigned since self._me.login is guaranteed non-None when mentions_me evaluates to True.

src/api/stats_api.py (8)

3-3: LGTM!

The import additions align with the API refactoring. methodcaller is used for clean sorting in convert_parsed, ParsedApiResponse replaces the previous type, and LeaderboardRating is now instantiated in handle_player_ratings.

Also applies to: 13-13, 17-17


36-43: LGTM!

The migration to get_by_query_parsed is correct, and the handler properly constructs LeaderboardRating objects with safe null-checking before accessing leaderboard.order().


50-59: LGTM!

The convert_parsed implementation follows the established template pattern and cleanly sorts leaderboards using methodcaller("order"), which aligns with the Leaderboard model's ordering logic.


77-77: LGTM!

The addition of request tracking via self.replies and the new abort() method provides proper request lifecycle management. The RuntimeError handling gracefully deals with already-finished replies.

Also applies to: 85-85, 90-96


130-136: LGTM!

The migration to get_by_query_parsed is consistent, and handle_season_scores correctly uses ParsedApiResponse with a runtime assertion to narrow the type of data from dict | list to list.

Also applies to: 143-143


157-160: LGTM!

The migration to get_by_query_parsed is correct and consistent with the broader API refactoring.


175-178: LGTM!

The migration to get_by_query_parsed aligns with the API refactoring and correctly handles the parsed response.


185-189: LGTM!

The convert_parsed implementation correctly follows the template pattern and efficiently uses a generator expression for lazy iteration over achievements.

src/playercard/plot.py (1)

167-167: DateAxisItem usage is now correct.

The change to pg.DateAxisItem() properly uses the public API as recommended in the previous review. This is clean and appropriate.

src/api/sim_mod_updater.py (3)

3-7: LGTM!

The imports are appropriate for the PyQt6-based asynchronous flow with parsed API responses.


13-13: LGTM!

The signal declaration and wait_loop setup are correctly implemented for the blocking pattern. The blocking event loop approach was already discussed in previous reviews and acknowledged as acceptable for now.

Also applies to: 18-19


31-32: LGTM!

The error handler lambda correctly accepts the QNetworkReply parameter and emits the ready signal with no arguments, matching the signal signature. The blocking wait_loop.exec() pattern was previously discussed and acknowledged as acceptable for now.

src/playercard/ratingtabwidget.py (8)

9-9: LGTM!

The QSpinBox import is correctly added to support the new pagination control.


84-84: Consider validating the minimum value for default_pages_box.

The wiring logic correctly stores the spinbox reference, connects the signal, and initializes _default_pages. However, if default_pages_box.value() is 0 or negative:

  • _loaded would immediately be True (since _current_page == 0 and 0 >= 0)
  • Line 143 in load_ratings() would prevent any data from loading
  • The tab would remain empty

Ensure that the QSpinBox has a minimum value of at least 1, or add validation to handle this edge case.

Also applies to: 104-106


118-120: LGTM!

The condition if self._current_page == 0 correctly ensures that the default page target is only updated before loading begins. This prevents confusing behavior where the target changes mid-flight while pages are already loading. Once a tab starts loading, its target page count is "locked in."


130-135: LGTM! Clever pagination logic.

The method correctly implements incremental page loading:

  1. Returns early if all pages are loaded (Line 131)
  2. If the default pages are reached, increments _default_pages by 1 (Line 134)
  3. Calls load_ratings() which will now proceed because _loaded becomes False after the increment

This provides a "load one more page" behavior, complementing load_more_ratings() which loads all remaining pages at once.


197-197: LGTM!

The controller correctly accepts and wires the new pagination controls:

  • load_next_button is connected to the new load_next_rating_page() method
  • default_pages_box is stored for passing to each RatingsPlotTab instance

The wiring pattern is consistent with the existing load_more_button handling.

Also applies to: 199-199, 204-204, 206-206


213-219: LGTM!

The RatingsPlotTab instantiation correctly includes the default_pages_box parameter. All tabs share the same spinbox control, which provides a consistent UX where one control affects pagination behavior across all rating leaderboards.


226-228: LGTM!

The method correctly delegates the "load next page" action to the currently active tab, following the same pattern as load_more_ratings().


15-15: The lazy-loading import is correctly implemented.

The from src.heavy_modules import pg import at line 15 properly leverages the lazy-loading mechanism in src/heavy_modules.py. The module uses Python's importlib.util.LazyLoader to defer the actual import of pyqtgraph until first use, and the pattern is consistently applied across the codebase.

@Gatsik Gatsik merged commit 5e9c247 into FAForever:develop Dec 29, 2025
3 checks passed
@Gatsik Gatsik deleted the perf branch January 3, 2026 14:54
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.

1 participant