From 61fc80505edbe5cffb1b86a71c1417e1c0b99058 Mon Sep 17 00:00:00 2001 From: Fabian Dill Date: Tue, 20 Jun 2023 01:01:18 +0200 Subject: [PATCH] Core: refactor some loading mechanisms (#1753) Co-authored-by: black-sliver <59490463+black-sliver@users.noreply.github.com> --- Launcher.py | 28 +++++++++++++----- worlds/__init__.py | 73 +++++++++++++++++++++++++++------------------- 2 files changed, 64 insertions(+), 37 deletions(-) diff --git a/Launcher.py b/Launcher.py index e87fd78e..c0d5b1f2 100644 --- a/Launcher.py +++ b/Launcher.py @@ -11,6 +11,7 @@ Scroll down to components= to add components to the launcher as well as setup.py import argparse import itertools +import logging import multiprocessing import shlex import subprocess @@ -55,7 +56,7 @@ def open_patch(): except Exception as e: messagebox('Error', str(e), error=True) else: - file, _, component = identify(filename) + file, component = identify(filename) if file and component: launch([*get_exe(component), file], component.cli) @@ -96,11 +97,13 @@ components.extend([ def identify(path: Union[None, str]): if path is None: - return None, None, None + return None, None for component in components: if component.handles_file(path): - return path, component.script_name, component - return (None, None, None) if '/' in path or '\\' in path else (None, path, None) + return path, component + elif path == component.display_name or path == component.script_name: + return None, component + return None, None def get_exe(component: Union[str, Component]) -> Optional[Sequence[str]]: @@ -223,6 +226,15 @@ def run_gui(): Launcher().run() +def run_component(component: Component, *args): + if component.func: + component.func(*args) + elif component.script_name: + subprocess.run([*get_exe(component.script_name), *args]) + else: + logging.warning(f"Component {component} does not appear to be executable.") + + def main(args: Optional[Union[argparse.Namespace, dict]] = None): if isinstance(args, argparse.Namespace): args = {k: v for k, v in args._get_kwargs()} @@ -230,16 +242,18 @@ def main(args: Optional[Union[argparse.Namespace, dict]] = None): args = {} if "Patch|Game|Component" in args: - file, component, _ = identify(args["Patch|Game|Component"]) + file, component = identify(args["Patch|Game|Component"]) if file: args['file'] = file if component: args['component'] = component + if not component: + logging.warning(f"Could not identify Component responsible for {args['Patch|Game|Component']}") if 'file' in args: - subprocess.run([*get_exe(args['component']), args['file'], *args['args']]) + run_component(args["component"], args["file"], *args["args"]) elif 'component' in args: - subprocess.run([*get_exe(args['component']), *args['args']]) + run_component(args["component"], *args["args"]) else: run_gui() diff --git a/worlds/__init__.py b/worlds/__init__.py index e2ebb786..c6208fa9 100644 --- a/worlds/__init__.py +++ b/worlds/__init__.py @@ -39,9 +39,50 @@ class DataPackage(typing.TypedDict): class WorldSource(typing.NamedTuple): path: str # typically relative path from this module is_zip: bool = False + relative: bool = True # relative to regular world import folder def __repr__(self): - return f"{self.__class__.__name__}({self.path}, is_zip={self.is_zip})" + return f"{self.__class__.__name__}({self.path}, is_zip={self.is_zip}, relative={self.relative})" + + @property + def resolved_path(self) -> str: + if self.relative: + return os.path.join(folder, self.path) + return self.path + + def load(self) -> bool: + try: + 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]) + 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]) + + mod.__package__ = f"worlds.{mod.__package__}" + mod.__name__ = f"worlds.{mod.__name__}" + sys.modules[mod.__name__] = mod + with warnings.catch_warnings(): + warnings.filterwarnings("ignore", message="__package__ != __spec__.parent") + # Found no equivalent for < 3.10 + if hasattr(importer, "exec_module"): + importer.exec_module(mod) + else: + importlib.import_module(f".{self.path}", "worlds") + return True + + except Exception as e: + # A single world failing can still mean enough is working for the user, log and carry on + import traceback + import io + file_like = io.StringIO() + print(f"Could not load world {self}:", file=file_like) + traceback.print_exc(file=file_like) + file_like.seek(0) + import logging + logging.exception(file_like.read()) + return False # find potential world containers, currently folders and zip-importable .apworld's @@ -58,35 +99,7 @@ for file in os.scandir(folder): # import all submodules to trigger AutoWorldRegister world_sources.sort() for world_source in world_sources: - try: - if world_source.is_zip: - importer = zipimport.zipimporter(os.path.join(folder, world_source.path)) - if hasattr(importer, "find_spec"): # new in Python 3.10 - spec = importer.find_spec(world_source.path.split(".", 1)[0]) - mod = importlib.util.module_from_spec(spec) - else: # TODO: remove with 3.8 support - mod = importer.load_module(world_source.path.split(".", 1)[0]) - - mod.__package__ = f"worlds.{mod.__package__}" - mod.__name__ = f"worlds.{mod.__name__}" - sys.modules[mod.__name__] = mod - with warnings.catch_warnings(): - warnings.filterwarnings("ignore", message="__package__ != __spec__.parent") - # Found no equivalent for < 3.10 - if hasattr(importer, "exec_module"): - importer.exec_module(mod) - else: - importlib.import_module(f".{world_source.path}", "worlds") - except Exception as e: - # A single world failing can still mean enough is working for the user, log and carry on - import traceback - import io - file_like = io.StringIO() - print(f"Could not load world {world_source}:", file=file_like) - traceback.print_exc(file=file_like) - file_like.seek(0) - import logging - logging.exception(file_like.read()) + world_source.load() lookup_any_item_id_to_name = {} lookup_any_location_id_to_name = {}