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
+
+
# __ __ ___
# | \/ |_ _| _ \_ _
# | |\/| | || | _/ || |