Merge pull request #2 from evennia/master

Merge with master fork
This commit is contained in:
delizin 2014-02-01 09:40:40 -08:00
commit 7fced8af5d
2 changed files with 689 additions and 104 deletions

388
src/utils/mudform.py Normal file
View file

@ -0,0 +1,388 @@
# coding=utf-8
"""
Evform - a way to create advanced ascii forms
This is intended for creating advanced ascii game forms, such as a
large pretty character sheet or info document.
The system works on the basis of a readin template that is given in a
separate python file imported into the handler. This file contains
some optional settings and a string mapping out the form. The template
has markers in it to denounce fields to fill. The markers map the
absolute size of the field and will be filled with an evtable.Cell
object when displaying the form.
Example of input file testform.py:
FORMCHAR = "x"
TABLECHAR = "c"
FORM = '''
.------------------------------------------------.
| |
| Name: xxxxx1xxxxx Player: xxxxxxx2xxxxxxx |
| xxxxxxxxxxx |
| |
>----------------------------------------------<
| |
| Desc: xxxxxxxxxxx STR: x4x DEX: x5x |
| xxxxx3xxxxx INT: x6x STA: x7x |
| xxxxxxxxxxx LUC: x8x MAG: x9x |
| |
>----------------------------------------------<
| | |
| cccccccc | ccccccccccccccccccccccccccccccccccc |
| cccccccc | ccccccccccccccccccccccccccccccccccc |
| cccAcccc | ccccccccccccccccccccccccccccccccccc |
| cccccccc | ccccccccccccccccccccccccccccccccccc |
| cccccccc | cccccccccccccccccBccccccccccccccccc |
| | |
`-----------------------------------------------´
'''
The first line of the FORM string is ignored. The forms and table
markers must mark out complete, unbroken rectangles, each containing
one embedded single-character identifier (so the smallest element
possible is a 3-character wide form). The identifier can be any
character except for the FORM_CHAR and TABLE_CHAR and some of the
common ascii-art elements, like space, _ | * etc (see
INVALID_FORMCHARS in this module). Form Rectangles can have any size,
but must be separated from each other by at least one other
character's width.
Use as follows:
import mudform
# create a new form from the template
form = mudform.MudForm("path/to/testform.py")
# add data to each tagged form cell
form.map(cells={1: "Tom the Bouncer",
2: "Griatch",
3: "A sturdy fellow",
4: 12,
5: 10,
6: 5,
7: 18,
8: 10,
9: 3})
# create the MudTables
tableA = mudform.MudTable("HP","MV","MP",
table=[["**"], ["*****"], ["***"]],
border="incols")
tableB = mudform.MudTable("Skill", "Value", "Exp",
table=[["Shooting", "Herbalism", "Smithing"],
[12,14,9],["550/1200", "990/1400", "205/900"]],
border="incols")
# add the tables to the proper ids in the form
form.map(tables={"A": tableA,
"B": tableB}
print form
This produces the following result:
.------------------------------------------------.
| |
| Name: Tom the Player: Griatch |
| Bouncer |
| |
>----------------------------------------------<
| |
| Desc: A sturdy STR: 12 DEX: 10 |
| fellow INT: 5 STA: 18 |
| LUC: 10 MAG: 3 |
| |
>----------------------------------------------<
| | |
| HP|MV|MP | Skill |Value |Exp |
| ~~+~~+~~ | ~~~~~~~~~~~+~~~~~~~~~~~+~~~~~~~~~~~ |
| **|**|** | Shooting |12 |550/1200 |
| |**|* | Herbalism |14 |990/1400 |
| |* | | Smithing |9 |205/900 |
| | |
------------------------------------------------
The marked forms have been replaced with Cells of text and with
MudTables. The form can be updated by simply re-applying form.map()
with the updated data.
When working with the template ascii file, you can use form.reload()
to re-read the template and re-apply all existing mappings.
Each component is restrained to the width and height specified by the
template, so it will resize to fit (or crop text if the area is too
small for it. If you try to fit a table into an area it cannot fit
into (when including its borders and at least one line of text), the
form will raise an error.
"""
import re
import copy
from src.utils.mudtable import Cell, MudTable
from src.utils.utils import all_from_module, to_str, to_unicode
# non-valid form-identifying characters (which can thus be
# used as separators between forms without being detected
# as an identifier). These should be listed in regex form.
INVALID_FORMCHARS = r"\s\/\|\\\*\_\-\#\<\>\~\^\:\;\.\,"
class MudForm(object):
"""
This object is instantiated with a text file and parses
it for rectangular form fields. It can then be fed a
mapping so as to populate the fields with fixed-width
Cell or Tablets.
"""
def __init__(self, filename=None, cells=None, tables=None, form=None, **kwargs):
"""
Initiate the form
keywords:
filename - path to template file
form - dictionary of {"CELLCHAR":char,
"TABLECHAR":char,
"FORM":templatestring}
if this is given, filename is not read.
cells - a dictionary mapping of {id:text}
tables - dictionary mapping of {id:MudTable}
other kwargs are fed as options to the Cells and MudTablets
(see mudtablet.Cell and mudtablet.MudTablet for more info).
"""
self.filename = filename
self.input_form_dict = form
self.cells_mapping = dict((str(key), value) for key, value in cells.items()) if cells else {}
self.tables_mapping = dict((str(key), value) for key, value in tables.items()) if tables else {}
self.cellchar = "x"
self.tablechar = "c"
self.raw_form = []
self.form = []
# clean kwargs (these cannot be overridden)
kwargs.pop("enforce_size", None)
kwargs.pop("width", None)
kwargs.pop("height", None)
# table/cell options
self.options = kwargs
self.reload()
def _parse_rectangles(self, cellchar, tablechar, form, **kwargs):
"""
Parse a form for rectangular formfields identified by
formchar enclosing an identifier.
"""
# update options given at creation with new input - this
# allows e.g. self.map() to add custom settings for individual
# cells/tables
custom_options = copy.copy(self.options)
custom_options.update(kwargs)
nform = len(form)
mapping = {}
cell_coords = {}
table_coords = {}
# Locate the identifier tags and the horizontal end coords for all forms
re_cellchar = re.compile(r"%s+([^%s%s])%s+" % (cellchar, INVALID_FORMCHARS, cellchar, cellchar))
re_tablechar = re.compile(r"%s+([^%s%s|])%s+" % (tablechar, INVALID_FORMCHARS, tablechar, tablechar))
for iy, line in enumerate(form):
# find cells
ix0 = 0
while True:
match = re_cellchar.search(line, ix0)
if match:
# get the width of the rectangle directly from the match
cell_coords[match.group(1)] = [iy, match.start(), match.end()]
ix0 = match.end()
else:
break
# find tables
ix0 = 0
while True:
match = re_tablechar.search(line, ix0)
if match:
# get the width of the rectangle directly from the match
table_coords[match.group(1)] = [iy, match.start(), match.end()]
ix0 = match.end()
else:
break
#print "table_coords:", table_coords
# get rectangles and assign Cells
for key, (iy, leftix, rightix) in cell_coords.items():
# scan up to find top of rectangle
dy_up = 0
if iy > 0:
for i in range(1,iy):
#print "dy_up:", [form[iy-i][ix] for ix in range(leftix, rightix)]
if all(form[iy-i][ix] == cellchar for ix in range(leftix, rightix)):
dy_up += 1
else:
break
# find bottom edge of rectangle
dy_down = 0
if iy < nform-1:
for i in range(1,nform-iy-1):
#print "dy_down:", [form[iy+i][ix]for ix in range(leftix, rightix)]
if all(form[iy+i][ix] == cellchar for ix in range(leftix, rightix)):
dy_down += 1
else:
break
# we have our rectangle. Calculate size of Cell.
iyup = iy - dy_up
iydown = iy + dy_down
width = rightix - leftix
height = abs(iyup - iydown) + 1
# we have all the coordinates we need. Create Cell.
data = self.cells_mapping.get(key, "")
#if key == "1":
#print "creating cell '%s' (%s):" % (key, data)
#print "iy=%s, iyup=%s, iydown=%s, leftix=%s, rightix=%s, width=%s, height=%s" % (iy, iyup, iydown, leftix, rightix, width, height)
options = { "pad_left":0, "pad_right":0, "pad_top":0, "pad_bottom":0, "align":"l", "valign":"t", "enforce_size":True}
options.update(custom_options)
#if key=="4":
#print "options:", options
mapping[key] = (iyup, leftix, width, height, Cell(data, width=width, height=height,**options))
# get rectangles and assign Tables
for key, (iy, leftix, rightix) in table_coords.items():
# scan up to find top of rectangle
dy_up = 0
if iy > 0:
for i in range(1,iy):
#print "dy_up:", [form[iy-i][ix] for ix in range(leftix, rightix)]
if all(form[iy-i][ix] == tablechar for ix in range(leftix, rightix)):
dy_up += 1
else:
break
# find bottom edge of rectangle
dy_down = 0
if iy < nform-1:
for i in range(1,nform-iy-1):
#print "dy_down:", [form[iy+i][ix]for ix in range(leftix, rightix)]
if all(form[iy+i][ix] == tablechar for ix in range(leftix, rightix)):
dy_down += 1
else:
break
# we have our rectangle. Calculate size of Table.
iyup = iy - dy_up
iydown = iy + dy_down
width = rightix - leftix
height = abs(iyup - iydown) + 1
# we have all the coordinates we need. Create Table.
table = self.tables_mapping.get(key, None)
#print "creating table '%s' (%s):" % (key, data)
#print "iy=%s, iyup=%s, iydown=%s, leftix=%s, rightix=%s, width=%s, height=%s" % (iy, iyup, iydown, leftix, rightix, width, height)
options = { "pad_left":0, "pad_right":0, "pad_top":0, "pad_bottom":0,
"align":"l", "valign":"t", "enforce_size":True}
options.update(custom_options)
#print "options:", options
if table:
table.reformat(width=width, height=height, **options)
else:
table = MudTable(width=width, height=height, **options)
mapping[key] = (iyup, leftix, width, height, table)
return mapping
def _populate_form(self, raw_form, mapping):
"""
Insert cell contents into form at given locations
"""
form = copy.copy(raw_form)
for key, (iy0, ix0, width, height, cell_or_table) in mapping.items():
# rect is a list of <height> lines, each <width> wide
rect = cell_or_table.get()
for il, rectline in enumerate(rect):
formline = form[iy0+il]
# insert new content, replacing old
form[iy0+il] = formline = formline[:ix0] + rectline + formline[ix0+width:]
return form
def map(self, cells=None, tables=None, **kwargs):
"""
Add mapping for form.
cells - a dictionary of {identifier:celltext}
tables - a dictionary of {identifier:table}
kwargs will be forwarded to tables/cells. See
mudtable.Cell and mudtable.MudTable for info.
"""
# clean kwargs (these cannot be overridden)
kwargs.pop("enforce_size", None)
kwargs.pop("width", None)
kwargs.pop("height", None)
new_cells = dict((to_str(key), value) for key, value in cells.items()) if cells else {}
new_tables = dict((to_str(key), value) for key, value in tables.items()) if tables else {}
self.cells_mapping.update(new_cells)
self.tables_mapping.update(new_tables)
self.reload()
def reload(self, filename=None, form=None, **kwargs):
"""
Creates the form from a stored file name
"""
# clean kwargs (these cannot be overridden)
kwargs.pop("enforce_size", None)
kwargs.pop("width", None)
kwargs.pop("height", None)
if form or self.input_form_dict:
datadict = form if form else self.input_form_dict
self.input_form_dict = datadict
elif filename or self.filename:
filename = filename if filename else self.filename
datadict = all_from_module(filename)
self.filename = filename
else:
datadict = {}
cellchar = to_str(datadict.get("FORMCHAR", "x"))
self.cellchar = to_str(cellchar[0] if len(cellchar) > 1 else cellchar)
tablechar = datadict.get("TABLECHAR", "c")
self.tablechar = tablechar[0] if len(tablechar) > 1 else tablechar
# split into a list of list of lines. Form can be indexed with form[iy][ix]
self.raw_form = to_unicode(datadict.get("FORM", "")).split("\n")
# strip first line
self.raw_form = self.raw_form[1:] if self.raw_form else self.raw_form
self.options.update(kwargs)
# parse and replace
self.mapping = self._parse_rectangles(self.cellchar, self.tablechar, self.raw_form, **kwargs)
self.form = self._populate_form(self.raw_form, self.mapping)
def __str__(self):
"Prints the form"
return "\n".join([to_str(line) for line in self.form])

View file

@ -1,4 +1,4 @@
# -*- coding: utf-8 -*-
# coding=utf-8
"""
Mudtable
@ -32,39 +32,44 @@ As seen, the table will automatically expand with empty cells to
make the table symmetric.
Tables can be restricted to a given width.
If we created the above table with the width=50 keyword to MudTable()
and then added the extra column and row, the result would be
table.reformat(width=50, align="l")
(We could just have added these keywords to the table
creation call) yields the following result:
+-----------+------------+-----------+-----------+
| Heading1 | Heading2 | | |
| Heading1 | Heading2 | | |
+===========+============+===========+===========+
| 1 | 4 | 7 | This is |
| 1 | 4 | 7 | This is |
| | | | long data |
+-----------+------------+-----------+-----------+
| | | | This is |
| 2 | 5 | 8 | even |
| | | | longer |
| | | | data |
| 2 | 5 | 8 | even |
| | | | longer |
| | | | data |
+-----------+------------+-----------+-----------+
| 3 | 6 | 9 | |
| 3 | 6 | 9 | |
+-----------+------------+-----------+-----------+
| This is a | | | |
| single | | | |
| row | | | |
| row | | | |
+-----------+------------+-----------+-----------+
When adding new rows/columns their data can have its
own alignments (left/center/right, top/center/bottom).
Contrary to prettytable, Mudtable does not allow
for importing from files.
If the height is restricted, cells will be restricted
from expanding vertically. This will lead to text
contents being cropped. Each cell can only shrink
to a minimum width and height of 1.
It is intended to be used with ANSIString for supporting
ANSI-coloured string types.
"""
from textwrap import wrap
from copy import deepcopy
from copy import deepcopy, copy
#from src.utils.ansi import ANSIString
@ -89,34 +94,47 @@ class Cell(object):
to this size.
height - desired height of cell. it will pad
to this size
pad_width - general padding width. This can be overruled
by individual settings below
pad_left - number of extra pad characters on the left
pad_right - extra pad characters on the right
pad_top - extra pad lines top (will pad with vpad_char)
pad_bottom - extra pad lines bottom (will pad with vpad_char)
pad_char - pad character to use both for extra horizontal
padding
pad_char - pad character to use for padding. This is overruled
by individual settings below (default " ")
hpad_char - pad character to use both for extra horizontal
padding (default " ")
vpad_char - pad character to use for extra vertical padding
and for vertical fill (default " ")
fill_char - character used for horizontal fill (default " ")
fill_char - character used to filling (expanding cells to
desired size). This can be overruled by individual
settings below.
hfill_char - character used for horizontal fill (default " ")
vfill_char - character used for vertical fill (default " ")
align - "l", "r" or "c", default is centered
valign - "t", "b" or "c", default is centered
border_width -general border width. This is overruled
- by individual settings below.
border_left - left border width
border_right - right border width
border_top - top border width
border_bottom - bottom border width
border_char - this will use a single border char for all borders.
overruled by individual settings below
border_left_char - char used for left border
border_right_char
border_top_char
border_bottom_char
cornerchar - character used when two borders cross.
(default is "")
corner_top_left - if this is given, it replaces the
cornerchar in the upper left
corner
border_right_char - char used for right border
border_top_char - char used for top border
border_bottom_char - char user for bottom border
corner_char - character used when two borders cross.
(default is ""). This is overruled by
individual settings below.
corner_top_left
corner_top_right
corner_bottom_left
corner_bottom_right
@ -127,38 +145,48 @@ class Cell(object):
than the cell growing vertically.
"""
self.pad_left = int(kwargs.get("pad_left", 1))
self.pad_right = int(kwargs.get("pad_right", 1))
self.pad_top = int( kwargs.get("pad_top", 0))
self.pad_bottom = int(kwargs.get("pad_bottom", 0))
padwidth = kwargs.get("pad_width", None)
padwidth = int(padwidth) if padwidth is not None else None
self.pad_left = int(kwargs.get("pad_left", padwidth if padwidth is not None else 1))
self.pad_right = int(kwargs.get("pad_right", padwidth if padwidth is not None else 1))
self.pad_top = int( kwargs.get("pad_top", padwidth if padwidth is not None else 0))
self.pad_bottom = int(kwargs.get("pad_bottom", padwidth if padwidth is not None else 0))
self.enforce_size = kwargs.get("enforce_size", False)
# avoid multi-char pad_chars messing up counting
pad_char = kwargs.get("pad_char", " ")
self.pad_char = pad_char[0] if pad_char else " "
vpad_char = kwargs.get("vpad_char", " ")
self.vpad_char = vpad_char[0] if vpad_char else " "
pad_char = pad_char[0] if pad_char else " "
hpad_char = kwargs.get("hpad_char", pad_char)
self.hpad_char = hpad_char[0] if hpad_char else pad_char
vpad_char = kwargs.get("vpad_char", pad_char)
self.vpad_char = vpad_char[0] if vpad_char else pad_char
fill_char = kwargs.get("fill_char", " ")
self.fill_char = fill_char[0] if fill_char else " "
vfill_char = kwargs.get("vfill_char", " ")
fill_char = fill_char[0] if fill_char else " "
hfill_char = kwargs.get("hfill_char", fill_char)
self.hfill_char = hfill_char[0] if hfill_char else " "
vfill_char = kwargs.get("vfill_char", fill_char)
self.vfill_char = vfill_char[0] if vfill_char else " "
# borders and corners
self.border_left = kwargs.get("border_left", 0)
self.border_right = kwargs.get("border_right", 0)
self.border_top = kwargs.get("border_top", 0)
self.border_bottom = kwargs.get("border_bottom", 0)
self.border_left_char = kwargs.get("border_left_char", "|")
self.border_right_char = kwargs.get("border_right_char", "|")
self.border_top_char = kwargs.get("border_topchar", "-")
self.border_bottom_char = kwargs.get("border_bottom_char", "-")
borderwidth = kwargs.get("border_width", 0)
self.border_left = kwargs.get("border_left", borderwidth)
self.border_right = kwargs.get("border_right", borderwidth)
self.border_top = kwargs.get("border_top", borderwidth)
self.border_bottom = kwargs.get("border_bottom", borderwidth)
self.corner = kwargs.get("corner", "+")
self.corner_top_left = kwargs.get("corner_top_left", self.corner)
self.corner_top_right = kwargs.get("corner_top_right", self.corner)
self.corner_bottom_left = kwargs.get("corner_bottom_left", self.corner)
self.corner_bottom_right = kwargs.get("corner_bottom_right", self.corner)
borderchar = kwargs.get("border_char", None)
self.border_left_char = kwargs.get("border_left_char", borderchar if borderchar else "|")
self.border_right_char = kwargs.get("border_right_char", borderchar if borderchar else "|")
self.border_top_char = kwargs.get("border_topchar", borderchar if borderchar else "-")
self.border_bottom_char = kwargs.get("border_bottom_char", borderchar if borderchar else "-")
corner = kwargs.get("corner_char", "+")
self.corner_top_left = kwargs.get("corner_top_left", corner)
self.corner_top_right = kwargs.get("corner_top_right", corner)
self.corner_bottom_left = kwargs.get("corner_bottom_left", corner)
self.corner_bottom_right = kwargs.get("corner_bottom_right", corner)
# alignments
self.align = kwargs.get("align", "c")
@ -244,11 +272,11 @@ class Cell(object):
"Align list of rows of cell"
align = self.align
if align == "l":
return [line.ljust(self.width, self.fill_char) for line in data]
return [line.ljust(self.width, self.hfill_char) for line in data]
elif align == "r":
return [line.rjust(self.width, self.fill_char) for line in data]
return [line.rjust(self.width, self.hfill_char) for line in data]
else:
return [self._center(line, self.width, self.fill_char) for line in data]
return [self._center(line, self.width, self.hfill_char) for line in data]
def _valign(self, data):
"align cell vertically"
@ -280,8 +308,8 @@ class Cell(object):
def _pad(self, data):
"Pad data with extra characters on all sides"
left = self.pad_char * self.pad_left
right = self.pad_char * self.pad_right
left = self.hpad_char * self.pad_left
right = self.hpad_char * self.pad_right
vfill = (self.width + self.pad_left + self.pad_right) * self.vpad_char
top = [vfill for i in range(self.pad_top)]
bottom = [vfill for i in range(self.pad_bottom)]
@ -293,7 +321,8 @@ class Cell(object):
left = self.border_left_char * self.border_left
right = self.border_right_char * self.border_right
cwidth = self.width + self.pad_left + self.pad_right
cwidth = self.width + self.pad_left + self.pad_right + \
max(0,self.border_left-1) + max(0, self.border_right-1)
vfill = self.corner_top_left if left else ""
vfill += cwidth * self.border_top_char
@ -307,12 +336,26 @@ class Cell(object):
return top + [left + line + right for line in data] + bottom
def get_min_height(self):
"""
Get the minimum possible height of cell, including at least
one line for data.
"""
return self.pad_top + self.pad_bottom + self.border_bottom + self.border_top + 1
def get_min_width(self):
"""
Get the minimum possible width of cell, including at least one
character-width for data.
"""
return self.pad_left + self.pad_right + self.border_left + self.border_right + 1
def get_height(self):
"Get height of cell, including padding"
"Get natural height of cell, including padding"
return len(self.formatted)
def get_width(self):
"Get width of cell, including padding"
"Get natural width of cell, including padding"
return len(self.formatted[0]) if self.formatted else 0
def replace_data(self, data, **kwargs):
@ -333,7 +376,51 @@ class Cell(object):
kwargs:
as the class __init__
"""
# keywords that require manipulations
# keywords that require manipulation
padwidth = kwargs.get("pad_width", None)
padwidth = int(padwidth) if padwidth is not None else None
self.pad_left = int(kwargs.get("pad_left", padwidth if padwidth is not None else self.pad_left))
self.pad_right = int(kwargs.get("pad_right", padwidth if padwidth is not None else self.pad_right))
self.pad_top = int( kwargs.get("pad_top", padwidth if padwidth is not None else self.pad_top))
self.pad_bottom = int(kwargs.get("pad_bottom", padwidth if padwidth is not None else self.pad_bottom))
padchar = kwargs.pop("pad_char", None)
hpad_char = kwargs.pop("hpad_char", padchar)
self.hpad_char = hpad_char[0] if hpad_char else self.hpad_char
vpad_char = kwargs.pop("vpad_char", padchar)
self.vpad_char = vpad_char[0] if vpad_char else self.vpad_char
fillchar = kwargs.pop("fill_char", None)
hfill_char = kwargs.pop("hfill_char", fillchar)
self.hfill_char = hfill_char[0] if hfill_char else self.hfill_char
vfill_char = kwargs.pop("vfill_char", fillchar)
self.vfill_char = vfill_char[0] if vfill_char else self.vfill_char
borderwidth = kwargs.get("border_width", None)
self.border_left = kwargs.pop("border_left", borderwidth if borderwidth is not None else self.border_left)
self.border_right = kwargs.get("border_right", borderwidth if borderwidth is not None else self.border_right)
self.border_top = kwargs.get("border_top", borderwidth if borderwidth is not None else self.border_top)
self.border_bottom = kwargs.get("border_bottom", borderwidth if borderwidth is not None else self.border_bottom)
borderchar = kwargs.get("border_char", None)
self.border_left_char = kwargs.get("border_left_char", borderchar if borderchar else self.border_left_char)
self.border_right_char = kwargs.get("border_right_char", borderchar if borderchar else self.border_right_char)
self.border_top_char = kwargs.get("border_topchar", borderchar if borderchar else self.border_top_char)
self.border_bottom_char = kwargs.get("border_bottom_char", borderchar if borderchar else self.border_bottom_char)
corner = kwargs.get("corner_char", None)
self.corner_top_left = kwargs.get("corner_top_left", corner if corner is not None else self.corner_top_left)
self.corner_top_right = kwargs.get("corner_top_right", corner if corner is not None else self.corner_top_right)
self.corner_bottom_left = kwargs.get("corner_bottom_left", corner if corner is not None else self.corner_bottom_left)
self.corner_bottom_right = kwargs.get("corner_bottom_right", corner if corner is not None else self.corner_bottom_right)
# fill all other properties
for key, value in kwargs.items():
setattr(self, key, value)
# Handle sizes
if "width" in kwargs:
width = kwargs.pop("width")
self.width = width - self.pad_left - self.pad_right - self.border_left - self.border_right
@ -345,20 +432,7 @@ class Cell(object):
if self.height <= 0:
raise Exception("Cell height too small, no room for data.")
pad_char = kwargs.pop("padchar", None)
self.pad_char = pad_char[0] if pad_char else self.pad_char
vpad_char = kwargs.pop("vpadchar", None)
self.vpad_char = vpad_char[0] if vpad_char else self.vpad_char
fill_char = kwargs.pop("fillchar", None)
self.fill_char = fill_char[0] if fill_char else self.fill_char
vfill_char = kwargs.pop("vfillchar", None)
self.vfill_char = vfill_char[0] if vfill_char else self.vfill_char
# fill all other properties
for key, value in kwargs.items():
setattr(self, key, value)
# reformat (this is with padding)
# reformat (to new sizes, padding, header and borders)
self.formatted = self._reformat()
def get(self):
@ -391,19 +465,43 @@ class MudTable(object):
table - list of columns (list of lists) for seeding
the table. If not given, the table will start
out empty
header - True/False - turn off header being treated
as a header (like extra underlining)
pad_width - how much empty space to pad your cells with
(default is 1)
border - None, or one of
"table" - only a border around the whole table
"tablecols" - table and column borders
"header" - only border under header
"cols" - only borders between columns
"cols" - only vertical borders
"incols" - vertical borders, no outer edges
"rows" - only borders between rows
"cells" - border around all cells
border_width - width of table borders, if border is active.
Note that widths wider than 1 may give artifacts in the
corners. Default is 1.
corner_char - character to use in corners when border is
active.
header_line_char - characters to use for underlining
the header row (default is '~')
Requires border to be active.
width - fixed width of table. If not set, width is
set by the total width of each column.
This will resize individual columns to fit.
This will resize individual columns in
the vertical direction to fit.
height - fixed height of table. Defaults to unset.
Width is still given precedence. If
height is given, table cells will crop
text rather than expand vertically.
evenwidth - (default True). Used with the width keyword.
Adjusts collumns to have as even width as
possible. This often looks best also for
mixed-length tables.
See also Cell class for kwargs to apply to each
individual data cell in the table.
See Cell class for further kwargs. These will be passed
to each cell in the table.
"""
# table itself is a 2D grid - a list of columns
@ -426,14 +524,28 @@ class MudTable(object):
self.table[ix].insert(0, heading)
else:
self.table = [[heading] for heading in header]
# even though we inserted the header, we can still turn off
# header border underling etc. We only allow this if a header
# was actually set
self.header = kwargs.pop("header", self.header) if self.header else False
hchar = kwargs.pop("header_line_char", "~")
self.header_line_char = hchar[0] if hchar else "~"
border = kwargs.pop("border", None)
if not border in (None, "table", "tablecols", "header", "cols", "rows", "cells"):
border = kwargs.pop("border", "none")
if border is None:
border = "none"
if not border in ("none", "table", "tablecols",
"header", "incols", "cols", "rows", "cells"):
raise Exception("Unsupported border type: '%s'" % border)
self.border = border
# border settings are passed into Cell as well (so kwargs.get and not pop)
self.border_width = kwargs.get("border_width", 1)
self.corner_char = kwargs.get("corner_char", "+")
self.width = kwargs.pop("width", None)
self.horizontal = kwargs.pop("horizontal", False)
self.height = kwargs.pop("height", None)
self.evenwidth = kwargs.pop("evenwidth", True)
# size in cell cols/rows
self.ncols = 0
self.nrows = 0
@ -480,42 +592,36 @@ class MudTable(object):
"add vertical border along left table edge"
if ix == 0:
ret["border_left"] = bwidth
ret["border_left_char"] = vchar
return ret
def top_edge(ret):
"add border along top table edge"
if iy == 0:
ret["border_top"] = bwidth
ret["border_top_char"] = hchar
return ret
def right_edge(ret):
"add vertical border along right table edge"
if ix == nx:# and 0 < iy < ny:
ret["border_right"] = bwidth
ret["border_right_char"] = vchar
return ret
def bottom_edge(ret):
"add border along bottom table edge"
if iy == ny:
ret["border_bottom"] = bwidth
ret["border_bottom_char"] = hchar
return ret
def cols(ret):
"Adding vertical borders inside the table"
if 0 <= ix < nx:
ret["border_right"] = bwidth
ret["border_right_char"] = vchar
return ret
def rows(ret):
"Adding horizontal borders inside the table"
if 0 <= iy < ny:
ret["border_bottom"] = bwidth
ret["border_bottom_char"] = hchar
return ret
def head(ret):
@ -531,22 +637,22 @@ class MudTable(object):
border = self.border
header = self.header
bwidth = 1
headchar = "="
cchar = "+"
vchar = "|"
hchar = "-"
bwidth = self.border_width
headchar = self.header_line_char
cchar = self.corner_char
# use the helper functions to define various
# table "styles"
if border in ("table", "tablecols","cells"):
ret = bottom_edge(right_edge(top_edge(left_edge(corners(ret)))))
headchar = "-"
if border in ("cols", "tablecols", "cells"):
ret = cols(right_edge(left_edge(ret)))
headchar = "-"
if border in ("incols"):
ret = cols(ret)
if border in ("rows", "cells"):
headchar = "="
ret = rows(bottom_edge(top_edge(ret)))
if header:
if header and not border in ("none", None):
ret = head(ret)
return ret
@ -573,6 +679,7 @@ class MudTable(object):
# actual table. This allows us to add columns/rows
# and re-balance over and over without issue.
self.worktable = deepcopy(self.table)
options = copy(self.options)
# balance number of rows
ncols = len(self.worktable)
@ -586,39 +693,104 @@ class MudTable(object):
self.ncols = ncols
self.nrows = nrowmax
# equalize heights for each row
cheights = [max(cell.get_height() for cell in (col[iy] for col in self.worktable)) for iy in range(nrowmax)]
# add borders - these add to the width/height, so we must do this before calculating width/height
self._borders()
# equalize widths within each column
cwidths = [max(cell.get_width() for cell in col) for col in self.worktable]
# width of worktable
if self.width:
# adjust widths of columns to fit in worktable width
cwidth = self.width // ncols
rest = self.width % ncols
# get the width of each col, spreading the rest among the first cols
cwidths = [cwidth + 1 if icol < rest else cwidth for icol, width in enumerate(cwidths)]
# we set a table width. Horizontal cells will be evenly distributed and
# expand vertically as needed (unless self.height is set, see below)
if ncols:
# get minimum possible cell widths for each row
cwidths_min = [max(cell.get_min_width() for cell in col) for col in self.worktable]
cwmin = sum(cwidths_min)
if cwmin > self.width:
# we cannot shrink any more
raise Exception("Cannot shrink table width to %s. Minimum size is %s." % (self.width, cwmin))
excess = self.width - cwmin
if self.evenwidth:
# make each collumn of equal width
for i in range(excess):
# flood-fill the minimum table starting with the smallest collumns
ci = cwidths_min.index(min(cwidths_min))
cwidths_min[ci] += 1
cwidths = cwidths_min
else:
# make each collumn expand more proportional to their data size
for i in range(excess):
# fill wider collumns first
ci = cwidths.index(max(cwidths))
cwidths_min[ci] += 1
cwidths[ci] -= 3
cwidths = cwidths_min
# reformat worktable (for width align)
for ix, col in enumerate(self.worktable):
for iy, cell in enumerate(col):
cell.reformat(width=cwidths[ix], **self.options)
try:
cell.reformat(width=cwidths[ix], **options)
except Exception, e:
msg = "ix=%s, iy=%s, width=%s: %s" % (ix, iy, cwidths[ix], e.message)
raise Exception ("Error in horizontal allign:\n %s" % msg)
# equalize heights for each row (we must do this here, since it may have changed to fit new widths)
cheights = [max(cell.get_height() for cell in (col[iy] for col in self.worktable)) for iy in range(nrowmax)]
if self.height:
# if we are fixing the table height, it means cells must crop text instead of resizing.
if nrowmax:
# get minimum possible cell heights for each collumn
cheights_min = [max(cell.get_min_height() for cell in (col[iy] for col in self.worktable)) for iy in range(nrowmax)]
chmin = sum(cheights_min)
#print "cheights_min:", cheights_min
if chmin > self.height:
# we cannot shrink any more
raise Exception("Cannot shrink table height to %s. Minimum size is %s." % (self.height, chmin))
# now we add all the extra height up to the desired table-height.
# We do this so that the tallest cells gets expanded first (and
# thus avoid getting cropped)
excess = self.height - chmin
even = self.height % 2 == 0
for i in range(excess):
# expand the cells with the most rows first
if 0 <= i < nrowmax and nrowmax > 1:
# avoid adding to header first round (looks bad on very small tables)
ci = cheights[1:].index(max(cheights[1:])) + 1
else:
ci = cheights.index(max(cheights))
cheights_min[ci] += 1
if ci == 0 and self.header:
# it doesn't look very good if header expands too fast
cheights[ci] -= 2 if even else 3
cheights[ci] -= 2 if even else 1
cheights = cheights_min
# we must tell cells to crop instead of expanding
options["enforce_size"] = True
#print "cheights2:", cheights
# reformat table (for vertical align)
for ix, col in enumerate(self.worktable):
for iy, cell in enumerate(col):
cell.reformat(height=cheights[iy], **self.options)
try:
cell.reformat(height=cheights[iy], **options)
except Exception, e:
msg = "ix=%s, iy=%s, height=%s: %s" % (ix, iy, cheights[iy], e.message)
raise Exception ("Error in vertical allign:\n %s" % msg)
# calculate actual table width/height in characters
self.cwidth = sum(cwidths)
self.cheight = sum(cheights)
#print "actual table width, height:", self.cwidth, self.cheight, self.width, self.height
def _generate_lines(self):
"""
@ -631,7 +803,6 @@ class MudTable(object):
cell_row = [col[iy] for col in self.worktable]
# this produces a list of lists, each of equal length
cell_data = [cell.get() for cell in cell_row]
print [len(lines) for lines in cell_data]
cell_height = min(len(lines) for lines in cell_data)
for iline in range(cell_height):
yield "".join(celldata[iline] for celldata in cell_data)
@ -735,6 +906,32 @@ class MudTable(object):
col.insert(ypos, row[icol])
self._balance()
def reformat(self, **kwargs):
"""
Force a re-shape of the entire table
"""
self.width = kwargs.pop("width", self.width)
self.height = kwargs.pop("height", self.height)
for key, value in kwargs.items():
setattr(self, key, value)
hchar = kwargs.pop("header_line_char", self.header_line_char)
# border settings are also passed on into Cells (so kwargs.get, not kwargs.pop)
self.header_line_char = hchar[0] if hchar else self.header_line_char
self.border_width = kwargs.get("border_width", self.border_width)
self.corner_char = kwargs.get("corner_char", self.corner_char)
self.header_line_char = kwargs.get("header_line_char", self.header_line_char)
self.options.update(kwargs)
self._balance()
def get(self):
"""
Return lines of table as a list
"""
return [line for line in self._generate_lines()]
def __str__(self):
"print table"
return "\n".join([line for line in self._generate_lines()])