diff --git a/CHANGELOG.md b/CHANGELOG.md index 3a27ebaf1..dbb2502c0 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -85,6 +85,8 @@ Arcade [PyPi Release History](https://pypi.org/project/arcade/#history) page. - Type checking with mypy and pyright now fails the CI check, like formatting and linting. Their existing errors are fixed. - Fixed code excerpts in the docs that highlighted or showed the wrong lines, because the example code had changed since their line numbers were set. This covers 62 excerpts: 48 in the platformer, pymunk platformer, menu, card game, compute shader, raycasting and shader toy glow tutorials, and 14 on example pages. Most now highlight the same code they were written for. Where the code was rewritten, they highlight the code that does the same job now. - Removed `arcade/experimental/perspective_parallax.py`, an unreferenced demo of `PerspectiveProjector` that didn't show a parallax effect. The `background_parallax` example shows parallax scrolling. +- Sped up `import arcade` from about 2.7 to 0.9 seconds on Windows ([#1159](https://github.com/pythonarcade/arcade/issues/1159)). Most of the time went to pyglet looking for game controllers when imported. Arcade now imports the controller code the first time `get_controllers()`, `ControllerManager`, `get_joysticks()` or `get_game_controllers()` is used, so the wait happens then instead. Sound still loads at import, so playing the first sound doesn't pause the game. + - Arcade's extra controller mappings now load at the same time. Code that calls `pyglet.input.get_controllers()` directly without using Arcade's controller functions first only gets pyglet's own mappings. ## 4.0.0.dev8 diff --git a/arcade/__init__.py b/arcade/__init__.py index f4fe451f5..278328a46 100644 --- a/arcade/__init__.py +++ b/arcade/__init__.py @@ -8,7 +8,7 @@ # Error out if we import Arcade with an incompatible version of Python. import sys import os -from typing import Final +from typing import TYPE_CHECKING, Final from pathlib import Path @@ -138,8 +138,9 @@ def configure_logging(level: int | None = None): from .screenshot import get_image from .screenshot import get_pixel -# We don't have joysticks game controllers in headless mode -if not headless: +# Game controllers are imported on first use, because pyglet lists the +# input devices when imported. See __getattr__ at the bottom of this file. +if TYPE_CHECKING: from .joysticks import get_game_controllers from .joysticks import get_joysticks from .controller import ControllerManager @@ -444,15 +445,24 @@ def configure_logging(level: int | None = None): __version__ = VERSION -# Piggyback on pyglet's doc run detection -if not getattr(sys, "is_pyglet_doc_run", False): - # Load additional game controller mappings to Pyglet - if not headless: - try: - import pyglet.input.controller - - mappings_file = resources.resolve(":system:gamecontrollerdb.txt") - # TODO: remove string conversion once fixed upstream - pyglet.input.controller.add_mappings_from_file(str(mappings_file)) - except AssertionError: - pass +_LAZY_IMPORTS: dict[str, str] = {} +# We don't have joysticks game controllers in headless mode +if not headless: + _LAZY_IMPORTS = { + "get_joysticks": "joysticks", + "get_game_controllers": "joysticks", + "joysticks": "joysticks", + "ControllerManager": "controller", + "get_controllers": "controller", + "controller": "controller", + } + + +def __getattr__(name: str): + module_name = _LAZY_IMPORTS.get(name) + if module_name is None: + raise AttributeError(f"module 'arcade' has no attribute {name!r}") + import importlib + + module = importlib.import_module(f".{module_name}", __name__) + return module if name == module_name else getattr(module, name) diff --git a/arcade/controller.py b/arcade/controller.py index 0e096f8e9..9f15a5854 100644 --- a/arcade/controller.py +++ b/arcade/controller.py @@ -5,10 +5,25 @@ https://pyglet.readthedocs.io/en/latest/programming_guide/input.html#using-controllers """ +import sys + import pyglet.input +import pyglet.input.controller + +from arcade import resources __all__ = ["get_controllers", "ControllerManager"] +# Load additional game controller mappings to pyglet. Piggyback on +# pyglet's doc run detection. +if not getattr(sys, "is_pyglet_doc_run", False): + try: + mappings_file = resources.resolve(":system:gamecontrollerdb.txt") + # TODO: remove string conversion once fixed upstream + pyglet.input.controller.add_mappings_from_file(str(mappings_file)) + except AssertionError: + pass + def get_controllers(): """ diff --git a/arcade/experimental/controller_window.py b/arcade/experimental/controller_window.py index 933146ee2..afa5bcdd3 100644 --- a/arcade/experimental/controller_window.py +++ b/arcade/experimental/controller_window.py @@ -1,9 +1,13 @@ -import warnings +from __future__ import annotations -from pyglet.input import Controller +import warnings +from typing import TYPE_CHECKING import arcade -from arcade import ControllerManager + +if TYPE_CHECKING: + # Importing pyglet.input is slow, so only type checkers do it here + from pyglet.input import Controller class _WindowControllerBridge: @@ -21,6 +25,8 @@ class _WindowControllerBridge: def __init__(self, window: arcade.Window): self.window = window + from arcade.controller import ControllerManager + self.cm = ControllerManager() self.cm.push_handlers(self) diff --git a/arcade/experimental/input_manager_example.py b/arcade/experimental/input_manager_example.py index 2619d48ba..2c65fd18d 100644 --- a/arcade/experimental/input_manager_example.py +++ b/arcade/experimental/input_manager_example.py @@ -122,7 +122,7 @@ def __init__( # with open("out.json", "w") as f: # json.dump(serialized, f) - self.controller_manager = pyglet.input.ControllerManager() + self.controller_manager = arcade.ControllerManager() self.controller_manager.set_handlers(self.on_connect, self.on_disconnect) for index, controller in enumerate(self.controller_manager.get_controllers()): diff --git a/arcade/gui/ui_manager.py b/arcade/gui/ui_manager.py index f7b6500d5..ced9c4bfc 100644 --- a/arcade/gui/ui_manager.py +++ b/arcade/gui/ui_manager.py @@ -10,10 +10,9 @@ from collections import defaultdict from collections.abc import Iterable -from typing import TypeGuard, TypeVar +from typing import TYPE_CHECKING, TypeGuard, TypeVar from pyglet.event import EVENT_HANDLED, EVENT_UNHANDLED, EventDispatcher -from pyglet.input import Controller from pyglet.math import Vec2 import arcade @@ -43,6 +42,10 @@ from arcade.gui.widgets import UIWidget from arcade.types import LBWH, AnchorPoint, Point2, Rect +if TYPE_CHECKING: + # Importing pyglet.input is slow, so only type checkers do it here + from pyglet.input import Controller + W = TypeVar("W", bound=UIWidget) @@ -496,27 +499,27 @@ def on_resize(self, width, height): self.trigger_render() - def on_connect(self, controller: Controller): + def on_connect(self, controller: "Controller"): """Called when a controller is connected.""" self.dispatch_ui_event(UIControllerConnectEvent(controller)) - def on_disconnect(self, controller: Controller): + def on_disconnect(self, controller: "Controller"): """Called when a controller is disconnected.""" self.dispatch_ui_event(UIControllerDisconnectEvent(controller)) - def on_stick_motion(self, controller: Controller, name: str, value: Vec2): + def on_stick_motion(self, controller: "Controller", name: str, value: Vec2): return self.dispatch_ui_event(UIControllerStickEvent(controller, name, value)) - def on_trigger_motion(self, controller: Controller, name: str, value: float): + def on_trigger_motion(self, controller: "Controller", name: str, value: float): return self.dispatch_ui_event(UIControllerTriggerEvent(controller, name, value)) - def on_button_press(self, controller: Controller, button: str): + def on_button_press(self, controller: "Controller", button: str): return self.dispatch_ui_event(UIControllerButtonPressEvent(controller, button)) - def on_button_release(self, controller: Controller, button: str): + def on_button_release(self, controller: "Controller", button: str): return self.dispatch_ui_event(UIControllerButtonReleaseEvent(controller, button)) - def on_dpad_motion(self, controller: Controller, value: Vec2): + def on_dpad_motion(self, controller: "Controller", value: Vec2): return self.dispatch_ui_event(UIControllerDpadEvent(controller, value)) @property diff --git a/arcade/input/manager.py b/arcade/input/manager.py index 9767f8aeb..07009a741 100644 --- a/arcade/input/manager.py +++ b/arcade/input/manager.py @@ -2,10 +2,9 @@ from collections.abc import Callable from enum import Enum -from typing import Any, TypeVar +from typing import TYPE_CHECKING, Any, TypeVar import pyglet -from pyglet.input.base import Controller from typing_extensions import TypedDict import arcade @@ -18,6 +17,10 @@ serialize_action, serialize_axis, ) + +if TYPE_CHECKING: + # Importing pyglet.input is slow, so only type checkers do it here + from pyglet.input import Controller from arcade.input.inputs import InputEnum, InputType from arcade.input.raw_dicts import RawAction, RawAxis from arcade.types import OneOrIterableOf @@ -247,7 +250,7 @@ def copy_existing(self, existing: InputManager): def from_existing( cls, existing: InputManager, - controller: pyglet.input.Controller | None = None, + controller: Controller | None = None, ) -> InputManager: """ Create a new InputManager from an existing one. This does not copy current input values,