You are viewing a plain text version of this content. The canonical link for it is here.
Posted to commits@iceberg.apache.org by fo...@apache.org on 2026/05/04 08:21:12 UTC

(iceberg-python) branch main updated: Fix deepcopy for `And`, `Or`, and `Not` expressions (#3295)

This is an automated email from the ASF dual-hosted git repository.

Fokko pushed a commit to branch main
in repository https://gitbox.apache.org/repos/asf/iceberg-python.git


The following commit(s) were added to refs/heads/main by this push:
     new 96bafaf1 Fix deepcopy for `And`, `Or`, and `Not` expressions (#3295)
96bafaf1 is described below

commit 96bafaf15c71ba48f5bf8e79b9e609cf65fa9f5e
Author: Rob Reeves <ro...@gmail.com>
AuthorDate: Mon May 4 01:21:05 2026 -0700

    Fix deepcopy for `And`, `Or`, and `Not` expressions (#3295)
    
    Closes #3297
    
    # Rationale for this change
    
    `copy.deepcopy()` on `And`, `Or`, and `Not` expressions raises
    `TypeError` because Pydantic v2's default `__deepcopy__` calls
    `cls.__new__(cls)` with no args, but these classes require positional
    arguments in `__new__`.
    
    The fix adds `__deepcopy__` to `And`, `Or`, and `Not` that deepcopy
    their fields and call the constructor directly.
    
    ## Are these changes tested?
    
    Yes. 11 new unit tests covering all expression types, balanced trees,
    nested expressions, and deepcopy followed by pickle.
    
    ## Are there any user-facing changes?
    
    Yes, `copy.deepcopy()` on expressions now works instead of raising an
    exception.
---
 pyiceberg/expressions/__init__.py     | 13 +++++++
 tests/expressions/test_expressions.py | 70 +++++++++++++++++++++++++++++++++++
 2 files changed, 83 insertions(+)

diff --git a/pyiceberg/expressions/__init__.py b/pyiceberg/expressions/__init__.py
index 3910a146..ef4cb250 100644
--- a/pyiceberg/expressions/__init__.py
+++ b/pyiceberg/expressions/__init__.py
@@ -17,6 +17,7 @@
 
 from __future__ import annotations
 
+import copy
 from abc import ABC, abstractmethod
 from collections.abc import Callable, Iterable, Sequence
 from functools import cached_property
@@ -339,6 +340,10 @@ class And(BooleanExpression):
         # De Morgan's law: not (A and B) = (not A) or (not B)
         return Or(~self.left, ~self.right)
 
+    def __deepcopy__(self, memo: dict[int, Any]) -> And:
+        """Return a deep copy of the And expression."""
+        return And(copy.deepcopy(self.left, memo), copy.deepcopy(self.right, memo))
+
     def __getnewargs__(self) -> tuple[BooleanExpression, BooleanExpression]:
         """Pickle the And class."""
         return (self.left, self.right)
@@ -386,6 +391,10 @@ class Or(BooleanExpression):
         # De Morgan's law: not (A or B) = (not A) and (not B)
         return And(~self.left, ~self.right)
 
+    def __deepcopy__(self, memo: dict[int, Any]) -> Or:
+        """Return a deep copy of the Or expression."""
+        return Or(copy.deepcopy(self.left, memo), copy.deepcopy(self.right, memo))
+
     def __getnewargs__(self) -> tuple[BooleanExpression, BooleanExpression]:
         """Pickle the Or class."""
         return (self.left, self.right)
@@ -428,6 +437,10 @@ class Not(BooleanExpression):
         """Transform the Expression into its negated version."""
         return self.child
 
+    def __deepcopy__(self, memo: dict[int, Any]) -> Not:
+        """Return a deep copy of the Not expression."""
+        return Not(copy.deepcopy(self.child, memo))
+
     def __getnewargs__(self) -> tuple[BooleanExpression]:
         """Pickle the Not class."""
         return (self.child,)
diff --git a/tests/expressions/test_expressions.py b/tests/expressions/test_expressions.py
index 157c1ada..d6c0ce1f 100644
--- a/tests/expressions/test_expressions.py
+++ b/tests/expressions/test_expressions.py
@@ -16,6 +16,7 @@
 # under the License.
 # pylint:disable=redefined-outer-name,eval-used
 
+import copy
 import pickle
 import uuid
 from decimal import Decimal
@@ -1292,6 +1293,75 @@ def test_bind_ambiguous_name() -> None:
     assert "Invalid schema, multiple fields for name foo.bar: 2 and 3" in str(exc_info)
 
 
+def test_deepcopy_and() -> None:
+    expr = And(EqualTo("x", 1), EqualTo("y", 2))
+    copied = copy.deepcopy(expr)
+    assert copied == expr
+    assert copied is not expr
+
+
+def test_deepcopy_or() -> None:
+    expr = Or(EqualTo("x", 1), EqualTo("y", 2))
+    copied = copy.deepcopy(expr)
+    assert copied == expr
+    assert copied is not expr
+
+
+def test_deepcopy_not() -> None:
+    expr = Not(EqualTo("x", 1))
+    copied = copy.deepcopy(expr)
+    assert copied == expr
+    assert copied is not expr
+
+
+def test_deepcopy_equal_to() -> None:
+    expr = EqualTo("x", 1)
+    copied = copy.deepcopy(expr)
+    assert copied == expr
+    assert copied is not expr
+
+
+def test_deepcopy_always_true() -> None:
+    copied = copy.deepcopy(AlwaysTrue())
+    assert copied is AlwaysTrue()
+
+
+def test_deepcopy_always_false() -> None:
+    copied = copy.deepcopy(AlwaysFalse())
+    assert copied is AlwaysFalse()
+
+
+def test_deepcopy_always_true_then_pickle() -> None:
+    copied = copy.deepcopy(AlwaysTrue())
+    restored = pickle.loads(pickle.dumps(copied))
+    assert restored is AlwaysTrue()
+
+
+def test_deepcopy_balanced_and() -> None:
+    expr = And(EqualTo("a", 1), EqualTo("b", 2), EqualTo("c", 3), EqualTo("d", 4))
+    copied = copy.deepcopy(expr)
+    assert copied == expr
+
+
+def test_deepcopy_balanced_or() -> None:
+    expr = Or(EqualTo("a", 1), EqualTo("b", 2), EqualTo("c", 3), EqualTo("d", 4))
+    copied = copy.deepcopy(expr)
+    assert copied == expr
+
+
+def test_deepcopy_nested_expression() -> None:
+    expr = And(Or(EqualTo("a", 1), EqualTo("b", 2)), Not(EqualTo("c", 3)))
+    copied = copy.deepcopy(expr)
+    assert copied == expr
+
+
+def test_deepcopy_then_pickle() -> None:
+    expr = And(EqualTo("x", 1), EqualTo("y", 2))
+    copied = copy.deepcopy(expr)
+    restored = pickle.loads(pickle.dumps(copied))
+    assert restored == expr
+
+
 #   __  __      ___
 #  |  \/  |_  _| _ \_  _
 #  | |\/| | || |  _/ || |