stdpy: Fix pickle sometimes duplicating Panda objects

We have to unify multiple Python wrappers pointing to the same C++ object.
This commit is contained in:
rdb 2020-12-31 16:47:00 +01:00
parent ac991e4c5e
commit a5557bc38d
2 changed files with 34 additions and 21 deletions

View File

@ -22,7 +22,7 @@ Unfortunately, cPickle cannot be supported, because it does not
support extensions of this nature. """ support extensions of this nature. """
import sys import sys
from panda3d.core import BamWriter, BamReader from panda3d.core import BamWriter, BamReader, TypedObject
if sys.version_info >= (3, 0): if sys.version_info >= (3, 0):
from copyreg import dispatch_table from copyreg import dispatch_table
@ -47,6 +47,7 @@ class _Pickler(BasePickler):
def __init__(self, *args, **kw): def __init__(self, *args, **kw):
self.bamWriter = BamWriter() self.bamWriter = BamWriter()
self._canonical = {}
BasePickler.__init__(self, *args, **kw) BasePickler.__init__(self, *args, **kw)
# We have to duplicate most of the save() method, so we can add # We have to duplicate most of the save() method, so we can add
@ -62,6 +63,21 @@ class _Pickler(BasePickler):
self.save_pers(pid) self.save_pers(pid)
return return
# Check if this is a Panda type that we've already saved; if so, store
# a mapping to the canonical copy, so that Python's memoization system
# works properly. This is needed because Python uses id(obj) for
# memoization, but there may be multiple Python wrappers for the same
# C++ pointer, and we don't want that to result in duplication.
t = type(obj)
if issubclass(t, TypedObject.__base__):
canonical = self._canonical.get(obj.this)
if canonical is not None:
obj = canonical
else:
# First time we're seeing this C++ pointer; save it as the
# "canonical" version.
self._canonical[obj.this] = obj
# Check the memo # Check the memo
x = self.memo.get(id(obj)) x = self.memo.get(id(obj))
if x: if x:
@ -69,7 +85,6 @@ class _Pickler(BasePickler):
return return
# Check the type dispatch table # Check the type dispatch table
t = type(obj)
f = self.dispatch.get(t) f = self.dispatch.get(t)
if f: if f:
f(self, obj) # Call unbound method with explicit self f(self, obj) # Call unbound method with explicit self
@ -157,25 +172,9 @@ class Unpickler(BaseUnpickler):
BaseUnpickler.dispatch[pickle.REDUCE] = load_reduce BaseUnpickler.dispatch[pickle.REDUCE] = load_reduce
if sys.version_info >= (3, 8): Pickler = _Pickler
# In Python 3.8 and up, we can use the C implementation of Pickler, which
# supports a reducer_override method.
class Pickler(pickle.Pickler):
def __init__(self, *args, **kw):
self.bamWriter = BamWriter()
pickle.Pickler.__init__(self, *args, **kw)
def reducer_override(self, obj): if sys.version_info < (3, 0):
reduce = getattr(obj, "__reduce_persist__", None)
if reduce:
return reduce(self)
return NotImplemented
else:
# Otherwise, we have to use our custom version that overrides save().
Pickler = _Pickler
if sys.version_info < (3, 0):
del _Pickler del _Pickler

View File

@ -12,6 +12,20 @@ def test_reduce_persist():
assert tuple(parent2.children) == (child2,) assert tuple(parent2.children) == (child2,)
def test_pickle_copy():
from panda3d.core import PandaNode, NodePath
# Make two Python wrappers pointing to the same node
node1 = PandaNode("node")
node2 = NodePath(node1).node()
assert node1.this == node2.this
assert id(node1) != id(node2)
# Test that pickling and loading still results in the same node object.
node1, node2 = loads(dumps([node1, node2]))
assert node1 == node2
def test_pickle_error(): def test_pickle_error():
class ErroneousPickleable(object): class ErroneousPickleable(object):
def __reduce__(self): def __reduce__(self):