ActiveState Code

Recipe 411791: Automatic indentation of output based on frame stack


Output stream wrapper; possibly useful for debugging code with print statements.

When write() is called, it makes a note of the calling frame. The indentation level is equal to the number of frames in the call stack which have been previously noted. See example.

Python
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
import sys

class AutoIndent(object):
    def __init__(self, stream):
        self.stream = stream
        self.offset = 0
        self.frame_cache = {}

    def indent_level(self):
        i = 0
        base = sys._getframe(2)
        f = base.f_back
        while f:
            if id(f) in self.frame_cache:
                i += 1
            f = f.f_back
        if i == 0:
            # clear out the frame cache
            self.frame_cache = {id(base): True}
        else:
            self.frame_cache[id(base)] = True
        return i

    def write(self, stuff):
        indentation = '  ' * self.indent_level()
        def indent(l):
            if l:
                return indentation + l
            else:
                return l
        stuff = '\n'.join([indent(line) for line in stuff.split('\n')])
        self.stream.write(stuff)

--------------------------------------------------------------------------
>>> # Example usage 
>>>
>>> def f(x):
...     print "f(%s)" % x
...     if x == 0:
...         return 0
...     elif x == 1:
...         return 1
...     else:
...         return f(x-1) + f(x-2)
>>>
>>> import sys
>>> sys.stdout = AutoIndent(sys.stdout)
>>>
>>> f(6)
f(6)
  f(5)
    f(4)
      f(3)
        f(2)
          f(1)
          f(0)
        f(1)
      f(2)
        f(1)
        f(0)
    f(3)
      f(2)
        f(1)
        f(0)
      f(1)
  f(4)
    f(3)
      f(2)
        f(1)
        f(0)
      f(1)
    f(2)
      f(1)
      f(0)
8
>>>

Comments

  1. 1. At 3:48 p.m. on 27 apr 2005, George Sakkis said:

    Pretty cool. Neat :) This would be a useful feature to the logging module.

  2. 2. At 3:13 p.m. on 28 apr 2005, Christopher Dunn said:

    Problems using this in a logging handler. I tried

    >>> handler = logging.StreamHandler(AutoIndent(sys.stderr))
    

    and the result was buggy. A function would get one indentation level the first time, and then a deeper level for every subsequent call. (Write me if you want the code.)

    For example,

    def f3():
        Unix.warning('I am f3')
    def f2():
        Unix.warning('I am f2')
        f3()
    def f1():
        Unix.warning('I am f1')
        f2()
    def go():
      Unix.warning('I am go.')
      f1()
      f2()
      f3()
    f1()
    go()
    

    Produced

      [.UnixWARNING]I am f1
      [.UnixWARNING]I am f2
        [.UnixWARNING]I am f3
    [.UnixWARNING]I am go.
      [.UnixWARNING]I am f1
        [.UnixWARNING]I am f2
        [.UnixWARNING]I am f3
    [.UnixWARNING]I am f2
      [.UnixWARNING]I am f3
    [.UnixWARNING]I am f3
    

    I fixed this by using

    def indent_level(self):
        import inspect
        return len(inspect.stack())-9
    

    which produced

    [.UnixWARNING]I am f1
      [.UnixWARNING]I am f2
        [.UnixWARNING]I am f3
    [.UnixWARNING]I am go.
      [.UnixWARNING]I am f1
        [.UnixWARNING]I am f2
          [.UnixWARNING]I am f3
      [.UnixWARNING]I am f2
        [.UnixWARNING]I am f3
      [.UnixWARNING]I am f3
    

    Unfortunately, using your class directly with my change, the output is completely unindented, while your version works perfectly.

    I don't know what's going on, but getting this to work with logging is not trivial.

  3. 3. At 3:22 p.m. on 28 apr 2005, Christopher Dunn said:

    My version works consistently. I had subtracted too much. Normally, the stack is not 9 deep! This works really well in general:

    def indent_level(self):
        import inspect
        return len(inspect.stack())-3
    

    Your version is definitely buggy when used inside a logging.Handler(), and that is a pretty big problem.

  4. 4. At 3:34 p.m. on 28 apr 2005, Christopher Dunn said:

    Better way to indent logging output. Another (better?) way is:

    from logging import Formatter
    class IndentFormatter(Formatter):
        def __init__( self, fmt=None, datefmt=None ):
            Formatter.__init__(self, fmt, datefmt)
        def format( self, rec ):
            import inspect
            rec.indent = ' '*(len(inspect.stack())-8)
            out = Formatter.format(self, rec)
            del rec.indent
            return out
    

    With %(indent)s in the format string, this produces

    [.UnixWARNING] I am Unix
    [.UnixWARNING]  I am f1
    [.UnixWARNING]   I am f2
    [.UnixWARNING]    I am f3
    [.UnixWARNING]  I am go.
    [.UnixWARNING]   I am f1
    [.UnixWARNING]    I am f2
    [.UnixWARNING]     I am f3
    [.UnixWARNING]   I am f2
    [.UnixWARNING]    I am f3
    [.UnixWARNING]   I am f3
    

    But then a regular formatter would raise an exception if given %(indent)s in the format string. So I'm not sure of the best way.

  5. 5. At 3:41 p.m. on 28 apr 2005, Christopher Dunn said:

    How do we discover the baseline stack depth? There has to be a way to find the baseline indentation level, rather than hard-coding it. For example, in ipython the stack is 8 deep in the shell alone!

    Aside from that, this solution is pretty good. I am going to try to get the function name from the call stack too, and stick that into a generally useful logging.Formatter, which might be a good cookbook entry by itself.

  6. 6. At 10:20 p.m. on 17 may 2005, Lonnie Princehouse (the author) said:

    base frame depth. I hadn't tried it in conjunction with the logging module... It may be that adding one to the argument to sys._getframe will solve the problem, but I'm not sure. I will look into it.

    As for the base stack depth-- the way I tried to avoid that was by only counting frames which write() had been called from, instead of counting all frames. Otherwise the indentation gets insane.

  7. 7. At 5:02 p.m. on 1 may 2008, Tony Maher said:

    Just what I wanted but I simplified slightly.

    import inspect
    class AutoIndent(object):
        '''Indent debug output based on function call depth.'''
    
        def __init__(self, stream, depth=len(inspect.stack())):
            '''
            stream is something like sys.stdout.
            depth is to compensate for stack depth.
            The default is to get current stack depth when class created.
    
            '''
            self.stream = stream
            self.depth = depth
    
        def indent_level(self):
            return len(inspect.stack()) - self.depth
    
        def write(self, data):
            indentation = '  ' * self.indent_level()
            def indent(l):
                if l:
                    return indentation + l
                else:
                    return l
            data = '\n'.join([indent(line) for line in data.split('\n')])
            self.stream.write(data)
    

Sign in to comment