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:
"""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)
m = re.search(r"game_([A-Za-z0-9]+)\.log$", base)
game_id = m.group(1) if m else os.path.splitext(base)[0]
result = _parse_and_extract_blunders(log_path, base)
if isinstance(result, int):
return result # Error code
cases, game_id = result
# Always append to the unified test file
unified = os.path.join(
os.path.dirname(__file__), "..", "tests", "test_blunders_all.py"
@ -381,6 +346,73 @@ def _process_single_log(log_path: str) -> int:
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:
"""Process log files and generate blunder test cases."""
script_dir = os.path.dirname(__file__)

View File

@ -1,6 +1,7 @@
"""Generate random colorful JPEG images with configurable parameters."""
import argparse
from dataclasses import dataclass
from datetime import datetime, timezone
import logging
import os
@ -13,24 +14,22 @@ logging.basicConfig(level=logging.INFO)
MAX_IMAGE_SIZE = 1000
def generate_bloated_jpeg(
size: int,
color_list: list[str],
block_size: int,
output_path: str,
quality: int,
image_index: int,
folder: str,
) -> str:
"""Generates a random JPEG image with given size, list of colors, and block size.
@dataclass
class ImageConfig:
"""Configuration for generating a bloated JPEG image."""
size: int
color_list: list[str]
block_size: int
output_path: str
quality: int
def generate_bloated_jpeg(config: ImageConfig, image_index: int, folder: str) -> str:
"""Generates a random JPEG image with given configuration.
Args:
size: Size of the image (both width and height,
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).
config: Image generation configuration.
image_index: Index of the image for unique naming.
folder: Folder to save the image.
@ -38,28 +37,29 @@ def generate_bloated_jpeg(
Path to the generated image.
"""
# 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 = (
f"Size must be {MAX_IMAGE_SIZE} pixels or less and divisible by block_size"
)
raise ValueError(msg)
# Create a new image
image = Image.new("RGB", (size, size))
image = Image.new("RGB", (config.size, config.size))
pixels = image.load()
# Convert hex colors to RGB
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
# of random colors from the list
for y in range(0, size, block_size):
for x in range(0, size, block_size):
for y in range(0, config.size, config.block_size):
for x in range(0, config.size, config.block_size):
color = random.choice(rgb_colors)
for i in range(block_size):
for j in range(block_size):
for i in range(config.block_size):
for j in range(config.block_size):
pixels[x + i, y + j] = color
# Create the folder if it does not exist
@ -69,11 +69,12 @@ def generate_bloated_jpeg(
# Generate unique output path
unique_output_path = os.path.join(
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
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
@ -149,14 +150,13 @@ if __name__ == "__main__":
logging.info(f" Output folder: {folder}")
# 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):
output_path = generate_bloated_jpeg(
args.size,
args.colors,
args.block_size,
args.output_path,
args.quality,
i,
folder,
)
output_path = generate_bloated_jpeg(config, i, folder)
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
# 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:
"""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:
return "Unknown"
if cp_loss <= CP_LOSS_BEST:
return "Best"
if cp_loss <= CP_LOSS_EXCELLENT:
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"
for threshold, classification in _CP_LOSS_BANDS:
if cp_loss <= threshold:
return classification
return "Blunder"

View File

@ -521,24 +521,34 @@ class PokerModifierApp:
def setup_gui(self) -> None:
"""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.title("🃏 Texas Hold'em Modifier")
self.root.geometry("650x750")
self.root.configure(bg="#0f4c3a")
self.root.resizable(True, True)
# Configure style
style = ttk.Style()
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.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(
main_frame,
parent,
text="🃏 Texas Hold'em Modifier",
font=("Arial", 24, "bold"),
fg="#ffd700",
@ -546,9 +556,13 @@ class PokerModifierApp:
)
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(
main_frame,
parent,
text="Settings",
font=("Arial", 12, "bold"),
fg="#ffd700",
@ -558,8 +572,13 @@ class PokerModifierApp:
)
settings_frame.pack(fill=tk.X, pady=(0, 20), padx=10, ipady=10)
# Probability setting
prob_frame = tk.Frame(settings_frame, bg="#1a6b4d")
self._create_probability_controls(settings_frame)
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)
tk.Label(
@ -596,11 +615,11 @@ class PokerModifierApp:
)
self.prob_label.pack(side=tk.RIGHT)
# Debug controls frame
debug_frame = tk.Frame(settings_frame, bg="#1a6b4d")
def _create_debug_controls(self, parent: tk.Widget) -> None:
"""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 mode toggle
self.debug_var = tk.BooleanVar(value=False)
debug_check = tk.Checkbutton(
debug_frame,
@ -616,7 +635,6 @@ class PokerModifierApp:
)
debug_check.pack(side=tk.LEFT, padx=(0, 15))
# Force endgame button (only visible in debug mode)
self.force_endgame_button = tk.Button(
debug_frame,
text="Force Endgame",
@ -629,8 +647,9 @@ class PokerModifierApp:
)
# Initially hidden
# Game length setting
length_frame = tk.Frame(settings_frame, bg="#1a6b4d")
def _create_length_controls(self, parent: tk.Widget) -> None:
"""Create the game length slider and label."""
length_frame = tk.Frame(parent, bg="#1a6b4d")
length_frame.pack(fill=tk.X, padx=10, pady=5)
tk.Label(
@ -667,14 +686,14 @@ class PokerModifierApp:
)
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(
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_propagate(False)
# Initial result text
self.result_label = tk.Label(
self.result_frame,
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)
# Button frame for Start and Reset
button_frame = tk.Frame(main_frame, bg="#0f4c3a")
def _create_buttons(self, parent: tk.Frame) -> None:
"""Create the start and reset buttons."""
button_frame = tk.Frame(parent, bg="#0f4c3a")
button_frame.pack(fill=tk.X, pady=(0, 20), padx=10)
# Start button
self.start_button = tk.Button(
button_frame,
text="Start Round",
@ -708,7 +727,6 @@ class PokerModifierApp:
side=tk.LEFT, fill=tk.X, expand=True, ipady=10, padx=(0, 5)
)
# Reset button
self.reset_button = tk.Button(
button_frame,
text="Reset Game",
@ -724,8 +742,9 @@ class PokerModifierApp:
)
self.reset_button.pack(side=tk.RIGHT, ipady=10, padx=(5, 0))
# Statistics frame
stats_frame = tk.Frame(main_frame, bg="#0f4c3a")
def _create_statistics_frame(self, parent: tk.Frame) -> None:
"""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)
# Rounds played

View File

@ -36,10 +36,6 @@ ignore = [
# Relaxed for script-heavy repository
"PTH", # Use pathlib instead of os.path - too invasive for existing code
"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
"S603", # subprocess without shell - known pattern
"S607", # start-process with partial path - acceptable
@ -93,6 +89,12 @@ unfixable = []
]
"PYTHON/lichess_bot/main.py" = [
"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" = [
"PERF203", # Try-except needed for parsing user input in loop