Repository: kivy/kivy

Issue:
kivy.animation.Sequence sends "on_complete" event while animation is still in progress
### Versions

* Python: 2.7
* OS: Linux Ubuntu 16.04
* Kivy: 1.10.1.dev0
* Kivy installation method: git clone and python setup.py install

### Description

The program uses Animation to pulse a Widget color from black to red and back again.  When the animation completes, the Widget is supposed to be black, but it is red.  Could be a canvas update problem, could be an animation bug.

### Code and Logs

```python
from kivy.app import App
from kivy.uix.widget import Widget
from kivy.uix.button import Button
from kivy.uix.boxlayout import BoxLayout
from kivy.graphics import Color, Line, Rectangle
from kivy.animation import Animation

class Blinker(Widget):

    def __init__(self, *args, **kwargs):
        super(Blinker, self).__init__(*args, **kwargs)
        self.color = None
        self.animation = None
        self.draw()
        self.bind(size=self.draw, pos=self.draw, parent=self.draw)

    def draw(self, *args):
        self.canvas.clear()
        with self.canvas:
            self.color = Color(0, 0, 0, 1)
            Rectangle(pos=self.pos, size=self.size)        

    def repaint(self, *args):
        if self.color is not None:
            self.color.rgba = (0, 0, 0, 1)
            self.canvas.ask_update()
            print 'color.rgba is', self.color.rgba

    def start(self, *args):
        if self.color is not None:
            self.animation = Animation(r=1, duration=0.5) + Animation(r=0, duration=0.5)
            self.animation.repeat = True
            self.animation.bind(on_complete=self.repaint)
            self.animation.start(self.color)

    def stop(self, *args):
        if self.animation is not None:
            self.animation.stop(self.color)
            self.animation = None


class A(App):

    def stop_start_animation(self, *args):
        if self.blinker.animation is not None:
            self.blinker.stop()
            self.button.text = "Start"
        else:
            self.blinker.start()
            self.button.text = "Stop"

    def build(self):
        top = BoxLayout(orientation="horizontal")
        self.button = (Button(on_press=self.stop_start_animation, text="Start"))
        top.add_widget(self.button)
        self.blinker = Blinker()
        top.add_widget(self.blinker)
        return top

if __name__ == "__main__":
    A().run()
```
Logs:
```
(testkivy) wjanssen@archie:/local/kivy-1.10$ python ~/projects/cbmx/platform/t.py
[INFO   ] [Logger      ] Record log in /home/wjanssen/.kivy/logs/kivy_17-05-31_34.txt
[WARNING] [Modules     ] Module <clipboard> not found
[WARNING] [Modules     ] Module <cutbuffer> not found
[INFO   ] [Factory     ] 194 symbols loaded
[INFO   ] [Image       ] Providers: img_tex, img_dds, img_sdl2, img_pil, img_gif (img_ffpyplayer ignored)
[INFO   ] [Text        ] Provider: sdl2
[INFO   ] [Kivy        ] v1.10.1.dev0, git-b7906e7, 20170531
[INFO   ] [Python      ] v2.7.13 |Continuum Analytics, Inc.| (default, Dec 20 2016, 23:09:15) 
[GCC 4.4.7 20120313 (Red Hat 4.4.7-1)]
[INFO   ] [OSC         ] using <multiprocessing> for socket
[INFO   ] [Window      ] Provider: sdl2(['window_egl_rpi'] ignored)
[INFO   ] [GL          ] Using the "OpenGL" graphics system
[INFO   ] [GL          ] Backend used <gl>
[INFO   ] [GL          ] OpenGL version <3.0 Mesa 12.0.6>
[INFO   ] [GL          ] OpenGL vendor <Intel Open Source Technology Center>
[INFO   ] [GL          ] OpenGL renderer <Mesa DRI Intel(R) HD Graphics 5500 (Broadwell GT2) >
[INFO   ] [GL          ] OpenGL parsed version: 3, 0
[INFO   ] [GL          ] Shading version <1.30>
[INFO   ] [GL          ] Texture max size <8192>
[INFO   ] [GL          ] Texture max units <32>
[INFO   ] [Window      ] auto add sdl2 input provider
[WARNING] [Modules     ] Module <clipboard> not found
[WARNING] [Modules     ] Module <cutbuffer> not found
[INFO   ] [Window      ] virtual keyboard not allowed, single mode, not docked
[INFO   ] [Base        ] Start application main loop
[INFO   ] [GL          ] NPOT texture support is available
color.rgba is [0.0, 0.0, 0.0, 1.0]
color.rgba is [0.0, 0.0, 0.0, 1.0]
color.rgba is [0.0, 0.0, 0.0, 1.0]
[INFO   ] [Base        ] Leaving application in progress...
```


Discussion:
matham: I wonder if `has_animation_properties` is a better name? And maybe it's better if its a `@property`?
matham: Oh, Never mind, the function already exists in master. I'll let someone with animation usage review it.
gottadiveintopython: yep it override `Animation.have_prop...()`
Thanks for the review.
tshirtman: I meant for the Sequence and Parallel to have a common method, (but i'm not sure which meaning of "like" you used here, if you meant "in the same way as", then ok). i.e:

```
class CompoundAnimation(Animation):
    def have_property_to_animate(self, widget):
        …

class Sequence(CompoundAnimation):
    …

class Parallel(CompoundAnimation):
    …
```

i think is slightly better than having the exact same method in two classes.
gottadiveintopython: ah I understand what you mean.

`stop()`, `stop_property()`, `cancel()`, `have_properties_to_animate()` are completely same. So moving them to `CompoundAnimation`, the base class. I feel that doing that loose some flexibility. But anyawys I'm doing that now.
gottadiveintopython: `Sequence` and `Parallel` are really buggy while `Animation` is not.
If I continue fixing thoe bugs, this PR will become not related to the issue(#5443).
So I'm probably going to make another PR since the issue is fixed.
tshirtman: Yes, there are certainly bugs left in them, thank you for looking into it.
gottadiveintopython: np :)
well I updated the PR. Can you review it?
gottadiveintopython: thanks
gottadiveintopython: When I ran [this code](https://gist.github.com/gottadiveintopython/ac5e4e1a33a93c31bc53d6875e2b035c) on master branch, `Parallel`s were never released. I assume that's the reason why `Parallel` doesn't override `cancel_property()`. That means I should revert 3608092fcefb6ce949a5ae92a6c347569b95f7f8.
gottadiveintopython: This PR contains this hackey code:

```python
old_max_fps = None


def setUpModule():
    global old_max_fps
    old_max_fps = Clock._max_fps
    Clock._max_fps = 0.0


def tearDownModule():
    global old_max_fps
    Clock._max_fps = old_max_fps
```

Does anyone know proper way of changing `maxfps` temporarily?
gottadiveintopython: ok I'll make this PR more simple.
um should I reset commits? I mean `git reset --hard/soft xxxx`.
Since you already spent a time on reading this PR, I better keep current commits?
tshirtman: It's ok to leave the commits, we can always squash & merge in the interface, if we decide the history is not important in the end. Thank you, just let us know when you are happy about the state, so i can review again :).
gottadiveintopython: ok I got it.
I might reset commits to make this pr easier to understand though.
gottadiveintopython: This should be ok. Can you review it?

>  it also contains tests that are expected to fail, with comments about things that are currently unclear, so i'm not sure what to make of it

sorry for that unreadable test code.
I created simple code: [this](https://gist.github.com/gottadiveintopython/cc23a1ecaa6aa5ffd8a2f60445478eaf) and [this](https://gist.github.com/gottadiveintopython/7605ac5e2d0eaebbc68f124619717087)
Those are not fixed by this PR and I'm not even sure that those should be fixed or not.
tshirtman: Hm, I'm really sorry this lingered for so long, but the code looks good, it does still feel worth merging, especially people still hits the bugs with reusing composed animations from time to time.
tshirtman: Hm, I'm really sorry this lingered for so long, but the code looks good, it does still feel worth merging, especially people still hits the bugs with reusing composed animations from time to time.
gottadiveintopython: @tshirtman Hi, is it better to `git rebase master` and rewrite unit tests in pytest-style? or better to keep the current state?
tshirtman: Hey, if you are willing to, rebasing/updating would be great.
gottadiveintopython: ok, I'll do it then.
gottadiveintopython: `Parallel` never fires `on_progress`, and I think it's corrent behavior. But it might confuse the user, so we may need to document it.
tshirtman: > `Parallel` never fires `on_progress`, and I think it's a corrent behavior. But it might confuse the user, so we may need to document it.
> 
> Or maybe `Parallel` should fire `on_progress` only when `Parallel.anim1` fires `on_progress`.

hm, if we can't then it's better not to document, i don't see how only firing for anim1 would be correct, ideally it would fire for both, and have the progress value take both durations into account (so if anim 1 is 3s and anim 2 is 2s, 1s into the first animation, we'd get progress at 0.2, 1/5th of the total), but i didn't check how hard it would be to implement that.
gottadiveintopython: > we'd get progress at 0.2, 1/5th of the total

Sounds like you are confusing `Sequence` with `Parallel`. But anyways It may be better to keep the current implementation. If we change it to fire `on_progress`, it probably impact the performance, because `on_progress` usually is fired very frequently.

> if we can't then it's better not to document

Ok, got it.

tshirtman: > > we'd get progress at 0.2, 1/5th of the total
> 
> Sounds like you are confusing `Sequence` with `Parallel`. But anyways It may be better to keep the current implementation. If we change it to fire `on_progress`, it probably impact the performance, because `on_progress` usually is fired very frequently.
> 

:facepalm: you are right, maybe we could report based on the longest, but yeah, in any case, no need to think about that in this PR, it would be a new feature imho, to be evaluated independently.

> > if we can't then it's better not to document
> 
> Ok, got it.
:+1: 
gottadiveintopython: `EventCounter.assert_()` is my concern.
gottadiveintopython: @tshirtman 
I believe the PR is mergeable. Could you check if it's ok or not?

> we could report based on the longest

Yeah I agree with that way if we implement it.

Patch:
diff --git a/doc/sources/guide/packaging-ios-prerequisites.rst b/doc/sources/guide/packaging-ios-prerequisites.rst
index 577238392..7cfcf2d7e 100644
--- a/doc/sources/guide/packaging-ios-prerequisites.rst
+++ b/doc/sources/guide/packaging-ios-prerequisites.rst
@@ -40,11 +40,9 @@ The first thing is to ensure you have run the following commands:
 
 .. parsed-literal::
 
-    brew install autoconf automake libtool pkg-config mercurial
-    brew link libtool
-    brew link mercurial
-    sudo easy_install pip
-    sudo pip install |cython_install|
+    $ brew install autoconf automake libtool pkg-config
+    $ brew link libtool
+    $ pip install |cython_install|
 
 If you still receive build errors, check your Homebrew is in a healthy state::
 
diff --git a/doc/sources/guide/packaging-ios.rst b/doc/sources/guide/packaging-ios.rst
index dc6a21cad..b767bccdc 100644
--- a/doc/sources/guide/packaging-ios.rst
+++ b/doc/sources/guide/packaging-ios.rst
@@ -5,7 +5,7 @@ Create a package for IOS
 
 .. note::
 
-    Currently, kivy-iOS builds packages with Python 2.7 and 3.7.
+    Currently, kivy-iOS builds packages with Python 3.7.
 
 The overall process for creating a package for IOS can be explained in 4 steps:
 
@@ -23,10 +23,9 @@ those dependencies:
 
 .. parsed-literal::
 
-    brew install autoconf automake libtool pkg-config
-    brew link libtool
-    sudo easy_install pip
-    sudo pip install |cython_install|
+    $ brew install autoconf automake libtool pkg-config
+    $ brew link libtool
+    $ pip install |cython_install|
 
 For more detail, see :ref:`IOS Prerequisites <packaging_ios_prerequisites>`.
 Just ensure that everything is ok before starting the second step!
@@ -38,12 +37,10 @@ Compile the distribution
 
 Open a terminal, and type::
 
-    $ git clone git://github.com/kivy/kivy-ios
-    $ cd kivy-ios
-    $ ./toolchain.py build kivy
+    $ pip install kivy-ios
+    $ toolchain build kivy
 
-Most of the python distribution is packed into `python27.zip`. If you
-experience any issues, please refer to our
+If you experience any issues, please refer to our
 `user group <https://groups.google.com/forum/#!forum/kivy-users>`_ or the
 `kivy-ios project page <https://github.com/kivy/kivy-ios>`_.
 
@@ -56,11 +53,11 @@ Before proceeding to the next step, ensure your application entry point is a fil
 named `main.py`.
 
 We provide a script that creates an initial Xcode project to start with. In the
-command line below, replace `test` with your project name. It must be a
+command line below, replace `title` with your project name. It must be a
 name without any spaces or illegal characters::
 
-    $ ./toolchain.py create <title> <app_directory>
-    $ ./toolchain.py create Touchtracer ~/code/kivy/examples/demo/touchtracer
+    $ toolchain create <title> <app_directory>
+    $ toolchain create Touchtracer ~/code/kivy/examples/demo/touchtracer
 
 .. Note::
     You must use a fully qualified path to your application directory.
@@ -86,11 +83,11 @@ Update the Xcode project
 Let's say you want to add numpy to your project but you did not compile it
 prior to creating your XCode project. First, ensure it is built::
 
-    $ ./toolchain.py build numpy
+    $ toolchain build numpy
 
 Then, update your Xcode project::
 
-    $ ./toolchain.py update touchtracer-ios
+    $ toolchain update touchtracer-ios
 
 All the libraries / frameworks necessary to run all the compiled recipes will be
 added to your Xcode project.
@@ -135,15 +132,15 @@ commenting out this line in `main.m`::
 
 Then you should see all the Kivy logging on the Xcode console.
 
-How can Apple accept a python app ?
-~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
+How can Apple accept a python app?
+~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
 
 We managed to merge the app binary with all the libraries into a single binary,
 called libpython. This means all binary modules are loaded beforehand, so
 nothing is dynamically loaded.
 
-Have you already submited a Kivy application to the App store ?
-~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
+Have you already submited a Kivy application to the App store?
+~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
 
 Yes, check:
 
diff --git a/kivy/animation.py b/kivy/animation.py
index 83123a594..fa64b187a 100644
--- a/kivy/animation.py
+++ b/kivy/animation.py
@@ -84,6 +84,7 @@ already in place in the animation module.
 __all__ = ('Animation', 'AnimationTransition')
 
 from math import sqrt, cos, sin, pi
+from collections import ChainMap
 from kivy.event import EventDispatcher
 from kivy.clock import Clock
 from kivy.compat import string_types, iterkeys
@@ -128,7 +129,7 @@ class Animation(EventDispatcher):
     __events__ = ('on_start', 'on_progress', 'on_complete')
 
     def __init__(self, **kw):
-        super(Animation, self).__init__()
+        super().__init__()
         # Initialize
         self._clock_installed = False
         self._duration = kw.pop('d', kw.pop('duration', 1.))
@@ -391,41 +392,7 @@ class Animation(EventDispatcher):
         return Parallel(self, animation)
 
 
-class Sequence(Animation):
-
-    def __init__(self, anim1, anim2):
-        super(Sequence, self).__init__()
-
-        #: Repeat the sequence. See 'Repeating animation' in the header
-        #: documentation.
-        self.repeat = False
-
-        self.anim1 = anim1
-        self.anim2 = anim2
-
-        self.anim1.bind(on_start=self.on_anim1_start,
-                        on_progress=self.on_anim1_progress)
-        self.anim2.bind(on_complete=self.on_anim2_complete,
-                        on_progress=self.on_anim2_progress)
-
-    @property
-    def duration(self):
-        return self.anim1.duration + self.anim2.duration
-
-    def start(self, widget):
-        self.stop(widget)
-        self._widgets[widget.uid] = True
-        self._register()
-        self.anim1.start(widget)
-        self.anim1.bind(on_complete=self.on_anim1_complete)
-
-    def stop(self, widget):
-        self.anim1.stop(widget)
-        self.anim2.stop(widget)
-        props = self._widgets.pop(widget.uid, None)
-        if props:
-            self.dispatch('on_complete', widget)
-        super(Sequence, self).cancel(widget)
+class CompoundAnimation(Animation):
 
     def stop_property(self, widget, prop):
         self.anim1.stop_property(widget, prop)
@@ -437,7 +404,7 @@ class Sequence(Animation):
     def cancel(self, widget):
         self.anim1.cancel(widget)
         self.anim2.cancel(widget)
-        super(Sequence, self).cancel(widget)
+        super().cancel(widget)
 
     def cancel_property(self, widget, prop):
         '''Even if an animation is running, remove a property. It will not be
@@ -455,11 +422,62 @@ class Sequence(Animation):
                 not self.anim2.have_properties_to_animate(widget)):
             self.cancel(widget)
 
-    def on_anim1_start(self, instance, widget):
+    def have_properties_to_animate(self, widget):
+        return (self.anim1.have_properties_to_animate(widget) or
+                self.anim2.have_properties_to_animate(widget))
+
+    @property
+    def animated_properties(self):
+        return ChainMap({},
+                        self.anim2.animated_properties,
+                        self.anim1.animated_properties)
+
+    @property
+    def transition(self):
+        # This property is impossible to implement
+        raise AttributeError(
+            "Can't lookup transition attribute of a CompoundAnimation")
+
+
+class Sequence(CompoundAnimation):
+
+    def __init__(self, anim1, anim2):
+        super().__init__()
+
+        #: Repeat the sequence. See 'Repeating animation' in the header
+        #: documentation.
+        self.repeat = False
+
+        self.anim1 = anim1
+        self.anim2 = anim2
+
+        self.anim1.bind(on_complete=self.on_anim1_complete,
+                        on_progress=self.on_anim1_progress)
+        self.anim2.bind(on_complete=self.on_anim2_complete,
+                        on_progress=self.on_anim2_progress)
+
+    @property
+    def duration(self):
+        return self.anim1.duration + self.anim2.duration
+
+    def stop(self, widget):
+        props = self._widgets.pop(widget.uid, None)
+        self.anim1.stop(widget)
+        self.anim2.stop(widget)
+        if props:
+            self.dispatch('on_complete', widget)
+        super().cancel(widget)
+
+    def start(self, widget):
+        self.stop(widget)
+        self._widgets[widget.uid] = True
+        self._register()
         self.dispatch('on_start', widget)
+        self.anim1.start(widget)
 
     def on_anim1_complete(self, instance, widget):
-        self.anim1.unbind(on_complete=self.on_anim1_complete)
+        if widget.uid not in self._widgets:
+            return
         self.anim2.start(widget)
 
     def on_anim1_progress(self, instance, widget, progress):
@@ -470,9 +488,10 @@ class Sequence(Animation):
 
         .. versionadded:: 1.7.1
         '''
+        if widget.uid not in self._widgets:
+            return
         if self.repeat:
             self.anim1.start(widget)
-            self.anim1.bind(on_complete=self.on_anim1_complete)
         else:
             self.dispatch('on_complete', widget)
             self.cancel(widget)
@@ -481,10 +500,10 @@ class Sequence(Animation):
         self.dispatch('on_progress', widget, .5 + progress / 2.)
 
 
-class Parallel(Animation):
+class Parallel(CompoundAnimation):
 
     def __init__(self, anim1, anim2):
-        super(Parallel, self).__init__()
+        super().__init__()
         self.anim1 = anim1
         self.anim2 = anim2
 
@@ -495,6 +514,13 @@ class Parallel(Animation):
     def duration(self):
         return max(self.anim1.duration, self.anim2.duration)
 
+    def stop(self, widget):
+        self.anim1.stop(widget)
+        self.anim2.stop(widget)
+        if self._widgets.pop(widget.uid, None):
+            self.dispatch('on_complete', widget)
+        super().cancel(widget)
+
     def start(self, widget):
         self.stop(widget)
         self.anim1.start(widget)
@@ -503,33 +529,13 @@ class Parallel(Animation):
         self._register()
         self.dispatch('on_start', widget)
 
-    def stop(self, widget):
-        self.anim1.stop(widget)
-        self.anim2.stop(widget)
-        props = self._widgets.pop(widget.uid, None)
-        if props:
-            self.dispatch('on_complete', widget)
-        super(Parallel, self).cancel(widget)
-
-    def stop_property(self, widget, prop):
-        self.anim1.stop_property(widget, prop)
-        self.anim2.stop_property(widget, prop)
-        if (not self.anim1.have_properties_to_animate(widget) and
-                not self.anim2.have_properties_to_animate(widget)):
-            self.stop(widget)
-
-    def cancel(self, widget):
-        self.anim1.cancel(widget)
-        self.anim2.cancel(widget)
-        super(Parallel, self).cancel(widget)
-
     def on_anim_complete(self, instance, widget):
         self._widgets[widget.uid]['complete'] += 1
         if self._widgets[widget.uid]['complete'] == 2:
             self.stop(widget)
 
 
-class AnimationTransition(object):
+class AnimationTransition:
     '''Collection of animation functions to be used with the Animation object.
     Easing Functions ported to Kivy from the Clutter Project
     https://developer.gnome.org/clutter/stable/ClutterAlpha.html
diff --git a/kivy/input/providers/hidinput.py b/kivy/input/providers/hidinput.py
index 7259d4d9f..42435e27c 100644
--- a/kivy/input/providers/hidinput.py
+++ b/kivy/input/providers/hidinput.py
@@ -21,10 +21,14 @@ To fix that, you can add these options to the argument line:
 
 * invert_x : 1 to invert X axis
 * invert_y : 1 to invert Y axis
-* min_position_x : X minimum
-* max_position_x : X maximum
-* min_position_y : Y minimum
-* max_position_y : Y maximum
+* min_position_x : X relative minimum
+* max_position_x : X relative maximum
+* min_position_y : Y relative minimum
+* max_position_y : Y relative maximum
+* min_abs_x : X absolute minimum
+* min_abs_y : Y absolute minimum
+* max_abs_x : X absolute maximum
+* max_abs_y : Y absolute maximum
 * min_pressure : pressure minimum
 * max_pressure : pressure maximum
 * rotation : rotate the input coordinate (0, 90, 180, 270)


Test patch:
diff --git a/kivy/tests/test_animations.py b/kivy/tests/test_animations.py
index 222297cc1..e6e09d48b 100644
--- a/kivy/tests/test_animations.py
+++ b/kivy/tests/test_animations.py
@@ -2,139 +2,384 @@
 Animations tests
 ================
 '''
+import pytest
 
-import unittest
-import gc
-from time import time, sleep
-from kivy.animation import Animation, AnimationTransition
-from kivy.uix.widget import Widget
-from kivy.clock import Clock
-from kivy.graphics import Scale
-from kivy.weakproxy import WeakProxy
 
+@pytest.fixture(scope='module')
+def ec_cls():
+    class EventCounter:
+        def __init__(self, anim):
+            self.n_start = 0
+            self.n_progress = 0
+            self.n_complete = 0
+            anim.bind(on_start=self.on_start,
+                      on_progress=self.on_progress,
+                      on_complete=self.on_complete)
 
-class AnimationTestCase(unittest.TestCase):
-    def sleep(self, t):
-        start = time()
-        while time() < start + t:
-            sleep(.01)
-            Clock.tick()
+        def on_start(self, anim, widget):
+            self.n_start += 1
 
-    def setUp(self):
-        self.assertEqual(len(Animation._instances), 0)
-        self.a = Animation(x=100, d=1, t='out_bounce')
-        self.w = Widget()
+        def on_progress(self, anim, widget, progress):
+            self.n_progress += 1
 
-    def tearDown(self):
-        self.assertEqual(len(Animation._instances), 0)
+        def on_complete(self, anim, widget):
+            self.n_complete += 1
+
+        def assert_(self, n_start, n_progress_greater_than_zero, n_complete):
+            assert self.n_start == n_start
+            if n_progress_greater_than_zero:
+                assert self.n_progress > 0
+            else:
+                assert self.n_progress == 0
+            assert self.n_complete == n_complete
+    return EventCounter
+
+
+@pytest.fixture(autouse=True)
+def cleanup():
+    from kivy.animation import Animation
+    for anim in Animation._instances.copy():
+        anim.cancel()
+    Animation._instances.clear()
+
+
+def no_animations_being_played():
+    from kivy.animation import Animation
+    return len(Animation._instances) == 0
+
+
+def sleep(t):
+    from time import time, sleep
+    from kivy.clock import Clock
+    tick = Clock.tick
+    start = time()
+    while time() < start + t:
+        sleep(.01)
+        tick()
+
+
+class TestAnimation:
 
     def test_start_animation(self):
-        self.a.start(self.w)
-        self.sleep(1.5)
-        self.assertAlmostEqual(self.w.x, 100)
+        from kivy.animation import Animation
+        from kivy.uix.widget import Widget
+        a = Animation(x=100, d=1)
+        w = Widget()
+        a.start(w)
+        sleep(1.5)
+        assert w.x == pytest.approx(100)
+        assert no_animations_being_played()
 
     def test_animation_duration_0(self):
+        from kivy.animation import Animation
+        from kivy.uix.widget import Widget
         a = Animation(x=100, d=0)
-        a.start(self.w)
-        self.sleep(.5)
+        w = Widget()
+        a.start(w)
+        sleep(.5)
+        assert no_animations_being_played()
 
     def test_stop_animation(self):
-        self.a.start(self.w)
-        self.sleep(.5)
-        self.a.stop(self.w)
-        self.assertNotAlmostEqual(self.w.x, 100)
-        self.assertNotAlmostEqual(self.w.x, 0)
+        from kivy.animation import Animation
+        from kivy.uix.widget import Widget
+        a = Animation(x=100, d=1)
+        w = Widget()
+        a.start(w)
+        sleep(.5)
+        a.stop(w)
+        assert w.x != pytest.approx(100)
+        assert w.x != pytest.approx(0)
+        assert no_animations_being_played()
 
     def test_stop_all(self):
-        self.a.start(self.w)
-        self.sleep(.5)
-        Animation.stop_all(self.w)
+        from kivy.animation import Animation
+        from kivy.uix.widget import Widget
+        a = Animation(x=100, d=1)
+        w = Widget()
+        a.start(w)
+        sleep(.5)
+        Animation.stop_all(w)
+        assert no_animations_being_played()
 
     def test_stop_all_2(self):
-        self.a.start(self.w)
-        self.sleep(.5)
-        Animation.stop_all(self.w, 'x')
+        from kivy.animation import Animation
+        from kivy.uix.widget import Widget
+        a = Animation(x=100, d=1)
+        w = Widget()
+        a.start(w)
+        sleep(.5)
+        Animation.stop_all(w, 'x')
+        assert no_animations_being_played()
 
     def test_duration(self):
-        self.assertEqual(self.a.duration, 1)
+        from kivy.animation import Animation
+        a = Animation(x=100, d=1)
+        assert a.duration == 1
 
     def test_transition(self):
-        self.assertEqual(self.a.transition, AnimationTransition.out_bounce)
+        from kivy.animation import Animation, AnimationTransition
+        a = Animation(x=100, t='out_bounce')
+        assert a.transition is AnimationTransition.out_bounce
 
     def test_animated_properties(self):
-        self.assertEqual(self.a.animated_properties['x'], 100)
+        from kivy.animation import Animation
+        a = Animation(x=100)
+        assert a.animated_properties == {'x': 100, }
 
     def test_animated_instruction(self):
+        from kivy.graphics import Scale
+        from kivy.animation import Animation
+        a = Animation(x=100, d=1)
         instruction = Scale(3)
-        self.a.start(instruction)
-        self.assertEqual(self.a.animated_properties['x'], 100)
-        self.assertAlmostEqual(instruction.x, 3)
-        self.sleep(1.5)
-        self.assertAlmostEqual(instruction.x, 100)
+        a.start(instruction)
+        assert a.animated_properties == {'x': 100, }
+        assert instruction.x == pytest.approx(3)
+        sleep(1.5)
+        assert instruction.x == pytest.approx(100)
+        assert no_animations_being_played()
 
     def test_weakref(self):
-        widget = Widget()
-        anim = Animation(x=100)
-        anim.start(widget.proxy_ref)
-        del widget
+        import gc
+        from kivy.animation import Animation
+        from kivy.uix.widget import Widget
+        w = Widget()
+        a = Animation(x=100)
+        a.start(w.proxy_ref)
+        del w
         gc.collect()
         try:
-            self.sleep(1.)
+            sleep(1.)
         except ReferenceError:
             pass
+        assert no_animations_being_played()
 
 
-class SequentialAnimationTestCase(unittest.TestCase):
-
-    def sleep(self, t):
-        start = time()
-        while time() < start + t:
-            sleep(.01)
-            Clock.tick()
-
-    def setUp(self):
-        self.assertEqual(len(Animation._instances), 0)
-        self.a = Animation(x=100, d=1, t='out_bounce')
-        self.a += Animation(x=0, d=1, t='out_bounce')
-        self.w = Widget()
-
-    def tearDown(self):
-        self.assertEqual(len(Animation._instances), 0)
+class TestSequence:
 
     def test_cancel_all(self):
-        self.a.start(self.w)
-        self.sleep(.5)
-        Animation.cancel_all(self.w)
+        from kivy.animation import Animation
+        from kivy.uix.widget import Widget
+        a = Animation(x=100) + Animation(x=0)
+        w = Widget()
+        a.start(w)
+        sleep(.5)
+        Animation.cancel_all(w)
+        assert no_animations_being_played()
 
     def test_cancel_all_2(self):
-        self.a.start(self.w)
-        self.sleep(.5)
-        Animation.cancel_all(self.w, 'x')
+        from kivy.animation import Animation
+        from kivy.uix.widget import Widget
+        a = Animation(x=100) + Animation(x=0)
+        w = Widget()
+        a.start(w)
+        sleep(.5)
+        Animation.cancel_all(w, 'x')
+        assert no_animations_being_played()
 
     def test_stop_all(self):
-        self.a.start(self.w)
-        self.sleep(.5)
-        Animation.stop_all(self.w)
+        from kivy.animation import Animation
+        from kivy.uix.widget import Widget
+        a = Animation(x=100) + Animation(x=0)
+        w = Widget()
+        a.start(w)
+        sleep(.5)
+        Animation.stop_all(w)
+        assert no_animations_being_played()
 
     def test_stop_all_2(self):
-        self.a.start(self.w)
-        self.sleep(.5)
-        Animation.stop_all(self.w, 'x')
-
-    def _test_on_progress(self, anim, widget, progress):
-        self._on_progress_called = True
-
-    def _test_on_complete(self, anim, widget):
-        self._on_complete_called = True
-
-    def test_events(self):
-        self._on_progress_called = False
-        self._on_complete_called = False
-        self.a.bind(on_progress=self._test_on_progress,
-                    on_complete=self._test_on_complete)
-        self.a.start(self.w)
-        self.sleep(.5)
-        self.assertTrue(self._on_progress_called)
-        self.sleep(2)
-        self.assertTrue(self._on_progress_called)
-        self.assertTrue(self._on_complete_called)
+        from kivy.animation import Animation
+        from kivy.uix.widget import Widget
+        a = Animation(x=100) + Animation(x=0)
+        w = Widget()
+        a.start(w)
+        sleep(.5)
+        Animation.stop_all(w, 'x')
+        assert no_animations_being_played()
+
+    def test_count_events(self, ec_cls):
+        from kivy.animation import Animation
+        from kivy.uix.widget import Widget
+        a = Animation(x=100, d=.5) + Animation(x=0, d=.5)
+        w = Widget()
+        ec = ec_cls(a)
+        ec1 = ec_cls(a.anim1)
+        ec2 = ec_cls(a.anim2)
+        a.start(w)
+
+        # right after the animation starts
+        ec.assert_(1, False, 0)
+        ec1.assert_(1, False, 0)
+        ec2.assert_(0, False, 0)
+        sleep(.2)
+
+        # during the first half of the animation
+        ec.assert_(1, True, 0)
+        ec1.assert_(1, True, 0)
+        ec2.assert_(0, False, 0)
+        sleep(.5)
+
+        # during the second half of the animation
+        ec.assert_(1, True, 0)
+        ec1.assert_(1, True, 1)
+        ec2.assert_(1, True, 0)
+        sleep(.5)
+
+        # after the animation completed
+        ec.assert_(1, True, 1)
+        ec1.assert_(1, True, 1)
+        ec2.assert_(1, True, 1)
+        assert no_animations_being_played()
+
+    def test_have_properties_to_animate(self):
+        from kivy.animation import Animation
+        from kivy.uix.widget import Widget
+        a = Animation(x=100) + Animation(x=0)
+        w = Widget()
+        assert not a.have_properties_to_animate(w)
+        a.start(w)
+        assert a.have_properties_to_animate(w)
+        a.stop(w)
+        assert not a.have_properties_to_animate(w)
+        assert no_animations_being_played()
+
+    def test_animated_properties(self):
+        from kivy.animation import Animation
+        a = Animation(x=100, y=200) + Animation(x=0)
+        assert a.animated_properties == {'x': 0, 'y': 200, }
+
+    def test_transition(self):
+        from kivy.animation import Animation
+        a = Animation(x=100) + Animation(x=0)
+        with pytest.raises(AttributeError):
+            a.transition
+
+
+class TestRepetitiveSequence:
+
+    def test_stop(self):
+        from kivy.animation import Animation
+        from kivy.uix.widget import Widget
+        a = Animation(x=100) + Animation(x=0)
+        a.repeat = True
+        w = Widget()
+        a.start(w)
+        a.stop(w)
+        assert no_animations_being_played()
+
+    def test_count_events(self, ec_cls):
+        from kivy.animation import Animation
+        from kivy.uix.widget import Widget
+        a = Animation(x=100, d=.5) + Animation(x=0, d=.5)
+        a.repeat = True
+        w = Widget()
+        ec = ec_cls(a)
+        ec1 = ec_cls(a.anim1)
+        ec2 = ec_cls(a.anim2)
+        a.start(w)
+
+        # right after the animation starts
+        ec.assert_(1, False, 0)
+        ec1.assert_(1, False, 0)
+        ec2.assert_(0, False, 0)
+        sleep(.2)
+
+        # during the first half of the first round of the animation
+        ec.assert_(1, True, 0)
+        ec1.assert_(1, True, 0)
+        ec2.assert_(0, False, 0)
+        sleep(.5)
+
+        # during the second half of the first round of the animation
+        ec.assert_(1, True, 0)
+        ec1.assert_(1, True, 1)
+        ec2.assert_(1, True, 0)
+        sleep(.5)
+
+        # during the first half of the second round of the animation
+        ec.assert_(1, True, 0)
+        ec1.assert_(2, True, 1)
+        ec2.assert_(1, True, 1)
+        sleep(.5)
+
+        # during the second half of the second round of the animation
+        ec.assert_(1, True, 0)
+        ec1.assert_(2, True, 2)
+        ec2.assert_(2, True, 1)
+        a.stop(w)
+
+        # after the animation stopped
+        ec.assert_(1, True, 1)
+        ec1.assert_(2, True, 2)
+        ec2.assert_(2, True, 2)
+        assert no_animations_being_played()
+
+
+class TestParallel:
+
+    def test_have_properties_to_animate(self):
+        from kivy.animation import Animation
+        from kivy.uix.widget import Widget
+        a = Animation(x=100) & Animation(y=100)
+        w = Widget()
+        assert not a.have_properties_to_animate(w)
+        a.start(w)
+        assert a.have_properties_to_animate(w)
+        a.stop(w)
+        assert not a.have_properties_to_animate(w)
+        assert no_animations_being_played()
+
+    def test_cancel_property(self):
+        from kivy.animation import Animation
+        from kivy.uix.widget import Widget
+        a = Animation(x=100) & Animation(y=100)
+        w = Widget()
+        a.start(w)
+        a.cancel_property(w, 'x')
+        assert not no_animations_being_played()
+        a.stop(w)
+        assert no_animations_being_played()
+
+    def test_animated_properties(self):
+        from kivy.animation import Animation
+        a = Animation(x=100) & Animation(y=100)
+        assert a.animated_properties == {'x': 100, 'y': 100, }
+
+    def test_transition(self):
+        from kivy.animation import Animation
+        a = Animation(x=100) & Animation(y=100)
+        with pytest.raises(AttributeError):
+            a.transition
+
+    def test_count_events(self, ec_cls):
+        from kivy.animation import Animation
+        from kivy.uix.widget import Widget
+        a = Animation(x=100) & Animation(y=100, d=.5)
+        w = Widget()
+        ec = ec_cls(a)
+        ec1 = ec_cls(a.anim1)
+        ec2 = ec_cls(a.anim2)
+        a.start(w)
+
+        # right after the animation started
+        ec.assert_(1, False, 0)
+        ec1.assert_(1, False, 0)
+        ec2.assert_(1, False, 0)
+        sleep(.2)
+
+        # during the first half of the animation
+        ec.assert_(1, False, 0)  # n_progress is still 0 !!
+        ec1.assert_(1, True, 0)
+        ec2.assert_(1, True, 0)
+        sleep(.5)
+
+        # during the second half of the animation
+        ec.assert_(1, False, 0)  # n_progress is still 0 !!
+        ec1.assert_(1, True, 0)
+        ec2.assert_(1, True, 1)
+        sleep(.5)
+
+        # after the animation compeleted
+        ec.assert_(1, False, 1)  # n_progress is still 0 !
+        ec1.assert_(1, True, 1)
+        ec2.assert_(1, True, 1)
+        assert no_animations_being_played()