152

From something like this:

print(get_indentation_level())

    print(get_indentation_level())

        print(get_indentation_level())

I would like to get something like this:

1
2
3

Can the code read itself in this way?

All I want is the output from the more nested parts of the code to be more nested. In the same way that this makes code easier to read, it would make the output easier to read.

Of course I could implement this manually, using e.g. .format(), but what I had in mind was a custom print function which would print(i*' ' + string) where i is the indentation level. This would be a quick way to make readable output on my terminal.

Is there a better way to do this which avoids painstaking manual formatting?

Craig Burgler
  • 1,749
  • 10
  • 19
Fab von Bellingshausen
  • 1,387
  • 1
  • 8
  • 16
  • 2
    you could have another file read your python file and print the indentation level – depperm Aug 26 '16 at 18:10
  • I don't think that's possible – Aaron Aug 26 '16 at 18:11
  • http://stackoverflow.com/a/25026120/7432 – Bryan Oakley Aug 26 '16 at 18:11
  • 1
    This is likely a duplicate of http://stackoverflow.com/q/6810999/7432. – Bryan Oakley Aug 26 '16 at 18:11
  • 72
    I'm really curious as to why you need this. – Harrison Aug 26 '16 at 18:12
  • @BryanOakley I stand corrected... – Aaron Aug 26 '16 at 18:13
  • 1
    @BryanOakley That appears to be about line number and not scope indention level? – jacob Aug 26 '16 at 18:27
  • 13
    @Harrison I wanted to indent the output of my code according to how it was indented in the code. – Fab von Bellingshausen Aug 26 '16 at 18:28
  • 1
    @jacob: true, but one of the answers shows how to get the line number, and from the line number you can read the file to get the indentation. Or, you can examine the stack to get the information. But that's why I didn't explicitly vote to close, since it wasn't an exact duplicate. It's basically asking the same thing though: "how do I get information about the current file/class/function/line". – Bryan Oakley Aug 26 '16 at 18:39
  • 14
    The real question is: Why would you need this? The indentation level is static; you know it with certainty when you put the `get_indentation_level()` statment into your code. You can just as well do `print(3)` or whatever directly. What might be more itneresting is the current level of nesting on the function call stack. – tobias_k Aug 26 '16 at 20:17
  • 19
    Is it for the purpose of debugging your code? This seems either a super genius way of logging the flow of execution or like a super over-engineered solution for a simple problem, and I'm not sure which it is... maybe both! – Blackhawk Aug 26 '16 at 21:26
  • 4
    "I wanted to indent the output of my code according to how it was indented in the code." Coupling the output of the program to the formatting of the code violates the [principle of least astonishment](https://en.wikipedia.org/wiki/Principle_of_least_astonishment). Imagine the surprise of whomever has to maintain this code in the future when their innocuous formatting changes in the code alters the output unexpectedly... – Michael Fredrickson Aug 26 '16 at 22:07
  • 5
    @Harrison @tobias_k @Blackhawk @user2357112 All I want is the output from the more nested parts of the code to be more nested. In the same way that this makes code easier to read, it would make the output easier to read. Of course I could implement this manually, using e.g. `.format()`, but what I had in mind was a custom `print` function which would `print(i*' ' + string)` where `i` is the indentation level. This would be a quick way to make readable output on my terminal. Is there a better way to do this which avoids painstaking manual formatting? – Fab von Bellingshausen Aug 26 '16 at 22:43
  • 8
    @FabvonBellingshausen: That sounds like it'd be a lot less readable than you're hoping. I think you might be better served by explicitly passing around a `depth` parameter and adding the appropriate value to it as necessary when you pass it to other functions. The nesting of your code isn't likely to correspond cleanly to the indentation you want out of your output. – user2357112 Aug 26 '16 at 23:07
  • 5
    You're likely to run into a lot of situations where you don't want some `if` or `try` to increase the output indentation, or where you want to refactor some bit of logic into its own function without affecting your output. In fact, you've already run into such problems, such as when you found that pure indentation level didn't match what you wanted in the presence of function calls. – user2357112 Aug 26 '16 at 23:11
  • 6
    There's a technical term for this kind of problems: http://xyproblem.info/ – Arturo Torres Sánchez Aug 26 '16 at 23:49
  • 2
    This code will never compile: unexpected indent. – user253751 Aug 27 '16 at 11:21
  • @immibis There are clearly implied "..." between those lines of code, where other stuff is. – GreenAsJade Aug 27 '16 at 12:01
  • Won't your code exit with an `IndentationError`? Also, why do you care for nesting level? *I wanted to indent the output of my code according to how it was indented in the code.* Why don't you just use something like `\s\sprint(2)`. The indentation is not variable in code, but it can be when using `exec`. For that, just use a variable denoting the number of spaces, and then do something like `' '*v+'print(%d)'%v`, although I don't think you would ever need such a thing. – EKons Aug 27 '16 at 20:47
  • 2
    @tobias_k "The indentation level is static; you know it with certainty when you put the get_indentation_level() statment into your code. " That may be true, but now add an if statement somewhere and indent one level. How many lines do you need to change? – Thomas Weller Aug 28 '16 at 19:10

5 Answers5

115

If you want indentation in terms of nesting level rather than spaces and tabs, things get tricky. For example, in the following code:

if True:
    print(
get_nesting_level())

the call to get_nesting_level is actually nested one level deep, despite the fact that there is no leading whitespace on the line of the get_nesting_level call. Meanwhile, in the following code:

print(1,
      2,
      get_nesting_level())

the call to get_nesting_level is nested zero levels deep, despite the presence of leading whitespace on its line.

In the following code:

if True:
  if True:
    print(get_nesting_level())

if True:
    print(get_nesting_level())

the two calls to get_nesting_level are at different nesting levels, despite the fact that the leading whitespace is identical.

In the following code:

if True: print(get_nesting_level())

is that nested zero levels, or one? In terms of INDENT and DEDENT tokens in the formal grammar, it's zero levels deep, but you might not feel the same way.


If you want to do this, you're going to have to tokenize the whole file up to the point of the call and count INDENT and DEDENT tokens. The tokenize module would be very useful for such a function:

import inspect
import tokenize

def get_nesting_level():
    caller_frame = inspect.currentframe().f_back
    filename, caller_lineno, _, _, _ = inspect.getframeinfo(caller_frame)
    with open(filename) as f:
        indentation_level = 0
        for token_record in tokenize.generate_tokens(f.readline):
            token_type, _, (token_lineno, _), _, _ = token_record
            if token_lineno > caller_lineno:
                break
            elif token_type == tokenize.INDENT:
                indentation_level += 1
            elif token_type == tokenize.DEDENT:
                indentation_level -= 1
        return indentation_level
user2357112
  • 260,549
  • 28
  • 431
  • 505
  • 1
    I tested this for simple examples and it seems to work. – Fab von Bellingshausen Aug 26 '16 at 20:47
  • 2
    This doesn't work in the way I would expect when `get_nesting_level()` is called within that function call―it returns the nesting level within that function. Could it be rewritten to return the 'global' nesting level? – Fab von Bellingshausen Aug 26 '16 at 22:08
  • 11
    @FabvonBellingshausen: You may be getting indentation nesting level and function call nesting level mixed up. This function gives indentation nesting level. Function call nesting level would be rather different, and would give a level of 0 for all of my examples. If you want a sort of indentation/call nesting level hybrid that increments for both function calls and control flow structures like `while` and `with`, that'd be doable, but it's not what you asked for, and changing the question to ask something different at this point would be a bad idea. – user2357112 Aug 26 '16 at 22:13
  • 39
    Incidentally, I'm in complete agreement with all the people saying this is a really weird thing to do. There's probably a much better way to solve whatever problem you're trying to solve, and relying on this is likely to hamstring you by forcing you to use all kinds of nasty hacks to avoid changing your indentation or function call structure when you need to make changes to your code. – user2357112 Aug 26 '16 at 22:21
  • 4
    i certainly did not expect someone to have actually answered this. (consider the `linecache` module for stuff like this though — it's used to print tracebacks, and can handle modules imported from zip files and other weird import tricks) – Eevee Aug 27 '16 at 15:47
  • 7
    @Eevee: I certainly wasn't expecting so many people to *upvote* this! `linecache` might be good for reducing the amount of file I/O (and thanks for reminding me about it), but if I started optimizing that, I'd be bothered by how we're redundantly re-tokenizing the same file for repetitions of the same call, or for multiple call sites within the same file. There are a number of ways we could optimize that too, but I'm not sure how much I really want to tune and bulletproof this crazy thing. – user2357112 Aug 27 '16 at 18:51
23

Yeah, that's definitely possible, here's a working example:

import inspect

def get_indentation_level():
    callerframerecord = inspect.stack()[1]
    frame = callerframerecord[0]
    info = inspect.getframeinfo(frame)
    cc = info.code_context[0]
    return len(cc) - len(cc.lstrip())

if 1:
    print get_indentation_level()
    if 1:
        print get_indentation_level()
        if 1:
            print get_indentation_level()
Giovanni Funchal
  • 8,934
  • 13
  • 61
  • 110
BPL
  • 9,632
  • 9
  • 59
  • 117
  • 4
    Relating to the comment made by @Prune, can this be made to return the indentation in levels instead of spaces? Will it always be ok to simply divide by 4? – Fab von Bellingshausen Aug 26 '16 at 18:30
  • 2
    No, divide by 4 to get indent level will not work with this code. Can verify by increasing level of indent of last print statement, last printed value is just increased. – Craig Burgler Aug 26 '16 at 18:32
  • 10
    A good start, but doesn't really answer the question imo. The number of spaces is not the same as the indentation level. – wim Aug 26 '16 at 19:00
  • @wim Only one space is required per indent. Simply replace every four spaces with a single space. Or even better, use tabs. Note that SO automatically replaces tabs with spaces in the process of formatting code. – mbomb007 Aug 26 '16 at 19:26
  • 1
    It's not that simple. Replacing 4 spaces with single spaces can change the logic of the code. – wim Aug 26 '16 at 19:29
  • 1
    But this code is perfect for what the OP was looking for: (OP comment #9): 'I wanted to indent the output of my code according to how it was indented in the code.' So he can do something like `print('{Space}'*get_indentation_level(), x)` – Craig Burgler Aug 26 '16 at 21:28
  • or `print(' '*get_indentation_level(), x, sep = '')` – Craig Burgler Aug 26 '16 at 21:47
10

You can use sys.current_frame.f_lineno in order to get the line number. Then in order to find the number of indentation level you need to find the previous line with zero indentation then be subtracting the current line number from that line's number you'll get the number of indentation:

import sys
current_frame = sys._getframe(0)

def get_ind_num():
    with open(__file__) as f:
        lines = f.readlines()
    current_line_no = current_frame.f_lineno
    to_current = lines[:current_line_no]
    previous_zoro_ind = len(to_current) - next(i for i, line in enumerate(to_current[::-1]) if not line[0].isspace())
    return current_line_no - previous_zoro_ind

Demo:

if True:
    print get_ind_num()
    if True:
        print(get_ind_num())
        if True:
            print(get_ind_num())
            if True: print(get_ind_num())
# Output
1
3
5
6

If you want the number of the indentation level based on the previouse lines with : you can just do it with a little change:

def get_ind_num():
    with open(__file__) as f:
        lines = f.readlines()

    current_line_no = current_frame.f_lineno
    to_current = lines[:current_line_no]
    previous_zoro_ind = len(to_current) - next(i for i, line in enumerate(to_current[::-1]) if not line[0].isspace())
    return sum(1 for line in lines[previous_zoro_ind-1:current_line_no] if line.strip().endswith(':'))

Demo:

if True:
    print get_ind_num()
    if True:
        print(get_ind_num())
        if True:
            print(get_ind_num())
            if True: print(get_ind_num())
# Output
1
2
3
3

And as an alternative answer here is a function for getting the number of indentation (whitespace):

import sys
from itertools import takewhile
current_frame = sys._getframe(0)

def get_ind_num():
    with open(__file__) as f:
        lines = f.readlines()
    return sum(1 for _ in takewhile(str.isspace, lines[current_frame.f_lineno - 1]))
Mazdak
  • 105,000
  • 18
  • 159
  • 188
  • question asked for number of indentation levels, not number of spaces. they are not necessarily proportional. – wim Aug 26 '16 at 19:03
  • For your demo code the output should be 1 - 2 - 3 - 3 – Craig Burgler Aug 26 '16 at 20:14
  • @CraigBurgler For getting 1 - 2 - 3 - 3 we can count the number of lines before the current line that are ends with a `:` until we encounter the line with zero indentation, Check out the edit! – Mazdak Aug 26 '16 at 21:01
  • 2
    hmmm ... ok ... now try some of @user2357112's test cases ;) – Craig Burgler Aug 26 '16 at 21:24
  • @CraigBurgler This solution is just for most cases, And regarding that answer, also it's a general way, it doesn't give a comprehensive solution too. Try `{3:4, \n 2:get_ind_num()}` – Mazdak Aug 26 '16 at 21:35
7

To solve the ”real” problem that lead to your question you could implement a contextmanager which keeps track of the indention level and make the with block structure in the code correspond to the indentation levels of the output. This way the code indentation still reflects the output indentation without coupling both too much. It is still possible to refactor the code into different functions and have other indentations based on code structure not messing with the output indentation.

#!/usr/bin/env python
# coding: utf8
from __future__ import absolute_import, division, print_function


class IndentedPrinter(object):

    def __init__(self, level=0, indent_with='  '):
        self.level = level
        self.indent_with = indent_with

    def __enter__(self):
        self.level += 1
        return self

    def __exit__(self, *_args):
        self.level -= 1

    def print(self, arg='', *args, **kwargs):
        print(self.indent_with * self.level + str(arg), *args, **kwargs)


def main():
    indented = IndentedPrinter()
    indented.print(indented.level)
    with indented:
        indented.print(indented.level)
        with indented:
            indented.print('Hallo', indented.level)
            with indented:
                indented.print(indented.level)
            indented.print('and back one level', indented.level)


if __name__ == '__main__':
    main()

Output:

0
  1
    Hallo 2
      3
    and back one level 2
BlackJack
  • 4,476
  • 1
  • 20
  • 25
6
>>> import inspect
>>> help(inspect.indentsize)
Help on function indentsize in module inspect:

indentsize(line)
    Return the indent size, in spaces, at the start of a line of text.
Craig Burgler
  • 1,749
  • 10
  • 19
  • 12
    This gives the indentation in spaces, not in levels. Unless the programmer uses consistent indentation amounts, this might be ugly to convert to levels. – Prune Aug 26 '16 at 18:25
  • 4
    Is the function undocumented? I cannot find it [here](https://docs.python.org/3/library/inspect.html) – GingerPlusPlus Aug 27 '16 at 14:28