Skip to content

Commit ebd0542

Browse files
committed
feat: Add the concept of Version-ing
`Version` is never stored on an `IR` node and should only be needed when converting back to another representation (`to_*`)
1 parent bbba0fe commit ebd0542

File tree

2 files changed

+83
-46
lines changed

2 files changed

+83
-46
lines changed

narwhals/_plan/common.py

Lines changed: 9 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,8 @@
33
from typing import TYPE_CHECKING
44
from typing import TypeVar
55

6+
from narwhals.utils import Version
7+
68
if TYPE_CHECKING:
79
from typing import Any
810
from typing import Callable
@@ -119,15 +121,17 @@ def __init__(self, **kwds: Any) -> None:
119121
class ExprIR(Immutable):
120122
"""Anything that can be a node on a graph of expressions."""
121123

122-
def to_narwhals(self) -> DummyExpr:
123-
from narwhals._plan.dummy import DummyExpr
124+
def to_narwhals(self, version: Version = Version.MAIN) -> DummyExpr:
125+
from narwhals._plan import dummy
124126

125-
return DummyExpr._from_ir(self)
127+
if version is Version.MAIN:
128+
return dummy.DummyExpr._from_ir(self)
129+
return dummy.DummyExprV1._from_ir(self)
126130

127-
def to_compliant(self) -> DummyCompliantExpr:
131+
def to_compliant(self, version: Version = Version.MAIN) -> DummyCompliantExpr:
128132
from narwhals._plan.dummy import DummyCompliantExpr
129133

130-
return DummyCompliantExpr._from_ir(self)
134+
return DummyCompliantExpr._from_ir(self, version)
131135

132136
@property
133137
def is_scalar(self) -> bool:

narwhals/_plan/dummy.py

Lines changed: 74 additions & 41 deletions
Original file line numberDiff line numberDiff line change
@@ -14,6 +14,7 @@
1414
from narwhals._plan.window import Over
1515
from narwhals.dtypes import DType
1616
from narwhals.utils import Version
17+
from narwhals.utils import _hasattr_static
1718
from narwhals.utils import flatten
1819

1920
if TYPE_CHECKING:
@@ -29,21 +30,26 @@
2930
# Entirely ignoring namespace + function binding
3031
class DummyExpr:
3132
_ir: ExprIR
33+
_version: t.ClassVar[Version] = Version.MAIN
3234

3335
def __repr__(self) -> str:
34-
return f"Narwhals DummyExpr:\n{self._ir!r}"
36+
return f"Narwhals DummyExpr ({self.version.name.lower()}):\n{self._ir!r}"
3537

3638
@classmethod
3739
def _from_ir(cls, ir: ExprIR, /) -> Self:
3840
obj = cls.__new__(cls)
3941
obj._ir = ir
4042
return obj
4143

44+
@property
45+
def version(self) -> Version:
46+
return self._version
47+
4248
def alias(self, name: str) -> Self:
4349
return self._from_ir(expr.Alias(expr=self._ir, name=name))
4450

4551
def cast(self, dtype: DType | type[DType]) -> Self:
46-
dtype = dtype if isinstance(dtype, DType) else Version.MAIN.dtypes.Unknown()
52+
dtype = dtype if isinstance(dtype, DType) else self.version.dtypes.Unknown()
4753
return self._from_ir(expr.Cast(expr=self._ir, dtype=dtype))
4854

4955
def count(self) -> Self:
@@ -92,7 +98,7 @@ def over(
9298
order_by: DummyExpr | t.Iterable[DummyExpr] | None = None,
9399
descending: bool = False,
94100
nulls_last: bool = False,
95-
) -> DummyExpr:
101+
) -> Self:
96102
order: tuple[Seq[ExprIR], SortOptions] | None = None
97103
partition = tuple(expr._ir for expr in flatten(partition_by))
98104
if not (partition) and order_by is None:
@@ -102,7 +108,7 @@ def over(
102108
by = tuple(expr._ir for expr in flatten([order_by]))
103109
options = SortOptions(descending=descending, nulls_last=nulls_last)
104110
order = by, options
105-
return Over().to_window_expr(self._ir, partition, order).to_narwhals()
111+
return self._from_ir(Over().to_window_expr(self._ir, partition, order))
106112

107113
def sort_by(
108114
self,
@@ -121,78 +127,98 @@ def sort_by(
121127
options = SortMultipleOptions(descending=desc, nulls_last=nulls)
122128
return self._from_ir(expr.SortBy(expr=self._ir, by=sort_by, options=options))
123129

124-
def __eq__(self, other: DummyExpr) -> DummyExpr: # type: ignore[override]
130+
def __eq__(self, other: DummyExpr) -> Self: # type: ignore[override]
125131
op = ops.Eq()
126-
return op.to_binary_expr(self._ir, other._ir).to_narwhals()
132+
return self._from_ir(op.to_binary_expr(self._ir, other._ir))
127133

128-
def __ne__(self, other: DummyExpr) -> DummyExpr: # type: ignore[override]
134+
def __ne__(self, other: DummyExpr) -> Self: # type: ignore[override]
129135
op = ops.NotEq()
130-
return op.to_binary_expr(self._ir, other._ir).to_narwhals()
136+
return self._from_ir(op.to_binary_expr(self._ir, other._ir))
131137

132-
def __lt__(self, other: DummyExpr) -> DummyExpr:
138+
def __lt__(self, other: DummyExpr) -> Self:
133139
op = ops.Lt()
134-
return op.to_binary_expr(self._ir, other._ir).to_narwhals()
140+
return self._from_ir(op.to_binary_expr(self._ir, other._ir))
135141

136-
def __le__(self, other: DummyExpr) -> DummyExpr:
142+
def __le__(self, other: DummyExpr) -> Self:
137143
op = ops.LtEq()
138-
return op.to_binary_expr(self._ir, other._ir).to_narwhals()
144+
return self._from_ir(op.to_binary_expr(self._ir, other._ir))
139145

140-
def __gt__(self, other: DummyExpr) -> DummyExpr:
146+
def __gt__(self, other: DummyExpr) -> Self:
141147
op = ops.Gt()
142-
return op.to_binary_expr(self._ir, other._ir).to_narwhals()
148+
return self._from_ir(op.to_binary_expr(self._ir, other._ir))
143149

144-
def __ge__(self, other: DummyExpr) -> DummyExpr:
150+
def __ge__(self, other: DummyExpr) -> Self:
145151
op = ops.GtEq()
146-
return op.to_binary_expr(self._ir, other._ir).to_narwhals()
152+
return self._from_ir(op.to_binary_expr(self._ir, other._ir))
147153

148-
def __add__(self, other: DummyExpr) -> DummyExpr:
154+
def __add__(self, other: DummyExpr) -> Self:
149155
op = ops.Add()
150-
return op.to_binary_expr(self._ir, other._ir).to_narwhals()
156+
return self._from_ir(op.to_binary_expr(self._ir, other._ir))
151157

152-
def __sub__(self, other: DummyExpr) -> DummyExpr:
158+
def __sub__(self, other: DummyExpr) -> Self:
153159
op = ops.Sub()
154-
return op.to_binary_expr(self._ir, other._ir).to_narwhals()
160+
return self._from_ir(op.to_binary_expr(self._ir, other._ir))
155161

156-
def __mul__(self, other: DummyExpr) -> DummyExpr:
162+
def __mul__(self, other: DummyExpr) -> Self:
157163
op = ops.Multiply()
158-
return op.to_binary_expr(self._ir, other._ir).to_narwhals()
164+
return self._from_ir(op.to_binary_expr(self._ir, other._ir))
159165

160-
def __truediv__(self, other: DummyExpr) -> DummyExpr:
166+
def __truediv__(self, other: DummyExpr) -> Self:
161167
op = ops.TrueDivide()
162-
return op.to_binary_expr(self._ir, other._ir).to_narwhals()
168+
return self._from_ir(op.to_binary_expr(self._ir, other._ir))
163169

164-
def __floordiv__(self, other: DummyExpr) -> DummyExpr:
170+
def __floordiv__(self, other: DummyExpr) -> Self:
165171
op = ops.FloorDivide()
166-
return op.to_binary_expr(self._ir, other._ir).to_narwhals()
172+
return self._from_ir(op.to_binary_expr(self._ir, other._ir))
167173

168-
def __mod__(self, other: DummyExpr) -> DummyExpr:
174+
def __mod__(self, other: DummyExpr) -> Self:
169175
op = ops.Modulus()
170-
return op.to_binary_expr(self._ir, other._ir).to_narwhals()
176+
return self._from_ir(op.to_binary_expr(self._ir, other._ir))
171177

172-
def __and__(self, other: DummyExpr) -> DummyExpr:
178+
def __and__(self, other: DummyExpr) -> Self:
173179
op = ops.And()
174-
return op.to_binary_expr(self._ir, other._ir).to_narwhals()
180+
return self._from_ir(op.to_binary_expr(self._ir, other._ir))
175181

176-
def __or__(self, other: DummyExpr) -> DummyExpr:
182+
def __or__(self, other: DummyExpr) -> Self:
177183
op = ops.Or()
178-
return op.to_binary_expr(self._ir, other._ir).to_narwhals()
184+
return self._from_ir(op.to_binary_expr(self._ir, other._ir))
185+
186+
def __invert__(self) -> Self:
187+
return self._from_ir(boolean.Not().to_function_expr(self._ir))
188+
179189

180-
def __invert__(self) -> DummyExpr:
181-
return boolean.Not().to_function_expr(self._ir).to_narwhals()
190+
class DummyExprV1(DummyExpr):
191+
_version: t.ClassVar[Version] = Version.V1
182192

183193

184194
class DummyCompliantExpr:
185195
_ir: ExprIR
196+
_version: Version
197+
198+
@property
199+
def version(self) -> Version:
200+
return self._version
186201

187202
@classmethod
188-
def _from_ir(cls, ir: ExprIR, /) -> Self:
203+
def _from_ir(cls, ir: ExprIR, /, version: Version) -> Self:
189204
obj = cls.__new__(cls)
190205
obj._ir = ir
206+
obj._version = version
191207
return obj
192208

209+
def to_narwhals(self) -> DummyExpr:
210+
if self.version is Version.MAIN:
211+
return DummyExpr._from_ir(self._ir)
212+
return DummyExprV1._from_ir(self._ir)
213+
193214

194215
class DummySeries:
195216
_compliant: DummyCompliantSeries
217+
_version: t.ClassVar[Version] = Version.MAIN
218+
219+
@property
220+
def version(self) -> Version:
221+
return self._version
196222

197223
@property
198224
def dtype(self) -> DType:
@@ -205,31 +231,38 @@ def name(self) -> str:
205231
@classmethod
206232
def from_native(cls, native: NativeSeries, /) -> Self:
207233
obj = cls.__new__(cls)
208-
obj._compliant = DummyCompliantSeries.from_native(native)
234+
obj._compliant = DummyCompliantSeries.from_native(native, cls._version)
209235
return obj
210236

211237

238+
class DummySeriesV1(DummySeries):
239+
_version: t.ClassVar[Version] = Version.V1
240+
241+
212242
class DummyCompliantSeries:
213243
_native: NativeSeries
214244
_name: str
245+
_version: Version
246+
247+
@property
248+
def version(self) -> Version:
249+
return self._version
215250

216251
@property
217252
def dtype(self) -> DType:
218-
return Version.MAIN.dtypes.Float64()
253+
return self.version.dtypes.Float64()
219254

220255
@property
221256
def name(self) -> str:
222257
return self._name
223258

224259
@classmethod
225-
def from_native(cls, native: NativeSeries, /) -> Self:
226-
from narwhals.utils import _hasattr_static
227-
260+
def from_native(cls, native: NativeSeries, /, version: Version) -> Self:
228261
name: str = "<PLACEHOLDER>"
229-
230262
if _hasattr_static(native, "name"):
231263
name = getattr(native, "name", name)
232264
obj = cls.__new__(cls)
233265
obj._native = native
234266
obj._name = name
267+
obj._version = version
235268
return obj

0 commit comments

Comments
 (0)