diff --git a/modelx/serialize/serializer_6.py b/modelx/serialize/serializer_6.py index 3fa722c0..422b235e 100644 --- a/modelx/serialize/serializer_6.py +++ b/modelx/serialize/serializer_6.py @@ -26,7 +26,7 @@ import modelx as mx from modelx.core.system import mxsys from modelx.core.model import Model -from modelx.core.base import Interface +from modelx.core.base import Interface, null_impl from modelx.core.util import ( abs_to_rel, rel_to_abs, abs_to_rel_tuple, rel_to_abs_tuple) from modelx.core.api import _new_cells_keep_source @@ -837,9 +837,13 @@ class InterfaceRefEncoder(BaseEncoder): @classmethod def condition(cls, ref, writer): value = ref.value - return isinstance(value, Interface) and value._is_valid() + return isinstance(value, Interface) def encode(self): + if not self.target.value._is_valid(): + return '("Interface", (None, "%s"))' % ( + type(self.target.value).__name__) + idtuple = TupleID(abs_to_rel_tuple( self.target.value._idtuple, self.parent._idtuple @@ -862,7 +866,10 @@ def pickle_value(self): idtuple.pickle_args(self.writer.pickledata, self.writer.assign_id) def instruct(self): - return Instruction(self.pickle_value) + if self.target.value._is_valid(): + return Instruction(self.pickle_value) + else: + return None class LiteralEncoder(BaseEncoder): @@ -1751,10 +1758,34 @@ class InterfaceDecoder(TupleDecoder): DECTYPE = "Interface" def decode(self): - return rel_to_abs(self.value[1], self.obj.fullname) + keys = self.value[1] + if (isinstance(keys, tuple) + and len(keys) == 2 + and keys[0] is None): + return None + else: + return rel_to_abs(keys, self.obj.fullname) def restore(self): keys = self.value[1] + if (isinstance(keys, tuple) + and len(keys) == 2 + and keys[0] is None): + from modelx.core.cells import Cells + from modelx.core.macro import Macro + from modelx.core.space import DynamicSpace, ItemSpace, UserSpace + + interface_types = { + cls.__name__: cls for cls in ( + Cells, DynamicSpace, ItemSpace, Macro, Model, UserSpace + ) + } + type_name = keys[1] + if type_name not in interface_types: + raise ValueError( + "unknown null interface type: %s" % type_name) + cls = interface_types[type_name] + return cls.__new__(cls, null_impl) if any(isinstance(k, int) and not self.reader.find_pickledata(k)[0] for k in keys): self._warn_lost_ref() diff --git a/modelx/tests/serialize/test_serialize.py b/modelx/tests/serialize/test_serialize.py index 1131c1d1..db2160be 100644 --- a/modelx/tests/serialize/test_serialize.py +++ b/modelx/tests/serialize/test_serialize.py @@ -272,6 +272,32 @@ def test_null_object(tmp_path, write_method): testutil.compare_model(m, m2) +@pytest.mark.parametrize("version", [6, 7, 8]) +def test_null_object_is_serialized_as_interface( + tmp_path, version, close_new_models): + m = mx.new_model() + space = m.new_space("Space") + cells = space.new_cells("foo") + space.relref(deleted=cells) + + del space.foo + path = tmp_path / "model" + mx.write_model(m, path, version=version) + m.close() + + source = (path / "Space" / "__init__.py").read_text(encoding="utf-8") + assert 'deleted = ("Interface", (None, "Cells"))' in source + assert not (path / "_data" / "data.pickle").exists() + + restored = mx.read_model(path) + assert not restored.Space.deleted._is_valid() + assert type(restored.Space.deleted) is type(cells) + # A relative ref cannot resolve against a deleted target. This matches + # the previous pickle path, which restored the ref in auto mode. + assert restored.Space._get_object( + "deleted", as_proxy=True).refmode == "auto" + + @pytest.mark.parametrize("write_method", ["write_model", "zip_model"]) def test_false_value(tmp_path, write_method): # https://github.com/fumitoh/modelx/issues/39