blob: 11c4aa6e37006c199eadb71db3577406c1c672ae [file]
# Licensed to the Apache Software Foundation (ASF) under one
# or more contributor license agreements. See the NOTICE file
# distributed with this work for additional information
# regarding copyright ownership. The ASF licenses this file
# to you under the Apache License, Version 2.0 (the
# "License"); you may not use this file except in compliance
# with the License. You may obtain a copy of the License at
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing,
# software distributed under the License is distributed on an
# "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
# KIND, either express or implied. See the License for the
# specific language governing permissions and limitations
# under the License.
from collections import UserDict, UserList, abc
from dataclasses import dataclass, make_dataclass
from io import BytesIO
from typing import AbstractSet, Any, Dict, List, Mapping, MutableMapping, MutableSequence, MutableSet, Optional, Sequence
import pytest
import pyfory
from pyfory.collection import MappingSerializer, SequenceSerializer, SetCollectionSerializer
from pyfory.error import TypeUnregisteredError
from pyfory.policy import DeserializationPolicy
class ListValue(list):
pass
class DictValue(dict):
pass
class SetValue(set):
pass
class SlottedList(list):
__slots__ = ("parent", "__label", "missing")
def __init__(self, required):
super().__init__([required])
self.__label = "base"
class ChildList(SlottedList):
__slots__ = ("child", "__dict__")
@abc.Sequence.register
class VirtualSequence:
def __init__(self, values):
self.values = values
def __len__(self):
return len(self.values)
def __getitem__(self, index):
return self.values[index]
def __iter__(self):
return iter(self.values)
class SetView(abc.Set):
def __init__(self, values):
self.values = set(values)
def __len__(self):
return len(self.values)
def __iter__(self):
return iter(self.values)
def __contains__(self, value):
return value in self.values
@pytest.mark.parametrize("ref", [False, True])
@pytest.mark.parametrize("compatible", [False, True])
@pytest.mark.parametrize(
"value,expected",
[
(UserDict(), {}),
(UserDict({"a": 1, "b": None, None: "c"}), {"a": 1, "b": None, None: "c"}),
(UserDict({str(i): i for i in range(600)}), {str(i): i for i in range(600)}),
(UserList(), []),
(UserList([1, None, "a", 2.5]), [1, None, "a", 2.5]),
(VirtualSequence([1, 2, 3]), [1, 2, 3]),
(range(4), [0, 1, 2, 3]),
(SetView([]), set()),
(SetView([1, 2, 3]), {1, 2, 3}),
(DictValue(a=1), {"a": 1}),
(ListValue([1, 2]), [1, 2]),
(SetValue([1, 2]), {1, 2}),
],
)
def test_xlang_containers(value, expected, ref, compatible):
writer = pyfory.Fory(xlang=True, ref=ref, compatible=compatible)
reader = pyfory.Fory(xlang=True, ref=ref, compatible=compatible)
builtin_serializer = writer.type_resolver.get_serializer(type(expected))
data = writer.dumps(value)
result = reader.loads(data)
assert type(result) is type(expected)
assert result == expected
assert writer.type_resolver.get_serializer(type(expected)) is builtin_serializer
@pytest.mark.parametrize("cls,items", [(ListValue, [1, None, "a"]), (DictValue, {"a": 1}), (SetValue, {1, 2})])
@pytest.mark.parametrize("ref", [False, True])
@pytest.mark.parametrize("named", [False, True])
def test_native_subclasses(cls, items, ref, named):
writer = pyfory.Fory(xlang=False, ref=ref)
reader = pyfory.Fory(xlang=False, ref=ref)
options = {"name": "test.Container"} if named else {"type_id": 100}
writer.register(cls, **options)
reader.register(cls, **options)
value = cls(items)
value.label = "example"
result = reader.loads(writer.dumps(value))
assert type(result) is cls
assert result == value
assert result.label == "example"
@pytest.mark.parametrize("cls", [ListValue, DictValue, SetValue])
def test_subclass_references(cls):
fory = pyfory.Fory(xlang=False, ref=True)
fory.register(cls)
value = cls()
value.self = value
if cls is ListValue:
value.append(value)
elif cls is DictValue:
value["self"] = value
value.state = value.__dict__
result, again = fory.loads(fory.dumps([value, value]))
assert result is again
assert result.self is result
assert result.state is result.__dict__
if cls is ListValue:
assert result[0] is result
elif cls is DictValue:
assert result["self"] is result
def test_inherited_slots():
fory = pyfory.Fory(xlang=False, ref=True)
fory.register(ChildList)
value = ChildList(7)
value.parent = value
value.child = {"value": value}
value.label = "child"
result = fory.loads(fory.dumps(value))
assert type(result) is ChildList
assert result == [7]
assert result.parent is result
assert result.child["value"] is result
assert result._SlottedList__label == "base"
assert result.label == "child"
assert not hasattr(result, "missing")
@pytest.mark.parametrize("cls", [ListValue, DictValue, SetValue])
def test_native_registration(cls):
fory = pyfory.Fory(xlang=False)
with pytest.raises(TypeUnregisteredError):
fory.dumps(cls())
fory = pyfory.Fory(xlang=False, strict=False)
result = fory.loads(fory.dumps(cls()))
assert type(result) is cls
@pytest.mark.parametrize("xlang,compatible", [(False, False), (True, False), (True, True)])
@pytest.mark.parametrize("ref", [False, True])
@pytest.mark.parametrize(
"hint,value,expected,serializer_type",
[
(Mapping[str, Optional[int]], UserDict(a=1, b=None), {"a": 1, "b": None}, MappingSerializer),
(MutableMapping[str, Any], UserDict(a=1, b="two"), {"a": 1, "b": "two"}, MappingSerializer),
(Sequence[Optional[int]], UserList([1, None, 2]), [1, None, 2], SequenceSerializer),
(MutableSequence[str], UserList(["a", "b"]), ["a", "b"], SequenceSerializer),
(AbstractSet[int], SetView([1, 2]), {1, 2}, SetCollectionSerializer),
(MutableSet[int], SetView([1, 2]), {1, 2}, SetCollectionSerializer),
(abc.Mapping, UserDict(a=1), {"a": 1}, MappingSerializer),
(abc.Sequence, VirtualSequence([1, "a"]), [1, "a"], SequenceSerializer),
(abc.Set, SetView([1, 2]), {1, 2}, SetCollectionSerializer),
],
)
def test_abc_fields(hint, value, expected, serializer_type, xlang, compatible, ref):
cls = make_dataclass("Container", [("value", hint)])
fory = pyfory.Fory(xlang=xlang, compatible=compatible, ref=ref)
fory.register(cls, name="test.Container")
from pyfory.struct import StructFieldSerializerVisitor
from pyfory.type_util import infer_field
assert isinstance(infer_field("value", hint, StructFieldSerializerVisitor(fory.type_resolver)), serializer_type)
result = fory.loads(fory.dumps(cls(value)))
assert type(result.value) is type(expected)
assert result.value == expected
@dataclass
class NestedContainers:
values: Mapping[str, Sequence[Mapping[str, Optional[int]]]]
@pytest.mark.parametrize("compatible", [False, True])
def test_nested_abc_fields(compatible):
fory = pyfory.Fory(xlang=True, compatible=compatible, ref=True)
fory.register(NestedContainers, name="test.Nested")
value = NestedContainers(UserDict(values=UserList([UserDict(a=1, b=None)])))
result = fory.loads(fory.dumps(value))
assert result == NestedContainers({"values": [{"a": 1, "b": None}]})
def test_subclass_policy():
class Policy(DeserializationPolicy):
def authorize_instantiation(self, cls):
if cls is DictValue:
raise ValueError("blocked container")
writer = pyfory.Fory(xlang=False)
reader = pyfory.Fory(xlang=False, policy=Policy())
writer.register(DictValue, type_id=100)
reader.register(DictValue, type_id=100)
with pytest.raises(ValueError, match="blocked container"):
reader.loads(writer.dumps(DictValue(a=1)))
assert reader.loads(writer.dumps({"a": 2})) == {"a": 2}
def test_subclass_setstate_policy():
class Policy(DeserializationPolicy):
def intercept_setstate(self, obj, state):
state["label"] = "sanitized"
fory = pyfory.Fory(xlang=False, policy=Policy())
fory.register(DictValue)
value = DictValue(a=1)
value.label = "original"
assert fory.loads(fory.dumps(value)).label == "sanitized"
@pytest.mark.parametrize("xlang", [False, True])
def test_custom_serializer_priority(xlang):
class CustomSerializer(pyfory.Serializer):
@classmethod
def support_subclass(cls):
return True
def write(self, context, value):
context.write_string(value.label)
def read(self, context):
result = self.type_()
result.label = context.read_string()
return result
class Child(DictValue):
pass
fory = pyfory.Fory(xlang=xlang)
fory.register(DictValue, serializer=CustomSerializer)
fory.register(Child)
value = Child(a=1)
value.label = "custom"
result = fory.loads(fory.dumps(value))
assert type(result) is Child
assert result == {}
assert result.label == "custom"
class StatefulList(list):
def __getstate__(self):
return {"label": self.label}
def __setstate__(self, state):
self.label = state["label"]
def test_state_hook_priority():
fory = pyfory.Fory(xlang=False)
fory.register(StatefulList)
value = StatefulList([1, 2])
value.label = "hook"
result = fory.loads(fory.dumps(value))
assert type(result) is StatefulList
assert result == value
assert result.label == "hook"
@pytest.mark.parametrize("value", [UserDict(), UserList()])
def test_xlang_cycles(value):
if isinstance(value, UserDict):
value["self"] = value
else:
value.append(value)
fory = pyfory.Fory(xlang=True, ref=True)
result, again = fory.loads(fory.dumps([value, value]))
assert result is again
assert (result["self"] if isinstance(result, dict) else result[0]) is result
@pytest.mark.parametrize("compatible", [False, True])
def test_abc_builtin_schema(compatible):
abc_type = make_dataclass("Container", [("values", Mapping[str, Sequence[Optional[int]]])])
builtin_type = make_dataclass("Container", [("values", Dict[str, List[Optional[int]]])])
writer = pyfory.Fory(xlang=True, compatible=compatible)
reader = pyfory.Fory(xlang=True, compatible=compatible)
writer.register(abc_type, name="test.Container")
reader.register(builtin_type, name="test.Container")
data = writer.dumps(abc_type(UserDict(a=UserList([1, None, 2]))))
result = reader.loads(data)
assert result.values == {"a": [1, None, 2]}
assert writer.loads(reader.dumps(result)).values == result.values
@pytest.mark.parametrize("base", [list, set, dict])
def test_overridden_container_methods(base):
def blocked(*args):
raise AssertionError("native storage must bypass overridden methods")
cls = type("Overridden", (base,), dict(__len__=blocked, __iter__=blocked, append=blocked, add=blocked, __setitem__=blocked, items=blocked))
value = cls()
if base is dict:
dict.__setitem__(value, "key", 42)
dict.__setitem__(value, None, None)
elif base is list:
list.append(value, 42)
else:
set.add(value, 42)
value.label = "state"
fory = pyfory.Fory(xlang=False)
fory.register(cls)
result = fory.loads(fory.dumps(value))
assert type(result) is cls
assert base.__eq__(result, value)
assert result.label == "state"
@pytest.mark.parametrize("base", [list, set, dict])
@pytest.mark.parametrize("setter", [False, True])
def test_container_state_hooks(base, setter):
class Container(base):
def __getstate__(self):
return {"label": self.label, "self": self}
if setter:
def setstate(self, state):
assert base.__len__(self) > 0
self.__dict__.update(state)
Container.__setstate__ = setstate
value = Container({"a": 1} if base is dict else [1, 2])
value.label = "hook"
fory = pyfory.Fory(xlang=False, ref=True)
fory.register(Container)
result = fory.loads(fory.dumps(value))
assert type(result) is Container
assert result == value
assert result.label == "hook"
assert result.self is result
@pytest.mark.parametrize("base", [list, set, dict])
@pytest.mark.parametrize("compatible", [False, True])
def test_replace_collection_serializer(base, compatible):
class Container(base):
pass
class CustomSerializer(pyfory.Serializer):
def write(self, context, value):
context.write_string(value.label)
def read(self, context):
value = self.type_()
value.label = context.read_string() + " restored"
return value
fory = pyfory.Fory(xlang=True, compatible=compatible)
fory.register(Container)
serializer = CustomSerializer(fory.type_resolver, Container)
fory.register_serializer(Container, serializer)
fory.register_serializer(Container, serializer)
value = Container()
value.label = "extension"
builtin = {"a": 1} if base is dict else base([1, 2])
result = fory.loads(fory.dumps([value, builtin]))
assert type(result[0]) is Container
assert result[0].label == "extension restored"
assert type(result[1]) is base
assert result[1] == builtin
@pytest.mark.parametrize("method", ["__new__", "__getnewargs__", "__getnewargs_ex__"])
def test_custom_construction_requires_serializer(method):
cls = type("CustomConstruction", (list,), {method: lambda *args: ()})
with pytest.raises(TypeError, match="explicit serializer or reduce hook"):
pyfory.Fory(xlang=False).register(cls)
@pytest.mark.parametrize("hint,values", [(Sequence, [1, 2]), (AbstractSet, {1, 2}), (Mapping, {"a": 1})])
def test_abc_stream_and_graph_budget(hint, values):
container = make_dataclass("Container", [("values", hint)])
writer = pyfory.Fory(xlang=True)
reader = pyfory.Fory(xlang=True, max_graph_memory_bytes=64)
writer.register(container, type_id=101)
reader.register(container, type_id=101)
value = container(values)
stream = BytesIO()
writer.dump(value, stream)
assert writer.loads(pyfory.Buffer.from_stream(BytesIO(stream.getvalue()))) == value
with pytest.raises(ValueError, match="graph memory"):
reader.loads(stream.getvalue())
assert reader.loads(writer.dumps(1)) == 1
class EmptyValue:
pass
class EmptySerializer(pyfory.Serializer):
def write(self, context, value):
pass
def read(self, context):
return EmptyValue()
@pytest.mark.parametrize("hint", [Sequence[EmptyValue], AbstractSet[EmptyValue], Mapping[EmptyValue, EmptyValue]])
def test_abc_unbacked_items(hint):
container = make_dataclass("Container", [("values", hint)])
values = [EmptyValue() for _ in range(1025)]
if hint.__origin__ is abc.Mapping:
values = dict(zip(values, values))
elif hint.__origin__ is abc.Set:
values = set(values)
fory = pyfory.Fory(xlang=True, ref=False, max_unbacked_container_items=0)
fory.register(EmptyValue, type_id=100, serializer=EmptySerializer)
fory.register(container, type_id=101)
with pytest.raises(Exception):
fory.loads(fory.dumps(container(values)))
assert fory.loads(fory.dumps(1)) == 1