Skip to content

Commit b11fb9a

Browse files
committed
Introduce SpriteSequence, a covariant supertype of SpriteList.
This is done by analogy to `collections.abc.Sequence`, which is a covariant supertype of `list`. Before this commit, many parts of the codebase used `SpriteList`s without type arguments (defaulting to `Unknown`). That was the only way to allow reasonable usages of the given methods and attributes. However, doing so results in weaker typing. Using `SpriteSequence`, we can add correct type arguments to almost all of the references that were using `SpriteList`s before. The only missing pieces are `Scene` and `TileMap`. Unfortunately, their APIs are fundamentally unsound wrt. the type arguments of their `SpriteList`s. We cannot make it sound without breaking their APIs, so we do not change them. As a bonus, we can now create lists of `SpriteList`s with varying type arguments, and generically call `draw` or `update` on them. Previously, the only common supertype of `SpriteList[A]` and `SpriteList[B]` was `object`, which meant it was not possible to call those methods on them. In a sense, that ability mostly subsumes the convenience provided by `Scene`. A `list[SpriteSequence[BasicSprite]]` is almost as convenient, while being type-safe.
1 parent 47b1961 commit b11fb9a

File tree

11 files changed

+264
-119
lines changed

11 files changed

+264
-119
lines changed

Diff for: arcade/__init__.py

+4
Original file line numberDiff line numberDiff line change
@@ -168,6 +168,7 @@ def configure_logging(level: int | None = None):
168168
from .sprite import PyMunk
169169
from .sprite import PymunkMixin
170170
from .sprite import SpriteType
171+
from .sprite import SpriteType_co
171172
from .sprite import Sprite
172173
from .sprite import BasicSprite
173174

@@ -176,6 +177,7 @@ def configure_logging(level: int | None = None):
176177
from .sprite import SpriteSolidColor
177178

178179
from .sprite_list import SpriteList
180+
from .sprite_list import SpriteSequence
179181
from .sprite_list import check_for_collision
180182
from .sprite_list import check_for_collision_with_list
181183
from .sprite_list import check_for_collision_with_lists
@@ -283,9 +285,11 @@ def configure_logging(level: int | None = None):
283285
"BasicSprite",
284286
"Sprite",
285287
"SpriteType",
288+
"SpriteType_co",
286289
"PymunkMixin",
287290
"SpriteCircle",
288291
"SpriteList",
292+
"SpriteSequence",
289293
"SpriteSolidColor",
290294
"Text",
291295
"Texture",

Diff for: arcade/future/input/input_manager_example.py

+1-1
Original file line numberDiff line numberDiff line change
@@ -26,7 +26,7 @@ class Player(arcade.Sprite):
2626
def __init__(
2727
self,
2828
texture,
29-
walls: arcade.SpriteList[arcade.Sprite],
29+
walls: arcade.SpriteSequence[arcade.BasicSprite],
3030
input_manager_template: InputManager,
3131
controller: pyglet.input.Controller | None = None,
3232
center_x: float = 0.0,

Diff for: arcade/paths.py

+12-4
Original file line numberDiff line numberDiff line change
@@ -4,14 +4,22 @@
44

55
import math
66

7-
from arcade import Sprite, SpriteList, check_for_collision_with_list, get_sprites_at_point
7+
from arcade import (
8+
BasicSprite,
9+
Sprite,
10+
SpriteSequence,
11+
check_for_collision_with_list,
12+
get_sprites_at_point,
13+
)
814
from arcade.math import get_distance, lerp_2d
915
from arcade.types import Point2
1016

1117
__all__ = ["AStarBarrierList", "astar_calculate_path", "has_line_of_sight"]
1218

1319

14-
def _spot_is_blocked(position: Point2, moving_sprite: Sprite, blocking_sprites: SpriteList) -> bool:
20+
def _spot_is_blocked(
21+
position: Point2, moving_sprite: Sprite, blocking_sprites: SpriteSequence[BasicSprite]
22+
) -> bool:
1523
"""
1624
Return if position is blocked
1725
@@ -275,7 +283,7 @@ class AStarBarrierList:
275283
def __init__(
276284
self,
277285
moving_sprite: Sprite,
278-
blocking_sprites: SpriteList,
286+
blocking_sprites: SpriteSequence[BasicSprite],
279287
grid_size: int,
280288
left: int,
281289
right: int,
@@ -372,7 +380,7 @@ def astar_calculate_path(
372380
def has_line_of_sight(
373381
observer: Point2,
374382
target: Point2,
375-
walls: SpriteList,
383+
walls: SpriteSequence[BasicSprite],
376384
max_distance: float = float("inf"),
377385
check_resolution: int = 2,
378386
) -> bool:

Diff for: arcade/physics_engines.py

+36-22
Original file line numberDiff line numberDiff line change
@@ -8,7 +8,7 @@
88
from arcade import (
99
BasicSprite,
1010
Sprite,
11-
SpriteList,
11+
SpriteSequence,
1212
SpriteType,
1313
check_for_collision,
1414
check_for_collision_with_lists,
@@ -20,7 +20,7 @@
2020
from arcade.utils import Chain, copy_dunders_unimplemented
2121

2222

23-
def _wiggle_until_free(colliding: Sprite, walls: Iterable[SpriteList]) -> None:
23+
def _wiggle_until_free(colliding: Sprite, walls: Iterable[SpriteSequence[BasicSprite]]) -> None:
2424
"""Kludge to 'guess' a colliding sprite out of a collision.
2525
2626
It works by iterating over increasing wiggle sizes of 8 points
@@ -80,7 +80,7 @@ def _wiggle_until_free(colliding: Sprite, walls: Iterable[SpriteList]) -> None:
8080

8181

8282
def _move_sprite(
83-
moving_sprite: Sprite, can_collide: Iterable[SpriteList[SpriteType]], ramp_up: bool
83+
moving_sprite: Sprite, can_collide: Iterable[SpriteSequence[SpriteType]], ramp_up: bool
8484
) -> list[SpriteType]:
8585
"""Update a sprite's angle and position, returning a list of collisions.
8686
@@ -273,11 +273,14 @@ def _move_sprite(
273273
return complete_hit_list
274274

275275

276-
def _add_to_list(dest: list[SpriteList], source: SpriteList | Iterable[SpriteList] | None) -> None:
277-
"""Helper function to add a SpriteList or list of SpriteLists to a list."""
276+
def _add_to_list(
277+
dest: list[SpriteSequence[SpriteType]],
278+
source: SpriteSequence[SpriteType] | Iterable[SpriteSequence[SpriteType]] | None,
279+
) -> None:
280+
"""Helper function to add a SpriteSequence or list of SpriteSequences to a list."""
278281
if not source:
279282
return
280-
elif isinstance(source, SpriteList):
283+
elif isinstance(source, SpriteSequence):
281284
dest.append(source)
282285
else:
283286
dest.extend(source)
@@ -310,17 +313,17 @@ class PhysicsEngineSimple:
310313
def __init__(
311314
self,
312315
player_sprite: Sprite,
313-
walls: SpriteList | Iterable[SpriteList] | None = None,
316+
walls: SpriteSequence[BasicSprite] | Iterable[SpriteSequence[BasicSprite]] | None = None,
314317
) -> None:
315318
self.player_sprite: Sprite = player_sprite
316319
"""The player-controlled :py:class:`.Sprite`."""
317-
self._walls: list[SpriteList] = []
320+
self._walls: list[SpriteSequence[BasicSprite]] = []
318321

319322
if walls:
320323
_add_to_list(self._walls, walls)
321324

322325
@property
323-
def walls(self) -> list[SpriteList]:
326+
def walls(self) -> list[SpriteSequence[BasicSprite]]:
324327
"""Which :py:class:`.SpriteList` instances block player movement.
325328
326329
.. important:: Avoid moving sprites in these lists!
@@ -334,7 +337,10 @@ def walls(self) -> list[SpriteList]:
334337
return self._walls
335338

336339
@walls.setter
337-
def walls(self, walls: SpriteList | Iterable[SpriteList] | None = None) -> None:
340+
def walls(
341+
self,
342+
walls: SpriteSequence[BasicSprite] | Iterable[SpriteSequence[BasicSprite]] | None = None,
343+
) -> None:
338344
if walls:
339345
_add_to_list(self._walls, walls)
340346
else:
@@ -429,17 +435,17 @@ class PhysicsEnginePlatformer:
429435
def __init__(
430436
self,
431437
player_sprite: Sprite,
432-
platforms: SpriteList | Iterable[SpriteList] | None = None,
438+
platforms: SpriteSequence[Sprite] | Iterable[SpriteSequence[Sprite]] | None = None,
433439
gravity_constant: float = 0.5,
434-
ladders: SpriteList | Iterable[SpriteList] | None = None,
435-
walls: SpriteList | Iterable[SpriteList] | None = None,
440+
ladders: SpriteSequence[BasicSprite] | Iterable[SpriteSequence[BasicSprite]] | None = None,
441+
walls: SpriteSequence[BasicSprite] | Iterable[SpriteSequence[BasicSprite]] | None = None,
436442
) -> None:
437443
if not isinstance(player_sprite, Sprite):
438444
raise TypeError("player_sprite must be a Sprite, not a basic_sprite!")
439445

440-
self._ladders: list[SpriteList] = []
441-
self._platforms: list[SpriteList] = []
442-
self._walls: list[SpriteList] = []
446+
self._ladders: list[SpriteSequence[BasicSprite]] = []
447+
self._platforms: list[SpriteSequence[Sprite]] = []
448+
self._walls: list[SpriteSequence[BasicSprite]] = []
443449
self._all_obstacles = Chain(self._walls, self._platforms)
444450

445451
_add_to_list(self._ladders, ladders)
@@ -517,7 +523,7 @@ def __init__(
517523
# TODO: figure out what do do with 15_ladders_moving_platforms.py
518524
# It's no longer used by any example or tutorial file
519525
@property
520-
def ladders(self) -> list[SpriteList]:
526+
def ladders(self) -> list[SpriteSequence[BasicSprite]]:
521527
"""Ladders turn off gravity while touched by the player.
522528
523529
This means that whenever the :py:attr:`player_sprite` collides
@@ -533,7 +539,10 @@ def ladders(self) -> list[SpriteList]:
533539
return self._ladders
534540

535541
@ladders.setter
536-
def ladders(self, ladders: SpriteList | Iterable[SpriteList] | None = None) -> None:
542+
def ladders(
543+
self,
544+
ladders: SpriteSequence[BasicSprite] | Iterable[SpriteSequence[BasicSprite]] | None = None,
545+
) -> None:
537546
if ladders:
538547
_add_to_list(self._ladders, ladders)
539548
else:
@@ -544,7 +553,7 @@ def ladders(self) -> None:
544553
self._ladders.clear()
545554

546555
@property
547-
def platforms(self) -> list[SpriteList]:
556+
def platforms(self) -> list[SpriteSequence[Sprite]]:
548557
""":py:class:`~arcade.sprite_list.sprite_list.SpriteList` instances containing platforms.
549558
550559
.. important:: For best performance, put non-moving terrain in
@@ -575,7 +584,9 @@ def platforms(self) -> list[SpriteList]:
575584
return self._platforms
576585

577586
@platforms.setter
578-
def platforms(self, platforms: SpriteList | Iterable[SpriteList] | None = None) -> None:
587+
def platforms(
588+
self, platforms: SpriteSequence[Sprite] | Iterable[SpriteSequence[Sprite]] | None = None
589+
) -> None:
579590
if platforms:
580591
_add_to_list(self._platforms, platforms)
581592
else:
@@ -586,7 +597,7 @@ def platforms(self) -> None:
586597
self._platforms.clear()
587598

588599
@property
589-
def walls(self) -> list[SpriteList]:
600+
def walls(self) -> list[SpriteSequence[BasicSprite]]:
590601
"""Exposes the :py:class:`SpriteList` instances use as terrain.
591602
592603
.. important:: For best performance, only add non-moving sprites!
@@ -611,7 +622,10 @@ def walls(self) -> list[SpriteList]:
611622
return self._walls
612623

613624
@walls.setter
614-
def walls(self, walls: SpriteList | Iterable[SpriteList] | None = None) -> None:
625+
def walls(
626+
self,
627+
walls: SpriteSequence[BasicSprite] | Iterable[SpriteSequence[BasicSprite]] | None = None,
628+
) -> None:
615629
if walls:
616630
_add_to_list(self._walls, walls)
617631
else:

Diff for: arcade/sprite/__init__.py

+2-1
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,7 @@
44

55
from arcade.texture import Texture
66
from arcade.resources import resolve
7-
from .base import BasicSprite, SpriteType
7+
from .base import BasicSprite, SpriteType, SpriteType_co
88
from .sprite import Sprite
99
from .mixins import PymunkMixin, PyMunk
1010
from .animated import (
@@ -69,6 +69,7 @@ def load_animated_gif(resource_name: str | Path) -> TextureAnimationSprite:
6969

7070
__all__ = [
7171
"SpriteType",
72+
"SpriteType_co",
7273
"BasicSprite",
7374
"Sprite",
7475
"PyMunk",

Diff for: arcade/sprite/base.py

+3
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,9 @@
1616
# Type from sprite that can be any BasicSprite or any subclass of BasicSprite
1717
SpriteType = TypeVar("SpriteType", bound="BasicSprite")
1818

19+
# Same as SpriteType, for covariant type parameters
20+
SpriteType_co = TypeVar("SpriteType_co", bound="BasicSprite", covariant=True)
21+
1922

2023
@copy_dunders_unimplemented # See https://github.com/pythonarcade/arcade/issues/2074
2124
class BasicSprite:

Diff for: arcade/sprite_list/__init__.py

+2-1
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
from .sprite_list import SpriteList
1+
from .sprite_list import SpriteList, SpriteSequence
22
from .spatial_hash import SpatialHash
33
from .collision import (
44
get_distance_between_sprites,
@@ -14,6 +14,7 @@
1414

1515
__all__ = [
1616
"SpriteList",
17+
"SpriteSequence",
1718
"SpatialHash",
1819
"get_distance_between_sprites",
1920
"get_closest_sprite",

0 commit comments

Comments
 (0)