From 92df3ce5ae0ea7fa3694e2f832058f5ea27283fc Mon Sep 17 00:00:00 2001 From: Griatch Date: Sun, 27 Aug 2017 14:56:05 +0200 Subject: [PATCH] Structure handlers to allow .get() to return lists See #1154. In the end I didn't modify the Attributehandler and TagHandler like this, instead I added the `return_list` argument for cases when one wants a guaranteed return. --- evennia/commands/cmdsethandler.py | 7 +- evennia/commands/default/building.py | 2 +- evennia/comms/channelhandler.py | 49 +- evennia/comms/models.py | 5 +- evennia/locks/lockhandler.py | 7 +- evennia/objects/objects.py | 2 +- evennia/scripts/monitorhandler.py | 4 + evennia/scripts/scripthandler.py | 2 +- evennia/scripts/taskhandler.py | 14 +- evennia/typeclasses/attributes.py | 22 +- evennia/typeclasses/tags.py | 15 +- evennia/utils/dbserialize.py | 1 - evennia/utils/prettytable.py | 1564 -------------------------- 13 files changed, 86 insertions(+), 1608 deletions(-) delete mode 100644 evennia/utils/prettytable.py diff --git a/evennia/commands/cmdsethandler.py b/evennia/commands/cmdsethandler.py index 09216bc484..84eea1fdc5 100644 --- a/evennia/commands/cmdsethandler.py +++ b/evennia/commands/cmdsethandler.py @@ -544,9 +544,9 @@ class CmdSetHandler(object): # legacy alias delete_default = remove_default - def all(self): + def get(self): """ - Show all cmdsets. + Get all cmdsets. Returns: cmdsets (list): All the command sets currently in the handler. @@ -554,6 +554,9 @@ class CmdSetHandler(object): """ return self.cmdset_stack + # backwards-compatible alias + all = get + def clear(self): """ Removes all Command Sets from the handler except the default one diff --git a/evennia/commands/default/building.py b/evennia/commands/default/building.py index 33c217e6b7..5e46763942 100644 --- a/evennia/commands/default/building.py +++ b/evennia/commands/default/building.py @@ -1841,7 +1841,7 @@ class CmdLock(ObjManipCommand): obj = caller.search(self.lhs) if not obj: return - caller.msg(obj.locks.all()) + caller.msg("\n".join(obj.locks.all())) class CmdExamine(ObjManipCommand): diff --git a/evennia/comms/channelhandler.py b/evennia/comms/channelhandler.py index 8683c0c9ec..02c9e19291 100644 --- a/evennia/comms/channelhandler.py +++ b/evennia/comms/channelhandler.py @@ -172,23 +172,25 @@ class ChannelHandler(object): Initializes the channel handler's internal state. """ - self.cached_channel_cmds = {} - self.cached_cmdsets = {} + self._cached_channel_cmds = {} + self._cached_cmdsets = {} + self._cached_channels = {} def __str__(self): """ Returns the string representation of the handler """ - return ", ".join(str(cmd) for cmd in self.cached_channel_cmds) + return ", ".join(str(cmd) for cmd in self._cached_channel_cmds) def clear(self): """ Reset the cache storage. """ - self.cached_channel_cmds = {} - self.cached_cmdsets = {} + self._cached_channel_cmds = {} + self._cached_cmdsets = {} + self._cached_channels = {} def add(self, channel): """ @@ -221,9 +223,11 @@ class ChannelHandler(object): key = channel.key cmd.__doc__ = cmd.__doc__.format(channelkey=key, lower_channelkey=key.strip().lower(), - channeldesc=channel.attributes.get("desc", default="").strip()) - self.cached_channel_cmds[channel] = cmd - self.cached_cmdsets = {} + channeldesc=channel.attributes.get( + "desc", default="").strip()) + self._cached_channel_cmds[channel] = cmd + self._cached_channels[key] = channel + self._cached_cmdsets = {} add_channel = add # legacy alias def remove(self, channel): @@ -247,11 +251,28 @@ class ChannelHandler(object): global _CHANNELDB if not _CHANNELDB: from evennia.comms.models import ChannelDB as _CHANNELDB - self.cached_channel_cmds = {} - self.cached_cmdsets = {} + self._cached_channel_cmds = {} + self._cached_cmdsets = {} + self._cached_channels = {} for channel in _CHANNELDB.objects.get_all_channels(): self.add(channel) + def get(self, channelname=None): + """ + Get a channel from the handler, or all channels + + Args: + channelame (str, optional): Channel key, case insensitive. + Returns + channels (list): The matching channels in a list, or all + channels in the handler. + + """ + if channelname: + channel = self._cached_channels.get(channelname.lower(), None) + return [channel] if channel else [] + return self._cached_channels.values() + def get_cmdset(self, source_object): """ Retrieve cmdset for channels this source_object has @@ -266,12 +287,12 @@ class ChannelHandler(object): access to. """ - if source_object in self.cached_cmdsets: - return self.cached_cmdsets[source_object] + if source_object in self._cached_cmdsets: + return self._cached_cmdsets[source_object] else: # create a new cmdset holding all viable channels chan_cmdset = None - chan_cmds = [channelcmd for channel, channelcmd in self.cached_channel_cmds.iteritems() + chan_cmds = [channelcmd for channel, channelcmd in self._cached_channel_cmds.iteritems() if channel.subscriptions.has(source_object) and channelcmd.access(source_object, 'send')] if chan_cmds: @@ -281,7 +302,7 @@ class ChannelHandler(object): chan_cmdset.duplicates = True for cmd in chan_cmds: chan_cmdset.add(cmd) - self.cached_cmdsets[source_object] = chan_cmdset + self._cached_cmdsets[source_object] = chan_cmdset return chan_cmdset diff --git a/evennia/comms/models.py b/evennia/comms/models.py index 3adb0c5c7a..700fcedbf7 100644 --- a/evennia/comms/models.py +++ b/evennia/comms/models.py @@ -533,7 +533,7 @@ class SubscriptionHandler(object): self.obj.db_object_subscriptions.add(subscriber) elif clsname == "AccountDB": self.obj.db_account_subscriptions.add(subscriber) - _CHANNELHANDLER.cached_cmdsets.pop(subscriber, None) + _CHANNELHANDLER._cached_cmdsets.pop(subscriber, None) self._recache() def remove(self, entity): @@ -556,7 +556,7 @@ class SubscriptionHandler(object): self.obj.db_account_subscriptions.remove(entity) elif clsname == "ObjectDB": self.obj.db_object_subscriptions.remove(entity) - _CHANNELHANDLER.cached_cmdsets.pop(subscriber, None) + _CHANNELHANDLER._cached_cmdsets.pop(subscriber, None) self._recache() def all(self): @@ -571,6 +571,7 @@ class SubscriptionHandler(object): if self._cache is None: self._recache() return self._cache + get = all # alias def online(self): """ diff --git a/evennia/locks/lockhandler.py b/evennia/locks/lockhandler.py index 30d17749a7..a8f5995b25 100644 --- a/evennia/locks/lockhandler.py +++ b/evennia/locks/lockhandler.py @@ -107,7 +107,6 @@ from __future__ import print_function from builtins import object import re -import inspect from django.conf import settings from evennia.utils import logger, utils from django.utils.translation import ugettext as _ @@ -370,13 +369,13 @@ class LockHandler(object): def all(self): """ - Return all lockstrings. + Return all lockstrings Returns: - lockstring (str): The full lockstring + lockstrings (list): All separate lockstrings """ - return self.get() + return str(self).split(';') def remove(self, access_type): """ diff --git a/evennia/objects/objects.py b/evennia/objects/objects.py index bbc517ba59..6db36f7299 100644 --- a/evennia/objects/objects.py +++ b/evennia/objects/objects.py @@ -7,7 +7,7 @@ entities. """ import time from builtins import object -from future.utils import listvalues, with_metaclass +from future.utils import with_metaclass from django.conf import settings diff --git a/evennia/scripts/monitorhandler.py b/evennia/scripts/monitorhandler.py index 6c1c268701..af6ae4fd61 100644 --- a/evennia/scripts/monitorhandler.py +++ b/evennia/scripts/monitorhandler.py @@ -93,6 +93,7 @@ class MonitorHandler(object): def at_update(self, obj, fieldname): """ Called by the field as it saves. + """ to_delete = [] if obj in self.monitors and fieldname in self.monitors[obj]: @@ -175,6 +176,9 @@ class MonitorHandler(object): """ List all monitors. + Returns: + monitors (list): The handled monitors. + """ output = [] for obj in self.monitors: diff --git a/evennia/scripts/scripthandler.py b/evennia/scripts/scripthandler.py index 33ce302731..375f602d2b 100644 --- a/evennia/scripts/scripthandler.py +++ b/evennia/scripts/scripthandler.py @@ -109,7 +109,7 @@ class ScriptHandler(object): scripts (list): The found scripts matching `key`. """ - return ScriptDB.objects.get_all_scripts_on_obj(self.obj, key=key) + return list(ScriptDB.objects.get_all_scripts_on_obj(self.obj, key=key)) def delete(self, key=None): """ diff --git a/evennia/scripts/taskhandler.py b/evennia/scripts/taskhandler.py index 1849134d35..27dc702142 100644 --- a/evennia/scripts/taskhandler.py +++ b/evennia/scripts/taskhandler.py @@ -6,7 +6,7 @@ from datetime import datetime, timedelta from twisted.internet import reactor, task from evennia.server.models import ServerConfig -from evennia.utils.logger import log_trace, log_err +from evennia.utils.logger import log_err from evennia.utils.dbserialize import dbserialize, dbunserialize TASK_HANDLER = None @@ -113,9 +113,9 @@ class TaskHandler(object): try: dbserialize(arg) except (TypeError, AttributeError): - logger.log_err("The positional argument {} cannot be " - "pickled and will not be present in the arguments " - "fed to the callback {}".format(arg, callback)) + log_err("The positional argument {} cannot be " + "pickled and will not be present in the arguments " + "fed to the callback {}".format(arg, callback)) else: safe_args.append(arg) @@ -123,9 +123,9 @@ class TaskHandler(object): try: dbserialize(value) except (TypeError, AttributeError): - logger.log_err("The {} keyword argument {} cannot be " - "pickled and will not be present in the arguments " - "fed to the callback {}".format(key, value, callback)) + log_err("The {} keyword argument {} cannot be " + "pickled and will not be present in the arguments " + "fed to the callback {}".format(key, value, callback)) else: safe_kwargs[key] = value diff --git a/evennia/typeclasses/attributes.py b/evennia/typeclasses/attributes.py index 6b35ad7607..84a21c0c27 100644 --- a/evennia/typeclasses/attributes.py +++ b/evennia/typeclasses/attributes.py @@ -387,7 +387,7 @@ class AttributeHandler(object): def get(self, key=None, default=None, category=None, return_obj=False, strattr=False, raise_exception=False, accessing_obj=None, - default_access=True): + default_access=True, return_list=False): """ Get the Attribute. @@ -398,7 +398,8 @@ class AttributeHandler(object): category (str, optional): the category within which to retrieve attribute(s). default (any, optional): The value to return if an - Attribute was not defined. + Attribute was not defined. If set, it will be returned in + a one-item list. return_obj (bool, optional): If set, the return is not the value of the Attribute but the Attribute object itself. strattr (bool, optional): Return the `strvalue` field of @@ -410,13 +411,15 @@ class AttributeHandler(object): accessing_obj (object, optional): If set, an `attrread` permission lock will be checked before returning each looked-after Attribute. - default_access (bool, optional): + default_access (bool, optional): If no `attrread` lock is set on + object, this determines if the lock should then be passed or not. + return_list (bool, optional): Returns: - result (any, Attribute or list): This will be the value of the found - Attribute unless `return_obj` is True, at which point it will be - the attribute object or None. If multiple keys are given, this - will be a list of values or attribute objects/None. + result (any or list): One or more matches for keys and/or categories. Each match will be + the value of the found Attribute(s) unless `return_obj` is True, at which point it + will be the attribute object itself or None. If `return_list` is True, this will + always be a list, regardless of the number of elements. Raises: AttributeError: If `raise_exception` is set and no matching Attribute @@ -453,7 +456,10 @@ class AttributeHandler(object): ret = ret if return_obj else [attr.strvalue for attr in ret if attr] else: ret = ret if return_obj else [attr.value for attr in ret if attr] - if not ret: + + if return_list: + return ret if ret else [default] if default is not None else [] + elif not ret: return ret if len(key) > 1 else default return ret[0] if len(ret) == 1 else ret diff --git a/evennia/typeclasses/tags.py b/evennia/typeclasses/tags.py index b3eb433ee9..e7c3bfdbb1 100644 --- a/evennia/typeclasses/tags.py +++ b/evennia/typeclasses/tags.py @@ -251,6 +251,8 @@ class TagHandler(object): """ if not tag: return + if not self._cache_complete: + self._fullcache() for tagstr in make_iter(tag): if not tagstr: continue @@ -265,7 +267,7 @@ class TagHandler(object): getattr(self.obj, self._m2m_fieldname).add(tagobj) self._setcache(tagstr, category, tagobj) - def get(self, key=None, default=None, category=None, return_tagobj=False): + def get(self, key=None, default=None, category=None, return_tagobj=False, return_list=False): """ Get the tag for the given key or list of tags. @@ -277,11 +279,14 @@ class TagHandler(object): category. return_tagobj (bool, optional): Return the Tag object itself instead of a string representation of the Tag. + return_list (bool, optional): Always return a list, regardless + of number of matches. Returns: - tags (str, TagObject or list): The matches, either string + tags (list): The matches, either string representations of the tags or the Tag objects themselves - depending on `return_tagobj`. + depending on `return_tagobj`. If 'default' is set, this + will be a list with the default value as its only element. """ ret = [] @@ -289,6 +294,8 @@ class TagHandler(object): # note - the _getcache call removes case sensitivity for us ret.extend([tag if return_tagobj else to_str(tag.db_key) for tag in self._getcache(keystr, category)]) + if return_list: + return ret if ret else [default] if default is not None else [] return ret[0] if len(ret) == 1 else (ret if ret else default) def remove(self, key, category=None): @@ -327,6 +334,8 @@ class TagHandler(object): category. """ + if not self._cache_complete: + self._fullcache() query = {"%s__id" % self._model: self._objid, "tag__db_model": self._model, "tag__db_tagtype": self._tagtype} if category: query["tag__db_category"] = category.strip().lower() diff --git a/evennia/utils/dbserialize.py b/evennia/utils/dbserialize.py index 6b216789a3..2735aa6da0 100644 --- a/evennia/utils/dbserialize.py +++ b/evennia/utils/dbserialize.py @@ -167,7 +167,6 @@ class _SaverMutable(object): non_saver_name = cls_name raise ValueError(_ERROR_DELETED_ATTR.format(cls_name=cls_name, obj=self, non_saver_name=non_saver_name)) - print("self._db_obj.pk") self._db_obj.value = self else: logger.log_err("_SaverMutable %s has no root Attribute to save to." % self) diff --git a/evennia/utils/prettytable.py b/evennia/utils/prettytable.py deleted file mode 100644 index c13e07797f..0000000000 --- a/evennia/utils/prettytable.py +++ /dev/null @@ -1,1564 +0,0 @@ -#!/usr/bin/env python -# -# Copyright (c) 2009-2013, Luke Maurits -# All rights reserved. -# With contributions from: -# * Chris Clark -# * Klein Stephane -# -# Redistribution and use in source and binary forms, with or without -# modification, are permitted provided that the following conditions are met: -# -# * Redistributions of source code must retain the above copyright notice, -# this list of conditions and the following disclaimer. -# * Redistributions in binary form must reproduce the above copyright notice, -# this list of conditions and the following disclaimer in the documentation -# and/or other materials provided with the distribution. -# * The name of the author may not be used to endorse or promote products -# derived from this software without specific prior written permission. -# -# THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" -# AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE -# IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE -# ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE -# LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR -# CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF -# SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS -# INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN -# CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) -# ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE -# POSSIBILITY OF SUCH DAMAGE. - -from __future__ import print_function -from builtins import object, range - -__version__ = "trunk" - -import copy -import csv -import random -import re -import sys -import textwrap -import itertools -import unicodedata - -from evennia.utils.ansi import parse_ansi - -py3k = sys.version_info[0] >= 3 -if py3k: - unicode = str - basestring = str - itermap = map - iterzip = zip - uni_chr = chr - from html.parser import HTMLParser -else: - itermap = itertools.imap - iterzip = itertools.izip - uni_chr = unichr - from HTMLParser import HTMLParser - -if py3k and sys.version_info[1] >= 2: - from html import escape -else: - from cgi import escape - -# hrule styles -FRAME, ALL, NONE, HEADER = range(4) - -# Table styles -DEFAULT = 10 -MSWORD_FRIENDLY = 11 -PLAIN_COLUMNS = 12 -RANDOM = 20 - -_re = re.compile("\033\[[0-9;]*m") - - -def _ansi(method): - """decorator for converting ansi in input""" - - def wrapper(self, *args, **kwargs): - def convert(inp): - if isinstance(inp, basestring): - return parse_ansi("|n%s|n" % inp) - elif hasattr(inp, '__iter__'): - li = [] - for element in inp: - if isinstance(element, basestring): - li.append(convert(element)) - elif hasattr(element, '__iter__'): - li.append(convert(element)) - else: - li.append(element) - return li - return inp - args = [convert(arg) for arg in args] - # kwargs = dict((key, convert(val)) for key, val in kwargs.items()) - return method(self, *args, **kwargs) - return wrapper - - -def _get_size(text): - lines = text.split("\n") - height = len(lines) - width = max([_str_block_width(line) for line in lines]) - return width, height - - -class PrettyTable(object): - - @_ansi - def __init__(self, field_names=None, **kwargs): - """Return a new PrettyTable instance - - Arguments: - - encoding - Unicode encoding scheme used to decode any encoded input - field_names - list or tuple of field names - fields - list or tuple of field names to include in displays - start - index of first data row to include in output - end - index of last data row to include in output PLUS ONE (list slice style) - header - print a header showing field names (True or False) - header_style - stylisation to apply to field names in header ("cap", "title", "upper", "lower" or None) - border - print a border around the table (True or False) - hrules - controls printing of horizontal rules after rows. Allowed values: FRAME, HEADER, ALL, NONE - vrules - controls printing of vertical rules between columns. Allowed values: FRAME, ALL, NONE - int_format - controls formatting of integer data - float_format - controls formatting of floating point data - padding_width - number of spaces on either side of column data (only used if left and right paddings are None) - left_padding_width - number of spaces on left hand side of column data - right_padding_width - number of spaces on right hand side of column data - vertical_char - single character string used to draw vertical lines - horizontal_char - single character string used to draw horizontal lines - junction_char - single character string used to draw line junctions - sortby - name of field to sort rows by - sort_key - sorting key function, applied to data points before sorting - valign - default valign for each row (None, "t", "m" or "b") - reversesort - True or False to sort in descending or ascending order""" - - self.encoding = kwargs.get("encoding", "UTF-8") - - # Data - self._field_names = [] - self._align = {} - self._valign = {} - self._max_width = {} - self._rows = [] - if field_names: - self.field_names = field_names - else: - self._widths = [] - - # Options - self._options = "start end fields header border sortby reversesort" \ - " sort_key attributes format hrules vrules".split() - self._options.extend("int_format float_format padding_width " - "left_padding_width right_padding_width".split()) - self._options.extend("vertical_char horizontal_char junction_char" - " header_style valign xhtml print_empty".split()) - for option in self._options: - if option in kwargs: - self._validate_option(option, kwargs[option]) - else: - kwargs[option] = None - - self._start = kwargs["start"] or 0 - self._end = kwargs["end"] or None - self._fields = kwargs["fields"] or None - - if kwargs["header"] in (True, False): - self._header = kwargs["header"] - else: - self._header = True - self._header_style = kwargs["header_style"] or None - if kwargs["border"] in (True, False): - self._border = kwargs["border"] - else: - self._border = True - self._hrules = kwargs["hrules"] or FRAME - self._vrules = kwargs["vrules"] or ALL - - self._sortby = kwargs["sortby"] or None - if kwargs["reversesort"] in (True, False): - self._reversesort = kwargs["reversesort"] - else: - self._reversesort = False - self._sort_key = kwargs["sort_key"] or (lambda x: x) - - self._int_format = kwargs["int_format"] or {} - self._float_format = kwargs["float_format"] or {} - self._padding_width = kwargs["padding_width"] or 1 - self._left_padding_width = kwargs["left_padding_width"] or None - self._right_padding_width = kwargs["right_padding_width"] or None - - self._vertical_char = kwargs["vertical_char"] or self._unicode("|") - self._horizontal_char = kwargs["horizontal_char"] or self._unicode("-") - self._junction_char = kwargs["junction_char"] or self._unicode("+") - - if kwargs["print_empty"] in (True, False): - self._print_empty = kwargs["print_empty"] - else: - self._print_empty = True - self._format = kwargs["format"] or False - self._xhtml = kwargs["xhtml"] or False - self._attributes = kwargs["attributes"] or {} - - def _unicode(self, value): - if not isinstance(value, basestring): - value = str(value) - if not isinstance(value, unicode): - value = unicode(value, self.encoding, "strict") - return value - - def _justify(self, text, width, align): - excess = width - _str_block_width(text) - if align == "l": - return text + excess * " " - elif align == "r": - return excess * " " + text - else: - if excess % 2: - # Uneven padding - # Put more space on right if text is of odd length... - if _str_block_width(text) % 2: - return (excess // 2) * " " + text + (excess // 2 + 1) * " " - # and more space on left if text is of even length - else: - return (excess // 2 + 1) * " " + text + (excess // 2) * " " - # Why distribute extra space this way? To match the behaviour of - # the inbuilt str.center() method. - else: - # Equal padding on either side - return (excess // 2) * " " + text + (excess // 2) * " " - - def __getattr__(self, name): - - if name == "rowcount": - return len(self._rows) - elif name == "colcount": - if self._field_names: - return len(self._field_names) - elif self._rows: - return len(self._rows[0]) - else: - return 0 - else: - raise AttributeError(name) - - def __getitem__(self, index): - - new = PrettyTable() - new.field_names = self.field_names - for attr in self._options: - setattr(new, "_" + attr, getattr(self, "_" + attr)) - setattr(new, "_align", getattr(self, "_align")) - if isinstance(index, slice): - for row in self._rows[index]: - new.add_row(row) - elif isinstance(index, int): - new.add_row(self._rows[index]) - else: - raise Exception("Index %s is invalid, must be an integer or slice" % str(index)) - return new - - if py3k: - def __str__(self): - return self.__unicode__() - else: - def __str__(self): - return self.__unicode__().encode(self.encoding) - - def __unicode__(self): - return self.get_string() - - ############################## - # ATTRIBUTE VALIDATORS # - ############################## - - # The method _validate_option is all that should be used elsewhere in the code base to validate options. - # It will call the appropriate validation method for that option. The individual validation methods should - # never need to be called directly (although nothing bad will happen if they *are*). - # Validation happens in TWO places. - # Firstly, in the property setters defined in the ATTRIBUTE MANAGMENT section. - # Secondly, in the _get_options method, where keyword arguments are mixed with persistent settings - - def _validate_option(self, option, val): - if option in "field_names": - self._validate_field_names(val) - elif option in ("start", "end", "max_width", "padding_width", - "left_padding_width", "right_padding_width", "format"): - self._validate_nonnegative_int(option, val) - elif option in "sortby": - self._validate_field_name(option, val) - elif option in "sort_key": - self._validate_function(option, val) - elif option in "hrules": - self._validate_hrules(option, val) - elif option in "vrules": - self._validate_vrules(option, val) - elif option in "fields": - self._validate_all_field_names(option, val) - elif option in ("header", "border", "reversesort", "xhtml", "print_empty"): - self._validate_true_or_false(option, val) - elif option in "header_style": - self._validate_header_style(val) - elif option in "int_format": - self._validate_int_format(option, val) - elif option in "float_format": - self._validate_float_format(option, val) - elif option in ("vertical_char", "horizontal_char", "junction_char"): - self._validate_single_char(option, val) - elif option in "attributes": - self._validate_attributes(option, val) - else: - raise Exception("Unrecognised option: %s!" % option) - - def _validate_field_names(self, val): - # Check for appropriate length - if self._field_names: - try: - assert len(val) == len(self._field_names) - except AssertionError: - raise Exception("Field name list has incorrect number of values, (actual) %d!=%d (expected)" - % (len(val), len(self._field_names))) - if self._rows: - try: - assert len(val) == len(self._rows[0]) - except AssertionError: - raise Exception("Field name list has incorrect number of values, (actual) %d!=%d (expected)" - % (len(val), len(self._rows[0]))) - # Check for uniqueness - try: - assert len(val) == len(set(val)) - except AssertionError: - raise Exception("Field names must be unique!") - - def _validate_header_style(self, val): - try: - assert val in ("cap", "title", "upper", "lower", None) - except AssertionError: - raise Exception("Invalid header style, use cap, title, upper, lower or None!") - - def _validate_align(self, val): - try: - assert val in ["l", "c", "r"] - except AssertionError: - raise Exception("Alignment %s is invalid, use l, c or r!" % val) - - def _validate_valign(self, val): - try: - assert val in ["t", "m", "b", None] - except AssertionError: - raise Exception("Alignment %s is invalid, use t, m, b or None!" % val) - - def _validate_nonnegative_int(self, name, val): - try: - assert int(val) >= 0 - except AssertionError: - raise Exception("Invalid value for %s: %s!" % (name, self._unicode(val))) - - def _validate_true_or_false(self, name, val): - try: - assert val in (True, False) - except AssertionError: - raise Exception("Invalid value for %s! Must be True or False." % name) - - def _validate_int_format(self, name, val): - if val == "": - return - try: - assert type(val) in (str, unicode) - assert val.isdigit() - except AssertionError: - raise Exception("Invalid value for %s! Must be an integer format string." % name) - - def _validate_float_format(self, name, val): - if val == "": - return - try: - assert type(val) in (str, unicode) - assert "." in val - bits = val.split(".") - assert len(bits) <= 2 - assert bits[0] == "" or bits[0].isdigit() - assert bits[1] == "" or bits[1].isdigit() - except AssertionError: - raise Exception("Invalid value for %s! Must be a float format string." % name) - - def _validate_function(self, name, val): - try: - assert hasattr(val, "__call__") - except AssertionError: - raise Exception("Invalid value for %s! Must be a function." % name) - - def _validate_hrules(self, name, val): - try: - assert val in (ALL, FRAME, HEADER, NONE) - except AssertionError: - raise Exception("Invalid value for %s! Must be ALL, FRAME, HEADER or NONE." % name) - - def _validate_vrules(self, name, val): - try: - assert val in (ALL, FRAME, NONE) - except AssertionError: - raise Exception("Invalid value for %s! Must be ALL, FRAME, or NONE." % name) - - def _validate_field_name(self, name, val): - try: - assert (val in self._field_names) or (val is None) - except AssertionError: - raise Exception("Invalid field name: %s!" % val) - - def _validate_all_field_names(self, name, val): - try: - for x in val: - self._validate_field_name(name, x) - except AssertionError: - raise Exception("fields must be a sequence of field names!") - - def _validate_single_char(self, name, val): - try: - assert _str_block_width(val) == 1 - except AssertionError: - raise Exception("Invalid value for %s! Must be a string of length 1." % name) - - def _validate_attributes(self, name, val): - try: - assert isinstance(val, dict) - except AssertionError: - raise Exception("attributes must be a dictionary of name/value pairs!") - - ############################## - # ATTRIBUTE MANAGEMENT # - ############################## - - def _get_field_names(self): - """The names of the fields - - Arguments: - - fields - list or tuple of field names""" - return self._field_names - - def _set_field_names(self, val): - val = [self._unicode(x) for x in val] - self._validate_option("field_names", val) - old_names = [] - if self._field_names: - old_names = self._field_names[:] - self._field_names = val - if self._align and old_names: - for old_name, new_name in zip(old_names, val): - self._align[new_name] = self._align[old_name] - for old_name in old_names: - if old_name not in self._align: - self._align.pop(old_name) - else: - for field in self._field_names: - self._align[field] = "l" - if self._valign and old_names: - for old_name, new_name in zip(old_names, val): - self._valign[new_name] = self._valign[old_name] - for old_name in old_names: - if old_name not in self._valign: - self._valign.pop(old_name) - else: - for field in self._field_names: - self._valign[field] = "t" - - field_names = property(_get_field_names, _set_field_names) - - def _get_align(self): - return self._align - - def _set_align(self, val): - self._validate_align(val) - for field in self._field_names: - self._align[field] = val - - align = property(_get_align, _set_align) - - def _get_valign(self): - return self._valign - - def _set_valign(self, val): - self._validate_valign(val) - for field in self._field_names: - self._valign[field] = val - - valign = property(_get_valign, _set_valign) - - def _get_max_width(self): - return self._max_width - - def _set_max_width(self, val): - self._validate_option("max_width", val) - for field in self._field_names: - self._max_width[field] = val - - max_width = property(_get_max_width, _set_max_width) - - def _get_fields(self): - """List or tuple of field names to include in displays - - Arguments: - - fields - list or tuple of field names to include in displays""" - return self._fields - - def _set_fields(self, val): - self._validate_option("fields", val) - self._fields = val - - fields = property(_get_fields, _set_fields) - - def _get_start(self): - """Start index of the range of rows to print - - Arguments: - - start - index of first data row to include in output""" - return self._start - - def _set_start(self, val): - self._validate_option("start", val) - self._start = val - - start = property(_get_start, _set_start) - - def _get_end(self): - """End index of the range of rows to print - - Arguments: - - end - index of last data row to include in output PLUS ONE (list slice style)""" - return self._end - - def _set_end(self, val): - self._validate_option("end", val) - self._end = val - - end = property(_get_end, _set_end) - - def _get_sortby(self): - """Name of field by which to sort rows - - Arguments: - - sortby - field name to sort by""" - return self._sortby - - def _set_sortby(self, val): - self._validate_option("sortby", val) - self._sortby = val - - sortby = property(_get_sortby, _set_sortby) - - def _get_reversesort(self): - """Controls direction of sorting (ascending vs descending) - - Arguments: - - reveresort - set to True to sort by descending order, or False to sort by ascending order""" - return self._reversesort - - def _set_reversesort(self, val): - self._validate_option("reversesort", val) - self._reversesort = val - - reversesort = property(_get_reversesort, _set_reversesort) - - def _get_sort_key(self): - """Sorting key function, applied to data points before sorting - - Arguments: - - sort_key - a function which takes one argument and returns something to be sorted""" - return self._sort_key - - def _set_sort_key(self, val): - self._validate_option("sort_key", val) - self._sort_key = val - - sort_key = property(_get_sort_key, _set_sort_key) - - def _get_header(self): - """Controls printing of table header with field names - - Arguments: - - header - print a header showing field names (True or False)""" - return self._header - - def _set_header(self, val): - self._validate_option("header", val) - self._header = val - - header = property(_get_header, _set_header) - - def _get_header_style(self): - """Controls stylisation applied to field names in header - - Arguments: - - header_style - stylisation to apply to field names in header ("cap", "title", "upper", "lower" or None)""" - return self._header_style - - def _set_header_style(self, val): - self._validate_header_style(val) - self._header_style = val - - header_style = property(_get_header_style, _set_header_style) - - def _get_border(self): - """Controls printing of border around table - - Arguments: - - border - print a border around the table (True or False)""" - return self._border - - def _set_border(self, val): - self._validate_option("border", val) - self._border = val - - border = property(_get_border, _set_border) - - def _get_hrules(self): - """Controls printing of horizontal rules after rows - - Arguments: - - hrules - horizontal rules style. Allowed values: FRAME, ALL, HEADER, NONE""" - return self._hrules - - def _set_hrules(self, val): - self._validate_option("hrules", val) - self._hrules = val - - hrules = property(_get_hrules, _set_hrules) - - def _get_vrules(self): - """Controls printing of vertical rules between columns - - Arguments: - - vrules - vertical rules style. Allowed values: FRAME, ALL, NONE""" - return self._vrules - - def _set_vrules(self, val): - self._validate_option("vrules", val) - self._vrules = val - - vrules = property(_get_vrules, _set_vrules) - - def _get_int_format(self): - """Controls formatting of integer data - Arguments: - - int_format - integer format string""" - return self._int_format - - def _set_int_format(self, val): - # self._validate_option("int_format", val) - for field in self._field_names: - self._int_format[field] = val - - int_format = property(_get_int_format, _set_int_format) - - def _get_float_format(self): - """Controls formatting of floating point data - Arguments: - - float_format - floating point format string""" - return self._float_format - - def _set_float_format(self, val): - # self._validate_option("float_format", val) - for field in self._field_names: - self._float_format[field] = val - - float_format = property(_get_float_format, _set_float_format) - - def _get_padding_width(self): - """The number of empty spaces between a column's edge and its content - - Arguments: - - padding_width - number of spaces, must be a positive integer""" - return self._padding_width - - def _set_padding_width(self, val): - self._validate_option("padding_width", val) - self._padding_width = val - - padding_width = property(_get_padding_width, _set_padding_width) - - def _get_left_padding_width(self): - """The number of empty spaces between a column's left edge and its content - - Arguments: - - left_padding - number of spaces, must be a positive integer""" - return self._left_padding_width - - def _set_left_padding_width(self, val): - self._validate_option("left_padding_width", val) - self._left_padding_width = val - - left_padding_width = property(_get_left_padding_width, _set_left_padding_width) - - def _get_right_padding_width(self): - """The number of empty spaces between a column's right edge and its content - - Arguments: - - right_padding - number of spaces, must be a positive integer""" - return self._right_padding_width - - def _set_right_padding_width(self, val): - self._validate_option("right_padding_width", val) - self._right_padding_width = val - - right_padding_width = property(_get_right_padding_width, _set_right_padding_width) - - def _get_vertical_char(self): - """The charcter used when printing table borders to draw vertical lines - - Arguments: - - vertical_char - single character string used to draw vertical lines""" - return self._vertical_char - - def _set_vertical_char(self, val): - val = self._unicode(val) - self._validate_option("vertical_char", val) - self._vertical_char = val - - vertical_char = property(_get_vertical_char, _set_vertical_char) - - def _get_horizontal_char(self): - """The charcter used when printing table borders to draw horizontal lines - - Arguments: - - horizontal_char - single character string used to draw horizontal lines""" - return self._horizontal_char - - def _set_horizontal_char(self, val): - val = self._unicode(val) - self._validate_option("horizontal_char", val) - self._horizontal_char = val - - horizontal_char = property(_get_horizontal_char, _set_horizontal_char) - - def _get_junction_char(self): - """The charcter used when printing table borders to draw line junctions - - Arguments: - - junction_char - single character string used to draw line junctions""" - return self._junction_char - - def _set_junction_char(self, val): - val = self._unicode(val) - self._validate_option("vertical_char", val) - self._junction_char = val - - junction_char = property(_get_junction_char, _set_junction_char) - - def _get_format(self): - """Controls whether or not HTML tables are formatted to match styling options - - Arguments: - - format - True or False""" - return self._format - - def _set_format(self, val): - self._validate_option("format", val) - self._format = val - - format = property(_get_format, _set_format) - - def _get_print_empty(self): - """Controls whether or not empty tables produce a header and frame or just an empty string - - Arguments: - - print_empty - True or False""" - return self._print_empty - - def _set_print_empty(self, val): - self._validate_option("print_empty", val) - self._print_empty = val - - print_empty = property(_get_print_empty, _set_print_empty) - - def _get_attributes(self): - """A dictionary of HTML attribute name/value pairs to be included in the tag when printing HTML - - Arguments: - - attributes - dictionary of attributes""" - return self._attributes - - def _set_attributes(self, val): - self._validate_option("attributes", val) - self._attributes = val - - attributes = property(_get_attributes, _set_attributes) - - ############################## - # OPTION MIXER # - ############################## - - def _get_options(self, kwargs): - - options = {} - for option in self._options: - if option in kwargs: - self._validate_option(option, kwargs[option]) - options[option] = kwargs[option] - else: - options[option] = getattr(self, "_" + option) - return options - - ############################## - # PRESET STYLE LOGIC # - ############################## - - def set_style(self, style): - - if style == DEFAULT: - self._set_default_style() - elif style == MSWORD_FRIENDLY: - self._set_msword_style() - elif style == PLAIN_COLUMNS: - self._set_columns_style() - elif style == RANDOM: - self._set_random_style() - else: - raise Exception("Invalid pre-set style!") - - def _set_default_style(self): - - self.header = True - self.border = True - self._hrules = FRAME - self._vrules = ALL - self.padding_width = 1 - self.left_padding_width = 1 - self.right_padding_width = 1 - self.vertical_char = "|" - self.horizontal_char = "-" - self.junction_char = "+" - - def _set_msword_style(self): - - self.header = True - self.border = True - self._hrules = NONE - self.padding_width = 1 - self.left_padding_width = 1 - self.right_padding_width = 1 - self.vertical_char = "|" - - def _set_columns_style(self): - - self.header = True - self.border = False - self.padding_width = 1 - self.left_padding_width = 0 - self.right_padding_width = 8 - - def _set_random_style(self): - - # Just for fun! - self.header = random.choice((True, False)) - self.border = random.choice((True, False)) - self._hrules = random.choice((ALL, FRAME, HEADER, NONE)) - self._vrules = random.choice((ALL, FRAME, NONE)) - self.left_padding_width = random.randint(0, 5) - self.right_padding_width = random.randint(0, 5) - self.vertical_char = random.choice("~!@#$%^&*()_+|-=\{}[];':\",./;<>?") - self.horizontal_char = random.choice("~!@#$%^&*()_+|-=\{}[];':\",./;<>?") - self.junction_char = random.choice("~!@#$%^&*()_+|-=\{}[];':\",./;<>?") - - ############################## - # DATA INPUT METHODS # - ############################## - - @_ansi - def add_row(self, row): - """Add a row to the table - - Arguments: - - row - row of data, should be a list with as many elements as the table - has fields""" - - if self._field_names and len(row) != len(self._field_names): - raise Exception("Row has incorrect number of values, (actual) %d!=%d (expected)" - % (len(row), len(self._field_names))) - if not self._field_names: - self.field_names = [("Field %d" % (n + 1)) for n in range(0, len(row))] - self._rows.append(list(row)) - - def del_row(self, row_index): - """Delete a row to the table - - Arguments: - - row_index - The index of the row you want to delete. Indexing starts at 0.""" - - if row_index > len(self._rows) - 1: - raise Exception("Can't delete row at index %d, table only has %d rows!" % (row_index, len(self._rows))) - del self._rows[row_index] - - @_ansi - def add_column(self, fieldname, column, align="l", valign="t"): - """Add a column to the table. - - Arguments: - - fieldname - name of the field to contain the new column of data - column - column of data, should be a list with as many elements as the - table has rows - align - desired alignment for this column - "l" for left, "c" for centre and "r" for right - valign - desired vertical alignment for new columns - "t" for top, "m" for middle and "b" for bottom""" - - if len(self._rows) in (0, len(column)): - self._validate_align(align) - self._validate_valign(valign) - self._field_names.append(fieldname) - self._align[fieldname] = align - self._valign[fieldname] = valign - for i in range(0, len(column)): - if len(self._rows) < i + 1: - self._rows.append([]) - self._rows[i].append(column[i]) - else: - raise Exception("Column length %d does not match number of rows %d!" % (len(column), len(self._rows))) - - def clear_rows(self): - """Delete all rows from the table but keep the current field names""" - - self._rows = [] - - def clear(self): - """Delete all rows and field names from the table, maintaining nothing but styling options""" - - self._rows = [] - self._field_names = [] - self._widths = [] - - ############################## - # MISC PUBLIC METHODS # - ############################## - - def copy(self): - return copy.deepcopy(self) - - ############################## - # MISC PRIVATE METHODS # - ############################## - - def _format_value(self, field, value): - if isinstance(value, int) and field in self._int_format: - value = self._unicode(("%%%sd" % self._int_format[field]) % value) - elif isinstance(value, float) and field in self._float_format: - value = self._unicode(("%%%sf" % self._float_format[field]) % value) - return self._unicode(value) - - def _compute_widths(self, rows, options): - if options["header"]: - widths = [_get_size(field)[0] for field in self._field_names] - else: - widths = len(self.field_names) * [0] - for row in rows: - for index, value in enumerate(row): - fieldname = self.field_names[index] - if fieldname in self.max_width: - widths[index] = max(widths[index], min(_get_size(value)[0], self.max_width[fieldname])) - else: - widths[index] = max(widths[index], _get_size(value)[0]) - self._widths = widths - - def _get_padding_widths(self, options): - - if options["left_padding_width"] is not None: - lpad = options["left_padding_width"] - else: - lpad = options["padding_width"] - if options["right_padding_width"] is not None: - rpad = options["right_padding_width"] - else: - rpad = options["padding_width"] - return lpad, rpad - - def _get_rows(self, options): - """Return only those data rows that should be printed, based on slicing and sorting. - - Arguments: - - options - dictionary of option settings.""" - - # Make a copy of only those rows in the slice range - rows = copy.deepcopy(self._rows[options["start"]:options["end"]]) - # Sort if necessary - if options["sortby"]: - sortindex = self._field_names.index(options["sortby"]) - # Decorate - rows = [[row[sortindex]] + row for row in rows] - # Sort - rows.sort(reverse=options["reversesort"], key=options["sort_key"]) - # Undecorate - rows = [row[1:] for row in rows] - return rows - - def _format_row(self, row, options): - return [self._format_value(field, value) for (field, value) in zip(self._field_names, row)] - - def _format_rows(self, rows, options): - return [self._format_row(row, options) for row in rows] - - ############################## - # PLAIN TEXT STRING METHODS # - ############################## - - def get_string(self, **kwargs): - """Return string representation of table in current state. - - Arguments: - - start - index of first data row to include in output - end - index of last data row to include in output PLUS ONE (list slice style) - fields - names of fields (columns) to include - header - print a header showing field names (True or False) - border - print a border around the table (True or False) - hrules - controls printing of horizontal rules after rows. Allowed values: ALL, FRAME, HEADER, NONE - vrules - controls printing of vertical rules between columns. Allowed values: FRAME, ALL, NONE - int_format - controls formatting of integer data - float_format - controls formatting of floating point data - padding_width - number of spaces on either side of column data (only used if left and right paddings are None) - left_padding_width - number of spaces on left hand side of column data - right_padding_width - number of spaces on right hand side of column data - vertical_char - single character string used to draw vertical lines - horizontal_char - single character string used to draw horizontal lines - junction_char - single character string used to draw line junctions - sortby - name of field to sort rows by - sort_key - sorting key function, applied to data points before sorting - reversesort - True or False to sort in descending or ascending order - print empty - if True, stringify just the header for an empty table, if False return an empty string """ - - options = self._get_options(kwargs) - - lines = [] - - # Don't think too hard about an empty table - # Is this the desired behaviour? Maybe we should still print the header? - if self.rowcount == 0 and (not options["print_empty"] or not options["border"]): - return "" - - # Get the rows we need to print, taking into account slicing, sorting, etc. - rows = self._get_rows(options) - - # Turn all data in all rows into Unicode, formatted as desired - formatted_rows = self._format_rows(rows, options) - - # Compute column widths - self._compute_widths(formatted_rows, options) - - # Add header or top of border - self._hrule = self._stringify_hrule(options) - if options["header"]: - lines.append(self._stringify_header(options)) - elif options["border"] and options["hrules"] in (ALL, FRAME): - lines.append(self._hrule) - - # Add rows - for row in formatted_rows: - lines.append(self._stringify_row(row, options)) - - # Add bottom of border - if options["border"] and options["hrules"] == FRAME: - lines.append(self._hrule) - - return self._unicode("\n").join(lines) - - def _stringify_hrule(self, options): - - if not options["border"]: - return "" - lpad, rpad = self._get_padding_widths(options) - if options['vrules'] in (ALL, FRAME): - bits = [options["junction_char"]] - else: - bits = [options["horizontal_char"]] - # For tables with no data or fieldnames - if not self._field_names: - bits.append(options["junction_char"]) - return "".join(bits) - for field, width in zip(self._field_names, self._widths): - if options["fields"] and field not in options["fields"]: - continue - bits.append((width + lpad + rpad) * options["horizontal_char"]) - if options['vrules'] == ALL: - bits.append(options["junction_char"]) - else: - bits.append(options["horizontal_char"]) - if options["vrules"] == FRAME: - bits.pop() - bits.append(options["junction_char"]) - return "".join(bits) - - def _stringify_header(self, options): - - bits = [] - lpad, rpad = self._get_padding_widths(options) - if options["border"]: - if options["hrules"] in (ALL, FRAME): - bits.append(self._hrule) - bits.append("\n") - if options["vrules"] in (ALL, FRAME): - bits.append(options["vertical_char"]) - else: - bits.append(" ") - # For tables with no data or field names - if not self._field_names: - if options["vrules"] in (ALL, FRAME): - bits.append(options["vertical_char"]) - else: - bits.append(" ") - for field, width, in zip(self._field_names, self._widths): - if options["fields"] and field not in options["fields"]: - continue - if self._header_style == "cap": - fieldname = field.capitalize() - elif self._header_style == "title": - fieldname = field.title() - elif self._header_style == "upper": - fieldname = field.upper() - elif self._header_style == "lower": - fieldname = field.lower() - else: - fieldname = field - bits.append(" " * lpad + self._justify(fieldname, width, self._align[field]) + " " * rpad) - if options["border"]: - if options["vrules"] == ALL: - bits.append(options["vertical_char"]) - else: - bits.append(" ") - # If vrules is FRAME, then we just appended a space at the end - # of the last field, when we really want a vertical character - if options["border"] and options["vrules"] == FRAME: - bits.pop() - bits.append(options["vertical_char"]) - if options["border"] and options["hrules"] != NONE: - bits.append("\n") - bits.append(self._hrule) - return "".join(bits) - - def _stringify_row(self, row, options): - - for index, field, value, width, in zip(range(0, len(row)), self._field_names, row, self._widths): - # Enforce max widths - lines = value.split("\n") - new_lines = [] - for line in lines: - if _str_block_width(line) > width: - line = textwrap.fill(line, width) - new_lines.append(line) - lines = new_lines - value = "\n".join(lines) - row[index] = value - - row_height = 0 - for c in row: - h = _get_size(c)[1] - if h > row_height: - row_height = h - - bits = [] - lpad, rpad = self._get_padding_widths(options) - for y in range(0, row_height): - bits.append([]) - if options["border"]: - if options["vrules"] in (ALL, FRAME): - bits[y].append(self.vertical_char) - else: - bits[y].append(" ") - - for field, value, width, in zip(self._field_names, row, self._widths): - - valign = self._valign[field] - lines = value.split("\n") - dheight = row_height - len(lines) - if dheight: - if valign == "m": - lines = [""] * (dheight // 2) + lines + [""] * (dheight - (dheight // 2)) - elif valign == "b": - lines = [""] * dheight + lines - else: - lines += [""] * dheight - - y = 0 - for l in lines: - if options["fields"] and field not in options["fields"]: - continue - - bits[y].append(" " * lpad + self._justify(l, width, self._align[field]) + " " * rpad) - if options["border"]: - if options["vrules"] == ALL: - bits[y].append(self.vertical_char) - else: - bits[y].append(" ") - y += 1 - - # If vrules is FRAME, then we just appended a space at the end - # of the last field, when we really want a vertical character - for y in range(0, row_height): - if options["border"] and options["vrules"] == FRAME: - bits[y].pop() - bits[y].append(options["vertical_char"]) - - if options["border"] and options["hrules"] == ALL: - bits[row_height - 1].append("\n") - bits[row_height - 1].append(self._hrule) - - for y in range(0, row_height): - bits[y] = "".join(bits[y]) - - return "\n".join(bits) - - ############################## - # HTML STRING METHODS # - ############################## - - def get_html_string(self, **kwargs): - """Return string representation of HTML formatted version of table in current state. - - Arguments: - - start - index of first data row to include in output - end - index of last data row to include in output PLUS ONE (list slice style) - fields - names of fields (columns) to include - header - print a header showing field names (True or False) - border - print a border around the table (True or False) - hrules - controls printing of horizontal rules after rows. Allowed values: ALL, FRAME, HEADER, NONE - vrules - controls printing of vertical rules between columns. Allowed values: FRAME, ALL, NONE - int_format - controls formatting of integer data - float_format - controls formatting of floating point data - padding_width - number of spaces on either side of column data (only used if left and right paddings are None) - left_padding_width - number of spaces on left hand side of column data - right_padding_width - number of spaces on right hand side of column data - sortby - name of field to sort rows by - sort_key - sorting key function, applied to data points before sorting - attributes - dictionary of name/value pairs to include as HTML attributes in the
tag - xhtml - print
tags if True,
tags if false""" - - options = self._get_options(kwargs) - - if options["format"]: - string = self._get_formatted_html_string(options) - else: - string = self._get_simple_html_string(options) - - return string - - def _get_simple_html_string(self, options): - - lines = [] - if options["xhtml"]: - linebreak = "
" - else: - linebreak = "
" - - open_tag = ["") - lines.append("".join(open_tag)) - - # Headers - if options["header"]: - lines.append(" ") - for field in self._field_names: - if options["fields"] and field not in options["fields"]: - continue - lines.append(" " % escape(field).replace("\n", linebreak)) - lines.append(" ") - - # Data - rows = self._get_rows(options) - formatted_rows = self._format_rows(rows, options) - for row in formatted_rows: - lines.append(" ") - for field, datum in zip(self._field_names, row): - if options["fields"] and field not in options["fields"]: - continue - lines.append(" " % escape(datum).replace("\n", linebreak)) - lines.append(" ") - - lines.append("
%s
%s
") - - return self._unicode("\n").join(lines) - - def _get_formatted_html_string(self, options): - - lines = [] - lpad, rpad = self._get_padding_widths(options) - if options["xhtml"]: - linebreak = "
" - else: - linebreak = "
" - - open_tag = ["") - lines.append("".join(open_tag)) - - # Headers - if options["header"]: - lines.append(" ") - for field in self._field_names: - if options["fields"] and field not in options["fields"]: - continue - lines.append(" %s" - % (lpad, rpad, escape(field).replace("\n", linebreak))) - lines.append(" ") - - # Data - rows = self._get_rows(options) - formatted_rows = self._format_rows(rows, options) - aligns = [] - valigns = [] - for field in self._field_names: - aligns.append(dict(l="left", r="right", c="center")[self._align[field]]) - valigns.append(dict(t="top", m="middle", b="bottom")[self._valign[field]]) - for row in formatted_rows: - lines.append(" ") - for field, datum, align, valign in zip(self._field_names, row, aligns, valigns): - if options["fields"] and field not in options["fields"]: - continue - lines.append(" %s" - % (lpad, rpad, align, valign, escape(datum).replace("\n", linebreak))) - lines.append(" ") - lines.append("") - - return self._unicode("\n").join(lines) - -############################## -# UNICODE WIDTH FUNCTIONS # -############################## - - -def _char_block_width(char): - # Basic Latin, which is probably the most common case - # if char in xrange(0x0021, 0x007e): - # if char >= 0x0021 and char <= 0x007e: - if 0x0021 <= char <= 0x007e: - return 1 - # Chinese, Japanese, Korean (common) - if 0x4e00 <= char <= 0x9fff: - return 2 - # Hangul - if 0xac00 <= char <= 0xd7af: - return 2 - # Combining? - if unicodedata.combining(uni_chr(char)): - return 0 - # Hiragana and Katakana - if 0x3040 <= char <= 0x309f or 0x30a0 <= char <= 0x30ff: - return 2 - # Full-width Latin characters - if 0xff01 <= char <= 0xff60: - return 2 - # CJK punctuation - if 0x3000 <= char <= 0x303e: - return 2 - # Backspace and delete - if char in (0x0008, 0x007f): - return -1 - # Other control characters - elif char in (0x0000, 0x001f): - return 0 - # Take a guess - return 1 - - -def _str_block_width(val): - - return sum(itermap(_char_block_width, itermap(ord, _re.sub("", val)))) - -############################## -# TABLE FACTORIES # -############################## - - -def from_csv(fp, field_names=None, **kwargs): - - dialect = csv.Sniffer().sniff(fp.read(1024)) - fp.seek(0) - reader = csv.reader(fp, dialect) - - table = PrettyTable(**kwargs) - if field_names: - table.field_names = field_names - else: - table.field_names = [x.strip() for x in next(reader)] - - for row in reader: - table.add_row([x.strip() for x in row]) - - return table - - -def from_db_cursor(cursor, **kwargs): - - if cursor.description: - table = PrettyTable(**kwargs) - table.field_names = [col[0] for col in cursor.description] - for row in cursor.fetchall(): - table.add_row(row) - return table - - -class TableHandler(HTMLParser): - - def __init__(self, **kwargs): - HTMLParser.__init__(self) - self.kwargs = kwargs - self.tables = [] - self.last_row = [] - self.rows = [] - self.max_row_width = 0 - self.active = None - self.last_content = "" - self.is_last_row_header = False - - def handle_starttag(self, tag, attrs): - self.active = tag - if tag == "th": - self.is_last_row_header = True - - def handle_endtag(self, tag): - if tag in ["th", "td"]: - stripped_content = self.last_content.strip() - self.last_row.append(stripped_content) - if tag == "tr": - self.rows.append( - (self.last_row, self.is_last_row_header)) - self.max_row_width = max(self.max_row_width, len(self.last_row)) - self.last_row = [] - self.is_last_row_header = False - if tag == "table": - table = self.generate_table(self.rows) - self.tables.append(table) - self.rows = [] - self.last_content = " " - self.active = None - - def handle_data(self, data): - self.last_content += data - - def generate_table(self, rows): - """ - Generates from a list of rows a PrettyTable object. - """ - table = PrettyTable(**self.kwargs) - for row in self.rows: - if len(row[0]) < self.max_row_width: - appends = self.max_row_width - len(row[0]) - for _ in range(1, appends): - row[0].append("-") - - if row[1] is True: - self.make_fields_unique(row[0]) - table.field_names = row[0] - else: - table.add_row(row[0]) - return table - - def make_fields_unique(self, fields): - """ - iterates over the row and make each field unique - """ - for i in range(0, len(fields)): - for j in range(i + 1, len(fields)): - if fields[i] == fields[j]: - fields[j] += "'" - - -def from_html(html_code, **kwargs): - """ - Generates a list of PrettyTables from a string of HTML code. Each in - the HTML becomes one PrettyTable object. - """ - - parser = TableHandler(**kwargs) - parser.feed(html_code) - return parser.tables - - -def from_html_one(html_code, **kwargs): - """ - Generates a PrettyTables from a string of HTML code which contains only a - single
- """ - - tables = from_html(html_code, **kwargs) - try: - assert len(tables) == 1 - except AssertionError: - raise Exception("More than one
in provided HTML code! Use from_html instead.") - return tables[0] - -############################## -# MAIN (TEST FUNCTION) # -############################## - - -def main(): - - x = PrettyTable(["City name", "Area", "Population", "Annual Rainfall"]) - x.sortby = "Population" - x.reversesort = True - x.int_format["Area"] = "04d" - x.float_format = "6.1f" - x.align["City name"] = "l" # Left align city names - x.add_row(["Adelaide", 1295, 1158259, 600.5]) - x.add_row(["Brisbane", 5905, 1857594, 1146.4]) - x.add_row(["Darwin", 112, 120900, 1714.7]) - x.add_row(["Hobart", 1357, 205556, 619.5]) - x.add_row(["Sydney", 2058, 4336374, 1214.8]) - x.add_row(["Melbourne", 1566, 3806092, 646.9]) - x.add_row(["Perth", 5386, 1554769, 869.4]) - print(x) - - -if __name__ == "__main__": - main()