diff --git a/pyiceberg/expressions/__init__.py b/pyiceberg/expressions/__init__.py index d0824cc315..cf33779e71 100644 --- a/pyiceberg/expressions/__init__.py +++ b/pyiceberg/expressions/__init__.py @@ -35,6 +35,8 @@ from pydantic import Field +from pydantic import Field, model_validator + from pyiceberg.expressions.literals import ( AboveMax, BelowMin, @@ -341,21 +343,27 @@ def __getnewargs__(self) -> Tuple[BooleanExpression, BooleanExpression]: return (self.left, self.right) -class Not(BooleanExpression): +class Not(IcebergBaseModel, BooleanExpression): """NOT operation expression - logical negation.""" child: BooleanExpression - def __new__(cls, child: BooleanExpression) -> BooleanExpression: # type: ignore + @model_validator(mode="before") + def _before(cls, values: Any) -> Any: + if isinstance(values, BooleanExpression): + return {"child": values} + return values + + @model_validator(mode="after") + def _normalize(cls, model: Any) -> Any: + child = model.child if child is AlwaysTrue(): return AlwaysFalse() elif child is AlwaysFalse(): return AlwaysTrue() elif isinstance(child, Not): return child.child - obj = super().__new__(cls) - obj.child = child - return obj + return model def __repr__(self) -> str: """Return the string representation of the Not class.""" diff --git a/tests/expressions/test_expressions.py b/tests/expressions/test_expressions.py index 5a0c8c9241..7a196101d1 100644 --- a/tests/expressions/test_expressions.py +++ b/tests/expressions/test_expressions.py @@ -738,6 +738,29 @@ def test_not() -> None: assert not_ == pickle.loads(pickle.dumps(not_)) +def test_not_json_serialization_and_deserialization() -> None: + expr = Not(AlwaysFalse()) + json_str = expr.model_dump_json() + restored = Not.model_validate_json(json_str) + assert isinstance(restored, AlwaysTrue) + + expr2 = Not(Not(AlwaysFalse())) + json_str2 = expr2.model_dump_json() + restored2 = Not.model_validate_json(json_str2) + assert isinstance(restored2, AlwaysFalse) + + class DummyExpr(BooleanExpression): + def __invert__(self) -> BooleanExpression: + return self + + dummy = DummyExpr() + not_dummy = Not(child=dummy) + json_str3 = not_dummy.model_dump_json() + restored3 = Not.model_validate_json(json_str3) + assert isinstance(restored3, Not) + assert isinstance(restored3.child, DummyExpr) + + def test_always_true() -> None: always_true = AlwaysTrue() assert always_true.model_dump_json() == '"true"'