diff --git a/docs/reST/ref/sndarray.rst b/docs/reST/ref/sndarray.rst index bc2899f614..733b49de1d 100644 --- a/docs/reST/ref/sndarray.rst +++ b/docs/reST/ref/sndarray.rst @@ -23,6 +23,8 @@ Each sample is an 8-bit or 16-bit integer, depending on the data format. A stereo sound file has two values per sample, while a mono sound file only has one. +.. versionchanged:: 2.5.3 sndarray module is lazily loaded to avoid loading NumPy needlessly + .. function:: array | :sl:`copy Sound samples into an array` diff --git a/docs/reST/ref/surfarray.rst b/docs/reST/ref/surfarray.rst index 48b917fbfd..8df7ef88b3 100644 --- a/docs/reST/ref/surfarray.rst +++ b/docs/reST/ref/surfarray.rst @@ -35,6 +35,8 @@ pixels from the surface and any changes performed to the array will make changes in the surface. As this last functions share memory with the surface, this one will be locked during the lifetime of the array. +.. versionchanged:: 2.5.3 surfarray module is lazily loaded to avoid loading NumPy needlessly + .. function:: array2d | :sl:`Copy pixels into a 2d array` diff --git a/src_py/__init__.py b/src_py/__init__.py index c227a9e55e..6676713fd3 100644 --- a/src_py/__init__.py +++ b/src_py/__init__.py @@ -72,6 +72,8 @@ class MissingModule: _NOT_IMPLEMENTED_ = True def __init__(self, name, urgent=0): + import sys # pylint: disable=reimported + self.name = name exc_type, exc_msg = sys.exc_info()[:2] self.info = str(exc_msg) @@ -259,7 +261,41 @@ def PixelArray(surface): # pylint: disable=unused-argument except (ImportError, OSError): transform = MissingModule("transform", urgent=1) + # lastly, the "optional" pygame modules + +# Private, persisting alias for use in __getattr__ +_MissingModule = MissingModule + + +def __getattr__(name): + """Implementation of lazy loading for some optional pygame modules. + + The surfarray and sndarray submodules use numpy, so they are loaded + lazily to avoid a heavy numpy import if the modules are never used. + + The first access of a lazily loaded submodule loads it and sets it + as an attribute on the pygame module. Pygame itself doesn't import these modules. + If the first access is an attribute access and not an import, then __getattr__ is + invoked (as the attribute isn't set yet), which imports the module dynamically. + + All lazy submodules are directly referenced in the packager_imports function. + """ + from importlib import import_module + + LAZY_MODULES = "surfarray", "sndarray" + if name not in LAZY_MODULES: + # Normal behavior for attribute accesses that aren't lazy modules + raise AttributeError(f"module '{__name__}' has no attribute '{name}'") + try: + module = import_module(f"{__name__}.{name}") + # A successful import automatically sets the module attribute on the package + except (ImportError, OSError): + module = _MissingModule(name, urgent=0) + globals()[name] = module + return module + + if "PYGAME_FREETYPE" in os.environ: try: import pygame.ftfont as font @@ -300,16 +336,6 @@ def PixelArray(surface): # pylint: disable=unused-argument except (ImportError, OSError): scrap = MissingModule("scrap", urgent=0) -try: - import pygame.surfarray -except (ImportError, OSError): - surfarray = MissingModule("surfarray", urgent=0) - -try: - import pygame.sndarray -except (ImportError, OSError): - sndarray = MissingModule("sndarray", urgent=0) - try: import pygame._debug from pygame._debug import print_debug_info @@ -366,6 +392,10 @@ def packager_imports(): import pygame.macosx import pygame.colordict + # lazily loaded pygame modules, just in case + import pygame.surfarray + import pygame.sndarray + # make Rects pickleable