fix: replace asserts with combined None checks for ruff S101

This commit is contained in:
Krzysztof kuhy Rudnicki 2025-11-30 16:03:14 +01:00
parent 97edab318f
commit 9576b20d6c
5 changed files with 168 additions and 112 deletions

View File

@ -327,47 +327,12 @@ def append_cases_to_unified_test(
def _process_single_log(log_path: str) -> int: def _process_single_log(log_path: str) -> int:
"""Process a single log file. Returns 0 on success, non-zero otherwise.""" """Process a single log file. Returns 0 on success, non-zero otherwise."""
try:
with open(log_path, encoding="utf-8") as fh:
text = fh.read()
except FileNotFoundError:
logging.exception(f"Log file not found: {log_path}")
return 2
try:
blunders = parse_columns_for_blunders(text)
except Exception:
logging.exception(f"Error parsing Columns in {os.path.basename(log_path)}")
return 2
if not blunders:
logging.warning(
f"No blunders found in Columns section: {os.path.basename(log_path)}"
)
return 1
pgn_text = extract_pgn(text)
if not pgn_text:
logging.warning(f"No PGN section found: {os.path.basename(log_path)}")
return 1
try:
cases = fen_and_uci_for_blunders(pgn_text, blunders)
except Exception:
logging.exception(
f"Error converting SAN to UCI in {os.path.basename(log_path)}"
)
return 2
if not cases:
logging.warning(
f"Failed to reconstruct any blunder positions "
f"from PGN: {os.path.basename(log_path)}"
)
return 1
base = os.path.basename(log_path) base = os.path.basename(log_path)
m = re.search(r"game_([A-Za-z0-9]+)\.log$", base) result = _parse_and_extract_blunders(log_path, base)
game_id = m.group(1) if m else os.path.splitext(base)[0] if isinstance(result, int):
return result # Error code
cases, game_id = result
# Always append to the unified test file # Always append to the unified test file
unified = os.path.join( unified = os.path.join(
os.path.dirname(__file__), "..", "tests", "test_blunders_all.py" os.path.dirname(__file__), "..", "tests", "test_blunders_all.py"
@ -381,6 +346,73 @@ def _process_single_log(log_path: str) -> int:
return 0 return 0
def _parse_and_extract_blunders(
log_path: str, base: str
) -> int | tuple[list[tuple[str, str, str, Blunder]], str]:
"""Parse log file and extract blunder cases.
Returns error code or (cases, game_id).
"""
text, err = _read_log_file(log_path)
if err is not None or text is None:
return err if err is not None else 2
blunders, err = _parse_blunders(text, base)
if err is not None or blunders is None:
return err if err is not None else 2
cases, err = _extract_cases(text, blunders, base)
if err is not None or cases is None:
return err if err is not None else 2
m = re.search(r"game_([A-Za-z0-9]+)\.log$", base)
game_id = m.group(1) if m else os.path.splitext(base)[0]
return cases, game_id
def _read_log_file(log_path: str) -> tuple[str | None, int | None]:
"""Read log file contents. Returns (text, None) or (None, error_code)."""
try:
with open(log_path, encoding="utf-8") as fh:
return fh.read(), None
except FileNotFoundError:
logging.exception(f"Log file not found: {log_path}")
return None, 2
def _parse_blunders(text: str, base: str) -> tuple[list[Blunder] | None, int | None]:
"""Parse blunders from text. Returns (blunders, None) or (None, error_code)."""
try:
blunders = parse_columns_for_blunders(text)
except Exception:
logging.exception(f"Error parsing Columns in {base}")
return None, 2
if not blunders:
logging.warning(f"No blunders found in Columns section: {base}")
return None, 1
return blunders, None
def _extract_cases(
text: str, blunders: list[Blunder], base: str
) -> tuple[list[tuple[str, str, str, Blunder]] | None, int | None]:
"""Extract FEN/UCI cases from PGN. Returns (cases, None) or (None, error_code)."""
pgn_text = extract_pgn(text)
if not pgn_text:
logging.warning(f"No PGN section found: {base}")
return None, 1
try:
cases = fen_and_uci_for_blunders(pgn_text, blunders)
except Exception:
logging.exception(f"Error converting SAN to UCI in {base}")
return None, 2
if not cases:
logging.warning(f"Failed to reconstruct any blunder positions from PGN: {base}")
return None, 1
return cases, None
def main(argv: list[str]) -> int: def main(argv: list[str]) -> int:
"""Process log files and generate blunder test cases.""" """Process log files and generate blunder test cases."""
script_dir = os.path.dirname(__file__) script_dir = os.path.dirname(__file__)

View File

@ -1,6 +1,7 @@
"""Generate random colorful JPEG images with configurable parameters.""" """Generate random colorful JPEG images with configurable parameters."""
import argparse import argparse
from dataclasses import dataclass
from datetime import datetime, timezone from datetime import datetime, timezone
import logging import logging
import os import os
@ -13,24 +14,22 @@ logging.basicConfig(level=logging.INFO)
MAX_IMAGE_SIZE = 1000 MAX_IMAGE_SIZE = 1000
def generate_bloated_jpeg( @dataclass
size: int, class ImageConfig:
color_list: list[str], """Configuration for generating a bloated JPEG image."""
block_size: int,
output_path: str, size: int
quality: int, color_list: list[str]
image_index: int, block_size: int
folder: str, output_path: str
) -> str: quality: int
"""Generates a random JPEG image with given size, list of colors, and block size.
def generate_bloated_jpeg(config: ImageConfig, image_index: int, folder: str) -> str:
"""Generates a random JPEG image with given configuration.
Args: Args:
size: Size of the image (both width and height, config: Image generation configuration.
must be divisible by block_size).
color_list: List of colors in hex format.
block_size: Size of the pixel blocks.
output_path: Output path for the JPEG image.
quality: Quality setting for the JPEG image (0-100).
image_index: Index of the image for unique naming. image_index: Index of the image for unique naming.
folder: Folder to save the image. folder: Folder to save the image.
@ -38,28 +37,29 @@ def generate_bloated_jpeg(
Path to the generated image. Path to the generated image.
""" """
# Ensure size is divisible by block_size and does not exceed MAX_IMAGE_SIZE # Ensure size is divisible by block_size and does not exceed MAX_IMAGE_SIZE
if size > MAX_IMAGE_SIZE or size % block_size != 0: if config.size > MAX_IMAGE_SIZE or config.size % config.block_size != 0:
msg = ( msg = (
f"Size must be {MAX_IMAGE_SIZE} pixels or less and divisible by block_size" f"Size must be {MAX_IMAGE_SIZE} pixels or less and divisible by block_size"
) )
raise ValueError(msg) raise ValueError(msg)
# Create a new image # Create a new image
image = Image.new("RGB", (size, size)) image = Image.new("RGB", (config.size, config.size))
pixels = image.load() pixels = image.load()
# Convert hex colors to RGB # Convert hex colors to RGB
rgb_colors = [ rgb_colors = [
tuple(int(color[i : i + 2], 16) for i in (1, 3, 5)) for color in color_list tuple(int(color[i : i + 2], 16) for i in (1, 3, 5))
for color in config.color_list
] ]
# Fill the image with block_size x block_size pixel squares # Fill the image with block_size x block_size pixel squares
# of random colors from the list # of random colors from the list
for y in range(0, size, block_size): for y in range(0, config.size, config.block_size):
for x in range(0, size, block_size): for x in range(0, config.size, config.block_size):
color = random.choice(rgb_colors) color = random.choice(rgb_colors)
for i in range(block_size): for i in range(config.block_size):
for j in range(block_size): for j in range(config.block_size):
pixels[x + i, y + j] = color pixels[x + i, y + j] = color
# Create the folder if it does not exist # Create the folder if it does not exist
@ -69,11 +69,12 @@ def generate_bloated_jpeg(
# Generate unique output path # Generate unique output path
unique_output_path = os.path.join( unique_output_path = os.path.join(
folder, folder,
f"{os.path.splitext(output_path)[0]}_{image_index}{os.path.splitext(output_path)[1]}", f"{os.path.splitext(config.output_path)[0]}_{image_index}"
f"{os.path.splitext(config.output_path)[1]}",
) )
# Save the image with specified quality to maximize file size # Save the image with specified quality to maximize file size
image.save(unique_output_path, "JPEG", quality=quality, optimize=False) image.save(unique_output_path, "JPEG", quality=config.quality, optimize=False)
return unique_output_path return unique_output_path
@ -149,14 +150,13 @@ if __name__ == "__main__":
logging.info(f" Output folder: {folder}") logging.info(f" Output folder: {folder}")
# Generate the specified number of images # Generate the specified number of images
config = ImageConfig(
size=args.size,
color_list=args.colors,
block_size=args.block_size,
output_path=args.output_path,
quality=args.quality,
)
for i in range(1, args.num_images + 1): for i in range(1, args.num_images + 1):
output_path = generate_bloated_jpeg( output_path = generate_bloated_jpeg(config, i, folder)
args.size,
args.colors,
args.block_size,
args.output_path,
args.quality,
i,
folder,
)
logging.info(f"Image {i} saved to {os.path.abspath(output_path)}") logging.info(f"Image {i} saved to {os.path.abspath(output_path)}")

View File

@ -109,6 +109,16 @@ CP_LOSS_INACCURACY = 99
CP_LOSS_MISTAKE = 299 CP_LOSS_MISTAKE = 299
# Centipawn loss thresholds for move classification
_CP_LOSS_BANDS = [
(CP_LOSS_BEST, "Best"),
(CP_LOSS_EXCELLENT, "Excellent"),
(CP_LOSS_GOOD, "Good"),
(CP_LOSS_INACCURACY, "Inaccuracy"),
(CP_LOSS_MISTAKE, "Mistake"),
]
def classify_cp_loss(cp_loss: int | None) -> str: def classify_cp_loss(cp_loss: int | None) -> str:
"""Classify move quality using Lichess-like centipawn loss bands. """Classify move quality using Lichess-like centipawn loss bands.
@ -123,16 +133,9 @@ def classify_cp_loss(cp_loss: int | None) -> str:
""" """
if cp_loss is None: if cp_loss is None:
return "Unknown" return "Unknown"
if cp_loss <= CP_LOSS_BEST: for threshold, classification in _CP_LOSS_BANDS:
return "Best" if cp_loss <= threshold:
if cp_loss <= CP_LOSS_EXCELLENT: return classification
return "Excellent"
if cp_loss <= CP_LOSS_GOOD:
return "Good"
if cp_loss <= CP_LOSS_INACCURACY:
return "Inaccuracy"
if cp_loss <= CP_LOSS_MISTAKE:
return "Mistake"
return "Blunder" return "Blunder"

View File

@ -521,24 +521,34 @@ class PokerModifierApp:
def setup_gui(self) -> None: def setup_gui(self) -> None:
"""Create and configure the main GUI window.""" """Create and configure the main GUI window."""
# Create main window self._setup_main_window()
main_frame = self._create_main_frame()
self._create_title(main_frame)
self._create_settings_frame(main_frame)
self._create_result_display(main_frame)
self._create_buttons(main_frame)
self._create_statistics_frame(main_frame)
def _setup_main_window(self) -> None:
"""Initialize the main Tk window."""
self.root = tk.Tk() self.root = tk.Tk()
self.root.title("🃏 Texas Hold'em Modifier") self.root.title("🃏 Texas Hold'em Modifier")
self.root.geometry("650x750") self.root.geometry("650x750")
self.root.configure(bg="#0f4c3a") self.root.configure(bg="#0f4c3a")
self.root.resizable(True, True) self.root.resizable(True, True)
# Configure style
style = ttk.Style() style = ttk.Style()
style.theme_use("clam") style.theme_use("clam")
# Main container def _create_main_frame(self) -> tk.Frame:
"""Create and return the main container frame."""
main_frame = tk.Frame(self.root, bg="#0f4c3a", padx=20, pady=20) main_frame = tk.Frame(self.root, bg="#0f4c3a", padx=20, pady=20)
main_frame.pack(fill=tk.BOTH, expand=True) main_frame.pack(fill=tk.BOTH, expand=True)
return main_frame
# Title def _create_title(self, parent: tk.Frame) -> None:
"""Create the title label."""
title_label = tk.Label( title_label = tk.Label(
main_frame, parent,
text="🃏 Texas Hold'em Modifier", text="🃏 Texas Hold'em Modifier",
font=("Arial", 24, "bold"), font=("Arial", 24, "bold"),
fg="#ffd700", fg="#ffd700",
@ -546,9 +556,13 @@ class PokerModifierApp:
) )
title_label.pack(pady=(0, 20)) title_label.pack(pady=(0, 20))
# Settings frame def _create_settings_frame(self, parent: tk.Frame) -> None:
"""Create the settings frame.
Includes probability, debug, and game length controls.
"""
settings_frame = tk.LabelFrame( settings_frame = tk.LabelFrame(
main_frame, parent,
text="Settings", text="Settings",
font=("Arial", 12, "bold"), font=("Arial", 12, "bold"),
fg="#ffd700", fg="#ffd700",
@ -558,8 +572,13 @@ class PokerModifierApp:
) )
settings_frame.pack(fill=tk.X, pady=(0, 20), padx=10, ipady=10) settings_frame.pack(fill=tk.X, pady=(0, 20), padx=10, ipady=10)
# Probability setting self._create_probability_controls(settings_frame)
prob_frame = tk.Frame(settings_frame, bg="#1a6b4d") self._create_debug_controls(settings_frame)
self._create_length_controls(settings_frame)
def _create_probability_controls(self, parent: tk.Widget) -> None:
"""Create the probability slider and label."""
prob_frame = tk.Frame(parent, bg="#1a6b4d")
prob_frame.pack(fill=tk.X, padx=10, pady=5) prob_frame.pack(fill=tk.X, padx=10, pady=5)
tk.Label( tk.Label(
@ -596,11 +615,11 @@ class PokerModifierApp:
) )
self.prob_label.pack(side=tk.RIGHT) self.prob_label.pack(side=tk.RIGHT)
# Debug controls frame def _create_debug_controls(self, parent: tk.Widget) -> None:
debug_frame = tk.Frame(settings_frame, bg="#1a6b4d") """Create the debug mode checkbox and force endgame button."""
debug_frame = tk.Frame(parent, bg="#1a6b4d")
debug_frame.pack(fill=tk.X, padx=10, pady=5) debug_frame.pack(fill=tk.X, padx=10, pady=5)
# Debug mode toggle
self.debug_var = tk.BooleanVar(value=False) self.debug_var = tk.BooleanVar(value=False)
debug_check = tk.Checkbutton( debug_check = tk.Checkbutton(
debug_frame, debug_frame,
@ -616,7 +635,6 @@ class PokerModifierApp:
) )
debug_check.pack(side=tk.LEFT, padx=(0, 15)) debug_check.pack(side=tk.LEFT, padx=(0, 15))
# Force endgame button (only visible in debug mode)
self.force_endgame_button = tk.Button( self.force_endgame_button = tk.Button(
debug_frame, debug_frame,
text="Force Endgame", text="Force Endgame",
@ -629,8 +647,9 @@ class PokerModifierApp:
) )
# Initially hidden # Initially hidden
# Game length setting def _create_length_controls(self, parent: tk.Widget) -> None:
length_frame = tk.Frame(settings_frame, bg="#1a6b4d") """Create the game length slider and label."""
length_frame = tk.Frame(parent, bg="#1a6b4d")
length_frame.pack(fill=tk.X, padx=10, pady=5) length_frame.pack(fill=tk.X, padx=10, pady=5)
tk.Label( tk.Label(
@ -667,14 +686,14 @@ class PokerModifierApp:
) )
self.length_label.pack(side=tk.RIGHT) self.length_label.pack(side=tk.RIGHT)
# Result display frame def _create_result_display(self, parent: tk.Frame) -> None:
"""Create the result display frame."""
self.result_frame = tk.Frame( self.result_frame = tk.Frame(
main_frame, bg="#2d2d2d", relief=tk.RIDGE, bd=3, height=150 parent, bg="#2d2d2d", relief=tk.RIDGE, bd=3, height=150
) )
self.result_frame.pack(fill=tk.BOTH, expand=True, pady=(0, 20), padx=10) self.result_frame.pack(fill=tk.BOTH, expand=True, pady=(0, 20), padx=10)
self.result_frame.pack_propagate(False) self.result_frame.pack_propagate(False)
# Initial result text
self.result_label = tk.Label( self.result_label = tk.Label(
self.result_frame, self.result_frame,
text="Click 'Start Round' to begin!", text="Click 'Start Round' to begin!",
@ -686,11 +705,11 @@ class PokerModifierApp:
) )
self.result_label.pack(expand=True, fill=tk.BOTH, padx=20, pady=20) self.result_label.pack(expand=True, fill=tk.BOTH, padx=20, pady=20)
# Button frame for Start and Reset def _create_buttons(self, parent: tk.Frame) -> None:
button_frame = tk.Frame(main_frame, bg="#0f4c3a") """Create the start and reset buttons."""
button_frame = tk.Frame(parent, bg="#0f4c3a")
button_frame.pack(fill=tk.X, pady=(0, 20), padx=10) button_frame.pack(fill=tk.X, pady=(0, 20), padx=10)
# Start button
self.start_button = tk.Button( self.start_button = tk.Button(
button_frame, button_frame,
text="Start Round", text="Start Round",
@ -708,7 +727,6 @@ class PokerModifierApp:
side=tk.LEFT, fill=tk.X, expand=True, ipady=10, padx=(0, 5) side=tk.LEFT, fill=tk.X, expand=True, ipady=10, padx=(0, 5)
) )
# Reset button
self.reset_button = tk.Button( self.reset_button = tk.Button(
button_frame, button_frame,
text="Reset Game", text="Reset Game",
@ -724,8 +742,9 @@ class PokerModifierApp:
) )
self.reset_button.pack(side=tk.RIGHT, ipady=10, padx=(5, 0)) self.reset_button.pack(side=tk.RIGHT, ipady=10, padx=(5, 0))
# Statistics frame def _create_statistics_frame(self, parent: tk.Frame) -> None:
stats_frame = tk.Frame(main_frame, bg="#0f4c3a") """Create the statistics display frame with rounds, modifiers, and phase."""
stats_frame = tk.Frame(parent, bg="#0f4c3a")
stats_frame.pack(fill=tk.X, padx=10) stats_frame.pack(fill=tk.X, padx=10)
# Rounds played # Rounds played

View File

@ -36,10 +36,6 @@ ignore = [
# Relaxed for script-heavy repository # Relaxed for script-heavy repository
"PTH", # Use pathlib instead of os.path - too invasive for existing code "PTH", # Use pathlib instead of os.path - too invasive for existing code
"C901", # Function is too complex - many scripts have complex logic "C901", # Function is too complex - many scripts have complex logic
"PLR0912", # Too many branches - relaxed for scripts
"PLR0915", # Too many statements - relaxed for scripts
"PLR0911", # Too many return statements - relaxed
"PLR0913", # Too many arguments - relaxed
"BLE001", # Blind except - will fix critical ones manually "BLE001", # Blind except - will fix critical ones manually
"S603", # subprocess without shell - known pattern "S603", # subprocess without shell - known pattern
"S607", # start-process with partial path - acceptable "S607", # start-process with partial path - acceptable
@ -93,6 +89,12 @@ unfixable = []
] ]
"PYTHON/lichess_bot/main.py" = [ "PYTHON/lichess_bot/main.py" = [
"PERF203", # Try-except needed for stream/move error handling in loops "PERF203", # Try-except needed for stream/move error handling in loops
"PLR0912", # Complex nested game event handling with many branches
"PLR0915", # Long function handling complete game lifecycle
]
"PYTHON/stockfish_analysis/analyze_chess_game.py" = [
"PLR0912", # Complex main() with many argument combinations and analysis modes
"PLR0915", # Long main() handling complete analysis workflow
] ]
"PYTHON/randomize_numbers/random_digits.py" = [ "PYTHON/randomize_numbers/random_digits.py" = [
"PERF203", # Try-except needed for parsing user input in loop "PERF203", # Try-except needed for parsing user input in loop