misc: replace Any with precise types

Curses screens, windows and pads are typed as curses.window, and the
widgets that may get no parent window take curses.window | None.

- EventHandler is generic over a ParamSpec, so each handler declares
  the arguments it passes, and MenuView is generic over the item its
  on_confirm hands out. Choice callbacks take no arguments.
- Scrollable is generic over its items, which DataTable and ListBox
  fix to tuple[str, str] and str.
- random.choice() and choice_low() are generic over the sequence, so
  the casts at their call sites are gone. BasicUserInterface.menu()
  and util.first() are generic too.
- Signal callbacks and their payloads are Callable[..., object] and
  object, since emit() only forwards them.
- JSON values in game_data are object and narrowed by isinstance,
  and data files are Traversable.
- Signal handlers take an int and a FrameType | None, and
  list_players() takes a TextIO.
- The focus_standout() context managers return
  Generator[None, None, None].

Any remains in three places: Player.__setstate__, whose pickled
__dict__ holds values of every type; Focusable.__init__, a mixin that
passes *args and **kwargs through to the next class; and the values
load_game_data() collects, whose element type depends on the field
each one fills.
This commit is contained in:
Dash committed 2026-10-17 15:55:59 +02:00
1 parent 00794aca37
commit fd4a7d151d
34 files changed
+219 -153

No files matched your search

+8 -4
View File
@@ -6,7 +6,8 @@ import sys
import time
from collections.abc import Sequence
from datetime import datetime, timedelta
from typing import Any
from types import FrameType
from typing import TypeVar
from pqcli import lingo
from pqcli.config import PRIME_STATS
@@ -25,6 +26,9 @@ LOGO = r"""
"""
T = TypeVar("T")
class MainMenu(enum.IntEnum):
create = 1
play = 2
@@ -43,7 +47,7 @@ class BasicUserInterface(BaseUserInterface):
) -> None:
super().__init__(roster, player, args, data)
def signal_handler(sig: Any, frame: Any) -> None:
def signal_handler(sig: int, frame: FrameType | None) -> None:
logging.info("Quitting")
if self.args.use_saves:
self.roster.save()
@@ -217,9 +221,9 @@ class BasicUserInterface(BaseUserInterface):
def menu(
self,
options: Sequence[tuple[Any, str]],
options: Sequence[tuple[T, str]],
title: str | None = None,
) -> Any:
) -> T:
print()
if title:
print(f"{title}:")
+2 -2
View File
@@ -3,7 +3,7 @@ import curses
import os
import signal
import time
from typing import Any
from types import FrameType
from pqcli import lingo
from pqcli.config import Class, Race
@@ -68,7 +68,7 @@ class CursesUserInterface(BaseUserInterface):
curses.init_pair(COLOR_PROGRESSBAR, 0, 9)
curses.init_pair(COLOR_HIGHLIGHT, 3, -1)
def signal_handler(sig: Any, frame: Any) -> None:
def signal_handler(sig: int, frame: FrameType | None) -> None:
if self.args.use_saves:
self.roster.save()
exit(0)
+7 -5
View File
@@ -1,15 +1,17 @@
from collections.abc import Callable
from typing import Any
from typing import Generic, ParamSpec
P = ParamSpec("P")
class EventHandler:
class EventHandler(Generic[P]):
def __init__(self) -> None:
self.callbacks: list[Callable[..., Any]] = []
self.callbacks: list[Callable[P, object]] = []
def __call__(self, *args: Any, **kwargs: Any) -> None:
def __call__(self, *args: P.args, **kwargs: P.kwargs) -> None:
for callback in self.callbacks:
callback(*args, **kwargs)
def __iadd__(self, callback: Callable[..., Any]) -> "EventHandler":
def __iadd__(self, callback: Callable[P, object]) -> "EventHandler[P]":
self.callbacks.append(callback)
return self
+5 -3
View File
@@ -2,7 +2,7 @@ import curses
import curses.ascii
from collections.abc import Callable, Iterable
from dataclasses import dataclass
from typing import Any, TypeVar, overload
from typing import TypeVar, overload
KEYS_CYCLE = {curses.ascii.TAB}
KEYS_DOWN = set(map(ord, "jJ")) | {curses.KEY_DOWN}
@@ -16,7 +16,7 @@ KEYS_CANCEL = set(map(ord, "qQ")) | {curses.ascii.ESC}
class Choice:
keys: list[int]
desc: str
callback: Callable[..., Any]
callback: Callable[[], object]
ItemT = TypeVar("ItemT")
@@ -31,5 +31,7 @@ def first(source: Iterable[ItemT]) -> ItemT | None: ...
def first(source: Iterable[ItemT], default: DefaultT) -> ItemT | DefaultT: ...
def first(source: Iterable[Any], default: Any = None) -> Any:
def first(
source: Iterable[ItemT], default: DefaultT | None = None
) -> ItemT | DefaultT | None:
return next(iter(source), default)
+2 -2
View File
@@ -1,8 +1,8 @@
from typing import Any
import curses
class BaseView:
def __init__(self, screen: Any) -> None:
def __init__(self, screen: curses.window) -> None:
self.screen = screen
def start(self) -> None:
@@ -1,14 +1,17 @@
import curses
import functools
from typing import Any
from pqcli.lingo import act_name, to_roman
from pqcli.mechanic import Player
from pqcli.roster import Roster
from pqcli.ui.curses.util import KEYS_CANCEL, Choice
from pqcli.ui.curses.views.menu_view import MenuView
class ChooseCharacterView(MenuView):
def __init__(self, screen: Any, roster: Roster, title: str) -> None:
class ChooseCharacterView(MenuView[Player]):
def __init__(
self, screen: curses.window, roster: Roster, title: str
) -> None:
super().__init__(screen, title)
for y, player in enumerate(roster.players, 1):
+4 -4
View File
@@ -1,4 +1,4 @@
from typing import Any
import curses
from pqcli.ui.curses.event_handler import EventHandler
from pqcli.ui.curses.util import Choice
@@ -8,12 +8,12 @@ from .base_view import BaseView
class ConfirmView(BaseView):
def __init__(self, screen: Any, title: str) -> None:
def __init__(self, screen: curses.window, title: str) -> None:
super().__init__(screen)
self.title = f"{title}:"
self.on_cancel = EventHandler()
self.on_confirm = EventHandler()
self.on_cancel = EventHandler[[]]()
self.on_confirm = EventHandler[[]]()
self.choices: list[Choice] = [
Choice(
@@ -1,16 +1,16 @@
import curses
import functools
from collections.abc import Sequence
from typing import Any
from pqcli.config import Class
from pqcli.ui.curses.util import KEYS_CANCEL, Choice
from pqcli.ui.curses.views.menu_view import MenuView
class ChooseCharacterClassView(MenuView):
class ChooseCharacterClassView(MenuView[Class]):
def __init__(
self,
screen: Any,
screen: curses.window,
classes: Sequence[Class],
class_: Class | None = None,
) -> None:
@@ -1,6 +1,5 @@
import curses
import curses.ascii
from typing import Any
from pqcli.mechanic import generate_name
from pqcli.ui.curses.event_handler import EventHandler
@@ -10,15 +9,17 @@ from pqcli.ui.curses.widgets.focusable import focus_standout
class ChooseCharacterNameView(BaseView):
def __init__(self, screen: Any, character_name: str | None = None) -> None:
def __init__(
self, screen: curses.window, character_name: str | None = None
) -> None:
super().__init__(screen)
self.on_cancel = EventHandler()
self.on_confirm = EventHandler()
self.on_cancel = EventHandler[[]]()
self.on_confirm = EventHandler[[str]]()
self._active_widget = 0
self._win: Any | None = None
self._text_win: Any | None = None
self._win: curses.window | None = None
self._text_win: curses.window | None = None
self._text = character_name or generate_name()
def start(self) -> None:
@@ -1,16 +1,16 @@
import curses
import functools
from collections.abc import Sequence
from typing import Any
from pqcli.config import Race
from pqcli.ui.curses.util import KEYS_CANCEL, Choice
from pqcli.ui.curses.views.menu_view import MenuView
class ChooseCharacterRaceView(MenuView):
class ChooseCharacterRaceView(MenuView[Race]):
def __init__(
self,
screen: Any,
screen: curses.window,
races: Sequence[Race],
race: Race | None = None,
) -> None:
@@ -1,8 +1,9 @@
import curses
import curses.ascii
from typing import Any
from pqcli.config import PRIME_STATS
from pqcli.mechanic import StatsBuilder
from pqcli.mechanic import Stats, StatsBuilder
from pqcli.random import RngState
from pqcli.ui.curses.event_handler import EventHandler
from pqcli.ui.curses.util import KEYS_CANCEL, KEYS_CYCLE, KEYS_DOWN, KEYS_UP
from pqcli.ui.curses.views.base_view import BaseView
@@ -10,15 +11,15 @@ from pqcli.ui.curses.widgets.focusable import focus_standout
class ChooseCharacterStatsView(BaseView):
def __init__(self, screen: Any) -> None:
def __init__(self, screen: curses.window) -> None:
super().__init__(screen)
self.on_cancel = EventHandler()
self.on_confirm = EventHandler()
self.on_cancel = EventHandler[[]]()
self.on_confirm = EventHandler[[Stats, RngState]]()
self._active_widget = 0
self._win: Any | None = None
self._stats_win: Any | None = None
self._win: curses.window | None = None
self._stats_win: curses.window | None = None
self._stats_builder = StatsBuilder()
self._stats = self._stats_builder.roll()
+3 -3
View File
@@ -2,7 +2,7 @@ import argparse
import curses
import curses.ascii
import datetime
from typing import Any, Protocol, runtime_checkable
from typing import Protocol, runtime_checkable
from pqcli.game_data import GameData
from pqcli.mechanic import Player, SignalMixin, Simulation
@@ -44,14 +44,14 @@ class PageScrollable(Protocol):
class GameView(BaseView):
def __init__(
self,
screen: Any,
screen: curses.window,
roster: Roster,
player: Player,
args: argparse.Namespace,
data: GameData,
) -> None:
super().__init__(screen)
self.on_exit = EventHandler()
self.on_exit = EventHandler[[]]()
self._roster = roster
self._player = player
@@ -1,4 +1,4 @@
from typing import Any
import curses
from pqcli.mechanic import Player, StatType
@@ -7,7 +7,13 @@ from .progress_bar_window import DataTableProgressBarWindow
class CharacterSheetWindow(DataTableProgressBarWindow):
def __init__(
self, player: Player, parent: Any, h: int, w: int, y: int, x: int
self,
player: Player,
parent: curses.window,
h: int,
w: int,
y: int,
x: int,
) -> None:
super().__init__(
parent,
@@ -1,4 +1,4 @@
from typing import Any
import curses
from pqcli.mechanic import EquipmentType, Player
from pqcli.ui.curses.widgets import DataTable, Focusable, WindowWrapper
@@ -6,7 +6,13 @@ from pqcli.ui.curses.widgets import DataTable, Focusable, WindowWrapper
class EquipmentWindow(Focusable, WindowWrapper):
def __init__(
self, player: Player, parent: Any, h: int, w: int, y: int, x: int
self,
player: Player,
parent: curses.window,
h: int,
w: int,
y: int,
x: int,
) -> None:
super().__init__(parent, h, w, y, x)
self._on_focus_change += self._render
@@ -1,4 +1,4 @@
from typing import Any
import curses
from pqcli.mechanic import InventoryItem, Player
@@ -7,7 +7,13 @@ from .progress_bar_window import DataTableProgressBarWindow
class InventoryWindow(DataTableProgressBarWindow):
def __init__(
self, player: Player, parent: Any, h: int, w: int, y: int, x: int
self,
player: Player,
parent: curses.window,
h: int,
w: int,
y: int,
x: int,
) -> None:
super().__init__(
parent,
@@ -1,4 +1,4 @@
from typing import Any
import curses
from pqcli.lingo import act_name
from pqcli.mechanic import Player
@@ -10,7 +10,13 @@ class PlotWindow(ListBoxProgressBarWindow):
cutoff = 100
def __init__(
self, player: Player, parent: Any, h: int, w: int, y: int, x: int
self,
player: Player,
parent: curses.window,
h: int,
w: int,
y: int,
x: int,
) -> None:
super().__init__(
parent, h, w, y, x, " Plot Development ", show_time=True
@@ -1,5 +1,4 @@
import curses
from typing import Any
from pqcli.ui.curses.widgets import (
DataTable,
@@ -13,7 +12,7 @@ from pqcli.ui.curses.widgets import (
class ProgressBarWindow(Focusable, WindowWrapper):
def __init__(
self,
parent: Any,
parent: curses.window,
h: int,
w: int,
y: int,
@@ -82,7 +81,7 @@ class ProgressBarWindow(Focusable, WindowWrapper):
class DataTableProgressBarWindow(ProgressBarWindow):
def __init__(
self,
parent: Any,
parent: curses.window,
h: int,
w: int,
y: int,
@@ -123,7 +122,7 @@ class DataTableProgressBarWindow(ProgressBarWindow):
class ListBoxProgressBarWindow(ProgressBarWindow):
def __init__(
self,
parent: Any,
parent: curses.window,
h: int,
w: int,
y: int,
@@ -1,4 +1,4 @@
from typing import Any
import curses
from pqcli.mechanic import Player
@@ -9,7 +9,13 @@ class QuestBookWindow(ListBoxProgressBarWindow):
cutoff = 100
def __init__(
self, player: Player, parent: Any, h: int, w: int, y: int, x: int
self,
player: Player,
parent: curses.window,
h: int,
w: int,
y: int,
x: int,
) -> None:
super().__init__(parent, h, w, y, x, " Quests ", show_time=True)
self._on_focus_change += self._render
@@ -1,4 +1,4 @@
from typing import Any
import curses
from pqcli.lingo import to_roman
from pqcli.mechanic import Player, Spell
@@ -7,7 +7,13 @@ from pqcli.ui.curses.widgets import DataTable, Focusable, WindowWrapper
class SpellBookWindow(Focusable, WindowWrapper):
def __init__(
self, player: Player, parent: Any, h: int, w: int, y: int, x: int
self,
player: Player,
parent: curses.window,
h: int,
w: int,
y: int,
x: int,
) -> None:
super().__init__(parent, h, w, y, x)
self._on_focus_change += self._render
@@ -1,4 +1,4 @@
from typing import Any
import curses
from pqcli.mechanic import Player
from pqcli.ui.curses.widgets import (
@@ -11,7 +11,13 @@ from pqcli.ui.curses.widgets import (
class TaskProgressWindow(Focusable, WindowWrapper):
def __init__(
self, player: Player, parent: Any, h: int, w: int, y: int, x: int
self,
player: Player,
parent: curses.window,
h: int,
w: int,
y: int,
x: int,
) -> None:
super().__init__(parent, h, w, y, x)
+8 -5
View File
@@ -1,19 +1,22 @@
from typing import Any
import curses
from typing import Generic, TypeVar
from pqcli.ui.curses.event_handler import EventHandler
from pqcli.ui.curses.util import Choice
from pqcli.ui.curses.views.base_view import BaseView
from pqcli.ui.curses.widgets import Menu
ItemT = TypeVar("ItemT")
class MenuView(BaseView):
class MenuView(BaseView, Generic[ItemT]):
def __init__(
self, screen: Any, title: str, active_choice: int = 0
self, screen: curses.window, title: str, active_choice: int = 0
) -> None:
super().__init__(screen)
self.on_cancel = EventHandler()
self.on_confirm = EventHandler()
self.on_cancel = EventHandler[[]]()
self.on_confirm = EventHandler[[ItemT]]()
self._title = f"{title}:"
self._choices: list[Choice] = []
+5 -6
View File
@@ -1,5 +1,4 @@
import curses
from typing import Any
from pqcli.ui.curses.colors import COLOR_LOGO, COLOR_LOGO_ALT, has_colors
from pqcli.ui.curses.event_handler import EventHandler
@@ -52,13 +51,13 @@ class MainMenu(Menu):
class RosterView(BaseView):
def __init__(self, screen: Any) -> None:
def __init__(self, screen: curses.window) -> None:
super().__init__(screen)
self.on_create = EventHandler()
self.on_play = EventHandler()
self.on_delete = EventHandler()
self.on_quit = EventHandler()
self.on_create = EventHandler[[]]()
self.on_play = EventHandler[[]]()
self.on_delete = EventHandler[[]]()
self.on_quit = EventHandler[[]]()
self.choices: list[Choice] = [
Choice(
+2 -3
View File
@@ -1,5 +1,4 @@
import curses
from typing import Any
class Widget:
@@ -12,10 +11,10 @@ class Widget:
class WindowWrapper(Widget):
def __init__(
self, parent: Any | None, h: int, w: int, y: int, x: int
self, parent: curses.window | None, h: int, w: int, y: int, x: int
) -> None:
self._parent = parent
self._win: Any | None
self._win: curses.window | None
if not parent:
self._win = None
+2 -4
View File
@@ -1,15 +1,14 @@
import curses
from typing import Any
from pqcli.ui.curses.colors import COLOR_HIGHLIGHT, has_colors
from .scrollable import Scrollable
class DataTable(Scrollable):
class DataTable(Scrollable[tuple[str, str]]):
def __init__(
self,
parent: Any,
parent: curses.window | None,
h: int,
w: int,
y: int,
@@ -19,7 +18,6 @@ class DataTable(Scrollable):
super().__init__(parent, h, w, y, x)
self._align_right = align_right
self._items: list[tuple[str, str]]
self._selected: int | None = None
def add(self, text: str, value: str) -> None:
+7 -3
View File
@@ -8,7 +8,9 @@ from pqcli.ui.curses.event_handler import EventHandler
@contextlib.contextmanager
def focus_standout(focused: bool, win: Any) -> Generator:
def focus_standout(
focused: bool, win: curses.window
) -> Generator[None, None, None]:
if focused:
if has_colors():
win.attron(curses.color_pair(COLOR_FOCUSED))
@@ -25,7 +27,7 @@ def focus_standout(focused: bool, win: Any) -> Generator:
class Focusable:
def __init__(self, *args: Any, **kwargs: Any) -> None:
self._focused = False
self._on_focus_change = EventHandler()
self._on_focus_change = EventHandler[[]]()
super().__init__(*args, **kwargs)
@property
@@ -38,6 +40,8 @@ class Focusable:
self._on_focus_change()
@contextlib.contextmanager
def focus_standout(self, win: Any) -> Generator:
def focus_standout(
self, win: curses.window
) -> Generator[None, None, None]:
with focus_standout(self._focused, win):
yield
+4 -5
View File
@@ -1,15 +1,14 @@
import curses
from typing import Any
from pqcli.ui.curses.colors import COLOR_HIGHLIGHT, has_colors
from .scrollable import Scrollable
class ListBox(Scrollable):
_items: list[str]
def __init__(self, parent: Any, h: int, w: int, y: int, x: int) -> None:
class ListBox(Scrollable[str]):
def __init__(
self, parent: curses.window | None, h: int, w: int, y: int, x: int
) -> None:
super().__init__(parent, h, w, y, x)
self._selected: int | None = None
+1 -2
View File
@@ -1,6 +1,5 @@
import curses
import curses.ascii
from typing import Any
from pqcli.ui.curses.util import KEYS_CYCLE, KEYS_DOWN, KEYS_UP, Choice
from pqcli.ui.curses.widgets.focusable import focus_standout
@@ -29,7 +28,7 @@ class Menu(Widget):
w = max(map(len, all_lines)) + 1
h = len(all_lines) + 1
pad = curses.newpad(h, w)
self._pad: Any | None = pad
self._pad: curses.window | None = pad
for y, line in enumerate(self._header_lines):
pad.move(y, 0)
+7 -2
View File
@@ -1,6 +1,5 @@
import curses
import datetime
from typing import Any
from pqcli.lingo import format_timespan
from pqcli.ui.curses.colors import COLOR_PROGRESSBAR, has_colors
@@ -9,7 +8,13 @@ from pqcli.ui.curses.widgets.base import WindowWrapper
class ProgressBar(WindowWrapper):
def __init__(
self, parent: Any, h: int, w: int, y: int, x: int, show_time: bool
self,
parent: curses.window | None,
h: int,
w: int,
y: int,
x: int,
show_time: bool,
) -> None:
super().__init__(parent, h, w, y, x)
+9 -5
View File
@@ -1,5 +1,5 @@
import curses
from typing import Any
from typing import Generic, TypeVar
from pqcli.ui.curses.colors import (
COLOR_SCROLLBAR_THUMB,
@@ -9,13 +9,17 @@ from pqcli.ui.curses.colors import (
from .base import WindowWrapper
ItemT = TypeVar("ItemT")
class Scrollable(WindowWrapper):
def __init__(self, parent: Any, h: int, w: int, y: int, x: int) -> None:
class Scrollable(WindowWrapper, Generic[ItemT]):
def __init__(
self, parent: curses.window | None, h: int, w: int, y: int, x: int
) -> None:
super().__init__(parent, h, w, y, x)
self._scroll_pos = 0
self._pad: Any | None = curses.newpad(1, 1)
self._items: list[Any] = []
self._pad: curses.window | None = curses.newpad(1, 1)
self._items: list[ItemT] = []
def clear(self) -> None:
self._items = []