Skip to content

Commit a696c70

Browse files
authored
feat(ir): add default implementation of pretty formatting nodes (ibis-project#8880)
We have custom nodes for example to lower expressions to certain backends. Pretty printing these are especially useful but currently we raise for unknown node types.
1 parent 38e7e14 commit a696c70

3 files changed

Lines changed: 70 additions & 49 deletions

File tree

ibis/expr/format.py

Lines changed: 9 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -169,7 +169,7 @@ def __repr__(self):
169169

170170

171171
@public
172-
def pretty(expr: ir.Expr, scope: Optional[dict[str, ir.Expr]] = None):
172+
def pretty(expr: ops.Node | ir.Expr, scope: Optional[dict[str, ir.Expr]] = None):
173173
"""Pretty print an expression.
174174
175175
Parameters
@@ -186,10 +186,13 @@ def pretty(expr: ir.Expr, scope: Optional[dict[str, ir.Expr]] = None):
186186
str
187187
A pretty printed representation of the expression.
188188
"""
189-
if not isinstance(expr, ir.Expr):
190-
raise TypeError(f"Expected an expression, got {type(expr)}")
189+
if isinstance(expr, ir.Expr):
190+
node = expr.op()
191+
elif isinstance(expr, ops.Node):
192+
node = expr
193+
else:
194+
raise TypeError(f"Expected an expression or a node, got {type(expr)}")
191195

192-
node = expr.op()
193196
refs = {}
194197
refcnt = itertools.count()
195198
variables = {v.op(): k for k, v in (scope or {}).items()}
@@ -224,7 +227,8 @@ def mapper(op, _, **kwargs):
224227

225228
@functools.singledispatch
226229
def fmt(op, **kwargs):
227-
raise NotImplementedError(f"no pretty printer for {type(op)}")
230+
top = f"{op.__class__.__name__}\n"
231+
return top + render_fields(kwargs, 1)
228232

229233

230234
@fmt.register(ops.Relation)
Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,9 @@
1+
r0 := UnboundTable: t
2+
a int64
3+
4+
ValueList
5+
values:
6+
1
7+
2.0
8+
'three'
9+
r0.a

ibis/expr/tests/test_format.py

Lines changed: 52 additions & 44 deletions
Original file line numberDiff line numberDiff line change
@@ -7,23 +7,20 @@
77

88
import ibis
99
import ibis.expr.datatypes as dt
10-
import ibis.expr.format
1110
import ibis.expr.operations as ops
1211
import ibis.legacy.udf.vectorized as udf
1312
from ibis import util
14-
15-
# easier to switch implementation if needed
16-
fmt = repr
13+
from ibis.expr.format import fmt, pretty
1714

1815

1916
@pytest.mark.parametrize("cls", [ops.PhysicalTable, ops.Relation])
2017
def test_tables_have_format_value_rules(cls):
21-
assert cls in ibis.expr.format.fmt.registry
18+
assert cls in fmt.registry
2219

2320

2421
def test_format_table_column(alltypes, snapshot):
2522
# GH #507
26-
result = fmt(alltypes.f)
23+
result = repr(alltypes.f)
2724
assert "float64" in result
2825
snapshot.assert_match(result, "repr.txt")
2926

@@ -32,14 +29,14 @@ def test_format_projection(alltypes, snapshot):
3229
# This should produce a ref to the projection
3330
proj = alltypes[["c", "a", "f"]]
3431
expr = proj["a"]
35-
result = fmt(expr)
32+
result = repr(expr)
3633
snapshot.assert_match(result, "repr.txt")
3734

3835

3936
def test_format_table_with_empty_schema(snapshot):
4037
# GH #6837
4138
schema = ibis.table({}, name="t")
42-
result = fmt(schema)
39+
result = repr(schema)
4340
snapshot.assert_match(result, "repr.txt")
4441

4542

@@ -55,7 +52,7 @@ def test_table_type_output(snapshot):
5552
)
5653

5754
expr = foo.dept_id == foo.view().dept_id
58-
result = fmt(expr)
55+
result = repr(expr)
5956
assert "UnboundTable: foo" in result
6057
snapshot.assert_match(result, "repr.txt")
6158

@@ -68,7 +65,7 @@ def test_aggregate_arg_names(alltypes, snapshot):
6865
metrics = [t.c.sum().name("c"), t.d.mean().name("d")]
6966

7067
expr = t.group_by(by_exprs).aggregate(metrics)
71-
result = fmt(expr)
68+
result = repr(expr)
7269
assert "metrics" in result
7370
assert "groups" in result
7471

@@ -103,7 +100,7 @@ def test_format_multiple_join_with_projection(snapshot):
103100
view = j2[[filtered, table2["value1"], table3["value2"]]]
104101

105102
# it works!
106-
result = fmt(view)
103+
result = repr(view)
107104
snapshot.assert_match(result, "repr.txt")
108105

109106

@@ -117,7 +114,7 @@ def test_memoize_filtered_table(snapshot):
117114
t = airlines[airlines.dest.isin(dests)]
118115
delay_filter = t.dest.topk(10, by=t.arrdelay.mean())
119116

120-
result = fmt(delay_filter)
117+
result = repr(delay_filter)
121118
snapshot.assert_match(result, "repr.txt")
122119

123120

@@ -126,8 +123,8 @@ def test_named_value_expr_show_name(alltypes, snapshot):
126123
expr2 = expr.name("baz")
127124

128125
# it works!
129-
result = fmt(expr)
130-
result2 = fmt(expr2)
126+
result = repr(expr)
127+
result2 = repr(expr2)
131128

132129
assert "baz" not in result
133130
assert "baz" in result2
@@ -157,14 +154,14 @@ def test_memoize_filtered_tables_in_join(snapshot):
157154
cond = left.region == right.region
158155
joined = left.join(right, cond)[left, right.total.name("right_total")]
159156

160-
result = fmt(joined)
157+
result = repr(joined)
161158
snapshot.assert_match(result, "repr.txt")
162159

163160

164161
def test_argument_repr_shows_name(snapshot):
165162
t = ibis.table([("fakecolname1", "int64")], name="fakename2")
166163
expr = t.fakecolname1.nullif(2)
167-
result = fmt(expr)
164+
result = repr(expr)
168165

169166
assert "fakecolname1" in result
170167
assert "fakename2" in result
@@ -182,7 +179,7 @@ def test_scalar_parameter_formatting():
182179
def test_same_column_multiple_aliases(snapshot):
183180
table = ibis.table([("col", "int64")], name="t")
184181
expr = table[table.col.name("fakealias1"), table.col.name("fakealias2")]
185-
result = fmt(expr)
182+
result = repr(expr)
186183

187184
assert "UnboundTable: t" in result
188185
assert "col int64" in result
@@ -193,7 +190,7 @@ def test_same_column_multiple_aliases(snapshot):
193190

194191
def test_scalar_parameter_repr():
195192
value = ibis.param(dt.timestamp).name("value")
196-
assert fmt(value) == "value: $(timestamp)"
193+
assert repr(value) == "value: $(timestamp)"
197194

198195

199196
def test_repr_exact(snapshot):
@@ -205,7 +202,7 @@ def test_repr_exact(snapshot):
205202
name="t",
206203
).mutate(col4=lambda t: t.col2.length())
207204

208-
result = fmt(table)
205+
result = repr(table)
209206
snapshot.assert_match(result, "repr.txt")
210207

211208

@@ -218,7 +215,7 @@ def test_complex_repr(snapshot):
218215
.aggregate(y=lambda t: t.a.sum())
219216
.limit(10)
220217
)
221-
result = fmt(t)
218+
result = repr(t)
222219

223220
snapshot.assert_match(result, "repr.txt")
224221

@@ -245,20 +242,20 @@ def test_schema_truncation(monkeypatch, snapshot):
245242

246243
monkeypatch.setattr(ibis.options.repr, "table_columns", 0)
247244
with pytest.raises(ValueError):
248-
fmt(t)
245+
repr(t)
249246

250247
monkeypatch.setattr(ibis.options.repr, "table_columns", 1)
251-
result = fmt(t)
248+
result = repr(t)
252249
assert util.VERTICAL_ELLIPSIS not in result
253250
snapshot.assert_match(result, "repr1.txt")
254251

255252
monkeypatch.setattr(ibis.options.repr, "table_columns", 8)
256-
result = fmt(t)
253+
result = repr(t)
257254
assert util.VERTICAL_ELLIPSIS in result
258255
snapshot.assert_match(result, "repr8.txt")
259256

260257
monkeypatch.setattr(ibis.options.repr, "table_columns", 1000)
261-
result = fmt(t)
258+
result = repr(t)
262259
assert util.VERTICAL_ELLIPSIS not in result
263260
snapshot.assert_match(result, "repr_all.txt")
264261

@@ -271,15 +268,15 @@ def test_table_count_expr(snapshot):
271268
join_cnt = t1.join(t2, t1.a == t2.a).count()
272269
union_cnt = ibis.union(t1, t2).count()
273270

274-
snapshot.assert_match(fmt(cnt), "cnt_repr.txt")
275-
snapshot.assert_match(fmt(join_cnt), "join_repr.txt")
276-
snapshot.assert_match(fmt(union_cnt), "union_repr.txt")
271+
snapshot.assert_match(repr(cnt), "cnt_repr.txt")
272+
snapshot.assert_match(repr(join_cnt), "join_repr.txt")
273+
snapshot.assert_match(repr(union_cnt), "union_repr.txt")
277274

278275

279276
def test_window_no_group_by(snapshot):
280277
t = ibis.table(dict(a="int64", b="string"), name="t")
281278
expr = t.a.mean().over(ibis.window(preceding=0))
282-
result = fmt(expr)
279+
result = repr(expr)
283280

284281
assert "group_by=[]" not in result
285282
snapshot.assert_match(result, "repr.txt")
@@ -289,7 +286,7 @@ def test_window_group_by(snapshot):
289286
t = ibis.table(dict(a="int64", b="string"), name="t")
290287
expr = t.a.mean().over(ibis.window(group_by=t.b))
291288

292-
result = fmt(expr)
289+
result = repr(expr)
293290
assert "start=0" not in result
294291
assert "group_by=[r0.b]" in result
295292
snapshot.assert_match(result, "repr.txt")
@@ -299,13 +296,13 @@ def test_fillna(snapshot):
299296
t = ibis.table(dict(a="int64", b="string"), name="t")
300297

301298
expr = t.fillna({"a": 3})
302-
snapshot.assert_match(fmt(expr), "fillna_dict_repr.txt")
299+
snapshot.assert_match(repr(expr), "fillna_dict_repr.txt")
303300

304301
expr = t[["a"]].fillna(3)
305-
snapshot.assert_match(fmt(expr), "fillna_int_repr.txt")
302+
snapshot.assert_match(repr(expr), "fillna_int_repr.txt")
306303

307304
expr = t[["b"]].fillna("foo")
308-
snapshot.assert_match(fmt(expr), "fillna_str_repr.txt")
305+
snapshot.assert_match(repr(expr), "fillna_str_repr.txt")
309306

310307

311308
def test_asof_join(snapshot):
@@ -315,7 +312,7 @@ def test_asof_join(snapshot):
315312
right, left.value == right.value2
316313
)
317314

318-
result = fmt(joined)
315+
result = repr(joined)
319316
snapshot.assert_match(result, "repr.txt")
320317

321318

@@ -330,7 +327,7 @@ def test_two_inner_joins(snapshot):
330327
right, left.value == right.value2
331328
)
332329

333-
result = fmt(joined)
330+
result = repr(joined)
334331
snapshot.assert_match(result, "repr.txt")
335332

336333

@@ -347,7 +344,7 @@ def multi_output_udf(v):
347344
return v.sum(), v.mean()
348345

349346
expr = table.aggregate(multi_output_udf(table["col"]).destructure())
350-
result = fmt(expr)
347+
result = repr(expr)
351348

352349
assert "sum: StructField(ReductionVectorizedUDF" in result
353350
assert "mean: StructField(ReductionVectorizedUDF" in result
@@ -360,41 +357,41 @@ def multi_output_udf(v):
360357
)
361358
def test_format_literal(literal, typ, output):
362359
expr = ibis.literal(literal, type=typ)
363-
assert fmt(expr) == output
360+
assert repr(expr) == output
364361

365362

366363
def test_format_dummy_table(snapshot):
367364
t = ops.DummyTable({"foo": ibis.array([1]).cast("array<int8>")}).to_expr()
368365

369-
result = fmt(t)
366+
result = repr(t)
370367
snapshot.assert_match(result, "repr.txt")
371368

372369

373370
def test_format_in_memory_table(snapshot):
374371
t = ibis.memtable([(1, 2), (3, 4), (5, 6)], columns=["x", "y"])
375372
expr = t.x.sum() + t.y.sum()
376373

377-
result = fmt(expr)
374+
result = repr(expr)
378375
assert "InMemoryTable" in result
379376
snapshot.assert_match(result, "repr.txt")
380377

381378

382379
def test_format_unbound_table_namespace(snapshot):
383380
t = ibis.table(name="bork", schema=(("a", "int"), ("b", "int")))
384381

385-
result = fmt(t)
382+
result = repr(t)
386383
snapshot.assert_match(result, "repr.txt")
387384

388385
t = ibis.table(name="bork", schema=(("a", "int"), ("b", "int")), database="bork")
389386

390-
result = fmt(t)
387+
result = repr(t)
391388
snapshot.assert_match(result, "reprdb.txt")
392389

393390
t = ibis.table(
394391
name="bork", schema=(("a", "int"), ("b", "int")), catalog="ork", database="bork"
395392
)
396393

397-
result = fmt(t)
394+
result = repr(t)
398395
snapshot.assert_match(result, "reprcatdb.txt")
399396

400397

@@ -413,7 +410,7 @@ def values(self):
413410

414411
table = MyRelation(alltypes, kind="foo").to_expr()
415412
expr = table[table, table.a.name("a2")]
416-
result = fmt(expr)
413+
result = repr(expr)
417414

418415
snapshot.assert_match(result, "repr.txt")
419416

@@ -431,7 +428,7 @@ def shape(self):
431428
return self.arg.shape
432429

433430
expr = Inc(alltypes.a).to_expr().name("incremented")
434-
result = fmt(expr)
431+
result = repr(expr)
435432
last_line = result.splitlines()[-1]
436433

437434
assert "Inc" in result
@@ -449,11 +446,22 @@ def test_format_show_variables(monkeypatch, alltypes, snapshot):
449446
sub = projected.a - projected.b
450447
expr = add * sub
451448

452-
result = fmt(expr)
449+
result = repr(expr)
453450

454451
assert "projected.a" in result
455452
assert "projected.b" in result
456453
assert "filtered" in result
457454
assert "ordered" in result
458455

459456
snapshot.assert_match(result, "repr.txt")
457+
458+
459+
def test_default_format_implementation(snapshot):
460+
class ValueList(ops.Node):
461+
values: tuple[ops.Value, ...]
462+
463+
t = ibis.table([("a", "int64")], name="t")
464+
vl = ValueList((1, 2.0, "three", t.a))
465+
result = pretty(vl)
466+
467+
snapshot.assert_match(result, "repr.txt")

0 commit comments

Comments
 (0)