Search code examples
pythonword-wrap

How to have multirow cells in Python table?


I'm trying to make a simple Python table class that accepts a list of lists as content and then builds a table string representation that can be printed to the terminal. A feature I want is wrapping of text in cells of the table.

I am happy to use the module textwrap in order to determine the appropriate text wrapping.

Basically, for the following content

[
    ["heading 1", "heading 2"],
    ["some text", "some more text"],
    ["lots and lots and lots and lots and lots of text", "some more text"]
]

I want a generated representation something like the following:

-------------------------------
|heading 1     |heading 2     |
-------------------------------
|some text     |some more text|
-------------------------------
|lots and lots |some more text|
|and lots and  |              |
|lots and lots |              |
|of text       |              |
-------------------------------

My question is: How can I implement the multiline cells, given the list representation of the text wrapping determined by textwrap?

The code I have is as follows:

import textwrap
import subprocess

def terminalWidth():
    return(
        int(
            subprocess.Popen(
                ["tput", "cols"],
                stdout = subprocess.PIPE
            ).communicate()[0].decode("utf-8").strip("\n")
        )
    )

class Table(object):

    def __init__(
        self,
        content         = None,
        widthTable      = None,
        columnDelimiter = "|",
        rowDelimiter    = "-"
        ):
        self.content    = content
        if widthTable is None:
            self.widthTable = terminalWidth()
        self.columnDelimiter = columnDelimiter
        self.rowDelimiter = rowDelimiter

    def value(self):
        self.numberOfColumns = len(self.content[0])
        self.widthOfColumns =\
            self.widthTable / self.numberOfColumns -\
            self.numberOfColumns * len(self.columnDelimiter)
        self.tableString = ""
        for row in self.content:
            for column in row:
                self.tableString =\
                    self.tableString +\
                    self.columnDelimiter +\
                    textwrap.wrap(column, self.widthOfColumns)
            self.tableString =\
                self.tableString +\
                self.columnDelimiter +\
                "\n" +\
                self.widthTable * self.rowDelimiter +\
                "\n" +\
        return(self.tableString)

    def __str__(self):
        return(self.value())

def main():

    table1Content = [
        ["heading 1", "heading 2"],
        ["some text", "some more text"],
        ["lots and lots and lots and lots and lots of text", "some more text"]
    ]

    table1 = Table(
        content    = table1Content,
        widthTable = 15
    )

    print(table1)

if __name__ == '__main__':
    main()

Solution

  • Here's a class that does what you want:

    import textwrap
    
    class Table:
    
        def __init__(self,
                     contents,
                     wrap,
                     wrapAtWordEnd = True,
                     colDelim = "|",
                     rowDelim = "-"):
    
            self.contents = contents
            self.wrap = wrap
            self.colDelim = colDelim
            self.wrapAtWordEnd = wrapAtWordEnd
    
            # Extra rowDelim characters where colDelim characters are
            p = len(self.colDelim) * (len(self.contents[0]) - 1)
    
            # Line gets too long for one concatenation
            self.rowDelim = self.colDelim
            self.rowDelim += rowDelim * (self.wrap * max([len(i) for i in self.contents]) + p)
            self.rowDelim += self.colDelim + "\n"
    
        def withoutTextWrap(self):
    
            string = self.rowDelim
    
            for row in self.contents:
                maxWrap = (max([len(i) for i in row]) // self.wrap) + 1
                for r in range(maxWrap):
                    string += self.colDelim
                    for column in row:
                        start = r * self.wrap
                        end = (r + 1) * self.wrap 
                        string += column[start : end].ljust(self.wrap)
                        string += self.colDelim
                    string += "\n"
                string += self.rowDelim
    
            return string
    
        def withTextWrap(self):
    
            print(self.wrap)
    
            string = self.rowDelim
    
            # Restructure to get textwrap.wrap output for each cell
            l = [[textwrap.wrap(col, self.wrap) for col in row] for row in self.contents]
    
            for row in l:
                for n in range(max([len(i) for i in row])):
                    string += self.colDelim
                    for col in row:
                        if n < len(col):
                            string += col[n].ljust(self.wrap)
                        else:
                            string += " " * self.wrap
                        string += self.colDelim
                    string += "\n"
                string += self.rowDelim
    
            return string
    
        def __str__(self):
    
            if self.wrapAtWordEnd:
    
                return self.withTextWrap() 
    
            else:
    
                return self.withoutTextWrap()
    
    if __name__ == "__main__":
    
        l = [["heading 1", "heading 2", "asdf"],
             ["some text", "some more text", "Lorem ipsum dolor sit amet."],
             ["lots and lots and lots and lots and lots of text", "some more text", "foo"]]
    
        table = Table(l, 20, True)
    
        print(table)
    

    withTextWrap() uses the textwrap module you mention, and makes use of its output to build a table representation. While playing around with this, I also came up with a way of doing what you want to do (almost), without the textwrap module, which you can see in the withoutTextWrap() method. I say "almost" because the textwrap module breaks lines properly at the end of a word, while my method breaks the strings directly at the wrap point.

    So if you create the table with the third constructor argument set to True, the textwrap module is used, which produces this output:

    |--------------------------------------------------------------|
    |heading 1           |heading 2           |asdf                |
    |--------------------------------------------------------------|
    |some text           |some more text      |Lorem ipsum dolor   |
    |                    |                    |sit amet.           |
    |--------------------------------------------------------------|
    |lots and lots and   |some more text      |foo                 |
    |lots and lots and   |                    |                    |
    |lots of text        |                    |                    |
    |--------------------------------------------------------------|
    

    And if that argument is False, the non-textwrap version is called:

    |--------------------------------------------------------------|
    |heading 1           |heading 2           |asdf                |
    |--------------------------------------------------------------|
    |some text           |some more text      |Lorem ipsum dolor si|
    |                    |                    |t amet.             |
    |--------------------------------------------------------------|
    |lots and lots and lo|some more text      |foo                 |
    |ts and lots and lots|                    |                    |
    | of text            |                    |                    |
    |--------------------------------------------------------------|
    

    Hope this helps.