Welcome, guest | Sign In | My Account | Store | Cart

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, 76 lines
 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
>>>

7 comments

George Sakkis 16 years, 7 months ago  # | flag

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

Christopher Dunn 16 years, 7 months ago  # | flag

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.

Christopher Dunn 16 years, 7 months ago  # | flag

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.

Christopher Dunn 16 years, 7 months ago  # | flag

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.

Christopher Dunn 16 years, 7 months ago  # | flag

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.

Lonnie Princehouse (author) 16 years, 6 months ago  # | flag

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.

Tony Maher 13 years, 7 months ago  # | flag

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)
Created by Lonnie Princehouse on Tue, 26 Apr 2005 (PSF)
Python recipes (4591)
Lonnie Princehouse's recipes (5)

Required Modules

  • (none specified)

Other Information and Tasks