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.

This commit is contained in:
Griatch 2016-03-20 23:12:00 +01:00
parent 8090d92d85
commit 77b178bf28

View file

@ -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()