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:
parent
8090d92d85
commit
77b178bf28
1 changed files with 129 additions and 109 deletions
|
|
@ -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.
|
call the handler's `save()` and `restore()` methods when the server reboots.
|
||||||
|
|
||||||
"""
|
"""
|
||||||
|
import inspect
|
||||||
from builtins import object
|
from builtins import object
|
||||||
from future.utils import listvalues
|
from future.utils import listvalues
|
||||||
|
|
||||||
|
|
@ -63,15 +64,15 @@ from evennia.scripts.scripts import ExtendedLoopingCall
|
||||||
from evennia.server.models import ServerConfig
|
from evennia.server.models import ServerConfig
|
||||||
from evennia.utils.logger import log_trace, log_err
|
from evennia.utils.logger import log_trace, log_err
|
||||||
from evennia.utils.dbserialize import dbserialize, dbunserialize, pack_dbobj
|
from evennia.utils.dbserialize import dbserialize, dbunserialize, pack_dbobj
|
||||||
|
from evennia.utils import variable_from_module
|
||||||
|
|
||||||
_GA = object.__getattribute__
|
_GA = object.__getattribute__
|
||||||
_SA = object.__setattr__
|
_SA = object.__setattr__
|
||||||
|
|
||||||
|
|
||||||
_ERROR_ADD_INTERVAL = \
|
_ERROR_ADD_TICKER = \
|
||||||
"""TickerHandler: Tried to add a ticker with invalid interval:
|
"""TickerHandler: Tried to add an invalid ticker:
|
||||||
obj={obj}, interval={interval}, args={args}, kwargs={kwargs}
|
{storekey}
|
||||||
store_key={store_key}
|
|
||||||
Ticker was not added."""
|
Ticker was not added."""
|
||||||
|
|
||||||
class Ticker(object):
|
class Ticker(object):
|
||||||
|
|
@ -98,14 +99,22 @@ class Ticker(object):
|
||||||
kwargs is used here to identify which hook method to call.
|
kwargs is used here to identify which hook method to call.
|
||||||
|
|
||||||
"""
|
"""
|
||||||
for store_key, (obj, args, kwargs) in self.subscriptions.items():
|
for store_key, (args, kwargs) in self.subscriptions.iteritems():
|
||||||
hook_key = yield kwargs.pop("_hook_key", "at_tick")
|
callback = yield kwargs.pop("_callback", "at_tick")
|
||||||
if not obj or not obj.pk:
|
|
||||||
# object was deleted between calls
|
|
||||||
self.remove(store_key)
|
|
||||||
continue
|
|
||||||
try:
|
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:
|
except ObjectDoesNotExist:
|
||||||
log_trace()
|
log_trace()
|
||||||
self.remove(store_key)
|
self.remove(store_key)
|
||||||
|
|
@ -113,7 +122,7 @@ class Ticker(object):
|
||||||
log_trace()
|
log_trace()
|
||||||
finally:
|
finally:
|
||||||
# make sure to re-store
|
# make sure to re-store
|
||||||
kwargs["_hook_key"] = hook_key
|
kwargs["_callback"] = callback
|
||||||
|
|
||||||
def __init__(self, interval):
|
def __init__(self, interval):
|
||||||
"""
|
"""
|
||||||
|
|
@ -138,34 +147,27 @@ class Ticker(object):
|
||||||
|
|
||||||
"""
|
"""
|
||||||
subs = self.subscriptions
|
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 self.task.running:
|
||||||
if not subs:
|
if not subs:
|
||||||
self.task.stop()
|
self.task.stop()
|
||||||
elif subs:
|
elif subs:
|
||||||
self.task.start(self.interval, now=False, start_delay=start_delay)
|
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.
|
Sign up a subscriber to this ticker.
|
||||||
Args:
|
Args:
|
||||||
store_key (str): Unique storage hash for this ticker subscription.
|
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.
|
args (any, optional): Arguments to call the hook method with.
|
||||||
|
|
||||||
Kwargs:
|
Kwargs:
|
||||||
_start_delay (int): If set, this will be
|
_start_delay (int): If set, this will be
|
||||||
used to delay the start of the trigger instead of
|
used to delay the start of the trigger instead of
|
||||||
`interval`.
|
`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)
|
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)
|
self.validate(start_delay=start_delay)
|
||||||
|
|
||||||
def remove(self, store_key):
|
def remove(self, store_key):
|
||||||
|
|
@ -204,46 +206,33 @@ class TickerPool(object):
|
||||||
"""
|
"""
|
||||||
self.tickers = {}
|
self.tickers = {}
|
||||||
|
|
||||||
def add(self, store_key, obj, interval, *args, **kwargs):
|
def add(self, store_key, *args, **kwargs):
|
||||||
"""
|
"""
|
||||||
Add new ticker subscriber.
|
Add new ticker subscriber.
|
||||||
|
|
||||||
Args:
|
Args:
|
||||||
store_key (str): Unique storage hash.
|
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.
|
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:
|
if not interval:
|
||||||
log_err(_ERROR_ADD_INTERVAL.format(store_key=store_key, obj=obj,
|
log_err(_ERROR_ADD_TICKER.format(store_key=store_key))
|
||||||
interval=interval, args=args, kwargs=kwargs))
|
|
||||||
return
|
return
|
||||||
|
|
||||||
if interval not in self.tickers:
|
if interval not in self.tickers:
|
||||||
self.tickers[interval] = self.ticker_class(interval)
|
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.
|
Remove subscription from pool.
|
||||||
|
|
||||||
Args:
|
Args:
|
||||||
store_key (str): Unique storage hash.
|
store_key (str): Unique storage hash to remove
|
||||||
interval (int): Ticker interval.
|
|
||||||
|
|
||||||
Notes:
|
|
||||||
A given subscription is uniquely identified both
|
|
||||||
via its `store_key` and its `interval`.
|
|
||||||
|
|
||||||
"""
|
"""
|
||||||
|
_, _, _, interval, _ = store_key
|
||||||
if interval in self.tickers:
|
if interval in self.tickers:
|
||||||
self.tickers[interval].remove(store_key)
|
self.tickers[interval].remove(store_key)
|
||||||
|
|
||||||
|
|
@ -285,33 +274,65 @@ class TickerHandler(object):
|
||||||
self.save_name = save_name
|
self.save_name = save_name
|
||||||
self.ticker_pool = self.ticker_pool_class()
|
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
|
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
|
(isdb, store_key) where isdb is a boolean True if obj was a
|
||||||
database object, False otherwise.
|
database object, False otherwise.
|
||||||
|
|
||||||
Args:
|
Args:
|
||||||
obj (Object): Subscribing object.
|
obj (Object or None): Subscribing object if any.
|
||||||
interval (int): Ticker interval
|
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
|
idstring (str, optional): Additional separator between
|
||||||
different subscription types.
|
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"):
|
outobj = pack_dbobj(obj) if obj and hasattr(obj, "db_key") else None
|
||||||
# create a store_key using the database representation
|
outpath = path if isinstance(basestring, path) else None
|
||||||
objkey = pack_dbobj(obj)
|
methodname = callfunc if callfunc and isinstance(basestring, callfunc) else None
|
||||||
isdb = True
|
return (outobj, methodname, outpath, interval, idstring)
|
||||||
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)
|
|
||||||
|
|
||||||
def save(self):
|
def save(self):
|
||||||
"""
|
"""
|
||||||
|
|
@ -346,82 +367,82 @@ class TickerHandler(object):
|
||||||
# load stored command instructions and use them to re-initialize handler
|
# load stored command instructions and use them to re-initialize handler
|
||||||
ticker_storage = ServerConfig.objects.conf(key=self.save_name)
|
ticker_storage = ServerConfig.objects.conf(key=self.save_name)
|
||||||
if ticker_storage:
|
if ticker_storage:
|
||||||
|
# the dbunserialize will convert all serialized dbobjs to real objects
|
||||||
self.ticker_storage = dbunserialize(ticker_storage)
|
self.ticker_storage = dbunserialize(ticker_storage)
|
||||||
for store_key, (args, kwargs) in self.ticker_storage.items():
|
for store_key, (args, kwargs) in self.ticker_storage.iteritems():
|
||||||
obj, interval, idstring = store_key
|
try:
|
||||||
_, store_key = self._store_key(obj, interval, idstring)
|
obj, methodname, path, interval, idstring = store_key
|
||||||
self.ticker_pool.add(store_key, obj, interval, *args, **kwargs)
|
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
|
Add object to tickerhandler
|
||||||
|
|
||||||
Args:
|
Args:
|
||||||
obj (Object): The object to subscribe to the ticker.
|
interval (int, optional): Interval in seconds between calling
|
||||||
interval (int): Interval in seconds between calling
|
`callable(*args, **kwargs)`
|
||||||
`hook_key` below.
|
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
|
idstring (str, optional): Identifier for separating
|
||||||
this ticker-subscription from others with the same
|
this ticker-subscription from others with the same
|
||||||
interval. Allows for managing multiple calls with
|
interval. Allows for managing multiple calls with
|
||||||
the same time interval
|
the same time interval and callback.
|
||||||
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.
|
|
||||||
args, kwargs (optional): These will be passed into the
|
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:
|
Notes:
|
||||||
The combination of `obj`, `interval` and `idstring`
|
The callback will be identified by type and stored either as
|
||||||
together uniquely defines the ticker subscription. They
|
as combination of serialized database object + methodname or
|
||||||
must all be supplied in order to unsubscribe from it
|
as a python-path to the module + funcname. These strings will
|
||||||
later.
|
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)
|
obj, path, callfunc = self._get_callback(callback)
|
||||||
if isdb:
|
store_key = self._store_key(obj, path, interval, callfunc, idstring)
|
||||||
self.ticker_storage[store_key] = (args, kwargs)
|
self.ticker_storage[store_key] = (args, kwargs)
|
||||||
self.save()
|
self.save()
|
||||||
kwargs["_hook_key"] = hook_key
|
kwargs["_obj"] = obj
|
||||||
self.ticker_pool.add(store_key, obj, interval, *args, **kwargs)
|
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
|
Remove object from ticker or only remove it from tickers with
|
||||||
a given interval.
|
a given interval.
|
||||||
|
|
||||||
Args:
|
Args:
|
||||||
obj (Object): The object subscribing to the ticker.
|
interval (int, optional): Interval of ticker to remove.
|
||||||
interval (int, optional): Interval of ticker to remove. If
|
callback (callable function or method): Either a function or
|
||||||
`None`, all tickers on this object matching `idstring`
|
the method of a typeclassed object.
|
||||||
will be removed, regardless of their `interval` setting.
|
|
||||||
idstring (str, optional): Identifier id of ticker to remove.
|
idstring (str, optional): Identifier id of ticker to remove.
|
||||||
|
|
||||||
"""
|
"""
|
||||||
if interval:
|
obj, path, callfunc = self._get_callback(callback)
|
||||||
isdb, store_key = self._store_key(obj, interval, idstring)
|
store_key = self._store_key(obj, path, interval, callfunc, idstring)
|
||||||
if isdb:
|
to_remove = self.ticker_storage.pop(store_key, None)
|
||||||
self.ticker_storage.pop(store_key, None)
|
if to_remove:
|
||||||
self.save()
|
self.ticker_pool.remove(store_key)
|
||||||
self.ticker_pool.remove(store_key, interval)
|
self.save()
|
||||||
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()
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
def clear(self, interval=None):
|
def clear(self, interval=None):
|
||||||
"""
|
"""
|
||||||
Stop/remove all tickers from handler.
|
Stop/remove tickers from handler.
|
||||||
|
|
||||||
Args:
|
Args:
|
||||||
interval (int): Only stop tickers with this interval.
|
interval (int): Only stop tickers with this interval.
|
||||||
|
|
@ -464,6 +485,5 @@ class TickerHandler(object):
|
||||||
if ticker:
|
if ticker:
|
||||||
return listvalues(ticker.subscriptions)
|
return listvalues(ticker.subscriptions)
|
||||||
|
|
||||||
|
|
||||||
# main tickerhandler
|
# main tickerhandler
|
||||||
TICKER_HANDLER = TickerHandler()
|
TICKER_HANDLER = TickerHandler()
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue