diff --git a/arcade/camera.py b/arcade/camera.py index e8a2f283eb..4fdaabf246 100644 --- a/arcade/camera.py +++ b/arcade/camera.py @@ -384,7 +384,7 @@ def scale(self) -> Tuple[float, float]: return self._scale @scale.setter - def scale(self, new_scale: Tuple[int, int]) -> None: + def scale(self, new_scale: Tuple[float, float]) -> None: """ Sets the x, y scale (zoom property just sets scale to the same value). This also updates the projection matrix with an orthogonal @@ -393,7 +393,7 @@ def scale(self, new_scale: Tuple[int, int]) -> None: if new_scale[0] <= 0 or new_scale[1] <= 0: raise ValueError("Scale must be greater than zero") - self._scale = new_scale + self._scale = (float(new_scale[0]), float(new_scale[1])) # Changing the scale (zoom) affects both projection_matrix and view_matrix self._set_projection_matrix( diff --git a/arcade/context.py b/arcade/context.py index c7489b3605..9bb3d7fbcb 100644 --- a/arcade/context.py +++ b/arcade/context.py @@ -313,7 +313,7 @@ def view_matrix_2d(self) -> Mat4: :type: pyglet.math.Mat4 """ - self.window.view + return self.window.view @view_matrix_2d.setter def view_matrix_2d(self, value: Mat4): diff --git a/arcade/gui/examples/scroll_area.py b/arcade/gui/examples/scroll_area.py index ab76bab0ca..24936d6343 100644 --- a/arcade/gui/examples/scroll_area.py +++ b/arcade/gui/examples/scroll_area.py @@ -22,12 +22,13 @@ from arcade import Window from arcade.gui import UIManager, UIWidget, Property, Surface, UIDummy, UIEvent, bind, \ UIMouseDragEvent, UIMouseScrollEvent, UIMouseEvent, UIBoxLayout, UIFlatButton, UIInputText +from arcade.types import Point class UIScrollArea(UIWidget): - scroll_x = Property(default=0) - scroll_y = Property(default=0) - canvas_size = Property(default=(300, 300)) + scroll_x = Property[float](default=0.0) + scroll_y = Property[float](default=0.0) + canvas_size = Property[Point](default=(300.0, 300.0)) scroll_speed = 1.3 invert_scroll = False diff --git a/arcade/gui/property.py b/arcade/gui/property.py index f4003e735f..bc4a84bfc5 100644 --- a/arcade/gui/property.py +++ b/arcade/gui/property.py @@ -1,26 +1,24 @@ import sys import traceback -from typing import TypeVar +from typing import Any, Callable, Generic, Optional, Set, TypeVar, cast from weakref import WeakKeyDictionary, ref +P = TypeVar("P") -class _Obs: +class _Obs(Generic[P]): """ Internal holder for Property value and change listeners """ __slots__ = "value", "listeners" - def __init__(self): - self.value = None + def __init__(self, value: P): + self.value = value # This will keep any added listener even if it is not referenced anymore and would be garbage collected - self.listeners = set() - - -P = TypeVar("P") + self.listeners: Set[Callable[[], Any]] = set() -class Property: +class Property(Generic[P]): """ An observable property which triggers observers when changed. @@ -31,22 +29,21 @@ class Property: __slots__ = "name", "default_factory", "obs" name: str - def __init__(self, default=None, default_factory=None): + def __init__(self, default: Optional[P] = None, default_factory: Optional[Callable[[Any, Any], P]] = None): if default_factory is None: - default_factory = lambda prop, instance: default + default_factory = lambda prop, instance: cast(P, default) self.default_factory = default_factory - self.obs = WeakKeyDictionary() + self.obs: WeakKeyDictionary[Any, _Obs] = WeakKeyDictionary() def _get_obs(self, instance) -> _Obs: obs = self.obs.get(instance) if obs is None: - obs = _Obs() - obs.value = self.default_factory(self, instance) + obs = _Obs(self.default_factory(self, instance)) self.obs[instance] = obs return obs - def get(self, instance): + def get(self, instance) -> P: obs = self._get_obs(instance) return obs.value @@ -77,9 +74,9 @@ def bind(self, instance, callback): def __set_name__(self, owner, name): self.name = name - def __get__(self, instance, owner): + def __get__(self, instance, owner) -> P: if instance is None: - return self + return self # type: ignore return self.get(instance) def __set__(self, instance, value): diff --git a/arcade/gui/widgets/layout.py b/arcade/gui/widgets/layout.py index c97a9f0fcc..2beeba3b61 100644 --- a/arcade/gui/widgets/layout.py +++ b/arcade/gui/widgets/layout.py @@ -553,10 +553,10 @@ def _update_size_hints(self): [None for _ in range(self.column_count)] for _ in range(self.row_count) ] - max_width_per_column = [ + max_width_per_column: list[list[tuple[int, int]]] = [ [(0, 1) for _ in range(self.row_count)] for _ in range(self.column_count) ] - max_height_per_row = [ + max_height_per_row: list[list[tuple[int, int]]] = [ [(0, 1) for _ in range(self.column_count)] for _ in range(self.row_count) ] @@ -662,10 +662,10 @@ def do_layout(self): [None for _ in range(self.column_count)] for _ in range(self.row_count) ] - max_width_per_column = [ + max_width_per_column: list[list[tuple[float, int]]] = [ [(0, 1) for _ in range(self.row_count)] for _ in range(self.column_count) ] - max_height_per_row = [ + max_height_per_row: list[list[tuple[float, int]]] = [ [(0, 1) for _ in range(self.column_count)] for _ in range(self.row_count) ] diff --git a/arcade/gui/widgets/slider.py b/arcade/gui/widgets/slider.py index 3e9249c421..31d4c4724e 100644 --- a/arcade/gui/widgets/slider.py +++ b/arcade/gui/widgets/slider.py @@ -226,7 +226,7 @@ def do_render(self, surface: Surface): ) def _cursor_pos(self) -> Tuple[int, int]: - return self.value_x, self.y + self.height // 2 + return self.value_x, int(self.y + self.height // 2) def _is_on_cursor(self, x: float, y: float) -> bool: cursor_center_x, cursor_center_y = self._cursor_pos() diff --git a/arcade/gui/widgets/text.py b/arcade/gui/widgets/text.py index 16daadb6d1..4d8b55f6ed 100644 --- a/arcade/gui/widgets/text.py +++ b/arcade/gui/widgets/text.py @@ -20,7 +20,7 @@ from arcade.gui.surface import Surface from arcade.gui.widgets import UIWidget, Rect from arcade.gui.widgets.layout import UIAnchorLayout -from arcade.types import RGBA255, Color +from arcade.types import RGBA255, Color, RGBOrA255 class UILabel(UIWidget): @@ -62,7 +62,7 @@ def __init__( text: str = "", font_name=("Arial",), font_size: float = 12, - text_color: RGBA255 = (255, 255, 255, 255), + text_color: RGBOrA255 = (255, 255, 255, 255), bold=False, italic=False, align="left", @@ -244,7 +244,7 @@ def __init__( text: str = "", font_name=("Arial",), font_size: float = 12, - text_color: RGBA255 = (0, 0, 0, 255), + text_color: RGBOrA255 = (0, 0, 0, 255), multiline=False, size_hint=None, size_hint_min=None, diff --git a/arcade/hitbox/base.py b/arcade/hitbox/base.py index 993898704e..0065af3ee7 100644 --- a/arcade/hitbox/base.py +++ b/arcade/hitbox/base.py @@ -1,7 +1,7 @@ from __future__ import annotations from math import cos, radians, sin -from typing import Any, Tuple +from typing import Any, Sequence, Tuple from PIL.Image import Image @@ -196,7 +196,7 @@ def create_rotatable( self._points, position=self._position, scale=self._scale, angle=angle ) - def get_adjusted_points(self) -> PointList: + def get_adjusted_points(self) -> Sequence[Point]: """ Return the positions of points, scaled and offset from the center. @@ -207,7 +207,7 @@ def get_adjusted_points(self) -> PointList: * After properties affecting adjusted position were changed """ if not self._adjusted_cache_dirty: - return self._adjusted_points + return self._adjusted_points # type: ignore def _adjust_point(point) -> Point: x, y = point @@ -219,7 +219,7 @@ def _adjust_point(point) -> Point: self._adjusted_points = [_adjust_point(point) for point in self.points] self._adjusted_cache_dirty = False - return self._adjusted_points + return self._adjusted_points # type: ignore [return-value] class RotatableHitBox(HitBox): diff --git a/arcade/shape_list.py b/arcade/shape_list.py index bb60b3d54b..8d0fcc1f45 100644 --- a/arcade/shape_list.py +++ b/arcade/shape_list.py @@ -118,7 +118,7 @@ def draw(self): if self.geometry is None: self._init_geometry() - self.geometry.render(self.program, mode=self.mode) + self.geometry.render(self.program, mode=self.mode) # pyright: ignore [reportOptionalMemberAccess] def create_line( diff --git a/arcade/sprite/base.py b/arcade/sprite/base.py index 53d0bd05fb..ecbda85f53 100644 --- a/arcade/sprite/base.py +++ b/arcade/sprite/base.py @@ -1,7 +1,7 @@ from typing import TYPE_CHECKING, Iterable, List, TypeVar import arcade -from arcade.types import Point, Color, RGBA255 +from arcade.types import Point, Color, RGBA255, PointList from arcade.color import BLACK from arcade.hitbox import HitBox from arcade.texture import Texture @@ -579,7 +579,7 @@ def draw_hit_box(self, color: RGBA255 = BLACK, line_thickness: float = 2.0) -> N :param color: Color of box :param line_thickness: How thick the box should be """ - points = self.hit_box.get_adjusted_points() + points: PointList = self.hit_box.get_adjusted_points() # NOTE: This is a COPY operation. We don't want to modify the points. points = tuple(points) + tuple(points[:-1]) arcade.draw_line_strip(points, color=color, line_width=line_thickness) diff --git a/arcade/sprite_list/collision.py b/arcade/sprite_list/collision.py index 413558d64a..7edd6c0d15 100644 --- a/arcade/sprite_list/collision.py +++ b/arcade/sprite_list/collision.py @@ -299,7 +299,7 @@ def get_sprites_at_point(point: Point, sprite_list: SpriteList[SpriteType]) -> L ] -def get_sprites_at_exact_point(point: Point, sprite_list: SpriteList) -> List[SpriteType]: +def get_sprites_at_exact_point(point: Point, sprite_list: SpriteList[SpriteType]) -> List[SpriteType]: """ Get a list of sprites whose center_x, center_y match the given point. This does NOT return sprites that overlap the point, the center has to be an exact match. @@ -328,7 +328,7 @@ def get_sprites_at_exact_point(point: Point, sprite_list: SpriteList) -> List[Sp return [s for s in sprites_to_check if s.position == point] -def get_sprites_in_rect(rect: Rect, sprite_list: SpriteList) -> List[SpriteType]: +def get_sprites_in_rect(rect: Rect, sprite_list: SpriteList[SpriteType]) -> List[SpriteType]: """ Get a list of sprites in a particular rectangle. This function sees if any sprite overlaps the specified rectangle. If a sprite has a different center_x/center_y but touches the rectangle, diff --git a/arcade/text.py b/arcade/text.py index 1f55244017..646abd0d4d 100644 --- a/arcade/text.py +++ b/arcade/text.py @@ -7,7 +7,7 @@ import pyglet import arcade -from arcade.types import Color, Point, RGBA255 +from arcade.types import Color, Point, RGBA255, Point3, RGBOrA255 from arcade.resources import resolve from arcade.utils import PerformanceWarning, warning @@ -180,7 +180,7 @@ def __init__( text: str, start_x: float, start_y: float, - color: RGBA255 = arcade.color.WHITE, + color: RGBOrA255 = arcade.color.WHITE, font_size: float = 12, width: Optional[int] = 0, align: str = "left", @@ -569,7 +569,7 @@ def position(self) -> Point: return self._label.x, self._label.y @position.setter - def position(self, point: Point): + def position(self, point: Union[Point, Point3]): # Starting with Pyglet 2.0b2 label positions take a z parameter. if len(point) == 3: self._label.position = point diff --git a/arcade/texture/tools.py b/arcade/texture/tools.py index 4585a1e458..ee4bf89b46 100644 --- a/arcade/texture/tools.py +++ b/arcade/texture/tools.py @@ -1,7 +1,7 @@ from PIL import Image, ImageDraw import arcade from .texture import ImageData, Texture -from arcade.types import Point +from arcade.types import IPoint from arcade import cache _DEFAULT_TEXTURE = None @@ -17,7 +17,7 @@ def cleanup_texture_cache(): arcade.cache.image_data_cache.clear() -def get_default_texture(size: Point = _DEFAULT_IMAGE_SIZE) -> Texture: +def get_default_texture(size: IPoint = _DEFAULT_IMAGE_SIZE) -> Texture: """ Creates and returns a default texture and caches it internally for future use. @@ -35,7 +35,7 @@ def get_default_texture(size: Point = _DEFAULT_IMAGE_SIZE) -> Texture: return _DEFAULT_TEXTURE -def get_default_image(size: Point = _DEFAULT_IMAGE_SIZE) -> ImageData: +def get_default_image(size: IPoint = _DEFAULT_IMAGE_SIZE) -> ImageData: """ Generates and returns a default image and caches it internally for future use. diff --git a/arcade/texture/transforms.py b/arcade/texture/transforms.py index 8ebd82dae9..d5c28675e9 100644 --- a/arcade/texture/transforms.py +++ b/arcade/texture/transforms.py @@ -5,7 +5,7 @@ We don't actually transform pixel data, we simply transform the texture coordinates and hit box points. """ -from typing import Tuple +from typing import Dict, Tuple from enum import Enum from arcade.math import rotate_point from arcade.types import PointList @@ -217,7 +217,7 @@ def transform_hit_box_points( # but it's faster to just pre-calculate it. # Key is the vertex order # Value is the orientation (flip_left_right, flip_top_down, rotation) -ORIENTATIONS = { +ORIENTATIONS: Dict[Tuple[int, int, int, int], Tuple[int, bool, bool]] = { (0, 1, 2, 3): (0, False, False), # Default (2, 0, 3, 1): (90, False, False), # Rotate 90 (3, 2, 1, 0): (180, False, False), # Rotate 180 diff --git a/arcade/texture_atlas/base.py b/arcade/texture_atlas/base.py index 85a0ab5f95..f5f7883b8a 100644 --- a/arcade/texture_atlas/base.py +++ b/arcade/texture_atlas/base.py @@ -31,6 +31,7 @@ from weakref import WeakSet import PIL +import PIL.Image from PIL import Image, ImageDraw from pyglet.image.atlas import ( Allocator, @@ -1003,7 +1004,7 @@ def to_image( for rg in self._image_regions.values(): p1 = rg.x, rg.y p2 = rg.x + rg.width - 1, rg.y + rg.height - 1 - draw.rectangle([p1, p2], outline=border_color, width=1) + draw.rectangle((p1, p2), outline=border_color, width=1) if flip: image = image.transpose(Image.Transpose.FLIP_TOP_BOTTOM) diff --git a/arcade/tilemap/tilemap.py b/arcade/tilemap/tilemap.py index 9d86e2708b..a0d23d3019 100644 --- a/arcade/tilemap/tilemap.py +++ b/arcade/tilemap/tilemap.py @@ -12,7 +12,7 @@ import os from collections import OrderedDict from pathlib import Path -from typing import TYPE_CHECKING, Any, Dict, List, Optional, Tuple, Union, cast +from typing import TYPE_CHECKING, Any, Callable, Dict, List, Optional, Tuple, Union, cast import pytiled_parser import pytiled_parser.tiled_object @@ -46,8 +46,9 @@ "read_tmx" ] +prop_to_float = cast(Callable[[pytiled_parser.Property], float], float) -def _get_image_info_from_tileset(tile: pytiled_parser.Tile): +def _get_image_info_from_tileset(tile: pytiled_parser.Tile) -> Tuple[int, int, int, int]: image_x = 0 image_y = 0 if tile.tileset.image is not None: @@ -556,18 +557,18 @@ def _create_sprite_from_tile( if tile.flipped_vertically: for point in points: - point[1] *= -1 + point = point[0], point[1] * -1 if tile.flipped_horizontally: for point in points: - point[0] *= -1 + point = point[0] * -1, point[1] if tile.flipped_diagonally: for point in points: - point[0], point[1] = point[1], point[0] + point = point[1], point[0] my_sprite.hit_box = RotatableHitBox( - points, + cast(List[Point], points), position=my_sprite.position, angle=my_sprite.angle, scale=my_sprite.scale_xy, @@ -859,28 +860,28 @@ def _process_object_layer( my_sprite.alpha = int(opacity * 255) if cur_object.properties and "change_x" in cur_object.properties: - my_sprite.change_x = float(cur_object.properties["change_x"]) + my_sprite.change_x = prop_to_float(cur_object.properties["change_x"]) if cur_object.properties and "change_y" in cur_object.properties: - my_sprite.change_y = float(cur_object.properties["change_y"]) + my_sprite.change_y = prop_to_float(cur_object.properties["change_y"]) if cur_object.properties and "boundary_bottom" in cur_object.properties: - my_sprite.boundary_bottom = float( + my_sprite.boundary_bottom = prop_to_float( cur_object.properties["boundary_bottom"] ) if cur_object.properties and "boundary_top" in cur_object.properties: - my_sprite.boundary_top = float( + my_sprite.boundary_top = prop_to_float( cur_object.properties["boundary_top"] ) if cur_object.properties and "boundary_left" in cur_object.properties: - my_sprite.boundary_left = float( + my_sprite.boundary_left = prop_to_float( cur_object.properties["boundary_left"] ) if cur_object.properties and "boundary_right" in cur_object.properties: - my_sprite.boundary_right = float( + my_sprite.boundary_right = prop_to_float( cur_object.properties["boundary_right"] ) diff --git a/arcade/types.py b/arcade/types.py index 024250a42b..e66f7ca714 100644 --- a/arcade/types.py +++ b/arcade/types.py @@ -2,6 +2,7 @@ Module specifying data custom types used for type hinting. """ from array import array +import ctypes import random from collections import namedtuple from collections.abc import ByteString @@ -415,6 +416,7 @@ def random( # Point = Union[Tuple[float, float], List[float]] # Vector = Point Point = Tuple[float, float] +Point3 = Tuple[float, float, float] IPoint = Tuple[int, int] Vector = Point NamedPoint = namedtuple("NamedPoint", ["x", "y"]) @@ -440,4 +442,4 @@ class TiledObject(NamedTuple): # the PEP and Python doc for more information: # https://peps.python.org/pep-0688/ # https://docs.python.org/3/c-api/buffer.html -BufferProtocol = Union[ByteString, memoryview, array] +BufferProtocol = Union[ByteString, memoryview, array, ctypes.Array] diff --git a/doc/conf.py b/doc/conf.py index 6a820e888a..39b6512dba 100644 --- a/doc/conf.py +++ b/doc/conf.py @@ -33,7 +33,7 @@ # Don't change to # from arcade.version import VERSION # or read the docs build will fail. -from version import VERSION +from version import VERSION # pyright: ignore [reportMissingImports] RELEASE = VERSION diff --git a/make.py b/make.py index 7e9f777e6e..50551b356a 100755 --- a/make.py +++ b/make.py @@ -290,8 +290,8 @@ def latex(): run_doc([SPHINXBUILD, "-b", "latex", *ALLSPHINXOPTS, f"{BUILDDIR}/latex"]) print() print(f"Build finished; the LaTeX files are in {FULL_BUILD_PREFIX}/latex.") - print("Run \`make' in that directory to run these through (pdf)latex" + - "(use \`make latexpdf' here to do that automatically).") + print("Run `make' in that directory to run these through (pdf)latex" + + "(use `make latexpdf' here to do that automatically).") @app.command(rich_help_panel="Additional Doc Formats") @@ -344,8 +344,8 @@ def texinfo(): run_doc([SPHINXBUILD, "-b", "texinfo", *ALLSPHINXOPTS, f"{BUILDDIR}/texinfo"]) print() print(f"Build finished. The Texinfo files are in {FULL_BUILD_PREFIX}/texinfo.") - print("Run \`make' in that directory to run these through makeinfo" + - "(use \`make info' here to do that automatically).") + print("Run `make' in that directory to run these through makeinfo" + + "(use `make info' here to do that automatically).") @app.command(rich_help_panel="Additional Doc Formats") diff --git a/pyproject.toml b/pyproject.toml index 56ca120592..bfa9dcfc9a 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -24,7 +24,7 @@ dependencies = [ "pyglet>=2.0.7,<2.1", "pillow~=9.4.0", "pymunk~=6.4.0", - "pytiled-parser~=2.2.0" + "pytiled-parser~=2.2.3" ] dynamic = ["version"] @@ -103,10 +103,26 @@ norecursedirs = ["doc", "holding", "arcade/examples", "build", ".venv", "env", " [tool.pyright] include = ["arcade"] -exclude = ["venv", "arcade/examples", "arcade/experimental", "tests", "doc"] +exclude = [ + "venv", + "arcade/__pyinstaller", + "arcade/examples", + "arcade/experimental", + "tests", + "doc", + "make.py" +] +typeCheckingMode = "basic" # Use type info from pytiled_parser and pyglet, which do not ship `py.typed` file useLibraryCodeForTypes = true reportMissingTypeStubs = "none" +# Ignore diagnostics about values that might be `None` +reportOptionalCall = "none" +reportOptionalContextManager = "none" +reportOptionalIterable = "none" +reportOptionalMemberAccess = "none" +reportOptionalOperand = "none" +reportOptionalSubscript = "none" [tool.coverage.run] source = ["arcade"] diff --git a/tests/conftest.py b/tests/conftest.py index 595264ea08..c65d1668d6 100644 --- a/tests/conftest.py +++ b/tests/conftest.py @@ -2,7 +2,7 @@ from pathlib import Path if os.environ.get("ARCADE_PYTEST_USE_RUST"): - import arcade_accelerate + import arcade_accelerate # pyright: ignore [reportMissingImports] arcade_accelerate.bootstrap() import pytest