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

+2 -2
View File
@@ -1,7 +1,7 @@
import argparse import argparse
import sys import sys
from pathlib import Path from pathlib import Path
from typing import Any from typing import TextIO
from xdg_base_dirs import xdg_config_home from xdg_base_dirs import xdg_config_home
@@ -84,7 +84,7 @@ def parse_args() -> argparse.Namespace:
return parser.parse_args() return parser.parse_args()
def list_players(roster: Roster, file: Any | None = None) -> None: def list_players(roster: Roster, file: TextIO | None = None) -> None:
for i, player in enumerate(roster.players, start=1): for i, player in enumerate(roster.players, start=1):
print(f"{i}. {player.name}", file=file) print(f"{i}. {player.name}", file=file)
+29 -22
View File
@@ -4,7 +4,7 @@ from collections.abc import Callable, Sequence
from dataclasses import dataclass, fields from dataclasses import dataclass, fields
from importlib import resources from importlib import resources
from pathlib import Path from pathlib import Path
from typing import Any, cast from typing import Any
from pqcli.config import ( from pqcli.config import (
Class, Class,
@@ -15,6 +15,11 @@ from pqcli.config import (
StatType, StatType,
) )
if sys.version_info >= (3, 11):
from importlib.resources.abc import Traversable
else:
from importlib.abc import Traversable
TARGET_PLACEHOLDER = "{target}" TARGET_PLACEHOLDER = "{target}"
@@ -56,8 +61,8 @@ class _Context:
def _check_keys( def _check_keys(
ctx: _Context, where: str, obj: Any, keys: Sequence[str] ctx: _Context, where: str, obj: object, keys: Sequence[str]
) -> dict[str, Any]: ) -> dict[str, object]:
if not isinstance(obj, dict): if not isinstance(obj, dict):
raise ctx.error(f"{where} must be an object") raise ctx.error(f"{where} must be an object")
missing = [key for key in keys if key not in obj] missing = [key for key in keys if key not in obj]
@@ -69,39 +74,39 @@ def _check_keys(
return obj return obj
def _str(ctx: _Context, where: str, value: Any) -> str: def _str(ctx: _Context, where: str, value: object) -> str:
if not isinstance(value, str): if not isinstance(value, str):
raise ctx.error(f"{where} must be a string") raise ctx.error(f"{where} must be a string")
return value return value
def _int(ctx: _Context, where: str, value: Any) -> int: def _int(ctx: _Context, where: str, value: object) -> int:
if isinstance(value, bool) or not isinstance(value, int): if isinstance(value, bool) or not isinstance(value, int):
raise ctx.error(f"{where} must be an integer") raise ctx.error(f"{where} must be an integer")
return value return value
def _stat(ctx: _Context, where: str, value: Any) -> StatType: def _stat(ctx: _Context, where: str, value: object) -> StatType:
_str(ctx, where, value) name = _str(ctx, where, value)
for stat in StatType: for stat in StatType:
if value.lower() in (stat.value.lower(), stat.name): if name.lower() in (stat.value.lower(), stat.name):
return stat return stat
allowed = ", ".join(stat.value for stat in StatType) allowed = ", ".join(stat.value for stat in StatType)
raise ctx.error(f"{where}: unknown stat {value!r} (use one of {allowed})") raise ctx.error(f"{where}: unknown stat {value!r} (use one of {allowed})")
def _parse_str(ctx: _Context, where: str, item: Any) -> str: def _parse_str(ctx: _Context, where: str, item: object) -> str:
return _str(ctx, where, item) return _str(ctx, where, item)
def _parse_verb(ctx: _Context, where: str, item: Any) -> str: def _parse_verb(ctx: _Context, where: str, item: object) -> str:
verb = _str(ctx, where, item) verb = _str(ctx, where, item)
if TARGET_PLACEHOLDER not in verb: if TARGET_PLACEHOLDER not in verb:
raise ctx.error(f"{where} must contain {TARGET_PLACEHOLDER}") raise ctx.error(f"{where} must contain {TARGET_PLACEHOLDER}")
return verb return verb
def _parse_modifier(ctx: _Context, where: str, item: Any) -> Modifier: def _parse_modifier(ctx: _Context, where: str, item: object) -> Modifier:
obj = _check_keys(ctx, where, item, ["name", "quality"]) obj = _check_keys(ctx, where, item, ["name", "quality"])
return Modifier( return Modifier(
_str(ctx, f"{where}.name", obj["name"]), _str(ctx, f"{where}.name", obj["name"]),
@@ -109,7 +114,7 @@ def _parse_modifier(ctx: _Context, where: str, item: Any) -> Modifier:
) )
def _parse_preset(ctx: _Context, where: str, item: Any) -> EquipmentPreset: def _parse_preset(ctx: _Context, where: str, item: object) -> EquipmentPreset:
obj = _check_keys(ctx, where, item, ["name", "quality"]) obj = _check_keys(ctx, where, item, ["name", "quality"])
return EquipmentPreset( return EquipmentPreset(
_str(ctx, f"{where}.name", obj["name"]), _str(ctx, f"{where}.name", obj["name"]),
@@ -117,7 +122,7 @@ def _parse_preset(ctx: _Context, where: str, item: Any) -> EquipmentPreset:
) )
def _parse_monster(ctx: _Context, where: str, item: Any) -> Monster: def _parse_monster(ctx: _Context, where: str, item: object) -> Monster:
obj = _check_keys(ctx, where, item, ["name", "level", "item"]) obj = _check_keys(ctx, where, item, ["name", "level", "item"])
loot = obj["item"] loot = obj["item"]
return Monster( return Monster(
@@ -127,13 +132,13 @@ def _parse_monster(ctx: _Context, where: str, item: Any) -> Monster:
) )
def _parse_attrs(ctx: _Context, where: str, value: Any) -> list[StatType]: def _parse_attrs(ctx: _Context, where: str, value: object) -> list[StatType]:
if not isinstance(value, list): if not isinstance(value, list):
raise ctx.error(f"{where} must be a list of stats") raise ctx.error(f"{where} must be a list of stats")
return [_stat(ctx, f"{where}[{i}]", stat) for i, stat in enumerate(value)] return [_stat(ctx, f"{where}[{i}]", stat) for i, stat in enumerate(value)]
def _parse_race(ctx: _Context, where: str, item: Any) -> Race: def _parse_race(ctx: _Context, where: str, item: object) -> Race:
obj = _check_keys(ctx, where, item, ["name", "attr"]) obj = _check_keys(ctx, where, item, ["name", "attr"])
return Race( return Race(
_str(ctx, f"{where}.name", obj["name"]), _str(ctx, f"{where}.name", obj["name"]),
@@ -141,7 +146,7 @@ def _parse_race(ctx: _Context, where: str, item: Any) -> Race:
) )
def _parse_class(ctx: _Context, where: str, item: Any) -> Class: def _parse_class(ctx: _Context, where: str, item: object) -> Class:
obj = _check_keys(ctx, where, item, ["name", "attr"]) obj = _check_keys(ctx, where, item, ["name", "attr"])
return Class( return Class(
_str(ctx, f"{where}.name", obj["name"]), _str(ctx, f"{where}.name", obj["name"]),
@@ -149,7 +154,7 @@ def _parse_class(ctx: _Context, where: str, item: Any) -> Class:
) )
_PARSERS: dict[str, Callable[[_Context, str, Any], Any]] = { _PARSERS: dict[str, Callable[[_Context, str, object], object]] = {
"spells": _parse_str, "spells": _parse_str,
"offense_attrib": _parse_modifier, "offense_attrib": _parse_modifier,
"defense_attrib": _parse_modifier, "defense_attrib": _parse_modifier,
@@ -172,7 +177,7 @@ _PARSERS: dict[str, Callable[[_Context, str, Any], Any]] = {
assert set(_PARSERS) == set(DATA_FILES) assert set(_PARSERS) == set(DATA_FILES)
def _parse(name: str, path: str, text: str) -> tuple[Any, ...]: def _parse(name: str, path: str, text: str) -> tuple[object, ...]:
ctx = _Context(path) ctx = _Context(path)
try: try:
value = json.loads(text) value = json.loads(text)
@@ -186,13 +191,13 @@ def _parse(name: str, path: str, text: str) -> tuple[Any, ...]:
return tuple(parser(ctx, f"[{i}]", item) for i, item in enumerate(value)) return tuple(parser(ctx, f"[{i}]", item) for i, item in enumerate(value))
def _builtin_dir() -> Any: def _builtin_dir() -> Traversable:
return resources.files("pqcli") / "data" return resources.files("pqcli") / "data"
def _read_text(path: Any, display: str) -> str: def _read_text(path: Traversable, display: str) -> str:
try: try:
return cast(str, path.read_text(encoding="utf-8")) return path.read_text(encoding="utf-8")
except UnicodeDecodeError as ex: except UnicodeDecodeError as ex:
raise DataError(f"{display}: not valid UTF-8: {ex}") from ex raise DataError(f"{display}: not valid UTF-8: {ex}") from ex
except OSError as ex: except OSError as ex:
@@ -219,11 +224,13 @@ def load_game_data(override_dir: Path | None = None) -> GameData:
file=sys.stderr, file=sys.stderr,
) )
# The element type of each tuple depends on the GameData field it
# fills, so it is only checked when GameData receives it.
values: dict[str, tuple[Any, ...]] = {} values: dict[str, tuple[Any, ...]] = {}
for name in DATA_FILES: for name in DATA_FILES:
if name in overrides: if name in overrides:
assert override_dir is not None assert override_dir is not None
source: Any = override_dir / f"{name}.json" source: Traversable = override_dir / f"{name}.json"
display = str(source) display = str(source)
else: else:
source = _builtin_dir() / f"{name}.json" source = _builtin_dir() / f"{name}.json"
+19 -25
View File
@@ -7,7 +7,7 @@ import math
from collections import defaultdict from collections import defaultdict
from collections.abc import Callable, Generator, Iterator, Sequence from collections.abc import Callable, Generator, Iterator, Sequence
from dataclasses import dataclass from dataclasses import dataclass
from typing import Any, cast from typing import Any
from pqcli import random from pqcli import random
from pqcli.config import ( from pqcli.config import (
@@ -37,7 +37,7 @@ from pqcli.random import Alea, RngState
logger = logging.getLogger(__name__) logger = logging.getLogger(__name__)
_SIGNALS: dict[tuple["SignalMixin", str], list[Callable[..., Any]]] = ( _SIGNALS: dict[tuple["SignalMixin", str], list[Callable[..., object]]] = (
defaultdict(list) defaultdict(list)
) )
@@ -55,17 +55,19 @@ class SignalMixin:
finally: finally:
SignalMixin._muted -= 1 SignalMixin._muted -= 1
def emit(self, signal_name: str, *user_data: Any) -> None: def emit(self, signal_name: str, *user_data: object) -> None:
if SignalMixin._muted: if SignalMixin._muted:
return return
for callback in _SIGNALS[self, signal_name]: for callback in _SIGNALS[self, signal_name]:
callback(*user_data) callback(*user_data)
def connect(self, signal_name: str, callback: Callable[..., Any]) -> None: def connect(
self, signal_name: str, callback: Callable[..., object]
) -> None:
_SIGNALS[self, signal_name].append(callback) _SIGNALS[self, signal_name].append(callback)
def disconnect( def disconnect(
self, signal_name: str, callback: Callable[..., Any] self, signal_name: str, callback: Callable[..., object]
) -> None: ) -> None:
try: try:
idx = _SIGNALS[self, signal_name].index(callback) idx = _SIGNALS[self, signal_name].index(callback)
@@ -383,7 +385,7 @@ class Player(SignalMixin):
self.task: BaseTask | None = None self.task: BaseTask | None = None
self.queue: list[BaseTask] = [] self.queue: list[BaseTask] = []
def __setstate__(self, obj: Any) -> None: def __setstate__(self, obj: dict[str, Any]) -> None:
self.__dict__.update(obj) self.__dict__.update(obj)
self.elapsed = obj.get("elapsed", 0.0) self.elapsed = obj.get("elapsed", 0.0)
if "rng" not in obj: if "rng" not in obj:
@@ -853,38 +855,30 @@ class Simulation:
def special_item(data: GameData) -> str: def special_item(data: GameData) -> str:
return ( return interesting_item(data) + " of " + random.choice(data.item_ofs)
interesting_item(data)
+ " of "
+ cast(str, random.choice(data.item_ofs))
)
def interesting_item(data: GameData) -> str: def interesting_item(data: GameData) -> str:
return ( return random.choice(data.item_attrib) + " " + random.choice(data.specials)
cast(str, random.choice(data.item_attrib))
+ " "
+ cast(str, random.choice(data.specials))
)
def boring_item(data: GameData) -> str: def boring_item(data: GameData) -> str:
return cast(str, random.choice(data.boring_items)) return random.choice(data.boring_items)
def impressive_guy(data: GameData) -> str: def impressive_guy(data: GameData) -> str:
if random.below(2): if random.below(2):
title = cast(str, random.choice(data.impressive_titles)) title = random.choice(data.impressive_titles)
race = cast(Race, random.choice(data.races)) race = random.choice(data.races)
return f"the {title} of the {plural(race.name)}" return f"the {title} of the {plural(race.name)}"
title = cast(str, random.choice(data.impressive_titles)) title = random.choice(data.impressive_titles)
return f"{title} {generate_name()} of {generate_name()}" return f"{title} {generate_name()} of {generate_name()}"
def unnamed_monster(data: GameData, level: int, iterations: int) -> Monster: def unnamed_monster(data: GameData, level: int, iterations: int) -> Monster:
result = cast(Monster, random.choice(data.monsters)) result = random.choice(data.monsters)
for _ in range(iterations): for _ in range(iterations):
alternative = cast(Monster, random.choice(data.monsters)) alternative = random.choice(data.monsters)
if abs(level - alternative.level) < abs(level - result.level): if abs(level - alternative.level) < abs(level - result.level):
result = alternative result = alternative
return result return result
@@ -898,9 +892,9 @@ def named_monster(data: GameData, level: int) -> str:
def pick_equipment( def pick_equipment(
source: Sequence[EquipmentPreset], goal: int source: Sequence[EquipmentPreset], goal: int
) -> EquipmentPreset: ) -> EquipmentPreset:
result = cast(EquipmentPreset, random.choice(source)) result = random.choice(source)
for _ in range(5): for _ in range(5):
alternative = cast(EquipmentPreset, random.choice(source)) alternative = random.choice(source)
if abs(goal - alternative.quality) < abs(goal - result.quality): if abs(goal - alternative.quality) < abs(goal - result.quality):
result = alternative result = alternative
return result return result
@@ -986,7 +980,7 @@ def monster_task(
verb = ( verb = (
data.combat_verbs[0] data.combat_verbs[0]
if len(data.combat_verbs) == 1 if len(data.combat_verbs) == 1
else cast(str, random.choice(data.combat_verbs)) else random.choice(data.combat_verbs)
) )
description = verb.replace(TARGET_PLACEHOLDER, result) description = verb.replace(TARGET_PLACEHOLDER, result)
return KillTask(description, duration, monster=monster) return KillTask(description, duration, monster=monster)
+5 -3
View File
@@ -1,10 +1,12 @@
import contextlib import contextlib
import time import time
from collections.abc import Callable, Generator, Sequence from collections.abc import Callable, Generator, Sequence
from typing import Any from typing import TypeVar
RngState = tuple[float, float, float, int] RngState = tuple[float, float, float, int]
T = TypeVar("T")
_2_POW_32 = 0x100000000 _2_POW_32 = 0x100000000
_2_POW_MINUS_32 = 2.3283064365386963e-10 _2_POW_MINUS_32 = 2.3283064365386963e-10
@@ -102,11 +104,11 @@ def using(rng: Alea) -> Generator[None, None, None]:
_current = previous _current = previous
def choice(source: Sequence[Any]) -> Any: def choice(source: Sequence[T]) -> T:
return source[below(len(source))] return source[below(len(source))]
def choice_low(source: Sequence[Any]) -> Any: def choice_low(source: Sequence[T]) -> T:
return source[below_low(len(source))] return source[below_low(len(source))]
+8 -4
View File
@@ -6,7 +6,8 @@ import sys
import time import time
from collections.abc import Sequence from collections.abc import Sequence
from datetime import datetime, timedelta from datetime import datetime, timedelta
from typing import Any from types import FrameType
from typing import TypeVar
from pqcli import lingo from pqcli import lingo
from pqcli.config import PRIME_STATS from pqcli.config import PRIME_STATS
@@ -25,6 +26,9 @@ LOGO = r"""
""" """
T = TypeVar("T")
class MainMenu(enum.IntEnum): class MainMenu(enum.IntEnum):
create = 1 create = 1
play = 2 play = 2
@@ -43,7 +47,7 @@ class BasicUserInterface(BaseUserInterface):
) -> None: ) -> None:
super().__init__(roster, player, args, data) 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") logging.info("Quitting")
if self.args.use_saves: if self.args.use_saves:
self.roster.save() self.roster.save()
@@ -217,9 +221,9 @@ class BasicUserInterface(BaseUserInterface):
def menu( def menu(
self, self,
options: Sequence[tuple[Any, str]], options: Sequence[tuple[T, str]],
title: str | None = None, title: str | None = None,
) -> Any: ) -> T:
print() print()
if title: if title:
print(f"{title}:") print(f"{title}:")
+2 -2
View File
@@ -3,7 +3,7 @@ import curses
import os import os
import signal import signal
import time import time
from typing import Any from types import FrameType
from pqcli import lingo from pqcli import lingo
from pqcli.config import Class, Race from pqcli.config import Class, Race
@@ -68,7 +68,7 @@ class CursesUserInterface(BaseUserInterface):
curses.init_pair(COLOR_PROGRESSBAR, 0, 9) curses.init_pair(COLOR_PROGRESSBAR, 0, 9)
curses.init_pair(COLOR_HIGHLIGHT, 3, -1) 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: if self.args.use_saves:
self.roster.save() self.roster.save()
exit(0) exit(0)
+7 -5
View File
@@ -1,15 +1,17 @@
from collections.abc import Callable 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: 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: for callback in self.callbacks:
callback(*args, **kwargs) callback(*args, **kwargs)
def __iadd__(self, callback: Callable[..., Any]) -> "EventHandler": def __iadd__(self, callback: Callable[P, object]) -> "EventHandler[P]":
self.callbacks.append(callback) self.callbacks.append(callback)
return self return self
+5 -3
View File
@@ -2,7 +2,7 @@ import curses
import curses.ascii import curses.ascii
from collections.abc import Callable, Iterable from collections.abc import Callable, Iterable
from dataclasses import dataclass from dataclasses import dataclass
from typing import Any, TypeVar, overload from typing import TypeVar, overload
KEYS_CYCLE = {curses.ascii.TAB} KEYS_CYCLE = {curses.ascii.TAB}
KEYS_DOWN = set(map(ord, "jJ")) | {curses.KEY_DOWN} KEYS_DOWN = set(map(ord, "jJ")) | {curses.KEY_DOWN}
@@ -16,7 +16,7 @@ KEYS_CANCEL = set(map(ord, "qQ")) | {curses.ascii.ESC}
class Choice: class Choice:
keys: list[int] keys: list[int]
desc: str desc: str
callback: Callable[..., Any] callback: Callable[[], object]
ItemT = TypeVar("ItemT") 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[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) return next(iter(source), default)
+2 -2
View File
@@ -1,8 +1,8 @@
from typing import Any import curses
class BaseView: class BaseView:
def __init__(self, screen: Any) -> None: def __init__(self, screen: curses.window) -> None:
self.screen = screen self.screen = screen
def start(self) -> None: def start(self) -> None:
@@ -1,14 +1,17 @@
import curses
import functools import functools
from typing import Any
from pqcli.lingo import act_name, to_roman from pqcli.lingo import act_name, to_roman
from pqcli.mechanic import Player
from pqcli.roster import Roster from pqcli.roster import Roster
from pqcli.ui.curses.util import KEYS_CANCEL, Choice from pqcli.ui.curses.util import KEYS_CANCEL, Choice
from pqcli.ui.curses.views.menu_view import MenuView from pqcli.ui.curses.views.menu_view import MenuView
class ChooseCharacterView(MenuView): class ChooseCharacterView(MenuView[Player]):
def __init__(self, screen: Any, roster: Roster, title: str) -> None: def __init__(
self, screen: curses.window, roster: Roster, title: str
) -> None:
super().__init__(screen, title) super().__init__(screen, title)
for y, player in enumerate(roster.players, 1): 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.event_handler import EventHandler
from pqcli.ui.curses.util import Choice from pqcli.ui.curses.util import Choice
@@ -8,12 +8,12 @@ from .base_view import BaseView
class ConfirmView(BaseView): class ConfirmView(BaseView):
def __init__(self, screen: Any, title: str) -> None: def __init__(self, screen: curses.window, title: str) -> None:
super().__init__(screen) super().__init__(screen)
self.title = f"{title}:" self.title = f"{title}:"
self.on_cancel = EventHandler() self.on_cancel = EventHandler[[]]()
self.on_confirm = EventHandler() self.on_confirm = EventHandler[[]]()
self.choices: list[Choice] = [ self.choices: list[Choice] = [
Choice( Choice(
@@ -1,16 +1,16 @@
import curses
import functools import functools
from collections.abc import Sequence from collections.abc import Sequence
from typing import Any
from pqcli.config import Class from pqcli.config import Class
from pqcli.ui.curses.util import KEYS_CANCEL, Choice from pqcli.ui.curses.util import KEYS_CANCEL, Choice
from pqcli.ui.curses.views.menu_view import MenuView from pqcli.ui.curses.views.menu_view import MenuView
class ChooseCharacterClassView(MenuView): class ChooseCharacterClassView(MenuView[Class]):
def __init__( def __init__(
self, self,
screen: Any, screen: curses.window,
classes: Sequence[Class], classes: Sequence[Class],
class_: Class | None = None, class_: Class | None = None,
) -> None: ) -> None:
@@ -1,6 +1,5 @@
import curses import curses
import curses.ascii import curses.ascii
from typing import Any
from pqcli.mechanic import generate_name from pqcli.mechanic import generate_name
from pqcli.ui.curses.event_handler import EventHandler from pqcli.ui.curses.event_handler import EventHandler
@@ -10,15 +9,17 @@ from pqcli.ui.curses.widgets.focusable import focus_standout
class ChooseCharacterNameView(BaseView): 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) super().__init__(screen)
self.on_cancel = EventHandler() self.on_cancel = EventHandler[[]]()
self.on_confirm = EventHandler() self.on_confirm = EventHandler[[str]]()
self._active_widget = 0 self._active_widget = 0
self._win: Any | None = None self._win: curses.window | None = None
self._text_win: Any | None = None self._text_win: curses.window | None = None
self._text = character_name or generate_name() self._text = character_name or generate_name()
def start(self) -> None: def start(self) -> None:
@@ -1,16 +1,16 @@
import curses
import functools import functools
from collections.abc import Sequence from collections.abc import Sequence
from typing import Any
from pqcli.config import Race from pqcli.config import Race
from pqcli.ui.curses.util import KEYS_CANCEL, Choice from pqcli.ui.curses.util import KEYS_CANCEL, Choice
from pqcli.ui.curses.views.menu_view import MenuView from pqcli.ui.curses.views.menu_view import MenuView
class ChooseCharacterRaceView(MenuView): class ChooseCharacterRaceView(MenuView[Race]):
def __init__( def __init__(
self, self,
screen: Any, screen: curses.window,
races: Sequence[Race], races: Sequence[Race],
race: Race | None = None, race: Race | None = None,
) -> None: ) -> None:
@@ -1,8 +1,9 @@
import curses
import curses.ascii import curses.ascii
from typing import Any
from pqcli.config import PRIME_STATS 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.event_handler import EventHandler
from pqcli.ui.curses.util import KEYS_CANCEL, KEYS_CYCLE, KEYS_DOWN, KEYS_UP from pqcli.ui.curses.util import KEYS_CANCEL, KEYS_CYCLE, KEYS_DOWN, KEYS_UP
from pqcli.ui.curses.views.base_view import BaseView 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): class ChooseCharacterStatsView(BaseView):
def __init__(self, screen: Any) -> None: def __init__(self, screen: curses.window) -> None:
super().__init__(screen) super().__init__(screen)
self.on_cancel = EventHandler() self.on_cancel = EventHandler[[]]()
self.on_confirm = EventHandler() self.on_confirm = EventHandler[[Stats, RngState]]()
self._active_widget = 0 self._active_widget = 0
self._win: Any | None = None self._win: curses.window | None = None
self._stats_win: Any | None = None self._stats_win: curses.window | None = None
self._stats_builder = StatsBuilder() self._stats_builder = StatsBuilder()
self._stats = self._stats_builder.roll() self._stats = self._stats_builder.roll()
+3 -3
View File
@@ -2,7 +2,7 @@ import argparse
import curses import curses
import curses.ascii import curses.ascii
import datetime import datetime
from typing import Any, Protocol, runtime_checkable from typing import Protocol, runtime_checkable
from pqcli.game_data import GameData from pqcli.game_data import GameData
from pqcli.mechanic import Player, SignalMixin, Simulation from pqcli.mechanic import Player, SignalMixin, Simulation
@@ -44,14 +44,14 @@ class PageScrollable(Protocol):
class GameView(BaseView): class GameView(BaseView):
def __init__( def __init__(
self, self,
screen: Any, screen: curses.window,
roster: Roster, roster: Roster,
player: Player, player: Player,
args: argparse.Namespace, args: argparse.Namespace,
data: GameData, data: GameData,
) -> None: ) -> None:
super().__init__(screen) super().__init__(screen)
self.on_exit = EventHandler() self.on_exit = EventHandler[[]]()
self._roster = roster self._roster = roster
self._player = player self._player = player
@@ -1,4 +1,4 @@
from typing import Any import curses
from pqcli.mechanic import Player, StatType from pqcli.mechanic import Player, StatType
@@ -7,7 +7,13 @@ from .progress_bar_window import DataTableProgressBarWindow
class CharacterSheetWindow(DataTableProgressBarWindow): class CharacterSheetWindow(DataTableProgressBarWindow):
def __init__( 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: ) -> None:
super().__init__( super().__init__(
parent, parent,
@@ -1,4 +1,4 @@
from typing import Any import curses
from pqcli.mechanic import EquipmentType, Player from pqcli.mechanic import EquipmentType, Player
from pqcli.ui.curses.widgets import DataTable, Focusable, WindowWrapper 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): class EquipmentWindow(Focusable, WindowWrapper):
def __init__( 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: ) -> None:
super().__init__(parent, h, w, y, x) super().__init__(parent, h, w, y, x)
self._on_focus_change += self._render self._on_focus_change += self._render
@@ -1,4 +1,4 @@
from typing import Any import curses
from pqcli.mechanic import InventoryItem, Player from pqcli.mechanic import InventoryItem, Player
@@ -7,7 +7,13 @@ from .progress_bar_window import DataTableProgressBarWindow
class InventoryWindow(DataTableProgressBarWindow): class InventoryWindow(DataTableProgressBarWindow):
def __init__( 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: ) -> None:
super().__init__( super().__init__(
parent, parent,
@@ -1,4 +1,4 @@
from typing import Any import curses
from pqcli.lingo import act_name from pqcli.lingo import act_name
from pqcli.mechanic import Player from pqcli.mechanic import Player
@@ -10,7 +10,13 @@ class PlotWindow(ListBoxProgressBarWindow):
cutoff = 100 cutoff = 100
def __init__( 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: ) -> None:
super().__init__( super().__init__(
parent, h, w, y, x, " Plot Development ", show_time=True parent, h, w, y, x, " Plot Development ", show_time=True
@@ -1,5 +1,4 @@
import curses import curses
from typing import Any
from pqcli.ui.curses.widgets import ( from pqcli.ui.curses.widgets import (
DataTable, DataTable,
@@ -13,7 +12,7 @@ from pqcli.ui.curses.widgets import (
class ProgressBarWindow(Focusable, WindowWrapper): class ProgressBarWindow(Focusable, WindowWrapper):
def __init__( def __init__(
self, self,
parent: Any, parent: curses.window,
h: int, h: int,
w: int, w: int,
y: int, y: int,
@@ -82,7 +81,7 @@ class ProgressBarWindow(Focusable, WindowWrapper):
class DataTableProgressBarWindow(ProgressBarWindow): class DataTableProgressBarWindow(ProgressBarWindow):
def __init__( def __init__(
self, self,
parent: Any, parent: curses.window,
h: int, h: int,
w: int, w: int,
y: int, y: int,
@@ -123,7 +122,7 @@ class DataTableProgressBarWindow(ProgressBarWindow):
class ListBoxProgressBarWindow(ProgressBarWindow): class ListBoxProgressBarWindow(ProgressBarWindow):
def __init__( def __init__(
self, self,
parent: Any, parent: curses.window,
h: int, h: int,
w: int, w: int,
y: int, y: int,
@@ -1,4 +1,4 @@
from typing import Any import curses
from pqcli.mechanic import Player from pqcli.mechanic import Player
@@ -9,7 +9,13 @@ class QuestBookWindow(ListBoxProgressBarWindow):
cutoff = 100 cutoff = 100
def __init__( 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: ) -> None:
super().__init__(parent, h, w, y, x, " Quests ", show_time=True) super().__init__(parent, h, w, y, x, " Quests ", show_time=True)
self._on_focus_change += self._render self._on_focus_change += self._render
@@ -1,4 +1,4 @@
from typing import Any import curses
from pqcli.lingo import to_roman from pqcli.lingo import to_roman
from pqcli.mechanic import Player, Spell from pqcli.mechanic import Player, Spell
@@ -7,7 +7,13 @@ from pqcli.ui.curses.widgets import DataTable, Focusable, WindowWrapper
class SpellBookWindow(Focusable, WindowWrapper): class SpellBookWindow(Focusable, WindowWrapper):
def __init__( 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: ) -> None:
super().__init__(parent, h, w, y, x) super().__init__(parent, h, w, y, x)
self._on_focus_change += self._render self._on_focus_change += self._render
@@ -1,4 +1,4 @@
from typing import Any import curses
from pqcli.mechanic import Player from pqcli.mechanic import Player
from pqcli.ui.curses.widgets import ( from pqcli.ui.curses.widgets import (
@@ -11,7 +11,13 @@ from pqcli.ui.curses.widgets import (
class TaskProgressWindow(Focusable, WindowWrapper): class TaskProgressWindow(Focusable, WindowWrapper):
def __init__( 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: ) -> None:
super().__init__(parent, h, w, y, x) 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.event_handler import EventHandler
from pqcli.ui.curses.util import Choice from pqcli.ui.curses.util import Choice
from pqcli.ui.curses.views.base_view import BaseView from pqcli.ui.curses.views.base_view import BaseView
from pqcli.ui.curses.widgets import Menu from pqcli.ui.curses.widgets import Menu
ItemT = TypeVar("ItemT")
class MenuView(BaseView):
class MenuView(BaseView, Generic[ItemT]):
def __init__( def __init__(
self, screen: Any, title: str, active_choice: int = 0 self, screen: curses.window, title: str, active_choice: int = 0
) -> None: ) -> None:
super().__init__(screen) super().__init__(screen)
self.on_cancel = EventHandler() self.on_cancel = EventHandler[[]]()
self.on_confirm = EventHandler() self.on_confirm = EventHandler[[ItemT]]()
self._title = f"{title}:" self._title = f"{title}:"
self._choices: list[Choice] = [] self._choices: list[Choice] = []
+5 -6
View File
@@ -1,5 +1,4 @@
import curses import curses
from typing import Any
from pqcli.ui.curses.colors import COLOR_LOGO, COLOR_LOGO_ALT, has_colors from pqcli.ui.curses.colors import COLOR_LOGO, COLOR_LOGO_ALT, has_colors
from pqcli.ui.curses.event_handler import EventHandler from pqcli.ui.curses.event_handler import EventHandler
@@ -52,13 +51,13 @@ class MainMenu(Menu):
class RosterView(BaseView): class RosterView(BaseView):
def __init__(self, screen: Any) -> None: def __init__(self, screen: curses.window) -> None:
super().__init__(screen) super().__init__(screen)
self.on_create = EventHandler() self.on_create = EventHandler[[]]()
self.on_play = EventHandler() self.on_play = EventHandler[[]]()
self.on_delete = EventHandler() self.on_delete = EventHandler[[]]()
self.on_quit = EventHandler() self.on_quit = EventHandler[[]]()
self.choices: list[Choice] = [ self.choices: list[Choice] = [
Choice( Choice(
+2 -3
View File
@@ -1,5 +1,4 @@
import curses import curses
from typing import Any
class Widget: class Widget:
@@ -12,10 +11,10 @@ class Widget:
class WindowWrapper(Widget): class WindowWrapper(Widget):
def __init__( 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: ) -> None:
self._parent = parent self._parent = parent
self._win: Any | None self._win: curses.window | None
if not parent: if not parent:
self._win = None self._win = None
+2 -4
View File
@@ -1,15 +1,14 @@
import curses import curses
from typing import Any
from pqcli.ui.curses.colors import COLOR_HIGHLIGHT, has_colors from pqcli.ui.curses.colors import COLOR_HIGHLIGHT, has_colors
from .scrollable import Scrollable from .scrollable import Scrollable
class DataTable(Scrollable): class DataTable(Scrollable[tuple[str, str]]):
def __init__( def __init__(
self, self,
parent: Any, parent: curses.window | None,
h: int, h: int,
w: int, w: int,
y: int, y: int,
@@ -19,7 +18,6 @@ class DataTable(Scrollable):
super().__init__(parent, h, w, y, x) super().__init__(parent, h, w, y, x)
self._align_right = align_right self._align_right = align_right
self._items: list[tuple[str, str]]
self._selected: int | None = None self._selected: int | None = None
def add(self, text: str, value: str) -> 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 @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 focused:
if has_colors(): if has_colors():
win.attron(curses.color_pair(COLOR_FOCUSED)) win.attron(curses.color_pair(COLOR_FOCUSED))
@@ -25,7 +27,7 @@ def focus_standout(focused: bool, win: Any) -> Generator:
class Focusable: class Focusable:
def __init__(self, *args: Any, **kwargs: Any) -> None: def __init__(self, *args: Any, **kwargs: Any) -> None:
self._focused = False self._focused = False
self._on_focus_change = EventHandler() self._on_focus_change = EventHandler[[]]()
super().__init__(*args, **kwargs) super().__init__(*args, **kwargs)
@property @property
@@ -38,6 +40,8 @@ class Focusable:
self._on_focus_change() self._on_focus_change()
@contextlib.contextmanager @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): with focus_standout(self._focused, win):
yield yield
+4 -5
View File
@@ -1,15 +1,14 @@
import curses import curses
from typing import Any
from pqcli.ui.curses.colors import COLOR_HIGHLIGHT, has_colors from pqcli.ui.curses.colors import COLOR_HIGHLIGHT, has_colors
from .scrollable import Scrollable from .scrollable import Scrollable
class ListBox(Scrollable): class ListBox(Scrollable[str]):
_items: list[str] def __init__(
self, parent: curses.window | None, h: int, w: int, y: int, x: int
def __init__(self, parent: Any, h: int, w: int, y: int, x: int) -> None: ) -> None:
super().__init__(parent, h, w, y, x) super().__init__(parent, h, w, y, x)
self._selected: int | None = None self._selected: int | None = None
+1 -2
View File
@@ -1,6 +1,5 @@
import curses import curses
import curses.ascii import curses.ascii
from typing import Any
from pqcli.ui.curses.util import KEYS_CYCLE, KEYS_DOWN, KEYS_UP, Choice from pqcli.ui.curses.util import KEYS_CYCLE, KEYS_DOWN, KEYS_UP, Choice
from pqcli.ui.curses.widgets.focusable import focus_standout from pqcli.ui.curses.widgets.focusable import focus_standout
@@ -29,7 +28,7 @@ class Menu(Widget):
w = max(map(len, all_lines)) + 1 w = max(map(len, all_lines)) + 1
h = len(all_lines) + 1 h = len(all_lines) + 1
pad = curses.newpad(h, w) pad = curses.newpad(h, w)
self._pad: Any | None = pad self._pad: curses.window | None = pad
for y, line in enumerate(self._header_lines): for y, line in enumerate(self._header_lines):
pad.move(y, 0) pad.move(y, 0)
+7 -2
View File
@@ -1,6 +1,5 @@
import curses import curses
import datetime import datetime
from typing import Any
from pqcli.lingo import format_timespan from pqcli.lingo import format_timespan
from pqcli.ui.curses.colors import COLOR_PROGRESSBAR, has_colors 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): class ProgressBar(WindowWrapper):
def __init__( 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: ) -> None:
super().__init__(parent, h, w, y, x) super().__init__(parent, h, w, y, x)
+9 -5
View File
@@ -1,5 +1,5 @@
import curses import curses
from typing import Any from typing import Generic, TypeVar
from pqcli.ui.curses.colors import ( from pqcli.ui.curses.colors import (
COLOR_SCROLLBAR_THUMB, COLOR_SCROLLBAR_THUMB,
@@ -9,13 +9,17 @@ from pqcli.ui.curses.colors import (
from .base import WindowWrapper 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) super().__init__(parent, h, w, y, x)
self._scroll_pos = 0 self._scroll_pos = 0
self._pad: Any | None = curses.newpad(1, 1) self._pad: curses.window | None = curses.newpad(1, 1)
self._items: list[Any] = [] self._items: list[ItemT] = []
def clear(self) -> None: def clear(self) -> None:
self._items = [] self._items = []
+2 -3
View File
@@ -2,7 +2,6 @@ import datetime
import json import json
import pickle import pickle
from pathlib import Path from pathlib import Path
from typing import Any
import pytest import pytest
@@ -18,7 +17,7 @@ from pqcli.game_data import (
from pqcli.mechanic import Player, Stats, monster_task from pqcli.mechanic import Player, Stats, monster_task
def write(directory: Path, name: str, value: Any) -> Path: def write(directory: Path, name: str, value: object) -> Path:
path = directory / f"{name}.json" path = directory / f"{name}.json"
path.write_text(json.dumps(value), encoding="utf-8") path.write_text(json.dumps(value), encoding="utf-8")
return path return path
@@ -98,7 +97,7 @@ def test_unknown_file_warns(
], ],
) )
def test_invalid_override_raises( def test_invalid_override_raises(
tmp_path: Path, name: str, value: Any, message: str tmp_path: Path, name: str, value: object, message: str
) -> None: ) -> None:
write(tmp_path, name, value) write(tmp_path, name, value)
with pytest.raises(DataError) as excinfo: with pytest.raises(DataError) as excinfo: