mirror of
https://github.com/rr-/pq-cli.git
synced 2026-11-12 22:40:36 -07:00
misc: fix all ty diagnostics
ty reported 20 diagnostics, and 0.0.89 adds a few more; this clears them all without suppressions. - The quest monster is held in a local before being stored on the quest book, so its name no longer reads from an Optional. - PgUp/PgDn narrow the focused window with isinstance against a runtime-checkable PageScrollable protocol instead of hasattr. - The ^t cheat used to monkeypatch SignalMixin.emit. SignalMixin now has a muted() context manager that emit() checks, and it restores emission even if a tick raises. - GameView keeps its children as GameWindow, a protocol declaring stop() and sync(), so the cheat's resync type-checks. - curses.ascii is imported explicitly where it is used. - ProgressBarWindow is now Focusable itself, instead of calling Focusable.focus_standout() on a class that is not Focusable. Its four subclasses drop the duplicate Focusable base; the MRO and initialisation order are unchanged. - MainMenu narrows the Optional pad before colouring the logo. - The tests that record tasks assert player.task is set. - Context managers are annotated as Generator rather than Iterator, which ty flags as deprecated for @contextmanager. - The redundant cast() around getch() is gone. - The frozen GameData test assigns through setattr(), since the direct assignment is an error by design. Bug fixes: - ListBox.select(None) crashed on `None < 0`; it now clears the selection, as DataTable.select(None) already does. - ListBox.get() caught ValueError, which indexing never raises; it now catches IndexError, so an out-of-range index returns None as intended.
This commit is contained in:
14 files changed
+80
-51
No files matched your search
+19
-5
@@ -1,10 +1,11 @@
|
|||||||
|
import contextlib
|
||||||
import datetime
|
import datetime
|
||||||
import functools
|
import functools
|
||||||
import itertools
|
import itertools
|
||||||
import logging
|
import logging
|
||||||
import math
|
import math
|
||||||
from collections import defaultdict
|
from collections import defaultdict
|
||||||
from collections.abc import Callable, 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, cast
|
||||||
|
|
||||||
@@ -42,7 +43,21 @@ _SIGNALS: dict[tuple["SignalMixin", str], list[Callable[..., Any]]] = (
|
|||||||
|
|
||||||
|
|
||||||
class SignalMixin:
|
class SignalMixin:
|
||||||
|
_muted = 0
|
||||||
|
|
||||||
|
@staticmethod
|
||||||
|
@contextlib.contextmanager
|
||||||
|
def muted() -> Generator[None, None, None]:
|
||||||
|
"""Suppress all signal emission for the duration of the block."""
|
||||||
|
SignalMixin._muted += 1
|
||||||
|
try:
|
||||||
|
yield
|
||||||
|
finally:
|
||||||
|
SignalMixin._muted -= 1
|
||||||
|
|
||||||
def emit(self, signal_name: str, *user_data: Any) -> None:
|
def emit(self, signal_name: str, *user_data: Any) -> None:
|
||||||
|
if SignalMixin._muted:
|
||||||
|
return
|
||||||
for callback in _SIGNALS[self, signal_name]:
|
for callback in _SIGNALS[self, signal_name]:
|
||||||
callback(*user_data)
|
callback(*user_data)
|
||||||
|
|
||||||
@@ -683,12 +698,11 @@ class Simulation:
|
|||||||
caption = ""
|
caption = ""
|
||||||
choice = random.below(5)
|
choice = random.below(5)
|
||||||
if choice == 0:
|
if choice == 0:
|
||||||
self.player.quest_book.monster = unnamed_monster(
|
monster = unnamed_monster(
|
||||||
self.data, self.player.level, iterations=3
|
self.data, self.player.level, iterations=3
|
||||||
)
|
)
|
||||||
caption = "Exterminate " + definite(
|
self.player.quest_book.monster = monster
|
||||||
self.player.quest_book.monster.name, 2
|
caption = "Exterminate " + definite(monster.name, 2)
|
||||||
)
|
|
||||||
elif choice == 1:
|
elif choice == 1:
|
||||||
caption = "Seek " + definite(interesting_item(self.data), 1)
|
caption = "Seek " + definite(interesting_item(self.data), 1)
|
||||||
elif choice == 2:
|
elif choice == 2:
|
||||||
|
|||||||
+2
-2
@@ -1,6 +1,6 @@
|
|||||||
import contextlib
|
import contextlib
|
||||||
import time
|
import time
|
||||||
from collections.abc import Callable, Iterator, Sequence
|
from collections.abc import Callable, Generator, Sequence
|
||||||
from typing import Any
|
from typing import Any
|
||||||
|
|
||||||
RngState = tuple[float, float, float, int]
|
RngState = tuple[float, float, float, int]
|
||||||
@@ -91,7 +91,7 @@ def seed(source: str) -> None:
|
|||||||
|
|
||||||
|
|
||||||
@contextlib.contextmanager
|
@contextlib.contextmanager
|
||||||
def using(rng: Alea) -> Iterator[None]:
|
def using(rng: Alea) -> Generator[None, None, None]:
|
||||||
"""Draw from rng instead of the shared generator inside the block."""
|
"""Draw from rng instead of the shared generator inside the block."""
|
||||||
global _current
|
global _current
|
||||||
previous = _current
|
previous = _current
|
||||||
|
|||||||
@@ -3,7 +3,7 @@ import curses
|
|||||||
import os
|
import os
|
||||||
import signal
|
import signal
|
||||||
import time
|
import time
|
||||||
from typing import Any, cast
|
from typing import Any
|
||||||
|
|
||||||
from pqcli import lingo
|
from pqcli import lingo
|
||||||
from pqcli.config import Class, Race
|
from pqcli.config import Class, Race
|
||||||
@@ -91,7 +91,7 @@ class CursesUserInterface(BaseUserInterface):
|
|||||||
self._switch_to_roster_view()
|
self._switch_to_roster_view()
|
||||||
|
|
||||||
while True:
|
while True:
|
||||||
key = cast(int, self._screen.getch())
|
key = self._screen.getch()
|
||||||
|
|
||||||
if key == curses.ERR:
|
if key == curses.ERR:
|
||||||
time.sleep(0.1)
|
time.sleep(0.1)
|
||||||
|
|||||||
@@ -1,7 +1,8 @@
|
|||||||
import argparse
|
import argparse
|
||||||
import curses
|
import curses
|
||||||
|
import curses.ascii
|
||||||
import datetime
|
import datetime
|
||||||
from typing import Any
|
from typing import Any, 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
|
||||||
@@ -16,7 +17,7 @@ from pqcli.ui.curses.util import (
|
|||||||
KEYS_UP,
|
KEYS_UP,
|
||||||
)
|
)
|
||||||
from pqcli.ui.curses.views.base_view import BaseView
|
from pqcli.ui.curses.views.base_view import BaseView
|
||||||
from pqcli.ui.curses.widgets import Focusable, Widget
|
from pqcli.ui.curses.widgets import Focusable
|
||||||
|
|
||||||
from .character_sheet_window import CharacterSheetWindow
|
from .character_sheet_window import CharacterSheetWindow
|
||||||
from .equipment_window import EquipmentWindow
|
from .equipment_window import EquipmentWindow
|
||||||
@@ -27,6 +28,19 @@ from .spell_book_window import SpellBookWindow
|
|||||||
from .task_progress_window import TaskProgressWindow
|
from .task_progress_window import TaskProgressWindow
|
||||||
|
|
||||||
|
|
||||||
|
class GameWindow(Protocol):
|
||||||
|
def stop(self) -> None: ...
|
||||||
|
|
||||||
|
def sync(self) -> None: ...
|
||||||
|
|
||||||
|
|
||||||
|
@runtime_checkable
|
||||||
|
class PageScrollable(Protocol):
|
||||||
|
def scroll_page_up(self) -> None: ...
|
||||||
|
|
||||||
|
def scroll_page_down(self) -> None: ...
|
||||||
|
|
||||||
|
|
||||||
class GameView(BaseView):
|
class GameView(BaseView):
|
||||||
def __init__(
|
def __init__(
|
||||||
self,
|
self,
|
||||||
@@ -53,11 +67,11 @@ class GameView(BaseView):
|
|||||||
self.on_exit()
|
self.on_exit()
|
||||||
|
|
||||||
elif key in {curses.KEY_PPAGE}:
|
elif key in {curses.KEY_PPAGE}:
|
||||||
if hasattr(focused, "scroll_page_up"):
|
if isinstance(focused, PageScrollable):
|
||||||
focused.scroll_page_up()
|
focused.scroll_page_up()
|
||||||
|
|
||||||
elif key in {curses.KEY_NPAGE}:
|
elif key in {curses.KEY_NPAGE}:
|
||||||
if hasattr(focused, "scroll_page_down"):
|
if isinstance(focused, PageScrollable):
|
||||||
focused.scroll_page_down()
|
focused.scroll_page_down()
|
||||||
|
|
||||||
elif key in KEYS_CYCLE:
|
elif key in KEYS_CYCLE:
|
||||||
@@ -118,11 +132,9 @@ class GameView(BaseView):
|
|||||||
self._simulation.tick()
|
self._simulation.tick()
|
||||||
|
|
||||||
elif self._args.cheats and key == curses.ascii.DC4: # ^t
|
elif self._args.cheats and key == curses.ascii.DC4: # ^t
|
||||||
old = SignalMixin.emit
|
with SignalMixin.muted():
|
||||||
SignalMixin.emit = lambda *_: None
|
for _ in range(10000):
|
||||||
for _ in range(10000):
|
self._simulation.tick()
|
||||||
self._simulation.tick()
|
|
||||||
SignalMixin.emit = old
|
|
||||||
for child in self._children:
|
for child in self._children:
|
||||||
child.sync()
|
child.sync()
|
||||||
|
|
||||||
@@ -225,16 +237,18 @@ class GameView(BaseView):
|
|||||||
x=0,
|
x=0,
|
||||||
)
|
)
|
||||||
|
|
||||||
self._focusable_children: list[Focusable] = [
|
focusable_children = (
|
||||||
self._char_sheet_win,
|
self._char_sheet_win,
|
||||||
self._spell_book_win,
|
self._spell_book_win,
|
||||||
self._equipment_win,
|
self._equipment_win,
|
||||||
self._inventory_win,
|
self._inventory_win,
|
||||||
self._plot_win,
|
self._plot_win,
|
||||||
self._quest_book_win,
|
self._quest_book_win,
|
||||||
]
|
)
|
||||||
self._children: list[Widget] = self._focusable_children + [
|
self._focusable_children: list[Focusable] = list(focusable_children)
|
||||||
self._task_win
|
self._children: list[GameWindow] = [
|
||||||
|
*focusable_children,
|
||||||
|
self._task_win,
|
||||||
]
|
]
|
||||||
|
|
||||||
def stop(self) -> None:
|
def stop(self) -> None:
|
||||||
|
|||||||
@@ -1,12 +1,11 @@
|
|||||||
from typing import Any
|
from typing import Any
|
||||||
|
|
||||||
from pqcli.mechanic import Player, StatType
|
from pqcli.mechanic import Player, StatType
|
||||||
from pqcli.ui.curses.widgets import Focusable
|
|
||||||
|
|
||||||
from .progress_bar_window import DataTableProgressBarWindow
|
from .progress_bar_window import DataTableProgressBarWindow
|
||||||
|
|
||||||
|
|
||||||
class CharacterSheetWindow(Focusable, 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: Any, h: int, w: int, y: int, x: int
|
||||||
) -> None:
|
) -> None:
|
||||||
|
|||||||
@@ -1,12 +1,11 @@
|
|||||||
from typing import Any
|
from typing import Any
|
||||||
|
|
||||||
from pqcli.mechanic import InventoryItem, Player
|
from pqcli.mechanic import InventoryItem, Player
|
||||||
from pqcli.ui.curses.widgets import Focusable
|
|
||||||
|
|
||||||
from .progress_bar_window import DataTableProgressBarWindow
|
from .progress_bar_window import DataTableProgressBarWindow
|
||||||
|
|
||||||
|
|
||||||
class InventoryWindow(Focusable, 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: Any, h: int, w: int, y: int, x: int
|
||||||
) -> None:
|
) -> None:
|
||||||
|
|||||||
@@ -2,12 +2,11 @@ from typing import Any
|
|||||||
|
|
||||||
from pqcli.lingo import act_name
|
from pqcli.lingo import act_name
|
||||||
from pqcli.mechanic import Player
|
from pqcli.mechanic import Player
|
||||||
from pqcli.ui.curses.widgets import Focusable
|
|
||||||
|
|
||||||
from .progress_bar_window import ListBoxProgressBarWindow
|
from .progress_bar_window import ListBoxProgressBarWindow
|
||||||
|
|
||||||
|
|
||||||
class PlotWindow(Focusable, ListBoxProgressBarWindow):
|
class PlotWindow(ListBoxProgressBarWindow):
|
||||||
cutoff = 100
|
cutoff = 100
|
||||||
|
|
||||||
def __init__(
|
def __init__(
|
||||||
|
|||||||
@@ -10,7 +10,7 @@ from pqcli.ui.curses.widgets import (
|
|||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
class ProgressBarWindow(WindowWrapper):
|
class ProgressBarWindow(Focusable, WindowWrapper):
|
||||||
def __init__(
|
def __init__(
|
||||||
self,
|
self,
|
||||||
parent: Any,
|
parent: Any,
|
||||||
@@ -51,7 +51,7 @@ class ProgressBarWindow(WindowWrapper):
|
|||||||
|
|
||||||
self._progress_bar_win.erase()
|
self._progress_bar_win.erase()
|
||||||
|
|
||||||
with Focusable.focus_standout(self, self._progress_bar_win):
|
with self.focus_standout(self._progress_bar_win):
|
||||||
self._progress_bar_win.border(
|
self._progress_bar_win.border(
|
||||||
curses.ACS_VLINE,
|
curses.ACS_VLINE,
|
||||||
curses.ACS_VLINE,
|
curses.ACS_VLINE,
|
||||||
@@ -105,7 +105,7 @@ class DataTableProgressBarWindow(ProgressBarWindow):
|
|||||||
if not self._win:
|
if not self._win:
|
||||||
return
|
return
|
||||||
|
|
||||||
with Focusable.focus_standout(self, self._win):
|
with self.focus_standout(self._win):
|
||||||
self._win.box()
|
self._win.box()
|
||||||
x = max(0, (self.getmaxyx()[1] - len(self._title)) // 2)
|
x = max(0, (self.getmaxyx()[1] - len(self._title)) // 2)
|
||||||
self._win.addnstr(
|
self._win.addnstr(
|
||||||
@@ -143,7 +143,7 @@ class ListBoxProgressBarWindow(ProgressBarWindow):
|
|||||||
if not self._win:
|
if not self._win:
|
||||||
return
|
return
|
||||||
|
|
||||||
with Focusable.focus_standout(self, self._win):
|
with self.focus_standout(self._win):
|
||||||
self._win.box()
|
self._win.box()
|
||||||
x = max(0, (self.getmaxyx()[1] - len(self._title)) // 2)
|
x = max(0, (self.getmaxyx()[1] - len(self._title)) // 2)
|
||||||
self._win.addnstr(
|
self._win.addnstr(
|
||||||
|
|||||||
@@ -1,12 +1,11 @@
|
|||||||
from typing import Any
|
from typing import Any
|
||||||
|
|
||||||
from pqcli.mechanic import Player
|
from pqcli.mechanic import Player
|
||||||
from pqcli.ui.curses.widgets import Focusable
|
|
||||||
|
|
||||||
from .progress_bar_window import ListBoxProgressBarWindow
|
from .progress_bar_window import ListBoxProgressBarWindow
|
||||||
|
|
||||||
|
|
||||||
class QuestBookWindow(Focusable, ListBoxProgressBarWindow):
|
class QuestBookWindow(ListBoxProgressBarWindow):
|
||||||
cutoff = 100
|
cutoff = 100
|
||||||
|
|
||||||
def __init__(
|
def __init__(
|
||||||
|
|||||||
@@ -38,15 +38,17 @@ class MainMenu(Menu):
|
|||||||
scr_height=scr_height,
|
scr_height=scr_height,
|
||||||
scr_width=scr_width,
|
scr_width=scr_width,
|
||||||
)
|
)
|
||||||
|
pad = self._pad
|
||||||
|
assert pad is not None
|
||||||
if has_colors():
|
if has_colors():
|
||||||
for y in range(len(self._header_lines)):
|
for y in range(len(self._header_lines)):
|
||||||
self._pad.chgat(y, 0, curses.color_pair(COLOR_LOGO))
|
pad.chgat(y, 0, curses.color_pair(COLOR_LOGO))
|
||||||
self._pad.chgat(3, 29, 5, curses.color_pair(COLOR_LOGO_ALT))
|
pad.chgat(3, 29, 5, curses.color_pair(COLOR_LOGO_ALT))
|
||||||
self._pad.chgat(4, 29, 5, curses.color_pair(COLOR_LOGO_ALT))
|
pad.chgat(4, 29, 5, curses.color_pair(COLOR_LOGO_ALT))
|
||||||
self._pad.chgat(5, 32, 4, curses.color_pair(COLOR_LOGO_ALT))
|
pad.chgat(5, 32, 4, curses.color_pair(COLOR_LOGO_ALT))
|
||||||
self._pad.chgat(5, 27, 4, curses.color_pair(COLOR_LOGO_ALT))
|
pad.chgat(5, 27, 4, curses.color_pair(COLOR_LOGO_ALT))
|
||||||
self._pad.chgat(6, 34, 2, curses.color_pair(COLOR_LOGO_ALT))
|
pad.chgat(6, 34, 2, curses.color_pair(COLOR_LOGO_ALT))
|
||||||
self._pad.chgat(6, 27, 2, curses.color_pair(COLOR_LOGO_ALT))
|
pad.chgat(6, 27, 2, curses.color_pair(COLOR_LOGO_ALT))
|
||||||
|
|
||||||
|
|
||||||
class RosterView(BaseView):
|
class RosterView(BaseView):
|
||||||
|
|||||||
@@ -29,14 +29,14 @@ class ListBox(Scrollable):
|
|||||||
idx %= len(self._items)
|
idx %= len(self._items)
|
||||||
try:
|
try:
|
||||||
return self._items[idx]
|
return self._items[idx]
|
||||||
except ValueError:
|
except IndexError:
|
||||||
return None
|
return None
|
||||||
|
|
||||||
def delete(self, idx: int, count: int = 1) -> None:
|
def delete(self, idx: int, count: int = 1) -> None:
|
||||||
del self._items[idx : idx + count]
|
del self._items[idx : idx + count]
|
||||||
|
|
||||||
def select(self, idx: int | None) -> None:
|
def select(self, idx: int | None) -> None:
|
||||||
if idx < 0 and len(self._items):
|
if idx is not None and idx < 0 and self._items:
|
||||||
idx %= len(self._items)
|
idx %= len(self._items)
|
||||||
self._selected = idx
|
self._selected = idx
|
||||||
|
|
||||||
|
|||||||
@@ -28,11 +28,12 @@ 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
|
||||||
self._pad: Any | None = curses.newpad(h, w)
|
pad = curses.newpad(h, w)
|
||||||
|
self._pad: Any | None = pad
|
||||||
|
|
||||||
for y, line in enumerate(self._header_lines):
|
for y, line in enumerate(self._header_lines):
|
||||||
self._pad.move(y, 0)
|
pad.move(y, 0)
|
||||||
self._pad.addstr(line)
|
pad.addstr(line)
|
||||||
|
|
||||||
self._active_choice = active_choice
|
self._active_choice = active_choice
|
||||||
|
|
||||||
|
|||||||
+6
-4
@@ -34,10 +34,12 @@ def run_trace(seed: str, ticks: int = 3000) -> list[str]:
|
|||||||
simulation = Simulation(player, data)
|
simulation = Simulation(player, data)
|
||||||
|
|
||||||
lines: list[str] = []
|
lines: list[str] = []
|
||||||
player.connect(
|
|
||||||
"new_task",
|
def record_task() -> None:
|
||||||
lambda: lines.append(f"task: {player.task.description}"),
|
assert player.task is not None
|
||||||
)
|
lines.append(f"task: {player.task.description}")
|
||||||
|
|
||||||
|
player.connect("new_task", record_task)
|
||||||
for _ in range(ticks):
|
for _ in range(ticks):
|
||||||
simulation.tick(100000.0)
|
simulation.tick(100000.0)
|
||||||
|
|
||||||
|
|||||||
@@ -179,4 +179,4 @@ def test_game_data_is_immutable() -> None:
|
|||||||
data = load_game_data()
|
data = load_game_data()
|
||||||
assert isinstance(data, GameData)
|
assert isinstance(data, GameData)
|
||||||
with pytest.raises(AttributeError):
|
with pytest.raises(AttributeError):
|
||||||
data.spells = () # type: ignore[misc]
|
setattr(data, "spells", ())
|
||||||
Reference in new issue
Block a user