Skip to content

Commit 0df89b2

Browse files
authored
Add exception-raising copy dunders via reusable decorator (#2076)
* Add exception-raising BasicSprite copy dunders * Add tests + correct deepcopy dunder issue * Replace BasicSprite's copy dunders with reusable decorator * Add copy dunder decorator stub to SpriteList * Apply decorator to SpriteList * Add tests for SpriteList * Add copy dunder warning + tests for Shapes & ShapeElementList * Apply copy dunder decorator to basic physics engines * Remove Self from decorator helper * Add tests for simpler physics engine no-copy decoration * Add copy decorator + test for PyMunkPhysicsEngine * Add copy dunder decorator to UIWidget * Add TypeVar to decorator to preserve type info
1 parent a9640aa commit 0df89b2

12 files changed

Lines changed: 181 additions & 5 deletions

File tree

arcade/gui/widgets/__init__.py

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -34,6 +34,7 @@
3434
from arcade.gui.property import Property, bind, ListProperty
3535
from arcade.gui.surface import Surface
3636
from arcade.types import RGBA255, Color, Point, AsFloat
37+
from arcade.utils import copy_dunders_unimplemented
3738

3839
if TYPE_CHECKING:
3940
from arcade.gui.ui_manager import UIManager
@@ -263,6 +264,7 @@ class _ChildEntry(NamedTuple):
263264
data: Dict
264265

265266

267+
@copy_dunders_unimplemented
266268
class UIWidget(EventDispatcher, ABC):
267269
"""
268270
The :class:`UIWidget` class is the base class required for creating widgets.

arcade/physics_engines.py

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -21,6 +21,8 @@
2121
"PhysicsEnginePlatformer"
2222
]
2323

24+
from arcade.utils import copy_dunders_unimplemented
25+
2426

2527
def _circular_check(player: Sprite, walls: List[SpriteList]):
2628
"""
@@ -221,6 +223,7 @@ def _move_sprite(moving_sprite: Sprite, walls: List[SpriteList[SpriteType]], ram
221223
return complete_hit_list
222224

223225

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

268271

272+
@copy_dunders_unimplemented
269273
class PhysicsEnginePlatformer:
270274
"""
271275
Simplistic physics engine for use in a platformer. It is easier to get

arcade/pymunk_physics_engine.py

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -18,6 +18,7 @@
1818
"PymunkPhysicsEngine"
1919
]
2020

21+
from arcade.utils import copy_dunders_unimplemented
2122

2223
LOG = logging.getLogger(__name__)
2324

@@ -36,6 +37,8 @@ class PymunkException(Exception):
3637
pass
3738

3839

40+
# Temp fix for https://github.com/pythonarcade/arcade/issues/2074
41+
@copy_dunders_unimplemented
3942
class PymunkPhysicsEngine:
4043
"""
4144
Pymunk Physics Engine

arcade/shape_list.py

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -26,6 +26,8 @@
2626
import pyglet.gl as gl
2727

2828
from arcade.types import Color, Point, PointList, RGBA255
29+
30+
from arcade.utils import copy_dunders_unimplemented
2931
from arcade import get_window, get_points_for_thick_line
3032
from arcade.gl import BufferDescription
3133
from arcade.gl import Program
@@ -59,6 +61,7 @@
5961
]
6062

6163

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

749752

753+
@copy_dunders_unimplemented
750754
class ShapeElementList(Generic[TShape]):
751755
"""
752756
A ShapeElementList is a list of shapes that can be drawn together

arcade/sprite/base.py

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,7 @@
77
from arcade.color import BLACK, WHITE
88
from arcade.hitbox import HitBox
99
from arcade.texture import Texture
10+
from arcade.utils import copy_dunders_unimplemented
1011

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

1718

19+
@copy_dunders_unimplemented # See https://github.com/pythonarcade/arcade/issues/2074
1820
class BasicSprite:
1921
"""
2022
The absolute minimum needed for a sprite.

arcade/sprite_list/sprite_list.py

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -38,6 +38,7 @@
3838
from arcade.gl.types import OpenGlFilter, BlendFunction, PyGLenum
3939
from arcade.gl.buffer import Buffer
4040
from arcade.gl.vertex_array import Geometry
41+
from arcade.utils import copy_dunders_unimplemented
4142

4243
if TYPE_CHECKING:
4344
from arcade import Texture, TextureAtlas
@@ -53,6 +54,7 @@
5354
_DEFAULT_CAPACITY = 100
5455

5556

57+
@copy_dunders_unimplemented # Temp fixes https://github.com/pythonarcade/arcade/issues/2074
5658
class SpriteList(Generic[SpriteType]):
5759
"""
5860
The purpose of the spriteList is to batch draw a list of sprites.

arcade/utils.py

Lines changed: 50 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -24,6 +24,7 @@
2424
"NormalizedRangeError",
2525
"PerformanceWarning",
2626
"ReplacementWarning",
27+
"copy_dunders_unimplemented",
2728
"warning",
2829
"generate_uuid_from_kwargs",
2930
"is_raspberry_pi",
@@ -111,6 +112,55 @@ def __init__(self, var_name: str, value: float):
111112
super().__init__(var_name, value, 0.0, 1.0)
112113

113114

115+
_TType = TypeVar('_TType', bound=Type)
116+
117+
def copy_dunders_unimplemented(decorated_type: _TType) -> _TType:
118+
"""Decorator stubs dunders raising :py:class:`NotImplementedError`.
119+
120+
Temp fixes https://github.com/pythonarcade/arcade/issues/2074 by
121+
stubbing the following instance methods:
122+
123+
* :py:meth:`object.__copy__` (used by :py:func:`copy.copy`)
124+
* :py:meth:`object.__deepcopy__` (used by :py:func:`copy.deepcopy`)
125+
126+
Example usage:
127+
128+
.. code-block:: python
129+
130+
import copy
131+
from arcade,utils import copy_dunders_unimplemented
132+
from arcade.hypothetical_module import HypotheticalNasty
133+
134+
# Example usage
135+
@copy_dunders_unimplemented
136+
class CantCopy:
137+
def __init__(self, nasty_state: HypotheticalNasty):
138+
self.nasty_state = nasty_state
139+
140+
instance = CantCopy(HypotheticalNasty())
141+
142+
# These raise NotImplementedError
143+
this_line_raises = copy.deepcopy(instance)
144+
this_line_also_raises = copy.copy(instance)
145+
146+
147+
"""
148+
def __copy__(self): # noqa
149+
raise NotImplementedError(
150+
f"{self.__class__.__name__} does not implement __copy__, but"
151+
f"you may implement it on a custom subclass."
152+
)
153+
decorated_type.__copy__ = __copy__
154+
155+
def __deepcopy__(self, memo): # noqa
156+
raise NotImplementedError(
157+
f"{self.__class__.__name__} does not implement __deepcopy__,"
158+
f" but you may implement it on a custom subclass."
159+
)
160+
decorated_type.__deepcopy__ = __deepcopy__
161+
162+
return decorated_type
163+
114164
class PerformanceWarning(Warning):
115165
"""Use this for issuing performance warnings."""
116166
pass

tests/unit/physics_engine/test_physics_engine2.py

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,7 @@
11
""" Physics engine tests. """
2+
import copy
3+
4+
import pytest
25

36
import arcade
47

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

289292

293+
# Temp fix for https://github.com/pythonarcade/arcade/issues/2074
294+
def nocopy_tests(physics_engine):
295+
with pytest.raises(NotImplementedError):
296+
_ = copy.copy(physics_engine)
297+
with pytest.raises(NotImplementedError):
298+
_ = copy.deepcopy(physics_engine)
299+
300+
290301
def test_main(window: arcade.Window):
291302
character_list = arcade.SpriteList()
292303
wall_list = arcade.SpriteList()
@@ -303,9 +314,11 @@ def test_main(window: arcade.Window):
303314
physics_engine = arcade.PhysicsEngineSimple(moving_sprite, wall_list)
304315
basic_tests(moving_sprite, wall_list, physics_engine)
305316
simple_engine_tests(moving_sprite, wall_list, physics_engine)
317+
nocopy_tests(physics_engine)
306318

307319
physics_engine = arcade.PhysicsEnginePlatformer(
308320
moving_sprite, wall_list, gravity_constant=0.0
309321
)
310322
basic_tests(moving_sprite, wall_list, physics_engine)
311323
platformer_tests(moving_sprite, wall_list, physics_engine)
324+
nocopy_tests(physics_engine)

tests/unit/physics_engine/test_pymunk.py

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -23,6 +23,18 @@ def test_pymunk():
2323
assert(my_sprite.center_y == -300.0)
2424

2525

26+
# Temp fix for https://github.com/pythonarcade/arcade/issues/2074
27+
def test_pymunk_engine_nocopy():
28+
import copy
29+
physics_engine = arcade.PymunkPhysicsEngine(
30+
damping=1.0, gravity=(0, -100))
31+
32+
with pytest.raises(NotImplementedError):
33+
_ = copy.copy(physics_engine)
34+
with pytest.raises(NotImplementedError):
35+
_ = copy.deepcopy(physics_engine)
36+
37+
2638
@pytest.mark.parametrize("moment_of_inertia_arg_name",
2739
(
2840
"moment_of_inertia",

tests/unit/shape_list/test_buffered_drawing.py

Lines changed: 34 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,6 @@
1+
import copy
2+
import pytest
3+
14
import arcade
25
from arcade.shape_list import (
36
ShapeElementList,
@@ -18,7 +21,9 @@
1821
SCREEN_HEIGHT = 600
1922

2023

21-
def test_buffered_drawing(window):
24+
@pytest.fixture
25+
def shape_list_instance() -> ShapeElementList:
26+
2227
shape_list = ShapeElementList()
2328

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

87-
shape_list.center_x = 200
88-
shape_list.center_y = 200
92+
return shape_list
93+
94+
95+
def test_shape_copy_dunders_raise_notimplemented_error(window, shape_list_instance):
96+
97+
for shape in shape_list_instance:
98+
with pytest.raises(NotImplementedError):
99+
_ = copy.copy(shape)
100+
with pytest.raises(NotImplementedError):
101+
_ = copy.deepcopy(shape)
102+
103+
104+
# Temp fix for https://github.com/pythonarcade/arcade/issues/2074
105+
def test_shapeelementlist_copy_dunders_raise_notimplemented_error(window, shape_list_instance):
106+
107+
with pytest.raises(NotImplementedError):
108+
_ = copy.copy(shape_list_instance)
109+
110+
with pytest.raises(NotImplementedError):
111+
_ = copy.deepcopy(shape_list_instance)
112+
113+
114+
def test_buffered_drawing(window, shape_list_instance):
115+
116+
shape_list_instance.center_x = 200
117+
shape_list_instance.center_y = 200
89118

90119
for _ in range(10):
91-
shape_list.draw()
120+
shape_list_instance.draw()
92121
window.flip()
93122
window.clear()
94-
shape_list.move(1, 1)
123+
shape_list_instance.move(1, 1)

0 commit comments

Comments
 (0)