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:
Fabian Dill 2024-11-27 03:28:00 +01:00 committed by GitHub
parent 6c939d2d59
commit 334781e976
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
39 changed files with 112 additions and 172 deletions

View File

@ -16,7 +16,7 @@
"reportMissingImports": true,
"reportMissingTypeStubs": true,
"pythonVersion": "3.8",
"pythonVersion": "3.10",
"pythonPlatform": "Windows",
"executionEnvironments": [

View File

@ -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 != ''

View File

@ -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

View File

@ -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

View File

@ -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.

View File

@ -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())

View File

@ -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")

View File

@ -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

View File

@ -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

View File

@ -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:

View File

@ -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

View File

@ -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"

View File

@ -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": ["*."],

View File

@ -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

View File

@ -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():

View File

@ -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

View File

@ -1 +0,0 @@
astunparse>=1.6.3; python_version <= '3.8'

View File

@ -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)

View File

@ -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:

View File

@ -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",

View File

@ -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",

View File

@ -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),

View File

@ -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

View File

@ -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",

View File

@ -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

View File

@ -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]

View File

@ -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:

View File

@ -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

View File

@ -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.

View File

@ -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

View File

@ -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, ...] = ()

View File

@ -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

View File

@ -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

View File

@ -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)

View File

@ -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:

View 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)

View File

@ -1,2 +0,0 @@
importlib_resources; python_version <= '3.8'
graphlib_backport; python_version <= '3.8'

View File

@ -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):

View File

@ -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