From 6c05651e1160c981189451af04ea1f345adacd11 Mon Sep 17 00:00:00 2001 From: davewiththenicehat <54369722+davewiththenicehat@users.noreply.github.com> Date: Wed, 7 Apr 2021 17:02:22 -0400 Subject: [PATCH 01/18] .gitignore Atom's remote sync settings file Atom's remote sync settings file It contains clear text passwords. As well as settings that are user specific. --- .gitignore | 3 +++ 1 file changed, 3 insertions(+) diff --git a/.gitignore b/.gitignore index 6641f347d9..79d177f411 100644 --- a/.gitignore +++ b/.gitignore @@ -50,3 +50,6 @@ twistd.bat # never commit docs/build docs/build + +# Atom's remote sync settings file (it contains clear text password) +.remote-sync.json From d0d2a9384f4ffb20474e21e6642aca8136872611 Mon Sep 17 00:00:00 2001 From: davewiththenicehat <54369722+davewiththenicehat@users.noreply.github.com> Date: Wed, 7 Apr 2021 17:36:54 -0400 Subject: [PATCH 02/18] Revert ".gitignore Atom's remote sync settings file" This reverts commit 6c05651e1160c981189451af04ea1f345adacd11. --- .gitignore | 3 --- 1 file changed, 3 deletions(-) diff --git a/.gitignore b/.gitignore index 79d177f411..6641f347d9 100644 --- a/.gitignore +++ b/.gitignore @@ -50,6 +50,3 @@ twistd.bat # never commit docs/build docs/build - -# Atom's remote sync settings file (it contains clear text password) -.remote-sync.json From f445cfb355a15d85c72ac72ca97313c55da5b9d8 Mon Sep 17 00:00:00 2001 From: davewiththenicehat <54369722+davewiththenicehat@users.noreply.github.com> Date: Wed, 7 Apr 2021 18:00:02 -0400 Subject: [PATCH 03/18] TaskHandler Return task_id if persistent TaskHandler Return task_id if persistent unit tests passed 645 ran --- evennia/scripts/taskhandler.py | 2 ++ 1 file changed, 2 insertions(+) diff --git a/evennia/scripts/taskhandler.py b/evennia/scripts/taskhandler.py index 6f5dccc4f9..9506addef4 100644 --- a/evennia/scripts/taskhandler.py +++ b/evennia/scripts/taskhandler.py @@ -149,6 +149,8 @@ class TaskHandler(object): callback = self.do_task args = [task_id] kwargs = {} + deferLater(reactor, timedelay, callback, *args, **kwargs) + return task_id return deferLater(reactor, timedelay, callback, *args, **kwargs) From f62ff2015fcb5e98f0dabbe5975745b4c1947d4a Mon Sep 17 00:00:00 2001 From: davewiththenicehat <54369722+davewiththenicehat@users.noreply.github.com> Date: Tue, 13 Apr 2021 17:01:55 -0400 Subject: [PATCH 04/18] taskhandler verified do_task causes errors if called manually (early) Any usage of taskhandler's method outside of the deferred instance calling it results in errors. Referencing: It's easier to access these tasks (should it be necessary) using `evennia.scripts.taskhandler.TASK_HANDLER` I can see it is intended to be used this way. More importantly usage of the global reactor would require usage of reactorbuilder API which is recomended for building reactors only. Commiting notes before switching to twisted's documented methods for making and testing deferrals. In short I need to get an instance of reactor's callLater. Creating and working with that call later will allow me to test taskhandler and make it function as intended. Usage of utils.delay will not change. --- evennia/scripts/taskhandler.py | 27 +++++++++++++++++--- evennia/utils/tests/test_utils.py | 42 ++++++++++++++++++++++++++++++- evennia/utils/utils.py | 15 +++++++++-- 3 files changed, 78 insertions(+), 6 deletions(-) diff --git a/evennia/scripts/taskhandler.py b/evennia/scripts/taskhandler.py index 9506addef4..1fd8a211f1 100644 --- a/evennia/scripts/taskhandler.py +++ b/evennia/scripts/taskhandler.py @@ -19,8 +19,11 @@ class TaskHandler(object): A light singleton wrapper allowing to access permanent tasks. When `utils.delay` is called, the task handler is used to create - the task. If `utils.delay` is called with `persistent=True`, the - task handler stores the new task and saves. + the task. + If `utils.delay` is called with `persistent=True`. The + task handler creates the task. It saves the delay time, callback, + arguments and kwa to the database. Each of these variables are + serialized to do this. It's easier to access these tasks (should it be necessary) using `evennia.scripts.taskhandler.TASK_HANDLER`, which contains one @@ -101,9 +104,27 @@ class TaskHandler(object): any (any): any additional positional arguments to send to the callback Keyword Args: - persistent (bool, optional): persist the task (store it). + persistent (bool, optional): persist the task (stores it). + Add will return the task's id for use with the global TASK_HANDLER. any (any): additional keyword arguments to send to the callback + Returns: + twisted.internet.defer.Deferred instance of the deferred task + task_id (int), the task's id intended for use with this class. + + Notes: + This method has two return types. + An instance of twisted's Deferred class is standard. + If the persistent kwarg is truthy instead a task ID will be returned. + This task id can be used with task handler's do_task and remove methods. + + If the persistent kwarg is truthy: + The callback, args and values for kwarg will be serialized. Type + and attribute errors during the serialization will be logged, + but will not throw exceptions. + Do not use memory references in the callback function or arguments. + As those memory references will no longer acurately point to + the variable desired. """ persistent = kwargs.get("persistent", False) if persistent: diff --git a/evennia/utils/tests/test_utils.py b/evennia/utils/tests/test_utils.py index 67625bbfbb..b1a97c984d 100644 --- a/evennia/utils/tests/test_utils.py +++ b/evennia/utils/tests/test_utils.py @@ -6,13 +6,15 @@ TODO: Not nearly all utilities are covered yet. """ import os.path - import mock + from django.test import TestCase from datetime import datetime +from twisted.internet import task, reactor from evennia.utils.ansi import ANSIString from evennia.utils import utils +from evennia.utils.test_resources import EvenniaTest class TestIsIter(TestCase): @@ -292,3 +294,41 @@ class LatinifyTest(TestCase): byte_str = utils.to_bytes(self.example_str) result = utils.latinify(byte_str) self.assertEqual(result, self.expected_output) + + +_TASK_HANDLER = None + + +def dummy_func(obj): + """ + Used in TestDelay. + + A function that: + can be serialized + uses no memory references + uses evennia objects + """ + # get a reference of object + from evennia.objects.models import ObjectDB + obj = ObjectDB.objects.object_search(obj) + obj = obj[0] + # make changes to object + obj.ndb.dummy_var = 'dummy_func ran' + + +class TestDelay(EvenniaTest): + """ + Test utils.delay. + """ + + def test_delay(self): + # get a reference of TASK_HANDLER + global _TASK_HANDLER + if _TASK_HANDLER is None: + from evennia.scripts.taskhandler import TASK_HANDLER as _TASK_HANDLER + self.char1.ndb.dummy_var = False + # test a persistent deferral + task_id = utils.delay(1, dummy_func, self.char1.dbref, persistent=True) + _TASK_HANDLER.do_task(task_id) # run the deferred task + self.assertEqual(self.char1.ndb.dummy_var, 'dummy_func ran') + self.char1.ndb.dummy_var = False diff --git a/evennia/utils/utils.py b/evennia/utils/utils.py index b0abe2b9c7..105f9474a6 100644 --- a/evennia/utils/utils.py +++ b/evennia/utils/utils.py @@ -1022,7 +1022,7 @@ _TASK_HANDLER = None def delay(timedelay, callback, *args, **kwargs): """ - Delay the return of a value. + Delay the calling of a callback (function). Args: timedelay (int or float): The delay in seconds @@ -1040,15 +1040,26 @@ def delay(timedelay, callback, *args, **kwargs): commandhandler callback chain, the callback chain can be defined directly in the command body and don't need to be specified here. + Reference twisted.internet.defer.Deferred + if persistent kwarg is truthy: + task_id (int): the task's id intended for use with + evennia.scripts.taskhandler.TASK_HANDLER's do_task and remove methods. Note: The task handler (`evennia.scripts.taskhandler.TASK_HANDLER`) will be called for persistent or non-persistent tasks. If persistent is set to True, the callback, its arguments - and other keyword arguments will be saved in the database, + and other keyword arguments will be saved (serialized) in the database, assuming they can be. The callback will be executed even after a server restart/reload, taking into account the specified delay (and server down time). + Keep in mind that persistent tasks arguments and callback should not + use memory references. + If persistent is set to True the delay function will return an int + which is the task's id itended for use with TASK_HANDLER's do_task + and remove methods. + + All task's whose time delays have passed will be called on server startup. """ global _TASK_HANDLER From 1a18b247e2e394a3bcdc2baf99af756ed1ca722b Mon Sep 17 00:00:00 2001 From: davewiththenicehat <54369722+davewiththenicehat@users.noreply.github.com> Date: Wed, 14 Apr 2021 12:14:04 -0400 Subject: [PATCH 05/18] Task_Handler unit test, after completion time only Unit test for task handler tasks that complete after delay time. Required creating local reference for a clock. All evennia unit tests pass with `evennia test evennia`. All of my projects unit tests passed. They have a very heavy usage of reactor. Verified delays working with project manually Moving forward with creating method to call callbacks early. --- evennia/scripts/taskhandler.py | 12 +++++++++--- evennia/utils/tests/test_utils.py | 10 ++++++++-- 2 files changed, 17 insertions(+), 5 deletions(-) diff --git a/evennia/scripts/taskhandler.py b/evennia/scripts/taskhandler.py index 1fd8a211f1..11ac144a94 100644 --- a/evennia/scripts/taskhandler.py +++ b/evennia/scripts/taskhandler.py @@ -29,11 +29,17 @@ class TaskHandler(object): `evennia.scripts.taskhandler.TASK_HANDLER`, which contains one instance of this class, and use its `add` and `remove` methods. + Dev notes: + deferLater creates an instance of IDelayedCall using reactor.callLater. + deferLater uses the cancel method on the IDelayedCall instance to create + the defer instance it returns. + """ def __init__(self): self.tasks = {} self.to_save = {} + self.clock = reactor def load(self): """Load from the ServerConfig. @@ -170,10 +176,10 @@ class TaskHandler(object): callback = self.do_task args = [task_id] kwargs = {} - deferLater(reactor, timedelay, callback, *args, **kwargs) + deferLater(self.clock, timedelay, callback, *args, **kwargs) return task_id - return deferLater(reactor, timedelay, callback, *args, **kwargs) + return deferLater(self.clock, timedelay, callback, *args, **kwargs) def remove(self, task_id): """Remove a persistent task without executing it. @@ -219,7 +225,7 @@ class TaskHandler(object): now = datetime.now() for task_id, (date, callbac, args, kwargs) in self.tasks.items(): seconds = max(0, (date - now).total_seconds()) - deferLater(reactor, seconds, self.do_task, task_id) + deferLater(self.clock, seconds, self.do_task, task_id) # Create the soft singleton diff --git a/evennia/utils/tests/test_utils.py b/evennia/utils/tests/test_utils.py index b1a97c984d..ea23601fce 100644 --- a/evennia/utils/tests/test_utils.py +++ b/evennia/utils/tests/test_utils.py @@ -326,9 +326,15 @@ class TestDelay(EvenniaTest): global _TASK_HANDLER if _TASK_HANDLER is None: from evennia.scripts.taskhandler import TASK_HANDLER as _TASK_HANDLER + _TASK_HANDLER.clock = task.Clock() self.char1.ndb.dummy_var = False - # test a persistent deferral + # test a persistent deferral, that completes after delay time task_id = utils.delay(1, dummy_func, self.char1.dbref, persistent=True) - _TASK_HANDLER.do_task(task_id) # run the deferred task + _TASK_HANDLER.clock.advance(1) # make time pass + self.assertEqual(self.char1.ndb.dummy_var, 'dummy_func ran') + self.char1.ndb.dummy_var = False + # test a non persisten deferral, that completes after delay time. + deferal_inst = utils.delay(1, dummy_func, self.char1.dbref) + _TASK_HANDLER.clock.advance(1) # make time pass self.assertEqual(self.char1.ndb.dummy_var, 'dummy_func ran') self.char1.ndb.dummy_var = False From c7bf7736055c0ae174823d1d2a21fa7a253940d7 Mon Sep 17 00:00:00 2001 From: davewiththenicehat <54369722+davewiththenicehat@users.noreply.github.com> Date: Fri, 16 Apr 2021 10:55:24 -0400 Subject: [PATCH 06/18] taskhandler suppress cancel error Added an errback to handle errors within task handler's deferred instance. Without this instances of deferred cause a traceback when a deferred is canceled without errback or callback having been called. This traceback does not end execution, and ultimately would only show to main console. Reference cancel: https://github.com/twisted/twisted/blob/trunk/src/twisted/internet/defer.py All evennia unit tests pass. --- evennia/scripts/taskhandler.py | 16 +++++++++++++++- evennia/utils/tests/test_utils.py | 8 +++++++- 2 files changed, 22 insertions(+), 2 deletions(-) diff --git a/evennia/scripts/taskhandler.py b/evennia/scripts/taskhandler.py index 11ac144a94..3273e27e8a 100644 --- a/evennia/scripts/taskhandler.py +++ b/evennia/scripts/taskhandler.py @@ -6,6 +6,7 @@ from datetime import datetime, timedelta from twisted.internet import reactor from twisted.internet.task import deferLater +from twisted.internet.defer import CancelledError as DefCancelledError from evennia.server.models import ServerConfig from evennia.utils.logger import log_err from evennia.utils.dbserialize import dbserialize, dbunserialize @@ -13,6 +14,17 @@ from evennia.utils.dbserialize import dbserialize, dbunserialize TASK_HANDLER = None +def handle_error(*args, **kwargs): + """ + Handle errors withing deferred objects. + """ + for arg in args: + # suppress cancel errors + if arg.type == DefCancelledError: + continue + raise arg + + class TaskHandler(object): """ @@ -179,7 +191,9 @@ class TaskHandler(object): deferLater(self.clock, timedelay, callback, *args, **kwargs) return task_id - return deferLater(self.clock, timedelay, callback, *args, **kwargs) + d = deferLater(self.clock, timedelay, callback, *args, **kwargs) + d.addErrback(handle_error) + return d def remove(self, task_id): """Remove a persistent task without executing it. diff --git a/evennia/utils/tests/test_utils.py b/evennia/utils/tests/test_utils.py index ea23601fce..bebea149ce 100644 --- a/evennia/utils/tests/test_utils.py +++ b/evennia/utils/tests/test_utils.py @@ -10,7 +10,7 @@ import mock from django.test import TestCase from datetime import datetime -from twisted.internet import task, reactor +from twisted.internet import task from evennia.utils.ansi import ANSIString from evennia.utils import utils @@ -338,3 +338,9 @@ class TestDelay(EvenniaTest): _TASK_HANDLER.clock.advance(1) # make time pass self.assertEqual(self.char1.ndb.dummy_var, 'dummy_func ran') self.char1.ndb.dummy_var = False + # test canceling a deferral. + deferal_inst = utils.delay(1, dummy_func, self.char1.dbref) + deferal_inst.cancel() + _TASK_HANDLER.clock.advance(1) # make time pass + self.assertEqual(self.char1.ndb.dummy_var, False) + self.char1.ndb.dummy_var = False From 97f7806348bb2c0bc8ef18ed7116804f9ccffdc9 Mon Sep 17 00:00:00 2001 From: davewiththenicehat <54369722+davewiththenicehat@users.noreply.github.com> Date: Sun, 18 Apr 2021 00:43:09 -0400 Subject: [PATCH 07/18] task handler, updated to only return task id Updated task handler to only return task id. updated code within evennia that relied on the deferral directly. Including unit test for one. all unit tests pass. Test server functional after restarting, no issues found would telnet web client. (delay was used in the telnet module in the portal folder. I needed to commit this before continuing forward. There is already a high line count change. --- evennia/contrib/slow_exit.py | 5 +- evennia/contrib/tests.py | 6 -- evennia/contrib/tutorial_world/objects.py | 7 +- evennia/scripts/taskhandler.py | 85 +++++++++++++++++------ evennia/server/portal/telnet.py | 5 +- evennia/utils/tests/test_utils.py | 19 +++-- 6 files changed, 86 insertions(+), 41 deletions(-) diff --git a/evennia/contrib/slow_exit.py b/evennia/contrib/slow_exit.py index 107d274b8c..3b060f7471 100644 --- a/evennia/contrib/slow_exit.py +++ b/evennia/contrib/slow_exit.py @@ -36,6 +36,7 @@ TickerHandler might be better. """ from evennia import DefaultExit, utils, Command +from evennia.scripts.taskhandler import TASK_HANDLER as _TASK_HANDLER MOVE_DELAY = {"stroll": 6, "walk": 4, "run": 2, "sprint": 1} @@ -70,11 +71,11 @@ class SlowExit(DefaultExit): traversing_object.msg("You start moving %s at a %s." % (self.key, move_speed)) # create a delayed movement - deferred = utils.delay(move_delay, move_callback) + task_id = utils.delay(move_delay, move_callback) # we store the deferred on the character, this will allow us # to abort the movement. We must use an ndb here since # deferreds cannot be pickled. - traversing_object.ndb.currently_moving = deferred + traversing_object.ndb.currently_moving = _TASK_HANDLER.get_deferred(task_id) # diff --git a/evennia/contrib/tests.py b/evennia/contrib/tests.py index a4e50a29f7..7b69d07cc9 100644 --- a/evennia/contrib/tests.py +++ b/evennia/contrib/tests.py @@ -1152,13 +1152,7 @@ from evennia.contrib import slow_exit slow_exit.MOVE_DELAY = {"stroll": 0, "walk": 0, "run": 0, "sprint": 0} -def _cancellable_mockdelay(time, callback, *args, **kwargs): - callback(*args, **kwargs) - return Mock() - - class TestSlowExit(CommandTest): - @patch("evennia.utils.delay", _cancellable_mockdelay) def test_exit(self): exi = create_object( slow_exit.SlowExit, key="slowexit", location=self.room1, destination=self.room2 diff --git a/evennia/contrib/tutorial_world/objects.py b/evennia/contrib/tutorial_world/objects.py index ca4147a715..759d1f1dc0 100644 --- a/evennia/contrib/tutorial_world/objects.py +++ b/evennia/contrib/tutorial_world/objects.py @@ -24,6 +24,7 @@ import random from evennia import DefaultObject, DefaultExit, Command, CmdSet from evennia.utils import search, delay, dedent from evennia.prototypes.spawner import spawn +from evennia.scripts.taskhandler import TASK_HANDLER as _TASK_HANDLER # ------------------------------------------------------------- # @@ -389,7 +390,8 @@ class LightSource(TutorialObject): # start the burn timer. When it runs out, self._burnout # will be called. We store the deferred so it can be # killed in unittesting. - self.deferred = delay(60 * 3, self._burnout) + task_id = delay(60 * 3, self._burnout) + self.deferred = _TASK_HANDLER.get_deferred(task_id) return True @@ -687,7 +689,8 @@ class CrumblingWall(TutorialObject, DefaultExit): self.db.exit_open = True # start a 45 second timer before closing again. We store the deferred so it can be # killed in unittesting. - self.deferred = delay(45, self.reset) + task_id = delay(45, self.reset) + self.deferred = _TASK_HANDLER.get_deferred(task_id) return True def _translate_position(self, root, ipos): diff --git a/evennia/scripts/taskhandler.py b/evennia/scripts/taskhandler.py index 3273e27e8a..b865a74185 100644 --- a/evennia/scripts/taskhandler.py +++ b/evennia/scripts/taskhandler.py @@ -44,7 +44,6 @@ class TaskHandler(object): Dev notes: deferLater creates an instance of IDelayedCall using reactor.callLater. deferLater uses the cancel method on the IDelayedCall instance to create - the defer instance it returns. """ @@ -79,16 +78,18 @@ class TaskHandler(object): continue callback = getattr(obj, method) - self.tasks[task_id] = (date, callback, args, kwargs) + self.tasks[task_id] = date, callback, args, kwargs, True, None if to_save: self.save() def save(self): """Save the tasks in ServerConfig.""" - for task_id, (date, callback, args, kwargs) in self.tasks.items(): + for task_id, (date, callback, args, kwargs, persistent, _) in self.tasks.items(): if task_id in self.to_save: continue + if not persistent: + continue if getattr(callback, "__self__", None): # `callback` is an instance method @@ -127,8 +128,8 @@ class TaskHandler(object): any (any): additional keyword arguments to send to the callback Returns: - twisted.internet.defer.Deferred instance of the deferred task task_id (int), the task's id intended for use with this class. + False, if the task has completed before addition finishes. Notes: This method has two return types. @@ -144,19 +145,23 @@ class TaskHandler(object): As those memory references will no longer acurately point to the variable desired. """ + # set the completion time + # Only used on persistent tasks after a restart + now = datetime.now() + delta = timedelta(seconds=timedelay) + comp_time = now + delta + # get an open task id + used_ids = list(self.tasks.keys()) + task_id = 1 + while task_id in used_ids: + task_id += 1 + + # record the task to the tasks dictionary persistent = kwargs.get("persistent", False) if persistent: del kwargs["persistent"] - now = datetime.now() - delta = timedelta(seconds=timedelay) - - # Choose a free task_id safe_args = [] safe_kwargs = {} - used_ids = list(self.tasks.keys()) - task_id = 1 - while task_id in used_ids: - task_id += 1 # Check that args and kwargs contain picklable information for arg in args: @@ -183,17 +188,28 @@ class TaskHandler(object): else: safe_kwargs[key] = value - self.tasks[task_id] = (now + delta, callback, safe_args, safe_kwargs) + self.tasks[task_id] = (comp_time, callback, safe_args, safe_kwargs, True, None) self.save() - callback = self.do_task - args = [task_id] - kwargs = {} - deferLater(self.clock, timedelay, callback, *args, **kwargs) - return task_id + else: # this is a non-persitent task + self.tasks[task_id] = (comp_time, callback, args, kwargs, True, None) + # defer the task + callback = self.do_task + args = [task_id] + kwargs = {} d = deferLater(self.clock, timedelay, callback, *args, **kwargs) d.addErrback(handle_error) - return d + + # some tasks may complete before the deferal can be added + if task_id in self.tasks: + task = self.tasks.get(task_id) + task = list(task) + task[4] = persistent + task[5] = d + self.tasks[task_id] = task + else: # the task already completed + return False + return task_id def remove(self, task_id): """Remove a persistent task without executing it. @@ -206,7 +222,8 @@ class TaskHandler(object): in the TaskHandler. """ - del self.tasks[task_id] + if task_id in self.tasks: + del self.tasks[task_id] if task_id in self.to_save: del self.to_save[task_id] @@ -222,13 +239,30 @@ class TaskHandler(object): This will also remove it from the list of current tasks. """ - date, callback, args, kwargs = self.tasks.pop(task_id) + date, callback, args, kwargs, persistent, d = self.tasks.pop(task_id) + if task_id in self.to_save: del self.to_save[task_id] self.save() callback(*args, **kwargs) + def get_deferred(self, task_id): + """ + Return the instance of the deferred the task id is using. + + Args: + task_id (int): a valid task ID. + + Returns: + An instance of a deferral or False if there is no task with the id. + None is returned if there is no deferral affiliated with this id. + """ + if task_id in self.tasks: + return self.tasks[task_id][5] + else: + return False + def create_delays(self): """Create the delayed tasks for the persistent tasks. @@ -237,9 +271,14 @@ class TaskHandler(object): """ now = datetime.now() - for task_id, (date, callbac, args, kwargs) in self.tasks.items(): + for task_id, (date, callbac, args, kwargs, _, _) in self.tasks.items(): + self.tasks[task_id] = date, callbac, args, kwargs, True, None seconds = max(0, (date - now).total_seconds()) - deferLater(self.clock, seconds, self.do_task, task_id) + d = deferLater(self.clock, seconds, self.do_task, task_id) + d.addErrback(handle_error) + # some tasks may complete before the deferal can be added + if self.tasks.get(task_id, False): + self.tasks[task_id] = date, callbac, args, kwargs, True, d # Create the soft singleton diff --git a/evennia/server/portal/telnet.py b/evennia/server/portal/telnet.py index 821e96aa24..42facf7e65 100644 --- a/evennia/server/portal/telnet.py +++ b/evennia/server/portal/telnet.py @@ -31,6 +31,7 @@ from evennia.server.portal.mccp import Mccp, mccp_compress, MCCP from evennia.server.portal.mxp import Mxp, mxp_parse from evennia.utils import ansi from evennia.utils.utils import to_bytes +from evennia.scripts.taskhandler import TASK_HANDLER as _TASK_HANDLER _RE_N = re.compile(r"\|n$") _RE_LEND = re.compile(br"\n$|\r$|\r\n$|\r\x00$|", re.MULTILINE) @@ -127,8 +128,8 @@ class TelnetProtocol(Telnet, StatefulTelnetProtocol, Session): from evennia.utils.utils import delay - # timeout the handshakes in case the client doesn't reply at all - self._handshake_delay = delay(2, callback=self.handshake_done, timeout=True) + task_id = delay(2, callback=self.handshake_done, timeout=True) + self._handshake_delay = _TASK_HANDLER.get_deferred(task_id) # TCP/IP keepalive watches for dead links self.transport.setTcpKeepAlive(1) diff --git a/evennia/utils/tests/test_utils.py b/evennia/utils/tests/test_utils.py index bebea149ce..ae4d49e90f 100644 --- a/evennia/utils/tests/test_utils.py +++ b/evennia/utils/tests/test_utils.py @@ -323,24 +323,31 @@ class TestDelay(EvenniaTest): def test_delay(self): # get a reference of TASK_HANDLER + timedelay = 5 global _TASK_HANDLER if _TASK_HANDLER is None: from evennia.scripts.taskhandler import TASK_HANDLER as _TASK_HANDLER _TASK_HANDLER.clock = task.Clock() self.char1.ndb.dummy_var = False # test a persistent deferral, that completes after delay time - task_id = utils.delay(1, dummy_func, self.char1.dbref, persistent=True) - _TASK_HANDLER.clock.advance(1) # make time pass + task_id = utils.delay(timedelay, dummy_func, self.char1.dbref, persistent=True) + _TASK_HANDLER.clock.advance(timedelay) # make time pass self.assertEqual(self.char1.ndb.dummy_var, 'dummy_func ran') self.char1.ndb.dummy_var = False # test a non persisten deferral, that completes after delay time. - deferal_inst = utils.delay(1, dummy_func, self.char1.dbref) - _TASK_HANDLER.clock.advance(1) # make time pass + utils.delay(timedelay, dummy_func, self.char1.dbref) + _TASK_HANDLER.clock.advance(timedelay) # make time pass + self.assertEqual(self.char1.ndb.dummy_var, 'dummy_func ran') + self.char1.ndb.dummy_var = False + # test a non persisten deferral, with a short timedelay + utils.delay(.1, dummy_func, self.char1.dbref) + _TASK_HANDLER.clock.advance(.1) # make time pass self.assertEqual(self.char1.ndb.dummy_var, 'dummy_func ran') self.char1.ndb.dummy_var = False # test canceling a deferral. - deferal_inst = utils.delay(1, dummy_func, self.char1.dbref) + task_id = utils.delay(timedelay, dummy_func, self.char1.dbref) + deferal_inst = _TASK_HANDLER.get_deferred(task_id) deferal_inst.cancel() - _TASK_HANDLER.clock.advance(1) # make time pass + _TASK_HANDLER.clock.advance(timedelay) # make time pass self.assertEqual(self.char1.ndb.dummy_var, False) self.char1.ndb.dummy_var = False From f3b546bcf63648aa925e30c7ea64eaf2105523ac Mon Sep 17 00:00:00 2001 From: davewiththenicehat <54369722+davewiththenicehat@users.noreply.github.com> Date: Sun, 18 Apr 2021 08:51:18 -0400 Subject: [PATCH 08/18] TaskHandler.remove() made functional TaskHandler.remove method now functions. Previous it would have removed the task from the TaskHandler.tasks dictionary, but never canceled the task. Making the "remove a persistent task without executing it" incorrect. Previous there was no method to get a persistent tasks's deferral instance, which was likely why TaskHandler.remove was not used within the module. Added unit tests to test TaskHandler.remove --- evennia/scripts/taskhandler.py | 27 ++++++++++++++++++++------- evennia/utils/tests/test_utils.py | 14 ++++++++++++++ 2 files changed, 34 insertions(+), 7 deletions(-) diff --git a/evennia/scripts/taskhandler.py b/evennia/scripts/taskhandler.py index b865a74185..2eb8623670 100644 --- a/evennia/scripts/taskhandler.py +++ b/evennia/scripts/taskhandler.py @@ -212,22 +212,35 @@ class TaskHandler(object): return task_id def remove(self, task_id): - """Remove a persistent task without executing it. + """ + Remove a task without executing it. + Deletes the instance of the task's deferral. Args: task_id (int): an existing task ID. - Note: - A non-persistent task doesn't have a task_id, it is not stored - in the TaskHandler. + Returns: + True (bool), if the removal completed successfully. + None, if there was a raise error """ + d = None + # delete the task from the tasks dictionary if task_id in self.tasks: - del self.tasks[task_id] + # if the task has not been run, cancel it + d = self.get_deferred(task_id) + if d: # it is remotely possible for a task to not have a deferral + if not d.called: + d.cancel() + del self.tasks[task_id] # delete the task from the tasks dictionary + # remove the task from the persistent dictionary and ServerConfig if task_id in self.to_save: del self.to_save[task_id] - - self.save() + self.save() # remove from ServerConfig.objects + # delete the instance of the deferral + if d: + del d + return True def do_task(self, task_id): """Execute the task (call its callback). diff --git a/evennia/utils/tests/test_utils.py b/evennia/utils/tests/test_utils.py index ae4d49e90f..6d9ae97d04 100644 --- a/evennia/utils/tests/test_utils.py +++ b/evennia/utils/tests/test_utils.py @@ -351,3 +351,17 @@ class TestDelay(EvenniaTest): _TASK_HANDLER.clock.advance(timedelay) # make time pass self.assertEqual(self.char1.ndb.dummy_var, False) self.char1.ndb.dummy_var = False + # test removing an active task + task_id = utils.delay(timedelay, dummy_func, self.char1.dbref) + success = _TASK_HANDLER.remove(task_id) + _TASK_HANDLER.clock.advance(timedelay) # make time pass + self.assertEqual(self.char1.ndb.dummy_var, False) + self.char1.ndb.dummy_var = False + # test removing a canceled active task + task_id = utils.delay(timedelay, dummy_func, self.char1.dbref) + deferal_inst = _TASK_HANDLER.get_deferred(task_id) + deferal_inst.cancel() + success = _TASK_HANDLER.remove(task_id) + _TASK_HANDLER.clock.advance(timedelay) # make time pass + self.assertEqual(self.char1.ndb.dummy_var, False) + self.char1.ndb.dummy_var = False From af442378383a0974066db8ba69638c0c6e127f57 Mon Sep 17 00:00:00 2001 From: davewiththenicehat <54369722+davewiththenicehat@users.noreply.github.com> Date: Sun, 18 Apr 2021 09:50:54 -0400 Subject: [PATCH 09/18] TaskHandler cancel and exists method's created Created task handler methods to cancel a task and check if a task exists. Modified unit tests to use these and test methods. unit test test_delay passes. --- evennia/scripts/taskhandler.py | 60 +++++++++++++++++++++++++++---- evennia/utils/tests/test_utils.py | 10 ++++-- 2 files changed, 62 insertions(+), 8 deletions(-) diff --git a/evennia/scripts/taskhandler.py b/evennia/scripts/taskhandler.py index 2eb8623670..d00b4cd91e 100644 --- a/evennia/scripts/taskhandler.py +++ b/evennia/scripts/taskhandler.py @@ -211,6 +211,56 @@ class TaskHandler(object): return False return task_id + def exists(self, task_id): + """ + Test if a task exists. + + Args: + task_id (int): an existing task ID. + + Returns: + True (bool): if the task exists. + False (bool): if the task does not exist. + + Note: + Most task handler methods check for existence for you. + """ + if task_id in self.tasks: + return True + else: + return False + + + def cancel(self, task_id): + """ + Stop a task from automatically executing. + This will not remove the task. + + Args: + task_id (int): an existing task ID. + + Returns: + True (bool): if the removal completed successfully. + False (bool): if the task: + does not exist, + has already run, + does not have a deferral instance created for the task. + None, if there was a raised exception + """ + if task_id in self.tasks: + # if the task has not been run, cancel it + d = self.get_deferred(task_id) + if d: # it is remotely possible for a task to not have a deferral + if d.called: + return False + else: # the callback has not been called yet. + d.cancel() + return True + else: # this task has no deferral + return False + else: + return False + def remove(self, task_id): """ Remove a task without executing it. @@ -220,18 +270,16 @@ class TaskHandler(object): task_id (int): an existing task ID. Returns: - True (bool), if the removal completed successfully. - None, if there was a raise error + True (bool): if the removal completed successfully or if the a + task with the id does not exist. + None, if there was a raised exception """ d = None # delete the task from the tasks dictionary if task_id in self.tasks: # if the task has not been run, cancel it - d = self.get_deferred(task_id) - if d: # it is remotely possible for a task to not have a deferral - if not d.called: - d.cancel() + self.cancel(task_id) del self.tasks[task_id] # delete the task from the tasks dictionary # remove the task from the persistent dictionary and ServerConfig if task_id in self.to_save: diff --git a/evennia/utils/tests/test_utils.py b/evennia/utils/tests/test_utils.py index 6d9ae97d04..60e85e34a7 100644 --- a/evennia/utils/tests/test_utils.py +++ b/evennia/utils/tests/test_utils.py @@ -345,9 +345,11 @@ class TestDelay(EvenniaTest): self.assertEqual(self.char1.ndb.dummy_var, 'dummy_func ran') self.char1.ndb.dummy_var = False # test canceling a deferral. + # after this the task_id 1 remains used by this canceled but unused task task_id = utils.delay(timedelay, dummy_func, self.char1.dbref) - deferal_inst = _TASK_HANDLER.get_deferred(task_id) - deferal_inst.cancel() + success = _TASK_HANDLER.cancel(task_id) + self.assertTrue(success) + self.assertTrue(_TASK_HANDLER.exists(task_id)) _TASK_HANDLER.clock.advance(timedelay) # make time pass self.assertEqual(self.char1.ndb.dummy_var, False) self.char1.ndb.dummy_var = False @@ -356,6 +358,7 @@ class TestDelay(EvenniaTest): success = _TASK_HANDLER.remove(task_id) _TASK_HANDLER.clock.advance(timedelay) # make time pass self.assertEqual(self.char1.ndb.dummy_var, False) + self.assertFalse(_TASK_HANDLER.exists(task_id)) self.char1.ndb.dummy_var = False # test removing a canceled active task task_id = utils.delay(timedelay, dummy_func, self.char1.dbref) @@ -364,4 +367,7 @@ class TestDelay(EvenniaTest): success = _TASK_HANDLER.remove(task_id) _TASK_HANDLER.clock.advance(timedelay) # make time pass self.assertEqual(self.char1.ndb.dummy_var, False) + self.assertFalse(_TASK_HANDLER.exists(task_id)) self.char1.ndb.dummy_var = False + # restart condictions + # cancel a diferall directly, without calling task handler's cancel From bbc60b0340b70b48eb893e49a60faa466625627e Mon Sep 17 00:00:00 2001 From: davewiththenicehat <54369722+davewiththenicehat@users.noreply.github.com> Date: Sun, 18 Apr 2021 09:59:45 -0400 Subject: [PATCH 10/18] TaskHandler.active method created TaskHandler.active method created to check if a task is currently active. test_delay unit test passes. --- evennia/scripts/taskhandler.py | 26 ++++++++++++++++++++++++++ evennia/utils/tests/test_utils.py | 9 +++++++++ 2 files changed, 35 insertions(+) diff --git a/evennia/scripts/taskhandler.py b/evennia/scripts/taskhandler.py index d00b4cd91e..6c648b3360 100644 --- a/evennia/scripts/taskhandler.py +++ b/evennia/scripts/taskhandler.py @@ -230,6 +230,32 @@ class TaskHandler(object): else: return False + def active(self, task_id): + """ + Check if a task is active (has not been called yet). + + Args: + task_id (int): an existing task ID. + + Returns: + True (bool): If a task is active (has not been called yet). + False (bool): if the task + is not active (has already been called), + does not exist + """ + if task_id in self.tasks: + # if the task has not been run, cancel it + d = self.get_deferred(task_id) + if d: # it is remotely possible for a task to not have a deferral + if d.called: + return False + else: # the callback has not been called yet. + return True + else: # this task has no deferral, and could not have been called + return True + else: + return False + def cancel(self, task_id): """ diff --git a/evennia/utils/tests/test_utils.py b/evennia/utils/tests/test_utils.py index 60e85e34a7..c5bf20d032 100644 --- a/evennia/utils/tests/test_utils.py +++ b/evennia/utils/tests/test_utils.py @@ -331,23 +331,28 @@ class TestDelay(EvenniaTest): self.char1.ndb.dummy_var = False # test a persistent deferral, that completes after delay time task_id = utils.delay(timedelay, dummy_func, self.char1.dbref, persistent=True) + self.assertTrue(_TASK_HANDLER.active(task_id)) _TASK_HANDLER.clock.advance(timedelay) # make time pass self.assertEqual(self.char1.ndb.dummy_var, 'dummy_func ran') self.char1.ndb.dummy_var = False # test a non persisten deferral, that completes after delay time. utils.delay(timedelay, dummy_func, self.char1.dbref) + self.assertTrue(_TASK_HANDLER.active(task_id)) _TASK_HANDLER.clock.advance(timedelay) # make time pass self.assertEqual(self.char1.ndb.dummy_var, 'dummy_func ran') self.char1.ndb.dummy_var = False # test a non persisten deferral, with a short timedelay utils.delay(.1, dummy_func, self.char1.dbref) + self.assertTrue(_TASK_HANDLER.active(task_id)) _TASK_HANDLER.clock.advance(.1) # make time pass self.assertEqual(self.char1.ndb.dummy_var, 'dummy_func ran') self.char1.ndb.dummy_var = False # test canceling a deferral. # after this the task_id 1 remains used by this canceled but unused task task_id = utils.delay(timedelay, dummy_func, self.char1.dbref) + self.assertTrue(_TASK_HANDLER.active(task_id)) success = _TASK_HANDLER.cancel(task_id) + self.assertFalse(_TASK_HANDLER.active(task_id)) self.assertTrue(success) self.assertTrue(_TASK_HANDLER.exists(task_id)) _TASK_HANDLER.clock.advance(timedelay) # make time pass @@ -355,15 +360,19 @@ class TestDelay(EvenniaTest): self.char1.ndb.dummy_var = False # test removing an active task task_id = utils.delay(timedelay, dummy_func, self.char1.dbref) + self.assertTrue(_TASK_HANDLER.active(task_id)) success = _TASK_HANDLER.remove(task_id) + self.assertFalse(_TASK_HANDLER.active(task_id)) _TASK_HANDLER.clock.advance(timedelay) # make time pass self.assertEqual(self.char1.ndb.dummy_var, False) self.assertFalse(_TASK_HANDLER.exists(task_id)) self.char1.ndb.dummy_var = False # test removing a canceled active task task_id = utils.delay(timedelay, dummy_func, self.char1.dbref) + self.assertTrue(_TASK_HANDLER.active(task_id)) deferal_inst = _TASK_HANDLER.get_deferred(task_id) deferal_inst.cancel() + self.assertFalse(_TASK_HANDLER.active(task_id)) success = _TASK_HANDLER.remove(task_id) _TASK_HANDLER.clock.advance(timedelay) # make time pass self.assertEqual(self.char1.ndb.dummy_var, False) From 16f6edb18db5416598985ea340dde0ae26133f57 Mon Sep 17 00:00:00 2001 From: davewiththenicehat <54369722+davewiththenicehat@users.noreply.github.com> Date: Sun, 18 Apr 2021 11:01:45 -0400 Subject: [PATCH 11/18] TaskHandler.do_task is now state aware & can return callback's return TaskHandler.do_task is now state aware and can be called manually. It can now return the callbacks returns. added unit tests to verify early callback is functional. Both persistent and non-persistent tasks. All evennia unit tests pass. --- evennia/scripts/taskhandler.py | 40 ++++++++++++++++++++++--------- evennia/utils/tests/test_utils.py | 21 +++++++++++++++- 2 files changed, 49 insertions(+), 12 deletions(-) diff --git a/evennia/scripts/taskhandler.py b/evennia/scripts/taskhandler.py index 6c648b3360..eceb61a1cf 100644 --- a/evennia/scripts/taskhandler.py +++ b/evennia/scripts/taskhandler.py @@ -256,7 +256,6 @@ class TaskHandler(object): else: return False - def cancel(self, task_id): """ Stop a task from automatically executing. @@ -298,7 +297,7 @@ class TaskHandler(object): Returns: True (bool): if the removal completed successfully or if the a task with the id does not exist. - None, if there was a raised exception + None: if there was a raised exception """ d = None @@ -317,22 +316,41 @@ class TaskHandler(object): return True def do_task(self, task_id): - """Execute the task (call its callback). + """ + Execute the task (call its callback). + If calling before timedelay cancel the deferral affliated to this task. + Remove the task from the dictionary of current tasks on a successful + callback. Args: task_id (int): a valid task ID. + Returns: + False (bool): if the: + task no longer exists, + has no affliated instance of deferral + The return of the callback passed on task creation. + This makes it possible for the callback to also return False + None: if there was a raised exception + Note: - This will also remove it from the list of current tasks. + On a successful call the task will be removed from the dictionary + of current tasks. """ - date, callback, args, kwargs, persistent, d = self.tasks.pop(task_id) - - if task_id in self.to_save: - del self.to_save[task_id] - - self.save() - callback(*args, **kwargs) + callback_return = False + if task_id in self.tasks: + date, callback, args, kwargs, persistent, d = self.tasks.get(task_id) + else: # the task does not exist + return False + if d: # it is remotely possible for a task to not have a deferral + if not d.called: # the task has not been called yet + d.cancel() # cancel the automated callback + else: # this task has no deferral, and should not be called + return False + callback_return = callback(*args, **kwargs) + self.remove(task_id) + return callback_return def get_deferred(self, task_id): """ diff --git a/evennia/utils/tests/test_utils.py b/evennia/utils/tests/test_utils.py index c5bf20d032..8de5ef1900 100644 --- a/evennia/utils/tests/test_utils.py +++ b/evennia/utils/tests/test_utils.py @@ -314,6 +314,7 @@ def dummy_func(obj): obj = obj[0] # make changes to object obj.ndb.dummy_var = 'dummy_func ran' + return True class TestDelay(EvenniaTest): @@ -335,12 +336,30 @@ class TestDelay(EvenniaTest): _TASK_HANDLER.clock.advance(timedelay) # make time pass self.assertEqual(self.char1.ndb.dummy_var, 'dummy_func ran') self.char1.ndb.dummy_var = False + # test a persistent deferral, that completes on a manual call + task_id = utils.delay(timedelay, dummy_func, self.char1.dbref, persistent=True) + self.assertTrue(_TASK_HANDLER.active(task_id)) + result = _TASK_HANDLER.do_task(task_id) + self.assertTrue(result) + self.assertFalse(_TASK_HANDLER.exists(task_id)) + _TASK_HANDLER.clock.advance(timedelay) # make time pass, important keep + self.assertEqual(self.char1.ndb.dummy_var, 'dummy_func ran') + self.char1.ndb.dummy_var = False # test a non persisten deferral, that completes after delay time. utils.delay(timedelay, dummy_func, self.char1.dbref) self.assertTrue(_TASK_HANDLER.active(task_id)) _TASK_HANDLER.clock.advance(timedelay) # make time pass self.assertEqual(self.char1.ndb.dummy_var, 'dummy_func ran') self.char1.ndb.dummy_var = False + # test a non-persistent deferral, that completes on a manual call + task_id = utils.delay(timedelay, dummy_func, self.char1.dbref) + self.assertTrue(_TASK_HANDLER.active(task_id)) + result = _TASK_HANDLER.do_task(task_id) + self.assertTrue(result) + self.assertFalse(_TASK_HANDLER.exists(task_id)) + _TASK_HANDLER.clock.advance(timedelay) # make time pass, important keep + self.assertEqual(self.char1.ndb.dummy_var, 'dummy_func ran') + self.char1.ndb.dummy_var = False # test a non persisten deferral, with a short timedelay utils.delay(.1, dummy_func, self.char1.dbref) self.assertTrue(_TASK_HANDLER.active(task_id)) @@ -367,7 +386,7 @@ class TestDelay(EvenniaTest): self.assertEqual(self.char1.ndb.dummy_var, False) self.assertFalse(_TASK_HANDLER.exists(task_id)) self.char1.ndb.dummy_var = False - # test removing a canceled active task + # test removing a canceled task task_id = utils.delay(timedelay, dummy_func, self.char1.dbref) self.assertTrue(_TASK_HANDLER.active(task_id)) deferal_inst = _TASK_HANDLER.get_deferred(task_id) From f57fb645c85f8c275d32104155940d114a451385 Mon Sep 17 00:00:00 2001 From: davewiththenicehat <54369722+davewiththenicehat@users.noreply.github.com> Date: Sun, 18 Apr 2021 18:25:39 -0400 Subject: [PATCH 12/18] taskhandler.Task created Created an object to represent a task. This allows for the return of TASK_HANDLER.add or utils.delay to be an object that has callable methods. It has been created to mock the most common methods and attributes of a twisted deferred object. Changed test_utils.test_delay for new usage. Returned previously changed modules slow_exit, tutorial_world.objects and portal.telnet to their previous states. As the return of utils.delay can be used as if it were a deferred. All evennia unit tests pass --- evennia/contrib/slow_exit.py | 5 +- evennia/contrib/tests.py | 6 + evennia/contrib/tutorial_world/objects.py | 7 +- evennia/scripts/taskhandler.py | 149 +++++++++++++++++++++- evennia/server/portal/telnet.py | 5 +- evennia/utils/tests/test_utils.py | 76 ++++++----- 6 files changed, 206 insertions(+), 42 deletions(-) diff --git a/evennia/contrib/slow_exit.py b/evennia/contrib/slow_exit.py index 3b060f7471..7f7f2cc249 100644 --- a/evennia/contrib/slow_exit.py +++ b/evennia/contrib/slow_exit.py @@ -36,7 +36,6 @@ TickerHandler might be better. """ from evennia import DefaultExit, utils, Command -from evennia.scripts.taskhandler import TASK_HANDLER as _TASK_HANDLER MOVE_DELAY = {"stroll": 6, "walk": 4, "run": 2, "sprint": 1} @@ -71,11 +70,11 @@ class SlowExit(DefaultExit): traversing_object.msg("You start moving %s at a %s." % (self.key, move_speed)) # create a delayed movement - task_id = utils.delay(move_delay, move_callback) + t = utils.delay(move_delay, move_callback) # we store the deferred on the character, this will allow us # to abort the movement. We must use an ndb here since # deferreds cannot be pickled. - traversing_object.ndb.currently_moving = _TASK_HANDLER.get_deferred(task_id) + traversing_object.ndb.currently_moving = t # diff --git a/evennia/contrib/tests.py b/evennia/contrib/tests.py index 7b69d07cc9..a4e50a29f7 100644 --- a/evennia/contrib/tests.py +++ b/evennia/contrib/tests.py @@ -1152,7 +1152,13 @@ from evennia.contrib import slow_exit slow_exit.MOVE_DELAY = {"stroll": 0, "walk": 0, "run": 0, "sprint": 0} +def _cancellable_mockdelay(time, callback, *args, **kwargs): + callback(*args, **kwargs) + return Mock() + + class TestSlowExit(CommandTest): + @patch("evennia.utils.delay", _cancellable_mockdelay) def test_exit(self): exi = create_object( slow_exit.SlowExit, key="slowexit", location=self.room1, destination=self.room2 diff --git a/evennia/contrib/tutorial_world/objects.py b/evennia/contrib/tutorial_world/objects.py index 759d1f1dc0..ca4147a715 100644 --- a/evennia/contrib/tutorial_world/objects.py +++ b/evennia/contrib/tutorial_world/objects.py @@ -24,7 +24,6 @@ import random from evennia import DefaultObject, DefaultExit, Command, CmdSet from evennia.utils import search, delay, dedent from evennia.prototypes.spawner import spawn -from evennia.scripts.taskhandler import TASK_HANDLER as _TASK_HANDLER # ------------------------------------------------------------- # @@ -390,8 +389,7 @@ class LightSource(TutorialObject): # start the burn timer. When it runs out, self._burnout # will be called. We store the deferred so it can be # killed in unittesting. - task_id = delay(60 * 3, self._burnout) - self.deferred = _TASK_HANDLER.get_deferred(task_id) + self.deferred = delay(60 * 3, self._burnout) return True @@ -689,8 +687,7 @@ class CrumblingWall(TutorialObject, DefaultExit): self.db.exit_open = True # start a 45 second timer before closing again. We store the deferred so it can be # killed in unittesting. - task_id = delay(45, self.reset) - self.deferred = _TASK_HANDLER.get_deferred(task_id) + self.deferred = delay(45, self.reset) return True def _translate_position(self, root, ipos): diff --git a/evennia/scripts/taskhandler.py b/evennia/scripts/taskhandler.py index eceb61a1cf..9ccb0ea307 100644 --- a/evennia/scripts/taskhandler.py +++ b/evennia/scripts/taskhandler.py @@ -209,7 +209,7 @@ class TaskHandler(object): self.tasks[task_id] = task else: # the task already completed return False - return task_id + return Task(task_id) def exists(self, task_id): """ @@ -388,3 +388,150 @@ class TaskHandler(object): # Create the soft singleton TASK_HANDLER = TaskHandler() + + +class Task: + """ + A light + """ + def __init__(self, task_id): + self.task_id = task_id + + def get_deferred(self): + """ + Return the instance of the deferred the task id is using. + + Returns: + An instance of a deferral or False if there is no task with the id. + None is returned if there is no deferral affiliated with this id. + """ + return TASK_HANDLER.get_deferred(self.task_id) + + def pause(self): + """ + Pause the callback of a task. + To resume use Task.unpause + """ + d = TASK_HANDLER.get_deferred(self.task_id) + if d: + d.pause() + + def unpause(self): + """ + Process all callbacks made since pause() was called. + """ + d = TASK_HANDLER.get_deferred(self.task_id) + if d: + d.unpause() + + @property + def paused(self): + """ + A task attribute to check if the deferral of a task has been paused. + + This exists to mock usage of a twisted deferred object. + + This will return None if the deferred object for the task does not + exist or if the task no longer exists. + """ + d = TASK_HANDLER.get_deferred(self.task_id) + if d: + return d.paused + else: + return None + + def do_task(self): + """ + Execute the task (call its callback). + If calling before timedelay cancel the deferral affliated to this task. + Remove the task from the dictionary of current tasks on a successful + callback. + + Returns: + False (bool): if the: + task no longer exists, + has no affliated instance of deferral + The return of the callback passed on task creation. + This makes it possible for the callback to also return False + None: if there was a raised exception + + Note: + On a successful call the task will be removed from the dictionary + of current tasks. + + """ + return TASK_HANDLER.do_task(self.task_id) + + def remove(self): + """ + Remove a task without executing it. + Deletes the instance of the task's deferral. + + Returns: + True (bool): if the removal completed successfully or if the a + task with the id does not exist. + None: if there was a raised exception + + """ + return TASK_HANDLER.remove(self.task_id) + + def cancel(self): + """ + Stop a task from automatically executing. + This will not remove the task. + + Returns: + True (bool): if the removal completed successfully. + False (bool): if the task: + does not exist, + has already run, + does not have a deferral instance created for the task. + None, if there was a raised exception + """ + return TASK_HANDLER.cancel(self.task_id) + + def active(self): + """ + Check if a task is active (has not been called yet). + + Returns: + True (bool): If a task is active (has not been called yet). + False (bool): if the task + is not active (has already been called), + does not exist + """ + return TASK_HANDLER.active(self.task_id) + + @property + def called(self): + """ + A task attribute to check if the deferral of a task has been called. + + This exists to mock usage of a twisted deferred object. + It will not set to false if Task.call has been called. + + """ + return not TASK_HANDLER.active(self.task_id) + + def exists(self): + """ + Test if a task exists. + + Returns: + True (bool): if the task exists. + False (bool): if the task does not exist. + + Note: + Most task handler methods check for existence for you. + """ + return TASK_HANDLER.exists(self.task_id) + + def get_id(self): + """ + Returns the global id for this task. For use with + `evennia.scripts.taskhandler.TASK_HANDLER`. + + Returns: + task_id (int): global task id for this task. + """ + return self.task_id diff --git a/evennia/server/portal/telnet.py b/evennia/server/portal/telnet.py index 42facf7e65..821e96aa24 100644 --- a/evennia/server/portal/telnet.py +++ b/evennia/server/portal/telnet.py @@ -31,7 +31,6 @@ from evennia.server.portal.mccp import Mccp, mccp_compress, MCCP from evennia.server.portal.mxp import Mxp, mxp_parse from evennia.utils import ansi from evennia.utils.utils import to_bytes -from evennia.scripts.taskhandler import TASK_HANDLER as _TASK_HANDLER _RE_N = re.compile(r"\|n$") _RE_LEND = re.compile(br"\n$|\r$|\r\n$|\r\x00$|", re.MULTILINE) @@ -128,8 +127,8 @@ class TelnetProtocol(Telnet, StatefulTelnetProtocol, Session): from evennia.utils.utils import delay - task_id = delay(2, callback=self.handshake_done, timeout=True) - self._handshake_delay = _TASK_HANDLER.get_deferred(task_id) + # timeout the handshakes in case the client doesn't reply at all + self._handshake_delay = delay(2, callback=self.handshake_done, timeout=True) # TCP/IP keepalive watches for dead links self.transport.setTcpKeepAlive(1) diff --git a/evennia/utils/tests/test_utils.py b/evennia/utils/tests/test_utils.py index 8de5ef1900..9bf252d0a8 100644 --- a/evennia/utils/tests/test_utils.py +++ b/evennia/utils/tests/test_utils.py @@ -331,71 +331,87 @@ class TestDelay(EvenniaTest): _TASK_HANDLER.clock = task.Clock() self.char1.ndb.dummy_var = False # test a persistent deferral, that completes after delay time - task_id = utils.delay(timedelay, dummy_func, self.char1.dbref, persistent=True) - self.assertTrue(_TASK_HANDLER.active(task_id)) + t = utils.delay(timedelay, dummy_func, self.char1.dbref, persistent=True) + self.assertTrue(_TASK_HANDLER.active(t.get_id())) # test Task.get_id + self.assertTrue(t.active()) _TASK_HANDLER.clock.advance(timedelay) # make time pass + self.assertTrue(t.called) # test Task.called property self.assertEqual(self.char1.ndb.dummy_var, 'dummy_func ran') self.char1.ndb.dummy_var = False # test a persistent deferral, that completes on a manual call - task_id = utils.delay(timedelay, dummy_func, self.char1.dbref, persistent=True) - self.assertTrue(_TASK_HANDLER.active(task_id)) - result = _TASK_HANDLER.do_task(task_id) + t = utils.delay(timedelay, dummy_func, self.char1.dbref, persistent=True) + self.assertTrue(t.active()) + result = t.do_task() self.assertTrue(result) - self.assertFalse(_TASK_HANDLER.exists(task_id)) + self.assertFalse(t.exists()) _TASK_HANDLER.clock.advance(timedelay) # make time pass, important keep self.assertEqual(self.char1.ndb.dummy_var, 'dummy_func ran') self.char1.ndb.dummy_var = False # test a non persisten deferral, that completes after delay time. - utils.delay(timedelay, dummy_func, self.char1.dbref) - self.assertTrue(_TASK_HANDLER.active(task_id)) + t = utils.delay(timedelay, dummy_func, self.char1.dbref) + self.assertTrue(t.active()) _TASK_HANDLER.clock.advance(timedelay) # make time pass self.assertEqual(self.char1.ndb.dummy_var, 'dummy_func ran') self.char1.ndb.dummy_var = False # test a non-persistent deferral, that completes on a manual call - task_id = utils.delay(timedelay, dummy_func, self.char1.dbref) - self.assertTrue(_TASK_HANDLER.active(task_id)) - result = _TASK_HANDLER.do_task(task_id) + t = utils.delay(timedelay, dummy_func, self.char1.dbref) + self.assertTrue(t.active()) + result = t.do_task() self.assertTrue(result) - self.assertFalse(_TASK_HANDLER.exists(task_id)) + self.assertFalse(t.exists()) _TASK_HANDLER.clock.advance(timedelay) # make time pass, important keep self.assertEqual(self.char1.ndb.dummy_var, 'dummy_func ran') self.char1.ndb.dummy_var = False # test a non persisten deferral, with a short timedelay - utils.delay(.1, dummy_func, self.char1.dbref) - self.assertTrue(_TASK_HANDLER.active(task_id)) + t = utils.delay(.1, dummy_func, self.char1.dbref) + self.assertTrue(t.active()) _TASK_HANDLER.clock.advance(.1) # make time pass self.assertEqual(self.char1.ndb.dummy_var, 'dummy_func ran') self.char1.ndb.dummy_var = False # test canceling a deferral. # after this the task_id 1 remains used by this canceled but unused task - task_id = utils.delay(timedelay, dummy_func, self.char1.dbref) - self.assertTrue(_TASK_HANDLER.active(task_id)) - success = _TASK_HANDLER.cancel(task_id) - self.assertFalse(_TASK_HANDLER.active(task_id)) + t = utils.delay(timedelay, dummy_func, self.char1.dbref) + self.assertTrue(t.active()) + success = t.cancel() + self.assertFalse(t.active()) self.assertTrue(success) - self.assertTrue(_TASK_HANDLER.exists(task_id)) + self.assertTrue(t.exists()) _TASK_HANDLER.clock.advance(timedelay) # make time pass self.assertEqual(self.char1.ndb.dummy_var, False) self.char1.ndb.dummy_var = False # test removing an active task - task_id = utils.delay(timedelay, dummy_func, self.char1.dbref) - self.assertTrue(_TASK_HANDLER.active(task_id)) - success = _TASK_HANDLER.remove(task_id) - self.assertFalse(_TASK_HANDLER.active(task_id)) + t = utils.delay(timedelay, dummy_func, self.char1.dbref) + self.assertTrue(t.active()) + success = t.remove() + self.assertFalse(t.active()) _TASK_HANDLER.clock.advance(timedelay) # make time pass self.assertEqual(self.char1.ndb.dummy_var, False) - self.assertFalse(_TASK_HANDLER.exists(task_id)) + self.assertFalse(t.exists()) self.char1.ndb.dummy_var = False # test removing a canceled task - task_id = utils.delay(timedelay, dummy_func, self.char1.dbref) - self.assertTrue(_TASK_HANDLER.active(task_id)) - deferal_inst = _TASK_HANDLER.get_deferred(task_id) + t = utils.delay(timedelay, dummy_func, self.char1.dbref) + self.assertTrue(t.active()) + deferal_inst = t.get_deferred() deferal_inst.cancel() - self.assertFalse(_TASK_HANDLER.active(task_id)) - success = _TASK_HANDLER.remove(task_id) + self.assertFalse(t.active()) + success = t.remove() _TASK_HANDLER.clock.advance(timedelay) # make time pass self.assertEqual(self.char1.ndb.dummy_var, False) - self.assertFalse(_TASK_HANDLER.exists(task_id)) + self.assertFalse(t.exists()) + self.char1.ndb.dummy_var = False + # test pause, paused and unpause + t = utils.delay(timedelay, dummy_func, self.char1.dbref) + self.assertTrue(t.active()) + t.pause() + self.assertTrue(t.paused) + t.unpause() + self.assertFalse(t.paused) + self.assertEqual(self.char1.ndb.dummy_var, False) + t.pause() + _TASK_HANDLER.clock.advance(timedelay) # make time pass + self.assertEqual(self.char1.ndb.dummy_var, False) + t.unpause() + self.assertEqual(self.char1.ndb.dummy_var, 'dummy_func ran') self.char1.ndb.dummy_var = False # restart condictions # cancel a diferall directly, without calling task handler's cancel From fea077d555ea93f43af3cf2bd0e019baf4bc8b7a Mon Sep 17 00:00:00 2001 From: davewiththenicehat <54369722+davewiththenicehat@users.noreply.github.com> Date: Mon, 19 Apr 2021 09:37:19 -0400 Subject: [PATCH 13/18] task handler automatic stale task cleanup Task handler will automatically remove uncalled but canceled from task handler. By default this will not occur until a canceled task has been uncalled for 60 second after the time it should have been called. To adjust this time use TASK_HANDLER.stale_timeout. If stale_timeout is 0 stale tasks will not be automatically removed. This is not done on a timer. I is done as new tasks are added or the load method is called. Added unit tests to test automatic removal. Including when it should not automatically removed. Both when it is too soon, or when the stale_timeout attribute is set to 0. --- evennia/scripts/taskhandler.py | 82 ++++++++++++++++++++++++------- evennia/utils/tests/test_utils.py | 53 +++++++++++++++++++- 2 files changed, 116 insertions(+), 19 deletions(-) diff --git a/evennia/scripts/taskhandler.py b/evennia/scripts/taskhandler.py index 9ccb0ea307..214d1e3f8a 100644 --- a/evennia/scripts/taskhandler.py +++ b/evennia/scripts/taskhandler.py @@ -32,18 +32,13 @@ class TaskHandler(object): When `utils.delay` is called, the task handler is used to create the task. - If `utils.delay` is called with `persistent=True`. The - task handler creates the task. It saves the delay time, callback, - arguments and kwa to the database. Each of these variables are - serialized to do this. - It's easier to access these tasks (should it be necessary) using - `evennia.scripts.taskhandler.TASK_HANDLER`, which contains one - instance of this class, and use its `add` and `remove` methods. - - Dev notes: - deferLater creates an instance of IDelayedCall using reactor.callLater. - deferLater uses the cancel method on the IDelayedCall instance to create + Task handler will automatically remove uncalled but canceled from task + handler. By default this will not occur until a canceled task + has been uncalled for 60 second after the time it should have been called. + To adjust this time use TASK_HANDLER.stale_timeout. If stale_timeout is 0 + stale tasks will not be automatically removed. + This is not done on a timer. I is done as new tasks are added or the load method is called. """ @@ -51,6 +46,9 @@ class TaskHandler(object): self.tasks = {} self.to_save = {} self.clock = reactor + # number of seconds before an uncalled canceled task is removed from TaskHandler + self.stale_timeout = 60 + self._now = False # used in unit testing to manually set now time def load(self): """Load from the ServerConfig. @@ -80,11 +78,37 @@ class TaskHandler(object): callback = getattr(obj, method) self.tasks[task_id] = date, callback, args, kwargs, True, None + if self.stale_timeout > 0: # cleanup stale tasks. + self.clean_stale_tasks() if to_save: self.save() + def clean_stale_tasks(self): + """ + remove uncalled but canceled from task handler. + + By default this will not occur until a canceled task + has been uncalled for 60 second after the time it should have been called. + To adjust this time use TASK_HANDLER.stale_timeout. + """ + clean_ids = [] + for task_id, (date, callback, args, kwargs, persistent, _) in self.tasks.items(): + if not self.active(task_id): + stale_date = date + timedelta(seconds=self.stale_timeout) + # if a now time is provided use it (intended for unit testing) + now = self._now if self._now else datetime.now() + # the task was canceled more than stale_timeout seconds ago + if now > stale_date: + clean_ids.append(task_id) + for task_id in clean_ids: + self.remove(task_id) + return True + def save(self): - """Save the tasks in ServerConfig.""" + """ + Save the tasks in ServerConfig. + """ + for task_id, (date, callback, args, kwargs, persistent, _) in self.tasks.items(): if task_id in self.to_save: continue @@ -209,6 +233,8 @@ class TaskHandler(object): self.tasks[task_id] = task else: # the task already completed return False + if self.stale_timeout > 0: + self.clean_stale_tasks() return Task(task_id) def exists(self, task_id): @@ -392,10 +418,28 @@ TASK_HANDLER = TaskHandler() class Task: """ - A light + A object to represent a single TaskHandler task. + + Instance Attributes: + task_id (int): the global id for this task + deferred (deferred): a reference to this task's deferred + Propert Attributes: + paused (bool): check if the deferral of a task has been paused. + called(self): A task attribute to check if the deferral of a task has been called. + + Methods: + pause(): Pause the callback of a task. + unpause(): Process all callbacks made since pause() was called. + do_task(): Execute the task (call its callback). + remove(): Remove a task without executing it. + cancel(): Stop a task from automatically executing. + active(): Check if a task is active (has not been called yet). + exists(): Check if a task exists. + get_id(): Returns the global id for this task. For use with """ def __init__(self, task_id): self.task_id = task_id + self.deferred = TASK_HANDLER.get_deferred(task_id) def get_deferred(self): """ @@ -412,7 +456,7 @@ class Task: Pause the callback of a task. To resume use Task.unpause """ - d = TASK_HANDLER.get_deferred(self.task_id) + d = self.deferred if d: d.pause() @@ -420,7 +464,7 @@ class Task: """ Process all callbacks made since pause() was called. """ - d = TASK_HANDLER.get_deferred(self.task_id) + d = self.deferred if d: d.unpause() @@ -434,7 +478,7 @@ class Task: This will return None if the deferred object for the task does not exist or if the task no longer exists. """ - d = TASK_HANDLER.get_deferred(self.task_id) + d = self.deferred if d: return d.paused else: @@ -511,7 +555,11 @@ class Task: It will not set to false if Task.call has been called. """ - return not TASK_HANDLER.active(self.task_id) + d = self.deferred + if d: + return d.called + else: + return None def exists(self): """ diff --git a/evennia/utils/tests/test_utils.py b/evennia/utils/tests/test_utils.py index 9bf252d0a8..53ec661521 100644 --- a/evennia/utils/tests/test_utils.py +++ b/evennia/utils/tests/test_utils.py @@ -7,6 +7,7 @@ TODO: Not nearly all utilities are covered yet. import os.path import mock +from datetime import datetime, timedelta from django.test import TestCase from datetime import datetime @@ -413,5 +414,53 @@ class TestDelay(EvenniaTest): t.unpause() self.assertEqual(self.char1.ndb.dummy_var, 'dummy_func ran') self.char1.ndb.dummy_var = False - # restart condictions - # cancel a diferall directly, without calling task handler's cancel + # test automated removal of stale tasks. + t = utils.delay(timedelay, dummy_func, self.char1.dbref, persistent=True) + t.cancel() + task_id = t.get_id() + self.assertFalse(t.active()) + _TASK_HANDLER.clock.advance(timedelay) # make time pass + self.assertTrue(task_id in _TASK_HANDLER.to_save) + self.assertTrue(task_id in _TASK_HANDLER.tasks) + # add a task to test automatic removal + _TASK_HANDLER._now = datetime.now() + timedelta(seconds=_TASK_HANDLER.stale_timeout + 6) # task handler time to 6 seconds after stale timeout + t2 = utils.delay(timedelay, dummy_func, self.char1.dbref, persistent=True) + self.assertFalse(task_id in _TASK_HANDLER.to_save) + self.assertFalse(task_id in _TASK_HANDLER.tasks) + self.assertEqual(self.char1.ndb.dummy_var, False) + # test manual cleanup + t2.cancel() + _TASK_HANDLER.clock.advance(timedelay) # advance twisted reactor time past callback time + _TASK_HANDLER._now = datetime.now() + timedelta(seconds=30) # set TaskHandler's time to 30 seconnds from now + task_id = t2.get_id() + # test before stale_timeout time + _TASK_HANDLER.clean_stale_tasks() # cleanup of stale tasks in in the save method + # still in the task handler because stale timeout has not been reached + self.assertTrue(task_id in _TASK_HANDLER.to_save) + self.assertTrue(task_id in _TASK_HANDLER.tasks) + # advance past stale timeout + _TASK_HANDLER._now = datetime.now() + timedelta(seconds=_TASK_HANDLER.stale_timeout + 6) # task handler time to 6 seconds after stale timeout + _TASK_HANDLER.clean_stale_tasks() # cleanup of stale tasks in in the save method + self.assertFalse(task_id in _TASK_HANDLER.to_save) + self.assertFalse(task_id in _TASK_HANDLER.tasks) + self.char1.ndb.dummy_var = False + _TASK_HANDLER._now = False + # if _TASK_HANDLER.stale_timeout is 0 or less, automatic cleanup should not run + _TASK_HANDLER.stale_timeout = 0 + t = utils.delay(timedelay, dummy_func, self.char1.dbref, persistent=True) + t.cancel() + self.assertFalse(t.active()) + _TASK_HANDLER.clock.advance(timedelay) # advance twisted's reactor past callback time + self.assertTrue(t.get_id() in _TASK_HANDLER.to_save) + self.assertTrue(t.get_id() in _TASK_HANDLER.tasks) + # add a task to test automatic removal + _TASK_HANDLER._now = datetime.now() + timedelta(seconds=_TASK_HANDLER.stale_timeout + 6) # task handler time to 6 seconds after stale timeout + t2 = utils.delay(timedelay, dummy_func, self.char1.dbref, persistent=True) + self.assertTrue(t.get_id() in _TASK_HANDLER.to_save) + self.assertTrue(t.get_id() in _TASK_HANDLER.tasks) + self.assertEqual(self.char1.ndb.dummy_var, False) + t.remove() + t2.remove() + self.char1.ndb.dummy_var = False + _TASK_HANDLER._now = False + # replicate a restart From 0b7cae600acae1180f07dcab57c9011d12cd7fca Mon Sep 17 00:00:00 2001 From: davewiththenicehat <54369722+davewiththenicehat@users.noreply.github.com> Date: Mon, 19 Apr 2021 17:16:37 -0400 Subject: [PATCH 14/18] task handler remove_all method created. task handler server restart unit test created. Added method remove_all to task handler. (intended for unit testing) Created a method to mimic a server restart for the purpose of task handler. test_delay unit test passes. --- evennia/scripts/taskhandler.py | 24 ++++++++++++++++++++ evennia/utils/tests/test_utils.py | 37 +++++++++++++++++++++---------- 2 files changed, 49 insertions(+), 12 deletions(-) diff --git a/evennia/scripts/taskhandler.py b/evennia/scripts/taskhandler.py index 214d1e3f8a..c9233adbd8 100644 --- a/evennia/scripts/taskhandler.py +++ b/evennia/scripts/taskhandler.py @@ -341,6 +341,30 @@ class TaskHandler(object): del d return True + def remove_all(self, save=True, cancel=True): + """ + Remove all tasks. + By default tasks are canceled and removed from the database also. + + Arguments: + save=True (bool): Should changes to persistent tasks be saved to database. + cancel=True (bool): Cancel scheduled tasks before removing it from task handler. + + Returns: + True (bool): if the removal completed successfully. + """ + tasks_ids = tuple(self.tasks.keys()) + for task_id in tasks_ids: + if cancel: + self.cancel(task_id) + del self.tasks[task_id] + tasks_ids = tuple(self.to_save.keys()) + for task_id in tasks_ids: + del self.to_save[task_id] + if save: + self.save() + return True + def do_task(self, task_id): """ Execute the task (call its callback). diff --git a/evennia/utils/tests/test_utils.py b/evennia/utils/tests/test_utils.py index 53ec661521..5c71fe19bf 100644 --- a/evennia/utils/tests/test_utils.py +++ b/evennia/utils/tests/test_utils.py @@ -417,32 +417,30 @@ class TestDelay(EvenniaTest): # test automated removal of stale tasks. t = utils.delay(timedelay, dummy_func, self.char1.dbref, persistent=True) t.cancel() - task_id = t.get_id() self.assertFalse(t.active()) _TASK_HANDLER.clock.advance(timedelay) # make time pass - self.assertTrue(task_id in _TASK_HANDLER.to_save) - self.assertTrue(task_id in _TASK_HANDLER.tasks) + self.assertTrue(t.get_id() in _TASK_HANDLER.to_save) + self.assertTrue(t.get_id() in _TASK_HANDLER.tasks) # add a task to test automatic removal _TASK_HANDLER._now = datetime.now() + timedelta(seconds=_TASK_HANDLER.stale_timeout + 6) # task handler time to 6 seconds after stale timeout t2 = utils.delay(timedelay, dummy_func, self.char1.dbref, persistent=True) - self.assertFalse(task_id in _TASK_HANDLER.to_save) - self.assertFalse(task_id in _TASK_HANDLER.tasks) + self.assertFalse(t.get_id() in _TASK_HANDLER.to_save) + self.assertFalse(t.get_id() in _TASK_HANDLER.tasks) self.assertEqual(self.char1.ndb.dummy_var, False) # test manual cleanup t2.cancel() _TASK_HANDLER.clock.advance(timedelay) # advance twisted reactor time past callback time _TASK_HANDLER._now = datetime.now() + timedelta(seconds=30) # set TaskHandler's time to 30 seconnds from now - task_id = t2.get_id() # test before stale_timeout time _TASK_HANDLER.clean_stale_tasks() # cleanup of stale tasks in in the save method # still in the task handler because stale timeout has not been reached - self.assertTrue(task_id in _TASK_HANDLER.to_save) - self.assertTrue(task_id in _TASK_HANDLER.tasks) + self.assertTrue(t2.get_id() in _TASK_HANDLER.to_save) + self.assertTrue(t2.get_id() in _TASK_HANDLER.tasks) # advance past stale timeout _TASK_HANDLER._now = datetime.now() + timedelta(seconds=_TASK_HANDLER.stale_timeout + 6) # task handler time to 6 seconds after stale timeout _TASK_HANDLER.clean_stale_tasks() # cleanup of stale tasks in in the save method - self.assertFalse(task_id in _TASK_HANDLER.to_save) - self.assertFalse(task_id in _TASK_HANDLER.tasks) + self.assertFalse(t2.get_id() in _TASK_HANDLER.to_save) + self.assertFalse(t2.get_id() in _TASK_HANDLER.tasks) self.char1.ndb.dummy_var = False _TASK_HANDLER._now = False # if _TASK_HANDLER.stale_timeout is 0 or less, automatic cleanup should not run @@ -459,8 +457,23 @@ class TestDelay(EvenniaTest): self.assertTrue(t.get_id() in _TASK_HANDLER.to_save) self.assertTrue(t.get_id() in _TASK_HANDLER.tasks) self.assertEqual(self.char1.ndb.dummy_var, False) - t.remove() - t2.remove() + _TASK_HANDLER.remove_all() self.char1.ndb.dummy_var = False _TASK_HANDLER._now = False # replicate a restart + _TASK_HANDLER.remove_all() + _TASK_HANDLER.save() + self.assertFalse(_TASK_HANDLER.tasks) + self.assertFalse(_TASK_HANDLER.to_save) + # create a persistent task. + t = utils.delay(timedelay, dummy_func, self.char1.dbref, persistent=True) + _TASK_HANDLER.save() + _TASK_HANDLER.remove_all(False) # remove all tasks, do not save this change. + _TASK_HANDLER.clock.advance(timedelay) # advance twisted reactor time past callback time + self.assertEqual(self.char1.ndb.dummy_var, False) # task has not run + _TASK_HANDLER.load() + _TASK_HANDLER.create_delays() + _TASK_HANDLER.clock.advance(timedelay) # Clock must advance to trigger, even if past timedelay + self.assertEqual(self.char1.ndb.dummy_var, 'dummy_func ran') + _TASK_HANDLER.remove_all() + self.char1.ndb.dummy_var = False From 3cc14e2e4ccfb173d644541c349ec3cba298df9c Mon Sep 17 00:00:00 2001 From: davewiththenicehat <54369722+davewiththenicehat@users.noreply.github.com> Date: Mon, 19 Apr 2021 17:36:42 -0400 Subject: [PATCH 15/18] task handler call_task, Task.call methods created task handler call_task, Task.call methods created Added unit tests for these methods. All evennia unit tests pass --- evennia/scripts/taskhandler.py | 36 ++++++++++++++++++++++++++++++- evennia/utils/tests/test_utils.py | 6 ++++++ 2 files changed, 41 insertions(+), 1 deletion(-) diff --git a/evennia/scripts/taskhandler.py b/evennia/scripts/taskhandler.py index c9233adbd8..c0905a5e09 100644 --- a/evennia/scripts/taskhandler.py +++ b/evennia/scripts/taskhandler.py @@ -365,6 +365,26 @@ class TaskHandler(object): self.save() return True + def call_task(self, task_id): + """ + Call the callback of a task. + Leave the task unaffected otherwise. + This does not use the task's deferred instance. + The only requirement is that the task exist in task handler. + + Args: + task_id (int): an existing task ID. + + Returns: + False (bool): if the task does not exist in task handler. + ?: The return of the task's callback. + """ + if task_id in self.tasks: + date, callback, args, kwargs, persistent, d = self.tasks.get(task_id) + else: # the task does not exist + return False + return callback(*args, **kwargs) + def do_task(self, task_id): """ Execute the task (call its callback). @@ -394,7 +414,7 @@ class TaskHandler(object): else: # the task does not exist return False if d: # it is remotely possible for a task to not have a deferral - if not d.called: # the task has not been called yet + if not d.called: # the task's deferred has not been called yet d.cancel() # cancel the automated callback else: # this task has no deferral, and should not be called return False @@ -455,6 +475,7 @@ class Task: pause(): Pause the callback of a task. unpause(): Process all callbacks made since pause() was called. do_task(): Execute the task (call its callback). + call(): Call the callback of this task. remove(): Remove a task without executing it. cancel(): Stop a task from automatically executing. active(): Check if a task is active (has not been called yet). @@ -530,6 +551,19 @@ class Task: """ return TASK_HANDLER.do_task(self.task_id) + def call(self): + """ + Call the callback of this task. + Leave the task unaffected otherwise. + This does not use the task's deferred instance. + The only requirement is that the task exist in task handler. + + Returns: + False (bool): if the task does not exist in task handler. + ?: The return of the task's callback. + """ + return TASK_HANDLER.call_task(self.task_id) + def remove(self): """ Remove a task without executing it. diff --git a/evennia/utils/tests/test_utils.py b/evennia/utils/tests/test_utils.py index 5c71fe19bf..fa67bc768f 100644 --- a/evennia/utils/tests/test_utils.py +++ b/evennia/utils/tests/test_utils.py @@ -333,8 +333,14 @@ class TestDelay(EvenniaTest): self.char1.ndb.dummy_var = False # test a persistent deferral, that completes after delay time t = utils.delay(timedelay, dummy_func, self.char1.dbref, persistent=True) + # call the task early to test Task.call and TaskHandler.call_task + result = t.call() + self.assertTrue(result) + del result + self.assertEqual(self.char1.ndb.dummy_var, 'dummy_func ran') self.assertTrue(_TASK_HANDLER.active(t.get_id())) # test Task.get_id self.assertTrue(t.active()) + self.char1.ndb.dummy_var = False # Set variable to continue completion after delay time test. _TASK_HANDLER.clock.advance(timedelay) # make time pass self.assertTrue(t.called) # test Task.called property self.assertEqual(self.char1.ndb.dummy_var, 'dummy_func ran') From 84193dd9a7a28ecf355e585a8b62e2de1740e040 Mon Sep 17 00:00:00 2001 From: davewiththenicehat <54369722+davewiththenicehat@users.noreply.github.com> Date: Sun, 25 Apr 2021 22:38:35 -0400 Subject: [PATCH 16/18] task handler update updating taskhandler.py before updating unit tests. All evennia unit tests pass. --- evennia/scripts/taskhandler.py | 509 +++++++++++++----------------- evennia/utils/tests/test_utils.py | 8 +- 2 files changed, 224 insertions(+), 293 deletions(-) diff --git a/evennia/scripts/taskhandler.py b/evennia/scripts/taskhandler.py index c0905a5e09..35b5b34889 100644 --- a/evennia/scripts/taskhandler.py +++ b/evennia/scripts/taskhandler.py @@ -15,9 +15,7 @@ TASK_HANDLER = None def handle_error(*args, **kwargs): - """ - Handle errors withing deferred objects. - """ + """Handle errors withing deferred objects.""" for arg in args: # suppress cancel errors if arg.type == DefCancelledError: @@ -25,10 +23,165 @@ def handle_error(*args, **kwargs): raise arg +class TaskHandlerTask: + """A object to represent a single TaskHandler task. + + Instance Attributes: + task_id (int): the global id for this task + deferred (deferred): a reference to this task's deferred + Propert Attributes: + paused (bool): check if the deferral of a task has been paused. + called(self): A task attribute to check if the deferral of a task has been called. + + Methods: + pause(): Pause the callback of a task. + unpause(): Process all callbacks made since pause() was called. + do_task(): Execute the task (call its callback). + call(): Call the callback of this task. + remove(): Remove a task without executing it. + cancel(): Stop a task from automatically executing. + active(): Check if a task is active (has not been called yet). + exists(): Check if a task exists. + get_id(): Returns the global id for this task. For use with + """ + + def __init__(self, task_id): + self.task_id = task_id + self.deferred = TASK_HANDLER.get_deferred(task_id) + + def get_deferred(self): + """Return the instance of the deferred the task id is using. + + Returns: + An instance of a deferral or False if there is no task with the id. + None is returned if there is no deferral affiliated with this id. + """ + return TASK_HANDLER.get_deferred(self.task_id) + + def pause(self): + """Pause the callback of a task. + To resume use TaskHandlerTask.unpause + """ + d = self.deferred + if d: + d.pause() + + def unpause(self): + """Process all callbacks made since pause() was called.""" + d = self.deferred + if d: + d.unpause() + + @property + def paused(self): + """A task attribute to check if the deferral of a task has been paused. + + This exists to mock usage of a twisted deferred object. + + This will return None if the deferred object for the task does not + exist or if the task no longer exists. + """ + d = self.deferred + if d: + return d.paused + else: + return None + + def do_task(self): + """Execute the task (call its callback). + If calling before timedelay cancel the deferral affliated to this task. + Remove the task from the dictionary of current tasks on a successful + callback. + + Returns: + bool or any: Set to `False` if the task does not exist in task + handler. Otherwise it will be the return of the task's callback. + + """ + return TASK_HANDLER.do_task(self.task_id) + + def call(self): + """Call the callback of a task. + Leave the task unaffected otherwise. + This does not use the task's deferred instance. + The only requirement is that the task exist in task handler. + + Returns: + bool or any: Set to `False` if the task does not exist in task + handler. Otherwise it will be the return of the task's callback. + + """ + return TASK_HANDLER.call_task(self.task_id) + + def remove(self): + """Remove a task without executing it. + Deletes the instance of the task's deferral. + + Args: + task_id (int): an existing task ID. + + Returns: + bool: True if the removal completed successfully. + + """ + return TASK_HANDLER.remove(self.task_id) + + def cancel(self): + """Stop a task from automatically executing. + This will not remove the task. + + Returns: + bool: True if the cancel completed successfully. + False if the cancel did not complete successfully. + """ + return TASK_HANDLER.cancel(self.task_id) + + def active(self): + """Check if a task is active (has not been called yet). + + Returns: + bool: True if a task is active (has not been called yet). False if + it is not (has been called) or if the task does not exist. + """ + return TASK_HANDLER.active(self.task_id) + + @property + def called(self): + """ + A task attribute to check if the deferral of a task has been called. + + This exists to mock usage of a twisted deferred object. + It will not set to false if Task.call has been called. + + """ + d = self.deferred + if d: + return d.called + else: + return None + + def exists(self): + """Check if a task exists. + Most task handler methods check for existence for you. + + Returns: + bool: Tru the task exists False if it does not. + """ + return TASK_HANDLER.exists(self.task_id) + + def get_id(self): + """ Returns the global id for this task. For use with + `evennia.scripts.taskhandler.TASK_HANDLER`. + + Returns: + task_id (int): global task id for this task. + """ + return self.task_id + + class TaskHandler(object): - """ - A light singleton wrapper allowing to access permanent tasks. + """A light singleton wrapper allowing to access permanent tasks. When `utils.delay` is called, the task handler is used to create the task. @@ -53,9 +206,8 @@ class TaskHandler(object): def load(self): """Load from the ServerConfig. - Note: - This should be automatically called when Evennia starts. - It populates `self.tasks` according to the ServerConfig. + This should be automatically called when Evennia starts. + It populates `self.tasks` according to the ServerConfig. """ to_save = False @@ -76,7 +228,7 @@ class TaskHandler(object): continue callback = getattr(obj, method) - self.tasks[task_id] = date, callback, args, kwargs, True, None + self.tasks[task_id] = (date, callback, args, kwargs, True, None) if self.stale_timeout > 0: # cleanup stale tasks. self.clean_stale_tasks() @@ -84,8 +236,7 @@ class TaskHandler(object): self.save() def clean_stale_tasks(self): - """ - remove uncalled but canceled from task handler. + """remove uncalled but canceled from task handler. By default this will not occur until a canceled task has been uncalled for 60 second after the time it should have been called. @@ -139,35 +290,30 @@ class TaskHandler(object): ServerConfig.objects.conf("delayed_tasks", self.to_save) def add(self, timedelay, callback, *args, **kwargs): - """Add a new persistent task in the configuration. + """Add a new task. + + If the persistent kwarg is truthy: + The callback, args and values for kwarg will be serialized. Type + and attribute errors during the serialization will be logged, + but will not throw exceptions. + For persisten tasks do not use memory references in the callback + function or arguments. After a restart those memory references are no + longer accurate. Args: timedelay (int or float): time in sedconds before calling the callback. callback (function or instance method): the callback itself any (any): any additional positional arguments to send to the callback - - Keyword Args: - persistent (bool, optional): persist the task (stores it). - Add will return the task's id for use with the global TASK_HANDLER. - any (any): additional keyword arguments to send to the callback + *args: positional arguments to pass to callback. + **kwargs: key word arguments to pass to callback. + persistent (bool, optional): persist the task (stores it). + persisten key and value is removed from kwargs it will + not be passed to callback. Returns: - task_id (int), the task's id intended for use with this class. - False, if the task has completed before addition finishes. + TaskHandlerTask: An object to represent a task. + Reference evennia.scripts.taskhandler.Task for complete details. - Notes: - This method has two return types. - An instance of twisted's Deferred class is standard. - If the persistent kwarg is truthy instead a task ID will be returned. - This task id can be used with task handler's do_task and remove methods. - - If the persistent kwarg is truthy: - The callback, args and values for kwarg will be serialized. Type - and attribute errors during the serialization will be logged, - but will not throw exceptions. - Do not use memory references in the callback function or arguments. - As those memory references will no longer acurately point to - the variable desired. """ # set the completion time # Only used on persistent tasks after a restart @@ -224,7 +370,7 @@ class TaskHandler(object): d = deferLater(self.clock, timedelay, callback, *args, **kwargs) d.addErrback(handle_error) - # some tasks may complete before the deferal can be added + # some tasks may complete before the deferred can be added if task_id in self.tasks: task = self.tasks.get(task_id) task = list(task) @@ -235,21 +381,18 @@ class TaskHandler(object): return False if self.stale_timeout > 0: self.clean_stale_tasks() - return Task(task_id) + return TaskHandlerTask(task_id) def exists(self, task_id): - """ - Test if a task exists. + """Check if a task exists. + Most task handler methods check for existence for you. Args: task_id (int): an existing task ID. Returns: - True (bool): if the task exists. - False (bool): if the task does not exist. + bool: Tru the task exists False if it does not. - Note: - Most task handler methods check for existence for you. """ if task_id in self.tasks: return True @@ -257,51 +400,37 @@ class TaskHandler(object): return False def active(self, task_id): - """ - Check if a task is active (has not been called yet). + """Check if a task is active (has not been called yet). Args: task_id (int): an existing task ID. Returns: - True (bool): If a task is active (has not been called yet). - False (bool): if the task - is not active (has already been called), - does not exist + bool: True if a task is active (has not been called yet). False if + it is not (has been called) or if the task does not exist. """ if task_id in self.tasks: # if the task has not been run, cancel it - d = self.get_deferred(task_id) - if d: # it is remotely possible for a task to not have a deferral - if d.called: - return False - else: # the callback has not been called yet. - return True - else: # this task has no deferral, and could not have been called - return True + deferred = self.get_deferred(task_id) + return not (deferred and deferred.called) else: return False def cancel(self, task_id): - """ - Stop a task from automatically executing. + """Stop a task from automatically executing. This will not remove the task. Args: task_id (int): an existing task ID. Returns: - True (bool): if the removal completed successfully. - False (bool): if the task: - does not exist, - has already run, - does not have a deferral instance created for the task. - None, if there was a raised exception + bool: True if the cancel completed successfully. + False if the cancel did not complete successfully. """ if task_id in self.tasks: # if the task has not been run, cancel it d = self.get_deferred(task_id) - if d: # it is remotely possible for a task to not have a deferral + if d: # it is remotely possible for a task to not have a deferred if d.called: return False else: # the callback has not been called yet. @@ -313,17 +442,14 @@ class TaskHandler(object): return False def remove(self, task_id): - """ - Remove a task without executing it. + """Remove a task without executing it. Deletes the instance of the task's deferral. Args: task_id (int): an existing task ID. Returns: - True (bool): if the removal completed successfully or if the a - task with the id does not exist. - None: if there was a raised exception + bool: True if the removal completed successfully. """ d = None @@ -341,33 +467,30 @@ class TaskHandler(object): del d return True - def remove_all(self, save=True, cancel=True): - """ - Remove all tasks. + def clear(self, save=True, cancel=True): + """clear all tasks. By default tasks are canceled and removed from the database also. - Arguments: + Args: save=True (bool): Should changes to persistent tasks be saved to database. cancel=True (bool): Cancel scheduled tasks before removing it from task handler. Returns: True (bool): if the removal completed successfully. """ - tasks_ids = tuple(self.tasks.keys()) - for task_id in tasks_ids: - if cancel: - self.cancel(task_id) - del self.tasks[task_id] - tasks_ids = tuple(self.to_save.keys()) - for task_id in tasks_ids: - del self.to_save[task_id] + if self.tasks: + for task_id in self.tasks.keys(): + if cancel: + self.cancel(task_id) + self.tasks = {} + if self.to_save: + self.to_save = {} if save: self.save() return True def call_task(self, task_id): - """ - Call the callback of a task. + """Call the callback of a task. Leave the task unaffected otherwise. This does not use the task's deferred instance. The only requirement is that the task exist in task handler. @@ -376,8 +499,9 @@ class TaskHandler(object): task_id (int): an existing task ID. Returns: - False (bool): if the task does not exist in task handler. - ?: The return of the task's callback. + bool or any: Set to `False` if the task does not exist in task + handler. Otherwise it will be the return of the task's callback. + """ if task_id in self.tasks: date, callback, args, kwargs, persistent, d = self.tasks.get(task_id) @@ -386,8 +510,7 @@ class TaskHandler(object): return callback(*args, **kwargs) def do_task(self, task_id): - """ - Execute the task (call its callback). + """Execute the task (call its callback). If calling before timedelay cancel the deferral affliated to this task. Remove the task from the dictionary of current tasks on a successful callback. @@ -396,16 +519,8 @@ class TaskHandler(object): task_id (int): a valid task ID. Returns: - False (bool): if the: - task no longer exists, - has no affliated instance of deferral - The return of the callback passed on task creation. - This makes it possible for the callback to also return False - None: if there was a raised exception - - Note: - On a successful call the task will be removed from the dictionary - of current tasks. + bool or any: Set to `False` if the task does not exist in task + handler. Otherwise it will be the return of the task's callback. """ callback_return = False @@ -430,214 +545,30 @@ class TaskHandler(object): task_id (int): a valid task ID. Returns: - An instance of a deferral or False if there is no task with the id. - None is returned if there is no deferral affiliated with this id. + deffered or None: An instance of a deferred or None if there is no + task with the id. None is returned if there is no deferral + affiliated with this id. """ if task_id in self.tasks: return self.tasks[task_id][5] else: - return False + return None def create_delays(self): """Create the delayed tasks for the persistent tasks. - - Note: - This method should be automatically called when Evennia starts. + This method should be automatically called when Evennia starts. """ now = datetime.now() - for task_id, (date, callbac, args, kwargs, _, _) in self.tasks.items(): - self.tasks[task_id] = date, callbac, args, kwargs, True, None + for task_id, (date, callback, args, kwargs, _, _) in self.tasks.items(): + self.tasks[task_id] = date, callback, args, kwargs, True, None seconds = max(0, (date - now).total_seconds()) d = deferLater(self.clock, seconds, self.do_task, task_id) d.addErrback(handle_error) - # some tasks may complete before the deferal can be added + # some tasks may complete before the deferred can be added if self.tasks.get(task_id, False): - self.tasks[task_id] = date, callbac, args, kwargs, True, d + self.tasks[task_id] = date, callback, args, kwargs, True, d # Create the soft singleton TASK_HANDLER = TaskHandler() - - -class Task: - """ - A object to represent a single TaskHandler task. - - Instance Attributes: - task_id (int): the global id for this task - deferred (deferred): a reference to this task's deferred - Propert Attributes: - paused (bool): check if the deferral of a task has been paused. - called(self): A task attribute to check if the deferral of a task has been called. - - Methods: - pause(): Pause the callback of a task. - unpause(): Process all callbacks made since pause() was called. - do_task(): Execute the task (call its callback). - call(): Call the callback of this task. - remove(): Remove a task without executing it. - cancel(): Stop a task from automatically executing. - active(): Check if a task is active (has not been called yet). - exists(): Check if a task exists. - get_id(): Returns the global id for this task. For use with - """ - def __init__(self, task_id): - self.task_id = task_id - self.deferred = TASK_HANDLER.get_deferred(task_id) - - def get_deferred(self): - """ - Return the instance of the deferred the task id is using. - - Returns: - An instance of a deferral or False if there is no task with the id. - None is returned if there is no deferral affiliated with this id. - """ - return TASK_HANDLER.get_deferred(self.task_id) - - def pause(self): - """ - Pause the callback of a task. - To resume use Task.unpause - """ - d = self.deferred - if d: - d.pause() - - def unpause(self): - """ - Process all callbacks made since pause() was called. - """ - d = self.deferred - if d: - d.unpause() - - @property - def paused(self): - """ - A task attribute to check if the deferral of a task has been paused. - - This exists to mock usage of a twisted deferred object. - - This will return None if the deferred object for the task does not - exist or if the task no longer exists. - """ - d = self.deferred - if d: - return d.paused - else: - return None - - def do_task(self): - """ - Execute the task (call its callback). - If calling before timedelay cancel the deferral affliated to this task. - Remove the task from the dictionary of current tasks on a successful - callback. - - Returns: - False (bool): if the: - task no longer exists, - has no affliated instance of deferral - The return of the callback passed on task creation. - This makes it possible for the callback to also return False - None: if there was a raised exception - - Note: - On a successful call the task will be removed from the dictionary - of current tasks. - - """ - return TASK_HANDLER.do_task(self.task_id) - - def call(self): - """ - Call the callback of this task. - Leave the task unaffected otherwise. - This does not use the task's deferred instance. - The only requirement is that the task exist in task handler. - - Returns: - False (bool): if the task does not exist in task handler. - ?: The return of the task's callback. - """ - return TASK_HANDLER.call_task(self.task_id) - - def remove(self): - """ - Remove a task without executing it. - Deletes the instance of the task's deferral. - - Returns: - True (bool): if the removal completed successfully or if the a - task with the id does not exist. - None: if there was a raised exception - - """ - return TASK_HANDLER.remove(self.task_id) - - def cancel(self): - """ - Stop a task from automatically executing. - This will not remove the task. - - Returns: - True (bool): if the removal completed successfully. - False (bool): if the task: - does not exist, - has already run, - does not have a deferral instance created for the task. - None, if there was a raised exception - """ - return TASK_HANDLER.cancel(self.task_id) - - def active(self): - """ - Check if a task is active (has not been called yet). - - Returns: - True (bool): If a task is active (has not been called yet). - False (bool): if the task - is not active (has already been called), - does not exist - """ - return TASK_HANDLER.active(self.task_id) - - @property - def called(self): - """ - A task attribute to check if the deferral of a task has been called. - - This exists to mock usage of a twisted deferred object. - It will not set to false if Task.call has been called. - - """ - d = self.deferred - if d: - return d.called - else: - return None - - def exists(self): - """ - Test if a task exists. - - Returns: - True (bool): if the task exists. - False (bool): if the task does not exist. - - Note: - Most task handler methods check for existence for you. - """ - return TASK_HANDLER.exists(self.task_id) - - def get_id(self): - """ - Returns the global id for this task. For use with - `evennia.scripts.taskhandler.TASK_HANDLER`. - - Returns: - task_id (int): global task id for this task. - """ - return self.task_id diff --git a/evennia/utils/tests/test_utils.py b/evennia/utils/tests/test_utils.py index fa67bc768f..e4d8cf6927 100644 --- a/evennia/utils/tests/test_utils.py +++ b/evennia/utils/tests/test_utils.py @@ -463,23 +463,23 @@ class TestDelay(EvenniaTest): self.assertTrue(t.get_id() in _TASK_HANDLER.to_save) self.assertTrue(t.get_id() in _TASK_HANDLER.tasks) self.assertEqual(self.char1.ndb.dummy_var, False) - _TASK_HANDLER.remove_all() + _TASK_HANDLER.clear() self.char1.ndb.dummy_var = False _TASK_HANDLER._now = False # replicate a restart - _TASK_HANDLER.remove_all() + _TASK_HANDLER.clear() _TASK_HANDLER.save() self.assertFalse(_TASK_HANDLER.tasks) self.assertFalse(_TASK_HANDLER.to_save) # create a persistent task. t = utils.delay(timedelay, dummy_func, self.char1.dbref, persistent=True) _TASK_HANDLER.save() - _TASK_HANDLER.remove_all(False) # remove all tasks, do not save this change. + _TASK_HANDLER.clear(False) # remove all tasks, do not save this change. _TASK_HANDLER.clock.advance(timedelay) # advance twisted reactor time past callback time self.assertEqual(self.char1.ndb.dummy_var, False) # task has not run _TASK_HANDLER.load() _TASK_HANDLER.create_delays() _TASK_HANDLER.clock.advance(timedelay) # Clock must advance to trigger, even if past timedelay self.assertEqual(self.char1.ndb.dummy_var, 'dummy_func ran') - _TASK_HANDLER.remove_all() + _TASK_HANDLER.clear() self.char1.ndb.dummy_var = False From 99568148c69e5e46f6678617375eec5ec4b8193a Mon Sep 17 00:00:00 2001 From: davewiththenicehat <54369722+davewiththenicehat@users.noreply.github.com> Date: Mon, 26 Apr 2021 08:59:35 -0400 Subject: [PATCH 17/18] task handler unit test revamp & bugfix revamped task handler unit tests found bug when a False persistent kwarg is passed to the add method. Resolved it. All evennia unit tests pass. Default run level and run level 2. --- evennia/scripts/taskhandler.py | 9 +- evennia/utils/tests/test_utils.py | 334 +++++++++++++++++------------- 2 files changed, 197 insertions(+), 146 deletions(-) diff --git a/evennia/scripts/taskhandler.py b/evennia/scripts/taskhandler.py index 35b5b34889..62a4c62987 100644 --- a/evennia/scripts/taskhandler.py +++ b/evennia/scripts/taskhandler.py @@ -67,7 +67,7 @@ class TaskHandlerTask: d.pause() def unpause(self): - """Process all callbacks made since pause() was called.""" + """Unpause a task, run the task if it has passed delay time.""" d = self.deferred if d: d.unpause() @@ -328,8 +328,9 @@ class TaskHandler(object): # record the task to the tasks dictionary persistent = kwargs.get("persistent", False) - if persistent: + if "persistent" in kwargs: del kwargs["persistent"] + if persistent: safe_args = [] safe_kwargs = {} @@ -358,10 +359,10 @@ class TaskHandler(object): else: safe_kwargs[key] = value - self.tasks[task_id] = (comp_time, callback, safe_args, safe_kwargs, True, None) + self.tasks[task_id] = (comp_time, callback, safe_args, safe_kwargs, persistent, None) self.save() else: # this is a non-persitent task - self.tasks[task_id] = (comp_time, callback, args, kwargs, True, None) + self.tasks[task_id] = (comp_time, callback, args, kwargs, persistent, None) # defer the task callback = self.do_task diff --git a/evennia/utils/tests/test_utils.py b/evennia/utils/tests/test_utils.py index e4d8cf6927..61c3ac0d33 100644 --- a/evennia/utils/tests/test_utils.py +++ b/evennia/utils/tests/test_utils.py @@ -323,163 +323,213 @@ class TestDelay(EvenniaTest): Test utils.delay. """ - def test_delay(self): + def setUp(self): + super().setUp() # get a reference of TASK_HANDLER - timedelay = 5 + self.timedelay = 5 global _TASK_HANDLER if _TASK_HANDLER is None: from evennia.scripts.taskhandler import TASK_HANDLER as _TASK_HANDLER _TASK_HANDLER.clock = task.Clock() self.char1.ndb.dummy_var = False - # test a persistent deferral, that completes after delay time - t = utils.delay(timedelay, dummy_func, self.char1.dbref, persistent=True) - # call the task early to test Task.call and TaskHandler.call_task - result = t.call() - self.assertTrue(result) - del result - self.assertEqual(self.char1.ndb.dummy_var, 'dummy_func ran') - self.assertTrue(_TASK_HANDLER.active(t.get_id())) # test Task.get_id - self.assertTrue(t.active()) - self.char1.ndb.dummy_var = False # Set variable to continue completion after delay time test. - _TASK_HANDLER.clock.advance(timedelay) # make time pass - self.assertTrue(t.called) # test Task.called property - self.assertEqual(self.char1.ndb.dummy_var, 'dummy_func ran') - self.char1.ndb.dummy_var = False - # test a persistent deferral, that completes on a manual call - t = utils.delay(timedelay, dummy_func, self.char1.dbref, persistent=True) - self.assertTrue(t.active()) - result = t.do_task() - self.assertTrue(result) - self.assertFalse(t.exists()) - _TASK_HANDLER.clock.advance(timedelay) # make time pass, important keep - self.assertEqual(self.char1.ndb.dummy_var, 'dummy_func ran') - self.char1.ndb.dummy_var = False - # test a non persisten deferral, that completes after delay time. + + def tearDown(self): + super().tearDown() + _TASK_HANDLER.clear() + + def test_call_early(self): + # call a task early with call + for pers in (True, False): + t = utils.delay(self.timedelay, dummy_func, self.char1.dbref, persistent=pers) + result = t.call() + self.assertTrue(result) + self.assertEqual(self.char1.ndb.dummy_var, 'dummy_func ran') + self.assertTrue(t.exists()) + self.assertTrue(t.active()) + self.char1.ndb.dummy_var = False + + def test_do_task(self): + # call the task early with do_task + for pers in (True, False): + t = utils.delay(self.timedelay, dummy_func, self.char1.dbref, persistent=pers) + # call the task early to test Task.call and TaskHandler.call_task + result = t.do_task() + self.assertTrue(result) + self.assertEqual(self.char1.ndb.dummy_var, 'dummy_func ran') + self.assertFalse(t.exists()) + self.char1.ndb.dummy_var = False + + def test_deferred_call(self): + # wait for deferred to call + timedelay = self.timedelay + for pers in (False, True): + t = utils.delay(timedelay, dummy_func, self.char1.dbref, persistent=pers) + self.assertTrue(t.active()) + _TASK_HANDLER.clock.advance(timedelay) # make time pass + self.assertEqual(self.char1.ndb.dummy_var, 'dummy_func ran') + self.assertFalse(t.exists()) + self.char1.ndb.dummy_var = False + + def test_short_deferred_call(self): + # wait for deferred to call with a very short time + timedelay = .1 + for pers in (False, True): + t = utils.delay(timedelay, dummy_func, self.char1.dbref, persistent=pers) + self.assertTrue(t.active()) + _TASK_HANDLER.clock.advance(timedelay) # make time pass + self.assertEqual(self.char1.ndb.dummy_var, 'dummy_func ran') + self.assertFalse(t.exists()) + self.char1.ndb.dummy_var = False + + def test_active(self): + timedelay = self.timedelay t = utils.delay(timedelay, dummy_func, self.char1.dbref) + self.assertTrue(_TASK_HANDLER.active(t.get_id())) self.assertTrue(t.active()) _TASK_HANDLER.clock.advance(timedelay) # make time pass - self.assertEqual(self.char1.ndb.dummy_var, 'dummy_func ran') - self.char1.ndb.dummy_var = False - # test a non-persistent deferral, that completes on a manual call - t = utils.delay(timedelay, dummy_func, self.char1.dbref) - self.assertTrue(t.active()) - result = t.do_task() - self.assertTrue(result) - self.assertFalse(t.exists()) - _TASK_HANDLER.clock.advance(timedelay) # make time pass, important keep - self.assertEqual(self.char1.ndb.dummy_var, 'dummy_func ran') - self.char1.ndb.dummy_var = False - # test a non persisten deferral, with a short timedelay - t = utils.delay(.1, dummy_func, self.char1.dbref) - self.assertTrue(t.active()) - _TASK_HANDLER.clock.advance(.1) # make time pass - self.assertEqual(self.char1.ndb.dummy_var, 'dummy_func ran') - self.char1.ndb.dummy_var = False - # test canceling a deferral. - # after this the task_id 1 remains used by this canceled but unused task - t = utils.delay(timedelay, dummy_func, self.char1.dbref) - self.assertTrue(t.active()) - success = t.cancel() + self.assertFalse(_TASK_HANDLER.active(t.get_id())) self.assertFalse(t.active()) - self.assertTrue(success) - self.assertTrue(t.exists()) - _TASK_HANDLER.clock.advance(timedelay) # make time pass - self.assertEqual(self.char1.ndb.dummy_var, False) - self.char1.ndb.dummy_var = False - # test removing an active task + + def test_called(self): + timedelay = self.timedelay t = utils.delay(timedelay, dummy_func, self.char1.dbref) - self.assertTrue(t.active()) - success = t.remove() - self.assertFalse(t.active()) + self.assertFalse(t.called) _TASK_HANDLER.clock.advance(timedelay) # make time pass - self.assertEqual(self.char1.ndb.dummy_var, False) - self.assertFalse(t.exists()) - self.char1.ndb.dummy_var = False - # test removing a canceled task - t = utils.delay(timedelay, dummy_func, self.char1.dbref) - self.assertTrue(t.active()) - deferal_inst = t.get_deferred() - deferal_inst.cancel() - self.assertFalse(t.active()) - success = t.remove() - _TASK_HANDLER.clock.advance(timedelay) # make time pass - self.assertEqual(self.char1.ndb.dummy_var, False) - self.assertFalse(t.exists()) - self.char1.ndb.dummy_var = False - # test pause, paused and unpause - t = utils.delay(timedelay, dummy_func, self.char1.dbref) - self.assertTrue(t.active()) - t.pause() - self.assertTrue(t.paused) - t.unpause() - self.assertFalse(t.paused) - self.assertEqual(self.char1.ndb.dummy_var, False) - t.pause() - _TASK_HANDLER.clock.advance(timedelay) # make time pass - self.assertEqual(self.char1.ndb.dummy_var, False) - t.unpause() - self.assertEqual(self.char1.ndb.dummy_var, 'dummy_func ran') - self.char1.ndb.dummy_var = False - # test automated removal of stale tasks. - t = utils.delay(timedelay, dummy_func, self.char1.dbref, persistent=True) - t.cancel() - self.assertFalse(t.active()) - _TASK_HANDLER.clock.advance(timedelay) # make time pass - self.assertTrue(t.get_id() in _TASK_HANDLER.to_save) - self.assertTrue(t.get_id() in _TASK_HANDLER.tasks) + self.assertTrue(t.called) + + def test_cancel(self): + timedelay = self.timedelay + for pers in (False, True): + t = utils.delay(timedelay, dummy_func, self.char1.dbref, persistent=pers) + self.assertTrue(t.active()) + success = t.cancel() + self.assertFalse(t.active()) + self.assertTrue(success) + self.assertTrue(t.exists()) + _TASK_HANDLER.clock.advance(timedelay) # make time pass + self.assertEqual(self.char1.ndb.dummy_var, False) + + def test_remove(self): + timedelay = self.timedelay + for pers in (False, True): + t = utils.delay(timedelay, dummy_func, self.char1.dbref, persistent=pers) + self.assertTrue(t.active()) + success = t.remove() + self.assertTrue(success) + self.assertFalse(t.active()) + self.assertFalse(t.exists()) + _TASK_HANDLER.clock.advance(timedelay) # make time pass + self.assertEqual(self.char1.ndb.dummy_var, False) + + def test_remove_canceled(self): + # remove a canceled task + timedelay = self.timedelay + for pers in (False, True): + t = utils.delay(timedelay, dummy_func, self.char1.dbref, persistent=pers) + self.assertTrue(t.active()) + success = t.cancel() + self.assertTrue(success) + self.assertTrue(t.exists()) + self.assertFalse(t.active()) + success = t.remove() + self.assertTrue(success) + self.assertFalse(t.exists()) + _TASK_HANDLER.clock.advance(timedelay) # make time pass + self.assertEqual(self.char1.ndb.dummy_var, False) + + def test_pause_unpause(self): + # remove a canceled task + timedelay = self.timedelay + for pers in (False, True): + t = utils.delay(timedelay, dummy_func, self.char1.dbref, persistent=pers) + self.assertTrue(t.active()) + t.pause() + self.assertTrue(t.paused) + t.unpause() + self.assertFalse(t.paused) + self.assertEqual(self.char1.ndb.dummy_var, False) + t.pause() + _TASK_HANDLER.clock.advance(timedelay) # make time pass + self.assertEqual(self.char1.ndb.dummy_var, False) + t.unpause() + self.assertEqual(self.char1.ndb.dummy_var, 'dummy_func ran') + self.char1.ndb.dummy_var = False + + def test_auto_stale_task_removal(self): + # automated removal of stale tasks. + timedelay = self.timedelay + for pers in (False, True): + t = utils.delay(timedelay, dummy_func, self.char1.dbref, persistent=pers) + t.cancel() + self.assertFalse(t.active()) + _TASK_HANDLER.clock.advance(timedelay) # make time pass + if pers: + self.assertTrue(t.get_id() in _TASK_HANDLER.to_save) + self.assertTrue(t.get_id() in _TASK_HANDLER.tasks) + # Make task handler's now time, after the stale timeout + _TASK_HANDLER._now = datetime.now() + timedelta(seconds=_TASK_HANDLER.stale_timeout + timedelay + 1) # add a task to test automatic removal - _TASK_HANDLER._now = datetime.now() + timedelta(seconds=_TASK_HANDLER.stale_timeout + 6) # task handler time to 6 seconds after stale timeout - t2 = utils.delay(timedelay, dummy_func, self.char1.dbref, persistent=True) - self.assertFalse(t.get_id() in _TASK_HANDLER.to_save) - self.assertFalse(t.get_id() in _TASK_HANDLER.tasks) - self.assertEqual(self.char1.ndb.dummy_var, False) - # test manual cleanup - t2.cancel() - _TASK_HANDLER.clock.advance(timedelay) # advance twisted reactor time past callback time - _TASK_HANDLER._now = datetime.now() + timedelta(seconds=30) # set TaskHandler's time to 30 seconnds from now - # test before stale_timeout time - _TASK_HANDLER.clean_stale_tasks() # cleanup of stale tasks in in the save method - # still in the task handler because stale timeout has not been reached - self.assertTrue(t2.get_id() in _TASK_HANDLER.to_save) - self.assertTrue(t2.get_id() in _TASK_HANDLER.tasks) - # advance past stale timeout - _TASK_HANDLER._now = datetime.now() + timedelta(seconds=_TASK_HANDLER.stale_timeout + 6) # task handler time to 6 seconds after stale timeout - _TASK_HANDLER.clean_stale_tasks() # cleanup of stale tasks in in the save method - self.assertFalse(t2.get_id() in _TASK_HANDLER.to_save) - self.assertFalse(t2.get_id() in _TASK_HANDLER.tasks) - self.char1.ndb.dummy_var = False - _TASK_HANDLER._now = False - # if _TASK_HANDLER.stale_timeout is 0 or less, automatic cleanup should not run + t2 = utils.delay(timedelay, dummy_func, self.char1.dbref) + if pers: + self.assertFalse(t.get_id() in _TASK_HANDLER.to_save) + self.assertFalse(t.get_id() in _TASK_HANDLER.tasks) + self.assertEqual(self.char1.ndb.dummy_var, False) + _TASK_HANDLER.clear() + + def test_manual_stale_task_removal(self): + # manual removal of stale tasks. + timedelay = self.timedelay + for pers in (False, True): + t = utils.delay(timedelay, dummy_func, self.char1.dbref, persistent=pers) + t.cancel() + self.assertFalse(t.active()) + _TASK_HANDLER.clock.advance(timedelay) # make time pass + if pers: + self.assertTrue(t.get_id() in _TASK_HANDLER.to_save) + self.assertTrue(t.get_id() in _TASK_HANDLER.tasks) + # Make task handler's now time, after the stale timeout + _TASK_HANDLER._now = datetime.now() + timedelta(seconds=_TASK_HANDLER.stale_timeout + timedelay + 1) + _TASK_HANDLER.clean_stale_tasks() # cleanup of stale tasks in in the save method + if pers: + self.assertFalse(t.get_id() in _TASK_HANDLER.to_save) + self.assertFalse(t.get_id() in _TASK_HANDLER.tasks) + self.assertEqual(self.char1.ndb.dummy_var, False) + _TASK_HANDLER.clear() + + def test_disable_stale_removal(self): + # manual removal of stale tasks. + timedelay = self.timedelay _TASK_HANDLER.stale_timeout = 0 - t = utils.delay(timedelay, dummy_func, self.char1.dbref, persistent=True) - t.cancel() - self.assertFalse(t.active()) - _TASK_HANDLER.clock.advance(timedelay) # advance twisted's reactor past callback time - self.assertTrue(t.get_id() in _TASK_HANDLER.to_save) - self.assertTrue(t.get_id() in _TASK_HANDLER.tasks) - # add a task to test automatic removal - _TASK_HANDLER._now = datetime.now() + timedelta(seconds=_TASK_HANDLER.stale_timeout + 6) # task handler time to 6 seconds after stale timeout - t2 = utils.delay(timedelay, dummy_func, self.char1.dbref, persistent=True) - self.assertTrue(t.get_id() in _TASK_HANDLER.to_save) - self.assertTrue(t.get_id() in _TASK_HANDLER.tasks) - self.assertEqual(self.char1.ndb.dummy_var, False) - _TASK_HANDLER.clear() - self.char1.ndb.dummy_var = False - _TASK_HANDLER._now = False - # replicate a restart - _TASK_HANDLER.clear() - _TASK_HANDLER.save() - self.assertFalse(_TASK_HANDLER.tasks) - self.assertFalse(_TASK_HANDLER.to_save) - # create a persistent task. - t = utils.delay(timedelay, dummy_func, self.char1.dbref, persistent=True) - _TASK_HANDLER.save() - _TASK_HANDLER.clear(False) # remove all tasks, do not save this change. + for pers in (False, True): + t = utils.delay(timedelay, dummy_func, self.char1.dbref, persistent=pers) + t.cancel() + self.assertFalse(t.active()) + _TASK_HANDLER.clock.advance(timedelay) # make time pass + if pers: + self.assertTrue(t.get_id() in _TASK_HANDLER.to_save) + self.assertTrue(t.get_id() in _TASK_HANDLER.tasks) + # Make task handler's now time, after the stale timeout + _TASK_HANDLER._now = datetime.now() + timedelta(seconds=_TASK_HANDLER.stale_timeout + timedelay + 1) + t2 = utils.delay(timedelay, dummy_func, self.char1.dbref) + if pers: + self.assertTrue(t.get_id() in _TASK_HANDLER.to_save) + self.assertTrue(t.get_id() in _TASK_HANDLER.tasks) + self.assertEqual(self.char1.ndb.dummy_var, False) + # manual removal should still work + _TASK_HANDLER.clean_stale_tasks() # cleanup of stale tasks in in the save method + if pers: + self.assertFalse(t.get_id() in _TASK_HANDLER.to_save) + self.assertFalse(t.get_id() in _TASK_HANDLER.tasks) + _TASK_HANDLER.clear() + + def test_server_restart(self): + # emulate a server restart + timedelay = self.timedelay + utils.delay(timedelay, dummy_func, self.char1.dbref, persistent=True) + _TASK_HANDLER.clear(False) # remove all tasks from task handler, do not save this change. _TASK_HANDLER.clock.advance(timedelay) # advance twisted reactor time past callback time self.assertEqual(self.char1.ndb.dummy_var, False) # task has not run - _TASK_HANDLER.load() - _TASK_HANDLER.create_delays() + _TASK_HANDLER.load() # load persistent tasks from database. + _TASK_HANDLER.create_delays() # create new deffered instances from persistent tasks _TASK_HANDLER.clock.advance(timedelay) # Clock must advance to trigger, even if past timedelay self.assertEqual(self.char1.ndb.dummy_var, 'dummy_func ran') - _TASK_HANDLER.clear() - self.char1.ndb.dummy_var = False From 248c8846058613b34649249ad78d54890379832a Mon Sep 17 00:00:00 2001 From: davewiththenicehat <54369722+davewiththenicehat@users.noreply.github.com> Date: Fri, 30 Apr 2021 11:26:49 -0400 Subject: [PATCH 18/18] doc strings updates. doc strings updates. Unit tests pass at default run level. --- evennia/scripts/taskhandler.py | 81 ++++++++++++++++++++-------------- 1 file changed, 48 insertions(+), 33 deletions(-) diff --git a/evennia/scripts/taskhandler.py b/evennia/scripts/taskhandler.py index 62a4c62987..bd979a9fde 100644 --- a/evennia/scripts/taskhandler.py +++ b/evennia/scripts/taskhandler.py @@ -15,7 +15,7 @@ TASK_HANDLER = None def handle_error(*args, **kwargs): - """Handle errors withing deferred objects.""" + """Handle errors within deferred objects.""" for arg in args: # suppress cancel errors if arg.type == DefCancelledError: @@ -24,14 +24,14 @@ def handle_error(*args, **kwargs): class TaskHandlerTask: - """A object to represent a single TaskHandler task. + """An object to represent a single TaskHandler task. Instance Attributes: task_id (int): the global id for this task deferred (deferred): a reference to this task's deferred - Propert Attributes: - paused (bool): check if the deferral of a task has been paused. - called(self): A task attribute to check if the deferral of a task has been called. + Property Attributes: + paused (bool): check if the deferred instance of a task has been paused. + called(self): A task attribute to check if the deferred instance of a task has been called. Methods: pause(): Pause the callback of a task. @@ -43,6 +43,7 @@ class TaskHandlerTask: active(): Check if a task is active (has not been called yet). exists(): Check if a task exists. get_id(): Returns the global id for this task. For use with + """ def __init__(self, task_id): @@ -53,8 +54,9 @@ class TaskHandlerTask: """Return the instance of the deferred the task id is using. Returns: - An instance of a deferral or False if there is no task with the id. - None is returned if there is no deferral affiliated with this id. + bool or deferred: An instance of a deferred or False if there is no task with the id. + None is returned if there is no deferred affiliated with this id. + """ return TASK_HANDLER.get_deferred(self.task_id) @@ -74,12 +76,14 @@ class TaskHandlerTask: @property def paused(self): - """A task attribute to check if the deferral of a task has been paused. + """A task attribute to check if the deferred instance of a task has been paused. This exists to mock usage of a twisted deferred object. - This will return None if the deferred object for the task does not - exist or if the task no longer exists. + Returns: + bool or None: True if the task was properly paused. None if the task does not have + a deferred instance. + """ d = self.deferred if d: @@ -89,7 +93,7 @@ class TaskHandlerTask: def do_task(self): """Execute the task (call its callback). - If calling before timedelay cancel the deferral affliated to this task. + If calling before timedelay, cancel the deferred instance affliated to this task. Remove the task from the dictionary of current tasks on a successful callback. @@ -115,7 +119,7 @@ class TaskHandlerTask: def remove(self): """Remove a task without executing it. - Deletes the instance of the task's deferral. + Deletes the instance of the task's deferred. Args: task_id (int): an existing task ID. @@ -133,6 +137,7 @@ class TaskHandlerTask: Returns: bool: True if the cancel completed successfully. False if the cancel did not complete successfully. + """ return TASK_HANDLER.cancel(self.task_id) @@ -142,16 +147,22 @@ class TaskHandlerTask: Returns: bool: True if a task is active (has not been called yet). False if it is not (has been called) or if the task does not exist. + """ return TASK_HANDLER.active(self.task_id) @property def called(self): """ - A task attribute to check if the deferral of a task has been called. + A task attribute to check if the deferred instance of a task has been called. This exists to mock usage of a twisted deferred object. - It will not set to false if Task.call has been called. + It will not set to True if Task.call has been called. This only happens if + task's deferred instance calls the callback. + + Returns: + bool: True if the deferred instance of this task has called the callback. + False if the deferred instnace of this task has not called the callback. """ d = self.deferred @@ -165,7 +176,8 @@ class TaskHandlerTask: Most task handler methods check for existence for you. Returns: - bool: Tru the task exists False if it does not. + bool: True the task exists False if it does not. + """ return TASK_HANDLER.exists(self.task_id) @@ -175,6 +187,7 @@ class TaskHandlerTask: Returns: task_id (int): global task id for this task. + """ return self.task_id @@ -241,6 +254,7 @@ class TaskHandler(object): By default this will not occur until a canceled task has been uncalled for 60 second after the time it should have been called. To adjust this time use TASK_HANDLER.stale_timeout. + """ clean_ids = [] for task_id, (date, callback, args, kwargs, persistent, _) in self.tasks.items(): @@ -256,9 +270,7 @@ class TaskHandler(object): return True def save(self): - """ - Save the tasks in ServerConfig. - """ + """Save the tasks in ServerConfig.""" for task_id, (date, callback, args, kwargs, persistent, _) in self.tasks.items(): if task_id in self.to_save: @@ -296,23 +308,23 @@ class TaskHandler(object): The callback, args and values for kwarg will be serialized. Type and attribute errors during the serialization will be logged, but will not throw exceptions. - For persisten tasks do not use memory references in the callback + For persistent tasks do not use memory references in the callback function or arguments. After a restart those memory references are no longer accurate. Args: - timedelay (int or float): time in sedconds before calling the callback. + timedelay (int or float): time in seconds before calling the callback. callback (function or instance method): the callback itself any (any): any additional positional arguments to send to the callback *args: positional arguments to pass to callback. - **kwargs: key word arguments to pass to callback. + **kwargs: keyword arguments to pass to callback. persistent (bool, optional): persist the task (stores it). - persisten key and value is removed from kwargs it will + persistent key and value is removed from kwargs it will not be passed to callback. Returns: TaskHandlerTask: An object to represent a task. - Reference evennia.scripts.taskhandler.Task for complete details. + Reference evennia.scripts.taskhandler.TaskHandlerTask for complete details. """ # set the completion time @@ -392,7 +404,7 @@ class TaskHandler(object): task_id (int): an existing task ID. Returns: - bool: Tru the task exists False if it does not. + bool: True the task exists False if it does not. """ if task_id in self.tasks: @@ -409,6 +421,7 @@ class TaskHandler(object): Returns: bool: True if a task is active (has not been called yet). False if it is not (has been called) or if the task does not exist. + """ if task_id in self.tasks: # if the task has not been run, cancel it @@ -427,6 +440,7 @@ class TaskHandler(object): Returns: bool: True if the cancel completed successfully. False if the cancel did not complete successfully. + """ if task_id in self.tasks: # if the task has not been run, cancel it @@ -437,14 +451,14 @@ class TaskHandler(object): else: # the callback has not been called yet. d.cancel() return True - else: # this task has no deferral + else: # this task has no deferred instance return False else: return False def remove(self, task_id): """Remove a task without executing it. - Deletes the instance of the task's deferral. + Deletes the instance of the task's deferred. Args: task_id (int): an existing task ID. @@ -463,7 +477,7 @@ class TaskHandler(object): if task_id in self.to_save: del self.to_save[task_id] self.save() # remove from ServerConfig.objects - # delete the instance of the deferral + # delete the instance of the deferred if d: del d return True @@ -478,6 +492,7 @@ class TaskHandler(object): Returns: True (bool): if the removal completed successfully. + """ if self.tasks: for task_id in self.tasks.keys(): @@ -512,7 +527,7 @@ class TaskHandler(object): def do_task(self, task_id): """Execute the task (call its callback). - If calling before timedelay cancel the deferral affliated to this task. + If calling before timedelay cancel the deferred instance affliated to this task. Remove the task from the dictionary of current tasks on a successful callback. @@ -529,10 +544,10 @@ class TaskHandler(object): date, callback, args, kwargs, persistent, d = self.tasks.get(task_id) else: # the task does not exist return False - if d: # it is remotely possible for a task to not have a deferral + if d: # it is remotely possible for a task to not have a deferred if not d.called: # the task's deferred has not been called yet d.cancel() # cancel the automated callback - else: # this task has no deferral, and should not be called + else: # this task has no deferred, and should not be called return False callback_return = callback(*args, **kwargs) self.remove(task_id) @@ -546,9 +561,9 @@ class TaskHandler(object): task_id (int): a valid task ID. Returns: - deffered or None: An instance of a deferred or None if there is no - task with the id. None is returned if there is no deferral - affiliated with this id. + bool or deferred: An instance of a deferred or False if there is no task with the id. + None is returned if there is no deferred affiliated with this id. + """ if task_id in self.tasks: return self.tasks[task_id][5]