Skip to content

Add exception-raising copy dunders via reusable decorator #2076

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 11 commits into from
Apr 26, 2024
2 changes: 2 additions & 0 deletions arcade/gui/widgets/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -34,6 +34,7 @@
from arcade.gui.property import Property, bind, ListProperty
from arcade.gui.surface import Surface
from arcade.types import RGBA255, Color, Point, AsFloat
from arcade.utils import copy_dunders_unimplemented

if TYPE_CHECKING:
from arcade.gui.ui_manager import UIManager
Expand Down Expand Up @@ -263,6 +264,7 @@ class _ChildEntry(NamedTuple):
data: Dict


@copy_dunders_unimplemented
class UIWidget(EventDispatcher, ABC):
"""
The :class:`UIWidget` class is the base class required for creating widgets.
Expand Down
4 changes: 4 additions & 0 deletions arcade/physics_engines.py
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,8 @@
"PhysicsEnginePlatformer"
]

from arcade.utils import copy_dunders_unimplemented


def _circular_check(player: Sprite, walls: List[SpriteList]):
"""
Expand Down Expand Up @@ -221,6 +223,7 @@ def _move_sprite(moving_sprite: Sprite, walls: List[SpriteList[SpriteType]], ram
return complete_hit_list


@copy_dunders_unimplemented
class PhysicsEngineSimple:
"""
Simplistic physics engine for use in games without gravity, such as top-down
Expand Down Expand Up @@ -266,6 +269,7 @@ def update(self):
return _move_sprite(self.player_sprite, self.walls, ramp_up=False)


@copy_dunders_unimplemented
class PhysicsEnginePlatformer:
"""
Simplistic physics engine for use in a platformer. It is easier to get
Expand Down
3 changes: 3 additions & 0 deletions arcade/pymunk_physics_engine.py
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,7 @@
"PymunkPhysicsEngine"
]

from arcade.utils import copy_dunders_unimplemented

LOG = logging.getLogger(__name__)

Expand All @@ -36,6 +37,8 @@ class PymunkException(Exception):
pass


# Temp fix for https://github.com/pythonarcade/arcade/issues/2074
@copy_dunders_unimplemented
class PymunkPhysicsEngine:
"""
Pymunk Physics Engine
Expand Down
4 changes: 4 additions & 0 deletions arcade/shape_list.py
Original file line number Diff line number Diff line change
Expand Up @@ -26,6 +26,8 @@
import pyglet.gl as gl

from arcade.types import Color, Point, PointList, RGBA255

from arcade.utils import copy_dunders_unimplemented
from arcade import get_window, get_points_for_thick_line
from arcade.gl import BufferDescription
from arcade.gl import Program
Expand Down Expand Up @@ -59,6 +61,7 @@
]


@copy_dunders_unimplemented # Temp fix for https://github.com/pythonarcade/arcade/issues/2074
class Shape:
"""
A container for arbitrary geometry representing a shape.
Expand Down Expand Up @@ -747,6 +750,7 @@ def create_ellipse_filled_with_colors(
TShape = TypeVar('TShape', bound=Shape)


@copy_dunders_unimplemented
class ShapeElementList(Generic[TShape]):
"""
A ShapeElementList is a list of shapes that can be drawn together
Expand Down
2 changes: 2 additions & 0 deletions arcade/sprite/base.py
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@
from arcade.color import BLACK, WHITE
from arcade.hitbox import HitBox
from arcade.texture import Texture
from arcade.utils import copy_dunders_unimplemented

if TYPE_CHECKING:
from arcade.sprite_list import SpriteList
Expand All @@ -15,6 +16,7 @@
SpriteType = TypeVar("SpriteType", bound="BasicSprite")


@copy_dunders_unimplemented # See https://github.com/pythonarcade/arcade/issues/2074
class BasicSprite:
"""
The absolute minimum needed for a sprite.
Expand Down
2 changes: 2 additions & 0 deletions arcade/sprite_list/sprite_list.py
Original file line number Diff line number Diff line change
Expand Up @@ -38,6 +38,7 @@
from arcade.gl.types import OpenGlFilter, BlendFunction, PyGLenum
from arcade.gl.buffer import Buffer
from arcade.gl.vertex_array import Geometry
from arcade.utils import copy_dunders_unimplemented

if TYPE_CHECKING:
from arcade import Texture, TextureAtlas
Expand All @@ -53,6 +54,7 @@
_DEFAULT_CAPACITY = 100


@copy_dunders_unimplemented # Temp fixes https://github.com/pythonarcade/arcade/issues/2074
class SpriteList(Generic[SpriteType]):
"""
The purpose of the spriteList is to batch draw a list of sprites.
Expand Down
50 changes: 50 additions & 0 deletions arcade/utils.py
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,7 @@
"NormalizedRangeError",
"PerformanceWarning",
"ReplacementWarning",
"copy_dunders_unimplemented",
"warning",
"generate_uuid_from_kwargs",
"is_raspberry_pi",
Expand Down Expand Up @@ -111,6 +112,55 @@ def __init__(self, var_name: str, value: float):
super().__init__(var_name, value, 0.0, 1.0)


_TType = TypeVar('_TType', bound=Type)

def copy_dunders_unimplemented(decorated_type: _TType) -> _TType:
"""Decorator stubs dunders raising :py:class:`NotImplementedError`.

Temp fixes https://github.com/pythonarcade/arcade/issues/2074 by
stubbing the following instance methods:

* :py:meth:`object.__copy__` (used by :py:func:`copy.copy`)
* :py:meth:`object.__deepcopy__` (used by :py:func:`copy.deepcopy`)

Example usage:

.. code-block:: python

import copy
from arcade,utils import copy_dunders_unimplemented
from arcade.hypothetical_module import HypotheticalNasty

# Example usage
@copy_dunders_unimplemented
class CantCopy:
def __init__(self, nasty_state: HypotheticalNasty):
self.nasty_state = nasty_state

instance = CantCopy(HypotheticalNasty())

# These raise NotImplementedError
this_line_raises = copy.deepcopy(instance)
this_line_also_raises = copy.copy(instance)


"""
def __copy__(self): # noqa
raise NotImplementedError(
f"{self.__class__.__name__} does not implement __copy__, but"
f"you may implement it on a custom subclass."
)
decorated_type.__copy__ = __copy__

def __deepcopy__(self, memo): # noqa
raise NotImplementedError(
f"{self.__class__.__name__} does not implement __deepcopy__,"
f" but you may implement it on a custom subclass."
)
decorated_type.__deepcopy__ = __deepcopy__

return decorated_type

class PerformanceWarning(Warning):
"""Use this for issuing performance warnings."""
pass
Expand Down
13 changes: 13 additions & 0 deletions tests/unit/physics_engine/test_physics_engine2.py
Original file line number Diff line number Diff line change
@@ -1,4 +1,7 @@
""" Physics engine tests. """
import copy

import pytest

import arcade

Expand Down Expand Up @@ -287,6 +290,14 @@ def platformer_tests(moving_sprite, wall_list, physics_engine):
assert moving_sprite.position == (3, -6)


# Temp fix for https://github.com/pythonarcade/arcade/issues/2074
def nocopy_tests(physics_engine):
with pytest.raises(NotImplementedError):
_ = copy.copy(physics_engine)
with pytest.raises(NotImplementedError):
_ = copy.deepcopy(physics_engine)


def test_main(window: arcade.Window):
character_list = arcade.SpriteList()
wall_list = arcade.SpriteList()
Expand All @@ -303,9 +314,11 @@ def test_main(window: arcade.Window):
physics_engine = arcade.PhysicsEngineSimple(moving_sprite, wall_list)
basic_tests(moving_sprite, wall_list, physics_engine)
simple_engine_tests(moving_sprite, wall_list, physics_engine)
nocopy_tests(physics_engine)

physics_engine = arcade.PhysicsEnginePlatformer(
moving_sprite, wall_list, gravity_constant=0.0
)
basic_tests(moving_sprite, wall_list, physics_engine)
platformer_tests(moving_sprite, wall_list, physics_engine)
nocopy_tests(physics_engine)
12 changes: 12 additions & 0 deletions tests/unit/physics_engine/test_pymunk.py
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,18 @@ def test_pymunk():
assert(my_sprite.center_y == -300.0)


# Temp fix for https://github.com/pythonarcade/arcade/issues/2074
def test_pymunk_engine_nocopy():
import copy
physics_engine = arcade.PymunkPhysicsEngine(
damping=1.0, gravity=(0, -100))

with pytest.raises(NotImplementedError):
_ = copy.copy(physics_engine)
with pytest.raises(NotImplementedError):
_ = copy.deepcopy(physics_engine)


@pytest.mark.parametrize("moment_of_inertia_arg_name",
(
"moment_of_inertia",
Expand Down
39 changes: 34 additions & 5 deletions tests/unit/shape_list/test_buffered_drawing.py
Original file line number Diff line number Diff line change
@@ -1,3 +1,6 @@
import copy
import pytest

import arcade
from arcade.shape_list import (
ShapeElementList,
Expand All @@ -18,7 +21,9 @@
SCREEN_HEIGHT = 600


def test_buffered_drawing(window):
@pytest.fixture
def shape_list_instance() -> ShapeElementList:

shape_list = ShapeElementList()

center_x = 0
Expand Down Expand Up @@ -84,11 +89,35 @@ def test_buffered_drawing(window):
shape = create_line_generic(points, arcade.color.ALIZARIN_CRIMSON, gl.GL_TRIANGLE_FAN)
shape_list.append(shape)

shape_list.center_x = 200
shape_list.center_y = 200
return shape_list


def test_shape_copy_dunders_raise_notimplemented_error(window, shape_list_instance):

for shape in shape_list_instance:
with pytest.raises(NotImplementedError):
_ = copy.copy(shape)
with pytest.raises(NotImplementedError):
_ = copy.deepcopy(shape)


# Temp fix for https://github.com/pythonarcade/arcade/issues/2074
def test_shapeelementlist_copy_dunders_raise_notimplemented_error(window, shape_list_instance):

with pytest.raises(NotImplementedError):
_ = copy.copy(shape_list_instance)

with pytest.raises(NotImplementedError):
_ = copy.deepcopy(shape_list_instance)


def test_buffered_drawing(window, shape_list_instance):

shape_list_instance.center_x = 200
shape_list_instance.center_y = 200

for _ in range(10):
shape_list.draw()
shape_list_instance.draw()
window.flip()
window.clear()
shape_list.move(1, 1)
shape_list_instance.move(1, 1)
43 changes: 43 additions & 0 deletions tests/unit/sprite/test_copy_dunders.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,43 @@
import copy
import pytest

import arcade


def test_copy_dunders_raise_notimplementederror():
"""Make sure our sprite types raise NotImplentedError for copy dunders.

See the following GitHub issue for more context:
https://github.com/pythonarcade/arcade/issues/2074
"""

# Make sure BasicSprite raises NotImplementedError
texture = arcade.load_texture(":resources:images/animated_characters/female_person/femalePerson_idle.png")
basic_sprite = arcade.BasicSprite(texture)

with pytest.raises(NotImplementedError):
copy.copy(basic_sprite)

with pytest.raises(NotImplementedError):
copy.deepcopy(basic_sprite)

sprite = arcade.Sprite(texture)
with pytest.raises(NotImplementedError):
copy.copy(sprite)

with pytest.raises(NotImplementedError):
copy.deepcopy(sprite)

circle = arcade.SpriteCircle(5, arcade.color.RED)
with pytest.raises(NotImplementedError):
copy.copy(circle)

with pytest.raises(NotImplementedError):
copy.deepcopy(circle)

solid = arcade.SpriteSolidColor(10, 10, color=arcade.color.RED)
with pytest.raises(NotImplementedError):
copy.copy(solid)

with pytest.raises(NotImplementedError):
copy.deepcopy(solid)
12 changes: 12 additions & 0 deletions tests/unit/spritelist/test_spritelist.py
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,18 @@ def make_named_sprites(amount):
return spritelist


# Temp fix for https://github.com/pythonarcade/arcade/issues/2074
def test_copy_dunder_stubs_raise_notimplementederror():
spritelist = arcade.SpriteList()
import copy

with pytest.raises(NotImplementedError):
_ = copy.copy(spritelist)

with pytest.raises(NotImplementedError):
_ = copy.deepcopy(spritelist)


def test_it_can_extend_a_spritelist_from_a_list():
spritelist = arcade.SpriteList()
sprites = []
Expand Down
Loading