diff --git a/pqcli/__main__.py b/pqcli/__main__.py index 0729c3a..50891cd 100644 --- a/pqcli/__main__.py +++ b/pqcli/__main__.py @@ -1,7 +1,7 @@ import argparse import sys -import typing as T from pathlib import Path +from typing import Any 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: T.Optional[T.Any] = None) -> None: +def list_players(roster: Roster, file: Any | None = None) -> None: for i, player in enumerate(roster.players, start=1): print(f"{i}. {player.name}", file=file) @@ -130,7 +130,7 @@ def main() -> None: list_players(roster) exit(0) - player: T.Optional[Player] = None + player: Player | None = None if args.load_save: try: player = list(roster.players)[args.load_save - 1] diff --git a/pqcli/config.py b/pqcli/config.py index 27cdbff..a4c8808 100644 --- a/pqcli/config.py +++ b/pqcli/config.py @@ -1,5 +1,4 @@ import enum -import typing as T from dataclasses import dataclass @@ -60,16 +59,16 @@ class EquipmentPreset: class Monster: name: str level: int - item: T.Optional[str] + item: str | None @dataclass class Race: name: str - attr: T.List[StatType] + attr: list[StatType] @dataclass class Class: name: str - attr: T.List[StatType] + attr: list[StatType] diff --git a/pqcli/game_data.py b/pqcli/game_data.py index 3ec8e07..ebf1ee1 100644 --- a/pqcli/game_data.py +++ b/pqcli/game_data.py @@ -1,9 +1,10 @@ import json import sys -import typing as T +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 pqcli.config import ( Class, @@ -23,24 +24,24 @@ class DataError(Exception): @dataclass(frozen=True) class GameData: - spells: T.Tuple[str, ...] - offense_attrib: T.Tuple[Modifier, ...] - defense_attrib: T.Tuple[Modifier, ...] - offense_bad: T.Tuple[Modifier, ...] - defense_bad: T.Tuple[Modifier, ...] - shields: T.Tuple[EquipmentPreset, ...] - armors: T.Tuple[EquipmentPreset, ...] - weapons: T.Tuple[EquipmentPreset, ...] - specials: T.Tuple[str, ...] - item_attrib: T.Tuple[str, ...] - item_ofs: T.Tuple[str, ...] - boring_items: T.Tuple[str, ...] - monsters: T.Tuple[Monster, ...] - races: T.Tuple[Race, ...] - classes: T.Tuple[Class, ...] - titles: T.Tuple[str, ...] - impressive_titles: T.Tuple[str, ...] - combat_verbs: T.Tuple[str, ...] + spells: tuple[str, ...] + offense_attrib: tuple[Modifier, ...] + defense_attrib: tuple[Modifier, ...] + offense_bad: tuple[Modifier, ...] + defense_bad: tuple[Modifier, ...] + shields: tuple[EquipmentPreset, ...] + armors: tuple[EquipmentPreset, ...] + weapons: tuple[EquipmentPreset, ...] + specials: tuple[str, ...] + item_attrib: tuple[str, ...] + item_ofs: tuple[str, ...] + boring_items: tuple[str, ...] + monsters: tuple[Monster, ...] + races: tuple[Race, ...] + classes: tuple[Class, ...] + titles: tuple[str, ...] + impressive_titles: tuple[str, ...] + combat_verbs: tuple[str, ...] DATA_FILES = tuple(field.name for field in fields(GameData)) @@ -55,8 +56,8 @@ class _Context: def _check_keys( - ctx: _Context, where: str, obj: T.Any, keys: T.Sequence[str] -) -> T.Dict[str, T.Any]: + ctx: _Context, where: str, obj: Any, keys: Sequence[str] +) -> dict[str, Any]: 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] @@ -68,19 +69,19 @@ def _check_keys( return obj -def _str(ctx: _Context, where: str, value: T.Any) -> str: +def _str(ctx: _Context, where: str, value: Any) -> str: if not isinstance(value, str): raise ctx.error(f"{where} must be a string") return value -def _int(ctx: _Context, where: str, value: T.Any) -> int: +def _int(ctx: _Context, where: str, value: Any) -> 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: T.Any) -> StatType: +def _stat(ctx: _Context, where: str, value: Any) -> StatType: _str(ctx, where, value) for stat in StatType: if value.lower() in (stat.value.lower(), stat.name): @@ -89,18 +90,18 @@ def _stat(ctx: _Context, where: str, value: T.Any) -> StatType: raise ctx.error(f"{where}: unknown stat {value!r} (use one of {allowed})") -def _parse_str(ctx: _Context, where: str, item: T.Any) -> str: +def _parse_str(ctx: _Context, where: str, item: Any) -> str: return _str(ctx, where, item) -def _parse_verb(ctx: _Context, where: str, item: T.Any) -> str: +def _parse_verb(ctx: _Context, where: str, item: Any) -> 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: T.Any) -> Modifier: +def _parse_modifier(ctx: _Context, where: str, item: Any) -> Modifier: obj = _check_keys(ctx, where, item, ["name", "quality"]) return Modifier( _str(ctx, f"{where}.name", obj["name"]), @@ -108,7 +109,7 @@ def _parse_modifier(ctx: _Context, where: str, item: T.Any) -> Modifier: ) -def _parse_preset(ctx: _Context, where: str, item: T.Any) -> EquipmentPreset: +def _parse_preset(ctx: _Context, where: str, item: Any) -> EquipmentPreset: obj = _check_keys(ctx, where, item, ["name", "quality"]) return EquipmentPreset( _str(ctx, f"{where}.name", obj["name"]), @@ -116,7 +117,7 @@ def _parse_preset(ctx: _Context, where: str, item: T.Any) -> EquipmentPreset: ) -def _parse_monster(ctx: _Context, where: str, item: T.Any) -> Monster: +def _parse_monster(ctx: _Context, where: str, item: Any) -> Monster: obj = _check_keys(ctx, where, item, ["name", "level", "item"]) loot = obj["item"] return Monster( @@ -126,13 +127,13 @@ def _parse_monster(ctx: _Context, where: str, item: T.Any) -> Monster: ) -def _parse_attrs(ctx: _Context, where: str, value: T.Any) -> T.List[StatType]: +def _parse_attrs(ctx: _Context, where: str, value: Any) -> 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: T.Any) -> Race: +def _parse_race(ctx: _Context, where: str, item: Any) -> Race: obj = _check_keys(ctx, where, item, ["name", "attr"]) return Race( _str(ctx, f"{where}.name", obj["name"]), @@ -140,7 +141,7 @@ def _parse_race(ctx: _Context, where: str, item: T.Any) -> Race: ) -def _parse_class(ctx: _Context, where: str, item: T.Any) -> Class: +def _parse_class(ctx: _Context, where: str, item: Any) -> Class: obj = _check_keys(ctx, where, item, ["name", "attr"]) return Class( _str(ctx, f"{where}.name", obj["name"]), @@ -148,7 +149,7 @@ def _parse_class(ctx: _Context, where: str, item: T.Any) -> Class: ) -_PARSERS: T.Dict[str, T.Callable[[_Context, str, T.Any], T.Any]] = { +_PARSERS: dict[str, Callable[[_Context, str, Any], Any]] = { "spells": _parse_str, "offense_attrib": _parse_modifier, "defense_attrib": _parse_modifier, @@ -171,7 +172,7 @@ _PARSERS: T.Dict[str, T.Callable[[_Context, str, T.Any], T.Any]] = { assert set(_PARSERS) == set(DATA_FILES) -def _parse(name: str, path: str, text: str) -> T.Tuple[T.Any, ...]: +def _parse(name: str, path: str, text: str) -> tuple[Any, ...]: ctx = _Context(path) try: value = json.loads(text) @@ -185,26 +186,26 @@ def _parse(name: str, path: str, text: str) -> T.Tuple[T.Any, ...]: return tuple(parser(ctx, f"[{i}]", item) for i, item in enumerate(value)) -def _builtin_dir() -> T.Any: +def _builtin_dir() -> Any: return resources.files("pqcli") / "data" -def _read_text(path: T.Any, display: str) -> str: +def _read_text(path: Any, display: str) -> str: try: - return T.cast(str, path.read_text(encoding="utf-8")) + return cast(str, 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: raise DataError(f"{display}: cannot read: {ex}") from ex -def load_game_data(override_dir: T.Optional[Path] = None) -> GameData: +def load_game_data(override_dir: Path | None = None) -> GameData: """Load the built-in word lists, replacing any that override_dir has. A missing override_dir is fine. Each .json in it replaces the whole built-in list of that name. """ - overrides: T.Set[str] = set() + overrides: set[str] = set() if override_dir is not None and override_dir.exists(): if not override_dir.is_dir(): raise DataError(f"{override_dir}: not a directory") @@ -218,11 +219,11 @@ def load_game_data(override_dir: T.Optional[Path] = None) -> GameData: file=sys.stderr, ) - values: T.Dict[str, T.Tuple[T.Any, ...]] = {} + values: dict[str, tuple[Any, ...]] = {} for name in DATA_FILES: if name in overrides: assert override_dir is not None - source: T.Any = override_dir / f"{name}.json" + source: Any = override_dir / f"{name}.json" display = str(source) else: source = _builtin_dir() / f"{name}.json" @@ -231,7 +232,7 @@ def load_game_data(override_dir: T.Optional[Path] = None) -> GameData: return GameData(**values) -def dump_builtin_data(target_dir: Path) -> T.List[Path]: +def dump_builtin_data(target_dir: Path) -> list[Path]: """Copy the built-in data files to target_dir, never overwriting.""" targets = [target_dir / f"{name}.json" for name in DATA_FILES] existing = [path for path in targets if path.exists()] diff --git a/pqcli/lingo.py b/pqcli/lingo.py index 717bd6d..6731d6d 100644 --- a/pqcli/lingo.py +++ b/pqcli/lingo.py @@ -1,5 +1,4 @@ import datetime -import typing as T from pqcli import random @@ -114,7 +113,7 @@ def definite(subject: str, qty: int) -> str: return "the " + subject -def prefix(a: T.List[str], m: int, subject: str, sep: str = " ") -> str: +def prefix(a: list[str], m: int, subject: str, sep: str = " ") -> str: m = abs(m) if m < 1 or m > len(a): return subject diff --git a/pqcli/mechanic.py b/pqcli/mechanic.py index cdd98b5..48204db 100644 --- a/pqcli/mechanic.py +++ b/pqcli/mechanic.py @@ -3,9 +3,10 @@ import functools import itertools import logging import math -import typing as T from collections import defaultdict +from collections.abc import Callable, Iterator, Sequence from dataclasses import dataclass +from typing import Any, cast from pqcli import random from pqcli.config import ( @@ -35,23 +36,21 @@ from pqcli.random import Alea, RngState logger = logging.getLogger(__name__) -_SIGNALS: T.Dict[ - T.Tuple["SignalMixin", str], T.List[T.Callable[..., T.Any]] -] = defaultdict(list) +_SIGNALS: dict[tuple["SignalMixin", str], list[Callable[..., Any]]] = ( + defaultdict(list) +) class SignalMixin: - def emit(self, signal_name: str, *user_data: T.Any) -> None: + def emit(self, signal_name: str, *user_data: Any) -> None: for callback in _SIGNALS[self, signal_name]: callback(*user_data) - def connect( - self, signal_name: str, callback: T.Callable[..., T.Any] - ) -> None: + def connect(self, signal_name: str, callback: Callable[..., Any]) -> None: _SIGNALS[self, signal_name].append(callback) def disconnect( - self, signal_name: str, callback: T.Callable[..., T.Any] + self, signal_name: str, callback: Callable[..., Any] ) -> None: try: idx = _SIGNALS[self, signal_name].index(callback) @@ -108,10 +107,10 @@ class Bar(SignalMixin): class Stats(SignalMixin): signals = ["change"] - def __init__(self, values: T.Dict[StatType, int]) -> None: + def __init__(self, values: dict[StatType, int]) -> None: self._values = values - def __iter__(self) -> T.Iterator[T.Tuple[StatType, int]]: + def __iter__(self) -> Iterator[tuple[StatType, int]]: return iter(self._values.items()) def __getitem__(self, stat: StatType) -> int: @@ -137,22 +136,22 @@ class QuestBook(SignalMixin): signals = ["start_act", "start_quest"] def __init__(self) -> None: - self._quests: T.List[str] = [] + self._quests: list[str] = [] self._act = 0 self.plot_bar = Bar(max_=1) self.quest_bar = Bar(max_=1) - self.monster: T.Optional[Monster] = None + self.monster: Monster | None = None @property def act(self) -> int: return self._act @property - def quests(self) -> T.List[str]: + def quests(self) -> list[str]: return self._quests @property - def current_quest(self) -> T.Optional[str]: + def current_quest(self) -> str | None: if self._quests: return self._quests[-1] return None @@ -178,7 +177,7 @@ class Inventory(SignalMixin): def __init__(self, capacity: int = 0) -> None: self._gold = 0 - self._items: T.List[InventoryItem] = [] + self._items: list[InventoryItem] = [] self.encum_bar = Bar(max_=capacity) @property @@ -188,7 +187,7 @@ class Inventory(SignalMixin): def __len__(self) -> int: return len(self._items) - def __iter__(self) -> T.Iterator[InventoryItem]: + def __iter__(self) -> Iterator[InventoryItem]: return iter(self._items) def __getitem__(self, idx: int) -> InventoryItem: @@ -234,16 +233,16 @@ class Equipment(SignalMixin): signals = ["change"] def __init__(self) -> None: - self._items: T.Dict[EquipmentType, str] = { + self._items: dict[EquipmentType, str] = { EquipmentType.weapon: "Sharp Rock", EquipmentType.hauberk: "-3 Burlap", } self.best = self[EquipmentType.weapon] - def __iter__(self) -> T.Iterator[T.Tuple[EquipmentType, str]]: + def __iter__(self) -> Iterator[tuple[EquipmentType, str]]: return iter(self._items.items()) - def __getitem__(self, equipment_type: EquipmentType) -> T.Optional[str]: + def __getitem__(self, equipment_type: EquipmentType) -> str | None: return self._items.get(equipment_type, None) def put(self, equipment_type: EquipmentType, item_name: str) -> None: @@ -267,9 +266,9 @@ class SpellBook(SignalMixin): signals = ["add", "change"] def __init__(self) -> None: - self._spells: T.List[Spell] = [] + self._spells: list[Spell] = [] - def __iter__(self) -> T.Iterator[Spell]: + def __iter__(self) -> Iterator[Spell]: return iter(self._spells) def __bool__(self) -> bool: @@ -292,7 +291,7 @@ class SpellBook(SignalMixin): self.emit("add", spell) @property - def best(self) -> T.Optional[Spell]: + def best(self) -> Spell | None: if not self._spells: return None return max(self._spells, key=lambda spell: spell.level) @@ -306,7 +305,7 @@ class BaseTask: @dataclass class KillTask(BaseTask): - monster: T.Optional[Monster] = None + monster: Monster | None = None class BuyTask(BaseTask): @@ -347,7 +346,7 @@ class Player(SignalMixin): race: Race, class_: Class, stats: Stats, - rng: T.Optional[Alea] = None, + rng: Alea | None = None, ) -> None: self.name: str = name self.birthday: datetime.datetime = birthday @@ -366,10 +365,10 @@ class Player(SignalMixin): self.spell_book = SpellBook() self.task_bar = Bar(max_=1) - self.task: T.Optional[BaseTask] = None - self.queue: T.List[BaseTask] = [] + self.task: BaseTask | None = None + self.queue: list[BaseTask] = [] - def __setstate__(self, obj: T.Any) -> None: + def __setstate__(self, obj: Any) -> None: self.__dict__.update(obj) self.elapsed = obj.get("elapsed", 0.0) if "rng" not in obj: @@ -402,7 +401,7 @@ class Player(SignalMixin): self.emit("level_up") def win_stat(self) -> bool: - chosen_stat: T.Optional[StatType] = None + chosen_stat: StatType | None = None if random.odds(1, 2): chosen_stat = random.choice(list(StatType)) @@ -437,9 +436,9 @@ class Player(SignalMixin): def win_equipment(self, data: GameData) -> None: choice = random.choice(list(EquipmentType)) - stuff: T.Sequence[EquipmentPreset] - better: T.Sequence[Modifier] - worse: T.Sequence[Modifier] + stuff: Sequence[EquipmentPreset] + better: Sequence[Modifier] + worse: Sequence[Modifier] if choice == EquipmentType.weapon: stuff = data.weapons @@ -566,7 +565,7 @@ class Simulation: self.player.task_bar.max_ / 1000 ) elif self.player.quest_book.act == 0 and not isinstance( - self.player.task, (LoadingTask, PlotTask) + self.player.task, LoadingTask | PlotTask ): # the Prologue story tasks fill the plot bar self.player.quest_book.plot_bar.increment( @@ -599,7 +598,7 @@ class Simulation: self.player.inventory.add_gold(-self.player.equip_price()) self.player.win_equipment(self.data) - elif isinstance(self.player.task, (HeadingToMarketTask, SellTask)): + elif isinstance(self.player.task, HeadingToMarketTask | SellTask): if isinstance(self.player.task, SellTask): item = self.player.inventory[0] amount = item.quantity * self.player.level @@ -629,7 +628,7 @@ class Simulation: self.player.set_task( HeadingToMarketTask("Heading to market to sell loot", 4000) ) - elif not isinstance(old, (KillTask, HeadingToKillingFieldsTask)): + elif not isinstance(old, KillTask | HeadingToKillingFieldsTask): if self.player.inventory.gold > self.player.equip_price(): self.player.set_task( BuyTask( @@ -838,35 +837,35 @@ def special_item(data: GameData) -> str: return ( interesting_item(data) + " of " - + T.cast(str, random.choice(data.item_ofs)) + + cast(str, random.choice(data.item_ofs)) ) def interesting_item(data: GameData) -> str: return ( - T.cast(str, random.choice(data.item_attrib)) + cast(str, random.choice(data.item_attrib)) + " " - + T.cast(str, random.choice(data.specials)) + + cast(str, random.choice(data.specials)) ) def boring_item(data: GameData) -> str: - return T.cast(str, random.choice(data.boring_items)) + return cast(str, random.choice(data.boring_items)) def impressive_guy(data: GameData) -> str: if random.below(2): - title = T.cast(str, random.choice(data.impressive_titles)) - race = T.cast(Race, random.choice(data.races)) + title = cast(str, random.choice(data.impressive_titles)) + race = cast(Race, random.choice(data.races)) return f"the {title} of the {plural(race.name)}" - title = T.cast(str, random.choice(data.impressive_titles)) + title = cast(str, random.choice(data.impressive_titles)) return f"{title} {generate_name()} of {generate_name()}" def unnamed_monster(data: GameData, level: int, iterations: int) -> Monster: - result = T.cast(Monster, random.choice(data.monsters)) + result = cast(Monster, random.choice(data.monsters)) for _ in range(iterations): - alternative = T.cast(Monster, random.choice(data.monsters)) + alternative = cast(Monster, random.choice(data.monsters)) if abs(level - alternative.level) < abs(level - result.level): result = alternative return result @@ -878,18 +877,18 @@ def named_monster(data: GameData, level: int) -> str: def pick_equipment( - source: T.Sequence[EquipmentPreset], goal: int + source: Sequence[EquipmentPreset], goal: int ) -> EquipmentPreset: - result = T.cast(EquipmentPreset, random.choice(source)) + result = cast(EquipmentPreset, random.choice(source)) for _ in range(5): - alternative = T.cast(EquipmentPreset, random.choice(source)) + alternative = cast(EquipmentPreset, random.choice(source)) if abs(goal - alternative.quality) < abs(goal - result.quality): result = alternative return result def monster_task( - data: GameData, player_level: int, quest_monster: T.Optional[Monster] + data: GameData, player_level: int, quest_monster: Monster | None ) -> KillTask: level = player_level for _ in range(level): @@ -899,7 +898,7 @@ def monster_task( level = 1 is_definite = False - monster: T.Optional[Monster] = None + monster: Monster | None = None if random.odds(1, 25): # use an NPC every once in a while race = random.choice(data.races) @@ -968,7 +967,7 @@ def monster_task( verb = ( data.combat_verbs[0] if len(data.combat_verbs) == 1 - else T.cast(str, random.choice(data.combat_verbs)) + else cast(str, random.choice(data.combat_verbs)) ) description = verb.replace(TARGET_PLACEHOLDER, result) return KillTask(description, duration, monster=monster) @@ -982,9 +981,9 @@ class _Roll: class StatsBuilder: - def __init__(self, rng: T.Optional[Alea] = None) -> None: + def __init__(self, rng: Alea | None = None) -> None: self._rng = rng or Alea() - self.history: T.List[_Roll] = [] + self.history: list[_Roll] = [] @property def seed(self) -> RngState: @@ -998,7 +997,7 @@ class StatsBuilder: def roll(self) -> Stats: seed = self._rng.state() with random.using(self._rng): - values: T.Dict[StatType, int] = { + values: dict[StatType, int] = { stat: 3 + random.below(6) + random.below(6) + random.below(6) for stat in PRIME_STATS } @@ -1024,7 +1023,7 @@ def create_player( race: Race, class_: Class, stats: Stats, - seed: T.Optional[RngState] = None, + seed: RngState | None = None, ) -> Player: player = Player( birthday=datetime.datetime.now(), diff --git a/pqcli/random.py b/pqcli/random.py index d67f036..9071d7a 100644 --- a/pqcli/random.py +++ b/pqcli/random.py @@ -1,8 +1,9 @@ import contextlib import time -import typing as T +from collections.abc import Callable, Iterator, Sequence +from typing import Any -RngState = T.Tuple[float, float, float, int] +RngState = tuple[float, float, float, int] _2_POW_32 = 0x100000000 _2_POW_MINUS_32 = 2.3283064365386963e-10 @@ -13,7 +14,7 @@ def _to_uint32(value: float) -> float: return float(int(value) % _2_POW_32) -def _mash() -> T.Callable[[str], float]: +def _mash() -> Callable[[str], float]: n = float(0xEFC8249D) def mash(data: str) -> float: @@ -90,7 +91,7 @@ def seed(source: str) -> None: @contextlib.contextmanager -def using(rng: Alea) -> T.Iterator[None]: +def using(rng: Alea) -> Iterator[None]: """Draw from rng instead of the shared generator inside the block.""" global _current previous = _current @@ -101,11 +102,11 @@ def using(rng: Alea) -> T.Iterator[None]: _current = previous -def choice(source: T.Sequence[T.Any]) -> T.Any: +def choice(source: Sequence[Any]) -> Any: return source[below(len(source))] -def choice_low(source: T.Sequence[T.Any]) -> T.Any: +def choice_low(source: Sequence[Any]) -> Any: return source[below_low(len(source))] diff --git a/pqcli/roster.py b/pqcli/roster.py index 5bd5dbd..eb10f98 100644 --- a/pqcli/roster.py +++ b/pqcli/roster.py @@ -1,6 +1,5 @@ import logging import pickle -import typing as T from datetime import datetime from pathlib import Path @@ -8,13 +7,13 @@ from pqcli.mechanic import Player class Roster: - def __init__(self, path: Path, players: T.List[Player]) -> None: + def __init__(self, path: Path, players: list[Player]) -> None: self.path = path self.players = players self._last_save = datetime.now() @staticmethod - def load(path: T.Union[str, Path]) -> "Roster": + def load(path: str | Path) -> "Roster": real_path = Path(path) if real_path.exists(): return Roster( diff --git a/pqcli/ui/base.py b/pqcli/ui/base.py index 2129cf2..8817bdb 100644 --- a/pqcli/ui/base.py +++ b/pqcli/ui/base.py @@ -1,6 +1,5 @@ import abc import argparse -import typing as T from pqcli.game_data import GameData from pqcli.mechanic import Player @@ -11,7 +10,7 @@ class BaseUserInterface(abc.ABC): def __init__( self, roster: Roster, - player: T.Optional[Player], + player: Player | None, args: argparse.Namespace, data: GameData, ) -> None: diff --git a/pqcli/ui/basic/__init__.py b/pqcli/ui/basic/__init__.py index f4a916a..14b4b6b 100644 --- a/pqcli/ui/basic/__init__.py +++ b/pqcli/ui/basic/__init__.py @@ -4,8 +4,9 @@ import logging import signal import sys import time -import typing as T +from collections.abc import Sequence from datetime import datetime, timedelta +from typing import Any from pqcli import lingo from pqcli.config import PRIME_STATS @@ -36,13 +37,13 @@ class BasicUserInterface(BaseUserInterface): def __init__( self, roster: Roster, - player: T.Optional[Player], + player: Player | None, args: argparse.Namespace, data: GameData, ) -> None: super().__init__(roster, player, args, data) - def signal_handler(sig: T.Any, frame: T.Any) -> None: + def signal_handler(sig: Any, frame: Any) -> None: logging.info("Quitting") if self.args.use_saves: self.roster.save() @@ -103,7 +104,7 @@ class BasicUserInterface(BaseUserInterface): self.quit() break - def create_player(self, *, auto_play: bool = True) -> T.Optional[Player]: + def create_player(self, *, auto_play: bool = True) -> Player | None: name = input("Name your new character: ") if not name: print("Cancelled.") @@ -196,7 +197,7 @@ class BasicUserInterface(BaseUserInterface): if self.args.use_saves: self.roster.save() - def choose_player(self) -> T.Optional[Player]: + def choose_player(self) -> Player | None: if not self.roster.players: print("No characters to choose from!") return None @@ -216,9 +217,9 @@ class BasicUserInterface(BaseUserInterface): def menu( self, - options: T.Sequence[T.Tuple[T.Any, str]], - title: T.Optional[str] = None, - ) -> T.Any: + options: Sequence[tuple[Any, str]], + title: str | None = None, + ) -> Any: print() if title: print(f"{title}:") diff --git a/pqcli/ui/curses/__init__.py b/pqcli/ui/curses/__init__.py index c03b566..5565f8d 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 -import typing as T +from typing import Any, cast 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: T.Any, frame: T.Any) -> None: + def signal_handler(sig: Any, frame: Any) -> None: if self.args.use_saves: self.roster.save() exit(0) @@ -91,7 +91,7 @@ class CursesUserInterface(BaseUserInterface): self._switch_to_roster_view() while True: - key = T.cast(int, self._screen.getch()) + key = cast(int, self._screen.getch()) if key == curses.ERR: time.sleep(0.1) @@ -115,7 +115,7 @@ class CursesUserInterface(BaseUserInterface): self._switch_view(self._roster_view) def _switch_to_create_char_name_view( - self, player_name: T.Optional[str] = None + self, player_name: str | None = None ) -> None: view = ChooseCharacterNameView(self._screen, player_name) view.on_cancel += self._switch_to_roster_view @@ -123,7 +123,7 @@ class CursesUserInterface(BaseUserInterface): self._switch_view(view) def _switch_to_create_char_race_view( - self, player_name: str, race: T.Optional[Race] = None + self, player_name: str, race: Race | None = None ) -> None: view = ChooseCharacterRaceView(self._screen, self.data.races, race) view.on_cancel += lambda: self._switch_to_create_char_name_view( @@ -135,7 +135,7 @@ class CursesUserInterface(BaseUserInterface): self._switch_view(view) def _switch_to_create_char_class_view( - self, player_name: str, race: Race, class_: T.Optional[Class] = None + self, player_name: str, race: Race, class_: Class | None = None ) -> None: view = ChooseCharacterClassView( self._screen, self.data.classes, class_ diff --git a/pqcli/ui/curses/event_handler.py b/pqcli/ui/curses/event_handler.py index c26ede5..2834783 100644 --- a/pqcli/ui/curses/event_handler.py +++ b/pqcli/ui/curses/event_handler.py @@ -1,14 +1,15 @@ -import typing as T +from collections.abc import Callable +from typing import Any class EventHandler: def __init__(self) -> None: - self.callbacks: T.List[T.Callable[..., T.Any]] = [] + self.callbacks: list[Callable[..., Any]] = [] - def __call__(self, *args: T.Any, **kwargs: T.Any) -> None: + def __call__(self, *args: Any, **kwargs: Any) -> None: for callback in self.callbacks: callback(*args, **kwargs) - def __iadd__(self, callback: T.Callable[..., T.Any]) -> "EventHandler": + def __iadd__(self, callback: Callable[..., Any]) -> "EventHandler": self.callbacks.append(callback) return self diff --git a/pqcli/ui/curses/util.py b/pqcli/ui/curses/util.py index 095bdc4..d829b43 100644 --- a/pqcli/ui/curses/util.py +++ b/pqcli/ui/curses/util.py @@ -1,7 +1,8 @@ import curses import curses.ascii -import typing as T +from collections.abc import Callable, Iterable from dataclasses import dataclass +from typing import Any, TypeVar, overload KEYS_CYCLE = {curses.ascii.TAB} KEYS_DOWN = set(map(ord, "jJ")) | {curses.KEY_DOWN} @@ -13,10 +14,22 @@ KEYS_CANCEL = set(map(ord, "qQ")) | {curses.ascii.ESC} @dataclass class Choice: - keys: T.List[int] + keys: list[int] desc: str - callback: T.Callable[..., T.Any] + callback: Callable[..., Any] -def first(source: T.Iterable[T.Any], default: T.Any = None) -> T.Any: +ItemT = TypeVar("ItemT") +DefaultT = TypeVar("DefaultT") + + +@overload +def first(source: Iterable[ItemT]) -> ItemT | None: ... + + +@overload +def first(source: Iterable[ItemT], default: DefaultT) -> ItemT | DefaultT: ... + + +def first(source: Iterable[Any], default: Any = None) -> Any: return next(iter(source), default) diff --git a/pqcli/ui/curses/views/base_view.py b/pqcli/ui/curses/views/base_view.py index 027f760..7dd9b50 100644 --- a/pqcli/ui/curses/views/base_view.py +++ b/pqcli/ui/curses/views/base_view.py @@ -1,8 +1,8 @@ -import typing as T +from typing import Any class BaseView: - def __init__(self, screen: T.Any) -> None: + def __init__(self, screen: Any) -> 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 142dc3b..033cb80 100644 --- a/pqcli/ui/curses/views/choose_character_view.py +++ b/pqcli/ui/curses/views/choose_character_view.py @@ -1,5 +1,5 @@ import functools -import typing as T +from typing import Any from pqcli.lingo import act_name, to_roman from pqcli.roster import Roster @@ -8,11 +8,11 @@ from pqcli.ui.curses.views.menu_view import MenuView class ChooseCharacterView(MenuView): - def __init__(self, screen: T.Any, roster: Roster, title: str) -> None: + def __init__(self, screen: Any, roster: Roster, title: str) -> None: super().__init__(screen, title) for y, player in enumerate(roster.players, 1): - key: T.Optional[str] = str(y % 10) if y <= 10 else None + key: str | None = str(y % 10) if y <= 10 else None best_equip = player.equipment.best diff --git a/pqcli/ui/curses/views/confirm_view.py b/pqcli/ui/curses/views/confirm_view.py index 1d456b0..ac231fd 100644 --- a/pqcli/ui/curses/views/confirm_view.py +++ b/pqcli/ui/curses/views/confirm_view.py @@ -1,4 +1,4 @@ -import typing as T +from typing import Any from pqcli.ui.curses.event_handler import EventHandler from pqcli.ui.curses.util import Choice @@ -8,14 +8,14 @@ from .base_view import BaseView class ConfirmView(BaseView): - def __init__(self, screen: T.Any, title: str) -> None: + def __init__(self, screen: Any, title: str) -> None: super().__init__(screen) self.title = f"{title}:" self.on_cancel = EventHandler() self.on_confirm = EventHandler() - self.choices: T.List[Choice] = [ + self.choices: list[Choice] = [ Choice( keys=list(map(ord, "yY")), desc="[Y] Yes", 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 e80202e..5451958 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,5 +1,6 @@ import functools -import typing as T +from collections.abc import Sequence +from typing import Any from pqcli.config import Class from pqcli.ui.curses.util import KEYS_CANCEL, Choice @@ -9,9 +10,9 @@ from pqcli.ui.curses.views.menu_view import MenuView class ChooseCharacterClassView(MenuView): def __init__( self, - screen: T.Any, - classes: T.Sequence[Class], - class_: T.Optional[Class] = None, + screen: Any, + classes: Sequence[Class], + class_: Class | None = None, ) -> None: super().__init__( screen, @@ -20,7 +21,7 @@ class ChooseCharacterClassView(MenuView): ) for y, class_ in enumerate(classes, 1): - key: T.Optional[str] = str(y % 10) if y <= 10 else None + key: str | None = str(y % 10) if y <= 10 else None self._choices.append( Choice( keys=[ord(key)] if key is not None else [], 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 12d806f..8f0db8f 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,6 @@ import curses import curses.ascii -import typing as T +from typing import Any from pqcli.mechanic import generate_name from pqcli.ui.curses.event_handler import EventHandler @@ -10,17 +10,15 @@ from pqcli.ui.curses.widgets.focusable import focus_standout class ChooseCharacterNameView(BaseView): - def __init__( - self, screen: T.Any, character_name: T.Optional[str] = None - ) -> None: + def __init__(self, screen: Any, character_name: str | None = None) -> None: super().__init__(screen) self.on_cancel = EventHandler() self.on_confirm = EventHandler() self._active_widget = 0 - self._win: T.Optional[T.Any] = None - self._text_win: T.Optional[T.Any] = None + self._win: Any | None = None + self._text_win: Any | 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 134a8ec..5ea54a5 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,5 +1,6 @@ import functools -import typing as T +from collections.abc import Sequence +from typing import Any from pqcli.config import Race from pqcli.ui.curses.util import KEYS_CANCEL, Choice @@ -9,9 +10,9 @@ from pqcli.ui.curses.views.menu_view import MenuView class ChooseCharacterRaceView(MenuView): def __init__( self, - screen: T.Any, - races: T.Sequence[Race], - race: T.Optional[Race] = None, + screen: Any, + races: Sequence[Race], + race: Race | None = None, ) -> None: super().__init__( screen, @@ -20,7 +21,7 @@ class ChooseCharacterRaceView(MenuView): ) for y, race in enumerate(races, 1): - key: T.Optional[str] = str(y % 10) if y <= 10 else None + key: str | None = str(y % 10) if y <= 10 else None self._choices.append( Choice( keys=[ord(key)] if key is not None else [], 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 f40eb06..49c3ccb 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,5 +1,5 @@ import curses.ascii -import typing as T +from typing import Any from pqcli.config import PRIME_STATS from pqcli.mechanic import StatsBuilder @@ -10,15 +10,15 @@ from pqcli.ui.curses.widgets.focusable import focus_standout class ChooseCharacterStatsView(BaseView): - def __init__(self, screen: T.Any) -> None: + def __init__(self, screen: Any) -> None: super().__init__(screen) self.on_cancel = EventHandler() self.on_confirm = EventHandler() self._active_widget = 0 - self._win: T.Optional[T.Any] = None - self._stats_win: T.Optional[T.Any] = None + self._win: Any | None = None + self._stats_win: Any | 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 5568272..a8610e4 100644 --- a/pqcli/ui/curses/views/game_view/__init__.py +++ b/pqcli/ui/curses/views/game_view/__init__.py @@ -1,7 +1,7 @@ import argparse import curses import datetime -import typing as T +from typing import Any from pqcli.game_data import GameData from pqcli.mechanic import Player, SignalMixin, Simulation @@ -30,7 +30,7 @@ from .task_progress_window import TaskProgressWindow class GameView(BaseView): def __init__( self, - screen: T.Any, + screen: Any, roster: Roster, player: Player, args: argparse.Namespace, @@ -225,7 +225,7 @@ class GameView(BaseView): x=0, ) - self._focusable_children: T.List[Focusable] = [ + self._focusable_children: list[Focusable] = [ self._char_sheet_win, self._spell_book_win, self._equipment_win, @@ -233,7 +233,7 @@ class GameView(BaseView): self._plot_win, self._quest_book_win, ] - self._children: T.List[Widget] = self._focusable_children + [ + self._children: list[Widget] = self._focusable_children + [ self._task_win ] 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 c05102d..7cf8c58 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 @@ -import typing as T +from typing import Any from pqcli.mechanic import Player, StatType from pqcli.ui.curses.widgets import Focusable @@ -8,7 +8,7 @@ from .progress_bar_window import DataTableProgressBarWindow class CharacterSheetWindow(Focusable, DataTableProgressBarWindow): def __init__( - self, player: Player, parent: T.Any, h: int, w: int, y: int, x: int + self, player: Player, parent: Any, 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 150a990..2463e50 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 @@ -import typing as T +from typing import Any from pqcli.mechanic import EquipmentType, Player from pqcli.ui.curses.widgets import DataTable, Focusable, WindowWrapper @@ -6,7 +6,7 @@ from pqcli.ui.curses.widgets import DataTable, Focusable, WindowWrapper class EquipmentWindow(Focusable, WindowWrapper): def __init__( - self, player: Player, parent: T.Any, h: int, w: int, y: int, x: int + self, player: Player, parent: Any, h: int, w: int, y: int, x: int ) -> None: super().__init__(parent, h, w, y, x) self._on_focus_change += self._render @@ -39,7 +39,7 @@ class EquipmentWindow(Focusable, WindowWrapper): self._render() def _sync_equipment_change( - self, equipment_type: EquipmentType, item_name: T.Optional[str] + self, equipment_type: EquipmentType, item_name: str | None ) -> None: self._data_table.set(equipment_type.value, item_name or "") self._data_table.select(equipment_type.value) diff --git a/pqcli/ui/curses/views/game_view/inventory_window.py b/pqcli/ui/curses/views/game_view/inventory_window.py index a006b9e..e3d3c0c 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 @@ -import typing as T +from typing import Any from pqcli.mechanic import InventoryItem, Player from pqcli.ui.curses.widgets import Focusable @@ -8,7 +8,7 @@ from .progress_bar_window import DataTableProgressBarWindow class InventoryWindow(Focusable, DataTableProgressBarWindow): def __init__( - self, player: Player, parent: T.Any, h: int, w: int, y: int, x: int + self, player: Player, parent: Any, 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 dbb6c3c..1d91359 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 @@ -import typing as T +from typing import Any from pqcli.lingo import act_name from pqcli.mechanic import Player @@ -11,7 +11,7 @@ class PlotWindow(Focusable, ListBoxProgressBarWindow): cutoff = 100 def __init__( - self, player: Player, parent: T.Any, h: int, w: int, y: int, x: int + self, player: Player, parent: Any, 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 cd18c0c..c14160a 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,5 @@ import curses -import typing as T +from typing import Any from pqcli.ui.curses.widgets import ( DataTable, @@ -13,7 +13,7 @@ from pqcli.ui.curses.widgets import ( class ProgressBarWindow(WindowWrapper): def __init__( self, - parent: T.Any, + parent: Any, h: int, w: int, y: int, @@ -82,7 +82,7 @@ class ProgressBarWindow(WindowWrapper): class DataTableProgressBarWindow(ProgressBarWindow): def __init__( self, - parent: T.Any, + parent: Any, h: int, w: int, y: int, @@ -123,7 +123,7 @@ class DataTableProgressBarWindow(ProgressBarWindow): class ListBoxProgressBarWindow(ProgressBarWindow): def __init__( self, - parent: T.Any, + parent: Any, 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 c11985d..25280eb 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 @@ -import typing as T +from typing import Any from pqcli.mechanic import Player from pqcli.ui.curses.widgets import Focusable @@ -10,7 +10,7 @@ class QuestBookWindow(Focusable, ListBoxProgressBarWindow): cutoff = 100 def __init__( - self, player: Player, parent: T.Any, h: int, w: int, y: int, x: int + self, player: Player, parent: Any, 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 877efc2..bd74be5 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 @@ -import typing as T +from typing import Any from pqcli.lingo import to_roman from pqcli.mechanic import Player, Spell @@ -7,7 +7,7 @@ from pqcli.ui.curses.widgets import DataTable, Focusable, WindowWrapper class SpellBookWindow(Focusable, WindowWrapper): def __init__( - self, player: Player, parent: T.Any, h: int, w: int, y: int, x: int + self, player: Player, parent: Any, 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 37af9ef..944e1ed 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 @@ -import typing as T +from typing import Any from pqcli.mechanic import Player from pqcli.ui.curses.widgets import ( @@ -11,7 +11,7 @@ from pqcli.ui.curses.widgets import ( class TaskProgressWindow(Focusable, WindowWrapper): def __init__( - self, player: Player, parent: T.Any, h: int, w: int, y: int, x: int + self, player: Player, parent: Any, 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 2938797..9fd6ab4 100644 --- a/pqcli/ui/curses/views/menu_view.py +++ b/pqcli/ui/curses/views/menu_view.py @@ -1,4 +1,4 @@ -import typing as T +from typing import Any from pqcli.ui.curses.event_handler import EventHandler from pqcli.ui.curses.util import Choice @@ -8,7 +8,7 @@ from pqcli.ui.curses.widgets import Menu class MenuView(BaseView): def __init__( - self, screen: T.Any, title: str, active_choice: int = 0 + self, screen: Any, title: str, active_choice: int = 0 ) -> None: super().__init__(screen) @@ -16,7 +16,7 @@ class MenuView(BaseView): self.on_confirm = EventHandler() self._title = f"{title}:" - self._choices: T.List[Choice] = [] + self._choices: list[Choice] = [] self._active_choice = active_choice def start(self) -> None: diff --git a/pqcli/ui/curses/views/roster_view.py b/pqcli/ui/curses/views/roster_view.py index a03e395..163c753 100644 --- a/pqcli/ui/curses/views/roster_view.py +++ b/pqcli/ui/curses/views/roster_view.py @@ -1,5 +1,5 @@ import curses -import typing as T +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 @@ -29,7 +29,7 @@ LOGO = """ class MainMenu(Menu): def __init__( - self, choices: T.List[Choice], scr_height: int, scr_width: int + self, choices: list[Choice], scr_height: int, scr_width: int ) -> None: super().__init__( header=LOGO, @@ -50,7 +50,7 @@ class MainMenu(Menu): class RosterView(BaseView): - def __init__(self, screen: T.Any) -> None: + def __init__(self, screen: Any) -> None: super().__init__(screen) self.on_create = EventHandler() @@ -58,7 +58,7 @@ class RosterView(BaseView): self.on_delete = EventHandler() self.on_quit = EventHandler() - self.choices: T.List[Choice] = [ + self.choices: list[Choice] = [ Choice( keys=list(map(ord, "cC")), desc="[C] Create new character", diff --git a/pqcli/ui/curses/widgets/base.py b/pqcli/ui/curses/widgets/base.py index 7310d76..b208286 100644 --- a/pqcli/ui/curses/widgets/base.py +++ b/pqcli/ui/curses/widgets/base.py @@ -1,5 +1,5 @@ import curses -import typing as T +from typing import Any class Widget: @@ -12,10 +12,10 @@ class Widget: class WindowWrapper(Widget): def __init__( - self, parent: T.Optional[T.Any], h: int, w: int, y: int, x: int + self, parent: Any | None, h: int, w: int, y: int, x: int ) -> None: self._parent = parent - self._win: T.Optional[T.Any] + self._win: Any | None if not parent: self._win = None @@ -30,12 +30,12 @@ class WindowWrapper(Widget): del self._win self._win = None - def getbegyx(self) -> T.Tuple[int, int]: + def getbegyx(self) -> tuple[int, int]: if not self._win: return (0, 0) return self._win.getbegyx() - def getmaxyx(self) -> T.Tuple[int, int]: + def getmaxyx(self) -> tuple[int, int]: if not self._win: return (0, 0) return self._win.getmaxyx() diff --git a/pqcli/ui/curses/widgets/data_table.py b/pqcli/ui/curses/widgets/data_table.py index 6eef088..aac1ee9 100644 --- a/pqcli/ui/curses/widgets/data_table.py +++ b/pqcli/ui/curses/widgets/data_table.py @@ -1,5 +1,5 @@ import curses -import typing as T +from typing import Any from pqcli.ui.curses.colors import COLOR_HIGHLIGHT, has_colors @@ -9,7 +9,7 @@ from .scrollable import Scrollable class DataTable(Scrollable): def __init__( self, - parent: T.Any, + parent: Any, h: int, w: int, y: int, @@ -19,8 +19,8 @@ class DataTable(Scrollable): super().__init__(parent, h, w, y, x) self._align_right = align_right - self._items: T.List[T.Tuple[str, str]] - self._selected: T.Optional[int] = None + self._items: list[tuple[str, str]] + self._selected: int | None = None def add(self, text: str, value: str) -> None: self._items.append((text, value)) @@ -40,10 +40,10 @@ class DataTable(Scrollable): del self._items[idx] self.scroll_to_item(idx) - def select(self, text: T.Optional[str]) -> None: + def select(self, text: str | None) -> None: self._selected = None if text is None else self.get_idx(text) - def get_idx(self, text: str) -> T.Optional[int]: + def get_idx(self, text: str) -> int | None: for idx, row in enumerate(self._items): if row[0].strip() == text.strip(): return idx diff --git a/pqcli/ui/curses/widgets/focusable.py b/pqcli/ui/curses/widgets/focusable.py index 1d3640e..879bd62 100644 --- a/pqcli/ui/curses/widgets/focusable.py +++ b/pqcli/ui/curses/widgets/focusable.py @@ -1,13 +1,14 @@ import contextlib import curses -import typing as T +from collections.abc import Generator +from typing import Any from pqcli.ui.curses.colors import COLOR_FOCUSED, has_colors from pqcli.ui.curses.event_handler import EventHandler @contextlib.contextmanager -def focus_standout(focused: bool, win: T.Any) -> T.Generator: +def focus_standout(focused: bool, win: Any) -> Generator: if focused: if has_colors(): win.attron(curses.color_pair(COLOR_FOCUSED)) @@ -22,7 +23,7 @@ def focus_standout(focused: bool, win: T.Any) -> T.Generator: class Focusable: - def __init__(self, *args: T.Any, **kwargs: T.Any) -> None: + def __init__(self, *args: Any, **kwargs: Any) -> None: self._focused = False self._on_focus_change = EventHandler() super().__init__(*args, **kwargs) @@ -37,6 +38,6 @@ class Focusable: self._on_focus_change() @contextlib.contextmanager - def focus_standout(self, win: T.Any) -> T.Generator: + def focus_standout(self, win: Any) -> Generator: 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 5d4fd68..3e0ed83 100644 --- a/pqcli/ui/curses/widgets/list_box.py +++ b/pqcli/ui/curses/widgets/list_box.py @@ -1,5 +1,5 @@ import curses -import typing as T +from typing import Any from pqcli.ui.curses.colors import COLOR_HIGHLIGHT, has_colors @@ -7,11 +7,11 @@ from .scrollable import Scrollable class ListBox(Scrollable): - _items: T.List[str] + _items: list[str] - def __init__(self, parent: T.Any, h: int, w: int, y: int, x: int) -> None: + def __init__(self, parent: Any, h: int, w: int, y: int, x: int) -> None: super().__init__(parent, h, w, y, x) - self._selected: T.Optional[int] = None + self._selected: int | None = None def add(self, text: str) -> None: self._items.append(text) @@ -22,7 +22,7 @@ class ListBox(Scrollable): idx %= len(self._items) self._items[idx] = value - def get(self, idx: int) -> T.Optional[str]: + def get(self, idx: int) -> str | None: if not self._items: return None if idx < 0: @@ -35,7 +35,7 @@ class ListBox(Scrollable): def delete(self, idx: int, count: int = 1) -> None: del self._items[idx : idx + count] - def select(self, idx: T.Optional[int]) -> None: + def select(self, idx: int | None) -> None: if idx < 0 and len(self._items): idx %= len(self._items) self._selected = idx diff --git a/pqcli/ui/curses/widgets/menu.py b/pqcli/ui/curses/widgets/menu.py index a26fb70..826f7db 100644 --- a/pqcli/ui/curses/widgets/menu.py +++ b/pqcli/ui/curses/widgets/menu.py @@ -1,6 +1,6 @@ import curses import curses.ascii -import typing as T +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 @@ -12,7 +12,7 @@ class Menu(Widget): def __init__( self, header: str, - choices: T.List[Choice], + choices: list[Choice], active_choice: int, scr_height: int, scr_width: int, @@ -28,7 +28,7 @@ class Menu(Widget): w = max(map(len, all_lines)) + 1 h = len(all_lines) + 1 - self._pad: T.Optional[T.Any] = curses.newpad(h, w) + self._pad: Any | None = curses.newpad(h, w) for y, line in enumerate(self._header_lines): self._pad.move(y, 0) @@ -40,7 +40,7 @@ class Menu(Widget): del self._pad self._pad = None - def getmaxyx(self) -> T.Tuple[int, int]: + def getmaxyx(self) -> tuple[int, int]: if not self._pad: return (0, 0) return self._pad.getmaxyx() diff --git a/pqcli/ui/curses/widgets/progress_bar.py b/pqcli/ui/curses/widgets/progress_bar.py index 667f0f0..da8085e 100644 --- a/pqcli/ui/curses/widgets/progress_bar.py +++ b/pqcli/ui/curses/widgets/progress_bar.py @@ -1,6 +1,6 @@ import curses import datetime -import typing as T +from typing import Any from pqcli.lingo import format_timespan from pqcli.ui.curses.colors import COLOR_PROGRESSBAR, has_colors @@ -9,7 +9,7 @@ from pqcli.ui.curses.widgets.base import WindowWrapper class ProgressBar(WindowWrapper): def __init__( - self, parent: T.Any, h: int, w: int, y: int, x: int, show_time: bool + self, parent: Any, h: int, w: int, y: int, x: int, show_time: bool ) -> None: super().__init__(parent, h, w, y, x) @@ -17,10 +17,10 @@ class ProgressBar(WindowWrapper): self._max_pos = 1.0 self._show_time = show_time - self._last_tick: T.Optional[T.Tuple[datetime.datetime, float]] = None + self._last_tick: tuple[datetime.datetime, float] | None = None @property - def time_left(self) -> T.Optional[datetime.timedelta]: + def time_left(self) -> datetime.timedelta | None: if self._last_tick is None: return None time_then, pos_then = self._last_tick diff --git a/pqcli/ui/curses/widgets/scrollable.py b/pqcli/ui/curses/widgets/scrollable.py index 07735f5..78abf90 100644 --- a/pqcli/ui/curses/widgets/scrollable.py +++ b/pqcli/ui/curses/widgets/scrollable.py @@ -1,5 +1,5 @@ import curses -import typing as T +from typing import Any from pqcli.ui.curses.colors import ( COLOR_SCROLLBAR_THUMB, @@ -11,11 +11,11 @@ from .base import WindowWrapper class Scrollable(WindowWrapper): - def __init__(self, parent: T.Any, h: int, w: int, y: int, x: int) -> None: + def __init__(self, parent: Any, h: int, w: int, y: int, x: int) -> None: super().__init__(parent, h, w, y, x) self._scroll_pos = 0 - self._pad: T.Optional[T.Any] = curses.newpad(1, 1) - self._items: T.List[T.Any] = [] + self._pad: Any | None = curses.newpad(1, 1) + self._items: list[Any] = [] def clear(self) -> None: self._items = [] diff --git a/pyproject.toml b/pyproject.toml index bcd57af..6ca1da2 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -46,7 +46,7 @@ dev = ["prek>=0.2", "pytest>=8.0"] line-length = 79 [tool.ruff.lint] -select = ["E", "F", "W", "I"] +select = ["E", "F", "W", "I", "UP"] [tool.ruff.lint.isort] known-first-party = ["pqcli"] diff --git a/tests/game_trace.py b/tests/game_trace.py index 31fe31a..1d8bb91 100644 --- a/tests/game_trace.py +++ b/tests/game_trace.py @@ -1,5 +1,4 @@ import datetime -import typing as T from pathlib import Path from pqcli import random @@ -20,7 +19,7 @@ STATS = { } -def run_trace(seed: str, ticks: int = 3000) -> T.List[str]: +def run_trace(seed: str, ticks: int = 3000) -> list[str]: """Play a deterministic game and return everything that happened.""" data = load_game_data() player = Player( @@ -34,7 +33,7 @@ def run_trace(seed: str, ticks: int = 3000) -> T.List[str]: random.seed(seed) simulation = Simulation(player, data) - lines: T.List[str] = [] + lines: list[str] = [] player.connect( "new_task", lambda: lines.append(f"task: {player.task.description}"), diff --git a/tests/test_game_data.py b/tests/test_game_data.py index b652fda..dacd260 100644 --- a/tests/test_game_data.py +++ b/tests/test_game_data.py @@ -1,8 +1,8 @@ import datetime import json import pickle -import typing as T from pathlib import Path +from typing import Any import pytest @@ -18,7 +18,7 @@ from pqcli.game_data import ( from pqcli.mechanic import Player, Stats, monster_task -def write(directory: Path, name: str, value: T.Any) -> Path: +def write(directory: Path, name: str, value: Any) -> Path: path = directory / f"{name}.json" path.write_text(json.dumps(value), encoding="utf-8") return path @@ -98,7 +98,7 @@ def test_unknown_file_warns( ], ) def test_invalid_override_raises( - tmp_path: Path, name: str, value: T.Any, message: str + tmp_path: Path, name: str, value: Any, message: str ) -> None: write(tmp_path, name, value) with pytest.raises(DataError) as excinfo: