Enable PLR2004: replace magic values with named constants

- Added constants for HTTP status codes (using http.HTTPStatus)
- Added validation limit constants in screen_locker
- Added centipawn loss threshold constants in chess analysis
- Added various other domain-specific constants across 9 files
This commit is contained in:
Krzysztof kuhy Rudnicki 2025-11-30 15:01:14 +01:00
parent 91a4532772
commit 14612a6434
10 changed files with 96 additions and 45 deletions

View File

@ -12,8 +12,10 @@ import requests
logging.basicConfig(level=logging.INFO)
MAX_REQUESTS = 90
requests_send = 0
while requests_send < 90:
while requests_send < MAX_REQUESTS:
res = requests.get("https://api.thecatapi.com/v1/images/search?limit=100&api_key=")
requests_send += 1
response = json.loads(res.text)

View File

@ -27,6 +27,7 @@ KEY_SELECTED_COLOR = (150, 150, 200)
KEY_AVAILABLE_COLOR = (100, 150, 100)
TEXT_COLOR = (255, 255, 255)
PLAYER_COLORS = [(255, 100, 100), (100, 100, 255)]
MIN_WORD_LENGTH = 3
# Keyboard layout
KEYBOARD_LAYOUT = [
@ -286,7 +287,7 @@ class KeyboardCoopGame:
def calculate_score(self, word_length):
"""Calculate score exponentially based on word length."""
if word_length < 3:
if word_length < MIN_WORD_LENGTH:
return 0
return 2 ** (word_length - 2)
@ -317,7 +318,9 @@ class KeyboardCoopGame:
def submit_word(self):
"""Submit the current word and check if it's valid."""
if len(self.current_word) >= 3 and self.is_valid_word(self.current_word):
if len(self.current_word) >= MIN_WORD_LENGTH and self.is_valid_word(
self.current_word
):
points = self.calculate_score(len(self.current_word))
self.score += points
self.message = (
@ -329,8 +332,11 @@ class KeyboardCoopGame:
self.generate_random_keyboard()
self.key_positions = self.calculate_key_positions()
elif len(self.current_word) < 3:
self.message = f"'{self.current_word}' is too short! (minimum 3 letters)"
elif len(self.current_word) < MIN_WORD_LENGTH:
self.message = (
f"'{self.current_word}' is too short! "
f"(minimum {MIN_WORD_LENGTH} letters)"
)
else:
self.message = f"'{self.current_word}' is not a valid word!"

View File

@ -2,6 +2,7 @@
from collections.abc import Generator
import contextlib
from http import HTTPStatus
import json
import logging
import time
@ -45,7 +46,7 @@ class LichessAPI:
raise
elapsed = time.monotonic() - t0
status = r.status_code
if status >= 400:
if status >= HTTPStatus.BAD_REQUEST:
# Log a brief error body snippet if available
snippet = None
try:
@ -88,7 +89,7 @@ class LichessAPI:
logging.debug(f"Skipping non-JSON line: {line}")
except requests.HTTPError as e:
status = getattr(e.response, "status_code", None)
if status == 429:
if status == HTTPStatus.TOO_MANY_REQUESTS:
logging.warning("Event stream hit 429; backing off")
time.sleep(backoff)
backoff = min(8.0, backoff * 2)
@ -160,11 +161,11 @@ class LichessAPI:
"""Submit a move to an active game."""
url = f"{LICHESS_API}/api/board/game/{game_id}/move/{move.uci()}"
r = self._request("POST", url, timeout=30)
if r.status_code in (400, 409):
if r.status_code in (HTTPStatus.BAD_REQUEST, HTTPStatus.CONFLICT):
# Likely not our turn or move already played; do not retry to avoid spam
r.raise_for_status()
return
if r.status_code == 429:
if r.status_code == HTTPStatus.TOO_MANY_REQUESTS:
logging.warning(f"HTTP POST {url} -> 429; retrying once after 0.5s")
time.sleep(0.5)
r = self._request("POST", url, timeout=30)
@ -178,6 +179,6 @@ class LichessAPI:
"""Fetch the authenticated user's ID."""
url = f"{LICHESS_API}/api/account"
r = self._request("GET", url, timeout=30)
if r.status_code == 200:
if r.status_code == HTTPStatus.OK:
return r.json().get("id")
return None

View File

@ -45,6 +45,10 @@ import chess.pgn
logging.basicConfig(level=logging.INFO)
# Expected columns in the log file:
# ply, side, move, played_eval, best_eval, loss, class, best_suggestion
EXPECTED_COLUMNS = 8
@dataclass
class Blunder:
@ -79,7 +83,7 @@ def parse_columns_for_blunders(text: str) -> list[Blunder]:
parts = re.split(r"\s{2,}", ln.strip())
# Expected columns:
# ply, side, move, played_eval, best_eval, loss, class, best_suggestion
if len(parts) < 8:
if len(parts) < EXPECTED_COLUMNS:
continue
try:
ply = int(parts[0])

View File

@ -10,6 +10,8 @@ from PIL import Image
logging.basicConfig(level=logging.INFO)
MAX_IMAGE_SIZE = 1000
def generate_bloated_jpeg(
size, color_list, block_size, output_path, quality, image_index, folder
@ -26,9 +28,11 @@ def generate_bloated_jpeg(
image_index (int): Index of the image for unique naming.
folder (str): Folder to save the image.
"""
# Ensure size is divisible by block_size and does not exceed 1000 pixels
if size > 1000 or size % block_size != 0:
msg = "Size must be 1000 pixels or less and divisible by block_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:
msg = (
f"Size must be {MAX_IMAGE_SIZE} pixels or less and divisible by block_size"
)
raise ValueError(msg)
# Create a new image

View File

@ -8,8 +8,15 @@ import sys
logging.basicConfig(level=logging.INFO)
DEFAULT_MIN_PERCENTAGE = 1
DEFAULT_MAX_PERCENTAGE = 20
def randomize_numbers(numbers, min_percentage=1, max_percentage=20):
def randomize_numbers(
numbers,
min_percentage=DEFAULT_MIN_PERCENTAGE,
max_percentage=DEFAULT_MAX_PERCENTAGE,
):
"""Apply random percentage variation to a list of numbers."""
randomized_numbers = []
for number in numbers:
@ -43,8 +50,10 @@ def parse_input(input_string):
return numbers, decimal_counts
MIN_ARGS = 2
if __name__ == "__main__":
if len(sys.argv) < 2:
if len(sys.argv) < MIN_ARGS:
logging.info(
"Usage: python random_digits.py <number1> <number2> ... "
"[min_percentage max_percentage]"
@ -54,8 +63,8 @@ if __name__ == "__main__":
try:
input_string = " ".join(sys.argv[1:])
numbers, decimal_counts = parse_input(input_string)
min_percentage = 1
max_percentage = 20
min_percentage = DEFAULT_MIN_PERCENTAGE
max_percentage = DEFAULT_MAX_PERCENTAGE
if len(numbers) == 0:
msg = "No valid numbers provided."

View File

@ -13,6 +13,15 @@ import tkinter as tk
logging.basicConfig(level=logging.INFO)
# Validation limits for workout data
MAX_DISTANCE_KM = 100
MAX_TIME_MINUTES = 600
MAX_PACE_MIN_PER_KM = 20
MIN_EXERCISE_NAME_LEN = 3
MAX_SETS = 20
MAX_REPS = 100
MAX_WEIGHT_KG = 500
class ScreenLocker:
"""Screen locker that requires workout logging to unlock."""
@ -294,16 +303,20 @@ class ScreenLocker:
pace = float(self.pace_entry.get())
# Sanity checks
if distance <= 0 or distance > 100:
self.show_error("Distance seems unrealistic (0-100 km)")
if distance <= 0 or distance > MAX_DISTANCE_KM:
self.show_error(f"Distance seems unrealistic (0-{MAX_DISTANCE_KM} km)")
return
if time_mins <= 0 or time_mins > 600:
self.show_error("Time seems unrealistic (0-600 minutes)")
if time_mins <= 0 or time_mins > MAX_TIME_MINUTES:
self.show_error(
f"Time seems unrealistic (0-{MAX_TIME_MINUTES} minutes)"
)
return
if pace <= 0 or pace > 20:
self.show_error("Pace seems unrealistic (0-20 min/km)")
if pace <= 0 or pace > MAX_PACE_MIN_PER_KM:
self.show_error(
f"Pace seems unrealistic (0-{MAX_PACE_MIN_PER_KM} min/km)"
)
return
# Calculate expected pace and check if close enough
@ -463,21 +476,21 @@ class ScreenLocker:
return
# Check for empty or lazy entries
if any(len(ex) < 3 for ex in exercises):
if any(len(ex) < MIN_EXERCISE_NAME_LEN for ex in exercises):
self.show_error("Exercise names too short - be specific")
return
# Sanity checks
if any(s < 1 or s > 20 for s in sets):
self.show_error("Sets should be between 1-20")
if any(s < 1 or s > MAX_SETS for s in sets):
self.show_error(f"Sets should be between 1-{MAX_SETS}")
return
if any(r < 1 or r > 100 for r in reps):
self.show_error("Reps should be between 1-100")
if any(r < 1 or r > MAX_REPS for r in reps):
self.show_error(f"Reps should be between 1-{MAX_REPS}")
return
if any(w < 0 or w > 500 for w in weights):
self.show_error("Weights should be between 0-500 kg")
if any(w < 0 or w > MAX_WEIGHT_KG for w in weights):
self.show_error(f"Weights should be between 0-{MAX_WEIGHT_KG} kg")
return
# Calculate expected total weight

View File

@ -44,6 +44,10 @@ except Exception: # pragma: no cover
logging.exception(" pip install -r PYTHON/stockfish_analysis/requirements.txt")
raise
# Memory configuration constants
MEMINFO_PARTS_MIN = 2
HIGH_THREAD_COUNT = 16
def extract_pgn_text(raw: str) -> str | None:
"""Try to extract a PGN block from a possibly noisy file.
@ -97,6 +101,14 @@ def score_to_cp(
return s.score(mate_score=None), None
# Centipawn loss thresholds for move quality classification (Lichess-like bands)
CP_LOSS_BEST = 10
CP_LOSS_EXCELLENT = 20
CP_LOSS_GOOD = 50
CP_LOSS_INACCURACY = 99
CP_LOSS_MISTAKE = 299
def classify_cp_loss(cp_loss: int | None) -> str:
"""Classify move quality using Lichess-like centipawn loss bands.
@ -111,15 +123,15 @@ def classify_cp_loss(cp_loss: int | None) -> str:
"""
if cp_loss is None:
return "Unknown"
if cp_loss <= 10:
if cp_loss <= CP_LOSS_BEST:
return "Best"
if cp_loss <= 20:
if cp_loss <= CP_LOSS_EXCELLENT:
return "Excellent"
if cp_loss <= 50:
if cp_loss <= CP_LOSS_GOOD:
return "Good"
if cp_loss <= 99:
if cp_loss <= CP_LOSS_INACCURACY:
return "Inaccuracy"
if cp_loss <= 299:
if cp_loss <= CP_LOSS_MISTAKE:
return "Mistake"
return "Blunder"
@ -172,7 +184,7 @@ def _detect_total_mem_mb() -> int | None:
for line in f:
if line.startswith("MemTotal:"):
parts = line.split()
if len(parts) >= 2 and parts[1].isdigit():
if len(parts) >= MEMINFO_PARTS_MIN and parts[1].isdigit():
# Value is in kB
kb = int(parts[1])
return kb // 1024
@ -196,7 +208,7 @@ def _auto_hash_mb(threads_wanted: int, engine_options) -> int:
if isinstance(max_allowed, int):
target = min(target, max_allowed)
# Some rough scaling: if very many threads, give a bit more (but not huge)
if threads_wanted >= 16:
if threads_wanted >= HIGH_THREAD_COUNT:
target = min(target + 1024, (total_mb * 3) // 4)
return max(64, int(target))

View File

@ -1,5 +1,6 @@
"""Integration tests for the articles C server API."""
from http import HTTPStatus
import json
import os
from pathlib import Path
@ -62,19 +63,19 @@ def test_crud_roundtrip(tmp_path):
"thumb": "data:image/png;base64,xyz",
},
)
assert code == 201
assert code == HTTPStatus.CREATED
created = json.loads(body)
art_id = created["id"]
# List
code, body = _req(base + "/api/articles")
assert code == 200
assert code == HTTPStatus.OK
items = json.loads(body)
assert any(a["id"] == art_id for a in items)
# Get one
code, body = _req(base + f"/api/articles/{art_id}")
assert code == 200
assert code == HTTPStatus.OK
got = json.loads(body)
assert got["title"] == "T1"
@ -82,13 +83,13 @@ def test_crud_roundtrip(tmp_path):
code, body = _req(
base + f"/api/articles/{art_id}", method="PUT", data={"title": "T2"}
)
assert code == 200
assert code == HTTPStatus.OK
updated = json.loads(body)
assert updated["title"] == "T2"
# Delete
code, _ = _req(base + f"/api/articles/{art_id}", method="DELETE")
assert code == 204
assert code == HTTPStatus.NO_CONTENT
# Ensure gone
try:
@ -96,7 +97,7 @@ def test_crud_roundtrip(tmp_path):
msg = "Expected 404"
raise AssertionError(msg)
except urllib.error.HTTPError as e:
assert e.code == 404
assert e.code == HTTPStatus.NOT_FOUND
finally:
srv.terminate()

View File

@ -34,7 +34,6 @@ ignore = [
"COM812", # Trailing comma missing (conflicts with formatter)
"ISC001", # Implicit string concatenation (conflicts with formatter)
# Relaxed for script-heavy repository
"PLR2004", # Magic value comparison - common in scripts
"S101", # Use of assert - acceptable in this codebase
"ANN001", # Missing type annotation for function argument
"ANN002", # Missing type annotation for *args