2

Is it possible to determine the level of indentation of a line in Python while the program is running? I want to be able to organize a log file according to an outline structure of the script that is being run.

In the following example, the 'first message' function would yield 0, 'second message' would be 1, 'third message' 2 and 'fourth, fifth, sixth' message would be 0

logger.debug('First message')
if True:
    logger.info('Second message')
    if True:
        logger.info('Third message')


logger.warning('Fourth message')
logger.error('Fifth message')
logger.critical('Sixth message')

The corresponding log file would look something like this:

First message
    Second message
        Third message
Fourth message
Fifth message
Sixth message
2
  • You could look at the enclosing frame: stackoverflow.com/q/5326539/3001761, but this seems like a lot of work for little benefit. Commented Jul 13, 2016 at 13:19
  • 1
    @jonrsharpe - The reason behind asking for this is to have an outline of a test case so that one can see a skeleton of why a failure occurred without having to dig through the test source code. The different indentation levels makes it a bit easier to read. Commented Jul 13, 2016 at 13:28

2 Answers 2

1

I was able to determine the indentation level using the inspect.getouterframes() function. This assumes that 4 ' ' characters are used instead of '\t' characters for indentation.

import inspect

def getIndentationLevel():    

    # get information about the previous stack frame
    frame, filename, line_number,function_name, lines, index = inspect.getouterframes(inspect.currentframe())[1]

    # count the number of characters in the line
    original_length = len(lines[0])

    # trim the line of left spaces and record the new length
    new_length = len(lines[0].lstrip())

    # take the difference between the two values and divide by 4 to get the level number
    return int((original_length - new_length) / 4)
Sign up to request clarification or add additional context in comments.

Comments

0

First, I retrieve the code context of the caller:

import inspect

context = inspect.getframeinfo(frame.f_back).code_context 

This gives me a list of code lines; I ignore all but the first of those lines. Then I use a regular expression to get the whitespace at the start of this line; if you are using tabs, you have to replace them with the appropriate amount of spaces first:

import re

indent = re.compile("^ *")

firstline = context[0]
firstline = firstline.replace("\t", "    ")
match = indent.match(firstline)

For testing purposes, I replace the spaces with dots, so I can see what's going on, and construct my prefix:

white = "." * match.span(0)[1] # Change "." to " "!

Now I can use this to modify my message msg I sent to my logger:

do_some_logging(white + msg)

It would be nice if I could simply wrap an existing logger with a decorator that turns this logger into a whitespace-aware logger. Suppose I have, for testing purposes, a really primitive logger that simply prints messages to stdout:

def pseudo_logger(msg):
    """Pseudo-logging function."""
    print(msg)

This logger has a first parameter for the logging message, and potentially some other positional parameters and keywords. I want to write a decorater for such a function:

from functools import wraps

def indented(func):
    """Turns the logger func(msg) into an indented logger."""
    @wraps(func)
    def wrapped(msg, *args, **kwargs):
        # ... compute white ...
        func(white + msg, *args, **kwargs)
    return wrapped

Now I can get a new logger as, for example:

new_logger = intented(pseudo_logger)

Putting it all together gives:

from functools import wraps
import inspect
import re


def indented(func):
    """Turns the logger func(msg) into an indented logger."""
    indent = re.compile("^ *")    
    @wraps(func)
    def wrapped(msg, *args, **kwargs):
        frame = inspect.currentframe()
        context = inspect.getframeinfo(frame.f_back).code_context
        firstline = context[0]
        match = indent.match(firstline)
        white = "." * match.span(0)[1] # Change "." to " "!
        func(white + msg, *args, **kwargs)
    return wrapped

@indented
def pseudo_logger(msg):
    """Pseudo-logging function."""
    print(msg)

def main():
    pseudo_logger("This is an indented message!")
    if True:
        pseudo_logger("Another message, but more indented!")

    pseudo_logger("This "
                  "will ignore the indention of the second "
                  "or third line.")

if __name__ == "__main__":
    main()

I would be hesitant to use this in production code, though. Using code inspection like this is brittle, and depending on where you call this, it might lead to unexpected effects.

2 Comments

Can you provide some context/explanation of your code example?
Sure, I added some commentary.

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service and acknowledge you have read our privacy policy.

Start asking to get answers

Find the answer to your question by asking.

Ask question

Explore related questions

See similar questions with these tags.