Begin the transition from INI to YAML
[mudpy.git] / lib / mudpy / misc.py
index 2edf966..1f3909b 100644 (file)
@@ -1,13 +1,11 @@
 # -*- coding: utf-8 -*-
 """Miscellaneous functions for the mudpy engine."""
 
-# Copyright (c) 2004-2013 Jeremy Stanley <fungi@yuggoth.org>. Permission
+# Copyright (c) 2004-2014 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.
 
 import codecs
-import ctypes
-import ctypes.util
 import os
 import random
 import re
@@ -76,8 +74,13 @@ class Element:
         self.origin = self.universe.files[filename]
 
         # add a data section to the origin if necessary
-        if not self.origin.data.has_section(self.key):
-            self.origin.data.add_section(self.key)
+        # 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)
 
         # add or replace this element in the universe
         self.universe.contents[self.key] = self
@@ -85,7 +88,7 @@ class Element:
 
     def reload(self):
         """Create a new element and replace this one."""
-        new_element = Element(self.key, self.universe, self.origin.filename)
+        Element(self.key, self.universe, self.origin.filename)
         del(self)
 
     def destroy(self):
@@ -97,10 +100,17 @@ class Element:
 
     def facets(self):
         """Return a list of non-inherited facets for this element."""
-        if self.key in self.origin.data.sections():
-            return self.origin.data.options(self.key)
+        # TODO(fungi): remove this indirection after the YAML transition
+        if self.origin._format == "yaml":
+            try:
+                return self.origin.data[self.key].keys()
+            except KeyError:
+                return []
         else:
-            return []
+            if self.key in self.origin.data.sections():
+                return self.origin.data.options(self.key)
+            else:
+                return []
 
     def has_facet(self, facet):
         """Return whether the non-inherited facet exists."""
@@ -129,31 +139,58 @@ class Element:
         """Retrieve values."""
         if default is None:
             default = ""
-        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)
+        # TODO(fungi): remove this indirection after the YAML transition
+        if self.origin._format == "yaml":
+            try:
+                return self.origin.data[self.key][facet]
+            except KeyError:
+                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:
-            return default
+            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
-        if self.origin.data.has_option(self.key, facet):
-            return self.origin.data.getboolean(self.key, facet)
-        elif self.has_facet("inherit"):
+        # 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():
-                if self.universe.contents[ancestor].has_facet(facet):
+                try:
                     return self.universe.contents[ancestor].getboolean(facet)
-        else:
+                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."""
@@ -204,9 +241,6 @@ class Element:
     def set(self, facet, value):
         """Set values."""
         if not self.has_facet(facet) or not self.get(facet) == value:
-            # TODO: remove this check after the switch to py3k
-            if repr(type(value)) == "<type 'unicode'>":
-                value = str(value)
             if not type(value) is str:
                 value = repr(value)
             self.origin.data.set(self.key, facet, value)
@@ -220,16 +254,6 @@ class Element:
         newlist.append(value)
         self.set(facet, newlist)
 
-    def new_event(self, action, when=None):
-        """Create, attach and enqueue an event element."""
-
-        # if when isn't specified, that means now
-        if not when:
-            when = self.universe.get_time()
-
-        # events are elements themselves
-        event = Element("event:" + self.key + ":" + counter)
-
     def send(
         self,
         message,
@@ -278,9 +302,9 @@ class Element:
 
     def update_location(self):
         """Make sure the location's contents contain this element."""
-        location = self.get("location")
-        if location in self.universe.contents:
-            self.universe.contents[location].contents[self.key] = self
+        area = self.get("location")
+        if area in self.universe.contents:
+            self.universe.contents[area].contents[self.key] = self
 
     def clean_contents(self):
         """Make sure the element's contents aren't bogus."""
@@ -288,15 +312,15 @@ class Element:
             if element.get("location") != self.key:
                 del self.contents[element.key]
 
-    def go_to(self, location):
-        """Relocate the element to a specific location."""
+    def go_to(self, area):
+        """Relocate the element to a specific area."""
         current = self.get("location")
         if current and self.key in self.universe.contents[current].contents:
             del universe.contents[current].contents[self.key]
-        if location in self.universe.contents:
-            self.set("location", location)
-        self.universe.contents[location].contents[self.key] = self
-        self.look_at(location)
+        if area in self.universe.contents:
+            self.set("location", area)
+        self.universe.contents[area].contents[self.key] = self
+        self.look_at(area)
 
     def go_home(self):
         """Relocate the element to its default location."""
@@ -381,9 +405,9 @@ class Element:
             self.send(message)
 
     def portals(self):
-        """Map the portal directions for a room to neighbors."""
+        """Map the portal directions for an area to neighbors."""
         portals = {}
-        if re.match("""^location:-?\d+,-?\d+,-?\d+$""", self.key):
+        if re.match("""^area:-?\d+,-?\d+,-?\d+$""", self.key):
             coordinates = [(int(x))
                            for x in self.key.split(":")[1].split(",")]
             directions = self.universe.categories["internal"]["directions"]
@@ -397,7 +421,7 @@ class Element:
             for portal in self.getlist("gridlinks"):
                 adjacent = map(lambda c, o: c + o,
                                coordinates, offsets[portal])
-                neighbor = "location:" + ",".join(
+                neighbor = "area:" + ",".join(
                     [(str(x)) for x in adjacent]
                 )
                 if neighbor in self.universe.contents:
@@ -435,8 +459,6 @@ class Universe:
         self.contents = {}
         self.default_origins = {}
         self.loglines = []
-        self.pending_events_long = {}
-        self.pending_events_short = {}
         self.private_files = []
         self.reload_flag = False
         self.startdir = os.getcwd()
@@ -477,7 +499,7 @@ class Universe:
 
             # clear out all read-only files
             if hasattr(self, "files"):
-                for data_filename in self.files.keys():
+                for data_filename in list(self.files.keys()):
                     if not self.files[data_filename].is_writeable():
                         del self.files[data_filename]
 
@@ -496,13 +518,13 @@ class Universe:
 
         # go through all elements to clear out inactive avatar locations
         for element in self.contents.values():
-            location = element.get("location")
-            if element in inactive_avatars and location:
-                if location in self.contents and element.key in self.contents[
-                   location
+            area = element.get("location")
+            if element in inactive_avatars and area:
+                if area in self.contents and element.key in self.contents[
+                   area
                    ].contents:
-                    del self.contents[location].contents[element.key]
-                element.set("default_location", location)
+                    del self.contents[area].contents[element.key]
+                element.set("default_location", area)
                 element.remove_facet("location")
 
         # another pass to straighten out all the element contents
@@ -638,8 +660,8 @@ class User:
                 logline += self.account.get("name")
             else:
                 logline += "an unknown user"
-            logline += " after idling too long in the " + \
-                self.state + " state."
+            logline += (" after idling too long in the " + self.state
+                        + " state.")
             log(logline, 2)
             self.state = "disconnecting"
             self.menu_seen = False
@@ -803,8 +825,8 @@ class User:
             # with the optional eol string passed to this function
             # and the ansi escape to return to normal text
             if not just_prompt and prepend_padding:
-                if not self.output_queue \
-                   or not self.output_queue[-1].endswith(b"\r\n"):
+                if (not self.output_queue or not
+                        self.output_queue[-1].endswith(b"\r\n")):
                     output = "$(eol)" + output
                 elif not self.output_queue[-1].endswith(
                     b"\r\n\x1b[0m\r\n"
@@ -910,14 +932,13 @@ class User:
             try:
                 self.connection.send(self.output_queue[0])
                 del self.output_queue[0]
-            except:
+            except BrokenPipeError:
                 if self.account and self.account.get("name"):
                     account = self.account.get("name")
                 else:
                     account = "an unknown user"
-                log("Sending to %s raised an exception (broken pipe?)."
-                    % account, 7)
-                pass
+                log("Broken pipe sending to %s." % account, 7)
+                self.state = "disconnecting"
 
     def enqueue_input(self):
         """Process and enqueue any new input."""
@@ -925,7 +946,7 @@ class User:
         # check for some input
         try:
             raw_input = self.connection.recv(1024)
-        except:
+        except (BlockingIOError, OSError):
             raw_input = b""
 
         # we got something
@@ -1198,11 +1219,7 @@ def wrap_ansi_text(text, width):
     escape = False
 
     # normalize any potentially composited unicode before we count it
-    # TODO: remove this check after the switch to py3k
-    try:
-        text = unicodedata.normalize("NFKC", text)
-    except TypeError:
-        text = unicodedata.normalize("NFKC", unicode(text))
+    text = unicodedata.normalize("NFKC", text)
 
     # iterate over each character from the begining of the text
     for each_character in text:
@@ -1242,8 +1259,7 @@ def wrap_ansi_text(text, width):
                 last_whitespace = abs_pos
 
             # insert an eol in place of the space
-            text = text[:last_whitespace] + \
-                "\r\n" + text[last_whitespace + 1:]
+            text = text[:last_whitespace] + "\r\n" + text[last_whitespace + 1:]
 
             # increase the absolute position because an eol is two
             # characters but the space it replaced was only one
@@ -1512,7 +1528,7 @@ def check_for_connection(listening_socket):
     # try to accept a new connection
     try:
         connection, address = listening_socket.accept()
-    except:
+    except BlockingIOError:
         return None
 
     # note that we got one
@@ -2064,8 +2080,8 @@ def command_help(actor, parameters):
                 else:
                     output += "   $(grn)"
                 output += item + "$(nrm) - " + description + "$(eol)"
-        output += "$(eol)Enter \"help COMMAND\" for help on a command " \
-            + "named \"COMMAND\"."
+        output += ("$(eol)Enter \"help COMMAND\" for help on a command "
+                   "named \"COMMAND\".")
 
     # send the accumulated output to the user
     actor.send(output)
@@ -2088,7 +2104,7 @@ def command_look(actor, parameters):
 
 
 def command_say(actor, parameters):
-    """Speak to others in the same room."""
+    """Speak to others in the same area."""
 
     # check for replacement macros and escape them
     parameters = escape_macros(parameters)
@@ -2152,7 +2168,7 @@ def command_say(actor, parameters):
             # capitalize the first letter
             message = message[0].upper() + message[1:]
 
-    # tell the room
+    # tell the area
     if message:
         actor.echo_to_location(
             actor.get("name") + " " + action + "s, \"" + message + "\""
@@ -2202,14 +2218,14 @@ def command_show(actor, parameters):
                 status = "rw"
             else:
                 status = "ro"
-            message += "$(eol)   $(red)(" + status + ") $(grn)" + filename \
-                + "$(nrm)"
+            message += ("$(eol)   $(red)(" + status + ") $(grn)" + filename
+                        + "$(nrm)")
     elif arguments[0] == "category":
         if len(arguments) != 2:
             message = "You must specify one category."
         elif arguments[1] in universe.categories:
-            message = "These are the elements in the \"" + arguments[1] \
-                + "\" category:$(eol)"
+            message = ("These are the elements in the \"" + arguments[1]
+                       + "\" category:$(eol)")
             elements = [
                 (
                     universe.categories[arguments[1]][x].key
@@ -2224,8 +2240,8 @@ def command_show(actor, parameters):
         if len(arguments) != 2:
             message = "You must specify one file."
         elif arguments[1] in universe.files:
-            message = "These are the elements in the \"" + arguments[1] \
-                + "\" file:$(eol)"
+            message = ("These are the elements in the \"" + arguments[1]
+                       + "\" file:$(eol)")
             elements = universe.files[arguments[1]].data.sections()
             elements.sort()
             for element in elements:
@@ -2237,15 +2253,14 @@ def command_show(actor, parameters):
             message = "You must specify one element."
         elif arguments[1] in universe.contents:
             element = universe.contents[arguments[1]]
-            message = "These are the properties of the \"" + arguments[1] \
-                + \
-                "\" element (in \"" + \
-                element.origin.filename + "\"):$(eol)"
+            message = ("These are the properties of the \"" + arguments[1]
+                       + "\" 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)"
+                message += ("$(eol)   $(grn)" + facet + ": $(red)"
+                            + escape_macros(element.get(facet)) + "$(nrm)")
         else:
             message = "Element \"" + arguments[1] + "\" does not exist."
     elif arguments[0] == "result":
@@ -2254,8 +2269,9 @@ def command_show(actor, parameters):
         else:
             try:
                 message = repr(eval(" ".join(arguments[1:])))
-            except:
-                message = "Your expression raised an exception!"
+            except Exception as e:
+                message = ("$(red)Your expression raised an exception...$(eol)"
+                           "$(eol)$(bld)%s$(nrm)" % e)
     elif arguments[0] == "log":
         if len(arguments) == 4:
             if re.match("^\d+$", arguments[3]) and int(arguments[3]) >= 0:
@@ -2284,8 +2300,8 @@ def command_show(actor, parameters):
         if level > -1 and start > -1 and stop > -1:
             message = get_loglines(level, start, stop)
         else:
-            message = "When specified, level must be 0-9 (default 1), " \
-                + "start and stop must be >=1 (default 10 and 1)."
+            message = ("When specified, level must be 0-9 (default 1), "
+                       "start and stop must be >=1 (default 10 and 1).")
     else:
         message = "I don't know what \"" + parameters + "\" is."
     actor.send(message)
@@ -2306,18 +2322,18 @@ def command_create(actor, parameters):
             if element in universe.contents:
                 message = "The \"" + element + "\" element already exists."
             else:
-                message = "You create \"" + \
-                    element + "\" within the universe."
+                message = ("You create \"" +
+                           element + "\" within the universe.")
                 logline = actor.owner.account.get(
                     "name"
                 ) + " created an element: " + element
                 if filename:
                     logline += " in file " + filename
                     if filename not in universe.files:
-                        message += " Warning: \"" + filename \
-                            + "\" is not yet included in any other file and will " \
-                            + \
-                            "not be read on startup unless this is remedied."
+                        message += (
+                            " Warning: \"" + filename + "\" is not yet "
+                            "included in any other file and will not be read "
+                            "on startup unless this is remedied.")
                 Element(element, universe, filename)
                 log(logline, 6)
         elif len(arguments) > 2:
@@ -2332,12 +2348,11 @@ def command_destroy(actor, parameters):
             message = "You must specify an element to destroy."
         else:
             if parameters not in universe.contents:
-                message = "The \"" + parameters + \
-                    "\" element does not exist."
+                message = "The \"" + parameters + "\" element does not exist."
             else:
                 universe.contents[parameters].destroy()
-                message = "You destroy \"" + parameters \
-                    + "\" within the universe."
+                message = ("You destroy \"" + parameters
+                           + "\" within the universe.")
                 log(
                     actor.owner.account.get(
                         "name"
@@ -2354,22 +2369,22 @@ def command_set(actor, parameters):
     else:
         arguments = parameters.split(" ", 2)
         if len(arguments) == 1:
-            message = "What facet of element \"" + arguments[0] \
-                + "\" would you like to set?"
+            message = ("What facet of element \"" + arguments[0]
+                       + "\" would you like to set?")
         elif len(arguments) == 2:
-            message = "What value would you like to set for the \"" \
-                + arguments[1] + "\" facet of the \"" + arguments[0] \
-                + "\" element?"
+            message = ("What value would you like to set for the \"" +
+                       arguments[1] + "\" facet of the \"" + arguments[0]
+                       + "\" element?")
         else:
             element, facet, value = arguments
             if element not in universe.contents:
                 message = "The \"" + element + "\" element does not exist."
             else:
                 universe.contents[element].set(facet, value)
-                message = "You have successfully (re)set the \"" + facet \
-                    + "\" facet of element \"" + element \
-                    + "\". Try \"show element " + \
-                    element + "\" for verification."
+                message = ("You have successfully (re)set the \"" + facet
+                           + "\" facet of element \"" + element
+                           + "\". Try \"show element " +
+                           element + "\" for verification.")
     actor.send(message)
 
 
@@ -2380,8 +2395,8 @@ def command_delete(actor, parameters):
     else:
         arguments = parameters.split(" ")
         if len(arguments) == 1:
-            message = "What facet of element \"" + arguments[0] \
-                + "\" would you like to delete?"
+            message = ("What facet of element \"" + arguments[0]
+                       + "\" would you like to delete?")
         elif len(arguments) != 2:
             message = "You may only specify an element and a facet."
         else:
@@ -2389,14 +2404,14 @@ def command_delete(actor, parameters):
             if element not in universe.contents:
                 message = "The \"" + element + "\" element does not exist."
             elif facet not in universe.contents[element].facets():
-                message = "The \"" + element + "\" element has no \"" + facet \
-                    + "\" facet."
+                message = ("The \"" + element + "\" element has no \"" + facet
+                           + "\" facet.")
             else:
                 universe.contents[element].remove_facet(facet)
-                message = "You have successfully deleted the \"" + facet \
-                    + "\" facet of element \"" + element \
-                    + "\". Try \"show element " + \
-                    element + "\" for verification."
+                message = ("You have successfully deleted the \"" + facet
+                           + "\" facet of element \"" + element
+                           + "\". Try \"show element " +
+                           element + "\" for verification.")
     actor.send(message)
 
 
@@ -2421,48 +2436,6 @@ def daemonize(universe):
     # only if this is what we're configured to do
     if universe.contents["internal:process"].getboolean("daemon"):
 
-        # if possible, we want to rename the process to the same as the script
-        new_argv = b"\x00".join(x.encode("utf-8") for x in sys.argv) + b"\x00"
-        short_argv0 = os.path.basename(sys.argv[0]).encode("utf-8") + b"\x00"
-
-        # attempt the linux way first
-        try:
-            argv_array = ctypes.POINTER(ctypes.c_char_p)
-            ctypes.pythonapi.Py_GetArgcArgv.argtypes = (
-                ctypes.POINTER(ctypes.c_int),
-                ctypes.POINTER(argv_array)
-            )
-            argc = argv_array()
-            ctypes.pythonapi.Py_GetArgcArgv(
-                ctypes.c_int(0),
-                ctypes.pointer(argc)
-            )
-            old_argv0_size = len(argc.contents.value)
-            ctypes.memset(argc.contents, 0, len(new_argv) + old_argv0_size)
-            ctypes.memmove(argc.contents, new_argv, len(new_argv))
-            ctypes.CDLL(ctypes.util.find_library("c")).prctl(
-                15,
-                short_argv0,
-                0,
-                0,
-                0
-            )
-
-        except:
-
-            # since that failed, maybe it's bsd?
-            try:
-
-                # much simpler, since bsd has a libc function call for this
-                ctypes.CDLL(ctypes.util.find_library("c")).setproctitle(
-                    new_argv
-                )
-
-            except:
-
-                # that didn't work either, so just log that we couldn't
-                log("Failed to rename the interpreter process (cosmetic).")
-
         # log before we start forking around, so the terminal gets the message
         log("Disassociating from the controlling terminal.")
 
@@ -2529,14 +2502,9 @@ def excepthook(excepttype, value, tracebackdata):
     # try to log it, if possible
     try:
         log(message, 9)
-    except:
-        pass
-
-    # try to write it to stderr, if possible
-    try:
-        sys.stderr.write(message)
-    except:
-        pass
+    except Exception as e:
+        # try to write it to stderr, if possible
+        sys.stderr.write(message + "\nException while logging...\n%s" % e)
 
 
 def sighook(what, where):
@@ -2607,8 +2575,7 @@ def setup():
 
 
 def finish():
-    """This contains functions to be performed when shutting down the
-        engine."""
+    """These are functions performed when shutting down the engine."""
 
     # the loop has terminated, so save persistent data
     universe.save()