From 334781e976e0b701841634cc155fccfc86ca7348 Mon Sep 17 00:00:00 2001 From: Fabian Dill Date: Wed, 27 Nov 2024 03:28:00 +0100 Subject: [PATCH] Core: purge py3.8 and py3.9 (#3973) Co-authored-by: Remy Jette Co-authored-by: Jouramie <16137441+Jouramie@users.noreply.github.com> Co-authored-by: Aaron Wagener --- .github/pyright-config.json | 2 +- .github/workflows/analyze-modified-files.yml | 2 +- .github/workflows/build.yml | 4 +-- .github/workflows/unittests.yml | 4 +-- BaseClasses.py | 12 ++++---- ModuleUpdate.py | 4 +-- Utils.py | 5 ++-- WebHost.py | 2 +- WebHostLib/requirements.txt | 4 +-- docs/contributing.md | 2 +- docs/running from source.md | 2 +- kvui.py | 5 +--- setup.py | 2 +- worlds/AutoSNIClient.py | 4 +-- worlds/__init__.py | 17 ++++------- worlds/hk/Extractor.py | 6 +--- worlds/hk/requirements.txt | 1 - worlds/messenger/__init__.py | 30 +++++++++---------- worlds/messenger/client_setup.py | 5 ++-- worlds/messenger/connections.py | 8 ++--- worlds/messenger/constants.py | 18 +++++------ worlds/messenger/options.py | 3 +- worlds/messenger/portals.py | 12 ++++---- worlds/messenger/regions.py | 13 ++++---- worlds/messenger/rules.py | 8 ++--- worlds/messenger/shop.py | 18 +++++------ worlds/messenger/subclasses.py | 10 +++---- worlds/messenger/test/test_shop.py | 2 +- worlds/stardew_valley/content/unpacking.py | 6 +--- worlds/stardew_valley/data/artisan.py | 4 +-- worlds/stardew_valley/data/game_item.py | 18 +++-------- worlds/stardew_valley/data/harvest.py | 17 +++++------ worlds/stardew_valley/data/shop.py | 13 ++++---- worlds/stardew_valley/data/skill.py | 4 +-- worlds/stardew_valley/items.py | 5 +--- worlds/stardew_valley/locations.py | 5 +--- worlds/stardew_valley/requirements.txt | 2 -- .../test/stability/TestStability.py | 2 -- worlds/zillion/options.py | 3 +- 39 files changed, 112 insertions(+), 172 deletions(-) delete mode 100644 worlds/hk/requirements.txt delete mode 100644 worlds/stardew_valley/requirements.txt diff --git a/.github/pyright-config.json b/.github/pyright-config.json index 6ad7fa5f..7d981778 100644 --- a/.github/pyright-config.json +++ b/.github/pyright-config.json @@ -16,7 +16,7 @@ "reportMissingImports": true, "reportMissingTypeStubs": true, - "pythonVersion": "3.8", + "pythonVersion": "3.10", "pythonPlatform": "Windows", "executionEnvironments": [ diff --git a/.github/workflows/analyze-modified-files.yml b/.github/workflows/analyze-modified-files.yml index c9995fa2..b59336fa 100644 --- a/.github/workflows/analyze-modified-files.yml +++ b/.github/workflows/analyze-modified-files.yml @@ -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 != '' diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml index 23c463fb..c013172e 100644 --- a/.github/workflows/build.yml +++ b/.github/workflows/build.yml @@ -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 diff --git a/.github/workflows/unittests.yml b/.github/workflows/unittests.yml index a38fef8f..88b5d129 100644 --- a/.github/workflows/unittests.yml +++ b/.github/workflows/unittests.yml @@ -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 diff --git a/BaseClasses.py b/BaseClasses.py index 46edeb5e..2e4efd60 100644 --- a/BaseClasses.py +++ b/BaseClasses.py @@ -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. diff --git a/ModuleUpdate.py b/ModuleUpdate.py index f49182bb..dada16ce 100644 --- a/ModuleUpdate.py +++ b/ModuleUpdate.py @@ -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()) diff --git a/Utils.py b/Utils.py index 2dfcd9d3..535933d8 100644 --- a/Utils.py +++ b/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") diff --git a/WebHost.py b/WebHost.py index 3bf75eb3..3790a5f6 100644 --- a/WebHost.py +++ b/WebHost.py @@ -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 diff --git a/WebHostLib/requirements.txt b/WebHostLib/requirements.txt index 5c794153..b7b14dea 100644 --- a/WebHostLib/requirements.txt +++ b/WebHostLib/requirements.txt @@ -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 diff --git a/docs/contributing.md b/docs/contributing.md index 9fd21408..96fc316b 100644 --- a/docs/contributing.md +++ b/docs/contributing.md @@ -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: diff --git a/docs/running from source.md b/docs/running from source.md index ef1594da..66dd1925 100644 --- a/docs/running from source.md +++ b/docs/running from source.md @@ -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 diff --git a/kvui.py b/kvui.py index 74d8ad06..27236542 100644 --- a/kvui.py +++ b/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" diff --git a/setup.py b/setup.py index afbe1772..f075551d 100644 --- a/setup.py +++ b/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": ["*."], diff --git a/worlds/AutoSNIClient.py b/worlds/AutoSNIClient.py index b3f40be2..f9444eee 100644 --- a/worlds/AutoSNIClient.py +++ b/worlds/AutoSNIClient.py @@ -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 diff --git a/worlds/__init__.py b/worlds/__init__.py index c277ac9c..7db651bd 100644 --- a/worlds/__init__.py +++ b/worlds/__init__.py @@ -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(): diff --git a/worlds/hk/Extractor.py b/worlds/hk/Extractor.py index 61fabc4d..86660848 100644 --- a/worlds/hk/Extractor.py +++ b/worlds/hk/Extractor.py @@ -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 diff --git a/worlds/hk/requirements.txt b/worlds/hk/requirements.txt deleted file mode 100644 index 1b410ffb..00000000 --- a/worlds/hk/requirements.txt +++ /dev/null @@ -1 +0,0 @@ -astunparse>=1.6.3; python_version <= '3.8' \ No newline at end of file diff --git a/worlds/messenger/__init__.py b/worlds/messenger/__init__.py index 9a38953f..59e724d3 100644 --- a/worlds/messenger/__init__.py +++ b/worlds/messenger/__init__.py @@ -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) diff --git a/worlds/messenger/client_setup.py b/worlds/messenger/client_setup.py index 77a0f634..6b98a1b4 100644 --- a/worlds/messenger/client_setup.py +++ b/worlds/messenger/client_setup.py @@ -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: diff --git a/worlds/messenger/connections.py b/worlds/messenger/connections.py index 69dd7aa7..79912a56 100644 --- a/worlds/messenger/connections.py +++ b/worlds/messenger/connections.py @@ -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", diff --git a/worlds/messenger/constants.py b/worlds/messenger/constants.py index ea15c710..47b5a1a8 100644 --- a/worlds/messenger/constants.py +++ b/worlds/messenger/constants.py @@ -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", diff --git a/worlds/messenger/options.py b/worlds/messenger/options.py index 59e694cd..8b61a943 100644 --- a/worlds/messenger/options.py +++ b/worlds/messenger/options.py @@ -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), diff --git a/worlds/messenger/portals.py b/worlds/messenger/portals.py index 17152a1a..896fefa6 100644 --- a/worlds/messenger/portals.py +++ b/worlds/messenger/portals.py @@ -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 diff --git a/worlds/messenger/regions.py b/worlds/messenger/regions.py index 153f8510..d53b84fe 100644 --- a/worlds/messenger/regions.py +++ b/worlds/messenger/regions.py @@ -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", diff --git a/worlds/messenger/rules.py b/worlds/messenger/rules.py index c354ad70..f09025c7 100644 --- a/worlds/messenger/rules.py +++ b/worlds/messenger/rules.py @@ -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 diff --git a/worlds/messenger/shop.py b/worlds/messenger/shop.py index 3c8c7bf6..6ab72f97 100644 --- a/worlds/messenger/shop.py +++ b/worlds/messenger/shop.py @@ -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] diff --git a/worlds/messenger/subclasses.py b/worlds/messenger/subclasses.py index b60aeb17..29e3ea89 100644 --- a/worlds/messenger/subclasses.py +++ b/worlds/messenger/subclasses.py @@ -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: diff --git a/worlds/messenger/test/test_shop.py b/worlds/messenger/test/test_shop.py index ce6fd19e..21a0c352 100644 --- a/worlds/messenger/test/test_shop.py +++ b/worlds/messenger/test/test_shop.py @@ -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 diff --git a/worlds/stardew_valley/content/unpacking.py b/worlds/stardew_valley/content/unpacking.py index f069866d..3c57f91a 100644 --- a/worlds/stardew_valley/content/unpacking.py +++ b/worlds/stardew_valley/content/unpacking.py @@ -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. diff --git a/worlds/stardew_valley/data/artisan.py b/worlds/stardew_valley/data/artisan.py index 593ab6a3..90be5b16 100644 --- a/worlds/stardew_valley/data/artisan.py +++ b/worlds/stardew_valley/data/artisan.py @@ -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 diff --git a/worlds/stardew_valley/data/game_item.py b/worlds/stardew_valley/data/game_item.py index 6c8d30ed..c6e4717c 100644 --- a/worlds/stardew_valley/data/game_item.py +++ b/worlds/stardew_valley/data/game_item.py @@ -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, ...] = () diff --git a/worlds/stardew_valley/data/harvest.py b/worlds/stardew_valley/data/harvest.py index 087d7c3f..0fdae954 100644 --- a/worlds/stardew_valley/data/harvest.py +++ b/worlds/stardew_valley/data/harvest.py @@ -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 diff --git a/worlds/stardew_valley/data/shop.py b/worlds/stardew_valley/data/shop.py index f14dbac8..cc950602 100644 --- a/worlds/stardew_valley/data/shop.py +++ b/worlds/stardew_valley/data/shop.py @@ -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 diff --git a/worlds/stardew_valley/data/skill.py b/worlds/stardew_valley/data/skill.py index d0674f34..4c754ddd 100644 --- a/worlds/stardew_valley/data/skill.py +++ b/worlds/stardew_valley/data/skill.py @@ -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) diff --git a/worlds/stardew_valley/items.py b/worlds/stardew_valley/items.py index 730cb24c..993863bf 100644 --- a/worlds/stardew_valley/items.py +++ b/worlds/stardew_valley/items.py @@ -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: diff --git a/worlds/stardew_valley/locations.py b/worlds/stardew_valley/locations.py index 43246a94..1d67d535 100644 --- a/worlds/stardew_valley/locations.py +++ b/worlds/stardew_valley/locations.py @@ -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) diff --git a/worlds/stardew_valley/requirements.txt b/worlds/stardew_valley/requirements.txt deleted file mode 100644 index 65e922a6..00000000 --- a/worlds/stardew_valley/requirements.txt +++ /dev/null @@ -1,2 +0,0 @@ -importlib_resources; python_version <= '3.8' -graphlib_backport; python_version <= '3.8' diff --git a/worlds/stardew_valley/test/stability/TestStability.py b/worlds/stardew_valley/test/stability/TestStability.py index 8bb904a5..137a7172 100644 --- a/worlds/stardew_valley/test/stability/TestStability.py +++ b/worlds/stardew_valley/test/stability/TestStability.py @@ -12,8 +12,6 @@ BYTES_TO_REMOVE = 4 # at 0x102ca98a0> lambda_regex = re.compile(r"^ at (.*)>$") -# Python 3.10.2\r\n -python_version_regex = re.compile(r"^Python (\d+)\.(\d+)\.(\d+)\s*$") class TestGenerationIsStable(SVTestCase): diff --git a/worlds/zillion/options.py b/worlds/zillion/options.py index 8df61265..ec0fdb0b 100644 --- a/worlds/zillion/options.py +++ b/worlds/zillion/options.py @@ -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