From 28c21654c4a1446fd648c86efb7f9a6d1e923e7d Mon Sep 17 00:00:00 2001 From: Aniket Singh Yadav Date: Wed, 8 Oct 2025 11:08:22 +0530 Subject: [PATCH 1/8] Make Not expression JSON serializable --- pyiceberg/expressions/__init__.py | 20 ++++++++++++++------ 1 file changed, 14 insertions(+), 6 deletions(-) diff --git a/pyiceberg/expressions/__init__.py b/pyiceberg/expressions/__init__.py index 2adf898fea..6e244523e5 100644 --- a/pyiceberg/expressions/__init__.py +++ b/pyiceberg/expressions/__init__.py @@ -32,6 +32,8 @@ Union, ) +from pydantic import model_validator + from pyiceberg.expressions.literals import ( AboveMax, BelowMin, @@ -39,7 +41,7 @@ literal, ) from pyiceberg.schema import Accessor, Schema -from pyiceberg.typedef import L, StructProtocol +from pyiceberg.typedef import IcebergBaseModel, L, StructProtocol from pyiceberg.types import DoubleType, FloatType, NestedField from pyiceberg.utils.singleton import Singleton @@ -329,21 +331,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.""" From bb091bd888e402836fefde4f86ea6a19e9168a43 Mon Sep 17 00:00:00 2001 From: Aniket Singh Yadav Date: Fri, 10 Oct 2025 09:45:42 +0530 Subject: [PATCH 2/8] Make Not expression JSON serializable --- pyiceberg/expressions/__init__.py | 2 +- tests/expressions/test_expressions.py | 23 +++++++++++++++++++++++ 2 files changed, 24 insertions(+), 1 deletion(-) diff --git a/pyiceberg/expressions/__init__.py b/pyiceberg/expressions/__init__.py index 6e244523e5..e8639f435f 100644 --- a/pyiceberg/expressions/__init__.py +++ b/pyiceberg/expressions/__init__.py @@ -32,7 +32,7 @@ Union, ) -from pydantic import model_validator +from pydantic import Field, model_validator from pyiceberg.expressions.literals import ( AboveMax, diff --git a/tests/expressions/test_expressions.py b/tests/expressions/test_expressions.py index 828d32704a..5d2c386454 100644 --- a/tests/expressions/test_expressions.py +++ b/tests/expressions/test_expressions.py @@ -736,6 +736,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 str(always_true) == "AlwaysTrue()" From ea625520a46abf408d77b85f4a642cb91ca3e313 Mon Sep 17 00:00:00 2001 From: Aniket <148300120+Aniketsy@users.noreply.github.com> Date: Wed, 22 Oct 2025 00:02:59 +0530 Subject: [PATCH 3/8] Update tests/expressions/test_expressions.py Co-authored-by: Fokko Driesprong --- tests/expressions/test_expressions.py | 23 +++-------------------- 1 file changed, 3 insertions(+), 20 deletions(-) diff --git a/tests/expressions/test_expressions.py b/tests/expressions/test_expressions.py index 7a196101d1..16bd7c273a 100644 --- a/tests/expressions/test_expressions.py +++ b/tests/expressions/test_expressions.py @@ -739,26 +739,9 @@ def test_not() -> None: 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) + not_expr = Not(GreaterThan("a", 22)) + json_str = not_expr.model_dump_json() + assert json_str == """{"type":"not","child":{"term":"a","type":"gt","value":22}}""" def test_always_true() -> None: From 22147b99abc4c9d1025818bbf0e5683743e7bc54 Mon Sep 17 00:00:00 2001 From: Aniket Singh Yadav Date: Thu, 23 Oct 2025 10:57:48 +0530 Subject: [PATCH 4/8] Make Not expression JSON serializable --- pyiceberg/expressions/__init__.py | 19 +++++++++---------- 1 file changed, 9 insertions(+), 10 deletions(-) diff --git a/pyiceberg/expressions/__init__.py b/pyiceberg/expressions/__init__.py index cf33779e71..9986cadd9f 100644 --- a/pyiceberg/expressions/__init__.py +++ b/pyiceberg/expressions/__init__.py @@ -346,24 +346,23 @@ def __getnewargs__(self) -> Tuple[BooleanExpression, BooleanExpression]: class Not(IcebergBaseModel, BooleanExpression): """NOT operation expression - logical negation.""" - child: BooleanExpression + model_config = ConfigDict(arbitrary_types_allowed=True) + + type: TypingLiteral["not"] = Field(default="not") + child: BooleanExpression = Field() - @model_validator(mode="before") - def _before(cls, values: Any) -> Any: - if isinstance(values, BooleanExpression): - return {"child": values} - return values + def __init__(self, child: BooleanExpression, **_) -> None: + super().__init__(child=child) - @model_validator(mode="after") - def _normalize(cls, model: Any) -> Any: - child = model.child + def __new__(cls, child: BooleanExpression, **_) -> BooleanExpression: # type: ignore if child is AlwaysTrue(): return AlwaysFalse() elif child is AlwaysFalse(): return AlwaysTrue() elif isinstance(child, Not): return child.child - return model + obj = super().__new__(cls) + return obj def __repr__(self) -> str: """Return the string representation of the Not class.""" From 6416b0c71707edabafbbc5d8fe718016bd686375 Mon Sep 17 00:00:00 2001 From: Fokko Driesprong Date: Thu, 23 Oct 2025 11:02:21 +0200 Subject: [PATCH 5/8] Remove unused import --- pyiceberg/expressions/__init__.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pyiceberg/expressions/__init__.py b/pyiceberg/expressions/__init__.py index 9986cadd9f..301fb374dc 100644 --- a/pyiceberg/expressions/__init__.py +++ b/pyiceberg/expressions/__init__.py @@ -35,7 +35,7 @@ from pydantic import Field -from pydantic import Field, model_validator +from pydantic import Field from pyiceberg.expressions.literals import ( AboveMax, From 313a2e6d772f4df7b81dbe17e8382b26c7379758 Mon Sep 17 00:00:00 2001 From: Fokko Driesprong Date: Thu, 23 Oct 2025 11:15:42 +0200 Subject: [PATCH 6/8] Remove duplicate import --- pyiceberg/expressions/__init__.py | 2 -- 1 file changed, 2 deletions(-) diff --git a/pyiceberg/expressions/__init__.py b/pyiceberg/expressions/__init__.py index 301fb374dc..177b049950 100644 --- a/pyiceberg/expressions/__init__.py +++ b/pyiceberg/expressions/__init__.py @@ -35,8 +35,6 @@ from pydantic import Field -from pydantic import Field - from pyiceberg.expressions.literals import ( AboveMax, BelowMin, From b89b54fcfddd763ad27028751e1bd7b2cedb1d47 Mon Sep 17 00:00:00 2001 From: Fokko Driesprong Date: Thu, 23 Oct 2025 11:21:06 +0200 Subject: [PATCH 7/8] Add type annotations --- pyiceberg/expressions/__init__.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/pyiceberg/expressions/__init__.py b/pyiceberg/expressions/__init__.py index 177b049950..0b84e79edc 100644 --- a/pyiceberg/expressions/__init__.py +++ b/pyiceberg/expressions/__init__.py @@ -349,10 +349,10 @@ class Not(IcebergBaseModel, BooleanExpression): type: TypingLiteral["not"] = Field(default="not") child: BooleanExpression = Field() - def __init__(self, child: BooleanExpression, **_) -> None: + def __init__(self, child: BooleanExpression, **_: Any) -> None: super().__init__(child=child) - def __new__(cls, child: BooleanExpression, **_) -> BooleanExpression: # type: ignore + def __new__(cls, child: BooleanExpression, **_: Any) -> BooleanExpression: # type: ignore if child is AlwaysTrue(): return AlwaysFalse() elif child is AlwaysFalse(): From b37f147f6481065b9b47617ef8d6bfe0a2cedc70 Mon Sep 17 00:00:00 2001 From: Fokko Driesprong Date: Thu, 23 Oct 2025 12:39:43 +0200 Subject: [PATCH 8/8] Add `__str__` --- pyiceberg/expressions/__init__.py | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/pyiceberg/expressions/__init__.py b/pyiceberg/expressions/__init__.py index 0b84e79edc..0b7a12ee58 100644 --- a/pyiceberg/expressions/__init__.py +++ b/pyiceberg/expressions/__init__.py @@ -362,6 +362,10 @@ def __new__(cls, child: BooleanExpression, **_: Any) -> BooleanExpression: # ty obj = super().__new__(cls) return obj + def __str__(self) -> str: + """Return the string representation of the Not class.""" + return f"Not(child={self.child})" + def __repr__(self) -> str: """Return the string representation of the Not class.""" return f"Not(child={repr(self.child)})"