diff --git a/evennia/contrib/slow_exit.py b/evennia/contrib/slow_exit.py index 3b060f747..7f7f2cc24 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 7b69d07cc..a4e50a29f 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 759d1f1dc..ca4147a71 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 eceb61a1c..9ccb0ea30 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 42facf7e6..821e96aa2 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 8de5ef190..9bf252d0a 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