From b4583e66fe915d60910ebc326878dc80593a229e Mon Sep 17 00:00:00 2001 From: Paul V Craven Date: Thu, 8 Oct 2026 13:40:12 -0500 Subject: [PATCH] Raise TypeError for unknown keyword arguments to sprites Sprite and BasicSprite took **kwargs and ignored anything they didn't recognize, so a wrong or outdated argument silently did nothing. They now accept only their named parameters (Sprite gains visible, the one extra parameter BasicSprite takes), so Python raises TypeError. SpriteSolidColor, SpriteCircle, TextureAnimationSprite and AnimatedWalkingSprite took **kwargs and dropped them entirely; they now pass them on to Sprite, so unknown ones raise and valid ones such as angle and visible work. Arguments that had silently done nothing: - tilemap image layers passed filename and hit_box_algorithm (the texture is already loaded with that hit box algorithm) - animated tiles passed path_or_texture (the animation sets the texture) - platform tutorial steps 19 and 20 passed scaling instead of scale, so bullets were drawn at full size instead of 80% Co-Authored-By: Claude Opus 5.5 --- CHANGELOG.md | 3 ++ .../examples/platform_tutorial/19_shooting.py | 2 +- arcade/examples/platform_tutorial/20_views.py | 2 +- arcade/sprite/animated.py | 6 +++ arcade/sprite/base.py | 3 +- arcade/sprite/colored.py | 7 ++- arcade/sprite/sprite.py | 6 ++- arcade/tilemap/tilemap.py | 7 ++- .../unit/sprite/test_sprite_init_arguments.py | 53 +++++++++++++++++++ 9 files changed, 79 insertions(+), 10 deletions(-) create mode 100644 tests/unit/sprite/test_sprite_init_arguments.py diff --git a/CHANGELOG.md b/CHANGELOG.md index b8b162edd4..43809bce57 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -47,6 +47,9 @@ Arcade [PyPi Release History](https://pypi.org/project/arcade/#history) page. - Fixed `InputManager.parse()` raising `AttributeError`, so a saved input configuration couldn't be loaded. ### Breaking Changes +- `Sprite`, `BasicSprite`, `SpriteSolidColor`, `SpriteCircle`, `TextureAnimationSprite` and `AnimatedWalkingSprite` raise `TypeError` for keyword arguments they don't accept, instead of silently ignoring them. For example, `Sprite(path, hit_box_algorithm=...)` used to do nothing; set the hit box algorithm when loading the texture instead. `Sprite` now takes `visible` as a named argument. + - `SpriteSolidColor`, `SpriteCircle`, `TextureAnimationSprite` and `AnimatedWalkingSprite` now pass extra keyword arguments on to `Sprite`, so arguments such as `angle` and `visible` work. They used to be dropped. + - This found arguments that had silently done nothing: tilemap image layers passed `filename` and `hit_box_algorithm`, animated tiles passed `path_or_texture`, and platform tutorial steps 19 and 20 passed `scaling` instead of `scale`, so their bullets were drawn at full size instead of 80%. - `arcade.math.get_angle_degrees()` now measures clockwise from straight up, like `Sprite.angle`: 0 is up, 90 is right, 180 is down and -90 is left. It used to measure clockwise from the right, which disagreed with `get_angle_radians()`, which already measured from up ([#2754](https://github.com/pythonarcade/arcade/issues/2754)). The two now always give the same angle in different units. A sprite whose image points up faces a point with `sprite.angle = get_angle_degrees(...)`. For the old value, subtract 90. - Fixed the `sprite_rotate_around_tank` example's corrected barrel, which sat 90 degrees off from the mouse, and updated `turn_and_move` for the new angles. `turn_and_move`'s sprite also now stops when moving straight along an axis, which it never did. diff --git a/arcade/examples/platform_tutorial/19_shooting.py b/arcade/examples/platform_tutorial/19_shooting.py index 433f3a00a5..9d7c690231 100644 --- a/arcade/examples/platform_tutorial/19_shooting.py +++ b/arcade/examples/platform_tutorial/19_shooting.py @@ -345,7 +345,7 @@ def on_update(self, delta_time): arcade.play_sound(self.shoot_sound) bullet = arcade.Sprite( ":resources:images/space_shooter/laserBlue01.png", - scaling=0.8, + scale=0.8, ) if self.player_sprite.facing_direction == RIGHT_FACING: bullet.change_x = 12 diff --git a/arcade/examples/platform_tutorial/20_views.py b/arcade/examples/platform_tutorial/20_views.py index 8f12a760a2..52426735aa 100644 --- a/arcade/examples/platform_tutorial/20_views.py +++ b/arcade/examples/platform_tutorial/20_views.py @@ -371,7 +371,7 @@ def on_update(self, delta_time): arcade.play_sound(self.shoot_sound) bullet = arcade.Sprite( ":resources:images/space_shooter/laserBlue01.png", - scaling=0.8, + scale=0.8, ) if self.player_sprite.facing_direction == RIGHT_FACING: bullet.change_x = 12 diff --git a/arcade/sprite/animated.py b/arcade/sprite/animated.py index c3fd7b5831..9828b12cdf 100644 --- a/arcade/sprite/animated.py +++ b/arcade/sprite/animated.py @@ -139,6 +139,8 @@ class TextureAnimationSprite(Sprite): Initial y position of the sprite. scale: Initial scale of the sprite. + **kwargs: + Passed to :py:class:`~arcade.Sprite`, such as ``angle``, ``scale`` or ``visible``. """ def __init__( @@ -153,6 +155,7 @@ def __init__( scale=scale, center_x=center_x, center_y=center_y, + **kwargs, ) self._time = 0.0 self._animation: TextureAnimation | None = None @@ -229,6 +232,8 @@ class AnimatedWalkingSprite(Sprite): Initial x position of the sprite. center_y: Initial y position of the sprite. + **kwargs: + Passed to :py:class:`~arcade.Sprite`, such as ``angle``, ``scale`` or ``visible``. """ def __init__( @@ -243,6 +248,7 @@ def __init__( scale=scale, center_x=center_x, center_y=center_y, + **kwargs, ) self.state = FACE_RIGHT self.stand_right_textures: list[Texture] = [] diff --git a/arcade/sprite/base.py b/arcade/sprite/base.py index 9e8726b497..a7f3af6b8f 100644 --- a/arcade/sprite/base.py +++ b/arcade/sprite/base.py @@ -39,6 +39,8 @@ class BasicSprite: Location of the sprite along the X axis in pixels. center_y: Location of the sprite along the Y axis in pixels. + visible: + Whether the sprite is drawn. """ __slots__ = ( @@ -63,7 +65,6 @@ def __init__( center_x: float = 0, center_y: float = 0, visible: bool = True, - **kwargs: Any, ) -> None: self._position = (center_x, center_y) self._depth = 0.0 diff --git a/arcade/sprite/colored.py b/arcade/sprite/colored.py index b348f05506..6bc04c0695 100644 --- a/arcade/sprite/colored.py +++ b/arcade/sprite/colored.py @@ -45,6 +45,8 @@ class SpriteSolidColor(Sprite): an RGBA tuple, or an RGB tuple. angle: Initial angle of the sprite in degrees + **kwargs: + Passed to :py:class:`~arcade.Sprite`, such as ``angle``, ``scale`` or ``visible``. """ __slots__ = () @@ -84,6 +86,7 @@ def __init__( center_x=center_x, center_y=center_y, angle=angle, + **kwargs, ) self.color = Color.from_iterable(color) @@ -143,6 +146,8 @@ class SpriteCircle(Sprite): Initial x position of the sprite center_y: Initial y position of the sprite + **kwargs: + Passed to :py:class:`~arcade.Sprite`, such as ``angle``, ``scale`` or ``visible``. """ # Local weak cache for textures to avoid creating multiple instances with the same configuration @@ -180,5 +185,5 @@ def __init__( self.__class__._texture_cache[cache_key] = texture # apply results to the new sprite - super().__init__(texture, center_x=center_x, center_y=center_y) + super().__init__(texture, center_x=center_x, center_y=center_y, **kwargs) self.color = Color.from_iterable(color) diff --git a/arcade/sprite/sprite.py b/arcade/sprite/sprite.py index b0a8630360..49f3d6e183 100644 --- a/arcade/sprite/sprite.py +++ b/arcade/sprite/sprite.py @@ -43,6 +43,8 @@ class Sprite(BasicSprite, PymunkMixin): Show the image at this many times its original size. angle: The initial rotation of the sprite in degrees + visible: + Whether the sprite is drawn. """ __slots__ = ( @@ -68,7 +70,7 @@ def __init__( center_x: float = 0.0, center_y: float = 0.0, angle: float = 0.0, - **kwargs: Any, + visible: bool = True, ) -> None: if isinstance(path_or_texture, Texture): _texture = path_or_texture @@ -87,7 +89,7 @@ def __init__( scale=scale, center_x=center_x, center_y=center_y, - **kwargs, + visible=visible, ) PymunkMixin.__init__(self) diff --git a/arcade/tilemap/tilemap.py b/arcade/tilemap/tilemap.py index 447822fe70..cd3580d979 100644 --- a/arcade/tilemap/tilemap.py +++ b/arcade/tilemap/tilemap.py @@ -512,8 +512,8 @@ def _create_sprite_from_tile( Custom classes for animated tiles must subclass TextureAnimationSprite. """ ) - # print(custom_class.__name__) - args = {"path_or_texture": image_file, "scale": scaling} + # The animation sets the texture + args = {"scale": scaling} my_sprite = custom_class(**custom_class_args, **args) # type: ignore else: if not custom_class: @@ -737,11 +737,10 @@ def _process_image_layer( """ ) + # The texture was loaded with the hit box algorithm above args = { - "filename": image_file, "scale": scaling, "path_or_texture": my_texture, - "hit_box_algorithm": hit_box_algorithm, } my_sprite = custom_class(**custom_class_args, **args) # type: ignore diff --git a/tests/unit/sprite/test_sprite_init_arguments.py b/tests/unit/sprite/test_sprite_init_arguments.py new file mode 100644 index 0000000000..a04dc15304 --- /dev/null +++ b/tests/unit/sprite/test_sprite_init_arguments.py @@ -0,0 +1,53 @@ +"""Sprites raise TypeError for keyword arguments they don't accept.""" + +import pytest + +import arcade + +COIN = ":resources:images/items/coinGold.png" + + +@pytest.mark.parametrize( + "make", + [ + lambda: arcade.Sprite(COIN, hit_box_algorithm="None"), + lambda: arcade.BasicSprite(arcade.load_texture(COIN), scaling=2.0), + lambda: arcade.SpriteSolidColor(10, 10, colour=(255, 0, 0)), + lambda: arcade.SpriteCircle(5, arcade.color.RED, bogus=1), + lambda: arcade.TextureAnimationSprite(bogus=1), + lambda: arcade.AnimatedWalkingSprite(bogus=1), + ], + ids=[ + "Sprite", + "BasicSprite", + "SpriteSolidColor", + "SpriteCircle", + "TextureAnimationSprite", + "AnimatedWalkingSprite", + ], +) +def test_unknown_keyword_argument_raises(make): + with pytest.raises(TypeError, match="unexpected keyword argument"): + make() + + +def test_subclass_init_with_unknown_argument_raises(): + class Card(arcade.Sprite): + def __init__(self): + super().__init__(COIN, 1.0, hit_box_algorithm="None") + + with pytest.raises(TypeError, match="hit_box_algorithm"): + Card() + + +def test_visible_argument(): + assert arcade.Sprite(COIN, visible=False).visible is False + assert arcade.BasicSprite(arcade.load_texture(COIN), visible=False).visible is False + + +def test_subclasses_pass_sprite_arguments_on(): + # These used to be accepted and silently ignored + assert arcade.SpriteCircle(5, arcade.color.RED, angle=30).angle == 30 + assert arcade.SpriteSolidColor(10, 10, visible=False).visible is False + assert arcade.TextureAnimationSprite(angle=45).angle == 45 + assert arcade.AnimatedWalkingSprite(angle=90, visible=False).angle == 90