fix(lint): convert os.path to pathlib - remove PTH per-file ignores

- Converted os.path patterns to pathlib.Path in 15+ files
- os.path.join → Path /
- os.path.dirname → Path.parent
- os.path.exists → Path.exists()
- os.path.isfile → Path.is_file()
- os.path.abspath → Path.resolve()
- os.mkdir → Path.mkdir()
- os.listdir → Path.iterdir()
- os.getcwd → Path.cwd()
- os.replace → Path.replace()
- Updated function type hints to accept str | Path

Added PTH123 (open() vs Path.open()) to global ignores as stylistic preference
This commit is contained in:
Krzysztof kuhy Rudnicki 2025-11-30 23:03:03 +01:00
parent 264b019d4d
commit bb6713eabb
16 changed files with 103 additions and 128 deletions

View File

@ -1,20 +1,20 @@
"""Tests to ensure website stays within size budget."""
import os
from pathlib import Path
# Budget for the entire website (single file) in bytes
BUDGET = 14 * 1024 # 14 KiB
HERE = os.path.dirname(__file__)
SITE_FILE = os.path.join(HERE, "index.html")
HERE = Path(__file__).parent
SITE_FILE = HERE / "index.html"
def test_site_file_exists() -> None:
"""Verify the main site HTML file exists."""
assert os.path.exists(SITE_FILE), f"Missing site file: {SITE_FILE}"
assert SITE_FILE.exists(), f"Missing site file: {SITE_FILE}"
def test_site_size_under_budget() -> None:
"""Verify site size is under the defined budget."""
size = os.path.getsize(SITE_FILE)
size = SITE_FILE.stat().st_size
assert size <= BUDGET, f"Site size {size} bytes exceeds budget {BUDGET}"

View File

@ -36,6 +36,8 @@ ignore = [
"ISC001", # Implicit string concatenation (conflicts with formatter)
# Logging style preference - f-strings are more readable
"G004", # Logging statement uses f-string (stylistic preference)
# Path style preference - open() with Path objects is valid Python
"PTH123", # open() should be Path.open() (style preference, not required)
]
# Allow ALL rules to be auto-fixed
@ -49,7 +51,6 @@ unfixable = []
"S101", # Allow assert in tests
"S603", # Allow subprocess calls in tests
"PLR2004", # Allow magic values in tests
"PTH", # Allow os.path in tests for simplicity
]
"**/test_*.py" = [
"S101", # Allow assert in tests
@ -57,61 +58,47 @@ unfixable = []
"S310", # Allow URL open in tests
"S607", # Allow partial executable path in tests
"PLC0415", # Allow late imports for test isolation
"PTH", # Allow os.path in tests for simplicity
]
"**/conftest.py" = [
"D100", # Allow missing module docstring
"D103", # Allow missing function docstring
"PTH", # Allow os.path in conftest
]
"python_pkg/random_jpg/generate_jpeg.py" = [
"PTH", # os.path patterns in existing code
]
"python_pkg/tag_divider/tag_divider.py" = [
"PTH", # os.path patterns in existing code
]
"poker_modifier_app/poker_modifier_app.py" = [
"FBT003", # Boolean positional values in tkinter API calls
]
"python_pkg/download_cats/generate_cats.py" = [
"PTH", # os.path patterns in existing code
]
"python_pkg/lichess_bot/main.py" = [
"C901", # Complex functions handling game lifecycle (run_bot, handle_game)
"PLR0912", # Complex nested game event handling with many branches
"PLR0915", # Long function handling complete game lifecycle
"S603", # Subprocess call for analysis script
"PTH", # os.path patterns in existing code
]
"python_pkg/lichess_bot/engine.py" = [
"S603", # Subprocess for engine communication
"PTH", # os.path patterns
]
"python_pkg/lichess_bot/utils.py" = [
"PTH", # os.path patterns
]
"python_pkg/lichess_bot/tools/generate_blunder_tests.py" = [
"PTH", # os.path patterns in tool
]
"python_pkg/stockfish_analysis/analyze_chess_game.py" = [
"C901", # Complex main() with many argument combinations and analysis modes
"PLR0912", # Complex main() with many argument combinations and analysis modes
"PLR0915", # Long main() handling complete analysis workflow
"PTH", # os.path patterns
]
"python_pkg/keyboard_coop/main.py" = [
"FBT003", # Boolean positional values in pygame API calls (e.g., font.render)
"PTH", # os.path patterns
]
"python_pkg/screen_locker/screen_lock.py" = [
"FBT003", # Boolean positional values in tkinter API calls
"PTH", # os.path patterns
]
"python_pkg/scrape_website/scrape_comics.py" = [
"PTH", # os.path patterns
]
"python_pkg/extract_links/main.py" = [
"PTH", # os.path patterns
]
[tool.ruff.lint.pydocstyle]

View File

@ -5,7 +5,6 @@ Fetches cat images in batches and saves them to a local directory.
import json
import logging
import os
from pathlib import Path
import requests
@ -28,8 +27,8 @@ def _download_single_image(url: str) -> None:
response.raise_for_status() # Raise an exception for HTTP errors
# Extract the image name from the URL
image_name = os.path.basename(url)
image_path = os.path.join("./CATS2/", image_name)
image_name = Path(url).name
image_path = Path("./CATS2/") / image_name
# Save the image to the directory
with open(image_path, "wb") as file:

View File

@ -13,7 +13,7 @@ from __future__ import annotations
import argparse
from html.parser import HTMLParser
import logging
import os
from pathlib import Path
from urllib.parse import urlparse
_logger = logging.getLogger(__name__)
@ -72,15 +72,16 @@ def main() -> int:
)
args = ap.parse_args()
input_path = args.input_html
if not os.path.isfile(input_path):
input_path = Path(args.input_html)
if not input_path.is_file():
msg = f"Input file not found: {input_path}"
raise SystemExit(msg)
out_path = args.output_txt
if not out_path:
base = os.path.splitext(os.path.basename(input_path))[0]
out_path = os.path.join(os.path.dirname(input_path), f"{base}_links.txt")
out_path = input_path.parent / f"{input_path.stem}_links.txt"
else:
out_path = Path(out_path)
with open(input_path, encoding="utf-8", errors="ignore") as f:
html_text = f.read()

View File

@ -5,7 +5,7 @@ Players take turns selecting adjacent keys to form valid English words.
import json
import logging
import os
from pathlib import Path
import secrets
import sys
@ -102,9 +102,7 @@ class KeyboardCoopGame:
def load_dictionary(self) -> set[str]:
"""Load dictionary from words_dictionary.json file."""
try:
dictionary_path = os.path.join(
os.path.dirname(__file__), "words_dictionary.json"
)
dictionary_path = Path(__file__).parent / "words_dictionary.json"
with open(dictionary_path, encoding="utf-8") as f:
dictionary_data = json.load(f)
# Convert to set for faster lookup (we only need the keys)

View File

@ -4,6 +4,7 @@ import contextlib
import json
import logging
import os
from pathlib import Path
import subprocess
import chess
@ -36,20 +37,14 @@ class RandomEngine:
# the C engine handles its own scoring/selection.
self.depth = depth
# Default relative path inside this repo
default_path = os.path.abspath(
os.path.join(
os.path.dirname(__file__),
"..",
"..",
"C",
"lichess_random_engine",
"random_engine",
)
default_path = (
Path(__file__).resolve().parent.parent.parent
/ "C"
/ "lichess_random_engine"
/ "random_engine"
)
self.engine_path = engine_path or default_path
if not os.path.isfile(self.engine_path) or not os.access(
self.engine_path, os.X_OK
):
self.engine_path = Path(engine_path) if engine_path else default_path
if not self.engine_path.is_file() or not os.access(self.engine_path, os.X_OK):
msg = (
f"C engine not found or not executable at '{self.engine_path}'. "
"Build it first (make -C C/lichess_random_engine)."

View File

@ -6,6 +6,7 @@ import datetime
import json
import logging
import os
from pathlib import Path
import subprocess
import sys
import threading
@ -65,7 +66,7 @@ def run_bot(log_level: str = "INFO", *, decline_correspondence: bool = False) ->
# start at -1 so we act on the first state (0 moves)
last_handled_len = -1
# Prepare a per-game log file
game_log_path = os.path.join(os.getcwd(), f"lichess_bot_game_{game_id}.log")
game_log_path = Path.cwd() / f"lichess_bot_game_{game_id}.log"
try:
with open(game_log_path, "w") as lf:
lf.write(f"game {game_id} started\n")
@ -277,12 +278,12 @@ def run_bot(log_level: str = "INFO", *, decline_correspondence: bool = False) ->
if game_log_path:
analysis_text: str | None = None
try:
analyze_script = os.path.join(
os.path.dirname(os.path.dirname(__file__)),
"stockfish_analysis",
"analyze_chess_game.py",
analyze_script = (
Path(__file__).resolve().parent.parent
/ "stockfish_analysis"
/ "analyze_chess_game.py"
)
if os.path.isfile(analyze_script):
if analyze_script.is_file():
# Estimate total plies from the final board
try:
total_plies = len(board.move_stack)

View File

@ -1,4 +1,3 @@
import os
from pathlib import Path
import sys
@ -6,7 +5,7 @@ import pytest
# Add repository root to sys.path so 'import python_pkg.*' works when running
# pytest with a subdirectory as rootdir.
ROOT = os.path.abspath(os.path.join(os.path.dirname(__file__), "../../.."))
ROOT = str(Path(__file__).resolve().parent.parent.parent.parent)
if ROOT not in sys.path:
sys.path.insert(0, ROOT)

View File

@ -1,23 +1,23 @@
"""Test the engine against Lichess puzzles."""
import csv
import os
from pathlib import Path
import chess
import pytest
from python_pkg.lichess_bot.engine import RandomEngine
_PUZZLE_CSV = os.path.join(os.path.dirname(__file__), "lichess_db_puzzle.csv")
_PUZZLE_CSV = Path(__file__).parent / "lichess_db_puzzle.csv"
def _load_top_puzzles(csv_path: str, limit: int = 8) -> list[tuple[str, str]]:
def _load_top_puzzles(csv_path: str | Path, limit: int = 8) -> list[tuple[str, str]]:
"""Return a list of (FEN, solution_moves_str) for the first `limit` rows in the CSV.
CSV columns: PuzzleId,FEN,Moves,...
"""
puzzles: list[tuple[str, str]] = []
if not os.path.isfile(csv_path):
if not Path(csv_path).is_file():
return puzzles
with open(csv_path, newline="", encoding="utf-8") as f:
reader = csv.DictReader(f)

View File

@ -36,7 +36,7 @@ from __future__ import annotations
from dataclasses import dataclass
import io
import logging
import os
from pathlib import Path
import re
import sys
@ -183,10 +183,10 @@ def fen_and_uci_for_blunders(
return results
def ensure_unified_test_file(target_path: str) -> None:
def ensure_unified_test_file(target_path: str | Path) -> None:
"""Create the unified test file skeleton if it doesn't exist."""
os.makedirs(os.path.dirname(target_path), exist_ok=True)
if os.path.exists(target_path):
Path(target_path).parent.mkdir(parents=True, exist_ok=True)
if Path(target_path).exists():
return
# Create skeleton unified test file
with open(target_path, "w", encoding="utf-8") as f:
@ -197,8 +197,8 @@ import chess
import pytest
# Ensure repo root is importable when running pytest directly
REPO_ROOT = os.path.dirname(
os.path.dirname(os.path.dirname(os.path.abspath(__file__)))
REPO_ROOT = str(
Path(__file__).resolve().parent.parent.parent
)
if REPO_ROOT not in sys.path:
sys.path.insert(0, REPO_ROOT)
@ -239,7 +239,7 @@ def test_engine_avoids_logged_blunder(fen, blunder_uci, label):
def append_cases_to_unified_test(
unified_path: str, cases: list[tuple[str, str, str, Blunder]]
unified_path: str | Path, cases: list[tuple[str, str, str, Blunder]]
) -> int:
"""Append new cases to BLUNDER_CASES in the unified test file, skipping duplicates.
@ -325,29 +325,27 @@ def append_cases_to_unified_test(
return len(lines) + updated_existing
def _process_single_log(log_path: str) -> int:
def _process_single_log(log_path: str | Path) -> int:
"""Process a single log file. Returns 0 on success, non-zero otherwise."""
base = os.path.basename(log_path)
base = Path(log_path).name
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"
)
unified = os.path.abspath(unified)
unified = Path(__file__).parent.parent / "tests" / "test_blunders_all.py"
unified = unified.resolve()
added = append_cases_to_unified_test(unified, cases)
_logger.info(
f"Appended {added} new blunder checks to "
f"{os.path.relpath(unified)} (game {game_id})."
f"{unified.relative_to(Path.cwd())} (game {game_id})."
)
return 0
def _parse_and_extract_blunders(
log_path: str, base: str
log_path: str | Path, base: str
) -> int | tuple[list[tuple[str, str, str, Blunder]], str]:
"""Parse log file and extract blunder cases.
@ -366,11 +364,11 @@ def _parse_and_extract_blunders(
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]
game_id = m.group(1) if m else Path(base).stem
return cases, game_id
def _read_log_file(log_path: str) -> tuple[str | None, int | None]:
def _read_log_file(log_path: str | Path) -> 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:
@ -415,24 +413,24 @@ def _extract_cases(
def main(argv: list[str]) -> int:
"""Process log files and generate blunder test cases."""
script_dir = os.path.dirname(__file__)
past_dir = os.path.abspath(os.path.join(script_dir, "past_games"))
script_dir = Path(__file__).parent
past_dir = (script_dir / "past_games").resolve()
# No argument: process all logs in past_games
if len(argv) == 1:
if not os.path.isdir(past_dir):
if not past_dir.is_dir():
_logger.error(f"No past_games directory found at {past_dir}")
return 2
logs = [
os.path.join(past_dir, name)
for name in os.listdir(past_dir)
if re.match(r"lichess_bot_game_[A-Za-z0-9]+\.log$", name)
path
for path in past_dir.iterdir()
if re.match(r"lichess_bot_game_[A-Za-z0-9]+\.log$", path.name)
]
if not logs:
_logger.warning(f"No logs found in {past_dir}")
return 1
# Sort by mtime ascending for determinism
logs.sort(key=lambda p: os.path.getmtime(p))
logs.sort(key=lambda p: Path(p).stat().st_mtime)
ok = 0
for lp in logs:
rc = _process_single_log(lp)
@ -446,16 +444,16 @@ def main(argv: list[str]) -> int:
# One argument: game id or file path
arg = argv[1]
candidate_path = None
if os.path.isfile(arg):
candidate_path: str | Path | None = None
if Path(arg).is_file():
candidate_path = arg
# Treat as game id, resolve within past_games
elif re.fullmatch(r"[A-Za-z0-9]+", arg):
candidate_path = os.path.join(past_dir, f"lichess_bot_game_{arg}.log")
candidate_path = past_dir / f"lichess_bot_game_{arg}.log"
else:
# Fallback: if it's a bare filename, try inside past_games
maybe = os.path.join(past_dir, arg)
if os.path.isfile(maybe):
maybe = past_dir / arg
if maybe.is_file():
candidate_path = maybe
if not candidate_path:

View File

@ -2,6 +2,7 @@
import logging
import os
from pathlib import Path
import time
_logger = logging.getLogger(__name__)
@ -15,7 +16,7 @@ def _version_file_path() -> str:
override = os.getenv("LICHESS_BOT_VERSION_FILE")
if override:
return override
return os.path.join(os.path.dirname(__file__), ".bot_version")
return str(Path(__file__).parent / ".bot_version")
def get_and_increment_version() -> int:
@ -36,10 +37,10 @@ def get_and_increment_version() -> int:
new_version = current + 1
try:
tmp_path = path + ".tmp"
tmp_path = Path(path + ".tmp")
with open(tmp_path, "w") as f:
f.write(str(new_version))
os.replace(tmp_path, path)
tmp_path.replace(path)
except OSError:
# As a fallback, try a direct write; failure is non-fatal to bot operation
try:

View File

@ -4,7 +4,7 @@ import argparse
from dataclasses import dataclass
from datetime import datetime, timezone
import logging
import os
from pathlib import Path
import secrets
from PIL import Image
@ -66,20 +66,18 @@ def generate_bloated_jpeg(config: ImageConfig, image_index: int, folder: str) ->
pixels[x + i, y + j] = color
# Create the folder if it does not exist
if not os.path.exists(folder):
os.makedirs(folder)
folder_path = Path(folder)
folder_path.mkdir(parents=True, exist_ok=True)
# Generate unique output path
unique_output_path = os.path.join(
folder,
f"{os.path.splitext(config.output_path)[0]}_{image_index}"
f"{os.path.splitext(config.output_path)[1]}",
)
output_stem = Path(config.output_path).stem
output_suffix = Path(config.output_path).suffix
unique_output_path = folder_path / f"{output_stem}_{image_index}{output_suffix}"
# Save the image with specified quality to maximize file size
image.save(unique_output_path, "JPEG", quality=config.quality, optimize=False)
return unique_output_path
return str(unique_output_path)
if __name__ == "__main__":
@ -162,4 +160,4 @@ if __name__ == "__main__":
)
for i in range(1, args.num_images + 1):
output_path = generate_bloated_jpeg(config, i, folder)
_logger.info("Image %s saved to %s", i, os.path.abspath(output_path))
_logger.info("Image %s saved to %s", i, Path(output_path).resolve())

View File

@ -2,7 +2,7 @@
import argparse
import logging
import os
from pathlib import Path
from urllib.parse import urlparse
import requests
@ -34,15 +34,16 @@ driver.get(url)
def download_image(url: str) -> bool:
"""Download an image from a URL and save it locally."""
# Extract image name from URL
image_name = os.path.basename(urlparse(url).path)
image_name = Path(urlparse(url).path).name
image_path = Path(image_name)
# Check if the image already exists
if os.path.exists(image_name):
if image_path.exists():
_logger.info("Image %s already exists, skipping download.", image_name)
return False
_logger.info("Downloading image from URL: %s", url)
img_data = requests.get(url, timeout=REQUEST_TIMEOUT).content
with open(image_name, "wb") as handler:
with open(image_path, "wb") as handler:
handler.write(img_data)
_logger.info("Image %s downloaded successfully", image_name)
return True

View File

@ -7,7 +7,7 @@ Requires user to log their workout to unlock the screen.
from datetime import datetime, timezone
import json
import logging
import os
from pathlib import Path
import sys
import tkinter as tk
@ -29,8 +29,8 @@ class ScreenLocker:
def __init__(self, *, demo_mode: bool = True) -> None:
"""Initialize screen locker with optional demo mode."""
# Set up log file path
script_dir = os.path.dirname(os.path.abspath(__file__))
self.log_file = os.path.join(script_dir, "workout_log.json")
script_dir = Path(__file__).resolve().parent
self.log_file = script_dir / "workout_log.json"
# Check if already logged today
if self.has_logged_today():
@ -628,7 +628,7 @@ class ScreenLocker:
def has_logged_today(self) -> bool:
"""Check if workout has been logged today."""
if not os.path.exists(self.log_file):
if not self.log_file.exists():
return False
try:
@ -644,7 +644,7 @@ class ScreenLocker:
"""Save workout data to log file."""
# Load existing logs
logs = {}
if os.path.exists(self.log_file):
if self.log_file.exists():
try:
with open(self.log_file) as f:
logs = json.load(f)

View File

@ -26,7 +26,7 @@ import contextlib
import io
import logging
import multiprocessing
import os
from pathlib import Path
import re
import sys
@ -271,7 +271,7 @@ def main() -> None:
)
args = ap.parse_args()
if not os.path.isfile(args.file):
if not Path(args.file).is_file():
_logger.error(f"Input not found: {args.file}")
sys.exit(1)

View File

@ -1,8 +1,8 @@
"""Sort images into folders using keyboard input."""
import logging
import os # for: os.getcwd; os.mkdir; os.listdir;
from os import path # for: os.path.abspath
import os # for: os.chdir
from pathlib import Path
import shutil # for: shutil.move
# for: cv2.imread; cv2.namedWindow; cv2.imshow;
@ -41,21 +41,18 @@ RIGHT_FOLDER_CODE = 97 # Default 97 - 'a'
first_folder_name = input("Enter first folder name: [a] ")
second_folder_name = input("Enter second folder name: [d] ")
current_path = os.path.abspath(
os.getcwd()
) # Stolen from: https://stackoverflow.com/q/3430372
current_path = Path.cwd().resolve()
os.chdir(current_path) # Change working directory to the path where the python file is
if (
path.isdir(first_folder_name) != 1
): # Check if folder already exists, if it does not make it
os.mkdir(first_folder_name)
if path.isdir(second_folder_name) != 1:
os.mkdir(second_folder_name)
if not Path(
first_folder_name
).is_dir(): # Check if folder already exists, if not make it
Path(first_folder_name).mkdir()
if not Path(second_folder_name).is_dir():
Path(second_folder_name).mkdir()
for filename in os.listdir(
os.getcwd()
): # Go through every file in the working directory
for file_path in Path.cwd().iterdir(): # Go through every file in the working directory
filename = file_path.name
if (filename.lower()).endswith(
IMAGE_EXTENSION
): # If the file name ends with image extension
@ -67,12 +64,12 @@ for filename in os.listdir(
key = cv2.waitKey()
if key == RIGHT_FOLDER_CODE:
shutil.move(
current_path + "/" + filename,
current_path + "/" + first_folder_name + "/" + filename,
current_path / filename,
current_path / first_folder_name / filename,
)
elif key == LEFT_FOLDER_CODE:
shutil.move(
current_path + "/" + filename,
current_path + "/" + second_folder_name + "/" + filename,
current_path / filename,
current_path / second_folder_name / filename,
)
cv2.destroyAllWindows()