Note

You are reading an old version of the Evennia documentation. The latest version is here

.

# Source code for evennia.scripts.monitorhandler

    """
    Monitors - catch changes to model fields and Attributes.

    The MONITOR_HANDLER singleton from this module offers the following
    functionality:

    - Field-monitor - track a object's specific database field and perform
        an action whenever that field *changes* for whatever reason.
    - Attribute-monitor tracks an object's specific Attribute and perform
        an action whenever that Attribute *changes* for whatever reason.

    """
    import inspect
    from collections import defaultdict

    from evennia.server.models import ServerConfig
    from evennia.utils import logger, variable_from_module
    from evennia.utils.dbserialize import dbserialize, dbunserialize

    _SA = object.__setattr__
    _GA = object.__getattribute__
    _DA = object.__delattr__

    class MonitorHandler(object):
        """
        This is a resource singleton that allows for registering
        callbacks for when a field or Attribute is updated (saved).

        """

        def __init__(self):
            """
            Initialize the handler.

            """
            self.savekey = "_monitorhandler_save"
            self.monitors = defaultdict(lambda: defaultdict(dict))

        def save(self):
            """
            Store our monitors to the database. This is called
            by the server process.

            Since dbserialize can't handle defaultdicts, we convert to an
            intermediary save format ((obj,fieldname, idstring, callback, kwargs), ...)

            """
            savedata = []
            if self.monitors:
                for obj in self.monitors:
                    for fieldname in self.monitors[obj]:
                        for idstring, (callback, persistent, kwargs) in self.monitors[obj][
                            fieldname
                        ].items():
                            path = "%s.%s" % (callback.__module__, callback.__name__)
                            savedata.append((obj, fieldname, idstring, path, persistent, kwargs))
                savedata = dbserialize(savedata)
                ServerConfig.objects.conf(key=self.savekey, value=savedata)

        def restore(self, server_reload=True):
            """
            Restore our monitors after a reload. This is called
            by the server process.

            Args:
                server_reload (bool, optional): If this is False, it means
                    the server went through a cold reboot and all
                    non-persistent tickers must be killed.

            """
            self.monitors = defaultdict(lambda: defaultdict(dict))
            restored_monitors = ServerConfig.objects.conf(key=self.savekey)
            if restored_monitors:
                restored_monitors = dbunserialize(restored_monitors)
                for obj, fieldname, idstring, path, persistent, kwargs in restored_monitors:
                    try:
                        if not server_reload and not persistent:
                            # this monitor will not be restarted
                            continue
                        if "session" in kwargs and not kwargs["session"]:
                            # the session was removed because it no longer
                            # exists. Don't restart the monitor.
                            continue
                        modname, varname = path.rsplit(".", 1)
                        callback = variable_from_module(modname, varname)

                        if obj and hasattr(obj, fieldname):
                            self.monitors[obj][fieldname][idstring] = (callback, persistent, kwargs)
                    except Exception:
                        continue
            # make sure to clean data from database
            ServerConfig.objects.conf(key=self.savekey, delete=True)

        def _attr_category_fieldname(self, fieldname, category):
            """
            Modify the saved fieldname to make sure to differentiate between Attributes
            with different categories.

            """
            return f"{fieldname}[{category}]" if category else fieldname

        def at_update(self, obj, fieldname):
            """
            Called by the field/attribute as it saves.

            """
            # if this an Attribute with a category we should differentiate
            fieldname = self._attr_category_fieldname(
                fieldname,
                obj.db_category if fieldname == "db_value" and hasattr(obj, "db_category") else None,
            )

            to_delete = []
            if obj in self.monitors and fieldname in self.monitors[obj]:
                for idstring, (callback, persistent, kwargs) in self.monitors[obj][fieldname].items():
                    try:
                        callback(obj=obj, fieldname=fieldname, **kwargs)
                    except Exception:
                        to_delete.append((obj, fieldname, idstring))
                        logger.log_trace("Monitor callback was removed.")
            # we cleanup non-found monitors (has to be done after loop)
            for obj, fieldname, idstring in to_delete:
                del self.monitors[obj][fieldname][idstring]

        def add(self, obj, fieldname, callback, idstring="", persistent=False, category=None, **kwargs):
            """
            Add monitoring to a given field or Attribute. A field must
            be specified with the full db_* name or it will be assumed
            to be an Attribute (so `db_key`, not just `key`).

            Args:
                obj (Typeclassed Entity): The entity on which to monitor a
                    field or Attribute.
                fieldname (str): Name of field (db_*) or Attribute to monitor.
                callback (callable): A callable on the form `callable(**kwargs),
                    where kwargs holds keys fieldname and obj.
                idstring (str, optional): An id to separate this monitor from other monitors
                    of the same field and object.
                persistent (bool, optional): If False, the monitor will survive
                    a server reload but not a cold restart. This is default.
                category (str, optional): This is only used if `fieldname` refers to
                    an Attribute (i.e. it does not start with `db_`). You must specify this
                    if you want to target an Attribute with a category.

            Keyword Args:
                session (Session): If this keyword is given, the monitorhandler will
                    correctly analyze it and remove the monitor if after a reload/reboot
                    the session is no longer valid.
                any (any): Any other kwargs are passed on to the callback. Remember that
                    all kwargs must be possible to pickle!

            """
            if not fieldname.startswith("db_") or not hasattr(obj, fieldname):
                # an Attribute - we track its db_value field
                obj = obj.attributes.get(fieldname, category=category, return_obj=True)
                if not obj:
                    return
                fieldname = self._attr_category_fieldname("db_value", category)

            # we try to serialize this data to test it's valid. Otherwise we won't accept it.
            try:
                if not inspect.isfunction(callback):
                    raise TypeError("callback is not a function.")
                dbserialize((obj, fieldname, callback, idstring, persistent, kwargs))
            except Exception:
                err = "Invalid monitor definition: \n" " (%s, %s, %s, %s, %s, %s)" % (
                    obj,
                    fieldname,
                    callback,
                    idstring,
                    persistent,
                    kwargs,
                )
                logger.log_trace(err)
            else:
                self.monitors[obj][fieldname][idstring] = (callback, persistent, kwargs)

        def remove(self, obj, fieldname, idstring="", category=None):
            """
            Remove a monitor.
            """
            if not fieldname.startswith("db_") or not hasattr(obj, fieldname):
                obj = obj.attributes.get(fieldname, return_obj=True)
                if not obj:
                    return
                fieldname = self._attr_category_fieldname("db_value", category)

            idstring_dict = self.monitors[obj][fieldname]
            if idstring in idstring_dict:
                del self.monitors[obj][fieldname][idstring]

        def clear(self):
            """
            Delete all monitors.
            """
            self.monitors = defaultdict(lambda: defaultdict(dict))

        def all(self, obj=None):
            """
            List all monitors or all monitors of a given object.

            Args:
                obj (Object): The object on which to list all monitors.

            Returns:
                monitors (list): The handled monitors.

            """
            output = []
            objs = [obj] if obj else self.monitors

            for obj in objs:
                for fieldname in self.monitors[obj]:
                    for idstring, (callback, persistent, kwargs) in self.monitors[obj][
                        fieldname
                    ].items():
                        output.append((obj, fieldname, idstring, persistent, kwargs))
            return output

    # access object
    MONITOR_HANDLER = MonitorHandler()

Note

You are reading an old version of the Evennia documentation. The latest version is here

.