Fix show element command for Py3K
[mudpy.git] / lib / mudpy / misc.py
index 84d1a5e..f4c8779 100644 (file)
@@ -1,7 +1,6 @@
-# -*- coding: utf-8 -*-
 """Miscellaneous functions for the mudpy engine."""
 
-# Copyright (c) 2004-2014 Jeremy Stanley <fungi@yuggoth.org>. Permission
+# Copyright (c) 2004-2016 Jeremy Stanley <fungi@yuggoth.org>. Permission
 # to use, copy, modify, and distribute this software is granted under
 # terms provided in the LICENSE file distributed with this software.
 
@@ -74,13 +73,8 @@ class Element:
         self.origin = self.universe.files[filename]
 
         # add a data section to the origin if necessary
-        # TODO(fungi): remove this indirection after the YAML transition
-        if self.origin._format == "yaml":
-            if self.key not in self.origin.data:
-                self.origin.data[self.key] = {}
-        else:
-            if not self.origin.data.has_section(self.key):
-                self.origin.data.add_section(self.key)
+        if self.key not in self.origin.data:
+            self.origin.data[self.key] = {}
 
         # add or replace this element in the universe
         self.universe.contents[self.key] = self
@@ -93,24 +87,17 @@ class Element:
 
     def destroy(self):
         """Remove an element from the universe and destroy it."""
-        self.origin.data.remove_section(self.key)
+        del(self.origin.data[self.key])
         del self.universe.categories[self.category][self.subkey]
         del self.universe.contents[self.key]
         del self
 
     def facets(self):
         """Return a list of non-inherited facets for this element."""
-        # TODO(fungi): remove this indirection after the YAML transition
-        if self.origin._format == "yaml":
-            try:
-                return self.origin.data[self.key].keys()
-            except (AttributeError, KeyError):
-                return []
-        else:
-            if self.key in self.origin.data.sections():
-                return self.origin.data.options(self.key)
-            else:
-                return []
+        try:
+            return self.origin.data[self.key].keys()
+        except (AttributeError, KeyError):
+            return []
 
     def has_facet(self, facet):
         """Return whether the non-inherited facet exists."""
@@ -119,13 +106,15 @@ class Element:
     def remove_facet(self, facet):
         """Remove a facet from the element."""
         if self.has_facet(facet):
-            self.origin.data.remove_option(self.key, facet)
+            del(self.origin.data[self.key][facet])
             self.origin.modified = True
 
     def ancestry(self):
         """Return a list of the element's inheritance lineage."""
         if self.has_facet("inherit"):
-            ancestry = self.getlist("inherit")
+            ancestry = self.get("inherit")
+            if not ancestry:
+                ancestry = []
             for parent in ancestry[:]:
                 ancestors = self.universe.contents[parent].ancestry()
                 for ancestor in ancestors:
@@ -139,118 +128,32 @@ class Element:
         """Retrieve values."""
         if default is None:
             default = ""
-        # TODO(fungi): remove this indirection after the YAML transition
-        if self.origin._format == "yaml":
-            try:
-                return self.origin.data[self.key][facet]
-            except (KeyError, TypeError):
-                pass
-            if self.has_facet("inherit"):
-                for ancestor in self.ancestry():
-                    if self.universe.contents[ancestor].has_facet(facet):
-                        return self.universe.contents[ancestor].get(facet)
-            else:
-                return default
-        else:
-            if self.origin.data.has_option(self.key, facet):
-                raw_data = self.origin.data.get(self.key, facet)
-                if raw_data.startswith("u\"") or raw_data.startswith("u'"):
-                    raw_data = raw_data[1:]
-                raw_data.strip("\"'")
-                return raw_data
-            elif self.has_facet("inherit"):
-                for ancestor in self.ancestry():
-                    if self.universe.contents[ancestor].has_facet(facet):
-                        return self.universe.contents[ancestor].get(facet)
-            else:
-                return default
-
-    def getboolean(self, facet, default=None):
-        """Retrieve values as boolean type."""
-        if default is None:
-            default = False
-        # TODO(fungi): remove this indirection after the YAML transition
-        if self.origin._format == "yaml":
-            try:
-                return bool(self.origin.data[self.key][facet])
-            except KeyError:
-                pass
-            for ancestor in self.ancestry():
-                try:
-                    return self.universe.contents[ancestor].getboolean(facet)
-                except KeyError:
-                    pass
-            return default
-        else:
-            if self.origin.data.has_option(self.key, facet):
-                return self.origin.data.getboolean(self.key, facet)
-            elif self.has_facet("inherit"):
-                for ancestor in self.ancestry():
-                    if self.universe.contents[ancestor].has_facet(facet):
-                        return self.universe.contents[ancestor].getboolean(
-                            facet)
-            else:
-                return default
-
-    def getint(self, facet, default=None):
-        """Return values as int type."""
-        if default is None:
-            default = 0
-        if self.origin.data.has_option(self.key, facet):
-            return self.origin.data.getint(self.key, facet)
-        elif self.has_facet("inherit"):
-            for ancestor in self.ancestry():
-                if self.universe.contents[ancestor].has_facet(facet):
-                    return self.universe.contents[ancestor].getint(facet)
-        else:
-            return default
-
-    def getfloat(self, facet, default=None):
-        """Return values as float type."""
-        if default is None:
-            default = 0.0
-        if self.origin.data.has_option(self.key, facet):
-            return self.origin.data.getfloat(self.key, facet)
-        elif self.has_facet("inherit"):
+        try:
+            return self.origin.data[self.key][facet]
+        except (KeyError, TypeError):
+            pass
+        if self.has_facet("inherit"):
             for ancestor in self.ancestry():
                 if self.universe.contents[ancestor].has_facet(facet):
-                    return self.universe.contents[ancestor].getfloat(facet)
-        else:
-            return default
-
-    def getlist(self, facet, default=None):
-        """Return values as list type."""
-        if default is None:
-            default = []
-        value = self.get(facet)
-        if value:
-            return mudpy.data.makelist(value)
-        else:
-            return default
-
-    def getdict(self, facet, default=None):
-        """Return values as dict type."""
-        if default is None:
-            default = {}
-        value = self.get(facet)
-        if value:
-            return mudpy.data.makedict(value)
+                    return self.universe.contents[ancestor].get(facet)
         else:
             return default
 
     def set(self, facet, value):
         """Set values."""
         if not self.has_facet(facet) or not self.get(facet) == value:
-            if not type(value) is str:
-                value = repr(value)
-            self.origin.data.set(self.key, facet, value)
+            if self.key not in self.origin.data:
+                self.origin.data[self.key] = {}
+            self.origin.data[self.key][facet] = value
             self.origin.modified = True
 
     def append(self, facet, value):
         """Append value to a list."""
-        if not type(value) is str:
-            value = repr(value)
-        newlist = self.getlist(facet)
+        newlist = self.get(facet)
+        if not newlist:
+            newlist = []
+        if type(newlist) is not list:
+            newlist = list(newlist)
         newlist.append(value)
         self.set(facet, newlist)
 
@@ -286,11 +189,11 @@ class Element:
             result = False
 
         # avatars of administrators can run any command
-        elif self.owner and self.owner.account.getboolean("administrator"):
+        elif self.owner and self.owner.account.get("administrator"):
             result = True
 
         # everyone can run non-administrative commands
-        elif not command.getboolean("administrative"):
+        elif not command.get("administrative"):
             result = True
 
         # otherwise the command cannot be run by this actor
@@ -338,7 +241,7 @@ class Element:
                 "internal"
             ][
                 "directions"
-            ].getdict(
+            ].get(
                 direction
             )[
                 "exit"
@@ -349,7 +252,7 @@ class Element:
                 "internal"
             ][
                 "directions"
-            ].getdict(
+            ].get(
                 direction
             )[
                 "exit"
@@ -367,7 +270,7 @@ class Element:
                 "internal"
             ][
                 "directions"
-            ].getdict(
+            ].get(
                 direction
             )[
                 "enter"
@@ -394,7 +297,7 @@ class Element:
             for element in self.universe.contents[
                 self.get("location")
             ].contents.values():
-                if element.getboolean("is_actor") and element is not self:
+                if element.get("is_actor") and element is not self:
                     message += "$(yel)" + element.get(
                         "name"
                     ) + " is here.$(nrm)$(eol)"
@@ -414,11 +317,11 @@ class Element:
             offsets = dict(
                 [
                     (
-                        x, directions.getdict(x)["vector"]
+                        x, directions.get(x)["vector"]
                     ) for x in directions.facets()
                 ]
             )
-            for portal in self.getlist("gridlinks"):
+            for portal in self.get("gridlinks"):
                 adjacent = map(lambda c, o: c + o,
                                coordinates, offsets[portal])
                 neighbor = "area:" + ",".join(
@@ -461,20 +364,17 @@ class Universe:
         self.loglines = []
         self.private_files = []
         self.reload_flag = False
+        self.setup_loglines = []
         self.startdir = os.getcwd()
         self.terminate_flag = False
         self.userlist = []
         if not filename:
             possible_filenames = [
-                ".mudpyrc",
-                ".mudpy/mudpyrc",
-                ".mudpy/mudpy.conf",
-                "mudpy.conf",
-                "etc/mudpy.conf",
-                "/usr/local/mudpy/mudpy.conf",
-                "/usr/local/mudpy/etc/mudpy.conf",
-                "/etc/mudpy/mudpy.conf",
-                "/etc/mudpy.conf"
+                "etc/mudpy.yaml",
+                "/usr/local/mudpy/etc/mudpy.yaml",
+                "/usr/local/etc/mudpy.yaml",
+                "/etc/mudpy/mudpy.yaml",
+                "/etc/mudpy.yaml"
             ]
             for filename in possible_filenames:
                 if os.access(filename, os.R_OK):
@@ -483,11 +383,15 @@ class Universe:
             filename = os.path.join(self.startdir, filename)
         self.filename = filename
         if load:
-            self.load()
+            # make sure to preserve any accumulated log entries during load
+            self.setup_loglines += self.load()
 
     def load(self):
         """Load universe data from persistent storage."""
 
+        # it's possible for this to enter before logging configuration is read
+        pending_loglines = []
+
         # the files dict must exist and filename needs to be read-only
         if not hasattr(
            self, "files"
@@ -509,9 +413,13 @@ class Universe:
         # make a list of inactive avatars
         inactive_avatars = []
         for account in self.categories["account"].values():
-            inactive_avatars += [
-                (self.contents[x]) for x in account.getlist("avatars")
-            ]
+            for avatar in account.get("avatars"):
+                try:
+                    inactive_avatars.append(self.contents[avatar])
+                except KeyError:
+                    pending_loglines.append((
+                        "Missing avatar \"%s\", possible data corruption" %
+                        avatar, 6))
         for user in self.userlist:
             if user.avatar in inactive_avatars:
                 inactive_avatars.remove(user.avatar)
@@ -531,6 +439,7 @@ class Universe:
         for element in self.contents.values():
             element.update_location()
             element.clean_contents()
+        return pending_loglines
 
     def new(self):
         """Create a new, empty Universe (the Big Bang)."""
@@ -551,7 +460,7 @@ class Universe:
 
         # need to know the local address and port number for the listener
         host = self.categories["internal"]["network"].get("host")
-        port = self.categories["internal"]["network"].getint("port")
+        port = self.categories["internal"]["network"].get("port")
 
         # if no host was specified, bind to all local addresses (preferring
         # ipv6)
@@ -594,7 +503,7 @@ class Universe:
 
     def get_time(self):
         """Convenience method to get the elapsed time counter."""
-        return self.categories["internal"]["counters"].getint("elapsed")
+        return self.categories["internal"]["counters"].get("elapsed")
 
 
 class User:
@@ -641,7 +550,7 @@ class User:
     def check_idle(self):
         """Warn or disconnect idle users as appropriate."""
         idletime = universe.get_time() - self.last_input
-        linkdead_dict = universe.categories["internal"]["time"].getdict(
+        linkdead_dict = universe.categories["internal"]["time"].get(
             "linkdead"
         )
         if self.state in linkdead_dict:
@@ -665,7 +574,7 @@ class User:
             log(logline, 2)
             self.state = "disconnecting"
             self.menu_seen = False
-        idle_dict = universe.categories["internal"]["time"].getdict("idle")
+        idle_dict = universe.categories["internal"]["time"].get("idle")
         if self.state in idle_dict:
             idle_state = self.state
         else:
@@ -762,7 +671,7 @@ class User:
                "internal"
                ][
                 "limits"
-            ].getlist(
+            ].get(
                 "default_admins"
             ):
                 self.account.set("administrator", "True")
@@ -931,14 +840,14 @@ class User:
         if self.output_queue:
             try:
                 self.connection.send(self.output_queue[0])
-                del self.output_queue[0]
             except BrokenPipeError:
                 if self.account and self.account.get("name"):
                     account = self.account.get("name")
                 else:
                     account = "an unknown user"
-                log("Broken pipe sending to %s." % account, 7)
                 self.state = "disconnecting"
+                log("Broken pipe sending to %s." % account, 7)
+            del self.output_queue[0]
 
     def enqueue_input(self):
         """Process and enqueue any new input."""
@@ -1031,14 +940,14 @@ class User:
         if self.avatar is universe.contents[avatar]:
             self.avatar = None
         universe.contents[avatar].destroy()
-        avatars = self.account.getlist("avatars")
+        avatars = self.account.get("avatars")
         avatars.remove(avatar)
         self.account.set("avatars", avatars)
 
     def activate_avatar_by_index(self, index):
         """Enter the world with a particular indexed avatar."""
         self.avatar = universe.contents[
-            self.account.getlist("avatars")[index]]
+            self.account.get("avatars")[index]]
         self.avatar.owner = self
         self.state = "active"
         self.avatar.go_home()
@@ -1061,17 +970,20 @@ class User:
 
     def destroy(self):
         """Destroy the user and associated avatars."""
-        for avatar in self.account.getlist("avatars"):
+        for avatar in self.account.get("avatars"):
             self.delete_avatar(avatar)
         self.account.destroy()
 
     def list_avatar_names(self):
         """List names of assigned avatars."""
-        return [
-            universe.contents[avatar].get(
-                "name"
-            ) for avatar in self.account.getlist("avatars")
-        ]
+        avatars = []
+        for avatar in self.account.get("avatars"):
+            try:
+                avatars.append(universe.contents[avatar].get("name"))
+            except KeyError:
+                log("Missing avatar \"%s\", possible data corruption." %
+                    avatar, 6)
+        return avatars
 
 
 def broadcast(message, add_prompt=True):
@@ -1085,16 +997,14 @@ def log(message, level=0):
 
     # a couple references we need
     file_name = universe.categories["internal"]["logging"].get("file")
-    max_log_lines = universe.categories["internal"]["logging"].getint(
+    max_log_lines = universe.categories["internal"]["logging"].get(
         "max_log_lines"
     )
     syslog_name = universe.categories["internal"]["logging"].get("syslog")
     timestamp = time.asctime()[4:19]
 
-    # turn the message into a list of lines
-    lines = filter(
-        lambda x: x != "", [(x.rstrip()) for x in message.split("\n")]
-    )
+    # turn the message into a list of nonempty lines
+    lines = [x for x in [(x.rstrip()) for x in message.split("\n")] if x != ""]
 
     # send the timestamp and line to a file
     if file_name:
@@ -1107,7 +1017,7 @@ def log(message, level=0):
         file_descriptor.close()
 
     # send the timestamp and line to standard output
-    if universe.categories["internal"]["logging"].getboolean("stdout"):
+    if universe.categories["internal"]["logging"].get("stdout"):
         for line in lines:
             print(timestamp + " " + line)
 
@@ -1124,9 +1034,9 @@ def log(message, level=0):
 
     # display to connected administrators
     for user in universe.userlist:
-        if user.state == "active" and user.account.getboolean(
+        if user.state == "active" and user.account.get(
            "administrator"
-           ) and user.account.getint("loglevel") <= level:
+           ) and user.account.get("loglevel", 0) <= level:
             # iterate over every line in the message
             full_message = ""
             for line in lines:
@@ -1146,7 +1056,7 @@ def get_loglines(level, start, stop):
     """Return a specific range of loglines filtered by level."""
 
     # filter the log lines
-    loglines = filter(lambda x: x[0] >= level, universe.loglines)
+    loglines = [x for x in universe.loglines if x[0] >= level]
 
     # we need these in several places
     total_count = str(len(universe.loglines))
@@ -1432,9 +1342,12 @@ def replace_macros(user, text, is_input=False):
     return text
 
 
-def escape_macros(text):
+def escape_macros(value):
     """Escapes replacement macros in text."""
-    return text.replace("$(", "$_(")
+    if type(value) is str:
+        return value.replace("$(", "$_(")
+    else:
+        return value
 
 
 def first_word(text, separator=" "):
@@ -1471,43 +1384,43 @@ def on_pulse():
         )
 
     # update the log every now and then
-    if not universe.categories["internal"]["counters"].getint("mark"):
+    if not universe.categories["internal"]["counters"].get("mark"):
         log(str(len(universe.userlist)) + " connection(s)")
         universe.categories["internal"]["counters"].set(
-            "mark", universe.categories["internal"]["time"].getint(
+            "mark", universe.categories["internal"]["time"].get(
                 "frequency_log"
             )
         )
     else:
         universe.categories["internal"]["counters"].set(
-            "mark", universe.categories["internal"]["counters"].getint(
+            "mark", universe.categories["internal"]["counters"].get(
                 "mark"
             ) - 1
         )
 
     # periodically save everything
-    if not universe.categories["internal"]["counters"].getint("save"):
+    if not universe.categories["internal"]["counters"].get("save"):
         universe.save()
         universe.categories["internal"]["counters"].set(
-            "save", universe.categories["internal"]["time"].getint(
+            "save", universe.categories["internal"]["time"].get(
                 "frequency_save"
             )
         )
     else:
         universe.categories["internal"]["counters"].set(
-            "save", universe.categories["internal"]["counters"].getint(
+            "save", universe.categories["internal"]["counters"].get(
                 "save"
             ) - 1
         )
 
     # pause for a configurable amount of time (decimal seconds)
     time.sleep(universe.categories["internal"]
-               ["time"].getfloat("increment"))
+               ["time"].get("increment"))
 
     # increase the elapsed increment counter
     universe.categories["internal"]["counters"].set(
-        "elapsed", universe.categories["internal"]["counters"].getint(
-            "elapsed"
+        "elapsed", universe.categories["internal"]["counters"].get(
+            "elapsed", 0
         ) + 1
     )
 
@@ -1582,7 +1495,7 @@ def get_menu(state, error=None, choices=None):
 
 def menu_echo_on(state):
     """True if echo is on, false if it is off."""
-    return universe.categories["menu"][state].getboolean("echo", True)
+    return universe.categories["menu"][state].get("echo", True)
 
 
 def get_echo_message(state):
@@ -1834,7 +1747,7 @@ def handler_checking_password(user):
         "internal"
     ][
         "limits"
-    ].getint(
+    ].get(
         "password_tries"
     ) - 1:
         user.password_tries += 1
@@ -1873,7 +1786,7 @@ def handler_entering_new_password(user):
         "internal"
     ][
         "limits"
-    ].getint(
+    ].get(
         "password_tries"
     ) - 1:
         user.password_tries += 1
@@ -1908,7 +1821,7 @@ def handler_verifying_new_password(user):
         "internal"
     ][
         "limits"
-    ].getint(
+    ].get(
         "password_tries"
     ) - 1:
         user.password_tries += 1
@@ -2028,7 +1941,7 @@ def command_help(actor, parameters):
             description = command.get("description")
             if not description:
                 description = "(no short description provided)"
-            if command.getboolean("administrative"):
+            if command.get("administrative"):
                 output = "$(red)"
             else:
                 output = "$(grn)"
@@ -2041,7 +1954,7 @@ def command_help(actor, parameters):
             output += help_text
 
             # list related commands
-            see_also = command.getlist("see_also")
+            see_also = command.get("see_also")
             if see_also:
                 really_see_also = ""
                 for item in see_also:
@@ -2050,7 +1963,7 @@ def command_help(actor, parameters):
                         if actor.can_run(command):
                             if really_see_also:
                                 really_see_also += ", "
-                            if command.getboolean("administrative"):
+                            if command.get("administrative"):
                                 really_see_also += "$(red)"
                             else:
                                 really_see_also += "$(grn)"
@@ -2075,7 +1988,7 @@ def command_help(actor, parameters):
                 description = command.get("description")
                 if not description:
                     description = "(no short description provided)"
-                if command.getboolean("administrative"):
+                if command.get("administrative"):
                     output += "   $(red)"
                 else:
                     output += "   $(grn)"
@@ -2124,14 +2037,16 @@ def command_say(actor, parameters):
     if message:
 
         # match the punctuation used, if any, to an action
-        actions = universe.categories["internal"]["language"].getdict(
+        actions = universe.categories["internal"]["language"].get(
             "actions"
         )
         default_punctuation = (
             universe.categories["internal"]["language"].get(
                 "default_punctuation"))
         action = ""
-        for mark in actions.keys():
+
+        # reverse sort punctuation options so the longest match wins
+        for mark in sorted(actions.keys(), reverse=True):
             if not literal and message.endswith(mark):
                 action = actions[mark]
                 break
@@ -2151,7 +2066,7 @@ def command_say(actor, parameters):
             message = message[0].lower() + message[1:]
 
             # iterate over all words in message, replacing typos
-            typos = universe.categories["internal"]["language"].getdict(
+            typos = universe.categories["internal"]["language"].get(
                 "typos"
             )
             words = message.split()
@@ -2242,7 +2157,7 @@ def command_show(actor, parameters):
         elif arguments[1] in universe.files:
             message = ("These are the elements in the \"" + arguments[1]
                        + "\" file:$(eol)")
-            elements = universe.files[arguments[1]].data.sections()
+            elements = universe.files[arguments[1]].data.keys()
             elements.sort()
             for element in elements:
                 message += "$(eol)   $(grn)" + element + "$(nrm)"
@@ -2257,10 +2172,9 @@ def command_show(actor, parameters):
                        + "\" element (in \"" + element.origin.filename
                        + "\"):$(eol)")
             facets = element.facets()
-            facets.sort()
-            for facet in facets:
-                message += ("$(eol)   $(grn)" + facet + ": $(red)"
-                            + escape_macros(element.get(facet)) + "$(nrm)")
+            for facet in sorted(facets):
+                message += ("$(eol)   $(grn)%s: $(red)%s$(nrm)" %
+                            (facet, escape_macros(element.get(facet))))
         else:
             message = "Element \"" + arguments[1] + "\" does not exist."
     elif arguments[0] == "result":
@@ -2293,8 +2207,8 @@ def command_show(actor, parameters):
                 level = int(arguments[1])
             else:
                 level = -1
-        elif 0 <= actor.owner.account.getint("loglevel") <= 9:
-            level = actor.owner.account.getint("loglevel")
+        elif 0 <= actor.owner.account.get("loglevel", 0) <= 9:
+            level = actor.owner.account.get("loglevel", 0)
         else:
             level = 1
         if level > -1 and start > -1 and stop > -1:
@@ -2434,7 +2348,7 @@ def daemonize(universe):
     """Fork and disassociate from everything."""
 
     # only if this is what we're configured to do
-    if universe.contents["internal:process"].getboolean("daemon"):
+    if universe.contents["internal:process"].get("daemon"):
 
         # log before we start forking around, so the terminal gets the message
         log("Disassociating from the controlling terminal.")
@@ -2555,6 +2469,11 @@ def setup():
     global universe
     universe = Universe(conffile, True)
 
+    # report any loglines which accumulated during setup
+    for logline in universe.setup_loglines:
+        log(*logline)
+    universe.setup_loglines = []
+
     # log an initial message
     log("Started mudpy with command line: " + " ".join(sys.argv))