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 sys
from pathlib import Path
from typing import Any
from typing import TextIO
from xdg_base_dirs import xdg_config_home
@@ -84,7 +84,7 @@ def parse_args() -> argparse.Namespace:
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):
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 importlib import resources
from pathlib import Path
from typing import Any, cast
from typing import Any
from pqcli.config import (
Class,
@@ -15,6 +15,11 @@ from pqcli.config import (
StatType,
)
if sys.version_info >= (3, 11):
from importlib.resources.abc import Traversable
else:
from importlib.abc import Traversable
TARGET_PLACEHOLDER = "{target}"
@@ -56,8 +61,8 @@ class _Context:
def _check_keys(
ctx: _Context, where: str, obj: Any, keys: Sequence[str]
) -> dict[str, Any]:
ctx: _Context, where: str, obj: object, keys: Sequence[str]
) -> dict[str, object]:
if not isinstance(obj, dict):
raise ctx.error(f"{where} must be an object")
missing = [key for key in keys if key not in obj]
@@ -69,39 +74,39 @@ def _check_keys(
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):
raise ctx.error(f"{where} must be a string")
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):
raise ctx.error(f"{where} must be an integer")
return value
def _stat(ctx: _Context, where: str, value: Any) -> StatType:
_str(ctx, where, value)
def _stat(ctx: _Context, where: str, value: object) -> StatType:
name = _str(ctx, where, value)
for stat in StatType:
if value.lower() in (stat.value.lower(), stat.name):
if name.lower() in (stat.value.lower(), stat.name):
return stat
allowed = ", ".join(stat.value for stat in StatType)
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)
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)
if TARGET_PLACEHOLDER not in verb:
raise ctx.error(f"{where} must contain {TARGET_PLACEHOLDER}")
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"])
return Modifier(
_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"])
return EquipmentPreset(
_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"])
loot = obj["item"]
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):
raise ctx.error(f"{where} must be a list of stats")
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"])
return Race(
_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"])
return Class(
_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,
"offense_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)
def _parse(name: str, path: str, text: str) -> tuple[Any, ...]:
def _parse(name: str, path: str, text: str) -> tuple[object, ...]:
ctx = _Context(path)
try:
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))
def _builtin_dir() -> Any:
def _builtin_dir() -> Traversable:
return resources.files("pqcli") / "data"
def _read_text(path: Any, display: str) -> str:
def _read_text(path: Traversable, display: str) -> str:
try:
return cast(str, path.read_text(encoding="utf-8"))
return path.read_text(encoding="utf-8")
except UnicodeDecodeError as ex:
raise DataError(f"{display}: not valid UTF-8: {ex}") from ex
except OSError as ex:
@@ -219,11 +224,13 @@ def load_game_data(override_dir: Path | None = None) -> GameData:
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, ...]] = {}
for name in DATA_FILES:
if name in overrides:
assert override_dir is not None
source: Any = override_dir / f"{name}.json"
source: Traversable = override_dir / f"{name}.json"
display = str(source)
else:
source = _builtin_dir() / f"{name}.json"
+19 -25
View File
@@ -7,7 +7,7 @@ import math
from collections import defaultdict
from collections.abc import Callable, Generator, Iterator, Sequence
from dataclasses import dataclass
from typing import Any, cast
from typing import Any
from pqcli import random
from pqcli.config import (
@@ -37,7 +37,7 @@ from pqcli.random import Alea, RngState
logger = logging.getLogger(__name__)
_SIGNALS: dict[tuple["SignalMixin", str], list[Callable[..., Any]]] = (
_SIGNALS: dict[tuple["SignalMixin", str], list[Callable[..., object]]] = (
defaultdict(list)
)
@@ -55,17 +55,19 @@ class SignalMixin:
finally:
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:
return
for callback in _SIGNALS[self, signal_name]:
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)
def disconnect(
self, signal_name: str, callback: Callable[..., Any]
self, signal_name: str, callback: Callable[..., object]
) -> None:
try:
idx = _SIGNALS[self, signal_name].index(callback)
@@ -383,7 +385,7 @@ class Player(SignalMixin):
self.task: BaseTask | None = None
self.queue: list[BaseTask] = []
def __setstate__(self, obj: Any) -> None:
def __setstate__(self, obj: dict[str, Any]) -> None:
self.__dict__.update(obj)
self.elapsed = obj.get("elapsed", 0.0)
if "rng" not in obj:
@@ -853,38 +855,30 @@ class Simulation:
def special_item(data: GameData) -> str:
return (
interesting_item(data)
+ " of "
+ cast(str, random.choice(data.item_ofs))
)
return interesting_item(data) + " of " + random.choice(data.item_ofs)
def interesting_item(data: GameData) -> str:
return (
cast(str, random.choice(data.item_attrib))
+ " "
+ cast(str, random.choice(data.specials))
)
return random.choice(data.item_attrib) + " " + random.choice(data.specials)
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:
if random.below(2):
title = cast(str, random.choice(data.impressive_titles))
race = cast(Race, random.choice(data.races))
title = random.choice(data.impressive_titles)
race = random.choice(data.races)
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()}"
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):
alternative = cast(Monster, random.choice(data.monsters))
alternative = random.choice(data.monsters)
if abs(level - alternative.level) < abs(level - result.level):
result = alternative
return result
@@ -898,9 +892,9 @@ def named_monster(data: GameData, level: int) -> str:
def pick_equipment(
source: Sequence[EquipmentPreset], goal: int
) -> EquipmentPreset:
result = cast(EquipmentPreset, random.choice(source))
result = random.choice(source)
for _ in range(5):
alternative = cast(EquipmentPreset, random.choice(source))
alternative = random.choice(source)
if abs(goal - alternative.quality) < abs(goal - result.quality):
result = alternative
return result
@@ -986,7 +980,7 @@ def monster_task(
verb = (
data.combat_verbs[0]
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)
return KillTask(description, duration, monster=monster)
+5 -3
View File
@@ -1,10 +1,12 @@
import contextlib
import time
from collections.abc import Callable, Generator, Sequence
from typing import Any
from typing import TypeVar
RngState = tuple[float, float, float, int]
T = TypeVar("T")
_2_POW_32 = 0x100000000
_2_POW_MINUS_32 = 2.3283064365386963e-10
@@ -102,11 +104,11 @@ def using(rng: Alea) -> Generator[None, None, None]:
_current = previous
def choice(source: Sequence[Any]) -> Any:
def choice(source: Sequence[T]) -> T:
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))]
+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 = []
+2 -3
View File
@@ -2,7 +2,6 @@ import datetime
import json
import pickle
from pathlib import Path
from typing import Any
import pytest
@@ -18,7 +17,7 @@ from pqcli.game_data import (
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.write_text(json.dumps(value), encoding="utf-8")
return path
@@ -98,7 +97,7 @@ def test_unknown_file_warns(
],
)
def test_invalid_override_raises(
tmp_path: Path, name: str, value: Any, message: str
tmp_path: Path, name: str, value: object, message: str
) -> None:
write(tmp_path, name, value)
with pytest.raises(DataError) as excinfo: