diff --git a/CHANGELOG.md b/CHANGELOG.md index b8b162edd..43809bce5 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 433f3a00a..9d7c69023 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 8f12a760a..52426735a 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 c3fd7b583..9828b12cd 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 9e8726b49..a7f3af6b8 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 b348f0550..6bc04c069 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 b0a863036..49f3d6e18 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 447822fe7..cd3580d97 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 000000000..a04dc1530 --- /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