misc: fix all ty diagnostics

ty reported 20 diagnostics, and 0.0.89 adds a few more; this clears
them all without suppressions.

- The quest monster is held in a local before being stored on the
  quest book, so its name no longer reads from an Optional.
- PgUp/PgDn narrow the focused window with isinstance against a
  runtime-checkable PageScrollable protocol instead of hasattr.
- The ^t cheat used to monkeypatch SignalMixin.emit. SignalMixin now
  has a muted() context manager that emit() checks, and it restores
  emission even if a tick raises.
- GameView keeps its children as GameWindow, a protocol declaring
  stop() and sync(), so the cheat's resync type-checks.
- curses.ascii is imported explicitly where it is used.
- ProgressBarWindow is now Focusable itself, instead of calling
  Focusable.focus_standout() on a class that is not Focusable. Its
  four subclasses drop the duplicate Focusable base; the MRO and
  initialisation order are unchanged.
- MainMenu narrows the Optional pad before colouring the logo.
- The tests that record tasks assert player.task is set.
- Context managers are annotated as Generator rather than Iterator,
  which ty flags as deprecated for @contextmanager.
- The redundant cast() around getch() is gone.
- The frozen GameData test assigns through setattr(), since the
  direct assignment is an error by design.

Bug fixes:

- ListBox.select(None) crashed on `None < 0`; it now clears the
  selection, as DataTable.select(None) already does.
- ListBox.get() caught ValueError, which indexing never raises; it
  now catches IndexError, so an out-of-range index returns None as
  intended.
This commit is contained in:
Dash committed 2026-10-17 15:55:58 +02:00
1 parent 0c1d5185a4
commit 9155ef418b
14 files changed
+78 -49

No files matched your search

+19 -5
View File
@@ -1,10 +1,11 @@
import contextlib
import datetime
import functools
import itertools
import logging
import math
from collections import defaultdict
from collections.abc import Callable, Iterator, Sequence
from collections.abc import Callable, Generator, Iterator, Sequence
from dataclasses import dataclass
from typing import Any, cast
@@ -42,7 +43,21 @@ _SIGNALS: dict[tuple["SignalMixin", str], list[Callable[..., Any]]] = (
class SignalMixin:
_muted = 0
@staticmethod
@contextlib.contextmanager
def muted() -> Generator[None, None, None]:
"""Suppress all signal emission for the duration of the block."""
SignalMixin._muted += 1
try:
yield
finally:
SignalMixin._muted -= 1
def emit(self, signal_name: str, *user_data: Any) -> None:
if SignalMixin._muted:
return
for callback in _SIGNALS[self, signal_name]:
callback(*user_data)
@@ -683,12 +698,11 @@ class Simulation:
caption = ""
choice = random.below(5)
if choice == 0:
self.player.quest_book.monster = unnamed_monster(
monster = unnamed_monster(
self.data, self.player.level, iterations=3
)
caption = "Exterminate " + definite(
self.player.quest_book.monster.name, 2
)
self.player.quest_book.monster = monster
caption = "Exterminate " + definite(monster.name, 2)
elif choice == 1:
caption = "Seek " + definite(interesting_item(self.data), 1)
elif choice == 2:
+2 -2
View File
@@ -1,6 +1,6 @@
import contextlib
import time
from collections.abc import Callable, Iterator, Sequence
from collections.abc import Callable, Generator, Sequence
from typing import Any
RngState = tuple[float, float, float, int]
@@ -91,7 +91,7 @@ def seed(source: str) -> None:
@contextlib.contextmanager
def using(rng: Alea) -> Iterator[None]:
def using(rng: Alea) -> Generator[None, None, None]:
"""Draw from rng instead of the shared generator inside the block."""
global _current
previous = _current
+2 -2
View File
@@ -3,7 +3,7 @@ import curses
import os
import signal
import time
from typing import Any, cast
from typing import Any
from pqcli import lingo
from pqcli.config import Class, Race
@@ -91,7 +91,7 @@ class CursesUserInterface(BaseUserInterface):
self._switch_to_roster_view()
while True:
key = cast(int, self._screen.getch())
key = self._screen.getch()
if key == curses.ERR:
time.sleep(0.1)
+25 -11
View File
@@ -1,7 +1,8 @@
import argparse
import curses
import curses.ascii
import datetime
from typing import Any
from typing import Any, Protocol, runtime_checkable
from pqcli.game_data import GameData
from pqcli.mechanic import Player, SignalMixin, Simulation
@@ -16,7 +17,7 @@ from pqcli.ui.curses.util import (
KEYS_UP,
)
from pqcli.ui.curses.views.base_view import BaseView
from pqcli.ui.curses.widgets import Focusable, Widget
from pqcli.ui.curses.widgets import Focusable
from .character_sheet_window import CharacterSheetWindow
from .equipment_window import EquipmentWindow
@@ -27,6 +28,19 @@ from .spell_book_window import SpellBookWindow
from .task_progress_window import TaskProgressWindow
class GameWindow(Protocol):
def stop(self) -> None: ...
def sync(self) -> None: ...
@runtime_checkable
class PageScrollable(Protocol):
def scroll_page_up(self) -> None: ...
def scroll_page_down(self) -> None: ...
class GameView(BaseView):
def __init__(
self,
@@ -53,11 +67,11 @@ class GameView(BaseView):
self.on_exit()
elif key in {curses.KEY_PPAGE}:
if hasattr(focused, "scroll_page_up"):
if isinstance(focused, PageScrollable):
focused.scroll_page_up()
elif key in {curses.KEY_NPAGE}:
if hasattr(focused, "scroll_page_down"):
if isinstance(focused, PageScrollable):
focused.scroll_page_down()
elif key in KEYS_CYCLE:
@@ -118,11 +132,9 @@ class GameView(BaseView):
self._simulation.tick()
elif self._args.cheats and key == curses.ascii.DC4: # ^t
old = SignalMixin.emit
SignalMixin.emit = lambda *_: None
with SignalMixin.muted():
for _ in range(10000):
self._simulation.tick()
SignalMixin.emit = old
for child in self._children:
child.sync()
@@ -225,16 +237,18 @@ class GameView(BaseView):
x=0,
)
self._focusable_children: list[Focusable] = [
focusable_children = (
self._char_sheet_win,
self._spell_book_win,
self._equipment_win,
self._inventory_win,
self._plot_win,
self._quest_book_win,
]
self._children: list[Widget] = self._focusable_children + [
self._task_win
)
self._focusable_children: list[Focusable] = list(focusable_children)
self._children: list[GameWindow] = [
*focusable_children,
self._task_win,
]
def stop(self) -> None:
@@ -1,12 +1,11 @@
from typing import Any
from pqcli.mechanic import Player, StatType
from pqcli.ui.curses.widgets import Focusable
from .progress_bar_window import DataTableProgressBarWindow
class CharacterSheetWindow(Focusable, DataTableProgressBarWindow):
class CharacterSheetWindow(DataTableProgressBarWindow):
def __init__(
self, player: Player, parent: Any, h: int, w: int, y: int, x: int
) -> None:
@@ -1,12 +1,11 @@
from typing import Any
from pqcli.mechanic import InventoryItem, Player
from pqcli.ui.curses.widgets import Focusable
from .progress_bar_window import DataTableProgressBarWindow
class InventoryWindow(Focusable, DataTableProgressBarWindow):
class InventoryWindow(DataTableProgressBarWindow):
def __init__(
self, player: Player, parent: Any, h: int, w: int, y: int, x: int
) -> None:
@@ -2,12 +2,11 @@ from typing import Any
from pqcli.lingo import act_name
from pqcli.mechanic import Player
from pqcli.ui.curses.widgets import Focusable
from .progress_bar_window import ListBoxProgressBarWindow
class PlotWindow(Focusable, ListBoxProgressBarWindow):
class PlotWindow(ListBoxProgressBarWindow):
cutoff = 100
def __init__(
@@ -10,7 +10,7 @@ from pqcli.ui.curses.widgets import (
)
class ProgressBarWindow(WindowWrapper):
class ProgressBarWindow(Focusable, WindowWrapper):
def __init__(
self,
parent: Any,
@@ -51,7 +51,7 @@ class ProgressBarWindow(WindowWrapper):
self._progress_bar_win.erase()
with Focusable.focus_standout(self, self._progress_bar_win):
with self.focus_standout(self._progress_bar_win):
self._progress_bar_win.border(
curses.ACS_VLINE,
curses.ACS_VLINE,
@@ -105,7 +105,7 @@ class DataTableProgressBarWindow(ProgressBarWindow):
if not self._win:
return
with Focusable.focus_standout(self, self._win):
with self.focus_standout(self._win):
self._win.box()
x = max(0, (self.getmaxyx()[1] - len(self._title)) // 2)
self._win.addnstr(
@@ -143,7 +143,7 @@ class ListBoxProgressBarWindow(ProgressBarWindow):
if not self._win:
return
with Focusable.focus_standout(self, self._win):
with self.focus_standout(self._win):
self._win.box()
x = max(0, (self.getmaxyx()[1] - len(self._title)) // 2)
self._win.addnstr(
@@ -1,12 +1,11 @@
from typing import Any
from pqcli.mechanic import Player
from pqcli.ui.curses.widgets import Focusable
from .progress_bar_window import ListBoxProgressBarWindow
class QuestBookWindow(Focusable, ListBoxProgressBarWindow):
class QuestBookWindow(ListBoxProgressBarWindow):
cutoff = 100
def __init__(
+9 -7
View File
@@ -38,15 +38,17 @@ class MainMenu(Menu):
scr_height=scr_height,
scr_width=scr_width,
)
pad = self._pad
assert pad is not None
if has_colors():
for y in range(len(self._header_lines)):
self._pad.chgat(y, 0, curses.color_pair(COLOR_LOGO))
self._pad.chgat(3, 29, 5, curses.color_pair(COLOR_LOGO_ALT))
self._pad.chgat(4, 29, 5, curses.color_pair(COLOR_LOGO_ALT))
self._pad.chgat(5, 32, 4, curses.color_pair(COLOR_LOGO_ALT))
self._pad.chgat(5, 27, 4, curses.color_pair(COLOR_LOGO_ALT))
self._pad.chgat(6, 34, 2, curses.color_pair(COLOR_LOGO_ALT))
self._pad.chgat(6, 27, 2, curses.color_pair(COLOR_LOGO_ALT))
pad.chgat(y, 0, curses.color_pair(COLOR_LOGO))
pad.chgat(3, 29, 5, curses.color_pair(COLOR_LOGO_ALT))
pad.chgat(4, 29, 5, curses.color_pair(COLOR_LOGO_ALT))
pad.chgat(5, 32, 4, curses.color_pair(COLOR_LOGO_ALT))
pad.chgat(5, 27, 4, curses.color_pair(COLOR_LOGO_ALT))
pad.chgat(6, 34, 2, curses.color_pair(COLOR_LOGO_ALT))
pad.chgat(6, 27, 2, curses.color_pair(COLOR_LOGO_ALT))
class RosterView(BaseView):
+2 -2
View File
@@ -29,14 +29,14 @@ class ListBox(Scrollable):
idx %= len(self._items)
try:
return self._items[idx]
except ValueError:
except IndexError:
return None
def delete(self, idx: int, count: int = 1) -> None:
del self._items[idx : idx + count]
def select(self, idx: int | None) -> None:
if idx < 0 and len(self._items):
if idx is not None and idx < 0 and self._items:
idx %= len(self._items)
self._selected = idx
+4 -3
View File
@@ -28,11 +28,12 @@ class Menu(Widget):
w = max(map(len, all_lines)) + 1
h = len(all_lines) + 1
self._pad: Any | None = curses.newpad(h, w)
pad = curses.newpad(h, w)
self._pad: Any | None = pad
for y, line in enumerate(self._header_lines):
self._pad.move(y, 0)
self._pad.addstr(line)
pad.move(y, 0)
pad.addstr(line)
self._active_choice = active_choice
+6 -4
View File
@@ -34,10 +34,12 @@ def run_trace(seed: str, ticks: int = 3000) -> list[str]:
simulation = Simulation(player, data)
lines: list[str] = []
player.connect(
"new_task",
lambda: lines.append(f"task: {player.task.description}"),
)
def record_task() -> None:
assert player.task is not None
lines.append(f"task: {player.task.description}")
player.connect("new_task", record_task)
for _ in range(ticks):
simulation.tick(100000.0)
+1 -1
View File
@@ -179,4 +179,4 @@ def test_game_data_is_immutable() -> None:
data = load_game_data()
assert isinstance(data, GameData)
with pytest.raises(AttributeError):
data.spells = () # type: ignore[misc]
setattr(data, "spells", ())