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 18 years, 11 months ago  # | flag

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

Christopher Dunn 18 years, 11 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 18 years, 11 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 18 years, 11 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 18 years, 11 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) 18 years, 10 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 15 years, 11 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