Skip to content

Commit df11ae6

Browse files
committedMar 16, 2025
Update body weakref to space to ref, and fix issue with del space. #278
1 parent 59a5251 commit df11ae6

File tree

4 files changed

+33
-17
lines changed

4 files changed

+33
-17
lines changed
 

‎pymunk/_callbacks.py

+2-2
Original file line numberDiff line numberDiff line change
@@ -290,8 +290,8 @@ def ext_cpBodyArbiterIteratorFunc(
290290
_body: ffi.CData, _arbiter: ffi.CData, data: ffi.CData
291291
) -> None:
292292
body, func, args, kwargs = ffi.from_handle(data)
293-
assert body._space is not None
294-
arbiter = Arbiter(_arbiter, body._space)
293+
assert body.space is not None
294+
arbiter = Arbiter(_arbiter, body.space)
295295
func(arbiter, *args, **kwargs)
296296

297297

‎pymunk/body.py

+10-12
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
11
__docformat__ = "reStructuredText"
22

3+
import weakref
34
from typing import ( # Literal,
45
TYPE_CHECKING,
56
Any,
@@ -106,12 +107,13 @@ class Body(PickleMixin, TypingAttrMixing, object):
106107
"is_sleeping",
107108
"_velocity_func",
108109
"_position_func",
110+
# "_space",
109111
]
110112

111113
_position_func: Optional[_PositionFunc] = None
112114
_velocity_func: Optional[_VelocityFunc] = None
113115

114-
_id_counter = 1
116+
_dead_ref = weakref.ref(set())
115117

116118
def __init__(
117119
self, mass: float = 0, moment: float = 0, body_type: _BodyType = DYNAMIC
@@ -217,9 +219,7 @@ def freebody(cp_body: ffi.CData) -> None:
217219
elif body_type == Body.STATIC:
218220
self._body = ffi.gc(lib.cpBodyNewStatic(), freebody)
219221

220-
self._space: Optional["Space"] = (
221-
None # Weak ref to the space holding this body (if any)
222-
)
222+
self._space: weakref.ref = Body._dead_ref
223223

224224
self._constraints: WeakSet["Constraint"] = (
225225
WeakSet()
@@ -264,7 +264,7 @@ def mass(self) -> float:
264264
@mass.setter
265265
def mass(self, mass: float) -> None:
266266
assert (
267-
self._space is None or mass > 0
267+
self.space is None or mass > 0
268268
), "Dynamic bodies must have mass > 0 if they are attached to a Space."
269269
lib.cpBodySetMass(self._body, mass)
270270

@@ -400,16 +400,14 @@ def rotation_vector(self) -> Vec2d:
400400
def space(self) -> Optional["Space"]:
401401
"""Get the :py:class:`Space` that the body has been added to (or
402402
None)."""
403-
assert hasattr(self, "_space"), ( # TODO: When can this happen?
403+
# This assert is tested in test_pickle_circular_ref
404+
assert hasattr(self, "_space"), (
404405
"_space not set. This can mean there's a direct or indirect"
405406
" circular reference between the Body and the Space. Circular"
406407
" references are not supported when using pickle or copy and"
407408
" might crash."
408409
)
409-
if self._space is not None:
410-
return self._space._get_self() # ugly hack because of weakref
411-
else:
412-
return None
410+
return self._space()
413411

414412
def _set_velocity_func(self, func: _VelocityFunc) -> None:
415413
if func == Body.update_velocity:
@@ -571,7 +569,7 @@ def sleep(self) -> None:
571569
572570
Cannot be called from a callback.
573571
"""
574-
if self._space == None:
572+
if self.space == None:
575573
raise Exception("Body not added to space")
576574
lib.cpBodySleep(self._body)
577575

@@ -589,7 +587,7 @@ def sleep_with_group(self, body: "Body") -> None:
589587
to initialize levels and start stacks of objects in a pre-sleeping
590588
state.
591589
"""
592-
if self._space == None:
590+
if self.space == None:
593591
raise Exception("Body not added to space")
594592
lib.cpBodySleepWithGroup(self._body, body._body)
595593

‎pymunk/space.py

+3-3
Original file line numberDiff line numberDiff line change
@@ -167,7 +167,7 @@ def constraints(self) -> List[Constraint]:
167167
return list(self._constraints)
168168

169169
def _setup_static_body(self, static_body: Body) -> None:
170-
static_body._space = weakref.proxy(self)
170+
static_body._space = weakref.ref(self)
171171
cp.cpSpaceAddBody(self._space, static_body._body)
172172

173173
@property
@@ -407,7 +407,7 @@ def _add_body(self, body: "Body") -> None:
407407
assert body not in self._bodies, "Body already added to this space."
408408
assert body.space == None, "Body already added to another space."
409409

410-
body._space = weakref.proxy(self)
410+
body._space = weakref.ref(self)
411411
self._bodies[body] = None
412412
self._bodies_to_check.add(body)
413413
cp.cpSpaceAddBody(self._space, body._body)
@@ -437,7 +437,7 @@ def _remove_shape(self, shape: "Shape") -> None:
437437
def _remove_body(self, body: "Body") -> None:
438438
"""Removes a body from the space"""
439439
assert body in self._bodies, "body not in space, already removed?"
440-
body._space = None
440+
body._space = body._dead_ref
441441
if body in self._bodies_to_check:
442442
self._bodies_to_check.remove(body)
443443
# During GC at program exit sometimes the shape might already be removed. Then skip this step.

‎pymunk/tests/test_space.py

+18
Original file line numberDiff line numberDiff line change
@@ -1168,6 +1168,24 @@ def testPickleCachedArbiters(self) -> None:
11681168
self.assertAlmostEqual(s.bodies[0].position.x, s_copy.bodies[0].position.x)
11691169
self.assertAlmostEqual(s.bodies[0].position.y, s_copy.bodies[0].position.y)
11701170

1171+
def testDeleteSpaceWithObjects(self) -> None:
1172+
s = p.Space()
1173+
1174+
b = p.Body(1)
1175+
1176+
c = p.Circle(b, 10)
1177+
1178+
j = p.PinJoint(b, s.static_body)
1179+
1180+
s.add(b, c, j)
1181+
1182+
del s
1183+
1184+
self.assertIsNone(b.space)
1185+
self.assertIsNone(c.space)
1186+
self.assertEqual(j.a, b)
1187+
self.assertEqual(j.b.body_type, p.Body.STATIC)
1188+
11711189

11721190
def f1(*args: Any, **kwargs: Any) -> None:
11731191
pass

0 commit comments

Comments
 (0)
Failed to load comments.