From 89801e1d4282a1fcbbf317c634acc3c66968434f Mon Sep 17 00:00:00 2001 From: Cal Corum Date: Wed, 4 Feb 2026 09:33:37 -0600 Subject: [PATCH] Fix circular import by moving play lock functions to separate module HOTFIX: Production bot failed to start due to circular import. Root cause: utilities/dropdown.py importing from command_logic/logic_gameplay.py while logic_gameplay.py imports from utilities/dropdown.py. Solution: Created play_lock.py as standalone module containing: - release_play_lock() - safe_play_lock() Both modules now import from play_lock.py instead of each other. Error message: ImportError: cannot import name 'release_play_lock' from partially initialized module 'command_logic.logic_gameplay' (most likely due to a circular import) Co-Authored-By: Claude Sonnet 4.5 --- cogs/gameplay.py | 3 +- command_logic/logic_gameplay.py | 68 +---------------------------- play_lock.py | 76 +++++++++++++++++++++++++++++++++ utilities/dropdown.py | 2 +- 4 files changed, 80 insertions(+), 69 deletions(-) create mode 100644 play_lock.py diff --git a/cogs/gameplay.py b/cogs/gameplay.py index d090786..8094e4b 100644 --- a/cogs/gameplay.py +++ b/cogs/gameplay.py @@ -14,7 +14,8 @@ import sqlalchemy from sqlmodel import func, or_ from api_calls import db_get -from command_logic.logic_gameplay import bunts, chaos, complete_game, defender_dropdown_view, doubles, flyballs, frame_checks, get_full_roster_from_sheets, checks_log_interaction, complete_play, get_scorebug_embed, groundballs, hit_by_pitch, homeruns, is_game_over, lineouts, manual_end_game, new_game_checks, new_game_conflicts, popouts, read_lineup, release_play_lock, relief_pitcher_dropdown_view, safe_play_lock, select_ai_reliever, show_defense_cards, singles, starting_pitcher_dropdown_view, steals, strikeouts, sub_batter_dropdown_view, substitute_player, triples, undo_play, update_game_settings, walks, xchecks, activate_last_play +from command_logic.logic_gameplay import bunts, chaos, complete_game, defender_dropdown_view, doubles, flyballs, frame_checks, get_full_roster_from_sheets, checks_log_interaction, complete_play, get_scorebug_embed, groundballs, hit_by_pitch, homeruns, is_game_over, lineouts, manual_end_game, new_game_checks, new_game_conflicts, popouts, read_lineup, relief_pitcher_dropdown_view, select_ai_reliever, show_defense_cards, singles, starting_pitcher_dropdown_view, steals, strikeouts, sub_batter_dropdown_view, substitute_player, triples, undo_play, update_game_settings, walks, xchecks, activate_last_play +from play_lock import release_play_lock, safe_play_lock from dice import ab_roll from exceptions import * import gauntlets diff --git a/command_logic/logic_gameplay.py b/command_logic/logic_gameplay.py index 0fa85ab..dd763e4 100644 --- a/command_logic/logic_gameplay.py +++ b/command_logic/logic_gameplay.py @@ -1,6 +1,5 @@ import asyncio import copy -from contextlib import contextmanager import logging import discord from discord import SelectOption @@ -13,6 +12,7 @@ from typing import Literal from api_calls import db_delete, db_get, db_post from dice import DTwentyRoll, d_twenty_roll, frame_plate_check, sa_fielding_roll from exceptions import * +from play_lock import release_play_lock, safe_play_lock from gauntlets import post_result from helpers import ( COLORS, @@ -81,58 +81,6 @@ WPA_DF = pd.read_csv(f"storage/wpa_data.csv").set_index("index") TO_BASE = {2: "to second", 3: "to third", 4: "home"} -@contextmanager -def safe_play_lock(session: Session, play: "Play"): - """ - Context manager for safely handling play locks. - - Ensures the lock is ALWAYS released even if an exception occurs during - command processing. This prevents permanent user lockouts. - - Usage: - with safe_play_lock(session, this_play): - # Do command processing - # Lock will be released automatically on exception or normal exit - pass - - Note: This only releases the lock on exception. On successful completion, - complete_play() should handle releasing the lock and marking the play complete. - """ - lock_released = False - try: - yield play - except Exception as e: - # Release lock on any exception - if play.locked and not play.complete: - logger.error( - f"Exception during play {play.id} processing, releasing lock. Error: {e}" - ) - play.locked = False - session.add(play) - try: - session.commit() - lock_released = True - except Exception as commit_error: - logger.error( - f"Failed to release lock on play {play.id}: {commit_error}" - ) - raise # Re-raise the original exception - finally: - # Final safety check - if not lock_released and play.locked and not play.complete: - logger.warning( - f"Play {play.id} still locked after processing, forcing unlock" - ) - play.locked = False - session.add(play) - try: - session.commit() - except Exception as commit_error: - logger.error( - f"Failed to force unlock play {play.id}: {commit_error}" - ) - - def safe_wpa_lookup( inning_half: str, inning_num: int, @@ -1248,20 +1196,6 @@ async def get_full_roster_from_sheets( ).all() -def release_play_lock(session: Session, play: Play) -> None: - """ - Release a play lock and commit the change. - - This is a safety mechanism to ensure locks are always released, - even when exceptions occur during command processing. - """ - if play.locked: - logger.info(f"Releasing lock on play {play.id}") - play.locked = False - session.add(play) - session.commit() - - async def checks_log_interaction( session: Session, interaction: discord.Interaction, command_name: str, lock_play: bool = True ) -> tuple[Game, Team, Play]: diff --git a/play_lock.py b/play_lock.py new file mode 100644 index 0000000..c1d0872 --- /dev/null +++ b/play_lock.py @@ -0,0 +1,76 @@ +""" +Play locking utilities to prevent concurrent play modifications. + +This module is separate to avoid circular imports between logic_gameplay and utilities. +""" +import logging +from contextlib import contextmanager +from sqlmodel import Session + +logger = logging.getLogger("discord_app") + + +def release_play_lock(session: Session, play: "Play") -> None: + """ + Release a play lock and commit the change. + + This is a safety mechanism to ensure locks are always released, + even when exceptions occur during command processing. + """ + if play.locked: + logger.info(f"Releasing lock on play {play.id}") + play.locked = False + session.add(play) + session.commit() + + +@contextmanager +def safe_play_lock(session: Session, play: "Play"): + """ + Context manager for safely handling play locks. + + Ensures the lock is ALWAYS released even if an exception occurs during + command processing. This prevents permanent user lockouts. + + Usage: + with safe_play_lock(session, this_play): + # Do command processing + # Lock will be released automatically on exception or normal exit + pass + + Note: This only releases the lock on exception. On successful completion, + complete_play() should handle releasing the lock and marking the play complete. + """ + lock_released = False + try: + yield play + except Exception as e: + # Release lock on any exception + if play.locked and not play.complete: + logger.error( + f"Exception during play {play.id} processing, releasing lock. Error: {e}" + ) + play.locked = False + session.add(play) + try: + session.commit() + lock_released = True + except Exception as commit_error: + logger.error( + f"Failed to release lock on play {play.id}: {commit_error}" + ) + raise # Re-raise the original exception + finally: + # Final safety check + if not lock_released and play.locked and not play.complete: + logger.warning( + f"Play {play.id} still locked after processing, forcing unlock" + ) + play.locked = False + session.add(play) + try: + session.commit() + except Exception as commit_error: + logger.error( + f"Failed to force unlock play {play.id}: {commit_error}" + ) diff --git a/utilities/dropdown.py b/utilities/dropdown.py index fd2f373..0dde48c 100644 --- a/utilities/dropdown.py +++ b/utilities/dropdown.py @@ -9,8 +9,8 @@ from discord.utils import MISSING from sqlmodel import Session from api_calls import db_delete, db_get, db_post -from command_logic.logic_gameplay import release_play_lock, safe_play_lock from exceptions import CardNotFoundException, LegalityCheckNotRequired, LineupsMissingException, PlayNotFoundException, PositionNotFoundException, log_exception, log_errors +from play_lock import release_play_lock, safe_play_lock from helpers import DEFENSE_NO_PITCHER_LITERAL, get_card_embeds, position_name_to_abbrev, random_insult from in_game.game_helpers import legal_check from in_game.gameplay_models import Game, Lineup, Play, Team