From 77b178bf289d94013bf6cd4382a33b300b2aecca Mon Sep 17 00:00:00 2001 From: Griatch Date: Sun, 20 Mar 2016 23:12:00 +0100 Subject: [PATCH] First version of a reworked tickerhandler. It will now also repeat normal functions in a module, not just methods on a database object. This means a backwards incompatible change, and API - old tickerhandler repeats will not restore properly with this. Currently untested. --- evennia/scripts/tickerhandler.py | 238 +++++++++++++++++-------------- 1 file changed, 129 insertions(+), 109 deletions(-) diff --git a/evennia/scripts/tickerhandler.py b/evennia/scripts/tickerhandler.py index 582ec6cd38..6c6bb420d8 100644 --- a/evennia/scripts/tickerhandler.py +++ b/evennia/scripts/tickerhandler.py @@ -54,6 +54,7 @@ a custom handler one can make a custom `AT_STARTSTOP_MODULE` entry to call the handler's `save()` and `restore()` methods when the server reboots. """ +import inspect from builtins import object from future.utils import listvalues @@ -63,15 +64,15 @@ from evennia.scripts.scripts import ExtendedLoopingCall from evennia.server.models import ServerConfig from evennia.utils.logger import log_trace, log_err from evennia.utils.dbserialize import dbserialize, dbunserialize, pack_dbobj +from evennia.utils import variable_from_module _GA = object.__getattribute__ _SA = object.__setattr__ -_ERROR_ADD_INTERVAL = \ -"""TickerHandler: Tried to add a ticker with invalid interval: -obj={obj}, interval={interval}, args={args}, kwargs={kwargs} -store_key={store_key} +_ERROR_ADD_TICKER = \ +"""TickerHandler: Tried to add an invalid ticker: +{storekey} Ticker was not added.""" class Ticker(object): @@ -98,14 +99,22 @@ class Ticker(object): kwargs is used here to identify which hook method to call. """ - for store_key, (obj, args, kwargs) in self.subscriptions.items(): - hook_key = yield kwargs.pop("_hook_key", "at_tick") - if not obj or not obj.pk: - # object was deleted between calls - self.remove(store_key) - continue + for store_key, (args, kwargs) in self.subscriptions.iteritems(): + callback = yield kwargs.pop("_callback", "at_tick") try: - yield _GA(obj, hook_key)(*args, **kwargs) + if callable(callback): + # call directly + yield callback(*args, **kwargs) + return + + # try object method + obj = yield kwargs.pop("_obj", None) + if not obj or not obj.pk: + # object was deleted between calls + self.remove(store_key) + continue + else: + yield _GA(obj, callback)(*args, **kwargs) except ObjectDoesNotExist: log_trace() self.remove(store_key) @@ -113,7 +122,7 @@ class Ticker(object): log_trace() finally: # make sure to re-store - kwargs["_hook_key"] = hook_key + kwargs["_callback"] = callback def __init__(self, interval): """ @@ -138,34 +147,27 @@ class Ticker(object): """ subs = self.subscriptions - if None in subs.values(): - # clean out objects that may have been deleted - subs = dict((store_key, obj) for store_key, obj in subs if obj) - self.subscriptions = subs if self.task.running: if not subs: self.task.stop() elif subs: self.task.start(self.interval, now=False, start_delay=start_delay) - def add(self, store_key, obj, *args, **kwargs): + def add(self, store_key, *args, **kwargs): """ Sign up a subscriber to this ticker. Args: store_key (str): Unique storage hash for this ticker subscription. - obj (Object): Object subscribing to this ticker. args (any, optional): Arguments to call the hook method with. Kwargs: _start_delay (int): If set, this will be used to delay the start of the trigger instead of `interval`. - _hooK_key (str): This carries the name of the hook method - to call. It is passed on as-is from this method. """ start_delay = kwargs.pop("_start_delay", None) - self.subscriptions[store_key] = (obj, args, kwargs) + self.subscriptions[store_key] = (args, kwargs) self.validate(start_delay=start_delay) def remove(self, store_key): @@ -204,46 +206,33 @@ class TickerPool(object): """ self.tickers = {} - def add(self, store_key, obj, interval, *args, **kwargs): + def add(self, store_key, *args, **kwargs): """ Add new ticker subscriber. Args: store_key (str): Unique storage hash. - obj (Object): Object subscribing. - interval (int): How often to call the ticker. args (any, optional): Arguments to send to the hook method. - Kwargs: - _start_delay (int): If set, this will be - used to delay the start of the trigger instead of - `interval`. It is passed on as-is from this method. - _hooK_key (str): This carries the name of the hook method - to call. It is passed on as-is from this method. - """ + _, _, _, interval, _ = store_key if not interval: - log_err(_ERROR_ADD_INTERVAL.format(store_key=store_key, obj=obj, - interval=interval, args=args, kwargs=kwargs)) + log_err(_ERROR_ADD_TICKER.format(store_key=store_key)) return if interval not in self.tickers: self.tickers[interval] = self.ticker_class(interval) - self.tickers[interval].add(store_key, obj, *args, **kwargs) + self.tickers[interval].add(store_key, *args, **kwargs) - def remove(self, store_key, interval): + def remove(self, store_key): """ Remove subscription from pool. Args: - store_key (str): Unique storage hash. - interval (int): Ticker interval. - - Notes: - A given subscription is uniquely identified both - via its `store_key` and its `interval`. + store_key (str): Unique storage hash to remove """ + _, _, _, interval, _ = store_key if interval in self.tickers: self.tickers[interval].remove(store_key) @@ -285,33 +274,65 @@ class TickerHandler(object): self.save_name = save_name self.ticker_pool = self.ticker_pool_class() - def _store_key(self, obj, interval, idstring=""): + def _get_callback(callback): + """ + Analyze callback and determine its consituents + + Args: + callback (function or method): This is either a stand-alone + function or class method on a typeclassed entitye (that is, + an entity that can be saved to the database). + + Returns: + ret (tuple): This is a tuple of the form `(obj, path, callfunc)`, + where `obj` is the database object the callback is defined on + if it's a method (otherwise `None`) and vice-versa, `path` is + the python-path to the stand-alone function (`None` if a method). + The `callfunc` is either the name of the method to call or the + callable function object itself. + + """ + outobj, outpath, outcallfunc = None, None, None + if callable(callback): + if inspect.ismethod(callback): + outobj = callback.im_self + outcallfunc = callback.im_func.func_name + elif inspect.isfunction(callback): + outpath = "%s.%s" % (callback.__module__, callback.func_name) + outcallfunc = callback + else: + raise TypeError("%s is not a callable function or method." % callback) + return outobj, outpath, outcallfunc + + def _store_key(self, obj, path, interval, callfunc, idstring=""): """ Tries to create a store_key for the object. Returns a tuple (isdb, store_key) where isdb is a boolean True if obj was a database object, False otherwise. Args: - obj (Object): Subscribing object. - interval (int): Ticker interval + obj (Object or None): Subscribing object if any. + path (str or None): Python-path to callable, if any. + interval (int): Ticker interval. + callfunc (callable or str): This is either the callable function or + the name of the method to call. Note that the callable is never + stored in the key; that is uniquely identified with the python-path. idstring (str, optional): Additional separator between different subscription types. + Returns: + isdb_and_store_key (tuple): A tuple `(obj, path, interval, + methodname, idstring)` that uniquely identifies the + ticker. `path` is `None` and `methodname` is the name of + the method if `obj_or_path` is a database object. + Vice-versa, `obj` and `methodname` are `None` if + `obj_or_path` is a python-path. + """ - if hasattr(obj, "db_key"): - # create a store_key using the database representation - objkey = pack_dbobj(obj) - isdb = True - else: - # non-db object, look for a property "key" on it, otherwise - # use its memory location. - try: - objkey = _GA(obj, "key") - except AttributeError: - objkey = id(obj) - isdb = False - # return sidb and store_key - return isdb, (objkey, interval, idstring) + outobj = pack_dbobj(obj) if obj and hasattr(obj, "db_key") else None + outpath = path if isinstance(basestring, path) else None + methodname = callfunc if callfunc and isinstance(basestring, callfunc) else None + return (outobj, methodname, outpath, interval, idstring) def save(self): """ @@ -346,82 +367,82 @@ class TickerHandler(object): # load stored command instructions and use them to re-initialize handler ticker_storage = ServerConfig.objects.conf(key=self.save_name) if ticker_storage: + # the dbunserialize will convert all serialized dbobjs to real objects self.ticker_storage = dbunserialize(ticker_storage) - for store_key, (args, kwargs) in self.ticker_storage.items(): - obj, interval, idstring = store_key - _, store_key = self._store_key(obj, interval, idstring) - self.ticker_pool.add(store_key, obj, interval, *args, **kwargs) + for store_key, (args, kwargs) in self.ticker_storage.iteritems(): + try: + obj, methodname, path, interval, idstring = store_key + if obj and methodname: + kwargs["_callable"] = methodname + kwargs["_obj"] = obj + elif path: + modname, varname = path.rsplit(".", 1) + callback = variable_from_module(modname, varname) + kwargs["_callable"] = callback + kwargs["_obj"] = None + except Exception as err: + # this suggests a malformed save or missing objects + log_err("%s\nTickerhandler: Removing malformed ticker: %s" % (err, str(store_key))) + continue + self.ticker_pool.add(store_key, *args, **kwargs) - def add(self, obj, interval, idstring="", hook_key="at_tick", *args, **kwargs): + def add(self, interval=60, callback=None, idstring="", *args, **kwargs): """ Add object to tickerhandler Args: - obj (Object): The object to subscribe to the ticker. - interval (int): Interval in seconds between calling - `hook_key` below. + interval (int, optional): Interval in seconds between calling + `callable(*args, **kwargs)` + callable (callable function or method, optional): This + should either be a stand-alone function or a method on a + typeclassed entity (that is, one that can be saved to the + database). idstring (str, optional): Identifier for separating this ticker-subscription from others with the same interval. Allows for managing multiple calls with - the same time interval - hook_key (str, optional): The name of the hook method - on `obj` to call every `interval` seconds. Defaults to - `at_tick(*args, **kwargs`. All hook methods must - always accept *args, **kwargs. + the same time interval and callback. args, kwargs (optional): These will be passed into the - method given by `hook_key` every time it is called. + callback every time it is called. Notes: - The combination of `obj`, `interval` and `idstring` - together uniquely defines the ticker subscription. They - must all be supplied in order to unsubscribe from it - later. + The callback will be identified by type and stored either as + as combination of serialized database object + methodname or + as a python-path to the module + funcname. These strings will + be combined iwth `interval` and `idstring` to define a + unique storage key for saving. These must thus all be supplied + when wanting to modify/remove the ticker later. """ - isdb, store_key = self._store_key(obj, interval, idstring) - if isdb: - self.ticker_storage[store_key] = (args, kwargs) - self.save() - kwargs["_hook_key"] = hook_key - self.ticker_pool.add(store_key, obj, interval, *args, **kwargs) + obj, path, callfunc = self._get_callback(callback) + store_key = self._store_key(obj, path, interval, callfunc, idstring) + self.ticker_storage[store_key] = (args, kwargs) + self.save() + kwargs["_obj"] = obj + kwargs["_callable"] = callfunc # either method-name or callable + self.ticker_pool.add(store_key, *args, **kwargs) - def remove(self, obj, interval=None, idstring=""): + def remove(self, interval=60, callback=None, idstring=""): """ Remove object from ticker or only remove it from tickers with a given interval. Args: - obj (Object): The object subscribing to the ticker. - interval (int, optional): Interval of ticker to remove. If - `None`, all tickers on this object matching `idstring` - will be removed, regardless of their `interval` setting. + interval (int, optional): Interval of ticker to remove. + callback (callable function or method): Either a function or + the method of a typeclassed object. idstring (str, optional): Identifier id of ticker to remove. """ - if interval: - isdb, store_key = self._store_key(obj, interval, idstring) - if isdb: - self.ticker_storage.pop(store_key, None) - self.save() - self.ticker_pool.remove(store_key, interval) - else: - # remove all objects with any intervals - intervals = list(self.ticker_pool.tickers) - should_save = False - for interval in intervals: - isdb, store_key = self._store_key(obj, interval, idstring) - if isdb: - self.ticker_storage.pop(store_key, None) - should_save = True - self.ticker_pool.remove(store_key, interval) - if should_save: - self.save() - - + obj, path, callfunc = self._get_callback(callback) + store_key = self._store_key(obj, path, interval, callfunc, idstring) + to_remove = self.ticker_storage.pop(store_key, None) + if to_remove: + self.ticker_pool.remove(store_key) + self.save() def clear(self, interval=None): """ - Stop/remove all tickers from handler. + Stop/remove tickers from handler. Args: interval (int): Only stop tickers with this interval. @@ -464,6 +485,5 @@ class TickerHandler(object): if ticker: return listvalues(ticker.subscriptions) - # main tickerhandler TICKER_HANDLER = TickerHandler()