diff --git a/pqcli/__main__.py b/pqcli/__main__.py index 50891cd..8eb1156 100644 --- a/pqcli/__main__.py +++ b/pqcli/__main__.py @@ -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) diff --git a/pqcli/game_data.py b/pqcli/game_data.py index ebf1ee1..97e6548 100644 --- a/pqcli/game_data.py +++ b/pqcli/game_data.py @@ -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" diff --git a/pqcli/mechanic.py b/pqcli/mechanic.py index 8d0a1d5..d642d6a 100644 --- a/pqcli/mechanic.py +++ b/pqcli/mechanic.py @@ -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) diff --git a/pqcli/random.py b/pqcli/random.py index ae0afd1..39e1567 100644 --- a/pqcli/random.py +++ b/pqcli/random.py @@ -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))] diff --git a/pqcli/ui/basic/__init__.py b/pqcli/ui/basic/__init__.py index 14b4b6b..d590941 100644 --- a/pqcli/ui/basic/__init__.py +++ b/pqcli/ui/basic/__init__.py @@ -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}:") diff --git a/pqcli/ui/curses/__init__.py b/pqcli/ui/curses/__init__.py index 4a4075e..45a09fa 100644 --- a/pqcli/ui/curses/__init__.py +++ b/pqcli/ui/curses/__init__.py @@ -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) diff --git a/pqcli/ui/curses/event_handler.py b/pqcli/ui/curses/event_handler.py index 2834783..113c734 100644 --- a/pqcli/ui/curses/event_handler.py +++ b/pqcli/ui/curses/event_handler.py @@ -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 diff --git a/pqcli/ui/curses/util.py b/pqcli/ui/curses/util.py index d829b43..d87e7d8 100644 --- a/pqcli/ui/curses/util.py +++ b/pqcli/ui/curses/util.py @@ -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) diff --git a/pqcli/ui/curses/views/base_view.py b/pqcli/ui/curses/views/base_view.py index 7dd9b50..c9d4c7e 100644 --- a/pqcli/ui/curses/views/base_view.py +++ b/pqcli/ui/curses/views/base_view.py @@ -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: diff --git a/pqcli/ui/curses/views/choose_character_view.py b/pqcli/ui/curses/views/choose_character_view.py index 033cb80..2a8f4bf 100644 --- a/pqcli/ui/curses/views/choose_character_view.py +++ b/pqcli/ui/curses/views/choose_character_view.py @@ -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): diff --git a/pqcli/ui/curses/views/confirm_view.py b/pqcli/ui/curses/views/confirm_view.py index ac231fd..54a0f70 100644 --- a/pqcli/ui/curses/views/confirm_view.py +++ b/pqcli/ui/curses/views/confirm_view.py @@ -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( diff --git a/pqcli/ui/curses/views/create_character_view/choose_character_class_view.py b/pqcli/ui/curses/views/create_character_view/choose_character_class_view.py index 5451958..b88582b 100644 --- a/pqcli/ui/curses/views/create_character_view/choose_character_class_view.py +++ b/pqcli/ui/curses/views/create_character_view/choose_character_class_view.py @@ -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: diff --git a/pqcli/ui/curses/views/create_character_view/choose_character_name_view.py b/pqcli/ui/curses/views/create_character_view/choose_character_name_view.py index 8f0db8f..9b7e1f4 100644 --- a/pqcli/ui/curses/views/create_character_view/choose_character_name_view.py +++ b/pqcli/ui/curses/views/create_character_view/choose_character_name_view.py @@ -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: diff --git a/pqcli/ui/curses/views/create_character_view/choose_character_race_view.py b/pqcli/ui/curses/views/create_character_view/choose_character_race_view.py index 5ea54a5..1de0f29 100644 --- a/pqcli/ui/curses/views/create_character_view/choose_character_race_view.py +++ b/pqcli/ui/curses/views/create_character_view/choose_character_race_view.py @@ -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: diff --git a/pqcli/ui/curses/views/create_character_view/choose_character_stats_view.py b/pqcli/ui/curses/views/create_character_view/choose_character_stats_view.py index 49c3ccb..2279328 100644 --- a/pqcli/ui/curses/views/create_character_view/choose_character_stats_view.py +++ b/pqcli/ui/curses/views/create_character_view/choose_character_stats_view.py @@ -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() diff --git a/pqcli/ui/curses/views/game_view/__init__.py b/pqcli/ui/curses/views/game_view/__init__.py index 8c46b81..2f7983a 100644 --- a/pqcli/ui/curses/views/game_view/__init__.py +++ b/pqcli/ui/curses/views/game_view/__init__.py @@ -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 diff --git a/pqcli/ui/curses/views/game_view/character_sheet_window.py b/pqcli/ui/curses/views/game_view/character_sheet_window.py index a8a7ccc..ef0356d 100644 --- a/pqcli/ui/curses/views/game_view/character_sheet_window.py +++ b/pqcli/ui/curses/views/game_view/character_sheet_window.py @@ -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, diff --git a/pqcli/ui/curses/views/game_view/equipment_window.py b/pqcli/ui/curses/views/game_view/equipment_window.py index 2463e50..6a726b7 100644 --- a/pqcli/ui/curses/views/game_view/equipment_window.py +++ b/pqcli/ui/curses/views/game_view/equipment_window.py @@ -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 diff --git a/pqcli/ui/curses/views/game_view/inventory_window.py b/pqcli/ui/curses/views/game_view/inventory_window.py index 767eb57..ac4536b 100644 --- a/pqcli/ui/curses/views/game_view/inventory_window.py +++ b/pqcli/ui/curses/views/game_view/inventory_window.py @@ -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, diff --git a/pqcli/ui/curses/views/game_view/plot_window.py b/pqcli/ui/curses/views/game_view/plot_window.py index 20dc165..3d67a8c 100644 --- a/pqcli/ui/curses/views/game_view/plot_window.py +++ b/pqcli/ui/curses/views/game_view/plot_window.py @@ -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 diff --git a/pqcli/ui/curses/views/game_view/progress_bar_window.py b/pqcli/ui/curses/views/game_view/progress_bar_window.py index 2bae7be..4c9eecf 100644 --- a/pqcli/ui/curses/views/game_view/progress_bar_window.py +++ b/pqcli/ui/curses/views/game_view/progress_bar_window.py @@ -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, diff --git a/pqcli/ui/curses/views/game_view/quest_book_window.py b/pqcli/ui/curses/views/game_view/quest_book_window.py index bd5073d..1b234d8 100644 --- a/pqcli/ui/curses/views/game_view/quest_book_window.py +++ b/pqcli/ui/curses/views/game_view/quest_book_window.py @@ -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 diff --git a/pqcli/ui/curses/views/game_view/spell_book_window.py b/pqcli/ui/curses/views/game_view/spell_book_window.py index bd74be5..77a477d 100644 --- a/pqcli/ui/curses/views/game_view/spell_book_window.py +++ b/pqcli/ui/curses/views/game_view/spell_book_window.py @@ -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 diff --git a/pqcli/ui/curses/views/game_view/task_progress_window.py b/pqcli/ui/curses/views/game_view/task_progress_window.py index 944e1ed..db514fa 100644 --- a/pqcli/ui/curses/views/game_view/task_progress_window.py +++ b/pqcli/ui/curses/views/game_view/task_progress_window.py @@ -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) diff --git a/pqcli/ui/curses/views/menu_view.py b/pqcli/ui/curses/views/menu_view.py index 9fd6ab4..a938b63 100644 --- a/pqcli/ui/curses/views/menu_view.py +++ b/pqcli/ui/curses/views/menu_view.py @@ -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] = [] diff --git a/pqcli/ui/curses/views/roster_view.py b/pqcli/ui/curses/views/roster_view.py index e3e1183..4c3040b 100644 --- a/pqcli/ui/curses/views/roster_view.py +++ b/pqcli/ui/curses/views/roster_view.py @@ -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( diff --git a/pqcli/ui/curses/widgets/base.py b/pqcli/ui/curses/widgets/base.py index b208286..0730114 100644 --- a/pqcli/ui/curses/widgets/base.py +++ b/pqcli/ui/curses/widgets/base.py @@ -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 diff --git a/pqcli/ui/curses/widgets/data_table.py b/pqcli/ui/curses/widgets/data_table.py index aac1ee9..b3a7b46 100644 --- a/pqcli/ui/curses/widgets/data_table.py +++ b/pqcli/ui/curses/widgets/data_table.py @@ -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: diff --git a/pqcli/ui/curses/widgets/focusable.py b/pqcli/ui/curses/widgets/focusable.py index 879bd62..4c2f1c2 100644 --- a/pqcli/ui/curses/widgets/focusable.py +++ b/pqcli/ui/curses/widgets/focusable.py @@ -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 diff --git a/pqcli/ui/curses/widgets/list_box.py b/pqcli/ui/curses/widgets/list_box.py index 8177aaa..f9df8c0 100644 --- a/pqcli/ui/curses/widgets/list_box.py +++ b/pqcli/ui/curses/widgets/list_box.py @@ -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 diff --git a/pqcli/ui/curses/widgets/menu.py b/pqcli/ui/curses/widgets/menu.py index 4504d38..4e228a8 100644 --- a/pqcli/ui/curses/widgets/menu.py +++ b/pqcli/ui/curses/widgets/menu.py @@ -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) diff --git a/pqcli/ui/curses/widgets/progress_bar.py b/pqcli/ui/curses/widgets/progress_bar.py index da8085e..21ac5d1 100644 --- a/pqcli/ui/curses/widgets/progress_bar.py +++ b/pqcli/ui/curses/widgets/progress_bar.py @@ -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) diff --git a/pqcli/ui/curses/widgets/scrollable.py b/pqcli/ui/curses/widgets/scrollable.py index 78abf90..73930a4 100644 --- a/pqcli/ui/curses/widgets/scrollable.py +++ b/pqcli/ui/curses/widgets/scrollable.py @@ -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 = [] diff --git a/tests/test_game_data.py b/tests/test_game_data.py index 06bcc52..c02f817 100644 --- a/tests/test_game_data.py +++ b/tests/test_game_data.py @@ -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: