Skip to content

Commit 68dfe39

Browse files
authored
feat(ir): support pretty printing arbitrary traversable objects (ibis-project#9043)
This enables pretty formatting the dereference mappings and also the replacements mappings we use in rewrites for better inspection. Also moved the irrelevant logic out from `format.py`.
1 parent 01b521c commit 68dfe39

5 files changed

Lines changed: 67 additions & 39 deletions

File tree

ibis/common/typing.py

Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
11
from __future__ import annotations
22

3+
import inspect
34
import re
45
import sys
56
from abc import abstractmethod
@@ -259,3 +260,21 @@ class Coercible(Abstract):
259260
@classmethod
260261
@abstractmethod
261262
def __coerce__(cls, value: Any, **kwargs: Any) -> Self: ...
263+
264+
265+
def get_defining_frame(obj):
266+
"""Locate the outermost frame where `obj` is defined."""
267+
for frame_info in inspect.stack()[::-1]:
268+
for var in frame_info.frame.f_locals.values():
269+
if obj is var:
270+
return frame_info.frame
271+
raise ValueError(f"No defining frame found for {obj}")
272+
273+
274+
def get_defining_scope(obj, types=None):
275+
"""Get variables in the scope where `expr` is first defined."""
276+
frame = get_defining_frame(obj)
277+
scope = {**frame.f_globals, **frame.f_locals}
278+
if types is not None:
279+
scope = {k: v for k, v in scope.items() if isinstance(v, types)}
280+
return scope

ibis/expr/format.py

Lines changed: 8 additions & 35 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,6 @@
11
from __future__ import annotations
22

33
import functools
4-
import inspect
54
import itertools
65
import textwrap
76
import types
@@ -13,8 +12,8 @@
1312
import ibis
1413
import ibis.expr.datatypes as dt
1514
import ibis.expr.operations as ops
16-
import ibis.expr.types as ir
1715
from ibis import util
16+
from ibis.common.graph import Node
1817

1918
_infix_ops = {
2019
# comparison operations
@@ -147,57 +146,31 @@ def inline_args(fields, prefer_positional=False):
147146
return ", ".join(f"{k}={v}" for k, v in fields.items())
148147

149148

150-
def get_defining_frame(expr):
151-
"""Locate the outermost frame where `expr` is defined."""
152-
for frame_info in inspect.stack()[::-1]:
153-
for var in frame_info.frame.f_locals.values():
154-
if isinstance(var, ir.Expr) and expr.equals(var):
155-
return frame_info.frame
156-
raise ValueError(f"No defining frame found for {expr}")
157-
158-
159-
def get_defining_scope(expr):
160-
"""Get variables in the scope where `expr` is first defined."""
161-
frame = get_defining_frame(expr)
162-
scope = {**frame.f_globals, **frame.f_locals}
163-
return {k: v for k, v in scope.items() if isinstance(v, ir.Expr)}
164-
165-
166149
class Rendered(str):
167150
def __repr__(self):
168151
return self
169152

170153

171154
@public
172-
def pretty(expr: ops.Node | ir.Expr, scope: Optional[dict[str, ir.Expr]] = None) -> str:
155+
def pretty(node: Node, scope: Optional[dict[str, Node]] = None) -> str:
173156
"""Pretty print an expression.
174157
175158
Parameters
176159
----------
177-
expr
178-
The expression to pretty print.
160+
node
161+
The graph node to pretty print.
179162
scope
180163
A dictionary of expression to name mappings used to intermediate
181-
assignments.
182-
If not provided the names of the expressions will either be
183-
- the variable name in the defining scope if
184-
`ibis.options.repr.show_variables` is enabled
185-
- generated names like `r0`, `r1`, etc. otherwise
164+
assignments. If not provided aliases will be generated for each
165+
relation.
186166
187167
Returns
188168
-------
189169
str
190170
A pretty printed representation of the expression.
191171
"""
192-
if isinstance(expr, ir.Expr):
193-
node = expr.op()
194-
elif isinstance(expr, ops.Node):
195-
node = expr
196-
else:
197-
raise TypeError(f"Expected an expression or a node, got {type(expr)}")
198-
199-
if scope is None and ibis.options.repr.show_variables:
200-
scope = get_defining_scope(expr)
172+
if not isinstance(node, Node):
173+
raise TypeError(f"Expected a graph node, got {type(node)}")
201174

202175
refs = {}
203176
refcnt = itertools.count()
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+
MyNode
5+
obj:
6+
r0.a
7+
children:
8+
r0.a
9+
r0.a + 1

ibis/expr/tests/test_format.py

Lines changed: 24 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,7 @@
1010
import ibis.expr.operations as ops
1111
import ibis.legacy.udf.vectorized as udf
1212
from ibis import util
13+
from ibis.common.graph import Node as Traversable
1314
from ibis.expr.format import fmt, pretty
1415

1516

@@ -465,3 +466,26 @@ class ValueList(ops.Node):
465466
result = pretty(vl)
466467

467468
snapshot.assert_match(result, "repr.txt")
469+
470+
471+
def test_arbitrary_traversables_are_supported(snapshot):
472+
class MyNode(Traversable):
473+
__slots__ = ("obj", "children")
474+
__argnames__ = ("obj", "children")
475+
476+
def __init__(self, obj, children):
477+
self.obj = obj.op()
478+
self.children = tuple(child.op() for child in children)
479+
480+
@property
481+
def __args__(self):
482+
return self.obj, self.children
483+
484+
def __hash__(self):
485+
return hash((self.__class__, self.obj, self.children))
486+
487+
t = ibis.table([("a", "int64")], name="t")
488+
node = MyNode(t.a, [t.a, t.a + 1])
489+
result = pretty(node)
490+
491+
snapshot.assert_match(result, "repr.txt")

ibis/expr/types/core.py

Lines changed: 7 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -16,8 +16,10 @@
1616
from ibis.common.exceptions import IbisError, TranslationError
1717
from ibis.common.grounds import Immutable
1818
from ibis.common.patterns import Coercible, CoercionError
19+
from ibis.common.typing import get_defining_scope
1920
from ibis.config import _default_backend
2021
from ibis.config import options as opts
22+
from ibis.expr.format import pretty
2123
from ibis.expr.types.pretty import to_rich
2224
from ibis.util import experimental
2325

@@ -44,7 +46,6 @@ def _repr_mimebundle_(self, *args, **kwargs):
4446
return bundle
4547

4648

47-
# TODO(kszucs): consider to subclass from Annotable with a single _arg field
4849
@public
4950
class Expr(Immutable, Coercible):
5051
"""Base expression class."""
@@ -53,9 +54,11 @@ class Expr(Immutable, Coercible):
5354
_arg: ops.Node
5455

5556
def _noninteractive_repr(self) -> str:
56-
from ibis.expr.format import pretty
57-
58-
return pretty(self)
57+
if ibis.options.repr.show_variables:
58+
scope = get_defining_scope(self, types=Expr)
59+
else:
60+
scope = None
61+
return pretty(self.op(), scope=scope)
5962

6063
def _interactive_repr(self) -> str:
6164
console = Console(force_terminal=False)

0 commit comments

Comments
 (0)