diff --git a/docs/conf.py b/docs/conf.py index a4a13e0..0cec1b9 100644 --- a/docs/conf.py +++ b/docs/conf.py @@ -14,7 +14,7 @@ # # import os import sys - +from typing import Any # sys.path.insert(0, os.path.abspath('.')) from unittest.mock import MagicMock @@ -22,7 +22,7 @@ # Mock modules class Mock(MagicMock): @classmethod - def __getattr__(cls, name): + def __getattr__(cls: Any, name: Any) -> MagicMock: return MagicMock() @@ -141,7 +141,7 @@ def __getattr__(cls, name): # -- Options for LaTeX output ------------------------------------------------ -latex_elements = { +latex_elements: dict[str, str] = { # The paper size ('letterpaper' or 'a4paper'). # # 'papersize': 'letterpaper', diff --git a/examples/advanced/animated_sprites.py b/examples/advanced/animated_sprites.py index 040d3ee..13240af 100644 --- a/examples/advanced/animated_sprites.py +++ b/examples/advanced/animated_sprites.py @@ -1,8 +1,10 @@ -import moderngl from pathlib import Path + +import glm +import moderngl + import moderngl_window as mglw from moderngl_window import geometry -import glm # from moderngl_window.conf import settings # settings.SCREENSHOT_PATH = 'screenshots' diff --git a/examples/advanced/compute_render_to_texture.py b/examples/advanced/compute_render_to_texture.py index 5b4e41b..56a402b 100644 --- a/examples/advanced/compute_render_to_texture.py +++ b/examples/advanced/compute_render_to_texture.py @@ -1,5 +1,7 @@ -import moderngl as mgl from pathlib import Path + +import moderngl as mgl + import moderngl_window as mglw from moderngl_window import geometry diff --git a/examples/advanced/fragment_picking.py b/examples/advanced/fragment_picking.py index 985d371..a12df80 100644 --- a/examples/advanced/fragment_picking.py +++ b/examples/advanced/fragment_picking.py @@ -1,8 +1,9 @@ import struct from pathlib import Path -import moderngl import glm +import moderngl + import moderngl_window from moderngl_window import geometry from moderngl_window.opengl.projection import Projection3D diff --git a/examples/advanced/navier_stokes.py b/examples/advanced/navier_stokes.py index 7ad220d..93c498f 100644 --- a/examples/advanced/navier_stokes.py +++ b/examples/advanced/navier_stokes.py @@ -8,6 +8,7 @@ import random from pathlib import Path + import glm import moderngl_window diff --git a/examples/advanced/pygame2.py b/examples/advanced/pygame2.py index d109274..b441c88 100644 --- a/examples/advanced/pygame2.py +++ b/examples/advanced/pygame2.py @@ -5,11 +5,13 @@ import math from pathlib import Path -import pygame + +import glm import moderngl +import pygame + import moderngl_window from moderngl_window import geometry -import glm # from moderngl_window.conf import settings # settings.SCREENSHOT_PATH = 'capture' diff --git a/examples/advanced/pygame2_background_image.py b/examples/advanced/pygame2_background_image.py index ee92c80..8240e60 100644 --- a/examples/advanced/pygame2_background_image.py +++ b/examples/advanced/pygame2_background_image.py @@ -7,8 +7,10 @@ """ import math from pathlib import Path -import pygame + import moderngl +import pygame + import moderngl_window from moderngl_window import geometry diff --git a/examples/advanced/pygame2_simple.py b/examples/advanced/pygame2_simple.py index 1e3befa..b075ea1 100644 --- a/examples/advanced/pygame2_simple.py +++ b/examples/advanced/pygame2_simple.py @@ -44,9 +44,9 @@ import math from array import array +import moderngl import pygame -import moderngl import moderngl_window diff --git a/examples/advanced/shader_includes.py b/examples/advanced/shader_includes.py index 9d9af2b..7a8fb8b 100644 --- a/examples/advanced/shader_includes.py +++ b/examples/advanced/shader_includes.py @@ -6,6 +6,7 @@ and render each quadrant of the screen with different blend types """ from pathlib import Path + import moderngl_window as mglw from moderngl_window import geometry diff --git a/examples/advanced/shadow_mapping.py b/examples/advanced/shadow_mapping.py index b062d50..cf565d8 100644 --- a/examples/advanced/shadow_mapping.py +++ b/examples/advanced/shadow_mapping.py @@ -5,14 +5,14 @@ import math from pathlib import Path -import glm +import glm import moderngl +from base import CameraWindow + import moderngl_window from moderngl_window import geometry -from base import CameraWindow - class ShadowMapping(CameraWindow): title = "Shadow Mapping" diff --git a/examples/advanced/shadow_mapping_56.py b/examples/advanced/shadow_mapping_56.py index 5979bde..9ea261b 100644 --- a/examples/advanced/shadow_mapping_56.py +++ b/examples/advanced/shadow_mapping_56.py @@ -5,14 +5,14 @@ import math from pathlib import Path -import glm +import glm import moderngl +from base import CameraWindow + import moderngl_window from moderngl_window import geometry -from base import CameraWindow - class ShadowMapping(CameraWindow): title = "Shadow Mapping" diff --git a/examples/advanced/tetrahedral_mesh.py b/examples/advanced/tetrahedral_mesh.py index 412dda1..aec5005 100644 --- a/examples/advanced/tetrahedral_mesh.py +++ b/examples/advanced/tetrahedral_mesh.py @@ -1,12 +1,13 @@ from pathlib import Path -import numpy as np -import glm +import glm import moderngl -from moderngl_window.opengl.vao import VAO -from moderngl_window import geometry +import numpy as np from base import CameraWindow +from moderngl_window import geometry +from moderngl_window.opengl.vao import VAO + class VolumetricTetrahedralMesh(CameraWindow): """Volumetric Tetrahedral Mesh. diff --git a/examples/advanced/voxel_cubes.py b/examples/advanced/voxel_cubes.py index a61576d..211c024 100644 --- a/examples/advanced/voxel_cubes.py +++ b/examples/advanced/voxel_cubes.py @@ -15,14 +15,14 @@ * We can reduce a voxel volume dramatically by just inspecting neighbors """ -from pathlib import Path -from typing import Tuple from array import array +from pathlib import Path +import glm import moderngl -from moderngl_window import geometry from base import CameraWindow -import glm + +from moderngl_window import geometry class CubeVoxel(CameraWindow): @@ -95,7 +95,7 @@ class Voxel: We are sticking to simple transforms at textures. """ - def __init__(self, *, ctx: moderngl.Context, size: Tuple[int, int, int]): + def __init__(self, *, ctx: moderngl.Context, size: tuple[int, int, int]): self.ctx = ctx self._size = size diff --git a/examples/advanced/water.py b/examples/advanced/water.py index aed61eb..0de8f7c 100644 --- a/examples/advanced/water.py +++ b/examples/advanced/water.py @@ -5,12 +5,12 @@ """ import random from pathlib import Path -import numpy as np import moderngl +import numpy as np + import moderngl_window -from moderngl_window import geometry -from moderngl_window import screenshot +from moderngl_window import geometry, screenshot class Water(moderngl_window.WindowConfig): diff --git a/examples/cube_model.py b/examples/cube_model.py index b9e8e95..7cdbca8 100644 --- a/examples/cube_model.py +++ b/examples/cube_model.py @@ -1,10 +1,11 @@ from pathlib import Path -import glm +import glm import moderngl -import moderngl_window from base import CameraWindow +import moderngl_window + class CubeModel(CameraWindow): aspect_ratio = 16 / 9 diff --git a/examples/cubes.py b/examples/cubes.py index 06e7af7..33922f9 100644 --- a/examples/cubes.py +++ b/examples/cubes.py @@ -5,10 +5,10 @@ from pathlib import Path +import glm import moderngl -import moderngl_window -import glm +import moderngl_window class Cubes(moderngl_window.WindowConfig): diff --git a/examples/custom_config_class.py b/examples/custom_config_class.py index 5e87b81..7fa3955 100644 --- a/examples/custom_config_class.py +++ b/examples/custom_config_class.py @@ -1,5 +1,6 @@ import math import random + import moderngl_window from moderngl_window.conf import settings from moderngl_window.timers.clock import Timer diff --git a/examples/custom_config_functions.py b/examples/custom_config_functions.py index a19e0e7..5bd0874 100644 --- a/examples/custom_config_functions.py +++ b/examples/custom_config_functions.py @@ -3,6 +3,7 @@ """ import math import random + import moderngl_window from moderngl_window.conf import settings from moderngl_window.timers.clock import Timer diff --git a/examples/drag_drop_file_input.py b/examples/drag_drop_file_input.py index ba54771..247aa52 100644 --- a/examples/drag_drop_file_input.py +++ b/examples/drag_drop_file_input.py @@ -5,13 +5,13 @@ Currently only working with the Pyglet backend. """ -from pathlib import Path import os +from pathlib import Path +import glm import moderngl -import moderngl_window -import glm +import moderngl_window class Cubes(moderngl_window.WindowConfig): diff --git a/examples/geometry_bbox.py b/examples/geometry_bbox.py index 94e5efe..26e915f 100644 --- a/examples/geometry_bbox.py +++ b/examples/geometry_bbox.py @@ -1,10 +1,9 @@ import glm +from base import CameraWindow import moderngl_window from moderngl_window import geometry -from base import CameraWindow - class GeometryBbox(CameraWindow): title = "BBox Geometry" diff --git a/examples/geometry_cube.py b/examples/geometry_cube.py index 50a7038..2e28099 100644 --- a/examples/geometry_cube.py +++ b/examples/geometry_cube.py @@ -1,12 +1,12 @@ from pathlib import Path -import glm +import glm import moderngl +from base import CameraWindow + import moderngl_window from moderngl_window import geometry -from base import CameraWindow - class CubeSimple(CameraWindow): title = "Plain Cube" diff --git a/examples/geometry_cube_instanced.py b/examples/geometry_cube_instanced.py index 892966a..8fa5911 100644 --- a/examples/geometry_cube_instanced.py +++ b/examples/geometry_cube_instanced.py @@ -7,12 +7,13 @@ from pathlib import Path -import numpy import glm import moderngl +import numpy +from base import CameraWindow + import moderngl_window from moderngl_window import geometry -from base import CameraWindow class CubeSimpleInstanced(CameraWindow): diff --git a/examples/geometry_lines.py b/examples/geometry_lines.py index 91aab6d..e7cfd03 100644 --- a/examples/geometry_lines.py +++ b/examples/geometry_lines.py @@ -1,10 +1,11 @@ from pathlib import Path + import glm +import moderngl import numpy +from base import CameraWindow -import moderngl import moderngl_window -from base import CameraWindow class LinesDemo(CameraWindow): diff --git a/examples/geometry_quad_fs.py b/examples/geometry_quad_fs.py index a39ff75..14fcdb5 100644 --- a/examples/geometry_quad_fs.py +++ b/examples/geometry_quad_fs.py @@ -1,8 +1,7 @@ from pathlib import Path import moderngl_window -from moderngl_window import geometry -from moderngl_window import resources +from moderngl_window import geometry, resources resources.register_dir((Path(__file__).parent / 'resources').resolve()) diff --git a/examples/geometry_quad_fs_mouse_scroll.py b/examples/geometry_quad_fs_mouse_scroll.py index b2f026e..42fb808 100644 --- a/examples/geometry_quad_fs_mouse_scroll.py +++ b/examples/geometry_quad_fs_mouse_scroll.py @@ -1,8 +1,7 @@ from pathlib import Path import moderngl_window -from moderngl_window import geometry -from moderngl_window import resources +from moderngl_window import geometry, resources resources.register_dir((Path(__file__).parent / "resources").resolve()) diff --git a/examples/gltf_scenes.py b/examples/gltf_scenes.py index f9e321b..bbc5d88 100644 --- a/examples/gltf_scenes.py +++ b/examples/gltf_scenes.py @@ -1,10 +1,11 @@ from pathlib import Path -import glm +import glm import moderngl +from base import CameraWindow + import moderngl_window as mglw from moderngl_window.scene.camera import KeyboardCamera -from base import CameraWindow class CubeModel(CameraWindow): diff --git a/examples/headless.py b/examples/headless.py index 64a3b74..3167df6 100644 --- a/examples/headless.py +++ b/examples/headless.py @@ -1,6 +1,7 @@ +import moderngl import numpy as np from PIL import Image -import moderngl + import moderngl_window diff --git a/examples/integration_imgui.py b/examples/integration_imgui.py index 1e640be..62d26f5 100644 --- a/examples/integration_imgui.py +++ b/examples/integration_imgui.py @@ -1,8 +1,10 @@ from pathlib import Path + +import glm +import moderngl # import imgui from imgui_bundle import imgui -import moderngl -import glm + import moderngl_window as mglw from moderngl_window import geometry from moderngl_window.integrations.imgui_bundle import ModernglWindowRenderer diff --git a/examples/integration_imgui_image.py b/examples/integration_imgui_image.py index 61fcb1d..7dd43f2 100644 --- a/examples/integration_imgui_image.py +++ b/examples/integration_imgui_image.py @@ -1,9 +1,10 @@ from pathlib import Path +import glm +import moderngl # import imgui from imgui_bundle import imgui -import moderngl -import glm + import moderngl_window as mglw from moderngl_window import geometry from moderngl_window.integrations.imgui_bundle import ModernglWindowRenderer diff --git a/examples/moderngl_logo.py b/examples/moderngl_logo.py index 4a09c6b..2aa8963 100644 --- a/examples/moderngl_logo.py +++ b/examples/moderngl_logo.py @@ -1,5 +1,6 @@ -import numpy as np import moderngl +import numpy as np + import moderngl_window as mglw diff --git a/examples/scheduling_example.py b/examples/scheduling_example.py index e31a123..19c0e8c 100644 --- a/examples/scheduling_example.py +++ b/examples/scheduling_example.py @@ -1,5 +1,6 @@ -import moderngl_window import random + +import moderngl_window from moderngl_window.utils.scheduler import Scheduler diff --git a/examples/skybox_cubemap.py b/examples/skybox_cubemap.py index 159ce8a..d40bf8d 100644 --- a/examples/skybox_cubemap.py +++ b/examples/skybox_cubemap.py @@ -1,8 +1,10 @@ from pathlib import Path + import moderngl +from base import CameraWindow + import moderngl_window from moderngl_window import geometry -from base import CameraWindow class Cubemap(CameraWindow): diff --git a/examples/ssao.py b/examples/ssao.py index 44fbbf6..cfe9319 100644 --- a/examples/ssao.py +++ b/examples/ssao.py @@ -2,12 +2,12 @@ import glm import moderngl -from imgui_bundle import imgui import numpy as np +from base import OrbitDragCameraWindow +from imgui_bundle import imgui import moderngl_window from moderngl_window.integrations.imgui_bundle import ModernglWindowRenderer -from base import OrbitDragCameraWindow class SSAODemo(OrbitDragCameraWindow): diff --git a/examples/texture_array.py b/examples/texture_array.py index 5ec1c24..804b063 100644 --- a/examples/texture_array.py +++ b/examples/texture_array.py @@ -1,11 +1,11 @@ from pathlib import Path -import glm +import glm import moderngl +from base import CameraWindow import moderngl_window from moderngl_window import geometry -from base import CameraWindow class TextureArrayExample(CameraWindow): diff --git a/examples/uniform_block.py b/examples/uniform_block.py index 31b9d5d..227e1dd 100644 --- a/examples/uniform_block.py +++ b/examples/uniform_block.py @@ -1,6 +1,6 @@ import glm - import moderngl + import moderngl_window from moderngl_window import geometry diff --git a/examples/video.py b/examples/video.py index f71c0b6..54ed3ca 100644 --- a/examples/video.py +++ b/examples/video.py @@ -5,13 +5,14 @@ """ import math -from typing import Tuple, Union from pathlib import Path +from typing import Union +import av import moderngl + import moderngl_window from moderngl_window import geometry -import av class Decoder: @@ -46,8 +47,8 @@ def frames(self) -> int: return self.video.frames @property - def video_size(self) -> Tuple[int, int]: - """Tuple[int, int]: The width and height of the video in pixels""" + def video_size(self) -> tuple[int, int]: + """tuple[int, int]: The width and height of the video in pixels""" return self.video.width, self.video.height @property @@ -106,8 +107,8 @@ def frames(self) -> int: return self._decoder.frames @property - def video_size(self) -> Tuple[int, int]: - """Tuple[int, int]: Video size in pixels""" + def video_size(self) -> tuple[int, int]: + """tuple[int, int]: Video size in pixels""" return self._decoder.video_size @property diff --git a/examples/window_config.py b/examples/window_config.py index 434de08..fadf59a 100644 --- a/examples/window_config.py +++ b/examples/window_config.py @@ -1,4 +1,5 @@ import math + import moderngl_window diff --git a/examples/window_events.py b/examples/window_events.py index 776f4a4..ad6c25c 100644 --- a/examples/window_events.py +++ b/examples/window_events.py @@ -1,7 +1,8 @@ import math -import moderngl_window import random +import moderngl_window + class WindowEvents(moderngl_window.WindowConfig): """ diff --git a/examples/with_pyopengl.py b/examples/with_pyopengl.py index 9ecb328..a68c1e7 100644 --- a/examples/with_pyopengl.py +++ b/examples/with_pyopengl.py @@ -1,7 +1,9 @@ import math -import moderngl_window + from OpenGL import GL +import moderngl_window + class PyOpenGL(moderngl_window.WindowConfig): gl_version = (3, 3) diff --git a/moderngl_window/__init__.py b/moderngl_window/__init__.py index fe33d45..5ac1f55 100644 --- a/moderngl_window/__init__.py +++ b/moderngl_window/__init__.py @@ -8,16 +8,17 @@ import os import sys import weakref - from pathlib import Path -from typing import List, Type, Optional +from typing import Any, Optional import moderngl -from moderngl_window.context.base import WindowConfig, BaseWindow -from moderngl_window.timers.clock import Timer + from moderngl_window.conf import settings +from moderngl_window.context.base import BaseWindow, WindowConfig +from moderngl_window.timers.clock import Timer +from moderngl_window.utils.keymaps import (AZERTY, QWERTY, KeyMap, # noqa + KeyMapFactory) from moderngl_window.utils.module_loading import import_string -from moderngl_window.utils.keymaps import KeyMapFactory, KeyMap, QWERTY, AZERTY # noqa __version__ = "3.0.0" @@ -47,7 +48,7 @@ logger = logging.getLogger(__name__) -def setup_basic_logging(level: int): +def setup_basic_logging(level: int) -> None: """Set up basic logging Args: @@ -73,7 +74,7 @@ class ContextRefs: CONTEXT: Optional[moderngl.Context] = None -def activate_context(window: BaseWindow = None, ctx: moderngl.Context = None): +def activate_context(window: Optional[BaseWindow] = None, ctx: Optional[moderngl.Context] = None) -> None: """ Register the active window and context. If only a window is supplied the context is taken from the window. @@ -85,11 +86,12 @@ def activate_context(window: BaseWindow = None, ctx: moderngl.Context = None): """ ContextRefs.WINDOW = window ContextRefs.CONTEXT = ctx - if not ctx: + if ctx is None: + assert window is not None, "The window parameter can not be None if ctx is None" ContextRefs.CONTEXT = window.ctx -def window(): +def window() -> BaseWindow: """Obtain the active window""" if ContextRefs.WINDOW: return ContextRefs.WINDOW @@ -97,7 +99,7 @@ def window(): raise ValueError("No active window and context. Call activate_window.") -def ctx(): +def ctx() -> moderngl.Context: """Obtain the active context""" if ContextRefs.CONTEXT: return ContextRefs.CONTEXT @@ -105,7 +107,7 @@ def ctx(): raise ValueError("No active window and context. Call activate_window.") -def get_window_cls(window: str = None) -> Type[BaseWindow]: +def get_window_cls(window: str = "") -> type[BaseWindow]: """ Attempt to obtain a window class using the full dotted python path. This can be used to import custom or modified @@ -118,10 +120,12 @@ def get_window_cls(window: str = None) -> Type[BaseWindow]: A reference to the requested window class. Raises exception if not found. """ logger.info("Attempting to load window class: %s", window) - return import_string(window) + win = import_string(window) + assert issubclass(win, BaseWindow), f"{win} is not derived from moderngl_window.context.base.BaseWindow" + return win -def get_local_window_cls(window: str = None) -> Type[BaseWindow]: +def get_local_window_cls(window: Optional[str] = None) -> type[BaseWindow]: """ Attempt to obtain a window class in the moderngl_window package using short window names such as ``pyglet`` or ``glfw``. @@ -133,13 +137,13 @@ def get_local_window_cls(window: str = None) -> Type[BaseWindow]: A reference to the requested window class. Raises exception if not found. """ window = os.environ.get("MODERNGL_WINDOW") or window - if not window: + if window is None: window = "pyglet" return get_window_cls("moderngl_window.context.{}.Window".format(window)) -def find_window_classes() -> List[str]: +def find_window_classes() -> list[str]: """ Find available window packages Returns: @@ -167,6 +171,8 @@ def create_window_from_settings() -> BaseWindow: """ window_cls = import_string(settings.WINDOW["class"]) window = window_cls(**settings.WINDOW) + + assert isinstance(window, BaseWindow), f"{type(window)} is not derived from moderngl_window.context.base.BaseWindow" activate_context(window=window) return window @@ -174,7 +180,7 @@ def create_window_from_settings() -> BaseWindow: # --- The simple window config system --- -def run_window_config(config_cls: WindowConfig, timer=None, args=None) -> None: +def run_window_config(config_cls: type[WindowConfig], timer: Optional[Timer] = None, args: Any = None) -> None: """ Run an WindowConfig entering a blocking main loop @@ -215,7 +221,8 @@ def run_window_config(config_cls: WindowConfig, timer=None, args=None) -> None: ) window.print_context_info() activate_context(window=window) - timer = timer or Timer() + if timer is None: + timer = Timer() config = config_cls(ctx=window.ctx, wnd=window, timer=timer) # Avoid the event assigning in the property setter for now # We want the even assigning to happen in WindowConfig.__init__ @@ -249,7 +256,7 @@ def run_window_config(config_cls: WindowConfig, timer=None, args=None) -> None: logger.info("Duration: {0:.2f}s @ {1:.2f} FPS".format(duration, window.frames / duration)) -def create_parser(): +def create_parser() -> argparse.ArgumentParser: """Create an argparser parsing the standard arguments for WindowConfig""" parser = argparse.ArgumentParser() @@ -315,7 +322,7 @@ def create_parser(): return parser -def parse_args(args=None, parser=None): +def parse_args(args: Optional[Any] = None, parser: Optional[argparse.ArgumentParser] = None) -> argparse.Namespace: """Parse arguments from sys.argv Passing in your own argparser can be user to extend the parser. @@ -331,22 +338,22 @@ def parse_args(args=None, parser=None): # --- Validators --- -def valid_bool(value): +def valid_bool(value: Optional[str]) -> Optional[bool]: """Validator for bool values""" - value = value.lower() if value is None: return None + value = value.lower() if value in OPTIONS_TRUE: return True if value in OPTIONS_FALSE: return False - raise argparse.ArgumentTypeError("Boolean value expected. Options: {}".format(OPTIONS_ALL)) + raise argparse.ArgumentTypeError(f"Boolean value expected. Options: {OPTIONS_ALL}") -def valid_window_size(value): +def valid_window_size(value: str) -> tuple[int, int]: """ Validator for window size parameter. @@ -363,7 +370,7 @@ def valid_window_size(value): ) -def valid_window_size_multiplier(value): +def valid_window_size_multiplier(value: str) -> float: """Validates window size multiplier Must be an integer or float greater than 0 diff --git a/moderngl_window/atlas/base.py b/moderngl_window/atlas/base.py index 5f6db0b..82a8861 100644 --- a/moderngl_window/atlas/base.py +++ b/moderngl_window/atlas/base.py @@ -1,5 +1,6 @@ +from typing import Any + from PIL.Image import Image -from typing import Any, Tuple class BaseImage: @@ -23,11 +24,11 @@ def height(self) -> int: raise NotImplementedError @property - def size(self) -> Tuple[int, int]: - """Tuple[int, int]: Size of the image in pixels (width, height)""" + def size(self) -> tuple[int, int]: + """tuple[int, int]: Size of the image in pixels (width, height)""" raise NotImplementedError - def get_pixel_data(self, components: int = 4): + def get_pixel_data(self, components: int = 4) -> bytes: """ Get the raw pixel data from the image. @@ -44,21 +45,21 @@ class AtlasImage(BaseImage): """An atlas image using Pillow""" def __init__(self, image: Image): - self.image = image + self._image = image @property def width(self) -> int: - return self._image.size[0] + return self._image.width @property def height(self) -> int: - return self._image.size[1] + return self._image.height @property - def size(self) -> Tuple[int, int]: + def size(self) -> tuple[int, int]: return self._image.size - def get_pixel_data(self, components: int = 4): + def get_pixel_data(self, components: int = 4) -> bytes: """ Get the raw pixel data from the image. diff --git a/moderngl_window/atlas/simple_atlas.py b/moderngl_window/atlas/simple_atlas.py index 47db9ee..c474547 100644 --- a/moderngl_window/atlas/simple_atlas.py +++ b/moderngl_window/atlas/simple_atlas.py @@ -8,9 +8,8 @@ https://github.com/pythonarcade/arcade/blob/development/arcade/texture_atlas.py """ -from typing import Tuple - import moderngl + from .base import BaseImage @@ -31,7 +30,7 @@ def __init__(self, y: int, max_height: int) -> None: self.max_height = max_height self.y2 = y - def add(self, width: int, height: int) -> Tuple[int, int]: + def add(self, width: int, height: int) -> tuple[int, int]: """Add a region to the row and return the position""" if width <= 0 or height <= 0: raise AllocatorException("Cannot allocate size: [{}, {}]".format(width, height)) @@ -44,7 +43,7 @@ def add(self, width: int, height: int) -> Tuple[int, int]: self.y2 = max(self.y + height, self.y2) return x, y - def compact(self): + def compact(self) -> None: """ Compacts the row to the smallest height. Should only be done once when the row is filled before adding a new row. @@ -62,12 +61,12 @@ def __init__(self, width: int, height: int): # when a new row is added self.rows = [_Row(0, self.height)] - def alloc(self, width: int, height: int) -> Tuple[int, int]: + def alloc(self, width: int, height: int) -> tuple[int, int]: """ Allocate a region. Returns: - Tuple[int, int]: The x,y location + tuple[int, int]: The x,y location Raises: AllocatorException: if no more space """ @@ -116,13 +115,13 @@ def __init__( self._auto_resize = auto_resize # The physical size limit for the current hardware - self._max_size = self._ctx.info["GL_MAX_VIEWPORT_DIMS"] + self._max_size: tuple[int, int] = self._ctx.info["GL_MAX_VIEWPORT_DIMS"] # Atlas content self._texture = self._ctx.texture(self.size, components=self._components) # We want to be able to render into the atlas texture - self._fbo = self._fbo = self._ctx.framebuffer(color_attachments=[self._texture]) + self._fbo = self._ctx.framebuffer(color_attachments=[self._texture]) self._allocator = Allocator(width, height) @property @@ -146,25 +145,25 @@ def height(self) -> int: return self._height @property - def size(self) -> Tuple[int, int]: - """Tuple[int, int]: The size of he atlas (width, height)""" + def size(self) -> tuple[int, int]: + """tuple[int, int]: The size of he atlas (width, height)""" return self._width, self._height @property - def max_size(self) -> Tuple[int, int]: + def max_size(self) -> tuple[int, int]: """ - Tuple[int,int]: The maximum size of the atlas in pixels (x, y) + tuple[int,int]: The maximum size of the atlas in pixels (x, y) """ return self._max_size - def add(self, image: BaseImage): + def add(self, image: BaseImage) -> None: pass - def remove(self, image: BaseImage): + def remove(self, image: BaseImage) -> None: pass - def resize(self, width: int, height: int): + def resize(self, width: int, height: int) -> None: pass - def rebuild(self): + def rebuild(self) -> None: pass diff --git a/moderngl_window/capture/base.py b/moderngl_window/capture/base.py index 9f05552..3839925 100644 --- a/moderngl_window/capture/base.py +++ b/moderngl_window/capture/base.py @@ -1,8 +1,9 @@ +import datetime import os -from typing import Union +from typing import Any, Optional, Union -import datetime import moderngl + from moderngl_window.timers.clock import Timer @@ -21,28 +22,28 @@ class BaseVideoCapture: def __init__( self, - source: Union[moderngl.Texture, moderngl.Framebuffer] = None, + source: Union[moderngl.Texture, moderngl.Framebuffer], framerate: Union[int, float] = 60, ): self._source = source self._framerate = framerate - self._recording = False + self._recording: Optional[bool] = False - self._last_time: float = None - self._filename: str = None - self._width: int = None - self._height: int = None + self._last_time: float = 0.0 + self._filename: str = "" + self._width: Optional[int] = None + self._height: Optional[int] = None self._timer = Timer() - self._components: int = None # for textures + self._components: int = 0 # for textures if isinstance(self._source, moderngl.Texture): self._components = self._source.components - def _dump_frame(self, frame): + def _dump_frame(self, frame: Any) -> None: """ custom function called during self.save() @@ -59,24 +60,24 @@ def _start_func(self) -> bool: """ raise NotImplementedError("override this function") - def _release_func(self): + def _release_func(self) -> None: """ custom function called during self.release() """ raise NotImplementedError("override this function") - def _get_wh(self): + def _get_wh(self) -> tuple[int, int]: """ Return a tuple of the width and the height of the source """ return self._source.width, self._source.height - def _remove_file(self): + def _remove_file(self) -> None: """Remove the filename of the video is it exist""" if os.path.exists(self._filename): os.remove(self._filename) - def start_capture(self, filename: str = None, framerate: Union[int, float] = 60): + def start_capture(self, filename: Optional[str] = None, framerate: Union[int, float] = 60) -> None: """ Start the capturing process @@ -100,6 +101,10 @@ def start_capture(self, filename: str = None, framerate: Union[int, float] = 60) print("source type: moderngl.Texture must have at least 3 components") return + if self._source is None: + print("No source defined, there is nothing to record") + return + if not filename: now = datetime.datetime.now() filename = f"video_{now:%Y%m%d_%H%M%S}.mp4" @@ -121,12 +126,15 @@ def start_capture(self, filename: str = None, framerate: Union[int, float] = 60) self._last_time = self._timer.time self._recording = True - def save(self): + def save(self) -> None: """ Save function to call at the end of render function """ if not self._recording: return + + if self._source is None: + return dt = 1.0 / self._framerate @@ -144,7 +152,7 @@ def save(self): frame = self._source.read() self._dump_frame(frame) - def release(self): + def release(self) -> None: """ Stop the recording process """ diff --git a/moderngl_window/capture/ffmpeg.py b/moderngl_window/capture/ffmpeg.py index 159ddd0..7c991ab 100644 --- a/moderngl_window/capture/ffmpeg.py +++ b/moderngl_window/capture/ffmpeg.py @@ -1,7 +1,10 @@ -from .base import BaseVideoCapture import subprocess +from typing import Any, Optional + import moderngl +from .base import BaseVideoCapture + class FFmpegCapture(BaseVideoCapture): """ @@ -46,9 +49,9 @@ def close(self): """ - def __init__(self, **kwargs): + def __init__(self, **kwargs: Any) -> None: super().__init__(**kwargs) - self._ffmpeg = None + self._ffmpeg: Optional[subprocess.Popen[bytes]] = None def _start_func(self) -> bool: """ @@ -92,19 +95,23 @@ def _start_func(self) -> bool: self._ffmpeg = subprocess.Popen(command, stdin=subprocess.PIPE, bufsize=0) except FileNotFoundError: print("ffmpeg command not found. Be sure to add it to PATH") - return + return False return True - def _release_func(self): + def _release_func(self) -> None: """ Safely release the capture """ + if (self._ffmpeg is None) or (self._ffmpeg.stdin is None): + return self._ffmpeg.stdin.close() _ = self._ffmpeg.wait() - def _dump_frame(self, frame): + def _dump_frame(self, frame: Any) -> None: """ write the frame data in to the ffmpeg pipe """ + if (self._ffmpeg is None) or (self._ffmpeg.stdin is None): + return self._ffmpeg.stdin.write(frame) diff --git a/moderngl_window/conf/__init__.py b/moderngl_window/conf/__init__.py index ab7eebd..c3108b1 100644 --- a/moderngl_window/conf/__init__.py +++ b/moderngl_window/conf/__init__.py @@ -4,13 +4,12 @@ # pylint: disable = invalid-name import importlib -import types import os - -from collections.abc import Iterable +import pathlib +from collections.abc import Generator, Iterable from pprint import pformat -from typing import Union - +from types import ModuleType as Module +from typing import Any, Optional, Union from moderngl_window.conf import default from moderngl_window.exceptions import ImproperlyConfigured @@ -44,7 +43,7 @@ class Settings: print(settings) """ - WINDOW = dict() + WINDOW: dict[str, Any] = dict() """ Window/screen properties. Most importantly the ``class`` attribute decides what class should be used to handle the window. @@ -89,14 +88,14 @@ class Settings: - color and depth buffer is cleared for every frame """ - SCREENSHOT_PATH = None + SCREENSHOT_PATH: Optional[str] = None """ Absolute path to the directory screenshots will be saved by the screenshot module. Screenshots will end up in the project root of not defined. If a path is configured, the directory will be auto-created. """ # Finders - PROGRAM_FINDERS = [] + PROGRAM_FINDERS: list[str] = [] """ Finder classes for locating programs/shaders. @@ -107,7 +106,7 @@ class Settings: "moderngl_window.finders.program.FileSystemFinder", ] """ - TEXTURE_FINDERS = [] + TEXTURE_FINDERS: list[str] = [] """ Finder classes for locating textures. @@ -118,7 +117,7 @@ class Settings: "moderngl_window.finders.texture.FileSystemFinder", ] """ - SCENE_FINDERS = [] + SCENE_FINDERS: list[str] = [] """ Finder classes for locating scenes. @@ -130,7 +129,7 @@ class Settings: ] """ - DATA_FINDERS = [] + DATA_FINDERS: list[str] = [] """ Finder classes for locating data files. @@ -142,29 +141,29 @@ class Settings: ] """ # Finder dirs - PROGRAM_DIRS = [] + PROGRAM_DIRS: list[Union[str, pathlib.Path]] = [] """ Lists of `str` or `pathlib.Path` used by ``FileSystemFinder`` to looks for programs/shaders. """ - TEXTURE_DIRS = [] + TEXTURE_DIRS: list[Union[str, pathlib.Path]] = [] """ Lists of `str` or `pathlib.Path` used by ``FileSystemFinder`` to looks for textures. """ - SCENE_DIRS = [] + SCENE_DIRS: list[Union[str, pathlib.Path]] = [] """ Lists of `str` or `pathlib.Path` used by ``FileSystemFinder`` to looks for scenes (obj, gltf, stl etc). """ - DATA_DIRS = [] + DATA_DIRS: list[Union[str, pathlib.Path]] = [] """ Lists of `str` or `pathlib.Path` used by ``FileSystemFinder`` to looks for data files. """ # Loaders - PROGRAM_LOADERS = [] + PROGRAM_LOADERS: list[str] = [] """ Classes responsible for loading programs/shaders. @@ -176,7 +175,7 @@ class Settings: 'moderngl_window.loaders.program.separate.Loader', ] """ - TEXTURE_LOADERS = [] + TEXTURE_LOADERS: list[str] = [] """ Classes responsible for loading textures. @@ -188,7 +187,7 @@ class Settings: 'moderngl_window.loaders.texture.array.Loader', ] """ - SCENE_LOADERS = [] + SCENE_LOADERS: list[str] = [] """ Classes responsible for loading scenes. @@ -202,7 +201,7 @@ class Settings: ] """ - DATA_LOADERS = [] + DATA_LOADERS: list[str] = [] """ Classes responsible for loading data files. @@ -216,7 +215,7 @@ class Settings: ] """ - def __init__(self): + def __init__(self) -> None: """Initialize settings with default values""" self.apply_default_settings() @@ -277,7 +276,7 @@ def apply_from_module_name(self, settings_module_name: str) -> None: self.apply_from_module(module) - def apply_from_dict(self, data: dict) -> None: + def apply_from_dict(self, data: dict[str, Any]) -> None: """ Apply settings values from a dictionary @@ -290,7 +289,7 @@ def apply_from_dict(self, data: dict) -> None: """ self.apply_from_iterable(data.items()) - def apply_from_module(self, module: types.ModuleType) -> None: + def apply_from_module(self, module: Module) -> None: """ Apply settings values from a python module @@ -307,7 +306,7 @@ def apply_from_module(self, module: types.ModuleType) -> None: """ self.apply_from_iterable(module.__dict__.items()) - def apply_from_cls(self, cls) -> None: + def apply_from_cls(self, cls: Any) -> None: """ Apply settings values from a class namespace @@ -323,11 +322,11 @@ def apply_from_cls(self, cls) -> None: """ self.apply_from_iterable(cls.__dict__.items()) - def apply_from_iterable(self, iterable: Union[Iterable, types.GeneratorType]) -> None: + def apply_from_iterable(self, iterable: Union[Iterable[Any], Generator[Any]]) -> None: """ Apply (key, value) pairs from an interable or generator """ - if not isinstance(iterable, Iterable) and not isinstance(self, types.GeneratorType): + if not isinstance(iterable, Iterable) and not isinstance(self, Generator): raise ValueError( "Input value is not a generator or interable, but of type: {}".format( type(iterable) @@ -338,7 +337,7 @@ def apply_from_iterable(self, iterable: Union[Iterable, types.GeneratorType]) -> if name.isupper(): setattr(self, name, value) - def to_dict(self): + def to_dict(self) -> dict[str, Any]: """Create a dict representation of the settings Only uppercase attributes are included diff --git a/moderngl_window/conf/default.py b/moderngl_window/conf/default.py index 7b83602..74ada05 100644 --- a/moderngl_window/conf/default.py +++ b/moderngl_window/conf/default.py @@ -32,10 +32,10 @@ ] # Finder directories: Where finders look for their resources -PROGRAM_DIRS = [] -TEXTURE_DIRS = [] -SCENE_DIRS = [] -DATA_DIRS = [] +PROGRAM_DIRS: list[str] = [] +TEXTURE_DIRS: list[str] = [] +SCENE_DIRS: list[str] = [] +DATA_DIRS: list[str] = [] # Loaders diff --git a/moderngl_window/context/base/__init__.py b/moderngl_window/context/base/__init__.py index 2a94eb6..c2fec8d 100644 --- a/moderngl_window/context/base/__init__.py +++ b/moderngl_window/context/base/__init__.py @@ -1,2 +1,7 @@ -from moderngl_window.context.base.keys import BaseKeys, KeyModifiers # noqa -from moderngl_window.context.base.window import BaseWindow, WindowConfig # noqa +from moderngl_window.context.base.keys import BaseKeys as BaseKeys # noqa +from moderngl_window.context.base.keys import \ + KeyModifiers as KeyModifiers # noqa +from moderngl_window.context.base.window import \ + BaseWindow as BaseWindow # noqa +from moderngl_window.context.base.window import \ + WindowConfig as WindowConfig # noqa diff --git a/moderngl_window/context/base/keys.py b/moderngl_window/context/base/keys.py index 943a815..5fa9788 100644 --- a/moderngl_window/context/base/keys.py +++ b/moderngl_window/context/base/keys.py @@ -9,10 +9,10 @@ class KeyModifiers: ctrl: Any = False alt: Any = False - def __repr__(self): + def __repr__(self) -> str: return str(self) - def __str__(self): + def __str__(self) -> str: return "".format(self.shift, self.ctrl, self.alt) diff --git a/moderngl_window/context/base/window.py b/moderngl_window/context/base/window.py index ab79035..e053198 100644 --- a/moderngl_window/context/base/window.py +++ b/moderngl_window/context/base/window.py @@ -1,33 +1,37 @@ -from argparse import ArgumentParser -from functools import wraps -from pathlib import Path import logging import sys import weakref -from typing import Any, Tuple, Type, List, Optional, Dict +from argparse import ArgumentParser, Namespace +from functools import wraps +from pathlib import Path +from typing import Any, Callable, Optional, Union import moderngl -from moderngl_window.context.base import KeyModifiers, BaseKeys -from moderngl_window.timers.base import BaseTimer + from moderngl_window import resources +from moderngl_window.context.base import BaseKeys, KeyModifiers from moderngl_window.geometry.attributes import AttributeNames -from moderngl_window.meta import ( - TextureDescription, - ProgramDescription, - SceneDescription, - DataDescription, -) from moderngl_window.loaders.texture.icon import IconLoader +from moderngl_window.meta import (DataDescription, ProgramDescription, + SceneDescription, TextureDescription) from moderngl_window.scene import Scene +from moderngl_window.timers.base import BaseTimer + +try: + from pygame.event import Event +except ModuleNotFoundError: + Event = Any + +FuncAny = Callable[[Any], Any] logger = logging.getLogger(__name__) -def require_callable(func): +def require_callable(func: Callable[[Any], Any]) -> Callable[[Any], Any]: """Decorator ensuring assigned callbacks are valid callables""" @wraps(func) - def wrapper(*args, **kwargs): + def wrapper(*args: Any, **kwargs: Any) -> Any: if not callable(args[1]): raise ValueError("{} is not a callable".format(args[1])) return func(*args, **kwargs) @@ -51,7 +55,7 @@ class MouseButtonStates: middle = False @property - def any(self): + def any(self) -> bool: """bool: if any mouse buttons are pressed""" return self.left or self.right or self.middle @@ -78,19 +82,19 @@ class BaseWindow: def __init__( self, - title="ModernGL", - gl_version=(3, 3), - size=(1280, 720), - resizable=True, - visible=True, - fullscreen=False, - vsync=True, - aspect_ratio: float = None, - samples=0, - cursor=True, + title: str="ModernGL", + gl_version: tuple[int, int] = (3, 3), + size: tuple[int, int] = (1280, 720), + resizable: bool= True, + visible: bool = True, + fullscreen: bool = False, + vsync: bool = True, + aspect_ratio: Optional[float] = None, + samples: int = 0, + cursor: bool = True, backend: Optional[str] = None, - **kwargs, - ): + **kwargs: Any, + ) -> None: """Initialize a window instance. Args: @@ -125,28 +129,28 @@ def __init__( self._fs_key = self.keys.F11 # Callback functions - self._render_func = dummy_func - self._resize_func = dummy_func - self._close_func = dummy_func - self._iconify_func = dummy_func - self._key_event_func = dummy_func - self._mouse_position_event_func = dummy_func - self._mouse_press_event_func = dummy_func - self._mouse_release_event_func = dummy_func - self._mouse_drag_event_func = dummy_func - self._mouse_scroll_event_func = dummy_func - self._unicode_char_entered_func = dummy_func - self._files_dropped_event_func = dummy_func - self._on_generic_event_func = dummy_func + self._render_func: Callable[[float, float], None] = dummy_func + self._resize_func: Callable[[int, int], None] = dummy_func + self._close_func: Callable[[], None] = dummy_func + self._iconify_func: Callable[[bool], None] = dummy_func + self._key_event_func: Callable[[Union[str, int], int, KeyModifiers], None] = dummy_func + self._mouse_position_event_func: Callable[[int, int, int, int], None] = dummy_func + self._mouse_press_event_func: Callable[[int, int, int], None] = dummy_func + self._mouse_release_event_func: Callable[[int, int, int], None] = dummy_func + self._mouse_drag_event_func: Callable[[int, int, int, int], None] = dummy_func + self._mouse_scroll_event_func: Callable[[float, float], None] = dummy_func + self._unicode_char_entered_func: Callable[[str], None] = dummy_func + self._files_dropped_event_func: Callable[[int, int, list[Union[str, Path]]], None] = dummy_func + self._on_generic_event_func: Callable[[Event], None] = dummy_func # Internal states - self._ctx = None # type: moderngl.Context - self._viewport = None + self._ctx: moderngl.Context + self._viewport: tuple[int, int, int, int] = (0, 0, 0, 0) self._position = 0, 0 self._frames = 0 # Frame counter self._close = False - self._config = None - self._key_pressed_map = {} + self._config: Optional[weakref.ReferenceType["WindowConfig"]] = None + self._key_pressed_map: dict[Union[str, int], bool] = {} self._modifiers = KeyModifiers() self._mouse_buttons = MouseButtonStates() # Manual tracking of mouse position used by some windows @@ -157,7 +161,7 @@ def __init__( if self._fullscreen: self._resizable = False - if not self.keys: + if self.keys is None: raise ValueError("Window class {} missing keys attribute".format(self.__class__)) def init_mgl_context(self) -> None: @@ -210,7 +214,7 @@ def title(self) -> str: return self._title @title.setter - def title(self, value: str): + def title(self, value: str) -> None: self._title = value @property @@ -233,7 +237,7 @@ def fullscreen_key(self) -> Any: return self._fs_key @fullscreen_key.setter - def fullscreen_key(self, value: Any): + def fullscreen_key(self, value: Any) -> None: self._fs_key = value @property @@ -256,12 +260,12 @@ def exit_key(self) -> Any: return self._exit_key @exit_key.setter - def exit_key(self, value: Any): + def exit_key(self, value: Any) -> None: self._exit_key = value @property - def gl_version(self) -> Tuple[int, int]: - """Tuple[int, int]: (major, minor) required OpenGL version""" + def gl_version(self) -> tuple[int, int]: + """tuple[int, int]: (major, minor) required OpenGL version""" return self._gl_version @property @@ -275,8 +279,8 @@ def height(self) -> int: return self._height @property - def size(self) -> Tuple[int, int]: - """Tuple[int, int]: current window size. + def size(self) -> tuple[int, int]: + """tuple[int, int]: current window size. This property also support assignment:: @@ -286,7 +290,7 @@ def size(self) -> Tuple[int, int]: return self._width, self._height @size.setter - def size(self, value: Tuple[int, int]): + def size(self, value: tuple[int, int]) -> None: self._width, self._height = int(value[0]), int(value[1]) @property @@ -300,13 +304,13 @@ def buffer_height(self) -> int: return self._buffer_height @property - def buffer_size(self) -> Tuple[int, int]: - """Tuple[int, int]: tuple with the current window buffer size""" + def buffer_size(self) -> tuple[int, int]: + """tuple[int, int]: tuple with the current window buffer size""" return self._buffer_width, self._buffer_height @property - def position(self) -> Tuple[int, int]: - """Tuple[int, int]: The current window position. + def position(self) -> tuple[int, int]: + """tuple[int, int]: The current window position. This property can also be set to move the window:: @@ -316,7 +320,7 @@ def position(self) -> Tuple[int, int]: return self._position @position.setter - def position(self, value: Tuple[int, int]): + def position(self, value: tuple[int, int]) -> None: self._position = int(value[0]), int(value[1]) @property @@ -325,13 +329,13 @@ def pixel_ratio(self) -> float: return self.buffer_size[0] / self.size[0] @property - def viewport(self) -> Tuple[int, int, int, int]: - """Tuple[int, int, int, int]: current window viewport""" + def viewport(self) -> tuple[int, int, int, int]: + """tuple[int, int, int, int]: current window viewport""" return self._viewport @property - def viewport_size(self) -> Tuple[int, int]: - """Tuple[int,int]: Size of the viewport. + def viewport_size(self) -> tuple[int, int]: + """tuple[int,int]: Size of the viewport. Equivalent to ``self.viewport[2], self.viewport[3]`` """ @@ -404,7 +408,7 @@ def fullscreen(self, value: bool) -> None: self._fullscreen = value @property - def config(self) -> "WindowConfig": + def config(self) -> Optional["WindowConfig"]: """Get the current WindowConfig instance DEPRECATED PROPERTY. This is not handled in `WindowConfig.__init__` @@ -421,7 +425,7 @@ def config(self) -> "WindowConfig": return None @config.setter - def config(self, config): + def config(self, config: "WindowConfig") -> None: config.assign_event_callbacks() self._config = weakref.ref(config) @@ -431,7 +435,7 @@ def vsync(self) -> bool: return self._vsync @vsync.setter - def vsync(self, value: bool): + def vsync(self, value: bool) -> None: self._set_vsync(value) self._vsync = value @@ -450,7 +454,7 @@ def aspect_ratio(self) -> float: return self.width / self.height @property - def fixed_aspect_ratio(self): + def fixed_aspect_ratio(self) -> Optional[float]: """float: The fixed aspect ratio for the window. Can be set to ``None`` to disable fixed aspect ratio @@ -468,11 +472,11 @@ def fixed_aspect_ratio(self): return self._fixed_aspect_ratio @fixed_aspect_ratio.setter - def fixed_aspect_ratio(self, value: float): + def fixed_aspect_ratio(self, value: float) -> None: self._fixed_aspect_ratio = value @property - def samples(self) -> float: + def samples(self) -> int: """float: Number of Multisample anti-aliasing (MSAA) samples""" return self._samples @@ -488,7 +492,7 @@ def cursor(self) -> bool: return self._cursor @cursor.setter - def cursor(self, value: bool): + def cursor(self, value: bool) -> None: self._cursor = value @property @@ -508,11 +512,11 @@ def mouse_exclusivity(self) -> bool: return self._mouse_exclusivity @mouse_exclusivity.setter - def mouse_exclusivity(self, value: bool): + def mouse_exclusivity(self, value: bool) -> None: self._mouse_exclusivity = value @property - def render_func(self): + def render_func(self) -> Callable[[float, float], None]: """callable: The render callable This property can also be used to assign a callable. @@ -521,121 +525,121 @@ def render_func(self): @render_func.setter @require_callable - def render_func(self, func): + def render_func(self, func: Callable[[float, float], None]) -> None: self._render_func = func @property - def resize_func(self): + def resize_func(self) -> Callable[[int, int], None]: """callable: Get or set the resize callable""" return self._resize_func @resize_func.setter @require_callable - def resize_func(self, func): + def resize_func(self, func: Callable[[int, int], None]) -> None: self._resize_func = func @property - def close_func(self): + def close_func(self) -> Callable[[], None]: """callable: Get or set the close callable""" return self._close_func - @property - def files_dropped_event_func(self): - """callable: Get or set the files_dropped callable""" - return self._files_dropped_event_func - @close_func.setter @require_callable - def close_func(self, func): + def close_func(self, func: Callable[[], None]) -> None: self._close_func = func + @property + def files_dropped_event_func(self) -> Callable[[int, int, list[Union[str, Path]]], None]: + """callable: Get or set the files_dropped callable""" + return self._files_dropped_event_func + @files_dropped_event_func.setter @require_callable - def files_dropped_event_func(self, func): + def files_dropped_event_func(self, func: Callable[[int, int, list[Union[str, Path]]], None]) -> None: self._files_dropped_event_func = func @property - def iconify_func(self): + def iconify_func(self) -> Callable[[bool], None]: """callable: Get or set ehe iconify/show/hide callable""" return self._iconify_func @iconify_func.setter @require_callable - def iconify_func(self, func): + def iconify_func(self, func: Callable[[bool], None]) -> None: self._iconify_func = func @property - def key_event_func(self): + def key_event_func(self) -> Callable[[Union[str, int], int, KeyModifiers], None]: """callable: Get or set the key_event callable""" return self._key_event_func @key_event_func.setter @require_callable - def key_event_func(self, func): + def key_event_func(self, func: Callable[[Union[str, int], int, KeyModifiers], None]) -> None: self._key_event_func = func @property - def mouse_position_event_func(self): + def mouse_position_event_func(self) -> Callable[[int, int, int, int], None]: """callable: Get or set the mouse_position callable""" return self._mouse_position_event_func @mouse_position_event_func.setter @require_callable - def mouse_position_event_func(self, func): + def mouse_position_event_func(self, func:Callable[[int, int, int, int], None]) -> None: self._mouse_position_event_func = func @property - def mouse_drag_event_func(self): + def mouse_drag_event_func(self) -> Callable[[int, int, int, int], None]: """callable: Get or set the mouse_drag callable""" return self._mouse_drag_event_func @mouse_drag_event_func.setter @require_callable - def mouse_drag_event_func(self, func): + def mouse_drag_event_func(self, func: Callable[[int, int, int, int], None]) -> None: self._mouse_drag_event_func = func @property - def mouse_press_event_func(self): + def mouse_press_event_func(self) -> Callable[[int, int, int], None]: """callable: Get or set the mouse_press callable""" return self._mouse_press_event_func @mouse_press_event_func.setter @require_callable - def mouse_press_event_func(self, func): + def mouse_press_event_func(self, func: Callable[[int, int, int], None]) -> None: self._mouse_press_event_func = func @property - def mouse_release_event_func(self): + def mouse_release_event_func(self) -> Callable[[int, int, int], None]: """callable: Get or set the mouse_release callable""" return self._mouse_release_event_func @mouse_release_event_func.setter @require_callable - def mouse_release_event_func(self, func): + def mouse_release_event_func(self, func: Callable[[int, int, int], None]) -> None: self._mouse_release_event_func = func @property - def unicode_char_entered_func(self): + def unicode_char_entered_func(self) -> Callable[[str], None]: """callable: Get or set the unicode_char_entered callable""" return self._unicode_char_entered_func @unicode_char_entered_func.setter @require_callable - def unicode_char_entered_func(self, func): + def unicode_char_entered_func(self, func: Callable[[str], None]) -> None: self._unicode_char_entered_func = func @property - def mouse_scroll_event_func(self): + def mouse_scroll_event_func(self) -> Callable[[float, float], None]: """callable: Get or set the mouse_scroll_event calable""" return self._mouse_scroll_event_func @mouse_scroll_event_func.setter @require_callable - def mouse_scroll_event_func(self, func): + def mouse_scroll_event_func(self, func: Callable[[float, float], None]) -> None: self._mouse_scroll_event_func = func @property - def modifiers(self) -> Type[KeyModifiers]: + def modifiers(self) -> KeyModifiers: """(KeyModifiers) The current keyboard modifiers""" return self._modifiers @@ -653,7 +657,7 @@ def mouse_states(self) -> MouseButtonStates: """ return self._mouse_buttons - def _handle_mouse_button_state_change(self, button: int, pressed: bool): + def _handle_mouse_button_state_change(self, button: int, pressed: bool) -> None: """Updates the internal mouse button state object. Args: @@ -669,7 +673,7 @@ def _handle_mouse_button_state_change(self, button: int, pressed: bool): else: raise ValueError("Incompatible mouse button number: {}".format(button)) - def convert_window_coordinates(self, x, y, x_flipped=False, y_flipped=False): + def convert_window_coordinates(self, x: int, y: int, x_flipped: bool = False, y_flipped: bool = False) -> tuple[int, int]: """ Convert window coordinates to top-left coordinate space. The default origin is the top left corner of the window. @@ -690,7 +694,7 @@ def convert_window_coordinates(self, x, y, x_flipped=False, y_flipped=False): else: return (self.width - x, self.height - y) - def is_key_pressed(self, key) -> bool: + def is_key_pressed(self, key: str) -> bool: """Returns: The press state of a key""" return self._key_pressed_map.get(key) is True @@ -700,7 +704,7 @@ def is_closing(self) -> bool: return self._close @is_closing.setter - def is_closing(self, value: bool): + def is_closing(self, value: bool) -> None: self._close = value def close(self) -> None: @@ -708,11 +712,11 @@ def close(self) -> None: self.is_closing = True self.close_func() - def use(self): + def use(self) -> None: """Bind the window's framebuffer""" self._ctx.screen.use() - def clear(self, red=0.0, green=0.0, blue=0.0, alpha=0.0, depth=1.0, viewport=None): + def clear(self, red: float =0.0, green: float=0.0, blue: float=0.0, alpha: float=0.0, depth: float =1.0, viewport: Optional[tuple[int, int, int, int]]=None) -> None: """ Binds and clears the default framebuffer @@ -729,7 +733,7 @@ def clear(self, red=0.0, green=0.0, blue=0.0, alpha=0.0, depth=1.0, viewport=Non red=red, green=green, blue=blue, alpha=alpha, depth=depth, viewport=viewport ) - def render(self, time=0.0, frame_time=0.0) -> None: + def render(self, time: float = 0.0, frame_time: float = 0.0) -> None: """ Renders a frame by calling the configured render callback @@ -745,12 +749,12 @@ def swap_buffers(self) -> None: """ raise NotImplementedError() - def resize(self, width, height) -> None: + def resize(self, width: int, height: int) -> None: """ Should be called every time window is resized so the example can adapt to the new size if needed """ - if self._resize_func: + if self._resize_func is not dummy_func: self._resize_func(width, height) def set_icon(self, icon_path: str) -> None: @@ -764,7 +768,7 @@ def set_icon(self, icon_path: str) -> None: resolved_path = loader.find_icon() self._set_icon(resolved_path) - def _set_icon(self, icon_path: str) -> None: + def _set_icon(self, icon_path: Path) -> None: """ A library specific destroy method is required. """ @@ -832,7 +836,7 @@ def gl_version_code(self) -> int: """ return self.gl_version[0] * 100 + self.gl_version[1] * 10 - def print_context_info(self): + def print_context_info(self) -> None: """Prints moderngl context info.""" logger.info("Context Version:") logger.info("ModernGL: %s", moderngl.__version__) @@ -848,21 +852,21 @@ def print_context_info(self): if err != "GL_NO_ERROR": logger.warning("glerror consumed after getting context info: %s", err) - def _calc_mouse_delta(self, xpos: int, ypos: int) -> Tuple[int, int]: + def _calc_mouse_delta(self, xpos: int, ypos: int) -> tuple[int, int]: """Calculates the mouse position delta for events not support this. Args: xpos (int): current mouse x ypos (int): current mouse y Returns: - Tuple[int, int]: The x, y delta values + tuple[int, int]: The x, y delta values """ dx, dy = xpos - self._mouse_pos[0], ypos - self._mouse_pos[1] self._mouse_pos = xpos, ypos return dx, dy @property - def on_generic_event_func(self): + def on_generic_event_func(self) -> Union[Callable[[int, int, int, int], None], Callable[[Event], None]]: """ callable: Get or set the on_generic_event callable used to funnel all non-processed events @@ -871,7 +875,7 @@ def on_generic_event_func(self): @on_generic_event_func.setter @require_callable - def on_generic_event_func(self, func): + def on_generic_event_func(self, func: Callable[[Event], None]) -> None: self._on_generic_event_func = func @@ -1049,18 +1053,18 @@ def key_event(self, key, action, modifiers): # Default value log_level = logging.INFO """ - argv = None # type: Namespace + argv: Optional[Namespace] = None """ The parsed command line arguments. """ def __init__( self, - ctx: moderngl.Context = None, - wnd: BaseWindow = None, - timer: BaseTimer = None, - **kwargs, - ): + ctx: Optional[moderngl.Context] = None, + wnd: Optional[BaseWindow] = None, + timer: Optional[BaseTimer] = None, + **kwargs: Any, + ) -> None: """Initialize the window config Keyword Args: @@ -1068,22 +1072,22 @@ def __init__( wnd: The window instance timer: The timer instance """ - self.ctx = ctx - self.wnd = wnd - self.timer = timer - if self.resource_dir: resources.register_dir(Path(self.resource_dir).resolve()) - if not self.ctx or not isinstance(self.ctx, moderngl.Context): - raise ValueError("WindowConfig requires a moderngl context. ctx={}".format(self.ctx)) + if ctx is None or not isinstance(ctx, moderngl.Context): + raise ValueError("WindowConfig requires a moderngl context. ctx={}".format(ctx)) - if not self.wnd or not isinstance(self.wnd, BaseWindow): - raise ValueError("WindowConfig requires a window. wnd={}".format(self.wnd)) + if wnd is None or not isinstance(wnd, BaseWindow): + raise ValueError("WindowConfig requires a window. wnd={}".format(wnd)) + + self.ctx = ctx + self.wnd = wnd + self.timer = timer self.assign_event_callbacks() - def assign_event_callbacks(self): + def assign_event_callbacks(self) -> None: """ Look for methods in the class instance and assign them to callbacks. This method is call by ``__init__``. @@ -1103,7 +1107,7 @@ def assign_event_callbacks(self): self.wnd.files_dropped_event_func = getattr(self, "files_dropped_event", dummy_func) @classmethod - def run(cls): + def run(cls: type["WindowConfig"]) -> None: """Shortcut for running a ``WindowConfig``. This executes the following code:: @@ -1116,7 +1120,7 @@ def run(cls): moderngl_window.run_window_config(cls) @classmethod - def add_arguments(cls, parser: ArgumentParser): + def add_arguments(cls: type["WindowConfig"], parser: ArgumentParser) -> None: """Add arguments to default argument parser. Add arguments using ``add_argument(..)``. @@ -1125,7 +1129,7 @@ def add_arguments(cls, parser: ArgumentParser): """ pass - def render(self, time: float, frame_time: float): + def render(self, time: float, frame_time: float) -> None: """Renders the assigned effect Args: @@ -1134,7 +1138,7 @@ def render(self, time: float, frame_time: float): """ raise NotImplementedError("WindowConfig.render not implemented") - def resize(self, width: int, height: int): + def resize(self, width: int, height: int) -> None: """ Called every time the window is resized in case the we need to do internal adjustments. @@ -1144,10 +1148,10 @@ def resize(self, width: int, height: int): height (int): height in buffer size (not window size) """ - def close(self): + def close(self) -> None: """Called when the window is closed""" - def files_dropped_event(self, x: int, y: int, paths: List[str]): + def files_dropped_event(self, x: int, y: int, paths: list[str]) -> None: """ Called when files dropped onto the window @@ -1157,7 +1161,7 @@ def files_dropped_event(self, x: int, y: int, paths: List[str]): paths (list): List of file paths dropped """ - def iconify(self, iconified: bool): + def iconify(self, iconified: bool) -> None: """ Called when the window is minimized/iconified or restored from this state @@ -1166,7 +1170,7 @@ def iconify(self, iconified: bool): iconified (bool): If ``True`` the window is iconified/minimized. Otherwise restored. """ - def key_event(self, key: Any, action: Any, modifiers: KeyModifiers): + def key_event(self, key: Any, action: Any, modifiers: KeyModifiers) -> None: """ Called for every key press and release. Depending on the library used, key events may @@ -1180,7 +1184,7 @@ def key_event(self, key: Any, action: Any, modifiers: KeyModifiers): modifiers: Modifier state for shift, ctrl and alt """ - def mouse_position_event(self, x: int, y: int, dx: int, dy: int): + def mouse_position_event(self, x: int, y: int, dx: int, dy: int) -> None: """Reports the current mouse cursor position in the window Args: @@ -1190,7 +1194,7 @@ def mouse_position_event(self, x: int, y: int, dx: int, dy: int): dy (int): Y delta position """ - def mouse_drag_event(self, x: int, y: int, dx: int, dy: int): + def mouse_drag_event(self, x: int, y: int, dx: int, dy: int) -> None: """Called when the mouse is moved while a button is pressed. Args: @@ -1200,7 +1204,7 @@ def mouse_drag_event(self, x: int, y: int, dx: int, dy: int): dy (int): Y delta position """ - def mouse_press_event(self, x: int, y: int, button: int): + def mouse_press_event(self, x: int, y: int, button: int) -> None: """Called when a mouse button in pressed Args: @@ -1209,7 +1213,7 @@ def mouse_press_event(self, x: int, y: int, button: int): button (int): 1 = Left button, 2 = right button """ - def mouse_release_event(self, x: int, y: int, button: int): + def mouse_release_event(self, x: int, y: int, button: int) -> None: """Called when a mouse button in released Args: @@ -1218,7 +1222,7 @@ def mouse_release_event(self, x: int, y: int, button: int): button (int): 1 = Left button, 2 = right button """ - def mouse_scroll_event(self, x_offset: float, y_offset: float): + def mouse_scroll_event(self, x_offset: float, y_offset: float) -> None: """Called when the mouse wheel is scrolled. Some input devices also support horizontal scrolling, @@ -1229,7 +1233,7 @@ def mouse_scroll_event(self, x_offset: float, y_offset: float): y_offset (int): Y scroll offset """ - def unicode_char_entered(self, char: str): + def unicode_char_entered(self, char: str) -> None: """Called when the user entered a unicode character. Args: @@ -1239,13 +1243,13 @@ def unicode_char_entered(self, char: str): def load_texture_2d( self, path: str, - flip=True, - flip_x=False, - flip_y=True, - mipmap=False, - mipmap_levels: Optional[Tuple[int, int]] = None, - anisotropy=1.0, - **kwargs, + flip: bool = True, + flip_x: bool = False, + flip_y: bool = True, + mipmap: bool = False, + mipmap_levels: Optional[tuple[int, int]] = None, + anisotropy: float = 1.0, + **kwargs: Any, ) -> moderngl.Texture: """Loads a 2D texture. @@ -1268,7 +1272,7 @@ def load_texture_2d( Returns: moderngl.Texture: Texture instance """ - return resources.textures.load( + texture = resources.textures.load( TextureDescription( path=path, flip=flip, @@ -1280,16 +1284,18 @@ def load_texture_2d( **kwargs, ) ) + assert isinstance(texture, moderngl.Texture), f"There was an error when loading the texture, {type(texture)} is not a moderngl.Texture" + return texture def load_texture_array( self, path: str, layers: int = 0, - flip=True, - mipmap=False, - mipmap_levels: Optional[Tuple[int, int]] = None, - anisotropy=1.0, - **kwargs, + flip: bool = True, + mipmap: bool = False, + mipmap_levels: Optional[tuple[int, int]] = None, + anisotropy: float = 1.0, + **kwargs: Any, ) -> moderngl.TextureArray: """Loads a texture array. @@ -1312,13 +1318,13 @@ def load_texture_array( Returns: moderngl.TextureArray: The texture instance """ - if not kwargs: + if kwargs is None: kwargs = {} if "kind" not in kwargs: kwargs["kind"] = "array" - return resources.textures.load( + texture = resources.textures.load( TextureDescription( path=path, layers=layers, @@ -1329,6 +1335,8 @@ def load_texture_array( **kwargs, ) ) + assert isinstance(texture, moderngl.TextureArray), f"There was an error when loading the texture, {type(texture)} is not a moderngl.TextureArray" + return texture def load_texture_cube( self, @@ -1338,13 +1346,13 @@ def load_texture_cube( neg_x: str = "", neg_y: str = "", neg_z: str = "", - flip=False, - flip_x=False, - flip_y=False, - mipmap=False, - mipmap_levels: Optional[Tuple[int, int]] = None, - anisotropy=1.0, - **kwargs, + flip : bool = False, + flip_x: bool = False, + flip_y: bool = False, + mipmap: bool = False, + mipmap_levels: Optional[tuple[int, int]] = None, + anisotropy: float = 1.0, + **kwargs: Any, ) -> moderngl.TextureCube: """Loads a texture cube. @@ -1371,7 +1379,7 @@ def load_texture_cube( Returns: moderngl.TextureCube: Texture instance """ - return resources.textures.load( + texture = resources.textures.load( TextureDescription( pos_x=pos_x, pos_y=pos_y, @@ -1390,16 +1398,19 @@ def load_texture_cube( ) ) + assert isinstance(texture, moderngl.TextureCube), f"There was an error when loading the texture, {type(texture)} is not a moderngl.TextureCube" + return texture + def load_program( self, - path=None, - vertex_shader=None, - geometry_shader=None, - fragment_shader=None, - tess_control_shader=None, - tess_evaluation_shader=None, - defines: Optional[dict] = None, - varyings: Optional[List[str]] = None, + path: Optional[str] = None, + vertex_shader: Optional[str] = None, + geometry_shader: Optional[str] = None, + fragment_shader: Optional[str] = None, + tess_control_shader: Optional[str] = None, + tess_evaluation_shader: Optional[str] = None, + defines: Optional[dict[str, Any]] = None, + varyings: Optional[list[str]] = None, ) -> moderngl.Program: """Loads a shader program. @@ -1419,11 +1430,11 @@ def load_program( tess_evaluation_shader (str): Path to tessellation eval shader defines (dict): ``#define`` values to replace in the shader source. Example: ``{'VALUE1': 10, 'VALUE2': '3.1415'}``. - varyings (List[str]): Out attribute names for transform shaders + varyings (list[str]): Out attribute names for transform shaders Returns: moderngl.Program: The program instance """ - return resources.programs.load( + prog = resources.programs.load( ProgramDescription( path=path, vertex_shader=vertex_shader, @@ -1436,8 +1447,11 @@ def load_program( ) ) + assert isinstance(prog, moderngl.Program), f"There was an error when loading the program, {type(prog)} is not a moderngl.Program" + return prog + def load_compute_shader( - self, path, defines: Optional[Dict] = None, **kwargs + self, path: str, defines: Optional[dict[str, Any]] = None, **kwargs: Any ) -> moderngl.ComputeShader: """Loads a compute shader. @@ -1448,11 +1462,14 @@ def load_compute_shader( Returns: moderngl.ComputeShader: The compute shader """ - return resources.programs.load( + shader = resources.programs.load( ProgramDescription(compute_shader=path, defines=defines, **kwargs) ) - def load_text(self, path: str, **kwargs) -> str: + assert isinstance(shader, moderngl.ComputeShader), f"There was an error when loading the shader, {type(shader)} is not a moderngl.ComputeShader" + return shader + + def load_text(self, path: str, **kwargs: Any) -> str: """Load a text file. If the path is relative the resource system is used expecting one or more @@ -1465,15 +1482,18 @@ def load_text(self, path: str, **kwargs) -> str: Returns: str: Contents of the text file """ - if not kwargs: + if kwargs is None: kwargs = {} if "kind" not in kwargs: kwargs["kind"] = "text" - return resources.data.load(DataDescription(path=path, **kwargs)) + text = resources.data.load(DataDescription(path=path, **kwargs)) - def load_json(self, path: str, **kwargs) -> dict: + assert isinstance(text, str), f"There was an error when loading the text, {type(text)} is not a string" + return text + + def load_json(self, path: str, **kwargs: Any) -> dict[str, Any]: """Load a json file If the path is relative the resource system is used expecting one or more @@ -1486,15 +1506,18 @@ def load_json(self, path: str, **kwargs) -> dict: Returns: dict: Contents of the json file """ - if not kwargs: + if kwargs is not None: kwargs = {} if "kind" not in kwargs: kwargs["kind"] = "json" - return resources.data.load(DataDescription(path=path, **kwargs)) + json = resources.data.load(DataDescription(path=path, **kwargs)) + + assert isinstance(json, dict), f"There was an error when loading the Texture, {type(json)} is not a dictionnary" + return json - def load_binary(self, path: str, **kwargs) -> bytes: + def load_binary(self, path: str, **kwargs: Any) -> bytes: """Load a file in binary mode. If the path is relative the resource system is used expecting one or more @@ -1507,16 +1530,19 @@ def load_binary(self, path: str, **kwargs) -> bytes: Returns: bytes: The byte data of the file """ - if not kwargs: + if kwargs is not None: kwargs = {} if "kind" not in kwargs: kwargs["kind"] = "binary" - return resources.data.load(DataDescription(path=path, kind="binary")) + binary = resources.data.load(DataDescription(path=path, kind="binary")) + + assert isinstance(binary, bytes), f"There was an error when loading the binary, {type(binary)} is not a binary file" + return binary def load_scene( - self, path: str, cache=False, attr_names=AttributeNames, kind=None, **kwargs + self, path: str, cache: bool = False, attr_names: type[AttributeNames] = AttributeNames, kind: Optional[str] = None, **kwargs: Any ) -> Scene: """Loads a scene. @@ -1533,7 +1559,7 @@ def load_scene( Returns: Scene: The scene instance """ - return resources.scenes.load( + scene = resources.scenes.load( SceneDescription( path=path, cache=cache, @@ -1543,7 +1569,10 @@ def load_scene( ) ) + assert isinstance(scene, Scene), f"There was an error when loading the scene, {type(scene)} is not a Scene" + return scene + -def dummy_func(*args, **kwargs) -> None: +def dummy_func(*args: Any, **kwargs: Any) -> None: """Dummy function used as the default for callbacks""" pass diff --git a/moderngl_window/context/glfw/__init__.py b/moderngl_window/context/glfw/__init__.py index 596ed57..440c2b7 100644 --- a/moderngl_window/context/glfw/__init__.py +++ b/moderngl_window/context/glfw/__init__.py @@ -1,2 +1,2 @@ -from .keys import Keys # noqa +from .keys import GLFW_key, Keys # noqa from .window import Window # noqa diff --git a/moderngl_window/context/glfw/keys.py b/moderngl_window/context/glfw/keys.py index 4194d8a..601565f 100644 --- a/moderngl_window/context/glfw/keys.py +++ b/moderngl_window/context/glfw/keys.py @@ -3,6 +3,8 @@ from moderngl_window.context.base import BaseKeys +GLFW_key = int + class Keys(BaseKeys): """ diff --git a/moderngl_window/context/glfw/window.py b/moderngl_window/context/glfw/window.py index 2e871e0..97cab29 100644 --- a/moderngl_window/context/glfw/window.py +++ b/moderngl_window/context/glfw/window.py @@ -1,10 +1,11 @@ -from typing import Tuple -import glfw +from pathlib import Path +from typing import Any +import glfw from PIL import Image from moderngl_window.context.base import BaseWindow -from moderngl_window.context.glfw.keys import Keys +from moderngl_window.context.glfw.keys import GLFW_key, Keys class Window(BaseWindow): @@ -24,7 +25,7 @@ class Window(BaseWindow): 2: 3, } - def __init__(self, **kwargs): + def __init__(self, **kwargs: Any): super().__init__(**kwargs) if not glfw.init(): @@ -126,8 +127,8 @@ def _set_vsync(self, value: bool) -> None: glfw.swap_interval(value) @property - def size(self) -> Tuple[int, int]: - """Tuple[int, int]: current window size. + def size(self) -> tuple[int, int]: + """tuple[int, int]: current window size. This property also support assignment:: @@ -137,12 +138,12 @@ def size(self) -> Tuple[int, int]: return self._width, self._height @size.setter - def size(self, value: Tuple[int, int]): + def size(self, value: tuple[int, int]) -> None: glfw.set_window_size(self._window, value[0], value[1]) @property - def position(self) -> Tuple[int, int]: - """Tuple[int, int]: The current window position. + def position(self) -> tuple[int, int]: + """tuple[int, int]: The current window position. This property can also be set to move the window:: @@ -152,7 +153,7 @@ def position(self) -> Tuple[int, int]: return glfw.get_window_pos(self._window) @position.setter - def position(self, value: Tuple[int, int]): + def position(self, value: tuple[int, int]) -> None: self._position = glfw.set_window_pos(self._window, value[0], value[1]) @property @@ -167,7 +168,7 @@ def visible(self) -> bool: return self._visible @visible.setter - def visible(self, value: bool): + def visible(self, value: bool) -> None: self._visible = value if value: glfw.show_window(self._window) @@ -186,7 +187,7 @@ def cursor(self) -> bool: return self._cursor @cursor.setter - def cursor(self, value: bool): + def cursor(self, value: bool) -> None: if not self.mouse_exclusivity: if value is True: glfw.set_input_mode(self._window, glfw.CURSOR, glfw.CURSOR_NORMAL) @@ -212,7 +213,7 @@ def mouse_exclusivity(self) -> bool: return self._mouse_exclusivity @mouse_exclusivity.setter - def mouse_exclusivity(self, value: bool): + def mouse_exclusivity(self, value: bool) -> None: self._mouse_exclusivity = value if value is True: self._mouse_pos = glfw.get_cursor_pos(self._window) @@ -231,7 +232,7 @@ def title(self) -> str: return self._title @title.setter - def title(self, value: str): + def title(self, value: str) -> None: glfw.set_window_title(self._window, value) self._title = value @@ -241,31 +242,31 @@ def close(self) -> None: self._close_func() @property - def is_closing(self): + def is_closing(self) -> bool: """bool: Checks if the window is scheduled for closing""" return glfw.window_should_close(self._window) @is_closing.setter - def is_closing(self, value: bool): + def is_closing(self, value: bool) -> None: glfw.set_window_should_close(self._window, value) - def swap_buffers(self): + def swap_buffers(self) -> None: """Swap buffers, increment frame counter and pull events""" glfw.swap_buffers(self._window) self._frames += 1 glfw.poll_events() - def _handle_modifiers(self, mods): + def _handle_modifiers(self, mods: GLFW_key) -> None: """Checks key modifiers""" self._modifiers.shift = mods & 1 == 1 self._modifiers.ctrl = mods & 2 == 2 self._modifiers.alt = mods & 4 == 4 - def _set_icon(self, icon_path) -> None: + def _set_icon(self, icon_path: Path) -> None: image = Image.open(icon_path) glfw.set_window_icon(self._window, 1, image) - def glfw_key_event_callback(self, window, key, scancode, action, mods): + def glfw_key_event_callback(self, window: Any, key: GLFW_key, scancode: int, action: GLFW_key, mods: GLFW_key) -> None: """Key event callback for glfw. Translates and forwards keyboard event to :py:func:`keyboard_event` @@ -291,7 +292,7 @@ def glfw_key_event_callback(self, window, key, scancode, action, mods): self._key_event_func(key, action, self._modifiers) - def glfw_mouse_event_callback(self, window, xpos, ypos): + def glfw_mouse_event_callback(self, window: Any, xpos: float, ypos: float) -> None: """Mouse position event callback from glfw. Translates the events forwarding them to :py:func:`cursor_event`. @@ -310,7 +311,7 @@ def glfw_mouse_event_callback(self, window, xpos, ypos): else: self._mouse_position_event_func(xpos, ypos, dx, dy) - def glfw_mouse_button_callback(self, window, button, action, mods): + def glfw_mouse_button_callback(self, window: Any, button: GLFW_key, action: GLFW_key, mods: GLFW_key) -> None: """Handle mouse button events and forward them to the example Args: @@ -320,8 +321,8 @@ def glfw_mouse_button_callback(self, window, button, action, mods): mods: They modifiers such as ctrl or shift """ self._handle_modifiers(mods) - button = self._mouse_button_map.get(button, None) - if button is None: + button = self._mouse_button_map.get(button, -1) + if button == -1: return xpos, ypos = glfw.get_cursor_pos(self._window) @@ -333,7 +334,7 @@ def glfw_mouse_button_callback(self, window, button, action, mods): self._handle_mouse_button_state_change(button, False) self._mouse_release_event_func(xpos, ypos, button) - def glfw_mouse_scroll_callback(self, window, x_offset: float, y_offset: float): + def glfw_mouse_scroll_callback(self, window: Any, x_offset: float, y_offset: float) -> None: """Handle mouse scroll events and forward them to the example Args: @@ -343,7 +344,7 @@ def glfw_mouse_scroll_callback(self, window, x_offset: float, y_offset: float): """ self._mouse_scroll_event_func(x_offset, y_offset) - def glfw_char_callback(self, window, codepoint: int): + def glfw_char_callback(self, window: Any, codepoint: int) -> None: """Handle text input (only unicode charaters) Args: @@ -352,7 +353,7 @@ def glfw_char_callback(self, window, codepoint: int): """ self._unicode_char_entered_func(chr(codepoint)) - def glfw_window_resize_callback(self, window, width, height): + def glfw_window_resize_callback(self, window: Any, width: int, height: int) -> None: """ Window resize callback for glfw @@ -367,7 +368,7 @@ def glfw_window_resize_callback(self, window, width, height): super().resize(self._buffer_width, self._buffer_height) - def glfw_window_focus(self, window, focused: int): + def glfw_window_focus(self, window: Any, focused: int) -> None: """Called when the window focus is changed. Args: @@ -376,7 +377,7 @@ def glfw_window_focus(self, window, focused: int): """ self._has_focus = True if focused == 1 else False - def glfw_cursor_enter(self, window, enter: int): + def glfw_cursor_enter(self, window: Any, enter: int) -> None: """called when the cursor enters or leaves the content area of the window. Args: @@ -385,7 +386,7 @@ def glfw_cursor_enter(self, window, enter: int): """ pass - def glfw_window_iconify(self, window, iconified: int): + def glfw_window_iconify(self, window: Any, iconified: int) -> None: """Called when the window is minimized or restored. Args: @@ -394,10 +395,10 @@ def glfw_window_iconify(self, window, iconified: int): """ self._iconify_func(True if iconified == 1 else False) - def glfw_window_close(self, window): + def glfw_window_close(self, window: Any) -> None: """Called when the window is closed""" self.close() - def destroy(self): + def destroy(self) -> None: """Gracefully terminate GLFW""" glfw.terminate() diff --git a/moderngl_window/context/headless/window.py b/moderngl_window/context/headless/window.py index 902143b..3a9d312 100644 --- a/moderngl_window/context/headless/window.py +++ b/moderngl_window/context/headless/window.py @@ -1,6 +1,8 @@ -from typing import Tuple +from pathlib import Path +from typing import Any, Optional import moderngl + from moderngl_window.context.base import BaseWindow from moderngl_window.context.headless.keys import Keys @@ -15,9 +17,9 @@ class Window(BaseWindow): name = "headless" keys = Keys - def __init__(self, **kwargs): + def __init__(self, **kwargs: Any): super().__init__(**kwargs) - self._fbo = None + self._fbo: Optional[moderngl.Framebuffer] = None self._vsync = False # We don't care about vsync in headless mode self._resizable = False # headless window is not resizable self._cursor = False # Headless don't have a cursor @@ -28,11 +30,12 @@ def __init__(self, **kwargs): @property def fbo(self) -> moderngl.Framebuffer: """moderngl.Framebuffer: The default framebuffer""" + assert self._fbo is not None, "No default framebuffer defined" return self._fbo def init_mgl_context(self) -> None: """Create an standalone context and framebuffer""" - if self._backend: + if self._backend is not None: self._ctx = moderngl.create_standalone_context( require=self.gl_version_code, backend=self._backend, @@ -45,7 +48,7 @@ def init_mgl_context(self) -> None: self._create_fbo() self.use() - def _create_fbo(self): + def _create_fbo(self) -> None: if self._fbo: for attachment in self._fbo.color_attachments: attachment.release() @@ -59,8 +62,8 @@ def _create_fbo(self): ) @property - def size(self) -> Tuple[int, int]: - """Tuple[int, int]: current window size. + def size(self) -> tuple[int, int]: + """tuple[int, int]: current window size. This property also support assignment:: @@ -70,17 +73,18 @@ def size(self) -> Tuple[int, int]: return self._width, self._height @size.setter - def size(self, value: Tuple[int, int]): + def size(self, value: tuple[int, int]) -> None: if value == (self._width, self._height): return self._width, self._height = value self._create_fbo() - def use(self): + def use(self) -> None: """Bind the window's framebuffer""" + assert self._fbo is not None, "No framebuffer defined, did you forget to call create_fbo()?" self._fbo.use() - def clear(self, red=0.0, green=0.0, blue=0.0, alpha=0.0, depth=1.0, viewport=None): + def clear(self, red: float = 0.0, green: float = 0.0, blue: float = 0.0, alpha: float = 0.0, depth: float = 1.0, viewport: Optional[tuple[int, int, int, int]] = None) -> None: """ Binds and clears the default framebuffer @@ -106,7 +110,7 @@ def swap_buffers(self) -> None: self._frames += 1 self._ctx.finish() - def _set_icon(self, icon_path: str) -> None: + def _set_icon(self, icon_path: Path) -> None: """Do nothing when icon is set""" pass diff --git a/moderngl_window/context/pygame2/window.py b/moderngl_window/context/pygame2/window.py index fd51a92..7ca55d2 100644 --- a/moderngl_window/context/pygame2/window.py +++ b/moderngl_window/context/pygame2/window.py @@ -1,8 +1,10 @@ -from typing import Tuple +from pathlib import Path +from typing import Any + import pygame +import pygame._sdl2 import pygame.display import pygame.event -import pygame._sdl2 from moderngl_window.context.base import BaseWindow from moderngl_window.context.pygame2.keys import Keys @@ -24,7 +26,7 @@ class Window(BaseWindow): 2: 3, } - def __init__(self, **kwargs): + def __init__(self, **kwargs: Any): super().__init__(**kwargs) pygame.display.init() @@ -65,7 +67,7 @@ def __init__(self, **kwargs): self.init_mgl_context() self.set_default_viewport() - def _set_mode(self): + def _set_mode(self) -> None: self._surface = pygame.display.set_mode( size=(self._width, self._height), flags=self._flags, @@ -84,8 +86,8 @@ def _set_vsync(self, value: bool) -> None: self._set_mode() @property - def size(self) -> Tuple[int, int]: - """Tuple[int, int]: current window size. + def size(self) -> tuple[int, int]: + """tuple[int, int]: current window size. This property also support assignment:: @@ -95,14 +97,14 @@ def size(self) -> Tuple[int, int]: return self._width, self._height @size.setter - def size(self, value: Tuple[int, int]): + def size(self, value: tuple[int, int]) -> None: self._width, self._height = value self._set_mode() self.resize(value[0], value[1]) @property - def position(self) -> Tuple[int, int]: - """Tuple[int, int]: The current window position. + def position(self) -> tuple[int, int]: + """tuple[int, int]: The current window position. This property can also be set to move the window:: @@ -112,7 +114,7 @@ def position(self) -> Tuple[int, int]: return self._sdl_window.position @position.setter - def position(self, value: Tuple[int, int]): + def position(self, value: tuple[int, int]) -> None: self._sdl_window.position = value @property @@ -127,7 +129,7 @@ def visible(self) -> bool: return self._visible @visible.setter - def visible(self, value: bool): + def visible(self, value: bool) -> None: self._visible = value if value: self._sdl_window.show() @@ -146,7 +148,7 @@ def cursor(self) -> bool: return self._cursor @cursor.setter - def cursor(self, value: bool): + def cursor(self, value: bool) -> None: pygame.mouse.set_visible(value) self._cursor = value @@ -167,7 +169,7 @@ def mouse_exclusivity(self) -> bool: return self._mouse_exclusivity @mouse_exclusivity.setter - def mouse_exclusivity(self, value: bool): + def mouse_exclusivity(self, value: bool) -> None: if self._cursor: self.cursor = False @@ -185,7 +187,7 @@ def title(self) -> str: return self._title @title.setter - def title(self, value: str): + def title(self, value: str) -> None: pygame.display.set_caption(value) self._title = value @@ -196,11 +198,11 @@ def swap_buffers(self) -> None: self.process_events() self._frames += 1 - def _set_icon(self, icon_path: str) -> None: + def _set_icon(self, icon_path: Path) -> None: icon = pygame.image.load(icon_path) pygame.display.set_icon(icon) - def resize(self, width, height) -> None: + def resize(self, width: int, height: int) -> None: """Resize callback Args: @@ -214,7 +216,7 @@ def resize(self, width, height) -> None: super().resize(self._buffer_width, self._buffer_height) - def close(self): + def close(self) -> None: """Close the window""" super().close() self._close_func() diff --git a/moderngl_window/context/pyglet/keys.py b/moderngl_window/context/pyglet/keys.py index 62f44b1..71fd3ff 100644 --- a/moderngl_window/context/pyglet/keys.py +++ b/moderngl_window/context/pyglet/keys.py @@ -1,5 +1,6 @@ # flake8: noqa E741 import platform + import pyglet # On OS X we need to disable the shadow context diff --git a/moderngl_window/context/pyglet/window.py b/moderngl_window/context/pyglet/window.py index d0eb04e..9acd6f1 100644 --- a/moderngl_window/context/pyglet/window.py +++ b/moderngl_window/context/pyglet/window.py @@ -1,5 +1,5 @@ -from typing import Tuple import platform + import pyglet # On OS X we need to disable the shadow context @@ -9,8 +9,11 @@ pyglet.options["debug_gl"] = False -from moderngl_window.context.pyglet.keys import Keys # noqa: E402 +from pathlib import Path +from typing import Any, Union + from moderngl_window.context.base import BaseWindow # noqa: E402 +from moderngl_window.context.pyglet.keys import Keys # noqa: E402 class Window(BaseWindow): @@ -30,7 +33,7 @@ class Window(BaseWindow): 2: 3, } - def __init__(self, **kwargs): + def __init__(self, **kwargs: Any): super().__init__(**kwargs) config = pyglet.gl.Config( @@ -89,8 +92,8 @@ def _set_fullscreen(self, value: bool) -> None: self._window.set_fullscreen(value) @property - def size(self) -> Tuple[int, int]: - """Tuple[int, int]: current window size. + def size(self) -> tuple[int, int]: + """tuple[int, int]: current window size. This property also support assignment:: @@ -100,12 +103,12 @@ def size(self) -> Tuple[int, int]: return self._width, self._height @size.setter - def size(self, value: Tuple[int, int]): + def size(self, value: tuple[int, int]) -> None: self._window.set_size(value[0], value[1]) @property - def position(self) -> Tuple[int, int]: - """Tuple[int, int]: The current window position. + def position(self) -> tuple[int, int]: + """tuple[int, int]: The current window position. This property can also be set to move the window:: @@ -115,7 +118,7 @@ def position(self) -> Tuple[int, int]: return self._window.get_location() @position.setter - def position(self, value: Tuple[int, int]): + def position(self, value: tuple[int, int]) -> None: self._window.set_location(value[0], value[1]) @property @@ -130,7 +133,7 @@ def visible(self) -> bool: return self._visible @visible.setter - def visible(self, value: bool): + def visible(self, value: bool) -> None: self._visible = value self._window.set_visible(value) @@ -146,7 +149,7 @@ def cursor(self) -> bool: return self._cursor @cursor.setter - def cursor(self, value: bool): + def cursor(self, value: bool) -> None: self._window.set_mouse_visible(value) self._cursor = value @@ -167,7 +170,7 @@ def mouse_exclusivity(self) -> bool: return self._mouse_exclusivity @mouse_exclusivity.setter - def mouse_exclusivity(self, value: bool): + def mouse_exclusivity(self, value: bool) -> None: self._window.set_exclusive_mouse(value) self._mouse_exclusivity = value @@ -182,7 +185,7 @@ def title(self) -> str: return self._title @title.setter - def title(self, value: str): + def title(self, value: str) -> None: self._window.set_caption(value) self._title = value @@ -192,7 +195,7 @@ def is_closing(self) -> bool: return self._window.has_exit or super().is_closing @is_closing.setter - def is_closing(self, value: bool): + def is_closing(self, value: bool) -> None: self._close = value def close(self) -> None: @@ -207,20 +210,20 @@ def swap_buffers(self) -> None: self._frames += 1 self._window.dispatch_events() - def _handle_modifiers(self, mods): + def _handle_modifiers(self, mods: int) -> None: """Update key modifier states""" self._modifiers.shift = mods & 1 == 1 self._modifiers.ctrl = mods & 2 == 2 self._modifiers.alt = mods & 4 == 4 - def _set_icon(self, icon_path: str) -> None: + def _set_icon(self, icon_path: Path) -> None: icon = pyglet.image.load(icon_path) self._window.set_icon(icon) def _set_vsync(self, value: bool) -> None: self._window.set_vsync(value) - def on_key_press(self, symbol, modifiers): + def on_key_press(self, symbol: int, modifiers: int) -> bool: """Pyglet specific key press callback. Forwards and translates the events to the standard methods. @@ -241,7 +244,7 @@ def on_key_press(self, symbol, modifiers): return pyglet.event.EVENT_HANDLED - def on_text(self, text): + def on_text(self, text: str) -> None: """Pyglet specific text input callback Forwards and translates the events to the standard methods. @@ -251,7 +254,7 @@ def on_text(self, text): """ self._unicode_char_entered_func(text) - def on_key_release(self, symbol, modifiers): + def on_key_release(self, symbol: int, modifiers: int) -> None: """Pyglet specific key release callback. Forwards and translates the events to standard methods. @@ -264,7 +267,7 @@ def on_key_release(self, symbol, modifiers): self._handle_modifiers(modifiers) self._key_event_func(symbol, self.keys.ACTION_RELEASE, self._modifiers) - def on_mouse_motion(self, x, y, dx, dy): + def on_mouse_motion(self, x: int, y: int, dx: int, dy: int) -> None: """Pyglet specific mouse motion callback. Forwards and translates the event to the standard methods. @@ -280,7 +283,7 @@ def on_mouse_motion(self, x, y, dx, dy): # other window libraries self._mouse_position_event_func(x, self._height - y, dx, -dy) - def on_mouse_drag(self, x, y, dx, dy, buttons, modifiers): + def on_mouse_drag(self, x: int, y: int, dx: int, dy: int, buttons: int, modifiers: int) -> None: """Pyglet specific mouse drag event. When a mouse button is pressed this is the only way @@ -289,7 +292,7 @@ def on_mouse_drag(self, x, y, dx, dy, buttons, modifiers): self._handle_modifiers(modifiers) self._mouse_drag_event_func(x, self._height - y, dx, -dy) - def on_mouse_press(self, x: int, y: int, button, mods): + def on_mouse_press(self, x: int, y: int, button: int, mods: int) -> None: """Handle mouse press events and forward to standard methods Args: @@ -299,8 +302,8 @@ def on_mouse_press(self, x: int, y: int, button, mods): mods: Modifiers """ self._handle_modifiers(mods) - button = self._mouse_button_map.get(button, None) - if button is not None: + button = self._mouse_button_map.get(button, -1) + if button != -1: self._handle_mouse_button_state_change(button, True) self._mouse_press_event_func( x, @@ -308,7 +311,7 @@ def on_mouse_press(self, x: int, y: int, button, mods): button, ) - def on_mouse_release(self, x: int, y: int, button, mods): + def on_mouse_release(self, x: int, y: int, button: int, mods: int) -> None: """Handle mouse release events and forward to standard methods Args: @@ -317,8 +320,8 @@ def on_mouse_release(self, x: int, y: int, button, mods): button: The button pressed mods: Modifiers """ - button = self._mouse_button_map.get(button, None) - if button is not None: + button = self._mouse_button_map.get(button, -1) + if button != -1: self._handle_mouse_button_state_change(button, False) self._mouse_release_event_func( x, @@ -326,7 +329,7 @@ def on_mouse_release(self, x: int, y: int, button, mods): button, ) - def on_mouse_scroll(self, x, y, x_offset: float, y_offset: float): + def on_mouse_scroll(self, x: int, y: int, x_offset: float, y_offset: float) -> None: """Handle mouse wheel. Args: @@ -336,7 +339,7 @@ def on_mouse_scroll(self, x, y, x_offset: float, y_offset: float): self._handle_modifiers(0) # No modifiers available self.mouse_scroll_event_func(x_offset, y_offset) - def on_resize(self, width: int, height: int): + def on_resize(self, width: int, height: int) -> None: """Pyglet specific callback for window resize events forwarding to standard methods Args: @@ -349,19 +352,19 @@ def on_resize(self, width: int, height: int): super().resize(self._buffer_width, self._buffer_height) - def on_close(self): + def on_close(self) -> None: """Pyglet specific window close callback""" self._close_func() - def on_show(self): + def on_show(self) -> None: """Called when window first appear or restored from hidden state""" self._iconify_func(False) - def on_hide(self): + def on_hide(self) -> None: """Called when window is minimized""" self._iconify_func(True) - def on_file_drop(self, x, y, paths): + def on_file_drop(self, x: int, y: int, paths: list[Union[str, Path]]) -> None: """Called when files dropped onto the window Args: @@ -375,7 +378,7 @@ def on_file_drop(self, x, y, paths): (x, y) = self.convert_window_coordinates(x, y, y_flipped=True) self._files_dropped_event_func(x, y, paths) - def destroy(self): + def destroy(self) -> None: """Destroy the pyglet window""" pass @@ -383,12 +386,12 @@ def destroy(self): class PygletWrapper(pyglet.window.Window): """Block out some window methods so pyglet don't trigger GL errors""" - def on_resize(self, width, height): + def on_resize(self, width: int, height: int) -> None: """Block out the resize method. For some reason pyglet calls this triggering errors. """ pass - def on_draw(self): + def on_draw(self) -> None: """Block out the default draw method to avoid GL errors""" pass diff --git a/moderngl_window/context/pyqt5/window.py b/moderngl_window/context/pyqt5/window.py index 00383b3..fbf15dc 100644 --- a/moderngl_window/context/pyqt5/window.py +++ b/moderngl_window/context/pyqt5/window.py @@ -1,5 +1,7 @@ -from typing import Tuple -from PyQt5 import QtCore, QtOpenGL, QtWidgets, QtGui +from pathlib import Path +from typing import Any + +from PyQt5 import QtCore, QtGui, QtOpenGL, QtWidgets from moderngl_window.context.base import BaseWindow from moderngl_window.context.pyqt5.keys import Keys @@ -29,7 +31,7 @@ class Window(BaseWindow): 4: 3, } - def __init__(self, **kwargs): + def __init__(self, **kwargs: Any): super().__init__(**kwargs) # Specify OpenGL context parameters @@ -128,8 +130,8 @@ def _set_vsync(self, value: bool) -> None: pass @property - def size(self) -> Tuple[int, int]: - """Tuple[int, int]: current window size. + def size(self) -> tuple[int, int]: + """tuple[int, int]: current window size. This property also support assignment:: @@ -139,13 +141,13 @@ def size(self) -> Tuple[int, int]: return self._width, self._height @size.setter - def size(self, value: Tuple[int, int]): + def size(self, value: tuple[int, int]) -> None: pos = self.position self._widget.setGeometry(pos[0], pos[1], value[0], value[1]) @property - def position(self) -> Tuple[int, int]: - """Tuple[int, int]: The current window position. + def position(self) -> tuple[int, int]: + """tuple[int, int]: The current window position. This property can also be set to move the window:: @@ -156,7 +158,7 @@ def position(self) -> Tuple[int, int]: return geo.x(), geo.y() @position.setter - def position(self, value: Tuple[int, int]): + def position(self, value: tuple[int, int]) -> None: self._widget.setGeometry(value[0], value[1], self._width, self._height) @property @@ -171,7 +173,7 @@ def visible(self) -> bool: return self._visible @visible.setter - def visible(self, value: bool): + def visible(self, value: bool) -> None: self._visible = value if value: self._widget.show() @@ -197,7 +199,7 @@ def cursor(self) -> bool: return self._cursor @cursor.setter - def cursor(self, value: bool): + def cursor(self, value: bool) -> None: if value is True: self._widget.setCursor(QtCore.Qt.ArrowCursor) else: @@ -216,7 +218,7 @@ def title(self) -> str: return self._title @title.setter - def title(self, value: str): + def title(self, value: str) -> None: self._widget.setWindowTitle(value) self._title = value @@ -238,16 +240,16 @@ def resize(self, width: int, height: int) -> None: # Make sure we notify the example about the resize super().resize(self._buffer_width, self._buffer_height) - def _handle_modifiers(self, mods) -> None: + def _handle_modifiers(self, mods: int) -> None: """Update modifiers""" self._modifiers.shift = bool(mods & QtCore.Qt.ShiftModifier) self._modifiers.ctrl = bool(mods & QtCore.Qt.ControlModifier) self._modifiers.alt = bool(mods & QtCore.Qt.AltModifier) - def _set_icon(self, icon_path: str) -> None: + def _set_icon(self, icon_path: Path) -> None: self._widget.setWindowIcon(QtGui.QIcon(icon_path)) - def key_pressed_event(self, event) -> None: + def key_pressed_event(self, event: QtCore.QEvent) -> None: """Process Qt key press events forwarding them to standard methods Args: @@ -267,7 +269,7 @@ def key_pressed_event(self, event) -> None: if text.strip() or event.key() == self.keys.SPACE: self._unicode_char_entered_func(text) - def key_release_event(self, event) -> None: + def key_release_event(self, event: QtCore.QEvent) -> None: """Process Qt key release events forwarding them to standard methods Args: @@ -277,7 +279,7 @@ def key_release_event(self, event) -> None: self._key_pressed_map[event.key()] = False self._key_event_func(event.key(), self.keys.ACTION_RELEASE, self._modifiers) - def mouse_move_event(self, event) -> None: + def mouse_move_event(self, event: QtCore.QEvent) -> None: """Forward mouse cursor position events to standard methods Args: @@ -291,7 +293,7 @@ def mouse_move_event(self, event) -> None: else: self._mouse_position_event_func(x, y, dx, dy) - def mouse_press_event(self, event) -> None: + def mouse_press_event(self, event: QtCore.QEvent) -> None: """Forward mouse press events to standard methods Args: @@ -305,7 +307,7 @@ def mouse_press_event(self, event) -> None: self._handle_mouse_button_state_change(button, True) self._mouse_press_event_func(event.x(), event.y(), button) - def mouse_release_event(self, event) -> None: + def mouse_release_event(self, event: QtCore.QEvent) -> None: """Forward mouse release events to standard methods Args: @@ -319,7 +321,7 @@ def mouse_release_event(self, event) -> None: self._handle_mouse_button_state_change(button, False) self._mouse_release_event_func(event.x(), event.y(), button) - def mouse_wheel_event(self, event): + def mouse_wheel_event(self, event: QtCore.QEvent) -> None: """Forward mouse wheel events to standard metods. From Qt docs: @@ -344,7 +346,7 @@ def mouse_wheel_event(self, event): point = event.angleDelta() self._mouse_scroll_event_func(point.x() / 120.0, point.y() / 120.0) - def close_event(self, event) -> None: + def close_event(self, event: QtCore.QEvent) -> None: """The standard PyQt close events Args: @@ -352,16 +354,16 @@ def close_event(self, event) -> None: """ self.close() - def close(self): + def close(self) -> None: """Close the window""" super().close() self._close_func() - def show_event(self, event): + def show_event(self, event: QtCore.QEvent) -> None: """The standard Qt show event""" self._iconify_func(False) - def hide_event(self, event): + def hide_event(self, event: QtCore.QEvent) -> None: """The standard Qt hide event""" self._iconify_func(True) diff --git a/moderngl_window/context/pyside2/window.py b/moderngl_window/context/pyside2/window.py index 2320570..eafd138 100644 --- a/moderngl_window/context/pyside2/window.py +++ b/moderngl_window/context/pyside2/window.py @@ -1,5 +1,7 @@ -from typing import Tuple -from PySide2 import QtCore, QtOpenGL, QtWidgets, QtGui +from pathlib import Path +from typing import Any + +from PySide2 import QtCore, QtGui, QtOpenGL, QtWidgets from moderngl_window.context.base import BaseWindow from moderngl_window.context.pyside2.keys import Keys @@ -29,7 +31,7 @@ class Window(BaseWindow): 4: 3, } - def __init__(self, **kwargs): + def __init__(self, **kwargs: Any): super().__init__(**kwargs) # Specify OpenGL context parameters @@ -128,8 +130,8 @@ def _set_vsync(self, value: bool) -> None: pass @property - def size(self) -> Tuple[int, int]: - """Tuple[int, int]: current window size. + def size(self) -> tuple[int, int]: + """tuple[int, int]: current window size. This property also support assignment:: @@ -139,7 +141,7 @@ def size(self) -> Tuple[int, int]: return self._width, self._height @size.setter - def size(self, value: Tuple[int, int]): + def size(self, value: tuple[int, int]) -> None: pos = self.position self._widget.setGeometry(pos[0], pos[1], value[0], value[1]) @@ -155,7 +157,7 @@ def visible(self) -> bool: return self._visible @visible.setter - def visible(self, value: bool): + def visible(self, value: bool) -> None: self._visible = value if value: self._widget.show() @@ -163,8 +165,8 @@ def visible(self, value: bool): self._widget.hide() @property - def position(self) -> Tuple[int, int]: - """Tuple[int, int]: The current window position. + def position(self) -> tuple[int, int]: + """tuple[int, int]: The current window position. This property can also be set to move the window:: @@ -175,7 +177,7 @@ def position(self) -> Tuple[int, int]: return geo.x(), geo.y() @position.setter - def position(self, value: Tuple[int, int]): + def position(self, value: tuple[int, int]) -> None: self._widget.setGeometry(value[0], value[1], self._width, self._height) @property @@ -190,7 +192,7 @@ def cursor(self) -> bool: return self._cursor @cursor.setter - def cursor(self, value: bool): + def cursor(self, value: bool) -> None: if value is True: self._widget.setCursor(QtCore.Qt.ArrowCursor) else: @@ -209,7 +211,7 @@ def title(self) -> str: return self._title @title.setter - def title(self, value: str): + def title(self, value: str) -> None: self._widget.setWindowTitle(value) self._title = value @@ -238,16 +240,16 @@ def resize(self, width: int, height: int) -> None: # Make sure we notify the example about the resize super().resize(self._buffer_width, self._buffer_height) - def _handle_modifiers(self, mods): + def _handle_modifiers(self, mods: QtCore.Qt.KeyboardModifier) -> None: """Update modifiers""" self._modifiers.shift = bool(mods & QtCore.Qt.ShiftModifier) self._modifiers.ctrl = bool(mods & QtCore.Qt.ControlModifier) self._modifiers.alt = bool(mods & QtCore.Qt.AltModifier) - def _set_icon(self, icon_path: str) -> None: + def _set_icon(self, icon_path: Path) -> None: self._widget.setWindowIcon(QtGui.QIcon(icon_path)) - def key_pressed_event(self, event): + def key_pressed_event(self, event: QtCore.QEvent) -> None: """Process Qt key press events forwarding them to standard methods Args: @@ -267,7 +269,7 @@ def key_pressed_event(self, event): if text.strip() or event.key() == self.keys.SPACE: self._unicode_char_entered_func(text) - def key_release_event(self, event): + def key_release_event(self, event: QtCore.QEvent) -> None: """Process Qt key release events forwarding them to standard methods Args: @@ -277,7 +279,7 @@ def key_release_event(self, event): self._key_pressed_map[event.key()] = False self.key_event_func(event.key(), self.keys.ACTION_RELEASE, self._modifiers) - def mouse_move_event(self, event) -> None: + def mouse_move_event(self, event: QtCore.QEvent) -> None: """Forward mouse cursor position events to standard methods Args: @@ -291,7 +293,7 @@ def mouse_move_event(self, event) -> None: else: self._mouse_position_event_func(x, y, dx, dy) - def mouse_press_event(self, event) -> None: + def mouse_press_event(self, event: QtCore.QEvent) -> None: """Forward mouse press events to standard methods Args: @@ -305,7 +307,7 @@ def mouse_press_event(self, event) -> None: self._handle_mouse_button_state_change(button, True) self.mouse_press_event_func(event.x(), event.y(), button) - def mouse_release_event(self, event) -> None: + def mouse_release_event(self, event: QtCore.QEvent) -> None: """Forward mouse release events to standard methods Args: @@ -319,7 +321,7 @@ def mouse_release_event(self, event) -> None: self._handle_mouse_button_state_change(button, False) self.mouse_release_event_func(event.x(), event.y(), button) - def mouse_wheel_event(self, event): + def mouse_wheel_event(self, event: QtCore.QEvent) -> None: """Forward mouse wheel events to standard metods. From Qt docs: @@ -344,7 +346,7 @@ def mouse_wheel_event(self, event): point = event.angleDelta() self._mouse_scroll_event_func(point.x() / 120.0, point.y() / 120.0) - def close_event(self, event) -> None: + def close_event(self, event: QtCore.QEvent) -> None: """The standard PyQt close events Args: @@ -352,16 +354,16 @@ def close_event(self, event) -> None: """ self.close() - def close(self): + def close(self) -> None: """Close the window""" super().close() self._close_func() - def show_event(self, event): + def show_event(self, event: QtCore.QEvent) -> None: """The standard Qt show event""" self._iconify_func(False) - def hide_event(self, event): + def hide_event(self, event: QtCore.QEvent) -> None: """The standard Qt hide event""" self._iconify_func(True) diff --git a/moderngl_window/context/sdl2/window.py b/moderngl_window/context/sdl2/window.py index 87df8d0..f89b1cb 100644 --- a/moderngl_window/context/sdl2/window.py +++ b/moderngl_window/context/sdl2/window.py @@ -1,5 +1,7 @@ -from typing import Tuple -from ctypes import c_int, c_char_p +from ctypes import c_char_p, c_int +from pathlib import Path +from typing import Any + import sdl2 import sdl2.ext import sdl2.video @@ -24,7 +26,7 @@ class Window(BaseWindow): 2: 3, } - def __init__(self, **kwargs): + def __init__(self, **kwargs: Any): super().__init__(**kwargs) if sdl2.SDL_Init(sdl2.SDL_INIT_VIDEO) != 0: @@ -83,15 +85,15 @@ def _set_fullscreen(self, value: bool) -> None: def _set_vsync(self, value: bool) -> None: sdl2.video.SDL_GL_SetSwapInterval(1 if value else 0) - def _get_drawable_size(self): + def _get_drawable_size(self) -> tuple[int, int]: x = c_int() y = c_int() sdl2.video.SDL_GL_GetDrawableSize(self._window, x, y) return x.value, y.value @property - def size(self) -> Tuple[int, int]: - """Tuple[int, int]: current window size. + def size(self) -> tuple[int, int]: + """tuple[int, int]: current window size. This property also support assignment:: @@ -101,14 +103,14 @@ def size(self) -> Tuple[int, int]: return self._width, self._height @size.setter - def size(self, value: Tuple[int, int]): + def size(self, value: tuple[int, int]) -> None: sdl2.SDL_SetWindowSize(self._window, value[0], value[1]) # SDL_SetWindowSize don't trigger a resize event self.resize(value[0], value[1]) @property - def position(self) -> Tuple[int, int]: - """Tuple[int, int]: The current window position. + def position(self) -> tuple[int, int]: + """tuple[int, int]: The current window position. This property can also be set to move the window:: @@ -121,7 +123,7 @@ def position(self) -> Tuple[int, int]: return x.value, y.value @position.setter - def position(self, value: Tuple[int, int]): + def position(self, value: tuple[int, int]) -> None: sdl2.SDL_SetWindowPosition(self._window, value[0], value[1]) @property @@ -136,7 +138,7 @@ def visible(self) -> bool: return self._visible @visible.setter - def visible(self, value: bool): + def visible(self, value: bool) -> None: self._visible = value if value: sdl2.SDL_ShowWindow(self._window) @@ -155,7 +157,7 @@ def cursor(self) -> bool: return self._cursor @cursor.setter - def cursor(self, value: bool): + def cursor(self, value: bool) -> None: sdl2.SDL_ShowCursor(sdl2.SDL_ENABLE if value else sdl2.SDL_DISABLE) self._cursor = value @@ -176,7 +178,7 @@ def mouse_exclusivity(self) -> bool: return self._mouse_exclusivity @mouse_exclusivity.setter - def mouse_exclusivity(self, value: bool): + def mouse_exclusivity(self, value: bool) -> None: if value is True: sdl2.SDL_SetRelativeMouseMode(sdl2.SDL_TRUE) else: @@ -195,7 +197,7 @@ def title(self) -> str: return self._title @title.setter - def title(self, value: str): + def title(self, value: str) -> None: data = c_char_p(value.encode()) sdl2.SDL_SetWindowTitle(self._window, data) self._title = value @@ -207,7 +209,7 @@ def swap_buffers(self) -> None: self.process_events() self._frames += 1 - def resize(self, width, height) -> None: + def resize(self, width: int, height: int) -> None: """Resize callback. Args: @@ -228,7 +230,7 @@ def _handle_mods(self) -> None: self._modifiers.ctrl = mods & sdl2.KMOD_CTRL self._modifiers.alt = mods & sdl2.KMOD_ALT - def _set_icon(self, icon_path: str) -> None: + def _set_icon(self, icon_path: Path) -> None: sdl2.SDL_SetWindowIcon(self._window, sdl2.ext.load_image(icon_path)) def process_events(self) -> None: @@ -313,7 +315,7 @@ def process_events(self) -> None: elif event.window.event == sdl2.SDL_WINDOWEVENT_RESTORED: self._iconify_func(False) - def close(self): + def close(self) -> None: """Close the window""" super().close() self._close_func() diff --git a/moderngl_window/context/tk/__init__.py b/moderngl_window/context/tk/__init__.py index 3c1a632..d5d55c2 100644 --- a/moderngl_window/context/tk/__init__.py +++ b/moderngl_window/context/tk/__init__.py @@ -1,2 +1,2 @@ -from moderngl_window.context.tk.window import Window # noqa from moderngl_window.context.tk.keys import Keys # noqa +from moderngl_window.context.tk.window import Window # noqa diff --git a/moderngl_window/context/tk/window.py b/moderngl_window/context/tk/window.py index 5cc437e..2d370a0 100644 --- a/moderngl_window/context/tk/window.py +++ b/moderngl_window/context/tk/window.py @@ -1,9 +1,11 @@ -from typing import Tuple import tkinter +from pathlib import Path +from typing import Any + +from pyopengltk import OpenGLFrame from moderngl_window.context.base import BaseWindow from moderngl_window.context.tk.keys import Keys -from pyopengltk import OpenGLFrame class Window(BaseWindow): @@ -18,7 +20,7 @@ class Window(BaseWindow): 2: 3, } - def __init__(self, **kwargs): + def __init__(self, **kwargs: Any): super().__init__(**kwargs) self._tk = tkinter.Tk() @@ -61,8 +63,8 @@ def _set_vsync(self, value: bool) -> None: pass @property - def size(self) -> Tuple[int, int]: - """Tuple[int, int]: current window size. + def size(self) -> tuple[int, int]: + """tuple[int, int]: current window size. This property also support assignment:: @@ -72,12 +74,12 @@ def size(self) -> Tuple[int, int]: return self._width, self._height @size.setter - def size(self, value: Tuple[int, int]): + def size(self, value: tuple[int, int]) -> None: self._tk.geometry("{}x{}".format(value[0], value[1])) @property - def position(self) -> Tuple[int, int]: - """Tuple[int, int]: The current window position. + def position(self) -> tuple[int, int]: + """tuple[int, int]: The current window position. This property can also be set to move the window:: @@ -88,7 +90,7 @@ def position(self) -> Tuple[int, int]: return int(x), int(y) @position.setter - def position(self, value: Tuple[int, int]): + def position(self, value: tuple[int, int]) -> None: self._tk.geometry("+{}+{}".format(value[0], value[1])) @property @@ -103,7 +105,7 @@ def visible(self) -> bool: return self._visible @visible.setter - def visible(self, value: bool): + def visible(self, value: bool) -> None: self._visible = value if value: self._tk.deiconify() @@ -122,7 +124,7 @@ def cursor(self) -> bool: return self._cursor @cursor.setter - def cursor(self, value: bool): + def cursor(self, value: bool) -> None: if value is True: self._tk.config(cursor="arrow") else: @@ -141,7 +143,7 @@ def title(self) -> str: return self._title @title.setter - def title(self, value: str): + def title(self, value: str) -> None: self._tk.title(value) self._title = value @@ -158,10 +160,10 @@ def swap_buffers(self) -> None: self._gl_widget.tkSwapBuffers() self._frames += 1 - def _set_icon(self, icon_path: str) -> None: + def _set_icon(self, icon_path: Path) -> None: self._tk.iconphoto(False, tkinter.PhotoImage(file=icon_path)) - def tk_key_press(self, event: tkinter.Event) -> None: + def tk_key_press(self, event: tkinter.Event[Any]) -> None: """Handle all queued key press events in tkinter dispatching events to standard methods""" self._key_event_func(event.keysym, self.keys.ACTION_PRESS, self._modifiers) @@ -175,7 +177,7 @@ def tk_key_press(self, event: tkinter.Event) -> None: if self._fs_key is not None and event.keysym == self._fs_key: self.fullscreen = not self.fullscreen - def tk_key_release(self, event: tkinter.Event) -> None: + def tk_key_release(self, event: tkinter.Event[Any]) -> None: """Handle all queued key release events in tkinter dispatching events to standard methods Args: @@ -184,7 +186,7 @@ def tk_key_release(self, event: tkinter.Event) -> None: self._handle_modifiers(event, False) self._key_event_func(event.keysym, self.keys.ACTION_RELEASE, self._modifiers) - def tk_mouse_motion(self, event: tkinter.Event) -> None: + def tk_mouse_motion(self, event: tkinter.Event[Any]) -> None: """Handle and translate tkinter mouse position events Args: @@ -198,7 +200,7 @@ def tk_mouse_motion(self, event: tkinter.Event) -> None: else: self._mouse_position_event_func(x, y, dx, dy) - def tk_mouse_button_press(self, event: tkinter.Event) -> None: + def tk_mouse_button_press(self, event: tkinter.Event[Any]) -> None: """Handle tkinter mouse press events. Args: @@ -212,7 +214,7 @@ def tk_mouse_button_press(self, event: tkinter.Event) -> None: self._handle_mouse_button_state_change(button, True) self._mouse_press_event_func(event.x, event.y, button) - def tk_mouse_button_release(self, event: tkinter.Event) -> None: + def tk_mouse_button_release(self, event: tkinter.Event[Any]) -> None: """Handle tkinter mouse press events. Args: @@ -226,7 +228,7 @@ def tk_mouse_button_release(self, event: tkinter.Event) -> None: self._handle_mouse_button_state_change(button, False) self._mouse_release_event_func(event.x, event.y, button) - def tk_mouse_wheel(self, event: tkinter.Event) -> None: + def tk_mouse_wheel(self, event: tkinter.Event[Any]) -> None: """Handle mouse wheel event. Args: @@ -235,7 +237,7 @@ def tk_mouse_wheel(self, event: tkinter.Event) -> None: self._handle_modifiers(event, True) self._mouse_scroll_event_func(0, event.delta / 120.0) - def _handle_modifiers(self, event: tkinter.Event, press: bool) -> None: + def _handle_modifiers(self, event: tkinter.Event[Any], press: bool) -> None: """Update internal key modifiers Args: @@ -249,7 +251,7 @@ def _handle_modifiers(self, event: tkinter.Event, press: bool) -> None: elif event.keysym in ["Alt_L", "Alt_R"]: self._modifiers.alt = press - def tk_resize(self, event) -> None: + def tk_resize(self, event: tkinter.Event[Any]) -> None: """tkinter specific window resize event. Forwards resize events to the configured resize function. @@ -273,10 +275,10 @@ def tk_close_window(self) -> None: self._close_func() self._close = True - def tk_map(self, event): + def tk_map(self, event: tkinter.Event[Any]) -> None: self._iconify_func(False) - def tk_unmap(self, event): + def tk_unmap(self, event: tkinter.Event[Any]) -> None: self._iconify_func(True) def destroy(self) -> None: @@ -285,22 +287,22 @@ def destroy(self) -> None: class ModernglTkWindow(OpenGLFrame): - def __init__(self, *args, **kwargs): + def __init__(self, *args: Any, **kwargs: Any): super().__init__(*args, **kwargs) - def redraw(self): + def redraw(self) -> None: """pyopengltk's own render method.""" pass - def initgl(self): + def initgl(self) -> None: """pyopengltk's user code for initialization.""" pass - def tkResize(self, event): + def tkResize(self, event: tkinter.Event[Any]) -> None: """Should never be called. Event overridden.""" raise ValueError("tkResize should never be called. The event is overridden.") - def tkMap(self, event): + def tkMap(self, event: tkinter.Event[Any]) -> None: """Called when frame goes onto the screen""" # Only create context once # In a window like this we are not likely to lose the context diff --git a/moderngl_window/finders/base.py b/moderngl_window/finders/base.py index 9d8bb2a..0979ecd 100644 --- a/moderngl_window/finders/base.py +++ b/moderngl_window/finders/base.py @@ -4,9 +4,9 @@ import functools import logging - from collections import namedtuple from pathlib import Path +from typing import Any, Optional from moderngl_window.conf import settings from moderngl_window.exceptions import ImproperlyConfigured @@ -20,12 +20,12 @@ class BaseFilesystemFinder: """Base class for searching filesystem directories""" - settings_attr = None + settings_attr = "" """str: Name of the attribute in :py:class:`~moderngl_window.conf.Settings` containing a list of paths the finder should search in. """ - def __init__(self): + def __init__(self) -> None: """Initialize finder class by looking up the paths referenced in ``settings_attr``.""" if not hasattr(settings, self.settings_attr): raise ImproperlyConfigured( @@ -34,7 +34,7 @@ def __init__(self): ) self.paths = getattr(settings, self.settings_attr) - def find(self, path: Path) -> Path: + def find(self, path: Path) -> Optional[Path]: """Finds a file in the configured paths returning its absolute path. Args: @@ -72,13 +72,13 @@ def find(self, path: Path) -> Path: if abspath.exists(): logger.debug("found %s", abspath) - return abspath + return Path(abspath) # Needed to please mypy, but is already be a path return None @functools.lru_cache(maxsize=None) -def get_finder(import_path: str): +def get_finder(import_path: str) -> BaseFilesystemFinder: """ Get a finder class from an import path. This function uses an lru cache. @@ -91,9 +91,10 @@ def get_finder(import_path: str): ImproperlyConfigured is the finder is not found """ Finder = import_string(import_path) - if not issubclass(Finder, BaseFilesystemFinder): + find = Finder() + if not isinstance(find, BaseFilesystemFinder): raise ImproperlyConfigured( "Finder {} is not a subclass of .finders.FileSystemFinder".format(import_path) ) - return Finder() + return find diff --git a/moderngl_window/finders/data.py b/moderngl_window/finders/data.py index 40910e9..2225bec 100644 --- a/moderngl_window/finders/data.py +++ b/moderngl_window/finders/data.py @@ -1,5 +1,7 @@ -from moderngl_window.finders import base +from collections.abc import Iterable + from moderngl_window.conf import settings +from moderngl_window.finders import base class FilesystemFinder(base.BaseFilesystemFinder): @@ -8,6 +10,6 @@ class FilesystemFinder(base.BaseFilesystemFinder): settings_attr = "DATA_DIRS" -def get_finders(): +def get_finders() -> Iterable[base.BaseFilesystemFinder]: for finder in settings.DATA_FINDERS: yield base.get_finder(finder) diff --git a/moderngl_window/finders/program.py b/moderngl_window/finders/program.py index cb7f6ac..7dd0fdb 100644 --- a/moderngl_window/finders/program.py +++ b/moderngl_window/finders/program.py @@ -1,5 +1,7 @@ -from moderngl_window.finders import base +from collections.abc import Iterator + from moderngl_window.conf import settings +from moderngl_window.finders import base class FilesystemFinder(base.BaseFilesystemFinder): @@ -8,6 +10,6 @@ class FilesystemFinder(base.BaseFilesystemFinder): settings_attr = "PROGRAM_DIRS" -def get_finders(): +def get_finders() -> Iterator[base.BaseFilesystemFinder]: for finder in settings.PROGRAM_FINDERS: yield base.get_finder(finder) diff --git a/moderngl_window/finders/scene.py b/moderngl_window/finders/scene.py index 25475ae..cfa9dfe 100644 --- a/moderngl_window/finders/scene.py +++ b/moderngl_window/finders/scene.py @@ -1,5 +1,7 @@ -from moderngl_window.finders import base +from collections.abc import Iterator + from moderngl_window.conf import settings +from moderngl_window.finders import base class FilesystemFinder(base.BaseFilesystemFinder): @@ -8,6 +10,6 @@ class FilesystemFinder(base.BaseFilesystemFinder): settings_attr = "SCENE_DIRS" -def get_finders(): +def get_finders() -> Iterator[base.BaseFilesystemFinder]: for finder in settings.SCENE_FINDERS: yield base.get_finder(finder) diff --git a/moderngl_window/finders/texture.py b/moderngl_window/finders/texture.py index 77cfc2b..e0c4bdb 100644 --- a/moderngl_window/finders/texture.py +++ b/moderngl_window/finders/texture.py @@ -1,5 +1,7 @@ -from moderngl_window.finders import base +from collections.abc import Iterator + from moderngl_window.conf import settings +from moderngl_window.finders import base class FilesystemFinder(base.BaseFilesystemFinder): @@ -8,6 +10,6 @@ class FilesystemFinder(base.BaseFilesystemFinder): settings_attr = "TEXTURE_DIRS" -def get_finders(): +def get_finders() -> Iterator[base.BaseFilesystemFinder]: for finder in settings.TEXTURE_FINDERS: yield base.get_finder(finder) diff --git a/moderngl_window/geometry/__init__.py b/moderngl_window/geometry/__init__.py index d9e6727..c512434 100644 --- a/moderngl_window/geometry/__init__.py +++ b/moderngl_window/geometry/__init__.py @@ -1,5 +1,7 @@ -from moderngl_window.geometry.attributes import AttributeNames # noqa -from moderngl_window.geometry.cube import cube # noqa -from moderngl_window.geometry.bbox import bbox # noqa -from moderngl_window.geometry.sphere import sphere # noqa -from moderngl_window.geometry.quad import quad_2d, quad_fs # noqa +from moderngl_window.geometry.attributes import \ + AttributeNames as AttributeNames +from moderngl_window.geometry.bbox import bbox as bbox +from moderngl_window.geometry.cube import cube as cube +from moderngl_window.geometry.quad import quad_2d as quad_2d +from moderngl_window.geometry.quad import quad_fs as quad_fs +from moderngl_window.geometry.sphere import sphere as sphere diff --git a/moderngl_window/geometry/attributes.py b/moderngl_window/geometry/attributes.py index c2a0062..c1916cd 100644 --- a/moderngl_window/geometry/attributes.py +++ b/moderngl_window/geometry/attributes.py @@ -3,6 +3,8 @@ https://github.com/KhronosGroup/glTF/blob/master/specification/2.0/README.md#meshes """ +from typing import Any, Optional + class AttributeNames: """Standard buffer/attribute names. @@ -24,15 +26,15 @@ class AttributeNames: def __init__( self, - position: str = None, - normal: str = None, - tangent: str = None, - texcoord_0: str = None, - texcoord_1: str = None, - color_0: str = None, - joints_0: str = None, - weights: str = None, - **kwargs, + position: Optional[str] = None, + normal: Optional[str] = None, + tangent: Optional[str] = None, + texcoord_0: Optional[str] = None, + texcoord_1: Optional[str] = None, + color_0: Optional[str] = None, + joints_0: Optional[str] = None, + weights: Optional[str] = None, + **kwargs: Any, ): """Override default values. All attributes will be set on the instance as upper case strings @@ -61,7 +63,7 @@ def __init__( } ) - def apply_values(self, kwargs): + def apply_values(self, kwargs: dict[str, Any]) -> None: """Only applies attribute values not None""" for key, value in kwargs.items(): if value: diff --git a/moderngl_window/geometry/bbox.py b/moderngl_window/geometry/bbox.py index 436fb8c..cd9d90e 100644 --- a/moderngl_window/geometry/bbox.py +++ b/moderngl_window/geometry/bbox.py @@ -1,11 +1,13 @@ -import numpy +from typing import Optional import moderngl -from moderngl_window.opengl.vao import VAO +import numpy + from moderngl_window.geometry import AttributeNames +from moderngl_window.opengl.vao import VAO -def bbox(size=(1.0, 1.0, 1.0), name=None, attr_names=AttributeNames): +def bbox(size: tuple[float, float, float] = (1.0, 1.0, 1.0), name: Optional[str] = None, attr_names: type[AttributeNames] = AttributeNames) -> VAO: """ Generates a bounding box with (0.0, 0.0, 0.0) as the center. This is simply a box with ``LINE_STRIP`` as draw mode. diff --git a/moderngl_window/geometry/cube.py b/moderngl_window/geometry/cube.py index 2d72be3..1da50e3 100644 --- a/moderngl_window/geometry/cube.py +++ b/moderngl_window/geometry/cube.py @@ -1,16 +1,18 @@ +from typing import Optional + import numpy -from moderngl_window.opengl.vao import VAO from moderngl_window.geometry import AttributeNames +from moderngl_window.opengl.vao import VAO def cube( - size=(1.0, 1.0, 1.0), - center=(0.0, 0.0, 0.0), - normals=True, - uvs=True, - name=None, - attr_names=AttributeNames, + size: tuple[float, float, float] = (1.0, 1.0, 1.0), + center: tuple[float, float, float]=(0.0, 0.0, 0.0), + normals: bool = True, + uvs: bool = True, + name: Optional[str] = None, + attr_names: type[AttributeNames] = AttributeNames, ) -> VAO: """Creates a cube VAO with normals and texture coordinates diff --git a/moderngl_window/geometry/quad.py b/moderngl_window/geometry/quad.py index 8e5e6c6..945132b 100644 --- a/moderngl_window/geometry/quad.py +++ b/moderngl_window/geometry/quad.py @@ -1,11 +1,13 @@ -import numpy +from typing import Optional import moderngl -from moderngl_window.opengl.vao import VAO +import numpy + from moderngl_window.geometry.attributes import AttributeNames +from moderngl_window.opengl.vao import VAO -def quad_fs(attr_names=AttributeNames, normals=True, uvs=True, name=None) -> VAO: +def quad_fs(attr_names: type[AttributeNames] = AttributeNames, normals: bool = True, uvs: bool = True, name: Optional[str] = None) -> VAO: """ Creates a screen aligned quad using two triangles with normals and texture coordinates. @@ -27,12 +29,12 @@ def quad_fs(attr_names=AttributeNames, normals=True, uvs=True, name=None) -> VAO def quad_2d( - size=(1.0, 1.0), - pos=(0.0, 0.0), - normals=True, - uvs=True, - attr_names=AttributeNames, - name=None, + size: tuple[float, float] = (1.0, 1.0), + pos: tuple[float, float] = (0.0, 0.0), + normals: bool = True, + uvs: bool = True, + attr_names: type[AttributeNames] = AttributeNames, + name: Optional[str] = None, ) -> VAO: """ Creates a 2D quad VAO using 2 triangles with normals and texture coordinates. diff --git a/moderngl_window/geometry/sphere.py b/moderngl_window/geometry/sphere.py index 67f2587..5693e74 100644 --- a/moderngl_window/geometry/sphere.py +++ b/moderngl_window/geometry/sphere.py @@ -1,20 +1,21 @@ import math +from typing import Any, Optional +import moderngl as mlg import numpy -import moderngl as mlg -from moderngl_window.opengl.vao import VAO from moderngl_window.geometry import AttributeNames +from moderngl_window.opengl.vao import VAO def sphere( - radius=0.5, - sectors=32, - rings=16, - normals=True, - uvs=True, - name: str = None, - attr_names=AttributeNames, + radius: float = 0.5, + sectors: int = 32, + rings: int = 16, + normals: bool = True, + uvs: bool = True, + name: Optional[str] = None, + attr_names: type[AttributeNames] = AttributeNames, ) -> VAO: """Creates a sphere. @@ -32,9 +33,10 @@ def sphere( R = 1.0 / (rings - 1) S = 1.0 / (sectors - 1) - vertices = [0] * (rings * sectors * 3) - normals = [0] * (rings * sectors * 3) - uvs = [0] * (rings * sectors * 2) + # Use those names as normals and uvs are part of the API + vertices_l = [0.0] * (rings * sectors * 3) + normals_l = [0.0] * (rings * sectors * 3) + uvs_l = [0.0] * (rings * sectors * 2) v, n, t = 0, 0, 0 for r in range(rings): @@ -43,16 +45,16 @@ def sphere( x = math.cos(2 * math.pi * s * S) * math.sin(math.pi * r * R) z = math.sin(2 * math.pi * s * S) * math.sin(math.pi * r * R) - uvs[t] = s * S - uvs[t + 1] = r * R + uvs_l[t] = s * S + uvs_l[t + 1] = r * R - vertices[v] = x * radius - vertices[v + 1] = y * radius - vertices[v + 2] = z * radius + vertices_l[v] = x * radius + vertices_l[v + 1] = y * radius + vertices_l[v + 2] = z * radius - normals[n] = x - normals[n + 1] = y - normals[n + 2] = z + normals_l[n] = x + normals_l[n + 1] = y + normals_l[n + 2] = z t += 2 v += 3 @@ -73,15 +75,15 @@ def sphere( vao = VAO(name or "sphere", mode=mlg.TRIANGLES) - vbo_vertices = numpy.array(vertices, dtype=numpy.float32) + vbo_vertices = numpy.array(vertices_l, dtype=numpy.float32) vao.buffer(vbo_vertices, "3f", [attr_names.POSITION]) if normals: - vbo_normals = numpy.array(normals, dtype=numpy.float32) + vbo_normals = numpy.array(normals_l, dtype=numpy.float32) vao.buffer(vbo_normals, "3f", [attr_names.NORMAL]) if uvs: - vbo_uvs = numpy.array(uvs, dtype=numpy.float32) + vbo_uvs = numpy.array(uvs_l, dtype=numpy.float32) vao.buffer(vbo_uvs, "2f", [attr_names.TEXCOORD_0]) vbo_elements = numpy.array(indices, dtype=numpy.uint32) diff --git a/moderngl_window/integrations/imgui_bundle.py b/moderngl_window/integrations/imgui_bundle.py index 3eda968..147bd0c 100644 --- a/moderngl_window/integrations/imgui_bundle.py +++ b/moderngl_window/integrations/imgui_bundle.py @@ -1,8 +1,8 @@ import ctypes +import moderngl from imgui_bundle import imgui from imgui_bundle.python_backends import compute_fb_scale -import moderngl class ModernglWindowMixin: diff --git a/moderngl_window/loaders/base.py b/moderngl_window/loaders/base.py index c1ada3b..0589342 100644 --- a/moderngl_window/loaders/base.py +++ b/moderngl_window/loaders/base.py @@ -1,10 +1,14 @@ import logging +from collections.abc import Iterable from pathlib import Path -from typing import Any +from typing import Any, Optional, Union import moderngl + import moderngl_window as mglw from moderngl_window.finders import data, program, scene, texture +from moderngl_window.finders.base import BaseFilesystemFinder +from moderngl_window.meta.base import ResourceDescription logger = logging.getLogger(__name__) @@ -18,7 +22,7 @@ class BaseLoader: This can be used when file extensions is not enough to decide what loader should be selected. """ - file_extensions = [] + file_extensions: list[list[str]] = [] """ A list defining the file extensions accepted by this loader. @@ -31,7 +35,7 @@ class BaseLoader: ] """ - def __init__(self, meta): + def __init__(self, meta: ResourceDescription) -> None: """Initialize loader. Loaders take a ResourceDescription instance @@ -46,13 +50,13 @@ def __init__(self, meta): raise ValueError("Loader {} doesn't have a kind".format(self.__class__)) @classmethod - def supports_file(cls, meta): + def supports_file(cls: type["BaseLoader"], meta: ResourceDescription) -> bool: """Check if the loader has a supported file extension. What extensions are supported can be defined in the :py:attr:`file_extensions` class attribute. """ - path = Path(meta.path) + path = Path(meta.path if meta.path is not None else "") for ext in cls.file_extensions: if path.suffixes[: len(ext)] == ext: @@ -71,7 +75,7 @@ def load(self) -> Any: """ raise NotImplementedError() - def find_data(self, path): + def find_data(self, path: Optional[Union[str, Path]]) -> Optional[Path]: """Find resource using data finders. This is mainly a shortcut method to simplify the task. @@ -79,9 +83,9 @@ def find_data(self, path): Args: path: Path to resource """ - return self._find(Path(path), data.get_finders()) + return self._find(path, data.get_finders()) - def find_program(self, path): + def find_program(self, path: Optional[Union[str, Path]]) -> Optional[Path]: """Find resource using program finders. This is mainly a shortcut method to simplify the task. @@ -89,9 +93,9 @@ def find_program(self, path): Args: path: Path to resource """ - return self._find(Path(path), program.get_finders()) + return self._find(path, program.get_finders()) - def find_texture(self, path): + def find_texture(self, path: Optional[Union[str, Path]]) -> Optional[Path]: """Find resource using texture finders. This is mainly a shortcut method to simplify the task. @@ -99,9 +103,9 @@ def find_texture(self, path): Args: path: Path to resource """ - return self._find(Path(path), texture.get_finders()) + return self._find(path, texture.get_finders()) - def find_scene(self, path): + def find_scene(self, path: Optional[Union[str, Path]]) -> Optional[Path]: """Find resource using scene finders. This is mainly a shortcut method to simplify the task. @@ -109,18 +113,20 @@ def find_scene(self, path): Args: path: Path to resource """ - return self._find(Path(path), scene.get_finders()) + return self._find(path, scene.get_finders()) - def _find(self, path: Path, finders: list): + def _find(self, path: Optional[Union[str, Path]], finders: Iterable[BaseFilesystemFinder]) -> Optional[Path]: """Find the first occurrance of this path in all finders. If the incoming path is an absolute path we assume this path exist and return it. Args: - path (Path): The path to find + path (str): The path to find """ if not path: return None + if isinstance(path, str): + path = Path(path) if path.is_absolute(): return path diff --git a/moderngl_window/loaders/data/binary.py b/moderngl_window/loaders/data/binary.py index 9b66a6f..19e8503 100644 --- a/moderngl_window/loaders/data/binary.py +++ b/moderngl_window/loaders/data/binary.py @@ -1,7 +1,7 @@ import logging -from moderngl_window.loaders.base import BaseLoader from moderngl_window.exceptions import ImproperlyConfigured +from moderngl_window.loaders.base import BaseLoader logger = logging.getLogger(__name__) diff --git a/moderngl_window/loaders/data/json.py b/moderngl_window/loaders/data/json.py index c8d1190..a52bcf6 100644 --- a/moderngl_window/loaders/data/json.py +++ b/moderngl_window/loaders/data/json.py @@ -1,8 +1,9 @@ import json import logging +from typing import Any -from moderngl_window.loaders.base import BaseLoader from moderngl_window.exceptions import ImproperlyConfigured +from moderngl_window.loaders.base import BaseLoader logger = logging.getLogger(__name__) @@ -13,12 +14,13 @@ class Loader(BaseLoader): [".json"], ] - def load(self) -> dict: + def load(self) -> dict[Any, Any]: """Load a file as json Returns: dict: The json contents """ + assert self.meta.path is not None, "the path is empty for this loader" self.meta.resolved_path = self.find_data(self.meta.path) if not self.meta.resolved_path: diff --git a/moderngl_window/loaders/data/text.py b/moderngl_window/loaders/data/text.py index ea4a23e..62fc983 100644 --- a/moderngl_window/loaders/data/text.py +++ b/moderngl_window/loaders/data/text.py @@ -18,6 +18,7 @@ def load(self) -> str: Returns: str: The string contents of the file """ + assert self.meta.path is not None, "the path is empty for this loader" self.meta.resolved_path = self.find_data(self.meta.path) if not self.meta.resolved_path: diff --git a/moderngl_window/loaders/program/separate.py b/moderngl_window/loaders/program/separate.py index 19efa80..406fdc7 100644 --- a/moderngl_window/loaders/program/separate.py +++ b/moderngl_window/loaders/program/separate.py @@ -1,16 +1,19 @@ -from typing import Union - import logging +from pathlib import Path +from typing import Optional, Union + import moderngl + +from moderngl_window.exceptions import ImproperlyConfigured from moderngl_window.loaders.base import BaseLoader from moderngl_window.opengl import program -from moderngl_window.exceptions import ImproperlyConfigured logger = logging.getLogger(__name__) class Loader(BaseLoader): kind = "separate" + meta: program.ProgramDescription def load( self, @@ -22,7 +25,7 @@ def load( Returns: moderngl.Program: The Program instance """ - prog = None + prog: Union[moderngl.Program, moderngl.ComputeShader, program.ReloadableProgram] vs_source = self._load_shader("vertex", self.meta.vertex_shader) geo_source = self._load_shader("geometry", self.meta.geometry_shader) @@ -58,9 +61,9 @@ def load( return prog - def _load_shader(self, shader_type: str, path: str): + def _load_shader(self, shader_type: str, path: Optional[str]) -> Optional[str]: """Load a single shader source""" - if path: + if path is not None: resolved_path = self.find_program(path) if not resolved_path: raise ImproperlyConfigured("Cannot find {} shader '{}'".format(shader_type, path)) @@ -69,17 +72,18 @@ def _load_shader(self, shader_type: str, path: str): with open(str(resolved_path), "r") as fd: return fd.read() + return None - def _load_source(self, path): + def _load_source(self, path: Union[Path, str]) -> tuple[Path, str]: """Finds and loads a single source file. Args: path: Path to resource Returns: - Tuple[resolved_path, source]: The resolved path and the source + tuple[resolved_path, source]: The resolved path and the source """ resolved_path = self.find_program(path) - if not resolved_path: + if resolved_path is None: raise ImproperlyConfigured("Cannot find program '{}'".format(path)) logger.info("Loading: %s", path) diff --git a/moderngl_window/loaders/program/single.py b/moderngl_window/loaders/program/single.py index 7631ab5..c647cc0 100644 --- a/moderngl_window/loaders/program/single.py +++ b/moderngl_window/loaders/program/single.py @@ -1,15 +1,19 @@ import logging +from pathlib import Path +from typing import Union import moderngl + +from moderngl_window.exceptions import ImproperlyConfigured from moderngl_window.loaders.base import BaseLoader from moderngl_window.opengl import program -from moderngl_window.exceptions import ImproperlyConfigured logger = logging.getLogger(__name__) class Loader(BaseLoader): kind = "single" + meta: program.ProgramDescription def load(self) -> moderngl.Program: """Loads a shader program from a single glsl file. @@ -53,6 +57,10 @@ def load(self) -> moderngl.Program: Returns: moderngl.Program: The Program instance """ + prog: Union[moderngl.Program, program.ReloadableProgram] + assert self.meta.path is not None, "There is no path for the resource" + assert self.meta.path is not None, "There is no path for the resource" + self.meta.resolved_path, source = self._load_source(self.meta.path) shaders = program.ProgramShaders.from_single(self.meta, source) shaders.handle_includes(self._load_source) @@ -67,13 +75,13 @@ def load(self) -> moderngl.Program: return prog - def _load_source(self, path): + def _load_source(self, path: Union[Path, str]) -> tuple[Path, str]: """Finds and loads a single source file. Args: path: Path to resource Returns: - Tuple[resolved_path, source]: The resolved path and the source + tuple[resolved_path, source]: The resolved path and the source """ resolved_path = self.find_program(path) if not resolved_path: diff --git a/moderngl_window/loaders/scene/gltf2.py b/moderngl_window/loaders/scene/gltf2.py index cc5d361..bb62d8b 100644 --- a/moderngl_window/loaders/scene/gltf2.py +++ b/moderngl_window/loaders/scene/gltf2.py @@ -1,24 +1,29 @@ # Spec: https://github.com/KhronosGroup/glTF/blob/master/specification/2.0/README.md#asset +from __future__ import annotations + import base64 import io import json import logging import struct from collections import namedtuple +from pathlib import Path +from typing import Any, Optional, Union +import glm +import moderngl import numpy +import numpy.typing as npt from PIL import Image -import glm -import moderngl import moderngl_window - +from moderngl_window.exceptions import ImproperlyConfigured from moderngl_window.loaders.base import BaseLoader from moderngl_window.loaders.texture import t2d -from moderngl_window.opengl.vao import VAO from moderngl_window.meta import SceneDescription, TextureDescription +from moderngl_window.opengl.vao import VAO +from moderngl_window.resources.textures import Textures from moderngl_window.scene import Material, MaterialTexture, Mesh, Node, Scene -from moderngl_window.exceptions import ImproperlyConfigured logger = logging.getLogger(__name__) @@ -74,7 +79,9 @@ class Loader(BaseLoader): ] #: Supported GLTF extensions #: https://github.com/KhronosGroup/glTF/tree/master/extensions - supported_extensions = [] + supported_extensions: list[str] = [] + + meta: SceneDescription def __init__(self, meta: SceneDescription): """Initialize loading GLTF 2 scene. @@ -86,17 +93,17 @@ def __init__(self, meta: SceneDescription): - glb Binary format """ super().__init__(meta) - self.scenes = [] - self.nodes = [] - self.meshes = [] - self.materials = [] - self.images = [] - self.samplers = [] - self.textures = [] - - self.path = None - self.scene = None - self.gltf = None + self.scenes: list[Scene] = [] + self.nodes: list[Node] = [] + self.meshes: list[list[Mesh]] = [] + self.materials: list[Material] = [] + self.images: list[moderngl.Texture] = [] + self.samplers: list[moderngl.Sampler] = [] + self.textures: list[MaterialTexture] = [] + + self.path: Optional[Path] = None + self.scene: Scene + self.gltf: GLTFMeta def load(self) -> Scene: """Load a GLTF 2 scene including referenced textures. @@ -104,11 +111,12 @@ def load(self) -> Scene: Returns: Scene: The scene instance """ + assert self.meta.path is not None, "The path to this resource is empty" self.path = self.find_scene(self.meta.path) if not self.path: raise ImproperlyConfigured("Scene '{}' not found".format(self.meta.path)) - self.scene = Scene(self.path) + self.scene = Scene(str(self.path)) # Load gltf json file if self.path.suffix == ".gltf": @@ -118,6 +126,8 @@ def load(self) -> Scene: if self.path.suffix == ".glb": self.load_glb() + assert self.gltf is not None, "There is a problem with your file, could not load gltf" + self.gltf.check_version() self.gltf.check_extensions(self.supported_extensions) self.load_images() @@ -132,19 +142,19 @@ def load(self) -> Scene: return self.scene - def load_gltf(self): + def load_gltf(self) -> None: """Loads a gltf json file parsing its contents""" with open(str(self.path)) as fd: - self.gltf = GLTFMeta(self.path, json.load(fd), self.meta) + self.gltf = GLTFMeta(str(self.path), json.load(fd), self.meta) - def load_glb(self): + def load_glb(self) -> None: """Loads a binary gltf file parsing its contents""" with open(str(self.path), "rb") as fd: # Check header magic = fd.read(4) if magic != GLTF_MAGIC_HEADER: raise ValueError( - "{} has incorrect header {} != {}".format(self.path, magic, GLTF_MAGIC_HEADER) + "{} has incorrect header {!r} != {!r}".format(self.path, magic, GLTF_MAGIC_HEADER) ) version = struct.unpack(" None: """Load images referenced in gltf metadata""" for image in self.gltf.images: self.images.append(image.load(self.path.parent)) - def load_samplers(self): + def load_samplers(self) -> None: """Load samplers referenced in gltf metadata""" for sampler in self.gltf.samplers: # Use a sane default sampler if the sampler data is empty @@ -208,7 +218,7 @@ def load_samplers(self): ) ) - def load_textures(self): + def load_textures(self) -> None: """Load textures referenced in gltf metadata""" for texture_meta in self.gltf.textures: texture = MaterialTexture() @@ -221,7 +231,7 @@ def load_textures(self): self.textures.append(texture) - def load_meshes(self): + def load_meshes(self) -> None: """Load meshes referenced in gltf metadata""" for meta_mesh in self.gltf.meshes: # Returns a list of meshes @@ -231,12 +241,12 @@ def load_meshes(self): for mesh in meshes: self.scene.meshes.append(mesh) - def load_materials(self): + def load_materials(self) -> None: """Load materials referenced in gltf metadata""" # Create material objects for meta_mat in self.gltf.materials: mat = Material(meta_mat.name) - mat.color = meta_mat.baseColorFactor or [1.0, 1.0, 1.0, 1.0] + mat.color = meta_mat.baseColorFactor or (1.0, 1.0, 1.0, 1.0) mat.double_sided = meta_mat.doubleSided if meta_mat.baseColorTexture is not None: @@ -245,14 +255,14 @@ def load_materials(self): self.materials.append(mat) self.scene.materials.append(mat) - def load_nodes(self): + def load_nodes(self) -> None: """Load nodes referenced in gltf metadata""" # Start with root nodes in the scene for node_id in self.gltf.scenes[0].nodes: node = self.load_node(self.gltf.nodes[node_id]) self.scene.root_nodes.append(node) - def load_node(self, meta, parent=None): + def load_node(self, meta: GLTFNode, parent: Optional[Node] = None) -> Node: """Load a single node""" # Create the node node = Node(name=meta.name) @@ -289,13 +299,13 @@ def load_node(self, meta, parent=None): class GLTFMeta: """Container for gltf metadata""" - def __init__(self, path, data, meta, binary_buffer=None): + def __init__(self, path: Union[Path, str], data: dict[Any, Any], meta: SceneDescription, binary_buffer: Optional[bytes] = None) -> None: """ :param file: GLTF file name loaded :param data: Metadata (json loaded) :param binary_buffer: Binary buffer when loading glb files """ - self.path = path + self.path = Path(path) if isinstance(path, str) else path self.data = data self.meta = meta @@ -335,7 +345,7 @@ def __init__(self, path, data, meta, binary_buffer=None): self.buffers_exist() self.images_exist() - def _link_data(self): + def _link_data(self) -> None: """Add references""" # accessors -> buffer_views -> buffers for acc in self.accessors: @@ -347,8 +357,8 @@ def _link_data(self): # Link accessors to mesh primitives for mesh in self.meshes: for primitive in mesh.primitives: - if getattr(primitive, "indices", None) is not None: - primitive.indices = self.accessors[primitive.indices] + if primitive.indices is not None: + primitive.accessor = self.accessors[primitive.indices] for name, value in primitive.attributes.items(): primitive.attributes[name] = self.accessors[value] @@ -358,10 +368,10 @@ def _link_data(self): image.bufferView = self.buffer_views[image.bufferViewId] @property - def version(self): + def version(self) -> str: return self.asset.version - def check_version(self, required="2.0"): + def check_version(self, required: str = "2.0") -> None: if not self.version == required: msg = ( f"GLTF Format version is not 2.0. Version states '{self.version}' " @@ -369,22 +379,23 @@ def check_version(self, required="2.0"): ) raise ValueError(msg) - def check_extensions(self, supported): + def check_extensions(self, supported: list[str]) -> None: """ "extensionsRequired": ["KHR_draco_mesh_compression"], "extensionsUsed": ["KHR_draco_mesh_compression"] """ - if self.data.get("extensionsRequired"): - for ext in self.data.get("extensionsRequired"): + extReq = self.data.get("extensionsRequired") + if extReq is not None: + for ext in extReq: if ext not in supported: raise ValueError(f"Extension {ext} not supported") - - if self.data.get("extensionsUsed"): - for ext in self.data.get("extensionsUsed"): + extUse = self.data.get("extensionsUsed") + if extUse is not None: + for ext in extUse: if ext not in supported: raise ValueError("Extension {ext} not supported") - def buffers_exist(self): + def buffers_exist(self) -> None: """Checks if the bin files referenced exist""" for buff in self.buffers: if not buff.is_separate_file: @@ -396,7 +407,7 @@ def buffers_exist(self): "Buffer {} referenced in {} not found".format(path, self.path) ) - def images_exist(self): + def images_exist(self) -> None: """checks if the images references in textures exist""" pass @@ -404,26 +415,31 @@ def images_exist(self): class GLTFAsset: """Asset Information""" - def __init__(self, data): - self.version = data.get("version") - self.generator = data.get("generator") - self.copyright = data.get("copyright") + def __init__(self, data: dict[str, str]): + self.version = data.get("version", "") + self.generator = data.get("generator", "") + self.copyright = data.get("copyright", "") class GLTFMesh: - def __init__(self, data: dict, meta: SceneDescription): - class Primitives: - def __init__(self, data): - self.attributes = data.get("attributes") - self.indices = data.get("indices") - self.mode = data.get("mode") - self.material = data.get("material") + class Primitives: + mode: int | None + accessor: GLTFAccessor | None + + def __init__(self, data: dict[str, Any]): + self.attributes: dict[str, Any] = data.get("attributes", {}) + self.indices = data.get("indices", 0) + self.mode = data.get("mode") + self.material = data.get("material", 0) + self.accessor = None + + def __init__(self, data: dict[str, Any], meta: SceneDescription): self.meta = meta - self.name = data.get("name") - self.primitives = [Primitives(p) for p in data.get("primitives")] + self.name = data.get("name", "") + self.primitives = [GLTFMesh.Primitives(p) for p in data["primitives"]] - def load(self, materials): + def load(self, materials: list[Material]) -> list[Mesh]: name_map = { "POSITION": self.meta.attr_names.POSITION, "NORMAL": self.meta.attr_names.NORMAL, @@ -489,17 +505,17 @@ def load(self, materials): return meshes - def load_indices(self, primitive): + def load_indices(self, primitive: Primitives) -> tuple[ComponentType, npt.NDArray[Any]] | tuple[None, None]: """Loads the index buffer / polygon list for a primitive""" - if getattr(primitive, "indices") is None: + if primitive.indices is None or primitive.accessor is None: return None, None - _, component_type, buffer = primitive.indices.read() + _, component_type, buffer = primitive.accessor.read() return component_type, buffer - def prepare_attrib_mapping(self, primitive): + def prepare_attrib_mapping(self, primitive: Primitives) -> list[VBOInfo]: """Pre-parse buffer mappings for each VBO to detect interleaved data for a primitive""" - buffer_info = [] + buffer_info: list[VBOInfo] = [] for name, accessor in primitive.attributes.items(): info = VBOInfo(*accessor.info()) info.attributes.append((name, info.components)) @@ -513,10 +529,10 @@ def prepare_attrib_mapping(self, primitive): return buffer_info - def get_bbox(self, primitive): + def get_bbox(self, primitive: Primitives) -> tuple[glm.vec3, glm.vec3]: """Get the bounding box for the mesh""" accessor = primitive.attributes.get("POSITION") - return accessor.min, accessor.max + return glm.vec3(accessor.min), glm.vec3(accessor.max) class VBOInfo: @@ -524,13 +540,13 @@ class VBOInfo: def __init__( self, - buffer=None, - buffer_view=None, - byte_length=None, - byte_offset=None, - component_type=None, - components=None, - count=None, + buffer: Optional[GLTFBuffer] = None, + buffer_view: Optional[GLTFBuffer]=None, + byte_length: int = 0, + byte_offset: int = 0, + component_type: ComponentType = ComponentType("", 0, 0), + components: int = 0, + count: int = 0, ): self.buffer = buffer # reference to the buffer self.buffer_view = buffer_view @@ -541,19 +557,20 @@ def __init__( self.count = count # number of elements of the component type size # list of (name, components) tuples - self.attributes = [] + self.attributes: list[Any] = [] - def interleaves(self, info): + def interleaves(self, info: VBOInfo) -> bool: """Does the buffer interleave with this one?""" - return info.byte_offset == self.component_type.size * self.components + return bool(info.byte_offset == (self.component_type.size * self.components)) - def merge(self, info): + def merge(self, info: VBOInfo) -> None: # NOTE: byte length is the same self.components += info.components self.attributes += info.attributes - def create(self): + def create(self) -> tuple[type[object], npt.NDArray[Any]]: """Create the VBO""" + assert self.buffer is not None, "No buffer defined" dtype = NP_COMPONENT_DTYPE[self.component_type.value] data = numpy.frombuffer( self.buffer.read(byte_length=self.byte_length, byte_offset=self.byte_offset), @@ -562,41 +579,42 @@ def create(self): ) return dtype, data - def __str__(self): + def __str__(self) -> str: + assert self.buffer is not None, "No buffer defined" + assert self.buffer_view is not None, "No buffer_view defined" return ( "VBOInfo str: return str(self) class GLTFAccessor: - def __init__(self, accessor_id, data): + def __init__(self, accessor_id: int, data: dict[str, Any]): self.id = accessor_id - self.bufferViewId = data.get("bufferView") or 0 - self.bufferView = None - self.byteOffset = data.get("byteOffset") or 0 + self.bufferViewId = data.get("bufferView", 0) + self.bufferView: GLTFBufferView + self.byteOffset = data.get("byteOffset", 0) self.componentType = COMPONENT_TYPE[data["componentType"]] - self.count = data.get("count") + self.count = data.get("count", 1) self.min = numpy.array(data.get("min") or [-0.5, -0.5, -0.5], dtype="f4") self.max = numpy.array(data.get("max") or [0.5, 0.5, 0.5], dtype="f4") - self.type = data.get("type") + self.type = data.get("type", "") - def read(self): + def read(self) -> tuple[int, ComponentType, npt.NDArray[Any]]: """ Reads buffer data :return: component count, component type, data @@ -613,7 +631,7 @@ def read(self): ), ) - def info(self): + def info(self) -> tuple[GLTFBuffer, GLTFBufferView, int, int, ComponentType, int, int]: """ Get underlying buffer info for this accessor :return: buffer, byte_length, byte_offset, component_type, count @@ -631,16 +649,16 @@ def info(self): class GLTFBufferView: - def __init__(self, view_id, data): + def __init__(self, view_id: int, data: dict[str, Any]): self.id = view_id - self.bufferId = data.get("buffer") - self.buffer = None - self.byteOffset = data.get("byteOffset") or 0 - self.byteLength = data.get("byteLength") - self.byteStride = data.get("byteStride") or 0 + self.bufferId = data.get("buffer", 0) + self.buffer: GLTFBuffer + self.byteOffset = data.get("byteOffset", 0) + self.byteLength = data.get("byteLength", 0) + self.byteStride = data.get("byteStride", 0) # Valid: 34962 (ARRAY_BUFFER) and 34963 (ELEMENT_ARRAY_BUFFER) or None - def read(self, byte_offset=0, dtype=None, count=0): + def read(self, byte_offset: int = 0, dtype: Optional[type[object]] = None, count: int = 0) -> npt.NDArray[Any]: data = self.buffer.read( byte_offset=byte_offset + self.byteOffset, byte_length=self.byteLength, @@ -648,10 +666,10 @@ def read(self, byte_offset=0, dtype=None, count=0): vbo = numpy.frombuffer(data, count=count, dtype=dtype) return vbo - def read_raw(self): + def read_raw(self) -> bytes: return self.buffer.read(byte_length=self.byteLength, byte_offset=self.byteOffset) - def info(self, byte_offset=0): + def info(self, byte_offset: int = 0) -> tuple[GLTFBuffer, int, int]: """ Get the underlying buffer info :param byte_offset: byte offset from accessor @@ -661,49 +679,52 @@ def info(self, byte_offset=0): class GLTFBuffer: - def __init__(self, buffer_id, data, path): + def __init__(self, buffer_id: int, data: dict[str, str], path: Path): self.id = buffer_id self.path = path self.byteLength = data.get("byteLength") - self.uri = data.get("uri") - self.data = None + uri = data.get("uri") + if uri is None: + uri = "" + self.uri = uri + self.data = b"" @property - def has_data_uri(self): + def has_data_uri(self) -> bool: """Is data embedded in json?""" - if not self.uri: + if self.uri == "": return False return self.uri.startswith("data:") @property - def is_separate_file(self): + def is_separate_file(self) -> bool: """Buffer represents an independent bin file?""" return self.uri is not None and not self.has_data_uri - def open(self): - if self.data: + def open(self) -> None: + if self.data != b"": return if self.has_data_uri: self.data = base64.b64decode(self.uri[self.uri.find(",") + 1 :]) return - with open(str(self.path / self.uri), "rb") as fd: + with open(str(self.path / (self.uri if self.uri is not None else "")), "rb") as fd: self.data = fd.read() - def read(self, byte_offset=0, byte_length=0): + def read(self, byte_offset: int = 0, byte_length: int = 0) -> bytes: self.open() return self.data[byte_offset : byte_offset + byte_length] class GLTFScene: - def __init__(self, data): + def __init__(self, data: dict[str, list[int]]): self.nodes = data["nodes"] class GLTFNode: - def __init__(self, data): + def __init__(self, data: dict[str, Any]) -> None: self.name = data.get("name") self.children = data.get("children") self.matrix = data.get("matrix") @@ -714,7 +735,7 @@ def __init__(self, data): self.rotation = data.get("rotation") self.scale = data.get("scale") - if self.matrix: + if self.matrix is not None: self.matrix = glm.mat4(*self.matrix) else: self.matrix = glm.mat4() @@ -735,18 +756,18 @@ def __init__(self, data): self.matrix = self.matrix * glm.scale(self.scale) @property - def has_children(self): + def has_children(self) -> bool: return self.children is not None and len(self.children) > 0 @property - def is_resource_node(self): + def is_resource_node(self) -> bool: """Is this just a reference node to a resource?""" return self.camera is not None or self.mesh is not None class GLTFMaterial: - def __init__(self, data): - self.name = data.get("name") + def __init__(self, data: dict[str, Any]): + self.name = data["name"] # Defaults to true if not defined self.doubleSided = data.get("doubleSided") or True @@ -763,13 +784,13 @@ class GLTFImage: May be a file, embedded data or pointer to data in bufferview """ - def __init__(self, data): + def __init__(self, data: dict[str, Any]): self.uri = data.get("uri") self.bufferViewId = data.get("bufferView") self.bufferView = None self.mimeType = data.get("mimeType") - def load(self, path): + def load(self, path: Path) -> moderngl.Texture: # data:image/png;base64,iVBOR # Image is stored in bufferView @@ -781,7 +802,7 @@ def load(self, path): image = Image.open(io.BytesIO(base64.b64decode(data))) logger.info("Loading embedded image") else: - path = path / self.uri + path = path / Path(self.uri if self.uri is not None else "") logger.info("Loading: %s", self.uri) image = Image.open(path) @@ -799,21 +820,21 @@ def load(self, path): class GLTFTexture: - def __init__(self, data): - self.sampler = data.get("sampler") - self.source = data.get("source") + def __init__(self, data: dict[str, int]): + self.sampler = data["sampler"] + self.source = data["source"] class GLTFSampler: - def __init__(self, data): - self.magFilter = data.get("magFilter") - self.minFilter = data.get("minFilter") - self.wrapS = data.get("wrapS") - self.wrapT = data.get("wrapT") + def __init__(self, data: dict[str, int]): + self.magFilter = data["magFilter"] + self.minFilter = data["minFilter"] + self.wrapS = data["wrapS"] + self.wrapT = data["wrapT"] class GLTFCamera: - def __init__(self, data): + def __init__(self, data: dict[str, str]): self.data = data # "perspective": { # "aspectRatio": 1.0, diff --git a/moderngl_window/loaders/scene/stl.py b/moderngl_window/loaders/scene/stl.py index 9812576..8734a8d 100644 --- a/moderngl_window/loaders/scene/stl.py +++ b/moderngl_window/loaders/scene/stl.py @@ -1,13 +1,15 @@ import gzip +from pathlib import Path +from typing import Union import moderngl import numpy import trimesh +from moderngl_window.exceptions import ImproperlyConfigured from moderngl_window.loaders.base import BaseLoader from moderngl_window.opengl.vao import VAO from moderngl_window.scene import Material, Mesh, Node, Scene -from moderngl_window.exceptions import ImproperlyConfigured class Loader(BaseLoader): @@ -32,7 +34,12 @@ def load(self) -> Scene: file_obj = gzip.GzipFile(file_obj) stl_mesh = trimesh.load(file_obj, file_type="stl") - scene = Scene(self.meta.resolved_path) + path = self.meta.resolved_path + if isinstance(path, Path): + resolved = path.as_posix() + else: + resolved = None + scene = Scene(resolved) scene_mesh = Mesh("mesh") scene_mesh.material = Material("default") diff --git a/moderngl_window/loaders/scene/wavefront.py b/moderngl_window/loaders/scene/wavefront.py index 52de149..f7f41c2 100644 --- a/moderngl_window/loaders/scene/wavefront.py +++ b/moderngl_window/loaders/scene/wavefront.py @@ -1,27 +1,27 @@ +import io import logging import os +from pathlib import Path +import moderngl import numpy - import pywavefront from pywavefront import cache from pywavefront.obj import ObjParser -import moderngl +from moderngl_window import resources +from moderngl_window.exceptions import ImproperlyConfigured +from moderngl_window.geometry.attributes import AttributeNames from moderngl_window.loaders.base import BaseLoader +from moderngl_window.meta import SceneDescription, TextureDescription from moderngl_window.opengl.vao import VAO -from moderngl_window import resources from moderngl_window.resources.decorators import texture_dirs -from moderngl_window.meta import SceneDescription, TextureDescription from moderngl_window.scene import Material, MaterialTexture, Mesh, Node, Scene -from moderngl_window.exceptions import ImproperlyConfigured -from moderngl_window.geometry.attributes import AttributeNames - logger = logging.getLogger(__name__) -def translate_buffer_format(vertex_format: str, attr_names: AttributeNames): +def translate_buffer_format(vertex_format: str, attr_names: AttributeNames) -> tuple[str, list[str], list[tuple[str, str, int]]]: """Translate the buffer format""" buffer_format = [] attributes = [] @@ -52,15 +52,15 @@ def translate_buffer_format(vertex_format: str, attr_names: AttributeNames): class VAOCacheLoader(cache.CacheLoader): """Load geometry data directly into vaos""" - attr_names = None + attr_names: AttributeNames - def load_vertex_buffer(self, fd, material, length): + def load_vertex_buffer(self, fd: io.TextIOWrapper, material: pywavefront.material.Material, length: int) -> None: buffer_format, attributes, mesh_attributes = translate_buffer_format( material.vertex_format, self.attr_names ) vao = VAO(material.name, mode=moderngl.TRIANGLES) - vao.buffer(fd.read(length), buffer_format, attributes) + vao.buffer(fd.read(length).encode(), buffer_format, attributes) setattr(material, "vao", vao) setattr(material, "buffer_format", buffer_format) @@ -80,17 +80,18 @@ class Loader(BaseLoader): [".obj", ".gz"], [".bin"], ] + meta: SceneDescription def __init__(self, meta: SceneDescription): super().__init__(meta) - def load(self): + def load(self) -> Scene: """Loads a wavefront/obj file including materials and textures Returns: Scene: The Scene instance """ - path = self.find_scene(self.meta.path) + path = self.find_scene(Path(self.meta.path if self.meta.path is not None else "")) logger.info("loading %s", path) if not path: @@ -102,8 +103,8 @@ def load(self): VAOCacheLoader.attr_names = self.meta.attr_names data = pywavefront.Wavefront(str(path), create_materials=True, cache=self.meta.cache) - scene = Scene(self.meta.resolved_path) - texture_cache = {} + scene = Scene(self.meta.resolved_path.as_posix() if self.meta.resolved_path is not None else "") + texture_cache: dict[str, pywavefront.material.Material] = {} for _, mat in data.materials.items(): mesh = Mesh(mat.name) diff --git a/moderngl_window/loaders/texture/array.py b/moderngl_window/loaders/texture/array.py index 6c56666..df9c794 100644 --- a/moderngl_window/loaders/texture/array.py +++ b/moderngl_window/loaders/texture/array.py @@ -1,18 +1,23 @@ -from moderngl_window.loaders.texture.pillow import PillowLoader, image_data +import moderngl + from moderngl_window.exceptions import ImproperlyConfigured +from moderngl_window.loaders.texture.pillow import PillowLoader, image_data +from moderngl_window.meta.base import ResourceDescription +from moderngl_window.meta.texture import TextureDescription class Loader(PillowLoader): kind = "array" + meta: TextureDescription - def __init__(self, meta): + def __init__(self, meta: ResourceDescription): super().__init__(meta) self.layers = self.meta.layers if self.layers is None: raise ImproperlyConfigured("TextureArray requires layers parameter") - def load(self): + def load(self) -> moderngl.TextureArray: """Load a texture array as described by the supplied ``TextureDescription``` Returns: diff --git a/moderngl_window/loaders/texture/cube.py b/moderngl_window/loaders/texture/cube.py index 8605d5f..127b9be 100644 --- a/moderngl_window/loaders/texture/cube.py +++ b/moderngl_window/loaders/texture/cube.py @@ -1,18 +1,24 @@ from collections import namedtuple +from typing import Any, Optional + +import moderngl -from moderngl_window.loaders.texture.pillow import PillowLoader, image_data from moderngl_window.exceptions import ImproperlyConfigured +from moderngl_window.loaders.texture.pillow import PillowLoader, image_data +from moderngl_window.meta.base import ResourceDescription +from moderngl_window.meta.texture import TextureDescription FaceInfo = namedtuple("FaceInfo", ["width", "height", "data", "components"]) class Loader(PillowLoader): kind = "cube" + meta: TextureDescription - def __init__(self, meta): + def __init__(self, meta: ResourceDescription): super().__init__(meta) - def load(self): + def load(self) -> moderngl.TextureCube: """Load a texture cube as described by the supplied ``TextureDescription``` Returns: @@ -48,11 +54,11 @@ def load(self): return texture - def _load_face(self, path: str, face_name: str = None): + def _load_face(self, path: Optional[str], face_name: Optional[str] = None) -> FaceInfo: """Obtain raw byte data for a face Returns: - Tuple[int, bytes]: number of components, byte data + tuple[int, bytes]: number of components, byte data """ if not path: raise ImproperlyConfigured(f"{face_name} texture face not supplied") @@ -61,7 +67,7 @@ def _load_face(self, path: str, face_name: str = None): components, data = image_data(image) return FaceInfo(width=image.size[0], height=image.size[1], data=data, components=components) - def _validate(self, faces): + def _validate(self, faces: list[FaceInfo]) -> Any: """Validates each face ensuring components and size it the same""" components = faces[0].components data_size = len(faces[0].data) diff --git a/moderngl_window/loaders/texture/icon.py b/moderngl_window/loaders/texture/icon.py index 04320f4..ffaca79 100644 --- a/moderngl_window/loaders/texture/icon.py +++ b/moderngl_window/loaders/texture/icon.py @@ -1,15 +1,19 @@ -from moderngl_window.loaders.base import BaseLoader -from moderngl_window.finders import texture from pathlib import Path +from moderngl_window.finders import texture +from moderngl_window.loaders.base import BaseLoader +from moderngl_window.meta.base import ResourceDescription +from moderngl_window.meta.texture import TextureDescription + class IconLoader(BaseLoader): kind = "icon" + meta: TextureDescription - def __init__(self, meta): + def __init__(self, meta: ResourceDescription) -> None: super().__init__(meta) - def find_icon(self): + def find_icon(self) -> Path: """Find resource using texture finders. This is mainly a shortcut method to simplify the task. @@ -17,7 +21,7 @@ def find_icon(self): Args: path: Path to resource """ - abs_path = self._find(Path(self.meta.path), texture.get_finders()) + abs_path = self._find(self.meta.path, texture.get_finders()) if abs_path is None: raise ValueError("Could not find the icon specified. {}".format(self.meta.path)) return abs_path diff --git a/moderngl_window/loaders/texture/pillow.py b/moderngl_window/loaders/texture/pillow.py index 5291358..8a78319 100644 --- a/moderngl_window/loaders/texture/pillow.py +++ b/moderngl_window/loaders/texture/pillow.py @@ -1,13 +1,17 @@ import logging -from typing import Any, Tuple +from pathlib import Path +from typing import Any, Optional, Union try: from PIL import Image except ImportError as ex: raise ImportError("Texture loader 'PillowLoader' requires Pillow: {}".format(ex)) -from moderngl_window.loaders.base import BaseLoader from moderngl_window.exceptions import ImproperlyConfigured +from moderngl_window.loaders.base import BaseLoader +from moderngl_window.meta.base import ResourceDescription +from moderngl_window.meta.texture import TextureDescription +from moderngl_window.resources.textures import TextureAny logger = logging.getLogger(__name__) @@ -16,15 +20,16 @@ class PillowLoader(BaseLoader): """Base loader using PIL/Pillow""" kind = "__unknown__" + image: Image.Image + meta: TextureDescription - def __init__(self, meta): + def __init__(self, meta: ResourceDescription): super().__init__(meta) - self.image = None - def load(self) -> Any: + def load(self) -> TextureAny: raise NotImplementedError() - def _open_image(self): + def _open_image(self) -> Image.Image: if self.meta.image: self.image = self.meta.image else: @@ -36,10 +41,10 @@ def _open_image(self): self.image = Image.open(self.meta.resolved_path) # If the image is animated (like a gif anim) we convert it into a vertical strip - if hasattr(self.image, "is_animated") and self.image.is_animated: + if hasattr(self.image, "is_animated") and self.image.is_animated and hasattr(self.image, "n_frames"): self.layers = self.image.n_frames anim = Image.new( - self.image.palette.mode, + self.image.palette.mode if self.image.palette is not None else "L", (self.image.width, self.image.height * self.image.n_frames), ) anim.putalpha(0) @@ -54,7 +59,7 @@ def _open_image(self): self.image = self._apply_modifiers(self.image) return self.image - def _load_texture(self, path): + def _load_texture(self, path: Union[str, Path]) -> Image.Image: """Find and load separate texture. Useful when multiple textue files needs to be loaded""" resolved_path = self.find_texture(path) logger.info("loading %s", resolved_path) @@ -64,16 +69,16 @@ def _load_texture(self, path): image = Image.open(resolved_path) return self._apply_modifiers(image) - def _apply_modifiers(self, image): + def _apply_modifiers(self, image: Image.Image) -> Image.Image: if self.meta.flip_x: - image = image.transpose(Image.FLIP_LEFT_RIGHT) + image = image.transpose(Image.Transpose.FLIP_LEFT_RIGHT) if self.meta.flip_y: - image = image.transpose(Image.FLIP_TOP_BOTTOM) + image = image.transpose(Image.Transpose.FLIP_TOP_BOTTOM) return self._palette_to_raw(image) - def _palette_to_raw(self, image, mode=None): + def _palette_to_raw(self, image: Image.Image, mode: Optional[str] = None) -> Image.Image: """Converts image to raw if palette is present""" if image.palette and image.palette.mode.lower() in ["rgb", "rgba"]: mode = mode or image.palette.mode @@ -82,17 +87,17 @@ def _palette_to_raw(self, image, mode=None): return image - def _close_image(self): + def _close_image(self) -> None: self.image.close() -def image_data(image: Image) -> Tuple[int, bytes]: +def image_data(image: Image.Image) -> tuple[int, bytes]: """Get components and bytes for an image. The number of components is assumed by image size and the byte length of the raw data. Returns: - Tuple[int, bytes]: Number of components, byte data + tuple[int, bytes]: Number of components, byte data """ # NOTE: We might want to check the actual image.mode # and convert to an acceptable format. diff --git a/moderngl_window/loaders/texture/t2d.py b/moderngl_window/loaders/texture/t2d.py index 37783b7..3c27e14 100644 --- a/moderngl_window/loaders/texture/t2d.py +++ b/moderngl_window/loaders/texture/t2d.py @@ -1,5 +1,7 @@ import logging +import moderngl + from moderngl_window.loaders.texture.pillow import PillowLoader, image_data logger = logging.getLogger(__name__) @@ -8,7 +10,7 @@ class Loader(PillowLoader): kind = "2d" - def load(self): + def load(self) -> moderngl.Texture: """Load a 2d texture as configured in the supplied ``TextureDescription`` Returns: diff --git a/moderngl_window/meta/__init__.py b/moderngl_window/meta/__init__.py index bc1e67c..cd914a2 100644 --- a/moderngl_window/meta/__init__.py +++ b/moderngl_window/meta/__init__.py @@ -1,4 +1,5 @@ -from .data import DataDescription # noqa -from .texture import TextureDescription # noqa -from .scene import SceneDescription # noqa -from .program import ProgramDescription # noqa +from .base import ResourceDescription as ResourceDescription +from .data import DataDescription as DataDescription +from .program import ProgramDescription as ProgramDescription +from .scene import SceneDescription as SceneDescription +from .texture import TextureDescription as TextureDescription diff --git a/moderngl_window/meta/base.py b/moderngl_window/meta/base.py index 84c1185..89a1b87 100644 --- a/moderngl_window/meta/base.py +++ b/moderngl_window/meta/base.py @@ -1,5 +1,5 @@ from pathlib import Path -from typing import Dict, Type +from typing import Any, Optional class ResourceDescription: @@ -13,7 +13,7 @@ class ResourceDescription: resource_type = "" # What resource type is described """str: A unique identifier for the resource type""" - def __init__(self, **kwargs): + def __init__(self, **kwargs: Any): """Initialize a resource description Args: @@ -22,12 +22,12 @@ def __init__(self, **kwargs): self._kwargs = kwargs @property - def path(self) -> str: + def path(self) -> Optional[str]: """str: The path to a resource when a single file is specified""" return self._kwargs.get("path") @property - def label(self) -> str: + def label(self) -> Optional[str]: """str: optional name for the resource Assigning a label is not mandatory but can help @@ -52,15 +52,18 @@ def kind(self) -> str: description.kind = 'something' """ - return self._kwargs.get("kind") or self.default_kind + k = self._kwargs.get("kind") + if k is None: + k = self.default_kind + return k @kind.setter - def kind(self, value) -> str: + def kind(self, value: str) -> None: self._kwargs["kind"] = value @property - def loader_cls(self) -> Type: - """Type: The loader class for this resource. + def loader_cls(self) -> Optional[type]: + """type: The loader class for this resource. This property is assigned to during the loading stage were a loader class is assigned based on @@ -69,11 +72,11 @@ def loader_cls(self) -> Type: return self._kwargs.get("loader_cls") @loader_cls.setter - def loader_cls(self, value: Type): + def loader_cls(self, value: type) -> None: self._kwargs["loader_cls"] = value @property - def resolved_path(self) -> Path: + def resolved_path(self) -> Optional[Path]: """pathlib.Path: The resolved path by a finder. The absolute path to the resource can optionally @@ -82,11 +85,11 @@ def resolved_path(self) -> Path: return self._kwargs.get("resolved_path") @resolved_path.setter - def resolved_path(self, value: Path): + def resolved_path(self, value: Path) -> None: self._kwargs["resolved_path"] = value @property - def attrs(self) -> Dict[str, str]: + def attrs(self) -> dict[str, Any]: """dict: All keywords arguments passed to the resource""" return self._kwargs diff --git a/moderngl_window/meta/data.py b/moderngl_window/meta/data.py index b3a5ba0..d36e75b 100644 --- a/moderngl_window/meta/data.py +++ b/moderngl_window/meta/data.py @@ -1,3 +1,5 @@ +from typing import Any, Optional + from moderngl_window.meta.base import ResourceDescription @@ -26,10 +28,10 @@ class DataDescription(ResourceDescription): DataDescription(path='data/data.bin', kind='binary') """ - default_kind = None + default_kind: str = "" resource_type = "data" - def __init__(self, path=None, kind=None, **kwargs): + def __init__(self, path: Optional[str] = None, kind: Optional[str] = None, **kwargs: Any) -> None: """Initialize the resource description. Keyword Args: diff --git a/moderngl_window/meta/program.py b/moderngl_window/meta/program.py index 70de57b..cbe007f 100644 --- a/moderngl_window/meta/program.py +++ b/moderngl_window/meta/program.py @@ -1,4 +1,5 @@ -from typing import List, Optional +from typing import Any, Optional + from moderngl_window.meta.base import ResourceDescription @@ -24,23 +25,23 @@ class ProgramDescription(ResourceDescription): ) """ - default_kind = None + default_kind = "" resource_type = "programs" def __init__( self, path: Optional[str] = None, kind: Optional[str] = None, - reloadable=False, + reloadable: bool = False, vertex_shader: Optional[str] = None, geometry_shader: Optional[str] = None, fragment_shader: Optional[str] = None, tess_control_shader: Optional[str] = None, tess_evaluation_shader: Optional[str] = None, compute_shader: Optional[str] = None, - defines: Optional[dict] = None, - varyings: Optional[List] = None, - **kwargs, + defines: Optional[dict[str, Any]] = None, + varyings: Optional[list[str]] = None, + **kwargs: Any, ): """Create a program description @@ -55,7 +56,7 @@ def __init__( tess_evaluation_shader (str): Path to tess eval shader compute_shader (str): Path to compute shader defines (dict): Dictionary with define values to replace in the source - varyings (List): List of varying names for transform shader + varyings (list): List of varying names for transform shader **kwargs: Optional custom attributes """ kwargs.update( @@ -76,50 +77,50 @@ def __init__( super().__init__(**kwargs) @property - def reloadable(self) -> bool: + def reloadable(self) -> Optional[bool]: """bool: if this program is reloadable""" return self._kwargs.get("reloadable") @reloadable.setter - def reloadable(self, value): + def reloadable(self, value: Any) -> None: self._kwargs["reloadable"] = value @property - def vertex_shader(self) -> str: + def vertex_shader(self) -> Optional[str]: """str: Relative path to vertex shader""" return self._kwargs.get("vertex_shader") @property - def geometry_shader(self) -> str: + def geometry_shader(self) -> Optional[str]: """str: Relative path to geometry shader""" return self._kwargs.get("geometry_shader") @property - def fragment_shader(self) -> str: + def fragment_shader(self) -> Optional[str]: """str: Relative path to fragment shader""" return self._kwargs.get("fragment_shader") @property - def tess_control_shader(self) -> str: + def tess_control_shader(self) -> Optional[str]: """str: Relative path to tess control shader""" return self._kwargs.get("tess_control_shader") @property - def tess_evaluation_shader(self) -> str: + def tess_evaluation_shader(self) -> Optional[str]: """str: Relative path to tessellation evaluation shader""" return self._kwargs.get("tess_evaluation_shader") @property - def compute_shader(self) -> str: + def compute_shader(self) -> Optional[str]: """str: Relative path to compute shader""" return self._kwargs.get("compute_shader") @property - def defines(self) -> dict: + def defines(self) -> dict[str, Any]: """dict: Dictionary with define values to replace in the source""" return self._kwargs.get("defines", {}) @property - def varyings(self) -> List: - """List: List of varying names for transform shaders""" + def varyings(self) -> list[str]: + """list: List of varying names for transform shaders""" return self._kwargs.get("varyings", []) diff --git a/moderngl_window/meta/scene.py b/moderngl_window/meta/scene.py index fe264e2..512fc1c 100644 --- a/moderngl_window/meta/scene.py +++ b/moderngl_window/meta/scene.py @@ -1,5 +1,7 @@ -from moderngl_window.meta.base import ResourceDescription +from typing import Any, Optional + from moderngl_window.geometry.attributes import AttributeNames +from moderngl_window.meta.base import ResourceDescription class SceneDescription(ResourceDescription): @@ -28,10 +30,10 @@ class SceneDescription(ResourceDescription): on the fly to speed up loading. """ - default_kind = None + default_kind = "" resource_type = "scenes" - def __init__(self, path=None, kind=None, cache=False, attr_names=AttributeNames, **kwargs): + def __init__(self, path: Optional[str] = None, kind: Optional[str] = None, cache: bool = False, attr_names: type[AttributeNames] = AttributeNames, **kwargs: Any): """Create a scene description. Keyword Args: @@ -50,7 +52,7 @@ def __init__(self, path=None, kind=None, cache=False, attr_names=AttributeNames, @property def cache(self) -> bool: """bool: Use cache feature in scene loader""" - return self._kwargs["cache"] + return bool(self._kwargs["cache"]) @property def attr_names(self) -> AttributeNames: diff --git a/moderngl_window/meta/texture.py b/moderngl_window/meta/texture.py index 5bb3003..01c3983 100644 --- a/moderngl_window/meta/texture.py +++ b/moderngl_window/meta/texture.py @@ -1,5 +1,7 @@ -from typing import Tuple +from typing import Any, Optional + from PIL.Image import Image + from moderngl_window.meta.base import ResourceDescription @@ -25,23 +27,23 @@ class TextureDescription(ResourceDescription): def __init__( self, - path: str = None, - kind: str = None, - flip=True, - flip_x=False, - flip_y=True, - mipmap=False, - mipmap_levels: Tuple[int, int] = None, - anisotropy=1.0, - image=None, - layers=None, - pos_x: str = None, - pos_y: str = None, - pos_z: str = None, - neg_x: str = None, - neg_y: str = None, - neg_z: str = None, - **kwargs, + path: Optional[str] = None, + kind: Optional[str] = None, + flip: bool = True, + flip_x: bool = False, + flip_y: bool = True, + mipmap: bool = False, + mipmap_levels: Optional[tuple[int, int]] = None, + anisotropy: float =1.0, + image: Optional[Image] = None, + layers: Optional[int] = None, + pos_x: Optional[str] = None, + pos_y: Optional[str] = None, + pos_z: Optional[str] = None, + neg_x: Optional[str] = None, + neg_y: Optional[str] = None, + neg_z: Optional[str] = None, + **kwargs: Any, ): """Describes a texture resource @@ -88,70 +90,70 @@ def __init__( super().__init__(**kwargs) @property - def flip_x(self) -> bool: + def flip_x(self) -> Optional[bool]: """bool: If the image should be flipped horizontally (left to right)""" return self._kwargs.get("flip_x") @property - def flip_y(self) -> bool: + def flip_y(self) -> Optional[bool]: """bool: If the image should be flipped vertically (top to bottom)""" return self._kwargs.get("flip_y") @property - def mipmap(self) -> bool: + def mipmap(self) -> Optional[bool]: """bool: If mipmaps should be generated""" return self._kwargs.get("mipmap") @mipmap.setter - def mipmap(self, value: float): + def mipmap(self, value: float) -> None: self._kwargs["mipmap"] = value @property - def mipmap_levels(self) -> Tuple[int, int]: - """Tuple[int, int]: base, max_level for mipmap generation""" + def mipmap_levels(self) -> Optional[tuple[int, int]]: + """tuple[int, int]: base, max_level for mipmap generation""" return self._kwargs.get("mipmap_levels") @property - def layers(self) -> int: + def layers(self) -> Optional[int]: """int: Number of layers in texture array""" return self._kwargs.get("layers") @property - def anisotropy(self) -> float: + def anisotropy(self) -> Optional[float]: """float: Number of samples for anisotropic filtering""" return self._kwargs.get("anisotropy") @property - def image(self) -> Image: + def image(self) -> Optional[Image]: """Image: PIL image when loading embedded resources""" return self._kwargs.get("image") @property - def pos_x(self): + def pos_x(self) -> Optional[str]: """str: Path to positive x in a cubemap texture""" return self._kwargs.get("pos_x") @property - def pos_y(self): + def pos_y(self) -> Optional[str]: """str: Path to positive y in a cubemap texture""" return self._kwargs.get("pos_y") @property - def pos_z(self): + def pos_z(self) -> Optional[str]: """str: Path to positive z in a cubemap texture""" return self._kwargs.get("pos_z") @property - def neg_x(self): + def neg_x(self) -> Optional[str]: """str: Path to negative x in a cubemap texture""" return self._kwargs.get("neg_x") @property - def neg_y(self): + def neg_y(self) -> Optional[str]: """str: Path to negative y in a cubemap texture""" return self._kwargs.get("neg_y") @property - def neg_z(self): + def neg_z(self) -> Optional[str]: """str: Path to negative z in a cubemap texture""" return self._kwargs.get("neg_z") diff --git a/moderngl_window/opengl/program.py b/moderngl_window/opengl/program.py index 859ea39..96122b2 100644 --- a/moderngl_window/opengl/program.py +++ b/moderngl_window/opengl/program.py @@ -2,12 +2,13 @@ Helper classes for loading shader """ -from typing import List, Tuple, Union, Optional import re +from typing import Any, Callable, Optional, Union import moderngl + import moderngl_window -from moderngl_window.meta import ProgramDescription +from moderngl_window.meta import ProgramDescription as ProgramDescription VERTEX_SHADER = "VERTEX_SHADER" GEOMETRY_SHADER = "GEOMETRY_SHADER" @@ -35,7 +36,7 @@ def ctx(self) -> moderngl.Context: return moderngl_window.ctx() @classmethod - def from_single(cls, meta: ProgramDescription, source: str): + def from_single(cls: type["ProgramShaders"], meta: ProgramDescription, source: str) -> "ProgramShaders": """Initialize a single glsl string containing all shaders""" instance = cls(meta) instance.vertex_source = ShaderSource( @@ -81,14 +82,14 @@ def from_single(cls, meta: ProgramDescription, source: str): @classmethod def from_separate( - cls, + cls: type["ProgramShaders"], meta: ProgramDescription, - vertex_source, - geometry_source=None, - fragment_source=None, - tess_control_source=None, - tess_evaluation_source=None, - ): + vertex_source: str, + geometry_source: Optional[str] = None, + fragment_source: Optional[str] = None, + tess_control_source: Optional[str] = None, + tess_evaluation_source: Optional[str] = None, + ) -> "ProgramShaders": """Initialize multiple shader strings""" instance = cls(meta) instance.vertex_source = ShaderSource( @@ -98,7 +99,7 @@ def from_separate( defines=meta.defines, ) - if geometry_source: + if geometry_source is not None: instance.geometry_source = ShaderSource( GEOMETRY_SHADER, meta.path or meta.geometry_shader, @@ -106,7 +107,7 @@ def from_separate( defines=meta.defines, ) - if fragment_source: + if fragment_source is not None: instance.fragment_source = ShaderSource( FRAGMENT_SHADER, meta.path or meta.fragment_shader, @@ -114,7 +115,7 @@ def from_separate( defines=meta.defines, ) - if tess_control_source: + if tess_control_source is not None: instance.tess_control_source = ShaderSource( TESS_CONTROL_SHADER, meta.path or meta.tess_control_shader, @@ -122,7 +123,7 @@ def from_separate( defines=meta.defines, ) - if tess_evaluation_source: + if tess_evaluation_source is not None: instance.tess_evaluation_source = ShaderSource( TESS_EVALUATION_SHADER, meta.path or meta.tess_control_shader, @@ -133,20 +134,21 @@ def from_separate( return instance @classmethod - def compute_shader(cls, meta: ProgramDescription, compute_shader_source: str = None): + def compute_shader(cls: type["ProgramShaders"], meta: ProgramDescription, compute_shader_source: str = "") -> "ProgramShaders": instance = cls(meta) instance.compute_shader_source = ShaderSource( COMPUTE_SHADER, - meta.compute_shader, + "" if meta.compute_shader is None else meta.compute_shader, compute_shader_source, defines=meta.defines, ) return instance - def create_compute_shader(self): + def create_compute_shader(self) -> moderngl.ComputeShader: + assert self.compute_shader_source is not None, "There is not compute_shader to create" return self.ctx.compute_shader(self.compute_shader_source.source) - def create(self): + def create(self) -> moderngl.Program: """ Creates a shader program. @@ -155,6 +157,8 @@ def create(self): """ # Get out varyings out_attribs = [] + + assert self.vertex_source is not None, "There is no vertex_source to use" # If no fragment shader is present we are doing transform feedback if not self.fragment_source: @@ -175,12 +179,12 @@ def create(self): tess_evaluation_shader=( self.tess_evaluation_source.source if self.tess_evaluation_source else None ), - varyings=out_attribs, + varyings=tuple(out_attribs), ) program.extra = {"meta": self.meta} return program - def handle_includes(self, load_source_func): + def handle_includes(self, load_source_func: Callable[[Any], Any]) -> None: """Resolves ``#include`` preprocessors Args: @@ -212,12 +216,12 @@ class ShaderSource: def __init__( self, - shader_type: str, - name: str, + shader_type: Optional[str], + name: Optional[str], source: str, - defines: dict = None, - id=0, - root=True, + defines: Optional[dict[str, str]] = None, + id: int = 0, + root: bool = True, ): """Create shader source. @@ -241,7 +245,7 @@ def __init__( ] # List of sources this shader consists of (original source + includes) self._type = shader_type self._name = name - self._defines = defines or {} + self._defines = {} if defines is None else defines if root: source = source.strip() self._lines = source.split("\n") @@ -253,7 +257,7 @@ def __init__( f"Missing #version in {self._name}. A version must be defined in the first line" ) - self.apply_defines(defines) + self.apply_defines(self._defines) # Inject source with shade type if self._root: @@ -271,18 +275,18 @@ def source(self) -> str: return "\n".join(self._lines) @property - def source_list(self) -> List["ShaderSource"]: - """List[ShaderSource]: List of all shader sources""" + def source_list(self) -> list["ShaderSource"]: + """list[ShaderSource]: List of all shader sources""" return self._source_list @property - def name(self) -> str: + def name(self) -> Optional[str]: """str: a path or name for this shader""" return self._name @property - def lines(self) -> List[str]: - """List[str]: The lines in this shader""" + def lines(self) -> list[str]: + """list[str]: The lines in this shader""" return self._lines @property @@ -291,11 +295,11 @@ def line_count(self) -> int: return len(self._lines) @property - def defines(self) -> dict: + def defines(self) -> dict[str, str]: """dict: Defines configured for this shader""" return self._defines - def handle_includes(self, load_source_func, depth=0, source_id=0): + def handle_includes(self, load_source_func: Callable[[Any], Any], depth: int = 0, source_id: int = 0) -> None: """Inject includes into the shader source. This happens recursively up to a max level in case the users has circular includes. We also build up a list of all the included @@ -315,7 +319,10 @@ def handle_includes(self, load_source_func, depth=0, source_id=0): for nr, line in enumerate(self._lines): line = line.strip() if line.startswith("#include"): - path = re.search(r'#include\s+"?([^"]+)', line)[1] + match = re.search(r'#include\s+"?([^"]+)', line) + if match is None: + raise ShaderError(f"Could not match '#include\\s+\"?([^\"]+)' in line {line}") + path = match[1] current_id += 1 _, source = load_source_func(path) source = ShaderSource( @@ -334,7 +341,7 @@ def handle_includes(self, load_source_func, depth=0, source_id=0): else: break - def apply_defines(self, defines: dict): + def apply_defines(self, defines: dict[str, str]) -> None: """Apply the configured define values""" if not defines: return @@ -352,12 +359,12 @@ def apply_defines(self, defines: dict): except IndexError: pass - def find_out_attribs(self) -> List[str]: + def find_out_attribs(self) -> list[str]: """ Get all out attributes in the shader source. Returns: - List[str]: List of out attribute names + list[str]: List of out attribute names """ names = [] for line in self.lines: @@ -367,7 +374,7 @@ def find_out_attribs(self) -> List[str]: return names - def print(self): + def print(self) -> None: """Print the shader lines (for debugging)""" print(f"---[ START {self.name} ]---") @@ -376,7 +383,7 @@ def print(self): print("---[ END {self.name} ]---") - def __repr__(self): + def __repr__(self) -> str: return f"" @@ -401,18 +408,18 @@ def __init__(self, meta: ProgramDescription, program: moderngl.Program): self.meta = meta @property - def name(self): + def name(self) -> Optional[str]: return self.meta.path or self.meta.vertex_shader @property - def _members(self): + def _members(self) -> dict[Any, Any]: return self.program._members @property def ctx(self) -> moderngl.Context: return self.program.ctx - def __getitem__(self, key) -> Union[ + def __getitem__(self, key: Any) -> Union[ moderngl.Uniform, moderngl.UniformBlock, moderngl.Subroutine, @@ -421,15 +428,15 @@ def __getitem__(self, key) -> Union[ ]: return self.program[key] - def get(self, key, default): + def get(self, key: Any, default: Any) -> Any: return self.program.get(key, default) @property - def extra(self): + def extra(self) -> Any: return self.program.extra @property - def mglo(self): + def mglo(self) -> moderngl.Program: """The ModernGL Program object""" return self.program.mglo @@ -442,7 +449,7 @@ def glo(self) -> int: return self.program.glo @property - def subroutines(self) -> Tuple[str, ...]: + def subroutines(self) -> tuple[str, ...]: """ tuple: The subroutine uniforms. """ @@ -473,5 +480,5 @@ def geometry_vertices(self) -> int: """ return self.program.geometry_vertices - def __repr__(self): + def __repr__(self) -> str: return f"" diff --git a/moderngl_window/opengl/projection.py b/moderngl_window/opengl/projection.py index ff0d1c8..b651654 100644 --- a/moderngl_window/opengl/projection.py +++ b/moderngl_window/opengl/projection.py @@ -1,13 +1,13 @@ -from typing import Tuple +from typing import Optional -import numpy as np import glm +import numpy as np class Projection3D: """3D Projection""" - def __init__(self, aspect_ratio=16 / 9, fov=75.0, near=1.0, far=100.0): + def __init__(self, aspect_ratio: float = 16 / 9, fov: float = 75.0, near: float = 1.0, far: float = 100.0): """Create a 3D projection Keyword Args: @@ -20,8 +20,8 @@ def __init__(self, aspect_ratio=16 / 9, fov=75.0, near=1.0, far=100.0): self._fov = fov self._near = near self._far = far - self._matrix = None - self._matrix_bytes = None + self._matrix = glm.mat4(0) + self._matrix_bytes = bytes(0) self.update() @property @@ -45,16 +45,16 @@ def far(self) -> float: return self._far @property - def matrix(self) -> np.ndarray: - """np.ndarray: Current numpy projection matrix""" + def matrix(self) -> glm.mat4: + """glm.mat4x4: Current projection matrix""" return self._matrix def update( self, - aspect_ratio: float = None, - fov: float = None, - near: float = None, - far: float = None, + aspect_ratio: Optional[float] = None, + fov: Optional[float] = None, + near: Optional[float] = None, + far: Optional[float] = None, ) -> None: """Update the projection matrix @@ -64,10 +64,14 @@ def update( near (float): Near plane value far (float): Far plane value """ - self._aspect_ratio = aspect_ratio or self._aspect_ratio - self._fov = fov or self._fov - self._near = near or self._near - self._far = far or self._far + if aspect_ratio is not None: + self._aspect_ratio = aspect_ratio + if fov is not None: + self._fov = fov + if near is not None: + self._near = near + if far is not None: + self._far = far self._matrix = glm.perspective( glm.radians(self._fov), self._aspect_ratio, self._near, self._far @@ -83,7 +87,7 @@ def tobytes(self) -> bytes: return self._matrix_bytes @property - def projection_constants(self) -> Tuple[float, float]: + def projection_constants(self) -> tuple[float, float]: """ (x, y) projection constants for the current projection. This is for example useful when reconstructing a view position diff --git a/moderngl_window/opengl/types.py b/moderngl_window/opengl/types.py index 55bd6ff..c144f7b 100644 --- a/moderngl_window/opengl/types.py +++ b/moderngl_window/opengl/types.py @@ -16,7 +16,6 @@ import re from functools import lru_cache -from typing import List VALID_DIVISORS = ["v", "i", "r"] @@ -27,7 +26,7 @@ def __init__( format_string: str, components: int, bytes_per_component: int, - per_instance=False, + per_instance: bool = False, ): """ Args: @@ -106,7 +105,7 @@ def attribute_format(attr_format: str) -> BufferFormat: ) -def parse_attribute_formats(frmt: str) -> List[BufferFormat]: +def parse_attribute_formats(frmt: str) -> list[BufferFormat]: return [attribute_format(attr) for attr in frmt.split()] diff --git a/moderngl_window/opengl/vao.py b/moderngl_window/opengl/vao.py index 84fabd1..2a8d615 100644 --- a/moderngl_window/opengl/vao.py +++ b/moderngl_window/opengl/vao.py @@ -1,11 +1,12 @@ -from typing import List +from typing import Any, Optional, Union -import numpy import moderngl +import numpy +import numpy.typing as npt + import moderngl_window as mglw from moderngl_window.opengl import types - # For sanity checking draw modes when creating the VAO DRAW_MODES = { moderngl.TRIANGLES: "TRIANGLES", @@ -28,8 +29,8 @@ def __init__( self, buffer: moderngl.Buffer, buffer_format: str, - attributes=None, - per_instance=False, + attributes: list[str] = [], + per_instance: bool = False, ): """ :param buffer: The vbo object @@ -54,8 +55,12 @@ def __init__( def vertex_size(self) -> int: return sum(f.bytes_total for f in self.attrib_formats) - def content(self, attributes: List[str]): - """Build content tuple for the buffer""" + def content(self, attributes: list[str]) -> Optional[tuple[object, ...]]: + """Build content tuple for the buffer + + Returns + The first value is the moderngl buffer + From the third to the end, it is the attributes of the class""" formats = [] attrs = [] for attrib_format, attrib in zip(self.attrib_formats, self.attributes): @@ -69,7 +74,7 @@ def content(self, attributes: List[str]): attributes.remove(attrib) - if not attrs: + if len(attrs) == 0: return None return ( @@ -78,7 +83,7 @@ def content(self, attributes: List[str]): *attrs, ) - def has_attribute(self, name): + def has_attribute(self, name: str) -> bool: return name in self.attributes @@ -120,7 +125,7 @@ class VAO: """ - def __init__(self, name="", mode=moderngl.TRIANGLES): + def __init__(self, name: str = "", mode: int = moderngl.TRIANGLES): """Create and empty VAO with a name and default render mode. Example:: @@ -139,19 +144,19 @@ def __init__(self, name="", mode=moderngl.TRIANGLES): except KeyError: raise VAOError("Invalid draw mode. Options are {}".format(DRAW_MODES.values())) - self._buffers = [] - self._index_buffer = None - self._index_element_size = None + self._buffers: list[BufferInfo] = [] + self._index_buffer: Optional[moderngl.Buffer] = None + self._index_element_size: Optional[int] = None self.vertex_count = 0 - self.vaos = {} + self.vaos: dict[Any, moderngl.VertexArray] = {} @property - def ctx(self): + def ctx(self) -> moderngl.Context: """moderngl.Context: The actite moderngl context""" return mglw.ctx() - def render(self, program: moderngl.Program, mode=None, vertices=-1, first=0, instances=1): + def render(self, program: moderngl.Program, mode: Optional[int] = None, vertices: int = -1, first: int = 0, instances: int = 1) -> None: """Render the VAO. An internal ``moderngl.VertexBuffer`` with compatible buffer bindings @@ -172,7 +177,7 @@ def render(self, program: moderngl.Program, mode=None, vertices=-1, first=0, ins vao.render(mode, vertices=vertices, first=first, instances=instances) - def render_indirect(self, program: moderngl.Program, buffer, mode=None, count=-1, *, first=0): + def render_indirect(self, program: moderngl.Program, buffer: moderngl.Buffer, mode: Optional[int] = None, count: int = -1, *, first: int = 0) -> None: """ The render primitive (mode) must be the same as the input primitive of the GeometryShader. @@ -199,11 +204,11 @@ def transform( self, program: moderngl.Program, buffer: moderngl.Buffer, - mode=None, - vertices=-1, - first=0, - instances=1, - ): + mode: Optional[int] = None, + vertices: int = -1, + first: int = 0, + instances: int = 1, + ) -> None: """Transform vertices. Stores the output in a single buffer. Args: @@ -222,7 +227,7 @@ def transform( vao.transform(buffer, mode=mode, vertices=vertices, first=first, instances=instances) - def buffer(self, buffer, buffer_format: str, attribute_names: List[str]): + def buffer(self, buffer: Union[moderngl.Buffer, npt.NDArray[Any], bytes], buffer_format: str, attribute_names: Union[list[str], str]) -> moderngl.Buffer: """Register a buffer/vbo for the VAO. This can be called multiple times. adding multiple buffers (interleaved or not). @@ -267,7 +272,7 @@ def buffer(self, buffer, buffer_format: str, attribute_names: List[str]): return buffer - def index_buffer(self, buffer, index_element_size=4): + def index_buffer(self, buffer: Union[moderngl.Buffer, npt.NDArray[Any], bytes], index_element_size: int = 4) -> None: """Set the index buffer for this VAO. Args: @@ -301,7 +306,7 @@ def instance(self, program: moderngl.Program) -> moderngl.VertexArray: ``moderngl.VertexArray``: instance """ vao = self.vaos.get(program.glo) - if vao: + if vao is not None and isinstance(vao, moderngl.VertexArray): return vao program_attributes = [ @@ -357,7 +362,7 @@ def instance(self, program: moderngl.Program) -> moderngl.VertexArray: self.vaos[program.glo] = vao return vao - def release(self, buffer=True): + def release(self, buffer: bool = True) -> None: """Destroy all internally cached vaos and release all buffers. Keyword Args: @@ -377,7 +382,7 @@ def release(self, buffer=True): self._buffers = [] - def get_buffer_by_name(self, name: str) -> BufferInfo: + def get_buffer_by_name(self, name: str) -> Optional[BufferInfo]: """Get the BufferInfo associated with a specific attribute name If no buffer is associated with the name `None` will be returned. diff --git a/moderngl_window/resources/__init__.py b/moderngl_window/resources/__init__.py index 1aa99a3..610b001 100644 --- a/moderngl_window/resources/__init__.py +++ b/moderngl_window/resources/__init__.py @@ -1,16 +1,16 @@ +from collections.abc import Iterator from contextlib import contextmanager from pathlib import Path from typing import Union from moderngl_window.conf import settings from moderngl_window.exceptions import ImproperlyConfigured - -from moderngl_window.resources.programs import programs # noqa -from moderngl_window.resources.textures import textures # noqa - +from moderngl_window.resources.data import data as data +from moderngl_window.resources.programs import programs as programs # from moderngl_window.resources.tracks import tracks # noqa -from moderngl_window.resources.scenes import scenes # noqa -from moderngl_window.resources.data import data # noqa +from moderngl_window.resources.scenes import scenes as scenes +from moderngl_window.resources.textures import TextureAny as TextureAny +from moderngl_window.resources.textures import textures as textures def register_dir(path: Union[Path, str]) -> None: @@ -61,7 +61,7 @@ def register_data_dir(path: Union[Path, str]) -> None: _append_unique_path(path, settings.DATA_DIRS) -def _append_unique_path(path: Union[Path, str], dest: list): +def _append_unique_path(path: Union[Path, str], dest: list[Union[Path, str]]) -> None: path = Path(path) if not path.is_absolute(): raise ImproperlyConfigured("Search path must be absolute: {}".format(path)) @@ -80,7 +80,7 @@ def _append_unique_path(path: Union[Path, str], dest: list): @contextmanager -def temporary_dirs(dirs: Union[Path, str]): +def temporary_dirs(dirs: list[Union[Path, str]]) -> Iterator[list[Union[Path, str]]]: """Temporarily changes all resource directories Example:: diff --git a/moderngl_window/resources/base.py b/moderngl_window/resources/base.py index 7a6d662..f1e4bb6 100644 --- a/moderngl_window/resources/base.py +++ b/moderngl_window/resources/base.py @@ -4,25 +4,26 @@ import inspect from functools import lru_cache -from typing import Any, Generator, Tuple +from typing import Any, Generator from moderngl_window.conf import settings from moderngl_window.exceptions import ImproperlyConfigured -from moderngl_window.utils.module_loading import import_string +from moderngl_window.loaders.base import BaseLoader from moderngl_window.meta.base import ResourceDescription +from moderngl_window.utils.module_loading import import_string class BaseRegistry: """Base class for all resource pools""" - settings_attr = None + settings_attr = "" """str: The name of the attribute in :py:class:`~moderngl_window.conf.Settings` containting a list of loader classes. """ - def __init__(self): + def __init__(self) -> None: """Initialize internal attributes""" - self._resources = [] + self._resources: list[ResourceDescription] = [] @property def count(self) -> int: @@ -32,14 +33,16 @@ def count(self) -> int: return len(self._resources) @property - def loaders(self): + def loaders(self) -> Generator[type[BaseLoader], None, None]: """Generator: Loader classes for this resource type""" for loader in getattr(settings, self.settings_attr): yield self._loader_cls(loader) @lru_cache(maxsize=None) - def _loader_cls(self, python_path: str): - return import_string(python_path) + def _loader_cls(self, python_path: str) -> type[BaseLoader]: + cls = import_string(python_path) + assert issubclass(cls, BaseLoader), f"{python_path} does not lead to a Loader" + return cls def load(self, meta: ResourceDescription) -> Any: """ @@ -50,7 +53,9 @@ def load(self, meta: ResourceDescription) -> Any: """ self._check_meta(meta) self.resolve_loader(meta) - return meta.loader_cls(meta).load() + cls = meta.loader_cls(meta) + assert cls is not None, f"Could not load {meta}, no arributes named 'loader_cls'" + return cls.load() def add(self, meta: ResourceDescription) -> None: """ @@ -64,7 +69,7 @@ def add(self, meta: ResourceDescription) -> None: self.resolve_loader(meta) self._resources.append(meta) - def load_pool(self) -> Generator[Tuple[ResourceDescription, Any], None, None]: + def load_pool(self) -> Generator[tuple[ResourceDescription, Any], None, None]: """ Loads all the data files using the configured finders. @@ -109,7 +114,7 @@ def resolve_loader(self, meta: ResourceDescription) -> None: raise ImproperlyConfigured("Could not find a loader for: {}".format(meta)) - def _check_meta(self, meta: Any): + def _check_meta(self, meta: Any) -> None: """Check is the instance is a resource description Raises: ImproperlyConfigured if not a ResourceDescription instance diff --git a/moderngl_window/resources/data.py b/moderngl_window/resources/data.py index 95b6a3b..33688a7 100644 --- a/moderngl_window/resources/data.py +++ b/moderngl_window/resources/data.py @@ -3,16 +3,18 @@ """ from typing import Any + +from moderngl_window.meta import DataDescription, ResourceDescription from moderngl_window.resources.base import BaseRegistry -from moderngl_window.meta import DataDescription class DataFiles(BaseRegistry): """Registry for requested data files""" settings_attr = "DATA_LOADERS" + meta: DataDescription - def load(self, meta: DataDescription) -> Any: + def load(self, meta: ResourceDescription) -> Any: """Load data file with the configured loaders. Args: diff --git a/moderngl_window/resources/decorators.py b/moderngl_window/resources/decorators.py index bfc5811..fb38592 100644 --- a/moderngl_window/resources/decorators.py +++ b/moderngl_window/resources/decorators.py @@ -1,15 +1,15 @@ +from contextlib import contextmanager from pathlib import Path -from typing import List, Union +from typing import Generator, Union -from contextlib import contextmanager from moderngl_window.conf import settings @contextmanager -def texture_dirs(paths: List[Union[Path, str]]): +def texture_dirs(paths: list[Union[Path, str]]) -> Generator[None, None, None]: """Context manager temporarily replacing texture paths Args: - paths (List[Union[Path, str]]): list of paths + paths (list[Union[Path, str]]): list of paths """ original_dirs = settings.DATA_DIRS settings.TEXTURE_DIRS = paths diff --git a/moderngl_window/resources/programs.py b/moderngl_window/resources/programs.py index 852adc7..68827bb 100644 --- a/moderngl_window/resources/programs.py +++ b/moderngl_window/resources/programs.py @@ -1,14 +1,16 @@ import moderngl + +from moderngl_window.meta import ProgramDescription, ResourceDescription from moderngl_window.resources.base import BaseRegistry -from moderngl_window.meta import ProgramDescription class Programs(BaseRegistry): """Handle program loading""" settings_attr = "PROGRAM_LOADERS" + meta: ProgramDescription - def resolve_loader(self, meta: ProgramDescription) -> None: + def resolve_loader(self, meta: ResourceDescription) -> None: """Resolve program loader. Determines if the references resource is a single @@ -17,12 +19,15 @@ def resolve_loader(self, meta: ProgramDescription) -> None: Args: meta (ProgramDescription): The resource description """ - if not meta.kind: - meta.kind = "single" if meta.path else "separate" + if meta.kind == "": + if meta.path is None: + meta.kind = "separate" + else: + meta.kind = "single" super().resolve_loader(meta) - def load(self, meta: ProgramDescription) -> moderngl.Program: + def load(self, meta: ResourceDescription) -> moderngl.Program: """Loads a shader program with the configured loaders Args: @@ -31,7 +36,10 @@ def load(self, meta: ProgramDescription) -> moderngl.Program: Returns: moderngl.Program: The shader program """ - return super().load(meta) + prog = super().load(meta) + # The tests fails with this line + # assert isinstance(prog, moderngl.Program), f"{meta} (type is {type(prog)}) do not load a moderngl.Program object, please correct this" + return prog programs = Programs() diff --git a/moderngl_window/resources/scenes.py b/moderngl_window/resources/scenes.py index 942dcae..0bc979b 100644 --- a/moderngl_window/resources/scenes.py +++ b/moderngl_window/resources/scenes.py @@ -2,17 +2,18 @@ Scene Registry """ +from moderngl_window.meta import ResourceDescription, SceneDescription from moderngl_window.resources.base import BaseRegistry from moderngl_window.scene import Scene -from moderngl_window.meta import SceneDescription class Scenes(BaseRegistry): """Handles scene loading""" settings_attr = "SCENE_LOADERS" + meta: SceneDescription - def load(self, meta: SceneDescription) -> Scene: + def load(self, meta: ResourceDescription) -> Scene: """Load a scene with the configured loaders. Args: @@ -21,7 +22,9 @@ def load(self, meta: SceneDescription) -> Scene: Returns: :py:class:`~moderngl_window.scene.Scene`: The loaded scene """ - return super().load(meta) + scene = super().load(meta) + assert isinstance(scene, Scene), f"{meta} did not load a moderngl_window.scene.Scene object, please correct it." + return scene scenes = Scenes() diff --git a/moderngl_window/resources/textures.py b/moderngl_window/resources/textures.py index 39e2f2e..d23d770 100644 --- a/moderngl_window/resources/textures.py +++ b/moderngl_window/resources/textures.py @@ -3,22 +3,26 @@ """ from typing import Union + import moderngl + +from moderngl_window.meta import ResourceDescription, TextureDescription from moderngl_window.resources.base import BaseRegistry -from moderngl_window.meta import TextureDescription +TextureAny = Union[ + moderngl.Texture, + moderngl.TextureArray, + moderngl.TextureCube, + moderngl.Texture3D, +] class Textures(BaseRegistry): """Handles texture resources""" settings_attr = "TEXTURE_LOADERS" + meta: TextureDescription - def load(self, meta: TextureDescription) -> Union[ - moderngl.Texture, - moderngl.TextureArray, - moderngl.TextureCube, - moderngl.Texture3D, - ]: + def load(self, meta: ResourceDescription) -> TextureAny: """Loads a texture with the configured loaders. Args: @@ -29,7 +33,9 @@ def load(self, meta: TextureDescription) -> Union[ Returns: moderngl.TextureArray: texture array if ``layers`` is supplied """ - return super().load(meta) + texture = super().load(meta) + assert isinstance(texture, moderngl.Texture) or isinstance(texture, moderngl.TextureArray) or isinstance(texture, moderngl.TextureCube) or isinstance(texture, moderngl.Texture3D), f"{meta} did not load a texture. Please correct it" + return texture textures = Textures() diff --git a/moderngl_window/resources/tracks.py b/moderngl_window/resources/tracks.py index d997fbf..10a3264 100644 --- a/moderngl_window/resources/tracks.py +++ b/moderngl_window/resources/tracks.py @@ -8,11 +8,11 @@ class Tracks: """Registry for requested rocket tracks""" - def __init__(self): - self.tacks = [] - self.track_map = {} + def __init__(self) -> None: + self.tacks: list[Track] = [] + self.track_map: dict[str, Track] = {} - def get(self, name) -> Track: + def get(self, name: str) -> Track: """ Get or create a Track object. diff --git a/moderngl_window/scene/__init__.py b/moderngl_window/scene/__init__.py index f135fd2..7c69146 100644 --- a/moderngl_window/scene/__init__.py +++ b/moderngl_window/scene/__init__.py @@ -1,8 +1,9 @@ # pylint: disable = missing-docstring -from .scene import Scene # noqa -from .node import Node # noqa -from .camera import Camera # noqa -from .camera import KeyboardCamera # noqa -from .mesh import Mesh # noqa -from .material import Material, MaterialTexture # noqa -from .programs import MeshProgram # noqa +from .camera import Camera as Camera +from .camera import KeyboardCamera as KeyboardCamera +from .material import Material as Material +from .material import MaterialTexture as MaterialTexture +from .mesh import Mesh as Mesh +from .node import Node as Node +from .programs import MeshProgram as MeshProgram +from .scene import Scene as Scene diff --git a/moderngl_window/scene/camera.py b/moderngl_window/scene/camera.py index b6b7773..7a3a739 100644 --- a/moderngl_window/scene/camera.py +++ b/moderngl_window/scene/camera.py @@ -1,12 +1,12 @@ import time -from math import cos, radians, sin +from typing import Any, Optional, Union -import numpy -from moderngl_window.utils.keymaps import QWERTY, KeyMapFactory import glm +from glm import cos, radians, sin -from moderngl_window.opengl.projection import Projection3D from moderngl_window.context.base import BaseKeys +from moderngl_window.opengl.projection import Projection3D +from moderngl_window.utils.keymaps import QWERTY, KeyMapFactory # Direction Definitions RIGHT = 1 @@ -37,7 +37,7 @@ class Camera: print(camera.projection.matrix) """ - def __init__(self, fov=60.0, aspect_ratio=1.0, near=1.0, far=100.0): + def __init__(self, fov: float = 60.0, aspect_ratio: float = 1.0, near: float = 1.0, far: float = 100.0): """Initialize camera using a specific projection Keyword Args: @@ -62,11 +62,11 @@ def __init__(self, fov=60.0, aspect_ratio=1.0, near=1.0, far=100.0): self._projection = Projection3D(aspect_ratio, fov, near, far) @property - def projection(self): + def projection(self) -> Projection3D: """:py:class:`~moderngl_window.opengl.projection.Projection3D`: The 3D projection""" return self._projection - def set_position(self, x, y, z) -> None: + def set_position(self, x: float, y: float, z: float) -> None: """Set the 3D position of the camera. Args: @@ -74,17 +74,17 @@ def set_position(self, x, y, z) -> None: y (float): y position z (float): z position """ - self.position = glm.vec3(float(x), float(y), float(z)) + self.position = glm.vec3(x, y, z) - def set_rotation(self, yaw, pitch) -> None: + def set_rotation(self, yaw: float, pitch: float) -> None: """Set the rotation of the camera. Args: yaw (float): yaw rotation pitch (float): pitch rotation """ - self._pitch = float(pitch) - self._yaw = float(yaw) + self._pitch = pitch + self._yaw = yaw self._update_yaw_and_pitch() @property @@ -93,8 +93,8 @@ def yaw(self) -> float: return self._yaw @yaw.setter - def yaw(self, value) -> None: - self._yaw = float(value) + def yaw(self, value: float) -> None: + self._yaw = value self._update_yaw_and_pitch() @property @@ -103,13 +103,13 @@ def pitch(self) -> float: return self._pitch @pitch.setter - def pitch(self, value) -> None: - self._pitch = float(value) + def pitch(self, value: float) -> None: + self._pitch = value self._update_yaw_and_pitch() @property - def matrix(self) -> numpy.ndarray: - """numpy.ndarray: The current view matrix for the camera""" + def matrix(self) -> glm.mat4: + """glm.mat4: The current view matrix for the camera""" self._update_yaw_and_pitch() return self._gl_look_at(self.position, self.position + self.dir, self._up) @@ -124,7 +124,7 @@ def _update_yaw_and_pitch(self) -> None: self.right = glm.normalize(glm.cross(self.dir, self._up)) self.up = glm.normalize(glm.cross(self.right, self.dir)) - def look_at(self, vec=None, pos=None) -> numpy.ndarray: + def look_at(self, vec: Optional[glm.vec3] = None, pos: Optional[tuple[float, float, float]] = None) -> glm.mat4: """Look at a specific point Either ``vec`` or ``pos`` needs to be supplied. @@ -133,7 +133,7 @@ def look_at(self, vec=None, pos=None) -> numpy.ndarray: vec (glm.vec3): position pos (tuple/list): list of tuple ``[x, y, x]`` / ``(x, y, x)`` Returns: - numpy.ndarray: Camera matrix + glm.mat4x4: Camera matrix """ if pos is not None: vec = glm.vec3(pos) @@ -143,7 +143,7 @@ def look_at(self, vec=None, pos=None) -> numpy.ndarray: return self._gl_look_at(self.position, vec, self._up) - def _gl_look_at(self, pos, target, up) -> numpy.ndarray: + def _gl_look_at(self, pos: glm.vec3, target: glm.vec3, up: glm.vec3) -> glm.mat4: """The standard lookAt method. Args: @@ -151,7 +151,7 @@ def _gl_look_at(self, pos, target, up) -> numpy.ndarray: target: target position to look at up: direction up Returns: - numpy.ndarray: The matrix + glm.mat4: The matrix """ z = glm.normalize(pos - target) x = glm.normalize(glm.cross(glm.normalize(up), z)) @@ -209,10 +209,10 @@ def __init__( self, keys: BaseKeys, keymap: KeyMapFactory = QWERTY, - fov=60.0, - aspect_ratio=1.0, - near=1.0, - far=100.0, + fov: float = 60.0, + aspect_ratio: float = 1.0, + near: float = 1.0, + far: float = 100.0, ): """Initialize the camera @@ -231,8 +231,8 @@ def __init__( self._xdir = STILL self._zdir = STILL self._ydir = STILL - self._last_time = 0 - self._last_rot_time = 0 + self._last_time = 0.0 + self._last_rot_time = 0.0 # Velocity in axis units per second self._velocity = 10.0 @@ -251,11 +251,11 @@ def mouse_sensitivity(self) -> float: return self._mouse_sensitivity @mouse_sensitivity.setter - def mouse_sensitivity(self, value: float): + def mouse_sensitivity(self, value: float) -> None: self._mouse_sensitivity = value @property - def velocity(self): + def velocity(self) -> float: """float: The speed this camera move based on key inputs The property can also be modified:: @@ -265,10 +265,10 @@ def velocity(self): return self._velocity @velocity.setter - def velocity(self, value: float): + def velocity(self, value: float) -> None: self._velocity = value - def key_input(self, key, action, modifiers) -> None: + def key_input(self, key: str, action: str, modifiers: Any) -> None: """Process key inputs and move camera Args: @@ -315,7 +315,7 @@ def key_input(self, key, action, modifiers) -> None: if action == self.keys.ACTION_RELEASE: self.move_up(False) - def move_left(self, activate) -> None: + def move_left(self, activate: bool) -> None: """The camera should be continiously moving to the left. Args: @@ -323,7 +323,7 @@ def move_left(self, activate) -> None: """ self.move_state(LEFT, activate) - def move_right(self, activate) -> None: + def move_right(self, activate: bool) -> None: """The camera should be continiously moving to the right. Args: @@ -331,7 +331,7 @@ def move_right(self, activate) -> None: """ self.move_state(RIGHT, activate) - def move_forward(self, activate) -> None: + def move_forward(self, activate: bool) -> None: """The camera should be continiously moving forward. Args: @@ -339,7 +339,7 @@ def move_forward(self, activate) -> None: """ self.move_state(FORWARD, activate) - def move_backward(self, activate) -> None: + def move_backward(self, activate: bool) -> None: """The camera should be continiously moving backwards. Args: @@ -347,7 +347,7 @@ def move_backward(self, activate) -> None: """ self.move_state(BACKWARD, activate) - def move_up(self, activate) -> None: + def move_up(self, activate: bool) -> None: """The camera should be continiously moving up. Args: @@ -355,7 +355,7 @@ def move_up(self, activate) -> None: """ self.move_state(UP, activate) - def move_down(self, activate): + def move_down(self, activate: bool) -> None: """The camera should be continiously moving down. Args: @@ -363,7 +363,7 @@ def move_down(self, activate): """ self.move_state(DOWN, activate) - def move_state(self, direction, activate) -> None: + def move_state(self, direction: int, activate: bool) -> None: """Set the camera position move state. Args: @@ -383,7 +383,7 @@ def move_state(self, direction, activate) -> None: elif direction == DOWN: self._ydir = NEGATIVE if activate else STILL - def rot_state(self, dx: int, dy: int) -> None: + def rot_state(self, dx: float, dy: float) -> None: """Update the rotation of the camera. This is done by passing in the relative @@ -421,8 +421,8 @@ def rot_state(self, dx: int, dy: int) -> None: self._update_yaw_and_pitch() @property - def matrix(self) -> numpy.ndarray: - """numpy.ndarray: The current view matrix for the camera""" + def matrix(self) -> glm.mat4: + """glm.mat4x4: The current view matrix for the camera""" # Use separate time in camera so we can move it when the demo is paused now = time.time() # If the camera has been inactive for a while, a large time delta @@ -482,7 +482,7 @@ class OrbitCamera(Camera): camera.projection.tobytes() """ - def __init__(self, target=(0.0, 0.0, 0.0), radius=2.0, angles=(45.0, -45.0), **kwargs): + def __init__(self, target: Union[glm.vec3, tuple[float, float, float]] = (0.0, 0.0, 0.0), radius: float = 2.0, angles: tuple[float, float] = (45.0, -45.0), **kwargs: Any): """Initialize the camera Keyword Args: @@ -497,8 +497,8 @@ def __init__(self, target=(0.0, 0.0, 0.0), radius=2.0, angles=(45.0, -45.0), **k # values for orbit camera self.radius = radius # radius in base units self.angle_x, self.angle_y = angles # angles in degrees - self.target = target # camera target in base units - self.up = (0.0, 1.0, 0.0) # camera up vector + self.target = glm.vec3(target) # camera target in base units + self.up = glm.vec3(0.0, 1.0, 0.0) # camera up vector self._mouse_sensitivity = 1.0 self._zoom_sensitivity = 1.0 @@ -506,8 +506,8 @@ def __init__(self, target=(0.0, 0.0, 0.0), radius=2.0, angles=(45.0, -45.0), **k super().__init__(**kwargs) @property - def matrix(self) -> numpy.ndarray: - """numpy.ndarray: The current view matrix for the camera""" + def matrix(self) -> glm.mat4: + """glm.mat4: The current view matrix for the camera""" # Compute camera (eye) position, calculated from angles and radius. position = ( cos(radians(self.angle_x)) * sin(radians(self.angle_y)) * self.radius + self.target[0], @@ -531,7 +531,7 @@ def angle_x(self) -> float: return self._angle_x @angle_x.setter - def angle_x(self, value: float): + def angle_x(self, value: float) -> None: """Set camera rotation_x in degrees.""" self._angle_x = value @@ -545,7 +545,7 @@ def angle_y(self) -> float: return self._angle_y @angle_y.setter - def angle_y(self, value: float): + def angle_y(self, value: float) -> None: """Set camera rotation_y in degrees.""" self._angle_y = value @@ -559,7 +559,7 @@ def mouse_sensitivity(self) -> float: return self._mouse_sensitivity @mouse_sensitivity.setter - def mouse_sensitivity(self, value: float): + def mouse_sensitivity(self, value: float) -> None: self._mouse_sensitivity = value @property @@ -572,7 +572,7 @@ def zoom_sensitivity(self) -> float: return self._zoom_sensitivity @zoom_sensitivity.setter - def zoom_sensitivity(self, value: float): + def zoom_sensitivity(self, value: float) -> None: self._zoom_sensitivity = value def rot_state(self, dx: float, dy: float) -> None: diff --git a/moderngl_window/scene/material.py b/moderngl_window/scene/material.py index 1048469..30a6f30 100644 --- a/moderngl_window/scene/material.py +++ b/moderngl_window/scene/material.py @@ -1,5 +1,6 @@ +from typing import Optional + import moderngl -from typing import Tuple class MaterialTexture: @@ -7,7 +8,7 @@ class MaterialTexture: Contains a texture and a sampler object. """ - def __init__(self, texture: moderngl.Texture = None, sampler: moderngl.Sampler = None): + def __init__(self, texture: Optional[moderngl.Texture] = None, sampler: Optional[moderngl.Sampler] = None): """Initialize instance. Args: @@ -18,28 +19,28 @@ def __init__(self, texture: moderngl.Texture = None, sampler: moderngl.Sampler = self._sampler = sampler @property - def texture(self) -> moderngl.Texture: + def texture(self) -> Optional[moderngl.Texture]: """moderngl.Texture: Texture instance""" return self._texture @texture.setter - def texture(self, value): + def texture(self, value: moderngl.Texture) -> None: self._texture = value @property - def sampler(self) -> moderngl.Sampler: + def sampler(self) -> Optional[moderngl.Sampler]: """moderngl.Sampler: Sampler instance""" return self._sampler @sampler.setter - def sampler(self, value): + def sampler(self, value: moderngl.Sampler) -> None: self._sampler = value class Material: """Generic material""" - def __init__(self, name: str = None): + def __init__(self, name: str = ""): """Initialize material. Args: @@ -47,7 +48,7 @@ def __init__(self, name: str = None): """ self._name = name or "default" self._color = (1.0, 1.0, 1.0, 1.0) - self._mat_texture = None + self._mat_texture: Optional[MaterialTexture] = None self._double_sided = True @property @@ -56,25 +57,25 @@ def name(self) -> str: return self._name @name.setter - def name(self, value): + def name(self, value: str) -> None: self._name = value @property - def color(self) -> Tuple[float, float, float, float]: - """Tuple[float, float, float, float]: RGBA color""" + def color(self) -> tuple[float, float, float, float]: + """tuple[float, float, float, float]: RGBA color""" return self._color @color.setter - def color(self, value): + def color(self, value: tuple[float, float, float, float]) -> None: self._color = value @property - def mat_texture(self) -> MaterialTexture: + def mat_texture(self) -> Optional[MaterialTexture]: """MaterialTexture: instance""" return self._mat_texture @mat_texture.setter - def mat_texture(self, value): + def mat_texture(self, value: MaterialTexture) -> None: self._mat_texture = value @property @@ -83,10 +84,10 @@ def double_sided(self) -> bool: return self._double_sided @double_sided.setter - def double_sided(self, value): + def double_sided(self, value: bool) -> None: self._double_sided = value - def release(self): + def release(self) -> None: if self._mat_texture: if self._mat_texture.texture: self._mat_texture.texture.release() diff --git a/moderngl_window/scene/mesh.py b/moderngl_window/scene/mesh.py index 5e823ff..43382ea 100644 --- a/moderngl_window/scene/mesh.py +++ b/moderngl_window/scene/mesh.py @@ -1,19 +1,28 @@ -import numpy +from typing import TYPE_CHECKING, Any, Optional, Union + import glm +import moderngl +import numpy + +from moderngl_window.opengl.vao import VAO +from .material import Material + +if TYPE_CHECKING: + from .programs import MeshProgram class Mesh: """Mesh info and geometry""" def __init__( self, - name, - vao=None, - material=None, - attributes=None, - bbox_min=None, - bbox_max=None, - ): + name: str, + vao: Optional[VAO] = None, + material: Optional[Material] = None, + attributes: Optional[dict[str, Any]] = None, + bbox_min: glm.vec3 = glm.vec3(), + bbox_max: glm.vec3 = glm.vec3(), + ) -> None: """Initialize mesh. Args: @@ -38,9 +47,9 @@ def __init__( self.attributes = attributes or {} self.bbox_min = bbox_min self.bbox_max = bbox_max - self.mesh_program = None + self.mesh_program: Optional["MeshProgram"] = None - def draw(self, projection_matrix=None, model_matrix=None, camera_matrix=None, time=0.0): + def draw(self, projection_matrix: Optional[glm.mat4] = None, model_matrix: Optional[glm.mat4] = None, camera_matrix: Optional[glm.mat4] = None, time: float = 0.0) -> None: """Draw the mesh using the assigned mesh program Keyword Args: @@ -48,7 +57,10 @@ def draw(self, projection_matrix=None, model_matrix=None, camera_matrix=None, ti view_matrix (bytes): view_matrix camera_matrix (bytes): camera_matrix """ - if self.mesh_program: + if self.mesh_program is not None: + assert projection_matrix is not None, "Can not draw, there is no projection matrix to use" + assert model_matrix is not None, "Can not draw, there is no model matrix to use" + assert camera_matrix is not None, "Can not draw, there is no camera matrix to use" self.mesh_program.draw( self, projection_matrix=projection_matrix, @@ -57,7 +69,7 @@ def draw(self, projection_matrix=None, model_matrix=None, camera_matrix=None, ti time=time, ) - def draw_bbox(self, proj_matrix, model_matrix, cam_matrix, program, vao): + def draw_bbox(self, proj_matrix: glm.mat4, model_matrix: glm.mat4, cam_matrix: glm.mat4, program: moderngl.Program, vao: VAO) -> None: """Renders the bounding box for this mesh. Args: @@ -67,25 +79,26 @@ def draw_bbox(self, proj_matrix, model_matrix, cam_matrix, program, vao): program: The moderngl.Program rendering the bounding box vao: The vao mesh for the bounding box """ - program["m_proj"].write(proj_matrix) - program["m_model"].write(model_matrix) - program["m_cam"].write(cam_matrix) - program["bb_min"].write(self.bbox_min.astype("f4").tobytes()) - program["bb_max"].write(self.bbox_max.astype("f4").tobytes()) + program["m_proj"].write(proj_matrix.to_bytes()) + program["m_model"].write(model_matrix.to_bytes()) + program["m_cam"].write(cam_matrix.to_bytes()) + program["bb_min"].write(self.bbox_min.to_bytes()) + program["bb_max"].write(self.bbox_max.to_bytes()) vao.render(program) - def draw_wireframe(self, proj_matrix, model_matrix, program): + def draw_wireframe(self, proj_matrix: glm.mat4, model_matrix: glm.mat4, program: moderngl.Program) -> None: """Render the mesh as wireframe. proj_matrix: Projection matrix model_matrix: View/model matrix program: The moderngl.Program rendering the wireframe """ - program["m_proj"].write(proj_matrix) - program["m_model"].write(model_matrix) + assert self.vao is not None, "Can not draw the wireframe, vao is empty" + program["m_proj"].write(proj_matrix.to_bytes()) + program["m_model"].write(model_matrix.to_bytes()) self.vao.render(program) - def add_attribute(self, attr_type, name, components): + def add_attribute(self, attr_type: str, name: str, components: int) -> None: """ Add metadata about the mesh :param attr_type: POSITION, NORMAL etc @@ -94,7 +107,7 @@ def add_attribute(self, attr_type, name, components): """ self.attributes[attr_type] = {"name": name, "components": components} - def calc_global_bbox(self, view_matrix, bbox_min, bbox_max): + def calc_global_bbox(self, view_matrix: glm.mat4, bbox_min: Optional[glm.vec3], bbox_max: Optional[glm.vec3]) -> tuple[glm.vec3, glm.vec3]: """Calculates the global bounding. Args: @@ -105,12 +118,13 @@ def calc_global_bbox(self, view_matrix, bbox_min, bbox_max): bbox_min, bbox_max: Combined bbox """ # Copy and extend to vec4 - bb1 = glm.vec4(*self.bbox_min[:], 1.0) - bb2 = glm.vec4(*self.bbox_max[:], 1.0) + bb1 = glm.vec4(self.bbox_min, 1.0) + bb2 = glm.vec4(self.bbox_max, 1.0) # Transform the bbox values - bmin = numpy.asarray(view_matrix * bb1, dtype="f4") - bmax = numpy.asarray(view_matrix * bb2, dtype="f4") + bmin = view_matrix * bb1 + bmax = view_matrix * bb2 + # If a rotation happened there is an axis change and we have to ensure max-min is positive for i in range(3): @@ -118,7 +132,10 @@ def calc_global_bbox(self, view_matrix, bbox_min, bbox_max): bmin[i], bmax[i] = bmax[i], bmin[i] if bbox_min is None or bbox_max is None: - return bmin[0:3], bmax[0:3] + return ( + glm.vec3(bmin.x, bmin.y, bmin.z), + glm.vec3(bmax.x, bmax.y, bmax.z) + ) for i in range(3): bbox_min[i] = min(bbox_min[i], bmin[i]) @@ -135,7 +152,7 @@ def has_normals(self) -> bool: """ return "NORMAL" in self.attributes - def has_uvs(self, layer=0) -> bool: + def has_uvs(self, layer: int = 0) -> bool: """ Returns: bool: Does the mesh have texture coordinates? diff --git a/moderngl_window/scene/node.py b/moderngl_window/scene/node.py index 9174b5d..1268615 100644 --- a/moderngl_window/scene/node.py +++ b/moderngl_window/scene/node.py @@ -2,15 +2,15 @@ Wrapper for a loaded mesh / vao with properties """ -from __future__ import annotations - -from typing import List, TYPE_CHECKING +from typing import TYPE_CHECKING, Optional import glm import moderngl -if TYPE_CHECKING: - from moderngl_window.scene import Camera, Mesh +from moderngl_window.opengl.vao import VAO + +from .camera import Camera +from .mesh import Mesh class Node: @@ -21,10 +21,10 @@ class Node: def __init__( self, - name: str | None = None, - camera: glm.mat4 | None = None, - mesh: Mesh | None = None, - matrix: glm.mat4 | None = None, + name: Optional[str] = None, + camera: Optional[Camera] = None, + mesh: Optional[Mesh] = None, + matrix: Optional[glm.mat4] = None, ): """Create a node. @@ -40,12 +40,12 @@ def __init__( # Local node matrix self._matrix = matrix # Global matrix - self._matrix_global = None + self._matrix_global: Optional[glm.mat4] = None self._children: list["Node"] = [] @property - def name(self) -> str: + def name(self) -> Optional[str]: """str: Get or set the node name""" return self._name @@ -54,26 +54,26 @@ def name(self, value: str) -> None: self._name = value @property - def mesh(self) -> "Mesh": + def mesh(self) -> Optional[Mesh]: """:py:class:`~moderngl_window.scene.Mesh`: The mesh if present""" return self._mesh @mesh.setter - def mesh(self, value: "Mesh") -> None: + def mesh(self, value: Mesh) -> None: self._mesh = value @property - def camera(self) -> "Camera": + def camera(self) -> Optional[Camera]: """:py:class:`~moderngl_window.scene.Camera`: The camera if present""" return self._camera @camera.setter - def camera(self, value: "Camera") -> None: + def camera(self, value: Camera) -> None: self._camera = value @property - def matrix(self) -> glm.mat4: - """numpy.ndarray: Note matrix (local)""" + def matrix(self) -> Optional[glm.mat4]: + """glm.mat4x4: Note matrix (local)""" return self._matrix @matrix.setter @@ -81,8 +81,8 @@ def matrix(self, value: glm.mat4) -> None: self._matrix = value @property - def matrix_global(self) -> glm.mat4: - """numpy.ndarray: The global node matrix containing transformations from parent nodes""" + def matrix_global(self) -> Optional[glm.mat4]: + """glm.matx4: The global node matrix containing transformations from parent nodes""" return self._matrix_global @matrix_global.setter @@ -90,7 +90,7 @@ def matrix_global(self, value: glm.mat4) -> None: self._matrix_global = value @property - def children(self) -> List["Node"]: + def children(self) -> list["Node"]: """list: List of children""" return self._children @@ -102,7 +102,7 @@ def add_child(self, node: "Node") -> None: """ self._children.append(node) - def draw(self, projection_matrix: glm.mat4, camera_matrix: glm.mat4, time=0): + def draw(self, projection_matrix: Optional[glm.mat4], camera_matrix: Optional[glm.mat4], time: float = 0.0) -> None: """Draw node and children. Keyword Args: @@ -127,20 +127,23 @@ def draw(self, projection_matrix: glm.mat4, camera_matrix: glm.mat4, time=0): def draw_bbox( self, - projection_matrix: glm.mat4, - camera_matrix: glm.mat4, + projection_matrix: Optional[glm.mat4], + camera_matrix: Optional[glm.mat4], program: moderngl.Program, - vao, - ): + vao: VAO, + ) -> None: """Draw bounding box around the node and children. Keyword Args: - projection_matrix (bytes): projection matrix - camera_matrix (bytes): camera_matrix + projection_matrix: projection matrix + camera_matrix: camera_matrix program (moderngl.Program): The program to render the bbox vao: The vertex array representing the bounding box """ if self._mesh: + assert projection_matrix is not None, "Can not draw bbox, the projection matrix is empty" + assert self._matrix_global is not None, "Can not draw bbox, the global matrix is empty" + assert camera_matrix is not None, "Can not draw bbox, the camera matrix is empty" self._mesh.draw_bbox( projection_matrix, self._matrix_global, camera_matrix, program, vao ) @@ -148,7 +151,7 @@ def draw_bbox( for child in self.children: child.draw_bbox(projection_matrix, camera_matrix, program, vao) - def draw_wireframe(self, projection_matrix, camera_matrix, program): + def draw_wireframe(self, projection_matrix: Optional[glm.mat4], camera_matrix: Optional[glm.mat4], program: moderngl.Program) -> None: """Render the node as wireframe. Keyword Args: @@ -157,12 +160,14 @@ def draw_wireframe(self, projection_matrix, camera_matrix, program): program (moderngl.Program): The program to render wireframe """ if self._mesh: + assert projection_matrix is not None, "Can not draw bbox, the projection matrix is empty" + assert self._matrix_global is not None, "Can not draw bbox, the global matrix is empty" self._mesh.draw_wireframe(projection_matrix, self._matrix_global, program) for child in self.children: child.draw_wireframe(projection_matrix, self._matrix_global, program) - def calc_global_bbox(self, view_matrix: glm.mat4, bbox_min, bbox_max) -> tuple: + def calc_global_bbox(self, view_matrix: glm.mat4, bbox_min: Optional[glm.vec3], bbox_max: Optional[glm.vec3]) -> tuple[glm.vec3, glm.vec3]: """Recursive calculation of scene bbox. Keyword Args: @@ -179,6 +184,8 @@ def calc_global_bbox(self, view_matrix: glm.mat4, bbox_min, bbox_max) -> tuple: for child in self._children: bbox_min, bbox_max = child.calc_global_bbox(view_matrix, bbox_min, bbox_max) + assert (bbox_max is not None) and (bbox_min is not None), "The bounding are not defined, please make sure your code is correct" + return bbox_min, bbox_max def calc_model_mat(self, model_matrix: glm.mat4) -> None: diff --git a/moderngl_window/scene/programs.py b/moderngl_window/scene/programs.py index bdad28c..09bfe89 100644 --- a/moderngl_window/scene/programs.py +++ b/moderngl_window/scene/programs.py @@ -1,16 +1,17 @@ from __future__ import annotations import os +from typing import Any, Optional import glm import moderngl -import moderngl_window +import moderngl_window from moderngl_window.conf import settings -from moderngl_window.resources import programs from moderngl_window.meta import ProgramDescription -from .mesh import Mesh +from moderngl_window.resources.programs import programs +from .mesh import Mesh settings.PROGRAM_DIRS.append(os.path.join(os.path.dirname(__file__), "programs")) @@ -20,7 +21,7 @@ class MeshProgram: Describes how a mesh is rendered using a specific shader program """ - def __init__(self, program: moderngl.Program | None = None, **kwargs) -> None: + def __init__(self, program: Optional[moderngl.Program] = None, **kwargs: Any) -> None: """Initialize. Args: @@ -39,7 +40,7 @@ def draw( projection_matrix: glm.mat4, model_matrix: glm.mat4, camera_matrix: glm.mat4, - time=0.0, + time: float = 0.0, ) -> None: """Draw code for the mesh @@ -51,6 +52,8 @@ def draw( camera_matrix (numpy.ndarray): camera_matrix (bytes) time (float): The current time """ + assert self.program is not None, "There is no program to draw" + assert mesh.vao is not None, "There is no vao to render" self.program["m_proj"].write(projection_matrix) self.program["m_mv"].write(model_matrix) self.program["m_cam"].write(camera_matrix) @@ -72,24 +75,26 @@ def apply(self, mesh: Mesh) -> "MeshProgram" | None: class VertexColorProgram(MeshProgram): """Vertex color program""" - def __init__(self, program=None, **kwargs) -> None: + def __init__(self, program: Optional[moderngl.Program] = None, **kwargs: Any) -> None: super().__init__(program=None) self.program = programs.load(ProgramDescription(path="scene_default/vertex_color.glsl")) def draw( self, - mesh, + mesh: Mesh, projection_matrix: glm.mat4, model_matrix: glm.mat4, camera_matrix: glm.mat4, - time=0.0, + time: float = 0.0, ) -> None: + assert self.program is not None, "There is no program to draw" + assert mesh.vao is not None, "There is no vao to render" self.program["m_proj"].write(projection_matrix) self.program["m_model"].write(model_matrix) self.program["m_cam"].write(camera_matrix) mesh.vao.render(self.program) - def apply(self, mesh: Mesh) -> "MeshProgram" | None: + def apply(self, mesh: Mesh) -> Optional[MeshProgram]: if not mesh.material: return None @@ -105,19 +110,21 @@ def apply(self, mesh: Mesh) -> "MeshProgram" | None: class ColorLightProgram(MeshProgram): """Simple color program with light""" - def __init__(self, program=None, **kwargs) -> None: + def __init__(self, program: Optional[moderngl.Program] = None, **kwargs: Any) -> None: super().__init__(program=None) self.program = programs.load(ProgramDescription(path="scene_default/color_light.glsl")) def draw( self, - mesh, + mesh: Mesh, projection_matrix: glm.mat4, model_matrix: glm.mat4, camera_matrix: glm.mat4, - time=0.0, + time: float = 0.0, ) -> None: - if mesh.material: + assert self.program is not None, "There is no program to draw" + assert mesh.vao is not None, "There is no vao to render" + if mesh.material is not None: # if mesh.material.double_sided: # self.ctx.disable(moderngl.CULL_FACE) # else: @@ -146,25 +153,31 @@ def apply(self, mesh: Mesh) -> "MeshProgram" | None: class TextureProgram(MeshProgram): """Plan textured""" - def __init__(self, program=None, **kwargs) -> None: + def __init__(self, program: moderngl.Program = None, **kwargs: Any) -> None: super().__init__(program=None) self.program = programs.load(ProgramDescription(path="scene_default/texture.glsl")) def draw( self, - mesh, + mesh: Mesh, projection_matrix: glm.mat4, model_matrix: glm.mat4, camera_matrix: glm.mat4, - time=0.0, + time: float = 0.0, ) -> None: + assert self.program is not None, "There is no program to draw" + assert mesh.vao is not None, "There is no vao to render" + assert mesh.material is not None, "There is no material to render" + assert mesh.material.mat_texture is not None, "The material does not have a texture to render" + assert mesh.material.mat_texture.texture is not None, "The material texture is not linked to a texture, so it can not be rendered" + mesh.material.mat_texture.texture.use() self.program["m_proj"].write(projection_matrix) self.program["m_model"].write(model_matrix) self.program["m_cam"].write(camera_matrix) mesh.vao.render(self.program) - def apply(self, mesh) -> "MeshProgram" | None: + def apply(self, mesh: Mesh) -> Optional[MeshProgram]: if not mesh.material: return None @@ -186,7 +199,7 @@ def apply(self, mesh) -> "MeshProgram" | None: class TextureVertexColorProgram(MeshProgram): """textured object with vertex color""" - def __init__(self, program: moderngl.Program | None = None, **kwargs) -> None: + def __init__(self, program: Optional[moderngl.Program] = None, **kwargs: Any) -> None: super().__init__(program=None) self.program = programs.load( ProgramDescription(path="scene_default/vertex_color_texture.glsl") @@ -198,8 +211,14 @@ def draw( projection_matrix: glm.mat4, model_matrix: glm.mat4, camera_matrix: glm.mat4, - time=0, + time: float = 0.0, ) -> None: + assert self.program is not None, "There is no program to draw" + assert mesh.vao is not None, "There is no vao to render" + assert mesh.material is not None, "There is no material to render" + assert mesh.material.mat_texture is not None, "The material does not have a texture to render" + assert mesh.material.mat_texture.texture is not None, "The material texture is not linked to a texture, so it can not be rendered" + mesh.material.mat_texture.texture.use() self.program["m_proj"].write(projection_matrix) self.program["m_model"].write(model_matrix) @@ -230,7 +249,7 @@ class TextureLightProgram(MeshProgram): Simple texture program """ - def __init__(self, program: moderngl.Program | None = None, **kwargs) -> None: + def __init__(self, program: Optional[moderngl.Program] = None, **kwargs: Any) -> None: super().__init__(program=None) self.program = programs.load(ProgramDescription(path="scene_default/texture_light.glsl")) @@ -240,8 +259,14 @@ def draw( projection_matrix: glm.mat4, model_matrix: glm.mat4, camera_matrix: glm.mat4, - time=0.0, + time: float = 0.0, ) -> None: + assert self.program is not None, "There is no program to draw" + assert mesh.vao is not None, "There is no vao to render" + assert mesh.material is not None, "There is no material to render" + assert mesh.material.mat_texture is not None, "The material does not have a texture to render" + assert mesh.material.mat_texture.texture is not None, "The material texture is not linked to a texture, so it can not be rendered" + # if mesh.material.double_sided: # self.ctx.disable(moderngl.CULL_FACE) # else: @@ -279,7 +304,7 @@ class FallbackProgram(MeshProgram): Fallback program only rendering positions in white """ - def __init__(self, program: moderngl.Program | None = None, **kwargs) -> None: + def __init__(self, program: Optional[moderngl.Program] = None, **kwargs: Any) -> None: super().__init__(program=None) self.program = programs.load(ProgramDescription(path="scene_default/fallback.glsl")) @@ -289,8 +314,11 @@ def draw( projection_matrix: glm.mat4, model_matrix: glm.mat4, camera_matrix: glm.mat4, - time=0.0, + time: float = 0.0, ) -> None: + assert self.program is not None, "There is no program to draw" + assert mesh.vao is not None, "There is no vao to render" + self.program["m_proj"].write(projection_matrix) self.program["m_model"].write(model_matrix) self.program["m_cam"].write(camera_matrix) diff --git a/moderngl_window/scene/scene.py b/moderngl_window/scene/scene.py index 4e33a87..7897e69 100644 --- a/moderngl_window/scene/scene.py +++ b/moderngl_window/scene/scene.py @@ -2,53 +2,50 @@ Wrapper for a loaded scene with properties. """ -from typing import TYPE_CHECKING import logging -import numpy -import glm +from typing import TYPE_CHECKING, Any, Optional +import glm import moderngl +import numpy + import moderngl_window as mglw -from moderngl_window.resources import programs -from moderngl_window.meta import ProgramDescription from moderngl_window import geometry +from moderngl_window.meta import ProgramDescription +from moderngl_window.resources.programs import programs -from .programs import ( - FallbackProgram, - VertexColorProgram, - ColorLightProgram, - MeshProgram, - TextureProgram, - TextureVertexColorProgram, - TextureLightProgram, -) +from .material import Material +from .node import Node +from .programs import (ColorLightProgram, FallbackProgram, MeshProgram, + TextureLightProgram, TextureProgram, + TextureVertexColorProgram, VertexColorProgram) logger = logging.getLogger(__name__) if TYPE_CHECKING: - from moderngl_window.scene import Node, Material + from moderngl_window.scene import Camera, Material, Mesh, Node class Scene: """Generic scene""" - def __init__(self, name, **kwargs): + def __init__(self, name: Optional[str], **kwargs: Any): """Create a scene with a name. Args: name (str): Unique name or path for the scene """ self.name = name - self.root_nodes = [] + self.root_nodes: list[Node] = [] # References resources in the scene - self.nodes = [] - self.materials = [] - self.meshes = [] - self.cameras = [] + self.nodes: list[Node] = [] + self.materials: list[Material] = [] + self.meshes: list[Mesh] = [] + self.cameras: list[Camera] = [] - self.bbox_min = None # Type: numpy.ndarray - self.bbox_max = None # Type: numpy.ndarray + self.bbox_min: glm.vec3 = glm.vec3() + self.bbox_max: glm.vec3 = glm.vec3() self.diagonal_size = 1.0 self.bbox_vao = geometry.bbox() @@ -80,24 +77,24 @@ def ctx(self) -> moderngl.Context: return mglw.ctx() @property - def matrix(self) -> numpy.ndarray: - """numpy.ndarray: The current model matrix + def matrix(self) -> glm.mat4: + """glm.mat4x4: The current model matrix This property is settable. """ return self._matrix @matrix.setter - def matrix(self, matrix: glm.mat4): + def matrix(self, matrix: glm.mat4) -> None: self._matrix = matrix for node in self.root_nodes: node.calc_model_mat(self._matrix) def draw( self, - projection_matrix: glm.mat4 = None, - camera_matrix: glm.mat4 = None, - time=0.0, + projection_matrix: Optional[glm.mat4] = None, + camera_matrix: Optional[glm.mat4] = None, + time: float = 0.0, ) -> None: """Draw all the nodes in the scene. @@ -117,16 +114,16 @@ def draw( def draw_bbox( self, - projection_matrix=None, - camera_matrix=None, - children=True, - color=(0.75, 0.75, 0.75), + projection_matrix: Optional[glm.mat4] = None, + camera_matrix: Optional[glm.mat4] = None, + children: float = True, + color: tuple[float, float, float] = (0.75, 0.75, 0.75), ) -> None: """Draw scene and mesh bounding boxes. Args: - projection_matrix (ndarray): mat4 projection - camera_matrix (ndarray): mat4 camera matrix + projection_matrix (glm.mat4): mat4 projection + camera_matrix (glm.mat4): mat4 camera matrix children (bool): Will draw bounding boxes for meshes as well color (tuple): Color of the bounding boxes """ @@ -150,8 +147,8 @@ def draw_bbox( node.draw_bbox(projection_matrix, camera_matrix, self.bbox_program, self.bbox_vao) def draw_wireframe( - self, projection_matrix=None, camera_matrix=None, color=(0.75, 0.75, 0.75, 1.0) - ): + self, projection_matrix: Optional[glm.mat4] = None, camera_matrix: Optional[glm.mat4] = None, color: tuple[float, float, float, float] = (0.75, 0.75, 0.75, 1.0) + ) -> None: """Render the scene in wireframe mode. Args: @@ -176,7 +173,7 @@ def draw_wireframe( self.ctx.wireframe = False - def apply_mesh_programs(self, mesh_programs=None, clear: bool = True) -> None: + def apply_mesh_programs(self, mesh_programs: Optional[list[MeshProgram]] = None, clear: bool = True) -> None: """Applies mesh programs to meshes. If not mesh programs are passed in we assign default ones. @@ -222,10 +219,13 @@ def apply_mesh_programs(self, mesh_programs=None, clear: bool = True) -> None: def calc_scene_bbox(self) -> None: """Calculate scene bbox""" - bbox_min, bbox_max = None, None + bbox_min: Optional[glm.vec3] = None + bbox_max: Optional[glm.vec3] = None for node in self.root_nodes: bbox_min, bbox_max = node.calc_global_bbox(glm.mat4(), bbox_min, bbox_max) + assert (bbox_max is not None) and (bbox_min is not None), "The bounding are not defined, please make sure your code is correct" + self.bbox_min = bbox_min self.bbox_max = bbox_max @@ -241,7 +241,7 @@ def prepare(self) -> None: # Recursively calculate model matrices self.matrix = glm.mat4() - def find_node(self, name: str = None) -> "Node": + def find_node(self, name: Optional[str] = None) -> Optional[Node]: """Finds a :py:class:`~moderngl_window.scene.Node` Keyword Args: @@ -255,7 +255,7 @@ def find_node(self, name: str = None) -> "Node": return None - def find_material(self, name: str = None) -> "Material": + def find_material(self, name: Optional[str] = None) -> Optional[Material]: """Finds a :py:class:`~moderngl_window.scene.Material` Keyword Args: @@ -269,14 +269,15 @@ def find_material(self, name: str = None) -> "Material": return None - def release(self): + def release(self) -> None: """Destroys the scene data and vertex buffers""" self.destroy() def destroy(self) -> None: """Destroys the scene data and vertex buffers""" for mesh in self.meshes: - mesh.vao.release() + if mesh.vao is not None: + mesh.vao.release() # if mesh.mesh_program: # mesh.mesh_program.program.release() diff --git a/moderngl_window/screenshot.py b/moderngl_window/screenshot.py index ab542c6..b0ed0a0 100644 --- a/moderngl_window/screenshot.py +++ b/moderngl_window/screenshot.py @@ -1,10 +1,11 @@ import logging import os from datetime import datetime -from typing import Union +from typing import Optional, Union import moderngl from PIL import Image + from moderngl_window.conf import settings logger = logging.getLogger(__name__) @@ -14,11 +15,11 @@ def create( source: Union[moderngl.Framebuffer, moderngl.Texture], - file_format="png", - name: str = None, - mode="RGB", - alignment=1, -): + file_format: str = "png", + name: Optional[str] = None, + mode: str = "RGB", + alignment: int = 1, +) -> None: """ Create a screenshot from a ``moderngl.Framebuffer`` or ``moderngl.Texture``. The screenshot will be written to :py:attr:`~moderngl_window.conf.Settings.SCREENSHOT_PATH` @@ -68,7 +69,7 @@ def create( else: raise ValueError("Source needs to be a FrameBuffer or Texture, not a %s", type(source)) - image = image.transpose(Image.FLIP_TOP_BOTTOM) + image = image.transpose(Image.Transpose.FLIP_TOP_BOTTOM) dest = os.path.join(str(dest), name) logger.info("Creating screenshot: %s", dest) image.save(dest, format=file_format) diff --git a/moderngl_window/text/bitmapped/base.py b/moderngl_window/text/bitmapped/base.py index 14d98b6..d06ea52 100644 --- a/moderngl_window/text/bitmapped/base.py +++ b/moderngl_window/text/bitmapped/base.py @@ -1,11 +1,21 @@ +from typing import Any, Generator, Optional, Union + import moderngl_window class FontMeta: """Metdata for texture array""" - def __init__(self, meta): + def __init__(self, meta: dict[str, Union[int, list[dict[str, int]]]]): self._meta = meta + + assert isinstance(self._meta["characters"], int) + assert isinstance(self._meta["character_height"], int) + assert isinstance(self._meta["character_width"], int) + assert isinstance(self._meta["atlas_height"], int) + assert isinstance(self._meta["atlas_width"], int) + assert isinstance(self._meta["character_ranges"], list) + self.characters = self._meta["characters"] self.character_ranges = self._meta["character_ranges"] self.character_height = self._meta["character_height"] @@ -14,32 +24,33 @@ def __init__(self, meta): self.atlas_width = self._meta["atlas_width"] @property - def char_aspect_wh(self): + def char_aspect_wh(self) -> float: return self.character_width / self.character_height - def char_aspect_hw(self): + def char_aspect_hw(self) -> float: return self.character_height / self.character_width class BaseText: """Simple base class for a bitmapped text rendered""" - def __init__(self): - self._meta = None - self._ct = None + def __init__(self) -> None: + self._meta: Optional[FontMeta] = None + self._ct: list[int] = [] self.ctx = moderngl_window.ContextRefs.CONTEXT - def draw(self, *args, **kwargs): + def draw(self, *args: Any, **kwargs: Any) -> None: raise NotImplementedError() - def _translate_string(self, data): + def _translate_string(self, data: str) -> Generator[int, None, None]: """Translate string into character texture positions""" - data = data.encode("iso-8859-1", errors="replace") + assert (self._meta is not None) and (self._ct is not None), "_meta or _ct (or both) are empty. Did you call _init()?" + data_bytes = data.encode("iso-8859-1", errors="replace") - for index, char in enumerate(data): + for index, char in enumerate(data_bytes): yield self._meta.characters - 1 - self._ct[char] - def _init(self, meta: FontMeta): + def _init(self, meta: FontMeta) -> None: self._meta = meta # Check if the atlas size is sane if not self._meta.characters * self._meta.character_height == self._meta.atlas_height: @@ -47,8 +58,9 @@ def _init(self, meta: FontMeta): self._generate_character_map() - def _generate_character_map(self): + def _generate_character_map(self) -> None: """Generate character translation map (latin1 pos to texture pos)""" + assert self._meta is not None, "You should not call _generate_character_map but _init" self._ct = [-1] * 256 index = 0 for c_range in self._meta.character_ranges: diff --git a/moderngl_window/text/bitmapped/text_2d.py b/moderngl_window/text/bitmapped/text_2d.py index 87bf051..b4fef2b 100644 --- a/moderngl_window/text/bitmapped/text_2d.py +++ b/moderngl_window/text/bitmapped/text_2d.py @@ -1,16 +1,14 @@ -import numpy -import glm - from pathlib import Path +from typing import Optional +import glm import moderngl -from moderngl_window.opengl.vao import VAO +import numpy + from moderngl_window import resources -from moderngl_window.meta import ( - DataDescription, - TextureDescription, - ProgramDescription, -) +from moderngl_window.meta import (DataDescription, ProgramDescription, + TextureDescription) +from moderngl_window.opengl.vao import VAO from .base import BaseText, FontMeta @@ -20,7 +18,7 @@ class TextWriter2D(BaseText): """Simple monspaced bitmapped text renderer""" - def __init__(self): + def __init__(self) -> None: super().__init__() meta = FontMeta(resources.data.load(DataDescription(path="bitmapped/text/meta.json"))) @@ -38,6 +36,8 @@ def __init__(self): self._init(meta) + assert self.ctx is not None, "There was a problem, we do not have a context" + self._string_buffer = self.ctx.buffer(reserve=1024 * 4) self._string_buffer.clear(chunk=b"\32") pos = self.ctx.buffer(data=bytes([0] * 4 * 3)) @@ -46,20 +46,20 @@ def __init__(self): self._vao.buffer(pos, "3f", "in_position") self._vao.buffer(self._string_buffer, "1u/i", "in_char_id") - self._text: str = None + self._text: Optional[str] = None @property - def text(self) -> str: + def text(self) -> Optional[str]: return self._text @text.setter - def text(self, value: str): + def text(self, value: str) -> None: self._text = value self._string_buffer.orphan(size=len(value) * 4) self._string_buffer.clear(chunk=b"\32") self._write(value) - def _write(self, text: str): + def _write(self, text: str) -> None: self._string_buffer.clear(chunk=b"\32") self._string_buffer.write( @@ -69,7 +69,11 @@ def _write(self, text: str): ) ) - def draw(self, pos, length=-1, size=24.0): + def draw(self, pos: tuple[float, float, float], length: int = -1, size: float = 24.0) -> None: + assert self.ctx is not None, "There was a problem, we do not have a context" + assert self.ctx.fbo is not None, "The current context do not have a framebuffer" + assert self._meta is not None, "We are missing the information needed to write text" + # Calculate ortho projection based on viewport vp = self.ctx.fbo.viewport w, h = vp[2], vp[3] @@ -88,4 +92,4 @@ def draw(self, pos, length=-1, size=24.0): self._program["font_texture"].value = 0 self._program["char_size"].value = self._meta.char_aspect_wh * size, size - self._vao.render(self._program, instances=len(self._text)) + self._vao.render(self._program, instances=len(self._text if self._text is not None else "")) diff --git a/moderngl_window/timers/base.py b/moderngl_window/timers/base.py index d60ba35..ca9cb3f 100644 --- a/moderngl_window/timers/base.py +++ b/moderngl_window/timers/base.py @@ -1,6 +1,3 @@ -from typing import Tuple - - class BaseTimer: """ A timer controls the time passed into the the render function. @@ -31,34 +28,34 @@ def time(self) -> float: raise NotImplementedError() @time.setter - def time(self, value: float): + def time(self, value: float) -> None: raise NotImplementedError() - def next_frame(self) -> Tuple[float, float]: + def next_frame(self) -> tuple[float, float]: """Get timer information for the next frame. Returns: - Tuple[float, float]: The frametime and current time + tuple[float, float]: The frametime and current time """ raise NotImplementedError() - def start(self): + def start(self) -> None: """Start the timer initially or resume after pause""" raise NotImplementedError() - def pause(self): + def pause(self) -> None: """Pause the timer""" raise NotImplementedError() - def toggle_pause(self): + def toggle_pause(self) -> None: """Toggle pause state""" raise NotImplementedError() - def stop(self) -> Tuple[float, float]: + def stop(self) -> tuple[float, float]: """ Stop the timer. Should only be called once when stopping the timer. Returns: - Tuple[float, float]> Current position in the timer, actual running duration + tuple[float, float]> Current position in the timer, actual running duration """ raise NotImplementedError() diff --git a/moderngl_window/timers/clock.py b/moderngl_window/timers/clock.py index 5ca3d43..5ede9fe 100644 --- a/moderngl_window/timers/clock.py +++ b/moderngl_window/timers/clock.py @@ -1,5 +1,5 @@ import time -from typing import Tuple +from typing import Any, Optional from moderngl_window.timers.base import BaseTimer @@ -7,12 +7,12 @@ class Timer(BaseTimer): """Timer based on python ``time``.""" - def __init__(self, **kwargs): - self._start_time = None - self._stop_time = None - self._pause_time = None - self._last_frame = None - self._offset = 0 + def __init__(self, **kwargs: Any) -> None: + self._start_time: Optional[float] = None + self._stop_time: Optional[float] = None + self._pause_time: Optional[float] = None + self._last_frame = 0.0 + self._offset = 0.0 @property def is_paused(self) -> bool: @@ -41,53 +41,58 @@ def time(self) -> float: return time.time() - self._start_time - self._offset @time.setter - def time(self, value: float): + def time(self, value: float) -> None: if value < 0: - value = 0 + value = 0.0 self._offset += self.time - value - def next_frame(self) -> Tuple[float, float]: + def next_frame(self) -> tuple[float, float]: """ Get the time and frametime for the next frame. This should only be called once per frame. Returns: - Tuple[float, float]: current time and frametime + tuple[float, float]: current time and frametime """ current = self.time delta, self._last_frame = current - self._last_frame, current return current, delta - def start(self): + def start(self) -> None: """Start the timer by recoding the current ``time.time()`` preparing to report the number of seconds since this timestamp. """ if self._start_time is None: self._start_time = time.time() self._last_frame = 0.0 - else: + elif self._pause_time is not None: self._offset += time.time() - self._pause_time self._pause_time = None + else: + print("The timer is already started") - def pause(self): + def pause(self) -> None: """Pause the timer by setting the internal pause time using ``time.time()``""" self._pause_time = time.time() - def toggle_pause(self): + def toggle_pause(self) -> None: """Toggle the paused state""" if self.is_paused: self.start() else: self.pause() - def stop(self) -> Tuple[float, float]: + def stop(self) -> tuple[float, float]: """ Stop the timer. Should only be called once when stopping the timer. Returns: - Tuple[float, float]: Current position in the timer, actual running duration + tuple[float, float]: Current position in the timer, actual running duration """ + if self._start_time is None: + return 0.0, 0.0 + self._stop_time = time.time() return ( self._stop_time - self._start_time - self._offset, diff --git a/moderngl_window/utils/module_loading.py b/moderngl_window/utils/module_loading.py index a5bdf8f..ad0e323 100644 --- a/moderngl_window/utils/module_loading.py +++ b/moderngl_window/utils/module_loading.py @@ -1,7 +1,8 @@ from importlib import import_module +from typing import Any, Callable -def import_string(dotted_path): +def import_string(dotted_path: str) -> Any: """ Import a dotted module path and return the attribute/class designated by the last name in the path. Raise ImportError if the import failed. diff --git a/moderngl_window/utils/scheduler.py b/moderngl_window/utils/scheduler.py index 2014c52..cc0e466 100644 --- a/moderngl_window/utils/scheduler.py +++ b/moderngl_window/utils/scheduler.py @@ -1,5 +1,7 @@ import sched import time +from typing import Any, Callable + from moderngl_window.timers.base import BaseTimer @@ -18,13 +20,13 @@ def __init__(self, timer: BaseTimer): "timer, {}, has to be a instance of BaseTimer or a callable!".format(timer) ) - self._events = dict() + self._events: dict[int, sched.Event] = dict() self._event_id = 0 self._scheduler = sched.scheduler(lambda: timer.time, time.sleep) def run_once( - self, action, delay: float, *, priority: int = 1, arguments=(), kwargs=dict() + self, action: Callable[[Any], Any], delay: float, *, priority: int = 1, arguments: tuple[Any, ...]=(), kwargs: dict[Any, Any] = dict() ) -> int: """Schedule a function for execution after a delay. @@ -48,7 +50,7 @@ def run_once( self._event_id += 1 return self._event_id - 1 - def run_at(self, action, time: float, *, priority: int = 1, arguments=(), kwargs=dict()) -> int: + def run_at(self, action: Callable[[Any], Any], time: float, *, priority: int = 1, arguments: tuple[Any, ...] = (), kwargs: dict[Any, Any] = dict()) -> int: """Schedule a function to be executed at a certain time. Args: @@ -73,13 +75,13 @@ def run_at(self, action, time: float, *, priority: int = 1, arguments=(), kwargs def run_every( self, - action, + action: Callable[[Any], Any], delay: float, *, priority: int = 1, initial_delay: float = 0.0, - arguments=(), - kwargs=dict(), + arguments: tuple[Any, ...] = (), + kwargs: dict[Any, Any] = dict(), ) -> int: """Schedule a recurring function to be called every `delay` seconds after a initial delay. @@ -108,7 +110,7 @@ def run_every( self._event_id += 1 return self._event_id - 1 - def _recurring_event_factory(self, function, arguments, kwargs, scheduling_info, id): + def _recurring_event_factory(self, function: Callable[[Any], Any], arguments: tuple[Any, ...], kwargs: dict[Any, Any], scheduling_info: tuple[Any, Any], id: int) -> Callable[[], None]: """Factory for creating recurring events that will reschedule themselves. Args: @@ -119,7 +121,7 @@ def _recurring_event_factory(self, function, arguments, kwargs, scheduling_info, id (int): event id this event should be assigned to. """ - def _f(): + def _f() -> None: function(*arguments, **kwargs) event = self._scheduler.enter(*scheduling_info, _f) self._events[id] = event @@ -142,7 +144,7 @@ def cancel(self, event_id: int, delay: float = 0) -> None: else: self.run_once(self._cancel, delay, priority=0, arguments=(event_id,)) - def _cancel(self, event_id: int): + def _cancel(self, event_id: int) -> None: if event_id not in self._events: raise ValueError("Recurring event with id {} does not exist".format(event_id)) event = self._events.pop(event_id) diff --git a/tests/headless.py b/tests/headless.py index 2f4e4fa..9d5d152 100644 --- a/tests/headless.py +++ b/tests/headless.py @@ -1,6 +1,7 @@ from unittest import TestCase import moderngl + import moderngl_window as mglw from moderngl_window.conf import settings diff --git a/tests/test_attribute_names.py b/tests/test_attribute_names.py index ee24eaf..12861a6 100644 --- a/tests/test_attribute_names.py +++ b/tests/test_attribute_names.py @@ -1,4 +1,5 @@ from unittest import TestCase + from moderngl_window.geometry import AttributeNames diff --git a/tests/test_camera.py b/tests/test_camera.py index d6e2b7a..b67c4d5 100644 --- a/tests/test_camera.py +++ b/tests/test_camera.py @@ -1,11 +1,12 @@ from unittest import TestCase -import numpy as np + import glm +import numpy as np +from moderngl_window.context.base.keys import BaseKeys, KeyModifiers +from moderngl_window.opengl.projection import Projection3D from moderngl_window.scene import Camera, KeyboardCamera from moderngl_window.scene import camera as cam -from moderngl_window.opengl.projection import Projection3D -from moderngl_window.context.base.keys import BaseKeys, KeyModifiers class CameraTest(TestCase): diff --git a/tests/test_conf.py b/tests/test_conf.py index f28f715..cff045d 100644 --- a/tests/test_conf.py +++ b/tests/test_conf.py @@ -1,10 +1,9 @@ import os import sys - from types import ModuleType from unittest import TestCase -from moderngl_window.conf import Settings, SETTINGS_ENV_VAR +from moderngl_window.conf import SETTINGS_ENV_VAR, Settings from moderngl_window.exceptions import ImproperlyConfigured diff --git a/tests/test_docs.py b/tests/test_docs.py index 82f400f..83eeb9e 100644 --- a/tests/test_docs.py +++ b/tests/test_docs.py @@ -16,6 +16,7 @@ from moderngl_window.utils import module_loading + # Mock modules class Mock(MagicMock): @classmethod diff --git a/tests/test_finders.py b/tests/test_finders.py index 28cd67f..f830beb 100644 --- a/tests/test_finders.py +++ b/tests/test_finders.py @@ -1,17 +1,12 @@ from pathlib import Path from unittest import TestCase -from moderngl_window.finders.base import BaseFilesystemFinder -from moderngl_window.finders import ( - data, - program, - texture, - scene, -) -from moderngl_window.exceptions import ImproperlyConfigured - from utils import settings_context +from moderngl_window.exceptions import ImproperlyConfigured +from moderngl_window.finders import data, program, scene, texture +from moderngl_window.finders.base import BaseFilesystemFinder + class FinderTestCase(TestCase): root = Path(__file__).parent / 'fixtures' / 'resources' diff --git a/tests/test_geometry.py b/tests/test_geometry.py index da34386..d35a2bc 100644 --- a/tests/test_geometry.py +++ b/tests/test_geometry.py @@ -1,8 +1,8 @@ from headless import HeadlessTestCase +from moderngl_window import geometry from moderngl_window.geometry import AttributeNames from moderngl_window.opengl.vao import BufferInfo -from moderngl_window import geometry class GeomtryTestCase(HeadlessTestCase): diff --git a/tests/test_headless.py b/tests/test_headless.py index 7d26fc3..d44c9fa 100644 --- a/tests/test_headless.py +++ b/tests/test_headless.py @@ -2,10 +2,10 @@ import moderngl from headless import HeadlessTestCase + +from moderngl_window import geometry, resources from moderngl_window.context.headless import Keys -from moderngl_window import resources from moderngl_window.meta import ProgramDescription -from moderngl_window import geometry resources.register_dir((Path(__file__).parent / 'fixtures' / 'resources').resolve()) diff --git a/tests/test_loaders_data.py b/tests/test_loaders_data.py index fc931e3..cbaad52 100644 --- a/tests/test_loaders_data.py +++ b/tests/test_loaders_data.py @@ -2,8 +2,8 @@ from unittest import TestCase from moderngl_window import resources -from moderngl_window.meta import DataDescription from moderngl_window.exceptions import ImproperlyConfigured +from moderngl_window.meta import DataDescription resources.register_dir((Path(__file__).parent / 'fixtures' / 'resources').resolve()) diff --git a/tests/test_loaders_program.py b/tests/test_loaders_program.py index bee067d..36b3164 100644 --- a/tests/test_loaders_program.py +++ b/tests/test_loaders_program.py @@ -1,13 +1,14 @@ -from pathlib import Path import platform -import pytest +from pathlib import Path import moderngl +import pytest from headless import HeadlessTestCase + from moderngl_window import resources +from moderngl_window.exceptions import ImproperlyConfigured from moderngl_window.meta import ProgramDescription from moderngl_window.opengl.program import ReloadableProgram -from moderngl_window.exceptions import ImproperlyConfigured resources.register_dir((Path(__file__).parent / 'fixtures' / 'resources').resolve()) diff --git a/tests/test_loaders_scene.py b/tests/test_loaders_scene.py index c577315..3ceb829 100644 --- a/tests/test_loaders_scene.py +++ b/tests/test_loaders_scene.py @@ -4,10 +4,11 @@ from pathlib import Path from headless import HeadlessTestCase + from moderngl_window import resources +from moderngl_window.exceptions import ImproperlyConfigured from moderngl_window.meta import SceneDescription from moderngl_window.scene import Scene -from moderngl_window.exceptions import ImproperlyConfigured resources.register_dir((Path(__file__).parent / 'fixtures' / 'resources').resolve()) diff --git a/tests/test_loaders_texture.py b/tests/test_loaders_texture.py index fcd4d72..e19d749 100644 --- a/tests/test_loaders_texture.py +++ b/tests/test_loaders_texture.py @@ -2,9 +2,10 @@ import moderngl from headless import HeadlessTestCase + from moderngl_window import resources -from moderngl_window.meta import TextureDescription from moderngl_window.exceptions import ImproperlyConfigured +from moderngl_window.meta import TextureDescription resources.register_dir((Path(__file__).parent / 'fixtures' / 'resources').resolve()) diff --git a/tests/test_moderngl_window.py b/tests/test_moderngl_window.py index 2e2d8be..d5fbc2f 100644 --- a/tests/test_moderngl_window.py +++ b/tests/test_moderngl_window.py @@ -1,6 +1,8 @@ import sys from unittest import TestCase, mock + import moderngl + import moderngl_window as mglw from moderngl_window.context.base import BaseWindow diff --git a/tests/test_projection.py b/tests/test_projection.py index 9eff4b3..c901740 100644 --- a/tests/test_projection.py +++ b/tests/test_projection.py @@ -1,5 +1,7 @@ from unittest import TestCase + import glm + from moderngl_window.opengl.projection import Projection3D diff --git a/tests/test_resource_descriptions.py b/tests/test_resource_descriptions.py index 6a5b4eb..2c8ad7b 100644 --- a/tests/test_resource_descriptions.py +++ b/tests/test_resource_descriptions.py @@ -1,11 +1,7 @@ from unittest import TestCase -from moderngl_window.meta import ( - DataDescription, - ProgramDescription, - SceneDescription, - TextureDescription, -) +from moderngl_window.meta import (DataDescription, ProgramDescription, + SceneDescription, TextureDescription) from moderngl_window.resources.base import ResourceDescription diff --git a/tests/test_resources.py b/tests/test_resources.py index 68ce6e3..02a86df 100644 --- a/tests/test_resources.py +++ b/tests/test_resources.py @@ -1,11 +1,11 @@ -from unittest import TestCase from pathlib import Path +from unittest import TestCase + +from utils import settings_context from moderngl_window import resources -from moderngl_window.exceptions import ImproperlyConfigured from moderngl_window.conf import settings - -from utils import settings_context +from moderngl_window.exceptions import ImproperlyConfigured class ResourcesTestCase(TestCase): diff --git a/tests/test_scheduler.py b/tests/test_scheduler.py index 7a62b98..dc39934 100644 --- a/tests/test_scheduler.py +++ b/tests/test_scheduler.py @@ -1,7 +1,8 @@ import time from unittest import TestCase -from moderngl_window.utils.scheduler import Scheduler + from moderngl_window.timers.clock import Timer +from moderngl_window.utils.scheduler import Scheduler class SchedulingTestCase(TestCase): diff --git a/tests/test_screenshot.py b/tests/test_screenshot.py index b91e939..4390fbc 100644 --- a/tests/test_screenshot.py +++ b/tests/test_screenshot.py @@ -1,9 +1,11 @@ from pathlib import Path from unittest import mock -from moderngl_window import screenshot + from headless import HeadlessTestCase from utils import settings_context +from moderngl_window import screenshot + class ScreenshotTestCase(HeadlessTestCase): diff --git a/tests/test_shader_source.py b/tests/test_shader_source.py index 9311228..568b9ef 100644 --- a/tests/test_shader_source.py +++ b/tests/test_shader_source.py @@ -1,8 +1,9 @@ from pathlib import Path from unittest import TestCase -from moderngl_window.opengl import program + from moderngl_window import resources from moderngl_window.meta import DataDescription +from moderngl_window.opengl import program resources.register_dir((Path(__file__).parent / 'fixtures/resources').resolve()) diff --git a/tests/test_timers.py b/tests/test_timers.py index 70f121d..2c7734c 100644 --- a/tests/test_timers.py +++ b/tests/test_timers.py @@ -1,5 +1,6 @@ import time from unittest import TestCase + from moderngl_window.timers import clock @@ -22,3 +23,10 @@ def test_clock_timer(self): pos, duration = timer.stop() self.assertTrue(pos >= 10) self.assertTrue(duration >= 0) + + def test_not_started(self) -> None: + """Make sure the timer return 0 when it is never started""" + timer = clock.Timer() + t, real_t = timer.stop() + self.assertTrue(t == 0) + self.assertTrue(real_t == 0) diff --git a/tests/test_vao.py b/tests/test_vao.py index 97dfe93..559202e 100644 --- a/tests/test_vao.py +++ b/tests/test_vao.py @@ -1,7 +1,7 @@ import moderngl import numpy - from headless import HeadlessTestCase + from moderngl_window.opengl.vao import VAO, BufferInfo, VAOError diff --git a/tests/test_windowconfig.py b/tests/test_windowconfig.py index 0829dd2..eecfb73 100644 --- a/tests/test_windowconfig.py +++ b/tests/test_windowconfig.py @@ -2,6 +2,7 @@ import moderngl from headless import WindowConfigTestCase + from moderngl_window import WindowConfig from moderngl_window.scene import Scene diff --git a/tests/utils.py b/tests/utils.py index 3c2f088..bc838f0 100644 --- a/tests/utils.py +++ b/tests/utils.py @@ -1,6 +1,6 @@ -from contextlib import contextmanager -import string import random +import string +from contextlib import contextmanager from moderngl_window import conf