Core: purge py3.8 and py3.9 (#3973)
Co-authored-by: Remy Jette <remy@remyjette.com> Co-authored-by: Jouramie <16137441+Jouramie@users.noreply.github.com> Co-authored-by: Aaron Wagener <mmmcheese158@gmail.com>
This commit is contained in:
		
							parent
							
								
									6c939d2d59
								
							
						
					
					
						commit
						334781e976
					
				|  | @ -16,7 +16,7 @@ | |||
|   "reportMissingImports": true, | ||||
|   "reportMissingTypeStubs": true, | ||||
| 
 | ||||
|   "pythonVersion": "3.8", | ||||
|   "pythonVersion": "3.10", | ||||
|   "pythonPlatform": "Windows", | ||||
| 
 | ||||
|   "executionEnvironments": [ | ||||
|  |  | |||
|  | @ -53,7 +53,7 @@ jobs: | |||
|       - uses: actions/setup-python@v5 | ||||
|         if: env.diff != '' | ||||
|         with: | ||||
|           python-version: 3.8 | ||||
|           python-version: '3.10' | ||||
| 
 | ||||
|       - name: "Install dependencies" | ||||
|         if: env.diff != '' | ||||
|  |  | |||
|  | @ -24,14 +24,14 @@ env: | |||
| jobs: | ||||
|   # build-release-macos: # LF volunteer | ||||
| 
 | ||||
|   build-win-py38: # RCs will still be built and signed by hand | ||||
|   build-win-py310: # RCs will still be built and signed by hand | ||||
|     runs-on: windows-latest | ||||
|     steps: | ||||
|       - uses: actions/checkout@v4 | ||||
|       - name: Install python | ||||
|         uses: actions/setup-python@v5 | ||||
|         with: | ||||
|           python-version: '3.8' | ||||
|           python-version: '3.10' | ||||
|       - name: Download run-time dependencies | ||||
|         run: | | ||||
|           Invoke-WebRequest -Uri https://github.com/Ijwu/Enemizer/releases/download/${Env:ENEMIZER_VERSION}/win-x64.zip -OutFile enemizer.zip | ||||
|  |  | |||
|  | @ -33,13 +33,11 @@ jobs: | |||
|       matrix: | ||||
|         os: [ubuntu-latest] | ||||
|         python: | ||||
|           - {version: '3.8'} | ||||
|           - {version: '3.9'} | ||||
|           - {version: '3.10'} | ||||
|           - {version: '3.11'} | ||||
|           - {version: '3.12'} | ||||
|         include: | ||||
|           - python: {version: '3.8'}  # win7 compat | ||||
|           - python: {version: '3.10'}  # old compat | ||||
|             os: windows-latest | ||||
|           - python: {version: '3.12'}  # current | ||||
|             os: windows-latest | ||||
|  |  | |||
|  | @ -1,18 +1,16 @@ | |||
| from __future__ import annotations | ||||
| 
 | ||||
| import collections | ||||
| import itertools | ||||
| import functools | ||||
| import logging | ||||
| import random | ||||
| import secrets | ||||
| import typing  # this can go away when Python 3.8 support is dropped | ||||
| from argparse import Namespace | ||||
| from collections import Counter, deque | ||||
| from collections.abc import Collection, MutableSequence | ||||
| from enum import IntEnum, IntFlag | ||||
| from typing import (AbstractSet, Any, Callable, ClassVar, Dict, Iterable, Iterator, List, Mapping, NamedTuple, | ||||
|                     Optional, Protocol, Set, Tuple, Union, Type) | ||||
|                     Optional, Protocol, Set, Tuple, Union, TYPE_CHECKING) | ||||
| 
 | ||||
| from typing_extensions import NotRequired, TypedDict | ||||
| 
 | ||||
|  | @ -20,7 +18,7 @@ import NetUtils | |||
| import Options | ||||
| import Utils | ||||
| 
 | ||||
| if typing.TYPE_CHECKING: | ||||
| if TYPE_CHECKING: | ||||
|     from worlds import AutoWorld | ||||
| 
 | ||||
| 
 | ||||
|  | @ -231,7 +229,7 @@ class MultiWorld(): | |||
|         for player in self.player_ids: | ||||
|             world_type = AutoWorld.AutoWorldRegister.world_types[self.game[player]] | ||||
|             self.worlds[player] = world_type(self, player) | ||||
|             options_dataclass: typing.Type[Options.PerGameCommonOptions] = world_type.options_dataclass | ||||
|             options_dataclass: type[Options.PerGameCommonOptions] = world_type.options_dataclass | ||||
|             self.worlds[player].options = options_dataclass(**{option_key: getattr(args, option_key)[player] | ||||
|                                                                for option_key in options_dataclass.type_hints}) | ||||
| 
 | ||||
|  | @ -975,7 +973,7 @@ class Region: | |||
|     entrances: List[Entrance] | ||||
|     exits: List[Entrance] | ||||
|     locations: List[Location] | ||||
|     entrance_type: ClassVar[Type[Entrance]] = Entrance | ||||
|     entrance_type: ClassVar[type[Entrance]] = Entrance | ||||
| 
 | ||||
|     class Register(MutableSequence): | ||||
|         region_manager: MultiWorld.RegionManager | ||||
|  | @ -1075,7 +1073,7 @@ class Region: | |||
|             return entrance.parent_region.get_connecting_entrance(is_main_entrance) | ||||
| 
 | ||||
|     def add_locations(self, locations: Dict[str, Optional[int]], | ||||
|                       location_type: Optional[Type[Location]] = None) -> None: | ||||
|                       location_type: Optional[type[Location]] = None) -> None: | ||||
|         """ | ||||
|         Adds locations to the Region object, where location_type is your Location class and locations is a dict of | ||||
|         location names to address. | ||||
|  |  | |||
|  | @ -5,8 +5,8 @@ import multiprocessing | |||
| import warnings | ||||
| 
 | ||||
| 
 | ||||
| if sys.version_info < (3, 8, 6): | ||||
|     raise RuntimeError("Incompatible Python Version. 3.8.7+ is supported.") | ||||
| if sys.version_info < (3, 10, 11): | ||||
|     raise RuntimeError(f"Incompatible Python Version found: {sys.version_info}. 3.10.11+ is supported.") | ||||
| 
 | ||||
| # don't run update if environment is frozen/compiled or if not the parent process (skip in subprocess) | ||||
| _skip_update = bool(getattr(sys, "frozen", False) or multiprocessing.parent_process()) | ||||
|  |  | |||
							
								
								
									
										5
									
								
								Utils.py
								
								
								
								
							
							
						
						
									
										5
									
								
								Utils.py
								
								
								
								
							|  | @ -19,8 +19,7 @@ import warnings | |||
| from argparse import Namespace | ||||
| from settings import Settings, get_settings | ||||
| from time import sleep | ||||
| from typing import BinaryIO, Coroutine, Optional, Set, Dict, Any, Union | ||||
| from typing_extensions import TypeGuard | ||||
| from typing import BinaryIO, Coroutine, Optional, Set, Dict, Any, Union, TypeGuard | ||||
| from yaml import load, load_all, dump | ||||
| 
 | ||||
| try: | ||||
|  | @ -48,7 +47,7 @@ class Version(typing.NamedTuple): | |||
|         return ".".join(str(item) for item in self) | ||||
| 
 | ||||
| 
 | ||||
| __version__ = "0.5.1" | ||||
| __version__ = "0.6.0" | ||||
| version_tuple = tuplize_version(__version__) | ||||
| 
 | ||||
| is_linux = sys.platform.startswith("linux") | ||||
|  |  | |||
|  | @ -17,7 +17,7 @@ from Utils import get_file_safe_name | |||
| if typing.TYPE_CHECKING: | ||||
|     from flask import Flask | ||||
| 
 | ||||
| Utils.local_path.cached_path = os.path.dirname(__file__) or "."  # py3.8 is not abs. remove "." when dropping 3.8 | ||||
| Utils.local_path.cached_path = os.path.dirname(__file__) | ||||
| settings.no_gui = True | ||||
| configpath = os.path.abspath("config.yaml") | ||||
| if not os.path.exists(configpath):  # fall back to config.yaml in home | ||||
|  |  | |||
|  | @ -5,9 +5,7 @@ waitress>=3.0.0 | |||
| Flask-Caching>=2.3.0 | ||||
| Flask-Compress>=1.15 | ||||
| Flask-Limiter>=3.8.0 | ||||
| bokeh>=3.1.1; python_version <= '3.8' | ||||
| bokeh>=3.4.3; python_version == '3.9' | ||||
| bokeh>=3.5.2; python_version >= '3.10' | ||||
| bokeh>=3.5.2 | ||||
| markupsafe>=2.1.5 | ||||
| Markdown>=3.7 | ||||
| mdx-breakless-lists>=1.0.1 | ||||
|  |  | |||
|  | @ -16,7 +16,7 @@ game contributions: | |||
| * **Do not introduce unit test failures/regressions.** | ||||
|   Archipelago supports multiple versions of Python. You may need to download older Python versions to fully test | ||||
|   your changes. Currently, the oldest supported version | ||||
|   is [Python 3.8](https://www.python.org/downloads/release/python-380/). | ||||
|   is [Python 3.10](https://www.python.org/downloads/release/python-31015/). | ||||
|   It is recommended that automated github actions are turned on in your fork to have github run unit tests after | ||||
|   pushing. | ||||
|   You can turn them on here:   | ||||
|  |  | |||
|  | @ -7,7 +7,7 @@ use that version. These steps are for developers or platforms without compiled r | |||
| ## General | ||||
| 
 | ||||
| What you'll need: | ||||
|  * [Python 3.8.7 or newer](https://www.python.org/downloads/), not the Windows Store version | ||||
|  * [Python 3.10.15 or newer](https://www.python.org/downloads/), not the Windows Store version | ||||
|    * Python 3.12.x is currently the newest supported version | ||||
|  * pip: included in downloads from python.org, separate in many Linux distributions | ||||
|  * Matching C compiler | ||||
|  |  | |||
							
								
								
									
										5
									
								
								kvui.py
								
								
								
								
							
							
						
						
									
										5
									
								
								kvui.py
								
								
								
								
							|  | @ -12,10 +12,7 @@ if sys.platform == "win32": | |||
| 
 | ||||
|     # kivy 2.2.0 introduced DPI awareness on Windows, but it makes the UI enter an infinitely recursive re-layout | ||||
|     # by setting the application to not DPI Aware, Windows handles scaling the entire window on its own, ignoring kivy's | ||||
|     try: | ||||
|         ctypes.windll.shcore.SetProcessDpiAwareness(0) | ||||
|     except FileNotFoundError:  # shcore may not be found on <= Windows 7 | ||||
|         pass  # TODO: remove silent except when Python 3.8 is phased out. | ||||
|     ctypes.windll.shcore.SetProcessDpiAwareness(0) | ||||
| 
 | ||||
| os.environ["KIVY_NO_CONSOLELOG"] = "1" | ||||
| os.environ["KIVY_NO_FILELOG"] = "1" | ||||
|  |  | |||
							
								
								
									
										2
									
								
								setup.py
								
								
								
								
							
							
						
						
									
										2
									
								
								setup.py
								
								
								
								
							|  | @ -634,7 +634,7 @@ cx_Freeze.setup( | |||
|             "excludes": ["numpy", "Cython", "PySide2", "PIL", | ||||
|                          "pandas", "zstandard"], | ||||
|             "zip_include_packages": ["*"], | ||||
|             "zip_exclude_packages": ["worlds", "sc2", "orjson"],  # TODO: remove orjson here once we drop py3.8 support | ||||
|             "zip_exclude_packages": ["worlds", "sc2"], | ||||
|             "include_files": [],  # broken in cx 6.14.0, we use more special sauce now | ||||
|             "include_msvcr": False, | ||||
|             "replace_paths": ["*."], | ||||
|  |  | |||
|  | @ -2,9 +2,7 @@ | |||
| from __future__ import annotations | ||||
| import abc | ||||
| import logging | ||||
| from typing import TYPE_CHECKING, ClassVar, Dict, Iterable, Tuple, Any, Optional, Union | ||||
| 
 | ||||
| from typing_extensions import TypeGuard | ||||
| from typing import TYPE_CHECKING, ClassVar, Dict, Iterable, Tuple, Any, Optional, Union, TypeGuard | ||||
| 
 | ||||
| from worlds.LauncherComponents import Component, SuffixIdentifier, Type, components | ||||
| 
 | ||||
|  |  | |||
|  | @ -66,19 +66,12 @@ class WorldSource: | |||
|             start = time.perf_counter() | ||||
|             if self.is_zip: | ||||
|                 importer = zipimport.zipimporter(self.resolved_path) | ||||
|                 if hasattr(importer, "find_spec"):  # new in Python 3.10 | ||||
|                     spec = importer.find_spec(os.path.basename(self.path).rsplit(".", 1)[0]) | ||||
|                     assert spec, f"{self.path} is not a loadable module" | ||||
|                     mod = importlib.util.module_from_spec(spec) | ||||
|                 else:  # TODO: remove with 3.8 support | ||||
|                     mod = importer.load_module(os.path.basename(self.path).rsplit(".", 1)[0]) | ||||
|                 spec = importer.find_spec(os.path.basename(self.path).rsplit(".", 1)[0]) | ||||
|                 assert spec, f"{self.path} is not a loadable module" | ||||
|                 mod = importlib.util.module_from_spec(spec) | ||||
| 
 | ||||
|                 mod.__package__ = f"worlds.{mod.__package__}" | ||||
| 
 | ||||
|                 if mod.__package__ is not None: | ||||
|                     mod.__package__ = f"worlds.{mod.__package__}" | ||||
|                 else: | ||||
|                     # load_module does not populate package, we'll have to assume mod.__name__ is correct here | ||||
|                     # probably safe to remove with 3.8 support | ||||
|                     mod.__package__ = f"worlds.{mod.__name__}" | ||||
|                 mod.__name__ = f"worlds.{mod.__name__}" | ||||
|                 sys.modules[mod.__name__] = mod | ||||
|                 with warnings.catch_warnings(): | ||||
|  |  | |||
|  | @ -9,11 +9,7 @@ import ast | |||
| 
 | ||||
| import jinja2 | ||||
| 
 | ||||
| try: | ||||
|     from ast import unparse | ||||
| except ImportError: | ||||
|     # Py 3.8 and earlier compatibility module | ||||
|     from astunparse import unparse | ||||
| from ast import unparse | ||||
| 
 | ||||
| from Utils import get_text_between | ||||
| 
 | ||||
|  |  | |||
|  | @ -1 +0,0 @@ | |||
| astunparse>=1.6.3; python_version <= '3.8' | ||||
|  | @ -1,5 +1,5 @@ | |||
| import logging | ||||
| from typing import Any, ClassVar, Dict, List, Optional, Set, TextIO | ||||
| from typing import Any, ClassVar, TextIO | ||||
| 
 | ||||
| from BaseClasses import CollectionState, Entrance, Item, ItemClassification, MultiWorld, Tutorial | ||||
| from Options import Accessibility | ||||
|  | @ -120,16 +120,16 @@ class MessengerWorld(World): | |||
|     required_seals: int = 0 | ||||
|     created_seals: int = 0 | ||||
|     total_shards: int = 0 | ||||
|     shop_prices: Dict[str, int] | ||||
|     figurine_prices: Dict[str, int] | ||||
|     _filler_items: List[str] | ||||
|     starting_portals: List[str] | ||||
|     plando_portals: List[str] | ||||
|     spoiler_portal_mapping: Dict[str, str] | ||||
|     portal_mapping: List[int] | ||||
|     transitions: List[Entrance] | ||||
|     shop_prices: dict[str, int] | ||||
|     figurine_prices: dict[str, int] | ||||
|     _filler_items: list[str] | ||||
|     starting_portals: list[str] | ||||
|     plando_portals: list[str] | ||||
|     spoiler_portal_mapping: dict[str, str] | ||||
|     portal_mapping: list[int] | ||||
|     transitions: list[Entrance] | ||||
|     reachable_locs: int = 0 | ||||
|     filler: Dict[str, int] | ||||
|     filler: dict[str, int] | ||||
| 
 | ||||
|     def generate_early(self) -> None: | ||||
|         if self.options.goal == Goal.option_power_seal_hunt: | ||||
|  | @ -178,7 +178,7 @@ class MessengerWorld(World): | |||
|                            for reg_name in sub_region] | ||||
| 
 | ||||
|         for region in complex_regions: | ||||
|             region_name = region.name.replace(f"{region.parent} - ", "") | ||||
|             region_name = region.name.removeprefix(f"{region.parent} - ") | ||||
|             connection_data = CONNECTIONS[region.parent][region_name] | ||||
|             for exit_region in connection_data: | ||||
|                 region.connect(self.multiworld.get_region(exit_region, self.player)) | ||||
|  | @ -191,7 +191,7 @@ class MessengerWorld(World): | |||
|         # create items that are always in the item pool | ||||
|         main_movement_items = ["Rope Dart", "Wingsuit"] | ||||
|         precollected_names = [item.name for item in self.multiworld.precollected_items[self.player]] | ||||
|         itempool: List[MessengerItem] = [ | ||||
|         itempool: list[MessengerItem] = [ | ||||
|             self.create_item(item) | ||||
|             for item in self.item_name_to_id | ||||
|             if item not in { | ||||
|  | @ -290,7 +290,7 @@ class MessengerWorld(World): | |||
|             for portal, output in portal_info: | ||||
|                 spoiler.set_entrance(f"{portal} Portal", output, "I can write anything I want here lmao", self.player) | ||||
| 
 | ||||
|     def fill_slot_data(self) -> Dict[str, Any]: | ||||
|     def fill_slot_data(self) -> dict[str, Any]: | ||||
|         slot_data = { | ||||
|             "shop": {SHOP_ITEMS[item].internal_name: price for item, price in self.shop_prices.items()}, | ||||
|             "figures": {FIGURINES[item].internal_name: price for item, price in self.figurine_prices.items()}, | ||||
|  | @ -316,7 +316,7 @@ class MessengerWorld(World): | |||
|         return self._filler_items.pop(0) | ||||
| 
 | ||||
|     def create_item(self, name: str) -> MessengerItem: | ||||
|         item_id: Optional[int] = self.item_name_to_id.get(name, None) | ||||
|         item_id: int | None = self.item_name_to_id.get(name, None) | ||||
|         return MessengerItem( | ||||
|             name, | ||||
|             ItemClassification.progression if item_id is None else self.get_item_classification(name), | ||||
|  | @ -351,7 +351,7 @@ class MessengerWorld(World): | |||
|         return ItemClassification.filler | ||||
| 
 | ||||
|     @classmethod | ||||
|     def create_group(cls, multiworld: "MultiWorld", new_player_id: int, players: Set[int]) -> World: | ||||
|     def create_group(cls, multiworld: "MultiWorld", new_player_id: int, players: set[int]) -> World: | ||||
|         group = super().create_group(multiworld, new_player_id, players) | ||||
|         assert isinstance(group, MessengerWorld) | ||||
|          | ||||
|  |  | |||
|  | @ -5,7 +5,7 @@ import os.path | |||
| import subprocess | ||||
| import urllib.request | ||||
| from shutil import which | ||||
| from typing import Any, Optional | ||||
| from typing import Any | ||||
| from zipfile import ZipFile | ||||
| from Utils import open_file | ||||
| 
 | ||||
|  | @ -17,7 +17,7 @@ from Utils import is_windows, messagebox, tuplize_version | |||
| MOD_URL = "https://api.github.com/repos/alwaysintreble/TheMessengerRandomizerModAP/releases/latest" | ||||
| 
 | ||||
| 
 | ||||
| def ask_yes_no_cancel(title: str, text: str) -> Optional[bool]: | ||||
| def ask_yes_no_cancel(title: str, text: str) -> bool | None: | ||||
|     """ | ||||
|     Wrapper for tkinter.messagebox.askyesnocancel, that creates a popup dialog box with yes, no, and cancel buttons. | ||||
| 
 | ||||
|  | @ -33,7 +33,6 @@ def ask_yes_no_cancel(title: str, text: str) -> Optional[bool]: | |||
|     return ret | ||||
| 
 | ||||
| 
 | ||||
| 
 | ||||
| def launch_game(*args) -> None: | ||||
|     """Check the game installation, then launch it""" | ||||
|     def courier_installed() -> bool: | ||||
|  |  | |||
|  | @ -1,6 +1,4 @@ | |||
| from typing import Dict, List | ||||
| 
 | ||||
| CONNECTIONS: Dict[str, Dict[str, List[str]]] = { | ||||
| CONNECTIONS: dict[str, dict[str, list[str]]] = { | ||||
|     "Ninja Village": { | ||||
|         "Right": [ | ||||
|             "Autumn Hills - Left", | ||||
|  | @ -640,7 +638,7 @@ CONNECTIONS: Dict[str, Dict[str, List[str]]] = { | |||
|     }, | ||||
| } | ||||
| 
 | ||||
| RANDOMIZED_CONNECTIONS: Dict[str, str] = { | ||||
| RANDOMIZED_CONNECTIONS: dict[str, str] = { | ||||
|     "Ninja Village - Right": "Autumn Hills - Left", | ||||
|     "Autumn Hills - Left": "Ninja Village - Right", | ||||
|     "Autumn Hills - Right": "Forlorn Temple - Left", | ||||
|  | @ -680,7 +678,7 @@ RANDOMIZED_CONNECTIONS: Dict[str, str] = { | |||
|     "Sunken Shrine - Left": "Howling Grotto - Bottom", | ||||
| } | ||||
| 
 | ||||
| TRANSITIONS: List[str] = [ | ||||
| TRANSITIONS: list[str] = [ | ||||
|     "Ninja Village - Right", | ||||
|     "Autumn Hills - Left", | ||||
|     "Autumn Hills - Right", | ||||
|  |  | |||
|  | @ -2,7 +2,7 @@ from .shop import FIGURINES, SHOP_ITEMS | |||
| 
 | ||||
| # items | ||||
| # listing individual groups first for easy lookup | ||||
| NOTES = [ | ||||
| NOTES: list[str] = [ | ||||
|     "Key of Hope", | ||||
|     "Key of Chaos", | ||||
|     "Key of Courage", | ||||
|  | @ -11,7 +11,7 @@ NOTES = [ | |||
|     "Key of Symbiosis", | ||||
| ] | ||||
| 
 | ||||
| PROG_ITEMS = [ | ||||
| PROG_ITEMS: list[str] = [ | ||||
|     "Wingsuit", | ||||
|     "Rope Dart", | ||||
|     "Lightfoot Tabi", | ||||
|  | @ -28,18 +28,18 @@ PROG_ITEMS = [ | |||
|     "Seashell", | ||||
| ] | ||||
| 
 | ||||
| PHOBEKINS = [ | ||||
| PHOBEKINS: list[str] = [ | ||||
|     "Necro", | ||||
|     "Pyro", | ||||
|     "Claustro", | ||||
|     "Acro", | ||||
| ] | ||||
| 
 | ||||
| USEFUL_ITEMS = [ | ||||
| USEFUL_ITEMS: list[str] = [ | ||||
|     "Windmill Shuriken", | ||||
| ] | ||||
| 
 | ||||
| FILLER = { | ||||
| FILLER: dict[str, int] = { | ||||
|     "Time Shard": 5, | ||||
|     "Time Shard (10)": 10, | ||||
|     "Time Shard (50)": 20, | ||||
|  | @ -48,13 +48,13 @@ FILLER = { | |||
|     "Time Shard (500)": 5, | ||||
| } | ||||
| 
 | ||||
| TRAPS = { | ||||
| TRAPS: dict[str, int] = { | ||||
|     "Teleport Trap": 5, | ||||
|     "Prophecy Trap": 10, | ||||
| } | ||||
| 
 | ||||
| # item_name_to_id needs to be deterministic and match upstream | ||||
| ALL_ITEMS = [ | ||||
| ALL_ITEMS: list[str] = [ | ||||
|     *NOTES, | ||||
|     "Windmill Shuriken", | ||||
|     "Wingsuit", | ||||
|  | @ -83,7 +83,7 @@ ALL_ITEMS = [ | |||
| # locations | ||||
| # the names of these don't actually matter, but using the upstream's names for now | ||||
| # order must be exactly the same as upstream | ||||
| ALWAYS_LOCATIONS = [ | ||||
| ALWAYS_LOCATIONS: list[str] = [ | ||||
|     # notes | ||||
|     "Sunken Shrine - Key of Love", | ||||
|     "Corrupted Future - Key of Courage", | ||||
|  | @ -160,7 +160,7 @@ ALWAYS_LOCATIONS = [ | |||
|     "Elemental Skylands Seal - Fire", | ||||
| ] | ||||
| 
 | ||||
| BOSS_LOCATIONS = [ | ||||
| BOSS_LOCATIONS: list[str] = [ | ||||
|     "Autumn Hills - Leaf Golem", | ||||
|     "Catacombs - Ruxxtin", | ||||
|     "Howling Grotto - Emerald Golem", | ||||
|  |  | |||
|  | @ -1,5 +1,4 @@ | |||
| from dataclasses import dataclass | ||||
| from typing import Dict | ||||
| 
 | ||||
| from schema import And, Optional, Or, Schema | ||||
| 
 | ||||
|  | @ -167,7 +166,7 @@ class ShopPrices(Range): | |||
|     default = 100 | ||||
| 
 | ||||
| 
 | ||||
| def planned_price(location: str) -> Dict[Optional, Or]: | ||||
| def planned_price(location: str) -> dict[Optional, Or]: | ||||
|     return { | ||||
|         Optional(location): Or( | ||||
|             And(int, lambda n: n >= 0), | ||||
|  |  | |||
|  | @ -1,5 +1,5 @@ | |||
| from copy import deepcopy | ||||
| from typing import List, TYPE_CHECKING | ||||
| from typing import TYPE_CHECKING | ||||
| 
 | ||||
| from BaseClasses import CollectionState, PlandoOptions | ||||
| from Options import PlandoConnection | ||||
|  | @ -8,7 +8,7 @@ if TYPE_CHECKING: | |||
|     from . import MessengerWorld | ||||
| 
 | ||||
| 
 | ||||
| PORTALS = [ | ||||
| PORTALS: list[str] = [ | ||||
|     "Autumn Hills", | ||||
|     "Riviere Turquoise", | ||||
|     "Howling Grotto", | ||||
|  | @ -18,7 +18,7 @@ PORTALS = [ | |||
| ] | ||||
| 
 | ||||
| 
 | ||||
| SHOP_POINTS = { | ||||
| SHOP_POINTS: dict[str, list[str]] = { | ||||
|     "Autumn Hills": [ | ||||
|         "Climbing Claws", | ||||
|         "Hope Path", | ||||
|  | @ -113,7 +113,7 @@ SHOP_POINTS = { | |||
| } | ||||
| 
 | ||||
| 
 | ||||
| CHECKPOINTS = { | ||||
| CHECKPOINTS: dict[str, list[str]] = { | ||||
|     "Autumn Hills": [ | ||||
|         "Hope Latch", | ||||
|         "Key of Hope", | ||||
|  | @ -186,7 +186,7 @@ CHECKPOINTS = { | |||
| } | ||||
| 
 | ||||
| 
 | ||||
| REGION_ORDER = [ | ||||
| REGION_ORDER: list[str] = [ | ||||
|     "Autumn Hills", | ||||
|     "Forlorn Temple", | ||||
|     "Catacombs", | ||||
|  | @ -228,7 +228,7 @@ def shuffle_portals(world: "MessengerWorld") -> None: | |||
| 
 | ||||
|         return parent | ||||
| 
 | ||||
|     def handle_planned_portals(plando_connections: List[PlandoConnection]) -> None: | ||||
|     def handle_planned_portals(plando_connections: list[PlandoConnection]) -> None: | ||||
|         """checks the provided plando connections for portals and connects them""" | ||||
|         nonlocal available_portals | ||||
| 
 | ||||
|  |  | |||
|  | @ -1,7 +1,4 @@ | |||
| from typing import Dict, List | ||||
| 
 | ||||
| 
 | ||||
| LOCATIONS: Dict[str, List[str]] = { | ||||
| LOCATIONS: dict[str, list[str]] = { | ||||
|     "Ninja Village - Nest": [ | ||||
|         "Ninja Village - Candle", | ||||
|         "Ninja Village - Astral Seed", | ||||
|  | @ -201,7 +198,7 @@ LOCATIONS: Dict[str, List[str]] = { | |||
| } | ||||
| 
 | ||||
| 
 | ||||
| SUB_REGIONS: Dict[str, List[str]] = { | ||||
| SUB_REGIONS: dict[str, list[str]] = { | ||||
|     "Ninja Village": [ | ||||
|         "Right", | ||||
|     ], | ||||
|  | @ -385,7 +382,7 @@ SUB_REGIONS: Dict[str, List[str]] = { | |||
| 
 | ||||
| 
 | ||||
| # order is slightly funky here for back compat | ||||
| MEGA_SHARDS: Dict[str, List[str]] = { | ||||
| MEGA_SHARDS: dict[str, list[str]] = { | ||||
|     "Autumn Hills - Lakeside Checkpoint": ["Autumn Hills Mega Shard"], | ||||
|     "Forlorn Temple - Outside Shop": ["Hidden Entrance Mega Shard"], | ||||
|     "Catacombs - Top Left": ["Catacombs Mega Shard"], | ||||
|  | @ -414,7 +411,7 @@ MEGA_SHARDS: Dict[str, List[str]] = { | |||
| } | ||||
| 
 | ||||
| 
 | ||||
| REGION_CONNECTIONS: Dict[str, Dict[str, str]] = { | ||||
| REGION_CONNECTIONS: dict[str, dict[str, str]] = { | ||||
|     "Menu": {"Tower HQ": "Start Game"}, | ||||
|     "Tower HQ": { | ||||
|         "Autumn Hills - Portal": "ToTHQ Autumn Hills Portal", | ||||
|  | @ -436,7 +433,7 @@ REGION_CONNECTIONS: Dict[str, Dict[str, str]] = { | |||
| 
 | ||||
| 
 | ||||
| # regions that don't have sub-regions | ||||
| LEVELS: List[str] = [ | ||||
| LEVELS: list[str] = [ | ||||
|     "Menu", | ||||
|     "Tower HQ", | ||||
|     "The Shop", | ||||
|  |  | |||
|  | @ -1,4 +1,4 @@ | |||
| from typing import Dict, TYPE_CHECKING | ||||
| from typing import TYPE_CHECKING | ||||
| 
 | ||||
| from BaseClasses import CollectionState | ||||
| from worlds.generic.Rules import CollectionRule, add_rule, allow_self_locking_items | ||||
|  | @ -12,9 +12,9 @@ if TYPE_CHECKING: | |||
| class MessengerRules: | ||||
|     player: int | ||||
|     world: "MessengerWorld" | ||||
|     connection_rules: Dict[str, CollectionRule] | ||||
|     region_rules: Dict[str, CollectionRule] | ||||
|     location_rules: Dict[str, CollectionRule] | ||||
|     connection_rules: dict[str, CollectionRule] | ||||
|     region_rules: dict[str, CollectionRule] | ||||
|     location_rules: dict[str, CollectionRule] | ||||
|     maximum_price: int | ||||
|     required_seals: int | ||||
| 
 | ||||
|  |  | |||
|  | @ -1,11 +1,11 @@ | |||
| from typing import Dict, List, NamedTuple, Optional, Set, TYPE_CHECKING, Tuple, Union | ||||
| from typing import NamedTuple, TYPE_CHECKING | ||||
| 
 | ||||
| if TYPE_CHECKING: | ||||
|     from . import MessengerWorld | ||||
| else: | ||||
|     MessengerWorld = object | ||||
| 
 | ||||
| PROG_SHOP_ITEMS: List[str] = [ | ||||
| PROG_SHOP_ITEMS: list[str] = [ | ||||
|     "Path of Resilience", | ||||
|     "Meditation", | ||||
|     "Strike of the Ninja", | ||||
|  | @ -14,7 +14,7 @@ PROG_SHOP_ITEMS: List[str] = [ | |||
|     "Aerobatics Warrior", | ||||
| ] | ||||
| 
 | ||||
| USEFUL_SHOP_ITEMS: List[str] = [ | ||||
| USEFUL_SHOP_ITEMS: list[str] = [ | ||||
|     "Karuta Plates", | ||||
|     "Serendipitous Bodies", | ||||
|     "Kusari Jacket", | ||||
|  | @ -29,10 +29,10 @@ class ShopData(NamedTuple): | |||
|     internal_name: str | ||||
|     min_price: int | ||||
|     max_price: int | ||||
|     prerequisite: Optional[Union[str, Set[str]]] = None | ||||
|     prerequisite: str | set[str] | None = None | ||||
| 
 | ||||
| 
 | ||||
| SHOP_ITEMS: Dict[str, ShopData] = { | ||||
| SHOP_ITEMS: dict[str, ShopData] = { | ||||
|     "Karuta Plates":        ShopData("HP_UPGRADE_1", 20, 200), | ||||
|     "Serendipitous Bodies": ShopData("ENEMY_DROP_HP", 20, 300, "The Shop - Karuta Plates"), | ||||
|     "Path of Resilience":   ShopData("DAMAGE_REDUCTION", 100, 500, "The Shop - Serendipitous Bodies"), | ||||
|  | @ -56,7 +56,7 @@ SHOP_ITEMS: Dict[str, ShopData] = { | |||
|     "Focused Power Sense":  ShopData("POWER_SEAL_WORLD_MAP", 300, 600, "The Shop - Power Sense"), | ||||
| } | ||||
| 
 | ||||
| FIGURINES: Dict[str, ShopData] = { | ||||
| FIGURINES: dict[str, ShopData] = { | ||||
|     "Green Kappa Figurine":         ShopData("GREEN_KAPPA", 100, 500), | ||||
|     "Blue Kappa Figurine":          ShopData("BLUE_KAPPA", 100, 500), | ||||
|     "Ountarde Figurine":            ShopData("OUNTARDE", 100, 500), | ||||
|  | @ -73,12 +73,12 @@ FIGURINES: Dict[str, ShopData] = { | |||
| } | ||||
| 
 | ||||
| 
 | ||||
| def shuffle_shop_prices(world: MessengerWorld) -> Tuple[Dict[str, int], Dict[str, int]]: | ||||
| def shuffle_shop_prices(world: MessengerWorld) -> tuple[dict[str, int], dict[str, int]]: | ||||
|     shop_price_mod = world.options.shop_price.value | ||||
|     shop_price_planned = world.options.shop_price_plan | ||||
| 
 | ||||
|     shop_prices: Dict[str, int] = {} | ||||
|     figurine_prices: Dict[str, int] = {} | ||||
|     shop_prices: dict[str, int] = {} | ||||
|     figurine_prices: dict[str, int] = {} | ||||
|     for item, price in shop_price_planned.value.items(): | ||||
|         if not isinstance(price, int): | ||||
|             price = world.random.choices(list(price.keys()), weights=list(price.values()))[0] | ||||
|  |  | |||
|  | @ -1,5 +1,5 @@ | |||
| from functools import cached_property | ||||
| from typing import Optional, TYPE_CHECKING | ||||
| from typing import TYPE_CHECKING | ||||
| 
 | ||||
| from BaseClasses import CollectionState, Entrance, Item, ItemClassification, Location, Region | ||||
| from .regions import LOCATIONS, MEGA_SHARDS | ||||
|  | @ -10,14 +10,14 @@ if TYPE_CHECKING: | |||
| 
 | ||||
| 
 | ||||
| class MessengerEntrance(Entrance): | ||||
|     world: Optional["MessengerWorld"] = None | ||||
|     world: "MessengerWorld | None" = None | ||||
| 
 | ||||
| 
 | ||||
| class MessengerRegion(Region): | ||||
|     parent: str | ||||
|     entrance_type = MessengerEntrance | ||||
| 
 | ||||
|     def __init__(self, name: str, world: "MessengerWorld", parent: Optional[str] = None) -> None: | ||||
|     def __init__(self, name: str, world: "MessengerWorld", parent: str | None = None) -> None: | ||||
|         super().__init__(name, world.player, world.multiworld) | ||||
|         self.parent = parent | ||||
|         locations = [] | ||||
|  | @ -48,7 +48,7 @@ class MessengerRegion(Region): | |||
| class MessengerLocation(Location): | ||||
|     game = "The Messenger" | ||||
| 
 | ||||
|     def __init__(self, player: int, name: str, loc_id: Optional[int], parent: MessengerRegion) -> None: | ||||
|     def __init__(self, player: int, name: str, loc_id: int | None, parent: MessengerRegion) -> None: | ||||
|         super().__init__(player, name, loc_id, parent) | ||||
|         if loc_id is None: | ||||
|             if name == "Rescue Phantom": | ||||
|  | @ -59,7 +59,7 @@ class MessengerLocation(Location): | |||
| class MessengerShopLocation(MessengerLocation): | ||||
|     @cached_property | ||||
|     def cost(self) -> int: | ||||
|         name = self.name.replace("The Shop - ", "")  # TODO use `remove_prefix` when 3.8 finally gets dropped | ||||
|         name = self.name.removeprefix("The Shop - ") | ||||
|         world = self.parent_region.multiworld.worlds[self.player] | ||||
|         shop_data = SHOP_ITEMS[name] | ||||
|         if shop_data.prerequisite: | ||||
|  |  | |||
|  | @ -77,7 +77,7 @@ class PlandoTest(MessengerTestBase): | |||
| 
 | ||||
|                 loc = f"The Shop - {loc}" | ||||
|                 self.assertLessEqual(price, self.multiworld.get_location(loc, self.player).cost) | ||||
|                 self.assertTrue(loc.replace("The Shop - ", "") in SHOP_ITEMS) | ||||
|                 self.assertTrue(loc.removeprefix("The Shop - ") in SHOP_ITEMS) | ||||
|         self.assertEqual(len(prices), len(SHOP_ITEMS)) | ||||
| 
 | ||||
|         figures = self.world.figurine_prices | ||||
|  |  | |||
|  | @ -1,16 +1,12 @@ | |||
| from __future__ import annotations | ||||
| 
 | ||||
| from graphlib import TopologicalSorter | ||||
| from typing import Iterable, Mapping, Callable | ||||
| 
 | ||||
| from .game_content import StardewContent, ContentPack, StardewFeatures | ||||
| from .vanilla.base import base_game as base_game_content_pack | ||||
| from ..data.game_item import GameItem, ItemSource | ||||
| 
 | ||||
| try: | ||||
|     from graphlib import TopologicalSorter | ||||
| except ImportError: | ||||
|     from graphlib_backport import TopologicalSorter  # noqa | ||||
| 
 | ||||
| 
 | ||||
| def unpack_content(features: StardewFeatures, packs: Iterable[ContentPack]) -> StardewContent: | ||||
|     # Base game is always registered first. | ||||
|  |  | |||
|  | @ -1,9 +1,9 @@ | |||
| from dataclasses import dataclass | ||||
| 
 | ||||
| from .game_item import kw_only, ItemSource | ||||
| from .game_item import ItemSource | ||||
| 
 | ||||
| 
 | ||||
| @dataclass(frozen=True, **kw_only) | ||||
| @dataclass(frozen=True, kw_only=True) | ||||
| class MachineSource(ItemSource): | ||||
|     item: str  # this should be optional (worm bin) | ||||
|     machine: str | ||||
|  |  | |||
|  | @ -1,5 +1,4 @@ | |||
| import enum | ||||
| import sys | ||||
| from abc import ABC | ||||
| from dataclasses import dataclass, field | ||||
| from types import MappingProxyType | ||||
|  | @ -7,11 +6,6 @@ from typing import List, Iterable, Set, ClassVar, Tuple, Mapping, Callable, Any | |||
| 
 | ||||
| from ..stardew_rule.protocol import StardewRule | ||||
| 
 | ||||
| if sys.version_info >= (3, 10): | ||||
|     kw_only = {"kw_only": True} | ||||
| else: | ||||
|     kw_only = {} | ||||
| 
 | ||||
| DEFAULT_REQUIREMENT_TAGS = MappingProxyType({}) | ||||
| 
 | ||||
| 
 | ||||
|  | @ -36,21 +30,17 @@ class ItemTag(enum.Enum): | |||
| class ItemSource(ABC): | ||||
|     add_tags: ClassVar[Tuple[ItemTag]] = () | ||||
| 
 | ||||
|     other_requirements: Tuple[Requirement, ...] = field(kw_only=True, default_factory=tuple) | ||||
| 
 | ||||
|     @property | ||||
|     def requirement_tags(self) -> Mapping[str, Tuple[ItemTag, ...]]: | ||||
|         return DEFAULT_REQUIREMENT_TAGS | ||||
| 
 | ||||
|     # FIXME this should just be an optional field, but kw_only requires python 3.10... | ||||
|     @property | ||||
|     def other_requirements(self) -> Iterable[Requirement]: | ||||
|         return () | ||||
| 
 | ||||
| 
 | ||||
| @dataclass(frozen=True, **kw_only) | ||||
| @dataclass(frozen=True, kw_only=True) | ||||
| class GenericSource(ItemSource): | ||||
|     regions: Tuple[str, ...] = () | ||||
|     """No region means it's available everywhere.""" | ||||
|     other_requirements: Tuple[Requirement, ...] = () | ||||
| 
 | ||||
| 
 | ||||
| @dataclass(frozen=True) | ||||
|  | @ -59,7 +49,7 @@ class CustomRuleSource(ItemSource): | |||
|     create_rule: Callable[[Any], StardewRule] | ||||
| 
 | ||||
| 
 | ||||
| @dataclass(frozen=True, **kw_only) | ||||
| @dataclass(frozen=True, kw_only=True) | ||||
| class CompoundSource(ItemSource): | ||||
|     sources: Tuple[ItemSource, ...] = () | ||||
| 
 | ||||
|  |  | |||
|  | @ -1,18 +1,17 @@ | |||
| from dataclasses import dataclass | ||||
| from typing import Tuple, Sequence, Mapping | ||||
| 
 | ||||
| from .game_item import ItemSource, kw_only, ItemTag, Requirement | ||||
| from .game_item import ItemSource, ItemTag | ||||
| from ..strings.season_names import Season | ||||
| 
 | ||||
| 
 | ||||
| @dataclass(frozen=True, **kw_only) | ||||
| @dataclass(frozen=True, kw_only=True) | ||||
| class ForagingSource(ItemSource): | ||||
|     regions: Tuple[str, ...] | ||||
|     seasons: Tuple[str, ...] = Season.all | ||||
|     other_requirements: Tuple[Requirement, ...] = () | ||||
| 
 | ||||
| 
 | ||||
| @dataclass(frozen=True, **kw_only) | ||||
| @dataclass(frozen=True, kw_only=True) | ||||
| class SeasonalForagingSource(ItemSource): | ||||
|     season: str | ||||
|     days: Sequence[int] | ||||
|  | @ -22,17 +21,17 @@ class SeasonalForagingSource(ItemSource): | |||
|         return ForagingSource(seasons=(self.season,), regions=self.regions) | ||||
| 
 | ||||
| 
 | ||||
| @dataclass(frozen=True, **kw_only) | ||||
| @dataclass(frozen=True, kw_only=True) | ||||
| class FruitBatsSource(ItemSource): | ||||
|     ... | ||||
| 
 | ||||
| 
 | ||||
| @dataclass(frozen=True, **kw_only) | ||||
| @dataclass(frozen=True, kw_only=True) | ||||
| class MushroomCaveSource(ItemSource): | ||||
|     ... | ||||
| 
 | ||||
| 
 | ||||
| @dataclass(frozen=True, **kw_only) | ||||
| @dataclass(frozen=True, kw_only=True) | ||||
| class HarvestFruitTreeSource(ItemSource): | ||||
|     add_tags = (ItemTag.CROPSANITY,) | ||||
| 
 | ||||
|  | @ -46,7 +45,7 @@ class HarvestFruitTreeSource(ItemSource): | |||
|         } | ||||
| 
 | ||||
| 
 | ||||
| @dataclass(frozen=True, **kw_only) | ||||
| @dataclass(frozen=True, kw_only=True) | ||||
| class HarvestCropSource(ItemSource): | ||||
|     add_tags = (ItemTag.CROPSANITY,) | ||||
| 
 | ||||
|  | @ -61,6 +60,6 @@ class HarvestCropSource(ItemSource): | |||
|         } | ||||
| 
 | ||||
| 
 | ||||
| @dataclass(frozen=True, **kw_only) | ||||
| @dataclass(frozen=True, kw_only=True) | ||||
| class ArtifactSpotSource(ItemSource): | ||||
|     amount: int | ||||
|  |  | |||
|  | @ -1,40 +1,39 @@ | |||
| from dataclasses import dataclass | ||||
| from typing import Tuple, Optional | ||||
| 
 | ||||
| from .game_item import ItemSource, kw_only, Requirement | ||||
| from .game_item import ItemSource | ||||
| from ..strings.season_names import Season | ||||
| 
 | ||||
| ItemPrice = Tuple[int, str] | ||||
| 
 | ||||
| 
 | ||||
| @dataclass(frozen=True, **kw_only) | ||||
| @dataclass(frozen=True, kw_only=True) | ||||
| class ShopSource(ItemSource): | ||||
|     shop_region: str | ||||
|     money_price: Optional[int] = None | ||||
|     items_price: Optional[Tuple[ItemPrice, ...]] = None | ||||
|     seasons: Tuple[str, ...] = Season.all | ||||
|     other_requirements: Tuple[Requirement, ...] = () | ||||
| 
 | ||||
|     def __post_init__(self): | ||||
|         assert self.money_price is not None or self.items_price is not None, "At least money price or items price need to be defined." | ||||
|         assert self.items_price is None or all(isinstance(p, tuple) for p in self.items_price), "Items price should be a tuple." | ||||
| 
 | ||||
| 
 | ||||
| @dataclass(frozen=True, **kw_only) | ||||
| @dataclass(frozen=True, kw_only=True) | ||||
| class MysteryBoxSource(ItemSource): | ||||
|     amount: int | ||||
| 
 | ||||
| 
 | ||||
| @dataclass(frozen=True, **kw_only) | ||||
| @dataclass(frozen=True, kw_only=True) | ||||
| class ArtifactTroveSource(ItemSource): | ||||
|     amount: int | ||||
| 
 | ||||
| 
 | ||||
| @dataclass(frozen=True, **kw_only) | ||||
| @dataclass(frozen=True, kw_only=True) | ||||
| class PrizeMachineSource(ItemSource): | ||||
|     amount: int | ||||
| 
 | ||||
| 
 | ||||
| @dataclass(frozen=True, **kw_only) | ||||
| @dataclass(frozen=True, kw_only=True) | ||||
| class FishingTreasureChestSource(ItemSource): | ||||
|     amount: int | ||||
|  |  | |||
|  | @ -1,9 +1,7 @@ | |||
| from dataclasses import dataclass, field | ||||
| 
 | ||||
| from ..data.game_item import kw_only | ||||
| 
 | ||||
| 
 | ||||
| @dataclass(frozen=True) | ||||
| class Skill: | ||||
|     name: str | ||||
|     has_mastery: bool = field(**kw_only) | ||||
|     has_mastery: bool = field(kw_only=True) | ||||
|  |  | |||
|  | @ -125,10 +125,7 @@ class StardewItemDeleter(Protocol): | |||
| 
 | ||||
| 
 | ||||
| def load_item_csv(): | ||||
|     try: | ||||
|         from importlib.resources import files | ||||
|     except ImportError: | ||||
|         from importlib_resources import files  # noqa | ||||
|     from importlib.resources import files | ||||
| 
 | ||||
|     items = [] | ||||
|     with files(data).joinpath("items.csv").open() as file: | ||||
|  |  | |||
|  | @ -130,10 +130,7 @@ class StardewLocationCollector(Protocol): | |||
| 
 | ||||
| 
 | ||||
| def load_location_csv() -> List[LocationData]: | ||||
|     try: | ||||
|         from importlib.resources import files | ||||
|     except ImportError: | ||||
|         from importlib_resources import files | ||||
|     from importlib.resources import files | ||||
| 
 | ||||
|     with files(data).joinpath("locations.csv").open() as file: | ||||
|         reader = csv.DictReader(file) | ||||
|  |  | |||
|  | @ -1,2 +0,0 @@ | |||
| importlib_resources; python_version <= '3.8' | ||||
| graphlib_backport; python_version <= '3.8' | ||||
|  | @ -12,8 +12,6 @@ BYTES_TO_REMOVE = 4 | |||
| 
 | ||||
| # <function Location.<lambda> at 0x102ca98a0> | ||||
| lambda_regex = re.compile(r"^<function Location\.<lambda> at (.*)>$") | ||||
| # Python 3.10.2\r\n | ||||
| python_version_regex = re.compile(r"^Python (\d+)\.(\d+)\.(\d+)\s*$") | ||||
| 
 | ||||
| 
 | ||||
| class TestGenerationIsStable(SVTestCase): | ||||
|  |  | |||
|  | @ -1,7 +1,6 @@ | |||
| from collections import Counter | ||||
| from dataclasses import dataclass | ||||
| from typing import ClassVar, Dict, Literal, Tuple | ||||
| from typing_extensions import TypeGuard  # remove when Python >= 3.10 | ||||
| from typing import ClassVar, Dict, Literal, Tuple, TypeGuard | ||||
| 
 | ||||
| from Options import Choice, DefaultOnToggle, NamedRange, OptionGroup, PerGameCommonOptions, Range, Removed, Toggle | ||||
| 
 | ||||
|  |  | |||
		Loading…
	
		Reference in New Issue