From 26c1fd25a41ed198a697a48029ea1834c1ae91c3 Mon Sep 17 00:00:00 2001 From: Fokko Driesprong Date: Tue, 18 Nov 2025 11:49:02 +0100 Subject: [PATCH 1/2] Follow up on cleaning up the removal of `Generic` --- pyiceberg/expressions/__init__.py | 18 +---- pyiceberg/expressions/visitors.py | 127 +++++++++++++++--------------- pyiceberg/typedef.py | 6 ++ 3 files changed, 73 insertions(+), 78 deletions(-) diff --git a/pyiceberg/expressions/__init__.py b/pyiceberg/expressions/__init__.py index c56a1de14f..f0dc4094d9 100644 --- a/pyiceberg/expressions/__init__.py +++ b/pyiceberg/expressions/__init__.py @@ -21,27 +21,17 @@ from abc import ABC, abstractmethod from collections.abc import Callable, Iterable, Sequence from functools import cached_property -from typing import TYPE_CHECKING, Any, cast +from typing import Any from typing import Literal as TypingLiteral from pydantic import ConfigDict, Field from pyiceberg.expressions.literals import AboveMax, BelowMin, Literal, literal from pyiceberg.schema import Accessor, Schema -from pyiceberg.typedef import IcebergBaseModel, IcebergRootModel, L, StructProtocol +from pyiceberg.typedef import IcebergBaseModel, IcebergRootModel, L, LiteralValue, StructProtocol from pyiceberg.types import DoubleType, FloatType, NestedField from pyiceberg.utils.singleton import Singleton -try: - from pydantic import ConfigDict -except ImportError: - ConfigDict = dict - -if TYPE_CHECKING: - LiteralValue = Literal[Any] -else: - LiteralValue = Literal - def _to_unbound_term(term: str | UnboundTerm) -> UnboundTerm: return Reference(term) if isinstance(term, str) else term @@ -606,7 +596,7 @@ class SetPredicate(IcebergBaseModel, UnboundPredicate, ABC): model_config = ConfigDict(arbitrary_types_allowed=True) type: TypingLiteral["in", "not-in"] = Field(default="in") - literals: set[Any] = Field(alias="items") + literals: set[LiteralValue] = Field(alias="items") def __init__(self, term: str | UnboundTerm, literals: Iterable[Any] | Iterable[LiteralValue]): literal_set = _to_literal_set(literals) @@ -615,7 +605,7 @@ def __init__(self, term: str | UnboundTerm, literals: Iterable[Any] | Iterable[L def bind(self, schema: Schema, case_sensitive: bool = True) -> BoundSetPredicate: bound_term = self.term.bind(schema, case_sensitive) - literal_set = cast(set[LiteralValue], self.literals) + literal_set = self.literals return self.as_bound(bound_term, {lit.to(bound_term.ref().field.field_type) for lit in literal_set}) def __str__(self) -> str: diff --git a/pyiceberg/expressions/visitors.py b/pyiceberg/expressions/visitors.py index 58143c1306..e4ab3befa3 100644 --- a/pyiceberg/expressions/visitors.py +++ b/pyiceberg/expressions/visitors.py @@ -54,11 +54,10 @@ Or, UnboundPredicate, ) -from pyiceberg.expressions.literals import Literal from pyiceberg.manifest import DataFile, ManifestFile, PartitionFieldSummary from pyiceberg.partitioning import UNPARTITIONED_PARTITION_SPEC, PartitionSpec from pyiceberg.schema import Schema -from pyiceberg.typedef import EMPTY_DICT, L, Record, StructProtocol +from pyiceberg.typedef import EMPTY_DICT, L, LiteralValue, Record, StructProtocol from pyiceberg.types import ( DoubleType, FloatType, @@ -275,27 +274,27 @@ def visit_not_null(self, term: BoundTerm) -> T: """Visit a bound NotNull predicate.""" @abstractmethod - def visit_equal(self, term: BoundTerm, literal: Literal[L]) -> T: + def visit_equal(self, term: BoundTerm, literal: LiteralValue) -> T: """Visit a bound Equal predicate.""" @abstractmethod - def visit_not_equal(self, term: BoundTerm, literal: Literal[L]) -> T: + def visit_not_equal(self, term: BoundTerm, literal: LiteralValue) -> T: """Visit a bound NotEqual predicate.""" @abstractmethod - def visit_greater_than_or_equal(self, term: BoundTerm, literal: Literal[L]) -> T: + def visit_greater_than_or_equal(self, term: BoundTerm, literal: LiteralValue) -> T: """Visit a bound GreaterThanOrEqual predicate.""" @abstractmethod - def visit_greater_than(self, term: BoundTerm, literal: Literal[L]) -> T: + def visit_greater_than(self, term: BoundTerm, literal: LiteralValue) -> T: """Visit a bound GreaterThan predicate.""" @abstractmethod - def visit_less_than(self, term: BoundTerm, literal: Literal[L]) -> T: + def visit_less_than(self, term: BoundTerm, literal: LiteralValue) -> T: """Visit a bound LessThan predicate.""" @abstractmethod - def visit_less_than_or_equal(self, term: BoundTerm, literal: Literal[L]) -> T: + def visit_less_than_or_equal(self, term: BoundTerm, literal: LiteralValue) -> T: """Visit a bound LessThanOrEqual predicate.""" @abstractmethod @@ -319,11 +318,11 @@ def visit_or(self, left_result: T, right_result: T) -> T: """Visit a bound Or predicate.""" @abstractmethod - def visit_starts_with(self, term: BoundTerm, literal: Literal[L]) -> T: + def visit_starts_with(self, term: BoundTerm, literal: LiteralValue) -> T: """Visit bound StartsWith predicate.""" @abstractmethod - def visit_not_starts_with(self, term: BoundTerm, literal: Literal[L]) -> T: + def visit_not_starts_with(self, term: BoundTerm, literal: LiteralValue) -> T: """Visit bound NotStartsWith predicate.""" def visit_unbound_predicate(self, predicate: UnboundPredicate) -> T: @@ -485,33 +484,33 @@ def visit_is_null(self, term: BoundTerm) -> bool: def visit_not_null(self, term: BoundTerm) -> bool: return term.eval(self.struct) is not None - def visit_equal(self, term: BoundTerm, literal: Literal[L]) -> bool: + def visit_equal(self, term: BoundTerm, literal: LiteralValue) -> bool: return term.eval(self.struct) == literal.value - def visit_not_equal(self, term: BoundTerm, literal: Literal[L]) -> bool: + def visit_not_equal(self, term: BoundTerm, literal: LiteralValue) -> bool: return term.eval(self.struct) != literal.value - def visit_greater_than_or_equal(self, term: BoundTerm, literal: Literal[L]) -> bool: + def visit_greater_than_or_equal(self, term: BoundTerm, literal: LiteralValue) -> bool: value = term.eval(self.struct) return value is not None and value >= literal.value - def visit_greater_than(self, term: BoundTerm, literal: Literal[L]) -> bool: + def visit_greater_than(self, term: BoundTerm, literal: LiteralValue) -> bool: value = term.eval(self.struct) return value is not None and value > literal.value - def visit_less_than(self, term: BoundTerm, literal: Literal[L]) -> bool: + def visit_less_than(self, term: BoundTerm, literal: LiteralValue) -> bool: value = term.eval(self.struct) return value is not None and value < literal.value - def visit_less_than_or_equal(self, term: BoundTerm, literal: Literal[L]) -> bool: + def visit_less_than_or_equal(self, term: BoundTerm, literal: LiteralValue) -> bool: value = term.eval(self.struct) return value is not None and value <= literal.value - def visit_starts_with(self, term: BoundTerm, literal: Literal[L]) -> bool: + def visit_starts_with(self, term: BoundTerm, literal: LiteralValue) -> bool: eval_res = term.eval(self.struct) return eval_res is not None and str(eval_res).startswith(str(literal.value)) - def visit_not_starts_with(self, term: BoundTerm, literal: Literal[L]) -> bool: + def visit_not_starts_with(self, term: BoundTerm, literal: LiteralValue) -> bool: return not self.visit_starts_with(term, literal) def visit_true(self) -> bool: @@ -628,7 +627,7 @@ def visit_not_null(self, term: BoundTerm) -> bool: return ROWS_MIGHT_MATCH - def visit_equal(self, term: BoundTerm, literal: Literal[L]) -> bool: + def visit_equal(self, term: BoundTerm, literal: LiteralValue) -> bool: pos = term.ref().accessor.position field = self.partition_fields[pos] @@ -648,12 +647,12 @@ def visit_equal(self, term: BoundTerm, literal: Literal[L]) -> bool: return ROWS_MIGHT_MATCH - def visit_not_equal(self, term: BoundTerm, literal: Literal[L]) -> bool: + def visit_not_equal(self, term: BoundTerm, literal: LiteralValue) -> bool: # because the bounds are not necessarily a min or max value, this cannot be answered using # them. notEq(col, X) with (X, Y) doesn't guarantee that X is a value in col. return ROWS_MIGHT_MATCH - def visit_greater_than_or_equal(self, term: BoundTerm, literal: Literal[L]) -> bool: + def visit_greater_than_or_equal(self, term: BoundTerm, literal: LiteralValue) -> bool: pos = term.ref().accessor.position field = self.partition_fields[pos] @@ -667,7 +666,7 @@ def visit_greater_than_or_equal(self, term: BoundTerm, literal: Literal[L]) -> b return ROWS_MIGHT_MATCH - def visit_greater_than(self, term: BoundTerm, literal: Literal[L]) -> bool: + def visit_greater_than(self, term: BoundTerm, literal: LiteralValue) -> bool: pos = term.ref().accessor.position field = self.partition_fields[pos] @@ -681,7 +680,7 @@ def visit_greater_than(self, term: BoundTerm, literal: Literal[L]) -> bool: return ROWS_MIGHT_MATCH - def visit_less_than(self, term: BoundTerm, literal: Literal[L]) -> bool: + def visit_less_than(self, term: BoundTerm, literal: LiteralValue) -> bool: pos = term.ref().accessor.position field = self.partition_fields[pos] @@ -695,7 +694,7 @@ def visit_less_than(self, term: BoundTerm, literal: Literal[L]) -> bool: return ROWS_MIGHT_MATCH - def visit_less_than_or_equal(self, term: BoundTerm, literal: Literal[L]) -> bool: + def visit_less_than_or_equal(self, term: BoundTerm, literal: LiteralValue) -> bool: pos = term.ref().accessor.position field = self.partition_fields[pos] @@ -709,7 +708,7 @@ def visit_less_than_or_equal(self, term: BoundTerm, literal: Literal[L]) -> bool return ROWS_MIGHT_MATCH - def visit_starts_with(self, term: BoundTerm, literal: Literal[L]) -> bool: + def visit_starts_with(self, term: BoundTerm, literal: LiteralValue) -> bool: pos = term.ref().accessor.position field = self.partition_fields[pos] prefix = str(literal.value) @@ -733,7 +732,7 @@ def visit_starts_with(self, term: BoundTerm, literal: Literal[L]) -> bool: return ROWS_MIGHT_MATCH - def visit_not_starts_with(self, term: BoundTerm, literal: Literal[L]) -> bool: + def visit_not_starts_with(self, term: BoundTerm, literal: LiteralValue) -> bool: pos = term.ref().accessor.position field = self.partition_fields[pos] prefix = str(literal.value) @@ -1041,28 +1040,28 @@ def visit_is_null(self, term: BoundTerm) -> list[tuple[str, str, Any]]: def visit_not_null(self, term: BoundTerm) -> list[tuple[str, str, Any]]: return [(term.ref().field.name, "!=", None)] - def visit_equal(self, term: BoundTerm, literal: Literal[L]) -> list[tuple[str, str, Any]]: + def visit_equal(self, term: BoundTerm, literal: LiteralValue) -> list[tuple[str, str, Any]]: return [(term.ref().field.name, "==", self._cast_if_necessary(term.ref().field.field_type, literal.value))] - def visit_not_equal(self, term: BoundTerm, literal: Literal[L]) -> list[tuple[str, str, Any]]: + def visit_not_equal(self, term: BoundTerm, literal: LiteralValue) -> list[tuple[str, str, Any]]: return [(term.ref().field.name, "!=", self._cast_if_necessary(term.ref().field.field_type, literal.value))] - def visit_greater_than_or_equal(self, term: BoundTerm, literal: Literal[L]) -> list[tuple[str, str, Any]]: + def visit_greater_than_or_equal(self, term: BoundTerm, literal: LiteralValue) -> list[tuple[str, str, Any]]: return [(term.ref().field.name, ">=", self._cast_if_necessary(term.ref().field.field_type, literal.value))] - def visit_greater_than(self, term: BoundTerm, literal: Literal[L]) -> list[tuple[str, str, Any]]: + def visit_greater_than(self, term: BoundTerm, literal: LiteralValue) -> list[tuple[str, str, Any]]: return [(term.ref().field.name, ">", self._cast_if_necessary(term.ref().field.field_type, literal.value))] - def visit_less_than(self, term: BoundTerm, literal: Literal[L]) -> list[tuple[str, str, Any]]: + def visit_less_than(self, term: BoundTerm, literal: LiteralValue) -> list[tuple[str, str, Any]]: return [(term.ref().field.name, "<", self._cast_if_necessary(term.ref().field.field_type, literal.value))] - def visit_less_than_or_equal(self, term: BoundTerm, literal: Literal[L]) -> list[tuple[str, str, Any]]: + def visit_less_than_or_equal(self, term: BoundTerm, literal: LiteralValue) -> list[tuple[str, str, Any]]: return [(term.ref().field.name, "<=", self._cast_if_necessary(term.ref().field.field_type, literal.value))] - def visit_starts_with(self, term: BoundTerm, literal: Literal[L]) -> list[tuple[str, str, Any]]: + def visit_starts_with(self, term: BoundTerm, literal: LiteralValue) -> list[tuple[str, str, Any]]: return [] - def visit_not_starts_with(self, term: BoundTerm, literal: Literal[L]) -> list[tuple[str, str, Any]]: + def visit_not_starts_with(self, term: BoundTerm, literal: LiteralValue) -> list[tuple[str, str, Any]]: return [] def visit_true(self) -> list[tuple[str, str, Any]]: @@ -1231,7 +1230,7 @@ def visit_not_nan(self, term: BoundTerm) -> bool: return ROWS_MIGHT_MATCH - def visit_less_than(self, term: BoundTerm, literal: Literal[L]) -> bool: + def visit_less_than(self, term: BoundTerm, literal: LiteralValue) -> bool: field = term.ref().field field_id = field.field_id @@ -1248,12 +1247,12 @@ def visit_less_than(self, term: BoundTerm, literal: Literal[L]) -> bool: # NaN indicates unreliable bounds. See the InclusiveMetricsEvaluator docs for more. return ROWS_MIGHT_MATCH - if lower_bound >= literal.value: # type: ignore[operator] + if lower_bound >= literal.value: return ROWS_CANNOT_MATCH return ROWS_MIGHT_MATCH - def visit_less_than_or_equal(self, term: BoundTerm, literal: Literal[L]) -> bool: + def visit_less_than_or_equal(self, term: BoundTerm, literal: LiteralValue) -> bool: field = term.ref().field field_id = field.field_id @@ -1269,12 +1268,12 @@ def visit_less_than_or_equal(self, term: BoundTerm, literal: Literal[L]) -> bool # NaN indicates unreliable bounds. See the InclusiveMetricsEvaluator docs for more. return ROWS_MIGHT_MATCH - if lower_bound > literal.value: # type: ignore[operator] + if lower_bound > literal.value: return ROWS_CANNOT_MATCH return ROWS_MIGHT_MATCH - def visit_greater_than(self, term: BoundTerm, literal: Literal[L]) -> bool: + def visit_greater_than(self, term: BoundTerm, literal: LiteralValue) -> bool: field = term.ref().field field_id = field.field_id @@ -1286,7 +1285,7 @@ def visit_greater_than(self, term: BoundTerm, literal: Literal[L]) -> bool: if upper_bound_bytes := self.upper_bounds.get(field_id): upper_bound = from_bytes(field.field_type, upper_bound_bytes) - if upper_bound <= literal.value: # type: ignore[operator] + if upper_bound <= literal.value: if self._is_nan(upper_bound): # NaN indicates unreliable bounds. See the InclusiveMetricsEvaluator docs for more. return ROWS_MIGHT_MATCH @@ -1295,7 +1294,7 @@ def visit_greater_than(self, term: BoundTerm, literal: Literal[L]) -> bool: return ROWS_MIGHT_MATCH - def visit_greater_than_or_equal(self, term: BoundTerm, literal: Literal[L]) -> bool: + def visit_greater_than_or_equal(self, term: BoundTerm, literal: LiteralValue) -> bool: field = term.ref().field field_id = field.field_id @@ -1307,7 +1306,7 @@ def visit_greater_than_or_equal(self, term: BoundTerm, literal: Literal[L]) -> b if upper_bound_bytes := self.upper_bounds.get(field_id): upper_bound = from_bytes(field.field_type, upper_bound_bytes) - if upper_bound < literal.value: # type: ignore[operator] + if upper_bound < literal.value: if self._is_nan(upper_bound): # NaN indicates unreliable bounds. See the InclusiveMetricsEvaluator docs for more. return ROWS_MIGHT_MATCH @@ -1316,7 +1315,7 @@ def visit_greater_than_or_equal(self, term: BoundTerm, literal: Literal[L]) -> b return ROWS_MIGHT_MATCH - def visit_equal(self, term: BoundTerm, literal: Literal[L]) -> bool: + def visit_equal(self, term: BoundTerm, literal: LiteralValue) -> bool: field = term.ref().field field_id = field.field_id @@ -1332,7 +1331,7 @@ def visit_equal(self, term: BoundTerm, literal: Literal[L]) -> bool: # NaN indicates unreliable bounds. See the InclusiveMetricsEvaluator docs for more. return ROWS_MIGHT_MATCH - if lower_bound > literal.value: # type: ignore[operator] + if lower_bound > literal.value: return ROWS_CANNOT_MATCH if upper_bound_bytes := self.upper_bounds.get(field_id): @@ -1341,12 +1340,12 @@ def visit_equal(self, term: BoundTerm, literal: Literal[L]) -> bool: # NaN indicates unreliable bounds. See the InclusiveMetricsEvaluator docs for more. return ROWS_MIGHT_MATCH - if upper_bound < literal.value: # type: ignore[operator] + if upper_bound < literal.value: return ROWS_CANNOT_MATCH return ROWS_MIGHT_MATCH - def visit_not_equal(self, term: BoundTerm, literal: Literal[L]) -> bool: + def visit_not_equal(self, term: BoundTerm, literal: LiteralValue) -> bool: return ROWS_MIGHT_MATCH def visit_in(self, term: BoundTerm, literals: set[L]) -> bool: @@ -1390,7 +1389,7 @@ def visit_not_in(self, term: BoundTerm, literals: set[L]) -> bool: # them. notIn(col, {X, ...}) with (X, Y) doesn't guarantee that X is a value in col. return ROWS_MIGHT_MATCH - def visit_starts_with(self, term: BoundTerm, literal: Literal[L]) -> bool: + def visit_starts_with(self, term: BoundTerm, literal: LiteralValue) -> bool: field = term.ref().field field_id: int = field.field_id @@ -1419,7 +1418,7 @@ def visit_starts_with(self, term: BoundTerm, literal: Literal[L]) -> bool: return ROWS_MIGHT_MATCH - def visit_not_starts_with(self, term: BoundTerm, literal: Literal[L]) -> bool: + def visit_not_starts_with(self, term: BoundTerm, literal: LiteralValue) -> bool: field = term.ref().field field_id: int = field.field_id @@ -1550,7 +1549,7 @@ def visit_not_nan(self, term: BoundTerm) -> bool: return ROWS_MIGHT_NOT_MATCH - def visit_less_than(self, term: BoundTerm, literal: Literal[L]) -> bool: + def visit_less_than(self, term: BoundTerm, literal: LiteralValue) -> bool: # Rows must match when: <----------Min----Max---X-------> field_id = term.ref().field.field_id @@ -1567,7 +1566,7 @@ def visit_less_than(self, term: BoundTerm, literal: Literal[L]) -> bool: return ROWS_MIGHT_NOT_MATCH - def visit_less_than_or_equal(self, term: BoundTerm, literal: Literal[L]) -> bool: + def visit_less_than_or_equal(self, term: BoundTerm, literal: LiteralValue) -> bool: # Rows must match when: <----------Min----Max---X-------> field_id = term.ref().field.field_id @@ -1584,7 +1583,7 @@ def visit_less_than_or_equal(self, term: BoundTerm, literal: Literal[L]) -> bool return ROWS_MIGHT_NOT_MATCH - def visit_greater_than(self, term: BoundTerm, literal: Literal[L]) -> bool: + def visit_greater_than(self, term: BoundTerm, literal: LiteralValue) -> bool: # Rows must match when: <-------X---Min----Max----------> field_id = term.ref().field.field_id @@ -1606,7 +1605,7 @@ def visit_greater_than(self, term: BoundTerm, literal: Literal[L]) -> bool: return ROWS_MIGHT_NOT_MATCH - def visit_greater_than_or_equal(self, term: BoundTerm, literal: Literal[L]) -> bool: + def visit_greater_than_or_equal(self, term: BoundTerm, literal: LiteralValue) -> bool: # Rows must match when: <-------X---Min----Max----------> field_id = term.ref().field.field_id @@ -1627,7 +1626,7 @@ def visit_greater_than_or_equal(self, term: BoundTerm, literal: Literal[L]) -> b return ROWS_MIGHT_NOT_MATCH - def visit_equal(self, term: BoundTerm, literal: Literal[L]) -> bool: + def visit_equal(self, term: BoundTerm, literal: LiteralValue) -> bool: # Rows must match when Min == X == Max field_id = term.ref().field.field_id @@ -1646,7 +1645,7 @@ def visit_equal(self, term: BoundTerm, literal: Literal[L]) -> bool: return ROWS_MIGHT_NOT_MATCH - def visit_not_equal(self, term: BoundTerm, literal: Literal[L]) -> bool: + def visit_not_equal(self, term: BoundTerm, literal: LiteralValue) -> bool: # Rows must match when X < Min or Max < X because it is not in the range field_id = term.ref().field.field_id @@ -1733,10 +1732,10 @@ def visit_not_in(self, term: BoundTerm, literals: set[L]) -> bool: return ROWS_MIGHT_NOT_MATCH - def visit_starts_with(self, term: BoundTerm, literal: Literal[L]) -> bool: + def visit_starts_with(self, term: BoundTerm, literal: LiteralValue) -> bool: return ROWS_MIGHT_NOT_MATCH - def visit_not_starts_with(self, term: BoundTerm, literal: Literal[L]) -> bool: + def visit_not_starts_with(self, term: BoundTerm, literal: LiteralValue) -> bool: return ROWS_MIGHT_NOT_MATCH def _get_field(self, field_id: int) -> NestedField: @@ -1825,37 +1824,37 @@ def visit_not_nan(self, term: BoundTerm) -> BooleanExpression: else: return self.visit_false() - def visit_less_than(self, term: BoundTerm, literal: Literal[L]) -> BooleanExpression: + def visit_less_than(self, term: BoundTerm, literal: LiteralValue) -> BooleanExpression: if term.eval(self.struct) < literal.value: return self.visit_true() else: return self.visit_false() - def visit_less_than_or_equal(self, term: BoundTerm, literal: Literal[L]) -> BooleanExpression: + def visit_less_than_or_equal(self, term: BoundTerm, literal: LiteralValue) -> BooleanExpression: if term.eval(self.struct) <= literal.value: return self.visit_true() else: return self.visit_false() - def visit_greater_than(self, term: BoundTerm, literal: Literal[L]) -> BooleanExpression: + def visit_greater_than(self, term: BoundTerm, literal: LiteralValue) -> BooleanExpression: if term.eval(self.struct) > literal.value: return self.visit_true() else: return self.visit_false() - def visit_greater_than_or_equal(self, term: BoundTerm, literal: Literal[L]) -> BooleanExpression: + def visit_greater_than_or_equal(self, term: BoundTerm, literal: LiteralValue) -> BooleanExpression: if term.eval(self.struct) >= literal.value: return self.visit_true() else: return self.visit_false() - def visit_equal(self, term: BoundTerm, literal: Literal[L]) -> BooleanExpression: + def visit_equal(self, term: BoundTerm, literal: LiteralValue) -> BooleanExpression: if term.eval(self.struct) == literal.value: return self.visit_true() else: return self.visit_false() - def visit_not_equal(self, term: BoundTerm, literal: Literal[L]) -> BooleanExpression: + def visit_not_equal(self, term: BoundTerm, literal: LiteralValue) -> BooleanExpression: if term.eval(self.struct) != literal.value: return self.visit_true() else: @@ -1873,14 +1872,14 @@ def visit_not_in(self, term: BoundTerm, literals: set[L]) -> BooleanExpression: else: return self.visit_false() - def visit_starts_with(self, term: BoundTerm, literal: Literal[L]) -> BooleanExpression: + def visit_starts_with(self, term: BoundTerm, literal: LiteralValue) -> BooleanExpression: eval_res = term.eval(self.struct) if eval_res is not None and str(eval_res).startswith(str(literal.value)): return AlwaysTrue() else: return AlwaysFalse() - def visit_not_starts_with(self, term: BoundTerm, literal: Literal[L]) -> BooleanExpression: + def visit_not_starts_with(self, term: BoundTerm, literal: LiteralValue) -> BooleanExpression: if not self.visit_starts_with(term, literal): return AlwaysTrue() else: diff --git a/pyiceberg/typedef.py b/pyiceberg/typedef.py index ebe1ba3c24..bed54caebf 100644 --- a/pyiceberg/typedef.py +++ b/pyiceberg/typedef.py @@ -36,9 +36,15 @@ from pydantic import BaseModel, ConfigDict, RootModel from typing_extensions import Self +from pyiceberg.expressions.literals import Literal as IcebergLiteral + if TYPE_CHECKING: from pyiceberg.types import StructType + LiteralValue = IcebergLiteral[Any] +else: + LiteralValue = IcebergLiteral + class FrozenDict(dict[Any, Any]): def __setitem__(self, instance: Any, value: Any) -> None: From 6c90390bed38535ff44d7eaceafc2dd23cd7060a Mon Sep 17 00:00:00 2001 From: Fokko Driesprong Date: Wed, 19 Nov 2025 23:47:41 +0100 Subject: [PATCH 2/2] Break circular import --- pyiceberg/typedef.py | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/pyiceberg/typedef.py b/pyiceberg/typedef.py index bed54caebf..80fc5303ad 100644 --- a/pyiceberg/typedef.py +++ b/pyiceberg/typedef.py @@ -36,14 +36,14 @@ from pydantic import BaseModel, ConfigDict, RootModel from typing_extensions import Self -from pyiceberg.expressions.literals import Literal as IcebergLiteral - if TYPE_CHECKING: + from pyiceberg.expressions.literals import Literal as IcebergLiteral from pyiceberg.types import StructType LiteralValue = IcebergLiteral[Any] else: - LiteralValue = IcebergLiteral + # Use Any for runtime to avoid circular import - type checkers will use TYPE_CHECKING version + LiteralValue = Any # type: ignore[assignment,misc] class FrozenDict(dict[Any, Any]):