diff --git a/contrib/evlang/command.py b/contrib/evlang/command.py index 778367c62..97393dcc7 100644 --- a/contrib/evlang/command.py +++ b/contrib/evlang/command.py @@ -13,6 +13,7 @@ a ScriptableObject. It will handle access checks. from ev import utils from ev import default_cmds +from src.utils import prettytable #------------------------------------------------------------ # Evlang-related commands @@ -94,17 +95,10 @@ class CmdCode(default_cmds.MuxCommand): scripts.extend([(name, "--", "--") for name in evlang_locks if name not in evlang_scripts]) scripts = sorted(scripts, key=lambda p: p[0]) - table = [["type"] + [tup[0] for tup in scripts], - ["creator"] + [tup[1] for tup in scripts], - ["code"] + [tup[2] for tup in scripts]] - ftable = utils.format_table(table, extra_space=5) - string = "{wEvlang scripts on %s:{n" % obj.key - for irow, row in enumerate(ftable): - if irow == 0: - string += "\n" + "".join("{w%s{n" % col for col in row) - else: - string += "\n" + "".join(col for col in row) - + table = prettytable.PrettyTable(["{wtype", "{wcreator", "{wcode"]) + for tup in scripts: + table.add_row([tup[0], tup[1], tup[2]]) + string = "{wEvlang scripts on %s:{n\n%s" % (obj.key, table) caller.msg(string) return @@ -131,6 +125,3 @@ class CmdCode(default_cmds.MuxCommand): # debug mode caller.msg("{wDebug: running script (look out for errors below) ...{n\n" + "-"*68) obj.ndb.evlang.run_by_name(codetype, caller, quiet=False) - - - diff --git a/src/commands/default/admin.py b/src/commands/default/admin.py index 38110bfbe..a1b88d9b1 100644 --- a/src/commands/default/admin.py +++ b/src/commands/default/admin.py @@ -10,7 +10,7 @@ from django.contrib.auth.models import User from src.players.models import PlayerDB from src.server.sessionhandler import SESSIONS from src.server.models import ServerConfig -from src.utils import utils +from src.utils import utils, prettytable from src.commands.default.muxcommand import MuxCommand PERMISSION_HIERARCHY = [p.lower() for p in settings.PERMISSION_HIERARCHY] @@ -106,29 +106,17 @@ IPREGEX = re.compile(r"[0-9*]{1,3}\.[0-9*]{1,3}\.[0-9*]{1,3}\.[0-9*]{1,3}") def list_bans(banlist): """ Helper function to display a list of active bans. Input argument - is the banlist read into the two commands @ban and @undban below. + is the banlist read into the two commands @ban and @unban below. """ if not banlist: return "No active bans were found." - table = [["id"], ["name/ip"], ["date"], ["reason"]] - table[0].extend([str(i+1) for i in range(len(banlist))]) - for ban in banlist: - if ban[0]: - table[1].append(ban[0]) - else: - table[1].append(ban[1]) - table[2].extend([ban[3] for ban in banlist]) - table[3].extend([ban[4] for ban in banlist]) - ftable = utils.format_table(table, 4) - string = "{wActive bans:{x" - for irow, row in enumerate(ftable): - if irow == 0: - srow = "\n" + "".join(row) - srow = "{w%s{n" % srow.rstrip() - else: - srow = "\n" + "{w%s{n" % row[0] + "".join(row[1:]) - string += srow.rstrip() + table = prettytable.PrettyTable(["{wid", "{wname/ip", "{wdate", "{wreason"]) + for inum, ban in enumerate(banlist): + table.add_row([str(inum+1), + ban[0] and ban[0] or ban[1], + ban[3], ban[4]]) + string = "{wActive bans:{n\n%s" % table return string class CmdBan(MuxCommand): diff --git a/src/commands/default/cmdset_character.py b/src/commands/default/cmdset_character.py index 938d38f8b..c0fcaf87a 100644 --- a/src/commands/default/cmdset_character.py +++ b/src/commands/default/cmdset_character.py @@ -39,6 +39,7 @@ class CharacterCmdSet(CmdSet): self.add(system.CmdPy()) self.add(system.CmdScripts()) self.add(system.CmdObjects()) + self.add(system.CmdPlayers()) self.add(system.CmdService()) self.add(system.CmdAbout()) self.add(system.CmdTime()) diff --git a/src/commands/default/comms.py b/src/commands/default/comms.py index 0627c9cc2..ed44da347 100644 --- a/src/commands/default/comms.py +++ b/src/commands/default/comms.py @@ -11,7 +11,7 @@ from django.conf import settings from src.comms.models import Channel, Msg, PlayerChannelConnection, ExternalChannelConnection from src.comms import irc, imc2, rss from src.comms.channelhandler import CHANNELHANDLER -from src.utils import create, utils +from src.utils import create, utils, prettytable from src.commands.default.muxcommand import MuxCommand, MuxPlayerCommand # limit symbol import for API @@ -33,12 +33,12 @@ def find_channel(caller, channelname, silent=False, noaliases=False): if channels: return channels[0] if not silent: - self.msg("Channel '%s' not found." % channelname) + caller.msg("Channel '%s' not found." % channelname) return None elif len(channels) > 1: matches = ", ".join(["%s(%s)" % (chan.key, chan.id) for chan in channels]) if not silent: - self.msg("Multiple channels match (be more specific): \n%s" % matches) + caller.msg("Multiple channels match (be more specific): \n%s" % matches) return None return channels[0] @@ -238,6 +238,7 @@ class CmdChannels(MuxCommand): Lists all channels available to you, wether you listen to them or not. Use 'comlist" to only view your current channel subscriptions. + Use addcom/delcom to join and leave channels """ key = "@channels" aliases = ["@clist", "channels", "comlist", "chanlist", "channellist", "all channels"] @@ -257,46 +258,27 @@ class CmdChannels(MuxCommand): # all channel we are already subscribed to subs = [conn.channel for conn in PlayerChannelConnection.objects.get_all_player_connections(caller)] - if self.cmdstring != "comlist": - - string = "\nChannels available:" - cols = [[" "], ["Channel"], ["Aliases"], ["Perms"], ["Description"]] - for chan in channels: - if chan in subs: - cols[0].append(">") - else: - cols[0].append(" ") - cols[1].append(chan.key) - cols[2].append(",".join(chan.aliases)) - cols[3].append(str(chan.locks)) - cols[4].append(chan.desc) - # put into table - for ir, row in enumerate(utils.format_table(cols)): - if ir == 0: - string += "\n{w" + "".join(row) + "{n" - else: - string += "\n" + "".join(row) - self.msg(string) - - string = "\nChannel subscriptions:" - if not subs: - string += "(None)" - else: - nicks = [nick for nick in caller.nicks.get(nick_type="channel")] - cols = [[" "], ["Channel"], ["Aliases"], ["Description"]] + if self.cmdstring == "comlist": + # just display the subscribed channels with no extra info + comtable = prettytable.PrettyTable(["{wchannel","{wmy aliases", "{wdescription"]) for chan in subs: - cols[0].append(" ") - cols[1].append(chan.key) - cols[2].append(",".join([nick.db_nick for nick in nicks - if nick.db_real.lower() == chan.key.lower()] + chan.aliases)) - cols[3].append(chan.desc) - # put into table - for ir, row in enumerate(utils.format_table(cols)): - if ir == 0: - string += "\n{w" + "".join(row) + "{n" - else: - string += "\n" + "".join(row) - self.msg(string) + clower = chan.key.lower() + nicks = [nick for nick in caller.nicks.get(nick_type="channel")] + comtable.add_row(["%s%s" % (chan.key, chan.aliases and "(%s)" % ",".join(chan.aliases) or ""), + "%s".join(nick.db_nick for nick in nicks if nick.db_real.lower()==clower()), + chan.desc]) + caller.msg("\n{wChannel subscriptions{n (use {w@channels{n to list all, {waddcom{n/{wdelcom{n to sub/unsub):{n\n%s" % comtable) + else: + # full listing (of channels caller is able to listen to) + comtable = prettytable.PrettyTable(["{wsub","{wchannel","{wmy aliases","{wlocks","{wdescription"]) + for chan in channels: + nicks = [nick for nick in caller.nicks.get(nick_type="channel")] + comtable.add_row([chan in subs and "{gYes{n" or "{rNo{n", + "%s%s" % (chan.key, chan.aliases and "(%s)" % ",".join(chan.aliases) or ""), + "%s".join(nick.db_nick for nick in nicks if nick.db_real.lower()==clower()), + chan.locks, + chan.desc]) + caller.msg("\n{wAvailable channels{n (use {wcomlist{n,{waddcom{n and {wdelcom{n to manage subscriptions):\n%s" % comtable) class CmdCdestroy(MuxCommand): """ @@ -774,17 +756,10 @@ class CmdIRC2Chan(MuxCommand): # show all connections connections = ExternalChannelConnection.objects.filter(db_external_key__startswith='irc_') if connections: - cols = [["Evennia channel"], ["IRC channel"]] + table = prettytable.PrettyTable(["Evennia channel", "IRC channel"]) for conn in connections: - cols[0].append(conn.channel.key) - cols[1].append(" ".join(conn.external_config.split('|'))) - ftable = utils.format_table(cols) - string = "" - for ir, row in enumerate(ftable): - if ir == 0: - string += "{w%s{n" % "".join(row) - else: - string += "\n" + "".join(row) + table.add_row([conn.channel.key, " ".join(conn.external_config.split('|'))]) + string = "{wIRC connections:{n\n%s" % table self.msg(string) else: self.msg("No connections found.") @@ -863,18 +838,10 @@ class CmdIMC2Chan(MuxCommand): # show all connections connections = ExternalChannelConnection.objects.filter(db_external_key__startswith='imc2_') if connections: - cols = [["Evennia channel"], ["<->"], ["IMC channel"]] + table = prettytable.PrettyTable(["Evennia channel", "IMC channel"]) for conn in connections: - cols[0].append(conn.channel.key) - cols[1].append("") - cols[2].append(conn.external_config) - ftable = utils.format_table(cols) - string = "" - for ir, row in enumerate(ftable): - if ir == 0: - string += "{w%s{n" % "".join(row) - else: - string += "\n" + "".join(row) + table.add_row([conn.channel.key, conn.external_config]) + string = "{wIMC connections:{n\n%s" % table self.msg(string) else: self.msg("No connections found.") @@ -966,20 +933,11 @@ class CmdIMCInfo(MuxCommand): string = "" nmuds = 0 for network in networks: - string += "\n {GMuds registered on %s:{n" % network - cols = [["Name"], ["Url"], ["Host"], ["Port"]] + table = prettytable.PrettyTable(["Name", "Url", "Host", "Port"]) for mud in (mud for mud in muds if mud.networkname == network): nmuds += 1 - cols[0].append(mud.name) - cols[1].append(mud.url) - cols[2].append(mud.host) - cols[3].append(mud.port) - ftable = utils.format_table(cols) - for ir, row in enumerate(ftable): - if ir == 0: - string += "\n{w" + "".join(row) + "{n" - else: - string += "\n" + "".join(row) + table.add_row([mud.name, mud.url, mud.host, mud.port]) + string += "\n{wMuds registered on %s:{n\n%s" % (network, table) string += "\n %i Muds found." % nmuds self.msg(string) @@ -999,24 +957,13 @@ class CmdIMCInfo(MuxCommand): channels = IMC2_CHANLIST.get_channel_list() string = "" nchans = 0 - string += "\n {GChannels on %s:{n" % IMC2_CLIENT.factory.network - cols = [["Full name"], ["Name"], ["Owner"], ["Perm"], ["Policy"]] - for channel in channels: + table = prettytable.PrettyTable(["Full name", "Name", "Owner", "Perm", "Policy"]) + for chan in channels: nchans += 1 - cols[0].append(channel.name) - cols[1].append(channel.localname) - cols[2].append(channel.owner) - cols[3].append(channel.level) - cols[4].append(channel.policy) - ftable = utils.format_table(cols) - for ir, row in enumerate(ftable): - if ir == 0: - string += "\n{w" + "".join(row) + "{n" - else: - string += "\n" + "".join(row) - string += "\n %i Channels found." % nchans + table.add_row([chan.name, chan.localname, chan.owner, chan.level, chan.policy]) + string += "\n{wChannels on %s:{n\n%s" % (IMC2_CLIENT.factory.network, table) + string += "\n%i Channels found." % nchans self.msg(string) - else: # no valid inputs string = "Usage: imcinfo|imcchanlist|imclist" @@ -1104,17 +1051,10 @@ class CmdRSS2Chan(MuxCommand): # show all connections connections = ExternalChannelConnection.objects.filter(db_external_key__startswith='rss_') if connections: - cols = [["Evennia-channel"], ["RSS-url"]] + table = prettytable.PrettyTable(["Evennia channel", "RSS url"]) for conn in connections: - cols[0].append(conn.channel.key) - cols[1].append(conn.external_config.split('|')[0]) - ftable = utils.format_table(cols) - string = "" - for ir, row in enumerate(ftable): - if ir == 0: - string += "{w%s{n" % "".join(row) - else: - string += "\n" + "".join(row) + table.add_row([conn.channel.key, conn.external_config.split('|')[0]]) + string = "{wConnections to RSS:{n\n%s" % table self.msg(string) else: self.msg("No connections found.") diff --git a/src/commands/default/general.py b/src/commands/default/general.py index 0bb33f15f..837deccd9 100644 --- a/src/commands/default/general.py +++ b/src/commands/default/general.py @@ -2,7 +2,7 @@ General Character commands usually availabe to all characters """ from django.conf import settings -from src.utils import utils +from src.utils import utils, prettytable from src.objects.models import ObjectNick as Nick from src.commands.default.muxcommand import MuxCommand @@ -124,20 +124,13 @@ class CmdNick(MuxCommand): caller = self.caller switches = self.switches - nicks = Nick.objects.filter(db_obj=caller.dbobj).exclude(db_type="channel") + if 'list' in switches: - string = "{wDefined Nicks:{n" - cols = [["Type"],["Nickname"],["Translates-to"] ] + table = prettytable.PrettyTable(["{wNickType", "{wNickname", "{wTranslates-to"]) for nick in nicks: - cols[0].append(nick.db_type) - cols[1].append(nick.db_nick) - cols[2].append(nick.db_real) - for ir, row in enumerate(utils.format_table(cols)): - if ir == 0: - string += "\n{w" + "".join(row) + "{n" - else: - string += "\n" + "".join(row) + table.add_row([nick.db_type, nick.db_nick, nick.db_real]) + string = "{wDefined Nicks:{n\n%s" % table caller.msg(string) return if 'clearall' in switches: @@ -197,19 +190,12 @@ class CmdInventory(MuxCommand): if not items: string = "You are not carrying anything." else: - # format item list into nice collumns - cols = [[],[]] + table = prettytable.PrettyTable(["name", "desc"]) + table.header = False + table.border = False for item in items: - cols[0].append(item.name) - desc = item.db.desc - if not desc: - desc = "" - cols[1].append(utils.crop(str(desc))) - # auto-format the columns to make them evenly wide - ftable = utils.format_table(cols) - string = "You are carrying:" - for row in ftable: - string += "\n " + "{C%s{n - %s" % (row[0], row[1]) + table.add_row(["{C%s{n" % item.name, item.db.desc and item.db.desc or ""]) + string = "{wYou are carrying:\n%s" % table self.caller.msg(string) class CmdGet(MuxCommand): diff --git a/src/commands/default/player.py b/src/commands/default/player.py index 9dc4008b0..7f538dbdf 100644 --- a/src/commands/default/player.py +++ b/src/commands/default/player.py @@ -20,7 +20,7 @@ import time from django.conf import settings from src.server.sessionhandler import SESSIONS from src.commands.default.muxcommand import MuxPlayerCommand -from src.utils import utils, create, search +from src.utils import utils, create, search, prettytable from settings import MAX_NR_CHARACTERS, MULTISESSION_MODE # limit symbol import for API @@ -302,27 +302,18 @@ class CmdSessions(MuxPlayerCommand): def func(self): "Implement function" - - # make sure we work on the player, not on the character player = self.caller sessions = player.get_all_sessions() - table = [["sessid"], ['protocol'], ["host"], ["puppet/character"], ["location"]] + table = prettytable.PrettyTable(["{wsessid", "{wprotocol", "{whost", "{wpuppet/character", "{wlocation"]) for sess in sorted(sessions, key=lambda x:x.sessid): sessid = sess.sessid char = player.get_puppet(sessid) - table[0].append(str(sess.sessid)) - table[1].append(str(sess.protocol_key)) - table[2].append(type(sess.address)==tuple and sess.address[0] or sess.address) - table[3].append(char and str(char) or "None") - table[4].append(char and str(char.location) or "N/A") - ftable = utils.format_table(table, 5) - string = "{wYour current session(s):{n" - for ir, row in enumerate(ftable): - if ir == 0: - string += "\n" + "{w%s{n" % ("".join(row)) - else: - string += "\n" + "".join(row) + table.add_row([str(sessid), str(sess.protocol_key), + type(sess.address)==tuple and sess.address[0] or sess.address, + char and str(char) or "None", + char and str(char.location) or "N/A"]) + string = "{wYour current session(s):{n\n%s" % table self.msg(string) class CmdWho(MuxPlayerCommand): @@ -354,52 +345,36 @@ class CmdWho(MuxPlayerCommand): else: show_session_data = player.check_permstring("Immortals") or player.check_permstring("Wizards") - if show_session_data: - table = [["Player Name"], ["On for"], ["Idle"], ["Room"], ["Cmds"], ["Host"]] - else: - table = [["Player Name"], ["On for"], ["Idle"]] - - for session in session_list: - if not session.logged_in: - continue - - delta_cmd = time.time() - session.cmd_last_visible - delta_conn = time.time() - session.conn_time - plr_pobject = session.get_puppet() - if not plr_pobject: - plr_pobject = session.get_player() - show_session_data = False - table = [["Player Name"], ["On for"], ["Idle"]] - if show_session_data: - table[0].append(plr_pobject.name[:25]) - table[1].append(utils.time_format(delta_conn, 0)) - table[2].append(utils.time_format(delta_cmd, 1)) - table[3].append(plr_pobject.location and plr_pobject.location.id or "None") - table[4].append(session.cmd_total) - table[5].append(session.address[0]) - else: - table[0].append(plr_pobject.name[:25]) - table[1].append(utils.time_format(delta_conn,0)) - table[2].append(utils.time_format(delta_cmd,1)) - - stable = [] - for row in table: # prettify values - stable.append([str(val).strip() for val in row]) - ftable = utils.format_table(stable, 5) - string = "" - for ir, row in enumerate(ftable): - if ir == 0: - string += "\n" + "{w%s{n" % ("".join(row)) - else: - string += "\n" + "".join(row) nplayers = (SESSIONS.player_count()) - if nplayers == 1: - string += '\nOne player logged in.' + if show_session_data: + table = prettytable.PrettyTable(["{wPlayer Name","{wOn for", "{wIdle", "{wRoom", "{wCmds", "{wHost"]) + for session in session_list: + if not session.logged_in: continue + delta_cmd = time.time() - session.cmd_last_visible + delta_conn = time.time() - session.conn_time + plr_pobject = session.get_puppet() + plr_pobject = plr_pobject or session.get_player() + table.add_row([utils.crop(plr_pobject.name, width=25), + utils.time_format(delta_conn, 0), + utils.time_format(delta_cmd, 1), + hasattr(plr_pobject, "location") and plr_pobject.location or "None", + session.cmd_total, type(session.address==tuple) and session.address[0] or session.address]) else: - string += '\n%d players logged in.' % nplayers + table = prettytable.PrettyTable(["{wPlayer name", "{wOn for", "{Idle"]) + for session in session_list: + if not session.logged_in: continue + delta_cmd = time.time() - session.cmd_last_visible + delta_conn = time.time() - session.conn_time + plr_pobject = session.get_puppet() + plr_pobject = plr_pobject or session.get_player() + table.add_row([utils.crop(plr_pobject.name, width=25), + utils.time.format(delta_conn, 0), + utils,time_format(delta_cmd, 1)]) + string = "{wPlayers:\n%s\n%s logged in." % (table, nplayers==1 and "One player" or nplayer) self.msg(string) + class CmdEncoding(MuxPlayerCommand): """ encoding - set a custom text encoding @@ -546,15 +521,29 @@ class CmdColorTest(MuxPlayerCommand): locks = "cmd:all()" help_category = "General" + def table_format(self, table): + """ + Helper method to format the ansi/xterm256 tables. + Takes a table of columns [[val,val,...],[val,val,...],...] + """ + if not table: + return [[]] + + extra_space = 1 + max_widths = [max([len(str(val)) for val in col]) for col in table] + ftable = [] + for irow in range(len(table[0])): + ftable.append([str(col[irow]).ljust(max_widths[icol]) + " " * extra_space + for icol, col in enumerate(table)]) + return ftable + def func(self): "Show color tables" player = self.caller - if not self.args or not self.args in ("ansi", "xterm256"): - self.msg("Usage: @color ansi|xterm256") - return - if self.args == "ansi": + if self.args.startswith("a"): + # show ansi 16-color table from src.utils import ansi ap = ansi.ANSI_PARSER # ansi colors @@ -570,7 +559,9 @@ class CmdColorTest(MuxPlayerCommand): #print string self.msg(string) self.msg("({{X and %%cx are black-on-black\n %%r - return, %%t - tab, %%b - space)") - elif self.args == "xterm256": + + elif self.args.startswith("x"): + # show xterm256 table table = [[],[],[],[],[],[],[],[],[],[],[],[]] for ir in range(6): for ig in range(6): @@ -581,11 +572,13 @@ class CmdColorTest(MuxPlayerCommand): table[6+ir].append("%%cb%i%i%i%%c%i%i%i%s{n" % (ir,ig,ib, 5-ir,5-ig,5-ib, "{{b%i%i%i" % (ir,ig,ib))) - table = utils.format_table(table) + table = self.table_format(table) string = "Xterm256 colors (if not all hues show, your client might not report that it can handle xterm256):" for row in table: string += "\n" + "".join(row) self.msg(string) self.msg("(e.g. %%c123 and %%cb123 also work)") - + else: + # malformed input + self.msg("Usage: @color ansi|xterm256") diff --git a/src/commands/default/system.py b/src/commands/default/system.py index 032ffea17..3dcdffeac 100644 --- a/src/commands/default/system.py +++ b/src/commands/default/system.py @@ -17,7 +17,7 @@ from src.server.sessionhandler import SESSIONS from src.scripts.models import ScriptDB from src.objects.models import ObjectDB from src.players.models import PlayerDB -from src.utils import logger, utils, gametime, create, is_pypy +from src.utils import logger, utils, gametime, create, is_pypy, prettytable from src.commands.default.muxcommand import MuxCommand # delayed imports @@ -210,47 +210,20 @@ def format_script_list(scripts): if not scripts: return "" - table = [["id"], ["obj"], ["key"], ["intval"], ["next"], ["rept"], ["db"], ["typeclass"], ["desc"]] + table = prettytable.PrettyTable(["{wid","{wobj","{wkey","{wintval","{wnext","{wrept","{wdb"," {wtypeclass","{wdesc"],align='r') + table.align = 'r' for script in scripts: - - table[0].append(script.id) - if not hasattr(script, 'obj') or not script.obj: - table[1].append("") - else: - table[1].append(script.obj.key) - table[2].append(script.key) - if not hasattr(script, 'interval') or script.interval < 0: - table[3].append("--") - else: - table[3].append("%ss" % script.interval) - next = script.time_until_next_repeat() - if not next: - table[4].append("--") - else: - table[4].append("%ss" % next) - - if not hasattr(script, 'repeats') or not script.repeats: - table[5].append("--") - else: - table[5].append("%s" % script.repeats) - if script.persistent: - table[6].append("*") - else: - table[6].append("-") - typeclass_path = script.typeclass_path.rsplit('.', 1) - table[7].append("%s" % typeclass_path[-1]) - table[8].append(script.desc) - - ftable = utils.format_table(table) - string = "" - for irow, row in enumerate(ftable): - if irow == 0: - srow = "\n" + "".join(row) - srow = "{w%s{n" % srow.rstrip() - else: - srow = "\n" + "{w%s{n" % row[0] + "".join(row[1:]) - string += srow.rstrip() - return string.strip() + nextrep = script.time_until_next_repeat() + table.add_row([script.id, + (not hasattr(script, 'obj') or not script.obj) and "" or script.obj.key, + script.key, + (not hasattr(script, 'interval') or script.interval < 0) and "--" or "%ss" % script.interval, + not nextrep and "--" or "%ss" % nextrep, + (not hasattr(script, 'repeats') or not script.repeats) and "--" or "%i" % script.repeats, + script.persistent and "*" or "-", + script.typeclass_path.rsplit('.', 1)[-1], + script.desc]) + return "%s" % table class CmdScripts(MuxCommand): @@ -352,7 +325,7 @@ class CmdScripts(MuxCommand): class CmdObjects(MuxCommand): """ - Give a summary of object types in database + @objects - Give a summary of object types in database Usage: @objects [] @@ -376,54 +349,81 @@ class CmdObjects(MuxCommand): else: nlim = 10 - string = "\n{wDatabase totals:{n" - - nplayers = PlayerDB.objects.count() nobjs = ObjectDB.objects.count() base_char_typeclass = settings.BASE_CHARACTER_TYPECLASS nchars = ObjectDB.objects.filter(db_typeclass_path=base_char_typeclass).count() nrooms = ObjectDB.objects.filter(db_location__isnull=True).exclude(db_typeclass_path=base_char_typeclass).count() nexits = ObjectDB.objects.filter(db_location__isnull=False, db_destination__isnull=False).count() + nother = nobjs - nchars - nrooms - nexits - string += "\n{wPlayers:{n %i" % nplayers - string += "\n{wObjects:{n %i" % nobjs - string += "\n{w Characters (BASE_CHARACTER_TYPECLASS):{n %i" % nchars - string += "\n{w Rooms (location==None):{n %i" % nrooms - string += "\n{w Exits (destination!=None):{n %i" % nexits - string += "\n{w Other:{n %i\n" % (nobjs - nchars - nrooms - nexits) + # total object sum table + totaltable = prettytable.PrettyTable(["{wtype","{wcomment","{wcount", "{w%%"]) + totaltable.align = 'l' + totaltable.add_row(["Characters", "(BASE_CHARACTER_TYPECLASS)", nchars, "%.2f" % ((float(nchars)/nobjs)*100)]) + totaltable.add_row(["Rooms", "(location=None)", nrooms, "%.2f" % ((float(nrooms)/nobjs)*100)]) + totaltable.add_row(["Exits", "(destination!=None)", nexits, "%.2f" % ((float(nexits)/nobjs)*100)]) + totaltable.add_row(["Other", "", nother, "%.2f" % ((float(nother)/nobjs)*100)]) + # typeclass table + typetable = prettytable.PrettyTable(["{wtypeclass","{wcount", "{w%%"]) + typetable.align = 'l' dbtotals = ObjectDB.objects.object_totals() - table = [["Count"], ["Typeclass"]] for path, count in dbtotals.items(): - table[0].append(count) - table[1].append(path) - ftable = utils.format_table(table, 3) - for irow, row in enumerate(ftable): - srow = "\n" + "".join(row) - srow = srow.rstrip() - if irow == 0: - srow = "{w%s{n" % srow - string += srow + typetable.add_row([path, count, "%.2f" % ((float(count)/nobjs)*100)]) - string += "\n\n{wLast %s Objects created:{n" % min(nobjs, nlim) + # last N table objs = ObjectDB.objects.all().order_by("db_date_created")[max(0, nobjs - nlim):] + latesttable = prettytable.PrettyTable(["{wcreated","{wdbref","{wname","{wtypeclass"]) + latesttable.align = 'l' + for obj in objs: + latesttable.add_row([utils.datetime_format(obj.date_created), obj.dbref, obj.key, obj.typeclass.path]) - table = [["Created"], ["dbref"], ["name"], ["typeclass"]] - for i, obj in enumerate(objs): - table[0].append(utils.datetime_format(obj.date_created)) - table[1].append(obj.dbref) - table[2].append(obj.key) - table[3].append(str(obj.typeclass.path)) - ftable = utils.format_table(table, 5) - for irow, row in enumerate(ftable): - srow = "\n" + "".join(row) - srow = srow.rstrip() - if irow == 0: - srow = "{w%s{n" % srow - string += srow - + string = "\n{wObject subtype totals (out of %i Objects):{n\n%s" % (nobjs, totaltable) + string += "\n{wObject typeclass distribution:{n\n%s" % typetable + string += "\n{wLast %s Objects created:{n\n%s" % (min(nobjs, nlim), latesttable) caller.msg(string) +class CmdPlayers(MuxCommand): + """ + @players - give a summary of all registed Players + + Usage: + @players [nr] + + Lists statistics about the Players registered with the game. + It will list the amount of latest registered players + If not given, defaults to 10. + """ + key = "@players" + aliases = ["@listplayers"] + locks = "cmd:perm(listplayers) or perm(Admins)" + def func(self): + "List the players" + + caller = self.caller + if self.args and self.args.is_digit(): + nlim = int(self.args) + else: + nlim = 10 + + nplayers = PlayerDB.objects.count() + + # typeclass table + dbtotals = PlayerDB.objects.object_totals() + typetable = prettytable.PrettyTable(["{wtypeclass", "{wcount", "{w%%"]) + typetable.align = 'l' + for path, count in dbtotals.items(): + typetable.add_row([path, count, "%.2f" % ((float(count)/nplayers)*100)]) + # last N table + plyrs = PlayerDB.objects.all().order_by("db_date_created")[max(0, nplayers - nlim):] + latesttable = prettytable.PrettyTable(["{wcreated", "{wdbref","{wname","{wtypeclass"]) + latesttable.align = 'l' + for ply in plyrs: + latesttable.add_row([utils.datetime_format(ply.date_created), ply.dbref, ply.key, ply.typeclass.path]) + + string = "\n{wPlayer typeclass distribution:{n\n%s" % typetable + string += "\n{wLast %s Players created:{n\n%s" % (min(nplayers, nlim), latesttable) + caller.msg(string) class CmdService(MuxCommand): """ @@ -468,18 +468,11 @@ class CmdService(MuxCommand): if not switches or switches[0] == "list": # Just display the list of installed services and their # status, then exit. - string = "-" * 78 - string += "\n{wServices{n (use @services/start|stop|delete):" - + table = prettytable.PrettyTable(["{wService{n (use @services/start|stop|delete)", "{wstatus"]) + table.align = 'l' for service in service_collection.services: - if service.running: - status = 'Running' - string += '\n * {g%s{n (%s)' % (service.name, status) - else: - status = 'Inactive' - string += '\n {R%s{n (%s)' % (service.name, status) - string += "\n" + "-" * 78 - caller.msg(string) + table.add_row([service.name, service.running and "{gRunning" or "{rNot Running"]) + caller.msg(str(table)) return # Get the service to start / stop @@ -581,7 +574,7 @@ class CmdTime(MuxCommand): Usage: @time - Server local time. + Server time statistics. """ key = "@time" aliases = "@uptime" @@ -589,30 +582,14 @@ class CmdTime(MuxCommand): help_category = "System" def func(self): - "Show times." - - table = [["Current server uptime:", - "Total server running time:", - "Total in-game time (realtime x %g):" % (gametime.TIMEFACTOR), - "Server time stamp:" - ], - [utils.time_format(time.time() - SESSIONS.server.start_time, 3), - utils.time_format(gametime.runtime(format=False), 2), - utils.time_format(gametime.gametime(format=False), 2), - datetime.datetime.now() - ]] - if utils.host_os_is('posix'): - loadavg = os.getloadavg() - table[0].append("Server load (per minute):") - table[1].append("%g" % (loadavg[0])) - stable = [] - for col in table: - stable.append([str(val).strip() for val in col]) - ftable = utils.format_table(stable, 5) - string = "" - for row in ftable: - string += "\n " + "{w%s{n" % row[0] + "".join(row[1:]) - self.caller.msg(string) + "Show server time data in a table." + table = prettytable.PrettyTable(["{wserver time statistic","{wtime"]) + table.align = 'l' + table.add_row(["Current server uptime", utils.time_format(time.time() - SESSIONS.server.start_time, 3)]) + table.add_row(["Total server running time", utils.time_format(gametime.runtime(format=False), 2)]) + table.add_row(["Total in-game time (realtime x %g" % (gametime.TIMEFACTOR), utils.time_format(gametime.gametime(format=False), 2)]) + table.add_row(["Server time stamp", datetime.datetime.now()]) + self.caller.msg(str(table)) class CmdServerLoad(MuxCommand): """ @@ -656,92 +633,60 @@ class CmdServerLoad(MuxCommand): if not utils.host_os_is('posix'): string = "Process listings are only available under Linux/Unix." - else: - global _resource, _idmapper - if not _resource: - import resource as _resource - if not _idmapper: - from src.utils.idmapper import base as _idmapper + caller.msg(string) + return - import resource - loadavg = os.getloadavg() - psize = _resource.getpagesize() - pid = os.getpid() - rmem = float(os.popen('ps -p %d -o %s | tail -1' % (pid, "rss")).read()) / 1024.0 - vmem = float(os.popen('ps -p %d -o %s | tail -1' % (pid, "vsz")).read()) / 1024.0 + global _resource, _idmapper + if not _resource: + import resource as _resource + if not _idmapper: + from src.utils.idmapper import base as _idmapper - rusage = resource.getrusage(resource.RUSAGE_SELF) - table = [["Server load (1 min):", - "Process ID:", - "Bytes per page:", - "CPU time used:", - "Resident memory:", - "Virtual memory:", - "Page faults:", - "Disk I/O:", - "Network I/O:", - "Context switching:" - ], - ["%g" % loadavg[0], - "%10d" % pid, - "%10d " % psize, - "%s (%gs)" % (utils.time_format(rusage.ru_utime), rusage.ru_utime), - #"%10d shared" % rusage.ru_ixrss, - #"%10d pages" % rusage.ru_maxrss, - "%10.2f MB" % rmem, - "%10.2f MB" % vmem, - "%10d hard" % rusage.ru_majflt, - "%10d reads" % rusage.ru_inblock, - "%10d in" % rusage.ru_msgrcv, - "%10d vol" % rusage.ru_nvcsw - ], - ["", "", "", - "(user: %gs)" % rusage.ru_stime, - "", #"%10d private" % rusage.ru_idrss, - "", #"%10d bytes" % (rusage.ru_maxrss * psize), - "%10d soft" % rusage.ru_minflt, - "%10d writes" % rusage.ru_oublock, - "%10d out" % rusage.ru_msgsnd, - "%10d forced" % rusage.ru_nivcsw - ], - ["", "", "", "", - "", #"%10d stack" % rusage.ru_isrss, - "", - "%10d swapouts" % rusage.ru_nswap, - "", "", - "%10d sigs" % rusage.ru_nsignals - ] - ] - stable = [] - for col in table: - stable.append([str(val).strip() for val in col]) - ftable = utils.format_table(stable, 5) - string = "" - for row in ftable: - string += "\n " + "{w%s{n" % row[0] + "".join(row[1:]) + import resource + loadavg = os.getloadavg() + psize = _resource.getpagesize() + pid = os.getpid() + rmem = float(os.popen('ps -p %d -o %s | tail -1' % (pid, "rss")).read()) / 1024.0 # resident memory + vmem = float(os.popen('ps -p %d -o %s | tail -1' % (pid, "vsz")).read()) / 1024.0 # virtual memory + pmem = float(os.popen('ps -p %d -o %s | tail -1' % (pid, "%mem")).read()) # percent of resident memory to total + rusage = resource.getrusage(resource.RUSAGE_SELF) - if not is_pypy: - # Cache size measurements are not available on PyPy because it lacks sys.getsizeof + # load table + loadtable = prettytable.PrettyTable(["property", "statistic"]) + loadtable.align = 'l' + loadtable.add_row(["Server load (1 min)","%g" % loadavg[0]]) + loadtable.add_row(["Process ID","%g" % pid]), + loadtable.add_row(["Bytes per page","%g " % psize]) + loadtable.add_row(["CPU time used (total)", "%s (%gs)" % (utils.time_format(rusage.ru_utime), rusage.ru_utime)]) + loadtable.add_row(["CPU time used (user)", "%s (%gs)" % (utils.time_format(rusage.ru_stime), rusage.ru_stime)]) + loadtable.add_row(["Memory usage","%g MB (%g%%)" % (rmem, pmem)]) + loadtable.add_row(["Virtual address space\n {x(resident+swap+caching){n", "%g MB" % vmem]) + loadtable.add_row(["Page faults","%g hard, %g soft, %g swapouts" % (rusage.ru_majflt, rusage.ru_minflt, rusage.ru_nswap)]) + loadtable.add_row(["Disk I/O", "%g reads, %g writes" % (rusage.ru_inblock, rusage.ru_oublock)]) + loadtable.add_row(["Network I/O", "%g in, %g out" % (rusage.ru_msgrcv, rusage.ru_msgsnd)]) + loadtable.add_row(["Context switching", "%g vol, %g forced, %g signals" % (rusage.ru_nvcsw, rusage.ru_nivcsw, rusage.ru_nsignals)]) - # object cache size - cachedict = _idmapper.cache_size() - totcache = cachedict["_total"] - string += "\n{w Database entity (idmapper) cache usage:{n %5.2f MB (%i items)" % (totcache[1], totcache[0]) - sorted_cache = sorted([(key, tup[0], tup[1]) for key, tup in cachedict.items() if key !="_total" and tup[0] > 0], - key=lambda tup: tup[2], reverse=True) - table = [[tup[0] for tup in sorted_cache], - ["%5.2f MB" % tup[2] for tup in sorted_cache], - ["%i item(s)" % tup[1] for tup in sorted_cache]] - ftable = utils.format_table(table, 5) - for row in ftable: - string += "\n " + row[0] + row[1] + row[2] - # get sizes of other caches - attr_cache_info, field_cache_info, prop_cache_info = get_cache_sizes() - #size = sum([sum([getsizeof(obj) for obj in dic.values()]) for dic in _attribute_cache.values()])/1024.0 - #count = sum([len(dic) for dic in _attribute_cache.values()]) - string += "\n{w On-entity Attribute cache usage:{n %5.2f MB (%i attrs)" % (attr_cache_info[1], attr_cache_info[0]) - string += "\n{w On-entity Field cache usage:{n %5.2f MB (%i fields)" % (field_cache_info[1], field_cache_info[0]) - string += "\n{w On-entity Property cache usage:{n %5.2f MB (%i props)" % (prop_cache_info[1], prop_cache_info[0]) + string = "{wServer CPU and Memory load:{n\n%s" % loadtable + + if not is_pypy: + # Cache size measurements are not available on PyPy because it lacks sys.getsizeof + + # object cache size + cachedict = _idmapper.cache_size() + totcache = cachedict["_total"] + sorted_cache = sorted([(key, tup[0], tup[1]) for key, tup in cachedict.items() if key !="_total" and tup[0] > 0], + key=lambda tup: tup[2], reverse=True) + memtable = prettytable.PrettyTable(["entity name", "number", "cache (MB)", "idmapper %%"]) + memtable.align = 'l' + for tup in sorted_cache: + memtable.add_row([tup[0], "%i" % tup [1], "%5.2f" % tup[2], "%.2f" % (float(tup[2]/totcache[1])*100)]) + + # get sizes of other caches + attr_cache_info, field_cache_info, prop_cache_info = get_cache_sizes() + string += "\n{w Entity idmapper cache usage:{n %5.2f MB (%i items)\n%s" % (totcache[1], totcache[0], memtable) + string += "\n{w On-entity Attribute cache usage:{n %5.2f MB (%i attrs)" % (attr_cache_info[1], attr_cache_info[0]) + string += "\n{w On-entity Field cache usage:{n %5.2f MB (%i fields)" % (field_cache_info[1], field_cache_info[0]) + string += "\n{w On-entity Property cache usage:{n %5.2f MB (%i props)" % (prop_cache_info[1], prop_cache_info[0]) caller.msg(string) diff --git a/src/typeclasses/typeclass.py b/src/typeclasses/typeclass.py index 80c2e6cad..13640d141 100644 --- a/src/typeclasses/typeclass.py +++ b/src/typeclasses/typeclass.py @@ -25,7 +25,7 @@ _DA = object.__delattr__ # to *in-game* safety (if you can edit typeclasses you have # full access anyway), so no protection against changing # e.g. 'locks' or 'permissions' should go here. -PROTECTED = ('id', 'dbobj', 'db', 'ndb', 'objects', 'typeclass', 'db_player', 'player', +PROTECTED = ('id', 'dbobj', 'db', 'ndb', 'objects', 'typeclass', 'db_player', 'attr', 'save', 'delete', 'db_model_name','attribute_class', 'typeclass_paths') diff --git a/src/utils/prettytable.py b/src/utils/prettytable.py new file mode 100644 index 000000000..9a9435f1c --- /dev/null +++ b/src/utils/prettytable.py @@ -0,0 +1,1503 @@ +#!/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. + +__version__ = "trunk" + +import copy +import csv +import random +import re +import sys +import textwrap +import itertools +import unicodedata + +from src.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 = 0 +ALL = 1 +NONE = 2 +HEADER = 3 + +# 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): + return self._field_names + """The names of the fields + + Arguments: + + fields - list or tuple of field names""" + def _set_field_names(self, val): + val = [self._unicode(x) for x in val] + self._validate_option("field_names", val) + 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("Cant 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 = [""] * int(dHeight / 2) + lines + [""] * (dHeight - int(dHeight / 2)) + elif valign == "b": + lines = [""] * dHeight + lines + else: + lines = 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 = [] + open_tag.append("") + 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 = [] + open_tag.append("") + 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({ "l" : "left", "r" : "right", "c" : "center" }[self._align[field]]) + valigns.append({"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: + if py3k: + table.field_names = [x.strip() for x in next(reader)] + else: + table.field_names = [x.strip() for x in reader.next()] + + 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 i in range(1,appends): + row[0].append("-") + + if row[1] == 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() diff --git a/src/utils/utils.py b/src/utils/utils.py index f3296c214..34eb3e249 100644 --- a/src/utils/utils.py +++ b/src/utils/utils.py @@ -434,39 +434,6 @@ def inherits_from(obj, parent): parent_path = "%s.%s" % (parent.__class__.__module__, parent.__class__.__name__) return any(1 for obj_path in obj_paths if obj_path == parent_path) -def format_table(table, extra_space=1): - """ - Takes a table of collumns: [[val,val,val,...], [val,val,val,...], ...] - where each val will be placed on a separate row in the column. All - collumns must have the same number of rows (some positions may be - empty though). - - The function formats the columns to be as wide as the widest member - of each column. - - extra_space defines how much extra padding should minimum be left between - collumns. - - print the resulting list e.g. with - - for ir, row in enumarate(ftable): - if ir == 0: - # make first row white - string += "\n{w" + ""join(row) + "{n" - else: - string += "\n" + "".join(row) - print string - - """ - if not table: - return [[]] - - max_widths = [max([len(str(val)) for val in col]) for col in table] - ftable = [] - for irow in range(len(table[0])): - ftable.append([str(col[irow]).ljust(max_widths[icol]) + " " * extra_space - for icol, col in enumerate(table)]) - return ftable def server_services(): """ @@ -989,3 +956,40 @@ def string_partial_matching(alternatives, inp, ret_index=True): return matches[max(matches)] return [] +def format_table(table, extra_space=1): + """ + Note: src.utils.prettytable is more powerful than this, but this + function can be useful when the number of columns and rows are + unknown and must be calculated on the fly. + + Takes a table of collumns: [[val,val,val,...], [val,val,val,...], ...] + where each val will be placed on a separate row in the column. All + collumns must have the same number of rows (some positions may be + empty though). + + The function formats the columns to be as wide as the widest member + of each column. + + extra_space defines how much extra padding should minimum be left between + collumns. + + print the resulting list e.g. with + + for ir, row in enumarate(ftable): + if ir == 0: + # make first row white + string += "\n{w" + ""join(row) + "{n" + else: + string += "\n" + "".join(row) + print string + + """ + if not table: + return [[]] + + max_widths = [max([len(str(val)) for val in col]) for col in table] + ftable = [] + for irow in range(len(table[0])): + ftable.append([str(col[irow]).ljust(max_widths[icol]) + " " * extra_space + for icol, col in enumerate(table)]) + return ftable