Python Forum
Thread Rating:
  • 1 Vote(s) - 4 Average
  • 1
  • 2
  • 3
  • 4
  • 5
columnize.py
#1
here is a "first working" case of my class to tightly columnize output.  as a command/program it will columnize STDIN to STDOUT.  this is just a first time running.  more work to do. posted before completion to let you see what i have been wasting my time on.

#!/usr/bin/env python3
# -*- coding: utf-8 -*-
from __future__ import division, print_function, unicode_literals
"""
file          columnize.py
purpose       class to convert a long stream of short lines to multiple
              columns in one or more pages passed to caller or printed
              to a given file, where columns are aligned vertically and
              can vary in width and number of columns per page (more can
              fit when they are narrower)
email         10054452614123394844460370234029112340408691

The intent is that this command works correctly under both Python 2 and
Python 3.  Please report failures or code improvement to the author.
"""

__license__ = """
Copyright © 2017, by Phil D. Howard - all other rights reserved

Permission to use, copy, modify, and/or distribute this software for any
purpose with or without fee is hereby granted, provided that the above
copyright notice and this permission notice appear in all copies.

THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES
WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF
MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR
ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES
WHATSOEVER RESULTING FROM LOSS OF USE, DATA, OR PROFITS, WHETHER IN AN
ACTION OF CONTRACT, NEGLIGENCE, OR OTHER TORTIOUS ACTION, ARISING OUT OF
OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE.

The author may be contacted by decoding the number
10054452614123394844460370234029112340408691
(provu igi la numeron al duuma)
"""

import fcntl, os, struct, sys, termios

import printobject
print_object = printobject.print_object

if sys.version_info.major<3:
   ints = (int,long,)
   strs = (str,unicode,)
   def sorteditems(d):
       return sorted(d.iteritems())
   BrokenPipeError = IOError
else:
   ints = (int,)
   strs = (str,bytes,bytearray,)
   def sorteditems(d):
       return sorted(d.items())

def filemap(f,m):
   """map various values to various file types"""
   if f is None:
       return None
   elif f is False:
       return sys.stderr
   elif f is True:
       return sys.stdout
   elif isinstance(f,int):
       return os.fdopen(f,m)
   if sys.version_info.major < 3:
       if isinstance(f,(str,unicode)):
           return open(f,m)
   else:
       if isinstance(f,str):
           return open(f,m)
   if isinstance(f,list):
       return [filemap(a,m) for a in f]
   elif isinstance(f,tuple):
       return tuple([filemap(a,m) for a in f])
   elif isinstance(f,set):
       return set([filemap(a,m) for a in f])
   elif isinstance(f,dict):
       if sys.version_info.major < 3:
           return {k:filemap(v,m) for k,v in f.iteritems()}
       else:
           return {k:filemap(v,m) for k,v in f.items()}
   else:
       return f

class colpage:
   """class to columnize a sequence of lines into columns and pages"""

   def __init__(self,*args,**opts):
       """Customize a colpage instance."""
       self._init()
       opts = self.set_opts(**opts)
       msg = ''
       if len(args)>0:
           for n,a in enumerate(args):
               print('unused argument',n,'=',repr(a),file=sys.stderr)
           msg += str(len(args))+' unused argument'+\
                  ('' if len(args)<2 else 's')
       if len(opts)>0:
           for k,v in sorteditems(opts):
               print('unknown option',repr(k)+'='+repr(v),file=sys.stderr)
           if len(msg) > 0:
               msg += ' and '
           msg += str(len(opts))+' unknown option'+\
                  ('' if len(opts)<2 else 's')
       if len(msg) > 0:
           raise ValueError(msg)
       return

   def _init(self):
       """Initialize variables to initial values."""
       self._columns = []
       self._closed  = False
       self._pages   = []
       self.bottom   = None
       self.call     = None
       self.file     = sys.stdout
       self.gutter   = 1
       self.height1  = 25
       self.heightx  = 24
       self.left     = None
       self.prefix1  = ''
       self.prefixx  = ''
       self.right    = None
       self.sep      = ' '
       self.suffix1  = ''
       self.suffixx  = ''
       self.top      = None
       self.width    = 72
       return

   def _is_enough_for_a_page(self):
       """Return True if enough data is available to make a full page."""
       width = - self.gutter
       for col in self._columns:
           width += self.gutter
           width += col[0]
           if width >= self.width:
               return True
       return False

   def _process_page(self):
       """Make a page and output it, popping columns used to make it."""
       # collect the columns to make the page with
       cols = []
       width = - self.gutter
       while self._columns:
           width += self.gutter
           width += self._columns[0][0]
           if width >= self.width:
               break
           cols.append(self._columns.pop(0))
       # form the collected columns into a page
       page = []
       sep = (' ' if self.sep is None else self.sep)*self.gutter
       for y in range(self.height1):
           y1 = y+1
           line = ''
           for x,col in enumerate(cols):
               if x: # no gutter before the first column
                   line = line+sep
               cell = '' if y1 >= len(col) else col[y1]
               line = line+cell+' '*(col[0]-len(cell))
           page.append(line)
       page[0] = self.prefix1 + page[0]
       page[-1] = page[-1] + self.suffix1
       self.prefix1 = self.prefixx
       self.suffix1 = self.suffixx
       self.height1 = self.heightx
       if self.file is None:
           self._pages.append(page[:])
           return
       try:
           for line in page:
               print(line,file=self.file)
           if 'flush' in dir(self.file):
               self.file.flush()
       except BrokenPipeError:
           return
       return

   def close(self):
       """Called when output is all done."""
       if self._closed:
           raise IOError('close after close')
       self._closed = True
       while self._columns:
           self._process_page()
       return

   def get_properties_dict(self):
       """Get all options/properties as a dictionary."""
       return dict(self.get_properties_list)
   get_properties = get_properties_dict
   get_options = get_properties
   get_props = get_properties
   get_opts = get_properties

   def get_properties_list(self):
       """Get all options/properties as a list or tuple of items."""
       return (
           ('bottom',  self.bottom),
           ('call',    self.call),
           ('file',    self.file),
           ('gutter',  self.gutter),
           ('height1', self.height1),
           ('heightx', self.heightx),
           ('left',    self.left),
           ('prefix1', self.prefix1),
           ('prefixx', self.prefixx),
           ('right',   self.right),
           ('sep',     self.sep),
           ('suffix1', self.suffix1),
           ('suffixx', self.suffixx),
           ('top',     self.top),
           ('width',   self.width),
       )

   def get_property_names(self):
       """Get all options/properties as a list or tuple of names."""
       return [i[0] for i in self.get_properties_list()]

   def get_page(self):
       """Get the next ready formatted page.  """\
       """This works only when no output file is set."""
       if self.file is not None:
           if self.file is sys.stdout:
               raise IOError('output is already set to STDOUT')
           elif self.file is sys.stderr:
               raise IOError('output is already set to STDERR')
           raise IOError('output is already set to a file')
       if self.pages:
           return self._pages.pop(0)
       return None
   get = get_page

   def next_column(self,**opts):
       """Output that follows goes to the next column."""
       if self._closed:
           raise IOError('next_column after close')
       self._columns.append([0])
       if self._is_enough_for_a_page():
           self._process_page()
       return

   def next_page(self,**opts):
       """Output that follows goes to the next page."""
       if self._closed:
           raise IOError('next_page after close')
       self._process_page()
       return

   def print(self,*args,**opts):
       """Print a line to a columnized stream."""
       # no args: close
       # one arg:
       # None   : close
       # False  : next column
       # True   : next page
       # other  : str() results
       # multiple args: str() each and join
       if self.height1 < 2:
           raise ValueError('self.height1 < 2')
       if self.width < 2:
           raise ValueError('self.width < 2')
       if opts:
           self.set_opts(opts)
       if len(args) < 1:
           return self.close()
       if args[0] is None:
           return self.close()
       if args[0] is True:
           return self.next_page()
       if args[0] is False:
           return self.next_column()
       line = ''
       n = 0
       for arg in args:
           if n:
               line = line + self.sep*self.gutter
           n += 1
           line = line + str(arg)
       # we now have a line to add to a column
       # if there are no columns, start the first one
       if len(self._columns) < 1:
           self._columns = [[0]]
       # if the current column is full, start a new one
       if len(self._columns[-1])-1 >= self.height1:
           self._columns.append([0])
       # append the line to the current column
       self._columns[-1].append(line)
       # update width
       if self._columns[-1][0] < len(line):
           self._columns[-1][0] = len(line)
       # check if page is full
       if self._is_enough_for_a_page():
           self._process_page()
       return

   def set_properties(self,*args,**opts):
       """Set options/properties and return a dict of unrecognized ones."""
       names = self.get_property_names()
       for n in args:
           opts[n[0]]=n[1]
       for n in names:
           if n in opts:
               v = opts.pop(n)
               if v != None:
                   setattr(self,n,v)
       return opts
   set_options = set_properties
   set_props = set_properties
   set_opts = set_properties

def main(args):
   """main"""
   if len(args) == 3:
       gutter  = 1
       width   = int(args[1])
       height1 = int(args[2])
       heightx = height1
   elif len(args) == 4:
       gutter  = int(args[1])
       width   = int(args[2])
       height1 = int(args[3])
       heightx = height1
   elif len(args) == 5:
       gutter  = int(args[1])
       width   = int(args[2])
       height1 = int(args[3])
       heightx = int(args[4])
   else:
       print('need 2 tp 4 arguments: [gutter] width [height1] heightx',
             file=sys.stderr)
       return 1
   c = colpage( gutter=gutter,
                width=width,
                height1=height1,
                heightx=heightx,
                prefix1=chr(12)+'<prefix1>',
                prefixx=chr(12)+'<prefixx>',
                suffix1='<suffix1>',
                suffixx='<suffixx>',
                file=sys.stdout
   )
   c.prefix1 = ''
   c.prefixx = ''
   c.suffix1 = ''
   c.suffixx = ''
   for line in sys.stdin:
       c.print( line.rstrip() )
   c.close()
   del c
   return 0

if __name__ == '__main__':
   try:
       result=main(sys.argv)
       sys.stdout.flush()
   except BrokenPipeError:
       result=99
   except KeyboardInterrupt:
       print('')
       result=98
   if result is 0 or result is None or result is True:
       exit(0)
   if result is 1 or result is False:
       exit(1)
   if isinstance(result,str):
       print(result,file=sys.stderr)
       exit(2)
   try:
       exit(int(result))
   except ValueError:
       print(str(result),file=sys.stderr)
       exit(3)
   except TypeError:
       exit(4)
# EOF
compared to other tools it has the ability to tightly fit as many columns as it can per page.  i plan to add the ability to add headings, footings, and side pillars on each page.  the page width and height is given in character units, as well as the minimum gutter space between columns.
Tradition is peer pressure from dead people

What do you call someone who speaks three languages? Trilingual. Two languages? Bilingual. One language? American.
Reply
#2
update ...

#!/usr/bin/env python3
# -*- coding: utf-8 -*-
from __future__ import division, print_function, unicode_literals
"""
file            columnize.py
purpose         class to convert a long stream of short lines to multiple
               tight columns in one or more pages passed to the caller
               or printed to a given file, where columns are aligned
               vertically and can vary in width and number of columns
               per page (more can fit when they are narrower).
email           10054452614123394844460370234029112340408691

The intent is that this code works correctly under both Python 2 and
Python 3.  Please report failures or code improvement to the author.
"""

__license__ = """
Copyright © 2017, by Phil D. Howard - all other rights reserved

Permission to use, copy, modify, and/or distribute this software for any
purpose with or without fee is hereby granted, provided that the above
copyright notice and this permission notice appear in all copies.

THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES
WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF
MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR
ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES
WHATSOEVER RESULTING FROM LOSS OF USE, DATA, OR PROFITS, WHETHER IN AN
ACTION OF CONTRACT, NEGLIGENCE, OR OTHER TORTIOUS ACTION, ARISING OUT OF
OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE.

The author may be contacted by decoding the number
10054452614123394844460370234029112340408691
(provu igi la numeron al duuma)
"""

import os, sys

try:
   from printobject import print_object
   have_print_object = True
except ImportError:
   have_print_object = False

if sys.version_info.major<3:
   ints = (int,long,)
   strs = (str,unicode,)
   ustr = (str,unicode,)
   def sorteditems(d):
       return sorted(d.iteritems())
   BrokenPipeError = IOError
else:
   ints = (int,)
   strs = (str,bytes,bytearray,)
   ustr = (str,)
   def sorteditems(d):
       return sorted(d.items())

def filemap(f,m):
   """Map various file= values to various file types."""
   if f is None:
       return None
   elif f is False:
       return sys.stderr
   elif f is True:
       return sys.stdout
   elif isinstance(f,int):
       return os.fdopen(f,m)
   elif isinstance(f,ustr):
       return open(f,m)
   return f

class columnize:
   """class to columnize a sequence of lines into columns and pages"""

   def __init__(self,*args,**opts):
       """Customize a columnize instance."""
       self._init()
       opts = self.set_opts(**opts)
       msg = ''
       if len(args)>0:
           for n,a in enumerate(args):
               print('unused argument',n,'=',repr(a),file=sys.stderr)
           msg += str(len(args))+' unused argument'+\
                  ('' if len(args)<2 else 's')
       if len(opts)>0:
           for k,v in sorteditems(opts):
               print('unknown option',repr(k)+'='+repr(v),file=sys.stderr)
           if len(msg) > 0:
               msg += ' and '
           msg += str(len(opts))+' unknown option'+\
                  ('' if len(opts)<2 else 's')
       if len(msg) > 0:
           raise ValueError(msg)
       return

   def _init(self):
       """Initialize variables to initial values."""
       self._columns = []
       self._closed  = False
       self._file    = None
       self._pages   = []
       self.bottom   = None
       self.call     = None
       self.file     = True
       self.gutter   = 1
       self.height1  = 25
       self.heightx  = 24
       self.left     = None
       self.prefix1  = ''
       self.prefixx  = ''
       self.right    = None
       self.sep      = ' '
       self.suffix1  = ''
       self.suffixx  = ''
       self.top      = None
       self.width    = 72
       return

   def _is_enough_for_a_page(self):
       """Return True if enough data is available to make a full page."""
       width = - self.gutter
       for col in self._columns:
           width += self.gutter
           width += col[0]
           if width >= self.width:
               return True
       return False

   def _process_page(self):
       """Make a page and output it, popping columns used to make it."""
       # collect the columns to make the page with
       cols = []
       width = - self.gutter
       while self._columns:
           width += self.gutter
           width += self._columns[0][0]
           if width >= self.width:
               break
           cols.append(self._columns.pop(0))
       # form the collected columns into a page
       page = []
       sep = (' ' if self.sep is None else self.sep)*self.gutter
       for y in range(self.height1):
           y1 = y+1
           line = ''
           for x,col in enumerate(cols):
               if x: # no gutter before the first column
                   line = line+sep
               cell = '' if y1 >= len(col) else col[y1]
               line = line+cell+' '*(col[0]-len(cell))
           page.append(line)
       page[0] = self.prefix1 + page[0]
       page[-1] = page[-1] + self.suffix1
       self.prefix1 = self.prefixx
       self.suffix1 = self.suffixx
       self.height1 = self.heightx
       if self._file is None:
           self._file = filemap(self.file,'w')
       if self._file is None:
           self._pages.append(page[:])
           return
       try:
           for line in page:
               print(line,file=self.file)
           if 'flush' in dir(self.file):
               self.file.flush()
       except BrokenPipeError:
           return
       return

   def close(self):
       """Called when output is all done."""
       if self._closed:
           raise IOError('close after close')
       self._closed = True
       while self._columns:
           self._process_page()
       if 'close' in dir(self.file):
           if self.file != sys.stdout and self.file != sys.stderr:
               self.file.close()
       return

   def get_properties_dict(self):
       """Get all options/properties as a dictionary."""
       return dict(self.get_properties_list)
   get_properties = get_properties_dict
   get_options = get_properties
   get_props = get_properties
   get_opts = get_properties

   def get_properties_list(self):
       """Get all options/properties as a list or tuple of items."""
       return (
           ('bottom',  self.bottom),
           ('call',    self.call),
           ('file',    self.file),
           ('gutter',  self.gutter),
           ('height1', self.height1),
           ('heightx', self.heightx),
           ('left',    self.left),
           ('prefix1', self.prefix1),
           ('prefixx', self.prefixx),
           ('right',   self.right),
           ('sep',     self.sep),
           ('suffix1', self.suffix1),
           ('suffixx', self.suffixx),
           ('top',     self.top),
           ('width',   self.width),
       )

   def get_property_names(self):
       """Get all options/properties as a list or tuple of names."""
       return [i[0] for i in self.get_properties_list()]

   def get_page(self):
       """Get the next ready formatted page.  """\
       """This works only when no output file is set."""
       if self.file is not None:
           if self.file is sys.stdout:
               raise IOError('output is already set to STDOUT')
           elif self.file is sys.stderr:
               raise IOError('output is already set to STDERR')
           raise IOError('output is already set to a file')
       if self.pages:
           return self._pages.pop(0)
       return None
   get = get_page

   def next_column(self,**opts):
       """Output that follows goes to the next column."""
       if self._closed:
           raise IOError('next_column after close')
       self._columns.append([0])
       if self._is_enough_for_a_page():
           self._process_page()
       return

   def next_page(self,**opts):
       """Output that follows goes to the next page."""
       if self._closed:
           raise IOError('next_page after close')
       self._process_page()
       return

   def print(self,*args,**opts):
       """Print a line to a columnized stream."""
       # no args: close
       # one arg:
       # None   : close
       # False  : next column
       # True   : next page
       # other  : str() results
       # multiple args: str() each and join
       if self.height1 < 2:
           raise ValueError('self.height1 < 2')
       if self.width < 2:
           raise ValueError('self.width < 2')
       if opts:
           self.set_opts(opts)
       if len(args) < 1:
           return self.close()
       if args[0] is None:
           return self.close()
       if args[0] is True:
           return self.next_page()
       if args[0] is False:
           return self.next_column()
       line = ''
       n = 0
       for arg in args:
           if n:
               line = line + self.sep*self.gutter
           n += 1
           line = line + str(arg)
       # we now have a line to add to a column
       # if there are no columns, start the first one
       if len(self._columns) < 1:
           self._columns = [[0]]
       # if the current column is full, start a new one
       if len(self._columns[-1])-1 >= self.height1:
           self._columns.append([0])
       # append the line to the current column
       self._columns[-1].append(line)
       # update width
       if self._columns[-1][0] < len(line):
           self._columns[-1][0] = len(line)
       # check if page is full
       if self._is_enough_for_a_page():
           self._process_page()
       return

   def set_properties(self,*args,**opts):
       """Set options/properties and return a dict of unrecognized ones."""
       names = self.get_property_names()
       for n in args:
           opts[n[0]]=n[1]
       for n in names:
           if n in opts:
               v = opts.pop(n)
               if v != None:
                   setattr(self,n,v)
       return opts
   set_options = set_properties
   set_props = set_properties
   set_opts = set_properties

def main(args):
   """main"""
   if len(args) == 3:
       gutter  = 1
       width   = int(args[1])
       height1 = int(args[2])
       heightx = height1
   elif len(args) == 4:
       gutter  = int(args[1])
       width   = int(args[2])
       height1 = int(args[3])
       heightx = height1
   elif len(args) == 5:
       gutter  = int(args[1])
       width   = int(args[2])
       height1 = int(args[3])
       heightx = int(args[4])
   else:
       print('need 2 tp 4 arguments: [gutter] width [height1] heightx',
             file=sys.stderr)
       return 1
   c = columnize( gutter=gutter,
                  width=width,
                  height1=height1,
                  heightx=heightx,
                  prefix1=chr(12)+'<prefix1>',
                  prefixx=chr(12)+'<prefixx>',
                  suffix1='<suffix1>',
                  suffixx='<suffixx>',
                  file=sys.stdout
   )
   c.prefix1 = ''
   c.prefixx = ''
   c.suffix1 = ''
   c.suffixx = ''
   for line in sys.stdin:
       c.print( line.rstrip() )
   c.close()
   del c
   return 0

if __name__ == '__main__':
   try:
       result=main(sys.argv)
       sys.stdout.flush()
   except BrokenPipeError:
       result=99
   except KeyboardInterrupt:
       print('')
       result=98
   if result is 0 or result is None or result is True:
       exit(0)
   if result is 1 or result is False:
       exit(1)
   if isinstance(result,str):
       print(result,file=sys.stderr)
       exit(2)
   try:
       exit(int(result))
   except ValueError:
       print(str(result),file=sys.stderr)
       exit(3)
   except TypeError:
       exit(4)
# EOF
Tradition is peer pressure from dead people

What do you call someone who speaks three languages? Trilingual. Two languages? Bilingual. One language? American.
Reply


Forum Jump:

User Panel Messages

Announcements
Announcement #1 8/1/2020
Announcement #2 8/2/2020
Announcement #3 8/6/2020