├── .gitignore ├── animation ├── __init__.py ├── animation.py └── transitions.py ├── readme.md ├── setup.py └── tests └── test_animation.py /.gitignore: -------------------------------------------------------------------------------- 1 | # Created by .ignore support plugin (hsz.mobi) 2 | ### Python template 3 | # Byte-compiled / optimized / DLL files 4 | __pycache__/ 5 | *.py[cod] 6 | *$py.class 7 | 8 | # C extensions 9 | *.so 10 | 11 | # Distribution / packaging 12 | .Python 13 | env/ 14 | build/ 15 | develop-eggs/ 16 | dist/ 17 | downloads/ 18 | eggs/ 19 | .eggs/ 20 | lib/ 21 | lib64/ 22 | parts/ 23 | sdist/ 24 | var/ 25 | *.egg-info/ 26 | .installed.cfg 27 | *.egg 28 | 29 | # PyInstaller 30 | # Usually these files are written by a python script from a template 31 | # before PyInstaller builds the exe, so as to inject date/other infos into it. 32 | *.manifest 33 | *.spec 34 | 35 | # Installer logs 36 | pip-log.txt 37 | pip-delete-this-directory.txt 38 | 39 | # Unit test / coverage reports 40 | htmlcov/ 41 | .tox/ 42 | .coverage 43 | .coverage.* 44 | .cache 45 | nosetests.xml 46 | coverage.xml 47 | *,cover 48 | 49 | # Translations 50 | *.mo 51 | *.pot 52 | 53 | # Django stuff: 54 | *.log 55 | 56 | # Sphinx documentation 57 | docs/_build/ 58 | 59 | # PyBuilder 60 | target/ 61 | 62 | -------------------------------------------------------------------------------- /animation/__init__.py: -------------------------------------------------------------------------------- 1 | from .animation import Animation, Task, remove_animations_of 2 | 3 | __version__ = '0.0.5' 4 | -------------------------------------------------------------------------------- /animation/animation.py: -------------------------------------------------------------------------------- 1 | from __future__ import division 2 | from __future__ import print_function 3 | 4 | import sys 5 | from collections import defaultdict 6 | 7 | import pygame 8 | 9 | from .transitions import AnimationTransition 10 | 11 | __all__ = ('Task', 'Animation', 'remove_animations_of') 12 | 13 | ANIMATION_NOT_STARTED = 0 14 | ANIMATION_RUNNING = 1 15 | ANIMATION_DELAYED = 2 16 | ANIMATION_FINISHED = 3 17 | 18 | PY2 = sys.version_info[0] == 2 19 | string_types = None 20 | text_type = None 21 | if PY2: 22 | string_types = basestring 23 | text_type = unicode 24 | else: 25 | string_types = text_type = str 26 | 27 | 28 | def is_number(value): 29 | """Test if an object is a number. 30 | :param value: some object 31 | :returns: True 32 | :raises: ValueError 33 | """ 34 | try: 35 | float(value) 36 | except (ValueError, TypeError): 37 | raise ValueError 38 | 39 | return True 40 | 41 | 42 | def remove_animations_of(target, group): 43 | """ Find animations that target objects and remove those animations 44 | 45 | :param target: any 46 | :param group: pygame.sprite.Group 47 | :returns: list of animations that were removed 48 | """ 49 | animations = [ani for ani in group.sprites() if isinstance(ani, Animation)] 50 | to_remove = [ani for ani in animations 51 | if target in [i[0] for i in ani.targets]] 52 | group.remove(*to_remove) 53 | return to_remove 54 | 55 | 56 | class AnimBase(pygame.sprite.Sprite): 57 | _valid_schedules = [] 58 | 59 | def __init__(self): 60 | super(AnimBase, self).__init__() 61 | self._callbacks = defaultdict(list) 62 | 63 | def schedule(self, func, when=None): 64 | """ Schedule a callback during operation of Task or Animation 65 | 66 | The callback is any callable object. You can specify different 67 | times for the callback to be executed, according to the following: 68 | 69 | * "on update": called each time the Task/Animation is updated 70 | * "on finish": called when the Task/Animation completes normally 71 | * "on abort": called if the Task/Animation is aborted 72 | 73 | If when is not passed, it will be "on finish": 74 | 75 | :type func: callable 76 | :type when: basestring 77 | :return: 78 | """ 79 | if when is None: 80 | when = self._valid_schedules[0] 81 | 82 | if when not in self._valid_schedules: 83 | print('invalid time to schedule a callback') 84 | print('valid:', self._valid_schedules) 85 | raise ValueError 86 | self._callbacks[when].append(func) 87 | 88 | def _execute_callbacks(self, when): 89 | try: 90 | callbacks = self._callbacks[when] 91 | except KeyError: 92 | return 93 | else: 94 | [cb() for cb in callbacks] 95 | 96 | 97 | class Task(AnimBase): 98 | """ Execute functions at a later time and optionally loop it 99 | 100 | This is a silly little class meant to make it easy to create 101 | delayed or looping events without any complicated hooks into 102 | pygame's clock or event loop. 103 | 104 | Tasks are created and must be added to a normal pygame group 105 | in order to function. This group must be updated, but not 106 | drawn. 107 | 108 | Setting the interval to 0 cause the callback to be called 109 | on the next update. 110 | 111 | Because the pygame clock returns milliseconds, the examples 112 | below use milliseconds. However, you are free to use what- 113 | ever time unit you wish, as long as it is used consistently 114 | 115 | task_group = pygame.sprite.Group() 116 | 117 | # like a delay 118 | def call_later(): 119 | pass 120 | task = Task(call_later, 1000) 121 | task_group.add(task) 122 | 123 | # do something 24 times at 1 second intervals 124 | task = Task(call_later, 1000, 24) 125 | 126 | # do something every 2.5 seconds forever 127 | task = Task(call_later, 2500, -1) 128 | 129 | # pass arguments using functools.partial 130 | from functools import partial 131 | task = Task(partial(call_later(1,2,3, key=value)), 1000) 132 | 133 | # a task must have at lease on callback, but others can be added 134 | task = Task(call_later, 2500, -1) 135 | task.schedule(some_thing_else) 136 | 137 | # chain tasks: when one task finishes, start another one 138 | task = Task(call_later, 2500) 139 | task.chain(Task(something_else)) 140 | 141 | When chaining tasks, do not add the chained tasks to a group. 142 | """ 143 | _valid_schedules = ('on interval', 'on finish', 'on abort') 144 | 145 | def __init__(self, callback, interval=0, times=1): 146 | if not callable(callback): 147 | raise ValueError 148 | 149 | if times == 0: 150 | raise ValueError 151 | 152 | super(Task, self).__init__() 153 | self._interval = interval 154 | self._loops = times 155 | self._duration = 0 156 | self._chain = list() 157 | self._state = ANIMATION_RUNNING 158 | self.schedule(callback) 159 | 160 | def chain(self, *others): 161 | """ Schedule Task(s) to execute when this one is finished 162 | 163 | If you attempt to chain a task that will never end (loops=-1), 164 | then ValueError will be raised. 165 | 166 | :param others: Task instances 167 | :returns: None 168 | """ 169 | if self._loops <= -1: 170 | raise ValueError 171 | for task in others: 172 | if not isinstance(task, Task): 173 | raise TypeError 174 | self._chain.append(task) 175 | return others 176 | 177 | def update(self, dt): 178 | """ Update the Task 179 | 180 | The unit of time passed must match the one used in the 181 | constructor. 182 | 183 | Task will not 'make up for lost time'. If an interval 184 | was skipped because of a lagging clock, then callbacks 185 | will not be made to account for the missed ones. 186 | 187 | :param dt: Time passed since last update. 188 | """ 189 | if self._state is not ANIMATION_RUNNING: 190 | # raise RuntimeError 191 | return 192 | 193 | self._duration += dt 194 | if self._duration >= self._interval: 195 | self._duration -= self._interval 196 | if self._loops >= 0: 197 | self._loops -= 1 198 | if self._loops == 0: 199 | self.finish() 200 | else: # not finished, but still are iterations left 201 | self._execute_callbacks("on interval") 202 | else: # loops == -1, run forever 203 | self._execute_callbacks("on interval") 204 | 205 | def finish(self): 206 | """ Force task to finish, while executing callbacks 207 | """ 208 | if self._state is ANIMATION_RUNNING: 209 | self._state = ANIMATION_FINISHED 210 | self._execute_callbacks("on interval") 211 | self._execute_callbacks("on finish") 212 | self._execute_chain() 213 | self._cleanup() 214 | 215 | def abort(self): 216 | """Force task to finish, without executing callbacks 217 | """ 218 | self._state = ANIMATION_FINISHED 219 | self.kill() 220 | 221 | def _cleanup(self): 222 | self._chain = None 223 | 224 | def _execute_chain(self): 225 | groups = self.groups() 226 | for task in self._chain: 227 | task.add(*groups) 228 | 229 | 230 | class Animation(AnimBase): 231 | """ Change numeric values over time 232 | 233 | To animate a target sprite/object's position, simply specify 234 | the target x/y values where you want the widget positioned at 235 | the end of the animation. Then call start while passing the 236 | target as the only parameter. 237 | ani = Animation(x=100, y=100, duration=1000) 238 | ani.start(sprite) 239 | 240 | The shorthand method of starting animations is to pass the 241 | targets as positional arguments in the constructor. 242 | ani = Animation(sprite.rect, x=100, y=0) 243 | 244 | If you would rather specify relative values, then pass the 245 | relative keyword and the values will be adjusted for you: 246 | ani = Animation(x=100, y=100, duration=1000, relative=True) 247 | ani.start(sprite) 248 | 249 | You can also specify a callback that will be executed when the 250 | animation finishes: 251 | ani.schedule(my_function, 'on finish') 252 | 253 | Another optional callback is available that is called after 254 | each update: 255 | ani.schedule(update_function, 'on update') 256 | 257 | Animations must be added to a sprite group in order for them 258 | to be updated. If the sprite group that contains them is 259 | drawn then an exception will be raised, so you should create 260 | a sprite group only for containing Animations. 261 | 262 | You can cancel the animation by calling Animation.abort(). 263 | 264 | When the Animation has finished, then it will remove itself 265 | from the sprite group that contains it. 266 | 267 | You can optionally delay the start of the animation using the 268 | delay keyword. 269 | 270 | 271 | Callable Attributes 272 | =================== 273 | 274 | Target values can also be callable. In this case, there is 275 | no way to determine the initial value unless it is specified 276 | in the constructor. If no initial value is specified, it will 277 | default to 0. 278 | 279 | Like target arguments, the initial value can also refer to a 280 | callable. 281 | 282 | NOTE: Specifying an initial value will set the initial value 283 | for all target names in the constructor. This 284 | limitation won't be resolved for a while. 285 | 286 | 287 | Pygame Rects 288 | ============ 289 | 290 | The 'round_values' parameter will be set to True automatically 291 | if pygame rects are used as an animation target. 292 | """ 293 | _valid_schedules = ('on finish', 'on update') 294 | default_duration = 1000. 295 | default_transition = 'linear' 296 | 297 | def __init__(self, *targets, **kwargs): 298 | super(Animation, self).__init__() 299 | self._targets = list() 300 | self._pre_targets = list() # used when there is a delay 301 | self._delay = kwargs.get('delay', 0) 302 | self._state = ANIMATION_NOT_STARTED 303 | self._round_values = kwargs.get('round_values', False) 304 | self._duration = float(kwargs.get('duration', self.default_duration)) 305 | self._transition = kwargs.get('transition', self.default_transition) 306 | self._initial = kwargs.get('initial', None) 307 | self._relative = kwargs.get('relative', False) 308 | if isinstance(self._transition, string_types): 309 | self._transition = getattr(AnimationTransition, self._transition) 310 | self._elapsed = 0. 311 | for key in ('duration', 'transition', 'round_values', 'delay', 312 | 'initial', 'relative'): 313 | kwargs.pop(key, None) 314 | if not kwargs: 315 | raise ValueError 316 | self.props = kwargs 317 | 318 | if targets: 319 | self.start(*targets) 320 | 321 | @property 322 | def targets(self): 323 | return list(self._targets) 324 | 325 | def _get_value(self, target, name): 326 | """ Get value of an attribute, even if it is callable 327 | 328 | :param target: object than contains attribute 329 | :param name: name of attribute to get value from 330 | :returns: Any 331 | """ 332 | if self._initial is None: 333 | value = getattr(target, name) 334 | else: 335 | value = self._initial 336 | 337 | if callable(value): 338 | value = value() 339 | 340 | return value 341 | 342 | def _set_value(self, target, name, value): 343 | """ Set a value on some other object 344 | 345 | If the name references a callable type, then 346 | the object of that name will be called with 'value' 347 | as the first and only argument. 348 | 349 | Because callables are 'write only', there is no way 350 | to determine the initial value. you can supply 351 | an initial value in the constructor as a value or 352 | reference to a callable object. 353 | 354 | :param target: object to be modified 355 | :param name: name of attribute to be modified 356 | :param value: value 357 | :returns: None 358 | """ 359 | if self._round_values: 360 | value = int(round(value, 0)) 361 | 362 | attr = getattr(target, name) 363 | if callable(attr): 364 | attr(value) 365 | else: 366 | setattr(target, name, value) 367 | 368 | def _gather_initial_values(self): 369 | self._targets = list() 370 | for target in self._pre_targets: 371 | props = dict() 372 | if isinstance(target, pygame.Rect): 373 | self._round_values = True 374 | for name, value in self.props.items(): 375 | initial = self._get_value(target, name) 376 | is_number(initial) 377 | is_number(value) 378 | if self._relative: 379 | value += initial 380 | props[name] = initial, value 381 | self._targets.append((target, props)) 382 | 383 | self.update(0) # required to 'prime' initial values of callable targets 384 | 385 | def update(self, dt): 386 | """ Update the animation 387 | 388 | The unit of time passed must match the one used in the 389 | constructor. 390 | 391 | Make sure that you start the animation, otherwise your 392 | animation will not be changed during update(). 393 | 394 | Will raise RuntimeError if animation is updated after 395 | it has finished. 396 | 397 | :param dt: Time passed since last update. 398 | :raises: RuntimeError 399 | """ 400 | if self._state is ANIMATION_FINISHED: 401 | # raise RuntimeError 402 | return 403 | 404 | if self._state is not ANIMATION_RUNNING: 405 | return 406 | 407 | self._elapsed += dt 408 | if self._delay > 0: 409 | if self._elapsed > self._delay: 410 | self._elapsed -= self._delay 411 | self._gather_initial_values() 412 | self._delay = 0 413 | return 414 | 415 | p = min(1., self._elapsed / self._duration) 416 | t = self._transition(p) 417 | for target, props in self._targets: 418 | for name, values in props.items(): 419 | a, b = values 420 | value = (a * (1. - t)) + (b * t) 421 | self._set_value(target, name, value) 422 | 423 | # update will be called with 0 it init the delay, but we 424 | # don't want to call the update callback in that case 425 | if dt: 426 | self._execute_callbacks("on update") 427 | 428 | if p >= 1: 429 | self.finish() 430 | 431 | def finish(self): 432 | """ Force animation to finish, apply transforms, and execute callbacks 433 | 434 | Update callback will be called because the value is changed 435 | Final callback ('callback') will be called 436 | Final values will be applied 437 | Animation will be removed from group 438 | 439 | Will raise RuntimeError if animation has not been started 440 | 441 | :returns: None 442 | :raises: RuntimeError 443 | """ 444 | # if self._state is not ANIMATION_RUNNING: 445 | # raise RuntimeError 446 | 447 | if self._targets is not None: 448 | for target, props in self._targets: 449 | for name, values in props.items(): 450 | a, b = values 451 | self._set_value(target, name, b) 452 | 453 | self._execute_callbacks("on update") 454 | self.abort() 455 | 456 | def abort(self): 457 | """ Force animation to finish, without any cleanup 458 | 459 | Update callback will not be executed 460 | Final callback will be executed 461 | Values will not change 462 | Animation will be removed from group 463 | 464 | Will raise RuntimeError if animation has not been started 465 | 466 | :returns: None 467 | :raises: RuntimeError 468 | """ 469 | # if self._state is not ANIMATION_RUNNING: 470 | # raise RuntimeError 471 | 472 | self._state = ANIMATION_FINISHED 473 | self._targets = None 474 | self.kill() 475 | self._execute_callbacks("on finish") 476 | 477 | def start(self, *targets): 478 | """ Start the animation on a target sprite/object 479 | 480 | Targets must have the attributes that were set when 481 | this animation was created. 482 | 483 | :param targets: Any valid python object 484 | :raises: RuntimeError 485 | """ 486 | # TODO: weakref the targets 487 | if self._state is not ANIMATION_NOT_STARTED: 488 | raise RuntimeError 489 | 490 | self._state = ANIMATION_RUNNING 491 | self._pre_targets = targets 492 | 493 | if self._delay == 0: 494 | self._gather_initial_values() 495 | 496 | -------------------------------------------------------------------------------- /animation/transitions.py: -------------------------------------------------------------------------------- 1 | from math import sqrt, cos, sin, pi 2 | 3 | __all__ = ('AnimationTransition',) 4 | 5 | 6 | class AnimationTransition(object): 7 | """Collection of animation functions to be used with the Animation object. 8 | Easing Functions ported to Kivy from the Clutter Project 9 | http://www.clutter-project.org/docs/clutter/stable/ClutterAlpha.html 10 | 11 | The `progress` parameter in each animation function is in the range 0-1. 12 | """ 13 | 14 | @staticmethod 15 | def linear(progress): 16 | return progress 17 | 18 | @staticmethod 19 | def in_quad(progress): 20 | return progress * progress 21 | 22 | @staticmethod 23 | def out_quad(progress): 24 | return -1.0 * progress * (progress - 2.0) 25 | 26 | @staticmethod 27 | def in_out_quad(progress): 28 | p = progress * 2 29 | if p < 1: 30 | return 0.5 * p * p 31 | p -= 1.0 32 | return -0.5 * (p * (p - 2.0) - 1.0) 33 | 34 | @staticmethod 35 | def in_cubic(progress): 36 | return progress * progress * progress 37 | 38 | @staticmethod 39 | def out_cubic(progress): 40 | p = progress - 1.0 41 | return p * p * p + 1.0 42 | 43 | @staticmethod 44 | def in_out_cubic(progress): 45 | p = progress * 2 46 | if p < 1: 47 | return 0.5 * p * p * p 48 | p -= 2 49 | return 0.5 * (p * p * p + 2.0) 50 | 51 | @staticmethod 52 | def in_quart(progress): 53 | return progress * progress * progress * progress 54 | 55 | @staticmethod 56 | def out_quart(progress): 57 | p = progress - 1.0 58 | return -1.0 * (p * p * p * p - 1.0) 59 | 60 | @staticmethod 61 | def in_out_quart(progress): 62 | p = progress * 2 63 | if p < 1: 64 | return 0.5 * p * p * p * p 65 | p -= 2 66 | return -0.5 * (p * p * p * p - 2.0) 67 | 68 | @staticmethod 69 | def in_quint(progress): 70 | return progress * progress * progress * progress * progress 71 | 72 | @staticmethod 73 | def out_quint(progress): 74 | p = progress - 1.0 75 | return p * p * p * p * p + 1.0 76 | 77 | @staticmethod 78 | def in_out_quint(progress): 79 | p = progress * 2 80 | if p < 1: 81 | return 0.5 * p * p * p * p * p 82 | p -= 2.0 83 | return 0.5 * (p * p * p * p * p + 2.0) 84 | 85 | @staticmethod 86 | def in_sine(progress): 87 | return -1.0 * cos(progress * (pi / 2.0)) + 1.0 88 | 89 | @staticmethod 90 | def out_sine(progress): 91 | return sin(progress * (pi / 2.0)) 92 | 93 | @staticmethod 94 | def in_out_sine(progress): 95 | return -0.5 * (cos(pi * progress) - 1.0) 96 | 97 | @staticmethod 98 | def in_expo(progress): 99 | if progress == 0: 100 | return 0.0 101 | return pow(2, 10 * (progress - 1.0)) 102 | 103 | @staticmethod 104 | def out_expo(progress): 105 | if progress == 1.0: 106 | return 1.0 107 | return -pow(2, -10 * progress) + 1.0 108 | 109 | @staticmethod 110 | def in_out_expo(progress): 111 | if progress == 0: 112 | return 0.0 113 | if progress == 1.: 114 | return 1.0 115 | p = progress * 2 116 | if p < 1: 117 | return 0.5 * pow(2, 10 * (p - 1.0)) 118 | p -= 1.0 119 | return 0.5 * (-pow(2, -10 * p) + 2.0) 120 | 121 | @staticmethod 122 | def in_circ(progress): 123 | return -1.0 * (sqrt(1.0 - progress * progress) - 1.0) 124 | 125 | @staticmethod 126 | def out_circ(progress): 127 | p = progress - 1.0 128 | return sqrt(1.0 - p * p) 129 | 130 | @staticmethod 131 | def in_out_circ(progress): 132 | p = progress * 2 133 | if p < 1: 134 | return -0.5 * (sqrt(1.0 - p * p) - 1.0) 135 | p -= 2.0 136 | return 0.5 * (sqrt(1.0 - p * p) + 1.0) 137 | 138 | @staticmethod 139 | def in_elastic(progress): 140 | p = .3 141 | s = p / 4.0 142 | q = progress 143 | if q == 1: 144 | return 1.0 145 | q -= 1.0 146 | return -(pow(2, 10 * q) * sin((q - s) * (2 * pi) / p)) 147 | 148 | @staticmethod 149 | def out_elastic(progress): 150 | p = .3 151 | s = p / 4.0 152 | q = progress 153 | if q == 1: 154 | return 1.0 155 | return pow(2, -10 * q) * sin((q - s) * (2 * pi) / p) + 1.0 156 | 157 | @staticmethod 158 | def in_out_elastic(progress): 159 | p = .3 * 1.5 160 | s = p / 4.0 161 | q = progress * 2 162 | if q == 2: 163 | return 1.0 164 | if q < 1: 165 | q -= 1.0 166 | return -.5 * (pow(2, 10 * q) * sin((q - s) * (2.0 * pi) / p)) 167 | else: 168 | q -= 1.0 169 | return pow(2, -10 * q) * sin((q - s) * (2.0 * pi) / p) * .5 + 1.0 170 | 171 | @staticmethod 172 | def in_back(progress): 173 | return progress * progress * ((1.70158 + 1.0) * progress - 1.70158) 174 | 175 | @staticmethod 176 | def out_back(progress): 177 | p = progress - 1.0 178 | return p * p * ((1.70158 + 1) * p + 1.70158) + 1.0 179 | 180 | @staticmethod 181 | def in_out_back(progress): 182 | p = progress * 2. 183 | s = 1.70158 * 1.525 184 | if p < 1: 185 | return 0.5 * (p * p * ((s + 1.0) * p - s)) 186 | p -= 2.0 187 | return 0.5 * (p * p * ((s + 1.0) * p + s) + 2.0) 188 | 189 | @staticmethod 190 | def _out_bounce_internal(t, d): 191 | p = t / d 192 | if p < (1.0 / 2.75): 193 | return 7.5625 * p * p 194 | elif p < (2.0 / 2.75): 195 | p -= (1.5 / 2.75) 196 | return 7.5625 * p * p + .75 197 | elif p < (2.5 / 2.75): 198 | p -= (2.25 / 2.75) 199 | return 7.5625 * p * p + .9375 200 | else: 201 | p -= (2.625 / 2.75) 202 | return 7.5625 * p * p + .984375 203 | 204 | @staticmethod 205 | def _in_bounce_internal(t, d): 206 | return 1.0 - AnimationTransition._out_bounce_internal(d - t, d) 207 | 208 | @staticmethod 209 | def in_bounce(progress): 210 | return AnimationTransition._in_bounce_internal(progress, 1.) 211 | 212 | @staticmethod 213 | def out_bounce(progress): 214 | return AnimationTransition._out_bounce_internal(progress, 1.) 215 | 216 | @staticmethod 217 | def in_out_bounce(progress): 218 | p = progress * 2. 219 | if p < 1.: 220 | return AnimationTransition._in_bounce_internal(p, 1.) * .5 221 | return AnimationTransition._out_bounce_internal(p - 1., 1.) * .5 + .5 222 | -------------------------------------------------------------------------------- /readme.md: -------------------------------------------------------------------------------- 1 | # animation.py 2 | 3 | Animation and timer for pygame games 4 | animation.py is public domain. 5 | 6 | * [Tasks: Delayed calls](#task) 7 | * [Animation: Change values over time](#animation) 8 | 9 | 10 | # Task 11 | ## Execute functions at a later time, once or many times 12 | 13 | This is a silly little class meant to make it easy to create 14 | delayed or looping events without any complicated hooks into 15 | pygame's clock or event loop. 16 | 17 | Tasks do not have their own clock, and must get their time 18 | from an update. Tasks can be added to a normal sprite group 19 | to get their time if you have several Tasks running at once. 20 | 21 | When a task or animation finishes or is aborted, they will be 22 | removed from the animation group, so there is no need to worry 23 | about managing instance of the Tasks or Animations. 24 | 25 | The only real requirement about the group, is that it must 26 | not be drawn. 27 | 28 | ### Examples 29 | 30 | Because the pygame clock returns milliseconds, the examples 31 | below use milliseconds. However, you are free to use what- 32 | ever time unit you wish, as long as it is used consistently 33 | 34 | The Task class is used to schedule the execution of functions 35 | at some point the the future. Below is a demonstration. 36 | Eventually, the function "call_later" will be called once. 37 | 38 | ```python 39 | import pygame 40 | 41 | task_group = pygame.sprite.Group() 42 | 43 | def call_later(): 44 | pass 45 | 46 | # call_later will now be called after 1 second 47 | task = Task(call_later, 1000) 48 | task_group.add(task) 49 | 50 | clock = pygame.clock.Clock() 51 | while game_is_running: 52 | dt = clock.tick() 53 | task_group.update(dt) 54 | ``` 55 | 56 | 57 | If you want to repeat the function, then pass another value. 58 | Below, the "call_later" function will be called 24 times, with 59 | 1000 milliseconds between each call. 60 | 61 | ```python 62 | task = Task(call_later, 1000, 24) 63 | ``` 64 | 65 | 66 | Set times to -1 to make the task repeat forever, or until aborted. 67 | 68 | ```python 69 | task = Task(call_later, interval=2500, times=-1) 70 | ``` 71 | 72 | 73 | Tasks must be initialized with at least one callback, but more 74 | can be added later on. 75 | 76 | ```python 77 | task = Task(heres_my_number, 2500, -1) 78 | task.schedule(call_me) # schedule something else 79 | task.schedule(maybe) # schedule something else 80 | ``` 81 | 82 | 83 | The Task and Animation classes do not directly support passing 84 | positional or keyword arguments to the scheduled callback 85 | functions. In order to do so, use the standard library function 86 | "partial" found in functools. 87 | 88 | ```python 89 | from functools import partial 90 | 91 | task = Task(partial(call_later(1,2,3, key=value)), 1000) 92 | 93 | # or schedule using a partial: 94 | task.schedule(partial(call_later(1,2,3, key=value)), 1000) 95 | ``` 96 | 97 | 98 | There are other useful ways to schedule with a Task: 99 | 100 | ```python 101 | # Task that does my_function 10x with 1 second interval 102 | task = Task(my_function, 1000, 10) 103 | 104 | # "quit" will be called after the 10th time (when task is totally finished) 105 | task.schedule(quit, 'on finish') 106 | ``` 107 | 108 | 109 | Tasks support simple sequencing. Use the chain method to 110 | create a sequence of tasks. Chained events will begin after 111 | the task has finished. Chaining can be useful if tasks are 112 | aborted, or if you don't care to, or can't compute the time 113 | it takes a complex sequence of actions takes. 114 | 115 | When chaining tasks, do not add the chained tasks to a group. 116 | 117 | ```python 118 | task = Task(call_later, 2500) # after 2.5 seconds 119 | task.chain(Task(something_else, 1)) # something_else is called 3.5 seconds later 120 | ``` 121 | 122 | 123 | # Animation 124 | ## Change numeric values over time 125 | 126 | The animation class is modeled after Kivy's excellent 127 | Animation module and is adapted for pygame. It is similar, 128 | but not a direct copy. 129 | 130 | ### What it does 131 | 132 | Animation objects are used to modify the attributes of other 133 | objects over time. It can be used to smoothly move sprites 134 | on the screen, fade surfaces, or any other purpose that involves 135 | the smooth transition of one value to another. 136 | 137 | Animations (and Tasks) are meant to follow the pygame "Sprite 138 | and Group" model. They do not implement their own clock, rather, 139 | they rely on being updated, just like sprites. 140 | 141 | 142 | ### Examples 143 | 144 | To animate a target sprite/object's position, simply specify 145 | the target x/y values where you want the widget positioned at 146 | the end of the animation. Then add the animation to a group. 147 | 148 | Animations will smoothly change values over time. So, instead 149 | of thinking "How many pixels do I add to move the rect?", you 150 | can define where you want the rect to be, without thinking 151 | about the math. 152 | 153 | 154 | ```python 155 | import pygame 156 | 157 | # this group will be only used to hold animations and tasks 158 | animations = pygame.sprite.Group() 159 | 160 | # create an animation to move a rect on the screen. 161 | # the x and y values below correspond to the x and y of 162 | # sprite.rect 163 | ani = Animation(sprite.rect, x=100, y=100, duration=1000) 164 | 165 | # animations do not have a clock, so they must be updated 166 | # since they behave like pygame sprites, you can add them to 167 | # a sprite group, and they will be updated from the group 168 | animations.add(ani) 169 | 170 | # game loop 171 | while game_is_running: 172 | ... 173 | time_delta = clock.tick() 174 | animations.update(time_delta) 175 | ... 176 | ``` 177 | 178 | 179 | The shorthand method of starting animations is to pass the 180 | targets as positional arguments in the constructor. 181 | If you don't know the target when the animation is created, 182 | you can use the `start` method to assign targets 183 | 184 | ```python 185 | # make an animation, but don't assign a target 186 | ani = Animation(x=100, y=0) 187 | 188 | # assign the animation to change this rect 189 | # (only call start once) 190 | ani.start(sprite.rect) 191 | ``` 192 | 193 | 194 | If you would rather specify relative values, then pass the 195 | relative keyword and the values will be adjusted for you. 196 | Below, the rect will be moved 100 pixels to the right and 197 | 100 pixels down. 198 | 199 | ```python 200 | ani = Animation(sprite.rect, x=100, y=100, duration=1000, relative=True) 201 | ``` 202 | 203 | 204 | Animations can also be configured for a delay. 205 | 206 | ```python 207 | # start the movement after 300ms 208 | ani = Animation(sprite.rect, x=100, y=100, duration=1000, delay=300) 209 | ``` 210 | 211 | 212 | Sometimes you need to stop an animation for a particular object, 213 | but you don have a reference handy for it. Use the included 214 | remove_animations_of function to do just that 215 | 216 | ```python 217 | from animation import Animation, remove_animations_of 218 | 219 | # make an animation for this sprite 220 | ani = Animation(sprite.rect, x=100, y=100, duration=1000) 221 | animations.add(ani) 222 | 223 | 224 | # oops, the sprite needs to be removed from the game; 225 | # lets remove animations of the rect 226 | remove_animations_of(sprite.rect, animations) 227 | ``` 228 | 229 | 230 | Animations can be aborted or finished early. Finishing an animation 231 | will cause the animation to end, but the target values will be applied. 232 | Aborting an animation will not change the values. 233 | 234 | ```python 235 | ani = Animation(sprite.rect, x=100, y=100, duration=1000) 236 | 237 | ani.abort() # animation stops; rect will be where it was when canceled 238 | ani.finish() # animation stops, but sprite will be moved to 100, 100 239 | ``` 240 | 241 | You can also specify a callback that will be executed when the 242 | animation finishes: 243 | 244 | ```python 245 | # "my_function" will be called after the animation finishes 246 | ani.schedule(my_function, 'on finish') 247 | 248 | # "on finish" can be omitted; the following line is equivalent to the above 249 | ani.schedule(my_function) 250 | 251 | # Other optional callback times are available 252 | ani.schedule(func, 'on update') # called after new values are applied each update 253 | ``` 254 | 255 | 256 | ### Callable Attributes 257 | 258 | Target attributes can also be callable. If callable, they will be called 259 | with the value of the animation each update. For example, if you are 260 | using an Animation on the "set_alpha" method of a pygame Surface, then 261 | each update, the animation will call Surface.set_alpha(x), where x is 262 | a value between the initial value and final value of the animation. 263 | 264 | This presents a special problem, as it may be impossible to 265 | determine the initial value of the animation. In this case, the 266 | following rules are used to determine the initial value of the animation: 267 | 268 | * If the callable returns a value, the initial value will be set to that 269 | * If there is no return value (or it is None), the initial will be zero 270 | 271 | To further complicate matters the the initial value can be passed 272 | in the Animation constructor. If passed, the value returned by the 273 | callable will not be considered. The initial value can also be a 274 | callable, and is subject to the following rules: 275 | 276 | * The value of the initial or return value of it are used 277 | * The initial must be a number 278 | * If the initial is None (or any other non-number), an exception is raised 279 | * The initial value is only determined once 280 | 281 | NOTE: Specifying an initial value will set the initial value 282 | for all target names in the constructor. This 283 | limitation won't be resolved for a while. 284 | 285 | ### Example of using callable attributes 286 | 287 | ```python 288 | import pygame 289 | 290 | surf = pygame.image.load("some_sprite.png") 291 | 292 | # this animation will change the alpha value of the surface 293 | # from 0 (the default) to 255 over 1 second 294 | ani = Animation(surf, set_alpha=255, duration=1000) 295 | 296 | # you can also specify the initial value 297 | # the following will fade from 255 to 0 298 | ani = Animation(surf, set_alpha=0, initial=255, duration=1000) 299 | ``` 300 | 301 | 302 | 303 | ### Rounding 304 | 305 | In some cases, you may want you values to be rounded to the 306 | nearest integer. For pygame rects, this is needed for smooth 307 | movement, and the Animation class will use rounded values 308 | automatically. For other cases, pass "round_values=True" 309 | 310 | ### Potential pitfalls 311 | 312 | Because Animations have a list of keyword arguments that configure 313 | it, those words cannot be used on target objects. 314 | 315 | For example, because "duration" is used to configure the Animation, 316 | you cannot use an animation to change the "duration" attribute of 317 | another object. This is an issue that I will fix sometime. 318 | 319 | For now, here is a list of reserved words can cannot be used: 320 | * duration 321 | * transition 322 | * initial 323 | * relative 324 | * round_values 325 | * delay 326 | 327 | 328 | ### More info 329 | 330 | The docstrings have some more detailed info about each class. Take 331 | a look at the source for more info about what is possible. 332 | -------------------------------------------------------------------------------- /setup.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | # -*- encoding: utf-8 -*- 3 | 4 | from setuptools import setup 5 | 6 | setup(name="pygame-animation", 7 | version="0.0.5", 8 | description="PyGame Animation and Tweening Library", 9 | author="bitcraft", 10 | author_email="", 11 | packages=['animation'], 12 | license="GPLv3", 13 | url="https://github.com/bitcraft/animation", 14 | download_url="", 15 | long_description="", 16 | package_data={}, 17 | classifiers=[ 18 | "Intended Audience :: Developers", 19 | "Development Status :: 5 - Production/Stable", 20 | "License :: OSI Approved :: GNU General Public License v3 (GPLv3)", 21 | "Natural Language :: English", 22 | "Programming Language :: Python :: 2.7", 23 | "Programming Language :: Python :: 3.3", 24 | "Programming Language :: Python :: 3.4", 25 | "Topic :: Games/Entertainment", 26 | "Topic :: Software Development :: Libraries", 27 | ], 28 | ) 29 | -------------------------------------------------------------------------------- /tests/test_animation.py: -------------------------------------------------------------------------------- 1 | from unittest import TestCase, skip 2 | 3 | from mock import Mock 4 | from pygame.sprite import Group 5 | 6 | from animation import Animation, Task, remove_animations_of 7 | from animation.animation import is_number 8 | 9 | 10 | class TestObject: 11 | """ Mocks don't work well with animations due to introspection, 12 | so this is to be used instead of a mock. 13 | """ 14 | 15 | def __init__(self): 16 | self.value = 0.0 17 | self.illegal_value = 'spam' 18 | self.callable = Mock(return_value=0) 19 | self.initial = 0.0 20 | 21 | def set_value(self, value): 22 | self.value = value 23 | 24 | def get_initial(self): 25 | return self.initial 26 | 27 | def get_illegal_value(self): 28 | return self.illegal_value 29 | 30 | 31 | class TestAnimation(TestCase): 32 | def setUp(self): 33 | self.mock = TestObject() 34 | 35 | def test_default_schedule_is_finish(self): 36 | m = Mock() 37 | a = Animation(value=1, duration=1) 38 | a.schedule(m) 39 | a.start(self.mock) 40 | self.simulate(a) 41 | self.assertTrue(m.called) 42 | self.assertEqual(m.call_count, 1) 43 | 44 | def test_is_number_int(self): 45 | self.assertTrue(is_number(1)) 46 | self.assertTrue(is_number('1')) 47 | 48 | def test_is_number_float(self): 49 | self.assertTrue(is_number(1.5)) 50 | self.assertTrue(is_number('1.5')) 51 | 52 | def test_is_number_non_number_raises_ValueError(self): 53 | with self.assertRaises(ValueError): 54 | is_number('abc') 55 | 56 | with self.assertRaises(ValueError): 57 | is_number((1, 2, 3)) 58 | 59 | def simulate(self, animation, times=100): 60 | """ used to simulate a clock updating the animation for some time 61 | default is one second 62 | """ 63 | for time in range(times): 64 | animation.update(.01) 65 | 66 | def test_can_add_to_group(self): 67 | a = Animation(value=1) 68 | g = Group(a) 69 | 70 | def test_finished_removes_from_group(self): 71 | a = Animation(value=1) 72 | g = Group(a) 73 | a.start(self.mock) 74 | a.finish() 75 | self.assertNotIn(a, g) 76 | 77 | def test_aborted_removes_from_group(self): 78 | a = Animation(value=1) 79 | g = Group(a) 80 | a.start(self.mock) 81 | a.abort() 82 | self.assertNotIn(a, g) 83 | 84 | def test_remove_animations_of(self): 85 | m0, m1 = TestObject(), TestObject() 86 | a0 = Animation(value=1) 87 | a0.start(m0) 88 | a1 = Animation(value=1) 89 | a1.start(m1) 90 | g = Group(a0, a1) 91 | self.assertEqual(len(g), 2) 92 | 93 | remove_animations_of(m0, g) 94 | self.assertEqual(len(g), 1) 95 | self.assertIn(a1, g) 96 | self.assertNotIn(a0, g) 97 | 98 | remove_animations_of(m1, g) 99 | self.assertEqual(len(g), 0) 100 | self.assertNotIn(a1, g) 101 | 102 | def test_round_values(self): 103 | """ verify that values are rounded to the nearest whole integer 104 | """ 105 | a = Animation(value=1.1, round_values=True, duration=1) 106 | a.start(self.mock) 107 | 108 | # verify that it rounds down (.25 => 0) 109 | self.simulate(a, 25) 110 | self.assertEqual(self.mock.value, 0) 111 | 112 | # verify that it rounds up (.75 => 1) 113 | self.simulate(a, 50) 114 | self.assertEqual(self.mock.value, 1) 115 | 116 | # verify that final value is also rounded 117 | self.simulate(a, 25) 118 | self.assertEqual(self.mock.value, 1) 119 | 120 | def test_relative_values(self): 121 | a = Animation(value=1, relative=True) 122 | self.mock.value = 1 123 | a.start(self.mock) 124 | a.finish() 125 | self.assertEqual(self.mock.value, 2) 126 | 127 | def test_start_will_not_set_values(self): 128 | """ verify that the animation will not change values at start 129 | """ 130 | a = Animation(value=1) 131 | a.start(self.mock) 132 | self.assertEqual(self.mock.value, 0) 133 | 134 | def test_update_attribute(self): 135 | """ verify that the animation can modify normal attributes 136 | """ 137 | a = Animation(value=1, duration=1) 138 | a.start(self.mock) 139 | a.update(1) 140 | self.assertEqual(self.mock.value, 1) 141 | 142 | def test_target_callable(self): 143 | """ verify that the animation will update callable attributes 144 | """ 145 | a = Animation(callable=1, duration=2) 146 | a.start(self.mock) 147 | 148 | # simulate passage of 1 second time 149 | a.update(1) 150 | self.assertGreaterEqual(self.mock.callable.call_count, 1) 151 | 152 | # .5 value is derived from: time elapsed (1) / duration (2) 153 | self.assertEqual(self.mock.callable.call_args[0], (.5,)) 154 | 155 | def test_target_callable_with_initial(self): 156 | """ verify that the animation will update callable attributes 157 | """ 158 | a = Animation(callable=1, initial=0, duration=2) 159 | a.start(self.mock) 160 | 161 | # simulate passage of 1 second time 162 | a.update(1) 163 | self.assertGreaterEqual(self.mock.callable.call_count, 1) 164 | 165 | # .5 value is derived from: time elapsed (1) / duration (2) 166 | self.assertEqual(self.mock.callable.call_args[0], (.5,)) 167 | 168 | def test_set_initial(self): 169 | """ verify that the animation will set initial values 170 | """ 171 | a = Animation(value=1, initial=.5) 172 | a.start(self.mock) 173 | 174 | # this will set the value to the initial 175 | a.update(0) 176 | self.assertEqual(self.mock.value, .5) 177 | 178 | def test_set_initial_callable(self): 179 | """ verify that the animation will set initial values from a callable 180 | """ 181 | a = Animation(value=1, initial=self.mock.get_initial) 182 | a.start(self.mock) 183 | 184 | # this will set the value to the initial 185 | a.update(0) 186 | self.assertEqual(self.mock.value, self.mock.initial) 187 | 188 | def test_delay(self): 189 | """ verify that this will not start until the delay 190 | """ 191 | a = Animation(value=1, delay=1, duration=1) 192 | a.start(self.mock) 193 | 194 | self.simulate(a, 100) 195 | self.assertEqual(self.mock.value, 0) 196 | 197 | self.simulate(a, 100) 198 | self.assertEqual(self.mock.value, 1) 199 | 200 | def test_finish_before_complete(self): 201 | """ verify that calling finish before complete will set final values 202 | """ 203 | a = Animation(value=1) 204 | a.start(self.mock) 205 | a.finish() 206 | self.assertEqual(self.mock.value, 1) 207 | 208 | def test_update_callback_called(self): 209 | """ verify that update_callback is called each update and final 210 | """ 211 | m = Mock() 212 | a = Animation(value=1, duration=1) 213 | a.schedule(m, "on update") 214 | a.start(self.mock) 215 | self.simulate(a) 216 | self.assertTrue(m.called) 217 | 218 | # 101 = 100 iterations of update + 1 iteration during the finalizer 219 | # may be more as the mock is called to get initial value 220 | self.assertGreaterEqual(m.call_count, 101) 221 | 222 | def test_final_callback_called_when_finished(self): 223 | """ verify that callback is called during the finalizer when finishes 224 | """ 225 | m = Mock() 226 | a = Animation(value=1, duration=1) 227 | a.schedule(m, "on finish") 228 | a.start(self.mock) 229 | self.simulate(a) 230 | self.assertTrue(m.called) 231 | self.assertEqual(m.call_count, 1) 232 | 233 | def test_final_callback_called_when_aborted(self): 234 | """ verify that callback is called during the finalizer when aborted 235 | """ 236 | m = Mock() 237 | a = Animation(value=1) 238 | a.schedule(m, "on finish") 239 | a.start(self.mock) 240 | a.abort() 241 | self.assertTrue(m.called) 242 | self.assertEqual(m.call_count, 1) 243 | 244 | def test_update_callback_not_called_when_aborted(self): 245 | m = Mock() 246 | a = Animation(value=1) 247 | self.assertFalse(m.called) 248 | a.update_callback = m 249 | self.assertFalse(m.called) 250 | a.start(self.mock) 251 | self.assertFalse(m.called) 252 | a.abort() 253 | self.assertFalse(m.called) 254 | 255 | def test_values_not_applied_when_aborted(self): 256 | a = Animation(value=1) 257 | a.start(self.mock) 258 | a.abort() 259 | self.assertEqual(self.mock.value, 0) 260 | 261 | def test_update_callable_with_initial(self): 262 | a = Animation(callable=1, duration=1, initial=0) 263 | a.start(self.mock) 264 | a.update(1) 265 | # call #1 is update, call #2 is finalizer 266 | # may be more as it will be called to gather initial value 267 | self.assertGreaterEqual(self.mock.callable.call_count, 2) 268 | 269 | def test_get_value_attribute(self): 270 | """ Verify getter properly handles attribute 271 | """ 272 | a = Animation(value=1) 273 | self.assertEqual(a._get_value(self.mock, 'value'), 0) 274 | 275 | def test_get_value_callable_attribute(self): 276 | """ Verify getter properly handles callable attribute 277 | """ 278 | assert (callable(self.mock.callable)) 279 | a = Animation(callable=1) 280 | self.assertEqual(a._get_value(self.mock, 'callable'), 0) 281 | 282 | def test_get_value_initial(self): 283 | """ Verify getter properly handles initial attribute 284 | """ 285 | a = Animation(value=1, initial=0) 286 | self.assertEqual(a._get_value(None, None), 0) 287 | 288 | def test_get_value_initial_callable(self): 289 | """ Verify getter properly handles callable initial attribute 290 | """ 291 | a = Animation(value=1, initial=self.mock.callable) 292 | self.assertEqual(a._get_value(None, 'value'), 0) 293 | 294 | def test_set_value_attribute(self): 295 | """ Verify setter properly handles attribute 296 | """ 297 | a = Animation(value=1) 298 | a._set_value(self.mock, 'value', 10) 299 | self.assertEqual(self.mock.value, 10) 300 | 301 | def test_set_value_callable_attribute(self): 302 | """ Verify setter properly handles callable attribute 303 | """ 304 | a = Animation(value=1) 305 | a._set_value(self.mock, 'callable', 0) 306 | self.assertTrue(self.mock.callable.called) 307 | self.assertEqual(self.mock.callable.call_count, 1) 308 | self.assertEqual(self.mock.callable.call_args[0], (0,)) 309 | 310 | def test_non_number_target_raises_valueerror(self): 311 | a = Animation(value=self.mock.illegal_value) 312 | with self.assertRaises(ValueError): 313 | a.start(self.mock) 314 | 315 | a = Animation(value=self.mock.get_illegal_value) 316 | with self.assertRaises(ValueError): 317 | a.start(self.mock) 318 | 319 | def test_non_number_initial_raises_valueerror(self): 320 | a = Animation(illegal_value=1) 321 | with self.assertRaises(ValueError): 322 | a.start(self.mock) 323 | 324 | a = Animation(value=1, initial=self.mock.get_illegal_value) 325 | with self.assertRaises(ValueError): 326 | a.start(self.mock) 327 | 328 | def test_no_targets_raises_valueerror(self): 329 | with self.assertRaises(ValueError): 330 | Animation() 331 | 332 | @skip("RuntimeError checking disabled") 333 | def test_abort_before_start_raises_runtimeerror(self): 334 | a = Animation(value=1) 335 | 336 | with self.assertRaises(RuntimeError): 337 | a.abort() 338 | 339 | @skip("RuntimeError checking disabled") 340 | def test_finish_before_start_raises_runtimeerror(self): 341 | a = Animation(value=1) 342 | 343 | with self.assertRaises(RuntimeError): 344 | a.finish() 345 | 346 | @skip("RuntimeError checking disabled") 347 | def test_exceed_duration_raises_runtimeerror(self): 348 | a = Animation(value=1, duration=1) 349 | a.start(self.mock) 350 | 351 | with self.assertRaises(RuntimeError): 352 | self.simulate(a, 101) 353 | 354 | @skip('changed api, now depreciated') 355 | def test_finish_then_update_raises_runtimeerror(self): 356 | a = Animation(value=1) 357 | a.start(self.mock) 358 | a.finish() 359 | 360 | with self.assertRaises(RuntimeError): 361 | a.update(1) 362 | 363 | def test_finish_then_start_raises_runtimeerror(self): 364 | a = Animation(value=1) 365 | a.start(self.mock) 366 | a.finish() 367 | 368 | with self.assertRaises(RuntimeError): 369 | a.start(None) 370 | 371 | @skip("RuntimeError checking disabled") 372 | def test_finish_twice_raises_runtimeerror(self): 373 | a = Animation(value=1) 374 | a.start(self.mock) 375 | a.finish() 376 | 377 | with self.assertRaises(RuntimeError): 378 | a.finish() 379 | 380 | def test_start_twice_raises_runtimeerror(self): 381 | a = Animation(value=1) 382 | a.start(self.mock) 383 | 384 | with self.assertRaises(RuntimeError): 385 | a.start(None) 386 | 387 | 388 | class TestTask(TestCase): 389 | def simulate(self, object_, duration=1, step=1): 390 | """ used to simulate a clock updating something for some time 391 | default is one second 392 | """ 393 | elapsed = 0 394 | while elapsed < duration: 395 | elapsed += step 396 | object_.update(step) 397 | 398 | def test_schedule_default_is_finish(self): 399 | m = Mock() 400 | t = Task(Mock(), interval=1) 401 | t.schedule(m) 402 | t.update(1) 403 | self.assertEqual(m.call_count, 1) 404 | 405 | def test_zero_times_raises_AssertionError(self): 406 | with self.assertRaises(ValueError): 407 | Task(Mock(), times=0) 408 | 409 | def test_not_callable_rases_AssertionError(self): 410 | with self.assertRaises(ValueError): 411 | Task(None) 412 | 413 | def test_update_once(self): 414 | m = Mock() 415 | t = Task(m, interval=1) 416 | t.update(1) 417 | self.assertEqual(m.call_count, 1) 418 | 419 | def test_parameters(self): 420 | m = Mock() 421 | t = Task(m, interval=1, times=10) 422 | self.simulate(t, 15) 423 | self.assertEqual(m.call_count, 10) 424 | 425 | def test_update_many(self): 426 | m = Mock() 427 | t = Task(m, interval=1, times=10) 428 | self.simulate(t, 15) 429 | self.assertEqual(m.call_count, 10) 430 | 431 | def test_abort_does_not_callback(self): 432 | m = Mock() 433 | t = Task(m, interval=0) 434 | t.abort() 435 | self.assertEqual(m.call_count, 0) 436 | 437 | def test_chain(self): 438 | m0, m1 = Mock(), Mock() 439 | t0 = Task(m0, interval=0) 440 | t1 = t0.chain(Task(m1, interval=0)) 441 | g = Group(t0) 442 | # sanity 443 | self.assertNotIn(t1, g) 444 | 445 | # this update will cause the chain to execute 446 | # it will also add chained Tasks to the group 447 | g.update(1) 448 | self.assertTrue(m0.called) 449 | self.assertIn(t1, g) 450 | 451 | # this update will now cause the chained Task to complete 452 | g.update(1) 453 | self.assertTrue(m1.called) 454 | 455 | @skip("RuntimeError checking disabled") 456 | def test_update_over_duration_raises_RuntimeError(self): 457 | m = Mock() 458 | t = Task(m, interval=1) 459 | with self.assertRaises(RuntimeError): 460 | self.simulate(t, 10) 461 | 462 | @skip("RuntimeError checking disabled") 463 | def test_update_after_abort_raises_RuntimeError(self): 464 | t = Task(Mock(), interval=1) 465 | t.abort() 466 | with self.assertRaises(RuntimeError): 467 | t.update(1) 468 | 469 | def test_chain_non_Task_raises_TypeError(self): 470 | with self.assertRaises(TypeError): 471 | Task(Mock()).chain(None) 472 | --------------------------------------------------------------------------------