From 7a54f047c0ea93f5e69c977e17223039773782fe Mon Sep 17 00:00:00 2001 From: HansBug Date: Sun, 4 Dec 2022 22:44:16 +0800 Subject: [PATCH] dev(hansbug): add support for constraint pickle --- test/tree/tree/test_constraint.py | 141 ++++++++++++++++++++++++++++- treevalue/tree/tree/constraint.pyx | 31 ++++++- 2 files changed, 166 insertions(+), 6 deletions(-) diff --git a/test/tree/tree/test_constraint.py b/test/tree/tree/test_constraint.py index ae19ad01bf..4933cc6fa9 100644 --- a/test/tree/tree/test_constraint.py +++ b/test/tree/tree/test_constraint.py @@ -1,9 +1,12 @@ +import pickle + import pytest from treevalue import delayed from treevalue.tree.tree import TreeValue, cleaf from treevalue.tree.tree.constraint import to_constraint, TypeConstraint, EmptyConstraint, LeafConstraint, \ - ValueConstraint, TreeConstraint, vval, vcheck, nval, ncheck, transact + ValueConstraint, TreeConstraint, vval, vcheck, nval, ncheck, transact, CompositeConstraint, ValueValidateConstraint, \ + ValueCheckConstraint, NodeCheckConstraint, NodeValidateConstraint class GreaterThanConstraint(ValueConstraint): @@ -21,9 +24,35 @@ class GreaterThanConstraint(ValueConstraint): return isinstance(other, GreaterThanConstraint) and self.value >= other.value +class OverValueCheck: + def __init__(self, value): + self.value = value + + def __call__(self, v): + return v > self.value + + +class OverValueValidation: + def __init__(self, value): + self.value = value + + def __call__(self, v): + if v <= self.value: + raise ValueError(f'Invalid value - {v!r}.') + + # noinspection DuplicatedCode,PyArgumentList,PyTypeChecker @pytest.mark.unittest class TestTreeTreeConstraint: + def test_example_constraint(self): + c = GreaterThanConstraint(10) + binary = pickle.dumps(c) + newc = pickle.loads(binary) + + assert isinstance(newc, GreaterThanConstraint) + assert newc.value == 10 + assert c == newc + def test_empty(self): c1 = to_constraint(None) assert isinstance(c1, EmptyConstraint) @@ -48,6 +77,10 @@ class TestTreeTreeConstraint: c1.validate(None) c1.validate(TreeValue({'a': 1, 'b': {'x': 2, 'y': 3}})) + binary = pickle.dumps(c1) + newc = pickle.loads(binary) + assert isinstance(newc, EmptyConstraint) + def test_type(self): c1 = to_constraint(int) assert isinstance(c1, TypeConstraint) @@ -106,6 +139,12 @@ class TestTreeTreeConstraint: with pytest.raises(TypeError): c1.validate(t2) + binary = pickle.dumps(c1) + newc = pickle.loads(binary) + assert isinstance(newc, TypeConstraint) + assert newc.type_ == int + assert newc == int + def test_leaf(self): c1 = to_constraint(cleaf()) assert isinstance(c1, LeafConstraint) @@ -145,6 +184,10 @@ class TestTreeTreeConstraint: with pytest.raises(TypeError): c1.validate(t1) + binary = pickle.dumps(c1) + newc = pickle.loads(binary) + assert isinstance(newc, LeafConstraint) + def test_custom_value(self): c1 = GreaterThanConstraint(3) assert c1 @@ -343,6 +386,11 @@ class TestTreeTreeConstraint: with pytest.raises(TypeError): c1.validate(t4) + binary = pickle.dumps(c1) + newc = pickle.loads(binary) + assert isinstance(newc, CompositeConstraint) + assert newc == [GreaterThanConstraint(3), int] + def test_tree(self): assert to_constraint({'a': None, 'b': []}) == to_constraint(None) @@ -456,6 +504,18 @@ class TestTreeTreeConstraint: with pytest.raises(TypeError): c1.validate(t7) + binary = pickle.dumps(c1) + newc = pickle.loads(binary) + assert isinstance(newc, TreeConstraint) + assert newc == { + 'a': [int, GreaterThanConstraint(3)], + 'b': { + 'x': [cleaf(), str, None], + 'y': float, + }, + 'c': None, + } + def test_composite_tree(self): c1 = to_constraint([ { @@ -510,6 +570,29 @@ class TestTreeTreeConstraint: assert not (c1 <= {'c': None}) assert not (c1 < {'c': None}) + binary = pickle.dumps(c1) + newc = pickle.loads(binary) + assert isinstance(newc, TreeConstraint) + assert newc == [ + { + 'a': [int, object], + 'b': { + 'x': [str, None], + 'y': object, + }, + 'c': None, + }, + { + 'b': { + 'x': [cleaf(), None], + 'y': float, + } + }, + { + 'a': GreaterThanConstraint(3), + }, + ] + def test_complex(self): c1 = to_constraint([ GreaterThanConstraint(4), @@ -542,6 +625,30 @@ class TestTreeTreeConstraint: }, ] + binary = pickle.dumps(c1) + newc = pickle.loads(binary) + assert isinstance(newc, CompositeConstraint) + assert newc == [ + GreaterThanConstraint(4), + { + 'a': [int, object], + 'b': { + 'x': [int, None], + 'y': object, + }, + 'c': None, + }, + { + 'b': { + 'x': [cleaf(), None], + 'y': int, + } + }, + { + 'a': GreaterThanConstraint(3), + }, + ] + def test_with_delay(self): c1 = to_constraint([ object, @@ -669,6 +776,22 @@ class TestTreeTreeConstraint: with pytest.raises(AssertionError): c2.validate(3) + cx = vval(OverValueValidation(5), 'over5') + binary = pickle.dumps(cx) + newcx = pickle.loads(binary) + assert isinstance(newcx, ValueValidateConstraint) + assert isinstance(newcx.func, OverValueValidation) + assert newcx.func.value == 5 + assert newcx.name == 'over5' + + cx = vcheck(OverValueCheck(5), 'over5') + binary = pickle.dumps(cx) + newcx = pickle.loads(binary) + assert isinstance(newcx, ValueCheckConstraint) + assert isinstance(newcx.func, OverValueCheck) + assert newcx.func.value == 5 + assert newcx.name == 'over5' + def test_node_func(self): def _n_validate(x: TreeValue): if 'a' in x and 'b' in x: @@ -772,6 +895,22 @@ class TestTreeTreeConstraint: with pytest.raises(TypeError): c2.validate(10) + cx = nval(OverValueValidation(5), 'over5') + binary = pickle.dumps(cx) + newcx = pickle.loads(binary) + assert isinstance(newcx, NodeValidateConstraint) + assert isinstance(newcx.func, OverValueValidation) + assert newcx.func.value == 5 + assert newcx.name == 'over5' + + cx = ncheck(OverValueCheck(5), 'over5') + binary = pickle.dumps(cx) + newcx = pickle.loads(binary) + assert isinstance(newcx, NodeCheckConstraint) + assert isinstance(newcx.func, OverValueCheck) + assert newcx.func.value == 5 + assert newcx.name == 'over5' + def test_hash_eq(self): d = { to_constraint(int): 2389, diff --git a/treevalue/tree/tree/constraint.pyx b/treevalue/tree/tree/constraint.pyx index 4be90fdb11..e8d6e34d45 100644 --- a/treevalue/tree/tree/constraint.pyx +++ b/treevalue/tree/tree/constraint.pyx @@ -195,6 +195,9 @@ cdef class TypeConstraint(ValueConstraint): cpdef bool _contains(self, Constraint other): return isinstance(other, TypeConstraint) and issubclass(self.type_, other.type_) + def __reduce__(self): + return TypeConstraint, (self.type_,) + cdef inline str _c_func_fullname(object f): cdef str fname = f.__name__ cdef str mname = getattr(f, '__module__', '') @@ -214,6 +217,9 @@ cdef class ValueFuncConstraint(ValueConstraint): def __repr__(self): return f'<{type(self).__name__} {self.name}>' + def __reduce__(self): + return type(self), (self.func, self.name) + @cython.final cdef class ValueValidateConstraint(ValueFuncConstraint): cpdef inline void _validate_value(self, object instance) except*: @@ -245,7 +251,7 @@ cdef class LeafConstraint(Constraint): return isinstance(other, LeafConstraint) cpdef inline Constraint _transaction(self, str key): - return _EMPTY_CONSTRAINT + return _EMPTY_CONSTRAINT # pragma: no cover def __repr__(self): return f'<{type(self).__name__}>' @@ -267,6 +273,9 @@ cdef class NodeFuncConstraint(NodeConstraint): def __repr__(self): return f'<{type(self).__name__} {self.name}>' + def __reduce__(self): + return type(self), (self.func, self.name) + @cython.final cdef class NodeValidateConstraint(NodeFuncConstraint): cpdef inline void _validate_node(self, object instance) except*: @@ -284,8 +293,11 @@ cpdef inline NodeCheckConstraint ncheck(object func, object name=None): return NodeCheckConstraint(func, str(name or _c_func_fullname(func))) cdef class TreeConstraint(Constraint): - def __cinit__(self, dict constraints): - self._constraints = {key: constraints[key] for key in sorted(constraints.keys())} + def __cinit__(self, dict constraints, bool need_sort=True): + if need_sort: + self._constraints = {key: constraints[key] for key in sorted(constraints.keys())} + else: + self._constraints = dict(constraints) cpdef void _validate_node(self, object instance) except*: pass @@ -322,6 +334,9 @@ cdef class TreeConstraint(Constraint): else: return _EMPTY_CONSTRAINT + def __reduce__(self): + return TreeConstraint, (self._constraints, False) + cdef inline Constraint _s_tree_merge(list constraints): cdef dict cmap = {} cdef str key @@ -362,8 +377,11 @@ cdef inline Constraint _s_tree(TreeConstraint constraint): return _EMPTY_CONSTRAINT cdef class CompositeConstraint(Constraint): - def __cinit__(self, list constraints): - self._constraints = tuple(sorted(constraints, key=lambda x: repr(x._features()))) + def __cinit__(self, list constraints, bool need_sort=True): + if need_sort: + self._constraints = tuple(sorted(constraints, key=lambda x: repr(x._features()))) + else: + self._constraints = tuple(constraints) cpdef void _validate_node(self, object instance) except*: cdef Constraint cons @@ -413,6 +431,9 @@ cdef class CompositeConstraint(Constraint): cpdef Constraint _transaction(self, str key): return CompositeConstraint([c._transaction(key) for c in self._constraints]) + def __reduce__(self): + return CompositeConstraint, (list(self._constraints), False) + cdef inline void _rec_composite_iter(Constraint constraint, list lst): cdef Constraint cons if isinstance(constraint, CompositeConstraint): -- GitLab