mirror of
https://github.com/evennia/evennia.git
synced 2026-03-28 02:36:32 +01:00
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.
|
||||
|
||||
"""
|
||||
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()
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue