d3pd
d3pd

Reputation: 8315

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()

Upvotes: 1

Views: 3758

Answers (1)

Peter Goldsborough
Peter Goldsborough

Reputation: 1388

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.

Upvotes: 5

Related Questions