fix: correct mypy ignore comment for attr-defined error

This commit is contained in:
Krzysztof kuhy Rudnicki 2025-11-30 15:49:40 +01:00
parent 4e178be185
commit 97edab318f
20 changed files with 150 additions and 118 deletions

View File

@ -24,8 +24,10 @@ class _HrefParser(HTMLParser):
super().__init__() super().__init__()
self.hrefs: list[str] = [] self.hrefs: list[str] = []
def handle_starttag(self, tag: str, attrs): # type: ignore[override] def handle_starttag( # type: ignore[override]
# Collect any href attribute on any tag self, tag: str, attrs: list[tuple[str, str | None]]
) -> None:
"""Collect href attributes from start tags."""
for k, v in attrs: for k, v in attrs:
if k.lower() == "href" and v is not None: if k.lower() == "href" and v is not None:
self.hrefs.append(v) self.hrefs.append(v)

View File

@ -11,12 +11,12 @@ if str(ROOT) not in sys.path:
SCRIPT = ROOT / "main.py" SCRIPT = ROOT / "main.py"
def read_lines(p: Path): def read_lines(p: Path) -> list[str]:
"""Read lines from a file, stripping newlines.""" """Read lines from a file, stripping newlines."""
return [line.rstrip("\n") for line in p.read_text(encoding="utf-8").splitlines()] return [line.rstrip("\n") for line in p.read_text(encoding="utf-8").splitlines()]
def test_extract_hosts_function(): def test_extract_hosts_function() -> None:
"""Test extract_hosts_from_html extracts unique hosts in order.""" """Test extract_hosts_from_html extracts unique hosts in order."""
from main import extract_hosts_from_html from main import extract_hosts_from_html
@ -31,7 +31,7 @@ def test_extract_hosts_function():
assert hosts == ["wiby.me", "example.com"], hosts assert hosts == ["wiby.me", "example.com"], hosts
def test_cli_writes_expected_output(tmp_path: Path): def test_cli_writes_expected_output(tmp_path: Path) -> None:
"""Test CLI writes correctly formatted output file.""" """Test CLI writes correctly formatted output file."""
# copy sample1.html to tmpdir and run the script # copy sample1.html to tmpdir and run the script
sample = ROOT / "tests" / "sample1.html" sample = ROOT / "tests" / "sample1.html"
@ -53,7 +53,7 @@ def test_cli_writes_expected_output(tmp_path: Path):
assert lines == ["*wiby.me*", "*example.com*"], lines assert lines == ["*wiby.me*", "*example.com*"], lines
def test_cli_default_output_name(tmp_path: Path): def test_cli_default_output_name(tmp_path: Path) -> None:
"""Test CLI generates default output filename from input.""" """Test CLI generates default output filename from input."""
sample = ROOT / "tests" / "sample2.html" sample = ROOT / "tests" / "sample2.html"
html_copy = tmp_path / "sample2.html" html_copy = tmp_path / "sample2.html"

View File

@ -70,7 +70,7 @@ KEY_ADJACENCY = {
class KeyboardCoopGame: class KeyboardCoopGame:
"""Main game class for the keyboard cooperative word game.""" """Main game class for the keyboard cooperative word game."""
def __init__(self): def __init__(self) -> None:
"""Initialize the game window, fonts, and game state.""" """Initialize the game window, fonts, and game state."""
self.screen = pygame.display.set_mode((SCREEN_WIDTH, SCREEN_HEIGHT)) self.screen = pygame.display.set_mode((SCREEN_WIDTH, SCREEN_HEIGHT))
pygame.display.set_caption("Keyboard Coop Game") pygame.display.set_caption("Keyboard Coop Game")
@ -96,7 +96,7 @@ class KeyboardCoopGame:
# Key positions # Key positions
self.key_positions = self.calculate_key_positions() self.key_positions = self.calculate_key_positions()
def load_dictionary(self): def load_dictionary(self) -> set[str]:
"""Load dictionary from words_dictionary.json file.""" """Load dictionary from words_dictionary.json file."""
try: try:
dictionary_path = os.path.join( dictionary_path = os.path.join(
@ -198,7 +198,7 @@ class KeyboardCoopGame:
"good", "good",
} }
def generate_random_keyboard(self): def generate_random_keyboard(self) -> None:
"""Generate a random keyboard layout and calculate adjacencies.""" """Generate a random keyboard layout and calculate adjacencies."""
# All 26 letters # All 26 letters
all_letters = list("abcdefghijklmnopqrstuvwxyz") all_letters = list("abcdefghijklmnopqrstuvwxyz")
@ -217,7 +217,7 @@ class KeyboardCoopGame:
# Calculate adjacencies based on new layout # Calculate adjacencies based on new layout
self.calculate_adjacencies() self.calculate_adjacencies()
def calculate_adjacencies(self): def calculate_adjacencies(self) -> None:
"""Calculate adjacencies based on current keyboard layout.""" """Calculate adjacencies based on current keyboard layout."""
self.key_adjacency = {} self.key_adjacency = {}
@ -248,9 +248,9 @@ class KeyboardCoopGame:
self.key_adjacency[letter] = adjacents self.key_adjacency[letter] = adjacents
def calculate_key_positions(self): def calculate_key_positions(self) -> dict[str, pygame.Rect]:
"""Calculate the position of each key on screen.""" """Calculate the position of each key on screen."""
positions = {} positions: dict[str, pygame.Rect] = {}
key_width = 60 key_width = 60
key_height = 60 key_height = 60
key_spacing = 8 key_spacing = 8
@ -266,14 +266,14 @@ class KeyboardCoopGame:
return positions return positions
def get_key_at_position(self, pos): def get_key_at_position(self, pos: tuple[int, int]) -> str | None:
"""Get the key at the given mouse position.""" """Get the key at the given mouse position."""
for key, rect in self.key_positions.items(): for key, rect in self.key_positions.items():
if rect.collidepoint(pos): if rect.collidepoint(pos):
return key return key
return None return None
def is_valid_move(self, letter): def is_valid_move(self, letter: str) -> bool:
"""Check if the letter is a valid move.""" """Check if the letter is a valid move."""
if not self.selected_letters: if not self.selected_letters:
return True # First move can be any letter return True # First move can be any letter
@ -281,17 +281,17 @@ class KeyboardCoopGame:
last_letter = self.selected_letters[-1] last_letter = self.selected_letters[-1]
return letter in self.key_adjacency[last_letter] return letter in self.key_adjacency[last_letter]
def is_valid_word(self, word): def is_valid_word(self, word: str) -> bool:
"""Check if the word is in the dictionary.""" """Check if the word is in the dictionary."""
return word.lower() in self.dictionary return word.lower() in self.dictionary
def calculate_score(self, word_length): def calculate_score(self, word_length: int) -> int:
"""Calculate score exponentially based on word length.""" """Calculate score exponentially based on word length."""
if word_length < MIN_WORD_LENGTH: if word_length < MIN_WORD_LENGTH:
return 0 return 0
return 2 ** (word_length - 2) return 2 ** (word_length - 2)
def handle_letter_click(self, letter): def handle_letter_click(self, letter: str) -> None:
"""Handle clicking on a letter.""" """Handle clicking on a letter."""
if letter in self.available_letters and self.is_valid_move(letter): if letter in self.available_letters and self.is_valid_move(letter):
self.selected_letters.append(letter) self.selected_letters.append(letter)
@ -316,7 +316,7 @@ class KeyboardCoopGame:
if not self.available_letters: if not self.available_letters:
self.submit_word() self.submit_word()
def submit_word(self): def submit_word(self) -> None:
"""Submit the current word and check if it's valid.""" """Submit the current word and check if it's valid."""
if len(self.current_word) >= MIN_WORD_LENGTH and self.is_valid_word( if len(self.current_word) >= MIN_WORD_LENGTH and self.is_valid_word(
self.current_word self.current_word
@ -345,7 +345,7 @@ class KeyboardCoopGame:
self.selected_letters = [] self.selected_letters = []
self.current_player = 0 self.current_player = 0
def reset_game(self): def reset_game(self) -> None:
"""Reset the game to initial state.""" """Reset the game to initial state."""
self.current_player = 0 self.current_player = 0
self.current_word = "" self.current_word = ""
@ -358,7 +358,7 @@ class KeyboardCoopGame:
self.generate_random_keyboard() self.generate_random_keyboard()
self.key_positions = self.calculate_key_positions() self.key_positions = self.calculate_key_positions()
def draw_keyboard(self): def draw_keyboard(self) -> None:
"""Draw the virtual keyboard.""" """Draw the virtual keyboard."""
mouse_pos = pygame.mouse.get_pos() mouse_pos = pygame.mouse.get_pos()
@ -382,7 +382,7 @@ class KeyboardCoopGame:
text_rect = text.get_rect(center=rect.center) text_rect = text.get_rect(center=rect.center)
self.screen.blit(text, text_rect) self.screen.blit(text, text_rect)
def draw_ui(self): def draw_ui(self) -> tuple[pygame.Rect, pygame.Rect]:
"""Draw the user interface.""" """Draw the user interface."""
# Title # Title
title = self.large_font.render("Keyboard Coop Game", True, TEXT_COLOR) title = self.large_font.render("Keyboard Coop Game", True, TEXT_COLOR)
@ -444,7 +444,7 @@ class KeyboardCoopGame:
return enter_rect, reset_rect return enter_rect, reset_rect
def handle_click(self, pos): def handle_click(self, pos: tuple[int, int]) -> None:
"""Handle mouse clicks.""" """Handle mouse clicks."""
# Check if clicked on a key # Check if clicked on a key
key = self.get_key_at_position(pos) key = self.get_key_at_position(pos)
@ -460,7 +460,7 @@ class KeyboardCoopGame:
elif reset_rect.collidepoint(pos): elif reset_rect.collidepoint(pos):
self.reset_game() self.reset_game()
def run(self): def run(self) -> None:
"""Main game loop.""" """Main game loop."""
running = True running = True

View File

@ -25,7 +25,7 @@ class RandomEngine:
engine_path: str | None = None, engine_path: str | None = None,
max_time_sec: float = 2.0, max_time_sec: float = 2.0,
depth: int | None = None, depth: int | None = None,
): ) -> None:
"""Initialize the engine wrapper with path and time settings.""" """Initialize the engine wrapper with path and time settings."""
self.max_time_sec = max_time_sec self.max_time_sec = max_time_sec
# depth is accepted for compatibility with existing callers but is unused; # depth is accepted for compatibility with existing callers but is unused;

View File

@ -16,7 +16,7 @@ LICHESS_API = "https://lichess.org"
class LichessAPI: class LichessAPI:
"""Client for interacting with the Lichess Bot API.""" """Client for interacting with the Lichess Bot API."""
def __init__(self, token: str, session: requests.Session | None = None): def __init__(self, token: str, session: requests.Session | None = None) -> None:
"""Initialize the API client with authentication token.""" """Initialize the API client with authentication token."""
self.token = token self.token = token
self.session = session or requests.Session() self.session = session or requests.Session()
@ -29,7 +29,12 @@ class LichessAPI:
) )
def _request( def _request(
self, method: str, url: str, *, raise_for_status: bool = False, **kwargs self,
method: str,
url: str,
*,
raise_for_status: bool = False,
**kwargs: object,
) -> requests.Response: ) -> requests.Response:
"""Wrapper around session.request that logs every request/response. """Wrapper around session.request that logs every request/response.
@ -40,7 +45,7 @@ class LichessAPI:
t0 = time.monotonic() t0 = time.monotonic()
logging.info(f"HTTP {method} {url} -> sending") logging.info(f"HTTP {method} {url} -> sending")
try: try:
r = self.session.request(method, url, **kwargs) r = self.session.request(method, url, **kwargs) # type: ignore[arg-type]
except Exception: except Exception:
logging.exception(f"HTTP {method} {url} -> exception") logging.exception(f"HTTP {method} {url} -> exception")
raise raise

View File

@ -38,7 +38,7 @@ def run_bot(log_level: str = "INFO", decline_correspondence: bool = False) -> No
game_threads = {} game_threads = {}
def handle_game(game_id: str, my_color: str | None = None): def handle_game(game_id: str, my_color: str | None = None) -> None:
logging.info(f"Starting game thread for {game_id} [bot v{bot_version}]") logging.info(f"Starting game thread for {game_id} [bot v{bot_version}]")
board = chess.Board() board = chess.Board()
color: str | None = my_color color: str | None = my_color
@ -453,7 +453,7 @@ def run_bot(log_level: str = "INFO", decline_correspondence: bool = False) -> No
backoff = backoff_sleep(backoff) backoff = backoff_sleep(backoff)
def main(): def main() -> None:
"""Parse arguments and run the Lichess bot.""" """Parse arguments and run the Lichess bot."""
parser = argparse.ArgumentParser(description="Run a minimal Lichess bot") parser = argparse.ArgumentParser(description="Run a minimal Lichess bot")
parser.add_argument( parser.add_argument(

View File

@ -2,6 +2,8 @@ import os
from pathlib import Path from pathlib import Path
import sys import sys
import pytest
# Add repository root to sys.path so 'import PYTHON.*' works when running # Add repository root to sys.path so 'import PYTHON.*' works when running
# pytest with a subdirectory as rootdir. # pytest with a subdirectory as rootdir.
ROOT = os.path.abspath(os.path.join(os.path.dirname(__file__), "../../..")) ROOT = os.path.abspath(os.path.join(os.path.dirname(__file__), "../../.."))
@ -9,7 +11,7 @@ if ROOT not in sys.path:
sys.path.insert(0, ROOT) sys.path.insert(0, ROOT)
def pytest_ignore_collect(collection_path: Path, config): def pytest_ignore_collect(collection_path: Path, config: pytest.Config) -> bool | None:
"""Ignore per-game blunder test files; keep only the unified one. """Ignore per-game blunder test files; keep only the unified one.
This lets us keep historical files in the repo without collecting them. This lets us keep historical files in the repo without collecting them.

View File

@ -33,7 +33,7 @@ def _load_top_puzzles(csv_path: str, limit: int = 8) -> list[tuple[str, str]]:
os.path.join(os.path.dirname(__file__), "lichess_db_puzzle.csv"), limit=8 os.path.join(os.path.dirname(__file__), "lichess_db_puzzle.csv"), limit=8
), ),
) )
def test_puzzle_engine_follow_solution(fen: str, moves_str: str): def test_puzzle_engine_follow_solution(fen: str, moves_str: str) -> None:
"""Verify the engine follows puzzle solutions correctly.""" """Verify the engine follows puzzle solutions correctly."""
board = chess.Board(fen) board = chess.Board(fen)
eng = RandomEngine(max_time_sec=1.0) eng = RandomEngine(max_time_sec=1.0)

View File

@ -1,13 +1,15 @@
"""Tests for utility functions.""" """Tests for utility functions."""
import pytest
from PYTHON.lichess_bot.utils import backoff_sleep from PYTHON.lichess_bot.utils import backoff_sleep
def test_backoff_sleep_increments_and_caps(monkeypatch): def test_backoff_sleep_increments_and_caps(monkeypatch: pytest.MonkeyPatch) -> None:
"""Test that backoff sleep increments and respects the cap.""" """Test that backoff sleep increments and respects the cap."""
slept = [] slept: list[float] = []
def fake_sleep(sec): def fake_sleep(sec: float) -> None:
slept.append(sec) slept.append(sec)
monkeypatch.setattr("time.sleep", fake_sleep) monkeypatch.setattr("time.sleep", fake_sleep)

View File

@ -1,9 +1,15 @@
"""Tests for bot version management.""" """Tests for bot version management."""
from pathlib import Path
import pytest
from PYTHON.lichess_bot.utils import get_and_increment_version from PYTHON.lichess_bot.utils import get_and_increment_version
def test_version_file_increments_and_persists(tmp_path, monkeypatch): def test_version_file_increments_and_persists(
tmp_path: Path, monkeypatch: pytest.MonkeyPatch
) -> None:
"""Test that version increments and persists to file.""" """Test that version increments and persists to file."""
version_file = tmp_path / "version.txt" version_file = tmp_path / "version.txt"
monkeypatch.setenv("LICHESS_BOT_VERSION_FILE", str(version_file)) monkeypatch.setenv("LICHESS_BOT_VERSION_FILE", str(version_file))

View File

@ -14,19 +14,28 @@ MAX_IMAGE_SIZE = 1000
def generate_bloated_jpeg( def generate_bloated_jpeg(
size, color_list, block_size, output_path, quality, image_index, folder 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. """Generates a random JPEG image with given size, list of colors, and block size.
Args: Args:
size (int): Size of the image (both width and height, size: Size of the image (both width and height,
must be divisible by block_size). must be divisible by block_size).
color_list (list of str): List of colors in hex format. color_list: List of colors in hex format.
block_size (int): Size of the pixel blocks. block_size: Size of the pixel blocks.
output_path (str): Output path for the JPEG image. output_path: Output path for the JPEG image.
quality (int): Quality setting for the JPEG image (0-100). quality: Quality setting for the JPEG image (0-100).
image_index (int): Index of the image for unique naming. image_index: Index of the image for unique naming.
folder (str): Folder to save the image. folder: Folder to save the image.
Returns:
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 size > MAX_IMAGE_SIZE or size % block_size != 0:

View File

@ -13,10 +13,10 @@ DEFAULT_MAX_PERCENTAGE = 20
def randomize_numbers( def randomize_numbers(
numbers, numbers: list[float],
min_percentage=DEFAULT_MIN_PERCENTAGE, min_percentage: float = DEFAULT_MIN_PERCENTAGE,
max_percentage=DEFAULT_MAX_PERCENTAGE, max_percentage: float = DEFAULT_MAX_PERCENTAGE,
): ) -> list[float]:
"""Apply random percentage variation to a list of numbers.""" """Apply random percentage variation to a list of numbers."""
randomized_numbers = [] randomized_numbers = []
for number in numbers: for number in numbers:
@ -29,7 +29,7 @@ def randomize_numbers(
return randomized_numbers return randomized_numbers
def parse_input(input_string): def parse_input(input_string: str) -> tuple[list[float], list[int]]:
"""Parse a string of numbers and return floats with decimal counts.""" """Parse a string of numbers and return floats with decimal counts."""
# Replace commas with dots and remove non-numeric characters # Replace commas with dots and remove non-numeric characters
# except dots, commas, and digits # except dots, commas, and digits
@ -37,8 +37,8 @@ def parse_input(input_string):
# Split the cleaned input into individual numbers # Split the cleaned input into individual numbers
number_strings = cleaned_input.split() number_strings = cleaned_input.split()
# Convert the number strings to floats # Convert the number strings to floats
numbers = [] numbers: list[float] = []
decimal_counts = [] decimal_counts: list[int] = []
for num in number_strings: for num in number_strings:
try: try:
float_num = float(num) float_num = float(num)

View File

@ -30,7 +30,7 @@ driver.get(url)
# A function to download images by URL # A function to download images by URL
def download_image(url): def download_image(url: str) -> bool:
"""Download an image from a URL and save it locally.""" """Download an image from a URL and save it locally."""
# Extract image name from URL # Extract image name from URL
image_name = os.path.basename(urlparse(url).path) image_name = os.path.basename(urlparse(url).path)

View File

@ -26,7 +26,7 @@ MAX_WEIGHT_KG = 500
class ScreenLocker: class ScreenLocker:
"""Screen locker that requires workout logging to unlock.""" """Screen locker that requires workout logging to unlock."""
def __init__(self, demo_mode=True): def __init__(self, demo_mode: bool = True) -> None:
"""Initialize screen locker with optional demo mode.""" """Initialize screen locker with optional demo mode."""
# Set up log file path # Set up log file path
script_dir = os.path.dirname(os.path.abspath(__file__)) script_dir = os.path.dirname(os.path.abspath(__file__))
@ -86,12 +86,12 @@ class ScreenLocker:
self.root.focus_force() self.root.focus_force()
self.root.grab_set_global() self.root.grab_set_global()
def clear_container(self): def clear_container(self) -> None:
"""Remove all widgets from the main container.""" """Remove all widgets from the main container."""
for widget in self.container.winfo_children(): for widget in self.container.winfo_children():
widget.destroy() widget.destroy()
def ask_workout_done(self): def ask_workout_done(self) -> None:
"""Display the initial workout question dialog.""" """Display the initial workout question dialog."""
self.clear_container() self.clear_container()
@ -131,7 +131,7 @@ class ScreenLocker:
) )
no_btn.pack(side="left", padx=20) no_btn.pack(side="left", padx=20)
def lockout(self): def lockout(self) -> None:
"""Display lockout screen with countdown timer.""" """Display lockout screen with countdown timer."""
self.clear_container() self.clear_container()
@ -156,7 +156,7 @@ class ScreenLocker:
self.remaining_time = self.lockout_time self.remaining_time = self.lockout_time
self.update_lockout_countdown() self.update_lockout_countdown()
def update_lockout_countdown(self): def update_lockout_countdown(self) -> None:
"""Update the lockout countdown timer display.""" """Update the lockout countdown timer display."""
if self.remaining_time > 0: if self.remaining_time > 0:
self.countdown_label.config(text=str(self.remaining_time)) self.countdown_label.config(text=str(self.remaining_time))
@ -165,7 +165,7 @@ class ScreenLocker:
else: else:
self.ask_workout_done() self.ask_workout_done()
def ask_workout_type(self): def ask_workout_type(self) -> None:
"""Display workout type selection dialog.""" """Display workout type selection dialog."""
self.clear_container() self.clear_container()
@ -205,7 +205,7 @@ class ScreenLocker:
) )
strength_btn.pack(side="left", padx=20) strength_btn.pack(side="left", padx=20)
def ask_running_details(self): def ask_running_details(self) -> None:
"""Display running workout input form.""" """Display running workout input form."""
self.clear_container() self.clear_container()
self.workout_data["type"] = "running" self.workout_data["type"] = "running"
@ -295,7 +295,7 @@ class ScreenLocker:
self.submit_command = self.verify_running_data self.submit_command = self.verify_running_data
self.update_submit_timer() self.update_submit_timer()
def verify_running_data(self): def verify_running_data(self) -> None:
"""Validate running workout data and unlock if valid.""" """Validate running workout data and unlock if valid."""
try: try:
distance = float(self.distance_entry.get()) distance = float(self.distance_entry.get())
@ -337,7 +337,7 @@ class ScreenLocker:
except ValueError: except ValueError:
self.show_error("Please enter valid numbers") self.show_error("Please enter valid numbers")
def ask_strength_details(self): def ask_strength_details(self) -> None:
"""Display strength training input form.""" """Display strength training input form."""
self.clear_container() self.clear_container()
self.workout_data["type"] = "strength" self.workout_data["type"] = "strength"
@ -459,7 +459,7 @@ class ScreenLocker:
self.submit_command = self.verify_strength_data self.submit_command = self.verify_strength_data
self.update_submit_timer() self.update_submit_timer()
def verify_strength_data(self): def verify_strength_data(self) -> None:
"""Validate strength workout data and unlock if valid.""" """Validate strength workout data and unlock if valid."""
try: try:
exercises = [e.strip() for e in self.exercises_entry.get().split(",")] exercises = [e.strip() for e in self.exercises_entry.get().split(",")]
@ -513,7 +513,7 @@ class ScreenLocker:
except ValueError: except ValueError:
self.show_error("Please enter valid data in correct format") self.show_error("Please enter valid data in correct format")
def update_submit_timer(self): def update_submit_timer(self) -> None:
"""Update countdown timer and check if submit can be enabled.""" """Update countdown timer and check if submit can be enabled."""
# Check if widgets still exist (user might have clicked back) # Check if widgets still exist (user might have clicked back)
try: try:
@ -544,7 +544,7 @@ class ScreenLocker:
# Widgets were destroyed (user clicked back), stop the timer # Widgets were destroyed (user clicked back), stop the timer
pass pass
def check_entries_filled(self): def check_entries_filled(self) -> None:
"""Continuously check if entries are filled after timer expires.""" """Continuously check if entries are filled after timer expires."""
try: try:
all_filled = all(entry.get().strip() for entry in self.entries_to_check) all_filled = all(entry.get().strip() for entry in self.entries_to_check)
@ -564,7 +564,7 @@ class ScreenLocker:
# Widgets were destroyed (user clicked back), stop checking # Widgets were destroyed (user clicked back), stop checking
pass pass
def show_error(self, message): def show_error(self, message: str) -> None:
"""Display error message with retry option.""" """Display error message with retry option."""
self.clear_container() self.clear_container()
@ -599,7 +599,7 @@ class ScreenLocker:
) )
retry_btn.pack(pady=30) retry_btn.pack(pady=30)
def unlock_screen(self): def unlock_screen(self) -> None:
"""Save workout log and display success message.""" """Save workout log and display success message."""
# Save workout data to log # Save workout data to log
self.save_workout_log() self.save_workout_log()
@ -626,7 +626,7 @@ class ScreenLocker:
self.root.after(1500, self.close) self.root.after(1500, self.close)
def has_logged_today(self): def has_logged_today(self) -> bool:
"""Check if workout has been logged today.""" """Check if workout has been logged today."""
if not os.path.exists(self.log_file): if not os.path.exists(self.log_file):
return False return False
@ -640,7 +640,7 @@ class ScreenLocker:
except (OSError, json.JSONDecodeError): except (OSError, json.JSONDecodeError):
return False return False
def save_workout_log(self): def save_workout_log(self) -> None:
"""Save workout data to log file.""" """Save workout data to log file."""
# Load existing logs # Load existing logs
logs = {} logs = {}
@ -665,12 +665,12 @@ class ScreenLocker:
except OSError as e: except OSError as e:
logging.warning(f"Could not save workout log: {e}") logging.warning(f"Could not save workout log: {e}")
def close(self): def close(self) -> None:
"""Close the application and exit.""" """Close the application and exit."""
self.root.destroy() self.root.destroy()
sys.exit(0) sys.exit(0)
def run(self): def run(self) -> None:
"""Start the Tkinter main event loop.""" """Start the Tkinter main event loop."""
self.root.mainloop() self.root.mainloop()

View File

@ -1,17 +1,22 @@
"""Distribute values symmetrically across N parts.""" """Distribute values symmetrically across N parts."""
def calculate_symmetric_weights(N, middle_weight, factors=None): def calculate_symmetric_weights(
n: int, middle_weight: float, factors: list[float] | None = None
) -> list[float]:
"""Calculate symmetric weights for both even and odd N. """Calculate symmetric weights for both even and odd N.
N: Number in which to split. Args:
middle_weight: The middle value for symmetry. n: Number of parts to split into.
factors: If provided, controls the difference in weights (used for the middle_weight: The middle value for symmetry.
`split_x_into_n_symmetrically` function). factors: If provided, controls the difference in weights.
Must have length N // 2 or N // 2 - 1 depending on N. Must have length n // 2 or n // 2 - 1 depending on n.
Returns:
List of symmetric weights.
""" """
half_n = N // 2 half_n = n // 2
weights_left = [middle_weight] weights_left: list[float] = [middle_weight]
if factors: if factors:
for factor in factors: for factor in factors:
@ -21,7 +26,7 @@ def calculate_symmetric_weights(N, middle_weight, factors=None):
for i in range(half_n - 1): for i in range(half_n - 1):
weights_left.append(middle_weight - (i + 1)) weights_left.append(middle_weight - (i + 1))
if N % 2 == 0: if n % 2 == 0:
weights = weights_left[::-1] + weights_left weights = weights_left[::-1] + weights_left
else: else:
weights = [*weights_left[::-1], middle_weight, *weights_left] weights = [*weights_left[::-1], middle_weight, *weights_left]
@ -29,24 +34,28 @@ def calculate_symmetric_weights(N, middle_weight, factors=None):
return weights return weights
def scale_to_total(X, weights): def scale_to_total(x: float, weights: list[float]) -> list[float]:
"""Scale the weights so that their sum is proportional to X. """Scale the weights so that their sum is proportional to X.
X: Total value to distribute. Args:
weights: The list of weights to be scaled. x: Total value to distribute.
weights: The list of weights to be scaled.
Returns:
List of scaled values summing to x.
""" """
total_weight = sum(weights) total_weight = sum(weights)
base_unit = X / total_weight base_unit = x / total_weight
return [base_unit * weight for weight in weights] return [base_unit * weight for weight in weights]
def split_x_into_n_symmetrically(X, N, factors): def split_x_into_n_symmetrically(x: float, n: int, factors: list[float]) -> list[float]:
"""Split X into N parts with symmetric weights controlled by factors.""" """Split X into N parts with symmetric weights controlled by factors."""
weights = calculate_symmetric_weights(N, middle_weight=1, factors=factors) weights = calculate_symmetric_weights(n, middle_weight=1, factors=factors)
return scale_to_total(X, weights) return scale_to_total(x, weights)
def split_x_into_n_middle(X, N, middle_value): def split_x_into_n_middle(x: float, n: int, middle_value: float) -> list[float]:
"""Split X into N parts with symmetric weights using middle_value as peak.""" """Split X into N parts with symmetric weights using middle_value as peak."""
weights = calculate_symmetric_weights(N, middle_weight=middle_value) weights = calculate_symmetric_weights(n, middle_weight=middle_value)
return scale_to_total(X, weights) return scale_to_total(x, weights)

View File

@ -193,7 +193,7 @@ def _detect_total_mem_mb() -> int | None:
return None return None
def _auto_hash_mb(threads_wanted: int, engine_options) -> int: def _auto_hash_mb(threads_wanted: int, engine_options: dict[str, object]) -> int:
total_mb = _detect_total_mem_mb() or 2048 total_mb = _detect_total_mem_mb() or 2048
# Heuristic: cap at 4 GiB by default; keep at most half of RAM; ensure >= 64MB # Heuristic: cap at 4 GiB by default; keep at most half of RAM; ensure >= 64MB
half_ram = max(64, total_mb // 2) half_ram = max(64, total_mb // 2)
@ -202,7 +202,7 @@ def _auto_hash_mb(threads_wanted: int, engine_options) -> int:
opt = engine_options.get("Hash") opt = engine_options.get("Hash")
max_allowed = None max_allowed = None
try: try:
max_allowed = opt.max if opt is not None else None max_allowed = opt.max if opt is not None else None # type: ignore[attr-defined]
except Exception: except Exception:
max_allowed = None max_allowed = None
if isinstance(max_allowed, int): if isinstance(max_allowed, int):
@ -213,7 +213,7 @@ def _auto_hash_mb(threads_wanted: int, engine_options) -> int:
return max(64, int(target)) return max(64, int(target))
def main(): def main() -> None:
"""Parse arguments and run chess game analysis.""" """Parse arguments and run chess game analysis."""
logging.basicConfig(level=logging.INFO, format="%(message)s") logging.basicConfig(level=logging.INFO, format="%(message)s")
ap = argparse.ArgumentParser( ap = argparse.ArgumentParser(

View File

@ -7,13 +7,16 @@ from pathlib import Path
import socket import socket
import subprocess import subprocess
import time import time
from typing import Any
import urllib.error import urllib.error
import urllib.request import urllib.request
import pytest import pytest
def _req(url, method="GET", data=None): def _req(
url: str, method: str = "GET", data: dict[str, Any] | bytes | None = None
) -> tuple[int, bytes]:
"""Send an HTTP request and return status code and body.""" """Send an HTTP request and return status code and body."""
if data is not None and not isinstance(data, bytes | bytearray): if data is not None and not isinstance(data, bytes | bytearray):
data = json.dumps(data).encode("utf-8") data = json.dumps(data).encode("utf-8")
@ -24,7 +27,7 @@ def _req(url, method="GET", data=None):
return resp.getcode(), body return resp.getcode(), body
def test_crud_roundtrip(tmp_path): def test_crud_roundtrip(tmp_path: Path) -> None:
"""Test full CRUD lifecycle for articles API.""" """Test full CRUD lifecycle for articles API."""
# Build C server # Build C server
here = Path(__file__).resolve().parent here = Path(__file__).resolve().parent

View File

@ -9,12 +9,12 @@ HERE = os.path.dirname(__file__)
SITE_FILE = os.path.join(HERE, "index.html") SITE_FILE = os.path.join(HERE, "index.html")
def test_site_file_exists(): def test_site_file_exists() -> None:
"""Verify the main site HTML file exists.""" """Verify the main site HTML file exists."""
assert os.path.exists(SITE_FILE), f"Missing site file: {SITE_FILE}" assert os.path.exists(SITE_FILE), f"Missing site file: {SITE_FILE}"
def test_site_size_under_budget(): def test_site_size_under_budget() -> None:
"""Verify site size is under the defined budget.""" """Verify site size is under the defined budget."""
size = os.path.getsize(SITE_FILE) size = os.path.getsize(SITE_FILE)
assert size <= BUDGET, f"Site size {size} bytes exceeds budget {BUDGET}" assert size <= BUDGET, f"Site size {size} bytes exceeds budget {BUDGET}"

View File

@ -11,7 +11,7 @@ logging.basicConfig(level=logging.INFO)
class PokerModifierApp: class PokerModifierApp:
"""GUI application for poker game modifiers.""" """GUI application for poker game modifiers."""
def __init__(self): def __init__(self) -> None:
"""Initialize the poker modifier app with default settings.""" """Initialize the poker modifier app with default settings."""
self.modifiers = [ self.modifiers = [
# Hand Bonus Modifiers (Balatro-inspired) # Hand Bonus Modifiers (Balatro-inspired)
@ -519,7 +519,7 @@ class PokerModifierApp:
self.setup_gui() self.setup_gui()
def setup_gui(self): def setup_gui(self) -> None:
"""Create and configure the main GUI window.""" """Create and configure the main GUI window."""
# Create main window # Create main window
self.root = tk.Tk() self.root = tk.Tk()
@ -787,16 +787,16 @@ class PokerModifierApp:
) )
self.phase_label.pack(pady=10) self.phase_label.pack(pady=10)
def update_prob_display(self, value): def update_prob_display(self, value: str) -> None:
"""Update the probability percentage display.""" """Update the probability percentage display."""
self.prob_label.config(text=f"{value}%") self.prob_label.config(text=f"{value}%")
def update_length_display(self, value): def update_length_display(self, value: str) -> None:
"""Update the game length display.""" """Update the game length display."""
self.length_label.config(text=str(value)) self.length_label.config(text=str(value))
self.total_game_rounds = int(value) self.total_game_rounds = int(value)
def toggle_debug_mode(self): def toggle_debug_mode(self) -> None:
"""Toggle debug mode and show/hide debug controls.""" """Toggle debug mode and show/hide debug controls."""
self.debug_mode = self.debug_var.get() self.debug_mode = self.debug_var.get()
if self.debug_mode: if self.debug_mode:
@ -807,7 +807,7 @@ class PokerModifierApp:
self.force_endgame = False self.force_endgame = False
logging.debug("Debug mode disabled") logging.debug("Debug mode disabled")
def toggle_force_endgame(self): def toggle_force_endgame(self) -> None:
"""Toggle forced endgame mode for testing.""" """Toggle forced endgame mode for testing."""
self.force_endgame = not self.force_endgame self.force_endgame = not self.force_endgame
if self.force_endgame: if self.force_endgame:
@ -817,7 +817,7 @@ class PokerModifierApp:
self.force_endgame_button.config(text="Force Endgame", bg="#ff6b6b") self.force_endgame_button.config(text="Force Endgame", bg="#ff6b6b")
logging.debug("Normal modifier selection restored") logging.debug("Normal modifier selection restored")
def is_endgame(self): def is_endgame(self) -> bool:
"""Determine if we're in endgame phase.""" """Determine if we're in endgame phase."""
if self.debug_mode and self.force_endgame: if self.debug_mode and self.force_endgame:
return True return True
@ -825,7 +825,7 @@ class PokerModifierApp:
endgame_round = int(self.total_game_rounds * self.endgame_threshold) endgame_round = int(self.total_game_rounds * self.endgame_threshold)
return self.rounds_played >= endgame_round return self.rounds_played >= endgame_round
def start_round(self): def start_round(self) -> None:
"""Start a new poker round and determine if modifier should be applied.""" """Start a new poker round and determine if modifier should be applied."""
# Button animation effect # Button animation effect
self.start_button.config(relief=tk.SUNKEN) self.start_button.config(relief=tk.SUNKEN)
@ -850,7 +850,7 @@ class PokerModifierApp:
else: else:
self.show_no_modifier() self.show_no_modifier()
def update_phase_indicator(self): def update_phase_indicator(self) -> None:
"""Update the game phase indicator based on current round.""" """Update the game phase indicator based on current round."""
if self.is_endgame(): if self.is_endgame():
self.phase_label.config(text="Endgame", fg="#ff6b6b") self.phase_label.config(text="Endgame", fg="#ff6b6b")
@ -861,7 +861,7 @@ class PokerModifierApp:
else: else:
self.phase_label.config(text="Early", fg="#4CAF50") self.phase_label.config(text="Early", fg="#4CAF50")
def apply_random_modifier(self): def apply_random_modifier(self) -> None:
"""Apply a random modifier and update display.""" """Apply a random modifier and update display."""
# Update modifier counter # Update modifier counter
self.modifiers_applied += 1 self.modifiers_applied += 1
@ -925,7 +925,7 @@ class PokerModifierApp:
text=modifier_text, fg="#ffd700", bg=bg_color, font=("Arial", 14, "bold") text=modifier_text, fg="#ffd700", bg=bg_color, font=("Arial", 14, "bold")
) )
def show_no_modifier(self): def show_no_modifier(self) -> None:
"""Show no modifier message.""" """Show no modifier message."""
# Update result frame styling for no modifier # Update result frame styling for no modifier
self.result_frame.config( self.result_frame.config(
@ -940,7 +940,7 @@ class PokerModifierApp:
font=("Arial", 14), font=("Arial", 14),
) )
def reset_game(self): def reset_game(self) -> None:
"""Reset the game to initial state.""" """Reset the game to initial state."""
self.rounds_played = 0 self.rounds_played = 0
self.modifiers_applied = 0 self.modifiers_applied = 0
@ -968,11 +968,11 @@ class PokerModifierApp:
logging.info("Game reset to initial state") logging.info("Game reset to initial state")
def add_modifier(self, name, description): def add_modifier(self, name: str, description: str) -> None:
"""Add a new modifier to the list.""" """Add a new modifier to the list."""
self.modifiers.append({"name": name, "description": description}) self.modifiers.append({"name": name, "description": description})
def get_stats(self): def get_stats(self) -> dict[str, int | float | bool]:
"""Get current statistics.""" """Get current statistics."""
modifier_rate = ( modifier_rate = (
0 0
@ -992,7 +992,7 @@ class PokerModifierApp:
"force_endgame": self.force_endgame, "force_endgame": self.force_endgame,
} }
def run(self): def run(self) -> None:
"""Start the application.""" """Start the application."""
logging.info("Texas Hold'em Modifier App started!") logging.info("Texas Hold'em Modifier App started!")
logging.info( logging.info(

View File

@ -34,12 +34,6 @@ ignore = [
"COM812", # Trailing comma missing (conflicts with formatter) "COM812", # Trailing comma missing (conflicts with formatter)
"ISC001", # Implicit string concatenation (conflicts with formatter) "ISC001", # Implicit string concatenation (conflicts with formatter)
# Relaxed for script-heavy repository # Relaxed for script-heavy repository
"ANN001", # Missing type annotation for function argument
"ANN002", # Missing type annotation for *args
"ANN003", # Missing type annotation for **kwargs
"ANN201", # Missing return type annotation for public function
"ANN202", # Missing return type annotation for private function
"ANN204", # Missing return type annotation for special method
"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 "PLR0912", # Too many branches - relaxed for scripts