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

+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)