diff --git a/Generate.py b/Generate.py index e72887c2..9bc8d106 100644 --- a/Generate.py +++ b/Generate.py @@ -10,8 +10,8 @@ import sys import urllib.parse import urllib.request from collections import Counter -from typing import Any, Dict, Tuple, Union from itertools import chain +from typing import Any import ModuleUpdate @@ -77,7 +77,7 @@ def get_seed_name(random_source) -> str: return f"{random_source.randint(0, pow(10, seeddigits) - 1)}".zfill(seeddigits) -def main(args=None) -> Tuple[argparse.Namespace, int]: +def main(args=None) -> tuple[argparse.Namespace, int]: # __name__ == "__main__" check so unittests that already imported worlds don't trip this. if __name__ == "__main__" and "worlds" in sys.modules: raise Exception("Worlds system should not be loaded before logging init.") @@ -95,7 +95,7 @@ def main(args=None) -> Tuple[argparse.Namespace, int]: logging.info("Race mode enabled. Using non-deterministic random source.") random.seed() # reset to time-based random source - weights_cache: Dict[str, Tuple[Any, ...]] = {} + weights_cache: dict[str, tuple[Any, ...]] = {} if args.weights_file_path and os.path.exists(args.weights_file_path): try: weights_cache[args.weights_file_path] = read_weights_yamls(args.weights_file_path) @@ -180,7 +180,7 @@ def main(args=None) -> Tuple[argparse.Namespace, int]: erargs.name = {} erargs.csv_output = args.csv_output - settings_cache: Dict[str, Tuple[argparse.Namespace, ...]] = \ + settings_cache: dict[str, tuple[argparse.Namespace, ...]] = \ {fname: (tuple(roll_settings(yaml, args.plando) for yaml in yamls) if args.sameoptions else None) for fname, yamls in weights_cache.items()} @@ -212,7 +212,7 @@ def main(args=None) -> Tuple[argparse.Namespace, int]: path = player_path_cache[player] if path: try: - settings: Tuple[argparse.Namespace, ...] = settings_cache[path] if settings_cache[path] else \ + settings: tuple[argparse.Namespace, ...] = settings_cache[path] if settings_cache[path] else \ tuple(roll_settings(yaml, args.plando) for yaml in weights_cache[path]) for settingsObject in settings: for k, v in vars(settingsObject).items(): @@ -242,7 +242,7 @@ def main(args=None) -> Tuple[argparse.Namespace, int]: return erargs, seed -def read_weights_yamls(path) -> Tuple[Any, ...]: +def read_weights_yamls(path) -> tuple[Any, ...]: try: if urllib.parse.urlparse(path).scheme in ('https', 'file'): yaml = str(urllib.request.urlopen(path).read(), "utf-8-sig") @@ -378,7 +378,7 @@ def update_weights(weights: dict, new_weights: dict, update_type: str, name: str return weights -def roll_meta_option(option_key, game: str, category_dict: Dict) -> Any: +def roll_meta_option(option_key, game: str, category_dict: dict) -> Any: from worlds import AutoWorldRegister if not game: diff --git a/Launcher.py b/Launcher.py index 594286fa..50349024 100644 --- a/Launcher.py +++ b/Launcher.py @@ -16,9 +16,10 @@ import subprocess import sys import urllib.parse import webbrowser +from collections.abc import Callable, Sequence from os.path import isfile from shutil import which -from typing import Callable, Optional, Sequence, Tuple, Union, Any +from typing import Any if __name__ == "__main__": import ModuleUpdate @@ -114,7 +115,7 @@ components.extend([ ]) -def handle_uri(path: str, launch_args: Tuple[str, ...]) -> None: +def handle_uri(path: str, launch_args: tuple[str, ...]) -> None: url = urllib.parse.urlparse(path) queries = urllib.parse.parse_qs(url.query) launch_args = (path, *launch_args) @@ -162,7 +163,7 @@ def handle_uri(path: str, launch_args: Tuple[str, ...]) -> None: ).open() -def identify(path: Union[None, str]) -> Tuple[Union[None, str], Union[None, Component]]: +def identify(path: None | str) -> tuple[None | str, None | Component]: if path is None: return None, None for component in components: @@ -173,7 +174,7 @@ def identify(path: Union[None, str]) -> Tuple[Union[None, str], Union[None, Comp return None, None -def get_exe(component: Union[str, Component]) -> Optional[Sequence[str]]: +def get_exe(component: str | Component) -> Sequence[str] | None: if isinstance(component, str): name = component component = None @@ -226,7 +227,7 @@ def create_shortcut(button: Any, component: Component) -> None: button.menu.dismiss() -refresh_components: Optional[Callable[[], None]] = None +refresh_components: Callable[[], None] | None = None def run_gui(path: str, args: Any) -> None: @@ -451,7 +452,7 @@ def run_component(component: Component, *args): logging.warning(f"Component {component} does not appear to be executable.") -def main(args: Optional[Union[argparse.Namespace, dict]] = None): +def main(args: argparse.Namespace | dict | None = None): if isinstance(args, argparse.Namespace): args = {k: v for k, v in args._get_kwargs()} elif not args: diff --git a/Main.py b/Main.py index 147fa382..442c2ff4 100644 --- a/Main.py +++ b/Main.py @@ -7,14 +7,13 @@ import tempfile import time import zipfile import zlib -from typing import Dict, List, Optional, Set, Tuple, Union import worlds -from BaseClasses import CollectionState, Item, Location, LocationProgressType, MultiWorld, Region +from BaseClasses import CollectionState, Item, Location, LocationProgressType, MultiWorld from Fill import FillError, balance_multiworld_progression, distribute_items_restrictive, flood_items, \ parse_planned_blocks, distribute_planned_blocks, resolve_early_locations_for_planned from Options import StartInventoryPool -from Utils import __version__, output_path, version_tuple, get_settings +from Utils import __version__, output_path, version_tuple from settings import get_settings from worlds import AutoWorld from worlds.generic.Rules import exclusion_rules, locality_rules @@ -22,7 +21,7 @@ from worlds.generic.Rules import exclusion_rules, locality_rules __all__ = ["main"] -def main(args, seed=None, baked_server_options: Optional[Dict[str, object]] = None): +def main(args, seed=None, baked_server_options: dict[str, object] | None = None): if not baked_server_options: baked_server_options = get_settings().server_options.as_dict() assert isinstance(baked_server_options, dict) @@ -140,7 +139,7 @@ def main(args, seed=None, baked_server_options: Optional[Dict[str, object]] = No # remove starting inventory from pool items. # Because some worlds don't actually create items during create_items this has to be as late as possible. fallback_inventory = StartInventoryPool({}) - depletion_pool: Dict[int, Dict[str, int]] = { + depletion_pool: dict[int, dict[str, int]] = { player: getattr(multiworld.worlds[player].options, "start_inventory_from_pool", fallback_inventory).value.copy() for player in multiworld.player_ids } @@ -149,7 +148,7 @@ def main(args, seed=None, baked_server_options: Optional[Dict[str, object]] = No } if target_per_player: - new_itempool: List[Item] = [] + new_itempool: list[Item] = [] # Make new itempool with start_inventory_from_pool items removed for item in multiworld.itempool: @@ -233,7 +232,7 @@ def main(args, seed=None, baked_server_options: Optional[Dict[str, object]] = No pool.submit(AutoWorld.call_single, multiworld, "generate_output", player, temp_dir)) # collect ER hint info - er_hint_data: Dict[int, Dict[int, str]] = {} + er_hint_data: dict[int, dict[int, str]] = {} AutoWorld.call_all(multiworld, 'extend_hint_information', er_hint_data) def write_multidata(): @@ -274,7 +273,7 @@ def main(args, seed=None, baked_server_options: Optional[Dict[str, object]] = No for player in multiworld.groups[location.item.player]["players"]: precollected_hints[player].add(hint) - locations_data: Dict[int, Dict[int, Tuple[int, int, int]]] = {player: {} for player in multiworld.player_ids} + locations_data: dict[int, dict[int, tuple[int, int, int]]] = {player: {} for player in multiworld.player_ids} for location in multiworld.get_filled_locations(): if type(location.address) == int: assert location.item.code is not None, "item code None should be event, " \ @@ -303,12 +302,12 @@ def main(args, seed=None, baked_server_options: Optional[Dict[str, object]] = No } data_package["Archipelago"] = worlds.network_data_package["games"]["Archipelago"] - checks_in_area: Dict[int, Dict[str, Union[int, List[int]]]] = {} + checks_in_area: dict[int, dict[str, int | list[int]]] = {} # get spheres -> filter address==None -> skip empty - spheres: List[Dict[int, Set[int]]] = [] + spheres: list[dict[int, set[int]]] = [] for sphere in multiworld.get_sendable_spheres(): - current_sphere: Dict[int, Set[int]] = collections.defaultdict(set) + current_sphere: dict[int, set[int]] = collections.defaultdict(set) for sphere_location in sphere: current_sphere[sphere_location.player].add(sphere_location.address) diff --git a/settings.py b/settings.py index 255c537f..ef1ea9ad 100644 --- a/settings.py +++ b/settings.py @@ -10,9 +10,10 @@ import sys import types import typing import warnings +from collections.abc import Iterator, Sequence from enum import IntEnum from threading import Lock -from typing import cast, Any, BinaryIO, ClassVar, Dict, Iterator, List, Optional, TextIO, Tuple, Union, TypeVar +from typing import cast, Any, BinaryIO, ClassVar, TextIO, TypeVar, Union __all__ = [ "get_settings", "fmt_doc", "no_gui", @@ -23,7 +24,7 @@ __all__ = [ no_gui = False skip_autosave = False -_world_settings_name_cache: Dict[str, str] = {} # TODO: cache on disk and update when worlds change +_world_settings_name_cache: dict[str, str] = {} # TODO: cache on disk and update when worlds change _world_settings_name_cache_updated = False _lock = Lock() @@ -53,7 +54,7 @@ def fmt_doc(cls: type, level: int) -> str: class Group: - _type_cache: ClassVar[Optional[Dict[str, Any]]] = None + _type_cache: ClassVar[dict[str, Any] | None] = None _dumping: bool = False _has_attr: bool = False _changed: bool = False @@ -106,7 +107,7 @@ class Group: self.__dict__.values())) @classmethod - def get_type_hints(cls) -> Dict[str, Any]: + def get_type_hints(cls) -> dict[str, Any]: """Returns resolved type hints for the class""" if cls._type_cache is None: if not cls.__annotations__ or not isinstance(next(iter(cls.__annotations__.values())), str): @@ -124,10 +125,10 @@ class Group: return self[key] return default - def items(self) -> List[Tuple[str, Any]]: + def items(self) -> list[tuple[str, Any]]: return [(key, getattr(self, key)) for key in self] - def update(self, dct: Dict[str, Any]) -> None: + def update(self, dct: dict[str, Any]) -> None: assert isinstance(dct, dict), f"{self.__class__.__name__}.update called with " \ f"{dct.__class__.__name__} instead of dict." @@ -196,7 +197,7 @@ class Group: warnings.warn(f"{self.__class__.__name__}.{k} " f"assigned from incompatible type {type(v).__name__}") - def as_dict(self, *args: str, downcast: bool = True) -> Dict[str, Any]: + def as_dict(self, *args: str, downcast: bool = True) -> dict[str, Any]: return { name: _to_builtin(cast(object, getattr(self, name))) if downcast else getattr(self, name) for name in self if not args or name in args @@ -211,7 +212,7 @@ class Group: f.write(f"{indent}{yaml_line}") @classmethod - def _dump_item(cls, name: Optional[str], attr: object, f: TextIO, level: int) -> None: + def _dump_item(cls, name: str | None, attr: object, f: TextIO, level: int) -> None: """Write a group, dict or sequence item to f, where attr can be a scalar or a collection""" # lazy construction of yaml Dumper to avoid loading Utils early @@ -223,7 +224,7 @@ class Group: def represent_mapping(self, tag: str, mapping: Any, flow_style: Any = None) -> MappingNode: from yaml import ScalarNode res: MappingNode = super().represent_mapping(tag, mapping, flow_style) - pairs = cast(List[Tuple[ScalarNode, Any]], res.value) + pairs = cast(list[tuple[ScalarNode, Any]], res.value) for k, v in pairs: k.style = None # remove quotes from keys return res @@ -329,9 +330,9 @@ class Path(str): """Marks the file as required and opens a file browser when missing""" is_exe: bool = False """Special cross-platform handling for executables""" - description: Optional[str] = None + description: str | None = None """Title to display when browsing for the file""" - copy_to: Optional[str] = None + copy_to: str | None = None """If not None, copy to AP folder instead of linking it""" @classmethod @@ -339,7 +340,7 @@ class Path(str): """Overload and raise to validate input files from browse""" pass - def browse(self: T, **kwargs: Any) -> Optional[T]: + def browse(self: T, **kwargs: Any) -> T | None: """Opens a file browser to search for the file""" raise NotImplementedError(f"Please use a subclass of Path for {self.__class__.__name__}") @@ -369,12 +370,12 @@ class _LocalPath(str): class FilePath(Path): # path to a file - md5s: ClassVar[List[Union[str, bytes]]] = [] + md5s: ClassVar[list[str | bytes]] = [] """MD5 hashes for default validator.""" def browse(self: T, - filetypes: Optional[typing.Sequence[typing.Tuple[str, typing.Sequence[str]]]] = None, **kwargs: Any)\ - -> Optional[T]: + filetypes: Sequence[tuple[str, Sequence[str]]] | None = None, **kwargs: Any)\ + -> T | None: from Utils import open_filename, is_windows if not filetypes: if self.is_exe: @@ -439,7 +440,7 @@ class FilePath(Path): class FolderPath(Path): # path to a folder - def browse(self: T, **kwargs: Any) -> Optional[T]: + def browse(self: T, **kwargs: Any) -> T | None: from Utils import open_directory res = open_directory(f"Select {self.description or self.__class__.__name__}", self) if res: @@ -597,16 +598,16 @@ class ServerOptions(Group): OFF = 0 ON = 1 - host: Optional[str] = None + host: str | None = None port: int = 38281 - password: Optional[str] = None - multidata: Optional[str] = None - savefile: Optional[str] = None + password: str | None = None + multidata: str | None = None + savefile: str | None = None disable_save: bool = False loglevel: str = "info" logtime: bool = False - server_password: Optional[ServerPassword] = None - disable_item_cheat: Union[DisableItemCheat, bool] = False + server_password: ServerPassword | None = None + disable_item_cheat: DisableItemCheat | bool = False location_check_points: LocationCheckPoints = LocationCheckPoints(1) hint_cost: HintCost = HintCost(10) release_mode: ReleaseMode = ReleaseMode("auto") @@ -702,7 +703,7 @@ does nothing if not found """ sni_path: SNIPath = SNIPath("SNI") - snes_rom_start: Union[SnesRomStart, bool] = True + snes_rom_start: SnesRomStart | bool = True class BizHawkClientOptions(Group): @@ -721,7 +722,7 @@ class BizHawkClientOptions(Group): """ emuhawk_path: EmuHawkPath = EmuHawkPath(None) - rom_start: Union[RomStart, bool] = True + rom_start: RomStart | bool = True # Top-level group with lazy loading of worlds @@ -733,7 +734,7 @@ class Settings(Group): sni_options: SNIOptions = SNIOptions() bizhawkclient_options: BizHawkClientOptions = BizHawkClientOptions() - _filename: Optional[str] = None + _filename: str | None = None def __getattribute__(self, key: str) -> Any: if key.startswith("_") or key in self.__class__.__dict__: @@ -787,7 +788,7 @@ class Settings(Group): return super().__getattribute__(key) - def __init__(self, location: Optional[str]): # change to PathLike[str] once we drop 3.8? + def __init__(self, location: str | None): # change to PathLike[str] once we drop 3.8? super().__init__() if location: from Utils import parse_yaml @@ -821,7 +822,7 @@ class Settings(Group): import atexit atexit.register(autosave) - def save(self, location: Optional[str] = None) -> None: # as above + def save(self, location: str | None = None) -> None: # as above from Utils import parse_yaml location = location or self._filename assert location, "No file specified" @@ -854,7 +855,7 @@ class Settings(Group): super().dump(f, level) @property - def filename(self) -> Optional[str]: + def filename(self) -> str | None: return self._filename @@ -867,7 +868,7 @@ def get_settings() -> Settings: if not res: from Utils import user_path, local_path filenames = ("options.yaml", "host.yaml") - locations: List[str] = [] + locations: list[str] = [] if os.path.join(os.getcwd()) != local_path(): locations += filenames # use files from cwd only if it's not the local_path locations += [user_path(filename) for filename in filenames] diff --git a/setup.py b/setup.py index 8d415932..2654cc69 100644 --- a/setup.py +++ b/setup.py @@ -1,22 +1,20 @@ import base64 import datetime +import io +import json import os import platform import shutil +import subprocess import sys import sysconfig +import threading +import urllib.request import warnings import zipfile -import urllib.request -import io -import json -import threading -import subprocess - +from collections.abc import Iterable, Sequence from hashlib import sha3_512 from pathlib import Path -from typing import Dict, Iterable, List, Optional, Sequence, Set, Tuple, Union - # This is a bit jank. We need cx-Freeze to be able to run anything from this script, so install it requirement = 'cx-Freeze==8.0.0' @@ -60,7 +58,7 @@ from Cython.Build import cythonize # On Python < 3.10 LogicMixin is not currently supported. -non_apworlds: Set[str] = { +non_apworlds: set[str] = { "A Link to the Past", "Adventure", "ArchipIDLE", @@ -147,7 +145,7 @@ def download_SNI() -> None: print(f"No SNI found for system spec {platform_name} {machine_name}") -signtool: Optional[str] +signtool: str | None if os.path.exists("X:/pw.txt"): print("Using signtool") with open("X:/pw.txt", encoding="utf-8-sig") as f: @@ -205,7 +203,7 @@ def remove_sprites_from_folder(folder: Path) -> None: os.remove(folder / file) -def _threaded_hash(filepath: Union[str, Path]) -> str: +def _threaded_hash(filepath: str | Path) -> str: hasher = sha3_512() hasher.update(open(filepath, "rb").read()) return base64.b85encode(hasher.digest()).decode() @@ -255,7 +253,7 @@ class BuildExeCommand(cx_Freeze.command.build_exe.build_exe): self.libfolder = Path(self.buildfolder, "lib") self.library = Path(self.libfolder, "library.zip") - def installfile(self, path: Path, subpath: Optional[Union[str, Path]] = None, keep_content: bool = False) -> None: + def installfile(self, path: Path, subpath: str | Path | None = None, keep_content: bool = False) -> None: folder = self.buildfolder if subpath: folder /= subpath @@ -374,7 +372,7 @@ class BuildExeCommand(cx_Freeze.command.build_exe.build_exe): from worlds.AutoWorld import AutoWorldRegister assert not non_apworlds - set(AutoWorldRegister.world_types), \ f"Unknown world {non_apworlds - set(AutoWorldRegister.world_types)} designated for .apworld" - folders_to_remove: List[str] = [] + folders_to_remove: list[str] = [] disabled_worlds_folder = "worlds_disabled" for entry in os.listdir(disabled_worlds_folder): if os.path.isdir(os.path.join(disabled_worlds_folder, entry)): @@ -446,12 +444,12 @@ class AppImageCommand(setuptools.Command): ("app-exec=", None, "The application to run inside the image."), ("yes", "y", 'Answer "yes" to all questions.'), ] - build_folder: Optional[Path] - dist_file: Optional[Path] - app_dir: Optional[Path] + build_folder: Path | None + dist_file: Path | None + app_dir: Path | None app_name: str - app_exec: Optional[Path] - app_icon: Optional[Path] # source file + app_exec: Path | None + app_icon: Path | None # source file app_id: str # lower case name, used for icon and .desktop yes: bool @@ -493,7 +491,7 @@ $APPDIR/$exe "$@" """) launcher_filename.chmod(0o755) - def install_icon(self, src: Path, name: Optional[str] = None, symlink: Optional[Path] = None) -> None: + def install_icon(self, src: Path, name: str | None = None, symlink: Path | None = None) -> None: assert self.app_dir, "Invalid app_dir" try: from PIL import Image @@ -556,7 +554,7 @@ $APPDIR/$exe "$@" subprocess.call(f'ARCH={build_arch} ./appimagetool -n "{self.app_dir}" "{self.dist_file}"', shell=True) -def find_libs(*args: str) -> Sequence[Tuple[str, str]]: +def find_libs(*args: str) -> Sequence[tuple[str, str]]: """Try to find system libraries to be included.""" if not args: return [] @@ -564,7 +562,7 @@ def find_libs(*args: str) -> Sequence[Tuple[str, str]]: arch = build_arch.replace('_', '-') libc = 'libc6' # we currently don't support musl - def parse(line: str) -> Tuple[Tuple[str, str, str], str]: + def parse(line: str) -> tuple[tuple[str, str, str], str]: lib, path = line.strip().split(' => ') lib, typ = lib.split(' ', 1) for test_arch in ('x86-64', 'i386', 'aarch64'): @@ -589,8 +587,8 @@ def find_libs(*args: str) -> Sequence[Tuple[str, str]]: k: v for k, v in (parse(line) for line in data if "=>" in line) } - def find_lib(lib: str, arch: str, libc: str) -> Optional[str]: - cache: Dict[Tuple[str, str, str], str] = getattr(find_libs, "cache") + def find_lib(lib: str, arch: str, libc: str) -> str | None: + cache: dict[tuple[str, str, str], str] = getattr(find_libs, "cache") for k, v in cache.items(): if k == (lib, arch, libc): return v @@ -599,7 +597,7 @@ def find_libs(*args: str) -> Sequence[Tuple[str, str]]: return v return None - res: List[Tuple[str, str]] = [] + res: list[tuple[str, str]] = [] for arg in args: # try exact match, empty libc, empty arch, empty arch and libc file = find_lib(arg, arch, libc) diff --git a/test/hosting/world.py b/test/hosting/world.py index e083e027..74126412 100644 --- a/test/hosting/world.py +++ b/test/hosting/world.py @@ -1,13 +1,12 @@ import re import shutil from pathlib import Path -from typing import Dict __all__ = ["copy", "delete"] -_new_worlds: Dict[str, str] = {} +_new_worlds: dict[str, str] = {} def copy(src: str, dst: str) -> None: